Merge branch 'heads/main' into daniel/kms-sign-verify

This commit is contained in:
Daniel Hougaard
2025-04-10 01:12:13 +04:00
423 changed files with 15652 additions and 1916 deletions
+7
View File
@@ -39,3 +39,10 @@ export const GenericResourceNameSchema = z
])(val),
"Name can only contain alphanumeric characters, dashes, underscores, and spaces"
);
export const BaseSecretNameSchema = z.string().trim().min(1);
export const SecretNameSchema = BaseSecretNameSchema.refine(
(el) => !el.includes(" "),
"Secret name cannot contain spaces."
).refine((el) => !el.includes(":"), "Secret name cannot contain colon.");
+43 -2
View File
@@ -76,6 +76,9 @@ import { secretReplicationServiceFactory } from "@app/ee/services/secret-replica
import { secretRotationDALFactory } from "@app/ee/services/secret-rotation/secret-rotation-dal";
import { secretRotationQueueFactory } from "@app/ee/services/secret-rotation/secret-rotation-queue";
import { secretRotationServiceFactory } from "@app/ee/services/secret-rotation/secret-rotation-service";
import { secretRotationV2DALFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-dal";
import { secretRotationV2QueueServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-queue";
import { secretRotationV2ServiceFactory } from "@app/ee/services/secret-rotation-v2/secret-rotation-v2-service";
import { gitAppDALFactory } from "@app/ee/services/secret-scanning/git-app-dal";
import { gitAppInstallSessionDALFactory } from "@app/ee/services/secret-scanning/git-app-install-session-dal";
import { secretScanningDALFactory } from "@app/ee/services/secret-scanning/secret-scanning-dal";
@@ -406,6 +409,8 @@ export const registerRoutes = async (
const gatewayDAL = gatewayDALFactory(db);
const projectGatewayDAL = projectGatewayDALFactory(db);
const secretRotationV2DAL = secretRotationV2DALFactory(db, folderDAL);
const permissionService = permissionServiceFactory({
permissionDAL,
orgRoleDAL,
@@ -662,6 +667,7 @@ export const registerRoutes = async (
});
const orgAdminService = orgAdminServiceFactory({
smtpService,
projectDAL,
permissionService,
projectUserMembershipRoleDAL,
@@ -964,7 +970,8 @@ export const registerRoutes = async (
projectSlackConfigDAL,
slackIntegrationDAL,
projectTemplateService,
groupProjectDAL
groupProjectDAL,
smtpService
});
const projectEnvService = projectEnvServiceFactory({
@@ -1497,6 +1504,35 @@ export const registerRoutes = async (
permissionService
});
const secretRotationV2Service = secretRotationV2ServiceFactory({
secretRotationV2DAL,
permissionService,
appConnectionService,
folderDAL,
projectBotService,
licenseService,
kmsService,
auditLogService,
secretV2BridgeDAL,
secretTagDAL,
secretVersionTagV2BridgeDAL,
secretVersionV2BridgeDAL,
keyStore,
resourceMetadataDAL,
snapshotService,
secretQueueService,
queueService
});
await secretRotationV2QueueServiceFactory({
secretRotationV2Service,
secretRotationV2DAL,
queueService,
projectDAL,
projectMembershipDAL,
smtpService
});
await superAdminService.initServerCfg();
// setup the communication with license key server
@@ -1598,7 +1634,8 @@ export const registerRoutes = async (
secretSync: secretSyncService,
kmip: kmipService,
kmipOperation: kmipOperationService,
gateway: gatewayService
gateway: gatewayService,
secretRotationV2: secretRotationV2Service
});
const cronJobs: CronJob[] = [];
@@ -1607,6 +1644,10 @@ export const registerRoutes = async (
if (rateLimitSyncJob) {
cronJobs.push(rateLimitSyncJob);
}
const licenseSyncJob = await licenseService.initializeBackgroundSync();
if (licenseSyncJob) {
cronJobs.push(licenseSyncJob);
}
}
server.decorate<FastifyZodProvider["store"]>("store", {
@@ -134,7 +134,9 @@ export const secretRawSchema = z.object({
membershipId: z.string().nullable().optional()
})
.optional()
.nullable()
.nullable(),
isRotatedSecret: z.boolean().optional(),
rotationId: z.string().uuid().nullish()
});
export const ProjectPermissionSchema = z.object({
@@ -24,8 +24,14 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
method: I["method"];
credentials: I["credentials"];
description?: string | null;
isPlatformManagedCredentials?: boolean;
}>;
updateSchema: z.ZodType<{
name?: string;
credentials?: I["credentials"];
description?: string | null;
isPlatformManagedCredentials?: boolean;
}>;
updateSchema: z.ZodType<{ name?: string; credentials?: I["credentials"]; description?: string | null }>;
sanitizedResponseSchema: z.ZodTypeAny;
}) => {
const appName = APP_CONNECTION_NAME_MAP[app];
@@ -208,10 +214,10 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, method, credentials, description } = req.body;
const { name, method, credentials, description, isPlatformManagedCredentials } = req.body;
const appConnection = (await server.services.appConnection.createAppConnection(
{ name, method, app, credentials, description },
{ name, method, app, credentials, description, isPlatformManagedCredentials },
req.permission
)) as T;
@@ -224,7 +230,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
name,
method,
app,
connectionId: appConnection.id
connectionId: appConnection.id,
isPlatformManagedCredentials
}
}
});
@@ -251,11 +258,11 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { name, credentials, description } = req.body;
const { name, credentials, description, isPlatformManagedCredentials } = req.body;
const { connectionId } = req.params;
const appConnection = (await server.services.appConnection.updateAppConnection(
{ name, credentials, connectionId, description },
{ name, credentials, connectionId, description, isPlatformManagedCredentials },
req.permission
)) as T;
@@ -268,7 +275,8 @@ export const registerAppConnectionEndpoints = <T extends TAppConnection, I exten
name,
description,
credentialsUpdated: Boolean(credentials),
connectionId
connectionId,
isPlatformManagedCredentials
}
}
});
@@ -22,6 +22,11 @@ import {
HumanitecConnectionListItemSchema,
SanitizedHumanitecConnectionSchema
} from "@app/services/app-connection/humanitec";
import { MsSqlConnectionListItemSchema, SanitizedMsSqlConnectionSchema } from "@app/services/app-connection/mssql";
import {
PostgresConnectionListItemSchema,
SanitizedPostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import { AuthMode } from "@app/services/auth/auth-type";
// can't use discriminated due to multiple schemas for certain apps
@@ -32,7 +37,9 @@ const SanitizedAppConnectionSchema = z.union([
...SanitizedAzureKeyVaultConnectionSchema.options,
...SanitizedAzureAppConfigurationConnectionSchema.options,
...SanitizedDatabricksConnectionSchema.options,
...SanitizedHumanitecConnectionSchema.options
...SanitizedHumanitecConnectionSchema.options,
...SanitizedPostgresConnectionSchema.options,
...SanitizedMsSqlConnectionSchema.options
]);
const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
@@ -42,7 +49,9 @@ const AppConnectionOptionsSchema = z.discriminatedUnion("app", [
AzureKeyVaultConnectionListItemSchema,
AzureAppConfigurationConnectionListItemSchema,
DatabricksConnectionListItemSchema,
HumanitecConnectionListItemSchema
HumanitecConnectionListItemSchema,
PostgresConnectionListItemSchema,
MsSqlConnectionListItemSchema
]);
export const registerAppConnectionRouter = async (server: FastifyZodProvider) => {
@@ -7,6 +7,8 @@ import { registerDatabricksConnectionRouter } from "./databricks-connection-rout
import { registerGcpConnectionRouter } from "./gcp-connection-router";
import { registerGitHubConnectionRouter } from "./github-connection-router";
import { registerHumanitecConnectionRouter } from "./humanitec-connection-router";
import { registerMsSqlConnectionRouter } from "./mssql-connection-router";
import { registerPostgresConnectionRouter } from "./postgres-connection-router";
export * from "./app-connection-router";
@@ -18,5 +20,7 @@ export const APP_CONNECTION_REGISTER_ROUTER_MAP: Record<AppConnection, (server:
[AppConnection.AzureKeyVault]: registerAzureKeyVaultConnectionRouter,
[AppConnection.AzureAppConfiguration]: registerAzureAppConfigurationConnectionRouter,
[AppConnection.Databricks]: registerDatabricksConnectionRouter,
[AppConnection.Humanitec]: registerHumanitecConnectionRouter
[AppConnection.Humanitec]: registerHumanitecConnectionRouter,
[AppConnection.Postgres]: registerPostgresConnectionRouter,
[AppConnection.MsSql]: registerMsSqlConnectionRouter
};
@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreateMsSqlConnectionSchema,
SanitizedMsSqlConnectionSchema,
UpdateMsSqlConnectionSchema
} from "@app/services/app-connection/mssql";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerMsSqlConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.MsSql,
server,
sanitizedResponseSchema: SanitizedMsSqlConnectionSchema,
createSchema: CreateMsSqlConnectionSchema,
updateSchema: UpdateMsSqlConnectionSchema
});
};
@@ -0,0 +1,18 @@
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
import {
CreatePostgresConnectionSchema,
SanitizedPostgresConnectionSchema,
UpdatePostgresConnectionSchema
} from "@app/services/app-connection/postgres";
import { registerAppConnectionEndpoints } from "./app-connection-endpoints";
export const registerPostgresConnectionRouter = async (server: FastifyZodProvider) => {
registerAppConnectionEndpoints({
app: AppConnection.Postgres,
server,
sanitizedResponseSchema: SanitizedPostgresConnectionSchema,
createSchema: CreatePostgresConnectionSchema,
updateSchema: UpdatePostgresConnectionSchema
});
};
+271 -62
View File
@@ -8,6 +8,7 @@ import {
ProjectPermissionSecretActions,
ProjectPermissionSub
} 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 { removeTrailingSlash } from "@app/lib/fn";
@@ -101,12 +102,30 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeFolders),
includeImports: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeImports),
includeSecretRotations: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeSecretRotations),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_OVERVIEW_LIST.includeDynamicSecrets)
}),
response: {
200: z.object({
folders: SecretFoldersSchema.extend({ environment: z.string() }).array().optional(),
dynamicSecrets: SanitizedDynamicSecretSchema.extend({ environment: z.string() }).array().optional(),
secretRotations: z
.intersection(
SecretRotationV2Schema,
z.object({
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SanitizedTagSchema.array().optional()
})
.nullable()
.array()
})
)
.array()
.optional(),
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
@@ -127,6 +146,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
totalImportCount: z.number().optional(),
totalSecretRotationCount: z.number().optional(),
totalCount: z.number()
})
}
@@ -144,7 +164,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeFolders,
includeSecrets,
includeImports,
includeDynamicSecrets
includeDynamicSecrets,
includeSecretRotations
} = req.query;
const environments = req.query.environments.split(",");
@@ -166,11 +187,15 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
let dynamicSecrets:
| Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnvs>>
| undefined;
let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
| undefined;
let totalFolderCount: number | undefined;
let totalDynamicSecretCount: number | undefined;
let totalSecretCount: number | undefined;
let totalImportCount: number | undefined;
let totalSecretRotationCount: number | undefined;
if (includeImports) {
totalImportCount = await server.services.secretImport.getProjectImportMultiEnvCount({
@@ -322,6 +347,56 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
if (includeSecretRotations) {
totalSecretRotationCount = await server.services.secretRotationV2.getDashboardSecretRotationCount(
{
projectId,
search,
environments,
secretPath
},
req.permission
);
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
{
projectId,
search,
orderBy,
orderDirection,
environments,
secretPath,
limit: remainingLimit,
offset: adjustedOffset
},
req.permission
);
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRET_ROTATIONS,
metadata: {
count: secretRotations.length,
rotationIds: secretRotations.map((rotation) => rotation.id),
secretPath,
environment: environments.join(",")
}
}
});
// get the count of unique secret rotation names to properly adjust remaining limit
const uniqueSecretRotationCount = new Set(secretRotations.map((rotation) => rotation.name)).size;
remainingLimit -= uniqueSecretRotationCount;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalSecretRotationCount);
}
}
if (includeSecrets) {
// this is the unique count, ie duplicate secrets across envs only count as 1
totalSecretCount = await server.services.secret.getSecretsCountMultiEnv({
@@ -353,38 +428,44 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
offset: adjustedOffset,
isInternal: true
});
}
}
for await (const environment of environments) {
const secretCountFromEnv = secrets.filter((secret) => secret.environment === environment).length;
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);
if (secretCountFromEnv) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountFromEnv
}
if (secretCountFromEnv) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCountFromEnv
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secretCountFromEnv,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secretCountFromEnv,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
}
}
@@ -395,12 +476,18 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
dynamicSecrets,
secrets,
imports,
secretRotations,
totalFolderCount,
totalDynamicSecretCount,
totalImportCount,
totalSecretCount,
totalSecretRotationCount,
totalCount:
(totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0) + (totalImportCount ?? 0)
(totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) +
(totalSecretCount ?? 0) +
(totalImportCount ?? 0) +
(totalSecretRotationCount ?? 0)
};
}
});
@@ -445,7 +532,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecrets),
includeFolders: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeFolders),
includeDynamicSecrets: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeDynamicSecrets),
includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports)
includeImports: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeImports),
includeSecretRotations: booleanSchema.describe(DASHBOARD.SECRET_DETAILS_LIST.includeSecretRotations)
}),
response: {
200: z.object({
@@ -457,6 +545,23 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
.optional(),
folders: SecretFoldersSchema.array().optional(),
dynamicSecrets: SanitizedDynamicSecretSchema.array().optional(),
secretRotations: z
.intersection(
SecretRotationV2Schema,
z.object({
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
secretPath: z.string().optional(),
secretMetadata: ResourceMetadataSchema.optional(),
tags: SanitizedTagSchema.array().optional()
})
.nullable()
.array()
})
)
.array()
.optional(),
secrets: secretRawSchema
.extend({
secretValueHidden: z.boolean(),
@@ -470,6 +575,7 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
totalFolderCount: z.number().optional(),
totalDynamicSecretCount: z.number().optional(),
totalSecretCount: z.number().optional(),
totalSecretRotationCount: z.number().optional(),
totalCount: z.number()
})
}
@@ -488,7 +594,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
includeFolders,
includeSecrets,
includeDynamicSecrets,
includeImports
includeImports,
includeSecretRotations
} = req.query;
if (!projectId || !environment) throw new BadRequestError({ message: "Missing workspace id or environment" });
@@ -507,11 +614,15 @@ 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"] | undefined;
let dynamicSecrets: Awaited<ReturnType<typeof server.services.dynamicSecret.listDynamicSecretsByEnv>> | undefined;
let secretRotations:
| Awaited<ReturnType<typeof server.services.secretRotationV2.getDashboardSecretRotations>>
| undefined;
let totalImportCount: number | undefined;
let totalFolderCount: number | undefined;
let totalDynamicSecretCount: number | undefined;
let totalSecretCount: number | undefined;
let totalSecretRotationCount: number | undefined;
if (includeImports) {
totalImportCount = await server.services.secretImport.getProjectImportCount({
@@ -594,6 +705,53 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
}
}
if (includeSecretRotations) {
totalSecretRotationCount = await server.services.secretRotationV2.getDashboardSecretRotationCount(
{
projectId,
search,
environments: [environment],
secretPath
},
req.permission
);
if (remainingLimit > 0 && totalSecretRotationCount > adjustedOffset) {
secretRotations = await server.services.secretRotationV2.getDashboardSecretRotations(
{
projectId,
search,
orderBy,
orderDirection,
environments: [environment],
secretPath,
limit: remainingLimit,
offset: adjustedOffset
},
req.permission
);
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRET_ROTATIONS,
metadata: {
count: secretRotations.length,
rotationIds: secretRotations.map((rotation) => rotation.id),
secretPath,
environment
}
}
});
remainingLimit -= secretRotations.length;
adjustedOffset = 0;
} else {
adjustedOffset = Math.max(0, adjustedOffset - totalSecretRotationCount);
}
}
try {
if (includeDynamicSecrets) {
totalDynamicSecretCount = await server.services.dynamicSecret.getDynamicSecretCount({
@@ -629,7 +787,13 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
adjustedOffset = Math.max(0, adjustedOffset - totalDynamicSecretCount);
}
}
} catch (error) {
if (!(error instanceof ForbiddenError)) {
throw error;
}
}
try {
if (includeSecrets) {
totalSecretCount = await server.services.secret.getSecretsCount({
actorId: req.permission.id,
@@ -663,34 +827,6 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
tagSlugs: tags
})
).secrets;
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secrets.length
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secrets.length,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
}
} catch (error) {
@@ -699,17 +835,57 @@ 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);
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRETS,
metadata: {
environment,
secretPath,
numberOfSecrets: secretCount
}
}
});
if (getUserAgentType(req.headers["user-agent"]) !== UserAgentType.K8_OPERATOR) {
await server.services.telemetry.sendPostHogEvents({
event: PostHogEventTypes.SecretPulled,
distinctId: getTelemetryDistinctId(req),
properties: {
numberOfSecrets: secretCount,
workspaceId: projectId,
environment,
secretPath,
channel: getUserAgentType(req.headers["user-agent"]),
...req.auditLogInfo
}
});
}
}
return {
imports,
folders,
dynamicSecrets,
secrets,
secretRotations,
totalImportCount,
totalFolderCount,
totalDynamicSecretCount,
totalSecretCount,
totalSecretRotationCount,
totalCount:
(totalImportCount ?? 0) + (totalFolderCount ?? 0) + (totalDynamicSecretCount ?? 0) + (totalSecretCount ?? 0)
(totalImportCount ?? 0) +
(totalFolderCount ?? 0) +
(totalDynamicSecretCount ?? 0) +
(totalSecretCount ?? 0) +
(totalSecretRotationCount ?? 0)
};
}
});
@@ -747,7 +923,8 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
tags: SanitizedTagSchema.array().optional()
})
.array()
.optional()
.optional(),
secretRotations: SecretRotationV2Schema.array().optional()
})
}
},
@@ -811,6 +988,17 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
req.permission
);
const secretRotations = searchHasTags
? []
: await server.services.secretRotationV2.getQuickSearchSecretRotations(
{
projectId,
folderMappings,
filters: sharedFilters
},
req.permission
);
for await (const environment of environments) {
const secretCountForEnv = secrets.filter((secret) => secret.environment === environment).length;
@@ -843,6 +1031,24 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
});
}
}
const secretRotationsFromEnv = secretRotations.filter((rotation) => rotation.environment.slug === environment);
if (secretRotationsFromEnv.length) {
await server.services.auditLog.createAuditLog({
projectId,
...req.auditLogInfo,
event: {
type: EventType.GET_SECRET_ROTATIONS,
metadata: {
count: secretRotationsFromEnv.length,
rotationIds: secretRotationsFromEnv.map((rotation) => rotation.id),
secretPath,
environment
}
}
});
}
}
const sliceQuickSearch = <T>(array: T[]) => array.slice(0, 25);
@@ -856,6 +1062,9 @@ export const registerDashboardRouter = async (server: FastifyZodProvider) => {
? dynamicSecrets.filter((dynamicSecret) => dynamicSecret.path.endsWith(searchPath))
: dynamicSecrets
),
secretRotations: sliceQuickSearch(
searchPath ? secretRotations.filter((rotation) => rotation.folder.path.endsWith(searchPath)) : secretRotations
),
folders: searchHasTags
? []
: sliceQuickSearch(
+109 -1
View File
@@ -3,15 +3,26 @@ import { z } from "zod";
import { IdentitiesSchema, IdentityOrgMembershipsSchema, OrgMembershipRole, OrgRolesSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { IDENTITIES } from "@app/lib/api-docs";
import { buildSearchZodSchema, SearchResourceOperators } from "@app/lib/search-resource/search";
import { OrderByDirection } from "@app/lib/types";
import { CharacterType, zodValidateCharacters } from "@app/lib/validator/validate-string";
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 { OrgIdentityOrderBy } from "@app/services/identity/identity-types";
import { isSuperAdmin } from "@app/services/super-admin/super-admin-fns";
import { PostHogEventTypes } from "@app/services/telemetry/telemetry-types";
import { SanitizedProjectSchema } from "../sanitizedSchemas";
const searchResourceZodValidate = zodValidateCharacters([
CharacterType.AlphaNumeric,
CharacterType.Spaces,
CharacterType.Underscore,
CharacterType.Hyphen
]);
export const registerIdentityRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@@ -245,7 +256,7 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
method: "GET",
url: "/",
config: {
rateLimit: writeLimit
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
@@ -289,6 +300,103 @@ export const registerIdentityRouter = async (server: FastifyZodProvider) => {
}
});
server.route({
method: "POST",
url: "/search",
config: {
rateLimit: readLimit
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
schema: {
description: "Search identities",
security: [
{
bearerAuth: []
}
],
body: z.object({
orderBy: z
.nativeEnum(OrgIdentityOrderBy)
.default(OrgIdentityOrderBy.Name)
.describe(IDENTITIES.SEARCH.orderBy)
.optional(),
orderDirection: z
.nativeEnum(OrderByDirection)
.default(OrderByDirection.ASC)
.describe(IDENTITIES.SEARCH.orderDirection)
.optional(),
limit: z.number().max(100).default(50).describe(IDENTITIES.SEARCH.limit),
offset: z.number().default(0).describe(IDENTITIES.SEARCH.offset),
search: buildSearchZodSchema(
z
.object({
name: z
.union([
searchResourceZodValidate(z.string().max(255), "Name"),
z
.object({
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Name $eq"),
[SearchResourceOperators.$contains]: searchResourceZodValidate(
z.string().max(255),
"Name $contains"
),
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Name $in").array()
})
.partial()
])
.describe(IDENTITIES.SEARCH.search.name),
role: z
.union([
searchResourceZodValidate(z.string().max(255), "Role"),
z
.object({
[SearchResourceOperators.$eq]: searchResourceZodValidate(z.string().max(255), "Role $eq"),
[SearchResourceOperators.$in]: searchResourceZodValidate(z.string().max(255), "Role $in").array()
})
.partial()
])
.describe(IDENTITIES.SEARCH.search.role)
})
.describe(IDENTITIES.SEARCH.search.desc)
.partial()
)
}),
response: {
200: z.object({
identities: IdentityOrgMembershipsSchema.extend({
customRole: OrgRolesSchema.pick({
id: true,
name: true,
slug: true,
permissions: true,
description: true
}).optional(),
identity: IdentitiesSchema.pick({ name: true, id: true }).extend({
authMethods: z.array(z.string())
})
}).array(),
totalCount: z.number()
})
}
},
handler: async (req) => {
const { identityMemberships, totalCount } = await server.services.identity.searchOrgIdentities({
actor: req.permission.type,
actorId: req.permission.id,
actorAuthMethod: req.permission.authMethod,
actorOrgId: req.permission.orgId,
searchFilter: req.body.search,
orgId: req.permission.orgId,
limit: req.body.limit,
offset: req.body.offset,
orderBy: req.body.orderBy,
orderDirection: req.body.orderDirection
});
return { identities: identityMemberships, totalCount };
}
});
server.route({
method: "GET",
url: "/:identityId/identity-memberships",
+107 -2
View File
@@ -8,15 +8,17 @@ import {
ProjectSlackConfigsSchema,
ProjectType,
SecretFoldersSchema,
SortDirection,
UserEncryptionKeysSchema,
UsersSchema
} from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { PROJECTS } from "@app/lib/api-docs";
import { CharacterType, characterValidator } from "@app/lib/validator/validate-string";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType } from "@app/services/project/project-types";
import { ActorType, AuthMode } from "@app/services/auth/auth-type";
import { ProjectFilterType, SearchProjectSortBy } from "@app/services/project/project-types";
import { validateSlackChannelsField } from "@app/services/slack/slack-auth-validators";
import { integrationAuthPubSchema, SanitizedProjectSchema } from "../sanitizedSchemas";
@@ -704,4 +706,107 @@ export const registerProjectRouter = async (server: FastifyZodProvider) => {
return environmentsFolders;
}
});
server.route({
method: "POST",
url: "/search",
config: {
rateLimit: readLimit
},
schema: {
body: z.object({
limit: z.number().default(100),
offset: z.number().default(0),
type: z.nativeEnum(ProjectType).optional(),
orderBy: z.nativeEnum(SearchProjectSortBy).optional().default(SearchProjectSortBy.NAME),
orderDirection: z.nativeEnum(SortDirection).optional().default(SortDirection.ASC),
name: z
.string()
.trim()
.refine((val) => characterValidator([CharacterType.AlphaNumeric, CharacterType.Hyphen])(val), {
message: "Invalid pattern: only alphanumeric characters, - are allowed."
})
.optional()
}),
response: {
200: z.object({
projects: SanitizedProjectSchema.extend({ isMember: z.boolean() }).array(),
totalCount: z.number()
})
}
},
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
handler: async (req) => {
const { docs: projects, totalCount } = await server.services.project.searchProjects({
permission: req.permission,
...req.body
});
return { projects, totalCount };
}
});
server.route({
method: "POST",
url: "/:workspaceId/project-access",
config: {
rateLimit: writeLimit
},
schema: {
params: z.object({
workspaceId: z.string().trim()
}),
body: z.object({
comment: z
.string()
.trim()
.max(2500)
.refine(
(val) =>
characterValidator([
CharacterType.AlphaNumeric,
CharacterType.Hyphen,
CharacterType.Comma,
CharacterType.Fullstop,
CharacterType.Spaces,
CharacterType.Exclamation
])(val),
{
message: "Invalid pattern: only alphanumeric characters, spaces, -.!, are allowed."
}
)
.optional()
}),
response: {
200: z.object({
message: z.string()
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
await server.services.project.requestProjectAccess({
permission: req.permission,
comment: req.body.comment,
projectId: req.params.workspaceId
});
if (req.auth.actor === ActorType.USER) {
await server.services.auditLog.createAuditLog({
...req.auditLogInfo,
projectId: req.params.workspaceId,
event: {
type: EventType.PROJECT_ACCESS_REQUEST,
metadata: {
projectId: req.params.workspaceId,
requesterEmail: req.auth.user.email || req.auth.user.username,
requesterId: req.auth.userId
}
}
});
}
return { message: "Project access request has been send to project admins" };
}
});
};
@@ -7,6 +7,7 @@ import { RAW_SECRETS, SECRETS } from "@app/lib/api-docs";
import { BadRequestError, NotFoundError } from "@app/lib/errors";
import { removeTrailingSlash } from "@app/lib/fn";
import { secretsLimit, writeLimit } from "@app/server/config/rateLimiter";
import { BaseSecretNameSchema, SecretNameSchema } from "@app/server/lib/schemas";
import { getTelemetryDistinctId } from "@app/server/lib/telemetry";
import { getUserAgentType } from "@app/server/plugins/audit-log";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
@@ -39,13 +40,6 @@ const SecretReferenceNodeTree: z.ZodType<TSecretReferenceNode> = SecretReference
children: z.lazy(() => SecretReferenceNodeTree.array())
});
const BaseSecretNameSchema = z.string().trim().min(1);
const SecretNameSchema = BaseSecretNameSchema.refine(
(el) => !el.includes(" "),
"Secret name cannot contain spaces."
).refine((el) => !el.includes(":"), "Secret name cannot contain colon.");
export const registerSecretRouter = async (server: FastifyZodProvider) => {
server.route({
method: "POST",
@@ -630,6 +624,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.optional()
.describe(RAW_SECRETS.UPDATE.secretValue),
secretPath: z
.string()
@@ -2049,6 +2044,7 @@ export const registerSecretRouter = async (server: FastifyZodProvider) => {
secretValue: z
.string()
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim()))
.optional()
.describe(RAW_SECRETS.UPDATE.secretValue),
secretPath: z
.string()