Compare commits
11 commits
ed2620c8b8
...
b3ddae6008
| Author | SHA1 | Date | |
|---|---|---|---|
| b3ddae6008 | |||
| 5a10a8d4b1 | |||
| 14012b155e | |||
| 27dcc5489d | |||
| d77237aefe | |||
| 6eb02c8412 | |||
| 5de9e1d74e | |||
| 88ed714e22 | |||
| 4d342e8b99 | |||
| 1accd45648 | |||
| 7949d64649 |
7 changed files with 217 additions and 7 deletions
11
Cargo.lock
generated
11
Cargo.lock
generated
|
|
@ -1725,12 +1725,14 @@ dependencies = [
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"log",
|
"log",
|
||||||
"p256",
|
"p256",
|
||||||
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower",
|
"tower",
|
||||||
|
"uuid",
|
||||||
"vsss-rs",
|
"vsss-rs",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
@ -2457,6 +2459,15 @@ version = "0.2.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
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]]
|
[[package]]
|
||||||
name = "vcpkg"
|
name = "vcpkg"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ sqlx = { version = "0.8.3", features = [
|
||||||
aes-gcm-siv = "0.11.1"
|
aes-gcm-siv = "0.11.1"
|
||||||
vsss-rs = { version = "5.1.0", optional = true, default-features = false, features = ["zeroize", "std"] }
|
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"] }
|
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]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|
|
||||||
25
migrations/20250407112735_BasicIdentity.sql
Normal file
25
migrations/20250407112735_BasicIdentity.sql
Normal file
|
|
@ -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
|
||||||
|
);
|
||||||
|
|
@ -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;
|
use crate::storage::DbPool;
|
||||||
|
|
||||||
// route prefix: `/auth/token/`
|
// route prefix: `/auth/token/`
|
||||||
|
|
@ -8,6 +11,6 @@ use crate::storage::DbPool;
|
||||||
// use self::token::token_auth_router;
|
// use self::token::token_auth_router;
|
||||||
|
|
||||||
pub fn auth_router(pool: DbPool) -> Router<DbPool> {
|
pub fn auth_router(pool: DbPool) -> Router<DbPool> {
|
||||||
Router::new().with_state(pool)
|
Router::new().nest("/token", token_auth_router(pool.clone())).with_state(pool)
|
||||||
// .nest("/token", token_auth_router())
|
// .nest("/token", token_auth_router())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
37
src/auth/auth_extractor.rs
Normal file
37
src/auth/auth_extractor.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<> FromRequestParts<DbPool> for AuthInfo
|
||||||
|
{
|
||||||
|
type Rejection = StatusCode;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, state: &DbPool) -> Result<Self, Self::Rejection> {
|
||||||
|
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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<String>,
|
||||||
|
parent_id: Option<String>,
|
||||||
|
expiry: Option<i64>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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<String, Error> {
|
||||||
|
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<String, &str> {
|
||||||
|
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<TokenDTO, Error> {
|
||||||
|
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<String> {
|
||||||
|
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<DbPool> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/lookup", post(post_lookup))
|
||||||
|
.with_state(pool)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async fn post_lookup(
|
||||||
|
State(pool): State<DbPool>,
|
||||||
|
Json(body): Json<RequestBodyPostLookup>
|
||||||
|
) -> Result<Response, ()> {
|
||||||
|
let token = body.token;
|
||||||
|
|
||||||
|
Ok(IntoResponse::into_response(token))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_accessors() {}
|
async fn get_accessors() {}
|
||||||
|
|
@ -14,7 +145,6 @@ async fn post_create_role() {}
|
||||||
|
|
||||||
async fn get_lookup() {}
|
async fn get_lookup() {}
|
||||||
|
|
||||||
async fn post_lookup() {}
|
|
||||||
|
|
||||||
async fn get_lookup_self() {}
|
async fn get_lookup_self() {}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,18 @@
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
|
||||||
extract::Request,
|
extract::Request,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
middleware::{self, Next},
|
middleware::{self, Next},
|
||||||
response::{IntoResponse, Response},
|
response::{IntoResponse, Response},
|
||||||
routing::get,
|
routing::get,
|
||||||
|
Router
|
||||||
};
|
};
|
||||||
use log::*;
|
use log::*;
|
||||||
use std::{env, net::SocketAddr, str::FromStr};
|
use std::{env, net::SocketAddr, str::FromStr};
|
||||||
use storage::DbPool;
|
use storage::DbPool;
|
||||||
use tokio::{net::TcpListener, signal};
|
use tokio::{net::TcpListener, signal};
|
||||||
|
use crate::auth::auth_extractor::AuthInfo;
|
||||||
use crate::common::HttpError;
|
use crate::common::HttpError;
|
||||||
|
|
||||||
mod auth;
|
mod auth;
|
||||||
|
|
@ -54,6 +54,8 @@ async fn main() {
|
||||||
storage::sealing::init_default(&pool).await;
|
storage::sealing::init_default(&pool).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auth::token::create_root_token_if_none_exist(&pool).await;
|
||||||
|
|
||||||
warn!("Listening on {listen_addr}");
|
warn!("Listening on {listen_addr}");
|
||||||
// Start listening
|
// Start listening
|
||||||
let listener = TcpListener::bind(listen_addr).await.unwrap();
|
let listener = TcpListener::bind(listen_addr).await.unwrap();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue