Compare commits

...

11 commits

Author SHA1 Message Date
b3ddae6008 Merge branch 'identity' into dev
# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/auth.rs
#	src/main.rs
2025-06-10 16:19:20 -07:00
5a10a8d4b1 Added basic token auth extractor 2025-05-19 13:38:43 +02:00
14012b155e Added basic token auth extractor 2025-05-19 13:21:10 +02:00
27dcc5489d Added basic service token functionality and root token creation. 2025-05-07 17:24:07 +02:00
d77237aefe
Refactor: Secret struct and feature-gates
- Shamir and its dependencies behind a default feature
- Secret has its own struct
2025-04-02 18:59:33 +02:00
6eb02c8412
Feat (sealing): Shamir Secret Sharing scheme 2025-04-02 08:28:28 +02:00
5de9e1d74e
Fix (sealing): Simple sealing with random nonce
Some checks failed
Rust / build (pull_request) Failing after 40s
2025-03-27 22:13:57 +01:00
88ed714e22
Feat (sealing): Simple Password sealing
Password is generated on first startup.
The password given to the user is not same as the one used to encrypt secrets
2025-03-27 17:13:48 +01:00
4d342e8b99
Feat (kv2): Support Sealing 2025-03-26 21:51:27 +01:00
1accd45648
WIP feat (sealing): Implement basic sealing functionality
Currently, the key is just stored plainly in the database
2025-03-26 21:49:59 +01:00
7949d64649
Chore: Rename DatabaseDriver to DbPool
and add a custom serde serializer `serialize_reject_none` as a utility
2025-03-26 21:39:07 +01:00
7 changed files with 217 additions and 7 deletions

11
Cargo.lock generated
View file

@ -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"

View file

@ -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

View 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
);

View file

@ -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())
} }

View 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),
}
}
}

View file

@ -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() {}

View file

@ -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();