users.rs 7.49 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
#[table_name = "user"]
pub struct User {
    pub id: String,
45
    pub role: Role,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
46 47
}

48
#[derive(Debug, Queryable, Identifiable, Insertable)]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
49 50 51 52 53 54 55 56
#[table_name = "localuser"]
#[primary_key(user_id)]
pub struct LocalUser {
    pub user_id: String,
    pub username: String,
    pub password: String,
}

57 58 59 60 61 62 63 64
#[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
65 66 67 68 69
// pub struct LdapUserAssociation {
//     user_id: Uuid,
//     ldap_id: String
// }

Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90
#[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,
}

91
#[derive(Debug)]
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
92
pub struct UserInfo {
93
    pub id: String,
94
    pub role: Role,
95
    pub username: String,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
96 97
}

98 99
#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserInfo {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
100
    type Error = ErrorResponse;
101

102
    async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
103 104 105 106 107 108 109 110
        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
111
            return ErrorResponse::from(UserError::MalformedHeader).into()
112 113
        };

114 115
        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
116

117 118 119
        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
120 121
            JwtErrorKind::ExpiredSignature => UserError::ExpiredToken,
            _ => UserError::BadToken,
122 123 124
        });

        let token_data = match token_data {
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
125
            Err(e) => return ErrorResponse::from(e).into(),
126 127 128 129 130
            Ok(data) => data
        };

        let user_id = token_data.sub;

131 132
        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
133 134
                Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(),
                Err(e) => ErrorResponse::from(e).into(),
135 136 137
                Ok(d) => Outcome::Success(d),
            }
        }).await
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
138 139
    }
}
140 141 142 143 144

#[derive(Debug)]
pub enum UserError {
    NotFound,
    UserExists,
145 146 147 148
    BadToken,
    ExpiredToken,
    MalformedHeader,
    PermissionDenied,
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164
    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 {
165
        UserError::PasswordError(e)
166 167 168 169
    }
}

impl LocalUser {
170
    pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
171 172 173 174 175 176 177 178
        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(),
            // TODO: Use role from request
179
            role: Role::ZoneAdmin,
180 181 182
        };

        let new_localuser = LocalUser {
183
            user_id: new_user_id,
184 185 186 187 188 189 190 191 192 193 194 195 196
            username: user_request.username.clone(),
            password: make_password_with_algorithm(&user_request.password, Algorithm::Argon2),
        };

        let res = UserInfo {
            id: new_user.id.clone(),
            role: new_user.role.clone(),
            username: new_localuser.username.clone(),
        };

        conn.immediate_transaction(|| -> diesel::QueryResult<()> {
            diesel::insert_into(user)
                .values(new_user)
197
                .execute(conn)?;
198 199 200

            diesel::insert_into(localuser)
                .values(new_localuser)
201
                .execute(conn)?;
202 203 204 205 206 207 208 209

            Ok(())
        })?;

        Ok(res)
    }

    pub fn get_user_by_creds(
210
        conn: &diesel::SqliteConnection,
211 212 213 214 215 216 217 218 219
        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))
220
            .get_result(conn)?;
221 222 223 224 225 226 227 228 229 230 231 232

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

        Ok(UserInfo {
            id: client_user.id,
            role: client_user.role,
            username: client_localuser.username,
        })
    }

233
    pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: String) -> Result<UserInfo, UserError> {
234 235 236 237 238
        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))
239
            .get_result(conn)?;
240 241 242 243 244 245

        Ok(UserInfo {
            id: client_user.id,
            role: client_user.role,
            username: client_localuser.username,
        })
246 247 248
    }

}
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
249 250 251 252 253 254 255 256

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 {
257
            jti,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
258
            sub: user_info.id.clone(),
259 260
            exp,
            iat,
Gaël Berthaud-Müller's avatar
Gaël Berthaud-Müller committed
261 262 263
        }
    }

264 265 266 267 268
    pub fn decode(token: &str, secret: &str) -> JwtResult<AuthClaims> {
        decode::<AuthClaims>(
            token,
            &DecodingKey::from_secret(secret.as_ref()),
            &Validation::new(JwtAlgorithm::HS256)
269
        ).map(|data| data.claims)
270 271
    }

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