nomilo/src/models/users.rs

415 lines
12 KiB
Rust
Raw Normal View History

2021-03-26 22:30:38 +00:00
use uuid::Uuid;
use diesel::prelude::*;
2021-03-27 05:45:59 +00:00
use diesel::result::Error as DieselError;
2021-03-26 22:30:38 +00:00
use diesel_derive_enum::DbEnum;
2021-04-02 17:33:59 +00:00
use rocket::{State, request::{FromRequest, Request, Outcome}};
2021-03-27 17:23:19 +00:00
use serde::{Serialize, Deserialize};
use chrono::serde::ts_seconds;
use chrono::prelude::{DateTime, Utc};
use chrono::Duration;
2021-03-27 05:45:59 +00:00
// TODO: Maybe just use argon2 crate directly
use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm};
2021-03-27 19:36:57 +00:00
use jsonwebtoken::{
encode, decode,
Header, Validation,
Algorithm as JwtAlgorithm, EncodingKey, DecodingKey,
errors::Result as JwtResult,
errors::ErrorKind as JwtErrorKind
};
2021-03-26 22:30:38 +00:00
use crate::schema::*;
2021-03-27 05:45:59 +00:00
use crate::DbConn;
2021-03-27 19:36:57 +00:00
use crate::config::Config;
2021-04-05 01:05:39 +00:00
use crate::models::errors::{ErrorResponse, make_500};
2021-05-02 15:19:32 +00:00
use crate::models::dns::AbsoluteName;
2021-03-27 19:36:57 +00:00
2021-04-05 01:05:39 +00:00
const BEARER: &str = "Bearer ";
const AUTH_HEADER: &str = "Authentication";
2021-03-26 22:30:38 +00:00
2021-03-27 17:23:19 +00:00
#[derive(Debug, DbEnum, Deserialize, Clone)]
2021-04-03 06:19:58 +00:00
#[serde(rename_all = "lowercase")]
2021-03-26 22:30:38 +00:00
pub enum Role {
2021-04-03 06:19:58 +00:00
#[db_rename = "admin"]
2021-03-26 22:30:38 +00:00
Admin,
#[db_rename = "zoneadmin"]
2021-03-26 22:30:38 +00:00
ZoneAdmin,
}
2021-03-27 05:45:59 +00:00
// TODO: Store Uuid instead of string??
#[derive(Debug, Queryable, Identifiable, Insertable)]
2021-03-26 22:30:38 +00:00
#[table_name = "user"]
pub struct User {
pub id: String,
}
2021-03-27 05:45:59 +00:00
#[derive(Debug, Queryable, Identifiable, Insertable)]
2021-03-26 22:30:38 +00:00
#[table_name = "localuser"]
#[primary_key(user_id)]
pub struct LocalUser {
pub user_id: String,
pub username: String,
pub password: String,
2021-04-05 22:56:15 +00:00
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,
}
2021-05-02 13:56:42 +00:00
#[derive(Debug, Serialize, Queryable, Identifiable, Insertable)]
2021-04-05 22:56:15 +00:00
#[table_name = "zone"]
pub struct Zone {
2021-05-02 13:56:42 +00:00
#[serde(skip)]
2021-04-05 22:56:15 +00:00
pub id: String,
pub name: String,
2021-03-26 22:30:38 +00:00
}
2021-03-27 05:45:59 +00:00
#[derive(Debug, Deserialize)]
pub struct CreateUserRequest {
pub username: String,
pub password: String,
pub email: String,
pub role: Option<Role>
}
#[derive(Debug, Deserialize)]
2021-05-02 13:56:42 +00:00
pub struct AddZoneMemberRequest {
pub id: String,
}
2021-05-02 15:19:32 +00:00
#[derive(Debug, Deserialize)]
pub struct CreateZoneRequest {
pub name: AbsoluteName,
}
2021-03-26 22:30:38 +00:00
// pub struct LdapUserAssociation {
// user_id: Uuid,
// ldap_id: String
// }
2021-03-27 17:23:19 +00:00
#[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,
}
2021-03-27 05:45:59 +00:00
#[derive(Debug)]
2021-03-26 22:30:38 +00:00
pub struct UserInfo {
2021-03-27 05:45:59 +00:00
pub id: String,
pub role: Role,
2021-03-27 05:45:59 +00:00
pub username: String,
2021-03-26 22:30:38 +00:00
}
2021-04-05 22:56:15 +00:00
impl UserInfo {
pub fn is_admin(&self) -> bool {
matches!(self.role, Role::Admin)
}
pub fn check_admin(&self) -> Result<(), UserError> {
if self.is_admin() {
Ok(())
} else {
Err(UserError::PermissionDenied)
}
}
2021-04-05 22:56:15 +00:00
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)
}
2021-05-02 13:56:42 +00:00
pub fn get_zones(&self, conn: &diesel::SqliteConnection) -> Result<Vec<Zone>, UserError> {
use crate::schema::user_zone::dsl::*;
use crate::schema::zone::dsl::*;
2021-05-02 13:56:42 +00:00
let res: Vec<(Zone, UserZone)> = zone.inner_join(user_zone)
.filter(user_id.eq(&self.id))
.get_results(conn)
.map_err(UserError::DbError)?;
2021-05-02 13:56:42 +00:00
Ok(res.into_iter().map(|(z, _)| z).collect())
}
2021-04-05 22:56:15 +00:00
}
2021-04-02 17:33:59 +00:00
#[rocket::async_trait]
impl<'r> FromRequest<'r> for UserInfo {
2021-04-02 21:12:29 +00:00
type Error = ErrorResponse;
2021-03-27 19:36:57 +00:00
2021-04-02 17:33:59 +00:00
async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> {
2021-03-27 19:36:57 +00:00
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 {
2021-04-02 21:12:29 +00:00
return ErrorResponse::from(UserError::MalformedHeader).into()
2021-03-27 19:36:57 +00:00
};
2021-04-05 01:05:39 +00:00
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));
2021-03-26 22:30:38 +00:00
2021-03-27 19:36:57 +00:00
let token_data = AuthClaims::decode(
token, &config.web_app.secret
).map_err(|e| match e.into_kind() {
2021-04-02 21:12:29 +00:00
JwtErrorKind::ExpiredSignature => UserError::ExpiredToken,
_ => UserError::BadToken,
2021-03-27 19:36:57 +00:00
});
let token_data = match token_data {
2021-04-02 21:12:29 +00:00
Err(e) => return ErrorResponse::from(e).into(),
2021-03-27 19:36:57 +00:00
Ok(data) => data
};
let user_id = token_data.sub;
2021-05-02 13:56:42 +00:00
conn.run(move |c| {
match LocalUser::get_user_by_uuid(c, &user_id) {
2021-04-02 21:12:29 +00:00
Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(),
Err(e) => ErrorResponse::from(e).into(),
2021-04-02 17:33:59 +00:00
Ok(d) => Outcome::Success(d),
}
}).await
2021-03-26 22:30:38 +00:00
}
}
2021-03-27 05:45:59 +00:00
#[derive(Debug)]
pub enum UserError {
2021-04-05 22:56:15 +00:00
ZoneNotFound,
2021-03-27 05:45:59 +00:00
NotFound,
UserConflict,
BadCreds,
2021-03-27 19:36:57 +00:00
BadToken,
ExpiredToken,
MalformedHeader,
PermissionDenied,
2021-03-27 05:45:59 +00:00
DbError(DieselError),
PasswordError(HasherError),
}
impl From<HasherError> for UserError {
fn from(e: HasherError) -> Self {
2021-04-05 01:05:39 +00:00
UserError::PasswordError(e)
2021-03-27 05:45:59 +00:00
}
}
impl From<DieselError> for UserError {
fn from(e: DieselError) -> Self {
UserError::DbError(e)
}
}
2021-03-27 05:45:59 +00:00
impl LocalUser {
2021-04-02 17:33:59 +00:00
pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
2021-03-27 05:45:59 +00:00
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 {
2021-04-05 01:05:39 +00:00
user_id: new_user_id,
2021-03-27 05:45:59 +00:00
username: user_request.username.clone(),
password: make_password_with_algorithm(&user_request.password, Algorithm::Argon2),
2021-04-05 22:56:15 +00:00
// TODO: Use role from request
role: Role::ZoneAdmin,
2021-03-27 05:45:59 +00:00
};
let res = UserInfo {
id: new_user.id.clone(),
2021-04-05 22:56:15 +00:00
role: new_localuser.role.clone(),
2021-03-27 05:45:59 +00:00
username: new_localuser.username.clone(),
};
conn.immediate_transaction(|| -> diesel::QueryResult<()> {
diesel::insert_into(user)
.values(new_user)
2021-04-02 17:33:59 +00:00
.execute(conn)?;
2021-03-27 05:45:59 +00:00
diesel::insert_into(localuser)
.values(new_localuser)
2021-04-02 17:33:59 +00:00
.execute(conn)?;
2021-03-27 05:45:59 +00:00
Ok(())
}).map_err(|e| match e {
DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict,
other => UserError::DbError(other)
2021-03-27 05:45:59 +00:00
})?;
Ok(res)
}
pub fn get_user_by_creds(
2021-04-02 17:33:59 +00:00
conn: &diesel::SqliteConnection,
2021-03-27 05:45:59 +00:00
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))
.get_result(conn)
.map_err(|e| match e {
DieselError::NotFound => UserError::BadCreds,
other => UserError::DbError(other)
})?;
2021-03-27 05:45:59 +00:00
if !check_password(&request_password, &client_localuser.password)? {
return Err(UserError::BadCreds);
2021-03-27 05:45:59 +00:00
}
Ok(UserInfo {
id: client_user.id,
2021-04-05 22:56:15 +00:00
role: client_localuser.role,
2021-03-27 05:45:59 +00:00
username: client_localuser.username,
})
}
2021-05-02 13:56:42 +00:00
pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: &str) -> Result<UserInfo, UserError> {
2021-03-27 19:36:57 +00:00
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))
.get_result(conn)
.map_err(|e| match e {
DieselError::NotFound => UserError::NotFound,
other => UserError::DbError(other)
})?;
2021-03-27 19:36:57 +00:00
Ok(UserInfo {
id: client_user.id,
2021-04-05 22:56:15 +00:00
role: client_localuser.role,
2021-03-27 19:36:57 +00:00
username: client_localuser.username,
})
2021-03-27 05:45:59 +00:00
}
}
2021-03-27 17:23:19 +00:00
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 {
2021-04-05 01:05:39 +00:00
jti,
2021-03-27 17:23:19 +00:00
sub: user_info.id.clone(),
2021-04-05 01:05:39 +00:00
exp,
iat,
2021-03-27 17:23:19 +00:00
}
}
2021-03-27 19:36:57 +00:00
pub fn decode(token: &str, secret: &str) -> JwtResult<AuthClaims> {
decode::<AuthClaims>(
token,
&DecodingKey::from_secret(secret.as_ref()),
&Validation::new(JwtAlgorithm::HS256)
2021-04-05 01:05:39 +00:00
).map(|data| data.claims)
2021-03-27 19:36:57 +00:00
}
2021-03-27 17:31:14 +00:00
pub fn encode(self, secret: &str) -> JwtResult<String> {
2021-03-27 19:36:57 +00:00
encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref()))
2021-03-27 17:23:19 +00:00
}
}
2021-05-02 13:56:42 +00:00
// NOTE: Should probably not be implemented here
// also, "UserError" seems like a misleading name
impl Zone {
pub fn get_all(conn: &diesel::SqliteConnection) -> Result<Vec<Zone>, UserError> {
use crate::schema::zone::dsl::*;
zone.get_results(conn)
.map_err(UserError::DbError)
}
pub fn get_by_name(conn: &diesel::SqliteConnection, zone_name: &str) -> Result<Zone, UserError> {
use crate::schema::zone::dsl::*;
zone.filter(name.eq(zone_name))
.get_result(conn)
.map_err(|e| match e {
DieselError::NotFound => UserError::ZoneNotFound,
other => UserError::DbError(other)
})
}
2021-05-02 15:19:32 +00:00
pub fn create_zone(conn: &diesel::SqliteConnection, zone_request: CreateZoneRequest) -> Result<Zone, UserError> {
use crate::schema::zone::dsl::*;
let new_zone = Zone {
id: Uuid::new_v4().to_simple().to_string(),
name: zone_request.name.to_utf8(),
};
diesel::insert_into(zone)
.values(&new_zone)
.execute(conn)
.map_err(|e| match e {
DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict,
other => UserError::DbError(other)
})?;
Ok(new_zone)
}
2021-05-02 13:56:42 +00:00
pub fn add_member(&self, conn: &diesel::SqliteConnection, new_member: &UserInfo) -> Result<(), UserError> {
use crate::schema::user_zone::dsl::*;
let new_user_zone = UserZone {
zone_id: self.id.clone(),
user_id: new_member.id.clone()
};
let res = diesel::insert_into(user_zone)
.values(new_user_zone)
.execute(conn);
match res {
// If user has already access to the zone, safely ignore the conflit
// TODO: use 'on conflict do nothing' in postgres when we get there
Err(DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _)) => (),
Err(e) => return Err(e.into()),
Ok(_) => ()
};
Ok(())
}
}