Skip to content

feat(reborn): DB-backed user role + admin gate on the WebChat-v2 caller (#5266)#5270

Open
zetyquickly wants to merge 2 commits into
mainfrom
feat/reborn-user-role
Open

feat(reborn): DB-backed user role + admin gate on the WebChat-v2 caller (#5266)#5270
zetyquickly wants to merge 2 commits into
mainfrom
feat/reborn-user-role

Conversation

@zetyquickly

@zetyquickly zetyquickly commented Jun 25, 2026

Copy link
Copy Markdown
Member

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_apiUserRole { Owner > Admin > Member } + UserStatus
    authority enums; wire-stable snake_case, least-privilege fallback,
    is_admin/is_owner.
  • ironclaw_reborn_identityrole/status on UserRecord + the persisted
    StoredUser (#[serde(default)] so older records rehydrate to
    member/active).
  • WebuiAuthentication carries role (env-bearer operator authenticates as
    Owner; default Member). WebUiAuthenticatedCaller carries role +
    is_admin() = role.is_admin() || operator_webui_config (the operator flag
    stays as a compat shim during migration).

Placing the enums in host_api (not reborn_identity) avoids a
product_workflow → reborn_identity dependency.

Follow-ups (not in this PR)

Tests

clippy -D warnings clean across host_api / reborn_identity /
product_workflow / reborn_composition; host_api role tests + reborn_identity
tests pass.


Role-model expansion (#5261)

This PR now also carries the role-rank primitives (UserRole::rank/outranks) and TurnActor.role plumbing that the role model builds on.

…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>
Copilot AI review requested due to automatic review settings June 25, 2026 22:39
@railway-app railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5270 June 25, 2026 22:39 Destroyed
@github-actions github-actions Bot added size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jun 25, 2026
@coderabbitai

coderabbitai Bot commented Jun 25, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds shared UserRole/UserStatus types, threads caller role through WebUI authentication, and persists role/status on user records. Test helpers and route call sites now construct authenticated callers with explicit member roles.

Changes

Role, auth, and persistence plumbing

Layer / File(s) Summary
Host API role vocabulary
crates/ironclaw_host_api/src/lib.rs, crates/ironclaw_host_api/src/role.rs
UserRole and UserStatus are exported from the host API, with snake_case serde, parsing helpers, predicates, and unit tests.
WebUI caller role propagation
crates/ironclaw_product_workflow/src/webui_serve.rs, crates/ironclaw_product_workflow/src/webui_inbound.rs
WebuiAuthentication and WebUiAuthenticatedCaller carry role, operator() sets Owner, authenticate_request stamps the role into the caller, and admin checks use the resolved role.
Persisted user role/status fields
crates/ironclaw_reborn_identity/src/lib.rs, crates/ironclaw_reborn_identity/src/filesystem_store/record.rs, crates/ironclaw_reborn_identity/src/filesystem_store.rs
UserRecord and StoredUser add typed role and status fields with serde defaults, and new stored users initialize both fields from default().
Test caller role defaults
crates/ironclaw_reborn_composition/src/slack_channel_routes.rs, crates/ironclaw_reborn_composition/src/slack_channel_routes/allowed/tests.rs, crates/ironclaw_reborn_composition/src/slack_host_beta.rs, crates/ironclaw_reborn_composition/src/slack_personal_binding_pairing_serve.rs
Test helpers and protected-route calls now build WebUiAuthenticatedCaller values with explicit Member roles.

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)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Suggested reviewers

  • serrrfirat
  • think-in-universe

Poem

Member walks in first, quiet and default,
Owner arrives through typed role vault.
Status in storage, serde holds the gate,
auth and persistence now share the same state.

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is substantive but does not follow the required template and omits most mandatory sections. Add the missing template sections: Change Type, Linked Issue, Validation, Security Impact, DB/rollback, trust-boundary checklist, blast radius, review follow-through, and review track.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title follows Conventional Commits style and accurately summarizes the role/admin-gating change.
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.

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.

@gemini-code-assist gemini-code-assist 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.

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.

Comment on lines +77 to +82
/// 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,

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.

medium

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.

Suggested change
/// 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,

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.

medium

Initialize the status field to Active in the WebUiAuthenticatedCaller::new constructor.

            role: ironclaw_host_api::UserRole::Member,
            status: ironclaw_host_api::UserStatus::Active,

Comment on lines +111 to +117
/// 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
}

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.

medium

Add with_status and is_active helper methods to WebUiAuthenticatedCaller to support setting and checking the user's status.

Suggested change
/// 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()
}

