diff --git a/Cargo.lock b/Cargo.lock index b6b47f1..46fdcd0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,7 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +version = 3 + [[package]] name = "aead" version = "0.3.2" @@ -54,6 +56,27 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aho-corasick" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5" +dependencies = [ + "memchr", +] + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + [[package]] name = "async-trait" version = "0.1.48" @@ -110,6 +133,17 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -119,12 +153,24 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63396b8a4b9de3f4fdfb320ab6080762242f66a8ef174c49d8e19b674db4cdbe" + [[package]] name = "byteorder" version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" +[[package]] +name = "cc" +version = "1.0.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd" + [[package]] name = "cfg-if" version = "0.1.10" @@ -146,6 +192,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "serde", "time", "winapi 0.3.9", ] @@ -159,6 +206,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "cookie" version = "0.11.4" @@ -187,6 +240,17 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcb25d077389e53838a8158c8e99174c5a9d902dee4904320db714f3c653ffba" +[[package]] +name = "crossbeam-utils" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49" +dependencies = [ + "autocfg", + "cfg-if 1.0.0", + "lazy_static", +] + [[package]] name = "crypto-mac" version = "0.10.0" @@ -288,6 +352,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "djangohashers" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b101df5b2cab337a012f1a43d4a48810a81c10ea3b6bc73b456f5707c7b4325" +dependencies = [ + "base64 0.13.0", + "constant_time_eq", + "lazy_static", + "rand", + "regex", + "rust-argon2", +] + [[package]] name = "endian-type" version = "0.1.2" @@ -591,6 +669,29 @@ version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" +[[package]] +name = "js-sys" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc15e39392125075f60c95ba416f5381ff6c3a948ff02ab12464715adf56c821" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "7.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afabcc15e437a6484fc4f12d0fd63068fe457bf93f1c148d3d9649c60b103f32" +dependencies = [ + "base64 0.12.3", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -774,8 +875,11 @@ name = "nomilo" version = "0.1.0-dev" dependencies = [ "base64 0.13.0", + "chrono", "diesel", "diesel-derive-enum", + "djangohashers", + "jsonwebtoken", "rocket", "rocket_contrib", "serde", @@ -813,6 +917,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.44" @@ -842,6 +957,12 @@ dependencies = [ "libc", ] +[[package]] +name = "once_cell" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" + [[package]] name = "opaque-debug" version = "0.3.0" @@ -895,6 +1016,17 @@ dependencies = [ "yansi", ] +[[package]] +name = "pem" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd56cbd21fea48d0c440b41cd69c589faacade08c992d9a54e471b79d0fd13eb" +dependencies = [ + "base64 0.13.0", + "once_cell", + "regex", +] + [[package]] name = "percent-encoding" version = "1.0.1" @@ -1048,6 +1180,38 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957056ecddbeba1b26965114e191d2e8589ce74db242b6ea25fc4062427a5c19" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5f089152e60f62d28b835fbff2cd2e8dc0baf1ac13343bef92ab7eed84548" + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rocket" version = "0.4.7" @@ -1129,6 +1293,18 @@ dependencies = [ "unicode-xid 0.1.0", ] +[[package]] +name = "rust-argon2" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" +dependencies = [ + "base64 0.13.0", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + [[package]] name = "ryu" version = "1.0.5" @@ -1209,6 +1385,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "simple_asn1" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692ca13de57ce0613a363c8c2f1de925adebc81b04c923ac60c5488bb44abe4b" +dependencies = [ + "chrono", + "num-bigint", + "num-traits", +] + [[package]] name = "slab" version = "0.4.2" @@ -1232,6 +1419,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "state" version = "0.4.2" @@ -1460,6 +1653,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "1.7.2" @@ -1528,6 +1727,70 @@ version = "0.10.2+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" +[[package]] +name = "wasm-bindgen" +version = "0.2.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fe8f61dba8e5d645a4d8132dc7a0a66861ed5e1045d2c0ed940fab33bac0fbe" +dependencies = [ + "cfg-if 1.0.0", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046ceba58ff062da072c7cb4ba5b22a37f00a302483f7e2a6cdc18fedbdc1fd3" +dependencies = [ + "bumpalo", + "lazy_static", + "log 0.4.14", + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.64", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ef9aa01d36cda046f797c57959ff5f3c615c9cc63997a8d545831ec7976819b" +dependencies = [ + "quote 1.0.9", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96eb45c1b2ee33545a813a92dbb53856418bf7eb54ab34f7f7ff1448a5b3735d" +dependencies = [ + "proc-macro2 1.0.24", + "quote 1.0.9", + "syn 1.0.64", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7148f4696fb4960a346eaa60bbfb42a1ac4ebba21f750f75fc1375b098d5ffa" + +[[package]] +name = "web-sys" +version = "0.3.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fe19d70f5dacc03f6e46777213facae5ac3801575d56ca6cbd4c93dcd12310" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.2.8" diff --git a/Cargo.toml b/Cargo.toml index 50c1fa9..124b8a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,13 @@ 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 = "0.4" rocket_contrib = { version = "0.4", default-features = false, features = ["json", "diesel_sqlite_pool"]} 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"] } diff --git a/migrations/2021-03-26-164945_create_users/up.sql b/migrations/2021-03-26-164945_create_users/up.sql index 0dbdbdc..c9753af 100644 --- a/migrations/2021-03-26-164945_create_users/up.sql +++ b/migrations/2021-03-26-164945_create_users/up.sql @@ -1,7 +1,7 @@ -- Your SQL goes here CREATE TABLE localuser ( user_id VARCHAR NOT NULL PRIMARY KEY, - username VARCHAR NOT NULL, + username VARCHAR NOT NULL UNIQUE, password VARCHAR NOT NULL, FOREIGN KEY(user_id) REFERENCES user(id) ); diff --git a/src/auth/routes.rs b/src/auth/routes.rs index d8b1fe5..247157b 100644 --- a/src/auth/routes.rs +++ b/src/auth/routes.rs @@ -1,15 +1,26 @@ use serde::{Serialize, Deserialize}; + use rocket_contrib::json::Json; -use diesel::prelude::*; +use rocket::Response; +use rocket::http::Status; +use uuid::Uuid; +use jsonwebtoken::{encode, Header, EncodingKey}; +use chrono::prelude::{DateTime, Utc}; +use chrono::Duration; +use chrono::serde::ts_seconds; + use crate::DbConn; -use crate::models::users::{UserInfo, LocalUser, User}; +use crate::models::errors::ErrorResponse; +use crate::models::users::{LocalUser, CreateUserRequest}; #[derive(Debug, Serialize, Deserialize)] struct AuthClaims { jti: String, sub: String, - exp: usize, - iat: usize, + #[serde(with = "ts_seconds")] + exp: DateTime, + #[serde(with = "ts_seconds")] + iat: DateTime, } #[derive(Debug, Serialize)] @@ -19,26 +30,35 @@ pub struct AuthTokenResponse { #[derive(Debug, Deserialize)] pub struct AuthTokenRequest { - user: String, + username: String, password: String, } #[post("/users/me/token", data = "")] -pub fn create_auth_token(conn: DbConn, auth_request: Json) -> Json { - use crate::schema::localuser::dsl::*; - use crate::schema::user::dsl::*; +pub fn create_auth_token(conn: DbConn, auth_request: Json) -> Result, ErrorResponse<()>> { + let user_info = LocalUser::get_user_by_creds(&conn, &auth_request.username, &auth_request.password)?; + let jti = Uuid::new_v4().to_simple().to_string(); + let iat = Utc::now(); + let exp = iat + Duration::minutes(1); - let client_user: Result<(User, LocalUser), _> = user.inner_join(localuser).filter(username.eq(&auth_request.user)).get_result(&*conn); - println!("{:?}", client_user); - Json(AuthTokenResponse { token: "".into() }) + let claims = AuthClaims { + jti: jti, + sub: user_info.id, + exp: exp, + iat: iat, + }; + + // TODO: catch error + let token = encode(&Header::default(), &claims, &EncodingKey::from_secret("changeme".as_ref())).unwrap(); + + Ok(Json(AuthTokenResponse { token })) } -/* -GET /users -> list all users -POST /users -{ - provider: local - ... +#[post("/users", data = "")] +pub fn create_user<'r>(conn: DbConn, user_request: Json) -> Result, ErrorResponse<()>>{ + // TODO: Check current user if any to check if user has permission to create users (with or without role) + let _user_info = LocalUser::create_user(&conn, user_request.into_inner())?; + Response::build() + .status(Status::Created) + .ok() } -/users// -*/ diff --git a/src/main.rs b/src/main.rs index bfb8332..d563a3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,6 @@ use rocket::State; use rocket::http::Status; use rocket_contrib::json::Json; -use rocket_contrib::databases::diesel as rocket_diesel; - -use diesel::prelude::*; use trust_dns_client::client::{Client, SyncClient}; use trust_dns_client::tcp::TcpClientConnection; @@ -66,5 +63,5 @@ fn main() { rocket::ignite() .manage(client) .attach(DbConn::fairing()) - .mount("/api/v1", routes![get_zone_records, create_auth_token]).launch(); + .mount("/api/v1", routes![get_zone_records, create_auth_token, create_user]).launch(); } diff --git a/src/models/errors.rs b/src/models/errors.rs index d08b697..856b28e 100644 --- a/src/models/errors.rs +++ b/src/models/errors.rs @@ -3,7 +3,7 @@ use rocket::http::Status; use rocket::request::Request; use rocket::response::{self, Response, Responder}; use rocket_contrib::json::Json; - +use crate::models::users::UserError; #[derive(Serialize, Debug)] pub struct ErrorResponse { @@ -53,3 +53,19 @@ impl<'r, T: Serialize> Responder<'r> for ErrorResponse { Response::build_from(Json(self).respond_to(req)?).status(status).ok() } } + +impl From for ErrorResponse<()> { + fn from(e: UserError) -> Self { + match e { + UserError::NotFound => ErrorResponse::new(Status::Unauthorized, "Incorrect password or username.".into()), + UserError::UserExists => ErrorResponse::new(Status::Conflict, "User already exists.".into()), + UserError::DbError(e) => make_500(e), + UserError::PasswordError(e) => make_500(e) + } + } +} + +fn make_500(e: E) -> ErrorResponse<()> { + println!("{:?}", e); + ErrorResponse::new(Status::InternalServerError, "An unexpected error occured.".into()) +} diff --git a/src/models/users.rs b/src/models/users.rs index 6d113a6..7772d51 100644 --- a/src/models/users.rs +++ b/src/models/users.rs @@ -1,25 +1,32 @@ use uuid::Uuid; use diesel::prelude::*; -use diesel::associations::HasTable; +use diesel::result::Error as DieselError; use rocket::request::{FromRequest, Request, Outcome}; use diesel_derive_enum::DbEnum; +use serde::Deserialize; +// TODO: Maybe just use argon2 crate directly +use djangohashers::{make_password_with_algorithm, check_password, HasherError, Algorithm}; use crate::schema::*; +use crate::DbConn; -#[derive(Debug, DbEnum)] +#[derive(Debug, DbEnum, Deserialize)] +#[serde(rename_all = "snake_case")] pub enum Role { Admin, ZoneAdmin, } -#[derive(Debug, Queryable, Identifiable)] +// 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: String, } -#[derive(Debug, Queryable, Identifiable)] +#[derive(Debug, Queryable, Identifiable, Insertable)] #[table_name = "localuser"] #[primary_key(user_id)] pub struct LocalUser { @@ -28,21 +35,128 @@ pub struct LocalUser { pub password: String, } +#[derive(Debug, Deserialize)] +pub struct CreateUserRequest { + pub username: String, + pub password: String, + pub email: String, + pub role: Option +} + // pub struct LdapUserAssociation { // user_id: Uuid, // ldap_id: String // } +#[derive(Debug)] pub struct UserInfo { - id: Uuid, - role: Role, - username: String, + pub id: String, + pub role: String, + pub username: String, } impl<'a, 'r> FromRequest<'a, 'r> for UserInfo { type Error = (); fn from_request(request: &'a Request<'r>) -> Outcome { + // LocalUser::get_user_by_uuid() Outcome::Forward(()) } } + +#[derive(Debug)] +pub enum UserError { + NotFound, + UserExists, + DbError(DieselError), + PasswordError(HasherError), +} + +impl From for UserError { + fn from(e: DieselError) -> Self { + match e { + DieselError::NotFound => UserError::NotFound, + DieselError::DatabaseError(diesel::result::DatabaseErrorKind::UniqueViolation, _) => UserError::UserExists, + other => UserError::DbError(other) + } + } +} + +impl From for UserError { + fn from(e: HasherError) -> Self { + match e { + other => UserError::PasswordError(other) + } + } +} + + +impl LocalUser { + pub fn create_user(conn: &DbConn, user_request: CreateUserRequest) -> Result { + 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: "zoneadmin".into(), + }; + + 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: &DbConn, + request_username: &str, + request_password: &str + ) -> Result { + + 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(user_id: Uuid) -> Result { + unimplemented!() + } + +}