Add authentication #1
					 18 changed files with 1558 additions and 583 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,3 +1,4 @@ | ||||||
| /target | /target | ||||||
| 
 | 
 | ||||||
| config.toml | config.toml | ||||||
|  | db.sqlite | ||||||
|  |  | ||||||
							
								
								
									
										1537
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1537
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										12
									
								
								Cargo.toml
									
										
									
									
									
								
							
							
						
						
									
										12
									
								
								Cargo.toml
									
										
									
									
									
								
							|  | @ -11,7 +11,15 @@ trust-dns-client = "0.20.1" | ||||||
| trust-dns-proto = "0.20.1" | trust-dns-proto = "0.20.1" | ||||||
| serde = { version = "1.0", features = ["derive"] } | serde = { version = "1.0", features = ["derive"] } | ||||||
| serde_json = "1.0" | serde_json = "1.0" | ||||||
| rocket = "0.4.7" | rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "0654890", version = "0.5.0-dev" } | ||||||
| rocket_contrib = { version = "0.4", default-features = false, features = ["json"]} | rocket_contrib = { git = "https://github.com/SergioBenitez/Rocket", rev = "0654890", default-features = false, features = ["json", "diesel_sqlite_pool"], version = "0.5.0-dev"} | ||||||
| toml = "0.5" | toml = "0.5" | ||||||
| base64 = "0.13.0" | base64 = "0.13.0" | ||||||
|  | uuid = { version = "0.8.2", features = ["v4", "serde"] } | ||||||
|  | diesel = { version = "1.4", features = ["sqlite"] } | ||||||
|  | diesel-derive-enum = { version = "1", features = ["sqlite"] } | ||||||
|  | djangohashers = { version = "1.4.0", features = ["with_argon2"], default-features = false } | ||||||
|  | jsonwebtoken = "7.2.0" | ||||||
|  | chrono = { version = "0.4", features = ["serde"] } | ||||||
|  | humantime = "2.1.0" | ||||||
|  | tokio = "1" | ||||||
|  |  | ||||||
							
								
								
									
										2
									
								
								Rocket.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								Rocket.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | [global.databases] | ||||||
|  | db = { url = "db.sqlite" } | ||||||
|  | @ -1,2 +1,7 @@ | ||||||
| [dns_server] | [web_app] | ||||||
| address = "127.0.0.1:53" | # base64 secret, change it (openssl rand -base64 32) | ||||||
|  | secret = "Y2hhbmdlbWUK" | ||||||
|  | token_duration = "1d" | ||||||
|  | 
 | ||||||
|  | [dns] | ||||||
|  | server = "127.0.0.1:53" | ||||||
|  |  | ||||||
							
								
								
									
										6
									
								
								diesel.toml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								diesel.toml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | # For documentation on how to configure this file, | ||||||
|  | # see diesel.rs/guides/configuring-diesel-cli | ||||||
|  | 
 | ||||||
|  | [print_schema] | ||||||
|  | file = "src/schema.rs" | ||||||
|  | import_types = ["diesel::sql_types::*", "crate::models::users::*"] | ||||||
							
								
								
									
										0
									
								
								migrations/.gitkeep
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								migrations/.gitkeep
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										3
									
								
								migrations/2021-03-26-164945_create_users/down.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								migrations/2021-03-26-164945_create_users/down.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,3 @@ | ||||||
|  | -- This file should undo anything in `up.sql` | ||||||
|  | DROP TABLE localuser; | ||||||
|  | DROP TABLE user; | ||||||
							
								
								
									
										12
									
								
								migrations/2021-03-26-164945_create_users/up.sql
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								migrations/2021-03-26-164945_create_users/up.sql
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | ||||||
|  | -- Your SQL goes here | ||||||
|  | CREATE TABLE localuser ( | ||||||
|  |   user_id VARCHAR NOT NULL PRIMARY KEY, | ||||||
|  |   username VARCHAR NOT NULL UNIQUE, | ||||||
|  |   password VARCHAR NOT NULL, | ||||||
|  |   FOREIGN KEY(user_id) REFERENCES user(id) | ||||||
|  | ); | ||||||
|  | 
 | ||||||
