Skip to content

Add component-based permission system for app data#3413

Open
tylerhawkes wants to merge 1 commit intomainfrom
app-data-permissions
Open

Add component-based permission system for app data#3413
tylerhawkes wants to merge 1 commit intomainfrom
app-data-permissions

Conversation

@tylerhawkes
Copy link
Copy Markdown
Contributor

@tylerhawkes tylerhawkes commented Apr 8, 2026

Introduces a ComponentId-based permission and registry system that will
replace the current fixed-field PolicySet. Key pieces:

  • ComponentId (u16 newtype) with XMTP/App/Immutable/Reserved ranges
  • ComponentRegistry (TlsMap<ComponentId, VLBytes>) storing per-component
    metadata: permissions (insert/update/delete) and data type
  • Three-layer validation: immutability -> hardcoded -> registry lookup
  • Hardcoded: COMPONENT_REGISTRY and SUPER_ADMIN_LIST (super admin only)
  • Constrained: ADMIN_LIST (must be AdminOrSuperAdmin or SuperAdmin)
  • Deny by default: no registry entry = write rejected

Co-Authored-By: Claude Opus 4.6 (1M context) noreply@anthropic.com

Note

Add component-based permission system for app data in xmtp_mls_common

  • Introduces ComponentId, a u16 newtype with defined ID ranges (XMTP, App, Reserved, Immutable) and QUIC variable-length integer TLS encoding in component_id.rs.
  • Adds ComponentRegistry, a validated TLS-serializable map keyed by ComponentId that enforces range rules, hardcoded exclusions, write-once immutability, and metadata completeness on set and load in component_registry.rs.
  • Implements validate_component_write in validation.rs, which enforces a three-layer permission check: immutability, hardcoded super-admin rules, and registry-based policy evaluation (AND/ANY combinators with deny-by-default).
  • Adds a component_permissions builder function using bon for constructing insert/update/delete policies without positional mix-ups.
  • Risk: constrained components (e.g., ADMIN_LIST) only accept AllowIfAdmin or AllowIfSuperAdmin policies; other policy kinds are rejected at write time.

Macroscope summarized 48387e3.

Copy link
Copy Markdown
Contributor Author

tylerhawkes commented Apr 8, 2026


How to use the Graphite Merge Queue

Add the label mergequeue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link
Copy Markdown

claude bot commented Apr 8, 2026

Claude finished @tylerhawkes's task —— View job


Code Review Complete

Review Date: 2026-04-10 00:22 UTC

Overall Assessment

This is a well-architected permission system with strong defensive programming practices. The code quality is high, test coverage is good (96.25%), and the implementation follows Rust best practices. I've identified a few areas for consideration below.


Security & Design

1. ComponentId deserialization hard-fails on u16 overflow

component_id.rs:214-218: The deserializer rejects values > u16::MAX with an error. While this is documented as forward-compatible with the wire format, it means:

  • Current clients cannot parse messages with future component IDs > 0xFFFF
  • Groups with mixed client versions may experience silent failures during upgrades

Consider: Document the upgrade strategy for when IDs need to exceed u16 (perhaps in a migration guide).

2. Empty combinator policies are marked Invalid vs explicit Deny

validation.rs:196-209: Empty AndCondition/AnyCondition policies return PolicyOutcome::Invalid rather than treating them as vacuously true/false. While conservative, this differs from typical boolean algebra:

  • Empty AND ≈ vacuously true (identity)
  • Empty OR ≈ vacuously false (identity)

Your choice of Invalid is safer (fail-closed), but callers might expect standard boolean semantics. The comment at line 188 justifies this well—consider adding similar rationale to the proto schema or a DESIGN.md.

3. Super-admin can bypass immutability checks but cannot bypass hardcoded checks

validation.rs:100-117: The three-layer validation stops at immutability first, then hardcoded, then registry. This means:

  • Immutable components reject update/delete for everyone, including super-admins
  • Hardcoded components require super-admin but allow all ops

