| author | |
| committer | |
| log | d61a846db111f203963efe7cda8ac7af96da1880 |
| tree | e30ba2430fc28474afcc4afa81c256f0ff43ab0d |
| parent | 235cd050154d9faf139ce8975ab370f5432db911 |
| signature | Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU |
Assisted-by: gpt-63 files changed, 18 insertions(+), 13 deletions(-)
dashboard/src/oidc.rs+9-4| ... | ... | @@ -313,9 +313,6 @@ fn authenticated_client( |
| 313 | 313 | form: &HashMap<String, String>, |
| 314 | 314 | ) -> Result<String> { |
| 315 | 315 | let (id, secret) = if let Some(header) = headers.get("authorization") { |
| 316 | if form.contains_key("client_secret") { | |
| 317 | return Err(invalid("invalid_request")); | |
| 318 | } | |
| 319 | 316 | let header = header |
| 320 | 317 | .to_str() |
| 321 | 318 | .ok() |
| ... | ... | @@ -337,7 +334,15 @@ fn authenticated_client( |
| 337 | 334 | .1 |
| 338 | 335 | .into_owned() |
| 339 | 336 | }; |
| 340 | (decode(id), decode(secret)) | |
| 337 | let (id, secret) = (decode(id), decode(secret)); | |
| 338 | // Shale repeats Basic credentials in its form; require the same secret. | |
| 339 | if form.get("client_secret").is_some_and(|supplied| { | |
| 340 | !(id == "shale" || id.starts_with("shale-preview-")) | |
| 341 | || mcp::hash(supplied) != mcp::hash(&secret) | |
| 342 | }) { | |
| 343 | return Err(invalid("invalid_request")); | |
| 344 | } | |
| 345 | (id, secret) | |
| 341 | 346 | } else { |
| 342 | 347 | ( |
| 343 | 348 | form.get("client_id").cloned().unwrap_or_default(), |
dashboard/web/components/Explorer.tsx+3-8| ... | ... | @@ -235,7 +235,6 @@ export function Explorer(props: { id: string; client: Client; root: string }) { |
| 235 | 235 | let anchor: string | undefined; |
| 236 | 236 | let table: HTMLTableElement | undefined; |
| 237 | 237 | let filesScroll!: HTMLDivElement; |
| 238 | let lastCursor: string | undefined; | |
| 239 | 238 | let revealCursor: string | undefined; |
| 240 | 239 | let patternInput!: HTMLInputElement; |
| 241 | 240 | |
| ... | ... | @@ -342,13 +341,9 @@ export function Explorer(props: { id: string; client: Client; root: string }) { |
| 342 | 341 | refreshTree(); |
| 343 | 342 | })); |
| 344 | 343 | |
| 345 | // A listing update must not scroll back to an old selection after the user scrolled elsewhere. | |
| 346 | // Keep a new cursor pending until its row arrives, then reveal it once in this scroller only. | |
| 347 | createEffect(on([cursor, rows], ([path, all]) => { | |
| 348 | if (path !== lastCursor) { | |
| 349 | lastCursor = path; | |
| 350 | revealCursor = path; | |
| 351 | } | |
| 344 | // Folder updates must not scroll back to a selection the user has scrolled away from. | |
| 345 | createEffect(on([cursor, rows], ([path, all], before) => { | |
| 346 | if (path !== before?.[0]) revealCursor = path; | |
| 352 | 347 | if (!path || revealCursor !== path) return; |
| 353 | 348 | const index = all.findIndex((row) => row.path === path); |
| 354 | 349 | if (index < 0) return; |
tools/dashboard-oidc-test.py+6-1| ... | ... | @@ -59,6 +59,7 @@ def main(): |
| 59 | 59 | provision('other', ['https://jelly.paperclover.net/callback']) |
| 60 | 60 | provision('bad', ['https://evil.example/callback'], status=1) |
| 61 | 61 | provision('bad', ['https://shale.paperclover.net/*'], status=1) |
| 62 | provision('shale-preview-bad', ['https://jelly.paperclover.net/-/callback'], guests=True, status=1) | |
| 62 | 63 | with socket.socket() as available: |
| 63 | 64 | available.bind(('127.0.0.1', 0)); port = available.getsockname()[1] |
| 64 | 65 | environment['PORT'] = str(port) |
| ... | ... | @@ -121,7 +122,11 @@ def main(): |
| 121 | 122 | 'redirect_uri': callback, 'code': code(), 'code_verifier': verifier} |
| 122 | 123 | for change in [{'client_secret': 'wrong'}, {'client_id': 'other'}, {'code_verifier': 'wrong'}, {'redirect_uri': 'https://evil.example/'}]: |
| 123 | 124 | request('/auth/oidc/token', 'POST', {**exchange, **change}, status=400, form=True) |
| 124 | tokens = request('/auth/oidc/token', 'POST', exchange, form=True) | |
| 125 | other_basic = {'Authorization': 'Basic ' + base64.b64encode(('other:' + secret).encode()).decode()} | |
| 126 | assert request('/auth/oidc/token', 'POST', {'client_id':'other','client_secret':secret,'grant_type':'authorization_code'}, extra=other_basic, status=400, form=True)['error'] == 'invalid_request' | |
| 127 | basic_header = {'Authorization': 'Basic ' + base64.b64encode(('shale:' + secret).encode()).decode()} | |
| 128 | request('/auth/oidc/token', 'POST', {**exchange, 'client_secret': 'different'}, extra=basic_header, status=400, form=True) | |
| 129 | tokens = request('/auth/oidc/token', 'POST', exchange, extra=basic_header, form=True) | |
| 125 | 130 | request('/auth/oidc/token', 'POST', exchange, status=400, form=True) |
| 126 | 131 | parts = tokens['id_token'].split('.') |
| 127 | 132 | key.verify(decode(parts[2]), (parts[0] + '.' + parts[1]).encode(), padding.PKCS1v15(), hashes.SHA256()) |