diff --git a/Cargo.lock b/Cargo.lock index c23661e..00277d2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1725,12 +1725,14 @@ dependencies = [ "env_logger", "log", "p256", + "rand", "serde", "serde_json", "sqlx", "time", "tokio", "tower", + "uuid", "vsss-rs", "zeroize", ] @@ -2457,6 +2459,15 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "uuid" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" +dependencies = [ + "getrandom 0.3.2", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index 107a780..dec2fa3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,8 @@ sqlx = { version = "0.8.3", features = [ aes-gcm-siv = "0.11.1" vsss-rs = { version = "5.1.0", optional = true, default-features = false, features = ["zeroize", "std"] } p256 = { version = "0.13.2", optional = true, default-features = false, features = ["std", "ecdsa"] } +rand = "0.8.5" +uuid = { version = "1.16.0", features = ["v4"] } [lints] workspace = true diff --git a/migrations/20250407112735_BasicIdentity.sql b/migrations/20250407112735_BasicIdentity.sql new file mode 100644 index 0000000..1f6d687 --- /dev/null +++ b/migrations/20250407112735_BasicIdentity.sql @@ -0,0 +1,25 @@ +CREATE TABLE identity ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL +); + +CREATE TABLE service_token_role_membership ( + role_name TEXT NOT NULL, + token_id TEXT NOT NULL + REFERENCES service_token(id) + ON DELETE CASCADE + ON UPDATE CASCADE, + PRIMARY KEY (role_name, token_id) +); + +CREATE TABLE service_token ( + id TEXT PRIMARY KEY NOT NULL, + key TEXT NOT NULL, + expiry INTEGER, + parent_id TEXT NULL REFERENCES service_token(id) + ON DELETE NO ACTION + ON UPDATE CASCADE, + identity_id TEXT NULL REFERENCES identity(id) + ON DELETE CASCADE + ON UPDATE CASCADE +); diff --git a/src/auth.rs b/src/auth.rs index f1767e3..ea850c5 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -1,5 +1,8 @@ -use axum::Router; +pub(crate) mod token; +pub mod auth_extractor; +use axum::Router; +use crate::auth::token::*; use crate::storage::DbPool; // route prefix: `/auth/token/` @@ -8,6 +11,6 @@ use crate::storage::DbPool; // use self::token::token_auth_router; pub fn auth_router(pool: DbPool) -> Router { - Router::new().with_state(pool) + Router::new().nest("/token", token_auth_router(pool.clone())).with_state(pool) // .nest("/token", token_auth_router()) } diff --git a/src/auth/auth_extractor.rs b/src/auth/auth_extractor.rs new file mode 100644 index 0000000..20de686 --- /dev/null +++ b/src/auth/auth_extractor.rs @@ -0,0 +1,37 @@ +use std::fmt::Debug; +use axum::extract::FromRequestParts; +use axum::http::request::Parts; +use axum::http::{header, StatusCode}; +use crate::auth::token::{get_roles_from_token, get_token_from_key, TokenDTO}; +use crate::storage::DbPool; + +#[derive(Debug)] +pub struct AuthInfo { + token: TokenDTO, + roles: Vec, +} + +impl<> FromRequestParts for AuthInfo +{ + type Rejection = StatusCode; + + async fn from_request_parts(parts: &mut Parts, state: &DbPool) -> Result { + let auth_header = parts + .headers + .get(header::AUTHORIZATION) + .and_then(|value| value.to_str().ok()); + + match auth_header { + Some(auth_header) => { + let token = get_token_from_key(auth_header, state).await; + if token.is_err() { + return Err(StatusCode::UNAUTHORIZED); + } + let token = token.unwrap(); + let roles = get_roles_from_token(&token, state).await; + Ok(Self {token, roles}) + } + _ => Err(StatusCode::UNAUTHORIZED), + } + } +} diff --git a/src/auth/token.rs b/src/auth/token.rs index 1001a2c..2d73318 100644 --- a/src/auth/token.rs +++ b/src/auth/token.rs @@ -1,7 +1,138 @@ -use axum::Router; +use std::ops::Index; +use axum::extract::{Path, Query, State}; +use axum::{Json, Router}; +use axum::response::{IntoResponse, NoContent, Response}; +use axum::routing::post; +use log::error; +use serde::Deserialize; +use sqlx::Error; +use rand::{distributions::Alphanumeric, Rng}; +use uuid::Uuid; +use crate::storage::DbPool; -pub fn token_auth_router() -> Router { +enum TokenType { + +} + +#[derive(Debug)] +pub struct IdentityDTO { + id: String, + name: String +} + + +#[derive(Debug)] +pub struct TokenDTO { + key: String, + id: String, + identity_id: Option, + parent_id: Option, + expiry: Option, +} + +#[derive(Debug)] +pub struct TokenRoleMembershipDTO { + role_name: String, + token_id: String, +} + +#[derive(Deserialize)] +struct RequestBodyPostLookup { + token: String, +} + +// TODO: Make string generation secure +fn get_random_string(len: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(len) + .map(char::from) + .collect() +} + +// Returns if a token was created or not. Prints out the created token to the console. +pub async fn create_root_token_if_none_exist(pool: &DbPool) -> bool { + let exists = sqlx::query!( + r#"SELECT service_token.* FROM service_token, service_token_role_membership + WHERE service_token.id = service_token_role_membership.token_id AND + service_token_role_membership.role_name = 'root' + LIMIT 1"#).fetch_one(pool).await + .is_ok(); + if exists { + return false; + } + let result = create_root_token(pool).await; + if result.is_err() { + let error = result.err().unwrap(); + error!("create_root_token failed: {:?}", error); + panic!("create_root_token failed: {:?}", error); + } + println!("\n\nYour root token is: {}", result.unwrap()); + println!("It will only be displayed once!\n\n"); + true +} + +// Return the token key if successful +async fn create_root_token(pool: &DbPool) -> Result { + let id = Uuid::new_v4().to_string(); + let key = "s.".to_string() + &get_random_string(24); + let result = sqlx::query!(r#" + INSERT INTO service_token (id, key) VALUES ($1, $2); + INSERT INTO service_token_role_membership (token_id, role_name) VALUES ($3, 'root'); + "#, id, key, id).execute(pool).await; + if result.is_ok() { + return Ok(key); + } + Err(result.unwrap_err()) +} + +// Gets the current time in seconds since unix epoch +fn get_time_as_int() -> i64 { + std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as i64 +} + +fn get_token_type(token: &TokenDTO) -> Result { + Ok(match token.key.clone().chars().next().unwrap_or('?') { + 's' => "service", + 'b' => "batch", + 'r' => "recovery", + _ => { + error!("Unsupported token type"); + return Err("Unsupported token type"); + } + }.to_string()) +} + +pub async fn get_token_from_key(token_key: &str, pool: &DbPool) -> Result { + let time = get_time_as_int(); + sqlx::query_as!( + TokenDTO, + r#"SELECT * FROM 'service_token' WHERE key = $1 AND (expiry IS NULL OR expiry > $2) LIMIT 1"#, + token_key, time).fetch_one(pool).await +} + +pub async fn get_roles_from_token(token: &TokenDTO, pool:&DbPool) -> Vec { + let result = sqlx::query_as!( + TokenRoleMembershipDTO, + r#"SELECT * FROM 'service_token_role_membership' WHERE token_id = $1"#, + token.id).fetch_all(pool).await; + result.unwrap_or(Vec::new()).iter().map(|r| r.role_name.to_string()).collect() +} + +pub fn token_auth_router(pool: DbPool) -> Router { Router::new() + .route("/lookup", post(post_lookup)) + .with_state(pool) +} + + +async fn post_lookup( + State(pool): State, + Json(body): Json +) -> Result { + let token = body.token; + + Ok(IntoResponse::into_response(token)) } async fn get_accessors() {} @@ -14,7 +145,6 @@ async fn post_create_role() {} async fn get_lookup() {} -async fn post_lookup() {} async fn get_lookup_self() {} diff --git a/src/main.rs b/src/main.rs index 5cf8d2f..ba594a4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,18 @@ #![forbid(unsafe_code)] use axum::{ - Router, extract::Request, http::StatusCode, middleware::{self, Next}, response::{IntoResponse, Response}, routing::get, + Router }; use log::*; use std::{env, net::SocketAddr, str::FromStr}; use storage::DbPool; use tokio::{net::TcpListener, signal}; - +use crate::auth::auth_extractor::AuthInfo; use crate::common::HttpError; mod auth; @@ -54,6 +54,8 @@ async fn main() { storage::sealing::init_default(&pool).await; } + auth::token::create_root_token_if_none_exist(&pool).await; + warn!("Listening on {listen_addr}"); // Start listening let listener = TcpListener::bind(listen_addr).await.unwrap();