feat: 2/7 add role membership read contract#25522
feat: 2/7 add role membership read contract#25522tabVersion wants to merge 1 commit intoralph/rbac-split-01-parser-surfacefrom
Conversation
|
Heads up for the stacked branch: #25521 now includes parser fix Because this PR is based on the previous #25521 head and also touches |
This PR adds the storage and read API contract needed before role grant and revoke behavior can become authoritative. It keeps grant and revoke RPCs out of the contract layer while adding the compile-coupled inherit flag and list membership surfaces.\n\nConstraint: Generated service traits require every added RPC to compile through meta service, RPC client, frontend client, and mocks.\nRejected: Add GrantRole and RevokeRole RPCs in this contract PR | would require behavior stubs before the authoritative controller implementation lands.\nConfidence: high\nScope-risk: moderate\nDirective: Keep write behavior out of this PR; grant and revoke RPCs belong with their meta implementations.\nTested: cargo fmt; cargo fmt --check; cargo check -p risingwave_meta -p risingwave_rpc_client -p risingwave_frontend\nNot-tested: Runtime migration/restore path against a live cluster
32fd4ac to
cb6b850
Compare
tabVersion
left a comment
There was a problem hiding this comment.
Stack-aware RBAC review after rebasing this PR onto #25521 head 69f15214e2: the new role-membership read contract mostly matches the intended 2/7 boundary, but I found one default-inheritance edge that should be fixed before this slice becomes the base for the grant-authority PR.
| can_create_user: Set(user.can_create_user), | ||
| can_login: Set(user.can_login), | ||
| is_admin: Set(user.is_admin), | ||
| can_inherit: Set(user.can_inherit), |
There was a problem hiding this comment.
Because can_inherit is a proto3 bool, any caller that builds PbUserInfo { name, ..Default::default() } now persists NOINHERIT through this conversion. The frontend create path sets can_inherit: true, but the meta/controller test fixture still uses that default shape, and downstream grant semantics use the member's can_inherit as the default for GRANT role TO member when WITH INHERIT is omitted. That means direct meta-created test users silently get inherit_option = false, which diverges from the PostgreSQL/default contract this PR is introducing. Please update the server-side role/user fixtures or constructors to set the default can_inherit: true and add/keep a regression around an omitted-WITH INHERIT role grant.
There was a problem hiding this comment.
Pull request overview
This PR introduces the read-side plumbing for role memberships (storage model, snapshot/restore, meta RPC/service/client passthrough) and extends user metadata with a new can_inherit flag that can be updated via UpdateUserRequest.
Changes:
- Add
user_role_membershipSeaORM entity + migration, and include it in meta snapshot v2 backup/restore flows. - Add
ListRoleMembershipsRPC end-to-end (meta service + controller + rpc_client + frontend meta client trait passthrough). - Add
can_inherittoUserInfoplus update semantics (UpdateField::INHERIT) and default initialization in frontend/test fixtures.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| src/storage/backup/src/meta_snapshot_v2.rs | Adds user_role_memberships to meta snapshot v2 model list. |
| src/rpc_client/src/meta_client.rs | Adds RPC client method + macro registration for ListRoleMemberships. |
| src/meta/src/controller/user.rs | Adds controller query for listing role memberships; adds INHERIT update handling. |
| src/meta/src/backup_restore/restore_impl/v2.rs | Restores role membership rows and updates auto-increment tracking. |
| src/meta/service/src/user_service.rs | Implements ListRoleMemberships RPC handler. |
| src/meta/model/src/user.rs | Adds can_inherit to user model/protobuf conversions. |
| src/meta/model/src/user_role_membership.rs | Introduces SeaORM entity for user_role_membership + Pb conversion. |
| src/meta/model/src/prelude.rs | Re-exports UserRoleMembership entity. |
| src/meta/model/src/lib.rs | Exposes module and RoleMembershipId type alias. |
| src/meta/model/migration/src/m20260422_000001_role_membership.rs | Creates user_role_membership table + uniqueness index + FKs. |
| src/meta/model/migration/src/m20260422_000002_user_inherit_flag.rs | Adds user.can_inherit column with default true. |
| src/meta/model/migration/src/lib.rs | Registers the two new migrations. |
| src/frontend/src/user/user_catalog.rs | Propagates can_inherit into frontend user catalog representation. |
| src/frontend/src/test_utils.rs | Updates mock user update handling and default fixture users with can_inherit = true. |
| src/frontend/src/meta_client.rs | Adds list_role_memberships to frontend meta client trait + impl. |
| src/frontend/src/handler/create_user.rs | Defaults created users to can_inherit = true. |
| proto/user.proto | Adds can_inherit, UpdateField::INHERIT, and role membership messages + RPC. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| repeated GrantPrivilege grant_privileges = 8; | ||
|
|
||
| bool is_admin = 9; | ||
| bool can_inherit = 10; |
| message RoleMembership { | ||
| uint32 role_id = 1; | ||
| uint32 member_id = 2; | ||
| uint32 granted_by = 3; | ||
| bool admin_option = 4; | ||
| bool inherit_option = 5; | ||
| bool set_option = 6; | ||
| uint32 id = 7; | ||
| } | ||
|
|
||
| message ListRoleMembershipsRequest { | ||
| repeated uint32 member_ids = 1; | ||
| } |
| impl From<PbUserInfo> for ActiveModel { | ||
| fn from(user: PbUserInfo) -> Self { | ||
| let user_id = if user.id == 0 { NotSet } else { Set(user.id) }; | ||
| Self { | ||
| user_id, | ||
| name: Set(user.name), | ||
| is_super: Set(user.is_super), | ||
| can_create_db: Set(user.can_create_db), | ||
| can_create_user: Set(user.can_create_user), | ||
| can_login: Set(user.can_login), | ||
| is_admin: Set(user.is_admin), | ||
| can_inherit: Set(user.can_inherit), | ||
| auth_info: Set(user.auth_info.as_ref().map(AuthInfo::from)), | ||
| } |
| let inner = self.inner.read().await; | ||
| let memberships = if member_ids.is_empty() { | ||
| UserRoleMembership::find().all(&inner.db).await? | ||
| } else { | ||
| UserRoleMembership::find() | ||
| .filter(user_role_membership::Column::MemberId.is_in(member_ids.iter().copied())) | ||
| .all(&inner.db) | ||
| .await? | ||
| }; |
| pub async fn list_role_memberships( | ||
| &self, | ||
| member_ids: &[UserId], | ||
| ) -> MetaResult<Vec<PbRoleMembership>> { | ||
| let inner = self.inner.read().await; | ||
| let memberships = if member_ids.is_empty() { | ||
| UserRoleMembership::find().all(&inner.db).await? | ||
| } else { | ||
| UserRoleMembership::find() | ||
| .filter(user_role_membership::Column::MemberId.is_in(member_ids.iter().copied())) | ||
| .all(&inner.db) | ||
| .await? | ||
| }; | ||
| Ok(memberships.into_iter().map(Into::into).collect()) | ||
| } |
| manager | ||
| .create_index( | ||
| Index::create() | ||
| .name("idx_user_role_membership_item") | ||
| .table(UserRoleMembership::Table) | ||
| .unique() | ||
| .col(UserRoleMembership::RoleId) | ||
| .col(UserRoleMembership::MemberId) | ||
| .col(UserRoleMembership::GrantedBy) | ||
| .to_owned(), | ||
| ) |
Stack: 2/7 for splitting
ralph/rbac-postgres-final-alignmentontoorigin/main.Base:
ralph/rbac-split-01-parser-surfaceNext:
ralph/rbac-split-03-meta-grant-authorityScope
RoleMembership,ListRoleMemberships, migrations/entity, backup/restore parity, and client/service list passthrough.UserInfo.can_inherit/UpdateField::INHERIT, including defaultcan_inherit = truefor created users/fixtures.Out of scope
GrantRoleandRevokeRoleRPCs and behavior.Tests
cargo fmt --checkcargo check -p risingwave_meta -p risingwave_rpc_client -p risingwave_frontend