This is an automated email from the ASF dual-hosted git repository. wu-sheng pushed a commit to branch fix/pre-1.0-audit-followups in repository https://gitbox.apache.org/repos/asf/skywalking-horizon-ui.git
commit b3f72aa6b712a7ed10cf7d1509f100fda03e8f14 Author: Wu Sheng <[email protected]> AuthorDate: Sun Jun 28 10:08:21 2026 +0800 fix: UI RBAC verb-matcher parity with the BFF + generic 500 error bodies Two pre-1.0 correctness fixes surfaced by an audit: - The UI auth store's verb matcher diverged from the BFF's rbac/matchOne: it ignored the `admin` sentinel and truncated verbs to two segments. So a custom `admin` grant was hidden in the UI, and a `*:write` grant showed rule:write:structural controls the BFF then denies. Port matchOne exactly (admin sentinel, split(':', 3), sub-action equality) so the advisory UI gate agrees with the enforcing BFF. - The global Fastify error handler returned raw err.message to the client for every non-HttpError 500, which can carry upstream response snippets or endpoint details. Log it server-side and return a generic body + requestId; keep err.message only in NODE_ENV=development. HttpError messages (intentionally client-facing) are unchanged. --- apps/bff/src/server.ts | 14 +++++++++++--- apps/ui/src/state/auth.ts | 15 ++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/apps/bff/src/server.ts b/apps/bff/src/server.ts index eba8ce9..beea85f 100644 --- a/apps/bff/src/server.ts +++ b/apps/bff/src/server.ts @@ -151,13 +151,21 @@ source.onChange((cfg) => { const app = Fastify({ logger: loggerOptions }); -app.setErrorHandler((err, _req, reply) => { +app.setErrorHandler((err, req, reply) => { if (err instanceof HttpError) { return reply.status(err.statusCode).send({ code: err.code, message: err.message, details: err.details }); } - const message = err instanceof Error ? err.message : 'internal error'; + // Never leak an internal / upstream exception message to the client — it can + // carry upstream response snippets or endpoint details. Log it server-side, + // return a generic body plus the request id for correlation; dev keeps the + // raw message for debugging. reply.log.error({ err }, 'unhandled'); - return reply.status(500).send({ code: 'internal_error', message }); + const isDev = process.env.NODE_ENV === 'development'; + return reply.status(500).send({ + code: 'internal_error', + message: isDev && err instanceof Error ? err.message : 'internal error', + requestId: req.id, + }); }); // Baseline security headers on every response (MIME-sniff / clickjacking / diff --git a/apps/ui/src/state/auth.ts b/apps/ui/src/state/auth.ts index 3d5f781..161feb5 100644 --- a/apps/ui/src/state/auth.ts +++ b/apps/ui/src/state/auth.ts @@ -77,14 +77,19 @@ export const useAuthStore = defineStore('auth', () => { user.value = null; } + // Mirrors the BFF's matchOne (apps/bff/src/rbac/verbs.ts) exactly. This UI gate + // is advisory — the BFF enforces — but it must agree, or it hides custom `admin` + // grants and shows three-segment controls (e.g. rule:write:structural) that a + // two-segment grant like `*:write` does not actually carry and the BFF denies. function hasVerb(verb: string): boolean { const grants = user.value?.verbs ?? []; for (const g of grants) { - if (g === '*' || g === verb) return true; - const [ga, gact] = g.split(':', 2); - const [ra, ract] = verb.split(':', 2); - if (gact === '*' && ga === ra) return true; - if (ga === '*' && gact === ract) return true; + if (g === '*' || g === 'admin' || g === verb) return true; + const [ga, gact, gsub] = g.split(':', 3); + const [ra, ract, rsub] = verb.split(':', 3); + if (ga === ra && gact === '*') return true; + if (ga === '*' && gact === ract && (gsub ?? '') === (rsub ?? '')) return true; + if (ga === ra && gact === ract && (gsub ?? '') === (rsub ?? '')) return true; } return false; }
