mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 19:16:22 +00:00
Merge remote-tracking branch 'origin/main' into feat/pki-ENG-3666
This commit is contained in:
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import {
|
||||
backfillPermissionV1SchemaToV2Schema,
|
||||
ProjectPermissionV1Schema
|
||||
@@ -50,6 +51,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = JSON.stringify(
|
||||
packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true))
|
||||
);
|
||||
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -61,7 +66,23 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -106,6 +127,10 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = req.body.permissions
|
||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
: undefined;
|
||||
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -114,11 +139,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions
|
||||
? JSON.stringify(packRules(backfillPermissionV1SchemaToV2Schema(req.body.permissions, true)))
|
||||
: undefined
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -155,6 +195,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: role.slug,
|
||||
name: role.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { OrgMembershipRole, OrgMembershipsSchema, OrgRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -42,6 +43,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.CREATE_ORG_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: JSON.stringify(req.body.permissions)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -116,6 +133,22 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.UPDATE_ORG_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: req.body.permissions ? JSON.stringify(req.body.permissions) : undefined
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -146,6 +179,16 @@ export const registerOrgRoleRouter = async (server: FastifyZodProvider) => {
|
||||
req.permission.authMethod,
|
||||
req.permission.orgId
|
||||
);
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
event: {
|
||||
type: EventType.DELETE_ORG_ROLE,
|
||||
metadata: { roleId: role.id, slug: role.slug, name: role.name }
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
@@ -52,6 +53,8 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
|
||||
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -63,9 +66,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -112,6 +132,7 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -120,9 +141,26 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -161,6 +199,21 @@ export const registerProjectRoleRouter = async (server: FastifyZodProvider) => {
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: role.slug,
|
||||
name: role.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
import { RelaysSchema } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { crypto } from "@app/lib/crypto/cryptography";
|
||||
import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { slugSchema } from "@app/server/lib/schemas";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
@@ -89,14 +90,59 @@ export const registerRelayRouter = async (server: FastifyZodProvider) => {
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
throw new BadRequestError({
|
||||
message: "Org relay registration is not yet supported"
|
||||
});
|
||||
|
||||
return server.services.relay.registerRelay({
|
||||
...req.body,
|
||||
identityId: req.permission.id,
|
||||
orgId: req.permission.orgId
|
||||
orgId: req.permission.orgId,
|
||||
actorAuthMethod: req.permission.authMethod
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/",
|
||||
schema: {
|
||||
response: {
|
||||
200: RelaysSchema.array()
|
||||
}
|
||||
},
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
return server.services.relay.getRelays({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "DELETE",
|
||||
url: "/:id",
|
||||
config: {
|
||||
rateLimit: writeLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: RelaysSchema
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
return server.services.relay.deleteRelay({
|
||||
id: req.params.id,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { packRules } from "@casl/ability/extra";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ProjectMembershipRole, ProjectRolesSchema } from "@app/db/schemas";
|
||||
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
|
||||
import { checkForInvalidPermissionCombination } from "@app/ee/services/permission/permission-fns";
|
||||
import { ProjectPermissionV2Schema } from "@app/ee/services/permission/project-permission";
|
||||
import { ApiDocsTags, PROJECT_ROLE } from "@app/lib/api-docs";
|
||||
@@ -52,6 +53,8 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = JSON.stringify(packRules(req.body.permissions));
|
||||
|
||||
const role = await server.services.projectRole.createRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -63,9 +66,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: JSON.stringify(packRules(req.body.permissions))
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.CREATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -112,6 +132,7 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const stringifiedPermissions = req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined;
|
||||
const role = await server.services.projectRole.updateRole({
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorId: req.permission.id,
|
||||
@@ -120,9 +141,26 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
roleId: req.params.roleId,
|
||||
data: {
|
||||
...req.body,
|
||||
permissions: req.body.permissions ? JSON.stringify(packRules(req.body.permissions)) : undefined
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.UPDATE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: req.body.slug,
|
||||
name: req.body.name,
|
||||
description: req.body.description,
|
||||
permissions: stringifiedPermissions
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
@@ -161,6 +199,21 @@ export const registerDeprecatedProjectRoleRouter = async (server: FastifyZodProv
|
||||
actor: req.permission.type,
|
||||
roleId: req.params.roleId
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
...req.auditLogInfo,
|
||||
orgId: req.permission.orgId,
|
||||
projectId: role.projectId,
|
||||
event: {
|
||||
type: EventType.DELETE_PROJECT_ROLE,
|
||||
metadata: {
|
||||
roleId: role.id,
|
||||
slug: role.slug,
|
||||
name: role.name
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { role };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -777,6 +777,20 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
.map((appUser) => appUser.email)
|
||||
.filter((email): email is string => !!email);
|
||||
|
||||
const approvalPath = `/projects/secret-management/${project.id}/approval`;
|
||||
const approvalUrl = `${cfg.SITE_URL}${approvalPath}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
approverUsersForEmail.map((approver) => ({
|
||||
userId: approver.id,
|
||||
orgId: actorOrgId,
|
||||
type: NotificationType.ACCESS_POLICY_BYPASSED,
|
||||
title: "Secret Access Policy Bypassed",
|
||||
body: `**${actingUser.firstName} ${actingUser.lastName}** (${actingUser.email}) has accessed a secret in **${policy.secretPath || "/"}** in the **${environment?.name || permissionEnvironment}** environment for project **${project.name}** without obtaining the required approval.`,
|
||||
link: approvalPath
|
||||
}))
|
||||
);
|
||||
|
||||
if (recipientEmails.length > 0) {
|
||||
await smtpService.sendMail({
|
||||
recipients: recipientEmails,
|
||||
@@ -788,7 +802,7 @@ export const accessApprovalRequestServiceFactory = ({
|
||||
bypassReason: bypassReason || "No reason provided",
|
||||
secretPath: policy.secretPath || "/",
|
||||
environment: environment?.name || permissionEnvironment,
|
||||
approvalUrl: `${cfg.SITE_URL}/projects/secret-management/${project.id}/approval`,
|
||||
approvalUrl,
|
||||
requestType: "access"
|
||||
},
|
||||
template: SmtpTemplates.AccessSecretRequestBypassed
|
||||
|
||||
@@ -486,9 +486,21 @@ export enum EventType {
|
||||
UPDATE_PROJECT = "update-project",
|
||||
DELETE_PROJECT = "delete-project",
|
||||
|
||||
CREATE_PROJECT_ROLE = "create-project-role",
|
||||
UPDATE_PROJECT_ROLE = "update-project-role",
|
||||
DELETE_PROJECT_ROLE = "delete-project-role",
|
||||
|
||||
CREATE_ORG_ROLE = "create-org-role",
|
||||
UPDATE_ORG_ROLE = "update-org-role",
|
||||
DELETE_ORG_ROLE = "delete-org-role",
|
||||
|
||||
CREATE_SECRET_REMINDER = "create-secret-reminder",
|
||||
GET_SECRET_REMINDER = "get-secret-reminder",
|
||||
DELETE_SECRET_REMINDER = "delete-secret-reminder"
|
||||
DELETE_SECRET_REMINDER = "delete-secret-reminder",
|
||||
|
||||
DASHBOARD_LIST_SECRETS = "dashboard-list-secrets",
|
||||
DASHBOARD_GET_SECRET_VALUE = "dashboard-get-secret-value",
|
||||
DASHBOARD_GET_SECRET_VERSION_VALUE = "dashboard-get-secret-version-value"
|
||||
}
|
||||
|
||||
export const filterableSecretEvents: EventType[] = [
|
||||
@@ -599,6 +611,7 @@ interface CreateSecretEvent {
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretTags?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -613,6 +626,7 @@ interface CreateSecretBatchEvent {
|
||||
secretPath?: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretTags?: string[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -626,6 +640,7 @@ interface UpdateSecretEvent {
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretTags?: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -640,6 +655,7 @@ interface UpdateSecretBatchEvent {
|
||||
secretVersion: number;
|
||||
secretMetadata?: TSecretMetadata;
|
||||
secretPath?: string;
|
||||
secretTags?: string[];
|
||||
}>;
|
||||
};
|
||||
}
|
||||
@@ -3581,6 +3597,96 @@ interface ProjectDeleteEvent {
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardListSecretsEvent {
|
||||
type: EventType.DASHBOARD_LIST_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
secretIds: string[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardGetSecretValueEvent {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VALUE;
|
||||
metadata: {
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DashboardGetSecretVersionValueEvent {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE;
|
||||
metadata: {
|
||||
secretId: string;
|
||||
version: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectRoleCreateEvent {
|
||||
type: EventType.CREATE_PROJECT_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
permissions: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectRoleUpdateEvent {
|
||||
type: EventType.UPDATE_PROJECT_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
permissions?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface ProjectRoleDeleteEvent {
|
||||
type: EventType.DELETE_PROJECT_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgRoleCreateEvent {
|
||||
type: EventType.CREATE_ORG_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
permissions: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgRoleUpdateEvent {
|
||||
type: EventType.UPDATE_ORG_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug?: string;
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
permissions?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface OrgRoleDeleteEvent {
|
||||
type: EventType.DELETE_ORG_ROLE;
|
||||
metadata: {
|
||||
roleId: string;
|
||||
slug: string;
|
||||
name: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
@@ -3905,4 +4011,13 @@ export type Event =
|
||||
| ProjectDeleteEvent
|
||||
| SecretReminderCreateEvent
|
||||
| SecretReminderGetEvent
|
||||
| SecretReminderDeleteEvent;
|
||||
| SecretReminderDeleteEvent
|
||||
| DashboardListSecretsEvent
|
||||
| DashboardGetSecretValueEvent
|
||||
| DashboardGetSecretVersionValueEvent
|
||||
| ProjectRoleCreateEvent
|
||||
| ProjectRoleUpdateEvent
|
||||
| ProjectRoleDeleteEvent
|
||||
| OrgRoleCreateEvent
|
||||
| OrgRoleUpdateEvent
|
||||
| OrgRoleDeleteEvent;
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
PutUserPolicyCommand,
|
||||
RemoveUserFromGroupCommand
|
||||
} from "@aws-sdk/client-iam";
|
||||
import { AssumeRoleCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { AssumeRoleCommand, GetSessionTokenCommand, STSClient } from "@aws-sdk/client-sts";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CustomAWSHasher } from "@app/lib/aws/hashing";
|
||||
@@ -26,9 +26,12 @@ import { BadRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||
import { sanitizeString } from "@app/lib/fn";
|
||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||
|
||||
import { AwsIamAuthType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
import { AwsIamAuthType, AwsIamCredentialType, DynamicSecretAwsIamSchema, TDynamicProviderFns } from "./models";
|
||||
import { compileUsernameTemplate } from "./templateUtils";
|
||||
|
||||
// AWS STS duration constants (in seconds)
|
||||
const AWS_STS_MIN_DURATION = 900;
|
||||
|
||||
const generateUsername = (usernameTemplate?: string | null, identity?: { name: string }) => {
|
||||
const randomUsername = alphaNumericNanoId(32);
|
||||
if (!usernameTemplate) return randomUsername;
|
||||
@@ -120,6 +123,58 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
const validateConnection = async (inputs: unknown, { projectId }: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
try {
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials: {
|
||||
accessKeyId: providerInputs.accessKey,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
}
|
||||
});
|
||||
|
||||
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
|
||||
return true;
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
const appCfg = getConfig();
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials:
|
||||
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
? {
|
||||
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
await stsClient.send(
|
||||
new AssumeRoleCommand({
|
||||
RoleArn: providerInputs.roleArn,
|
||||
RoleSessionName: `infisical-validation-${crypto.nativeCrypto.randomUUID()}`,
|
||||
DurationSeconds: AWS_STS_MIN_DURATION,
|
||||
ExternalId: projectId
|
||||
})
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.IRSA) {
|
||||
const stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher
|
||||
});
|
||||
|
||||
await stsClient.send(new GetSessionTokenCommand({ DurationSeconds: AWS_STS_MIN_DURATION }));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const client = await $getClient(providerInputs, projectId);
|
||||
const isConnected = await client
|
||||
.send(new GetUserCommand({}))
|
||||
@@ -137,7 +192,7 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
});
|
||||
return isConnected;
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [];
|
||||
const sensitiveTokens: string[] = [];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
@@ -163,102 +218,269 @@ export const AwsIamProvider = (): TDynamicProviderFns => {
|
||||
};
|
||||
metadata: { projectId: string };
|
||||
}) => {
|
||||
const { inputs, usernameTemplate, metadata, identity } = data;
|
||||
const { inputs, usernameTemplate, metadata, identity, expireAt } = data;
|
||||
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
|
||||
try {
|
||||
let stsClient: STSClient;
|
||||
let entityId: string;
|
||||
|
||||
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
|
||||
const additionalTags = providerInputs.tags.map((tag) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}));
|
||||
awsTags.push(...additionalTags);
|
||||
const currentTime = Date.now();
|
||||
const requestedDuration = Math.floor((expireAt - currentTime) / 1000);
|
||||
|
||||
if (requestedDuration <= 0) {
|
||||
throw new BadRequestError({ message: "Expiration time must be in the future" });
|
||||
}
|
||||
|
||||
let durationSeconds: number;
|
||||
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
durationSeconds = requestedDuration;
|
||||
const appCfg = getConfig();
|
||||
stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials:
|
||||
appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID && appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
? {
|
||||
accessKeyId: appCfg.DYNAMIC_SECRET_AWS_ACCESS_KEY_ID,
|
||||
secretAccessKey: appCfg.DYNAMIC_SECRET_AWS_SECRET_ACCESS_KEY
|
||||
}
|
||||
: undefined
|
||||
});
|
||||
|
||||
const assumeRoleRes = await stsClient.send(
|
||||
new AssumeRoleCommand({
|
||||
RoleArn: providerInputs.roleArn,
|
||||
RoleSessionName: `infisical-temp-cred-${crypto.nativeCrypto.randomUUID()}`,
|
||||
DurationSeconds: durationSeconds,
|
||||
ExternalId: metadata.projectId
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
!assumeRoleRes.Credentials?.AccessKeyId ||
|
||||
!assumeRoleRes.Credentials?.SecretAccessKey ||
|
||||
!assumeRoleRes.Credentials?.SessionToken
|
||||
) {
|
||||
throw new BadRequestError({ message: "Failed to assume role - verify credentials and role configuration" });
|
||||
}
|
||||
|
||||
entityId = `assume-role-${alphaNumericNanoId(8)}`;
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
ACCESS_KEY: assumeRoleRes.Credentials.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: assumeRoleRes.Credentials.SecretAccessKey,
|
||||
SESSION_TOKEN: assumeRoleRes.Credentials.SessionToken
|
||||
}
|
||||
};
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
durationSeconds = requestedDuration;
|
||||
stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher,
|
||||
credentials: {
|
||||
accessKeyId: providerInputs.accessKey,
|
||||
secretAccessKey: providerInputs.secretAccessKey
|
||||
}
|
||||
});
|
||||
|
||||
const sessionTokenRes = await stsClient.send(
|
||||
new GetSessionTokenCommand({
|
||||
DurationSeconds: durationSeconds
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
!sessionTokenRes.Credentials?.AccessKeyId ||
|
||||
!sessionTokenRes.Credentials?.SecretAccessKey ||
|
||||
!sessionTokenRes.Credentials?.SessionToken
|
||||
) {
|
||||
throw new BadRequestError({ message: "Failed to get session token - verify credentials and permissions" });
|
||||
}
|
||||
|
||||
entityId = `session-token-${alphaNumericNanoId(8)}`;
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
|
||||
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
|
||||
}
|
||||
};
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.IRSA) {
|
||||
durationSeconds = requestedDuration;
|
||||
stsClient = new STSClient({
|
||||
region: providerInputs.region,
|
||||
useFipsEndpoint: crypto.isFipsModeEnabled(),
|
||||
sha256: CustomAWSHasher
|
||||
});
|
||||
|
||||
const sessionTokenRes = await stsClient.send(
|
||||
new GetSessionTokenCommand({
|
||||
DurationSeconds: durationSeconds
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
!sessionTokenRes.Credentials?.AccessKeyId ||
|
||||
!sessionTokenRes.Credentials?.SecretAccessKey ||
|
||||
!sessionTokenRes.Credentials?.SessionToken
|
||||
) {
|
||||
throw new BadRequestError({
|
||||
message: "Failed to get session token - verify IRSA credentials and permissions"
|
||||
});
|
||||
}
|
||||
|
||||
entityId = `irsa-session-${alphaNumericNanoId(8)}`;
|
||||
return {
|
||||
entityId,
|
||||
data: {
|
||||
ACCESS_KEY: sessionTokenRes.Credentials.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: sessionTokenRes.Credentials.SecretAccessKey,
|
||||
SESSION_TOKEN: sessionTokenRes.Credentials.SessionToken
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Unsupported authentication method for temporary credentials" });
|
||||
} catch (err) {
|
||||
const sensitiveTokens: string[] = [];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
|
||||
let errorMessage = (err as Error)?.message || "Unknown error";
|
||||
|
||||
if (err && typeof err === "object" && "name" in err && "$metadata" in err) {
|
||||
const awsError = err as { name?: string; message?: string; $metadata?: object };
|
||||
if (awsError.name) {
|
||||
errorMessage = `${awsError.name}: ${errorMessage}`;
|
||||
}
|
||||
}
|
||||
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: errorMessage,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create temporary credentials: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.IamUser) {
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
const username = generateUsername(usernameTemplate, identity);
|
||||
const { policyArns, userGroups, policyDocument, awsPath, permissionBoundaryPolicyArn } = providerInputs;
|
||||
const awsTags = [{ Key: "createdBy", Value: "infisical-dynamic-secret" }];
|
||||
|
||||
if (providerInputs.tags && Array.isArray(providerInputs.tags)) {
|
||||
const additionalTags = providerInputs.tags.map((tag) => ({
|
||||
Key: tag.key,
|
||||
Value: tag.value
|
||||
}));
|
||||
awsTags.push(...additionalTags);
|
||||
}
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(
|
||||
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
|
||||
try {
|
||||
const createUserRes = await client.send(
|
||||
new CreateUserCommand({
|
||||
Path: awsPath,
|
||||
PermissionsBoundary: permissionBoundaryPolicyArn || undefined,
|
||||
Tags: awsTags,
|
||||
UserName: username
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
if (!createUserRes.User) throw new BadRequestError({ message: "Failed to create AWS IAM User" });
|
||||
if (userGroups) {
|
||||
await Promise.all(
|
||||
userGroups
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((group) =>
|
||||
client.send(new AddUserToGroupCommand({ UserName: createUserRes?.User?.UserName, GroupName: group }))
|
||||
)
|
||||
);
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
if (policyArns) {
|
||||
await Promise.all(
|
||||
policyArns
|
||||
.split(",")
|
||||
.filter(Boolean)
|
||||
.map((policyArn) =>
|
||||
client.send(
|
||||
new AttachUserPolicyCommand({ UserName: createUserRes?.User?.UserName, PolicyArn: policyArn })
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
if (policyDocument) {
|
||||
await client.send(
|
||||
new PutUserPolicyCommand({
|
||||
UserName: createUserRes.User.UserName,
|
||||
PolicyName: `infisical-dynamic-policy-${alphaNumericNanoId(4)}`,
|
||||
PolicyDocument: policyDocument
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const createAccessKeyRes = await client.send(
|
||||
new CreateAccessKeyCommand({
|
||||
UserName: createUserRes.User.UserName
|
||||
})
|
||||
);
|
||||
if (!createAccessKeyRes.AccessKey)
|
||||
throw new BadRequestError({ message: "Failed to create AWS IAM User access key" });
|
||||
|
||||
return {
|
||||
entityId: username,
|
||||
data: {
|
||||
ACCESS_KEY: createAccessKeyRes.AccessKey.AccessKeyId,
|
||||
SECRET_ACCESS_KEY: createAccessKeyRes.AccessKey.SecretAccessKey,
|
||||
USERNAME: username
|
||||
}
|
||||
};
|
||||
} catch (err) {
|
||||
const sensitiveTokens = [username];
|
||||
if (providerInputs.method === AwsIamAuthType.AccessKey) {
|
||||
sensitiveTokens.push(providerInputs.accessKey, providerInputs.secretAccessKey);
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
if (providerInputs.method === AwsIamAuthType.AssumeRole) {
|
||||
sensitiveTokens.push(providerInputs.roleArn);
|
||||
}
|
||||
const sanitizedErrorMessage = sanitizeString({
|
||||
unsanitizedString: (err as Error)?.message,
|
||||
tokens: sensitiveTokens
|
||||
});
|
||||
throw new BadRequestError({
|
||||
message: `Failed to create lease from provider: ${sanitizedErrorMessage}`
|
||||
});
|
||||
}
|
||||
|
||||
throw new BadRequestError({ message: "Invalid credential type specified" });
|
||||
};
|
||||
|
||||
const revoke = async (inputs: unknown, entityId: string, metadata: { projectId: string }) => {
|
||||
const providerInputs = await validateProviderInputs(inputs);
|
||||
|
||||
if (providerInputs.credentialType === AwsIamCredentialType.TemporaryCredentials) {
|
||||
return { entityId };
|
||||
}
|
||||
|
||||
const client = await $getClient(providerInputs, metadata.projectId);
|
||||
|
||||
const username = entityId;
|
||||
|
||||
@@ -32,6 +32,11 @@ export enum AwsIamAuthType {
|
||||
IRSA = "irsa"
|
||||
}
|
||||
|
||||
export enum AwsIamCredentialType {
|
||||
IamUser = "iam-user",
|
||||
TemporaryCredentials = "temporary-credentials"
|
||||
}
|
||||
|
||||
export enum ElasticSearchAuthTypes {
|
||||
User = "user",
|
||||
ApiKey = "api-key"
|
||||
@@ -203,6 +208,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||
z.discriminatedUnion("method", [
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.AccessKey),
|
||||
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
|
||||
accessKey: z.string().trim().min(1),
|
||||
secretAccessKey: z.string().trim().min(1),
|
||||
region: z.string().trim().min(1),
|
||||
@@ -215,6 +221,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.AssumeRole),
|
||||
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
|
||||
roleArn: z.string().trim().min(1, "Role ARN required"),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
@@ -226,6 +233,7 @@ export const DynamicSecretAwsIamSchema = z.preprocess(
|
||||
}),
|
||||
z.object({
|
||||
method: z.literal(AwsIamAuthType.IRSA),
|
||||
credentialType: z.nativeEnum(AwsIamCredentialType).default(AwsIamCredentialType.IamUser),
|
||||
region: z.string().trim().min(1),
|
||||
awsPath: z.string().trim().optional(),
|
||||
permissionBoundaryPolicyArn: z.string().trim().optional(),
|
||||
|
||||
@@ -395,7 +395,8 @@ export const gatewayV2ServiceFactory = ({
|
||||
relayId: gateway.relayId,
|
||||
orgId: gateway.orgId,
|
||||
orgName: gateway.orgName,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
gatewayName: gateway.name
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -508,7 +509,8 @@ export const gatewayV2ServiceFactory = ({
|
||||
const relayCredentials = await relayService.getCredentialsForGateway({
|
||||
relayName,
|
||||
orgId,
|
||||
gatewayId: gateway.id
|
||||
gatewayId: gateway.id,
|
||||
gatewayName: gateway.name
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -160,7 +160,10 @@ export const licenseServiceFactory = ({
|
||||
}
|
||||
|
||||
if (isValidOfflineLicense) {
|
||||
onPremFeatures = contents.license.features;
|
||||
onPremFeatures = {
|
||||
...contents.license.features,
|
||||
slug: "enterprise"
|
||||
};
|
||||
instanceType = InstanceType.EnterpriseOnPremOffline;
|
||||
logger.info(`Instance type: ${InstanceType.EnterpriseOnPremOffline}`);
|
||||
isValidLicense = true;
|
||||
|
||||
@@ -24,7 +24,7 @@ export type TOfflineLicense = {
|
||||
|
||||
export type TFeatureSet = {
|
||||
_id: null;
|
||||
slug: null;
|
||||
slug: string | null;
|
||||
tier: -1;
|
||||
workspaceLimit: null;
|
||||
workspacesUsed: number;
|
||||
|
||||
@@ -58,6 +58,13 @@ export enum OrgPermissionGatewayActions {
|
||||
AttachGateways = "attach-gateways"
|
||||
}
|
||||
|
||||
export enum OrgPermissionRelayActions {
|
||||
CreateRelays = "create-relays",
|
||||
ListRelays = "list-relays",
|
||||
EditRelays = "edit-relays",
|
||||
DeleteRelays = "delete-relays"
|
||||
}
|
||||
|
||||
export enum OrgPermissionIdentityActions {
|
||||
Read = "read",
|
||||
Create = "create",
|
||||
@@ -109,6 +116,7 @@ export enum OrgPermissionSubjects {
|
||||
AppConnections = "app-connections",
|
||||
Kmip = "kmip",
|
||||
Gateway = "gateway",
|
||||
Relay = "relay",
|
||||
SecretShare = "secret-share"
|
||||
}
|
||||
|
||||
@@ -136,6 +144,7 @@ export type OrgPermissionSet =
|
||||
| [OrgPermissionAuditLogsActions, OrgPermissionSubjects.AuditLogs]
|
||||
| [OrgPermissionActions, OrgPermissionSubjects.ProjectTemplates]
|
||||
| [OrgPermissionGatewayActions, OrgPermissionSubjects.Gateway]
|
||||
| [OrgPermissionRelayActions, OrgPermissionSubjects.Relay]
|
||||
| [
|
||||
OrgPermissionAppConnectionActions,
|
||||
(
|
||||
@@ -279,6 +288,12 @@ export const OrgPermissionSchema = z.discriminatedUnion("subject", [
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionGatewayActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
}),
|
||||
z.object({
|
||||
subject: z.literal(OrgPermissionSubjects.Relay).describe("The entity this permission pertains to."),
|
||||
action: CASL_ACTION_SCHEMA_NATIVE_ENUM(OrgPermissionRelayActions).describe(
|
||||
"Describe what action an entity can take."
|
||||
)
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -383,6 +398,11 @@ const buildAdminPermission = () => {
|
||||
can(OrgPermissionGatewayActions.DeleteGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
can(OrgPermissionAdminConsoleAction.AccessAllProjects, OrgPermissionSubjects.AdminConsole);
|
||||
|
||||
can(OrgPermissionKmipActions.Setup, OrgPermissionSubjects.Kmip);
|
||||
@@ -445,6 +465,10 @@ const buildMemberPermission = () => {
|
||||
can(OrgPermissionGatewayActions.CreateGateways, OrgPermissionSubjects.Gateway);
|
||||
can(OrgPermissionGatewayActions.AttachGateways, OrgPermissionSubjects.Gateway);
|
||||
|
||||
can(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.CreateRelays, OrgPermissionSubjects.Relay);
|
||||
can(OrgPermissionRelayActions.EditRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
can(OrgPermissionMachineIdentityAuthTemplateActions.ListTemplates, OrgPermissionSubjects.MachineIdentityAuthTemplate);
|
||||
can(
|
||||
OrgPermissionMachineIdentityAuthTemplateActions.UnlinkTemplates,
|
||||
|
||||
@@ -754,7 +754,8 @@ export const pitServiceFactory = ({
|
||||
secrets: newSecrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
@@ -781,7 +782,8 @@ export const pitServiceFactory = ({
|
||||
secrets: updatedSecrets.map((secret) => ({
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version
|
||||
secretVersion: secret.version,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const RELAY_CONNECTING_GATEWAY_INFO = "1.3.6.1.4.1.12345.100.3";
|
||||
@@ -1,9 +1,13 @@
|
||||
import { isIP } from "node:net";
|
||||
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import * as x509 from "@peculiar/x509";
|
||||
|
||||
import { TRelays } from "@app/db/schemas";
|
||||
import { PgSqlLock } from "@app/keystore/keystore";
|
||||
import { crypto } from "@app/lib/crypto";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||
import { constructPemChainFromCerts, prependCertToPemChain } from "@app/services/certificate/certificate-fns";
|
||||
import { CertExtendedKeyUsage, CertKeyAlgorithm, CertKeyUsage } from "@app/services/certificate/certificate-types";
|
||||
import {
|
||||
@@ -14,11 +18,15 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
|
||||
import { verifyHostInputValidity } from "../dynamic-secret/dynamic-secret-fns";
|
||||
import { TLicenseServiceFactory } from "../license/license-service";
|
||||
import { OrgPermissionRelayActions, OrgPermissionSubjects } from "../permission/org-permission";
|
||||
import { TPermissionServiceFactory } from "../permission/permission-service-types";
|
||||
import { createSshCert, createSshKeyPair } from "../ssh/ssh-certificate-authority-fns";
|
||||
import { SshCertType } from "../ssh/ssh-certificate-authority-types";
|
||||
import { SshCertKeyAlgorithm } from "../ssh-certificate/ssh-certificate-types";
|
||||
import { TInstanceRelayConfigDALFactory } from "./instance-relay-config-dal";
|
||||
import { TOrgRelayConfigDALFactory } from "./org-relay-config-dal";
|
||||
import { RELAY_CONNECTING_GATEWAY_INFO } from "./relay-constants";
|
||||
import { TRelayDALFactory } from "./relay-dal";
|
||||
|
||||
export type TRelayServiceFactory = ReturnType<typeof relayServiceFactory>;
|
||||
@@ -29,12 +37,16 @@ export const relayServiceFactory = ({
|
||||
instanceRelayConfigDAL,
|
||||
orgRelayConfigDAL,
|
||||
relayDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
licenseService,
|
||||
permissionService
|
||||
}: {
|
||||
instanceRelayConfigDAL: TInstanceRelayConfigDALFactory;
|
||||
orgRelayConfigDAL: TOrgRelayConfigDALFactory;
|
||||
relayDAL: TRelayDALFactory;
|
||||
kmsService: TKmsServiceFactory;
|
||||
licenseService: TLicenseServiceFactory;
|
||||
permissionService: TPermissionServiceFactory;
|
||||
}) => {
|
||||
const $getInstanceCAs = async () => {
|
||||
const instanceConfig = await instanceRelayConfigDAL.transaction(async (tx) => {
|
||||
@@ -639,8 +651,9 @@ export const relayServiceFactory = ({
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.SERVER_AUTH]], true),
|
||||
|
||||
// san
|
||||
new x509.SubjectAlternativeNameExtension([{ type: "ip", value: host }], false)
|
||||
new x509.SubjectAlternativeNameExtension([{ type: isIP(host) ? "ip" : "dns", value: host }], false)
|
||||
];
|
||||
|
||||
const relayServerSerialNumber = createSerialNumber();
|
||||
@@ -689,6 +702,7 @@ export const relayServiceFactory = ({
|
||||
|
||||
const $generateRelayClientCredentials = async ({
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
orgId,
|
||||
orgName,
|
||||
relayPkiClientCaCertificate,
|
||||
@@ -697,6 +711,7 @@ export const relayServiceFactory = ({
|
||||
relayPkiServerCaCertificateChain
|
||||
}: {
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
relayPkiClientCaCertificate: Buffer;
|
||||
@@ -727,6 +742,16 @@ export const relayServiceFactory = ({
|
||||
const clientCertPrivateKey = crypto.nativeCrypto.KeyObject.from(clientKeys.privateKey);
|
||||
const clientCertSerialNumber = createSerialNumber();
|
||||
|
||||
const connectingGatewayInfoExtension = new x509.Extension(
|
||||
RELAY_CONNECTING_GATEWAY_INFO,
|
||||
false,
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
name: gatewayName
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// Build standard extensions
|
||||
const extensions: x509.Extension[] = [
|
||||
new x509.BasicConstraintsExtension(false),
|
||||
@@ -740,7 +765,8 @@ export const relayServiceFactory = ({
|
||||
x509.KeyUsageFlags[CertKeyUsage.KEY_AGREEMENT],
|
||||
true
|
||||
),
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true)
|
||||
new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage[CertExtendedKeyUsage.CLIENT_AUTH]], true),
|
||||
connectingGatewayInfoExtension
|
||||
];
|
||||
|
||||
const clientCert = await x509.X509CertificateGenerator.create({
|
||||
@@ -768,11 +794,13 @@ export const relayServiceFactory = ({
|
||||
const getCredentialsForGateway = async ({
|
||||
relayName,
|
||||
orgId,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
gatewayName
|
||||
}: {
|
||||
relayName: string;
|
||||
orgId: string;
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
}) => {
|
||||
let relay: TRelays | null = await relayDAL.findOne({
|
||||
orgId,
|
||||
@@ -819,10 +847,10 @@ export const relayServiceFactory = ({
|
||||
const relayClientSshCert = await createSshCert({
|
||||
caPrivateKey: orgCAs.relaySshClientCaPrivateKey.toString("utf8"),
|
||||
clientPublicKey: relayClientSshPublicKey,
|
||||
keyId: `relay-client-${relay.id}`,
|
||||
principals: [gatewayId],
|
||||
keyId: `client-${relayName}`,
|
||||
principals: [gatewayId, gatewayName],
|
||||
certType: SshCertType.USER,
|
||||
requestedTtl: "30d"
|
||||
requestedTtl: "1d"
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -837,12 +865,14 @@ export const relayServiceFactory = ({
|
||||
relayId,
|
||||
orgId,
|
||||
orgName,
|
||||
gatewayId
|
||||
gatewayId,
|
||||
gatewayName
|
||||
}: {
|
||||
relayId: string;
|
||||
orgId: string;
|
||||
orgName: string;
|
||||
gatewayId: string;
|
||||
gatewayName: string;
|
||||
}) => {
|
||||
const relay = await relayDAL.findOne({
|
||||
id: relayId
|
||||
@@ -860,6 +890,7 @@ export const relayServiceFactory = ({
|
||||
const instanceCAs = await $getInstanceCAs();
|
||||
const relayCertificateCredentials = await $generateRelayClientCredentials({
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
orgId,
|
||||
orgName,
|
||||
relayPkiClientCaCertificate: instanceCAs.instanceRelayPkiClientCaCertificate,
|
||||
@@ -877,6 +908,7 @@ export const relayServiceFactory = ({
|
||||
const orgCAs = await $getOrgCAs(orgId);
|
||||
const relayCertificateCredentials = await $generateRelayClientCredentials({
|
||||
gatewayId,
|
||||
gatewayName,
|
||||
orgId,
|
||||
orgName,
|
||||
relayPkiClientCaCertificate: orgCAs.relayPkiClientCaCertificate,
|
||||
@@ -895,11 +927,13 @@ export const relayServiceFactory = ({
|
||||
host,
|
||||
name,
|
||||
identityId,
|
||||
actorAuthMethod,
|
||||
orgId
|
||||
}: {
|
||||
host: string;
|
||||
name: string;
|
||||
identityId?: string;
|
||||
actorAuthMethod?: ActorAuthMethod;
|
||||
orgId?: string;
|
||||
}) => {
|
||||
let relay: TRelays;
|
||||
@@ -908,6 +942,27 @@ export const relayServiceFactory = ({
|
||||
await verifyHostInputValidity(host);
|
||||
|
||||
if (isOrgRelay) {
|
||||
const orgLicensePlan = await licenseService.getPlan(orgId);
|
||||
if (!orgLicensePlan.gateway) {
|
||||
throw new BadRequestError({
|
||||
message:
|
||||
"Relay registration failed due to organization plan restrictions. Please upgrade your instance to Infisical's Enterprise plan."
|
||||
});
|
||||
}
|
||||
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
ActorType.IDENTITY,
|
||||
identityId,
|
||||
orgId,
|
||||
actorAuthMethod!,
|
||||
orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
OrgPermissionRelayActions.CreateRelays,
|
||||
OrgPermissionSubjects.Relay
|
||||
);
|
||||
|
||||
relay = await relayDAL.transaction(async (tx) => {
|
||||
const existingRelay = await relayDAL.findOne(
|
||||
{
|
||||
@@ -995,9 +1050,75 @@ export const relayServiceFactory = ({
|
||||
});
|
||||
};
|
||||
|
||||
const getRelays = async ({
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
actorId: string;
|
||||
actor: ActorType;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.ListRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
const instanceRelays = await relayDAL.find({
|
||||
orgId: null
|
||||
});
|
||||
|
||||
const orgRelays = await relayDAL.find({
|
||||
orgId: actorOrgId
|
||||
});
|
||||
|
||||
return [...instanceRelays, ...orgRelays];
|
||||
};
|
||||
|
||||
const deleteRelay = async ({
|
||||
id,
|
||||
actorId,
|
||||
actor,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
}: {
|
||||
id: string;
|
||||
actorId: string;
|
||||
actor: ActorType;
|
||||
actorAuthMethod: ActorAuthMethod;
|
||||
actorOrgId: string;
|
||||
}) => {
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor,
|
||||
actorId,
|
||||
actorOrgId,
|
||||
actorAuthMethod,
|
||||
actorOrgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(OrgPermissionRelayActions.DeleteRelays, OrgPermissionSubjects.Relay);
|
||||
|
||||
const relay = await relayDAL.findById(id);
|
||||
if (!relay || relay.orgId !== actorOrgId || relay.orgId === null) {
|
||||
throw new NotFoundError({ message: "Relay not found" });
|
||||
}
|
||||
|
||||
const deletedRelay = await relayDAL.deleteById(id);
|
||||
return deletedRelay;
|
||||
};
|
||||
|
||||
return {
|
||||
registerRelay,
|
||||
getCredentialsForGateway,
|
||||
getCredentialsForClient
|
||||
getCredentialsForClient,
|
||||
getRelays,
|
||||
deleteRelay
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { TSecretApprovalRequests } from "@app/db/schemas";
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
@@ -11,6 +13,7 @@ type TSendApprovalEmails = {
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectId: string;
|
||||
secretApprovalRequest: TSecretApprovalRequests;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export const sendApprovalEmailsFn = async ({
|
||||
@@ -18,7 +21,8 @@ export const sendApprovalEmailsFn = async ({
|
||||
projectDAL,
|
||||
smtpService,
|
||||
projectId,
|
||||
secretApprovalRequest
|
||||
secretApprovalRequest,
|
||||
notificationService
|
||||
}: TSendApprovalEmails) => {
|
||||
const cfg = getConfig();
|
||||
|
||||
@@ -26,6 +30,17 @@ export const sendApprovalEmailsFn = async ({
|
||||
|
||||
const project = await projectDAL.findProjectWithOrg(projectId);
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
policy.userApprovers.map((approver) => ({
|
||||
userId: approver.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_CHANGE_REQUEST,
|
||||
title: "Secret Change Request",
|
||||
body: `You have a new secret change request pending your review for the project **${project.name}** in the organization **${project.organization.name}**.`,
|
||||
link: `/projects/secret-management/${project.id}/approval?requestId=${secretApprovalRequest.id}`
|
||||
}))
|
||||
);
|
||||
|
||||
// now we need to go through each of the reviewers and print out all the commits that they need to approve
|
||||
for await (const reviewerUser of policy.userApprovers) {
|
||||
await smtpService.sendMail({
|
||||
|
||||
@@ -28,6 +28,8 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { KmsDataKey } from "@app/services/kms/kms-types";
|
||||
import { TMicrosoftTeamsServiceFactory } from "@app/services/microsoft-teams/microsoft-teams-service";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "@app/services/microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
import { TProjectEnvDALFactory } from "@app/services/project-env/project-env-dal";
|
||||
@@ -140,6 +142,7 @@ type TSecretApprovalRequestServiceFactoryDep = {
|
||||
projectMicrosoftTeamsConfigDAL: Pick<TProjectMicrosoftTeamsConfigDALFactory, "getIntegrationDetailsByProject">;
|
||||
microsoftTeamsService: Pick<TMicrosoftTeamsServiceFactory, "sendNotification">;
|
||||
folderCommitService: Pick<TFolderCommitServiceFactory, "createCommit">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TSecretApprovalRequestServiceFactory = ReturnType<typeof secretApprovalRequestServiceFactory>;
|
||||
@@ -172,7 +175,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService,
|
||||
folderCommitService
|
||||
folderCommitService,
|
||||
notificationService
|
||||
}: TSecretApprovalRequestServiceFactoryDep) => {
|
||||
const requestCount = async ({
|
||||
projectId,
|
||||
@@ -1035,6 +1039,17 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
}
|
||||
});
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
approverUsers.map((approver) => ({
|
||||
userId: approver.id,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_CHANGE_POLICY_BYPASSED,
|
||||
title: "Secret Change Policy Bypassed",
|
||||
body: `**${requestedByUser.firstName} ${requestedByUser.lastName}** (${requestedByUser.email}) has merged a secret to **${policy.secretPath}** in the **${env.name}** environment for project **${project.name}** without obtaining the required approval.`,
|
||||
link: `/projects/secret-management/${project.id}/approval`
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: approverUsers.filter((approver) => approver.email).map((approver) => approver.email!),
|
||||
subjectLine: "Infisical Secret Change Policy Bypassed",
|
||||
@@ -1069,7 +1084,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
@@ -1085,7 +1102,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1104,7 +1123,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
});
|
||||
@@ -1120,7 +1141,9 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretKey: secret.key as string,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO
|
||||
secretMetadata: secret.secretMetadata as ResourceMetadataDTO,
|
||||
// @ts-expect-error not present on v1 secrets
|
||||
secretTags: (secret.tags as { name: string }[])?.map((tag) => tag.name)
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1446,7 +1469,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequest,
|
||||
smtpService,
|
||||
projectId
|
||||
projectId,
|
||||
notificationService
|
||||
});
|
||||
|
||||
return secretApprovalRequest;
|
||||
@@ -1813,7 +1837,8 @@ export const secretApprovalRequestServiceFactory = ({
|
||||
secretApprovalPolicyDAL,
|
||||
secretApprovalRequest,
|
||||
smtpService,
|
||||
projectId
|
||||
projectId,
|
||||
notificationService
|
||||
});
|
||||
return secretApprovalRequest;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
import { getConfig } from "@app/lib/config/env";
|
||||
import { logger } from "@app/lib/logger";
|
||||
import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
@@ -28,6 +30,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
projectMembershipDAL: Pick<TProjectMembershipDALFactory, "findAllProjectMembers">;
|
||||
projectDAL: Pick<TProjectDALFactory, "findById">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export const secretRotationV2QueueServiceFactory = async ({
|
||||
@@ -36,7 +39,8 @@ export const secretRotationV2QueueServiceFactory = async ({
|
||||
secretRotationV2Service,
|
||||
projectMembershipDAL,
|
||||
projectDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
notificationService
|
||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -152,6 +156,19 @@ export const secretRotationV2QueueServiceFactory = async ({
|
||||
|
||||
const rotationType = SECRET_ROTATION_NAME_MAP[type as SecretRotation];
|
||||
|
||||
const rotationPath = `/projects/secret-management/${projectId}/secrets/${environment.slug}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((admin) => ({
|
||||
userId: admin.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_ROTATION_FAILED,
|
||||
title: "Secret Rotation Failed",
|
||||
body: `Your **${rotationType}** rotation **${rotationName}** failed to rotate.`,
|
||||
link: rotationPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretRotationFailed,
|
||||
@@ -165,9 +182,7 @@ export const secretRotationV2QueueServiceFactory = async ({
|
||||
secretPath: folder.path,
|
||||
environment: environment.name,
|
||||
projectName: project.name,
|
||||
rotationUrl: encodeURI(
|
||||
`${appCfg.SITE_URL}/projects/secret-management/${projectId}/secrets/${environment.slug}`
|
||||
)
|
||||
rotationUrl: encodeURI(`${appCfg.SITE_URL}${rotationPath}`)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -21,6 +21,8 @@ import { decryptAppConnection } from "@app/services/app-connection/app-connectio
|
||||
import { TAppConnection } from "@app/services/app-connection/app-connection-types";
|
||||
import { ActorType } from "@app/services/auth/auth-type";
|
||||
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
|
||||
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
|
||||
import { NotificationType } from "@app/services/notification/notification-types";
|
||||
import { TProjectDALFactory } from "@app/services/project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "@app/services/project-membership/project-membership-dal";
|
||||
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
@@ -52,6 +54,7 @@ type TSecretRotationV2QueueServiceFactoryDep = {
|
||||
appConnectionDAL: Pick<TAppConnectionDALFactory, "updateById">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
keyStore: Pick<TKeyStoreFactory, "acquireLock" | "getItem">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TSecretScanningV2QueueServiceFactory = Awaited<ReturnType<typeof secretScanningV2QueueServiceFactory>>;
|
||||
@@ -65,7 +68,8 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
kmsService,
|
||||
auditLogService,
|
||||
keyStore,
|
||||
appConnectionDAL
|
||||
appConnectionDAL,
|
||||
notificationService
|
||||
}: TSecretRotationV2QueueServiceFactoryDep) => {
|
||||
const queueDataSourceFullScan = async (
|
||||
dataSource: TSecretScanningDataSourceWithConnection,
|
||||
@@ -592,16 +596,38 @@ export const secretScanningV2QueueServiceFactory = async ({
|
||||
|
||||
const timestamp = new Date().toISOString();
|
||||
|
||||
const subjectLine =
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? "Incident Alert: Secret(s) Leaked"
|
||||
: `Secret Scanning Failed`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
recipients.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? NotificationType.SECRET_SCANNING_SECRETS_DETECTED
|
||||
: NotificationType.SECRET_SCANNING_SCAN_FAILED,
|
||||
title: subjectLine,
|
||||
body:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? `Uncovered **${payload.numberOfSecrets}** secret(s) ${payload.isDiffScan ? " from a recent commit to" : " in"} **${resourceName}**.`
|
||||
: `Encountered an error while attempting to scan the resource **${resourceName}**: ${payload.errorMessage}`,
|
||||
link:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? `/projects/secret-scanning/${projectId}/findings?search=scanId:${payload.scanId}`
|
||||
: `/projects/secret-scanning/${projectId}/data-sources/${dataSource.type}/${dataSource.id}`
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: recipients.map((member) => member.user.email!).filter(Boolean),
|
||||
template:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? SmtpTemplates.SecretScanningV2SecretsDetected
|
||||
: SmtpTemplates.SecretScanningV2ScanFailed,
|
||||
subjectLine:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? "Incident Alert: Secret(s) Leaked"
|
||||
: `Secret Scanning Failed`,
|
||||
subjectLine,
|
||||
substitutions:
|
||||
payload.status === SecretScanningScanStatus.Completed
|
||||
? {
|
||||
|
||||
Reference in New Issue
Block a user