diff --git a/proto/user.proto b/proto/user.proto index 55e14e8586670..5611e76b4266a 100644 --- a/proto/user.proto +++ b/proto/user.proto @@ -35,6 +35,7 @@ message UserInfo { repeated GrantPrivilege grant_privileges = 8; bool is_admin = 9; + bool can_inherit = 10; } enum Action { @@ -100,6 +101,7 @@ message UpdateUserRequest { RENAME = 5; CREATE_USER = 6; ADMIN = 7; + INHERIT = 8; } UserInfo user = 1; repeated UpdateField update_fields = 2; @@ -136,6 +138,24 @@ message RevokePrivilegeResponse { uint64 version = 2; } +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; +} + +message ListRoleMembershipsResponse { + repeated RoleMembership memberships = 1; +} + message AlterDefaultPrivilegeRequest { repeated uint32 user_ids = 1; uint32 database_id = 2; @@ -175,4 +195,6 @@ service UserService { rpc RevokePrivilege(RevokePrivilegeRequest) returns (RevokePrivilegeResponse); // AlterDefaultPrivilege alters the default privileges. rpc AlterDefaultPrivilege(AlterDefaultPrivilegeRequest) returns (AlterDefaultPrivilegeResponse); + // ListRoleMemberships lists direct role memberships for the requested members. + rpc ListRoleMemberships(ListRoleMembershipsRequest) returns (ListRoleMembershipsResponse); } diff --git a/src/frontend/src/handler/create_user.rs b/src/frontend/src/handler/create_user.rs index 60afa7ea93316..efe82a4cded96 100644 --- a/src/frontend/src/handler/create_user.rs +++ b/src/frontend/src/handler/create_user.rs @@ -45,6 +45,7 @@ pub async fn handle_create_user( name: user_name.clone(), // the LOGIN option is implied if it is not explicitly specified. can_login: true, + can_inherit: true, ..Default::default() }; let mut notices = vec![]; diff --git a/src/frontend/src/meta_client.rs b/src/frontend/src/meta_client.rs index a7c424f62705b..57ad3c518037a 100644 --- a/src/frontend/src/meta_client.rs +++ b/src/frontend/src/meta_client.rs @@ -15,7 +15,7 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use anyhow::Context; -use risingwave_common::id::{ConnectionId, JobId, SourceId, TableId, WorkerId}; +use risingwave_common::id::{ConnectionId, JobId, SourceId, TableId, UserId, WorkerId}; use risingwave_common::session_config::SessionConfig; use risingwave_common::system_param::reader::SystemParamsReader; use risingwave_common::util::cluster_limit::ClusterLimit; @@ -46,6 +46,7 @@ use risingwave_pb::meta::{ RefreshRequest, RefreshResponse, list_sink_log_store_tables_response, }; use risingwave_pb::secret::PbSecretRef; +use risingwave_pb::user::RoleMembership as PbRoleMembership; use risingwave_rpc_client::error::Result; use risingwave_rpc_client::{HummockMetaClient, MetaClient}; @@ -210,6 +211,9 @@ pub trait FrontendMetaClient: Send + Sync { async fn list_hosted_iceberg_tables(&self) -> Result>; + async fn list_role_memberships(&self, member_ids: Vec) + -> Result>; + async fn get_fragment_by_id( &self, fragment_id: FragmentId, @@ -543,6 +547,13 @@ impl FrontendMetaClient for FrontendMetaClientImpl { self.0.list_hosted_iceberg_tables().await } + async fn list_role_memberships( + &self, + member_ids: Vec, + ) -> Result> { + self.0.list_role_memberships(member_ids).await + } + async fn get_fragment_by_id( &self, fragment_id: FragmentId, diff --git a/src/frontend/src/test_utils.rs b/src/frontend/src/test_utils.rs index 00695fcf90c05..95de506569bcd 100644 --- a/src/frontend/src/test_utils.rs +++ b/src/frontend/src/test_utils.rs @@ -75,7 +75,7 @@ use risingwave_pb::secret::PbSecretRef; use risingwave_pb::stream_plan::StreamFragmentGraph; use risingwave_pb::user::alter_default_privilege_request::Operation as AlterDefaultPrivilegeOperation; use risingwave_pb::user::update_user_request::UpdateField; -use risingwave_pb::user::{GrantPrivilege, UserInfo}; +use risingwave_pb::user::{GrantPrivilege, RoleMembership, UserInfo}; use risingwave_rpc_client::error::Result as RpcResult; use tempfile::{Builder, NamedTempFile}; @@ -1050,6 +1050,7 @@ impl UserInfoWriter for MockUserInfoWriter { UpdateField::AuthInfo => user_info.auth_info.clone_from(&update_user.auth_info), UpdateField::Rename => user_info.name.clone_from(&update_user.name), UpdateField::Admin => user_info.is_admin = update_user.is_admin, + UpdateField::Inherit => user_info.can_inherit = update_user.can_inherit, UpdateField::Unspecified => unreachable!(), }); lock.update_user(update_user); @@ -1122,6 +1123,7 @@ impl MockUserInfoWriter { can_create_db: true, can_create_user: true, can_login: true, + can_inherit: true, ..Default::default() }); user_info.write().create_user(UserInfo { @@ -1132,6 +1134,7 @@ impl MockUserInfoWriter { can_create_user: true, can_login: true, is_admin: true, + can_inherit: true, ..Default::default() }); Self { @@ -1383,6 +1386,13 @@ impl FrontendMetaClient for MockFrontendMetaClient { unimplemented!() } + async fn list_role_memberships( + &self, + _member_ids: Vec, + ) -> RpcResult> { + Ok(vec![]) + } + async fn list_iceberg_compaction_status(&self) -> RpcResult> { Ok(vec![]) } diff --git a/src/frontend/src/user/user_catalog.rs b/src/frontend/src/user/user_catalog.rs index dbe863531d13a..46ad7d2e5dd05 100644 --- a/src/frontend/src/user/user_catalog.rs +++ b/src/frontend/src/user/user_catalog.rs @@ -31,6 +31,7 @@ pub struct UserCatalog { pub can_create_user: bool, pub can_login: bool, pub is_admin: bool, + pub can_inherit: bool, pub auth_info: Option, pub grant_privileges: Vec, @@ -51,6 +52,7 @@ impl From for UserCatalog { can_create_user: user.can_create_user, can_login: user.can_login, is_admin: user.is_admin, + can_inherit: user.can_inherit, auth_info: user.auth_info, grant_privileges: user.grant_privileges, database_acls: Default::default(), @@ -73,6 +75,7 @@ impl UserCatalog { can_create_user: self.can_create_user, can_login: self.can_login, is_admin: self.is_admin, + can_inherit: self.can_inherit, auth_info: self.auth_info.clone(), grant_privileges: self.grant_privileges.clone(), } diff --git a/src/meta/model/migration/src/lib.rs b/src/meta/model/migration/src/lib.rs index af23b7071d6cb..1e1b976de9bd8 100644 --- a/src/meta/model/migration/src/lib.rs +++ b/src/meta/model/migration/src/lib.rs @@ -68,6 +68,8 @@ mod m20251224_142321_sink_schema_change; mod m20251231_000000_sink_ignore_delete; mod m20260119_153927_streaming_job_is_serverless_backfill; mod m20260120_120000_streaming_job_backfill_orders; +mod m20260422_000001_role_membership; +mod m20260422_000002_user_inherit_flag; mod utils; pub struct Migrator; @@ -174,6 +176,8 @@ impl MigratorTrait for Migrator { Box::new(m20251231_000000_sink_ignore_delete::Migration), Box::new(m20260119_153927_streaming_job_is_serverless_backfill::Migration), Box::new(m20260120_120000_streaming_job_backfill_orders::Migration), + Box::new(m20260422_000001_role_membership::Migration), + Box::new(m20260422_000002_user_inherit_flag::Migration), ] } } diff --git a/src/meta/model/migration/src/m20260422_000001_role_membership.rs b/src/meta/model/migration/src/m20260422_000001_role_membership.rs new file mode 100644 index 0000000000000..cfee7f92a990c --- /dev/null +++ b/src/meta/model/migration/src/m20260422_000001_role_membership.rs @@ -0,0 +1,131 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .create_table( + Table::create() + .table(UserRoleMembership::Table) + .if_not_exists() + .col( + ColumnDef::new(UserRoleMembership::Id) + .integer() + .primary_key() + .auto_increment(), + ) + .col( + ColumnDef::new(UserRoleMembership::RoleId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(UserRoleMembership::MemberId) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(UserRoleMembership::GrantedBy) + .integer() + .not_null(), + ) + .col( + ColumnDef::new(UserRoleMembership::AdminOption) + .boolean() + .not_null() + .default(false), + ) + .col( + ColumnDef::new(UserRoleMembership::InheritOption) + .boolean() + .not_null() + .default(true), + ) + .col( + ColumnDef::new(UserRoleMembership::SetOption) + .boolean() + .not_null() + .default(true), + ) + .foreign_key( + &mut ForeignKey::create() + .name("FK_user_role_membership_role_id") + .from(UserRoleMembership::Table, UserRoleMembership::RoleId) + .to(User::Table, User::UserId) + .on_delete(ForeignKeyAction::Cascade) + .to_owned(), + ) + .foreign_key( + &mut ForeignKey::create() + .name("FK_user_role_membership_member_id") + .from(UserRoleMembership::Table, UserRoleMembership::MemberId) + .to(User::Table, User::UserId) + .on_delete(ForeignKeyAction::Cascade) + .to_owned(), + ) + .foreign_key( + &mut ForeignKey::create() + .name("FK_user_role_membership_granted_by") + .from(UserRoleMembership::Table, UserRoleMembership::GrantedBy) + .to(User::Table, User::UserId) + .to_owned(), + ) + .to_owned(), + ) + .await?; + + 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(), + ) + .await?; + + Ok(()) + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_index( + Index::drop() + .name("idx_user_role_membership_item") + .table(UserRoleMembership::Table) + .to_owned(), + ) + .await?; + + manager + .drop_table(Table::drop().table(UserRoleMembership::Table).to_owned()) + .await?; + + Ok(()) + } +} + +#[derive(DeriveIden)] +enum UserRoleMembership { + Table, + Id, + RoleId, + MemberId, + GrantedBy, + AdminOption, + InheritOption, + SetOption, +} + +#[derive(DeriveIden)] +enum User { + #[sea_orm(iden = "user")] + Table, + UserId, +} diff --git a/src/meta/model/migration/src/m20260422_000002_user_inherit_flag.rs b/src/meta/model/migration/src/m20260422_000002_user_inherit_flag.rs new file mode 100644 index 0000000000000..5f8b134ca8713 --- /dev/null +++ b/src/meta/model/migration/src/m20260422_000002_user_inherit_flag.rs @@ -0,0 +1,41 @@ +use sea_orm_migration::prelude::*; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(User::Table) + .add_column( + ColumnDef::new(User::CanInherit) + .boolean() + .not_null() + .default(true), + ) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .alter_table( + Table::alter() + .table(User::Table) + .drop_column(User::CanInherit) + .to_owned(), + ) + .await + } +} + +#[derive(DeriveIden)] +enum User { + #[sea_orm(iden = "user")] + Table, + CanInherit, +} diff --git a/src/meta/model/src/lib.rs b/src/meta/model/src/lib.rs index 5a95b930badef..d3804fa27857d 100644 --- a/src/meta/model/src/lib.rs +++ b/src/meta/model/src/lib.rs @@ -69,6 +69,7 @@ pub mod table; pub mod user; pub mod user_default_privilege; pub mod user_privilege; +pub mod user_role_membership; pub mod view; pub mod worker; pub mod worker_property; @@ -77,6 +78,7 @@ pub type TransactionId = i32; pub type PrivilegeId = i32; pub type DefaultPrivilegeId = i32; +pub type RoleMembershipId = i32; pub use risingwave_pb::id::{CompactionGroupId, HummockSstableObjectId, HummockVersionId}; pub type Epoch = i64; diff --git a/src/meta/model/src/prelude.rs b/src/meta/model/src/prelude.rs index f0f3229f3f34b..2e3ba3d33f64b 100644 --- a/src/meta/model/src/prelude.rs +++ b/src/meta/model/src/prelude.rs @@ -47,6 +47,7 @@ pub use super::table::Entity as Table; pub use super::user::Entity as User; pub use super::user_default_privilege::Entity as UserDefaultPrivilege; pub use super::user_privilege::Entity as UserPrivilege; +pub use super::user_role_membership::Entity as UserRoleMembership; pub use super::view::Entity as View; pub use super::worker::Entity as Worker; pub use super::worker_property::Entity as WorkerProperty; diff --git a/src/meta/model/src/user.rs b/src/meta/model/src/user.rs index 73eec74619ed2..c94cdc756c974 100644 --- a/src/meta/model/src/user.rs +++ b/src/meta/model/src/user.rs @@ -32,6 +32,7 @@ pub struct Model { pub can_create_user: bool, pub can_login: bool, pub is_admin: bool, + pub can_inherit: bool, pub auth_info: Option, } @@ -60,6 +61,7 @@ impl From for ActiveModel { 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)), } } @@ -75,6 +77,7 @@ impl From for PbUserInfo { can_create_user: val.can_create_user, can_login: val.can_login, is_admin: val.is_admin, + can_inherit: val.can_inherit, auth_info: val.auth_info.map(|x| x.to_protobuf()), grant_privileges: vec![], // fill in later } diff --git a/src/meta/model/src/user_role_membership.rs b/src/meta/model/src/user_role_membership.rs new file mode 100644 index 0000000000000..c516855cf69b7 --- /dev/null +++ b/src/meta/model/src/user_role_membership.rs @@ -0,0 +1,76 @@ +// Copyright 2026 RisingWave Labs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use risingwave_pb::user::PbRoleMembership; +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +use crate::{RoleMembershipId, UserId}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "user_role_membership")] +pub struct Model { + #[sea_orm(primary_key)] + pub id: RoleMembershipId, + pub role_id: UserId, + pub member_id: UserId, + pub granted_by: UserId, + pub admin_option: bool, + pub inherit_option: bool, + pub set_option: bool, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::RoleId", + to = "super::user::Column::UserId", + on_update = "NoAction", + on_delete = "Cascade" + )] + Role, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::MemberId", + to = "super::user::Column::UserId", + on_update = "NoAction", + on_delete = "Cascade" + )] + Member, + #[sea_orm( + belongs_to = "super::user::Entity", + from = "Column::GrantedBy", + to = "super::user::Column::UserId", + on_update = "NoAction", + on_delete = "NoAction" + )] + Grantor, +} + +impl ActiveModelBehavior for ActiveModel {} + +impl From for PbRoleMembership { + fn from(value: Model) -> Self { + Self { + id: value.id as _, + role_id: value.role_id.as_raw_id(), + member_id: value.member_id.as_raw_id(), + granted_by: value.granted_by.as_raw_id(), + admin_option: value.admin_option, + inherit_option: value.inherit_option, + set_option: value.set_option, + } + } +} diff --git a/src/meta/service/src/user_service.rs b/src/meta/service/src/user_service.rs index 20e53aec028fb..913a8062ca683 100644 --- a/src/meta/service/src/user_service.rs +++ b/src/meta/service/src/user_service.rs @@ -21,8 +21,8 @@ use risingwave_pb::user::user_service_server::UserService; use risingwave_pb::user::{ AlterDefaultPrivilegeRequest, AlterDefaultPrivilegeResponse, CreateUserRequest, CreateUserResponse, DropUserRequest, DropUserResponse, GrantPrivilegeRequest, - GrantPrivilegeResponse, RevokePrivilegeRequest, RevokePrivilegeResponse, UpdateUserRequest, - UpdateUserResponse, + GrantPrivilegeResponse, ListRoleMembershipsRequest, ListRoleMembershipsResponse, + RevokePrivilegeRequest, RevokePrivilegeResponse, UpdateUserRequest, UpdateUserResponse, }; use tonic::{Request, Response, Status}; @@ -144,6 +144,25 @@ impl UserService for UserServiceImpl { })) } + async fn list_role_memberships( + &self, + request: Request, + ) -> Result, Status> { + let req = request.into_inner(); + let member_ids = req + .member_ids + .iter() + .map(|id| UserId::from(*id)) + .collect_vec(); + let memberships = self + .metadata_manager + .catalog_controller + .list_role_memberships(&member_ids) + .await?; + + Ok(Response::new(ListRoleMembershipsResponse { memberships })) + } + async fn alter_default_privilege( &self, request: Request, diff --git a/src/meta/src/backup_restore/restore_impl/v2.rs b/src/meta/src/backup_restore/restore_impl/v2.rs index eab3ccbb971a2..49291a8f3c904 100644 --- a/src/meta/src/backup_restore/restore_impl/v2.rs +++ b/src/meta/src/backup_restore/restore_impl/v2.rs @@ -109,6 +109,15 @@ impl Writer for WriterModelV2ToMetaStoreV2 { insert_models(metadata.workers.clone(), db).await?; insert_models(metadata.worker_properties.clone(), db).await?; insert_models(metadata.users.clone(), db).await?; + insert_models( + metadata + .user_role_memberships + .iter() + .sorted_by_key(|m| m.id) + .cloned(), + db, + ) + .await?; // The sort is required to pass table's foreign key check. use risingwave_meta_model::object::ObjectType; insert_models( @@ -214,6 +223,7 @@ macro_rules! for_all_auto_increment { {"worker", workers, worker_id}, {"object", objects, oid}, {"user", users, user_id}, + {"user_role_membership", user_role_memberships, id}, {"user_privilege", user_privileges, id}, {"fragment", fragments, fragment_id}, {"object_dependency", object_dependencies, id} diff --git a/src/meta/src/controller/user.rs b/src/meta/src/controller/user.rs index 328f64d8756d8..48d496141a9db 100644 --- a/src/meta/src/controller/user.rs +++ b/src/meta/src/controller/user.rs @@ -17,18 +17,20 @@ use std::collections::{HashMap, HashSet}; use itertools::Itertools; use risingwave_common::catalog::{DEFAULT_SUPER_USER, DEFAULT_SUPER_USER_FOR_PG}; use risingwave_meta_model::object::ObjectType; -use risingwave_meta_model::prelude::{Object, User, UserDefaultPrivilege, UserPrivilege}; +use risingwave_meta_model::prelude::{ + Object, User, UserDefaultPrivilege, UserPrivilege, UserRoleMembership, +}; use risingwave_meta_model::user_privilege::Action; use risingwave_meta_model::{ AuthInfo, DatabaseId, DefaultPrivilegeId, PrivilegeId, SchemaId, UserId, object, user, - user_default_privilege, user_privilege, + user_default_privilege, user_privilege, user_role_membership, }; use risingwave_pb::common::PbObjectType; use risingwave_pb::meta::subscribe_response::{ Info as NotificationInfo, Operation as NotificationOperation, }; use risingwave_pb::user::update_user_request::PbUpdateField; -use risingwave_pb::user::{PbAction, PbGrantPrivilege, PbUserInfo}; +use risingwave_pb::user::{PbAction, PbGrantPrivilege, PbRoleMembership, PbUserInfo}; use sea_orm::ActiveValue::Set; use sea_orm::sea_query::{OnConflict, SimpleExpr, Value}; use sea_orm::{ @@ -127,6 +129,7 @@ impl CatalogController { } PbUpdateField::Rename => user.name = Set(update_user.name.clone()), PbUpdateField::Admin => user.is_admin = Set(update_user.is_admin), + PbUpdateField::Inherit => user.can_inherit = Set(update_user.can_inherit), }); let user = user.update(&inner.db).await?; @@ -142,6 +145,22 @@ impl CatalogController { Ok(version) } + pub async fn list_role_memberships( + &self, + member_ids: &[UserId], + ) -> MetaResult> { + 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()) + } + #[cfg(test)] pub async fn get_user(&self, id: UserId) -> MetaResult { let inner = self.inner.read().await; diff --git a/src/rpc_client/src/meta_client.rs b/src/rpc_client/src/meta_client.rs index 452c2757a9579..52b27e3ffbc72 100644 --- a/src/rpc_client/src/meta_client.rs +++ b/src/rpc_client/src/meta_client.rs @@ -1029,6 +1029,17 @@ impl MetaClient { Ok(resp.version) } + pub async fn list_role_memberships( + &self, + member_ids: Vec, + ) -> Result> { + let request = ListRoleMembershipsRequest { + member_ids: member_ids.into_iter().map(|id| id.as_raw_id()).collect(), + }; + let resp = self.inner.list_role_memberships(request).await?; + Ok(resp.memberships) + } + pub async fn alter_default_privilege( &self, user_ids: Vec, @@ -2751,6 +2762,7 @@ macro_rules! for_all_meta_rpc { ,{ user_client, drop_user, DropUserRequest, DropUserResponse } ,{ user_client, grant_privilege, GrantPrivilegeRequest, GrantPrivilegeResponse } ,{ user_client, revoke_privilege, RevokePrivilegeRequest, RevokePrivilegeResponse } + ,{ user_client, list_role_memberships, ListRoleMembershipsRequest, ListRoleMembershipsResponse } ,{ user_client, alter_default_privilege, AlterDefaultPrivilegeRequest, AlterDefaultPrivilegeResponse } ,{ scale_client, get_cluster_info, GetClusterInfoRequest, GetClusterInfoResponse } ,{ scale_client, reschedule, RescheduleRequest, RescheduleResponse } diff --git a/src/storage/backup/src/meta_snapshot_v2.rs b/src/storage/backup/src/meta_snapshot_v2.rs index 91b476f61b971..75df1d1a93674 100644 --- a/src/storage/backup/src/meta_snapshot_v2.rs +++ b/src/storage/backup/src/meta_snapshot_v2.rs @@ -74,7 +74,8 @@ macro_rules! for_all_metadata_models_v2 { {pending_sink_state, risingwave_meta_model::pending_sink_state}, {refresh_jobs, risingwave_meta_model::refresh_job}, {cdc_table_snapshot_splits, risingwave_meta_model::cdc_table_snapshot_split}, - {hummock_table_change_logs, risingwave_meta_model::hummock_table_change_log} + {hummock_table_change_logs, risingwave_meta_model::hummock_table_change_log}, + {user_role_memberships, risingwave_meta_model::user_role_membership} } }; }