mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 03:28:43 +00:00
Merge pull request #4770 from Infisical/misc/add-infisical-specific-otel-metrics
misc: add custom metrics
This commit is contained in:
Vendored
+14
@@ -135,9 +135,23 @@ import { TWorkflowIntegrationServiceFactory } from "@app/services/workflow-integ
|
|||||||
declare module "@fastify/request-context" {
|
declare module "@fastify/request-context" {
|
||||||
interface RequestContextData {
|
interface RequestContextData {
|
||||||
reqId: string;
|
reqId: string;
|
||||||
|
ip?: string;
|
||||||
|
userAgent?: string;
|
||||||
orgId?: string;
|
orgId?: string;
|
||||||
|
orgName?: string;
|
||||||
|
userAuthInfo?: {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
};
|
||||||
|
projectDetails?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
identityAuthInfo?: {
|
identityAuthInfo?: {
|
||||||
identityId: string;
|
identityId: string;
|
||||||
|
identityName: string;
|
||||||
|
authMethod: string;
|
||||||
oidc?: {
|
oidc?: {
|
||||||
claims: Record<string, string>;
|
claims: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// All the any rules are disabled because passport typesense with fastify is really poor
|
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||||
|
|
||||||
import { Authenticator } from "@fastify/passport";
|
import { Authenticator } from "@fastify/passport";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import fastifySession from "@fastify/session";
|
import fastifySession from "@fastify/session";
|
||||||
import { MultiSamlStrategy } from "@node-saml/passport-saml";
|
import { MultiSamlStrategy } from "@node-saml/passport-saml";
|
||||||
import { FastifyRequest } from "fastify";
|
import { FastifyRequest } from "fastify";
|
||||||
@@ -17,6 +18,7 @@ import { ApiDocsTags, SamlSso } from "@app/lib/api-docs";
|
|||||||
import { getConfig } from "@app/lib/config/env";
|
import { getConfig } from "@app/lib/config/env";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
import { readLimit, writeLimit } from "@app/server/config/rateLimiter";
|
||||||
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
import { verifyAuth } from "@app/server/plugins/auth/verify-auth";
|
||||||
import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
|
import { SanitizedSamlConfigSchema } from "@app/server/routes/sanitizedSchema/directory-config";
|
||||||
@@ -102,7 +104,6 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req, profile, cb) => {
|
async (req, profile, cb) => {
|
||||||
try {
|
|
||||||
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
if (!profile) throw new BadRequestError({ message: "Missing profile" });
|
||||||
|
|
||||||
const email =
|
const email =
|
||||||
@@ -111,6 +112,7 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
(profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/email"] as string) ??
|
(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\
|
(profile?.emailAddress as string); // emailRippling is added because in Rippling the field `email` reserved\
|
||||||
|
|
||||||
|
try {
|
||||||
const firstName = (profile.firstName ??
|
const firstName = (profile.firstName ??
|
||||||
// entra sends data in this format
|
// entra sends data in this format
|
||||||
profile["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/firstName"]) as string;
|
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));
|
.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,
|
externalId: profile.nameID,
|
||||||
email: email.toLowerCase(),
|
email: email.toLowerCase(),
|
||||||
firstName,
|
firstName,
|
||||||
@@ -154,8 +156,32 @@ export const registerSamlRouter = async (server: FastifyZodProvider) => {
|
|||||||
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
|
orgId: (req as unknown as FastifyRequest).ssoConfig?.orgId,
|
||||||
metadata: userMetadata
|
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 });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} 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);
|
logger.error(error);
|
||||||
cb(error as Error);
|
cb(error as Error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
/* eslint-disable @typescript-eslint/no-unsafe-call */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
import { Issuer, Issuer as OpenIdIssuer, Strategy as OpenIdStrategy, TokenSet } from "openid-client";
|
||||||
|
|
||||||
import { AccessScope, OrganizationActionScope, OrgMembershipStatus, TableName, TUsers } from "@app/db/schemas";
|
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 { getConfig } from "@app/lib/config/env";
|
||||||
import { crypto } from "@app/lib/crypto";
|
import { crypto } from "@app/lib/crypto";
|
||||||
import { BadRequestError, ForbiddenRequestError, NotFoundError, OidcAuthError } from "@app/lib/errors";
|
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 { OrgServiceActor } from "@app/lib/types";
|
||||||
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
import { ActorType, AuthMethod, AuthTokenType } from "@app/services/auth/auth-type";
|
||||||
import { TAuthTokenServiceFactory } from "@app/services/auth-token/auth-token-service";
|
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 ({
|
const updateOidcCfg = async ({
|
||||||
@@ -754,10 +756,35 @@ export const oidcConfigServiceFactory = ({
|
|||||||
callbackPort,
|
callbackPort,
|
||||||
manageGroupMemberships: oidcCfg.manageGroupMemberships
|
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 });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.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);
|
cb(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -337,6 +337,12 @@ export const permissionServiceFactory = ({
|
|||||||
throw new NotFoundError({ message: `Project with ${projectId} not found` });
|
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) {
|
if (projectDetails.orgId !== actorOrgId) {
|
||||||
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
throw new ForbiddenRequestError({ name: "You are not logged into this organization" });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -769,7 +769,7 @@ export const samlConfigServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { isUserCompleted, providerAuthToken };
|
return { isUserCompleted, providerAuthToken, user, organization };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
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 { TOrgPermission } from "@app/lib/types";
|
||||||
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
import { ActorAuthMethod, ActorType } from "@app/services/auth/auth-type";
|
||||||
|
|
||||||
@@ -78,5 +78,7 @@ export type TSamlConfigServiceFactory = {
|
|||||||
samlLogin: (arg: TSamlLoginDTO) => Promise<{
|
samlLogin: (arg: TSamlLoginDTO) => Promise<{
|
||||||
isUserCompleted: boolean;
|
isUserCompleted: boolean;
|
||||||
providerAuthToken: string;
|
providerAuthToken: string;
|
||||||
|
user: TUsers;
|
||||||
|
organization: TOrganizations;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
import opentelemetry from "@opentelemetry/api";
|
||||||
|
|
||||||
|
import { getConfig } from "../config/env";
|
||||||
|
|
||||||
|
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
|
||||||
|
|
||||||
|
export enum AuthAttemptAuthMethod {
|
||||||
|
EMAIL = "email",
|
||||||
|
SAML = "saml",
|
||||||
|
OIDC = "oidc",
|
||||||
|
GOOGLE = "google",
|
||||||
|
GITHUB = "github",
|
||||||
|
GITLAB = "gitlab",
|
||||||
|
TOKEN_AUTH = "token-auth",
|
||||||
|
UNIVERSAL_AUTH = "universal-auth",
|
||||||
|
KUBERNETES_AUTH = "kubernetes-auth",
|
||||||
|
GCP_AUTH = "gcp-auth",
|
||||||
|
ALICLOUD_AUTH = "alicloud-auth",
|
||||||
|
AWS_AUTH = "aws-auth",
|
||||||
|
AZURE_AUTH = "azure-auth",
|
||||||
|
TLS_CERT_AUTH = "tls-cert-auth",
|
||||||
|
OCI_AUTH = "oci-auth",
|
||||||
|
OIDC_AUTH = "oidc-auth",
|
||||||
|
JWT_AUTH = "jwt-auth",
|
||||||
|
LDAP_AUTH = "ldap-auth"
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AuthAttemptAuthResult {
|
||||||
|
SUCCESS = "success",
|
||||||
|
FAILURE = "failure"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const authAttemptCounter = infisicalMeter.createCounter("infisical.auth.attempt.count", {
|
||||||
|
description: "Authentication attempts (both successful and failed)",
|
||||||
|
unit: "{attempt}"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const secretReadCounter = infisicalMeter.createCounter("infisical.secret.read.count", {
|
||||||
|
description: "Number of secret read operations",
|
||||||
|
unit: "{operation}"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const recordSecretReadMetric = (params: { environment: string; secretPath: string; name?: string }) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
const attributes: Record<string, string> = {
|
||||||
|
"infisical.environment": params.environment,
|
||||||
|
"infisical.secret.path": params.secretPath,
|
||||||
|
...(params.name ? { "infisical.secret.name": params.name } : {})
|
||||||
|
};
|
||||||
|
|
||||||
|
const orgId = requestContext.get("orgId");
|
||||||
|
if (orgId) {
|
||||||
|
attributes["infisical.organization.id"] = orgId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const orgName = requestContext.get("orgName");
|
||||||
|
if (orgName) {
|
||||||
|
attributes["infisical.organization.name"] = orgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectDetails = requestContext.get("projectDetails");
|
||||||
|
if (projectDetails?.id) {
|
||||||
|
attributes["infisical.project.id"] = projectDetails.id;
|
||||||
|
}
|
||||||
|
if (projectDetails?.name) {
|
||||||
|
attributes["infisical.project.name"] = projectDetails.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAuthInfo = requestContext.get("userAuthInfo");
|
||||||
|
if (userAuthInfo?.userId) {
|
||||||
|
attributes["infisical.user.id"] = userAuthInfo.userId;
|
||||||
|
}
|
||||||
|
if (userAuthInfo?.email) {
|
||||||
|
attributes["infisical.user.email"] = userAuthInfo.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
const identityAuthInfo = requestContext.get("identityAuthInfo");
|
||||||
|
if (identityAuthInfo?.identityId) {
|
||||||
|
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
|
||||||
|
}
|
||||||
|
if (identityAuthInfo?.identityName) {
|
||||||
|
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = requestContext.get("userAgent");
|
||||||
|
if (userAgent) {
|
||||||
|
attributes["user_agent.original"] = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ip = requestContext.get("ip");
|
||||||
|
if (ip) {
|
||||||
|
attributes["client.address"] = ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
secretReadCounter.add(1, attributes);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -141,7 +141,9 @@ export const main = async ({
|
|||||||
await server.register(fastifyRequestContext, {
|
await server.register(fastifyRequestContext, {
|
||||||
defaultStoreValues: (req) => ({
|
defaultStoreValues: (req) => ({
|
||||||
reqId: req.id,
|
reqId: req.id,
|
||||||
log: req.log.child({ reqId: req.id })
|
log: req.log.child({ reqId: req.id }),
|
||||||
|
ip: req.realIp,
|
||||||
|
userAgent: req.headers["user-agent"]
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,26 @@
|
|||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import opentelemetry from "@opentelemetry/api";
|
import opentelemetry from "@opentelemetry/api";
|
||||||
import fp from "fastify-plugin";
|
import fp from "fastify-plugin";
|
||||||
|
|
||||||
export const apiMetrics = fp(async (fastify) => {
|
|
||||||
const apiMeter = opentelemetry.metrics.getMeter("API");
|
const apiMeter = opentelemetry.metrics.getMeter("API");
|
||||||
|
|
||||||
const latencyHistogram = apiMeter.createHistogram("API_latency", {
|
const latencyHistogram = apiMeter.createHistogram("API_latency", {
|
||||||
unit: "ms"
|
unit: "ms"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
|
||||||
|
|
||||||
|
const requestCounter = infisicalMeter.createCounter("infisical.http.server.request.count", {
|
||||||
|
description: "Total number of API requests to Infisical (covers both human users and machine identities)",
|
||||||
|
unit: "{request}"
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestDurationHistogram = infisicalMeter.createHistogram("infisical.http.server.request.duration", {
|
||||||
|
description: "API request latency",
|
||||||
|
unit: "s"
|
||||||
|
});
|
||||||
|
|
||||||
|
export const apiMetrics = fp(async (fastify) => {
|
||||||
fastify.addHook("onResponse", async (request, reply) => {
|
fastify.addHook("onResponse", async (request, reply) => {
|
||||||
const { method } = request;
|
const { method } = request;
|
||||||
const route = request.routerPath;
|
const route = request.routerPath;
|
||||||
@@ -17,5 +31,67 @@ export const apiMetrics = fp(async (fastify) => {
|
|||||||
method,
|
method,
|
||||||
statusCode
|
statusCode
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const orgId = requestContext.get("orgId");
|
||||||
|
const orgName = requestContext.get("orgName");
|
||||||
|
const userAuthInfo = requestContext.get("userAuthInfo");
|
||||||
|
const identityAuthInfo = requestContext.get("identityAuthInfo");
|
||||||
|
const projectDetails = requestContext.get("projectDetails");
|
||||||
|
const userAgent = requestContext.get("userAgent");
|
||||||
|
const ip = requestContext.get("ip");
|
||||||
|
|
||||||
|
const attributes: Record<string, string | number> = {
|
||||||
|
"http.request.method": method,
|
||||||
|
"http.route": route,
|
||||||
|
"http.response.status_code": statusCode
|
||||||
|
};
|
||||||
|
|
||||||
|
if (orgId) {
|
||||||
|
attributes["infisical.organization.id"] = orgId;
|
||||||
|
}
|
||||||
|
if (orgName) {
|
||||||
|
attributes["infisical.organization.name"] = orgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAuthInfo) {
|
||||||
|
if (userAuthInfo.userId) {
|
||||||
|
attributes["infisical.user.id"] = userAuthInfo.userId;
|
||||||
|
}
|
||||||
|
if (userAuthInfo.email) {
|
||||||
|
attributes["infisical.user.email"] = userAuthInfo.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityAuthInfo) {
|
||||||
|
if (identityAuthInfo.identityId) {
|
||||||
|
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
|
||||||
|
}
|
||||||
|
if (identityAuthInfo.identityName) {
|
||||||
|
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
|
||||||
|
}
|
||||||
|
if (identityAuthInfo.authMethod) {
|
||||||
|
attributes["infisical.auth.method"] = identityAuthInfo.authMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectDetails) {
|
||||||
|
if (projectDetails.id) {
|
||||||
|
attributes["infisical.project.id"] = projectDetails.id;
|
||||||
|
}
|
||||||
|
if (projectDetails.name) {
|
||||||
|
attributes["infisical.project.name"] = projectDetails.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAgent) {
|
||||||
|
attributes["user_agent.original"] = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ip) {
|
||||||
|
attributes["client.address"] = ip;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestCounter.add(1, attributes);
|
||||||
|
requestDurationHistogram.record(reply.elapsedTime / 1000, attributes);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { requestContext } from "@fastify/request-context";
|
import { requestContext, RequestContextData } from "@fastify/request-context";
|
||||||
import { FastifyRequest } from "fastify";
|
import { FastifyRequest } from "fastify";
|
||||||
import fp from "fastify-plugin";
|
import fp from "fastify-plugin";
|
||||||
import type { JwtPayload } from "jsonwebtoken";
|
import type { JwtPayload } from "jsonwebtoken";
|
||||||
@@ -159,10 +159,11 @@ export const injectIdentity = fp(
|
|||||||
|
|
||||||
switch (authMode) {
|
switch (authMode) {
|
||||||
case AuthMode.JWT: {
|
case AuthMode.JWT: {
|
||||||
const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } =
|
const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } =
|
||||||
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
|
await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector);
|
||||||
requestContext.set("orgId", orgId);
|
requestContext.set("orgId", orgId);
|
||||||
|
requestContext.set("orgName", orgName);
|
||||||
|
requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" });
|
||||||
req.auth = {
|
req.auth = {
|
||||||
authMode: AuthMode.JWT,
|
authMode: AuthMode.JWT,
|
||||||
user,
|
user,
|
||||||
@@ -186,6 +187,7 @@ export const injectIdentity = fp(
|
|||||||
);
|
);
|
||||||
const serverCfg = await getServerCfg();
|
const serverCfg = await getServerCfg();
|
||||||
requestContext.set("orgId", identity.orgId);
|
requestContext.set("orgId", identity.orgId);
|
||||||
|
requestContext.set("orgName", identity.orgName);
|
||||||
req.auth = {
|
req.auth = {
|
||||||
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
authMode: AuthMode.IDENTITY_ACCESS_TOKEN,
|
||||||
actor,
|
actor,
|
||||||
@@ -198,24 +200,23 @@ export const injectIdentity = fp(
|
|||||||
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
|
isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId),
|
||||||
token
|
token
|
||||||
};
|
};
|
||||||
if (token?.identityAuth?.oidc) {
|
const identityAuthInfo: RequestContextData["identityAuthInfo"] = {
|
||||||
requestContext.set("identityAuthInfo", {
|
|
||||||
identityId: identity.identityId,
|
identityId: identity.identityId,
|
||||||
oidc: token?.identityAuth?.oidc
|
identityName: identity.name,
|
||||||
});
|
authMethod: identity.authMethod
|
||||||
|
};
|
||||||
|
|
||||||
|
if (token?.identityAuth?.oidc) {
|
||||||
|
identityAuthInfo.oidc = token?.identityAuth?.oidc;
|
||||||
}
|
}
|
||||||
if (token?.identityAuth?.kubernetes) {
|
if (token?.identityAuth?.kubernetes) {
|
||||||
requestContext.set("identityAuthInfo", {
|
identityAuthInfo.kubernetes = token?.identityAuth?.kubernetes;
|
||||||
identityId: identity.identityId,
|
|
||||||
kubernetes: token?.identityAuth?.kubernetes
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (token?.identityAuth?.aws) {
|
if (token?.identityAuth?.aws) {
|
||||||
requestContext.set("identityAuthInfo", {
|
identityAuthInfo.aws = token?.identityAuth?.aws;
|
||||||
identityId: identity.identityId,
|
|
||||||
aws: token?.identityAuth?.aws
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
requestContext.set("identityAuthInfo", identityAuthInfo);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AuthMode.SERVICE_TOKEN: {
|
case AuthMode.SERVICE_TOKEN: {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError, PureAbility } from "@casl/ability";
|
import { ForbiddenError, PureAbility } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import opentelemetry from "@opentelemetry/api";
|
import opentelemetry from "@opentelemetry/api";
|
||||||
import fastifyPlugin from "fastify-plugin";
|
import fastifyPlugin from "fastify-plugin";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
@@ -47,6 +48,12 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
unit: "1"
|
unit: "1"
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const infisicalMeter = opentelemetry.metrics.getMeter("Infisical");
|
||||||
|
const errorCounter = infisicalMeter.createCounter("infisical.http.server.error.count", {
|
||||||
|
description: "Total number of API errors in Infisical (covers both human users and machine identities)",
|
||||||
|
unit: "{error}"
|
||||||
|
});
|
||||||
|
|
||||||
server.setErrorHandler((error, req, res) => {
|
server.setErrorHandler((error, req, res) => {
|
||||||
req.log.error(error);
|
req.log.error(error);
|
||||||
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
@@ -61,6 +68,67 @@ export const fastifyErrHandler = fastifyPlugin(async (server: FastifyZodProvider
|
|||||||
type: errorType,
|
type: errorType,
|
||||||
name: error.name
|
name: error.name
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const orgId = requestContext.get("orgId");
|
||||||
|
const orgName = requestContext.get("orgName");
|
||||||
|
const userAuthInfo = requestContext.get("userAuthInfo");
|
||||||
|
const identityAuthInfo = requestContext.get("identityAuthInfo");
|
||||||
|
const projectDetails = requestContext.get("projectDetails");
|
||||||
|
|
||||||
|
const attributes: Record<string, string | number> = {
|
||||||
|
"http.request.method": method,
|
||||||
|
"http.route": route,
|
||||||
|
"error.type": errorType,
|
||||||
|
"error.name": error.name
|
||||||
|
};
|
||||||
|
|
||||||
|
if (orgId) {
|
||||||
|
attributes["infisical.organization.id"] = orgId;
|
||||||
|
}
|
||||||
|
if (orgName) {
|
||||||
|
attributes["infisical.organization.name"] = orgName;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (userAuthInfo) {
|
||||||
|
if (userAuthInfo.userId) {
|
||||||
|
attributes["infisical.user.id"] = userAuthInfo.userId;
|
||||||
|
}
|
||||||
|
if (userAuthInfo.email) {
|
||||||
|
attributes["infisical.user.email"] = userAuthInfo.email;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (identityAuthInfo) {
|
||||||
|
if (identityAuthInfo.identityId) {
|
||||||
|
attributes["infisical.identity.id"] = identityAuthInfo.identityId;
|
||||||
|
}
|
||||||
|
if (identityAuthInfo.identityName) {
|
||||||
|
attributes["infisical.identity.name"] = identityAuthInfo.identityName;
|
||||||
|
}
|
||||||
|
if (identityAuthInfo.authMethod) {
|
||||||
|
attributes["infisical.auth.method"] = identityAuthInfo.authMethod;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (projectDetails) {
|
||||||
|
if (projectDetails.id) {
|
||||||
|
attributes["infisical.project.id"] = projectDetails.id;
|
||||||
|
}
|
||||||
|
if (projectDetails.name) {
|
||||||
|
attributes["infisical.project.name"] = projectDetails.name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const userAgent = req.headers["user-agent"];
|
||||||
|
if (userAgent) {
|
||||||
|
attributes["user_agent.original"] = userAgent;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.realIp) {
|
||||||
|
attributes["client.address"] = req.realIp;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorCounter.add(1, attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof BadRequestError) {
|
if (error instanceof BadRequestError) {
|
||||||
|
|||||||
@@ -1705,7 +1705,8 @@ export const registerRoutes = async (
|
|||||||
licenseService,
|
licenseService,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService,
|
kmsService,
|
||||||
membershipIdentityDAL
|
membershipIdentityDAL,
|
||||||
|
orgDAL
|
||||||
});
|
});
|
||||||
|
|
||||||
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
const identityAwsAuthService = identityAwsAuthServiceFactory({
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
// All the any rules are disabled because passport typesense with fastify is really poor
|
// All the any rules are disabled because passport typesense with fastify is really poor
|
||||||
|
|
||||||
import { Authenticator } from "@fastify/passport";
|
import { Authenticator } from "@fastify/passport";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import fastifySession from "@fastify/session";
|
import fastifySession from "@fastify/session";
|
||||||
import RedisStore from "connect-redis";
|
import RedisStore from "connect-redis";
|
||||||
import { CronJob } from "cron";
|
import { CronJob } from "cron";
|
||||||
@@ -21,6 +22,7 @@ import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { ms } from "@app/lib/ms";
|
import { ms } from "@app/lib/ms";
|
||||||
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
|
import { fetchGithubEmails, fetchGithubUser } from "@app/lib/requests/github";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
import { authRateLimit } from "@app/server/config/rateLimiter";
|
import { authRateLimit } from "@app/server/config/rateLimiter";
|
||||||
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
|
import { addAuthOriginDomainCookie } from "@app/server/lib/cookie";
|
||||||
import { AuthMethod } from "@app/services/auth/auth-type";
|
import { AuthMethod } from "@app/services/auth/auth-type";
|
||||||
@@ -51,7 +53,6 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req, _accessToken, _refreshToken, profile, cb) => {
|
async (req, _accessToken, _refreshToken, profile, cb) => {
|
||||||
try {
|
|
||||||
// @ts-expect-error this is because this is express type and not fastify
|
// @ts-expect-error this is because this is express type and not fastify
|
||||||
const callbackPort = req.session.get("callbackPort");
|
const callbackPort = req.session.get("callbackPort");
|
||||||
// @ts-expect-error this is because this is express type and not fastify
|
// @ts-expect-error this is because this is express type and not fastify
|
||||||
@@ -64,7 +65,9 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
name: "OauthGoogleRegister"
|
name: "OauthGoogleRegister"
|
||||||
});
|
});
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
try {
|
||||||
|
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||||
|
await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile?.name?.givenName || "",
|
firstName: profile?.name?.givenName || "",
|
||||||
lastName: profile?.name?.familyName || "",
|
lastName: profile?.name?.familyName || "",
|
||||||
@@ -72,9 +75,32 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
callbackPort,
|
callbackPort,
|
||||||
orgSlug
|
orgSlug
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.user.id": user.id,
|
||||||
|
"infisical.organization.id": orgId,
|
||||||
|
"infisical.organization.name": orgName,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
cb(null, { isUserCompleted, providerAuthToken });
|
cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.GOOGLE,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
cb(error as Error, false);
|
cb(error as Error, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,27 +127,50 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
|
async (req: any, accessToken: string, _refreshToken: string, _profile: any, done: Function) => {
|
||||||
try {
|
|
||||||
const ghEmails = await fetchGithubEmails(accessToken);
|
const ghEmails = await fetchGithubEmails(accessToken);
|
||||||
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
const { email } = ghEmails.filter((gitHubEmail) => gitHubEmail.primary)[0];
|
||||||
|
|
||||||
if (!email) throw new Error("No primary email found");
|
if (!email) throw new Error("No primary email found");
|
||||||
|
|
||||||
|
try {
|
||||||
// profile does not get automatically populated so we need to manually fetch user info
|
// profile does not get automatically populated so we need to manually fetch user info
|
||||||
const user = await fetchGithubUser(accessToken);
|
const githubUser = await fetchGithubUser(accessToken);
|
||||||
|
|
||||||
const callbackPort = req.session.get("callbackPort");
|
const callbackPort = req.session.get("callbackPort");
|
||||||
|
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||||
|
await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: user.name || user.login,
|
firstName: githubUser.name || githubUser.login,
|
||||||
lastName: "",
|
lastName: "",
|
||||||
authMethod: AuthMethod.GITHUB,
|
authMethod: AuthMethod.GITHUB,
|
||||||
callbackPort
|
callbackPort
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.user.id": user.id,
|
||||||
|
"infisical.organization.id": orgId,
|
||||||
|
"infisical.organization.name": orgName,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
done(null, { isUserCompleted, providerAuthToken, externalProviderAccessToken: accessToken });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.GITHUB,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
logger.error(err);
|
logger.error(err);
|
||||||
done(err as Error, false);
|
done(err as Error, false);
|
||||||
}
|
}
|
||||||
@@ -147,11 +196,13 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
pkce: true
|
pkce: true
|
||||||
},
|
},
|
||||||
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
async (req: any, _accessToken: string, _refreshToken: string, profile: any, cb: any) => {
|
||||||
|
const email = profile.emails[0].value;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const callbackPort = req.session.get("callbackPort");
|
const callbackPort = req.session.get("callbackPort");
|
||||||
|
|
||||||
const email = profile.emails[0].value;
|
const { isUserCompleted, providerAuthToken, user, orgId, orgName } =
|
||||||
const { isUserCompleted, providerAuthToken } = await server.services.login.oauth2Login({
|
await server.services.login.oauth2Login({
|
||||||
email,
|
email,
|
||||||
firstName: profile.displayName || profile.username || "",
|
firstName: profile.displayName || profile.username || "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
@@ -159,8 +210,31 @@ export const registerOauthMiddlewares = (server: FastifyZodProvider) => {
|
|||||||
callbackPort
|
callbackPort
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.user.id": user.id,
|
||||||
|
"infisical.organization.id": orgId,
|
||||||
|
"infisical.organization.name": orgName,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return cb(null, { isUserCompleted, providerAuthToken });
|
return cb(null, { isUserCompleted, providerAuthToken });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.GITLAB,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
cb(error as Error, false);
|
cb(error as Error, false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
|
|||||||
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
|
if (!user || !user.isAccepted) throw new NotFoundError({ message: `User with ID '${session.userId}' not found` });
|
||||||
|
|
||||||
let orgId = "";
|
let orgId = "";
|
||||||
|
let orgName = "";
|
||||||
let rootOrgId = "";
|
let rootOrgId = "";
|
||||||
let parentOrgId = "";
|
let parentOrgId = "";
|
||||||
if (token.organizationId) {
|
if (token.organizationId) {
|
||||||
@@ -235,9 +236,11 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
|
|||||||
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
|
throw new ForbiddenRequestError({ message: "User organization membership is inactive" });
|
||||||
}
|
}
|
||||||
orgId = subOrganization.id;
|
orgId = subOrganization.id;
|
||||||
|
orgName = subOrganization.name;
|
||||||
rootOrgId = token.organizationId;
|
rootOrgId = token.organizationId;
|
||||||
parentOrgId = subOrganization.parentOrgId as string;
|
parentOrgId = subOrganization.parentOrgId as string;
|
||||||
} else {
|
} else {
|
||||||
|
const organization = await orgDAL.findOne({ id: token.organizationId });
|
||||||
const orgMembership = await membershipUserDAL.findOne({
|
const orgMembership = await membershipUserDAL.findOne({
|
||||||
actorUserId: user.id,
|
actorUserId: user.id,
|
||||||
scopeOrgId: token.organizationId,
|
scopeOrgId: token.organizationId,
|
||||||
@@ -253,12 +256,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD
|
|||||||
}
|
}
|
||||||
|
|
||||||
orgId = token.organizationId;
|
orgId = token.organizationId;
|
||||||
|
orgName = organization.name;
|
||||||
rootOrgId = token.organizationId;
|
rootOrgId = token.organizationId;
|
||||||
parentOrgId = token.organizationId;
|
parentOrgId = token.organizationId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId };
|
return { user, tokenVersionId: token.tokenVersionId, orgId, orgName, rootOrgId, parentOrgId };
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { getUserPrivateKey } from "@app/lib/crypto/srp";
|
|||||||
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError, ForbiddenRequestError, UnauthorizedError } from "@app/lib/errors";
|
||||||
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
|
import { getMinExpiresIn, removeTrailingSlash } from "@app/lib/fn";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
import { getUserAgentType } from "@app/server/plugins/audit-log";
|
||||||
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
import { getServerCfg } from "@app/services/super-admin/super-admin-service";
|
||||||
|
|
||||||
@@ -385,6 +386,9 @@ export const authLoginServiceFactory = ({
|
|||||||
providerAuthToken?: string;
|
providerAuthToken?: string;
|
||||||
captchaToken?: string;
|
captchaToken?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
|
|
||||||
|
try {
|
||||||
const usersByUsername = await userDAL.findUserEncKeyByUsername({
|
const usersByUsername = await userDAL.findUserEncKeyByUsername({
|
||||||
username: email
|
username: email
|
||||||
});
|
});
|
||||||
@@ -394,7 +398,10 @@ export const authLoginServiceFactory = ({
|
|||||||
if (!userEnc) throw new BadRequestError({ message: "User not found" });
|
if (!userEnc) throw new BadRequestError({ message: "User not found" });
|
||||||
|
|
||||||
if (userEnc.encryptionVersion !== UserEncryption.V2) {
|
if (userEnc.encryptionVersion !== UserEncryption.V2) {
|
||||||
throw new BadRequestError({ message: "Legacy encryption scheme not supported", name: "LegacyEncryptionScheme" });
|
throw new BadRequestError({
|
||||||
|
message: "Legacy encryption scheme not supported",
|
||||||
|
name: "LegacyEncryptionScheme"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!userEnc.hashedPassword) {
|
if (!userEnc.hashedPassword) {
|
||||||
@@ -435,6 +442,18 @@ export const authLoginServiceFactory = ({
|
|||||||
organizationId
|
organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.organization.id": organizationId,
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.user.id": userEnc.userId,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": ip,
|
||||||
|
"user_agent.original": userAgent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
tokens: {
|
tokens: {
|
||||||
accessToken: token.access,
|
accessToken: token.access,
|
||||||
@@ -442,6 +461,19 @@ export const authLoginServiceFactory = ({
|
|||||||
},
|
},
|
||||||
user: userEnc
|
user: userEnc
|
||||||
} as const;
|
} as const;
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.user.email": email,
|
||||||
|
"infisical.auth.method": AuthAttemptAuthMethod.EMAIL,
|
||||||
|
"infisical.auth.result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": ip,
|
||||||
|
"user_agent.original": userAgent
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectOrganization = async ({
|
const selectOrganization = async ({
|
||||||
@@ -965,7 +997,8 @@ export const authLoginServiceFactory = ({
|
|||||||
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
expiresIn: appCfg.JWT_PROVIDER_AUTH_LIFETIME
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
return { isUserCompleted, providerAuthToken };
|
|
||||||
|
return { isUserCompleted, providerAuthToken, user, orgId, orgName };
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
let orgId = "";
|
let orgId = "";
|
||||||
|
let orgName = "";
|
||||||
let parentOrgId = "";
|
let parentOrgId = "";
|
||||||
const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId });
|
const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId });
|
||||||
const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id;
|
const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id;
|
||||||
@@ -229,8 +230,12 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Identity does not belong to any organization" });
|
throw new BadRequestError({ message: "Identity does not belong to any organization" });
|
||||||
}
|
}
|
||||||
orgId = subOrganization.id;
|
orgId = subOrganization.id;
|
||||||
|
orgName = subOrganization.name;
|
||||||
|
|
||||||
parentOrgId = subOrganization.parentOrgId as string;
|
parentOrgId = subOrganization.parentOrgId as string;
|
||||||
} else {
|
} else {
|
||||||
|
const organization = await orgDAL.findOne({ id: rootOrgId });
|
||||||
|
|
||||||
const identityOrgMembership = await membershipIdentityDAL.findOne({
|
const identityOrgMembership = await membershipIdentityDAL.findOne({
|
||||||
scope: AccessScope.Organization,
|
scope: AccessScope.Organization,
|
||||||
actorIdentityId: identityAccessToken.identityId,
|
actorIdentityId: identityAccessToken.identityId,
|
||||||
@@ -242,6 +247,7 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
orgId = rootOrgId;
|
orgId = rootOrgId;
|
||||||
|
orgName = organization.name;
|
||||||
parentOrgId = rootOrgId;
|
parentOrgId = rootOrgId;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -253,7 +259,7 @@ export const identityAccessTokenServiceFactory = ({
|
|||||||
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
|
await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses });
|
||||||
|
|
||||||
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
|
await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1);
|
||||||
return { ...identityAccessToken, orgId, rootOrgId, parentOrgId };
|
return { ...identityAccessToken, orgId, rootOrgId, parentOrgId, orgName };
|
||||||
};
|
};
|
||||||
|
|
||||||
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };
|
return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -65,6 +67,7 @@ export const identityAliCloudAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityAliCloudAuthServiceFactoryDep) => {
|
}: TIdentityAliCloudAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
|
const login = async ({ identityId, ...params }: TLoginAliCloudAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
|
const identityAliCloudAuth = await identityAliCloudAuthDAL.findOne({ identityId });
|
||||||
if (!identityAliCloudAuth) {
|
if (!identityAliCloudAuth) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -75,6 +78,9 @@ export const identityAliCloudAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityAliCloudAuth.identityId);
|
const identity = await identityDAL.findById(identityAliCloudAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
|
||||||
|
try {
|
||||||
const requestUrl = new URL("https://sts.aliyuncs.com");
|
const requestUrl = new URL("https://sts.aliyuncs.com");
|
||||||
|
|
||||||
for (const key of Object.keys(params)) {
|
for (const key of Object.keys(params)) {
|
||||||
@@ -121,7 +127,6 @@ export const identityAliCloudAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityAliCloudAuth.identityId,
|
identityId: identityAliCloudAuth.identityId,
|
||||||
@@ -136,12 +141,40 @@ export const identityAliCloudAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityAliCloudAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityAliCloudAuth,
|
identityAliCloudAuth,
|
||||||
accessToken,
|
accessToken,
|
||||||
identityAccessToken,
|
identityAccessToken,
|
||||||
identity
|
identity
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityAliCloudAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.ALICLOUD_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachAliCloudAuth = async ({
|
const attachAliCloudAuth = async ({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -98,6 +100,7 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityAwsAuthServiceFactoryDep) => {
|
}: TIdentityAwsAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
|
const login = async ({ identityId, iamHttpRequestMethod, iamRequestBody, iamRequestHeaders }: TLoginAwsAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
|
const identityAwsAuth = await identityAwsAuthDAL.findOne({ identityId });
|
||||||
if (!identityAwsAuth) {
|
if (!identityAwsAuth) {
|
||||||
throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" });
|
throw new NotFoundError({ message: "AWS auth method not found for identity, did you configure AWS auth?" });
|
||||||
@@ -106,6 +109,8 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityAwsAuth.identityId);
|
const identity = await identityDAL.findById(identityAwsAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
try {
|
||||||
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
|
const headers: TAwsGetCallerIdentityHeaders = JSON.parse(Buffer.from(iamRequestHeaders, "base64").toString());
|
||||||
const body: string = Buffer.from(iamRequestBody, "base64").toString();
|
const body: string = Buffer.from(iamRequestBody, "base64").toString();
|
||||||
|
|
||||||
@@ -196,7 +201,6 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const splitArn = extractPrincipalArnEntity(Arn);
|
const splitArn = extractPrincipalArnEntity(Arn);
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
@@ -226,7 +230,35 @@ export const identityAwsAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityAwsAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { accessToken, identityAwsAuth, identityAccessToken, identity };
|
return { accessToken, identityAwsAuth, identityAccessToken, identity };
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityAwsAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.AWS_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachAwsAuth = async ({
|
const attachAwsAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -61,6 +63,7 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityAzureAuthServiceFactoryDep) => {
|
}: TIdentityAzureAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
|
const login = async ({ identityId, jwt: azureJwt }: TLoginAzureAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
const identityAzureAuth = await identityAzureAuthDAL.findOne({ identityId });
|
||||||
if (!identityAzureAuth) {
|
if (!identityAzureAuth) {
|
||||||
throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" });
|
throw new NotFoundError({ message: "Azure auth method not found for identity, did you configure Azure Auth?" });
|
||||||
@@ -69,6 +72,9 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityAzureAuth.identityId);
|
const identity = await identityDAL.findById(identityAzureAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
|
||||||
|
try {
|
||||||
const azureIdentity = await validateAzureIdentity({
|
const azureIdentity = await validateAzureIdentity({
|
||||||
tenantId: identityAzureAuth.tenantId,
|
tenantId: identityAzureAuth.tenantId,
|
||||||
resource: identityAzureAuth.resource,
|
resource: identityAzureAuth.resource,
|
||||||
@@ -115,7 +121,6 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityAzureAuth.identityId,
|
identityId: identityAzureAuth.identityId,
|
||||||
@@ -131,7 +136,35 @@ export const identityAzureAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityAzureAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { accessToken, identityAzureAuth, identityAccessToken, identity };
|
return { accessToken, identityAzureAuth, identityAccessToken, identity };
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityAzureAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.AZURE_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachAzureAuth = async ({
|
const attachAzureAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -18,6 +19,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -59,6 +61,7 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityGcpAuthServiceFactoryDep) => {
|
}: TIdentityGcpAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
|
const login = async ({ identityId, jwt: gcpJwt }: TLoginGcpAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
const identityGcpAuth = await identityGcpAuthDAL.findOne({ identityId });
|
||||||
if (!identityGcpAuth) {
|
if (!identityGcpAuth) {
|
||||||
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" });
|
throw new NotFoundError({ message: "GCP auth method not found for identity, did you configure GCP auth?" });
|
||||||
@@ -67,6 +70,8 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityGcpAuth.identityId);
|
const identity = await identityDAL.findById(identityGcpAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
try {
|
||||||
let gcpIdentityDetails: TGcpIdentityDetails;
|
let gcpIdentityDetails: TGcpIdentityDetails;
|
||||||
switch (identityGcpAuth.type) {
|
switch (identityGcpAuth.type) {
|
||||||
case "gce": {
|
case "gce": {
|
||||||
@@ -102,7 +107,11 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (identityGcpAuth.type === "gce" && identityGcpAuth.allowedProjects && gcpIdentityDetails.computeEngineDetails) {
|
if (
|
||||||
|
identityGcpAuth.type === "gce" &&
|
||||||
|
identityGcpAuth.allowedProjects &&
|
||||||
|
gcpIdentityDetails.computeEngineDetails
|
||||||
|
) {
|
||||||
// validate if the project that the service account belongs to is in the list of allowed projects
|
// validate if the project that the service account belongs to is in the list of allowed projects
|
||||||
|
|
||||||
const isProjectAllowed = identityGcpAuth.allowedProjects
|
const isProjectAllowed = identityGcpAuth.allowedProjects
|
||||||
@@ -151,8 +160,6 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
);
|
);
|
||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityGcpAuth.identityId,
|
identityId: identityGcpAuth.identityId,
|
||||||
@@ -168,7 +175,35 @@ export const identityGcpAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityGcpAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { accessToken, identityGcpAuth, identityAccessToken, identity };
|
return { accessToken, identityGcpAuth, identityAccessToken, identity };
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityGcpAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.GCP_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachGcpAuth = async ({
|
const attachGcpAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import { JwksClient } from "jwks-rsa";
|
import { JwksClient } from "jwks-rsa";
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
import { getValueByDot } from "@app/lib/template/dot-access";
|
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
@@ -67,6 +69,7 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityJwtAuthServiceFactoryDep) => {
|
}: TIdentityJwtAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
|
const login = async ({ identityId, jwt: jwtValue }: TLoginJwtAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
const identityJwtAuth = await identityJwtAuthDAL.findOne({ identityId });
|
||||||
if (!identityJwtAuth) {
|
if (!identityJwtAuth) {
|
||||||
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
|
throw new NotFoundError({ message: "JWT auth method not found for identity, did you configure JWT auth?" });
|
||||||
@@ -75,6 +78,8 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityJwtAuth.identityId);
|
const identity = await identityDAL.findById(identityJwtAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
try {
|
||||||
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor: orgDataKeyDecryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
orgId: identity.orgId
|
orgId: identity.orgId
|
||||||
@@ -228,7 +233,6 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityJwtAuth.identityId,
|
identityId: identityJwtAuth.identityId,
|
||||||
@@ -244,7 +248,35 @@ export const identityJwtAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityJwtAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { accessToken, identityJwtAuth, identityAccessToken, identity };
|
return { accessToken, identityJwtAuth, identityAccessToken, identity };
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityJwtAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.JWT_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachJwtAuth = async ({
|
const attachJwtAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import axios, { AxiosError } from "axios";
|
import axios, { AxiosError } from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
@@ -37,6 +38,7 @@ import { GatewayHttpProxyActions, GatewayProxyProtocol, withGatewayProxy } from
|
|||||||
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
import { withGatewayV2Proxy } from "@app/lib/gateway-v2/gateway-v2";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -182,6 +184,7 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
|
const login = async ({ identityId, jwt: serviceAccountJwt }: TLoginKubernetesAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
const identityKubernetesAuth = await identityKubernetesAuthDAL.findOne({ identityId });
|
||||||
if (!identityKubernetesAuth) {
|
if (!identityKubernetesAuth) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -192,6 +195,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityKubernetesAuth.identityId);
|
const identity = await identityDAL.findById(identityKubernetesAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
|
||||||
|
try {
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
orgId: identity.orgId
|
orgId: identity.orgId
|
||||||
@@ -242,7 +248,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
kind: "TokenReview",
|
kind: "TokenReview",
|
||||||
spec: {
|
spec: {
|
||||||
token: serviceAccountJwt,
|
token: serviceAccountJwt,
|
||||||
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
|
...(identityKubernetesAuth.allowedAudience
|
||||||
|
? { audiences: [identityKubernetesAuth.allowedAudience] }
|
||||||
|
: {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -295,7 +303,9 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
kind: "TokenReview",
|
kind: "TokenReview",
|
||||||
spec: {
|
spec: {
|
||||||
token: serviceAccountJwt,
|
token: serviceAccountJwt,
|
||||||
...(identityKubernetesAuth.allowedAudience ? { audiences: [identityKubernetesAuth.allowedAudience] } : {})
|
...(identityKubernetesAuth.allowedAudience
|
||||||
|
? { audiences: [identityKubernetesAuth.allowedAudience] }
|
||||||
|
: {})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -457,7 +467,6 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityKubernetesAuth.identityId,
|
identityId: identityKubernetesAuth.identityId,
|
||||||
@@ -479,7 +488,35 @@ export const identityKubernetesAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityKubernetesAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { accessToken, identityKubernetesAuth, identityAccessToken, identity };
|
return { accessToken, identityKubernetesAuth, identityAccessToken, identity };
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityKubernetesAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.KUBERNETES_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachKubernetesAuth = async ({
|
const attachKubernetesAuth = async ({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import slugify from "@sindresorhus/slugify";
|
import slugify from "@sindresorhus/slugify";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
@@ -29,6 +30,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -151,6 +153,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const login = async ({ identityId }: TLoginLdapAuthDTO) => {
|
const login = async ({ identityId }: TLoginLdapAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
const identityLdapAuth = await identityLdapAuthDAL.findOne({ identityId });
|
||||||
|
|
||||||
if (!identityLdapAuth) {
|
if (!identityLdapAuth) {
|
||||||
@@ -162,6 +165,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityLdapAuth.identityId);
|
const identity = await identityDAL.findById(identityLdapAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
const plan = await licenseService.getPlan(identity.orgId);
|
const plan = await licenseService.getPlan(identity.orgId);
|
||||||
if (!plan.ldap) {
|
if (!plan.ldap) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -170,6 +174,7 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
|
const identityAccessToken = await identityLdapAuthDAL.transaction(async (tx) => {
|
||||||
await membershipIdentityDAL.update(
|
await membershipIdentityDAL.update(
|
||||||
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
{ scope: AccessScope.Organization, scopeOrgId: identity.orgId, actorIdentityId: identity.id },
|
||||||
@@ -191,7 +196,6 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityLdapAuth.identityId,
|
identityId: identityLdapAuth.identityId,
|
||||||
@@ -207,7 +211,35 @@ export const identityLdapAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityLdapAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { accessToken, identityLdapAuth, identityAccessToken, identity };
|
return { accessToken, identityLdapAuth, identityAccessToken, identity };
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityLdapAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.LDAP_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachLdapAuth = async ({
|
const attachLdapAuth = async ({
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import RE2 from "re2";
|
import RE2 from "re2";
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -63,6 +65,7 @@ export const identityOciAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityOciAuthServiceFactoryDep) => {
|
}: TIdentityOciAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
|
const login = async ({ identityId, headers, userOcid }: TLoginOciAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
|
const identityOciAuth = await identityOciAuthDAL.findOne({ identityId });
|
||||||
if (!identityOciAuth) {
|
if (!identityOciAuth) {
|
||||||
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
|
throw new NotFoundError({ message: "OCI auth method not found for identity, did you configure OCI auth?" });
|
||||||
@@ -71,6 +74,8 @@ export const identityOciAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityOciAuth.identityId);
|
const identity = await identityDAL.findById(identityOciAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
try {
|
||||||
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
|
// Validate OCI host format. Ensures that the host is in "identity.<region>.oraclecloud.com" format.
|
||||||
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
|
if (!headers.host || !new RE2("^identity\\.([a-z]{2}-[a-z]+-[1-9])\\.oraclecloud\\.com$").test(headers.host)) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
@@ -124,7 +129,6 @@ export const identityOciAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityOciAuth.identityId,
|
identityId: identityOciAuth.identityId,
|
||||||
@@ -139,12 +143,40 @@ export const identityOciAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityOciAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityOciAuth,
|
identityOciAuth,
|
||||||
accessToken,
|
accessToken,
|
||||||
identityAccessToken,
|
identityAccessToken,
|
||||||
identity
|
identity
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityOciAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.OCI_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachOciAuth = async ({
|
const attachOciAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import https from "https";
|
import https from "https";
|
||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
@@ -22,6 +23,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
import { getValueByDot } from "@app/lib/template/dot-access";
|
import { getValueByDot } from "@app/lib/template/dot-access";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
@@ -67,6 +69,7 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
orgDAL
|
orgDAL
|
||||||
}: TIdentityOidcAuthServiceFactoryDep) => {
|
}: TIdentityOidcAuthServiceFactoryDep) => {
|
||||||
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
|
const login = async ({ identityId, jwt: oidcJwt }: TLoginOidcAuthDTO) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
const identityOidcAuth = await identityOidcAuthDAL.findOne({ identityId });
|
||||||
if (!identityOidcAuth) {
|
if (!identityOidcAuth) {
|
||||||
throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
|
throw new NotFoundError({ message: "OIDC auth method not found for identity, did you configure OIDC auth?" });
|
||||||
@@ -75,6 +78,8 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityOidcAuth.identityId);
|
const identity = await identityDAL.findById(identityOidcAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
try {
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
orgId: identity.orgId
|
orgId: identity.orgId
|
||||||
@@ -198,7 +203,6 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityOidcAuth.identityId,
|
identityId: identityOidcAuth.identityId,
|
||||||
@@ -219,7 +223,35 @@ export const identityOidcAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityOidcAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData };
|
return { accessToken, identityOidcAuth, identityAccessToken, identity, oidcTokenData: tokenData };
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityOidcAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.OIDC_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachOidcAuth = async ({
|
const attachOidcAuth = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -19,6 +20,7 @@ import {
|
|||||||
UnauthorizedError
|
UnauthorizedError
|
||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
import { extractIPDetails, isValidIpOrCidr } from "@app/lib/ip";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -27,6 +29,7 @@ import { TIdentityAccessTokenJwtPayload } from "../identity-access-token/identit
|
|||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
import { TKmsServiceFactory } from "../kms/kms-service";
|
||||||
import { KmsDataKey } from "../kms/kms-types";
|
import { KmsDataKey } from "../kms/kms-types";
|
||||||
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
|
import { TMembershipIdentityDALFactory } from "../membership-identity/membership-identity-dal";
|
||||||
|
import { TOrgDALFactory } from "../org/org-dal";
|
||||||
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
import { validateIdentityUpdateForSuperAdminPrivileges } from "../super-admin/super-admin-fns";
|
||||||
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
|
import { TIdentityTlsCertAuthDALFactory } from "./identity-tls-cert-auth-dal";
|
||||||
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
|
import { TIdentityTlsCertAuthServiceFactory } from "./identity-tls-cert-auth-types";
|
||||||
@@ -42,6 +45,7 @@ type TIdentityTlsCertAuthServiceFactoryDep = {
|
|||||||
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
licenseService: Pick<TLicenseServiceFactory, "getPlan">;
|
||||||
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
permissionService: Pick<TPermissionServiceFactory, "getOrgPermission">;
|
||||||
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
kmsService: Pick<TKmsServiceFactory, "createCipherPairWithDataKey">;
|
||||||
|
orgDAL: Pick<TOrgDALFactory, "findById">;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseSubjectDetails = (data: string) => {
|
const parseSubjectDetails = (data: string) => {
|
||||||
@@ -60,9 +64,11 @@ export const identityTlsCertAuthServiceFactory = ({
|
|||||||
membershipIdentityDAL,
|
membershipIdentityDAL,
|
||||||
licenseService,
|
licenseService,
|
||||||
permissionService,
|
permissionService,
|
||||||
kmsService
|
kmsService,
|
||||||
|
orgDAL
|
||||||
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
|
}: TIdentityTlsCertAuthServiceFactoryDep): TIdentityTlsCertAuthServiceFactory => {
|
||||||
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
|
const login: TIdentityTlsCertAuthServiceFactory["login"] = async ({ identityId, clientCertificate }) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
const identityTlsCertAuth = await identityTlsCertAuthDAL.findOne({ identityId });
|
||||||
if (!identityTlsCertAuth) {
|
if (!identityTlsCertAuth) {
|
||||||
throw new NotFoundError({
|
throw new NotFoundError({
|
||||||
@@ -73,6 +79,9 @@ export const identityTlsCertAuthServiceFactory = ({
|
|||||||
const identity = await identityDAL.findById(identityTlsCertAuth.identityId);
|
const identity = await identityDAL.findById(identityTlsCertAuth.identityId);
|
||||||
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
if (!identity) throw new UnauthorizedError({ message: "Identity not found" });
|
||||||
|
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
|
||||||
|
try {
|
||||||
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
const { decryptor } = await kmsService.createCipherPairWithDataKey({
|
||||||
type: KmsDataKey.Organization,
|
type: KmsDataKey.Organization,
|
||||||
orgId: identity.orgId
|
orgId: identity.orgId
|
||||||
@@ -140,7 +149,6 @@ export const identityTlsCertAuthServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityTlsCertAuth.identityId,
|
identityId: identityTlsCertAuth.identityId,
|
||||||
@@ -155,12 +163,40 @@ export const identityTlsCertAuthServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityTlsCertAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
identityTlsCertAuth,
|
identityTlsCertAuth,
|
||||||
accessToken,
|
accessToken,
|
||||||
identityAccessToken,
|
identityAccessToken,
|
||||||
identity
|
identity
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityTlsCertAuth.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.TLS_CERT_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({
|
const attachTlsCertAuth: TIdentityTlsCertAuthServiceFactory["attachTlsCertAuth"] = async ({
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
import { ForbiddenError } from "@casl/ability";
|
||||||
|
import { requestContext } from "@fastify/request-context";
|
||||||
|
|
||||||
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
import { AccessScope, IdentityAuthMethod, OrganizationActionScope } from "@app/db/schemas";
|
||||||
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
|
||||||
@@ -21,6 +22,7 @@ import {
|
|||||||
} from "@app/lib/errors";
|
} from "@app/lib/errors";
|
||||||
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
import { checkIPAgainstBlocklist, extractIPDetails, isValidIpOrCidr, TIp } from "@app/lib/ip";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
|
import { AuthAttemptAuthMethod, AuthAttemptAuthResult, authAttemptCounter } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
import { ActorType, AuthTokenType } from "../auth/auth-type";
|
||||||
import { TIdentityDALFactory } from "../identity/identity-dal";
|
import { TIdentityDALFactory } from "../identity/identity-dal";
|
||||||
@@ -77,6 +79,7 @@ export const identityUaServiceFactory = ({
|
|||||||
identityDAL
|
identityDAL
|
||||||
}: TIdentityUaServiceFactoryDep) => {
|
}: TIdentityUaServiceFactoryDep) => {
|
||||||
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
const login = async (clientId: string, clientSecret: string, ip: string) => {
|
||||||
|
const appCfg = getConfig();
|
||||||
const identityUa = await identityUaDAL.findOne({ clientId });
|
const identityUa = await identityUaDAL.findOne({ clientId });
|
||||||
if (!identityUa) {
|
if (!identityUa) {
|
||||||
throw new UnauthorizedError({
|
throw new UnauthorizedError({
|
||||||
@@ -84,6 +87,10 @@ export const identityUaServiceFactory = ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const identity = await identityDAL.findById(identityUa.identityId);
|
||||||
|
const org = await orgDAL.findById(identity.orgId);
|
||||||
|
|
||||||
|
try {
|
||||||
checkIPAgainstBlocklist({
|
checkIPAgainstBlocklist({
|
||||||
ipAddress: ip,
|
ipAddress: ip,
|
||||||
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
|
trustedIps: identityUa.clientSecretTrustedIps as TIp[]
|
||||||
@@ -221,7 +228,6 @@ export const identityUaServiceFactory = ({
|
|||||||
accessTokenMaxTTL: 1000000000
|
accessTokenMaxTTL: 1000000000
|
||||||
};
|
};
|
||||||
|
|
||||||
const identity = await identityDAL.findById(identityUa.identityId);
|
|
||||||
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
const identityAccessToken = await identityUaDAL.transaction(async (tx) => {
|
||||||
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
|
const uaClientSecretDoc = await identityUaClientSecretDAL.incrementUsage(validClientSecretInfo!.id, tx);
|
||||||
await membershipIdentityDAL.update(
|
await membershipIdentityDAL.update(
|
||||||
@@ -249,7 +255,6 @@ export const identityUaServiceFactory = ({
|
|||||||
return newToken;
|
return newToken;
|
||||||
});
|
});
|
||||||
|
|
||||||
const appCfg = getConfig();
|
|
||||||
const accessToken = crypto.jwt().sign(
|
const accessToken = crypto.jwt().sign(
|
||||||
{
|
{
|
||||||
identityId: identityUa.identityId,
|
identityId: identityUa.identityId,
|
||||||
@@ -266,6 +271,19 @@ export const identityUaServiceFactory = ({
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityUa.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.SUCCESS,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessToken,
|
accessToken,
|
||||||
identityUa,
|
identityUa,
|
||||||
@@ -274,6 +292,21 @@ export const identityUaServiceFactory = ({
|
|||||||
identity,
|
identity,
|
||||||
...accessTokenTTLParams
|
...accessTokenTTLParams
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
if (appCfg.OTEL_TELEMETRY_COLLECTION_ENABLED) {
|
||||||
|
authAttemptCounter.add(1, {
|
||||||
|
"infisical.identity.id": identityUa.identityId,
|
||||||
|
"infisical.identity.name": identity.name,
|
||||||
|
"infisical.organization.id": org.id,
|
||||||
|
"infisical.organization.name": org.name,
|
||||||
|
"infisical.identity.auth_method": AuthAttemptAuthMethod.UNIVERSAL_AUTH,
|
||||||
|
"infisical.identity.auth_result": AuthAttemptAuthResult.FAILURE,
|
||||||
|
"client.address": requestContext.get("ip"),
|
||||||
|
"user_agent.original": requestContext.get("userAgent")
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const attachUniversalAuth = async ({
|
const attachUniversalAuth = async ({
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import { diff, groupBy } from "@app/lib/fn";
|
|||||||
import { setKnexStringValue } from "@app/lib/knex";
|
import { setKnexStringValue } from "@app/lib/knex";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
import { alphaNumericNanoId } from "@app/lib/nanoid";
|
||||||
|
import { recordSecretReadMetric } from "@app/lib/telemetry/metrics";
|
||||||
|
|
||||||
import { ActorType } from "../auth/auth-type";
|
import { ActorType } from "../auth/auth-type";
|
||||||
import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
import { TCommitResourceChangeDTO, TFolderCommitServiceFactory } from "../folder-commit/folder-commit-service";
|
||||||
@@ -1052,6 +1053,11 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
});
|
});
|
||||||
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
|
throwIfMissingSecretReadValueOrDescribePermission(permission, ProjectPermissionSecretActions.DescribeSecret);
|
||||||
|
|
||||||
|
recordSecretReadMetric({
|
||||||
|
environment,
|
||||||
|
secretPath: path
|
||||||
|
});
|
||||||
|
|
||||||
const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId));
|
const cachedSecretDalVersion = await keyStore.pgGetIntItem(SecretServiceCacheKeys.getSecretDalVersion(projectId));
|
||||||
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
const secretDalVersion = Number(cachedSecretDalVersion || 0);
|
||||||
const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, {
|
const cacheKey = SecretServiceCacheKeys.getSecretsOfServiceLayer(projectId, secretDalVersion, {
|
||||||
@@ -1482,6 +1488,12 @@ export const secretV2BridgeServiceFactory = ({
|
|||||||
secretTags: (secret?.tags || []).map((el) => el.slug)
|
secretTags: (secret?.tags || []).map((el) => el.slug)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
recordSecretReadMetric({
|
||||||
|
environment,
|
||||||
|
secretPath: path,
|
||||||
|
name: secretName
|
||||||
|
});
|
||||||
|
|
||||||
// this will throw if the user doesn't have read value permission no matter what
|
// this will throw if the user doesn't have read value permission no matter what
|
||||||
// because if its an expansion, it will fully depend on the value.
|
// because if its an expansion, it will fully depend on the value.
|
||||||
const { expandSecretReferences } = expandSecretReferencesFactory({
|
const { expandSecretReferences } = expandSecretReferencesFactory({
|
||||||
|
|||||||
@@ -319,80 +319,137 @@ helm install otel-collector open-telemetry/opentelemetry-collector \
|
|||||||
--set config.exporters.prometheus.endpoint=0.0.0.0:8889
|
--set config.exporters.prometheus.endpoint=0.0.0.0:8889
|
||||||
```
|
```
|
||||||
|
|
||||||
## Alternative Backends
|
|
||||||
|
|
||||||
Since Infisical exports in OpenTelemetry format, you can easily configure the collector to send metrics to other backends instead of (or in addition to) Prometheus:
|
|
||||||
|
|
||||||
### Cloud-Native Examples
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# Add to your otel-collector-config.yaml exporters section
|
|
||||||
exporters:
|
|
||||||
# AWS CloudWatch
|
|
||||||
awsemf:
|
|
||||||
region: us-west-2
|
|
||||||
log_group_name: /aws/emf/infisical
|
|
||||||
log_stream_name: metrics
|
|
||||||
|
|
||||||
# Google Cloud Monitoring
|
|
||||||
googlecloud:
|
|
||||||
project_id: your-project-id
|
|
||||||
|
|
||||||
# Azure Monitor
|
|
||||||
azuremonitor:
|
|
||||||
connection_string: "your-connection-string"
|
|
||||||
|
|
||||||
# Datadog
|
|
||||||
datadog:
|
|
||||||
api:
|
|
||||||
key: "your-api-key"
|
|
||||||
site: "datadoghq.com"
|
|
||||||
|
|
||||||
# New Relic
|
|
||||||
newrelic:
|
|
||||||
apikey: "your-api-key"
|
|
||||||
host_override: "otlp.nr-data.net"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-Backend Configuration
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
service:
|
|
||||||
pipelines:
|
|
||||||
metrics:
|
|
||||||
receivers: [otlp]
|
|
||||||
processors: [batch]
|
|
||||||
exporters: [prometheus, awsemf, datadog] # Send to multiple backends
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setting Up Grafana
|
|
||||||
|
|
||||||
1. **Access Grafana**: Navigate to your Grafana instance
|
|
||||||
2. **Login**: Use your configured credentials
|
|
||||||
3. **Add Prometheus Data Source**:
|
|
||||||
- Go to Configuration → Data Sources
|
|
||||||
- Click "Add data source"
|
|
||||||
- Select "Prometheus"
|
|
||||||
- Set URL to your Prometheus endpoint
|
|
||||||
- Click "Save & Test"
|
|
||||||
|
|
||||||
## Available Metrics
|
## Available Metrics
|
||||||
|
|
||||||
Infisical exposes the following key metrics in OpenTelemetry format:
|
Infisical exposes the following key metrics in OpenTelemetry format:
|
||||||
|
|
||||||
### API Performance Metrics
|
### Core API Metrics
|
||||||
|
|
||||||
- `API_latency` - API request latency histogram in milliseconds
|
These metrics track all HTTP API requests to Infisical, including request counts, latency, and errors. Use these to monitor overall API health, identify performance bottlenecks, and track usage patterns across users and machine identities.
|
||||||
|
|
||||||
- **Labels**: `route`, `method`, `statusCode`
|
#### Total API Requests
|
||||||
- **Example**: Monitor response times for specific endpoints
|
|
||||||
|
|
||||||
- `API_errors` - API error count histogram
|
- **Metric Name**: `infisical.http.server.request.count`
|
||||||
- **Labels**: `route`, `method`, `type`, `name`
|
- **Type**: Counter
|
||||||
- **Example**: Track error rates by endpoint and error type
|
- **Unit**: `{request}`
|
||||||
|
- **Description**: Total number of API requests to Infisical (covers both human users and machine identities)
|
||||||
|
- **Attributes**:
|
||||||
|
- `infisical.organization.id` (string): Organization ID
|
||||||
|
- `infisical.organization.name` (string): Organization name (e.g., "Platform Engineering Team")
|
||||||
|
- `infisical.user.id` (string, optional): User ID if human user
|
||||||
|
- `infisical.user.email` (string, optional): User email (e.g., "[email protected]")
|
||||||
|
- `infisical.identity.id` (string, optional): Machine identity ID
|
||||||
|
- `infisical.identity.name` (string, optional): Machine identity name (e.g., "prod-k8s-operator")
|
||||||
|
- `infisical.auth.method` (string, optional): Auth method used
|
||||||
|
- `http.request.method` (string): HTTP method (GET, POST, PUT, DELETE)
|
||||||
|
- `http.route` (string): API endpoint route pattern
|
||||||
|
- `http.response.status_code` (int): HTTP status code
|
||||||
|
- `infisical.project.id` (string, optional): Project ID
|
||||||
|
- `infisical.project.name` (string, optional): Project name
|
||||||
|
- `user_agent.original` (string, optional): User agent string
|
||||||
|
- `client.address` (string, optional): IP address
|
||||||
|
|
||||||
|
#### Request Duration
|
||||||
|
|
||||||
|
- **Metric Name**: `infisical.http.server.request.duration`
|
||||||
|
- **Type**: Histogram
|
||||||
|
- **Unit**: `s` (seconds)
|
||||||
|
- **Description**: API request latency
|
||||||
|
- **Buckets**: [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
||||||
|
- **Attributes**:
|
||||||
|
- `infisical.organization.id` (string): Organization ID
|
||||||
|
- `infisical.organization.name` (string): Organization name
|
||||||
|
- `infisical.user.id` (string, optional): User ID if human user
|
||||||
|
- `infisical.user.email` (string, optional): User email
|
||||||
|
- `infisical.identity.id` (string, optional): Machine identity ID
|
||||||
|
- `infisical.identity.name` (string, optional): Machine identity name
|
||||||
|
- `http.request.method` (string): HTTP method
|
||||||
|
- `http.route` (string): API endpoint route pattern
|
||||||
|
- `http.response.status_code` (int): HTTP status code
|
||||||
|
- `infisical.project.id` (string, optional): Project ID
|
||||||
|
- `infisical.project.name` (string, optional): Project name
|
||||||
|
|
||||||
|
#### API Errors by Actor
|
||||||
|
|
||||||
|
- **Metric Name**: `infisical.http.server.error.count`
|
||||||
|
- **Type**: Counter
|
||||||
|
- **Unit**: `{error}`
|
||||||
|
- **Description**: API errors grouped by actor (for identifying misconfigured services)
|
||||||
|
- **Attributes**:
|
||||||
|
- `infisical.organization.id` (string): Organization ID
|
||||||
|
- `infisical.organization.name` (string): Organization name
|
||||||
|
- `infisical.user.id` (string, optional): User ID if human
|
||||||
|
- `infisical.user.email` (string, optional): User email
|
||||||
|
- `infisical.identity.id` (string, optional): Identity ID if machine
|
||||||
|
- `infisical.identity.name` (string, optional): Identity name
|
||||||
|
- `http.route` (string): API endpoint where error occurred
|
||||||
|
- `http.request.method` (string): HTTP method
|
||||||
|
- `error.type` (string): Error category/type (client_error, server_error, auth_error, rate_limit_error, etc.)
|
||||||
|
- `infisical.project.id` (string, optional): Project ID
|
||||||
|
- `infisical.project.name` (string, optional): Project name
|
||||||
|
- `client.address` (string, optional): IP address
|
||||||
|
- `user_agent.original` (string, optional): User agent information
|
||||||
|
|
||||||
|
### Secret Operations Metrics
|
||||||
|
|
||||||
|
These metrics provide visibility into secret access patterns, helping you understand which secrets are being accessed, by whom, and from where. Essential for security auditing and access pattern analysis.
|
||||||
|
|
||||||
|
#### Secret Read Operations
|
||||||
|
|
||||||
|
- **Metric Name**: `infisical.secret.read.count`
|
||||||
|
- **Type**: Counter
|
||||||
|
- **Unit**: `{operation}`
|
||||||
|
- **Description**: Number of secret read operations
|
||||||
|
- **Attributes**:
|
||||||
|
- `infisical.organization.id` (string): Organization ID
|
||||||
|
- `infisical.organization.name` (string): Organization name
|
||||||
|
- `infisical.project.id` (string): Project ID
|
||||||
|
- `infisical.project.name` (string): Project name (e.g., "payment-service-secrets")
|
||||||
|
- `infisical.environment` (string): Environment (dev, staging, prod)
|
||||||
|
- `infisical.secret.path` (string): Path to secrets (e.g., "/microservice-a/database")
|
||||||
|
- `infisical.secret.name` (string, optional): Name of secret
|
||||||
|
- `infisical.user.id` (string, optional): User ID if human
|
||||||
|
- `infisical.user.email` (string, optional): User email
|
||||||
|
- `infisical.identity.id` (string, optional): Machine identity ID
|
||||||
|
- `infisical.identity.name` (string, optional): Machine identity name
|
||||||
|
- `user_agent.original` (string, optional): User agent/SDK information
|
||||||
|
- `client.address` (string, optional): IP address
|
||||||
|
|
||||||
|
### Authentication Metrics
|
||||||
|
|
||||||
|
These metrics track authentication attempts and outcomes, enabling you to monitor login success rates, detect potential security threats, and identify authentication issues.
|
||||||
|
|
||||||
|
#### Login Attempts
|
||||||
|
|
||||||
|
- **Metric Name**: `infisical.auth.attempt.count`
|
||||||
|
- **Type**: Counter
|
||||||
|
- **Unit**: `{attempt}`
|
||||||
|
- **Description**: Authentication attempts (both successful and failed)
|
||||||
|
- **Attributes**:
|
||||||
|
- `infisical.organization.id` (string): Organization ID
|
||||||
|
- `infisical.organization.name` (string): Organization name
|
||||||
|
- `infisical.user.id` (string, optional): User ID if human (if identifiable)
|
||||||
|
- `infisical.user.email` (string, optional): User email (if identifiable)
|
||||||
|
- `infisical.identity.id` (string, optional): Identity ID if machine (if identifiable)
|
||||||
|
- `infisical.identity.name` (string, optional): Identity name (if identifiable)
|
||||||
|
- `infisical.auth.method` (string): Authentication method attempted
|
||||||
|
- `infisical.auth.result` (string): success or failure
|
||||||
|
- `error.type` (string, optional): Reason for failure if failed (invalid_credentials, expired_token, invalid_token, etc.)
|
||||||
|
- `client.address` (string): IP address
|
||||||
|
- `user_agent.original` (string, optional): User agent/client information
|
||||||
|
- `infisical.auth.attempt.username` (string, optional): Attempted username/email (if available)
|
||||||
|
|
||||||
|
### Legacy Metrics
|
||||||
|
|
||||||
|
These metrics are from the previous instrumentation and may be deprecated in future versions. Consider migrating to the new Core API Metrics for more comprehensive observability.
|
||||||
|
|
||||||
|
- `API_latency` - API request latency histogram in milliseconds (Labels: `route`, `method`, `statusCode`)
|
||||||
|
- `API_errors` - API error count histogram (Labels: `route`, `method`, `type`, `name`)
|
||||||
|
|
||||||
### Integration & Secret Sync Metrics
|
### Integration & Secret Sync Metrics
|
||||||
|
|
||||||
|
These metrics monitor secret synchronization operations between Infisical and external systems, helping you track sync health, identify integration failures, and troubleshoot connectivity issues.
|
||||||
|
|
||||||
- `integration_secret_sync_errors` - Integration secret sync error count
|
- `integration_secret_sync_errors` - Integration secret sync error count
|
||||||
|
|
||||||
- **Labels**: `version`, `integration`, `integrationId`, `type`, `status`, `name`, `projectId`
|
- **Labels**: `version`, `integration`, `integrationId`, `type`, `status`, `name`, `projectId`
|
||||||
@@ -414,16 +471,11 @@ Infisical exposes the following key metrics in OpenTelemetry format:
|
|||||||
|
|
||||||
### System Metrics
|
### System Metrics
|
||||||
|
|
||||||
These metrics are automatically collected by OpenTelemetry's HTTP instrumentation:
|
These low-level HTTP metrics are automatically collected by OpenTelemetry's instrumentation layer, providing baseline performance data for all HTTP traffic.
|
||||||
|
|
||||||
- `http_server_duration` - HTTP server request duration metrics (histogram buckets, count, sum)
|
- `http_server_duration` - HTTP server request duration metrics (histogram buckets, count, sum)
|
||||||
- `http_client_duration` - HTTP client request duration metrics (histogram buckets, count, sum)
|
- `http_client_duration` - HTTP client request duration metrics (histogram buckets, count, sum)
|
||||||
|
|
||||||
### Custom Business Metrics
|
|
||||||
|
|
||||||
- `infisical_secret_operations_total` - Total secret operations
|
|
||||||
- `infisical_secrets_processed_total` - Total secrets processed
|
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Common Issues
|
### Common Issues
|
||||||
|
|||||||
Reference in New Issue
Block a user