From 2a3354456a426903a76fb53bfce5fcdb46604a0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20Berthaud-M=C3=BCller?= Date: Mon, 5 Apr 2021 22:39:56 -0400 Subject: [PATCH] add route to give user access to a zone --- src/main.rs | 7 +++- src/models/dns.rs | 21 ++++++++++-- src/models/errors.rs | 7 ++-- src/models/users.rs | 82 ++++++++++++++++++++++++++++++++++++-------- src/routes/users.rs | 33 ++++++++++++++++-- src/routes/zones.rs | 2 +- 6 files changed, 128 insertions(+), 24 deletions(-) diff --git a/src/main.rs b/src/main.rs index efd3513..c9cc829 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,5 +24,10 @@ async fn rocket() -> rocket::Rocket { rocket::ignite() .manage(app_config) .attach(DbConn::fairing()) - .mount("/api/v1", routes![get_zone_records, create_auth_token, create_user]) + .mount("/api/v1", routes![ + get_zone_records, + create_user_zone, + create_auth_token, + create_user + ]) } diff --git a/src/models/dns.rs b/src/models/dns.rs index cd4ea90..2bb22b9 100644 --- a/src/models/dns.rs +++ b/src/models/dns.rs @@ -5,7 +5,7 @@ use std::ops::{Deref, DerefMut}; use rocket::{Request, State, http::Status, request::{FromParam, FromRequest, Outcome}}; -use serde::{Serialize, Deserialize}; +use serde::{Deserialize, Deserializer, Serialize}; use tokio::{net::TcpStream as TokioTcpStream, task}; @@ -250,13 +250,14 @@ impl From for Record { } } +#[derive(Debug)] pub struct AbsoluteName(Name); impl<'r> FromParam<'r> for AbsoluteName { type Error = ProtoError; fn from_param(param: &'r str) -> Result { - let mut name = Name::from_utf8(¶m).unwrap(); + let mut name = Name::from_utf8(¶m)?; if !name.is_fqdn() { name.set_fqdn(true); } @@ -264,6 +265,21 @@ impl<'r> FromParam<'r> for AbsoluteName { } } +impl<'de> Deserialize<'de> for AbsoluteName { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de> + { + use serde::de::Error; + + String::deserialize(deserializer) + .and_then(|string| + AbsoluteName::from_param(&string) + .map_err(|e| Error::custom(e.to_string())) + ) + } +} + impl Deref for AbsoluteName { type Target = Name; fn deref(&self) -> &Self::Target { @@ -271,6 +287,7 @@ impl Deref for AbsoluteName { } } + pub struct DnsClient(AsyncClient); impl Deref for DnsClient { diff --git a/src/models/errors.rs b/src/models/errors.rs index 71c463d..589d10d 100644 --- a/src/models/errors.rs +++ b/src/models/errors.rs @@ -56,13 +56,14 @@ impl<'r> Responder<'r, 'static> for ErrorResponse { impl From for ErrorResponse { fn from(e: UserError) -> Self { match e { - UserError::NotFound => ErrorResponse::new(Status::Unauthorized, "Provided credentials or token do not match any existing user".into()), - UserError::UserExists => ErrorResponse::new(Status::Conflict, "User already exists".into()), + UserError::BadCreds => ErrorResponse::new(Status::Unauthorized, "Provided credentials or token do not match any existing user".into()), + UserError::UserConflict => ErrorResponse::new(Status::Conflict, "This user already exists".into()), + UserError::NotFound => ErrorResponse::new(Status::NotFound, "User does not exist".into()), UserError::BadToken => ErrorResponse::new(Status::BadRequest, "Malformed token".into()), UserError::ExpiredToken => ErrorResponse::new(Status::Unauthorized, "The provided token has expired".into()), UserError::MalformedHeader => ErrorResponse::new(Status::BadRequest, "Malformed authorization header".into()), UserError::PermissionDenied => ErrorResponse::new(Status::Forbidden, "Bearer is not authorized to access the resource".into()), - UserError::ZoneNotFound => ErrorResponse::new(Status::NotFound, "DNS zone does not exists".into()), + UserError::ZoneNotFound => ErrorResponse::new(Status::NotFound, "DNS zone does not exist".into()), UserError::DbError(e) => make_500(e), UserError::PasswordError(e) => make_500(e) } diff --git a/src/models/users.rs b/src/models/users.rs index 2e4d319..71def04 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -21,6 +21,7 @@ use crate::schema::*; use crate::DbConn; use crate::config::Config; use crate::models::errors::{ErrorResponse, make_500}; +use crate::models::dns::AbsoluteName; const BEARER: &str = "Bearer "; @@ -77,6 +78,11 @@ pub struct CreateUserRequest { pub role: Option } +#[derive(Debug, Deserialize)] +pub struct CreateUserZoneRequest { + pub zone: AbsoluteName, +} + // pub struct LdapUserAssociation { // user_id: Uuid, // ldap_id: String @@ -115,6 +121,14 @@ impl UserInfo { matches!(self.role, Role::Admin) } + pub fn check_admin(&self) -> Result<(), UserError> { + if self.is_admin() { + Ok(()) + } else { + Err(UserError::PermissionDenied) + } + } + pub fn get_zone(&self, conn: &diesel::SqliteConnection, zone_name: &str) -> Result { use crate::schema::user_zone::dsl::*; use crate::schema::zone::dsl::*; @@ -130,6 +144,38 @@ impl UserInfo { Ok(res_zone) } + + pub fn add_zone(&self, conn: &diesel::SqliteConnection, request: CreateUserZoneRequest) -> Result { + use crate::schema::user_zone::dsl::*; + use crate::schema::zone::dsl::*; + + let zone_name = request.zone.to_utf8(); + let current_zone: Zone = zone.filter(name.eq(zone_name)) + .get_result(conn) + .map_err(|e| match e { + DieselError::NotFound => UserError::ZoneNotFound, + other => UserError::DbError(other) + })?; + + let new_user_zone = UserZone { + zone_id: current_zone.id.clone(), + user_id: self.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(current_zone) + } } #[rocket::async_trait] @@ -179,7 +225,8 @@ impl<'r> FromRequest<'r> for UserInfo { pub enum UserError { ZoneNotFound, NotFound, - UserExists, + UserConflict, + BadCreds, BadToken, ExpiredToken, MalformedHeader, @@ -188,22 +235,18 @@ pub enum UserError { PasswordError(HasherError), } -impl From 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 for UserError { fn from(e: HasherError) -> Self { UserError::PasswordError(e) } } +impl From for UserError { + fn from(e: DieselError) -> Self { + UserError::DbError(e) + } +} + impl LocalUser { pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result { use crate::schema::localuser::dsl::*; @@ -239,6 +282,9 @@ impl LocalUser { .execute(conn)?; Ok(()) + }).map_err(|e| match e { + DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserConflict, + other => UserError::DbError(other) })?; Ok(res) @@ -255,10 +301,14 @@ impl LocalUser { let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser) .filter(username.eq(request_username)) - .get_result(conn)?; + .get_result(conn) + .map_err(|e| match e { + DieselError::NotFound => UserError::BadCreds, + other => UserError::DbError(other) + })?; if !check_password(&request_password, &client_localuser.password)? { - return Err(UserError::NotFound); + return Err(UserError::BadCreds); } Ok(UserInfo { @@ -274,7 +324,11 @@ impl LocalUser { let (client_user, client_localuser): (User, LocalUser) = user.inner_join(localuser) .filter(id.eq(request_user_id)) - .get_result(conn)?; + .get_result(conn) + .map_err(|e| match e { + DieselError::NotFound => UserError::NotFound, + other => UserError::DbError(other) + })?; Ok(UserInfo { id: client_user.id, diff --git a/src/routes/users.rs b/src/routes/users.rs index d4c65a3..9626531 100644 --- a/src/routes/users.rs +++ b/src/routes/users.rs @@ -5,7 +5,15 @@ use rocket::http::Status; use crate::config::Config; use crate::DbConn; use crate::models::errors::{ErrorResponse, make_500}; -use crate::models::users::{LocalUser, CreateUserRequest, AuthClaims, AuthTokenRequest, AuthTokenResponse}; +use crate::models::users::{ + UserInfo, + LocalUser, + CreateUserRequest, + CreateUserZoneRequest, + AuthClaims, + AuthTokenRequest, + AuthTokenResponse +}; #[post("/users/me/token", data = "")] @@ -27,9 +35,9 @@ pub async fn create_auth_token( } #[post("/users", data = "")] -pub async fn create_user<'r>(conn: DbConn, user_request: Json) -> Result, ErrorResponse>{ +pub async fn create_user<'r>(conn: DbConn, user_request: Json) -> Result, ErrorResponse> { // TODO: Check current user if any to check if user has permission to create users (with or without role) - let _user_info = conn.run(|c| { + conn.run(|c| { LocalUser::create_user(&c, user_request.into_inner()) }).await?; @@ -37,3 +45,22 @@ pub async fn create_user<'r>(conn: DbConn, user_request: Json .status(Status::Created) .ok() } + +#[post("/users//zones", data = "")] +pub async fn create_user_zone<'r>( + conn: DbConn, + user_info: Result, + user_id: String, + user_zone_request: Json +) -> Result, ErrorResponse>{ + user_info?.check_admin()?; + + conn.run(move |c| { + let user_info = LocalUser::get_user_by_uuid(&c, user_id)?; + user_info.add_zone(&c, user_zone_request.into_inner()) + }).await?; + + Response::build() + .status(Status::Created) + .ok() +} diff --git a/src/routes/zones.rs b/src/routes/zones.rs index d5ee92d..c872d72 100644 --- a/src/routes/zones.rs +++ b/src/routes/zones.rs @@ -29,7 +29,7 @@ pub async fn get_zone_records( } let response = { - let query = client.query((*zone).clone(), DNSClass::IN, RecordType::AXFR); + let query = client.query(zone.clone(), DNSClass::IN, RecordType::AXFR); query.await.map_err(make_500)? };