|  | CREATE TABLE user ( | ||||||
|  |   id VARCHAR NOT NULL PRIMARY KEY, | ||||||
|  |   role TEXT CHECK(role IN ('admin', 'zoneadmin')) NOT NULL -- note: migrate to postgres so enum are actually a thing | ||||||
|  | ); | ||||||
|  | @ -2,20 +2,39 @@ use std::net::SocketAddr; | ||||||
| use std::path::PathBuf; | use std::path::PathBuf; | ||||||
| use std::fs; | use std::fs; | ||||||
| 
 | 
 | ||||||
| use serde::{Deserialize}; | use serde::{Deserialize, Deserializer}; | ||||||
|  | use chrono::Duration; | ||||||
| use toml; | use toml; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| #[derive(Deserialize)] | #[derive(Debug, Deserialize)] | ||||||
| pub struct Config { | pub struct Config { | ||||||
|     pub dns_server: DnsServerConfig |     pub dns: DnsConfig, | ||||||
|  |     pub web_app: WebAppConfig, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Deserialize)] | #[derive(Debug, Deserialize)] | ||||||
| pub struct DnsServerConfig { | pub struct DnsConfig { | ||||||
|     pub address: SocketAddr |     pub server: SocketAddr | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Deserialize)] | ||||||
|  | pub struct WebAppConfig { | ||||||
|  |     pub secret: String, | ||||||
|  |     #[serde(deserialize_with = "from_duration")] | ||||||
|  |     pub token_duration: Duration, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | fn from_duration<'de, D>(deserializer: D) -> Result<Duration, D::Error> | ||||||
|  |     where D: Deserializer<'de> | ||||||
|  | { | ||||||
|  |     use serde::de::Error; | ||||||
|  |     String::deserialize(deserializer) | ||||||
|  |         .and_then(|string| humantime::parse_duration(&string).map_err(|err| Error::custom(err.to_string()))) | ||||||
|  |         .and_then(|duration| Duration::from_std(duration).map_err(|err| Error::custom(err.to_string()))) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| pub fn load(file_name: PathBuf) -> Config { | pub fn load(file_name: PathBuf) -> Config { | ||||||
|     toml::from_str(&fs::read_to_string(file_name).expect("could not read config file")).expect("could not parse config file") |     let file_content = fs::read_to_string(file_name).expect("could not read config file"); | ||||||
|  |     toml::from_str(&file_content).expect("could not parse config file") | ||||||
| } | } | ||||||
|  |  | ||||||
							
								
								
									
										68
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										68
									
								
								src/main.rs
									
										
									
									
									
								
							|  | @ -1,56 +1,48 @@ | ||||||
| #![feature(proc_macro_hygiene, decl_macro)] | #![feature(proc_macro_hygiene, decl_macro)] | ||||||
| 
 | 
 | ||||||
| #[macro_use] extern crate rocket; | #[macro_use] extern crate rocket; | ||||||
| use rocket::State; | #[macro_use] extern crate rocket_contrib; | ||||||
| use rocket::http::Status; | #[macro_use] extern crate diesel; | ||||||
| use rocket_contrib::json::Json; |  | ||||||
| 
 | 
 | ||||||
| use trust_dns_client::client::{Client, SyncClient}; | use trust_dns_client::client::AsyncClient; | ||||||
| use trust_dns_client::tcp::TcpClientConnection; | use trust_dns_client::tcp::TcpClientStream; | ||||||
| use trust_dns_client::op::{DnsResponse, ResponseCode}; | use trust_dns_proto::xfer::dns_multiplexer::DnsMultiplexer; | ||||||
| use trust_dns_client::rr::{DNSClass, Name, RecordType}; | use trust_dns_proto::iocompat::AsyncIoTokioAsStd; | ||||||
|  | use trust_dns_client::rr::dnssec::Signer; | ||||||
|  | use tokio::net::TcpStream as TokioTcpStream; | ||||||
|  | use tokio::task; | ||||||
|  | 
 | ||||||
|  | use std::sync::{Arc, Mutex}; | ||||||
| 
 | 
 | ||||||
| mod models; | mod models; | ||||||
| mod config; | mod config; | ||||||
|  | mod schema; | ||||||
|  | mod routes; | ||||||
| 
 | 
 | ||||||
| use models::errors::ErrorResponse; | use routes::users::*; | ||||||
|  | use routes::zones::*; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| #[get("/zones/<zone>/records")] | #[database("db")] | ||||||
| fn get_zone_records(client: State<SyncClient<TcpClientConnection>>, zone: String) -> Result<Json<Vec<models::dns::Record>>, ErrorResponse<()>> { | pub struct DbConn(diesel::SqliteConnection); | ||||||
|     // TODO: Implement FromParam for Name
 |  | ||||||
|     let name = Name::from_utf8(&zone).unwrap(); |  | ||||||
| 
 | 
 | ||||||
|     let response: DnsResponse = client.query(&name, DNSClass::IN, RecordType::AXFR).unwrap(); | type DnsClient = Arc<Mutex<AsyncClient>>; | ||||||
| 
 | 
 | ||||||
|     if response.response_code() != ResponseCode::NoError { |  | ||||||
|         return ErrorResponse::new( |  | ||||||
|             Status::NotFound, |  | ||||||
|             format!("zone {} could not be found", name.to_utf8()) |  | ||||||
|         ).err() |  | ||||||
|     } |  | ||||||
| 
 | 
 | ||||||
|     let answers = response.answers(); | #[launch] | ||||||
|     let mut records: Vec<_> = answers.to_vec().into_iter() | async fn rocket() -> rocket::Rocket { | ||||||
|         .map(|record| models::dns::Record::from(record)) |  | ||||||
|         .filter(|record| match record.rdata { |  | ||||||
|             models::dns::RData::NULL { .. } | models::dns::RData::DNSSEC(_) => false, |  | ||||||
|             _ => true, |  | ||||||
|         }).collect(); |  | ||||||
| 
 |  | ||||||
|     // AXFR response ends with SOA, we remove it so it is not doubled in the response.
 |  | ||||||
|     records.pop(); |  | ||||||
| 
 |  | ||||||
|     Ok(Json(records)) |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| fn main() { |  | ||||||
|     let app_config = config::load("config.toml".into()); |     let app_config = config::load("config.toml".into()); | ||||||
|  |     println!("{:#?}", app_config); | ||||||
| 
 | 
 | ||||||
|     let conn = TcpClientConnection::new(app_config.dns_server.address).unwrap(); |     let (stream, handle) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(app_config.dns.server); | ||||||
|     let client = SyncClient::new(conn); |     let multiplexer = DnsMultiplexer::<_, Signer>::new(stream, handle, None); | ||||||
|  |     let client = AsyncClient::connect(multiplexer); | ||||||
|  |     let (client, bg) = client.await.expect("connection failed"); | ||||||
|  |     task::spawn(bg); | ||||||
| 
 | 
 | ||||||
|     rocket::ignite() |     rocket::ignite() | ||||||
|         .manage(client) |         .manage(Arc::new(Mutex::new(client))) | ||||||
|         .mount("/api/v1", routes![get_zone_records]).launch(); |         .manage(app_config) | ||||||
|  |         .attach(DbConn::fairing()) | ||||||
|  |         .mount("/api/v1", routes![get_zone_records, create_auth_token, create_user]) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -1,21 +1,22 @@ | ||||||
| use serde::Serialize; | use serde::Serialize; | ||||||
| use rocket::http::Status; | use rocket::http::Status; | ||||||
| use rocket::request::Request; | use rocket::request::{Request, Outcome}; | ||||||
| use rocket::response::{self, Response, Responder}; | use rocket::response::{self, Response, Responder}; | ||||||
| use rocket_contrib::json::Json; | use rocket_contrib::json::Json; | ||||||
|  | use crate::models::users::UserError; | ||||||
|  | use serde_json::Value; | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| #[derive(Serialize, Debug)] | #[derive(Serialize, Debug)] | ||||||
| pub struct ErrorResponse<T> { | pub struct ErrorResponse { | ||||||
|     #[serde(with = "StatusDef")] |     #[serde(with = "StatusDef")] | ||||||
|     #[serde(flatten)] |     #[serde(flatten)] | ||||||
|     pub status: Status, |     pub status: Status, | ||||||
|     pub message: String, |     pub message: String, | ||||||
|     #[serde(skip_serializing_if = "Option::is_none")] |     #[serde(skip_serializing_if = "Option::is_none")] | ||||||
|     pub details: Option<T> |     pub details: Option<Value> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| #[derive(Serialize)] | #[derive(Serialize)] | ||||||
| #[serde(remote = "Status")] | #[serde(remote = "Status")] | ||||||
| struct StatusDef { | struct StatusDef { | ||||||
|  | @ -24,9 +25,8 @@ struct StatusDef { | ||||||
|     reason: &'static str, |     reason: &'static str, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | impl ErrorResponse { | ||||||
| impl<T> ErrorResponse<T> { |     pub fn new(status: Status, message: String) -> ErrorResponse { | ||||||
|     pub fn new(status: Status, message: String) -> ErrorResponse<T> { |  | ||||||
|         ErrorResponse { |         ErrorResponse { | ||||||
|             status, |             status, | ||||||
|             message, |             message, | ||||||
|  | @ -34,22 +34,58 @@ impl<T> ErrorResponse<T> { | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn with_details(self, details: T) -> ErrorResponse<T> { |     pub fn with_details<T: Serialize> (self, details: T) -> ErrorResponse { | ||||||
|         ErrorResponse { |         ErrorResponse { | ||||||
|             details: Some(details), |             details: serde_json::to_value(details).ok(), | ||||||
|             ..self |             ..self | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn err<R>(self) -> Result<R, ErrorResponse<T>> { |     pub fn err<R>(self) -> Result<R, ErrorResponse> { | ||||||
|         Err(self) |         Err(self) | ||||||
|     } |     } | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| 
 | impl<'r> Responder<'r, 'static> for ErrorResponse { | ||||||
| impl<'r, T: Serialize> Responder<'r> for ErrorResponse<T> { |     fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { | ||||||
|     fn respond_to(self, req: &Request) -> response::Result<'r> { |  | ||||||
|         let status = self.status; |         let status = self.status; | ||||||
|         Response::build_from(Json(self).respond_to(req)?).status(status).ok() |         Response::build_from(Json(self).respond_to(req)?).status(status).ok() | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | 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::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::DbError(e) => make_500(e), | ||||||
|  |             UserError::PasswordError(e) => make_500(e) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | impl<S> Into<Outcome<S, ErrorResponse>> for ErrorResponse { | ||||||
|  |     fn into(self) -> Outcome<S, ErrorResponse> { | ||||||
|  |         Outcome::Failure(self.into()) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl Into<(Status, ErrorResponse)> for ErrorResponse { | ||||||
|  |     fn into(self) -> (Status, ErrorResponse) { | ||||||
|  |         (self.status.clone(), self) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn make_500<E: std::fmt::Debug>(e: E) -> ErrorResponse { | ||||||
|  |     println!("Making 500 for Error: {:?}", e); | ||||||
|  |     ErrorResponse::new(Status::InternalServerError, "An unexpected error occured.".into()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | pub fn make_500_tuple<E: std::fmt::Debug>(e: E) -> (Status, ErrorResponse) { | ||||||
|  |     make_500(e).into() | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @ -1,5 +1,6 @@ | ||||||
| pub mod dns; | pub mod dns; | ||||||
| pub mod errors; | pub mod errors; | ||||||
|  | pub mod users; | ||||||
| 
 | 
 | ||||||
| pub mod trust_dns_types { | pub mod trust_dns_types { | ||||||
|     pub use trust_dns_client::rr::rdata::{ |     pub use trust_dns_client::rr::rdata::{ | ||||||
|  |  | ||||||
							
								
								
									
										277
									
								
								src/models/users.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										277
									
								
								src/models/users.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,277 @@ | ||||||
|  | use uuid::Uuid; | ||||||
|  | use diesel::prelude::*; | ||||||
|  | use diesel::result::Error as DieselError; | ||||||
|  | use diesel_derive_enum::DbEnum; | ||||||
|  | use rocket::{State, request::{FromRequest, Request, Outcome}}; | ||||||
|  | use serde::{Serialize, Deserialize}; | ||||||
|  | use chrono::serde::ts_seconds; | ||||||
|  | use chrono::prelude::{DateTime, Utc}; | ||||||
|  | use chrono::Duration; | ||||||
|  | // TODO: Maybe just use argon2 crate directly
 | ||||||
|  | use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm}; | ||||||
|  | use jsonwebtoken::{ | ||||||
|  |     encode, decode, | ||||||
|  |     Header, Validation, | ||||||
|  |     Algorithm as JwtAlgorithm, EncodingKey, DecodingKey, | ||||||
|  |     errors::Result as JwtResult, | ||||||
|  |     errors::ErrorKind as JwtErrorKind | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | use crate::schema::*; | ||||||
|  | use crate::DbConn; | ||||||
|  | use crate::config::Config; | ||||||
|  | use crate::models::errors::{ErrorResponse, make_500_tuple}; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | const BEARER: &'static str = "Bearer "; | ||||||
|  | const AUTH_HEADER: &'static str = "Authentication"; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, DbEnum, Deserialize, Clone)] | ||||||
|  | #[serde(rename_all = "lowercase")] | ||||||
|  | pub enum Role { | ||||||
|  |     #[db_rename = "admin"] | ||||||
|  |     Admin, | ||||||
|  |     #[db_rename = "zoneadmin"] | ||||||
|  |     ZoneAdmin, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // TODO: Store Uuid instead of string??
 | ||||||
|  | // TODO: Store role as Role and not String.
 | ||||||
|  | #[derive(Debug, Queryable, Identifiable, Insertable)] | ||||||
|  | #[table_name = "user"] | ||||||
|  | pub struct User { | ||||||
|  |     pub id: String, | ||||||
|  |     pub role: Role, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Queryable, Identifiable, Insertable)] | ||||||
|  | #[table_name = "localuser"] | ||||||
|  | #[primary_key(user_id)] | ||||||
|  | pub struct LocalUser { | ||||||
|  |     pub user_id: String, | ||||||
|  |     pub username: String, | ||||||
|  |     pub password: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug, Deserialize)] | ||||||
|  | pub struct CreateUserRequest { | ||||||
|  |     pub username: String, | ||||||
|  |     pub password: String, | ||||||
|  |     pub email: String, | ||||||
|  |     pub role: Option<Role> | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | // pub struct LdapUserAssociation {
 | ||||||
|  | //     user_id: Uuid,
 | ||||||
|  | //     ldap_id: String
 | ||||||
|  | // }
 | ||||||
|  | 
 | ||||||
|  | #[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, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub struct UserInfo { | ||||||
|  |     pub id: String, | ||||||
|  |     pub role: Role, | ||||||
|  |     pub username: String, | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[rocket::async_trait] | ||||||
|  | impl<'r> FromRequest<'r> for UserInfo { | ||||||
|  |     type Error = ErrorResponse; | ||||||
|  | 
 | ||||||
|  |     async fn from_request(request: &'r Request<'_>) -> Outcome<Self, Self::Error> { | ||||||
|  |         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 { | ||||||
|  |             return ErrorResponse::from(UserError::MalformedHeader).into() | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let config = try_outcome!(request.guard::<State<Config>>().await.map_failure(make_500_tuple)); | ||||||
|  |         let conn = try_outcome!(request.guard::<DbConn>().await.map_failure(make_500_tuple)); | ||||||
|  | 
 | ||||||
|  |         let token_data = AuthClaims::decode( | ||||||
|  |             token, &config.web_app.secret | ||||||
|  |         ).map_err(|e| match e.into_kind() { | ||||||
|  |             JwtErrorKind::ExpiredSignature => UserError::ExpiredToken, | ||||||
|  |             _ => UserError::BadToken, | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         let token_data = match token_data { | ||||||
|  |             Err(e) => return ErrorResponse::from(e).into(), | ||||||
|  |             Ok(data) => data | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let user_id = token_data.sub; | ||||||
|  | 
 | ||||||
|  |         conn.run(|c| { | ||||||
|  |             match LocalUser::get_user_by_uuid(c, user_id) { | ||||||
|  |                 Err(UserError::NotFound) => ErrorResponse::from(UserError::NotFound).into(), | ||||||
|  |                 Err(e) => ErrorResponse::from(e).into(), | ||||||
|  |                 Ok(d) => Outcome::Success(d), | ||||||
|  |             } | ||||||
|  |         }).await | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | #[derive(Debug)] | ||||||
|  | pub enum UserError { | ||||||
|  |     NotFound, | ||||||
|  |     UserExists, | ||||||
|  |     BadToken, | ||||||
|  |     ExpiredToken, | ||||||
|  |     MalformedHeader, | ||||||
|  |     PermissionDenied, | ||||||
|  |     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 { | ||||||
|  |         match e { | ||||||
|  |             other => UserError::PasswordError(other) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | impl LocalUser { | ||||||
|  |     pub fn create_user(conn: &diesel::SqliteConnection, user_request: CreateUserRequest) -> Result<UserInfo, UserError> { | ||||||
|  |         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
 | ||||||
|  |             role: Role::ZoneAdmin, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let new_localuser = LocalUser { | ||||||
|  |             user_id: new_user_id.clone(), | ||||||
|  |             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) | ||||||
|  |                 .execute(conn)?; | ||||||
|  | 
 | ||||||
|  |             diesel::insert_into(localuser) | ||||||
|  |                 .values(new_localuser) | ||||||
|  |                 .execute(conn)?; | ||||||
|  | 
 | ||||||
|  |             Ok(()) | ||||||
|  |         })?; | ||||||
|  | 
 | ||||||
|  |         Ok(res) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn get_user_by_creds( | ||||||
|  |         conn: &diesel::SqliteConnection, | ||||||
|  |         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)?; | ||||||
|  | 
 | ||||||
|  |         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, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn get_user_by_uuid(conn: &diesel::SqliteConnection, request_user_id: String) -> 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(id.eq(request_user_id)) | ||||||
|  |             .get_result(conn)?; | ||||||
|  | 
 | ||||||
|  |         Ok(UserInfo { | ||||||
|  |             id: client_user.id, | ||||||
|  |             role: client_user.role, | ||||||
|  |             username: client_localuser.username, | ||||||
|  |         }) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 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 { | ||||||
|  |             jti: jti, | ||||||
|  |             sub: user_info.id.clone(), | ||||||
|  |             exp: exp, | ||||||
|  |             iat: iat, | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn decode(token: &str, secret: &str) -> JwtResult<AuthClaims> { | ||||||
|  |         decode::<AuthClaims>( | ||||||
|  |             token, | ||||||
|  |             &DecodingKey::from_secret(secret.as_ref()), | ||||||
|  |             &Validation::new(JwtAlgorithm::HS256) | ||||||
|  |         ).and_then(|data| Ok(data.claims)) | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     pub fn encode(self, secret: &str) -> JwtResult<String> { | ||||||
|  |         encode(&Header::default(), &self, &EncodingKey::from_secret(secret.as_ref())) | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										2
									
								
								src/routes/mod.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/routes/mod.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,2 @@ | ||||||
|  | pub mod users; | ||||||
|  | pub mod zones; | ||||||
							
								
								
									
										39
									
								
								src/routes/users.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/routes/users.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,39 @@ | ||||||
|  | use rocket_contrib::json::Json; | ||||||
|  | use rocket::{Response, State}; | ||||||
|  | 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}; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #[post("/users/me/token", data = "<auth_request>")] | ||||||
|  | pub async fn create_auth_token( | ||||||
|  |     conn: DbConn, | ||||||
|  |     config: State<'_, Config>, | ||||||
|  |     auth_request: Json<AuthTokenRequest> | ||||||
|  | ) -> Result<Json<AuthTokenResponse>, ErrorResponse> { | ||||||
|  | 
 | ||||||
|  |     let user_info = conn.run(move |c| { | ||||||
|  |         LocalUser::get_user_by_creds(c, &auth_request.username, &auth_request.password) | ||||||
|  |     }).await?; | ||||||
|  | 
 | ||||||
|  |     let token = AuthClaims::new(&user_info, config.web_app.token_duration) | ||||||
|  |         .encode(&config.web_app.secret) | ||||||
|  |         .map_err(|e| make_500(e))?; | ||||||
|  | 
 | ||||||
|  |     Ok(Json(AuthTokenResponse { 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| { | ||||||
|  |         LocalUser::create_user(&c, user_request.into_inner()) | ||||||
|  |     }).await?; | ||||||
|  | 
 | ||||||
|  |     Response::build() | ||||||
|  |         .status(Status::Created) | ||||||
|  |         .ok() | ||||||
|  | } | ||||||
							
								
								
									
										51
									
								
								src/routes/zones.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/routes/zones.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,51 @@ | ||||||
|  | use rocket::State; | ||||||
|  | use rocket::http::Status; | ||||||
|  | 
 | ||||||
|  | use rocket_contrib::json::Json; | ||||||
|  | 
 | ||||||
|  | use trust_dns_client::client::ClientHandle; | ||||||
|  | use trust_dns_client::op::{DnsResponse, ResponseCode}; | ||||||
|  | use trust_dns_client::rr::{DNSClass, Name, RecordType}; | ||||||
|  | 
 | ||||||
|  | use crate::models::dns; | ||||||
|  | use crate::models::errors::{ErrorResponse, make_500}; | ||||||
|  | use crate::models::users::UserInfo; | ||||||
|  | use crate::DnsClient; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #[get("/zones/<zone>/records")] | ||||||
|  | pub async fn get_zone_records( | ||||||
|  |     client: State<'_, DnsClient>, | ||||||
|  |     user_info: Result<UserInfo, ErrorResponse>, | ||||||
|  |     zone: String | ||||||
|  | ) -> Result<Json<Vec<dns::Record>>, ErrorResponse> { | ||||||
|  |     println!("{:#?}", user_info?); | ||||||
|  | 
 | ||||||
|  |     // TODO: Implement FromParam for Name
 | ||||||
|  |     let name = Name::from_utf8(&zone).unwrap(); | ||||||
|  | 
 | ||||||
|  |     let response: DnsResponse = { | ||||||
|  |         let query = client.lock().unwrap().query(name.clone(), DNSClass::IN, RecordType::AXFR); | ||||||
|  |         query.await.map_err(make_500)? | ||||||
|  |     }; | ||||||
|  | 
 | ||||||
|  |     if response.response_code() != ResponseCode::NoError { | ||||||
|  |         return ErrorResponse::new( | ||||||
|  |             Status::NotFound, | ||||||
|  |             format!("zone {} could not be found", name.to_utf8()) | ||||||
|  |         ).err() | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     let answers = response.answers(); | ||||||
|  |     let mut records: Vec<_> = answers.to_vec().into_iter() | ||||||
|  |         .map(|record| dns::Record::from(record)) | ||||||
|  |         .filter(|record| match record.rdata { | ||||||
|  |             dns::RData::NULL { .. } | dns::RData::DNSSEC(_) => false, | ||||||
|  |             _ => true, | ||||||
|  |         }).collect(); | ||||||
|  | 
 | ||||||
|  |     // AXFR response ends with SOA, we remove it so it is not doubled in the response.
 | ||||||
|  |     records.pop(); | ||||||
|  | 
 | ||||||
|  |     Ok(Json(records)) | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								src/schema.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/schema.rs
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,26 @@ | ||||||
|  | table! { | ||||||
|  |     use diesel::sql_types::*; | ||||||
|  | 
 | ||||||
|  |     localuser (user_id) { | ||||||
|  |         user_id -> Text, | ||||||
|  |         username -> Text, | ||||||
|  |         password -> Text, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | table! { | ||||||
|  |     use diesel::sql_types::*; | ||||||
|  |     use crate::models::users::*; | ||||||
|  | 
 | ||||||
|  |     user (id) { | ||||||
|  |         id -> Text, | ||||||
|  |         role -> RoleMapping, | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | joinable!(localuser -> user (user_id)); | ||||||
|  | 
 | ||||||
|  | allow_tables_to_appear_in_same_query!( | ||||||
|  |     localuser, | ||||||
|  |     user, | ||||||
|  | ); | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue