mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-16 12:04:03 +00:00
Merge branch 'main' into feature/slack-secret-sync-error-notification
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" });
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user