fix(authz): emit authorization events before role and tag writes - #1934
fix(authz): emit authorization events before role and tag writes#1934AndreaBozzo wants to merge 1 commit into
Conversation
The `authorize_*` helpers in `role.rs` and `tag.rs` ran the authorization check and the catalog write together, and the handler emitted the authorization event only afterwards. The write therefore committed before the attempt was audited, and a post-authz write failure (DB error, unique conflict) came back as an `AuthZError` and was logged as an `AuthorizationFailedEvent` even though authorization had succeeded. Split each helper at its first side effect: the pure `require_*_action` check now feeds `emit_authz`, and the write runs after, with failures mapped through `authz_to_error_no_audit` so the outcome is not logged a second time under the wrong label. Covers all 19 handlers from the issue — the four role endpoints, the three tag-definition endpoints, and the twelve set/delete tag handlers via the shared set/delete flows. The immutability guards (system/managed roles, reserved tag definitions) and the tag scope/value validations move with the write: they are request rejections, not authorization denials. Denials are unaffected — the check still runs first. Two duplicated resolve-and-authorize blocks are factored out along the way: `check_role_action` for the three role endpoints that resolve a role, and `check_tag_definition_action`, which the two tag read paths also used verbatim. Adds five regression tests driving the real handlers: each triggers a write that fails only after authorization succeeded (duplicate create, rename onto an existing name, deleting an attached tag definition) and asserts the attempt is audited exactly once as a success, with no failure event. All five fail against the unfixed code. `delete_role`, `update_role_source_system` and the set/delete tag handlers have no deterministic post-authz write failure and are not discriminated by them. Closes lakekeeper#1933
WalkthroughRole and tag management now separate authorization auditing from catalog writes. Post-authorization failures use non-audited error mapping. Integration tests verify that failed writes retain successful authorization events. ChangesAuthorization audit ordering
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ PR Title Formatted CorrectlyThe title of this PR match the correct format. Thank you! |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
crates/lakekeeper/src/api/management/v1/role.rs (1)
559-568: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUpdate the stale reference to the authz helper.
Both handlers now follow the same shape:
check_role_action,emit_authz, then a write-onlyapply_*helper. That part is correct.One doc comment is now inaccurate. The handler comment above
reject_managed_provider(lines 584-586) states that the check on the current role "lives inside the authz helper because it needs the role resolved." That check now lives inapply_update_role_source_system(line 890), which runs after the authorization event is emitted. Point the comment at the apply helper so a reader does not look for the check incheck_role_action.Also applies to: 608-627
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@crates/lakekeeper/src/api/management/v1/role.rs` around lines 559 - 568, Update the doc comment above reject_managed_provider to state that the current-role check is performed by apply_update_role_source_system, not the authorization helper check_role_action. Keep the handler flow and implementation unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@crates/lakekeeper/src/api/management/v1/role.rs`:
- Around line 559-568: Update the doc comment above reject_managed_provider to
state that the current-role check is performed by
apply_update_role_source_system, not the authorization helper check_role_action.
Keep the handler flow and implementation unchanged.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: d89a3c92-64a0-41e6-9c87-b1ce60353720
📒 Files selected for processing (5)
crates/lakekeeper-integration-tests/src/lib.rscrates/lakekeeper-integration-tests/tests/role_ops.rscrates/lakekeeper-integration-tests/tests/tag_ops.rscrates/lakekeeper/src/api/management/v1/role.rscrates/lakekeeper/src/api/management/v1/tag.rs
|
Hi @c-thiel , let me know if this is still needed/wanted so i'll solve the merge conflicts if it is. Take care, |
Follow-up to #1924, which fixed the ordering for the OpenFGA assignment endpoints. The role and tag management endpoints had the same inversion.
Problem
In
role.rsandtag.rstheauthorize_*helpers ran the authorization check and the write (begin_write→ catalog write →commit), and the handler calledemit_authzonly on the result. So:AuthZErrorand was logged as anAuthorizationFailedEvent— even though authorization had succeeded.Denials were always audited correctly (the check runs first); this is about the success / write-failure path and the ordering.
Fix
Each helper is split at its first side effect. The pure
require_*_actioncheck feedsemit_authz; the write runs after and its failures are mapped withauthz_to_error_no_audit, so nothing is logged a second time under the wrong label.All 19 handlers from the issue:
role.rs:create_role,delete_role,update_role,update_role_source_system— newapply_*helpers, pluscheck_role_actionshared by the three that resolve a role first.tag.rs:create/update/delete_tag_definition— newapply_*helpers, pluscheck_tag_definition_action.tag.rs: the 12set_*_tag/delete_*_taghandlers — the shared flows becomecheck_set_tag_on_target/check_delete_tag_from_target, producing anAuthorizedTagChangethatapply_tag_to_target/remove_tag_from_targetconsume after the emit.Two behavioural notes:
SystemRoleImmutable,ManagedRoleImmutable, reserved tag definitions) and the tag scope/value validations move to the write side. They are request rejections, not authorization denials, so they now follow anAuthorizationSucceededevent instead of being reported as an authorization failure. This matchesremove_role_memberinrole_membership.rs, which already ranreject_managed_roleafter the emit. The HTTP status and error type for those rejections are unchanged.check_tag_definition_actionalso replaces two verbatim copies of the same resolve-and-authorize block in the tag read paths (get_tag_definition,list_tag_attachments).No API, schema or config change.
Tests
Five new regression tests in
role_ops.rs/tag_ops.rsdrive the real handlers and assert on the dispatched authorization events via a newCapturingAuthzListenerhelper. Each triggers a write that can only fail after authorization succeeded:test_create_role_audits_authz_before_failing_writetest_update_role_audits_authz_before_failing_writetest_create_tag_definition_audits_authz_before_failing_writetest_update_tag_definition_audits_authz_before_failing_writetest_delete_tag_definition_audits_authz_before_failing_writeEach asserts the attempt is audited exactly once as a success and that no
AuthorizationFailedEventis emitted. All five fail against the unfixed code (verified by revertingrole.rs+tag.rsand re-running): the create cases see 1 success event instead of 2, the update/delete cases see a failure event instead of a success.They do not discriminate
delete_role,update_role_source_systemor the 12 set/delete tag handlers — those have no deterministic post-authz write failure to trigger. Those paths are covered by the existingrole_ops.rs/tag_ops.rsbehaviour tests, which still pass (27 + 39).Locally:
cargo clippyclean on the default,--no-default-featuresandtest-utilsfeature sets,cargo +nightly fmt --checkclean. The--all-featuresclippy legs need libclang/cmake and were not run locally — CI covers them.Release notes
Role and tag management endpoints now dispatch the authorization audit event before applying the write, so a crash between the two can no longer apply a change that was never audited. A write that fails after a successful authorization check (for example a duplicate name) is no longer reported in the audit log as an authorization failure.
Summary by CodeRabbit