fix: enforce the signed-in user's role on cross-node proxied requests (#1505)

Proxied requests authenticated to a remote node previously ran as admin
regardless of the originating user's role, so a non-admin using the UI
against a remote node could reach admin-only handlers there.

The forwarding primary now asserts the user's role on a trusted header
that the remote honors only for node_proxy/pilot_tunnel bearers (the same
trust model as the license tier header), and the gateway overwrites the
header on every proxied request so a client cannot smuggle it. An absent
header keeps admin for direct instance-to-instance and background service
calls; an unrecognized role fails closed to read-only.
This commit is contained in:
Anso
2026-06-28 16:32:50 -04:00
committed by GitHub
parent d6ce60d280
commit 78a742fb44
5 changed files with 262 additions and 4 deletions
+8 -1
View File
@@ -304,7 +304,14 @@ export interface WebhookExecution {
export type AuthProvider = 'local' | 'ldap' | 'oidc_google' | 'oidc_github' | 'oidc_okta' | 'oidc_custom';
export type UserRole = 'admin' | 'viewer' | 'deployer' | 'node-admin' | 'auditor';
// Source of truth for the role set. UserRole derives from it so the union and
// the runtime list cannot drift (adding a role here updates both).
export const USER_ROLES = ['admin', 'viewer', 'deployer', 'node-admin', 'auditor'] as const;
export type UserRole = typeof USER_ROLES[number];
/** Narrow an untrusted string (e.g. a proxied role header) to a known UserRole. */
export function isUserRole(value: unknown): value is UserRole {
return typeof value === 'string' && (USER_ROLES as readonly string[]).includes(value);
}
export type ResourceType = 'stack' | 'node';
export interface User {
+10
View File
@@ -6,3 +6,13 @@
* authenticated as a node_proxy bearer.
*/
export const PROXY_TIER_HEADER = 'x-sencho-tier';
/**
* Carries the signed-in user's role from the forwarding primary to the remote
* node, so the remote enforces that user's RBAC instead of treating every
* proxied request as admin. Trusted under the same rule as PROXY_TIER_HEADER:
* only a request authenticated as a node_proxy/pilot_tunnel bearer may set it,
* and the gateway overwrites it on every proxied request so a browser or API
* client cannot smuggle a role through.
*/
export const PROXY_ROLE_HEADER = 'x-sencho-actor-role';