Add authentication #1
					 18 changed files with 1558 additions and 583 deletions
				
			
		
							
								
								
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
										
									
									
										vendored
									
									
								
							|  | @ -1,3 +1,4 @@ | |||
| /target | ||||
| 
 | ||||
| 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" | ||||
| serde = { version = "1.0", features = ["derive"] } | ||||
| serde_json = "1.0" | ||||
| rocket = "0.4.7" | ||||
| rocket_contrib = { version = "0.4", default-features = false, features = ["json"]} | ||||
| rocket = { git = "https://github.com/SergioBenitez/Rocket", rev = "0654890", version = "0.5.0-dev" } | ||||
| 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" | ||||
| 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] | ||||
| address = "127.0.0.1:53" | ||||
| [web_app] | ||||
| # 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::fs; | ||||
| 
 | ||||
| use serde::{Deserialize}; | ||||
| use serde::{Deserialize, Deserializer}; | ||||
| use chrono::Duration; | ||||
| use toml; | ||||
| 
 | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct Config { | ||||
|     pub dns_server: DnsServerConfig | ||||
|     pub dns: DnsConfig, | ||||
|     pub web_app: WebAppConfig, | ||||
| } | ||||
| 
 | ||||
| #[derive(Deserialize)] | ||||
| pub struct DnsServerConfig { | ||||
|     pub address: SocketAddr | ||||
| #[derive(Debug, Deserialize)] | ||||
| pub struct DnsConfig { | ||||
|     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 { | ||||
|     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)] | ||||
| 
 | ||||
| #[macro_use] extern crate rocket; | ||||
| use rocket::State; | ||||
| use rocket::http::Status; | ||||
| use rocket_contrib::json::Json; | ||||
| #[macro_use] extern crate rocket_contrib; | ||||
| #[macro_use] extern crate diesel; | ||||
| 
 | ||||
| use trust_dns_client::client::{Client, SyncClient}; | ||||
| use trust_dns_client::tcp::TcpClientConnection; | ||||
| use trust_dns_client::op::{DnsResponse, ResponseCode}; | ||||
| use trust_dns_client::rr::{DNSClass, Name, RecordType}; | ||||
| use trust_dns_client::client::AsyncClient; | ||||
| use trust_dns_client::tcp::TcpClientStream; | ||||
| use trust_dns_proto::xfer::dns_multiplexer::DnsMultiplexer; | ||||
| 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 config; | ||||
| mod schema; | ||||
| mod routes; | ||||
| 
 | ||||
| use models::errors::ErrorResponse; | ||||
| use routes::users::*; | ||||
| use routes::zones::*; | ||||
| 
 | ||||
| 
 | ||||
| #[get("/zones/<zone>/records")] | ||||
| fn get_zone_records(client: State<SyncClient<TcpClientConnection>>, zone: String) -> Result<Json<Vec<models::dns::Record>>, ErrorResponse<()>> { | ||||
|     // TODO: Implement FromParam for Name
 | ||||
|     let name = Name::from_utf8(&zone).unwrap(); | ||||
| #[database("db")] | ||||
| pub struct DbConn(diesel::SqliteConnection); | ||||
| 
 | ||||
|     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(); | ||||
|     let mut records: Vec<_> = answers.to_vec().into_iter() | ||||
|         .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() { | ||||
| #[launch] | ||||
| async fn rocket() -> rocket::Rocket { | ||||
|     let app_config = config::load("config.toml".into()); | ||||
|     println!("{:#?}", app_config); | ||||
| 
 | ||||
|     let conn = TcpClientConnection::new(app_config.dns_server.address).unwrap(); | ||||
|     let client = SyncClient::new(conn); | ||||
|     let (stream, handle) = TcpClientStream::<AsyncIoTokioAsStd<TokioTcpStream>>::new(app_config.dns.server); | ||||
|     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() | ||||
|         .manage(client) | ||||
|         .mount("/api/v1", routes![get_zone_records]).launch(); | ||||
|         .manage(Arc::new(Mutex::new(client))) | ||||
|         .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 rocket::http::Status; | ||||
| use rocket::request::Request; | ||||
| use rocket::request::{Request, Outcome}; | ||||
| use rocket::response::{self, Response, Responder}; | ||||
| use rocket_contrib::json::Json; | ||||
| use crate::models::users::UserError; | ||||
| use serde_json::Value; | ||||
| 
 | ||||
| 
 | ||||
| #[derive(Serialize, Debug)] | ||||
| pub struct ErrorResponse<T> { | ||||
| pub struct ErrorResponse { | ||||
|     #[serde(with = "StatusDef")] | ||||
|     #[serde(flatten)] | ||||
|     pub status: Status, | ||||
|     pub message: String, | ||||
|     #[serde(skip_serializing_if = "Option::is_none")] | ||||
|     pub details: Option<T> | ||||
|     pub details: Option<Value> | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| #[derive(Serialize)] | ||||
| #[serde(remote = "Status")] | ||||
| struct StatusDef { | ||||
|  | @ -24,9 +25,8 @@ struct StatusDef { | |||
|     reason: &'static str, | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| impl<T> ErrorResponse<T> { | ||||
|     pub fn new(status: Status, message: String) -> ErrorResponse<T> { | ||||
| impl ErrorResponse { | ||||
|     pub fn new(status: Status, message: String) -> ErrorResponse { | ||||
|         ErrorResponse { | ||||
|             status, | ||||
|             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 { | ||||
|             details: Some(details), | ||||
|             details: serde_json::to_value(details).ok(), | ||||
|             ..self | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn err<R>(self) -> Result<R, ErrorResponse<T>> { | ||||
|     pub fn err<R>(self) -> Result<R, ErrorResponse> { | ||||
|         Err(self) | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| impl<'r, T: Serialize> Responder<'r> for ErrorResponse<T> { | ||||
|     fn respond_to(self, req: &Request) -> response::Result<'r> { | ||||
| impl<'r> Responder<'r, 'static> for ErrorResponse { | ||||
|     fn respond_to(self, req: &'r Request<'_>) -> response::Result<'static> { | ||||
|         let status = self.status; | ||||
|         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 errors; | ||||
| pub mod users; | ||||
| 
 | ||||
| pub mod trust_dns_types { | ||||
|     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