add some kind of authorization #5

Open
hannaeko wants to merge 2 commits from feature/authz into master
6 changed files with 128 additions and 24 deletions
Showing only changes of commit 2a3354456a - Show all commits

View File

@ -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
])
}

View File

@ -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<trust_dns_types::Record> for Record {
}
}
#[derive(Debug)]
pub struct AbsoluteName(Name);
impl<'r> FromParam<'r> for AbsoluteName {
type Error = ProtoError;
fn from_param(param: &'r str) -> Result<Self, Self::Error> {
let mut name = Name::from_utf8(&param).unwrap();
let mut name = Name::from_utf8(&param)?;
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<D>(deserializer: D) -> Result<Self, D::Error>
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 {

View File

@ -56,13 +56,14 @@ impl<'r> Responder<'r, 'static> for ErrorResponse {
impl From<UserError> 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)
}

View File

@ -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<Role>
}
#[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<Zone, UserError> {
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<Zone, UserError> {
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<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 {
UserError::PasswordError(e)
}
}
impl From<DieselError> for UserError {
fn from(e: DieselError) -> Self {
UserError::DbError(e)
}
}
impl LocalUser {
pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> {
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,

View File

@ -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 = "<auth_request>")]
@ -29,7 +37,7 @@ pub async fn create_auth_token(
#[post("/users", data = "<user_request>")]
pub async fn create_user<'r>(conn: DbConn, user_request: Json<CreateUserRequest>) -> Result<Response<'r>, 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<CreateUserRequest>
.status(Status::Created)
.ok()
}
#[post("/users/<user_id>/zones", data = "<user_zone_request>")]
pub async fn create_user_zone<'r>(
conn: DbConn,
user_info: Result<UserInfo, ErrorResponse>,
user_id: String,
user_zone_request: Json<CreateUserZoneRequest>
) -> Result<Response<'r>, 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()
}

View File

@ -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)?
};