feat(reborn): DB-backed user role + admin gate on the WebChat-v2 caller (#5266)#5270
feat(reborn): DB-backed user role + admin gate on the WebChat-v2 caller (#5266)#5270zetyquickly wants to merge 2 commits into
Conversation
…er (#5266) Gives the Reborn WebChat-v2 stack a typed user role so the facade can gate admin operations — the prerequisite for admin-grants-permissions in epic #5261. - ironclaw_host_api: new UserRole { Owner > Admin > Member } + UserStatus authority enums (alongside UserId/TenantId); wire-stable snake_case, as_str/parse with least-privilege fallback, is_admin/is_owner. - ironclaw_reborn_identity: surface role/status on UserRecord + the persisted StoredUser (serde-default so pre-existing records rehydrate to member/active); re-export the enums so crate::UserRole resolves. - WebuiAuthentication carries role (env-bearer operator authenticates as Owner; default Member; session/OIDC populate from the persisted record in a follow-up). WebUiAuthenticatedCaller carries role + is_admin() (role.is_admin() || operator_webui_config — operator flag kept as a compat shim). The admin gate USAGE lands with the grant/revoke methods in #5268. clippy -D warnings clean across host_api/identity/product_workflow/composition; host_api role + identity tests pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
📝 WalkthroughWalkthroughAdds shared ChangesRole, auth, and persistence plumbing
Sequence Diagram(s)sequenceDiagram
participant WebuiAuthentication
participant authenticate_request
participant WebUiAuthenticatedCaller
participant TurnActor
WebuiAuthentication->>authenticate_request: auth.role
authenticate_request->>WebUiAuthenticatedCaller: with_role(auth.role)
WebUiAuthenticatedCaller->>WebUiAuthenticatedCaller: role.is_admin() or operator_webui_config
WebUiAuthenticatedCaller->>TurnActor: with_role(self.role)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related issues
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
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 |
There was a problem hiding this comment.
Code Review
This pull request introduces the UserRole and UserStatus enums to manage user authority and lifecycle status, integrating UserRole into the authentication pipeline and user records. The review feedback suggests also propagating the newly introduced UserStatus enum through the authentication pipeline (WebUiAuthenticatedCaller and WebuiAuthentication) and updating associated constructors, helper methods, and tests to prevent a security gap where suspended or deactivated users might otherwise be treated as active.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| /// Resolved role of the authenticated caller. Default is the | ||
| /// least-privilege `Member`; the env-bearer operator authenticates as | ||
| /// `Owner`. Carried so the facade can gate admin operations via | ||
| /// [`Self::is_admin`]. | ||
| #[serde(default)] | ||
| pub role: ironclaw_host_api::UserRole, |
There was a problem hiding this comment.
Since UserStatus is introduced in this PR to represent suspended or deactivated users, WebUiAuthenticatedCaller should also carry the user's status. This ensures that the authentication and authorization pipeline can gate operations for suspended/deactivated users, preventing a security gap where suspended users are treated as active.
| /// Resolved role of the authenticated caller. Default is the | |
| /// least-privilege `Member`; the env-bearer operator authenticates as | |
| /// `Owner`. Carried so the facade can gate admin operations via | |
| /// [`Self::is_admin`]. | |
| #[serde(default)] | |
| pub role: ironclaw_host_api::UserRole, | |
| /// Resolved role of the authenticated caller. Default is the | |
| /// least-privilege `Member`; the env-bearer operator authenticates as | |
| /// `Owner`. Carried so the facade can gate admin operations via | |
| /// [`Self::is_admin`]. | |
| #[serde(default)] | |
| pub role: ironclaw_host_api::UserRole, | |
| /// Resolved status of the authenticated caller. Default is `Active`. | |
| /// Carried so the facade can gate operations for suspended/deactivated users. | |
| #[serde(default)] | |
| pub status: ironclaw_host_api::UserStatus, |
| agent_id, | ||
| project_id, | ||
| operator_webui_config: false, | ||
| role: ironclaw_host_api::UserRole::Member, |
| /// Override the caller's role (the default from [`Self::new`] is the | ||
| /// least-privilege `Member`). | ||
| #[must_use] | ||
| pub fn with_role(mut self, role: ironclaw_host_api::UserRole) -> Self { | ||
| self.role = role; | ||
| self | ||
| } |
There was a problem hiding this comment.
Add with_status and is_active helper methods to WebUiAuthenticatedCaller to support setting and checking the user's status.
| /// Override the caller's role (the default from [`Self::new`] is the | |
| /// least-privilege `Member`). | |
| #[must_use] | |
| pub fn with_role(mut self, role: ironclaw_host_api::UserRole) -> Self { | |
| self.role = role; | |
| self | |
| } | |
| /// Override the caller's role (the default from [`Self::new`] is the | |
| /// least-privilege `Member`). | |
| #[must_use] | |
| pub fn with_role(mut self, role: ironclaw_host_api::UserRole) -> Self { | |
| self.role = role; | |
| self | |
| } | |
| /// Override the caller's status (the default from [`Self::new`] is `Active`). | |
| #[must_use] | |
| pub fn with_status(mut self, status: ironclaw_host_api::UserStatus) -> Self { | |
| self.status = status; | |
| self | |
| } | |
| /// `true` when the caller's account is active and usable. | |
| pub fn is_active(&self) -> bool { | |
| self.status.is_active() | |
| } |
| /// Resolved role of the authenticated caller. Defaults to the | ||
| /// least-privilege `Member`; the env-bearer operator authenticates as | ||
| /// `Owner`. Session/OIDC authenticators populate this from the persisted | ||
| /// user record once per-user role reads land. | ||
| pub role: ironclaw_reborn_identity::UserRole, |
There was a problem hiding this comment.
Add the status field to WebuiAuthentication to carry the user's status through the authentication layer.
/// Resolved role of the authenticated caller. Defaults to the
/// least-privilege `Member`; the env-bearer operator authenticates as
/// `Owner`. Session/OIDC authenticators populate this from the persisted
/// user record once per-user role reads land.
pub role: ironclaw_reborn_identity::UserRole,
/// Resolved status of the authenticated caller. Defaults to `Active`.
pub status: ironclaw_reborn_identity::UserStatus,| role: ironclaw_reborn_identity::UserRole::Member, | ||
| } |
| agent_id: Some(AgentId::new(AGENT).expect("agent")), | ||
| project_id: Some(ProjectId::new(PROJECT).expect("project")), | ||
| operator_webui_config: false, | ||
| role: ironclaw_host_api::UserRole::Member, |
| agent_id: Some(AgentId::new(AGENT).expect("agent")), | ||
| project_id: Some(ProjectId::new(PROJECT).expect("project")), | ||
| operator_webui_config: true, | ||
| role: ironclaw_host_api::UserRole::Member, |
| agent_id: Some(AgentId::new(AGENT).expect("agent")), | ||
| project_id: Some(ProjectId::new(PROJECT).expect("project")), | ||
| operator_webui_config: false, | ||
| role: ironclaw_host_api::UserRole::Member, |
| agent_id: Some(AgentId::new(AGENT).expect("agent")), | ||
| project_id: Some(ProjectId::new(PROJECT).expect("project")), | ||
| operator_webui_config: false, | ||
| role: ironclaw_host_api::UserRole::Member, |
| agent_id: None, | ||
| project_id: None, | ||
| operator_webui_config: false, | ||
| role: ironclaw_host_api::UserRole::Member, |
There was a problem hiding this comment.
Pull request overview
This PR introduces a typed, wire-stable user authority model (UserRole, UserStatus) and threads the caller’s resolved role through the Reborn WebChat-v2 authentication/caller path, enabling upcoming admin-gated operations in the product workflow/facade.
Changes:
- Add shared
UserRole { Owner > Admin > Member }andUserStatus { Active | Suspended | Deactivated }enums inironclaw_host_apiand re-export them fromironclaw_reborn_identity. - Extend identity persistence shapes to include role/status with serde defaults for back-compat rehydration.
- Carry
rolethroughWebuiAuthentication→WebUiAuthenticatedCallerand addcaller.is_admin()(role-based, with operator-flag compat shim).
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| crates/ironclaw_reborn_identity/src/lib.rs | Re-export host authority enums; add role/status to UserRecord and update docs. |
| crates/ironclaw_reborn_identity/src/filesystem_store/record.rs | Persist role/status in filesystem-backed stored user rows with #[serde(default)]. |
| crates/ironclaw_reborn_identity/src/filesystem_store.rs | Initialize new filesystem user rows with default role/status. |
| crates/ironclaw_reborn_composition/src/webui_serve.rs | Add role to WebuiAuthentication, default to Member, set env-bearer operator to Owner, and propagate into the request caller extension. |
| crates/ironclaw_reborn_composition/src/slack_personal_binding_pairing_serve.rs | Update tests to construct callers with the new role field. |
| crates/ironclaw_reborn_composition/src/slack_host_beta.rs | Update tests to include the new role field in caller extensions. |
| crates/ironclaw_reborn_composition/src/slack_channel_routes/allowed/tests.rs | Update test request helper to include role. |
| crates/ironclaw_reborn_composition/src/slack_channel_routes.rs | Update tests to include role in caller construction. |
| crates/ironclaw_product_workflow/src/webui_inbound.rs | Add role to WebUiAuthenticatedCaller plus with_role and is_admin() helper. |
| crates/ironclaw_host_api/src/role.rs | New shared UserRole/UserStatus enums with stable string forms and helpers. |
| crates/ironclaw_host_api/src/lib.rs | Export the new role module and re-export its public types. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| #[default] | ||
| Member, |
| pub enum UserStatus { | ||
| /// Normal, usable account. Default for unknown/missing values. | ||
| #[default] | ||
| Active, | ||
| /// Temporarily blocked by an admin; identity retained. | ||
| Suspended, | ||
| /// Deactivated; retained for audit, not usable. | ||
| Deactivated, | ||
| } |
| // `UserRole` / `UserStatus` are shared identity vocabulary owned by | ||
| // `ironclaw_host_api` (alongside `UserId` / `TenantId`), re-exported here so | ||
| // `crate::UserRole` and `UserRecord`'s fields resolve without every consumer | ||
| // depending on this store crate. |
|
🚅 Deployed to the ironclaw-pr-5270 environment in ironclaw-ci-preview
|
…#5266 #5261) Foundation for the capability-policy role model: - UserRole::rank() (Owner=2 > Admin=1 > Member=0) + outranks() (strict) in ironclaw_host_api — no derived Ord (variant order would invert privilege). - TurnActor gains role (#[serde(default)] → Member for legacy/channel actors) + with_role(); WebUiAuthenticatedCaller::actor() now carries the caller's role to dispatch (it was dropped before), so the availability resolver can be role-aware. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
crates/ironclaw_product_workflow/src/webui_inbound.rs (1)
119-128: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick winKeep admin checks and dispatched actor authority in sync.
Line 124 treats
operator_webui_configas admin, but Line 128 stamps the dispatch actor fromself.roleonly. A legacy operator caller with defaultMemberpasses the WebUI admin gate, then enters turn handling asMember.Proposed fix
+ fn effective_role(&self) -> ironclaw_host_api::UserRole { + if self.operator_webui_config { + ironclaw_host_api::UserRole::Owner + } else { + self.role + } + } + pub fn is_admin(&self) -> bool { - self.role.is_admin() || self.operator_webui_config + self.effective_role().is_admin() } pub fn actor(&self) -> TurnActor { - TurnActor::new(self.user_id.clone()).with_role(self.role) + TurnActor::new(self.user_id.clone()).with_role(self.effective_role()) }Also add a caller-level regression for
with_operator_webui_config(true).actor().role. As per coding guidelines, “Preserve tenant/user/agent/project/mission/thread scope on authority, state, memory, process, network, outbound, resource, and event records”.🤖 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/ironclaw_product_workflow/src/webui_inbound.rs` around lines 119 - 128, Keep the admin gate and dispatched authority consistent in webui_inbound.rs: `is_admin()` treats `operator_webui_config` as admin, but `actor()` currently builds `TurnActor` from `self.role` only, so legacy operator callers can pass admin checks and still dispatch as `Member`. Update `TurnInbound::actor` to mirror the same elevated authority used by `is_admin()` when `operator_webui_config` is set, and add a regression test covering `with_operator_webui_config(true).actor().role` to verify the actor role matches the admin path.Source: Coding guidelines
🤖 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.
Outside diff comments:
In `@crates/ironclaw_product_workflow/src/webui_inbound.rs`:
- Around line 119-128: Keep the admin gate and dispatched authority consistent
in webui_inbound.rs: `is_admin()` treats `operator_webui_config` as admin, but
`actor()` currently builds `TurnActor` from `self.role` only, so legacy operator
callers can pass admin checks and still dispatch as `Member`. Update
`TurnInbound::actor` to mirror the same elevated authority used by `is_admin()`
when `operator_webui_config` is set, and add a regression test covering
`with_operator_webui_config(true).actor().role` to verify the actor role matches
the admin path.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 3ef15eae-e084-44e5-9c95-f57c16a7a95d
📒 Files selected for processing (3)
crates/ironclaw_host_api/src/role.rscrates/ironclaw_product_workflow/src/webui_inbound.rscrates/ironclaw_turns/src/scope.rs
Part of #5266 · epic #5261.
Gives the Reborn WebChat-v2 stack a typed user role so the facade can gate
admin operations — the prerequisite for "admin grants users permissions". The
gate's consumers (REST users, admin grant surfaces) land on the control-plane
PR #5355, which builds on this.
Changes
ironclaw_host_api—UserRole { Owner > Admin > Member }+UserStatusauthority enums; wire-stable snake_case, least-privilege fallback,
is_admin/is_owner.ironclaw_reborn_identity—role/statusonUserRecord+ the persistedStoredUser(#[serde(default)]so older records rehydrate tomember/active).WebuiAuthenticationcarriesrole(env-bearer operator authenticates asOwner; defaultMember).WebUiAuthenticatedCallercarriesrole+is_admin()=role.is_admin() || operator_webui_config(the operator flagstays as a compat shim during migration).
Placing the enums in
host_api(notreborn_identity) avoids aproduct_workflow → reborn_identitydependency.Follow-ups (not in this PR)
Member; populatingrole from the persisted user record at
authenticate()is a follow-up.surfaces gated on
caller.is_admin()— is the control-plane PR feat(reborn): capability-policy control plane — REST users + admin grants (epic #5261) #5355.Tests
clippy -D warningsclean acrosshost_api/reborn_identity/product_workflow/reborn_composition;host_apirole tests +reborn_identitytests pass.
Role-model expansion (#5261)
This PR now also carries the role-rank primitives (
UserRole::rank/outranks) andTurnActor.roleplumbing that the role model builds on.