users.rs 8.51 KB
Newer Older
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
1 2
use uuid::Uuid;
use diesel::prelude::*;
3
use diesel::result::Error as DieselError;
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
4
use diesel_derive_enum::DbEnum;
5
use rocket::{State, request::{FromRequest, Request, Outcome}};
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
6 7 8 9
use serde::{Serialize, Deserialize};
use chrono::serde::ts_seconds;
use chrono::prelude::{DateTime, Utc};
use chrono::Duration;
10 11
// TODO: Maybe just use argon2 crate directly
use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm};
12 13 14 15 16 17 18
use jsonwebtoken::{
    encode, decode,
    Header, Validation,
    Algorithm as JwtAlgorithm, EncodingKey, DecodingKey,
    errors::Result as JwtResult,
    errors::ErrorKind as JwtErrorKind
};
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
19 20

use crate::schema::*;
21
use crate::DbConn;
22
use crate::config::Config;
23
use crate::models::errors::{ErrorResponse, make_500};
24 25


26 27
const BEARER: &str = "Bearer ";
const AUTH_HEADER: &str = "Authentication";
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
28

Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
29

30
#[derive(Debug, DbEnum, Deserialize, Clone)]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
31
#[serde(rename_all = "lowercase")]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
32
pub enum Role {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
33
    #[db_rename = "admin"]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
34
    Admin,
35
    #[db_rename = "zoneadmin"]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
36 37 38
    ZoneAdmin,
}

39 40 41
// TODO: Store Uuid instead of string??
// TODO: Store role as Role and not String.
#[derive(Debug, Queryable, Identifiable, Insertable)]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
42 43 44 45 46
#[table_name = "user"]
pub struct User {
    pub id: String,
}

47
#[derive(Debug, Queryable, Identifiable, Insertable)]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
48 49 50 51 52 53
#[table_name = "localuser"]
#[primary_key(user_id)]
pub struct LocalUser {
    pub user_id: String,
    pub username: String,
    pub password: String,
54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69
    pub role: Role,
}

#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "user_zone"]
#[primary_key(user_id, zone_id)]
pub struct UserZone {
    pub user_id: String,
    pub zone_id: String,
}

#[derive(Debug, Queryable, Identifiable, Insertable)]
#[table_name = "zone"]
pub struct Zone {
    pub id: String,
    pub name: String,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
70 71
}

72 73 74 75 76 77 78 79
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
    pub username: String,
    pub password: String,
    pub email: String,
    pub role: Option<Role>
}

Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
80 81 82 83 84
// pub struct LdapUserAssociation {
//     user_id: Uuid,
//     ldap_id: String
// }

Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105
#[derive(Debug, Serialize, Deserialize)]
pub struct AuthClaims {
    pub jti: String,
    pub sub: String,
    #[serde(with = "ts_seconds")]
    pub exp: DateTime<Utc>,
    #[serde(with = "ts_seconds")]
    pub iat: DateTime<Utc>,
}

#[derive(Debug, Serialize)]
pub struct AuthTokenResponse {
    pub token: String
}

#[derive(Debug, Deserialize)]
pub struct AuthTokenRequest {
    pub username: String,
    pub password: String,
}

106
#[derive(Debug)]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
107
pub struct UserInfo {
108
    pub id: String,
109
    pub role: Role,
110
    pub username: String,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
111 112
}

113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
impl UserInfo {
    pub fn is_admin(&self) -> bool {
        matches!(self.role, Role::Admin)
    }

    pub fn get_zone(&self, conn: &diesel::SqliteConnection, zone_name: &str) -> Result<Zone, UserError> {
        use crate::schema::user_zone::dsl::*;
        use crate::schema::zone::dsl::*;

        let (res_zone, _): (Zone, UserZone) = zone.inner_join(user_zone)
            .filter(name.eq(zone_name))
            .filter(user_id.eq(&self.id))
            .get_result(conn)
            .map_err(|e| match e {
                DieselError::NotFound => UserError::ZoneNotFound,
                other => UserError::DbError(other)
            })?;

        Ok(res_zone)
    }
}

135 136
#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserInfo {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
137
    type Error = ErrorResponse;
138

139
    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
140 141 142 143 144 145 146 147
        let auth_header = match request.headers().get_one(AUTH_HEADER) {
            None => return Outcome::Forward(()),
            Some(auth_header) => auth_header,
        };

        let token = if auth_header.starts_with(BEARER) {
            auth_header.trim_start_matches(BEARER)
        } else {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
148
            return ErrorResponse::from(UserError::MalformedHeader).into()
149 150
        };

151 152
        let config = try_outcome!(request.guard::<State<Config>>().await.map_failure(make_500));
        let conn = try_outcome!(request.guard::<DbConn>().await.map_failure(make_500));
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
153

