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
    }
}