diff --git a/server/src/domain/handler.rs b/server/src/domain/handler.rs index bec8186..0efc41f 100644 --- a/server/src/domain/handler.rs +++ b/server/src/domain/handler.rs @@ -34,7 +34,7 @@ impl From for UserId { } } -#[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] +#[derive(PartialEq, Eq, Debug, Clone, Serialize, Deserialize)] #[cfg_attr(not(target_arch = "wasm32"), derive(sqlx::FromRow))] pub struct User { pub user_id: UserId, @@ -134,9 +134,19 @@ pub struct GroupId(pub i32); #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, sqlx::FromRow)] pub struct GroupIdAndName(pub GroupId, pub String); +#[derive(Debug, Clone, PartialEq)] +pub struct UserAndGroups { + pub user: User, + pub groups: Option>, +} + #[async_trait] pub trait BackendHandler: Clone + Send { - async fn list_users(&self, filters: Option) -> Result>; + async fn list_users( + &self, + filters: Option, + get_groups: bool, + ) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; async fn get_group_details(&self, group_id: GroupId) -> Result; @@ -159,7 +169,7 @@ mockall::mock! { } #[async_trait] impl BackendHandler for TestBackendHandler { - async fn list_users(&self, filters: Option) -> Result>; + async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; async fn get_group_details(&self, group_id: GroupId) -> Result; diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index e8ee667..1b2c739 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -3,7 +3,7 @@ use crate::infra::configuration::Configuration; use async_trait::async_trait; use futures_util::StreamExt; use sea_query::{Expr, Iden, Order, Query, SimpleExpr}; -use sqlx::Row; +use sqlx::{FromRow, Row}; use std::collections::HashSet; #[derive(Debug, Clone)] @@ -109,7 +109,11 @@ fn get_group_filter_expr(filter: GroupRequestFilter) -> SimpleExpr { #[async_trait] impl BackendHandler for SqlBackendHandler { - async fn list_users(&self, filters: Option) -> Result> { + async fn list_users( + &self, + filters: Option, + get_groups: bool, + ) -> Result> { let query = { let mut query_builder = Query::select() .column((Users::Table, Users::UserId)) @@ -122,6 +126,26 @@ impl BackendHandler for SqlBackendHandler { .from(Users::Table) .order_by((Users::Table, Users::UserId), Order::Asc) .to_owned(); + let add_join_group_tables = |builder: &mut sea_query::SelectStatement| { + builder + .left_join( + Memberships::Table, + Expr::tbl(Users::Table, Users::UserId) + .equals(Memberships::Table, Memberships::UserId), + ) + .left_join( + Groups::Table, + Expr::tbl(Memberships::Table, Memberships::GroupId) + .equals(Groups::Table, Groups::GroupId), + ); + }; + if get_groups { + add_join_group_tables(&mut query_builder); + query_builder + .column((Groups::Table, Groups::GroupId)) + .column((Groups::Table, Groups::DisplayName)) + .order_by((Groups::Table, Groups::DisplayName), Order::Asc); + } if let Some(filter) = filters { if filter == UserRequestFilter::Not(Box::new(UserRequestFilter::And(Vec::new()))) { return Ok(Vec::new()); @@ -131,31 +155,48 @@ impl BackendHandler for SqlBackendHandler { { let (RequiresGroup(requires_group), condition) = get_user_filter_expr(filter); query_builder.and_where(condition); - if requires_group { - query_builder - .left_join( - Memberships::Table, - Expr::tbl(Users::Table, Users::UserId) - .equals(Memberships::Table, Memberships::UserId), - ) - .left_join( - Groups::Table, - Expr::tbl(Memberships::Table, Memberships::GroupId) - .equals(Groups::Table, Groups::GroupId), - ); + if requires_group && !get_groups { + add_join_group_tables(&mut query_builder); } } } query_builder.to_string(DbQueryBuilder {}) }; + log::error!("query: {}", &query); - let results = sqlx::query_as::<_, User>(&query) - .fetch(&self.sql_pool) - .collect::>>() - .await; + // For group_by. + use itertools::Itertools; + let mut users = Vec::new(); + // The rows are returned sorted by user_id. We group them by + // this key which gives us one element (`rows`) per group. + for (_, rows) in &sqlx::query(&query) + .fetch_all(&self.sql_pool) + .await? + .into_iter() + .group_by(|row| row.get::(&*Users::UserId.to_string())) + { + let mut rows = rows.peekable(); + users.push(UserAndGroups { + user: User::from_row(rows.peek().unwrap()).unwrap(), + groups: if get_groups { + Some( + rows.map(|row| { + GroupIdAndName( + GroupId(row.get::(&*Groups::GroupId.to_string())), + row.get::(&*Groups::DisplayName.to_string()), + ) + }) + .filter(|g| !g.1.is_empty()) + .collect(), + ) + } else { + None + }, + }); + } - Ok(results.into_iter().collect::>>()?) + Ok(users) } async fn list_groups(&self, filters: Option) -> Result> { @@ -486,6 +527,19 @@ mod tests { .unwrap(); } + async fn get_user_names( + handler: &SqlBackendHandler, + filters: Option, + ) -> Vec { + handler + .list_users(filters, false) + .await + .unwrap() + .into_iter() + .map(|u| u.user.user_id.to_string()) + .collect::>() + } + #[tokio::test] async fn test_bind_admin() { let sql_pool = get_in_memory_db().await; @@ -558,50 +612,70 @@ mod tests { insert_user(&handler, "bob", "bob00").await; insert_user(&handler, "patrick", "pass").await; insert_user(&handler, "John", "Pa33w0rd!").await; + let group_1 = insert_group(&handler, "Best Group").await; + let group_2 = insert_group(&handler, "Worst Group").await; + insert_membership(&handler, group_1, "bob").await; + insert_membership(&handler, group_1, "patrick").await; + insert_membership(&handler, group_2, "patrick").await; + insert_membership(&handler, group_2, "John").await; { - let users = handler - .list_users(None) - .await - .unwrap() - .into_iter() - .map(|u| u.user_id.to_string()) - .collect::>(); + let users = get_user_names(&handler, None).await; assert_eq!(users, vec!["bob", "john", "patrick"]); } { - let users = handler - .list_users(Some(UserRequestFilter::UserId(UserId::new("bob")))) - .await - .unwrap() - .into_iter() - .map(|u| u.user_id.to_string()) - .collect::>(); + let users = get_user_names( + &handler, + Some(UserRequestFilter::UserId(UserId::new("bob"))), + ) + .await; assert_eq!(users, vec!["bob"]); } { - let users = handler - .list_users(Some(UserRequestFilter::Or(vec![ + let users = get_user_names( + &handler, + Some(UserRequestFilter::Or(vec![ UserRequestFilter::UserId(UserId::new("bob")), UserRequestFilter::UserId(UserId::new("John")), - ]))) - .await - .unwrap() - .into_iter() - .map(|u| u.user_id.to_string()) - .collect::>(); + ])), + ) + .await; assert_eq!(users, vec!["bob", "john"]); } + { + let users = get_user_names( + &handler, + Some(UserRequestFilter::Not(Box::new(UserRequestFilter::UserId( + UserId::new("bob"), + )))), + ) + .await; + assert_eq!(users, vec!["john", "patrick"]); + } { let users = handler - .list_users(Some(UserRequestFilter::Not(Box::new( - UserRequestFilter::UserId(UserId::new("bob")), - )))) + .list_users(None, true) .await .unwrap() .into_iter() - .map(|u| u.user_id.to_string()) + .map(|u| { + ( + u.user.user_id.to_string(), + u.groups + .unwrap() + .into_iter() + .map(|g| g.0) + .collect::>(), + ) + }) .collect::>(); - assert_eq!(users, vec!["john", "patrick"]); + assert_eq!( + users, + vec![ + ("bob".to_string(), vec![group_1]), + ("john".to_string(), vec![group_2]), + ("patrick".to_string(), vec![group_1, group_2]), + ] + ); } } @@ -763,29 +837,13 @@ mod tests { // Remove a user let _request_result = handler.delete_user(&UserId::new("Jennz")).await.unwrap(); - let users = handler - .list_users(None) - .await - .unwrap() - .into_iter() - .map(|u| u.user_id.to_string()) - .collect::>(); - - assert_eq!(users, vec!["hector", "val"]); + assert_eq!(get_user_names(&handler, None).await, vec!["hector", "val"]); // Insert new user and remove two insert_user(&handler, "NewBoi", "Joni").await; let _request_result = handler.delete_user(&UserId::new("Hector")).await.unwrap(); let _request_result = handler.delete_user(&UserId::new("NewBoi")).await.unwrap(); - let users = handler - .list_users(None) - .await - .unwrap() - .into_iter() - .map(|u| u.user_id.to_string()) - .collect::>(); - - assert_eq!(users, vec!["val"]); + assert_eq!(get_user_names(&handler, None).await, vec!["val"]); } } diff --git a/server/src/infra/graphql/query.rs b/server/src/infra/graphql/query.rs index 497f8f3..31f6312 100644 --- a/server/src/infra/graphql/query.rs +++ b/server/src/infra/graphql/query.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; type DomainRequestFilter = crate::domain::handler::UserRequestFilter; type DomainUser = crate::domain::handler::User; type DomainGroup = crate::domain::handler::Group; +type DomainUserAndGroups = crate::domain::handler::UserAndGroups; use super::api::Context; #[derive(PartialEq, Eq, Debug, GraphQLInputObject)] @@ -126,7 +127,7 @@ impl Query { } Ok(context .handler - .list_users(filters.map(TryInto::try_into).transpose()?) + .list_users(filters.map(TryInto::try_into).transpose()?, false) .await .map(|v| v.into_iter().map(Into::into).collect())?) } @@ -215,6 +216,15 @@ impl From for User { } } +impl From for User { + fn from(user: DomainUserAndGroups) -> Self { + Self { + user: user.user, + _phantom: std::marker::PhantomData, + } + } +} + #[derive(PartialEq, Eq, Debug, Serialize, Deserialize)] /// Represents a single group. pub struct Group { @@ -239,9 +249,10 @@ impl Group { } Ok(context .handler - .list_users(Some(DomainRequestFilter::MemberOfId(GroupId( - self.group_id, - )))) + .list_users( + Some(DomainRequestFilter::MemberOfId(GroupId(self.group_id))), + false, + ) .await .map(|v| v.into_iter().map(Into::into).collect())?) } @@ -365,21 +376,33 @@ mod tests { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::Or(vec![ - UserRequestFilter::Equality("id".to_string(), "bob".to_string()), - UserRequestFilter::Equality("email".to_string(), "robert@bobbers.on".to_string()), - ])))) - .return_once(|_| { + .with( + eq(Some(UserRequestFilter::Or(vec![ + UserRequestFilter::Equality("id".to_string(), "bob".to_string()), + UserRequestFilter::Equality( + "email".to_string(), + "robert@bobbers.on".to_string(), + ), + ]))), + eq(false), + ) + .return_once(|_, _| { Ok(vec![ - DomainUser { - user_id: UserId::new("bob"), - email: "bob@bobbers.on".to_string(), - ..Default::default() + DomainUserAndGroups { + user: DomainUser { + user_id: UserId::new("bob"), + email: "bob@bobbers.on".to_string(), + ..Default::default() + }, + groups: None, }, - DomainUser { - user_id: UserId::new("robert"), - email: "robert@bobbers.on".to_string(), - ..Default::default() + DomainUserAndGroups { + user: DomainUser { + user_id: UserId::new("robert"), + email: "robert@bobbers.on".to_string(), + ..Default::default() + }, + groups: None, }, ]) }); diff --git a/server/src/infra/ldap_handler.rs b/server/src/infra/ldap_handler.rs index 1b039af..1ffe061 100644 --- a/server/src/infra/ldap_handler.rs +++ b/server/src/infra/ldap_handler.rs @@ -1,8 +1,8 @@ use crate::{ domain::{ handler::{ - BackendHandler, BindRequest, Group, GroupRequestFilter, LoginHandler, User, UserId, - UserRequestFilter, + BackendHandler, BindRequest, Group, GroupIdAndName, GroupRequestFilter, LoginHandler, + User, UserId, UserRequestFilter, }, opaque_handler::OpaqueHandler, }, @@ -109,6 +109,8 @@ fn get_user_attribute( user: &User, attribute: &str, dn: &str, + base_dn_str: &str, + groups: Option<&[GroupIdAndName]>, ignored_user_attributes: &[String], ) -> Result>> { let attribute = attribute.to_ascii_lowercase(); @@ -124,6 +126,11 @@ fn get_user_attribute( "mail" => vec![user.email.clone()], "givenname" => vec![user.first_name.clone()], "sn" => vec![user.last_name.clone()], + "memberof" => groups + .into_iter() + .flatten() + .map(|id_and_name| format!("uid={},ou=groups,{}", &id_and_name.1, base_dn_str)) + .collect(), "cn" | "displayname" => vec![user.display_name.clone()], "createtimestamp" | "modifytimestamp" => vec![user.creation_date.to_rfc3339()], "1.1" => return Ok(None), @@ -179,17 +186,24 @@ fn make_ldap_search_user_result_entry( user: User, base_dn_str: &str, attributes: &[String], + groups: Option<&[GroupIdAndName]>, ignored_user_attributes: &[String], ) -> Result { let dn = format!("uid={},ou=people,{}", user.user_id.as_str(), base_dn_str); - let expanded_attributes = expand_attribute_wildcards(attributes, ALL_USER_ATTRIBUTE_KEYS); Ok(LdapSearchResultEntry { dn: dn.clone(), - attributes: expanded_attributes + attributes: attributes .iter() .filter_map(|a| { - let values = match get_user_attribute(&user, a, &dn, ignored_user_attributes) { + let values = match get_user_attribute( + &user, + a, + &dn, + base_dn_str, + groups, + ignored_user_attributes, + ) { Err(e) => return Some(Err(e)), Ok(v) => v, }?; @@ -625,7 +639,16 @@ impl LdapHandler users, Err(e) => { return vec![make_search_error( @@ -639,9 +662,10 @@ impl LdapHandler) -> Result>; + async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; async fn get_group_details(&self, group_id: GroupId) -> Result; @@ -1070,15 +1094,21 @@ mod tests { async fn test_search_regular_user() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::And(vec![]), - UserRequestFilter::UserId(UserId::new("test")), - ])))) + .with( + eq(Some(UserRequestFilter::And(vec![ + UserRequestFilter::And(vec![]), + UserRequestFilter::UserId(UserId::new("test")), + ]))), + eq(false), + ) .times(1) - .return_once(|_| { - Ok(vec![User { - user_id: UserId::new("test"), - ..Default::default() + .return_once(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("test"), + ..Default::default() + }, + groups: None, }]) }); let mut ldap_handler = setup_bound_handler_with_group(mock, "regular").await; @@ -1101,9 +1131,9 @@ mod tests { async fn test_search_readonly_user() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::And(vec![])))) + .with(eq(Some(UserRequestFilter::And(vec![]))), eq(false)) .times(1) - .return_once(|_| Ok(vec![])); + .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_readonly_handler(mock).await; let request = @@ -1114,6 +1144,42 @@ mod tests { ); } + #[tokio::test] + async fn test_search_member_of() { + let mut mock = MockTestBackendHandler::new(); + mock.expect_list_users() + .with(eq(Some(UserRequestFilter::And(vec![]))), eq(true)) + .times(1) + .return_once(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob"), + ..Default::default() + }, + groups: Some(vec![GroupIdAndName(GroupId(42), "rockstars".to_string())]), + }]) + }); + let mut ldap_handler = setup_bound_readonly_handler(mock).await; + + let request = make_user_search_request::( + LdapFilter::And(vec![]), + vec!["memberOf".to_string()], + ); + assert_eq!( + ldap_handler.do_search(&request).await, + vec![ + LdapOp::SearchResultEntry(LdapSearchResultEntry { + dn: "uid=bob,ou=people,dc=example,dc=com".to_string(), + attributes: vec![LdapPartialAttribute { + atype: "memberOf".to_string(), + vals: vec!["uid=rockstars,ou=groups,dc=example,dc=com".to_string()] + }], + }), + make_search_success(), + ], + ); + } + #[tokio::test] async fn test_bind_invalid_dn() { let mock = MockTestBackendHandler::new(); @@ -1199,23 +1265,29 @@ mod tests { async fn test_search_users() { use chrono::prelude::*; let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().times(1).return_once(|_| { + mock.expect_list_users().times(1).return_once(|_, _| { Ok(vec![ - User { - user_id: UserId::new("bob_1"), - email: "bob@bobmail.bob".to_string(), - display_name: "Bôb Böbberson".to_string(), - first_name: "Bôb".to_string(), - last_name: "Böbberson".to_string(), - ..Default::default() + UserAndGroups { + user: User { + user_id: UserId::new("bob_1"), + email: "bob@bobmail.bob".to_string(), + display_name: "Bôb Böbberson".to_string(), + first_name: "Bôb".to_string(), + last_name: "Böbberson".to_string(), + ..Default::default() + }, + groups: None, }, - User { - user_id: UserId::new("jim"), - email: "jim@cricket.jim".to_string(), - display_name: "Jimminy Cricket".to_string(), - first_name: "Jim".to_string(), - last_name: "Cricket".to_string(), - creation_date: Utc.ymd(2014, 7, 8).and_hms(9, 10, 11), + UserAndGroups { + user: User { + user_id: UserId::new("jim"), + email: "jim@cricket.jim".to_string(), + display_name: "Jimminy Cricket".to_string(), + first_name: "Jim".to_string(), + last_name: "Cricket".to_string(), + creation_date: Utc.ymd(2014, 7, 8).and_hms(9, 10, 11), + }, + groups: None, }, ]) }); @@ -1559,19 +1631,24 @@ mod tests { async fn test_search_filters() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::Or(vec![ - UserRequestFilter::Not(Box::new(UserRequestFilter::UserId(UserId::new("bob")))), - UserRequestFilter::And(vec![]), - UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), - UserRequestFilter::And(vec![]), - UserRequestFilter::And(vec![]), - UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), - UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), - ]), - ])))) + .with( + eq(Some(UserRequestFilter::And(vec![UserRequestFilter::Or( + vec![ + UserRequestFilter::Not(Box::new(UserRequestFilter::UserId(UserId::new( + "bob", + )))), + UserRequestFilter::And(vec![]), + UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), + UserRequestFilter::And(vec![]), + UserRequestFilter::And(vec![]), + UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), + UserRequestFilter::Not(Box::new(UserRequestFilter::And(vec![]))), + ], + )]))), + eq(false), + ) .times(1) - .return_once(|_| Ok(vec![])); + .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_admin_handler(mock).await; let request = make_user_search_request( LdapFilter::And(vec![LdapFilter::Or(vec![ @@ -1595,12 +1672,15 @@ mod tests { } #[tokio::test] - async fn test_search_member_of() { + async fn test_search_member_of_filter() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::MemberOf("group_1".to_string())))) + .with( + eq(Some(UserRequestFilter::MemberOf("group_1".to_string()))), + eq(false), + ) .times(1) - .return_once(|_| Ok(vec![])); + .return_once(|_, _| Ok(vec![])); let mut ldap_handler = setup_bound_admin_handler(mock).await; let request = make_user_search_request( LdapFilter::Equality( @@ -1644,16 +1724,22 @@ mod tests { async fn test_search_filters_lowercase() { let mut mock = MockTestBackendHandler::new(); mock.expect_list_users() - .with(eq(Some(UserRequestFilter::And(vec![ - UserRequestFilter::Or(vec![UserRequestFilter::Not(Box::new( - UserRequestFilter::Equality("first_name".to_string(), "bob".to_string()), - ))]), - ])))) + .with( + eq(Some(UserRequestFilter::And(vec![UserRequestFilter::Or( + vec![UserRequestFilter::Not(Box::new( + UserRequestFilter::Equality("first_name".to_string(), "bob".to_string()), + ))], + )]))), + eq(false), + ) .times(1) - .return_once(|_| { - Ok(vec![User { - user_id: UserId::new("bob_1"), - ..Default::default() + .return_once(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob_1"), + ..Default::default() + }, + groups: None, }]) }); let mut ldap_handler = setup_bound_admin_handler(mock).await; @@ -1686,14 +1772,17 @@ mod tests { #[tokio::test] async fn test_search_both() { let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().times(1).return_once(|_| { - Ok(vec![User { - user_id: UserId::new("bob_1"), - email: "bob@bobmail.bob".to_string(), - display_name: "Bôb Böbberson".to_string(), - first_name: "Bôb".to_string(), - last_name: "Böbberson".to_string(), - ..Default::default() + mock.expect_list_users().times(1).return_once(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob_1"), + email: "bob@bobmail.bob".to_string(), + display_name: "Bôb Böbberson".to_string(), + first_name: "Bôb".to_string(), + last_name: "Böbberson".to_string(), + ..Default::default() + }, + groups: None, }]) }); mock.expect_list_groups() @@ -1763,14 +1852,17 @@ mod tests { use chrono::TimeZone; let mut mock = MockTestBackendHandler::new(); - mock.expect_list_users().returning(|_| { - Ok(vec![User { - user_id: UserId::new("bob_1"), - email: "bob@bobmail.bob".to_string(), - display_name: "Bôb Böbberson".to_string(), - first_name: "Bôb".to_string(), - last_name: "Böbberson".to_string(), - ..Default::default() + mock.expect_list_users().returning(|_, _| { + Ok(vec![UserAndGroups { + user: User { + user_id: UserId::new("bob_1"), + email: "bob@bobmail.bob".to_string(), + display_name: "Bôb Böbberson".to_string(), + first_name: "Bôb".to_string(), + last_name: "Böbberson".to_string(), + ..Default::default() + }, + groups: None, }]) }); mock.expect_list_groups() diff --git a/server/src/infra/tcp_backend_handler.rs b/server/src/infra/tcp_backend_handler.rs index 0dee16c..c6f8d4d 100644 --- a/server/src/infra/tcp_backend_handler.rs +++ b/server/src/infra/tcp_backend_handler.rs @@ -35,7 +35,7 @@ mockall::mock! { } #[async_trait] impl BackendHandler for TestTcpBackendHandler { - async fn list_users(&self, filters: Option) -> Result>; + async fn list_users(&self, filters: Option, get_groups: bool) -> Result>; async fn list_groups(&self, filters: Option) -> Result>; async fn get_user_details(&self, user_id: &UserId) -> Result; async fn get_group_details(&self, group_id: GroupId) -> Result;