Comment on lines +148 to +152
/// 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,

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.

medium

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,

Comment on lines +160 to 161
role: ironclaw_reborn_identity::UserRole::Member,
}

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.

medium

Initialize the status field to Active in WebuiAuthentication::new.

            role: ironclaw_reborn_identity::UserRole::Member,
            status: ironclaw_reborn_identity::UserStatus::Active,
        }

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,

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.

medium

Initialize the status field in the test's WebUiAuthenticatedCaller struct literal.

                        role: ironclaw_host_api::UserRole::Member,
                        status: ironclaw_host_api::UserStatus::Active,

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,

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.

medium

Initialize the status field in the test's WebUiAuthenticatedCaller struct literal.

                        role: ironclaw_host_api::UserRole::Member,
                        status: ironclaw_host_api::UserStatus::Active,

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,

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.

medium

Initialize the status field in the test's WebUiAuthenticatedCaller struct literal.

                        role: ironclaw_host_api::UserRole::Member,
                        status: ironclaw_host_api::UserStatus::Active,

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,

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.

medium

Initialize the status field in the test's WebUiAuthenticatedCaller struct literal.

                        role: ironclaw_host_api::UserRole::Member,
                        status: ironclaw_host_api::UserStatus::Active,

agent_id: None,
project_id: None,
operator_webui_config: false,
role: ironclaw_host_api::UserRole::Member,

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.

medium

Initialize the status field in the test's WebUiAuthenticatedCaller struct literal.

                role: ironclaw_host_api::UserRole::Member,
                status: ironclaw_host_api::UserStatus::Active,

Copilot AI 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.

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 } and UserStatus { Active | Suspended | Deactivated } enums in ironclaw_host_api and re-export them from ironclaw_reborn_identity.
  • Extend identity persistence shapes to include role/status with serde defaults for back-compat rehydration.
  • Carry role through WebuiAuthenticationWebUiAuthenticatedCaller and add caller.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.

Comment on lines +21 to +22
#[default]
Member,
Comment on lines +60 to +68
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,
}
Comment on lines +103 to +106
// `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.
@railway-app

railway-app Bot commented Jun 25, 2026

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5270 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jun 26, 2026 at 9:39 pm

@zetyquickly zetyquickly changed the title feat(reborn): DB-backed user role + admin gate on the WebChat-v2 caller (#5266) feat(reborn): capability-policy control plane — user role + REST users + admin grants (epic #5261, fat PR 3/3) Jun 26, 2026
@zetyquickly zetyquickly changed the title feat(reborn): capability-policy control plane — user role + REST users + admin grants (epic #5261, fat PR 3/3) feat(reborn): DB-backed user role + admin gate on the WebChat-v2 caller (#5266) Jun 26, 2026
…#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>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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 win

Keep admin checks and dispatched actor authority in sync.

Line 124 treats operator_webui_config as admin, but Line 128 stamps the dispatch actor from self.role only. A legacy operator caller with default Member passes the WebUI admin gate, then enters turn handling as Member.

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

📥 Commits

Reviewing files that changed from the base of the PR and between fa455cc and 9e40dee.

📒 Files selected for processing (3)
  • crates/ironclaw_host_api/src/role.rs
  • crates/ironclaw_product_workflow/src/webui_inbound.rs
  • crates/ironclaw_turns/src/scope.rs

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

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules size: L 200-499 changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants