Commit 84a764a
committed
admin: enforce JWT role gate in S3Handler.principalForWrite
Codex P2 + coderabbitai Minor on PR #669 caught that the S3
admin handler diverged from the Dynamo path in production
wiring. The previous shape was:
if h.roles == nil { /* JWT check */ ... return principal }
/* h.roles != nil — production: skip JWT, only live role */
role, _ := h.roles.LookupRole(...)
if !role.AllowsWrite() { 403 }
So a session whose JWT was minted as `read-only` could
**escalate** to write capability the moment the live role index
promoted that access key to `full` — exactly the kind of
between-login-and-revocation window the JWT was supposed to
freeze. DynamoHandler.principalForWrite (dynamo_handler.go:413)
fires the JWT gate unconditionally and then layers the live
role check on top, which is the safer ordering: the JWT can
only narrow capability, never widen it.
Aligned the S3 handler with that contract. The fix also picks
up a second case the Dynamo path already covers: a JWT minted
as `full` whose live role has since been demoted to read-only
or removed entirely is now rejected with 403 instead of
silently sliding through.
Per CLAUDE.md "test the bug first" — `newS3HandlerForTest`
never called `WithRoleStore`, so every existing test ran the
`h.roles == nil` branch and the production path was unproven.
Added two new tests:
TestS3Handler_PrincipalForWrite_JWTGateFiresWithRoleStore
— table sweep over all three S3 admin write endpoints
(create, put_acl, delete) with WithRoleStore wired and
JWT=read-only / live=full. All three must 403.
TestS3Handler_PrincipalForWrite_LiveRoleNarrowsJWT
— JWT=full + live=read-only or removed-from-index.
The full live-role-narrowing branch was unproven before;
this pins both demotion and revocation cases.
Also documented the AdminDeleteBucket TOCTOU that coderabbitai
flagged as 🔴 / 🟠 in the same review:
AdminDeleteBucket scans ObjectManifestPrefixForBucket at
readTS (limit=1, an "is bucket empty?" probe) and dispatches
the BucketMetaKey delete with that point key only in
ReadKeys. A concurrent PutObject inserting a manifest key
in the scanned prefix between readTS and commit will not
conflict — the OCC validator only inspects keys appearing
in ReadKeys, and there is no ReadRanges mechanism.
This is a pre-existing race that adapter/s3.go:deleteBucket
(SigV4 path) inherits as well — closing the gap requires
either bumping BucketGenerationKey on every PutObject so it
serves as an OCC token here, or extending OperationGroup with
ReadRanges and teaching the FSM to validate range emptiness
atomically with commit. Both are larger schema changes outside
this PR's scope. Recorded the limitation as a code comment on
AdminDeleteBucket so the next reader of the function knows the
contract; tracking the fix as a follow-up rather than expanding
the PR's surface.
go test -race ./internal/admin/ — passes.
golangci-lint run ./internal/admin/... ./adapter/... — 0 issues.1 parent e911458 commit 84a764a
3 files changed
Lines changed: 139 additions & 18 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
358 | 358 | | |
359 | 359 | | |
360 | 360 | | |
| 361 | + | |
| 362 | + | |
| 363 | + | |
| 364 | + | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
361 | 385 | | |
362 | 386 | | |
363 | 387 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
362 | 362 | | |
363 | 363 | | |
364 | 364 | | |
365 | | - | |
366 | | - | |
367 | | - | |
368 | | - | |
369 | | - | |
| 365 | + | |
| 366 | + | |
| 367 | + | |
| 368 | + | |
| 369 | + | |
| 370 | + | |
| 371 | + | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
370 | 375 | | |
371 | 376 | | |
372 | 377 | | |
373 | 378 | | |
374 | 379 | | |
375 | 380 | | |
376 | 381 | | |
377 | | - | |
378 | | - | |
379 | | - | |
380 | | - | |
381 | | - | |
| 382 | + | |
| 383 | + | |
| 384 | + | |
| 385 | + | |
| 386 | + | |
| 387 | + | |
| 388 | + | |
| 389 | + | |
| 390 | + | |
| 391 | + | |
| 392 | + | |
| 393 | + | |
| 394 | + | |
| 395 | + | |
| 396 | + | |
| 397 | + | |
| 398 | + | |
| 399 | + | |
| 400 | + | |
| 401 | + | |
382 | 402 | | |
383 | 403 | | |
384 | 404 | | |
385 | 405 | | |
386 | | - | |
387 | | - | |
388 | | - | |
389 | | - | |
390 | | - | |
391 | | - | |
392 | | - | |
| 406 | + | |
| 407 | + | |
| 408 | + | |
| 409 | + | |
393 | 410 | | |
394 | | - | |
| 411 | + | |
395 | 412 | | |
396 | 413 | | |
397 | 414 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
440 | 440 | | |
441 | 441 | | |
442 | 442 | | |
| 443 | + | |
| 444 | + | |
| 445 | + | |
| 446 | + | |
| 447 | + | |
| 448 | + | |
| 449 | + | |
| 450 | + | |
| 451 | + | |
| 452 | + | |
| 453 | + | |
| 454 | + | |
| 455 | + | |
| 456 | + | |
| 457 | + | |
| 458 | + | |
| 459 | + | |
| 460 | + | |
| 461 | + | |
| 462 | + | |
| 463 | + | |
| 464 | + | |
| 465 | + | |
| 466 | + | |
| 467 | + | |
| 468 | + | |
| 469 | + | |
| 470 | + | |
| 471 | + | |
| 472 | + | |
| 473 | + | |
| 474 | + | |
| 475 | + | |
| 476 | + | |
| 477 | + | |
| 478 | + | |
| 479 | + | |
| 480 | + | |
| 481 | + | |
| 482 | + | |
| 483 | + | |
| 484 | + | |
| 485 | + | |
| 486 | + | |
| 487 | + | |
| 488 | + | |
| 489 | + | |
| 490 | + | |
| 491 | + | |
| 492 | + | |
| 493 | + | |
| 494 | + | |
| 495 | + | |
| 496 | + | |
| 497 | + | |
| 498 | + | |
| 499 | + | |
| 500 | + | |
| 501 | + | |
| 502 | + | |
| 503 | + | |
| 504 | + | |
| 505 | + | |
| 506 | + | |
| 507 | + | |
| 508 | + | |
| 509 | + | |
| 510 | + | |
| 511 | + | |
| 512 | + | |
| 513 | + | |
| 514 | + | |
| 515 | + | |
| 516 | + | |
| 517 | + | |
| 518 | + | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
443 | 523 | | |
444 | 524 | | |
445 | 525 | | |
| |||
0 commit comments