vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

auth.rs (23954B)


      1 use crate::{
      2     config::{self, Config},
      3     error::Error,
      4 };
      5 use chrono::{TimeDelta, Utc};
      6 use jsonwebtoken::{self, errors::ErrorKind, Algorithm, DecodingKey, EncodingKey, Header};
      7 use openssl::pkey::{Id, PKey};
      8 use serde::de::DeserializeOwned;
      9 use serde::ser::Serialize;
     10 use std::fs::File;
     11 use std::io::{Read, Write};
     12 use std::sync::OnceLock;
     13 
     14 static DEFAULT_VALIDITY: OnceLock<TimeDelta> = OnceLock::new();
     15 #[inline]
     16 fn init_default_validity() {
     17     DEFAULT_VALIDITY
     18         .set(TimeDelta::try_hours(2).expect("TimeDelta::try_hours(2) should work"))
     19         .expect("DEFAULT_VALIDITY must only be initialized once");
     20 }
     21 #[inline]
     22 pub fn get_default_validity() -> &'static TimeDelta {
     23     DEFAULT_VALIDITY
     24         .get()
     25         .expect("DEFAULT_VALIDITY must be initialized in main")
     26 }
     27 static JWT_HEADER: OnceLock<Header> = OnceLock::new();
     28 #[inline]
     29 fn init_jwt_header() {
     30     JWT_HEADER
     31         .set(Header::new(JWT_ALGORITHM))
     32         .expect("JWT_HEADER must only be initialized once");
     33 }
     34 #[inline]
     35 fn get_jwt_header() -> &'static Header {
     36     JWT_HEADER
     37         .get()
     38         .expect("JWT_HEADER must be initialized in main")
     39 }
     40 static JWT_LOGIN_ISSUER: OnceLock<String> = OnceLock::new();
     41 #[inline]
     42 fn init_jwt_login_issuer() {
     43     JWT_LOGIN_ISSUER
     44         .set(format!("{}|login", config::get_config().domain_origin()))
     45         .expect("JWT_LOGIN_ISSUER must only be initialized once");
     46 }
     47 #[inline]
     48 pub fn get_jwt_login_issuer() -> &'static str {
     49     JWT_LOGIN_ISSUER
     50         .get()
     51         .expect("JWT_LOGIN_ISSUER must be initialized in main")
     52         .as_str()
     53 }
     54 static JWT_INVITE_ISSUER: OnceLock<String> = OnceLock::new();
     55 #[inline]
     56 fn init_jwt_invite_issuer() {
     57     JWT_INVITE_ISSUER
     58         .set(format!("{}|invite", config::get_config().domain_origin()))
     59         .expect("JWT_INVITE_ISSUER must only be initialized once");
     60 }
     61 #[inline]
     62 fn get_jwt_invite_issuer() -> &'static str {
     63     JWT_INVITE_ISSUER
     64         .get()
     65         .expect("JWT_INVITE_ISSUER must be initialized in main")
     66         .as_str()
     67 }
     68 static JWT_DELETE_ISSUER: OnceLock<String> = OnceLock::new();
     69 #[inline]
     70 fn init_jwt_delete_issuer() {
     71     JWT_DELETE_ISSUER
     72         .set(format!("{}|delete", config::get_config().domain_origin()))
     73         .expect("JWT_DELETE_ISSUER must only be initialized once");
     74 }
     75 #[inline]
     76 fn get_jwt_delete_issuer() -> &'static str {
     77     JWT_DELETE_ISSUER
     78         .get()
     79         .expect("JWT_DELETE_ISSUER must be initialized in main")
     80         .as_str()
     81 }
     82 const JWT_ALGORITHM: Algorithm = Algorithm::EdDSA;
     83 static ED_KEYS: OnceLock<(EncodingKey, DecodingKey)> = OnceLock::new();
     84 #[allow(clippy::map_err_ignore, clippy::verbose_file_reads)]
     85 #[inline]
     86 fn init_ed_keys() -> Result<(), Error> {
     87     let mut file = File::options()
     88         .create(true)
     89         .read(true)
     90         .truncate(false)
     91         .write(true)
     92         .open(Config::PRIVATE_ED25519_KEY)?;
     93     let mut priv_pem = Vec::with_capacity(128);
     94     let ed_key = if file.read_to_end(&mut priv_pem)? == 0 {
     95         let ed_key = PKey::generate_ed25519()?;
     96         priv_pem = ed_key.private_key_to_pem_pkcs8()?;
     97         file.write_all(priv_pem.as_slice())?;
     98         ed_key
     99     } else {
    100         let ed_key = PKey::private_key_from_pem(priv_pem.as_slice())?;
    101         if ed_key.id() == Id::ED25519 {
    102             ed_key
    103         } else {
    104             let msg = format!(
    105                 "{} is not a private Ed25519 key",
    106                 Config::PRIVATE_ED25519_KEY
    107             );
    108             return Err(Error::new(msg.as_str(), msg.as_str()));
    109         }
    110     };
    111     ED_KEYS
    112         .set((
    113             EncodingKey::from_ed_pem(priv_pem.as_slice())?,
    114             DecodingKey::from_ed_pem(ed_key.public_key_to_pem()?.as_slice())?,
    115         ))
    116         .map_err(|_| {
    117             const MSG: &str = "ED_KEYS must only be initialized once";
    118             Error::new(MSG, MSG)
    119         })
    120 }
    121 #[inline]
    122 fn get_private_ed_key() -> &'static EncodingKey {
    123     &ED_KEYS
    124         .get()
    125         .expect("ED_KEYS must be initialized in main")
    126         .0
    127 }
    128 #[inline]
    129 fn get_public_ed_key() -> &'static DecodingKey {
    130     &ED_KEYS
    131         .get()
    132         .expect("ED_KEYS must be initialized in main")
    133         .1
    134 }
    135 #[inline]
    136 pub fn init_values() {
    137     init_default_validity();
    138     init_jwt_header();
    139     init_jwt_login_issuer();
    140     init_jwt_invite_issuer();
    141     init_jwt_delete_issuer();
    142     init_ed_keys().expect("error creating Ed25519 keys");
    143 }
    144 pub fn encode_jwt<T: Serialize>(claims: &T) -> String {
    145     match jsonwebtoken::encode(get_jwt_header(), claims, get_private_ed_key()) {
    146         Ok(token) => token,
    147         Err(e) => panic!("Error encoding jwt {e}"),
    148     }
    149 }
    150 
    151 #[allow(clippy::match_same_arms)]
    152 fn decode_jwt<T: DeserializeOwned>(token: &str, issuer: String) -> Result<T, Error> {
    153     let mut validation = jsonwebtoken::Validation::new(JWT_ALGORITHM);
    154     validation.leeway = 30; // 30 seconds
    155     validation.validate_exp = true;
    156     validation.validate_nbf = true;
    157     validation.set_issuer(&[issuer]);
    158     let token = token.replace(char::is_whitespace, "");
    159     match jsonwebtoken::decode(&token, get_public_ed_key(), &validation) {
    160         Ok(d) => Ok(d.claims),
    161         Err(err) => match *err.kind() {
    162             ErrorKind::InvalidToken => err!("Token is invalid"),
    163             ErrorKind::InvalidIssuer => err!("Issuer is invalid"),
    164             ErrorKind::ExpiredSignature => err!("Token has expired"),
    165             ErrorKind::InvalidSignature
    166             | ErrorKind::InvalidEcdsaKey
    167             | ErrorKind::InvalidRsaKey(_)
    168             | ErrorKind::RsaFailedSigning
    169             | ErrorKind::InvalidAlgorithmName
    170             | ErrorKind::InvalidKeyFormat
    171             | ErrorKind::MissingRequiredClaim(_)
    172             | ErrorKind::InvalidAudience
    173             | ErrorKind::InvalidSubject
    174             | ErrorKind::ImmatureSignature
    175             | ErrorKind::InvalidAlgorithm
    176             | ErrorKind::MissingAlgorithm
    177             | ErrorKind::Base64(_)
    178             | ErrorKind::Json(_)
    179             | ErrorKind::Utf8(_)
    180             | ErrorKind::Crypto(_) => err!("Error decoding JWT"),
    181             _ => err!("Error decoding JWT"),
    182         },
    183     }
    184 }
    185 pub fn decode_login(token: &str) -> Result<LoginJwtClaims, Error> {
    186     decode_jwt(token, get_jwt_login_issuer().to_owned())
    187 }
    188 pub fn decode_invite(token: &str) -> Result<InviteJwtClaims, Error> {
    189     decode_jwt(token, get_jwt_invite_issuer().to_owned())
    190 }
    191 pub fn decode_delete(token: &str) -> Result<BasicJwtClaims, Error> {
    192     decode_jwt(token, get_jwt_delete_issuer().to_owned())
    193 }
    194 
    195 #[derive(Serialize, Deserialize)]
    196 pub struct LoginJwtClaims {
    197     // Not before
    198     pub nbf: i64,
    199     // Expiration time
    200     pub exp: i64,
    201     // Issuer
    202     pub iss: String,
    203     // Subject
    204     pub sub: String,
    205     pub premium: bool,
    206     pub name: String,
    207     pub email: String,
    208     pub email_verified: bool,
    209     // ---
    210     // Disabled these keys to be added to the JWT since they could cause the JWT to get too large
    211     // Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
    212     // Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
    213     // See: https://github.com/dani-garcia/vaultwarden/issues/4156
    214     // ---
    215     // pub orgowner: Vec<String>,
    216     // pub orgadmin: Vec<String>,
    217     // pub orguser: Vec<String>,
    218     // pub orgmanager: Vec<String>,
    219     // user security_stamp
    220     pub sstamp: String,
    221     // device uuid
    222     pub device: String,
    223     // [ "api", "offline_access" ]
    224     pub scope: Vec<String>,
    225     // [ "Application" ]
    226     pub amr: Vec<String>,
    227 }
    228 
    229 #[derive(Serialize, Deserialize)]
    230 pub struct InviteJwtClaims {
    231     // Not before
    232     nbf: i64,
    233     // Expiration time
    234     exp: i64,
    235     // Issuer
    236     iss: String,
    237     // Subject
    238     sub: String,
    239     pub email: String,
    240     pub org_id: Option<String>,
    241     pub user_org_id: Option<String>,
    242     invited_by_email: Option<String>,
    243 }
    244 
    245 #[derive(Serialize, Deserialize)]
    246 pub struct FileDownloadClaims {
    247     // Not before
    248     nbf: i64,
    249     // Expiration time
    250     exp: i64,
    251     // Issuer
    252     iss: String,
    253     // Subject
    254     pub sub: String,
    255     pub file_id: String,
    256 }
    257 #[derive(Serialize, Deserialize)]
    258 pub struct BasicJwtClaims {
    259     // Not before
    260     nbf: i64,
    261     // Expiration time
    262     exp: i64,
    263     // Issuer
    264     iss: String,
    265     // Subject
    266     pub sub: String,
    267 }
    268 use crate::db::{
    269     models::{
    270         Collection, Device, User, UserOrgStatus, UserOrgType, UserOrganization, UserStampException,
    271     },
    272     DbConn,
    273 };
    274 use rocket::{
    275     outcome::try_outcome,
    276     request::{FromRequest, Outcome, Request},
    277 };
    278 
    279 struct Host {
    280     host: String,
    281 }
    282 
    283 #[rocket::async_trait]
    284 impl<'r> FromRequest<'r> for Host {
    285     type Error = &'static str;
    286     async fn from_request(_: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    287         Outcome::Success(Self {
    288             host: config::get_config().domain_url().to_owned(),
    289         })
    290     }
    291 }
    292 pub struct ClientHeaders {
    293     #[allow(dead_code)]
    294     pub device_type: i32,
    295     pub ip: ClientIp,
    296 }
    297 
    298 #[rocket::async_trait]
    299 impl<'r> FromRequest<'r> for ClientHeaders {
    300     type Error = &'static str;
    301     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    302         let Outcome::Success(ip) = ClientIp::from_request(request).await else {
    303             err_handler!("Error getting Client IP")
    304         };
    305         // When unknown or unable to parse, return 14, which is 'Unknown Browser'
    306         let device_type: i32 = request
    307             .headers()
    308             .get_one("device-type")
    309             .map_or(14i32, |d| d.parse().unwrap_or(14i32));
    310 
    311         Outcome::Success(Self { device_type, ip })
    312     }
    313 }
    314 
    315 pub struct Headers {
    316     pub host: String,
    317     pub device: Device,
    318     pub user: User,
    319     pub ip: ClientIp,
    320 }
    321 #[allow(clippy::else_if_without_else)]
    322 #[rocket::async_trait]
    323 impl<'r> FromRequest<'r> for Headers {
    324     type Error = &'static str;
    325     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    326         let headers = request.headers();
    327         let host = try_outcome!(Host::from_request(request).await).host;
    328         let Outcome::Success(ip) = ClientIp::from_request(request).await else {
    329             err_handler!("Error getting Client IP")
    330         };
    331         // Get access_token
    332         let access_token: &str = match headers.get_one("Authorization") {
    333             Some(a) => match a.rsplit("Bearer ").next() {
    334                 Some(split) => split,
    335                 None => err_handler!("No access token provided"),
    336             },
    337             None => err_handler!("No access token provided"),
    338         };
    339         // Check JWT token is valid and get device and user from it
    340         let Ok(claims) = decode_login(access_token) else {
    341             err_handler!("Invalid claim")
    342         };
    343         let device_uuid = claims.device;
    344         let user_uuid = claims.sub;
    345         let Outcome::Success(conn) = DbConn::from_request(request).await else {
    346             err_handler!("Error getting DB")
    347         };
    348         let Some(device) = Device::find_by_uuid_and_user(&device_uuid, &user_uuid, &conn).await
    349         else {
    350             err_handler!("Invalid device id")
    351         };
    352         let Some(user) = User::find_by_uuid(&user_uuid, &conn).await else {
    353             err_handler!("Device has no user associated")
    354         };
    355         if user.security_stamp != claims.sstamp {
    356             if let Some(stamp_exception) = user
    357                 .stamp_exception
    358                 .as_deref()
    359                 .and_then(|s| serde_json::from_str::<UserStampException>(s).ok())
    360             {
    361                 let Some(current_route) = request.route().and_then(|r| r.name.as_deref()) else {
    362                     err_handler!("Error getting current route for stamp exception")
    363                 };
    364                 // Check if the stamp exception has expired first.
    365                 // Then, check if the current route matches any of the allowed routes.
    366                 // After that check the stamp in exception matches the one in the claims.
    367                 if u64::try_from(Utc::now().naive_utc().and_utc().timestamp()).expect("underflow")
    368                     > stamp_exception.expire
    369                 {
    370                     // If the stamp exception has been expired remove it from the database.
    371                     // This prevents checking this stamp exception for new requests.
    372                     let mut user = user;
    373                     user.reset_stamp_exception();
    374                     if let Err(e) = user.save(&conn).await {
    375                         error!("Error updating user: {:#?}", e);
    376                     }
    377                     err_handler!("Stamp exception is expired")
    378                 } else if !stamp_exception.routes.contains(&current_route.to_owned()) {
    379                     err_handler!(
    380                         "Invalid security stamp: Current route and exception route do not match"
    381                     )
    382                 } else if stamp_exception.security_stamp != claims.sstamp {
    383                     err_handler!("Invalid security stamp for matched stamp exception")
    384                 }
    385             } else {
    386                 err_handler!("Invalid security stamp")
    387             }
    388         }
    389         Outcome::Success(Self {
    390             host,
    391             device,
    392             user,
    393             ip,
    394         })
    395     }
    396 }
    397 
    398 pub struct OrgHeaders {
    399     host: String,
    400     device: Device,
    401     user: User,
    402     org_user_type: UserOrgType,
    403     org_user: UserOrganization,
    404     ip: ClientIp,
    405 }
    406 
    407 #[rocket::async_trait]
    408 impl<'r> FromRequest<'r> for OrgHeaders {
    409     type Error = &'static str;
    410     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    411         let headers = try_outcome!(Headers::from_request(request).await);
    412         // org_id is usually the second path param ("/organizations/<org_id>"),
    413         // but there are cases where it is a query value.
    414         // First check the path, if this is not a valid uuid, try the query values.
    415         let url_org_id: Option<&str> = {
    416             let mut url_org_id = None;
    417             if let Some(Ok(org_id)) = request.param::<&str>(1) {
    418                 if uuid::Uuid::parse_str(org_id).is_ok() {
    419                     url_org_id = Some(org_id);
    420                 }
    421             }
    422             if let Some(Ok(org_id)) = request.query_value::<&str>("organizationId") {
    423                 if uuid::Uuid::parse_str(org_id).is_ok() {
    424                     url_org_id = Some(org_id);
    425                 }
    426             }
    427             url_org_id
    428         };
    429         match url_org_id {
    430             Some(org_id) => {
    431                 let user = headers.user;
    432                 let org_user = match DbConn::from_request(request).await {
    433                     Outcome::Success(conn) => {
    434                         match UserOrganization::find_by_user_and_org(&user.uuid, org_id, &conn)
    435                             .await
    436                         {
    437                             Some(user) => {
    438                                 if user.status == i32::from(UserOrgStatus::Confirmed) {
    439                                     user
    440                                 } else {
    441                                     err_handler!(
    442                                     "The current user isn't confirmed member of the organization"
    443                                 )
    444                                 }
    445                             }
    446                             None => {
    447                                 err_handler!("The current user isn't member of the organization")
    448                             }
    449                         }
    450                     }
    451                     Outcome::Error(_) | Outcome::Forward(_) => err_handler!("Error getting DB"),
    452                 };
    453                 Outcome::Success(Self {
    454                     host: headers.host,
    455                     device: headers.device,
    456                     user,
    457                     org_user_type: {
    458                         if let Ok(org_usr_type) = UserOrgType::try_from(org_user.atype) {
    459                             org_usr_type
    460                         } else {
    461                             // This should only happen if the DB is corrupted
    462                             err_handler!("Unknown user type in the database")
    463                         }
    464                     },
    465                     org_user,
    466                     ip: headers.ip,
    467                 })
    468             }
    469             _ => err_handler!("Error getting the organization id"),
    470         }
    471     }
    472 }
    473 
    474 pub struct AdminHeaders {
    475     pub host: String,
    476     pub device: Device,
    477     pub user: User,
    478     pub org_user_type: UserOrgType,
    479     pub client_version: Option<String>,
    480     pub ip: ClientIp,
    481 }
    482 
    483 #[rocket::async_trait]
    484 impl<'r> FromRequest<'r> for AdminHeaders {
    485     type Error = &'static str;
    486     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    487         let headers = try_outcome!(OrgHeaders::from_request(request).await);
    488         let client_version = request
    489             .headers()
    490             .get_one("Bitwarden-Client-Version")
    491             .map(String::from);
    492         if headers.org_user_type >= UserOrgType::Admin {
    493             Outcome::Success(Self {
    494                 host: headers.host,
    495                 device: headers.device,
    496                 user: headers.user,
    497                 org_user_type: headers.org_user_type,
    498                 client_version,
    499                 ip: headers.ip,
    500             })
    501         } else {
    502             err_handler!("You need to be Admin or Owner to call this endpoint")
    503         }
    504     }
    505 }
    506 
    507 impl From<AdminHeaders> for Headers {
    508     fn from(h: AdminHeaders) -> Self {
    509         Self {
    510             host: h.host,
    511             device: h.device,
    512             user: h.user,
    513             ip: h.ip,
    514         }
    515     }
    516 }
    517 
    518 // col_id is usually the fourth path param ("/organizations/<org_id>/collections/<col_id>"),
    519 // but there could be cases where it is a query value.
    520 // First check the path, if this is not a valid uuid, try the query values.
    521 fn get_col_id(request: &Request<'_>) -> Option<String> {
    522     if let Some(Ok(col_id)) = request.param::<String>(3) {
    523         if uuid::Uuid::parse_str(&col_id).is_ok() {
    524             return Some(col_id);
    525         }
    526     }
    527     if let Some(Ok(col_id)) = request.query_value::<String>("collectionId") {
    528         if uuid::Uuid::parse_str(&col_id).is_ok() {
    529             return Some(col_id);
    530         }
    531     }
    532     None
    533 }
    534 
    535 /// The ManagerHeaders are used to check if you are at least a Manager
    536 /// and have access to the specific collection provided via the <col_id>/collections/collectionId.
    537 /// This does strict checking on the collection_id, ManagerHeadersLoose does not.
    538 pub struct ManagerHeaders {
    539     host: String,
    540     device: Device,
    541     pub user: User,
    542     ip: ClientIp,
    543 }
    544 
    545 #[rocket::async_trait]
    546 impl<'r> FromRequest<'r> for ManagerHeaders {
    547     type Error = &'static str;
    548     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    549         let headers = try_outcome!(OrgHeaders::from_request(request).await);
    550         if headers.org_user_type >= UserOrgType::Manager {
    551             match get_col_id(request) {
    552                 Some(col_id) => {
    553                     let Outcome::Success(conn) = DbConn::from_request(request).await else {
    554                         err_handler!("Error getting DB")
    555                     };
    556 
    557                     if !Collection::can_access_collection(&headers.org_user, &col_id, &conn).await {
    558                         err_handler!("The current user isn't a manager for this collection")
    559                     }
    560                 }
    561                 _ => err_handler!("Error getting the collection id"),
    562             }
    563 
    564             Outcome::Success(Self {
    565                 host: headers.host,
    566                 device: headers.device,
    567                 user: headers.user,
    568                 ip: headers.ip,
    569             })
    570         } else {
    571             err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
    572         }
    573     }
    574 }
    575 
    576 impl From<ManagerHeaders> for Headers {
    577     fn from(h: ManagerHeaders) -> Self {
    578         Self {
    579             host: h.host,
    580             device: h.device,
    581             user: h.user,
    582             ip: h.ip,
    583         }
    584     }
    585 }
    586 
    587 /// The ManagerHeadersLoose is used when you at least need to be a Manager,
    588 /// but there is no collection_id sent with the request (either in the path or as form data).
    589 pub struct ManagerHeadersLoose {
    590     host: String,
    591     device: Device,
    592     pub user: User,
    593     pub org_user: UserOrganization,
    594     ip: ClientIp,
    595 }
    596 
    597 #[rocket::async_trait]
    598 impl<'r> FromRequest<'r> for ManagerHeadersLoose {
    599     type Error = &'static str;
    600     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    601         let headers = try_outcome!(OrgHeaders::from_request(request).await);
    602         if headers.org_user_type >= UserOrgType::Manager {
    603             Outcome::Success(Self {
    604                 host: headers.host,
    605                 device: headers.device,
    606                 user: headers.user,
    607                 org_user: headers.org_user,
    608                 ip: headers.ip,
    609             })
    610         } else {
    611             err_handler!("You need to be a Manager, Admin or Owner to call this endpoint")
    612         }
    613     }
    614 }
    615 
    616 impl From<ManagerHeadersLoose> for Headers {
    617     fn from(h: ManagerHeadersLoose) -> Self {
    618         Self {
    619             host: h.host,
    620             device: h.device,
    621             user: h.user,
    622             ip: h.ip,
    623         }
    624     }
    625 }
    626 
    627 impl ManagerHeaders {
    628     pub async fn from_loose(
    629         h: ManagerHeadersLoose,
    630         collections: &Vec<String>,
    631         conn: &DbConn,
    632     ) -> Result<Self, Error> {
    633         for col_id in collections {
    634             if uuid::Uuid::parse_str(col_id).is_err() {
    635                 err!("Collection Id is malformed!");
    636             }
    637             if !Collection::can_access_collection(&h.org_user, col_id, conn).await {
    638                 err!("You don't have access to all collections!");
    639             }
    640         }
    641         Ok(Self {
    642             host: h.host,
    643             device: h.device,
    644             user: h.user,
    645             ip: h.ip,
    646         })
    647     }
    648 }
    649 
    650 pub struct OwnerHeaders {
    651     #[allow(dead_code)]
    652     pub device: Device,
    653     pub user: User,
    654     #[allow(dead_code)]
    655     pub ip: ClientIp,
    656 }
    657 
    658 #[rocket::async_trait]
    659 impl<'r> FromRequest<'r> for OwnerHeaders {
    660     type Error = &'static str;
    661     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    662         let headers = try_outcome!(OrgHeaders::from_request(request).await);
    663         if headers.org_user_type == UserOrgType::Owner {
    664             Outcome::Success(Self {
    665                 device: headers.device,
    666                 user: headers.user,
    667                 ip: headers.ip,
    668             })
    669         } else {
    670             err_handler!("You need to be Owner to call this endpoint")
    671         }
    672     }
    673 }
    674 
    675 //
    676 // Client IP address detection
    677 //
    678 use std::net::IpAddr;
    679 #[derive(Clone, Copy)]
    680 pub struct ClientIp {
    681     pub ip: IpAddr,
    682 }
    683 
    684 #[rocket::async_trait]
    685 impl<'r> FromRequest<'r> for ClientIp {
    686     type Error = ();
    687     #[allow(clippy::map_err_ignore, clippy::string_slice)]
    688     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    689         let ip = request.headers().get_one("X-Real-IP").and_then(|ip| {
    690             ip.find(',')
    691                 .map_or(ip, |idx| &ip[..idx])
    692                 .parse()
    693                 .map_err(|_| warn!("'X-Real-IP' header is malformed: {ip}"))
    694                 .ok()
    695         });
    696         let ip = ip
    697             .or_else(|| request.remote().map(|r| r.ip()))
    698             .unwrap_or_else(|| "0.0.0.0".parse().unwrap());
    699         Outcome::Success(Self { ip })
    700     }
    701 }
    702 
    703 pub struct WsAccessTokenHeader {
    704     #[allow(dead_code)]
    705     pub access_token: Option<String>,
    706 }
    707 
    708 #[rocket::async_trait]
    709 impl<'r> FromRequest<'r> for WsAccessTokenHeader {
    710     type Error = ();
    711     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
    712         let headers = request.headers();
    713         let access_token = headers
    714             .get_one("Authorization")
    715             .and_then(|a| a.rsplit("Bearer ").next().map(String::from));
    716         Outcome::Success(Self { access_token })
    717     }
    718 }