diff --git a/backend/src/@types/fastify.d.ts b/backend/src/@types/fastify.d.ts index 273e6d982..fdd3cabc0 100644 --- a/backend/src/@types/fastify.d.ts +++ b/backend/src/@types/fastify.d.ts @@ -136,8 +136,20 @@ declare module "@fastify/request-context" { interface RequestContextData { reqId: string; orgId?: string; + orgName?: string; + userAuthInfo?: { + userId: string; + email: string; + }; + projectDetails?: { + id: string; + name: string; + slug: string; + }; identityAuthInfo?: { identityId: string; + identityName: string; + authMethod: string; oidc?: { claims: Record; }; diff --git a/backend/src/ee/services/permission/permission-service.ts b/backend/src/ee/services/permission/permission-service.ts index 48b78d980..b71e63c10 100644 --- a/backend/src/ee/services/permission/permission-service.ts +++ b/backend/src/ee/services/permission/permission-service.ts @@ -337,6 +337,12 @@ export const permissionServiceFactory = ({ throw new NotFoundError({ message: `Project with ${projectId} not found` }); } + requestContext.set("projectDetails", { + id: projectDetails.id, + name: projectDetails.name, + slug: projectDetails.slug + }); + if (projectDetails.orgId !== actorOrgId) { throw new ForbiddenRequestError({ name: "You are not logged into this organization" }); } diff --git a/backend/src/server/plugins/api-metrics.ts b/backend/src/server/plugins/api-metrics.ts index 2e3a20a23..12bf2578c 100644 --- a/backend/src/server/plugins/api-metrics.ts +++ b/backend/src/server/plugins/api-metrics.ts @@ -1,21 +1,99 @@ +import { requestContext } from "@fastify/request-context"; import opentelemetry from "@opentelemetry/api"; import fp from "fastify-plugin"; export const apiMetrics = fp(async (fastify) => { const apiMeter = opentelemetry.metrics.getMeter("API"); + const latencyHistogram = apiMeter.createHistogram("API_latency", { 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}" + }); + fastify.addHook("onResponse", async (request, reply) => { const { method } = request; const route = request.routerPath; const { statusCode } = reply; + // Record latency latencyHistogram.record(reply.elapsedTime, { route, method, statusCode }); + + // Get context data + 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"); + + // Build attributes object + const attributes: Record = { + "http.request.method": method, + "http.route": route, + "http.response.status_code": statusCode + }; + + // Add organization info + if (orgId) { + attributes["infisical.organization.id"] = orgId; + } + if (orgName) { + attributes["infisical.organization.name"] = orgName; + } + + // Add user info (for human users) + if (userAuthInfo) { + if (userAuthInfo.userId) { + attributes["infisical.user.id"] = userAuthInfo.userId; + } + if (userAuthInfo.email) { + attributes["infisical.user.email"] = userAuthInfo.email; + } + } + + // Add identity info (for machine identities) + 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; + } + } + + // Add project info + if (projectDetails) { + if (projectDetails.id) { + attributes["infisical.project.id"] = projectDetails.id; + } + if (projectDetails.name) { + attributes["infisical.project.name"] = projectDetails.name; + } + } + + // Add user agent + const userAgent = request.headers["user-agent"]; + if (userAgent) { + attributes["user_agent.original"] = userAgent; + } + + // Add client IP address + if (request.realIp) { + attributes["client.address"] = request.realIp; + } + + requestCounter.add(1, attributes); }); }); diff --git a/backend/src/server/plugins/auth/inject-identity.ts b/backend/src/server/plugins/auth/inject-identity.ts index 9de15cd34..31f3139ec 100644 --- a/backend/src/server/plugins/auth/inject-identity.ts +++ b/backend/src/server/plugins/auth/inject-identity.ts @@ -1,4 +1,4 @@ -import { requestContext } from "@fastify/request-context"; +import { requestContext, RequestContextData } from "@fastify/request-context"; import { FastifyRequest } from "fastify"; import fp from "fastify-plugin"; import type { JwtPayload } from "jsonwebtoken"; @@ -159,10 +159,11 @@ export const injectIdentity = fp( switch (authMode) { case AuthMode.JWT: { - const { user, tokenVersionId, orgId, rootOrgId, parentOrgId } = + const { user, tokenVersionId, orgId, orgName, rootOrgId, parentOrgId } = await server.services.authToken.fnValidateJwtIdentity(token, subOrganizationSelector); requestContext.set("orgId", orgId); - + requestContext.set("orgName", orgName); + requestContext.set("userAuthInfo", { userId: user.id, email: user.email || "" }); req.auth = { authMode: AuthMode.JWT, user, @@ -186,6 +187,7 @@ export const injectIdentity = fp( ); const serverCfg = await getServerCfg(); requestContext.set("orgId", identity.orgId); + requestContext.set("orgName", identity.orgName); req.auth = { authMode: AuthMode.IDENTITY_ACCESS_TOKEN, actor, @@ -198,24 +200,23 @@ export const injectIdentity = fp( isInstanceAdmin: serverCfg?.adminIdentityIds?.includes(identity.identityId), token }; + const identityAuthInfo: RequestContextData["identityAuthInfo"] = { + identityId: identity.identityId, + identityName: identity.name, + authMethod: identity.authMethod + }; + if (token?.identityAuth?.oidc) { - requestContext.set("identityAuthInfo", { - identityId: identity.identityId, - oidc: token?.identityAuth?.oidc - }); + identityAuthInfo.oidc = token?.identityAuth?.oidc; } if (token?.identityAuth?.kubernetes) { - requestContext.set("identityAuthInfo", { - identityId: identity.identityId, - kubernetes: token?.identityAuth?.kubernetes - }); + identityAuthInfo.kubernetes = token?.identityAuth?.kubernetes; } if (token?.identityAuth?.aws) { - requestContext.set("identityAuthInfo", { - identityId: identity.identityId, - aws: token?.identityAuth?.aws - }); + identityAuthInfo.aws = token?.identityAuth?.aws; } + + requestContext.set("identityAuthInfo", identityAuthInfo); break; } case AuthMode.SERVICE_TOKEN: { diff --git a/backend/src/services/auth-token/auth-token-service.ts b/backend/src/services/auth-token/auth-token-service.ts index 28a986fe8..fb7213109 100644 --- a/backend/src/services/auth-token/auth-token-service.ts +++ b/backend/src/services/auth-token/auth-token-service.ts @@ -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` }); let orgId = ""; + let orgName = ""; let rootOrgId = ""; let parentOrgId = ""; if (token.organizationId) { @@ -235,9 +236,11 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD throw new ForbiddenRequestError({ message: "User organization membership is inactive" }); } orgId = subOrganization.id; + orgName = subOrganization.name; rootOrgId = token.organizationId; parentOrgId = subOrganization.parentOrgId as string; } else { + const organization = await orgDAL.findOne({ id: token.organizationId }); const orgMembership = await membershipUserDAL.findOne({ actorUserId: user.id, scopeOrgId: token.organizationId, @@ -253,12 +256,13 @@ export const tokenServiceFactory = ({ tokenDAL, userDAL, membershipUserDAL, orgD } orgId = token.organizationId; + orgName = organization.name; rootOrgId = token.organizationId; parentOrgId = token.organizationId; } } - return { user, tokenVersionId: token.tokenVersionId, orgId, rootOrgId, parentOrgId }; + return { user, tokenVersionId: token.tokenVersionId, orgId, orgName, rootOrgId, parentOrgId }; }; return { diff --git a/backend/src/services/identity-access-token/identity-access-token-service.ts b/backend/src/services/identity-access-token/identity-access-token-service.ts index 02660a0ae..3479d929e 100644 --- a/backend/src/services/identity-access-token/identity-access-token-service.ts +++ b/backend/src/services/identity-access-token/identity-access-token-service.ts @@ -210,6 +210,7 @@ export const identityAccessTokenServiceFactory = ({ }); } let orgId = ""; + let orgName = ""; let parentOrgId = ""; const identityOrgDetails = await orgDAL.findOne({ id: identityAccessToken.identityScopeOrgId }); const rootOrgId = identityOrgDetails.rootOrgId || identityOrgDetails.id; @@ -229,8 +230,12 @@ export const identityAccessTokenServiceFactory = ({ throw new BadRequestError({ message: "Identity does not belong to any organization" }); } orgId = subOrganization.id; + orgName = subOrganization.name; + parentOrgId = subOrganization.parentOrgId as string; } else { + const organization = await orgDAL.findOne({ id: rootOrgId }); + const identityOrgMembership = await membershipIdentityDAL.findOne({ scope: AccessScope.Organization, actorIdentityId: identityAccessToken.identityId, @@ -242,6 +247,7 @@ export const identityAccessTokenServiceFactory = ({ } orgId = rootOrgId; + orgName = organization.name; parentOrgId = rootOrgId; } @@ -253,7 +259,7 @@ export const identityAccessTokenServiceFactory = ({ await validateAccessTokenExp({ ...identityAccessToken, accessTokenNumUses }); await accessTokenQueue.updateIdentityAccessTokenStatus(identityAccessToken.id, Number(accessTokenNumUses) + 1); - return { ...identityAccessToken, orgId, rootOrgId, parentOrgId }; + return { ...identityAccessToken, orgId, rootOrgId, parentOrgId, orgName }; }; return { renewAccessToken, revokeAccessToken, fnValidateIdentityAccessToken };