154 155 156
        let token_data = AuthClaims::decode(
            token, &config.web_app.secret
        ).map_err(|e| match e.into_kind() {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
157 158
            JwtErrorKind::ExpiredSignature => UserError::ExpiredToken,
            _ => UserError::BadToken,
159 160 161
        });

        let token_data = match token_data {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
162
            Err(e) => return ErrorResponse::from(e).into(),
163 164 165 166 167
            Ok(data) => data
        };

        let user_id = token_data.sub;

168 169
        conn.run(|c| {
            match LocalUser::get_user_by_uuid(c, user_id) {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
170 171
                Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(),
                Err(e) => ErrorResponse::from(e).into(),
172 173 174
                Ok(d) => Outcome::Success(d),
            }
        }).await
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
175 176
    }
}
177 178 179

#[derive(Debug)]
pub enum UserError {
180
    ZoneNotFound,
181 182
    NotFound,
    UserExists,
183 184 185 186
    BadToken,
    ExpiredToken,
    MalformedHeader,
    PermissionDenied,
187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202
    DbError(DieselError),
    PasswordError(HasherError),
}

impl From<DieselError> for UserError {
    fn from(e: DieselError) -> Self {
        match e {
            DieselError::NotFound => UserError::NotFound,
            DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserExists,
            other => UserError::DbError(other)
        }
    }
}

impl From<HasherError> for UserError {
    fn from(e: HasherError) -> Self {
203
        UserError::PasswordError(e)
204 205 206 207
    }
}

impl LocalUser {
208
    pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
209 210 211 212 213 214 215 216 217 218
        use crate::schema::localuser::dsl::*;
        use crate::schema::user::dsl::*;

        let new_user_id = Uuid::new_v4().to_simple().to_string();

        let new_user = User {
            id: new_user_id.clone(),
        };

        let new_localuser = LocalUser {
219
            user_id: new_user_id,
220 221
            username: user_request.username.clone(),
            password: make_password_with_algorithm(&user_request.password, Algorithm::Argon2),
222 223
            // TODO: Use role from request
            role: Role::ZoneAdmin,
224 225 226 227
        };

        let res = UserInfo {
            id: new_user.id.clone(),
228
            role: new_localuser.role.clone(),
229 230 231 232 233 234
            username: new_localuser.username.clone(),
        };

        conn.immediate_transaction(|| -> diesel::QueryResult<()> {
            diesel::insert_into(user)
                .values(new_user)
235
                .execute(conn)?;
236 237 238

            diesel::insert_into(localuser)
                .values(new_localuser)
239
                .execute(conn)?;
240 241 242 243 244 245 246 247

            Ok(())
        })?;

        Ok(res)
    }

    pub fn get_user_by_creds(
248
        conn: &diesel::SqliteConnection,
249 250 251 252 253 254 255 256 257
        request_username: &str,
        request_password: &str
    ) ->  Result<UserInfo, UserError> {

        use crate::schema::localuser::dsl::*;
        use crate::schema::user::dsl::*;

        let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser)
            .filter(username.eq(request_username))
258
            .get_result(conn)?;
259 260 261 262 263 264 265

        if !check_password(&request_password, &client_localuser.password)? {
            return Err(UserError::NotFound);
        }

        Ok(UserInfo {
            id: client_user.id,
266
            role: client_localuser.role,
267 268 269 270
            username: client_localuser.username,
        })
    }

271
    pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: String) -> Result<UserInfo, UserError> {
272 273 274 275 276
        use crate::schema::localuser::dsl::*;
        use crate::schema::user::dsl::*;

        let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser)
            .filter(id.eq(request_user_id))
277
            .get_result(conn)?;
278 279 280

        Ok(UserInfo {
            id: client_user.id,
281
            role: client_localuser.role,
282 283
            username: client_localuser.username,
        })
284 285 286
    }

}
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
287 288 289 290 291 292 293 294

impl AuthClaims {
    pub fn new(user_info: &UserInfo, token_duration: Duration) -> AuthClaims {
        let jti = Uuid::new_v4().to_simple().to_string();
        let iat = Utc::now();
        let exp = iat + token_duration;

        AuthClaims {
295
            jti,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
296
            sub: user_info.id.clone(),
297 298
            exp,
            iat,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
299 300 301
        }
    }

302 303 304 305 306
    pub fn decode(token: &str, secret: &str) -> JwtResult<AuthClaims> {
        decode::<AuthClaims>(
            token,
            &DecodingKey::from_secret(secret.as_ref()),
            &Validation::new(JwtAlgorithm::HS256)
307
        ).map(|data| data.claims)
308 309
    }

310
    pub fn encode(self, secret: &str) -> JwtResult<String> {
311
        encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref()))
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
312 313
    }
}