Verify: Is this the intended privilege hierarchy? If super-admins should be able to force-update immutable XMTP components in emergencies, the order may need adjustment.


Code Quality

4. QUIC variable-length encoding size calculation uses stack buffer trick

component_id.rs:189-194: The vlen_encoding_bytes function writes to a stack buffer just to measure size. This is clever (ensures consistency with tls_codec), but the function could benefit from a comment explaining why this indirect approach is preferred over hardcoded size thresholds.

Suggestion: Add a comment like // Delegates to tls_codec to avoid drift if upstream size boundaries change.

5. Missing coverage in 42 lines (per codecov)

The codecov report shows 42 lines missing coverage. Most are likely error paths (decode errors, constraint violations), which is typical. However:

  • validation.rs:23 lines uncovered — Check which policy evaluation paths lack tests
  • component_registry.rs:10 lines uncovered
  • component_id.rs:9 lines uncovered

Recommend: Use cargo tarpaulin --show-missing-lines (or codecov's drill-down) to identify specific uncovered branches, especially in the policy combinator logic.


Performance

6. Registry operations are O(n) due to TlsMap's Vec backing

component_registry.rs:86: ComponentRegistry wraps TlsMap<ComponentId, VLBytes>, which is backed by a sorted Vec (O(n) insert/update/delete per the TlsMap docs). For small registries (<100 components), this is fine. For larger registries with frequent updates:

  • Consider lazy deserialization (store raw bytes, decode on-demand)
  • Or document the expected registry size constraints

Note: The PR description doesn't specify expected registry size. If this scales to hundreds of app-defined components, the O(n) updates may become a bottleneck in hot paths.


Documentation

7. Excellent inline documentation

The module-level comments, invariant checks (line 32-50), and error messages are exemplary. The comment at component_registry.rs:122-130 clearly explains the security rationale for validation layers.

Minor: Consider adding a crate-level app_data/README.md or module doc explaining the overall permission model, especially the hardcoded vs. registry distinction, to help future contributors.


Test Coverage

8. Test-only from_inner_for_test constructor for corruption testing

component_registry.rs:329-338: This is excellent defense-in-depth testing (exercising decode error paths that the public API can't reach). Well done.

9. Tests thoroughly cover constraint violations and hardcoded component rejection

The test suite systematically validates:

  • Hardcoded component rejection (tests 451-481)
  • Constrained component policy restrictions (tests 484-608)
  • Immutability enforcement (tests 679-709)
  • Wire format validation (tests 755-932)

This is comprehensive.


Summary

Strengths:

  • Robust three-layer validation model
  • Comprehensive test coverage of security-critical paths
  • Excellent use of Rust type system (newtype ComponentId, builder patterns)
  • Defense-in-depth error handling (structured errors, no panics)

Action Items:

  1. Verify the intended privilege hierarchy for immutable components vs. super-admins
  2. Identify and add tests for the 42 uncovered lines (especially policy combinator edge cases)
  3. Document upgrade strategy for ComponentId > u16::MAX
  4. Consider documenting expected registry size constraints given O(n) operations

Recommendation: Approve after addressing (1) and (2). The immutability vs. super-admin question is a design decision that should be explicitly confirmed, and closing the coverage gap will strengthen the already-solid test suite.


Tasks

  • Read and analyze component_id.rs
  • Read and analyze component_permissions.rs
  • Read and analyze component_registry.rs
  • Read and analyze validation.rs
  • Review test coverage and security implications
  • Formulate and post review feedback

Comment thread crates/xmtp_mls_common/src/app_data/component_registry.rs
@codecov
Copy link
Copy Markdown

codecov bot commented Apr 8, 2026

Codecov Report

❌ Patch coverage is 96.25000% with 42 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.36%. Comparing base (34d036d) to head (ac6aaff).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
crates/xmtp_mls_common/src/app_data/validation.rs 93.44% 23 Missing ⚠️
...xmtp_mls_common/src/app_data/component_registry.rs 98.12% 10 Missing ⚠️
...rates/xmtp_mls_common/src/app_data/component_id.rs 95.00% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3413      +/-   ##
==========================================
+ Coverage   83.06%   83.36%   +0.29%     
==========================================
  Files         378      382       +4     
  Lines       51829    52949    +1120     
==========================================
+ Hits        43054    44140    +1086     
- Misses       8775     8809      +34     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@tylerhawkes tylerhawkes changed the base branch from tls-set to graphite-base/3413 April 8, 2026 18:32
@tylerhawkes tylerhawkes force-pushed the app-data-permissions branch from c5c2bb7 to 3b72916 Compare April 8, 2026 19:06
@tylerhawkes tylerhawkes changed the base branch from graphite-base/3413 to tls-set April 8, 2026 19:06
Base automatically changed from tls-set to main April 8, 2026 20:34
@tylerhawkes tylerhawkes force-pushed the app-data-permissions branch from 3b72916 to 898c1e8 Compare April 9, 2026 20:58
@tylerhawkes tylerhawkes marked this pull request as ready for review April 9, 2026 21:20
@tylerhawkes tylerhawkes requested a review from a team as a code owner April 9, 2026 21:20
@macroscopeapp
Copy link
Copy Markdown
Contributor

macroscopeapp bot commented Apr 9, 2026

Approvability

Verdict: Needs human review

This PR introduces a new component-based permission system with security-critical authorization logic (admin/super-admin checks, immutability enforcement, policy evaluation). While the author owns all files and the code includes comprehensive tests, new permission/authorization infrastructure of this scope warrants human review to validate the security model design.

You can customize Macroscope's approvability policy. Learn more.

@tylerhawkes tylerhawkes force-pushed the app-data-permissions branch from 898c1e8 to 867dc8c Compare April 9, 2026 21:56
/// existing entry — there is no audit log here because the audit trail
/// lives in the MLS commit history that produced the change.
///
/// Rejects invalid IDs, IDs in the reserved range, hardcoded components
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.

It sounds like a hardcoded component can never move to the registry, and a registry component can never become hardcoded (without a hammer release and waiting for everyone to upgrade). Not a bad thing on its own, just want to make sure I understand how to evolve from the initial state.

Would we ever introduce additional hardcoded components?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I went back and forth on this. The two components it is tied to is the component registry with all the permissions and the super admin list. Both of which should probably ever only be modified by super admins. Everything else is in the registry. If we enforced both of those via the registry we would still have a carve out like we do for the admin list (can only be admin or superadmin) and I think it would still be a hammer release.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It might be possible to avoid a hammer release by letting anything proposed by a super admin go through without checks, but that has it's own set of risks.

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.

I'd be worried about the "anything the super admin does is correct" loophole coming to bite us on things like user-controlled data fields. Even a super-admin shouldn't be able to modify data meant to be owned by a specific group member. Also application-controlled fields, where the app developer probably isn't thinking about the super-admin having god mode powers.

Having two magic hardcoded fields that are both extremely unlikely to to have their permissions change seems fine.

@tylerhawkes tylerhawkes force-pushed the app-data-permissions branch 2 times, most recently from 5d76ea3 to ac6aaff Compare April 10, 2026 00:09
Introduces a ComponentId-based permission and registry system that will
replace the current fixed-field PolicySet. Key pieces:

- ComponentId (u16 newtype) with XMTP/App/Immutable/Reserved ranges
- ComponentRegistry (TlsMap<ComponentId, VLBytes>) storing per-component
  metadata: permissions (insert/update/delete) and data type
- Three-layer validation: immutability -> hardcoded -> registry lookup
- Hardcoded: COMPONENT_REGISTRY and SUPER_ADMIN_LIST (super admin only)
- Constrained: ADMIN_LIST (must be AdminOrSuperAdmin or SuperAdmin)
- Deny by default: no registry entry = write rejected

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tylerhawkes tylerhawkes force-pushed the app-data-permissions branch from ac6aaff to 48387e3 Compare April 10, 2026 00:21
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.

2 participants