Merge branch 'main' into feature/slack-secret-sync-error-notification

This commit is contained in:
Victor Santos
2025-11-03 15:55:10 -03:00
679 changed files with 19636 additions and 8864 deletions
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
@@ -127,8 +126,8 @@ export const registerDynamicSecretLeaseRouter = async (server: FastifyZodProvide
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
projectSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.RENEW.projectSlug),
path: z
@@ -2,7 +2,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { ApiDocsTags, DYNAMIC_SECRET_LEASES } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { writeLimit } from "@app/server/config/rateLimiter";
@@ -32,8 +31,8 @@ export const registerKubernetesDynamicSecretLeaseRouter = async (server: Fastify
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
path: z.string().trim().default("/").transform(removeTrailingSlash).describe(DYNAMIC_SECRET_LEASES.CREATE.path),
environmentSlug: z.string().min(1).describe(DYNAMIC_SECRET_LEASES.CREATE.environmentSlug),
@@ -3,7 +3,6 @@ import { z } from "zod";
import { DynamicSecretLeasesSchema } from "@app/db/schemas";
import { DynamicSecretProviderSchema } from "@app/ee/services/dynamic-secret/providers/models";
import { ApiDocsTags, DYNAMIC_SECRETS } from "@app/lib/api-docs";
import { daysToMillisecond } from "@app/lib/dates";
import { removeTrailingSlash } from "@app/lib/fn";
import { ms } from "@app/lib/ms";
import { isValidHandleBarTemplate } from "@app/lib/template/validate-handlebars";
@@ -60,8 +59,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
maxTTL: z
.string()
@@ -72,8 +71,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
})
.nullable(),
path: z.string().describe(DYNAMIC_SECRETS.CREATE.path).trim().default("/").transform(removeTrailingSlash),
@@ -130,8 +129,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
}),
maxTTL: z
.string()
@@ -142,8 +141,8 @@ export const registerDynamicSecretRouter = async (server: FastifyZodProvider) =>
const valMs = ms(val);
if (valMs < 60 * 1000)
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be a greater than 1min" });
if (valMs > daysToMillisecond(1))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than a day" });
if (valMs > ms("10y"))
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "TTL must be less than 10 years" });
})
.nullable(),
newName: z.string().describe(DYNAMIC_SECRETS.UPDATE.newName).optional(),
+4 -2
View File
@@ -182,7 +182,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
algorithm: z.string(),
isActive: z.boolean(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
kmipMetadata: z.record(z.any()).nullish()
})
}
},
@@ -384,7 +385,8 @@ export const registerKmipSpecRouter = async (server: FastifyZodProvider) => {
isActive: z.boolean(),
algorithm: z.string(),
createdAt: z.date(),
updatedAt: z.date()
updatedAt: z.date(),
kmipMetadata: z.record(z.any()).nullish()
})
.array()
})
@@ -1,3 +1,8 @@
import {
CreateMySQLAccountSchema,
SanitizedMySQLAccountWithResourceSchema,
UpdateMySQLAccountSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import {
CreatePostgresAccountSchema,
@@ -16,5 +21,14 @@ export const PAM_ACCOUNT_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fasti
createAccountSchema: CreatePostgresAccountSchema,
updateAccountSchema: UpdatePostgresAccountSchema
});
},
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.MySQL,
accountResponseSchema: SanitizedMySQLAccountWithResourceSchema,
createAccountSchema: CreateMySQLAccountSchema,
updateAccountSchema: UpdateMySQLAccountSchema
});
}
};
@@ -2,6 +2,7 @@ import { z } from "zod";
import { PamFoldersSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { SanitizedMySQLAccountWithResourceSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import { SanitizedPostgresAccountWithResourceSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { BadRequestError } from "@app/lib/errors";
@@ -10,8 +11,10 @@ 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";
// Use z.union([...]) when more resources are added
const SanitizedAccountSchema = SanitizedPostgresAccountWithResourceSchema;
const SanitizedAccountSchema = z.union([
SanitizedPostgresAccountWithResourceSchema,
SanitizedMySQLAccountWithResourceSchema
]);
export const registerPamAccountRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -1,3 +1,8 @@
import {
CreateMySQLResourceSchema,
MySQLResourceSchema,
UpdateMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PamResource } from "@app/ee/services/pam-resource/pam-resource-enums";
import {
CreatePostgresResourceSchema,
@@ -16,5 +21,14 @@ export const PAM_RESOURCE_REGISTER_ROUTER_MAP: Record<PamResource, (server: Fast
createResourceSchema: CreatePostgresResourceSchema,
updateResourceSchema: UpdatePostgresResourceSchema
});
},
[PamResource.MySQL]: async (server: FastifyZodProvider) => {
registerPamResourceEndpoints({
server,
resourceType: PamResource.MySQL,
resourceResponseSchema: MySQLResourceSchema,
createResourceSchema: CreateMySQLResourceSchema,
updateResourceSchema: UpdateMySQLResourceSchema
});
}
};
@@ -1,6 +1,10 @@
import { z } from "zod";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import {
MySQLResourceListItemSchema,
SanitizedMySQLResourceSchema
} from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import {
PostgresResourceListItemSchema,
SanitizedPostgresResourceSchema
@@ -9,10 +13,12 @@ import { readLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { AuthMode } from "@app/services/auth/auth-type";
// Use z.union([...]) when more resources are added
const SanitizedResourceSchema = SanitizedPostgresResourceSchema;
const SanitizedResourceSchema = z.union([SanitizedPostgresResourceSchema, SanitizedMySQLResourceSchema]);
const ResourceOptionsSchema = z.discriminatedUnion("resource", [PostgresResourceListItemSchema]);
const ResourceOptionsSchema = z.discriminatedUnion("resource", [
PostgresResourceListItemSchema,
MySQLResourceListItemSchema
]);
export const registerPamResourceRouter = async (server: FastifyZodProvider) => {
server.route({
@@ -2,14 +2,14 @@ import { z } from "zod";
import { PamSessionsSchema } from "@app/db/schemas";
import { EventType } from "@app/ee/services/audit-log/audit-log-types";
import { MySQLSessionCredentialsSchema } from "@app/ee/services/pam-resource/mysql/mysql-resource-schemas";
import { PostgresSessionCredentialsSchema } from "@app/ee/services/pam-resource/postgres/postgres-resource-schemas";
import { PamSessionCommandLogSchema, SanitizedSessionSchema } from "@app/ee/services/pam-session/pam-session-schemas";
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";
// Use z.union([]) once there's multiple
const SessionCredentialsSchema = PostgresSessionCredentialsSchema;
const SessionCredentialsSchema = z.union([PostgresSessionCredentialsSchema, MySQLSessionCredentialsSchema]);
export const registerPamSessionRouter = async (server: FastifyZodProvider) => {
// Meant to be hit solely by gateway identities
+35 -9
View File
@@ -7,6 +7,7 @@
// All the any rules are disabled because passport typesense with fastify is really poor
import { Authenticator } from "@fastify/passport";
import { requestContext } from "@fastify/request-context";
import fastifySession from "@fastify/session";
import { MultiSamlStrategy } from "@node-saml/passport-saml";
import { FastifyRequest } from "fastify";
@@ -17,6 +18,7 @@ import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
import { getConfig } from "@app/lib/config/env";
import { BadRequestError } from "@app/lib/errors";
import { logger } from "@app/lib/logger";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
@@ -102,15 +104,15 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
},
// eslint-disable-next-line
async (req, profile, cb) => {
if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email =
profile?.email ??
// entra sends data in this format
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
try {
if (!profile) throw new BadRequestError({ message: "Missing profile" });
const email =
profile?.email ??
// entra sends data in this format
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
const firstName = (profile.firstName ??
// entra sends data in this format
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
@@ -144,7 +146,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
})
.filter((el) => el.key && !["email", "firstName", "lastName"].includes(el.key));
const { isUserCompleted, providerAuthToken } = await server.services.saml.samlLogin({
const { isUserCompleted, providerAuthToken, user, organization } = await server.services.saml.samlLogin({
externalId: profile.nameID,
email: email.toLowerCase(),
firstName,
@@ -154,8 +156,32 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
metadata: userMetadata
});
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": organization.id,
"infisical.organization.name": organization.name,
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken });
} catch (error) {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": email.toLowerCase(),
"infisical.auth.method": AuthAttemptAuthMethod.SAML,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
logger.error(error);
cb(error as Error);
}
@@ -340,6 +340,8 @@ export enum EventType {
ISSUE_PKI_SUBSCRIBER_CERT = "issue-pki-subscriber-cert",
SIGN_PKI_SUBSCRIBER_CERT = "sign-pki-subscriber-cert",
AUTOMATED_RENEW_SUBSCRIBER_CERT = "automated-renew-subscriber-cert",
AUTOMATED_RENEW_CERTIFICATE = "automated-renew-certificate",
AUTOMATED_RENEW_CERTIFICATE_FAILED = "automated-renew-certificate-failed",
LIST_PKI_SUBSCRIBER_CERTS = "list-pki-subscriber-certs",
GET_SUBSCRIBER_ACTIVE_CERT_BUNDLE = "get-subscriber-active-cert-bundle",
CREATE_KMS = "create-kms",
@@ -367,6 +369,9 @@ export enum EventType {
ISSUE_CERTIFICATE_FROM_PROFILE = "issue-certificate-from-profile",
SIGN_CERTIFICATE_FROM_PROFILE = "sign-certificate-from-profile",
ORDER_CERTIFICATE_FROM_PROFILE = "order-certificate-from-profile",
RENEW_CERTIFICATE = "renew-certificate",
UPDATE_CERTIFICATE_RENEWAL_CONFIG = "update-certificate-renewal-config",
DISABLE_CERTIFICATE_RENEWAL_CONFIG = "disable-certificate-renewal-config",
ATTEMPT_CREATE_SLACK_INTEGRATION = "attempt-create-slack-integration",
ATTEMPT_REINSTALL_SLACK_INTEGRATION = "attempt-reinstall-slack-integration",
GET_PROJECT_SLACK_CONFIG = "get-project-slack-config",
@@ -421,6 +426,7 @@ export enum EventType {
SECRET_SYNC_REMOVE_SECRETS = "secret-sync-remove-secrets",
GET_PKI_SYNCS = "get-pki-syncs",
GET_PKI_SYNC = "get-pki-sync",
GET_PKI_SYNC_CERTIFICATES = "get-pki-sync-certificates",
CREATE_PKI_SYNC = "create-pki-sync",
UPDATE_PKI_SYNC = "update-pki-sync",
DELETE_PKI_SYNC = "delete-pki-sync",
@@ -2458,6 +2464,29 @@ interface AutomatedRenewPkiSubscriberCert {
};
}
interface AutomatedRenewCertificate {
type: EventType.AUTOMATED_RENEW_CERTIFICATE;
metadata: {
certificateId: string;
commonName: string;
profileId: string;
renewBeforeDays: string;
profileName: string;
};
}
interface AutomatedRenewCertificateFailed {
type: EventType.AUTOMATED_RENEW_CERTIFICATE_FAILED;
metadata: {
certificateId: string;
commonName: string;
profileId: string;
renewBeforeDays: string;
profileName: string;
error: string;
};
}
interface SignPkiSubscriberCert {
type: EventType.SIGN_PKI_SUBSCRIBER_CERT;
metadata: {
@@ -2720,6 +2749,16 @@ interface OrderCertificateFromProfile {
};
}
interface RenewCertificate {
type: EventType.RENEW_CERTIFICATE;
metadata: {
originalCertificateId: string;
newCertificateId: string;
profileName: string;
commonName: string;
};
}
interface AttemptCreateSlackIntegration {
type: EventType.ATTEMPT_CREATE_SLACK_INTEGRATION;
metadata: {
@@ -3123,6 +3162,16 @@ interface GetPkiSyncEvent {
};
}
interface GetPkiSyncCertificatesEvent {
type: EventType.GET_PKI_SYNC_CERTIFICATES;
metadata: {
syncId: string;
count: number;
certificateIds: string[];
destination: string;
};
}
interface CreatePkiSyncEvent {
type: EventType.CREATE_PKI_SYNC;
metadata: {
@@ -4009,6 +4058,23 @@ interface PamResourceDeleteEvent {
};
}
interface UpdateCertificateRenewalConfigEvent {
type: EventType.UPDATE_CERTIFICATE_RENEWAL_CONFIG;
metadata: {
certificateId: string;
renewBeforeDays: string;
commonName: string;
};
}
interface DisableCertificateRenewalConfigEvent {
type: EventType.DISABLE_CERTIFICATE_RENEWAL_CONFIG;
metadata: {
certificateId: string;
commonName: string;
};
}
export type Event =
| CreateSubOrganizationEvent
| UpdateSubOrganizationEvent
@@ -4216,6 +4282,7 @@ export type Event =
| IssueCertificateFromProfile
| SignCertificateFromProfile
| OrderCertificateFromProfile
| RenewCertificate
| GetAzureAdCsTemplatesEvent
| AttemptCreateSlackIntegration
| AttemptReinstallSlackIntegration
@@ -4273,6 +4340,7 @@ export type Event =
| SecretSyncRemoveSecretsEvent
| GetPkiSyncsEvent
| GetPkiSyncEvent
| GetPkiSyncCertificatesEvent
| CreatePkiSyncEvent
| UpdatePkiSyncEvent
| DeletePkiSyncEvent
@@ -4373,4 +4441,8 @@ export type Event =
| PamResourceGetEvent
| PamResourceCreateEvent
| PamResourceUpdateEvent
| PamResourceDeleteEvent;
| PamResourceDeleteEvent
| UpdateCertificateRenewalConfigEvent
| DisableCertificateRenewalConfigEvent
| AutomatedRenewCertificate
| AutomatedRenewCertificateFailed;
@@ -112,7 +112,7 @@ export const dynamicSecretServiceFactory = ({
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
const selectedProvider = dynamicSecretProviders[provider.type];
const inputs = await selectedProvider.validateProviderInputs(provider.inputs, { projectId });
@@ -265,7 +265,7 @@ export const dynamicSecretServiceFactory = ({
if (newName) {
const existingDynamicSecret = await dynamicSecretDAL.findOne({ name: newName, folderId: folder.id });
if (existingDynamicSecret)
throw new BadRequestError({ message: "Provided dynamic secret already exist under the folder" });
throw new BadRequestError({ message: "Provided dynamic secret already exists under the folder" });
}
const { encryptor: secretManagerEncryptor, decryptor: secretManagerDecryptor } =
await kmsService.createCipherPairWithDataKey({
@@ -25,7 +25,7 @@ import { KmsDataKey } from "@app/services/kms/kms-types";
import { TNotificationServiceFactory } from "@app/services/notification/notification-service";
import { NotificationType } from "@app/services/notification/notification-types";
import { TOrgDALFactory } from "@app/services/org/org-dal";
import { SmtpTemplates, TSmtpService } from "@app/services/smtp/smtp-service";
import { TSmtpService } from "@app/services/smtp/smtp-service";
import { TLicenseServiceFactory } from "../license/license-service";
import { PamResource } from "../pam-resource/pam-resource-enums";
@@ -61,8 +61,7 @@ export const gatewayV2ServiceFactory = ({
relayDAL,
permissionService,
orgDAL,
notificationService,
smtpService
notificationService
}: TGatewayV2ServiceFactoryDep) => {
const $validateIdentityAccessToGateway = async (orgId: string, actorId: string, actorAuthMethod: ActorAuthMethod) => {
const orgLicensePlan = await licenseService.getPlan(orgId);
@@ -931,15 +930,17 @@ export const gatewayV2ServiceFactory = ({
}))
);
await smtpService.sendMail({
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
subjectLine: "Gateway Health Alert",
substitutions: {
type: "gateway",
names: gatewayNames
},
template: SmtpTemplates.HealthAlert
});
// Temporarily disabled email notifications due to excessive noise. Will be revised later
//
// await smtpService.sendMail({
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
// subjectLine: "Gateway Health Alert",
// substitutions: {
// type: "gateway",
// names: gatewayNames
// },
// template: SmtpTemplates.HealthAlert
// });
await Promise.all(gateways.map((gw) => gatewayV2DAL.updateById(gw.id, { healthAlertedAt: new Date() })));
} catch (error) {
@@ -341,7 +341,8 @@ export const kmipOperationServiceFactory = ({
algorithm: completeKeyDetails.internalKms.encryptionAlgorithm,
isActive: !key.isDisabled,
createdAt: key.createdAt,
updatedAt: key.updatedAt
updatedAt: key.updatedAt,
kmipMetadata: key.kmipMetadata as Record<string, unknown>
};
};
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/no-unsafe-call */
import { ForbiddenError } from "@casl/ability";
import { requestContext } from "@fastify/request-context";
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
@@ -15,6 +16,7 @@ import { TPermissionServiceFactory } from "@app/ee/services/permission/permissio
import { getConfig } from "@app/lib/config/env";
import { crypto } from "@app/lib/crypto";
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
@@ -471,7 +473,7 @@ export const oidcConfigServiceFactory = ({
});
}
return { isUserCompleted, providerAuthToken };
return { isUserCompleted, providerAuthToken, user };
};
const updateOidcCfg = async ({
@@ -754,10 +756,35 @@ export const oidcConfigServiceFactory = ({
callbackPort,
manageGroupMemberships: oidcCfg.manageGroupMemberships
})
.then(({ isUserCompleted, providerAuthToken }) => {
.then(({ isUserCompleted, providerAuthToken, user }) => {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.user.id": user.id,
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(null, { isUserCompleted, providerAuthToken });
})
.catch((error) => {
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
authAttemptCounter.add(1, {
"infisical.user.email": claims?.email?.toLowerCase(),
"infisical.organization.id": org.id,
"infisical.organization.name": org.name,
"infisical.auth.method": AuthAttemptAuthMethod.OIDC,
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
"client.address": requestContext.get("ip"),
"user_agent.original": requestContext.get("userAgent")
});
}
cb(error);
});
}
@@ -45,17 +45,47 @@ export const decryptAccountCredentials = async ({
return JSON.parse(decryptedPlainTextBlob.toString()) as TPamAccountCredentials;
};
export const decryptAccount = async <T extends { encryptedCredentials: Buffer }>(
export const decryptAccountMessage = async ({
projectId,
encryptedMessage,
kmsService
}: {
projectId: string;
encryptedMessage: Buffer;
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
}) => {
const { decryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId
});
const decryptedPlainTextBlob = decryptor({
cipherTextBlob: encryptedMessage
});
return decryptedPlainTextBlob.toString();
};
export const decryptAccount = async <
T extends { encryptedCredentials: Buffer; encryptedLastRotationMessage?: Buffer | null }
>(
account: T,
projectId: string,
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">
): Promise<T & { credentials: TPamAccountCredentials }> => {
): Promise<T & { credentials: TPamAccountCredentials; lastRotationMessage: string | null }> => {
return {
...account,
credentials: await decryptAccountCredentials({
encryptedCredentials: account.encryptedCredentials,
projectId,
kmsService
})
} as T & { credentials: TPamAccountCredentials };
}),
lastRotationMessage: account.encryptedLastRotationMessage
? await decryptAccountMessage({
encryptedMessage: account.encryptedLastRotationMessage,
projectId,
kmsService
})
: null
};
};
@@ -15,6 +15,7 @@ import { logger } from "@app/lib/logger";
import { OrgServiceActor } from "@app/lib/types";
import { ActorType } from "@app/services/auth/auth-type";
import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { TProjectDALFactory } from "@app/services/project/project-dal";
import { TUserDALFactory } from "@app/services/user/user-dal";
@@ -353,6 +354,7 @@ export const pamAccountServiceFactory = ({
TPamAccounts & {
resource: Pick<TPamResources, "id" | "name" | "resourceType"> & { rotationCredentialsConfigured: boolean };
credentials: TPamAccountCredentials;
lastRotationMessage: string | null;
}
> = [];
@@ -376,6 +378,7 @@ export const pamAccountServiceFactory = ({
) {
// Decrypt the account only if the user has permission to read it
const decryptedAccount = await decryptAccount(account, account.projectId, kmsService);
decryptedAndPermittedAccounts.push({
...decryptedAccount,
resource: {
@@ -575,10 +578,10 @@ export const pamAccountServiceFactory = ({
for (let i = 0; i < accounts.length; i += ROTATION_CONCURRENCY_LIMIT) {
const batch = accounts.slice(i, i + ROTATION_CONCURRENCY_LIMIT);
const rotationPromises = batch.map(async (account) =>
pamAccountDAL.transaction(async (tx) => {
let logResourceType = "unknown";
try {
const rotationPromises = batch.map(async (account) => {
let logResourceType = "unknown";
try {
await pamAccountDAL.transaction(async (tx) => {
const resource = await pamResourceDAL.findById(account.resourceId, tx);
if (!resource || !resource.encryptedRotationAccountCredentials) return;
logResourceType = resource.resourceType;
@@ -619,7 +622,9 @@ export const pamAccountServiceFactory = ({
account.id,
{
encryptedCredentials,
lastRotatedAt: new Date()
lastRotatedAt: new Date(),
rotationStatus: "success",
encryptedLastRotationMessage: null
},
tx
);
@@ -640,32 +645,45 @@ export const pamAccountServiceFactory = ({
}
}
});
} catch (error) {
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
});
} catch (error) {
logger.error(error, `Failed to rotate credentials for account [accountId=${account.id}]`);
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
const errorMessage = error instanceof Error ? error.message : "An unknown error occurred";
await auditLogService.createAuditLog({
projectId: account.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
metadata: {
accountId: account.id,
accountName: account.name,
resourceId: account.resourceId,
resourceType: logResourceType,
errorMessage
}
const { encryptor } = await kmsService.createCipherPairWithDataKey({
type: KmsDataKey.SecretManager,
projectId: account.projectId
});
const { cipherTextBlob: encryptedMessage } = encryptor({
plainText: Buffer.from(errorMessage)
});
await pamAccountDAL.updateById(account.id, {
rotationStatus: "failed",
encryptedLastRotationMessage: encryptedMessage
});
await auditLogService.createAuditLog({
projectId: account.projectId,
actor: {
type: ActorType.PLATFORM,
metadata: {}
},
event: {
type: EventType.PAM_ACCOUNT_CREDENTIAL_ROTATION_FAILED,
metadata: {
accountId: account.id,
accountName: account.name,
resourceId: account.resourceId,
resourceType: logResourceType,
errorMessage
}
});
throw error; // Rollback transaction
}
})
);
}
});
}
});
// eslint-disable-next-line no-await-in-loop
await Promise.all(rotationPromises);
@@ -0,0 +1,8 @@
import { MySQLResourceListItemSchema } from "./mysql-resource-schemas";
export const getMySQLResourceListItem = () => {
return {
name: MySQLResourceListItemSchema.shape.name.value,
resource: MySQLResourceListItemSchema.shape.resource.value
};
};
@@ -0,0 +1,76 @@
import { z } from "zod";
import { PamResource } from "../pam-resource-enums";
import {
BaseCreatePamAccountSchema,
BaseCreatePamResourceSchema,
BasePamAccountSchema,
BasePamAccountSchemaWithResource,
BasePamResourceSchema,
BaseUpdatePamAccountSchema,
BaseUpdatePamResourceSchema
} from "../pam-resource-schemas";
import {
BaseSqlAccountCredentialsSchema,
BaseSqlResourceConnectionDetailsSchema
} from "../shared/sql/sql-resource-schemas";
// Resources
export const MySQLResourceConnectionDetailsSchema = BaseSqlResourceConnectionDetailsSchema.extend({
// MySQL db in many cases the db will not be provided when making connection
database: z.string().trim()
});
export const MySQLAccountCredentialsSchema = BaseSqlAccountCredentialsSchema;
const BaseMySQLResourceSchema = BasePamResourceSchema.extend({ resourceType: z.literal(PamResource.MySQL) });
export const MySQLResourceSchema = BaseMySQLResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema,
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
});
export const SanitizedMySQLResourceSchema = BaseMySQLResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema,
rotationAccountCredentials: MySQLAccountCredentialsSchema.pick({
username: true
})
.nullable()
.optional()
});
export const MySQLResourceListItemSchema = z.object({
name: z.literal("MySQL"),
resource: z.literal(PamResource.MySQL)
});
export const CreateMySQLResourceSchema = BaseCreatePamResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema,
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
});
export const UpdateMySQLResourceSchema = BaseUpdatePamResourceSchema.extend({
connectionDetails: MySQLResourceConnectionDetailsSchema.optional(),
rotationAccountCredentials: MySQLAccountCredentialsSchema.nullable().optional()
});
// Accounts
export const MySQLAccountSchema = BasePamAccountSchema.extend({
credentials: MySQLAccountCredentialsSchema
});
export const CreateMySQLAccountSchema = BaseCreatePamAccountSchema.extend({
credentials: MySQLAccountCredentialsSchema
});
export const UpdateMySQLAccountSchema = BaseUpdatePamAccountSchema.extend({
credentials: MySQLAccountCredentialsSchema.optional()
});
export const SanitizedMySQLAccountWithResourceSchema = BasePamAccountSchemaWithResource.extend({
credentials: MySQLAccountCredentialsSchema.pick({
username: true
})
});
// Sessions
export const MySQLSessionCredentialsSchema = MySQLResourceConnectionDetailsSchema.and(MySQLAccountCredentialsSchema);
@@ -0,0 +1,16 @@
import { z } from "zod";
import {
MySQLAccountCredentialsSchema,
MySQLAccountSchema,
MySQLResourceConnectionDetailsSchema,
MySQLResourceSchema
} from "./mysql-resource-schemas";
// Resources
export type TMySQLResource = z.infer<typeof MySQLResourceSchema>;
export type TMySQLResourceConnectionDetails = z.infer<typeof MySQLResourceConnectionDetailsSchema>;
// Accounts
export type TMySQLAccount = z.infer<typeof MySQLAccountSchema>;
export type TMySQLAccountCredentials = z.infer<typeof MySQLAccountCredentialsSchema>;
@@ -1,3 +1,4 @@
export enum PamResource {
Postgres = "postgres"
Postgres = "postgres",
MySQL = "mysql"
}
@@ -5,5 +5,6 @@ import { sqlResourceFactory } from "./shared/sql/sql-resource-factory";
type TPamResourceFactoryImplementation = TPamResourceFactory<TPamResourceConnectionDetails, TPamAccountCredentials>;
export const PAM_RESOURCE_FACTORY_MAP: Record<PamResource, TPamResourceFactoryImplementation> = {
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation
[PamResource.Postgres]: sqlResourceFactory as TPamResourceFactoryImplementation,
[PamResource.MySQL]: sqlResourceFactory as TPamResourceFactoryImplementation
};
@@ -3,11 +3,12 @@ import { TKmsServiceFactory } from "@app/services/kms/kms-service";
import { KmsDataKey } from "@app/services/kms/kms-types";
import { decryptAccountCredentials } from "../pam-account/pam-account-fns";
import { getMySQLResourceListItem } from "./mysql/mysql-resource-fns";
import { TPamResource, TPamResourceConnectionDetails } from "./pam-resource-types";
import { getPostgresResourceListItem } from "./postgres/postgres-resource-fns";
export const listResourceOptions = () => {
return [getPostgresResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
return [getPostgresResourceListItem(), getMySQLResourceListItem()].sort((a, b) => a.name.localeCompare(b.name));
};
// Resource
@@ -33,7 +33,9 @@ export const BasePamAccountSchemaWithResource = BasePamAccountSchema.extend({
resourceType: true
}).extend({
rotationCredentialsConfigured: z.boolean()
})
}),
lastRotationMessage: z.string().nullable().optional(),
rotationStatus: z.string().nullable().optional()
});
export const BaseCreatePamAccountSchema = z.object({
@@ -1,4 +1,10 @@
import { TGatewayV2ServiceFactory } from "../gateway-v2/gateway-v2-service";
import {
TMySQLAccount,
TMySQLAccountCredentials,
TMySQLResource,
TMySQLResourceConnectionDetails
} from "./mysql/mysql-resource-types";
import { PamResource } from "./pam-resource-enums";
import {
TPostgresAccount,
@@ -8,12 +14,13 @@ import {
} from "./postgres/postgres-resource-types";
// Resource types
export type TPamResource = TPostgresResource;
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails;
export type TPamResource = TPostgresResource | TMySQLResource;
export type TPamResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
// Account types
export type TPamAccount = TPostgresAccount;
export type TPamAccountCredentials = TPostgresAccountCredentials;
export type TPamAccount = TPostgresAccount | TMySQLAccount;
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
export type TPamAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
// Resource DTOs
export type TCreateResourceDTO = Pick<
@@ -1,4 +1,6 @@
import knex, { Knex } from "knex";
import knex from "knex";
import mysql, { Connection } from "mysql2/promise";
import * as pg from "pg";
import tls, { PeerCertificate } from "tls";
import { verifyHostInputValidity } from "@app/ee/services/dynamic-secret/dynamic-secret-fns";
@@ -20,30 +22,162 @@ const EXTERNAL_REQUEST_TIMEOUT = 10 * 1000;
const TEST_CONNECTION_USERNAME = "infisical-gateway-connection-test";
const TEST_CONNECTION_PASSWORD = "infisical-gateway-connection-test-password";
const SIMPLE_QUERY = "select 1";
const SQL_CONNECTION_CLIENT_MAP = {
[PamResource.Postgres]: "pg"
};
export interface SqlResourceConnection {
/**
* Check and see if the connection is good or not.
*
* @param connectOnly when true, if we only want to know that making the connection is possible or not,
* we don't care about authentication failures
* @returns Promise to be resolved when the connection is good, otherwise an error will be errbacked
*/
validate: (connectOnly: boolean) => Promise<void>;
const getConnectionConfig = (
resourceType: PamResource,
{ host, sslEnabled, sslRejectUnauthorized, sslCertificate }: TSqlResourceConnectionDetails
) => {
switch (resourceType) {
/**
* Rotate password and return the new credentials.
*
* @param currentCredentials the current credentials to rotate
*
* @returns Promise to be resolved with the new credentials
*/
rotateCredentials: (
currentCredentials: TSqlAccountCredentials,
newPassword: string
) => Promise<TSqlAccountCredentials>;
/**
* Close the connection.
*
* @returns Promise for closing the connection
*/
close: () => Promise<void>;
}
const makeSqlConnection = (
proxyPort: number,
config: {
connectionDetails: TSqlResourceConnectionDetails;
resourceType: PamResource;
username?: string;
password?: string;
}
): SqlResourceConnection => {
const { connectionDetails, resourceType, username, password } = config;
const { host, sslEnabled, sslRejectUnauthorized, sslCertificate } = connectionDetails;
const actualUsername = username ?? TEST_CONNECTION_USERNAME; // Use provided username or fallback
const actualPassword = password ?? TEST_CONNECTION_PASSWORD; // Use provided password or fallback
switch (config.resourceType) {
case PamResource.Postgres: {
const client = knex({
client: "pg",
connection: {
host: "localhost",
port: proxyPort,
user: actualUsername,
password: actualPassword,
database: connectionDetails.database,
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
ssl: sslEnabled
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate,
servername: host,
// When using proxy, we need to bypass hostname validation since we connect to localhost
// but validate the certificate against the actual hostname
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
return tls.checkServerIdentity(host, cert);
}
}
: false
}
});
return {
ssl: sslEnabled
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate,
servername: host,
// When using proxy, we need to bypass hostname validation since we connect to localhost
// but validate the certificate against the actual hostname
checkServerIdentity: (hostname: string, cert: PeerCertificate) => {
return tls.checkServerIdentity(host, cert);
validate: async (connectOnly) => {
try {
await client.raw(SIMPLE_QUERY);
} catch (error) {
if (error instanceof pg.DatabaseError) {
// Hacky way to know if we successfully hit the database.
// TODO: potentially two approaches to solve the problem.
// 1. change the work flow, add account first then resource
// 2. modify relay to add a new endpoint for returning if the target host is healthy or not
// (like being able to do an auth handshake regardless pass or not)
if (
connectOnly &&
(error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"` ||
error.message.includes("no pg_hba.conf entry for host"))
) {
return;
}
}
: false
throw new BadRequestError({
message: `Unable to validate connection to ${resourceType}: ${(error as Error).message || String(error)}`
});
}
},
rotateCredentials: async (currentCredentials, newPassword) => {
// Note: The generated random password is not really going to make SQL Injection possible.
// The reason we are not using parameters binding is that the "ALTER USER" syntax is DDL,
// parameters binding is not supported. But just in case if the this code got copied
// around and repurposed, let's just do some naive escaping regardless
await client.raw(`ALTER USER :username: WITH PASSWORD '${newPassword.replace(/'/g, "''")}'`, {
username: currentCredentials.username
});
return { username: currentCredentials.username, password: newPassword };
},
close: () => client.destroy()
};
}
case PamResource.MySQL: {
return {
validate: async (connectOnly) => {
let client: Connection | null = null;
try {
// Notice: the reason we are not using Knex for mysql2 is because we don't need any fancy feature from Knex.
// mysql2 doesn't provide custom ssl verification function pass in.
// ref: https://github.com/sidorares/node-mysql2/blob/2543272a2ada8d8a07f74582549d7dd3fe948e2d/lib/base/connection.js#L358-L362
// and then even I tried to workaround it with Knex's pool afterCreate hook, but then encounter a bug:
// ref: https://github.com/knex/knex/issues/5352
// It appears that using Knex causing more troubles than not, we are just checking the connections,
// so it's much easier to create raw connection with the driver lib directly
client = await mysql.createConnection({
host: "localhost",
port: proxyPort,
user: actualUsername, // Use provided username or fallback
password: actualPassword, // Use provided password or fallback
database: connectionDetails.database,
ssl: sslEnabled
? {
rejectUnauthorized: sslRejectUnauthorized,
ca: sslCertificate
}
: undefined
});
await client.query(SIMPLE_QUERY);
} catch (error) {
if (connectOnly) {
// Hacky way to know if we successfully hit the database.
if (
error instanceof Error &&
error.message.startsWith(`Access denied for user '${TEST_CONNECTION_USERNAME}'@`)
) {
return;
}
}
// TODO: handle other errors, and throw standardlized errors providing user-friendly msg
throw error;
} finally {
await client?.end();
}
},
rotateCredentials: async () => {
// TODO: the pwd rotation for MySQL is not supported yet
throw new BadRequestError({
message: "Unsupported operation"
});
},
close: async () => {}
};
}
default:
@@ -62,10 +196,9 @@ export const executeWithGateway = async <T>(
password?: string;
},
gatewayV2Service: Pick<TGatewayV2ServiceFactory, "getPlatformConnectionDetailsByGatewayId">,
operation: (client: Knex) => Promise<T>
operation: (connection: SqlResourceConnection) => Promise<T>
): Promise<T> => {
const { connectionDetails, resourceType, gatewayId, username, password } = config;
const { connectionDetails, gatewayId } = config;
const [targetHost] = await verifyHostInputValidity(connectionDetails.host, true);
const platformConnectionDetails = await gatewayV2Service.getPlatformConnectionDetailsByGatewayId({
gatewayId,
@@ -79,22 +212,11 @@ export const executeWithGateway = async <T>(
return withGatewayV2Proxy(
async (proxyPort) => {
const client = knex({
client: SQL_CONNECTION_CLIENT_MAP[resourceType],
connection: {
database: connectionDetails.database,
port: proxyPort,
host: "localhost",
user: username ?? TEST_CONNECTION_USERNAME, // Use provided username or fallback
password: password ?? TEST_CONNECTION_PASSWORD, // Use provided password or fallback
connectionTimeoutMillis: EXTERNAL_REQUEST_TIMEOUT,
...getConnectionConfig(resourceType, connectionDetails)
}
});
const connection = makeSqlConnection(proxyPort, config);
try {
return await operation(client);
return await operation(connection);
} finally {
await client.destroy();
await connection.close();
}
},
{
@@ -115,25 +237,14 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
const validateConnection = async () => {
try {
await executeWithGateway({ connectionDetails, gatewayId, resourceType }, gatewayV2Service, async (client) => {
await client.raw("Select 1");
await client.validate(true);
});
return connectionDetails;
} catch (error) {
// Hacky way to know if we successfully hit the database
if (error instanceof BadRequestError) {
if (error.message === `password authentication failed for user "${TEST_CONNECTION_USERNAME}"`) {
return connectionDetails;
}
if (error.message.includes("no pg_hba.conf entry for host")) {
return connectionDetails;
}
if (error.message === "Connection terminated unexpectedly") {
throw new BadRequestError({
message: "Connection terminated unexpectedly. Verify that host and port are correct"
});
}
if (error instanceof BadRequestError && error.message === "Connection terminated unexpectedly") {
throw new BadRequestError({
message: "Connection terminated unexpectedly. Verify that host and port are correct"
});
}
throw new BadRequestError({
@@ -156,11 +267,12 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
},
gatewayV2Service,
async (client) => {
await client.raw("Select 1");
await client.validate(false);
}
);
return credentials;
} catch (error) {
// TODO: extract these logic into each SQL connection
if (error instanceof BadRequestError) {
if (error.message === `password authentication failed for user "${credentials.username}"`) {
throw new BadRequestError({
@@ -185,10 +297,9 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
rotationAccountCredentials,
currentCredentials
) => {
const newPassword = alphaNumericNanoId(32);
try {
const newPassword = alphaNumericNanoId(32);
await executeWithGateway(
return await executeWithGateway(
{
connectionDetails,
gatewayId,
@@ -197,20 +308,8 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
password: rotationAccountCredentials.password
},
gatewayV2Service,
async (client) => {
switch (resourceType) {
case PamResource.Postgres:
await client.raw(`ALTER USER ?? WITH PASSWORD '${newPassword}'`, [currentCredentials.username]);
break;
default:
throw new BadRequestError({
message: `Password rotation for ${resourceType as PamResource} is not supported.`
});
}
}
(client) => client.rotateCredentials(currentCredentials, newPassword)
);
return { username: currentCredentials.username, password: newPassword };
} catch (error) {
if (error instanceof BadRequestError) {
if (error.message === `password authentication failed for user "${rotationAccountCredentials.username}"`) {
@@ -232,8 +331,10 @@ export const sqlResourceFactory: TPamResourceFactory<TSqlResourceConnectionDetai
}
}
const sanitizedErrorMessage = ((error as Error).message || String(error)).replaceAll(newPassword, "REDACTED");
throw new BadRequestError({
message: `Unable to rotate account credentials for ${resourceType}: ${(error as Error).message || String(error)}`
message: `Unable to rotate account credentials for ${resourceType}: ${sanitizedErrorMessage}`
});
}
};
@@ -1,7 +1,9 @@
import { TMySQLAccountCredentials, TMySQLResourceConnectionDetails } from "../../mysql/mysql-resource-types";
import {
TPostgresAccountCredentials,
TPostgresResourceConnectionDetails
} from "../../postgres/postgres-resource-types";
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails;
export type TSqlAccountCredentials = TPostgresAccountCredentials;
export type TSqlResourceConnectionDetails = TPostgresResourceConnectionDetails | TMySQLResourceConnectionDetails;
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
export type TSqlAccountCredentials = TPostgresAccountCredentials | TMySQLAccountCredentials;
@@ -337,6 +337,12 @@ export const permissionServiceFactory = ({
throw new NotFoundError({ message: `Project with ${projectId} not found` });
}
requestContext.set("projectDetails", {
id: projectDetails.id,
name: projectDetails.name,
slug: projectDetails.slug
});
if (projectDetails.orgId !== actorOrgId) {
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
}
+11 -9
View File
@@ -1268,15 +1268,17 @@ export const relayServiceFactory = ({
}))
);
await smtpService.sendMail({
recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
subjectLine: "Relay Health Alert",
substitutions: {
type: "relay",
names: relayNames
},
template: SmtpTemplates.HealthAlert
});
// Temporarily disabled email notifications due to excessive noise. Will be revised later
//
// await smtpService.sendMail({
// recipients: admins.map((admin) => admin.user.email).filter((v): v is string => !!v),
// subjectLine: "Relay Health Alert",
// substitutions: {
// type: "relay",
// names: relayNames
// },
// template: SmtpTemplates.HealthAlert
// });
}
await Promise.all(relays.map((r) => relayDAL.updateById(r.id, { healthAlertedAt: new Date() })));
@@ -769,7 +769,7 @@ export const samlConfigServiceFactory = ({
});
}
return { isUserCompleted, providerAuthToken };
return { isUserCompleted, providerAuthToken, user, organization };
};
return {
@@ -1,4 +1,4 @@
import { TSamlConfigs } from "@app/db/schemas";
import { TOrganizations, TSamlConfigs, TUsers } from "@app/db/schemas";
import { TOrgPermission } from "@app/lib/types";
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
@@ -78,5 +78,7 @@ export type TSamlConfigServiceFactory = {
samlLogin: (arg: TSamlLoginDTO) => Promise<{
isUserCompleted: boolean;
providerAuthToken: string;
user: TUsers;
organization: TOrganizations;
}>;
};
@@ -1517,7 +1517,7 @@ export const secretApprovalRequestServiceFactory = ({
}))
);
if (secrets.length)
throw new BadRequestError({ message: `Secret already exist: ${secrets.map((el) => el.key).join(",")}` });
throw new BadRequestError({ message: `Secret already exists: ${secrets.map((el) => el.key).join(",")}` });
commits.push(
...createdSecrets.map((createdSecret) => ({