Add authentication #1
|
@ -1,3 +1,4 @@
|
||||||
/target
|
/target
|
||||||
|
|
||||||
config.toml
|
config.toml
|
||||||
|
db.sqlite
|
||||||
|
|
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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,0 +1,3 @@
|
||||||
|
-- This file should undo anything in `up.sql`
|
||||||
|
DROP TABLE localuser;
|
||||||
|
DROP TABLE user;
|
|
@ -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::{
|
||||||
|
|
|
@ -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()))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod users;
|
||||||
|
pub mod zones;
|
|
@ -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()
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
|
@ -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…
Reference in New Issue