2020-03-09 21:04:03 +00:00
use once_cell ::sync ::Lazy ;
2020-06-03 15:07:32 +00:00
use serde ::de ::DeserializeOwned ;
2020-07-14 16:00:09 +00:00
use serde_json ::Value ;
2021-11-28 12:02:27 +00:00
use std ::env ;
2018-12-18 00:53:21 +00:00
2021-11-07 17:53:39 +00:00
use rocket ::serde ::json ::Json ;
2020-07-14 16:00:09 +00:00
use rocket ::{
2021-11-07 17:53:39 +00:00
form ::Form ,
2022-11-26 23:00:01 +00:00
http ::{ Cookie , CookieJar , MediaType , SameSite , Status } ,
request ::{ FromRequest , Outcome , Request } ,
2022-09-06 15:14:16 +00:00
response ::{ content ::RawHtml as Html , Redirect } ,
2022-11-26 23:00:01 +00:00
Catcher , Route ,
2020-07-14 16:00:09 +00:00
} ;
2019-01-19 20:36:34 +00:00
2020-07-14 16:00:09 +00:00
use crate ::{
2023-06-11 11:28:18 +00:00
api ::{ core ::log_event , unregister_push_device , ApiResult , EmptyResult , JsonResult , Notify , NumberOrString } ,
2020-07-14 16:00:09 +00:00
auth ::{ decode_admin , encode_jwt , generate_admin_claims , ClientIp } ,
config ::ConfigBuilder ,
2021-03-27 23:10:01 +00:00
db ::{ backup_database , get_sql_server_version , models ::* , DbConn , DbConnType } ,
2020-07-14 16:00:09 +00:00
error ::{ Error , MapResult } ,
mail ,
2021-10-07 22:01:24 +00:00
util ::{
docker_base_image , format_naive_datetime_local , get_display_size , get_reqwest_client , is_running_in_docker ,
} ,
2022-01-23 22:40:59 +00:00
CONFIG , VERSION ,
2020-07-14 16:00:09 +00:00
} ;
2018-12-18 17:52:58 +00:00
2019-01-19 20:36:34 +00:00
pub fn routes ( ) -> Vec < Route > {
2020-04-10 03:55:08 +00:00
if ! CONFIG . disable_admin_token ( ) & & ! CONFIG . is_admin_token_set ( ) {
2019-02-10 14:26:19 +00:00
return routes! [ admin_disabled ] ;
2019-01-19 20:36:34 +00:00
}
2018-12-18 00:53:21 +00:00
2019-01-26 18:28:54 +00:00
routes! [
2020-05-28 08:42:36 +00:00
get_users_json ,
2021-05-08 20:03:03 +00:00
get_user_json ,
2023-02-20 15:39:56 +00:00
get_user_by_mail_json ,
2019-01-26 18:28:54 +00:00
post_admin_login ,
admin_page ,
2023-05-25 21:12:24 +00:00
admin_page_login ,
2019-01-26 18:28:54 +00:00
invite_user ,
2019-12-01 20:15:14 +00:00
logout ,
2019-01-26 18:28:54 +00:00
delete_user ,
deauth_user ,
2020-11-30 22:12:56 +00:00
disable_user ,
enable_user ,
2019-08-21 15:13:06 +00:00
remove_2fa ,
2021-02-03 17:43:54 +00:00
update_user_org_type ,
2019-03-07 20:08:33 +00:00
update_revision_users ,
2019-02-02 00:09:21 +00:00
post_config ,
2019-02-06 16:32:13 +00:00
delete_config ,
2019-05-03 13:46:29 +00:00
backup_db ,
2020-02-26 10:02:22 +00:00
test_smtp ,
2020-05-28 08:42:36 +00:00
users_overview ,
organizations_overview ,
2021-02-03 17:43:54 +00:00
delete_organization ,
2020-05-28 08:42:36 +00:00
diagnostics ,
2023-03-30 12:23:16 +00:00
get_diagnostics_config ,
resend_user_invite ,
2019-01-26 18:28:54 +00:00
]
2019-01-19 20:36:34 +00:00
}
2018-12-18 00:53:21 +00:00
2022-11-26 23:00:01 +00:00
pub fn catchers ( ) -> Vec < Catcher > {
if ! CONFIG . disable_admin_token ( ) & & ! CONFIG . is_admin_token_set ( ) {
catchers! [ ]
} else {
2022-11-28 16:35:36 +00:00
catchers! [ admin_login ]
2022-11-26 23:00:01 +00:00
}
}
2021-01-19 16:55:21 +00:00
static DB_TYPE : Lazy < & str > = Lazy ::new ( | | {
DbConnType ::from_url ( & CONFIG . database_url ( ) )
. map ( | t | match t {
DbConnType ::sqlite = > " SQLite " ,
DbConnType ::mysql = > " MySQL " ,
DbConnType ::postgresql = > " PostgreSQL " ,
} )
. unwrap_or ( " Unknown " )
} ) ;
2021-04-06 20:55:28 +00:00
static CAN_BACKUP : Lazy < bool > =
Lazy ::new ( | | DbConnType ::from_url ( & CONFIG . database_url ( ) ) . map ( | t | t = = DbConnType ::sqlite ) . unwrap_or ( false ) ) ;
2019-06-01 22:08:52 +00:00
2019-02-10 14:26:19 +00:00
#[ get( " / " ) ]
fn admin_disabled ( ) -> & 'static str {
" The admin panel is disabled, please configure the 'ADMIN_TOKEN' variable to enable it "
}
2022-01-23 22:40:59 +00:00
const COOKIE_NAME : & str = " VW_ADMIN " ;
2019-01-28 22:58:32 +00:00
const ADMIN_PATH : & str = " /admin " ;
2022-06-01 13:26:11 +00:00
const DT_FMT : & str = " %Y-%m-%d %H:%M:%S %Z " ;
2019-01-19 20:36:34 +00:00
2019-02-02 15:47:27 +00:00
const BASE_TEMPLATE : & str = " admin/base " ;
2019-01-19 21:12:52 +00:00
2022-11-20 18:15:45 +00:00
const ACTING_ADMIN_USER : & str = " vaultwarden-admin-00000-000000000000 " ;
2020-02-19 05:27:00 +00:00
fn admin_path ( ) -> String {
format! ( " {} {} " , CONFIG . domain_path ( ) , ADMIN_PATH )
}
2021-03-27 23:10:01 +00:00
#[ derive(Debug) ]
struct IpHeader ( Option < String > ) ;
2021-11-07 17:53:39 +00:00
#[ rocket::async_trait ]
impl < ' r > FromRequest < ' r > for IpHeader {
2021-03-27 23:10:01 +00:00
type Error = ( ) ;
2021-11-07 17:53:39 +00:00
async fn from_request ( req : & ' r Request < '_ > ) -> Outcome < Self , Self ::Error > {
2021-03-27 23:10:01 +00:00
if req . headers ( ) . get_one ( & CONFIG . ip_header ( ) ) . is_some ( ) {
Outcome ::Success ( IpHeader ( Some ( CONFIG . ip_header ( ) ) ) )
} else if req . headers ( ) . get_one ( " X-Client-IP " ) . is_some ( ) {
Outcome ::Success ( IpHeader ( Some ( String ::from ( " X-Client-IP " ) ) ) )
} else if req . headers ( ) . get_one ( " X-Real-IP " ) . is_some ( ) {
Outcome ::Success ( IpHeader ( Some ( String ::from ( " X-Real-IP " ) ) ) )
} else if req . headers ( ) . get_one ( " X-Forwarded-For " ) . is_some ( ) {
Outcome ::Success ( IpHeader ( Some ( String ::from ( " X-Forwarded-For " ) ) ) )
} else {
Outcome ::Success ( IpHeader ( None ) )
}
}
}
2022-11-26 23:00:01 +00:00
fn admin_url ( ) -> String {
format! ( " {} {} " , CONFIG . domain_origin ( ) , admin_path ( ) )
2020-05-01 07:23:46 +00:00
}
2022-09-06 15:14:16 +00:00
#[ derive(Responder) ]
enum AdminResponse {
2022-11-28 16:35:36 +00:00
#[ response(status = 200) ]
Ok ( ApiResult < Html < String > > ) ,
2022-09-06 15:14:16 +00:00
#[ response(status = 401) ]
Unauthorized ( ApiResult < Html < String > > ) ,
#[ response(status = 429) ]
TooManyRequests ( ApiResult < Html < String > > ) ,
}
2022-11-28 16:35:36 +00:00
#[ catch(401) ]
fn admin_login ( request : & Request < '_ > ) -> ApiResult < Html < String > > {
if request . format ( ) = = Some ( & MediaType ::JSON ) {
err_code! ( " Authorization failed. " , Status ::Unauthorized . code ) ;
}
let redirect = request . segments ::< std ::path ::PathBuf > ( 0 .. ) . unwrap_or_default ( ) . display ( ) . to_string ( ) ;
render_admin_login ( None , Some ( redirect ) )
2022-09-06 15:14:16 +00:00
}
2022-11-28 16:35:36 +00:00
fn render_admin_login ( msg : Option < & str > , redirect : Option < String > ) -> ApiResult < Html < String > > {
2019-01-19 20:36:34 +00:00
// If there is an error, show it
2022-09-06 15:14:16 +00:00
let msg = msg . map ( | msg | format! ( " Error: {msg} " ) ) ;
2021-03-31 20:45:05 +00:00
let json = json! ( {
" page_content " : " admin/login " ,
" error " : msg ,
2022-11-28 16:35:36 +00:00
" redirect " : redirect ,
2021-03-31 20:45:05 +00:00
" urlpath " : CONFIG . domain_path ( )
} ) ;
2019-01-19 20:36:34 +00:00
// Return the page
2019-02-02 15:47:27 +00:00
let text = CONFIG . render_template ( BASE_TEMPLATE , & json ) ? ;
2019-01-19 20:36:34 +00:00
Ok ( Html ( text ) )
2018-12-18 00:53:21 +00:00
}
2019-01-19 21:12:52 +00:00
#[ derive(FromForm) ]
struct LoginForm {
token : String ,
2022-11-28 16:35:36 +00:00
redirect : Option < String > ,
2019-01-19 21:12:52 +00:00
}
2022-11-28 16:35:36 +00:00
#[ post( " / " , data = " <data> " ) ]
fn post_admin_login ( data : Form < LoginForm > , cookies : & CookieJar < '_ > , ip : ClientIp ) -> Result < Redirect , AdminResponse > {
2019-01-19 20:36:34 +00:00
let data = data . into_inner ( ) ;
2022-11-28 16:35:36 +00:00
let redirect = data . redirect ;
2019-01-19 20:36:34 +00:00
2021-12-22 20:48:49 +00:00
if crate ::ratelimit ::check_limit_admin ( & ip . ip ) . is_err ( ) {
2022-11-28 16:35:36 +00:00
return Err ( AdminResponse ::TooManyRequests ( render_admin_login (
Some ( " Too many requests, try again later. " ) ,
redirect ,
) ) ) ;
2021-12-22 20:48:49 +00:00
}
2019-01-20 16:43:56 +00:00
// If the token is invalid, redirect to login page
2019-01-19 20:36:34 +00:00
if ! _validate_token ( & data . token ) {
error! ( " Invalid admin token. IP: {} " , ip . ip ) ;
2022-11-28 16:35:36 +00:00
Err ( AdminResponse ::Unauthorized ( render_admin_login ( Some ( " Invalid admin token, please try again. " ) , redirect ) ) )
2019-01-19 20:36:34 +00:00
} else {
// If the token received is valid, generate JWT and save it as a cookie
let claims = generate_admin_claims ( ) ;
let jwt = encode_jwt ( & claims ) ;
let cookie = Cookie ::build ( COOKIE_NAME , jwt )
2020-02-19 05:27:00 +00:00
. path ( admin_path ( ) )
2023-02-20 16:02:14 +00:00
. max_age ( rocket ::time ::Duration ::minutes ( CONFIG . admin_session_lifetime ( ) ) )
2019-01-20 14:36:33 +00:00
. same_site ( SameSite ::Strict )
2019-01-19 20:36:34 +00:00
. http_only ( true )
. finish ( ) ;
cookies . add ( cookie ) ;
2022-11-28 16:35:36 +00:00
if let Some ( redirect ) = redirect {
Ok ( Redirect ::to ( format! ( " {} {} " , admin_path ( ) , redirect ) ) )
} else {
Err ( AdminResponse ::Ok ( render_admin_page ( ) ) )
}
2019-01-19 20:36:34 +00:00
}
}
fn _validate_token ( token : & str ) -> bool {
2019-01-25 17:23:51 +00:00
match CONFIG . admin_token ( ) . as_ref ( ) {
2019-01-19 20:36:34 +00:00
None = > false ,
2023-02-28 22:09:51 +00:00
Some ( t ) if t . starts_with ( " $argon2 " ) = > {
use argon2 ::password_hash ::PasswordVerifier ;
match argon2 ::password_hash ::PasswordHash ::new ( t ) {
Ok ( h ) = > {
// NOTE: hash params from `ADMIN_TOKEN` are used instead of what is configured in the `Argon2` instance.
argon2 ::Argon2 ::default ( ) . verify_password ( token . trim ( ) . as_ref ( ) , & h ) . is_ok ( )
}
Err ( e ) = > {
error! ( " The configured Argon2 PHC in `ADMIN_TOKEN` is invalid: {e} " ) ;
false
}
}
}
2019-03-07 19:21:50 +00:00
Some ( t ) = > crate ::crypto ::ct_eq ( t . trim ( ) , token . trim ( ) ) ,
2019-01-19 20:36:34 +00:00
}
}
2019-02-02 15:47:27 +00:00
#[ derive(Serialize) ]
struct AdminTemplateData {
page_content : String ,
2021-06-19 17:22:19 +00:00
page_data : Option < Value > ,
2019-12-27 17:37:14 +00:00
logged_in : bool ,
2020-02-22 16:49:33 +00:00
urlpath : String ,
2019-02-02 15:47:27 +00:00
}
impl AdminTemplateData {
2022-12-28 19:05:10 +00:00
fn new ( page_content : & str , page_data : Value ) -> Self {
2020-05-28 08:42:36 +00:00
Self {
2021-06-19 17:22:19 +00:00
page_content : String ::from ( page_content ) ,
page_data : Some ( page_data ) ,
2020-05-28 08:42:36 +00:00
logged_in : true ,
urlpath : CONFIG . domain_path ( ) ,
2019-02-02 15:47:27 +00:00
}
}
fn render ( self ) -> Result < String , Error > {
CONFIG . render_template ( BASE_TEMPLATE , & self )
}
}
2022-11-28 16:35:36 +00:00
fn render_admin_page ( ) -> ApiResult < Html < String > > {
2022-12-28 19:05:10 +00:00
let settings_json = json! ( {
" config " : CONFIG . prepare_json ( ) ,
" can_backup " : * CAN_BACKUP ,
} ) ;
let text = AdminTemplateData ::new ( " admin/settings " , settings_json ) . render ( ) ? ;
2019-01-19 20:36:34 +00:00
Ok ( Html ( text ) )
}
2022-11-28 16:35:36 +00:00
#[ get( " / " ) ]
fn admin_page ( _token : AdminToken ) -> ApiResult < Html < String > > {
render_admin_page ( )
}
2023-05-25 21:12:24 +00:00
#[ get( " / " , rank = 2) ]
fn admin_page_login ( ) -> ApiResult < Html < String > > {
render_admin_login ( None , None )
}
2019-01-19 20:36:34 +00:00
#[ derive(Deserialize, Debug) ]
#[ allow(non_snake_case) ]
struct InviteData {
2019-02-02 15:47:27 +00:00
email : String ,
2018-12-18 00:53:21 +00:00
}
2022-05-20 21:39:47 +00:00
async fn get_user_or_404 ( uuid : & str , conn : & mut DbConn ) -> ApiResult < User > {
2021-11-16 16:07:55 +00:00
if let Some ( user ) = User ::find_by_uuid ( uuid , conn ) . await {
2021-05-09 02:28:08 +00:00
Ok ( user )
} else {
err_code! ( " User doesn't exist " , Status ::NotFound . code ) ;
}
}
2018-12-18 17:52:58 +00:00
#[ post( " /invite " , data = " <data> " ) ]
2022-05-20 21:39:47 +00:00
async fn invite_user ( data : Json < InviteData > , _token : AdminToken , mut conn : DbConn ) -> JsonResult {
2019-02-02 15:47:27 +00:00
let data : InviteData = data . into_inner ( ) ;
let email = data . email . clone ( ) ;
2022-05-20 21:39:47 +00:00
if User ::find_by_mail ( & data . email , & mut conn ) . await . is_some ( ) {
2021-05-09 02:29:41 +00:00
err_code! ( " User already exists " , Status ::Conflict . code )
2018-12-18 00:53:21 +00:00
}
2019-04-12 21:42:42 +00:00
let mut user = User ::new ( email ) ;
2022-05-20 21:39:47 +00:00
async fn _generate_invite ( user : & User , conn : & mut DbConn ) -> EmptyResult {
2021-05-09 02:29:41 +00:00
if CONFIG . mail_enabled ( ) {
2022-07-06 21:57:37 +00:00
mail ::send_invite ( & user . email , & user . uuid , None , None , & CONFIG . invitation_org_name ( ) , None ) . await
2021-05-09 02:29:41 +00:00
} else {
2022-11-26 18:07:28 +00:00
let invitation = Invitation ::new ( & user . email ) ;
2021-11-16 16:07:55 +00:00
invitation . save ( conn ) . await
2021-05-09 02:29:41 +00:00
}
2021-11-16 16:07:55 +00:00
}
2021-05-09 02:29:41 +00:00
2022-05-20 21:39:47 +00:00
_generate_invite ( & user , & mut conn ) . await . map_err ( | e | e . with_code ( Status ::InternalServerError . code ) ) ? ;
user . save ( & mut conn ) . await . map_err ( | e | e . with_code ( Status ::InternalServerError . code ) ) ? ;
2021-05-03 23:42:29 +00:00
2022-05-20 21:39:47 +00:00
Ok ( Json ( user . to_json ( & mut conn ) . await ) )
2018-12-18 00:53:21 +00:00
}
2020-02-26 10:02:22 +00:00
#[ post( " /test/smtp " , data = " <data> " ) ]
2022-07-06 21:57:37 +00:00
async fn test_smtp ( data : Json < InviteData > , _token : AdminToken ) -> EmptyResult {
2020-02-26 10:02:22 +00:00
let data : InviteData = data . into_inner ( ) ;
if CONFIG . mail_enabled ( ) {
2022-07-06 21:57:37 +00:00
mail ::send_test ( & data . email ) . await
2020-02-26 10:02:22 +00:00
} else {
err! ( " Mail is not enabled " )
}
}
2019-12-01 20:15:14 +00:00
#[ get( " /logout " ) ]
2022-11-26 23:00:01 +00:00
fn logout ( cookies : & CookieJar < '_ > ) -> Redirect {
2022-02-22 19:48:00 +00:00
cookies . remove ( Cookie ::build ( COOKIE_NAME , " " ) . path ( admin_path ( ) ) . finish ( ) ) ;
2022-11-26 23:00:01 +00:00
Redirect ::to ( admin_path ( ) )
2019-12-01 20:15:14 +00:00
}
2019-04-05 18:49:58 +00:00
#[ get( " /users " ) ]
2022-05-20 21:39:47 +00:00
async fn get_users_json ( _token : AdminToken , mut conn : DbConn ) -> Json < Value > {
2023-02-28 19:43:22 +00:00
let users = User ::get_all ( & mut conn ) . await ;
let mut users_json = Vec ::with_capacity ( users . len ( ) ) ;
for u in users {
2022-05-20 21:39:47 +00:00
let mut usr = u . to_json ( & mut conn ) . await ;
usr [ " UserEnabled " ] = json! ( u . enabled ) ;
usr [ " CreatedAt " ] = json! ( format_naive_datetime_local ( & u . created_at , DT_FMT ) ) ;
2023-10-02 15:33:52 +00:00
usr [ " LastActive " ] = match u . last_active ( & mut conn ) . await {
Some ( dt ) = > json! ( format_naive_datetime_local ( & dt , DT_FMT ) ) ,
None = > json! ( " Never " ) ,
} ;
2022-05-20 21:39:47 +00:00
users_json . push ( usr ) ;
}
2019-04-05 18:49:58 +00:00
2021-03-27 15:07:26 +00:00
Json ( Value ::Array ( users_json ) )
2019-04-05 18:49:58 +00:00
}
2020-05-28 08:42:36 +00:00
#[ get( " /users/overview " ) ]
2022-05-20 21:39:47 +00:00
async fn users_overview ( _token : AdminToken , mut conn : DbConn ) -> ApiResult < Html < String > > {
2023-02-28 19:43:22 +00:00
let users = User ::get_all ( & mut conn ) . await ;
let mut users_json = Vec ::with_capacity ( users . len ( ) ) ;
for u in users {
2022-05-20 21:39:47 +00:00
let mut usr = u . to_json ( & mut conn ) . await ;
usr [ " cipher_count " ] = json! ( Cipher ::count_owned_by_user ( & u . uuid , & mut conn ) . await ) ;
usr [ " attachment_count " ] = json! ( Attachment ::count_by_user ( & u . uuid , & mut conn ) . await ) ;
usr [ " attachment_size " ] = json! ( get_display_size ( Attachment ::size_by_user ( & u . uuid , & mut conn ) . await as i32 ) ) ;
usr [ " user_enabled " ] = json! ( u . enabled ) ;
usr [ " created_at " ] = json! ( format_naive_datetime_local ( & u . created_at , DT_FMT ) ) ;
usr [ " last_active " ] = match u . last_active ( & mut conn ) . await {
Some ( dt ) = > json! ( format_naive_datetime_local ( & dt , DT_FMT ) ) ,
None = > json! ( " Never " ) ,
} ;
users_json . push ( usr ) ;
}
2020-05-28 08:42:36 +00:00
2022-12-28 19:05:10 +00:00
let text = AdminTemplateData ::new ( " admin/users " , json! ( users_json ) ) . render ( ) ? ;
2020-05-28 08:42:36 +00:00
Ok ( Html ( text ) )
}
2023-02-20 15:39:56 +00:00
#[ get( " /users/by-mail/<mail> " ) ]
2023-04-30 15:18:12 +00:00
async fn get_user_by_mail_json ( mail : & str , _token : AdminToken , mut conn : DbConn ) -> JsonResult {
if let Some ( u ) = User ::find_by_mail ( mail , & mut conn ) . await {
2023-02-20 15:39:56 +00:00
let mut usr = u . to_json ( & mut conn ) . await ;
usr [ " UserEnabled " ] = json! ( u . enabled ) ;
usr [ " CreatedAt " ] = json! ( format_naive_datetime_local ( & u . created_at , DT_FMT ) ) ;
Ok ( Json ( usr ) )
} else {
err_code! ( " User doesn't exist " , Status ::NotFound . code ) ;
}
}
2021-05-08 20:03:03 +00:00
#[ get( " /users/<uuid> " ) ]
2023-04-30 15:18:12 +00:00
async fn get_user_json ( uuid : & str , _token : AdminToken , mut conn : DbConn ) -> JsonResult {
let u = get_user_or_404 ( uuid , & mut conn ) . await ? ;
2022-05-20 21:39:47 +00:00
let mut usr = u . to_json ( & mut conn ) . await ;
2022-06-01 13:26:11 +00:00
usr [ " UserEnabled " ] = json! ( u . enabled ) ;
usr [ " CreatedAt " ] = json! ( format_naive_datetime_local ( & u . created_at , DT_FMT ) ) ;
Ok ( Json ( usr ) )
2021-05-08 20:03:03 +00:00
}
2018-12-18 17:52:58 +00:00
#[ post( " /users/<uuid>/delete " ) ]
2023-04-30 15:18:12 +00:00
async fn delete_user ( uuid : & str , token : AdminToken , mut conn : DbConn ) -> EmptyResult {
let user = get_user_or_404 ( uuid , & mut conn ) . await ? ;
2022-11-20 18:15:45 +00:00
// Get the user_org records before deleting the actual user
2023-04-30 15:18:12 +00:00
let user_orgs = UserOrganization ::find_any_state_by_user ( uuid , & mut conn ) . await ;
2022-11-20 18:15:45 +00:00
let res = user . delete ( & mut conn ) . await ;
for user_org in user_orgs {
log_event (
EventType ::OrganizationUserRemoved as i32 ,
& user_org . uuid ,
2023-04-30 15:18:12 +00:00
& user_org . org_uuid ,
2022-11-20 18:15:45 +00:00
String ::from ( ACTING_ADMIN_USER ) ,
14 , // Use UnknownBrowser type
2023-03-09 15:31:28 +00:00
& token . ip . ip ,
2022-11-20 18:15:45 +00:00
& mut conn ,
)
. await ;
}
res
2018-12-18 00:53:21 +00:00
}
2019-01-26 18:28:54 +00:00
#[ post( " /users/<uuid>/deauth " ) ]
2023-04-30 15:18:12 +00:00
async fn deauth_user ( uuid : & str , _token : AdminToken , mut conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
let mut user = get_user_or_404 ( uuid , & mut conn ) . await ? ;
2019-01-26 18:28:54 +00:00
2023-06-16 21:34:16 +00:00
nt . send_logout ( & user , None ) . await ;
2023-06-11 11:28:18 +00:00
if CONFIG . push_enabled ( ) {
2023-06-16 21:34:16 +00:00
for device in Device ::find_push_devices_by_user ( & user . uuid , & mut conn ) . await {
2023-06-11 11:28:18 +00:00
match unregister_push_device ( device . uuid ) . await {
Ok ( r ) = > r ,
Err ( e ) = > error! ( " Unable to unregister devices from Bitwarden server: {} " , e ) ,
} ;
}
}
2022-12-30 20:23:55 +00:00
2023-06-11 11:28:18 +00:00
Device ::delete_all_by_user ( & user . uuid , & mut conn ) . await ? ;
user . reset_security_stamp ( ) ;
2022-12-30 20:23:55 +00:00
2023-06-11 11:28:18 +00:00
user . save ( & mut conn ) . await
2019-01-26 18:28:54 +00:00
}
2020-11-30 22:12:56 +00:00
#[ post( " /users/<uuid>/disable " ) ]
2023-04-30 15:18:12 +00:00
async fn disable_user ( uuid : & str , _token : AdminToken , mut conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
let mut user = get_user_or_404 ( uuid , & mut conn ) . await ? ;
2022-05-20 21:39:47 +00:00
Device ::delete_all_by_user ( & user . uuid , & mut conn ) . await ? ;
2020-11-30 22:12:56 +00:00
user . reset_security_stamp ( ) ;
user . enabled = false ;
2022-12-30 20:23:55 +00:00
let save_result = user . save ( & mut conn ) . await ;
2023-06-16 21:34:16 +00:00
nt . send_logout ( & user , None ) . await ;
2022-12-30 20:23:55 +00:00
save_result
2020-11-30 22:12:56 +00:00
}
#[ post( " /users/<uuid>/enable " ) ]
2023-04-30 15:18:12 +00:00
async fn enable_user ( uuid : & str , _token : AdminToken , mut conn : DbConn ) -> EmptyResult {
let mut user = get_user_or_404 ( uuid , & mut conn ) . await ? ;
2020-11-30 22:12:56 +00:00
user . enabled = true ;
2022-05-20 21:39:47 +00:00
user . save ( & mut conn ) . await
2020-11-30 22:12:56 +00:00
}
2019-08-21 15:13:06 +00:00
#[ post( " /users/<uuid>/remove-2fa " ) ]
2023-04-30 15:18:12 +00:00
async fn remove_2fa ( uuid : & str , _token : AdminToken , mut conn : DbConn ) -> EmptyResult {
let mut user = get_user_or_404 ( uuid , & mut conn ) . await ? ;
2022-05-20 21:39:47 +00:00
TwoFactor ::delete_all_by_user ( & user . uuid , & mut conn ) . await ? ;
2019-08-21 15:13:06 +00:00
user . totp_recover = None ;
2022-05-20 21:39:47 +00:00
user . save ( & mut conn ) . await
2019-08-21 15:13:06 +00:00
}
2023-03-30 12:23:16 +00:00
#[ post( " /users/<uuid>/invite/resend " ) ]
2023-04-30 15:18:12 +00:00
async fn resend_user_invite ( uuid : & str , _token : AdminToken , mut conn : DbConn ) -> EmptyResult {
if let Some ( user ) = User ::find_by_uuid ( uuid , & mut conn ) . await {
2023-03-31 11:03:37 +00:00
//TODO: replace this with user.status check when it will be available (PR#3397)
if ! user . password_hash . is_empty ( ) {
err_code! ( " User already accepted invitation " , Status ::BadRequest . code ) ;
}
2023-03-30 12:23:16 +00:00
if CONFIG . mail_enabled ( ) {
mail ::send_invite ( & user . email , & user . uuid , None , None , & CONFIG . invitation_org_name ( ) , None ) . await
} else {
Ok ( ( ) )
}
} else {
err_code! ( " User doesn't exist " , Status ::NotFound . code ) ;
}
}
2021-02-03 17:43:54 +00:00
#[ derive(Deserialize, Debug) ]
struct UserOrgTypeData {
user_type : NumberOrString ,
user_uuid : String ,
org_uuid : String ,
}
#[ post( " /users/org_type " , data = " <data> " ) ]
2023-03-09 15:31:28 +00:00
async fn update_user_org_type ( data : Json < UserOrgTypeData > , token : AdminToken , mut conn : DbConn ) -> EmptyResult {
2021-02-03 17:43:54 +00:00
let data : UserOrgTypeData = data . into_inner ( ) ;
2022-05-20 21:39:47 +00:00
let mut user_to_edit =
match UserOrganization ::find_by_user_and_org ( & data . user_uuid , & data . org_uuid , & mut conn ) . await {
Some ( user ) = > user ,
None = > err! ( " The specified user isn't member of the organization " ) ,
} ;
2021-02-03 17:43:54 +00:00
let new_type = match UserOrgType ::from_str ( & data . user_type . into_string ( ) ) {
Some ( new_type ) = > new_type as i32 ,
None = > err! ( " Invalid type " ) ,
} ;
if user_to_edit . atype = = UserOrgType ::Owner & & new_type ! = UserOrgType ::Owner {
2022-12-28 19:05:10 +00:00
// Removing owner permission, check that there is at least one other confirmed owner
2022-05-20 21:39:47 +00:00
if UserOrganization ::count_confirmed_by_org_and_type ( & data . org_uuid , UserOrgType ::Owner , & mut conn ) . await < = 1 {
2021-02-03 17:43:54 +00:00
err! ( " Can't change the type of the last owner " )
}
}
2022-08-20 14:42:36 +00:00
// This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type
// It returns different error messages per function.
if new_type < UserOrgType ::Admin {
2022-05-20 21:39:47 +00:00
match OrgPolicy ::is_user_allowed ( & user_to_edit . user_uuid , & user_to_edit . org_uuid , true , & mut conn ) . await {
2022-08-20 14:42:36 +00:00
Ok ( _ ) = > { }
Err ( OrgPolicyErr ::TwoFactorMissing ) = > {
err! ( " You cannot modify this user to this type because it has no two-step login method activated " ) ;
}
Err ( OrgPolicyErr ::SingleOrgEnforced ) = > {
err! ( " You cannot modify this user to this type because it is a member of an organization which forbids it " ) ;
}
}
}
2022-11-20 18:15:45 +00:00
log_event (
EventType ::OrganizationUserUpdated as i32 ,
& user_to_edit . uuid ,
2023-04-30 15:18:12 +00:00
& data . org_uuid ,
2022-11-20 18:15:45 +00:00
String ::from ( ACTING_ADMIN_USER ) ,
14 , // Use UnknownBrowser type
2023-03-09 15:31:28 +00:00
& token . ip . ip ,
2022-11-20 18:15:45 +00:00
& mut conn ,
)
. await ;
2022-07-10 14:39:38 +00:00
user_to_edit . atype = new_type ;
2022-05-20 21:39:47 +00:00
user_to_edit . save ( & mut conn ) . await
2021-02-03 17:43:54 +00:00
}
2019-03-07 20:08:33 +00:00
#[ post( " /users/update_revision " ) ]
2022-05-20 21:39:47 +00:00
async fn update_revision_users ( _token : AdminToken , mut conn : DbConn ) -> EmptyResult {
User ::update_all_revisions ( & mut conn ) . await
2019-03-07 20:08:33 +00:00
}
2020-05-28 08:42:36 +00:00
#[ get( " /organizations/overview " ) ]
2022-05-20 21:39:47 +00:00
async fn organizations_overview ( _token : AdminToken , mut conn : DbConn ) -> ApiResult < Html < String > > {
2023-02-28 19:43:22 +00:00
let organizations = Organization ::get_all ( & mut conn ) . await ;
let mut organizations_json = Vec ::with_capacity ( organizations . len ( ) ) ;
for o in organizations {
2022-05-20 21:39:47 +00:00
let mut org = o . to_json ( ) ;
org [ " user_count " ] = json! ( UserOrganization ::count_by_org ( & o . uuid , & mut conn ) . await ) ;
org [ " cipher_count " ] = json! ( Cipher ::count_by_org ( & o . uuid , & mut conn ) . await ) ;
2023-02-28 19:43:22 +00:00
org [ " collection_count " ] = json! ( Collection ::count_by_org ( & o . uuid , & mut conn ) . await ) ;
org [ " group_count " ] = json! ( Group ::count_by_org ( & o . uuid , & mut conn ) . await ) ;
org [ " event_count " ] = json! ( Event ::count_by_org ( & o . uuid , & mut conn ) . await ) ;
2022-05-20 21:39:47 +00:00
org [ " attachment_count " ] = json! ( Attachment ::count_by_org ( & o . uuid , & mut conn ) . await ) ;
org [ " attachment_size " ] = json! ( get_display_size ( Attachment ::size_by_org ( & o . uuid , & mut conn ) . await as i32 ) ) ;
organizations_json . push ( org ) ;
}
2020-05-28 08:42:36 +00:00
2022-12-28 19:05:10 +00:00
let text = AdminTemplateData ::new ( " admin/organizations " , json! ( organizations_json ) ) . render ( ) ? ;
2020-05-28 08:42:36 +00:00
Ok ( Html ( text ) )
}
2021-02-03 17:43:54 +00:00
#[ post( " /organizations/<uuid>/delete " ) ]
2023-04-30 15:18:12 +00:00
async fn delete_organization ( uuid : & str , _token : AdminToken , mut conn : DbConn ) -> EmptyResult {
let org = Organization ::find_by_uuid ( uuid , & mut conn ) . await . map_res ( " Organization doesn't exist " ) ? ;
2022-05-20 21:39:47 +00:00
org . delete ( & mut conn ) . await
2021-02-03 17:43:54 +00:00
}
2020-06-03 15:07:32 +00:00
#[ derive(Deserialize) ]
struct WebVaultVersion {
2020-05-28 08:42:36 +00:00
version : String ,
}
2020-06-03 15:07:32 +00:00
#[ derive(Deserialize) ]
struct GitRelease {
tag_name : String ,
}
#[ derive(Deserialize) ]
struct GitCommit {
sha : String ,
}
2023-02-28 19:43:22 +00:00
#[ derive(Deserialize) ]
struct TimeApi {
year : u16 ,
month : u8 ,
day : u8 ,
hour : u8 ,
minute : u8 ,
seconds : u8 ,
}
async fn get_json_api < T : DeserializeOwned > ( url : & str ) -> Result < T , Error > {
let json_api = get_reqwest_client ( ) ;
2020-05-28 18:25:25 +00:00
2023-02-28 19:43:22 +00:00
Ok ( json_api . get ( url ) . send ( ) . await ? . error_for_status ( ) ? . json ::< T > ( ) . await ? )
2021-01-19 16:55:21 +00:00
}
2021-11-07 17:53:39 +00:00
async fn has_http_access ( ) -> bool {
2021-04-06 20:04:37 +00:00
let http_access = get_reqwest_client ( ) ;
2021-01-19 16:55:21 +00:00
2021-11-07 17:53:39 +00:00
match http_access . head ( " https://github.com/dani-garcia/vaultwarden " ) . send ( ) . await {
2021-01-19 16:55:21 +00:00
Ok ( r ) = > r . status ( ) . is_success ( ) ,
_ = > false ,
}
2020-05-28 18:25:25 +00:00
}
2022-06-08 17:46:33 +00:00
use cached ::proc_macro ::cached ;
/// Cache this function to prevent API call rate limit. Github only allows 60 requests per hour, and we use 3 here already.
/// It will cache this function for 300 seconds (5 minutes) which should prevent the exhaustion of the rate limit.
#[ cached(time = 300, sync_writes = true) ]
async fn get_release_info ( has_http_access : bool , running_within_docker : bool ) -> ( String , String , String ) {
2021-01-19 16:55:21 +00:00
// If the HTTP Check failed, do not even attempt to check for new versions since we were not able to connect with github.com anyway.
2022-06-08 17:46:33 +00:00
if has_http_access {
2020-06-03 15:07:32 +00:00
(
2023-02-28 19:43:22 +00:00
match get_json_api ::< GitRelease > ( " https://api.github.com/repos/dani-garcia/vaultwarden/releases/latest " )
2021-11-07 17:53:39 +00:00
. await
{
2020-06-03 15:07:32 +00:00
Ok ( r ) = > r . tag_name ,
2021-01-19 16:55:21 +00:00
_ = > " - " . to_string ( ) ,
2020-06-03 15:07:32 +00:00
} ,
2023-02-28 19:43:22 +00:00
match get_json_api ::< GitCommit > ( " https://api.github.com/repos/dani-garcia/vaultwarden/commits/main " ) . await {
2020-06-03 15:07:32 +00:00
Ok ( mut c ) = > {
c . sha . truncate ( 8 ) ;
c . sha
2021-01-19 16:55:21 +00:00
}
_ = > " - " . to_string ( ) ,
2020-06-03 15:07:32 +00:00
} ,
2021-02-03 17:43:54 +00:00
// Do not fetch the web-vault version when running within Docker.
// The web-vault version is embedded within the container it self, and should not be updated manually
if running_within_docker {
" - " . to_string ( )
} else {
2023-02-28 19:43:22 +00:00
match get_json_api ::< GitRelease > (
2021-03-31 20:18:35 +00:00
" https://api.github.com/repos/dani-garcia/bw_web_builds/releases/latest " ,
2021-11-07 17:53:39 +00:00
)
. await
{
2021-02-03 17:43:54 +00:00
Ok ( r ) = > r . tag_name . trim_start_matches ( 'v' ) . to_string ( ) ,
_ = > " - " . to_string ( ) ,
}
2020-06-03 15:07:32 +00:00
} ,
)
} else {
( " - " . to_string ( ) , " - " . to_string ( ) , " - " . to_string ( ) )
2022-06-08 17:46:33 +00:00
}
}
2023-02-28 19:43:22 +00:00
async fn get_ntp_time ( has_http_access : bool ) -> String {
if has_http_access {
if let Ok ( ntp_time ) = get_json_api ::< TimeApi > ( " https://www.timeapi.io/api/Time/current/zone?timeZone=UTC " ) . await
{
return format! (
" {year}-{month:02}-{day:02} {hour:02}:{minute:02}:{seconds:02} UTC " ,
year = ntp_time . year ,
month = ntp_time . month ,
day = ntp_time . day ,
hour = ntp_time . hour ,
minute = ntp_time . minute ,
seconds = ntp_time . seconds
) ;
}
}
String ::from ( " Unable to fetch NTP time. " )
}
2022-06-08 17:46:33 +00:00
#[ get( " /diagnostics " ) ]
2022-05-20 21:39:47 +00:00
async fn diagnostics ( _token : AdminToken , ip_header : IpHeader , mut conn : DbConn ) -> ApiResult < Html < String > > {
2022-06-08 17:46:33 +00:00
use chrono ::prelude ::* ;
use std ::net ::ToSocketAddrs ;
// Get current running versions
let web_vault_version : WebVaultVersion =
2022-11-04 11:56:02 +00:00
match std ::fs ::read_to_string ( format! ( " {} / {} " , CONFIG . web_vault_folder ( ) , " vw-version.json " ) ) {
2022-06-08 17:46:33 +00:00
Ok ( s ) = > serde_json ::from_str ( & s ) ? ,
2022-11-04 11:56:02 +00:00
_ = > match std ::fs ::read_to_string ( format! ( " {} / {} " , CONFIG . web_vault_folder ( ) , " version.json " ) ) {
2022-06-08 17:46:33 +00:00
Ok ( s ) = > serde_json ::from_str ( & s ) ? ,
_ = > WebVaultVersion {
version : String ::from ( " Version file missing " ) ,
} ,
} ,
} ;
// Execute some environment checks
let running_within_docker = is_running_in_docker ( ) ;
let has_http_access = has_http_access ( ) . await ;
let uses_proxy = env ::var_os ( " HTTP_PROXY " ) . is_some ( )
| | env ::var_os ( " http_proxy " ) . is_some ( )
| | env ::var_os ( " HTTPS_PROXY " ) . is_some ( )
| | env ::var_os ( " https_proxy " ) . is_some ( ) ;
// Check if we are able to resolve DNS entries
let dns_resolved = match ( " github.com " , 0 ) . to_socket_addrs ( ) . map ( | mut i | i . next ( ) ) {
Ok ( Some ( a ) ) = > a . ip ( ) . to_string ( ) ,
2023-02-28 19:43:22 +00:00
_ = > " Unable to resolve domain name. " . to_string ( ) ,
2020-05-28 18:25:25 +00:00
} ;
2020-08-12 17:07:52 +00:00
2022-06-08 17:46:33 +00:00
let ( latest_release , latest_commit , latest_web_build ) =
get_release_info ( has_http_access , running_within_docker ) . await ;
2021-03-27 23:10:01 +00:00
let ip_header_name = match & ip_header . 0 {
Some ( h ) = > h ,
2021-03-31 20:18:35 +00:00
_ = > " " ,
2021-03-27 23:10:01 +00:00
} ;
2020-05-28 08:42:36 +00:00
let diagnostics_json = json! ( {
" dns_resolved " : dns_resolved ,
2022-12-28 19:05:10 +00:00
" current_release " : VERSION ,
2020-05-28 18:25:25 +00:00
" latest_release " : latest_release ,
" latest_commit " : latest_commit ,
2021-04-05 13:09:16 +00:00
" web_vault_enabled " : & CONFIG . web_vault_enabled ( ) ,
2023-02-27 15:37:58 +00:00
" web_vault_version " : web_vault_version . version . trim_start_matches ( 'v' ) ,
2020-06-03 15:07:32 +00:00
" latest_web_build " : latest_web_build ,
2021-01-19 16:55:21 +00:00
" running_within_docker " : running_within_docker ,
2022-12-28 19:05:10 +00:00
" docker_base_image " : if running_within_docker { docker_base_image ( ) } else { " Not applicable " } ,
2021-01-19 16:55:21 +00:00
" has_http_access " : has_http_access ,
2021-03-27 23:10:01 +00:00
" ip_header_exists " : & ip_header . 0. is_some ( ) ,
2021-03-28 09:59:49 +00:00
" ip_header_match " : ip_header_name = = CONFIG . ip_header ( ) ,
2021-03-27 23:10:01 +00:00
" ip_header_name " : ip_header_name ,
" ip_header_config " : & CONFIG . ip_header ( ) ,
2021-01-19 16:55:21 +00:00
" uses_proxy " : uses_proxy ,
" db_type " : * DB_TYPE ,
2022-05-20 21:39:47 +00:00
" db_version " : get_sql_server_version ( & mut conn ) . await ,
2022-11-26 23:00:01 +00:00
" admin_url " : format ! ( " {}/diagnostics " , admin_url ( ) ) ,
2021-06-19 17:22:19 +00:00
" overrides " : & CONFIG . get_overrides ( ) . join ( " , " ) ,
2022-12-28 19:05:10 +00:00
" host_arch " : std ::env ::consts ::ARCH ,
" host_os " : std ::env ::consts ::OS ,
2021-04-05 13:09:16 +00:00
" server_time_local " : Local ::now ( ) . format ( " %Y-%m-%d %H:%M:%S %Z " ) . to_string ( ) ,
2023-02-28 19:43:22 +00:00
" server_time " : Utc ::now ( ) . format ( " %Y-%m-%d %H:%M:%S UTC " ) . to_string ( ) , // Run the server date/time check as late as possible to minimize the time difference
" ntp_time " : get_ntp_time ( has_http_access ) . await , // Run the ntp check as late as possible to minimize the time difference
2020-05-28 08:42:36 +00:00
} ) ;
2022-12-28 19:05:10 +00:00
let text = AdminTemplateData ::new ( " admin/diagnostics " , diagnostics_json ) . render ( ) ? ;
2020-05-28 08:42:36 +00:00
Ok ( Html ( text ) )
}
2021-01-19 16:55:21 +00:00
#[ get( " /diagnostics/config " ) ]
2021-03-27 15:07:26 +00:00
fn get_diagnostics_config ( _token : AdminToken ) -> Json < Value > {
2021-01-19 16:55:21 +00:00
let support_json = CONFIG . get_support_json ( ) ;
2021-03-27 15:07:26 +00:00
Json ( support_json )
2021-01-19 16:55:21 +00:00
}
2019-02-02 00:09:21 +00:00
#[ post( " /config " , data = " <data> " ) ]
2019-02-02 15:47:27 +00:00
fn post_config ( data : Json < ConfigBuilder > , _token : AdminToken ) -> EmptyResult {
let data : ConfigBuilder = data . into_inner ( ) ;
CONFIG . update_config ( data )
2019-02-02 00:09:21 +00:00
}
2019-02-06 16:32:13 +00:00
#[ post( " /config/delete " ) ]
fn delete_config ( _token : AdminToken ) -> EmptyResult {
CONFIG . delete_user_config ( )
}
2019-05-03 13:46:29 +00:00
#[ post( " /config/backup_db " ) ]
2022-05-20 21:39:47 +00:00
async fn backup_db ( _token : AdminToken , mut conn : DbConn ) -> EmptyResult {
2019-06-01 22:08:52 +00:00
if * CAN_BACKUP {
2022-05-20 21:39:47 +00:00
backup_database ( & mut conn ) . await
2019-06-01 22:08:52 +00:00
} else {
2021-04-05 13:09:16 +00:00
err! ( " Can't back up current DB (Only SQLite supports this feature) " ) ;
2019-06-01 22:08:52 +00:00
}
2019-05-03 13:46:29 +00:00
}
2023-03-09 15:31:28 +00:00
pub struct AdminToken {
ip : ClientIp ,
}
2018-12-18 00:53:21 +00:00
2021-11-07 17:53:39 +00:00
#[ rocket::async_trait ]
impl < ' r > FromRequest < ' r > for AdminToken {
2018-12-18 00:53:21 +00:00
type Error = & 'static str ;
2022-11-26 23:00:01 +00:00
async fn from_request ( request : & ' r Request < '_ > ) -> Outcome < Self , Self ::Error > {
2023-03-09 15:31:28 +00:00
let ip = match ClientIp ::from_request ( request ) . await {
Outcome ::Success ( ip ) = > ip ,
_ = > err_handler! ( " Error getting Client IP " ) ,
} ;
2019-02-20 20:44:35 +00:00
if CONFIG . disable_admin_token ( ) {
2023-03-09 15:31:28 +00:00
Outcome ::Success ( Self {
ip ,
} )
2019-03-03 15:11:55 +00:00
} else {
2021-11-07 17:53:39 +00:00
let cookies = request . cookies ( ) ;
2019-02-20 20:44:35 +00:00
let access_token = match cookies . get ( COOKIE_NAME ) {
Some ( cookie ) = > cookie . value ( ) ,
2023-05-25 21:12:24 +00:00
None = > {
let requested_page =
request . segments ::< std ::path ::PathBuf > ( 0 .. ) . unwrap_or_default ( ) . display ( ) . to_string ( ) ;
// When the requested page is empty, it is `/admin`, in that case, Forward, so it will render the login page
// Else, return a 401 failure, which will be caught
if requested_page . is_empty ( ) {
return Outcome ::Forward ( Status ::Unauthorized ) ;
} else {
return Outcome ::Failure ( ( Status ::Unauthorized , " Unauthorized " ) ) ;
}
}
2019-02-20 20:44:35 +00:00
} ;
if decode_admin ( access_token ) . is_err ( ) {
// Remove admin cookie
2022-02-22 19:48:00 +00:00
cookies . remove ( Cookie ::build ( COOKIE_NAME , " " ) . path ( admin_path ( ) ) . finish ( ) ) ;
2023-03-09 15:31:28 +00:00
error! ( " Invalid or expired admin JWT. IP: {}. " , & ip . ip ) ;
2022-11-26 23:00:01 +00:00
return Outcome ::Failure ( ( Status ::Unauthorized , " Session expired " ) ) ;
2019-02-20 20:44:35 +00:00
}
2023-03-09 15:31:28 +00:00
Outcome ::Success ( Self {
ip ,
} )
2018-12-18 00:53:21 +00:00
}
}
2018-12-18 17:52:58 +00:00
}