@@ -16,6 +16,7 @@ use mas_data_model::{
1616 User ,
1717} ;
1818use mas_matrix:: HomeserverConnection ;
19+ use mas_policy:: { Policy , Requester , ViolationCode , model:: CompatLoginType } ;
1920use mas_storage:: {
2021 BoxRepository , BoxRepositoryFactory , RepositoryAccess ,
2122 compat:: {
@@ -37,6 +38,7 @@ use crate::{
3738 BoundActivityTracker , Limiter , METER , RequesterFingerprint , impl_from_error_for_route,
3839 passwords:: { PasswordManager , PasswordVerificationResult } ,
3940 rate_limit:: PasswordCheckLimitedError ,
41+ session:: count_user_sessions_for_limiting,
4042} ;
4143
4244static LOGIN_COUNTER : LazyLock < Counter < u64 > > = LazyLock :: new ( || {
@@ -213,9 +215,16 @@ pub enum RouteError {
213215
214216 #[ error( "failed to provision device" ) ]
215217 ProvisionDeviceFailed ( #[ source] anyhow:: Error ) ,
218+
219+ #[ error( "login rejected by policy" ) ]
220+ PolicyRejected ,
221+
222+ #[ error( "login rejected by policy (hard session limit reached)" ) ]
223+ PolicyHardSessionLimitReached ,
216224}
217225
218226impl_from_error_for_route ! ( mas_storage:: RepositoryError ) ;
227+ impl_from_error_for_route ! ( mas_policy:: EvaluationError ) ;
219228
220229impl From < anyhow:: Error > for RouteError {
221230 fn from ( err : anyhow:: Error ) -> Self {
@@ -274,6 +283,16 @@ impl IntoResponse for RouteError {
274283 error : "User account has been locked" ,
275284 status : StatusCode :: UNAUTHORIZED ,
276285 } ,
286+ Self :: PolicyRejected => MatrixError {
287+ errcode : "M_FORBIDDEN" ,
288+ error : "Login denied by the policy enforced by this service" ,
289+ status : StatusCode :: FORBIDDEN ,
290+ } ,
291+ Self :: PolicyHardSessionLimitReached => MatrixError {
292+ errcode : "M_FORBIDDEN" ,
293+ error : "You have reached your hard device limit. Please visit your account page to sign some out." ,
294+ status : StatusCode :: FORBIDDEN ,
295+ } ,
277296 } ;
278297
279298 ( sentry_event_id, response) . into_response ( )
@@ -290,6 +309,7 @@ pub(crate) async fn post(
290309 State ( homeserver) : State < Arc < dyn HomeserverConnection > > ,
291310 State ( site_config) : State < SiteConfig > ,
292311 State ( limiter) : State < Limiter > ,
312+ mut policy : Policy ,
293313 requester : RequesterFingerprint ,
294314 user_agent : Option < TypedHeader < headers:: UserAgent > > ,
295315 MatrixJsonBody ( input) : MatrixJsonBody < RequestBody > ,
@@ -329,6 +349,11 @@ pub(crate) async fn post(
329349 & limiter,
330350 requester,
331351 & mut repo,
352+ & mut policy,
353+ Requester {
354+ ip_address : activity_tracker. ip ( ) ,
355+ user_agent : user_agent. clone ( ) ,
356+ } ,
332357 username,
333358 password,
334359 input. device_id , // TODO check for validity
@@ -342,6 +367,11 @@ pub(crate) async fn post(
342367 & mut rng,
343368 & clock,
344369 & mut repo,
370+ & mut policy,
371+ Requester {
372+ ip_address : activity_tracker. ip ( ) ,
373+ user_agent : user_agent. clone ( ) ,
374+ } ,
345375 & token,
346376 input. device_id ,
347377 input. initial_device_display_name ,
@@ -459,6 +489,8 @@ async fn token_login(
459489 rng : & mut ( dyn RngCore + Send ) ,
460490 clock : & dyn Clock ,
461491 repo : & mut BoxRepository ,
492+ policy : & mut Policy ,
493+ requester : Requester ,
462494 token : & str ,
463495 requested_device_id : Option < String > ,
464496 initial_device_display_name : Option < String > ,
@@ -548,6 +580,27 @@ async fn token_login(
548580 . finish_sessions_to_replace_device ( clock, & browser_session. user , & device)
549581 . await ?;
550582
583+ let session_counts = count_user_sessions_for_limiting ( repo, & browser_session. user ) . await ?;
584+
585+ let res = policy
586+ . evaluate_compat_login ( mas_policy:: CompatLoginInput {
587+ user : & browser_session. user ,
588+ login_type : CompatLoginType :: WebSso ,
589+ session_counts,
590+ requester,
591+ } )
592+ . await ?;
593+ if !res. valid ( ) {
594+ if res. violations . len ( ) == 1 {
595+ let violation = & res. violations [ 0 ] ;
596+ if violation. code == Some ( ViolationCode :: TooManySessions ) {
597+ // The only violation is having reached the session limit.
598+ return Err ( RouteError :: PolicyHardSessionLimitReached ) ;
599+ }
600+ }
601+ return Err ( RouteError :: PolicyRejected ) ;
602+ }
603+
551604 // We first create the session in the database, commit the transaction, then
552605 // create it on the homeserver, scheduling a device sync job afterwards to
553606 // make sure we don't end up in an inconsistent state.
@@ -578,6 +631,8 @@ async fn user_password_login(
578631 limiter : & Limiter ,
579632 requester : RequesterFingerprint ,
580633 repo : & mut BoxRepository ,
634+ policy : & mut Policy ,
635+ policy_requester : Requester ,
581636 username : & str ,
582637 password : String ,
583638 requested_device_id : Option < String > ,
@@ -651,6 +706,27 @@ async fn user_password_login(
651706 . finish_sessions_to_replace_device ( clock, & user, & device)
652707 . await ?;
653708
709+ let session_counts = count_user_sessions_for_limiting ( repo, & user) . await ?;
710+
711+ let res = policy
712+ . evaluate_compat_login ( mas_policy:: CompatLoginInput {
713+ user : & user,
714+ login_type : CompatLoginType :: Password ,
715+ session_counts,
716+ requester : policy_requester,
717+ } )
718+ . await ?;
719+ if !res. valid ( ) {
720+ if res. violations . len ( ) == 1 {
721+ let violation = & res. violations [ 0 ] ;
722+ if violation. code == Some ( ViolationCode :: TooManySessions ) {
723+ // The only violation is having reached the session limit.
724+ return Err ( RouteError :: PolicyHardSessionLimitReached ) ;
725+ }
726+ }
727+ return Err ( RouteError :: PolicyRejected ) ;
728+ }
729+
654730 let session = repo
655731 . compat_session ( )
656732 . add (
0 commit comments