From 40b49cdd100d0947defb511d44aa5380c187a25f Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 28 Jul 2023 18:25:54 +0200 Subject: [PATCH] Add a way to lock users --- crates/cli/src/commands/manage.rs | 52 +++++++++++++ crates/data-model/src/users.rs | 14 +++- crates/handlers/src/compat/login.rs | 2 + crates/handlers/src/oauth2/introspection.rs | 4 + crates/handlers/src/upstream_oauth2/link.rs | 6 +- crates/handlers/src/views/login.rs | 1 + ...e561e6521c45ce07d3a42411984c9a6b75fdc.json | 14 ++++ ...13b91fbccfe5fbdbead8c4868d52a61a0f9d.json} | 16 +++- ...4599f6374c96bb4a6827d400acb22fb0fd39.json} | 12 ++- ...a75d18e914f823902587b63c9f295407144b1.json | 15 ++++ ...e1a6ac868c95bfaee3a6960df1cf484d53da.json} | 12 ++- .../migrations/20230728154304_user_lock.sql | 19 +++++ crates/storage-pg/src/iden.rs | 2 + crates/storage-pg/src/user/mod.rs | 77 ++++++++++++++++++- crates/storage-pg/src/user/session.rs | 14 ++++ crates/storage/src/user/mod.rs | 29 +++++++ 16 files changed, 277 insertions(+), 12 deletions(-) create mode 100644 crates/storage-pg/.sqlx/query-22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc.json rename crates/storage-pg/.sqlx/{query-25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e.json => query-73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d.json} (66%) rename crates/storage-pg/.sqlx/{query-836fb7567d84057fa7f1edaab834c21a158a5762fe220b6bfacd6576be6c613c.json => query-bfa5eaeaa5b4574bb083c86711eb4599f6374c96bb4a6827d400acb22fb0fd39.json} (67%) create mode 100644 crates/storage-pg/.sqlx/query-c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1.json rename crates/storage-pg/.sqlx/{query-08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35.json => query-e0ea7d93ab3f565828b2faab4cc5e1a6ac868c95bfaee3a6960df1cf484d53da.json} (67%) create mode 100644 crates/storage-pg/migrations/20230728154304_user_lock.sql diff --git a/crates/cli/src/commands/manage.rs b/crates/cli/src/commands/manage.rs index c2c41e28..192b46c4 100644 --- a/crates/cli/src/commands/manage.rs +++ b/crates/cli/src/commands/manage.rs @@ -69,6 +69,18 @@ enum Subcommand { #[arg(long)] dry_run: bool, }, + + /// Lock a user + LockUser { + /// User to lock + username: String, + }, + + /// Unlock a user + UnlockUser { + /// User to unlock + username: String, + }, } impl Options { @@ -330,6 +342,46 @@ impl Options { Ok(()) } + + SC::LockUser { username } => { + let _span = info_span!("cli.manage.lock_user", user.username = username).entered(); + let config: DatabaseConfig = root.load_config()?; + let pool = database_from_config(&config).await?; + let mut repo = PgRepository::from_pool(&pool).await?.boxed(); + + let user = repo + .user() + .find_by_username(&username) + .await? + .context("User not found")?; + + info!(%user.id, "Locking user"); + + repo.user().lock(&clock, user).await?; + repo.save().await?; + + Ok(()) + } + + SC::UnlockUser { username } => { + let _span = info_span!("cli.manage.lock_user", user.username = username).entered(); + let config: DatabaseConfig = root.load_config()?; + let pool = database_from_config(&config).await?; + let mut repo = PgRepository::from_pool(&pool).await?.boxed(); + + let user = repo + .user() + .find_by_username(&username) + .await? + .context("User not found")?; + + info!(%user.id, "Unlocking user"); + + repo.user().unlock(user).await?; + repo.save().await?; + + Ok(()) + } } } } diff --git a/crates/data-model/src/users.rs b/crates/data-model/src/users.rs index d53a688b..9858356f 100644 --- a/crates/data-model/src/users.rs +++ b/crates/data-model/src/users.rs @@ -25,6 +25,16 @@ pub struct User { pub username: String, pub sub: String, pub primary_user_email_id: Option, + pub created_at: DateTime, + pub locked_at: Option>, +} + +impl User { + /// Returns `true` unless the user is locked. + #[must_use] + pub fn is_valid(&self) -> bool { + self.locked_at.is_none() + } } impl User { @@ -35,6 +45,8 @@ impl User { username: "john".to_owned(), sub: "123-456".to_owned(), primary_user_email_id: None, + created_at: now, + locked_at: None, }] } } @@ -65,7 +77,7 @@ pub struct BrowserSession { impl BrowserSession { #[must_use] pub fn active(&self) -> bool { - self.finished_at.is_none() + self.finished_at.is_none() && self.user.is_valid() } } diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 2541ad6a..cbf365ab 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -335,6 +335,7 @@ async fn token_login( .user() .lookup(session.user_id) .await? + .filter(mas_data_model::User::is_valid) .ok_or(RouteError::UserNotFound)?; repo.compat_sso_login().exchange(clock, login).await?; @@ -355,6 +356,7 @@ async fn user_password_login( .user() .find_by_username(&username) .await? + .filter(mas_data_model::User::is_valid) .ok_or(RouteError::UserNotFound)?; // Lookup its password diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 73049851..6dc58233 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -188,6 +188,7 @@ pub(crate) async fn post( .browser_session() .lookup(session.user_session_id) .await? + .filter(|b| b.user.is_valid()) // XXX: is that the right error to bubble up? .ok_or(RouteError::UnknownToken)?; @@ -227,6 +228,7 @@ pub(crate) async fn post( .browser_session() .lookup(session.user_session_id) .await? + .filter(|b| b.user.is_valid()) // XXX: is that the right error to bubble up? .ok_or(RouteError::UnknownToken)?; @@ -265,6 +267,7 @@ pub(crate) async fn post( .user() .lookup(session.user_id) .await? + .filter(mas_data_model::User::is_valid) // XXX: is that the right error to bubble up? .ok_or(RouteError::UnknownToken)?; @@ -311,6 +314,7 @@ pub(crate) async fn post( .user() .lookup(session.user_id) .await? + .filter(mas_data_model::User::is_valid) // XXX: is that the right error to bubble up? .ok_or(RouteError::UnknownToken)?; diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index b5d97578..6320adf8 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -23,7 +23,7 @@ use mas_axum_utils::{ csrf::{CsrfExt, ProtectedForm}, SessionInfoExt, }; -use mas_data_model::UpstreamOAuthProviderImportPreference; +use mas_data_model::{UpstreamOAuthProviderImportPreference, User}; use mas_jose::jwt::Jwt; use mas_keystore::Encrypter; use mas_storage::{ @@ -239,6 +239,8 @@ pub(crate) async fn get( .user() .lookup(user_id) .await? + // XXX: is that right? + .filter(User::is_valid) .ok_or(RouteError::UserNotFound)?; let ctx = UpstreamExistingLinkContext::new(user) @@ -263,6 +265,7 @@ pub(crate) async fn get( .user() .lookup(user_id) .await? + .filter(mas_data_model::User::is_valid) .ok_or(RouteError::UserNotFound)?; let ctx = UpstreamExistingLinkContext::new(user).with_csrf(csrf_token.form_value()); @@ -390,6 +393,7 @@ pub(crate) async fn post( .user() .lookup(user_id) .await? + .filter(mas_data_model::User::is_valid) .ok_or(RouteError::UserNotFound)?; repo.browser_session().add(&mut rng, &clock, &user).await? diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index fd4a07e2..f9168003 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -202,6 +202,7 @@ async fn login( .find_by_username(username) .await .map_err(|_e| FormError::Internal)? + .filter(mas_data_model::User::is_valid) .ok_or(FormError::InvalidCredentials)?; // And its password diff --git a/crates/storage-pg/.sqlx/query-22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc.json b/crates/storage-pg/.sqlx/query-22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc.json new file mode 100644 index 00000000..8d157756 --- /dev/null +++ b/crates/storage-pg/.sqlx/query-22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc.json @@ -0,0 +1,14 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE users\n SET locked_at = NULL\n WHERE user_id = $1\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "22896e8f2a002f307089c3e0f9ee561e6521c45ce07d3a42411984c9a6b75fdc" +} diff --git a/crates/storage-pg/.sqlx/query-25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e.json b/crates/storage-pg/.sqlx/query-73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d.json similarity index 66% rename from crates/storage-pg/.sqlx/query-25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e.json rename to crates/storage-pg/.sqlx/query-73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d.json index 6ea5b01e..14662495 100644 --- a/crates/storage-pg/.sqlx/query-25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e.json +++ b/crates/storage-pg/.sqlx/query-73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", + "query": "\n SELECT s.user_session_id\n , s.created_at AS \"user_session_created_at\"\n , s.finished_at AS \"user_session_finished_at\"\n , u.user_id\n , u.username AS \"user_username\"\n , u.primary_user_email_id AS \"user_primary_user_email_id\"\n , u.created_at AS \"user_created_at\"\n , u.locked_at AS \"user_locked_at\"\n FROM user_sessions s\n INNER JOIN users u\n USING (user_id)\n WHERE s.user_session_id = $1\n ", "describe": { "columns": [ { @@ -32,6 +32,16 @@ "ordinal": 5, "name": "user_primary_user_email_id", "type_info": "Uuid" + }, + { + "ordinal": 6, + "name": "user_created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 7, + "name": "user_locked_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -45,8 +55,10 @@ true, false, false, + true, + false, true ] }, - "hash": "25d61a373560556deafe056c8cd2982ac472f5ec2fab08b0b5275c4b78c11a7e" + "hash": "73fe61f03a41778c6273b1c2dbdb13b91fbccfe5fbdbead8c4868d52a61a0f9d" } diff --git a/crates/storage-pg/.sqlx/query-836fb7567d84057fa7f1edaab834c21a158a5762fe220b6bfacd6576be6c613c.json b/crates/storage-pg/.sqlx/query-bfa5eaeaa5b4574bb083c86711eb4599f6374c96bb4a6827d400acb22fb0fd39.json similarity index 67% rename from crates/storage-pg/.sqlx/query-836fb7567d84057fa7f1edaab834c21a158a5762fe220b6bfacd6576be6c613c.json rename to crates/storage-pg/.sqlx/query-bfa5eaeaa5b4574bb083c86711eb4599f6374c96bb4a6827d400acb22fb0fd39.json index aa1a90fe..f70906f2 100644 --- a/crates/storage-pg/.sqlx/query-836fb7567d84057fa7f1edaab834c21a158a5762fe220b6bfacd6576be6c613c.json +++ b/crates/storage-pg/.sqlx/query-bfa5eaeaa5b4574bb083c86711eb4599f6374c96bb4a6827d400acb22fb0fd39.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n FROM users\n WHERE username = $1\n ", + "query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n , locked_at\n FROM users\n WHERE username = $1\n ", "describe": { "columns": [ { @@ -22,6 +22,11 @@ "ordinal": 3, "name": "created_at", "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "locked_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -33,8 +38,9 @@ false, false, true, - false + false, + true ] }, - "hash": "836fb7567d84057fa7f1edaab834c21a158a5762fe220b6bfacd6576be6c613c" + "hash": "bfa5eaeaa5b4574bb083c86711eb4599f6374c96bb4a6827d400acb22fb0fd39" } diff --git a/crates/storage-pg/.sqlx/query-c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1.json b/crates/storage-pg/.sqlx/query-c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1.json new file mode 100644 index 00000000..3ba8612b --- /dev/null +++ b/crates/storage-pg/.sqlx/query-c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1.json @@ -0,0 +1,15 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE users\n SET locked_at = $1\n WHERE user_id = $2\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Timestamptz", + "Uuid" + ] + }, + "nullable": [] + }, + "hash": "c29fa41743811a6ac3a9b952b6ea75d18e914f823902587b63c9f295407144b1" +} diff --git a/crates/storage-pg/.sqlx/query-08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35.json b/crates/storage-pg/.sqlx/query-e0ea7d93ab3f565828b2faab4cc5e1a6ac868c95bfaee3a6960df1cf484d53da.json similarity index 67% rename from crates/storage-pg/.sqlx/query-08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35.json rename to crates/storage-pg/.sqlx/query-e0ea7d93ab3f565828b2faab4cc5e1a6ac868c95bfaee3a6960df1cf484d53da.json index 9a30425a..c38eb7db 100644 --- a/crates/storage-pg/.sqlx/query-08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35.json +++ b/crates/storage-pg/.sqlx/query-e0ea7d93ab3f565828b2faab4cc5e1a6ac868c95bfaee3a6960df1cf484d53da.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n FROM users\n WHERE user_id = $1\n ", + "query": "\n SELECT user_id\n , username\n , primary_user_email_id\n , created_at\n , locked_at\n FROM users\n WHERE user_id = $1\n ", "describe": { "columns": [ { @@ -22,6 +22,11 @@ "ordinal": 3, "name": "created_at", "type_info": "Timestamptz" + }, + { + "ordinal": 4, + "name": "locked_at", + "type_info": "Timestamptz" } ], "parameters": { @@ -33,8 +38,9 @@ false, false, true, - false + false, + true ] }, - "hash": "08d7df347c806ef14b6d0fb031cab041d79ba48528420160e23286369db7af35" + "hash": "e0ea7d93ab3f565828b2faab4cc5e1a6ac868c95bfaee3a6960df1cf484d53da" } diff --git a/crates/storage-pg/migrations/20230728154304_user_lock.sql b/crates/storage-pg/migrations/20230728154304_user_lock.sql new file mode 100644 index 00000000..a015c752 --- /dev/null +++ b/crates/storage-pg/migrations/20230728154304_user_lock.sql @@ -0,0 +1,19 @@ +-- Copyright 2023 The Matrix.org Foundation C.I.C. +-- +-- 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. + +-- Add a new column in on the `users` to record when an account gets locked +ALTER TABLE "users" + ADD COLUMN "locked_at" + TIMESTAMP WITH TIME ZONE + DEFAULT NULL; \ No newline at end of file diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index 29978ab8..c5175f9f 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -29,6 +29,8 @@ pub enum Users { UserId, Username, PrimaryUserEmailId, + CreatedAt, + LockedAt, } #[derive(sea_query::Iden)] diff --git a/crates/storage-pg/src/user/mod.rs b/crates/storage-pg/src/user/mod.rs index 468736e6..f5cf37d8 100644 --- a/crates/storage-pg/src/user/mod.rs +++ b/crates/storage-pg/src/user/mod.rs @@ -55,9 +55,8 @@ struct UserLookup { user_id: Uuid, username: String, primary_user_email_id: Option, - - #[allow(dead_code)] created_at: DateTime, + locked_at: Option>, } impl From for User { @@ -68,6 +67,8 @@ impl From for User { username: value.username, sub: id.to_string(), primary_user_email_id: value.primary_user_email_id.map(Into::into), + created_at: value.created_at, + locked_at: value.locked_at, } } } @@ -93,6 +94,7 @@ impl<'c> UserRepository for PgUserRepository<'c> { , username , primary_user_email_id , created_at + , locked_at FROM users WHERE user_id = $1 "#, @@ -124,6 +126,7 @@ impl<'c> UserRepository for PgUserRepository<'c> { , username , primary_user_email_id , created_at + , locked_at FROM users WHERE username = $1 "#, @@ -176,6 +179,8 @@ impl<'c> UserRepository for PgUserRepository<'c> { username, sub: id.to_string(), primary_user_email_id: None, + created_at, + locked_at: None, }) } @@ -203,4 +208,72 @@ impl<'c> UserRepository for PgUserRepository<'c> { Ok(exists) } + + #[tracing::instrument( + name = "db.user.lock", + skip_all, + fields( + db.statement, + %user.id, + ), + err, + )] + async fn lock(&mut self, clock: &dyn Clock, mut user: User) -> Result { + if user.locked_at.is_some() { + return Ok(user); + } + + let locked_at = clock.now(); + let res = sqlx::query!( + r#" + UPDATE users + SET locked_at = $1 + WHERE user_id = $2 + "#, + locked_at, + Uuid::from(user.id), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + user.locked_at = Some(locked_at); + + Ok(user) + } + + #[tracing::instrument( + name = "db.user.unlock", + skip_all, + fields( + db.statement, + %user.id, + ), + err, + )] + async fn unlock(&mut self, mut user: User) -> Result { + if user.locked_at.is_none() { + return Ok(user); + } + + let res = sqlx::query!( + r#" + UPDATE users + SET locked_at = NULL + WHERE user_id = $1 + "#, + Uuid::from(user.id), + ) + .traced() + .execute(&mut *self.conn) + .await?; + + DatabaseError::ensure_affected_rows(&res, 1)?; + + user.locked_at = None; + + Ok(user) + } } diff --git a/crates/storage-pg/src/user/session.rs b/crates/storage-pg/src/user/session.rs index c0afdc63..3b094c48 100644 --- a/crates/storage-pg/src/user/session.rs +++ b/crates/storage-pg/src/user/session.rs @@ -53,6 +53,8 @@ struct SessionLookup { user_id: Uuid, user_username: String, user_primary_user_email_id: Option, + user_created_at: DateTime, + user_locked_at: Option>, } impl TryFrom for BrowserSession { @@ -65,6 +67,8 @@ impl TryFrom for BrowserSession { username: value.user_username, sub: id.to_string(), primary_user_email_id: value.user_primary_user_email_id.map(Into::into), + created_at: value.user_created_at, + locked_at: value.user_locked_at, }; Ok(BrowserSession { @@ -99,6 +103,8 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { , u.user_id , u.username AS "user_username" , u.primary_user_email_id AS "user_primary_user_email_id" + , u.created_at AS "user_created_at" + , u.locked_at AS "user_locked_at" FROM user_sessions s INNER JOIN users u USING (user_id) @@ -232,6 +238,14 @@ impl<'c> BrowserSessionRepository for PgBrowserSessionRepository<'c> { Expr::col((Users::Table, Users::PrimaryUserEmailId)), SessionLookupIden::UserPrimaryUserEmailId, ) + .expr_as( + Expr::col((Users::Table, Users::CreatedAt)), + SessionLookupIden::UserCreatedAt, + ) + .expr_as( + Expr::col((Users::Table, Users::LockedAt)), + SessionLookupIden::UserLockedAt, + ) .from(UserSessions::Table) .inner_join( Users::Table, diff --git a/crates/storage/src/user/mod.rs b/crates/storage/src/user/mod.rs index 65709ecf..82ad656e 100644 --- a/crates/storage/src/user/mod.rs +++ b/crates/storage/src/user/mod.rs @@ -96,6 +96,33 @@ pub trait UserRepository: Send + Sync { /// /// Returns [`Self::Error`] if the underlying repository fails async fn exists(&mut self, username: &str) -> Result; + + /// Lock a [`User`] + /// + /// Returns the locked [`User`] + /// + /// # Parameters + /// + /// * `clock`: The clock used to generate timestamps + /// * `user`: The [`User`] to lock + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result; + + /// Unlock a [`User`] + /// + /// Returns the unlocked [`User`] + /// + /// # Parameters + /// + /// * `user`: The [`User`] to unlock + /// + /// # Errors + /// + /// Returns [`Self::Error`] if the underlying repository fails + async fn unlock(&mut self, user: User) -> Result; } repository_impl!(UserRepository: @@ -108,4 +135,6 @@ repository_impl!(UserRepository: username: String, ) -> Result; async fn exists(&mut self, username: &str) -> Result; + async fn lock(&mut self, clock: &dyn Clock, user: User) -> Result; + async fn unlock(&mut self, user: User) -> Result; );