mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 00:03:08 +00:00
fix(apps): confine the SDK token's users scope to the viewer's identity
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018Gmsk9kAG7p9t2Qy6ADRJz
This commit is contained in:
co-authored by
Claude Fable 5
parent
96387e312c
commit
429c91001f
@@ -489,8 +489,12 @@ pub fn check_route_access(
|
||||
}
|
||||
}
|
||||
|
||||
// Raw-app SDK tokens (sentinel) hold `jobs:run` only to run the deployed
|
||||
// runnables the viewer already can — never request-supplied code.
|
||||
// Raw-app SDK tokens (sentinel). The viewer approved a named list of
|
||||
// permissions, so each must grant what its prompt says and no more:
|
||||
// `jobs:run` runs the deployed runnables the viewer already can — never
|
||||
// request-supplied code — and `users:read` reads their own identity, not the
|
||||
// workspace member directory (`users/list`, `users/list_usage`) the domain
|
||||
// scope would otherwise reach.
|
||||
if has_raw_app_sdk_sentinel(Some(token_scopes)) {
|
||||
if let Some(suffix) = route_suffix.as_deref() {
|
||||
if is_request_supplied_code_route(suffix) {
|
||||
@@ -499,6 +503,12 @@ pub fn check_route_access(
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
if required_domain == ScopeDomain::Users && suffix != "users/whoami" {
|
||||
return Err(Error::PermissionDenied(
|
||||
"Access denied. A raw app frontend SDK token can only read the viewer's own identity."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4974,6 +4974,11 @@ mod embed_token_tests {
|
||||
("/api/w/test/jobs/run/dependencies_async", "POST"),
|
||||
("/api/w/test/jobs/run/flow_dependencies", "POST"),
|
||||
("/api/w/test/jobs/run/flow_dependencies_async", "POST"),
|
||||
// `users:read` is presented to the viewer as "read your identity", so
|
||||
// the workspace member directory must stay out of reach.
|
||||
("/api/w/test/users/list", "GET"),
|
||||
("/api/w/test/users/list_usage", "GET"),
|
||||
("/api/w/test/users/username_to_email/admin", "GET"),
|
||||
];
|
||||
for (path, method) in denied {
|
||||
assert!(
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
|
||||
import { hasStoredSdkConsent, storeSdkConsent } from './sdkScopes'
|
||||
|
||||
describe('stored frontend SDK consent', () => {
|
||||
beforeEach(() => localStorage.clear())
|
||||
|
||||
it('only covers scopes the viewer actually approved', () => {
|
||||
storeSdkConsent('a@w.dev', 'ws', 'u/a/app', ['users:read'])
|
||||
expect(hasStoredSdkConsent('a@w.dev', 'ws', 'u/a/app', ['users:read'])).toBe(true)
|
||||
// The app added a scope after the viewer consented: it must ask again
|
||||
// rather than silently minting a broader token.
|
||||
expect(hasStoredSdkConsent('a@w.dev', 'ws', 'u/a/app', ['users:read', 'jobs:run'])).toBe(false)
|
||||
})
|
||||
|
||||
it('does not leak one viewer or app to another', () => {
|
||||
storeSdkConsent('a@w.dev', 'ws', 'u/a/app', ['users:read'])
|
||||
expect(hasStoredSdkConsent('b@w.dev', 'ws', 'u/a/app', ['users:read'])).toBe(false)
|
||||
expect(hasStoredSdkConsent('a@w.dev', 'ws', 'u/a/other', ['users:read'])).toBe(false)
|
||||
expect(hasStoredSdkConsent('a@w.dev', 'other', 'u/a/app', ['users:read'])).toBe(false)
|
||||
})
|
||||
|
||||
it('treats unreadable storage as no consent', () => {
|
||||
localStorage.setItem('wm_sdk_consent:a@w.dev:ws:u/a/app', 'not json')
|
||||
expect(hasStoredSdkConsent('a@w.dev', 'ws', 'u/a/app', ['users:read'])).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -12,12 +12,12 @@ export const FRONTEND_SDK_SCOPES: { value: string; label: string; description: s
|
||||
{
|
||||
value: 'jobs:read',
|
||||
label: 'Read jobs and results',
|
||||
description: 'Poll jobs by id and read their results'
|
||||
description: 'Read jobs and their results, and list the runs the viewer can see'
|
||||
},
|
||||
{
|
||||
value: 'users:read',
|
||||
label: 'Read your identity',
|
||||
description: 'Read the viewer username and email (whoami)'
|
||||
description: 'Read the viewer username and email — not the workspace member list'
|
||||
},
|
||||
{
|
||||
value: 'resources:read',
|
||||
|
||||
Reference in New Issue
Block a user