From 4d6c18988b3d81879fe393cbd7c055436e8f3695 Mon Sep 17 00:00:00 2001 From: Priec Date: Sun, 9 Aug 2026 12:56:57 +0200 Subject: [PATCH] dynamic rbac and roles --- client | 2 +- common/proto/auth.proto | 88 +++- common/src/proto/descriptor.bin | Bin 151941 -> 156384 bytes common/src/proto/komp_ac.auth.rs | 549 ++++++++++++++++++++- server | 2 +- web/src/pages/register/logic.rs | 1 - web/src/pages/register/state.rs | 2 - web/src/pages/register/suggestions.rs | 2 - web/src/pages/register/ui.rs | 2 - web/templates/pages/register/register.html | 9 +- 10 files changed, 630 insertions(+), 27 deletions(-) diff --git a/client b/client index 443d7b9a..7c491c38 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 443d7b9ab9113dacf1de73b457b53dd759e5b63d +Subproject commit 7c491c38cab7c363396efa6fcbab0d2c14b1d068 diff --git a/common/proto/auth.proto b/common/proto/auth.proto index 1d8bc78d..39aa6eb9 100644 --- a/common/proto/auth.proto +++ b/common/proto/auth.proto @@ -7,11 +7,25 @@ import "common.proto"; service AuthService { rpc Register(RegisterRequest) returns (AuthResponse); rpc Login(LoginRequest) returns (LoginResponse); + // Claims a bootstrap account that has never had a password set. + rpc SetInitialPassword(SetInitialPasswordRequest) returns (AuthResponse); rpc GetAuthorization(GetAuthorizationRequest) returns (AuthorizationSnapshot); rpc SetTimezone(SetTimezoneRequest) returns (UserPreferences); + + // Role administration. Every call requires the struct:role area, and every + // target role must rank strictly below the caller's own role. rpc ListRoles(ListRolesRequest) returns (ListRolesResponse); rpc AddRole(AddRoleRequest) returns (Role); rpc RemoveRole(RemoveRoleRequest) returns (Role); + + // Grant administration on the data plane. + rpc GrantPermission(GrantPermissionRequest) returns (RolePermissions); + rpc RevokePermission(RevokePermissionRequest) returns (RolePermissions); + rpc ListRolePermissions(ListRolePermissionsRequest) returns (RolePermissions); + + // User administration. + rpc AssignUserRole(AssignUserRoleRequest) returns (UserSummary); + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse); } message RegisterRequest { @@ -19,16 +33,21 @@ message RegisterRequest { string email = 2; string password = 3; string password_confirmation = 4; - string role = 5; - string timezone = 6; // IANA timezone, for example Europe/Bratislava - string phone_country = 7; // ISO 3166-1 alpha-2 country used for national phone numbers, for example SK + string timezone = 5; // IANA timezone, for example Europe/Bratislava + string phone_country = 6; // ISO 3166-1 alpha-2 country used for national phone numbers, for example SK } message AuthResponse { string id = 1; // UUID in string format string username = 2; // Registered username string email = 3; // Registered email (if provided) - string role = 4; // Default role: 'accountant' + string role = 4; // Always 'guest' for a self-registration +} + +message SetInitialPasswordRequest { + string username = 1; + string password = 2; + string password_confirmation = 3; } message LoginRequest { @@ -59,18 +78,31 @@ message UserPreferences { message GetAuthorizationRequest {} message Permission { - string resource = 1; + // Canonical object string, one of: + // struct: structural area, never grantable at runtime + // data:/ one root table and its whole template family + // data:/* every table in a profile, present and future + // data:* every table everywhere + // journal: one profile's accounting journal + // journal:* every profile's journal + string object = 1; + // manage for structural areas; read/insert/update/delete on the data plane. string action = 2; } message AuthorizationSnapshot { string role = 1; + // Every permission the role holds, inherited ones included. repeated Permission permissions = 2; } message Role { string name = 1; - bool built_in = 2; + // 'structural' (designs the system, never writes data) or 'data'. + string kind = 2; + bool built_in = 3; + // Role this one inherits every grant from; empty when it has no parent. + string parent = 4; } message ListRolesRequest {} @@ -81,8 +113,52 @@ message ListRolesResponse { message AddRoleRequest { string name = 1; + // Optional data role to inherit from. Must rank below the caller. + string parent = 2; } message RemoveRoleRequest { string name = 1; } + +message GrantPermissionRequest { + string role = 1; + string object = 2; + string action = 3; +} + +message RevokePermissionRequest { + string role = 1; + string object = 2; + string action = 3; +} + +message ListRolePermissionsRequest { + string role = 1; +} + +message RolePermissions { + string role = 1; + // Grants stored against this role alone, without inherited ones. + repeated Permission permissions = 2; + // Everything the role can actually do, inheritance resolved. + repeated Permission effective_permissions = 3; +} + +message AssignUserRoleRequest { + string username = 1; + string role = 2; +} + +message UserSummary { + string id = 1; + string username = 2; + string email = 3; + string role = 4; +} + +message ListUsersRequest {} + +message ListUsersResponse { + repeated UserSummary users = 1; +} diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index ef79f9073b3180826635268f6b1da6e81b481332..ff0f25cb5bfcca1fd15541287075fde9ee4d1590 100644 GIT binary patch delta 6773 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 TAeO(ydd+L!{^48p+UfrQ(}QtB delta 2720 zcmX|CO>Y}j6rFi99?v{~)PC_-94B?0G`8a;P8(=wNkW@cP1@4Xh9(v20zzvDxdt19Z6x!|V?H+V*c1os}h_L_k@n^ADrMb`ApMQG)LHn;yO?mN) zl5~`98rS`;=L=@q|MvMOy@a$KLkMMg;)+bl7y%Q=DTIj5ic=x7fKIK-fCeDN89uT= zlTJ27ph-0vB8qG;FwIt0LM-T`o*``T63!8hw82Ycsv!a|(VAmkOj$`W$wx8Jl1?f_ zpe56l5P_Dgx0vQAD*fQaNvk&fbJgi$aHMoDRdu3&lJeynMu?N##A= za?oth@|moBT{i`R1^>$zr>0``6;WtPg~(wHlmZ;SqbZn6(SP>h`b7tnVnm?{6}_;( z4k*R`vL1=4?6Pq*7zuT(?B?Vsq)ZygfXLK}_uexr(|`~hU_l^+4tNP&5)cFj(nn-X z6LD1WfBR>2yb^_za)m9M!bErlmM3*MW-;hb|J&{jM&UGrK^~ncXa<8AT~mhx3<*`M zRL^xj*gV+YuhJuNx?117d2{z*XMb~Nzb@GpO=u_*r3npjr%gSeA#|G7QYd@a80EdB z+{0)>9oYmmq2~Ya-$)OiP^+;T5W;F+uzEmnRVxI`tnc~=Zx;Y)BN2@nF~W_&8E7MD z1evguJ!VV?gCMJ8ZeEt)!6FZJfAeLohfj#rSqunabuX{01%g<;)Gv=}5{??8R8!UU zy@So|?K{=lVrR44>DGegSlU-3k|JLXE(d3jYM`7`S_%VdiehcT|NG_fX-m*`X^J>! zrFJ#lV7GAwVl!WmElmU%zqfAIVVp)W9%ZBY#y$ODpgy`Q<9#_<)+F4O7Ur9R@lLnC zr^g$5rmNG6ROsroc%YWPIxP&8-V=*FPVfjU(WCn_5%K8$Oz;d4iR=@6!!3dPGbzTo zHbX@C$o&uM^=W}Z72r?p!UIo&w`&=w+^Y4!j;{qKhaw9E^t zPup;nebzV^*q{cVbqBQ#muxKZ6Ab{pMb-mC;3DrC5abql&s?&xB*wH20POCPz_Rx6 z#|4OHEG2^L2}GfV;V={`OQXlMDNotwjSGP(4^ijcq&DS|sbw|=08`6s3J8JAYzhc+ z%WTRMRR0P$2MD|s;poIb;H|`i-T;BOg5J>Rq_Qp<1A1~2L6_W;c9&50W#g^DT>`&f zcKfxv1i4!kWvvT<+E=M|l%D7b1GTRvf;2b;Z#5Hi0tiW0@lesLmsIvu structural area, never grantable at runtime + /// data:/
one root table and its whole template family + /// data:/\* every table in a profile, present and future + /// data:\* every table everywhere + /// journal: one profile's accounting journal + /// journal:\* every profile's journal #[prost(string, tag = "1")] - pub resource: ::prost::alloc::string::String, + pub object: ::prost::alloc::string::String, + /// manage for structural areas; read/insert/update/delete on the data plane. #[prost(string, tag = "2")] pub action: ::prost::alloc::string::String, } @@ -91,6 +106,7 @@ pub struct Permission { pub struct AuthorizationSnapshot { #[prost(string, tag = "1")] pub role: ::prost::alloc::string::String, + /// Every permission the role holds, inherited ones included. #[prost(message, repeated, tag = "2")] pub permissions: ::prost::alloc::vec::Vec, } @@ -98,8 +114,14 @@ pub struct AuthorizationSnapshot { pub struct Role { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, - #[prost(bool, tag = "2")] + /// 'structural' (designs the system, never writes data) or 'data'. + #[prost(string, tag = "2")] + pub kind: ::prost::alloc::string::String, + #[prost(bool, tag = "3")] pub built_in: bool, + /// Role this one inherits every grant from; empty when it has no parent. + #[prost(string, tag = "4")] + pub parent: ::prost::alloc::string::String, } #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct ListRolesRequest {} @@ -112,12 +134,74 @@ pub struct ListRolesResponse { pub struct AddRoleRequest { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, + /// Optional data role to inherit from. Must rank below the caller. + #[prost(string, tag = "2")] + pub parent: ::prost::alloc::string::String, } #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct RemoveRoleRequest { #[prost(string, tag = "1")] pub name: ::prost::alloc::string::String, } +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GrantPermissionRequest { + #[prost(string, tag = "1")] + pub role: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub object: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub action: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RevokePermissionRequest { + #[prost(string, tag = "1")] + pub role: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub object: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub action: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListRolePermissionsRequest { + #[prost(string, tag = "1")] + pub role: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct RolePermissions { + #[prost(string, tag = "1")] + pub role: ::prost::alloc::string::String, + /// Grants stored against this role alone, without inherited ones. + #[prost(message, repeated, tag = "2")] + pub permissions: ::prost::alloc::vec::Vec, + /// Everything the role can actually do, inheritance resolved. + #[prost(message, repeated, tag = "3")] + pub effective_permissions: ::prost::alloc::vec::Vec, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct AssignUserRoleRequest { + #[prost(string, tag = "1")] + pub username: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub role: ::prost::alloc::string::String, +} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct UserSummary { + #[prost(string, tag = "1")] + pub id: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub username: ::prost::alloc::string::String, + #[prost(string, tag = "3")] + pub email: ::prost::alloc::string::String, + #[prost(string, tag = "4")] + pub role: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct ListUsersRequest {} +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ListUsersResponse { + #[prost(message, repeated, tag = "1")] + pub users: ::prost::alloc::vec::Vec, +} /// Generated client implementations. pub mod auth_service_client { #![allow( @@ -251,6 +335,30 @@ pub mod auth_service_client { .insert(GrpcMethod::new("komp_ac.auth.AuthService", "Login")); self.inner.unary(req, path, codec).await } + /// Claims a bootstrap account that has never had a password set. + pub async fn set_initial_password( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.auth.AuthService/SetInitialPassword", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.auth.AuthService", "SetInitialPassword"), + ); + self.inner.unary(req, path, codec).await + } pub async fn get_authorization( &mut self, request: impl tonic::IntoRequest, @@ -299,6 +407,8 @@ pub mod auth_service_client { .insert(GrpcMethod::new("komp_ac.auth.AuthService", "SetTimezone")); self.inner.unary(req, path, codec).await } + /// Role administration. Every call requires the struct:role area, and every + /// target role must rank strictly below the caller's own role. pub async fn list_roles( &mut self, request: impl tonic::IntoRequest, @@ -365,6 +475,127 @@ pub mod auth_service_client { .insert(GrpcMethod::new("komp_ac.auth.AuthService", "RemoveRole")); self.inner.unary(req, path, codec).await } + /// Grant administration on the data plane. + pub async fn grant_permission( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.auth.AuthService/GrantPermission", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("komp_ac.auth.AuthService", "GrantPermission")); + self.inner.unary(req, path, codec).await + } + pub async fn revoke_permission( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.auth.AuthService/RevokePermission", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("komp_ac.auth.AuthService", "RevokePermission")); + self.inner.unary(req, path, codec).await + } + pub async fn list_role_permissions( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.auth.AuthService/ListRolePermissions", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.auth.AuthService", "ListRolePermissions"), + ); + self.inner.unary(req, path, codec).await + } + /// User administration. + pub async fn assign_user_role( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result, tonic::Status> { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.auth.AuthService/AssignUserRole", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("komp_ac.auth.AuthService", "AssignUserRole")); + self.inner.unary(req, path, codec).await + } + pub async fn list_users( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/komp_ac.auth.AuthService/ListUsers", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("komp_ac.auth.AuthService", "ListUsers")); + self.inner.unary(req, path, codec).await + } } } /// Generated server implementations. @@ -388,6 +619,11 @@ pub mod auth_service_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; + /// Claims a bootstrap account that has never had a password set. + async fn set_initial_password( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; async fn get_authorization( &self, request: tonic::Request, @@ -399,6 +635,8 @@ pub mod auth_service_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; + /// Role administration. Every call requires the struct:role area, and every + /// target role must rank strictly below the caller's own role. async fn list_roles( &self, request: tonic::Request, @@ -414,6 +652,31 @@ pub mod auth_service_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; + /// Grant administration on the data plane. + async fn grant_permission( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn revoke_permission( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn list_role_permissions( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// User administration. + async fn assign_user_role( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + async fn list_users( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; } #[derive(Debug)] pub struct AuthServiceServer { @@ -579,6 +842,52 @@ pub mod auth_service_server { }; Box::pin(fut) } + "/komp_ac.auth.AuthService/SetInitialPassword" => { + #[allow(non_camel_case_types)] + struct SetInitialPasswordSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for SetInitialPasswordSvc { + type Response = super::AuthResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::set_initial_password(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = SetInitialPasswordSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } "/komp_ac.auth.AuthService/GetAuthorization" => { #[allow(non_camel_case_types)] struct GetAuthorizationSvc(pub Arc); @@ -804,6 +1113,232 @@ pub mod auth_service_server { }; Box::pin(fut) } + "/komp_ac.auth.AuthService/GrantPermission" => { + #[allow(non_camel_case_types)] + struct GrantPermissionSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for GrantPermissionSvc { + type Response = super::RolePermissions; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::grant_permission(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GrantPermissionSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.auth.AuthService/RevokePermission" => { + #[allow(non_camel_case_types)] + struct RevokePermissionSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for RevokePermissionSvc { + type Response = super::RolePermissions; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::revoke_permission(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = RevokePermissionSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.auth.AuthService/ListRolePermissions" => { + #[allow(non_camel_case_types)] + struct ListRolePermissionsSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for ListRolePermissionsSvc { + type Response = super::RolePermissions; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_role_permissions(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListRolePermissionsSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.auth.AuthService/AssignUserRole" => { + #[allow(non_camel_case_types)] + struct AssignUserRoleSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for AssignUserRoleSvc { + type Response = super::UserSummary; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::assign_user_role(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = AssignUserRoleSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/komp_ac.auth.AuthService/ListUsers" => { + #[allow(non_camel_case_types)] + struct ListUsersSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for ListUsersSvc { + type Response = super::ListUsersResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::list_users(&inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = ListUsersSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } _ => { Box::pin(async move { let mut response = http::Response::new( diff --git a/server b/server index f0945096..5dd123bf 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit f09450969170f8a098b38dff6c07d57ddf27b17e +Subproject commit 5dd123bf34e3254520e4d198494872601a34f8d0 diff --git a/web/src/pages/register/logic.rs b/web/src/pages/register/logic.rs index 89c01223..a1a8ab38 100644 --- a/web/src/pages/register/logic.rs +++ b/web/src/pages/register/logic.rs @@ -31,7 +31,6 @@ pub(crate) async fn register( email: input.email.trim().to_string(), password: input.password.trim().to_string(), password_confirmation: input.password_confirmation.trim().to_string(), - role: input.role.trim().to_string(), timezone: timezone.clone(), phone_country: phone_country.clone(), })) diff --git a/web/src/pages/register/state.rs b/web/src/pages/register/state.rs index d5359060..f5598be5 100644 --- a/web/src/pages/register/state.rs +++ b/web/src/pages/register/state.rs @@ -12,8 +12,6 @@ pub(crate) struct RegisterInput { #[serde(default)] pub password_confirmation: String, #[serde(default)] - pub role: String, - #[serde(default)] pub timezone: String, #[serde(default)] pub phone_country: String, diff --git a/web/src/pages/register/suggestions.rs b/web/src/pages/register/suggestions.rs index a77d5e75..82a26bc3 100644 --- a/web/src/pages/register/suggestions.rs +++ b/web/src/pages/register/suggestions.rs @@ -6,8 +6,6 @@ //! The client filters these itself as the field is typed into; here the lists //! ship whole in `` elements and the browser does the filtering. -pub(crate) const ROLES: &[&str] = &["admin", "moderator", "accountant", "viewer"]; - pub(crate) fn timezones() -> Vec { jiff::tz::db() .available() diff --git a/web/src/pages/register/ui.rs b/web/src/pages/register/ui.rs index 0d958c21..fe475ba7 100644 --- a/web/src/pages/register/ui.rs +++ b/web/src/pages/register/ui.rs @@ -12,7 +12,6 @@ use super::suggestions; #[template(path = "pages/register/register.html")] struct RegisterPage { nav: Nav, - roles: &'static [&'static str], timezones: Vec, phone_countries: Vec, } @@ -20,7 +19,6 @@ struct RegisterPage { pub(crate) fn render_page(nav: Nav) -> String { render(&RegisterPage { nav, - roles: suggestions::ROLES, timezones: suggestions::timezones(), phone_countries: suggestions::phone_countries(), }) diff --git a/web/templates/pages/register/register.html b/web/templates/pages/register/register.html index fdb30164..0a7c47c3 100644 --- a/web/templates/pages/register/register.html +++ b/web/templates/pages/register/register.html @@ -13,14 +13,13 @@ {# - The three fields the client offers suggestions for. A datalist keeps the + The two fields the client offers suggestions for. A datalist keeps the lists open — the backend, not this form, decides what is accepted — while still showing the same choices the client's suggestion popup does. + + There is no role field: everyone registers as `guest` and an admin + assigns a real role afterwards. #} - - - {% for role in roles %}{% endfor %} - {% for timezone in timezones %}{% endfor %}