From a80b389a3f11c86f101f440d09583e3ace2daa32 Mon Sep 17 00:00:00 2001 From: Priec Date: Mon, 17 Aug 2026 13:27:54 +0200 Subject: [PATCH] auth revocation implemented --- Cargo.lock | 2 + client | 2 +- common/proto/auth.proto | 20 ++++ common/src/proto/descriptor.bin | Bin 184824 -> 185848 bytes common/src/proto/komp_ac.auth.rs | 174 +++++++++++++++++++++++++++++++ server | 2 +- 6 files changed, 198 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3cdea451..f48dc70b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6958,6 +6958,7 @@ dependencies = [ "flate2", "futures", "gtin-validate", + "http", "iban_validate", "jiff", "jiff-sqlx", @@ -6996,6 +6997,7 @@ dependencies = [ "tokio-test", "tonic", "tonic-reflection", + "tower", "tracing", "tracing-subscriber", "tui-canvas-validation-core", diff --git a/client b/client index 7319ea41..20316569 160000 --- a/client +++ b/client @@ -1 +1 @@ -Subproject commit 7319ea4178e68e0f7cdaf52ff1433e55bae91ed4 +Subproject commit 20316569327165c51760005e063513ba414458b3 diff --git a/common/proto/auth.proto b/common/proto/auth.proto index 9ee65430..f8c057cb 100644 --- a/common/proto/auth.proto +++ b/common/proto/auth.proto @@ -12,6 +12,16 @@ service AuthService { rpc GetAuthorization(GetAuthorizationRequest) returns (AuthorizationSnapshot); rpc SetTimezone(SetTimezoneRequest) returns (UserPreferences); + // Ends the caller's own sessions. Every token issued to them before this + // call, on every device, stops being accepted -- including the one used to + // make the call, so the caller must log in again afterwards. Discarding a + // token client-side is not a logout; this is. + rpc Logout(LogoutRequest) returns (LogoutResponse); + // Ends every session of another user, for a leaked token or a departing + // account. Requires the struct:user area, and the target must rank strictly + // below the caller. + rpc RevokeUserSessions(RevokeUserSessionsRequest) returns (RevokeUserSessionsResponse); + // 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); @@ -80,6 +90,16 @@ message UserPreferences { string timezone = 1; } +message LogoutRequest {} + +message LogoutResponse {} + +message RevokeUserSessionsRequest { + string username = 1; +} + +message RevokeUserSessionsResponse {} + message GetAuthorizationRequest {} message Permission { diff --git a/common/src/proto/descriptor.bin b/common/src/proto/descriptor.bin index 7d9b50831af6863895a9bba2cca43dbfdf323da1..96660fe24b585af8b7269a7e8467b3329d55542f 100644 GIT binary patch delta 6031 zcmai2`)?H26`pfp?PlX0W6Hy-CaA- zQl_ce5Cyf0+Q76SsX`irNYhfKK^BUrLX}YEQH3N`8X{F{`%C|Ys6X{PckUglN~Qi{ z?YHNA-+A0~AL}oFRsH3!tH1vy6>F)mFk4F<_PRQ^W=HXGajvq@|Mr|;uH@?7TJuGD zwm4Jvb0co+K7YP=)IU)6OW*LzC$=c%%*=>1-C#`6_n z(x0E2@cX@Tr8rxL=hVz$uP`wY__Gx#3=DWvGZWKull(OYFV6VhTv=(m-m$__R*h1E zgbJtg^N!7xE8cYRFa*59;R63XRPjqs7D|)lytjF(Jb_Otnya&(n4a=yDg))IN#6^m z%HB+|;uTl|!!xR~M`3x_UE(IWGlbSSOAHb~HCclaK;3KH-;-*hBNG{MFnf`rCPszp z&UwW{UICGg_$4l^)bF8JULo+NeN@LiWM%N;7t%~bLp5A{^Zk5;B! zDa}n(zQ!_Mq2w3(p$r8ys8T2$_A60ON`;xDXm@F9qB4ELMa!p)PsUyJOHqM$p+W~0 zW}FsXlY7I=$tdfarc%hSi7F2V? zw21+2N`cnq)EER>TWz~Rptbe%XgcHt~88ir- zp3(g}y?UAG6&tmw9<<(go$EpCjn}yzv|g;U>k8YFcAs?@;d8>a!0Aghvz%=SOD6h7 zCSW~AIBEJt${=|5ryC4{UVmeg=D{=|mTD7$HW1T@ZRuKL18tx|Y8o=k3$=QP#^+jI z(H`R90=#@2PJow>!vU-k%e7ej64ElGQ=b$8TTE`4pP(Om%@ z;@%F4`do8(zL(kufgwDk6@lP8l&)5J13_`9w$9BdCQQSj(@iT@4#)niJ*+b(LGT|& z#skV9Z5;{schjnsBe6fXb3`u#32x^ImH~r@%mCJfzv-rtU|sCbeO#ATo1Z<{rM^K` zD5O1VZHg*n)zMUkT28?gt`|}30Z`t0Ee8b0^=Y-^Y#_+3Z#OiUHVCz;0@!$ZHpDzG zb39Xk#Cua+ zswi99_gPz7qN;cr?!&tEpgT5KwK@Fv9du`~S?l9Fcx+Bb0}lkz&6#Kq*&KRI4>bU) z8H;IL&6r*^*gzY@qVbkYN_(rdFAC4YwKdh`wjg|x!*3JWT(vhIzS2XDE4OJiSm3u! z_XY@R+t8a%#e!)({Cf{|tsIa2Ir_LxI;o>=y^>zxGtMT|bhj&V;IPI%j z8=kxscg8B*{GEF6V1vp|3?3g(Nj@QV>DB@$6hy}pAb7iUZ-KzurO%6`z-7N%s6!Vb zjza2iWdz#pmTq071KRH1J9RkRpnKwQRS35y&XTXxJsj;2PRgXH0&h2Q`0>chQ?bq?xs0Fc%}Ee8b0gIW#SrqUe05KaTyyYv*$Gf{qS==mi}e2#y6E9SG42Iyx@< zNvm5$!BxL0F`3G#=(x=fSvmDR-{pr=oho{bw2xTjC_3NOM=-s=O(1*?ho4=j>Z8|a z5CGw4b$B3n&ZgD#h#e7rRxWiPR5X}M;XnFme^8_Law%rS@S@d(6@nJzVWK+rh8q8sfOcAZRq*ZKiPIq~b_yQw8=UFtZelR75=&JY%b zqX3~Oz^Eud_?{v3S6iWahHBB_-HHqAk10GcNL%iY9I!a}CZ=O`0Ci6!PYED;!bvTa z&U0k_R426*p3kB5@SZiaK45#Jbe<>c=i1%~`}36I3*WXIIq9<|X&^)ejFJX~?^%;H zAflcnwM8<+F6O00Di;H&r~#5<<(C$@T+K_Snv8T_ChHYlO$MGX6AnwRCd1WSFx3D- z7BI>P5WW{oF@R9L5EUcrX6d|2)@xAR$D5%UuCEd<6qapf*^5RN2(o~YEFgR@8d*Td zUSwH5J({_aOX1UNX#DH=j9qj|?IMgjAU4q@!lB0F3&^g7Q%ytYTp}E6<4Ok&yBx~3 zwB?azflXy!Uev*@4;brk$jg-J(>mO|%amV*QiRCo%Wb?t_zA#?10e_?PU8q-{clip)>U@{%;4Ujwwy5|$_>J=0*=xqop;FklaA5` z&vyv7HjjB5M|qd*v&;Quh>n~ zKUl%NX?9+_blxND&pM-ac)mv|{-=#?w{u4Cn~Z>v5nz-NAbj6983E!N1=cCG^J0Hs zZZIHl0S%Wk`hf78f~$cQ8GR7lSR58J^r2}t5U7BL%EBKKese`so|_M&_6EZ4kj_VB z{Y6*P0oRWx#dnTvcW^bgNJMiB1X;jH77)I-2tSNj77((x2)~RYDvTeSMs%oR01cIk z`527|G?%OS*fgSptNDcRPm4&Hs{u4zuI3Z7C*TX}C%h-3ut1-hgRevF3P3~UUGb?| zRam+IpYpQu+i;n*KQph6WpMn=ygE9i^Ev+Et2@*Q)92>Z(a9aUO(NPKK$vsDXwCsK i=eMaknomGbZ&P!-stXYRE4FW+UN%Y#%SOc)=l%mKxVuCE delta 5072 zcmYLNYi|_i6`u3X?09z8UVC4AFZN<%udnf9bFm4*rWhMA)I>O?j#|Ve0@;m$VpAJ| zh(d=X6(m)tB3Wc2QKdo%A<_^9T4JhjNu$so(W+njsXqXzt<>kdm+`lGe9rTn%X{97 z_vIfmFK=hA{FxTBba?e#mRh~lnnwDmFaDEWJ=#onY4t&w-gMJ{9SLh>ik!d2_3LSj z)~`;lr$x%#pwROjk&T1Df<3EE2NSmAQ(A;K_l_;j1cKAl-ai?xNu+04eY1 zjaPtJ`N9T^z{zi$G_lgsEr=;&N`qGL3l@P^C=OZ#T4DQsLkp!_6mJ+)2wKr^wn)gx zEw&6>2wrjQpy6etTM};>QwF?}-(nHMmCCPL1X^kQSlFp5tCen9Otci*-&~$sYCkc1 z_SC|uglf<9sCU*)pSPX2?U*Lwmz&Vf0#DJKk1{c{tbygD{e4(k|6kRK%N82 zA8j0n|IkMp`v($#Zr^}uI|**z0NRd0Lp}h5@ykBi77Zr;+_%BBItbW7AV$paDrlhyi*{{R%NbL;rVF}?=9DYo+R5I;TJAXFQetz-H+=4nw*aY!0BMt=G zm}wOd#K+L8^-2V$U2(Na9sRo!e@N0kPmiA0~U2b1m{NM&RmH2 z3aPV|5omi$-KIz%w7q?=m~go9CX;YA2sfEz$@kwRNBad1%9N-9Z!!so8*iT|8_58? zeF=}Rz(vye zr8ncerNi(wJ%e%i%EelxInL45e+|+Xv@3EFRzE35)mAajPk4hW7X&71*2 z?j+`HKavP}N6*G9n`uvkZ;bA&(6b7J;hD|o;|Yiz3ms~ufuJ$l)vZ#>NbhZDL8rtI zinoJWHB=c+Y2Ks+fRyGHs(kMV=qLl+HP2H9q_vIv12wm+GXq3x3I9@c(1cKZ}vjkaQf=lSic>DCZ_`)!asV3mh5V({G@M>H# zO@Ivom&};fOYdFh*E*kic)e??t>=6$n|uHuciG4R!SS-m2MBVPkxwnkYmkwDg`D3I zp(@vF!1Ln@1$-NOUIQe(_XwJGSa~2^H-LeK@O_VHljL)mWuKK zQ6H=kJA?Z^p+X%NZ-o!mxEMHwmdnZ?7%r$7IY31fUQYT~$@#4*CI`=}ghPf^b6m_d zTMQ6X0fR8&41wW%%~k^l*=x3%9LN07W&{K-pw0+TS#>o~;$l9u#pJMgypPtXTnwP4 zLO23WxLnOgwi+q@kIDI+sYW8o#}x4E#PcLqbKO=01X(~W3kcuqwirODUf0D4uTlD+ zkn^cgZNypReL_Jo{_imDP^UyASGy8l-9}Rr_{JM&g|Kh9O@MG;A)H+XvMb^6G7y1Q z2&dN+a%|+%Z^ZI7nmmSY=-gP-!EFkd=y1pzRHzyqZo>^4+=yC**ChSV$oaiVxCx%0 zQP3H_gAh&JhMV!D*XY7Qe4`0BZ4&_Dd6Uw51OcIWlZqv!nV0_OP~!lga$z53AwXIzfYMKz5Rf}y-($e^+uHYgvU2WDN6rq za{gqZ6yf1FmMu0jaK=^)RGXiuY#wMi_ zFZL?o%Hn*1zy-8i&S;hJRyS5;v`V<9bXd&LW83bMnj%0;W#Pw!e{D3Cr|7Y6ZzQ~C z>3>Vkccz+VxPD84K8u^VnkOXm+yWsZKrIUh-zS9sKUo$KvQG&AfO1C79P_Dd1Q4iz zmdeFEMI$1^<^DdkjcDeh;~C+%04>aW0?=|<`I+4l@CEf5?+G0i=(#-^o7JuWv{c>| z&+V$h%Kd-N%gT3si}b!HyfFACY=PtVgeL}1U|IS<;Kwu5p)yQA5bj-$SLP1AAffjM p5at|E&p9CG`~_w7d;)^{f{N=@U4Zy8%zJUIZH!ji#>C%0{Xg>!pIQI_ diff --git a/common/src/proto/komp_ac.auth.rs b/common/src/proto/komp_ac.auth.rs index bdc6dd63..b943e170 100644 --- a/common/src/proto/komp_ac.auth.rs +++ b/common/src/proto/komp_ac.auth.rs @@ -88,6 +88,17 @@ pub struct UserPreferences { pub timezone: ::prost::alloc::string::String, } #[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct LogoutRequest {} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct LogoutResponse {} +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RevokeUserSessionsRequest { + #[prost(string, tag = "1")] + pub username: ::prost::alloc::string::String, +} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct RevokeUserSessionsResponse {} +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] pub struct GetAuthorizationRequest {} #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] pub struct Permission { @@ -450,6 +461,60 @@ pub mod auth_service_client { .insert(GrpcMethod::new("komp_ac.auth.AuthService", "SetTimezone")); self.inner.unary(req, path, codec).await } + /// Ends the caller's own sessions. Every token issued to them before this + /// call, on every device, stops being accepted -- including the one used to + /// make the call, so the caller must log in again afterwards. Discarding a + /// token client-side is not a logout; this is. + pub async fn logout( + &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/Logout", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("komp_ac.auth.AuthService", "Logout")); + self.inner.unary(req, path, codec).await + } + /// Ends every session of another user, for a leaked token or a departing + /// account. Requires the struct:user area, and the target must rank strictly + /// below the caller. + pub async fn revoke_user_sessions( + &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/RevokeUserSessions", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("komp_ac.auth.AuthService", "RevokeUserSessions"), + ); + 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( @@ -734,6 +799,24 @@ pub mod auth_service_server { &self, request: tonic::Request, ) -> std::result::Result, tonic::Status>; + /// Ends the caller's own sessions. Every token issued to them before this + /// call, on every device, stops being accepted -- including the one used to + /// make the call, so the caller must log in again afterwards. Discarding a + /// token client-side is not a logout; this is. + async fn logout( + &self, + request: tonic::Request, + ) -> std::result::Result, tonic::Status>; + /// Ends every session of another user, for a leaked token or a departing + /// account. Requires the struct:user area, and the target must rank strictly + /// below the caller. + async fn revoke_user_sessions( + &self, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + 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( @@ -1091,6 +1174,97 @@ pub mod auth_service_server { }; Box::pin(fut) } + "/komp_ac.auth.AuthService/Logout" => { + #[allow(non_camel_case_types)] + struct LogoutSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for LogoutSvc { + type Response = super::LogoutResponse; + 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 { + ::logout(&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 = LogoutSvc(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/RevokeUserSessions" => { + #[allow(non_camel_case_types)] + struct RevokeUserSessionsSvc(pub Arc); + impl< + T: AuthService, + > tonic::server::UnaryService + for RevokeUserSessionsSvc { + type Response = super::RevokeUserSessionsResponse; + 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_user_sessions(&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 = RevokeUserSessionsSvc(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/ListRoles" => { #[allow(non_camel_case_types)] struct ListRolesSvc(pub Arc); diff --git a/server b/server index f3efeae0..891aa003 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit f3efeae04b782b5bef8cf5a4d9827933ac58190d +Subproject commit 891aa0038bb4c5083a36cc02470f088776078f99