mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 02:26:40 +00:00
Merge remote-tracking branch 'origin/main' into feat/pki-ENG-3666
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
import { Knex } from "knex";
|
||||
|
||||
import { TableName } from "../schemas";
|
||||
|
||||
export async function up(knex: Knex): Promise<void> {
|
||||
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
|
||||
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
|
||||
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
|
||||
|
||||
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
if (hasAllowedNames) t.string("allowedNames", 1000).notNullable().alter();
|
||||
if (hasAllowedNamespaces) t.string("allowedNamespaces", 1000).notNullable().alter();
|
||||
if (hasAllowedAudience) t.string("allowedAudience", 1000).notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(knex: Knex): Promise<void> {
|
||||
const hasAllowedNamespaces = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNamespaces");
|
||||
const hasAllowedNames = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedNames");
|
||||
const hasAllowedAudience = await knex.schema.hasColumn(TableName.IdentityKubernetesAuth, "allowedAudience");
|
||||
|
||||
if (hasAllowedNamespaces || hasAllowedNames || hasAllowedAudience) {
|
||||
await knex.schema.alterTable(TableName.IdentityKubernetesAuth, (t) => {
|
||||
if (hasAllowedNames) t.string("allowedNames", 255).notNullable().alter();
|
||||
if (hasAllowedNamespaces) t.string("allowedNamespaces", 255).notNullable().alter();
|
||||
if (hasAllowedAudience) t.string("allowedAudience", 255).notNullable().alter();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
? {
|
||||
|
||||
@@ -254,6 +254,8 @@ export type TQueueJobTypes = {
|
||||
[QueueName.ImportSecretsFromExternalSource]: {
|
||||
name: QueueJobs.ImportSecretsFromExternalSource;
|
||||
payload: {
|
||||
orgId: string;
|
||||
actorId: string;
|
||||
actorEmail: string;
|
||||
importType: ExternalPlatforms;
|
||||
data: {
|
||||
|
||||
@@ -776,7 +776,8 @@ export const registerRoutes = async (
|
||||
orgDAL,
|
||||
totpService,
|
||||
orgMembershipDAL,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
notificationService
|
||||
});
|
||||
const passwordService = authPaswordServiceFactory({
|
||||
tokenService,
|
||||
@@ -890,7 +891,8 @@ export const registerRoutes = async (
|
||||
projectDAL,
|
||||
permissionService,
|
||||
projectUserMembershipRoleDAL,
|
||||
projectMembershipDAL
|
||||
projectMembershipDAL,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const rateLimitService = rateLimitServiceFactory({
|
||||
@@ -929,7 +931,8 @@ export const registerRoutes = async (
|
||||
projectRoleDAL,
|
||||
groupProjectDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
notificationService
|
||||
});
|
||||
const projectUserAdditionalPrivilegeService = projectUserAdditionalPrivilegeServiceFactory({
|
||||
permissionService,
|
||||
@@ -1096,7 +1099,9 @@ export const registerRoutes = async (
|
||||
instanceRelayConfigDAL,
|
||||
orgRelayConfigDAL,
|
||||
relayDAL,
|
||||
kmsService
|
||||
kmsService,
|
||||
licenseService,
|
||||
permissionService
|
||||
});
|
||||
|
||||
const gatewayV2Service = gatewayV2ServiceFactory({
|
||||
@@ -1134,7 +1139,8 @@ export const registerRoutes = async (
|
||||
appConnectionDAL,
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretQueueService = secretQueueFactory({
|
||||
@@ -1218,7 +1224,8 @@ export const registerRoutes = async (
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
reminderService
|
||||
reminderService,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const projectEnvService = projectEnvServiceFactory({
|
||||
@@ -1352,7 +1359,8 @@ export const registerRoutes = async (
|
||||
resourceMetadataDAL,
|
||||
projectMicrosoftTeamsConfigDAL,
|
||||
microsoftTeamsService,
|
||||
folderCommitService
|
||||
folderCommitService,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretService = secretServiceFactory({
|
||||
@@ -1803,7 +1811,8 @@ export const registerRoutes = async (
|
||||
secretV2BridgeService,
|
||||
resourceMetadataDAL,
|
||||
folderCommitService,
|
||||
folderVersionDAL
|
||||
folderVersionDAL,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const migrationService = externalMigrationServiceFactory({
|
||||
@@ -2054,7 +2063,8 @@ export const registerRoutes = async (
|
||||
queueService,
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretScanningV2Queue = await secretScanningV2QueueServiceFactory({
|
||||
@@ -2066,7 +2076,8 @@ export const registerRoutes = async (
|
||||
smtpService,
|
||||
kmsService,
|
||||
keyStore,
|
||||
appConnectionDAL
|
||||
appConnectionDAL,
|
||||
notificationService
|
||||
});
|
||||
|
||||
const secretScanningV2Service = secretScanningV2ServiceFactory({
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { z } from "zod";
|
||||
|
||||
import { SecretFoldersSchema, SecretImportsSchema, UsersSchema } from "@app/db/schemas";
|
||||
import { SecretFoldersSchema, SecretImportsSchema, SecretType, UsersSchema } from "@app/db/schemas";
|
||||
import { RemindersSchema } from "@app/db/schemas/reminders";
|
||||
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";
|
||||
import { DASHBOARD } from "@app/lib/api-docs";
|
||||
import { BadRequestError } from "@app/lib/errors";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
import { removeTrailingSlash } from "@app/lib/fn";
|
||||
import { OrderByDirection } from "@app/lib/types";
|
||||
import { secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { readLimit, secretsLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
@@ -111,6 +111,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
SecretRotationV2Schema,
|
||||
z.object({
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
@@ -124,7 +125,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
isEmpty: z.boolean(),
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
@@ -219,7 +222,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
let imports: Awaited<ReturnType<typeof server.services.secretImport.getImportsMultiEnv>> | undefined;
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFoldersMultiEnv>> | undefined;
|
||||
let secrets: Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>> | undefined;
|
||||
let secrets:
|
||||
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRawMultiEnv>>[number] & { isEmpty: boolean })[]
|
||||
| undefined;
|
||||
let dynamicSecrets:
|
||||
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
|
||||
| undefined;
|
||||
@@ -426,43 +431,51 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
});
|
||||
|
||||
if (remainingLimit > 0 && totalSecretCount > adjustedOffset) {
|
||||
secrets = await server.services.secret.getSecretsRawMultiEnv({
|
||||
viewSecretValue: true,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environments,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
isInternal: true
|
||||
});
|
||||
secrets = (
|
||||
await server.services.secret.getSecretsRawMultiEnv({
|
||||
viewSecretValue: true,
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environments,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
search,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset,
|
||||
isInternal: true
|
||||
})
|
||||
).map((secret) => ({ ...secret, isEmpty: !secret.secretValue }));
|
||||
}
|
||||
}
|
||||
|
||||
if (secrets?.length || secretRotations?.length) {
|
||||
for await (const environment of environments) {
|
||||
const secretCountFromEnv =
|
||||
(secrets?.filter((secret) => secret.environment === environment).length ?? 0) +
|
||||
(secretRotations
|
||||
?.filter((rotation) => rotation.environment.slug === environment)
|
||||
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
|
||||
const secretIds = [
|
||||
...new Set(
|
||||
[
|
||||
...(secrets?.filter((secret) => secret.environment === environment) ?? []),
|
||||
...(secretRotations
|
||||
?.filter((rotation) => rotation.environment.slug === environment)
|
||||
.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
|
||||
].map((secret) => secret.id)
|
||||
)
|
||||
];
|
||||
|
||||
if (secretCountFromEnv) {
|
||||
if (secretIds) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCountFromEnv
|
||||
numberOfSecrets: secretIds.length,
|
||||
secretIds
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -473,7 +486,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secretCountFromEnv,
|
||||
numberOfSecrets: secretIds.length,
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -584,7 +597,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.optional(),
|
||||
search: z.string().trim().describe(DASHBOARD.SECRET_DETAILS_LIST.search).optional(),
|
||||
tags: z.string().trim().transform(decodeURIComponent).describe(DASHBOARD.SECRET_DETAILS_LIST.tags).optional(),
|
||||
viewSecretValue: booleanSchema.default(true),
|
||||
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
|
||||
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
|
||||
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
|
||||
@@ -606,7 +618,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
SecretRotationV2Schema,
|
||||
z.object({
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
isEmpty: z.boolean(),
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
secretMetadata: ResourceMetadataSchema.optional(),
|
||||
@@ -619,7 +633,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
isEmpty: z.boolean(),
|
||||
secretReminderRecipients: z
|
||||
.object({
|
||||
user: UsersSchema.pick({ id: true, email: true, username: true }),
|
||||
@@ -715,12 +731,21 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
let folders: Awaited<ReturnType<typeof server.services.folder.getFolders>> | undefined;
|
||||
let secrets:
|
||||
| (Awaited<ReturnType<typeof server.services.secret.getSecretsRaw>>["secrets"][number] & {
|
||||
isEmpty: boolean;
|
||||
reminder: Awaited<ReturnType<typeof server.services.reminder.getRemindersForDashboard>>[string] | null;
|
||||
})[]
|
||||
| undefined;
|
||||
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
|
||||
let secretRotations:
|
||||
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
|
||||
| (Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>[number] & {
|
||||
secrets: (NonNullable<
|
||||
Awaited<
|
||||
ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>
|
||||
>[number]["secrets"][number] & {
|
||||
isEmpty: boolean;
|
||||
}
|
||||
> | null)[];
|
||||
})[]
|
||||
| undefined;
|
||||
|
||||
let totalImportCount: number | undefined;
|
||||
@@ -822,19 +847,31 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
|
||||
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environments: [environment],
|
||||
secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
secretRotations = (
|
||||
await server.services.secretRotationV2.getDashboardSecretRotations(
|
||||
{
|
||||
projectId,
|
||||
search,
|
||||
orderBy,
|
||||
orderDirection,
|
||||
environments: [environment],
|
||||
secretPath,
|
||||
limit: remainingLimit,
|
||||
offset: adjustedOffset
|
||||
},
|
||||
req.permission
|
||||
)
|
||||
).map((rotation) => ({
|
||||
...rotation,
|
||||
secrets: rotation.secrets.map((secret) =>
|
||||
secret
|
||||
? {
|
||||
...secret,
|
||||
isEmpty: !secret.secretValue
|
||||
}
|
||||
: secret
|
||||
)
|
||||
}));
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
@@ -919,7 +956,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
viewSecretValue: req.query.viewSecretValue,
|
||||
viewSecretValue: true,
|
||||
throwOnMissingReadValuePermission: false,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
@@ -943,6 +980,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
|
||||
secrets = rawSecrets.map((secret) => ({
|
||||
...secret,
|
||||
isEmpty: !secret.secretValue,
|
||||
reminder: reminders[secret.id] ?? null
|
||||
}));
|
||||
}
|
||||
@@ -977,19 +1015,25 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}));
|
||||
|
||||
if (secrets?.length || secretRotations?.length) {
|
||||
const secretCount =
|
||||
(secrets?.length ?? 0) +
|
||||
(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))).length ?? 0);
|
||||
const secretIds = [
|
||||
...new Set(
|
||||
[
|
||||
...(secrets ?? []),
|
||||
...(secretRotations?.flatMap((rotation) => rotation.secrets.filter((secret) => Boolean(secret))) ?? [])
|
||||
].map((secret) => secret.id)
|
||||
)
|
||||
];
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCount
|
||||
numberOfSecrets: secretIds.length,
|
||||
secretIds
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1000,7 +1044,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
numberOfSecrets: secretCount,
|
||||
numberOfSecrets: secretIds.length,
|
||||
projectId,
|
||||
environment,
|
||||
secretPath,
|
||||
@@ -1060,6 +1104,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
.array()
|
||||
.optional(),
|
||||
secrets: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
secretPath: z.string().optional(),
|
||||
@@ -1145,18 +1190,20 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
);
|
||||
|
||||
for await (const environment of environments) {
|
||||
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
|
||||
const envSecrets = secrets.filter((secret) => secret.environment === environment);
|
||||
const secretCountForEnv = envSecrets.length;
|
||||
|
||||
if (secretCountForEnv) {
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secretCountForEnv
|
||||
numberOfSecrets: secretCountForEnv,
|
||||
secretIds: envSecrets.map((secret) => secret.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1259,6 +1306,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretPath: z.string().optional(),
|
||||
@@ -1310,6 +1358,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
// TODO(scott): omit secretValue here, but requires refactor of uploading env/copy from board
|
||||
secrets: secretRawSchema
|
||||
.extend({
|
||||
secretValueHidden: z.boolean(),
|
||||
@@ -1345,11 +1394,12 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRETS,
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
numberOfSecrets: secrets.length
|
||||
numberOfSecrets: secrets.length,
|
||||
secretIds: secrets.map((secret) => secret.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1373,4 +1423,256 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
|
||||
return { secrets };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secret-value",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
security: [
|
||||
{
|
||||
bearerAuth: []
|
||||
}
|
||||
],
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
secretKey: z.string().trim(),
|
||||
isOverride: z
|
||||
.enum(["true", "false"])
|
||||
.transform((value) => value === "true")
|
||||
.optional()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
valueOverride: z.string().optional(),
|
||||
value: z.string().optional()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { secretPath, projectId, environment, secretKey, isOverride } = req.query;
|
||||
|
||||
// TODO (scott): just get the secret instead of searching for it in list
|
||||
const { secrets } = await server.services.secret.getSecretsRaw({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
viewSecretValue: true,
|
||||
throwOnMissingReadValuePermission: false,
|
||||
actorOrgId: req.permission.orgId,
|
||||
environment,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
projectId,
|
||||
path: secretPath,
|
||||
search: secretKey,
|
||||
includeTagsInSearch: true,
|
||||
includeMetadataInSearch: true
|
||||
});
|
||||
|
||||
if (isOverride) {
|
||||
const personalSecret = secrets.find(
|
||||
(secret) => secret.type === SecretType.Personal && secret.secretKey === secretKey
|
||||
);
|
||||
|
||||
if (!personalSecret)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find personal secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
if (personalSecret)
|
||||
return {
|
||||
valueOverride: personalSecret.secretValue
|
||||
};
|
||||
}
|
||||
|
||||
const sharedSecret = secrets.find(
|
||||
(secret) => secret.type === SecretType.Shared && secret.secretKey === secretKey
|
||||
);
|
||||
|
||||
if (!sharedSecret)
|
||||
throw new BadRequestError({
|
||||
message: `Could not find secret with key "${secretKey}" at secret path "${secretPath}" in environment "${environment}" for project with ID "${projectId}"`
|
||||
});
|
||||
|
||||
// only audit if not personal
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VALUE,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
secretPath: req.query.secretPath,
|
||||
secretKey,
|
||||
secretId: sharedSecret.id
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { value: sharedSecret.secretValue };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
url: "/secret-imports",
|
||||
method: "GET",
|
||||
config: {
|
||||
rateLimit: secretsLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
projectId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
path: z.string().trim().default("/").transform(removeTrailingSlash)
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secrets: z
|
||||
.object({
|
||||
secretPath: z.string(),
|
||||
environment: z.string(),
|
||||
environmentInfo: z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string()
|
||||
}),
|
||||
folderId: z.string().optional(),
|
||||
secrets: secretRawSchema.omit({ secretValue: true }).extend({ isEmpty: z.boolean() }).array()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const importedSecrets = await server.services.secretImport.getRawSecretsFromImports({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
...req.query
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: req.query.projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DASHBOARD_LIST_SECRETS,
|
||||
metadata: {
|
||||
environment: req.query.environment,
|
||||
secretPath: req.query.path,
|
||||
numberOfSecrets: importedSecrets.length,
|
||||
secretIds: importedSecrets.map((secret) => secret.id)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
secrets: importedSecrets.map((importData) => ({
|
||||
...importData,
|
||||
secrets: importData.secrets.map((secret) => ({
|
||||
...secret,
|
||||
isEmpty: !secret.secretValue
|
||||
}))
|
||||
}))
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secret-versions/:secretId",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretId: z.string()
|
||||
}),
|
||||
querystring: z.object({
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
secretVersions: secretRawSchema
|
||||
.omit({ secretValue: true })
|
||||
.extend({
|
||||
secretValueHidden: z.boolean()
|
||||
})
|
||||
.array()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const secretVersions = await server.services.secret.getSecretVersions({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
limit: req.query.limit,
|
||||
offset: req.query.offset,
|
||||
secretId: req.params.secretId
|
||||
});
|
||||
|
||||
return { secretVersions };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/secret-versions/:secretId/value/:version",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
params: z.object({
|
||||
secretId: z.string(),
|
||||
version: z.string()
|
||||
}),
|
||||
|
||||
response: {
|
||||
200: z.object({
|
||||
value: z.string()
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const { version, secretId } = req.params;
|
||||
|
||||
const [secretVersion] = await server.services.secret.getSecretVersions({
|
||||
actor: req.permission.type,
|
||||
actorId: req.permission.id,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
actorOrgId: req.permission.orgId,
|
||||
secretId,
|
||||
secretVersions: [version]
|
||||
});
|
||||
|
||||
if (!secretVersion)
|
||||
throw new NotFoundError({
|
||||
message: `Could not find secret version "${version}" for secret with ID "${secretId}`
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId: secretVersion.workspace,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.DASHBOARD_GET_SECRET_VERSION_VALUE,
|
||||
metadata: {
|
||||
secretId,
|
||||
version
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { value: secretVersion.secretValue };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -3,8 +3,10 @@ import { z } from "zod";
|
||||
import { UserNotificationsSchema } from "@app/db/schemas/user-notifications";
|
||||
import { UnauthorizedError } from "@app/lib/errors";
|
||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
|
||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||
import { AuthMode } from "@app/services/auth/auth-type";
|
||||
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
|
||||
|
||||
export const registerNotificationRouter = async (server: FastifyZodProvider) => {
|
||||
server.route({
|
||||
@@ -97,6 +99,16 @@ export const registerNotificationRouter = async (server: FastifyZodProvider) =>
|
||||
...req.body
|
||||
});
|
||||
|
||||
await server.services.telemetry.sendPostHogEvents({
|
||||
event: PostHogEventTypes.NotificationUpdated,
|
||||
distinctId: getTelemetryDistinctId(req),
|
||||
organizationId: req.permission.orgId,
|
||||
properties: {
|
||||
notificationId: req.params.notificationId,
|
||||
...req.body
|
||||
}
|
||||
});
|
||||
|
||||
return { notification };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -627,7 +627,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -780,7 +781,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -2154,7 +2156,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -2288,7 +2291,6 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
return { approval: secretOperation.approval };
|
||||
}
|
||||
const { secrets } = secretOperation;
|
||||
|
||||
const secretMetadataMap = new Map(
|
||||
inputSecrets.map(({ secretKey, secretMetadata }) => [secretKey, secretMetadata])
|
||||
);
|
||||
@@ -2308,7 +2310,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -2328,7 +2331,8 @@ export const registerDeprecatedSecretRouter = async (server: FastifyZodProvider)
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -478,7 +478,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -621,7 +622,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretId: secret.id,
|
||||
secretKey: req.params.secretName,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: req.body.secretMetadata
|
||||
secretMetadata: req.body.secretMetadata,
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -762,7 +764,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
hide: false,
|
||||
tags: [ApiDocsTags.Secrets],
|
||||
body: z.object({
|
||||
projectSlug: z.string().trim(),
|
||||
projectId: z.string().trim(),
|
||||
sourceEnvironment: z.string().trim(),
|
||||
sourceSecretPath: z.string().trim().default("/").transform(removeTrailingSlash),
|
||||
destinationEnvironment: z.string().trim(),
|
||||
@@ -911,7 +913,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretId: secret.id,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1063,7 +1066,8 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
secretPath: secret.secretPath,
|
||||
secretKey: secret.secretKey,
|
||||
secretVersion: secret.version,
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey)
|
||||
secretMetadata: secretMetadataMap.get(secret.secretKey),
|
||||
secretTags: secret.tags?.map((tag) => tag.name)
|
||||
}))
|
||||
}
|
||||
}
|
||||
@@ -1262,7 +1266,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
handler: async (req) => {
|
||||
const { secretName } = req.params;
|
||||
const { secretPath, environment, projectId } = req.query;
|
||||
const { tree, value } = await server.services.secret.getSecretReferenceTree({
|
||||
const { tree, value, secret } = await server.services.secret.getSecretReferenceTree({
|
||||
actorId: req.permission.id,
|
||||
actor: req.permission.type,
|
||||
actorAuthMethod: req.permission.authMethod,
|
||||
@@ -1273,6 +1277,21 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
|
||||
environment
|
||||
});
|
||||
|
||||
await server.services.auditLog.createAuditLog({
|
||||
projectId,
|
||||
...req.auditLogInfo,
|
||||
event: {
|
||||
type: EventType.GET_SECRET,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secretId: secret.id,
|
||||
secretKey: secretName,
|
||||
secretVersion: secret.version
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return { tree, value };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -266,9 +266,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -316,9 +316,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -475,9 +475,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission: orgPermission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
if (appConnection.projectId) {
|
||||
@@ -633,9 +633,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
@@ -803,9 +803,9 @@ export const appConnectionServiceFactory = ({
|
||||
const { permission } = await permissionService.getOrgPermission(
|
||||
actor.type,
|
||||
actor.id,
|
||||
actor.orgId,
|
||||
appConnection.orgId,
|
||||
actor.authMethod,
|
||||
appConnection.orgId
|
||||
actor.orgId
|
||||
);
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
|
||||
@@ -14,6 +14,8 @@ import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||
|
||||
import { TAuthTokenServiceFactory } from "../auth-token/auth-token-service";
|
||||
import { TokenType } from "../auth-token/auth-token-types";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { getDefaultOrgMembershipRole } from "../org/org-role-fns";
|
||||
import { TOrgMembershipDALFactory } from "../org-membership/org-membership-dal";
|
||||
@@ -47,6 +49,7 @@ type TAuthLoginServiceFactoryDep = {
|
||||
totpService: Pick<TTotpServiceFactory, "verifyUserTotp" | "verifyWithUserRecoveryCode">;
|
||||
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||
orgMembershipDAL: TOrgMembershipDALFactory;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TAuthLoginFactory = ReturnType<typeof authLoginServiceFactory>;
|
||||
@@ -57,7 +60,8 @@ export const authLoginServiceFactory = ({
|
||||
orgDAL,
|
||||
orgMembershipDAL,
|
||||
totpService,
|
||||
auditLogService
|
||||
auditLogService,
|
||||
notificationService
|
||||
}: TAuthLoginServiceFactoryDep) => {
|
||||
/*
|
||||
* Private
|
||||
@@ -71,6 +75,16 @@ export const authLoginServiceFactory = ({
|
||||
if (!isDeviceSeen) {
|
||||
const newDeviceList = devices.concat([{ ip, userAgent }]);
|
||||
await userDAL.updateById(user.id, { devices: JSON.stringify(newDeviceList) }, tx);
|
||||
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: user.id,
|
||||
type: NotificationType.LOGIN_FROM_NEW_DEVICE,
|
||||
title: "Login From New Device",
|
||||
body: `A new device with IP **${ip}** and User Agent **${userAgent}** has logged into your account.`
|
||||
}
|
||||
]);
|
||||
|
||||
if (user.email) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.NewDeviceJoin,
|
||||
@@ -563,6 +577,18 @@ export const authLoginServiceFactory = ({
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
if (adminEmails.length > 0) {
|
||||
await notificationService.createUserNotifications(
|
||||
orgAdmins
|
||||
.filter((admin) => admin.user.id !== user.id)
|
||||
.map((admin) => ({
|
||||
userId: admin.user.id,
|
||||
orgId: organizationId,
|
||||
type: NotificationType.ADMIN_SSO_BYPASS,
|
||||
title: "Security Alert: Admin SSO Bypass",
|
||||
body: `The org admin **${user.email}** has bypassed enforced SSO login.`
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: adminEmails,
|
||||
subjectLine: "Security Alert: Admin SSO Bypass",
|
||||
|
||||
@@ -5,6 +5,8 @@ import { QueueJobs, QueueName, TQueueServiceFactory } from "@app/queue";
|
||||
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectServiceFactory } from "../project/project-service";
|
||||
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
||||
@@ -42,6 +44,7 @@ export type TExternalMigrationQueueFactoryDep = {
|
||||
folderVersionDAL: Pick<TSecretFolderVersionDALFactory, "create">;
|
||||
|
||||
resourceMetadataDAL: Pick<TResourceMetadataDALFactory, "insertMany" | "delete">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TExternalMigrationQueueFactory = ReturnType<typeof externalMigrationQueueFactory>;
|
||||
@@ -62,9 +65,12 @@ export const externalMigrationQueueFactory = ({
|
||||
folderDAL,
|
||||
folderCommitService,
|
||||
folderVersionDAL,
|
||||
resourceMetadataDAL
|
||||
resourceMetadataDAL,
|
||||
notificationService
|
||||
}: TExternalMigrationQueueFactoryDep) => {
|
||||
const startImport = async (dto: {
|
||||
orgId: string;
|
||||
actorId: string;
|
||||
actorEmail: string;
|
||||
importType: ExternalPlatforms;
|
||||
data: {
|
||||
@@ -87,9 +93,19 @@ export const externalMigrationQueueFactory = ({
|
||||
};
|
||||
|
||||
queueService.start(QueueName.ImportSecretsFromExternalSource, async (job) => {
|
||||
const { data, actorEmail, importType } = job.data;
|
||||
const { data, actorEmail, importType, actorId, orgId } = job.data;
|
||||
|
||||
try {
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_STARTED,
|
||||
title: "Import Started",
|
||||
body: `An import from **${importType}** to Infisical has been started.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import started",
|
||||
@@ -137,6 +153,16 @@ export const externalMigrationQueueFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_SUCCESSFUL,
|
||||
title: "Import Successful",
|
||||
body: `An import from **${importType}** to Infisical has successfully completed.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [actorEmail],
|
||||
subjectLine: "Infisical import successful",
|
||||
@@ -146,6 +172,17 @@ export const externalMigrationQueueFactory = ({
|
||||
template: SmtpTemplates.ExternalImportSuccessful
|
||||
});
|
||||
} catch (err) {
|
||||
await notificationService.createUserNotifications([
|
||||
{
|
||||
userId: actorId,
|
||||
orgId,
|
||||
type: NotificationType.IMPORT_FAILED,
|
||||
title: "Import Failed",
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-member-access
|
||||
body: `An import from **${importType}** to Infisical has failed: ${(err as any)?.message || "Unknown error"}.`
|
||||
}
|
||||
]);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: [job.data.actorEmail],
|
||||
subjectLine: "Infisical import failed",
|
||||
|
||||
@@ -73,6 +73,8 @@ export const externalMigrationServiceFactory = ({
|
||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorId: user.id,
|
||||
orgId: actorOrgId,
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.EnvKey,
|
||||
data: {
|
||||
@@ -131,6 +133,8 @@ export const externalMigrationServiceFactory = ({
|
||||
const encrypted = crypto.encryption().symmetric().encryptWithRootEncryptionKey(stringifiedJson);
|
||||
|
||||
await externalMigrationQueue.startImport({
|
||||
actorId: user.id,
|
||||
orgId: actorOrgId,
|
||||
actorEmail: user.email!,
|
||||
importType: ExternalPlatforms.Vault,
|
||||
data: {
|
||||
|
||||
@@ -84,69 +84,70 @@ export const identityUaServiceFactory = ({
|
||||
|
||||
const LOCKOUT_KEY = `lockout:identity:${identityUa.identityId}:${IdentityAuthMethod.UNIVERSAL_AUTH}:${clientId}`;
|
||||
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||
if (identityUa.lockoutEnabled) {
|
||||
try {
|
||||
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 500, {
|
||||
retryCount: 3,
|
||||
retryDelay: 300,
|
||||
retryJitter: 100
|
||||
});
|
||||
} catch (e) {
|
||||
logger.info(
|
||||
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
|
||||
);
|
||||
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
|
||||
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||
|
||||
let lockout: LockoutObject | undefined;
|
||||
if (lockoutRaw) {
|
||||
lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||
}
|
||||
|
||||
if (lockout && lockout.lockedOut) {
|
||||
throw new UnauthorizedError({
|
||||
message: "This identity auth method is temporarily locked, please try again later"
|
||||
});
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
|
||||
const clientSecretPrefix = clientSecret.slice(0, 4);
|
||||
const clientSecretInfo = await identityUaClientSecretDAL.find({
|
||||
identityUAId: identityUa.id,
|
||||
isClientSecretRevoked: false,
|
||||
clientSecretPrefix
|
||||
});
|
||||
|
||||
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
|
||||
for await (const info of clientSecretInfo) {
|
||||
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
|
||||
|
||||
if (isMatch) {
|
||||
validClientSecretInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const lockoutRaw = await keyStore.getItem(LOCKOUT_KEY);
|
||||
if (!validClientSecretInfo) {
|
||||
if (identityUa.lockoutEnabled) {
|
||||
let lock: Awaited<ReturnType<typeof keyStore.acquireLock>> | undefined;
|
||||
try {
|
||||
lock = await keyStore.acquireLock([KeyStorePrefixes.IdentityLockoutLock(LOCKOUT_KEY)], 300, {
|
||||
retryCount: 3,
|
||||
retryDelay: 300,
|
||||
retryJitter: 100
|
||||
});
|
||||
|
||||
let lockout: LockoutObject | undefined;
|
||||
if (lockoutRaw) {
|
||||
lockout = JSON.parse(lockoutRaw) as LockoutObject;
|
||||
}
|
||||
|
||||
if (lockout && lockout.lockedOut) {
|
||||
throw new UnauthorizedError({
|
||||
message: "This identity auth method is temporarily locked, please try again later"
|
||||
});
|
||||
}
|
||||
|
||||
const identityMembershipOrg = await identityOrgMembershipDAL.findOne({ identityId: identityUa.identityId });
|
||||
if (!identityMembershipOrg) {
|
||||
throw new UnauthorizedError({
|
||||
message: "Invalid credentials"
|
||||
});
|
||||
}
|
||||
|
||||
const clientSecretPrefix = clientSecret.slice(0, 4);
|
||||
const clientSecretInfo = await identityUaClientSecretDAL.find({
|
||||
identityUAId: identityUa.id,
|
||||
isClientSecretRevoked: false,
|
||||
clientSecretPrefix
|
||||
});
|
||||
|
||||
let validClientSecretInfo: (typeof clientSecretInfo)[0] | null = null;
|
||||
for await (const info of clientSecretInfo) {
|
||||
const isMatch = await crypto.hashing().compareHash(clientSecret, info.clientSecretHash);
|
||||
|
||||
if (isMatch) {
|
||||
validClientSecretInfo = info;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!validClientSecretInfo) {
|
||||
if (identityUa.lockoutEnabled) {
|
||||
if (!lockout) {
|
||||
// Re-fetch the latest lockout data while holding the lock
|
||||
const lockoutRawNew = await keyStore.getItem(LOCKOUT_KEY);
|
||||
if (lockoutRawNew) {
|
||||
lockout = JSON.parse(lockoutRawNew) as LockoutObject;
|
||||
} else {
|
||||
lockout = {
|
||||
lockedOut: false,
|
||||
failedAttempts: 0
|
||||
};
|
||||
}
|
||||
|
||||
if (lockout.lockedOut) {
|
||||
throw new UnauthorizedError({
|
||||
message: "This identity auth method is temporarily locked, please try again later"
|
||||
});
|
||||
}
|
||||
|
||||
lockout.failedAttempts += 1;
|
||||
if (lockout.failedAttempts >= identityUa.lockoutThreshold) {
|
||||
lockout.lockedOut = true;
|
||||
@@ -157,110 +158,121 @@ export const identityUaServiceFactory = ({
|
||||
lockout.lockedOut ? identityUa.lockoutDurationSeconds : identityUa.lockoutCounterResetSeconds,
|
||||
JSON.stringify(lockout)
|
||||
);
|
||||
}
|
||||
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
} else if (lockout) {
|
||||
await keyStore.deleteItem(LOCKOUT_KEY);
|
||||
}
|
||||
|
||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||
if (Number(clientSecretTTL) > 0) {
|
||||
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationTime) {
|
||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to expired client secret"
|
||||
});
|
||||
} catch (e) {
|
||||
if (lock === undefined) {
|
||||
logger.info(
|
||||
`identity login failed to acquire lock [identityId=${identityUa.identityId}] [authMethod=${IdentityAuthMethod.UNIVERSAL_AUTH}]`
|
||||
);
|
||||
throw new RateLimitError({ message: "Failed to acquire lock: rate limit exceeded" });
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
if (lock) {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
|
||||
// number of times client secret can be used for
|
||||
// a login operation reached
|
||||
throw new UnauthorizedError({ message: "Invalid credentials" });
|
||||
} else if (lockout) {
|
||||
// If credentials are valid, clear any existing lockout record
|
||||
await keyStore.deleteItem(LOCKOUT_KEY);
|
||||
}
|
||||
|
||||
const { clientSecretTTL, clientSecretNumUses, clientSecretNumUsesLimit } = validClientSecretInfo;
|
||||
if (Number(clientSecretTTL) > 0) {
|
||||
const clientSecretCreated = new Date(validClientSecretInfo.createdAt);
|
||||
const ttlInMilliseconds = Number(clientSecretTTL) * 1000;
|
||||
const currentDate = new Date();
|
||||
const expirationTime = new Date(clientSecretCreated.getTime() + ttlInMilliseconds);
|
||||
|
||||
if (currentDate > expirationTime) {
|
||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to client secret usage limit reached"
|
||||
message: "Access denied due to expired client secret"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const accessTokenTTLParams =
|
||||
Number(identityUa.accessTokenPeriod) === 0
|
||||
? {
|
||||
accessTokenTTL: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
|
||||
}
|
||||
: {
|
||||
accessTokenTTL: identityUa.accessTokenPeriod,
|
||||
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
|
||||
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
|
||||
accessTokenMaxTTL: 1000000000
|
||||
};
|
||||
|
||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
|
||||
await identityOrgMembershipDAL.updateById(
|
||||
identityMembershipOrg.id,
|
||||
{
|
||||
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
lastLoginTime: new Date()
|
||||
},
|
||||
tx
|
||||
);
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
isAccessTokenRevoked: false,
|
||||
identityUAClientSecretId: uaClientSecretDoc.id,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
|
||||
accessTokenPeriod: identityUa.accessTokenPeriod,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
...accessTokenTTLParams
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return newToken;
|
||||
if (clientSecretNumUsesLimit > 0 && clientSecretNumUses >= clientSecretNumUsesLimit) {
|
||||
// number of times client secret can be used for
|
||||
// a login operation reached
|
||||
await identityUaClientSecretDAL.updateById(validClientSecretInfo.id, {
|
||||
isClientSecretRevoked: true
|
||||
});
|
||||
throw new UnauthorizedError({
|
||||
message: "Access denied due to client secret usage limit reached"
|
||||
});
|
||||
}
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = crypto.jwt().sign(
|
||||
const accessTokenTTLParams =
|
||||
Number(identityUa.accessTokenPeriod) === 0
|
||||
? {
|
||||
accessTokenTTL: identityUa.accessTokenTTL,
|
||||
accessTokenMaxTTL: identityUa.accessTokenMaxTTL
|
||||
}
|
||||
: {
|
||||
accessTokenTTL: identityUa.accessTokenPeriod,
|
||||
// We set a very large Max TTL for periodic tokens to ensure that clients (even outdated ones) can always renew their token
|
||||
// without them having to update their SDKs, CLIs, etc. This workaround sets it to 30 years to emulate "forever"
|
||||
accessTokenMaxTTL: 1000000000
|
||||
};
|
||||
|
||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
|
||||
await identityOrgMembershipDAL.updateById(
|
||||
identityMembershipOrg.id,
|
||||
{
|
||||
lastLoginAuthMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
lastLoginTime: new Date()
|
||||
},
|
||||
tx
|
||||
);
|
||||
const newToken = await identityAccessTokenDAL.create(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
clientSecretId: validClientSecretInfo.id,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
isAccessTokenRevoked: false,
|
||||
identityUAClientSecretId: uaClientSecretDoc.id,
|
||||
accessTokenNumUses: 0,
|
||||
accessTokenNumUsesLimit: identityUa.accessTokenNumUsesLimit,
|
||||
accessTokenPeriod: identityUa.accessTokenPeriod,
|
||||
authMethod: IdentityAuthMethod.UNIVERSAL_AUTH,
|
||||
...accessTokenTTLParams
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
identityUa,
|
||||
validClientSecretInfo,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg,
|
||||
...accessTokenTTLParams
|
||||
};
|
||||
} finally {
|
||||
if (lock) await lock.release();
|
||||
}
|
||||
return newToken;
|
||||
});
|
||||
|
||||
const appCfg = getConfig();
|
||||
const accessToken = crypto.jwt().sign(
|
||||
{
|
||||
identityId: identityUa.identityId,
|
||||
clientSecretId: validClientSecretInfo.id,
|
||||
identityAccessTokenId: identityAccessToken.id,
|
||||
authTokenType: AuthTokenType.IDENTITY_ACCESS_TOKEN
|
||||
} as TIdentityAccessTokenJwtPayload,
|
||||
appCfg.AUTH_SECRET,
|
||||
// akhilmhdh: for non-expiry tokens you should not even set the value, including undefined. Even for undefined jsonwebtoken throws error
|
||||
Number(identityAccessToken.accessTokenTTL) === 0
|
||||
? undefined
|
||||
: {
|
||||
expiresIn: Number(identityAccessToken.accessTokenTTL)
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
accessToken,
|
||||
identityUa,
|
||||
validClientSecretInfo,
|
||||
identityAccessToken,
|
||||
identityMembershipOrg,
|
||||
...accessTokenTTLParams
|
||||
};
|
||||
};
|
||||
|
||||
const attachUniversalAuth = async ({
|
||||
|
||||
@@ -1,6 +1,21 @@
|
||||
export enum NotificationType {
|
||||
ACCESS_APPROVAL_REQUEST = "access-approval-request",
|
||||
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated"
|
||||
ACCESS_APPROVAL_REQUEST_UPDATED = "access-approval-request-updated",
|
||||
ACCESS_POLICY_BYPASSED = "access-policy-bypassed",
|
||||
SECRET_CHANGE_REQUEST = "secret-change-request",
|
||||
SECRET_CHANGE_POLICY_BYPASSED = "secret-change-policy-bypassed",
|
||||
SECRET_ROTATION_FAILED = "secret-rotation-failed",
|
||||
SECRET_SCANNING_SECRETS_DETECTED = "secret-scanning-secrets-detected",
|
||||
SECRET_SCANNING_SCAN_FAILED = "secret-scanning-scan-failed",
|
||||
LOGIN_FROM_NEW_DEVICE = "login-from-new-device",
|
||||
ADMIN_SSO_BYPASS = "admin-sso-bypass",
|
||||
IMPORT_STARTED = "import-started",
|
||||
IMPORT_SUCCESSFUL = "import-successful",
|
||||
IMPORT_FAILED = "import-failed",
|
||||
DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN = "direct-project-access-issued-to-admin",
|
||||
PROJECT_ACCESS_REQUEST = "project-access-request",
|
||||
PROJECT_INVITATION = "project-invitation",
|
||||
SECRET_SYNC_FAILED = "secret-sync-failed"
|
||||
}
|
||||
|
||||
export interface TCreateUserNotificationDTO {
|
||||
|
||||
@@ -5,6 +5,8 @@ import { OrgPermissionAdminConsoleAction, OrgPermissionSubjects } from "@app/ee/
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
||||
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectMembershipDALFactory } from "../project-membership/project-membership-dal";
|
||||
import { TProjectUserMembershipRoleDALFactory } from "../project-membership/project-user-membership-role-dal";
|
||||
@@ -20,6 +22,7 @@ type TOrgAdminServiceFactoryDep = {
|
||||
>;
|
||||
projectUserMembershipRoleDAL: Pick<TProjectUserMembershipRoleDALFactory, "create" | "delete">;
|
||||
smtpService: Pick<TSmtpService, "sendMail">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TOrgAdminServiceFactory = ReturnType<typeof orgAdminServiceFactory>;
|
||||
@@ -29,7 +32,8 @@ export const orgAdminServiceFactory = ({
|
||||
projectDAL,
|
||||
projectMembershipDAL,
|
||||
projectUserMembershipRoleDAL,
|
||||
smtpService
|
||||
smtpService,
|
||||
notificationService
|
||||
}: TOrgAdminServiceFactoryDep) => {
|
||||
const listOrgProjects = async ({
|
||||
actor,
|
||||
@@ -130,23 +134,34 @@ export const orgAdminServiceFactory = ({
|
||||
});
|
||||
|
||||
const projectMembers = await projectMembershipDAL.findAllProjectMembers(projectId);
|
||||
const filteredProjectMembers = projectMembers
|
||||
.filter(
|
||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||
)
|
||||
.map((el) => el.user.email!)
|
||||
.filter(Boolean);
|
||||
const projectAdmins = projectMembers.filter(
|
||||
(member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin) && member.userId !== actorId
|
||||
);
|
||||
const mappedProjectAdmins = projectAdmins.map((el) => el.user.email!).filter(Boolean);
|
||||
const actorEmail = projectMembers.find((el) => el.userId === actorId)?.user?.username;
|
||||
|
||||
if (filteredProjectMembers.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||
recipients: filteredProjectMembers,
|
||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
email: projectMembers.find((el) => el.userId === actorId)?.user?.username
|
||||
}
|
||||
});
|
||||
if (actorEmail) {
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.DIRECT_PROJECT_ACCESS_ISSUED_TO_ADMIN,
|
||||
title: "Direct Project Access Issued",
|
||||
body: `The organization admin **${actorEmail}** has self-issued direct access to the project **${project.name}**.`
|
||||
}))
|
||||
);
|
||||
|
||||
if (mappedProjectAdmins.length) {
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.OrgAdminProjectDirectAccess,
|
||||
recipients: mappedProjectAdmins,
|
||||
subjectLine: "Organization Admin Project Direct Access Issued",
|
||||
substitutions: {
|
||||
projectName: project.name,
|
||||
email: actorEmail
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
return { isExistingMember: false, membership: updatedMembership };
|
||||
};
|
||||
|
||||
@@ -18,6 +18,8 @@ import { ms } from "@app/lib/ms";
|
||||
import { TUserGroupMembershipDALFactory } from "../../ee/services/group/user-group-membership-dal";
|
||||
import { ActorType } from "../auth/auth-type";
|
||||
import { TGroupProjectDALFactory } from "../group-project/group-project-dal";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TProjectDALFactory } from "../project/project-dal";
|
||||
import { TProjectBotDALFactory } from "../project-bot/project-bot-dal";
|
||||
@@ -56,6 +58,7 @@ type TProjectMembershipServiceFactoryDep = {
|
||||
projectUserAdditionalPrivilegeDAL: Pick<TProjectUserAdditionalPrivilegeDALFactory, "delete">;
|
||||
secretReminderRecipientsDAL: Pick<TSecretReminderRecipientsDALFactory, "delete">;
|
||||
groupProjectDAL: TGroupProjectDALFactory;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TProjectMembershipServiceFactory = ReturnType<typeof projectMembershipServiceFactory>;
|
||||
@@ -74,7 +77,8 @@ export const projectMembershipServiceFactory = ({
|
||||
projectDAL,
|
||||
projectKeyDAL,
|
||||
secretReminderRecipientsDAL,
|
||||
licenseService
|
||||
licenseService,
|
||||
notificationService
|
||||
}: TProjectMembershipServiceFactoryDep) => {
|
||||
const getProjectMemberships = async ({
|
||||
actorId,
|
||||
@@ -236,6 +240,16 @@ export const projectMembershipServiceFactory = ({
|
||||
});
|
||||
|
||||
if (sendEmails) {
|
||||
await notificationService.createUserNotifications(
|
||||
orgMembers.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.PROJECT_INVITATION,
|
||||
title: "Project Invitation",
|
||||
body: `You've been invited to join the project **${project.name}**.`
|
||||
}))
|
||||
);
|
||||
|
||||
const appCfg = getConfig();
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.WorkspaceInvite,
|
||||
|
||||
@@ -57,6 +57,8 @@ import { TKmsServiceFactory } from "../kms/kms-service";
|
||||
import { validateMicrosoftTeamsChannelsSchema } from "../microsoft-teams/microsoft-teams-fns";
|
||||
import { TMicrosoftTeamsIntegrationDALFactory } from "../microsoft-teams/microsoft-teams-integration-dal";
|
||||
import { TProjectMicrosoftTeamsConfigDALFactory } from "../microsoft-teams/project-microsoft-teams-config-dal";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
import { TOrgDALFactory } from "../org/org-dal";
|
||||
import { TPkiAlertDALFactory } from "../pki-alert/pki-alert-dal";
|
||||
import { TPkiCollectionDALFactory } from "../pki-collection/pki-collection-dal";
|
||||
@@ -183,6 +185,7 @@ type TProjectServiceFactoryDep = {
|
||||
>;
|
||||
projectTemplateService: TProjectTemplateServiceFactory;
|
||||
reminderService: Pick<TReminderServiceFactory, "deleteReminderBySecretId">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
export type TProjectServiceFactory = ReturnType<typeof projectServiceFactory>;
|
||||
@@ -227,7 +230,8 @@ export const projectServiceFactory = ({
|
||||
projectTemplateService,
|
||||
groupProjectDAL,
|
||||
smtpService,
|
||||
reminderService
|
||||
reminderService,
|
||||
notificationService
|
||||
}: TProjectServiceFactoryDep) => {
|
||||
/*
|
||||
* Create workspace. Make user the admin
|
||||
@@ -1924,6 +1928,21 @@ export const projectServiceFactory = ({
|
||||
projectTypeUrl = "cert-management";
|
||||
}
|
||||
|
||||
const callbackPath = `/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectMembers
|
||||
.filter((member) => member.roles.some((role) => role.role === ProjectMembershipRole.Admin))
|
||||
.map((member) => ({
|
||||
userId: member.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.PROJECT_ACCESS_REQUEST,
|
||||
title: "Project Access Request",
|
||||
body: `**${userDetails.firstName} ${userDetails.lastName}** (${userDetails.email}) has requested access to the project **${project.name}**.`,
|
||||
link: callbackPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
template: SmtpTemplates.ProjectAccessRequest,
|
||||
recipients: filteredProjectMembers,
|
||||
@@ -1934,7 +1953,7 @@ export const projectServiceFactory = ({
|
||||
projectName: project?.name,
|
||||
orgName: org?.name,
|
||||
note: comment,
|
||||
callback_url: `${appCfg.SITE_URL}/projects/${projectTypeUrl}/${project.id}/access-management?selectedTab=members&requesterEmail=${userDetails.email}`
|
||||
callback_url: `${appCfg.SITE_URL}${callbackPath}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -61,6 +61,8 @@ import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
|
||||
|
||||
import { TAppConnectionDALFactory } from "../app-connection/app-connection-dal";
|
||||
import { TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||
import { TNotificationServiceFactory } from "../notification/notification-service";
|
||||
import { NotificationType } from "../notification/notification-types";
|
||||
|
||||
export type TSecretSyncQueueFactory = ReturnType<typeof secretSyncQueueFactory>;
|
||||
|
||||
@@ -100,6 +102,7 @@ type TSecretSyncQueueFactoryDep = {
|
||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">;
|
||||
notificationService: Pick<TNotificationServiceFactory, "createUserNotifications">;
|
||||
};
|
||||
|
||||
type SecretSyncActionJob = Job<
|
||||
@@ -142,7 +145,8 @@ export const secretSyncQueueFactory = ({
|
||||
folderCommitService,
|
||||
licenseService,
|
||||
gatewayService,
|
||||
gatewayV2Service
|
||||
gatewayV2Service,
|
||||
notificationService
|
||||
}: TSecretSyncQueueFactoryDep) => {
|
||||
const appCfg = getConfig();
|
||||
|
||||
@@ -898,6 +902,19 @@ export const secretSyncQueueFactory = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const syncPath = `/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`;
|
||||
|
||||
await notificationService.createUserNotifications(
|
||||
projectAdmins.map((admin) => ({
|
||||
userId: admin.userId,
|
||||
orgId: project.orgId,
|
||||
type: NotificationType.SECRET_SYNC_FAILED,
|
||||
title: `Secret Sync Failed to ${actionLabel} Secrets`,
|
||||
body: `Your **${syncDestination}** sync **${name}** failed to complete${failureMessage ? `: \`${failureMessage}\`` : ""}`,
|
||||
link: syncPath
|
||||
}))
|
||||
);
|
||||
|
||||
await smtpService.sendMail({
|
||||
recipients: projectAdmins.map((member) => member.user.email!).filter(Boolean),
|
||||
template: SmtpTemplates.SecretSyncFailed,
|
||||
@@ -910,7 +927,7 @@ export const secretSyncQueueFactory = ({
|
||||
secretPath: folder?.path,
|
||||
environment: environment?.name,
|
||||
projectName: project.name,
|
||||
syncUrl: `${appCfg.SITE_URL}/projects/secret-management/${projectId}/integrations/secret-syncs/${destination}/${secretSync.id}`
|
||||
syncUrl: `${appCfg.SITE_URL}${syncPath}`
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -446,9 +446,10 @@ export const secretV2BridgeDALFactory = ({ db, keyStore }: TSecretV2DalArg) => {
|
||||
}
|
||||
})
|
||||
.where((bd) => {
|
||||
void bd
|
||||
.whereNull(`${TableName.SecretV2}.userId`)
|
||||
.orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
|
||||
void bd.whereNull(`${TableName.SecretV2}.userId`);
|
||||
// scott: removing this as we don't need to count overrides
|
||||
// and there is currently a bug when you move secrets that doesn't move the override so this can skew count
|
||||
// .orWhere({ [`${TableName.SecretV2}.userId` as "userId"]: userId || null });
|
||||
})
|
||||
.countDistinct(`${TableName.SecretV2}.key`);
|
||||
|
||||
|
||||
@@ -597,19 +597,27 @@ export const expandSecretReferencesFactory = ({
|
||||
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
|
||||
}
|
||||
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) return { value: "", tags: [] };
|
||||
const secrets = await secretDAL.findByFolderId({ folderId: folder.id });
|
||||
try {
|
||||
const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!folder) return { value: "", tags: [] };
|
||||
const secrets = await secretDAL.findByFolderId({ folderId: folder.id });
|
||||
|
||||
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prev[secret.key] = { value: decryptSecret(secret.encryptedValue) || "", tags: secret.tags?.map((el) => el.slug) };
|
||||
return prev;
|
||||
}, {});
|
||||
const decryptedSecret = secrets.reduce<Record<string, { value: string; tags: string[] }>>((prev, secret) => {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
prev[secret.key] = {
|
||||
value: decryptSecret(secret.encryptedValue) || "",
|
||||
tags: secret.tags?.map((el) => el.slug)
|
||||
};
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
secretCache[cacheKey] = decryptedSecret;
|
||||
secretCache[cacheKey] = decryptedSecret;
|
||||
|
||||
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
|
||||
return secretCache[cacheKey][secretKey] || { value: "", tags: [] };
|
||||
} catch (error) {
|
||||
secretCache[cacheKey] = {};
|
||||
return { value: "", tags: [] };
|
||||
}
|
||||
};
|
||||
|
||||
const recursivelyExpandSecret = async (dto: {
|
||||
@@ -622,11 +630,16 @@ export const expandSecretReferencesFactory = ({
|
||||
const stackTrace = { ...dto, key: "root", children: [] } as TSecretReferenceTraceNode;
|
||||
|
||||
if (!dto.value) return { expandedValue: "", stackTrace };
|
||||
const stack = [{ ...dto, depth: 0, trace: stackTrace }];
|
||||
|
||||
// Track visited secrets to prevent circular references
|
||||
const createSecretId = (env: string, secretPath: string, key: string) => `${env}:${secretPath}:${key}`;
|
||||
|
||||
const currentSecretId = createSecretId(dto.environment, dto.secretPath, dto.secretKey);
|
||||
const stack = [{ ...dto, depth: 0, trace: stackTrace, visitedSecrets: new Set<string>([currentSecretId]) }];
|
||||
let expandedValue = dto.value;
|
||||
|
||||
while (stack.length) {
|
||||
const { value, secretPath, environment, depth, trace } = stack.pop()!;
|
||||
const { value, secretPath, environment, depth, trace, visitedSecrets } = stack.pop()!;
|
||||
|
||||
// eslint-disable-next-line no-continue
|
||||
if (depth > MAX_SECRET_REFERENCE_DEPTH) continue;
|
||||
@@ -664,6 +677,7 @@ export const expandSecretReferencesFactory = ({
|
||||
});
|
||||
|
||||
const cacheKey = getCacheUniqueKey(environment, secretPath);
|
||||
if (!secretCache[cacheKey]) secretCache[cacheKey] = {};
|
||||
secretCache[cacheKey][secretKey] = referredValue;
|
||||
|
||||
referencedSecretValue = referredValue.value;
|
||||
@@ -683,6 +697,7 @@ export const expandSecretReferencesFactory = ({
|
||||
});
|
||||
|
||||
const cacheKey = getCacheUniqueKey(secretReferenceEnvironment, secretReferencePath);
|
||||
if (!secretCache[cacheKey]) secretCache[cacheKey] = {};
|
||||
secretCache[cacheKey][secretReferenceKey] = referedValue;
|
||||
|
||||
referencedSecretValue = referedValue.value;
|
||||
@@ -700,17 +715,27 @@ export const expandSecretReferencesFactory = ({
|
||||
trace
|
||||
};
|
||||
|
||||
const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue);
|
||||
// Check for circular reference
|
||||
const referencedSecretId = createSecretId(
|
||||
referencedSecretEnvironmentSlug,
|
||||
referencedSecretPath,
|
||||
referencedSecretKey
|
||||
);
|
||||
const isCircular = visitedSecrets.has(referencedSecretId);
|
||||
|
||||
const newVisitedSecrets = new Set([...visitedSecrets, referencedSecretId]);
|
||||
|
||||
const shouldExpandMore = INTERPOLATION_TEST_REGEX.test(referencedSecretValue) && !isCircular;
|
||||
if (dto.shouldStackTrace) {
|
||||
const stackTraceNode = { ...node, children: [], key: referencedSecretKey, trace: null };
|
||||
trace?.children.push(stackTraceNode);
|
||||
// if stack trace this would be child node
|
||||
if (shouldExpandMore) {
|
||||
stack.push({ ...node, trace: stackTraceNode });
|
||||
stack.push({ ...node, trace: stackTraceNode, visitedSecrets: newVisitedSecrets });
|
||||
}
|
||||
} else if (shouldExpandMore) {
|
||||
// if no stack trace is needed we just keep going with root node
|
||||
stack.push(node);
|
||||
stack.push({ ...node, visitedSecrets: newVisitedSecrets });
|
||||
}
|
||||
|
||||
if (referencedSecretValue) {
|
||||
|
||||
@@ -159,17 +159,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const uniqueReferenceEnvironmentSlugs = Array.from(new Set(references.map((el) => el.environment)));
|
||||
const referencesEnvironments = await projectEnvDAL.findBySlugs(projectId, uniqueReferenceEnvironmentSlugs, tx);
|
||||
|
||||
if (referencesEnvironments.length !== uniqueReferenceEnvironmentSlugs.length)
|
||||
throw new BadRequestError({
|
||||
message: `Referenced environment not found. Missing ${diff(
|
||||
uniqueReferenceEnvironmentSlugs,
|
||||
referencesEnvironments.map((el) => el.slug)
|
||||
).join(",")}`
|
||||
});
|
||||
|
||||
// Filter out references to non-existent environments
|
||||
const referencesEnvironmentGroupBySlug = groupBy(referencesEnvironments, (i) => i.slug);
|
||||
const validEnvironmentReferences = references.filter((el) => referencesEnvironmentGroupBySlug[el.environment]);
|
||||
|
||||
if (validEnvironmentReferences.length === 0) return;
|
||||
|
||||
const referredFolders = await folderDAL.findByManySecretPath(
|
||||
references.map((el) => ({
|
||||
validEnvironmentReferences.map((el) => ({
|
||||
secretPath: el.secretPath,
|
||||
envId: referencesEnvironmentGroupBySlug[el.environment][0].id
|
||||
})),
|
||||
@@ -177,58 +174,71 @@ export const secretV2BridgeServiceFactory = ({
|
||||
);
|
||||
|
||||
const referencesFolderGroupByPath = groupBy(referredFolders.filter(Boolean), (i) => `${i?.envId}-${i?.path}`);
|
||||
|
||||
// Find only references that have valid folders (don't throw for missing paths)
|
||||
const validReferences = validEnvironmentReferences.filter((el) => {
|
||||
const folderId =
|
||||
referencesFolderGroupByPath[`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`]?.[0]
|
||||
?.id;
|
||||
return folderId;
|
||||
});
|
||||
|
||||
if (validReferences.length === 0) return;
|
||||
|
||||
const referredSecrets = await secretDAL.find(
|
||||
{
|
||||
$complex: {
|
||||
operator: "or",
|
||||
value: references.map((el) => {
|
||||
const folderId =
|
||||
referencesFolderGroupByPath[
|
||||
`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`
|
||||
][0]?.id;
|
||||
if (!folderId) throw new BadRequestError({ message: `Referenced path ${el.secretPath} doesn't exist` });
|
||||
value: validReferences
|
||||
.map((el) => {
|
||||
const folderGroup =
|
||||
referencesFolderGroupByPath[
|
||||
`${referencesEnvironmentGroupBySlug[el.environment][0].id}-${el.secretPath}`
|
||||
];
|
||||
if (!folderGroup || !folderGroup[0]) return null;
|
||||
|
||||
return {
|
||||
operator: "and",
|
||||
value: [
|
||||
{
|
||||
operator: "eq",
|
||||
field: "folderId",
|
||||
value: folderId
|
||||
},
|
||||
{
|
||||
operator: "eq",
|
||||
field: `${TableName.SecretV2}.key` as "key",
|
||||
value: el.secretKey
|
||||
}
|
||||
]
|
||||
};
|
||||
})
|
||||
const folderId = folderGroup[0].id;
|
||||
|
||||
return {
|
||||
operator: "and",
|
||||
value: [
|
||||
{
|
||||
operator: "eq",
|
||||
field: "folderId",
|
||||
value: folderId
|
||||
},
|
||||
{
|
||||
operator: "eq",
|
||||
field: `${TableName.SecretV2}.key` as "key",
|
||||
value: el.secretKey
|
||||
}
|
||||
]
|
||||
};
|
||||
})
|
||||
.filter((query) => query !== null) as Array<{
|
||||
operator: "and";
|
||||
value: Array<{
|
||||
operator: "eq";
|
||||
field: "folderId" | "key";
|
||||
value: string;
|
||||
}>;
|
||||
}>
|
||||
}
|
||||
},
|
||||
{ tx }
|
||||
);
|
||||
|
||||
if (
|
||||
referredSecrets.length !==
|
||||
new Set(references.map(({ secretKey, secretPath, environment }) => `${secretKey}.${secretPath}.${environment}`))
|
||||
.size // only count unique references
|
||||
)
|
||||
throw new BadRequestError({
|
||||
message: `Referenced secret(s) not found: ${diff(
|
||||
references.map((el) => el.secretKey),
|
||||
referredSecrets.map((el) => el.key)
|
||||
).join(",")}`
|
||||
});
|
||||
|
||||
const referredSecretsGroupBySecretKey = groupBy(referredSecrets, (i) => i.key);
|
||||
references.forEach((el) => {
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
environment: el.environment,
|
||||
secretPath: el.secretPath,
|
||||
secretName: el.secretKey,
|
||||
secretTags: referredSecretsGroupBySecretKey[el.secretKey][0]?.tags?.map((i) => i.slug)
|
||||
});
|
||||
// Only check permissions for secrets that actually exist
|
||||
referredSecrets.forEach((secret) => {
|
||||
const reference = validReferences.find((ref) => ref.secretKey === secret.key);
|
||||
if (reference) {
|
||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret, {
|
||||
environment: reference.environment,
|
||||
secretPath: reference.secretPath,
|
||||
secretName: reference.secretKey,
|
||||
secretTags: secret.tags?.map((i) => i.slug)
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return referredSecrets;
|
||||
@@ -478,15 +488,16 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secret = sharedSecretToModify;
|
||||
}
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
secretTags: secret.tags.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
if (secret.type !== SecretType.Personal)
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
secretTags: secret.tags.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
|
||||
// validate tags
|
||||
// fetch all tags and if not same count throw error meaning one was invalid tags
|
||||
@@ -497,17 +508,18 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const tagsToCheck = inputSecret.tagIds ? newTags : secret.tags;
|
||||
|
||||
// now check with new ids
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
...(tagsToCheck.length && {
|
||||
secretTags: tagsToCheck.map((el) => el.slug)
|
||||
if (secret.type !== SecretType.Personal)
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: inputSecret.secretName,
|
||||
...(tagsToCheck.length && {
|
||||
secretTags: tagsToCheck.map((el) => el.slug)
|
||||
})
|
||||
})
|
||||
})
|
||||
);
|
||||
);
|
||||
|
||||
if (inputSecret.newSecretName) {
|
||||
const doesNewNameSecretExist = await secretDAL.findOne({
|
||||
@@ -546,6 +558,14 @@ export const secretV2BridgeServiceFactory = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (secretValue) {
|
||||
const { nestedReferences, localReferences } = getAllSecretReferences(secretValue);
|
||||
const allSecretReferences = nestedReferences.concat(
|
||||
localReferences.map((el) => ({ secretKey: el, secretPath, environment }))
|
||||
);
|
||||
await $validateSecretReferences(projectId, permission, allSecretReferences);
|
||||
}
|
||||
|
||||
const { encryptor: secretManagerEncryptor } = await kmsService.createCipherPairWithDataKey({
|
||||
type: KmsDataKey.SecretManager,
|
||||
projectId
|
||||
@@ -706,15 +726,17 @@ export const secretV2BridgeServiceFactory = ({
|
||||
})
|
||||
});
|
||||
if (!secretToDelete) throw new NotFoundError({ message: "Secret not found" });
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretToDelete.key,
|
||||
secretTags: secretToDelete.tags?.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
|
||||
if (secretToDelete.type !== SecretType.Personal)
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath,
|
||||
secretName: secretToDelete.key,
|
||||
secretTags: secretToDelete.tags?.map((el) => el.slug)
|
||||
})
|
||||
);
|
||||
|
||||
try {
|
||||
const deletedSecret = await secretDAL.transaction(async (tx) => {
|
||||
@@ -1658,7 +1680,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
await scanSecretPolicyViolations(projectId, secretPath, inputSecrets, project.secretDetectionIgnoreValues || []);
|
||||
|
||||
// get all tags
|
||||
const sanitizedTagIds = inputSecrets.flatMap(({ tagIds = [] }) => tagIds);
|
||||
const sanitizedTagIds = [...new Set(inputSecrets.flatMap(({ tagIds = [] }) => tagIds))];
|
||||
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds) : [];
|
||||
if (tags.length !== sanitizedTagIds.length)
|
||||
throw new NotFoundError({ message: `Tag not found. Found ${tags.map((el) => el.slug).join(",")}` });
|
||||
@@ -1906,7 +1928,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
});
|
||||
|
||||
// get all tags
|
||||
const sanitizedTagIds = secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds);
|
||||
const sanitizedTagIds = [...new Set(secretsToUpdate.flatMap(({ tagIds = [] }) => tagIds))];
|
||||
const tags = sanitizedTagIds.length ? await secretTagDAL.findManyTagsById(projectId, sanitizedTagIds, tx) : [];
|
||||
if (tags.length !== sanitizedTagIds.length) throw new NotFoundError({ message: "Tag not found" });
|
||||
const tagsGroupByID = groupBy(tags, (i) => i.id);
|
||||
@@ -2333,7 +2355,8 @@ export const secretV2BridgeServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
secretId
|
||||
secretId,
|
||||
secretVersions: secretVersionsFilter
|
||||
}: TGetSecretVersionsDTO) => {
|
||||
const secret = await secretDAL.findById(secretId);
|
||||
|
||||
@@ -2370,6 +2393,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
const secretVersions = await secretVersionDAL.findVersionsBySecretIdWithActors({
|
||||
secretId,
|
||||
projectId: folder.projectId,
|
||||
secretVersions: secretVersionsFilter,
|
||||
findOpt: {
|
||||
offset,
|
||||
limit,
|
||||
@@ -2939,7 +2963,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
secretKey: secretName
|
||||
});
|
||||
|
||||
return { tree: stackTrace, value: expandedValue };
|
||||
return { tree: stackTrace, value: expandedValue, secret };
|
||||
};
|
||||
|
||||
const getAccessibleSecrets = async ({
|
||||
@@ -3155,6 +3179,7 @@ export const secretV2BridgeServiceFactory = ({
|
||||
getSecretById,
|
||||
getAccessibleSecrets,
|
||||
getSecretVersionsByIds,
|
||||
findSecretIdsByFolderIdAndKeys
|
||||
findSecretIdsByFolderIdAndKeys,
|
||||
$validateSecretReferences
|
||||
};
|
||||
};
|
||||
|
||||
@@ -159,6 +159,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
secretId: string;
|
||||
secretVersions?: string[];
|
||||
};
|
||||
|
||||
export type TSecretReference = { environment: string; secretPath: string; secretKey: string };
|
||||
|
||||
@@ -2568,7 +2568,8 @@ export const secretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
limit = 20,
|
||||
offset = 0,
|
||||
secretId
|
||||
secretId,
|
||||
secretVersions: filterSecretVersions
|
||||
}: TGetSecretVersionsDTO) => {
|
||||
const secretVersionV2 = await secretV2BridgeService
|
||||
.getSecretVersions({
|
||||
@@ -2578,7 +2579,8 @@ export const secretServiceFactory = ({
|
||||
actorAuthMethod,
|
||||
limit,
|
||||
offset,
|
||||
secretId
|
||||
secretId,
|
||||
secretVersions: filterSecretVersions
|
||||
})
|
||||
.catch((err) => {
|
||||
if ((err as Error).message === "BadRequest: Failed to find secret") {
|
||||
|
||||
@@ -331,6 +331,7 @@ export type TGetSecretVersionsDTO = Omit<TProjectPermission, "projectId"> & {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
secretId: string;
|
||||
secretVersions?: string[];
|
||||
};
|
||||
|
||||
export type TSecretReference = { environment: string; secretPath: string };
|
||||
|
||||
@@ -32,7 +32,8 @@ export enum PostHogEventTypes {
|
||||
IssueSshHostHostCert = "Issue SSH Host Host Certificate",
|
||||
SignCert = "Sign PKI Certificate",
|
||||
IssueCert = "Issue PKI Certificate",
|
||||
InvalidateCache = "Invalidate Cache"
|
||||
InvalidateCache = "Invalidate Cache",
|
||||
NotificationUpdated = "Notification Updated"
|
||||
}
|
||||
|
||||
export type TSecretModifiedEvent = {
|
||||
@@ -232,6 +233,14 @@ export type TInvalidateCacheEvent = {
|
||||
};
|
||||
};
|
||||
|
||||
export type TNotificationUpdatedEvent = {
|
||||
event: PostHogEventTypes.NotificationUpdated;
|
||||
properties: {
|
||||
notificationId: string;
|
||||
isRead?: boolean;
|
||||
};
|
||||
};
|
||||
|
||||
export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TSecretModifiedEvent
|
||||
| TAdminInitEvent
|
||||
@@ -251,4 +260,5 @@ export type TPostHogEvent = { distinctId: string; organizationId?: string } & (
|
||||
| TSignCertificateEvent
|
||||
| TIssueCertificateEvent
|
||||
| TInvalidateCacheEvent
|
||||
| TNotificationUpdatedEvent
|
||||
);
|
||||
|
||||
@@ -294,11 +294,18 @@ export const userServiceFactory = ({
|
||||
// Delete all user aliases since the email is changing
|
||||
await userAliasDAL.delete({ userId }, tx);
|
||||
|
||||
// Ensure EMAIL auth method is included if not already present
|
||||
const currentAuthMethods = user.authMethods || [];
|
||||
const updatedAuthMethods = currentAuthMethods.includes(AuthMethod.EMAIL)
|
||||
? currentAuthMethods
|
||||
: [...currentAuthMethods, AuthMethod.EMAIL];
|
||||
|
||||
const updatedUser = await userDAL.updateById(
|
||||
userId,
|
||||
{
|
||||
email: newEmail.toLowerCase(),
|
||||
username: newEmail.toLowerCase()
|
||||
username: newEmail.toLowerCase(),
|
||||
authMethods: updatedAuthMethods
|
||||
},
|
||||
tx
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user