Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(authz): emit authorization events before role and tag writes - #1934

Open
AndreaBozzo wants to merge 1 commit into
lakekeeper:mainfrom
AndreaBozzo:fix/audit-authz-before-role-tag-writes
Open

fix(authz): emit authorization events before role and tag writes#1934
AndreaBozzo wants to merge 1 commit into
lakekeeper:mainfrom
AndreaBozzo:fix/audit-authz-before-role-tag-writes

Conversation

@AndreaBozzo

@AndreaBozzo AndreaBozzo commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

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.rs and tag.rs the authorize_* helpers ran the authorization check and the write (begin_write → catalog write → commit), and the handler called emit_authz only on the result. So:

  • the write committed before the authorization event was dispatched, 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.

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_*_action check feeds emit_authz; the write runs after and its failures are mapped with authz_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 — new apply_* helpers, plus check_role_action shared by the three that resolve a role first.
  • tag.rs: create/update/delete_tag_definition — new apply_* helpers, plus check_tag_definition_action.
  • tag.rs: the 12 set_*_tag / delete_*_tag handlers — the shared flows become check_set_tag_on_target / check_delete_tag_from_target, producing an AuthorizedTagChange that apply_tag_to_target / remove_tag_from_target consume after the emit.

Two behavioural notes:

  • The immutability guards (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 an AuthorizationSucceeded event instead of being reported as an authorization failure. This matches remove_role_member in role_membership.rs, which already ran reject_managed_role after the emit. The HTTP status and error type for those rejections are unchanged.
  • check_tag_definition_action also 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.rs drive the real handlers and assert on the dispatched authorization events via a new CapturingAuthzListener helper. Each triggers a write that can only fail after authorization succeeded:

test failing write
test_create_role_audits_authz_before_failing_write duplicate role name
test_update_role_audits_authz_before_failing_write rename onto an existing role name
test_create_tag_definition_audits_authz_before_failing_write duplicate definition name
test_update_tag_definition_audits_authz_before_failing_write rename onto an existing definition name
test_delete_tag_definition_audits_authz_before_failing_write deleting a definition still attached to a target

Each asserts the attempt is audited exactly once as a success and that no AuthorizationFailedEvent is emitted. All five fail against the unfixed code (verified by reverting role.rs + tag.rs and 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_system or the 12 set/delete tag handlers — those have no deterministic post-authz write failure to trigger. Those paths are covered by the existing role_ops.rs / tag_ops.rs behaviour tests, which still pass (27 + 39).

Locally: cargo clippy clean on the default, --no-default-features and test-utils feature sets, cargo +nightly fmt --check clean. The --all-features clippy 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

  • Bug Fixes
    • Authorization audit events are now recorded in the correct order for role and tag operations.
    • Authorized requests that later fail during persistence no longer produce duplicate or misleading authorization failure events.
    • Improved consistency across role management and tag operations, including create, update, delete, apply, and remove actions.
    • Existing protections for system roles, managed providers, and transactional changes remain enforced.

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
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Role 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.

Changes

Authorization audit ordering

Layer / File(s) Summary
Audit listener and ordering tests
crates/lakekeeper-integration-tests/src/lib.rs, crates/lakekeeper-integration-tests/tests/role_ops.rs, crates/lakekeeper-integration-tests/tests/tag_ops.rs
Added an authorization listener that captures outcomes and settling counts. Added tests for role and tag writes that fail after successful authorization.
Role authorization and mutation flow
crates/lakekeeper/src/api/management/v1/role.rs
Separated role authorization and event emission from create, delete, update, and source-system write helpers. Post-authorization failures no longer emit duplicate authorization failures.
Tag-definition authorization and mutation
crates/lakekeeper/src/api/management/v1/tag.rs
Separated tag-definition authorization from create, update, and delete operations. Shared checks resolve and authorize definitions before catalog mutations.
Authorized tag attachment changes
crates/lakekeeper/src/api/management/v1/tag.rs
Added AuthorizedTagChange and split tag attachment checks from apply/remove mutations across all supported target types. Existing idempotent behavior remains in the mutation helpers.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: c-thiel

Poem

A rabbit checks the gate,
Then records the choice in slate.
Writes may stumble, tags may sway,
But success is logged before the way.
Roles and tags now hop in line.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: emitting authorization events before role and tag write operations.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

✅ PR Title Formatted Correctly

The title of this PR match the correct format. Thank you!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
crates/lakekeeper/src/api/management/v1/role.rs (1)

559-568: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Update the stale reference to the authz helper.

Both handlers now follow the same shape: check_role_action, emit_authz, then a write-only apply_* 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 in apply_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 in check_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

📥 Commits

Reviewing files that changed from the base of the PR and between cd153f8 and 1793933.

📒 Files selected for processing (5)
  • crates/lakekeeper-integration-tests/src/lib.rs
  • crates/lakekeeper-integration-tests/tests/role_ops.rs
  • crates/lakekeeper-integration-tests/tests/tag_ops.rs
  • crates/lakekeeper/src/api/management/v1/role.rs
  • crates/lakekeeper/src/api/management/v1/tag.rs

@AndreaBozzo

Copy link
Copy Markdown
Contributor Author

Hi @c-thiel , let me know if this is still needed/wanted so i'll solve the merge conflicts if it is.

Take care,
Andrea

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant