multiple changes

This commit is contained in:
Hlars 2025-04-05 19:10:55 +02:00
parent dc5c92c1e8
commit 86ab1a451b
50 changed files with 1731 additions and 570 deletions

View File

@ -0,0 +1,10 @@
type: folder
model: folder
id: fl_1twfKS3Z0A
createdAt: 2025-01-04T16:33:57
updatedAt: 2025-01-12T08:36:42
workspaceId: wk_SlydsyH2WI
folderId: null
name: API Test
description: ''
sortPriority: 1000.0

View File

@ -0,0 +1,40 @@
type: http_request
model: http_request
id: rq_1YHYKIkG8x
createdAt: 2025-01-06T11:35:07
updatedAt: 2025-03-31T17:29:24.409065
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"signature": "untrusted comment: {\"timestamp\":\"2025-01-07T21:23:48.632759\",\"user_id\":\"TestUser\"}\nRUSvcn8DqG4zxAhjcXsihWr/GlBX5bqPs64sPuU2PsFWgALzwkv+dRUF2on5Xr2RFMAl+U0WRwz1jR65FbEHGnzHP6/Lq48IcgM=\ntrusted comment: timestamp:1736284801\nLHdY9CWqzzhFtkGGUXuaYRiFrpg6CId8rX2zRJJigmWJxCPx7+d3ZtWPel4TmHUB21Ce6C84ObFCZRwWE8v6Bw==\n",
"signed_from": ""
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
method: GET
name: Verify Something
sortPriority: 6000.0
url: http://localhost:8080/api/signature/verify
urlParameters: []

View File

@ -0,0 +1,48 @@
type: http_request
model: http_request
id: rq_A81dnTkhL5
createdAt: 2024-12-31T10:05:25
updatedAt: 2025-03-31T17:28:59.911928
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"user_id": "",
"surname": "",
"status_flag": "Deleted",
"name": "",
"permissions": [
"Test"
],
"active_directory_auth": true,
"email": "",
"group_permissions": []
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
method: GET
name: Users
sortPriority: 2000.0
url: http://localhost:8080/api/users
urlParameters: []

View File

@ -0,0 +1,35 @@
type: http_request
model: http_request
id: rq_Eb5hJAiaKG
createdAt: 2024-12-01T08:39:12
updatedAt: 2025-03-02T07:37:25.323717
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication: {}
authenticationType: null
body:
text: |-
{
"id": "TestUser",
"password": "test"
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: true
name: ''
value: ''
id: null
method: POST
name: Auth
sortPriority: 0.0
url: http://localhost:8080/api/login
urlParameters: []

View File

@ -0,0 +1,39 @@
type: http_request
model: http_request
id: rq_FJT5RHtZ2B
createdAt: 2025-04-05T16:17:53.461290
updatedAt: 2025-04-05T16:52:50.453961
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"draft_id": "2015-09-05 23:56:04"
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
- enabled: true
name: Content-Type
value: application/json
id: FWIGbxmTAH
method: GET
name: Get attached Draft Files
sortPriority: 6000.002
url: http://localhost:8080/api/files/draft
urlParameters: []

View File

@ -0,0 +1,40 @@
type: http_request
model: http_request
id: rq_FmsM38yvHM
createdAt: 2025-01-06T10:53:14
updatedAt: 2025-03-31T17:29:12.196535
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"secret": "EinTestSecret",
"ttl": 30
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
method: PUT
name: Verify Signing Key
sortPriority: 4000.0
url: http://localhost:8080/api/signature/key
urlParameters: []

View File

@ -0,0 +1,48 @@
type: http_request
model: http_request
id: rq_G6OxWcR5j2
createdAt: 2025-01-06T10:50:59
updatedAt: 2025-03-31T17:29:16.792639
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"user_id": "",
"surname": "",
"status_flag": "Deleted",
"name": "",
"permissions": [
"Test"
],
"active_directory_auth": true,
"email": "",
"group_permissions": []
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
method: GET
name: Verify Signing Key
sortPriority: 4500.0
url: http://localhost:8080/api/signature/key
urlParameters: []

View File

@ -0,0 +1,39 @@
type: http_request
model: http_request
id: rq_SFA4nX8S0k
createdAt: 2025-01-06T10:55:51
updatedAt: 2025-03-31T17:29:07.550911
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"secret": "EinTestSecret"
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
method: POST
name: Create Signing Key
sortPriority: 3000.0
url: http://localhost:8080/api/signature/key
urlParameters: []

View File

@ -0,0 +1,40 @@
type: http_request
model: http_request
id: rq_eFkZk6Z6QS
createdAt: 2025-04-05T16:52:35.932090
updatedAt: 2025-04-05T16:55:08.986210
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"draft_id": "2015-09-05 23:56:04",
"hash": "9CE71B15548BEAC9745F464531B70BEE30F71F310908D3A0A86E73E07F61ED61"
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
- enabled: true
name: Content-Type
value: application/json
id: FWIGbxmTAH
method: GET
name: Download Draft File
sortPriority: 6000.003
url: http://localhost:8080/api/files/draft/file
urlParameters: []

View File

@ -0,0 +1,48 @@
type: http_request
model: http_request
id: rq_gU10vzCIxt
createdAt: 2025-01-04T18:07:52
updatedAt: 2025-03-31T17:28:46.667681
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"user_id": "",
"surname": "",
"status_flag": "Deleted",
"name": "",
"permissions": [
"Test"
],
"active_directory_auth": true,
"email": "",
"group_permissions": []
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
method: GET
name: Api Keys
sortPriority: 1000.0
url: http://localhost:8080/api/apikeys
urlParameters: []

View File

@ -0,0 +1,39 @@
type: http_request
model: http_request
id: rq_lGuHoFSNGa
createdAt: 2025-01-06T10:56:17
updatedAt: 2025-03-31T17:29:20.133348
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
text: |-
{
"secret": "EinTestSecret"
}
bodyType: application/json
description: ''
headers:
- enabled: true
name: Content-Type
value: application/json
id: null
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
method: POST
name: Sign Something
sortPriority: 5000.0
url: http://localhost:8080/api/signature/sign
urlParameters: []

View File

@ -0,0 +1,52 @@
type: http_request
model: http_request
id: rq_t5k4XC8Poe
createdAt: 2025-04-02T16:41:11.369047
updatedAt: 2025-04-05T17:00:29.492513
workspaceId: wk_SlydsyH2WI
folderId: fl_1twfKS3Z0A
authentication:
token: ${[ response.body.raw(request='rq_Eb5hJAiaKG') ]}
authenticationType: bearer
body:
form:
- enabled: true
file: ''
id: u6yikyzRTH
name: file
- enabled: true
id: njtBaKefpN
name: name
value: '"TestValue"'
- enabled: true
id: Iav8TimHS8
name: draft_id
value: 2015-09-05 23:56:04
- enabled: true
id: d57ZzimswD
name: ''
value: ''
bodyType: multipart/form-data
description: ''
headers:
- enabled: true
name: x-api-key
value: ${[ ApiKey ]}
id: null
- enabled: false
name: ''
value: ''
id: null
- enabled: true
name: ''
value: ''
id: null
- enabled: true
name: Content-Type
value: multipart/form-data
id: rf64n05TdT
method: POST
name: Upload Draft File
sortPriority: 6000.001
url: http://localhost:8080/api/files/draft
urlParameters: []

View File

@ -0,0 +1,10 @@
type: workspace
model: workspace
id: wk_SlydsyH2WI
createdAt: 2024-12-01T08:38:45
updatedAt: 2024-12-01T08:38:45
name: Yaak
description: ''
settingValidateCertificates: true
settingFollowRedirects: true
settingRequestTimeout: 0

View File

@ -1,5 +1,6 @@
use axum_jwt_login::{AuthBackend, UserPermissions};
use ldap::LDAPBackend;
use private_key_cache::PrivateKeyCache;
use sqlx::PgPool;
use crate::{
@ -11,11 +12,13 @@ use crate::{
use super::routes::{auth::models::Credentials, users::models::User};
pub mod ldap;
pub mod private_key_cache;
#[derive(Debug, Clone)]
pub struct ApiBackend {
pool: PgPool,
config: Configuration,
private_key_cache: PrivateKeyCache,
}
impl ApiBackend {
@ -23,6 +26,7 @@ impl ApiBackend {
Self {
pool,
config: config.clone(),
private_key_cache: PrivateKeyCache::new(),
}
}
@ -33,6 +37,10 @@ impl ApiBackend {
pub fn config(&self) -> &Configuration {
&self.config
}
pub fn private_key_cache(&self) -> PrivateKeyCache {
self.private_key_cache.clone()
}
}
impl AuthBackend<User> for ApiBackend {

View File

@ -0,0 +1,77 @@
use std::{
collections::HashMap,
sync::Arc,
time::{Duration, Instant},
};
use minisign::SecretKey;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use tracing::debug;
#[derive(Debug, Clone)]
pub struct PrivateKeyCache {
cache: Arc<Mutex<HashMap<String, PrivateKeyEntry>>>,
}
#[derive(Debug, Clone)]
pub struct PrivateKeyEntry {
key: SecretKey,
ttl: Duration,
creation: Instant,
clean_token: CancellationToken,
}
impl PrivateKeyEntry {
pub fn secret_key(&self) -> SecretKey {
self.key.clone()
}
}
impl PrivateKeyCache {
pub fn new() -> Self {
Self {
cache: Arc::new(Mutex::new(HashMap::new())),
}
}
pub async fn add_key(&self, user: &str, key: &SecretKey, ttl: Duration) {
// create token to abort cleaning task
let clean_token = CancellationToken::new();
// create private key entry
let entry = PrivateKeyEntry {
key: key.clone(),
ttl,
creation: Instant::now(),
clean_token: clean_token.clone(),
};
// lock cache
let mut cache = self.cache.lock().await;
// add private key to cache
if let Some(old_entry) = cache.insert(user.to_string(), entry) {
// abort cleaning task of old entry
old_entry.clean_token.cancel();
}
// spawn task to remove private key from cache after storage expiration
let cache = self.cache.clone();
let user = user.to_string();
tokio::spawn(async move {
tokio::select! {
_ = tokio::time::sleep(ttl) => {
let mut lock = cache.lock().await;
lock.remove(&user);
debug!("Deleted cached Secret Key after TTL expiration")
},
_ = clean_token.cancelled() => {
debug!("Cancelled automated cache clearance after TTL renewal")
}
}
});
}
pub async fn get_key(&self, user_id: &str) -> Option<PrivateKeyEntry> {
let lock = self.cache.lock().await;
lock.get(user_id).cloned()
}
}

View File

@ -8,12 +8,14 @@ pub const USERS_TAG: &str = "Users";
pub const ORDER_TAG: &str = "order";
pub const API_KEY_TAG: &str = "API Keys";
pub const SIGNATURE_TAG: &str = "Signature";
pub const FILE_TAG: &str = "Files";
#[derive(OpenApi)]
#[openapi(
modifiers(&SecurityAddon),
tags(
(name = AUTH_TAG, description = "API Authentication endpoints"),
(name = FILE_TAG, description = "Upload and Download Files"),
(name = ORDER_TAG, description = "Order API endpoints")
),
)]

View File

@ -0,0 +1,35 @@
use axum::{debug_handler, extract::Query, Extension};
use serde::Deserialize;
use utoipa::IntoParams;
use crate::{
api::{backend::ApiBackend, description::API_KEY_TAG, routes::api_keys::sql},
errors::ApiError,
};
#[derive(Debug, Deserialize, IntoParams)]
pub struct DeleteQueryParameters {
key_id: String,
}
#[debug_handler]
#[utoipa::path(
delete,
path = "/apikeys",
summary = "Delete API Key",
description = "Delete an API Key.",
params(DeleteQueryParameters),
responses(
(status = OK, description = "API Key successfully deleted"),
),
security(
("user_auth" = ["write:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn delete_api_key(
Extension(backend): Extension<ApiBackend>,
Query(params): Query<DeleteQueryParameters>,
) -> Result<(), ApiError> {
sql::delete_api_key(backend.pool(), &params.key_id).await?;
Ok(())
}

View File

@ -0,0 +1,29 @@
use axum::{debug_handler, Extension, Json};
use crate::{
api::{
backend::ApiBackend,
description::API_KEY_TAG,
routes::api_keys::{models::ApiKey, sql},
},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
get,
path = "/apikeys",
summary = "Get all API Keys",
description = "Get a list of all configured API Keys.",
responses(
(status = OK, body = Vec<ApiKey>, description = "List of API Keys"),
),
security(
("user_auth" = ["read:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn get_api_keys(
Extension(backend): Extension<ApiBackend>,
) -> Result<Json<Vec<ApiKey>>, ApiError> {
Ok(Json(sql::get_api_keys(backend.pool()).await?))
}

View File

@ -0,0 +1,50 @@
use axum::{debug_handler, Json};
use crate::{
api::{
description::API_KEY_TAG,
routes::{
api_keys::{models::ApiKey, sql},
AuthBackendType,
},
},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
post,
path = "/apikeys",
summary = "Create new API Key",
description = "Create a new API Key.",
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
responses(
(status = OK, description = "API Key successfully created (API Key Secret in Body)", body = String),
),
security(
("user_auth" = ["write:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn create_api_key(
auth_session: AuthBackendType,
Json(api_key): Json<ApiKey>,
) -> Result<String, ApiError> {
let backend = auth_session.backend();
// create new API key
let (key_secret, key) = ApiKey::create(
&api_key.name,
api_key.auth_required,
api_key.permissions.0,
backend.config(),
)?;
// insert API Key into database
sql::create_api_key(backend.pool(), &key).await?;
// add API Key to session
auth_session.add_api_key(key).await;
// Return key secret in response
Ok(key_secret)
}

View File

@ -0,0 +1,32 @@
use axum::{debug_handler, Extension, Json};
use crate::{
api::{
backend::ApiBackend,
description::API_KEY_TAG,
routes::api_keys::{models::ApiKey, sql},
},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
put,
path = "/apikeys",
summary = "Update API Key",
description = "Update an API Key.",
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
responses(
(status = OK, description = "API Key successfully updated"),
),
security(
("user_auth" = ["write:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn update_api_key(
Extension(backend): Extension<ApiBackend>,
Json(api_key): Json<ApiKey>,
) -> Result<(), ApiError> {
sql::update_api_key(backend.pool(), &api_key).await?;
Ok(())
}

View File

@ -0,0 +1,4 @@
pub mod apikeys_delete;
pub mod apikeys_get;
pub mod apikeys_post;
pub mod apikeys_put;

View File

@ -1,24 +1,15 @@
use axum::{debug_handler, extract::Query, Extension, Json};
use models::ApiKey;
use serde::Deserialize;
use utoipa::IntoParams;
use utoipa_axum::{router::OpenApiRouter, routes};
use crate::{
api::{
backend::ApiBackend,
description::API_KEY_TAG,
routes::{
users::permissions::{Permission, PermissionDetail},
User,
},
},
errors::ApiError,
api::routes::users::permissions::{Permission, PermissionDetail},
permission_required,
};
use handlers::apikeys_delete::{__path_delete_api_key, delete_api_key};
use handlers::apikeys_get::{__path_get_api_keys, get_api_keys};
use handlers::apikeys_post::{__path_create_api_key, create_api_key};
use handlers::apikeys_put::{__path_update_api_key, update_api_key};
use super::AuthBackendType;
mod handlers;
pub mod models;
pub mod sql;
@ -37,109 +28,3 @@ pub fn router() -> OpenApiRouter {
OpenApiRouter::new().merge(read).merge(write)
}
#[debug_handler]
#[utoipa::path(
get,
path = "/apikeys",
summary = "Get all API Keys",
description = "Get a list of all configured API Keys.",
responses(
(status = OK, body = Vec<ApiKey>, description = "List of API Keys"),
),
security(
("user_auth" = ["read:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn get_api_keys(
Extension(backend): Extension<ApiBackend>,
) -> Result<Json<Vec<ApiKey>>, ApiError> {
Ok(Json(sql::get_api_keys(backend.pool()).await?))
}
#[debug_handler]
#[utoipa::path(
post,
path = "/apikeys",
summary = "Create new API Key",
description = "Create a new API Key.",
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
responses(
(status = OK, description = "API Key successfully created (API Key Secret in Body)", body = String),
),
security(
("user_auth" = ["write:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn create_api_key(
auth_session: AuthBackendType,
Json(api_key): Json<ApiKey>,
) -> Result<String, ApiError> {
let backend = auth_session.backend();
// create new API key
let (key_secret, key) = ApiKey::create(
&api_key.name,
api_key.auth_required,
api_key.permissions.0,
backend.config(),
)?;
// insert API Key into database
sql::create_api_key(backend.pool(), &key).await?;
// add API Key to session
auth_session.add_api_key(key).await;
// Return key secret in response
Ok(key_secret)
}
#[debug_handler]
#[utoipa::path(
put,
path = "/apikeys",
summary = "Update API Key",
description = "Update an API Key.",
request_body(content = ApiKey, description = "API Key details", content_type = "application/json"),
responses(
(status = OK, description = "API Key successfully updated"),
),
security(
("user_auth" = ["write:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn update_api_key(
Extension(backend): Extension<ApiBackend>,
Json(api_key): Json<ApiKey>,
) -> Result<(), ApiError> {
sql::update_api_key(backend.pool(), &api_key).await?;
Ok(())
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct DeleteQueryParameters {
key_id: String,
}
#[debug_handler]
#[utoipa::path(
delete,
path = "/apikeys",
summary = "Delete API Key",
description = "Delete an API Key.",
params(DeleteQueryParameters),
responses(
(status = OK, description = "API Key successfully deleted"),
),
security(
("user_auth" = ["write:apikeys",]),
),
tag = API_KEY_TAG)]
pub async fn delete_api_key(
Extension(backend): Extension<ApiBackend>,
Query(params): Query<DeleteQueryParameters>,
) -> Result<(), ApiError> {
sql::delete_api_key(backend.pool(), &params.key_id).await?;
Ok(())
}

View File

@ -0,0 +1,40 @@
use axum::{debug_handler, Json};
use crate::{
api::{
description::AUTH_TAG,
routes::{auth::models::Credentials, AuthBackendType},
},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
post,
path = "/login",
summary = "Authenticate as user",
description = "Authenticate as user and receive a JWT auth token.",
request_body(content = Credentials, description = "User credentials", content_type = "application/json"),
responses(
(status = OK, body = String, description = "Successfully logged in (JWT Token in body)"),
(status = UNAUTHORIZED, description = "Invalid credentials or unauthorized user")
),
tag = AUTH_TAG)]
pub async fn authorize(
mut auth_session: AuthBackendType,
Json(credentials): Json<Credentials>,
) -> Result<String, ApiError> {
let token = match auth_session.authenticate(credentials).await {
Ok(Some(_user)) => {
if let Some(token) = auth_session.get_auth_token() {
token
} else {
return Err(ApiError::InvalidCredentials);
}
}
Ok(None) => return Err(ApiError::InvalidCredentials),
Err(_) => return Err(ApiError::InvalidCredentials),
};
Ok(token)
}

View File

@ -0,0 +1,21 @@
use axum::debug_handler;
use crate::{
api::{description::AUTH_TAG, routes::AuthBackendType},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
post,
path = "/logout",
summary = "Logout",
description = "Log the currently logged in user out.",
responses(
(status = OK, description = "Logout successful")
),
tag = AUTH_TAG)]
pub async fn logout(mut auth_session: AuthBackendType) -> Result<(), ApiError> {
auth_session.logout().await?;
Ok(())
}

View File

@ -0,0 +1,2 @@
pub mod login_post;
pub mod logout_post;

View File

@ -1,11 +1,9 @@
use axum::{debug_handler, Json};
use models::Credentials;
use utoipa_axum::{router::OpenApiRouter, routes};
use crate::{api::description::AUTH_TAG, errors::ApiError};
use super::AuthBackendType;
use crate::api::routes::auth::handlers::login_post::{__path_authorize, authorize};
use crate::api::routes::auth::handlers::logout_post::{__path_logout, logout};
mod handlers;
pub mod models;
// expose the OpenAPI to parent module
@ -14,49 +12,3 @@ pub fn router() -> OpenApiRouter {
.routes(routes!(authorize))
.routes(routes!(logout))
}
#[debug_handler]
#[utoipa::path(
post,
path = "/login",
summary = "Authenticate as user",
description = "Authenticate as user and receive a JWT auth token.",
request_body(content = Credentials, description = "User credentials", content_type = "application/json"),
responses(
(status = OK, body = String, description = "Successfully logged in (JWT Token in body)"),
(status = UNAUTHORIZED, description = "Invalid credentials or unauthorized user")
),
tag = AUTH_TAG)]
pub async fn authorize(
mut auth_session: AuthBackendType,
Json(credentials): Json<Credentials>,
) -> Result<String, ApiError> {
let token = match auth_session.authenticate(credentials).await {
Ok(Some(_user)) => {
if let Some(token) = auth_session.get_auth_token() {
token
} else {
return Err(ApiError::InvalidCredentials);
}
}
Ok(None) => return Err(ApiError::InvalidCredentials),
Err(_) => return Err(ApiError::InvalidCredentials),
};
Ok(token)
}
#[debug_handler]
#[utoipa::path(
post,
path = "/logout",
summary = "Logout",
description = "Log the currently logged in user out.",
responses(
(status = OK, description = "Logout successful")
),
tag = AUTH_TAG)]
pub async fn logout(mut auth_session: AuthBackendType) -> Result<(), ApiError> {
auth_session.logout().await?;
Ok(())
}

View File

@ -0,0 +1,64 @@
use axum::{
body::Body,
debug_handler,
http::header,
response::{IntoResponse, Response},
Json,
};
use chrono::NaiveDateTime;
use serde::Deserialize;
use ts_rs::TS;
use utoipa::ToSchema;
use crate::{
api::{
description::FILE_TAG,
routes::{files::sql, AuthBackendType},
},
errors::ApiError,
};
#[derive(Debug, Deserialize, TS, ToSchema)]
#[ts(export)]
pub struct GetFileRequest {
draft_id: String,
hash: String,
}
#[debug_handler]
#[utoipa::path(
get,
path = "/files/draft/file",
summary = "Download specified file",
description = "Download specified file.",
request_body(content = GetFileRequest, description = "Request to download specified file", content_type = "application/json"),
responses(
(status = OK, description = "File Data", content_type = "application/octet-stream"),
),
tag = FILE_TAG)]
pub async fn get_specified_draft_file(
auth_session: AuthBackendType,
Json(request): Json<GetFileRequest>,
) -> Result<impl IntoResponse, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
let draft_id =
NaiveDateTime::parse_from_str(&request.draft_id, "%Y-%m-%d %H:%M:%S").map_err(|_| {
ApiError::InvalidRequest("Could not parse NaiveDateTime from draft id".to_string())
})?;
match sql::get_specified_draft_file(backend.pool(), user, draft_id, request.hash).await? {
Some(file) => Ok(Response::builder()
.header(header::CONTENT_TYPE, file.content_type)
.header(
header::CONTENT_DISPOSITION,
format!("attachment; filename=\"{}\"", file.name),
)
.body(Body::from(file.data))
.unwrap_or_default()),
None => Err(ApiError::FileNotFound),
}
}

View File

@ -0,0 +1,51 @@
use axum::{debug_handler, Json};
use chrono::NaiveDateTime;
use serde::Deserialize;
use ts_rs::TS;
use utoipa::ToSchema;
use crate::{
api::{
description::FILE_TAG,
routes::{
files::{models::AttachedFile, sql},
AuthBackendType,
},
},
errors::ApiError,
};
#[derive(Debug, Deserialize, TS, ToSchema)]
#[ts(export)]
pub struct GetAttachedFilesRequest {
draft_id: String,
}
#[debug_handler]
#[utoipa::path(
get,
path = "/files/draft",
summary = "Get Files attached to Draft",
description = "Get list of all files that are attached to a specified draft.",
request_body(content = GetAttachedFilesRequest, description = "Request for attached files", content_type = "application/json"),
responses(
(status = OK, body = AttachedFile, description = "Attached Files List", content_type = "application/json"),
),
tag = FILE_TAG)]
pub async fn get_attached_draft_files(
auth_session: AuthBackendType,
Json(request): Json<GetAttachedFilesRequest>,
) -> Result<Json<Vec<AttachedFile>>, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
let draft_id =
NaiveDateTime::parse_from_str(&request.draft_id, "%Y-%m-%d %H:%M:%S").map_err(|_| {
ApiError::InvalidRequest("Could not parse NaiveDateTime from draft id".to_string())
})?;
Ok(Json(
sql::get_draft_attached_files(backend.pool(), user, draft_id).await?,
))
}

View File

@ -0,0 +1,112 @@
use axum::{debug_handler, extract::Multipart, http::StatusCode};
use chrono::NaiveDateTime;
use serde::Deserialize;
use tokio_util::bytes::Bytes;
use ts_rs::TS;
use utoipa::ToSchema;
use crate::{
api::{
description::FILE_TAG,
routes::{
files::{models::File, sql},
AuthBackendType,
},
},
errors::ApiError,
};
use super::super::FILE_SIZE_LIMIT_MB;
#[derive(Debug, Clone, Deserialize, TS, ToSchema)]
#[ts(export)]
#[allow(unused)]
pub struct FileUploadRequest {
name: Option<String>,
#[schema(value_type = String)]
draft_id: NaiveDateTime,
#[schema(format = Binary, content_media_type = "application/octet-stream")]
file: String,
}
#[debug_handler]
#[utoipa::path(
post,
path = "/files/draft",
summary = "Draft File Upload",
description = "Upload a file as draft file.",
request_body(content = FileUploadRequest, description = "File Data", content_type = "multipart/form-data"),
responses(
(status = OK, body = String, description = "Successfully uploaded and stored file"),
(status = 413, description = format!("The size of the uploaded file is too large (max {FILE_SIZE_LIMIT_MB} MB)"))
),
tag = FILE_TAG)]
pub async fn upload_draft_file(
auth_session: AuthBackendType,
mut multipart: Multipart,
) -> Result<(), ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
let mut name: Option<String> = None;
let mut draft_id: Option<NaiveDateTime> = None;
let mut content_type: Option<String> = None;
let mut file_name: Option<String> = None;
let mut bytes: Option<Bytes> = None;
let mut size: Option<i32> = None;
while let Some(field) = multipart.next_field().await.unwrap() {
let field_name = field.name();
match &field_name {
Some("name") => name = Some(field.text().await?),
Some("draft_id") => {
draft_id = Some(
NaiveDateTime::parse_from_str(&field.text().await?, "%Y-%m-%d %H:%M:%S")
.map_err(|_| {
ApiError::InvalidRequest(
"Could not parse NaiveDateTime from draft id".to_string(),
)
})?,
)
}
Some("file") => {
file_name = field.file_name().map(ToString::to_string);
content_type = field.content_type().map(ToString::to_string);
let _bytes = field.bytes().await?;
size = Some(_bytes.len() as i32);
bytes = Some(_bytes);
}
_ => (),
};
}
// store file in database
if let (Some(data), Some(draft_id), Some(content_type), Some(filename)) =
(bytes, draft_id, content_type, file_name)
{
sql::insert_new_draft_file(
backend.pool(),
user,
File {
name: match name {
Some(name) => name,
None => filename,
},
draft_id,
content_type,
data,
size: size.unwrap_or_default(),
},
)
.await
} else {
Err(ApiError::MultipartForm(
StatusCode::BAD_REQUEST,
"Missing fields in request".to_string(),
))
}
}

View File

@ -0,0 +1,3 @@
pub mod files_draft_file_get;
pub mod files_draft_get;
pub mod files_draft_post;

View File

@ -0,0 +1,26 @@
mod handlers;
mod models;
mod sql;
use axum::extract::DefaultBodyLimit;
use utoipa_axum::{router::OpenApiRouter, routes};
use crate::{
api::routes::users::permissions::{Permission, PermissionDetail},
permission_required,
};
use handlers::{files_draft_file_get::*, files_draft_get::*, files_draft_post::*};
const FILE_SIZE_LIMIT_MB: usize = 20;
// expose the OpenAPI to parent module
pub fn router() -> OpenApiRouter {
OpenApiRouter::new()
.routes(routes!(get_specified_draft_file))
.routes(routes!(upload_draft_file, get_attached_draft_files))
.layer(DefaultBodyLimit::max(FILE_SIZE_LIMIT_MB * 1000 * 1000))
// .route_layer(permission_required!(Permission::Write(
// PermissionDetail::Users // TODO adjust permissions
// )))
}

View File

@ -0,0 +1,33 @@
use chrono::NaiveDateTime;
use serde::Serialize;
use sha2::{Digest, Sha256};
use tokio_util::bytes::Bytes;
use ts_rs::TS;
use utoipa::ToSchema;
#[derive(Debug, Clone, Serialize, TS, ToSchema)]
#[ts(export)]
pub struct AttachedFile {
pub name: String,
pub hash: String,
pub content_type: String,
pub size: i32,
}
pub struct File {
pub name: String,
pub draft_id: NaiveDateTime,
pub content_type: String,
pub data: Bytes,
pub size: i32,
}
impl File {
pub fn hash(&self) -> String {
let mut hasher = Sha256::new();
hasher.update(&self.data);
let hash = hasher.finalize();
format!("{hash:X}")
}
}

View File

@ -0,0 +1,84 @@
use chrono::NaiveDateTime;
use sqlx::PgPool;
use crate::{api::routes::users::models::User, errors::ApiError};
use super::models::{AttachedFile, File};
pub async fn insert_new_draft_file(pool: &PgPool, user: &User, file: File) -> Result<(), ApiError> {
let mut transaction = pool.begin().await?;
let affected_rows = sqlx::query!(
r#"INSERT INTO draftfiles
("UserID", "DraftID", "Hash", "ContentType", "Name", "Data", "Size")
VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT DO NOTHING"#,
user.user_id,
file.draft_id,
file.hash(),
file.content_type,
file.name,
&file.data.to_vec(),
file.size
)
.execute(&mut *transaction)
.await?
.rows_affected();
// commit transaction
transaction.commit().await?;
Ok(())
}
pub async fn get_draft_attached_files(
pool: &PgPool,
user: &User,
draft_id: NaiveDateTime,
) -> Result<Vec<AttachedFile>, ApiError> {
Ok(sqlx::query_as!(
AttachedFile,
r#"SELECT
"Hash" as hash,
"ContentType" as content_type,
"Name" as name,
"Size" as size
FROM
"draftfiles"
WHERE
"UserID" = $1 AND "DraftID" = $2
ORDER BY "Name" ASC"#,
user.user_id,
draft_id
)
.fetch_all(pool)
.await?)
}
pub async fn get_specified_draft_file(
pool: &PgPool,
user: &User,
draft_id: NaiveDateTime,
hash: String,
) -> Result<Option<File>, ApiError> {
let file = sqlx::query_as!(
File,
r#"SELECT
"ContentType" as content_type,
"Name" as name,
"Data" as data,
"DraftID" as draft_id,
"Size" as size
FROM
"draftfiles"
WHERE
"UserID" = $1 AND "DraftID" = $2 AND "Hash" = $3
ORDER BY "Name" ASC"#,
user.user_id,
draft_id,
hash
)
.fetch_optional(pool)
.await?;
Ok(file)
}

View File

@ -1,5 +1,6 @@
pub mod api_keys;
pub mod auth;
mod files;
mod signature;
pub mod users;
@ -44,6 +45,7 @@ pub fn create_routes(session: AuthBackendType) -> Router {
.nest(API_BASE, users::router())
.nest(API_BASE, api_keys::router())
.nest(API_BASE, signature::router())
.nest(API_BASE, files::router())
// .nest(
// "/api/order",
// // order::router().route_layer(crate::login_required!(AuthenticationBackend<ApiKey>)),

View File

@ -0,0 +1,5 @@
pub mod signature_key_get;
pub mod signature_key_post;
pub mod signature_key_put;
pub mod signature_sign_post;
pub mod signature_verify_get;

View File

@ -0,0 +1,36 @@
use super::super::{models::SigningKeyStatus, sql};
use axum::{debug_handler, Json};
use crate::{
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
get,
path = "/signature/key",
summary = "Check for signing key",
description = "Check if the logged in user has configured a signing key.",
responses(
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn check_signing_key(
auth_session: AuthBackendType,
) -> Result<Json<SigningKeyStatus>, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
let private_key = sql::get_private_key(backend.pool(), &user.user_id).await?;
Ok(Json(SigningKeyStatus {
configured: private_key.key.is_some(),
verified: None,
}))
}

View File

@ -0,0 +1,60 @@
use super::super::sql;
use axum::{debug_handler, Json};
use chrono::Utc;
use minisign::KeyPair;
use serde::Deserialize;
use ts_rs::TS;
use utoipa::ToSchema;
use crate::{
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
errors::ApiError,
};
#[derive(Debug, Clone, Deserialize, TS, ToSchema)]
#[ts(export)]
pub struct CreateSigningKeyRequest {
secret: String,
}
#[debug_handler]
#[utoipa::path(
post,
path = "/signature/key",
summary = "Create signing key",
description = "Create signing key for the currently logged in user.",
request_body(content = CreateSigningKeyRequest, description = "Signing Key Creation Request", content_type = "application/json"),
responses(
(status = OK, description = "Successfully created signing key"),
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn create_signing_key(
auth_session: AuthBackendType,
Json(key_request): Json<CreateSigningKeyRequest>,
) -> Result<(), ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
// create Key Pair
let KeyPair { pk, sk } = KeyPair::generate_encrypted_keypair(Some(key_request.secret))?;
let public_key = pk.to_box()?.to_string();
let private_key = sk
.to_box(Some(&format!(
"{} {} - {}",
user.name,
user.surname,
Utc::now()
)))? // Optional comment about the key
.to_string();
// store keys in database
sql::update_private_key(backend.pool(), &user.user_id, private_key).await?;
sql::store_public_key(backend.pool(), &user.user_id, public_key).await?;
Ok(())
}

View File

@ -0,0 +1,48 @@
use super::super::models::{SigningKeyRequest, SigningKeyStatus};
use axum::{debug_handler, Json};
use crate::{
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
put,
path = "/signature/key",
summary = "Verify signing key",
description = "Verify signing key for the currently logged in user.",
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
responses(
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn verify_signing_key(
auth_session: AuthBackendType,
Json(key_request): Json<SigningKeyRequest>,
) -> Result<Json<SigningKeyStatus>, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
// get private key
let status = match key_request
.to_private_key(backend.pool(), &user.user_id, backend.private_key_cache())
.await?
{
Some(_key) => SigningKeyStatus {
configured: true,
verified: Some(true),
},
None => SigningKeyStatus {
configured: false,
verified: None,
},
};
Ok(Json(status))
}

View File

@ -0,0 +1,87 @@
use std::io::Cursor;
use super::super::{
models::{SignatureTest, SigningKeyRequest, VerificationRequest},
sql,
};
use axum::{debug_handler, Json};
use crate::{
api::{description::SIGNATURE_TAG, routes::AuthBackendType},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
post,
path = "/signature/sign",
summary = "Sign something with your signing key",
description = "Sign something as the currently logged in user.",
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
responses(
(status = OK, description = "Successfully signed"),
(status = 401, description = "Unauthorized"),
(status = 500, description = "Internal Server Error")
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn sign(
auth_session: AuthBackendType,
Json(key_request): Json<SigningKeyRequest>,
) -> Result<Json<VerificationRequest>, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
// get private key
let private_key = key_request
.to_private_key(backend.pool(), &user.user_id, backend.private_key_cache())
.await?
.ok_or(ApiError::AccessDenied)?;
// get current public key
let public_key_id = sql::get_current_public_key(backend.pool(), &user.user_id)
.await?
.ok_or(ApiError::InternalError(
"No corresponding public key found".to_string(),
))?;
let data = SignatureTest {
value1: "blfjdljfa".to_string(),
value2: false,
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
};
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
let signature_box = minisign::sign(
None,
&private_key,
data_reader,
None,
Some(&serde_json::to_string(&public_key_id)?),
)?;
// We have a signature! Let's inspect it a little bit.
println!(
"Untrusted comment: [{}]",
signature_box.untrusted_comment().unwrap()
);
println!(
"Trusted comment: [{}]",
signature_box.trusted_comment().unwrap()
);
// store signature
// Converting the signature box to a string in order to save it is easy.
let signature = signature_box.into_string();
println!("BLA");
println!("{signature}");
Ok(Json(VerificationRequest {
signature,
signed_from: "".to_string(),
}))
}

View File

@ -0,0 +1,66 @@
use std::io::Cursor;
use super::super::{
models::{SignatureTest, SigningKeyRequest, VerificationRequest},
sql,
};
use axum::{debug_handler, Json};
use minisign::{PublicKeyBox, SignatureBox};
use crate::{
api::{
description::SIGNATURE_TAG,
routes::{signature::models::PublicKeyID, AuthBackendType},
},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
get,
path = "/signature/verify",
summary = "Verify a signature",
description = "Verify the signature.",
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
responses(
(status = OK, description = "Successfully signed"),
),
tag = SIGNATURE_TAG)]
pub async fn verify(
auth_session: AuthBackendType,
Json(verification_request): Json<VerificationRequest>,
) -> Result<(), ApiError> {
let backend = auth_session.backend();
// Now, let's verify the signature.
// Assuming we just loaded it into `signature_box_str`, get the box back.
let signature_box = SignatureBox::from_string(&verification_request.signature)?;
// get public key id from signature
let public_key_id: PublicKeyID = serde_json::from_str(&signature_box.untrusted_comment()?)?;
// Load the public key of the user that signed.
let public_key_str = sql::get_public_key_by_id(backend.pool(), public_key_id)
.await?
.key
.ok_or(ApiError::InternalError(
"No corresponding Public Key was found.".to_string(),
))?;
let public_key_box = PublicKeyBox::from_string(&public_key_str)?;
let public_key = public_key_box.into_public_key()?;
// And verify the data.
let data = SignatureTest {
value1: "blfjdljfa".to_string(),
value2: false,
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
};
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
let verified = minisign::verify(&public_key, &signature_box, data_reader, true, false, false);
match verified {
Ok(()) => println!("Success!"),
Err(_) => println!("Verification failed"),
};
Ok(())
}

View File

@ -1,24 +1,17 @@
use std::io::Cursor;
use axum::{debug_handler, Json};
use chrono::Utc;
use minisign::{KeyPair, PublicKeyBox, SecretKeyBox, SignatureBox};
use models::{
PublicKeyID, SignatureTest, SigningKeyRequest, SigningKeyStatus, VerificationRequest,
};
use utoipa_axum::{router::OpenApiRouter, routes};
use crate::{
api::{
description::SIGNATURE_TAG,
routes::users::permissions::{Permission, PermissionDetail},
},
errors::ApiError,
api::routes::users::permissions::{Permission, PermissionDetail},
permission_required,
};
use super::AuthBackendType;
use handlers::signature_key_get::*;
use handlers::signature_key_post::*;
use handlers::signature_key_put::*;
use handlers::signature_sign_post::*;
use handlers::signature_verify_get::*;
mod handlers;
pub mod models;
pub mod sql;
@ -38,238 +31,3 @@ pub fn router() -> OpenApiRouter {
OpenApiRouter::new().merge(write).merge(verify)
}
#[debug_handler]
#[utoipa::path(
get,
path = "/signature/key",
summary = "Check for signing key",
description = "Check if the logged in user has configured a signing key.",
responses(
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn check_signing_key(
auth_session: AuthBackendType,
) -> Result<Json<SigningKeyStatus>, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
let private_key = sql::get_private_key(backend.pool(), &user.user_id).await?;
Ok(Json(SigningKeyStatus {
configured: private_key.key.is_some(),
verified: None,
}))
}
#[debug_handler]
#[utoipa::path(
post,
path = "/signature/key",
summary = "Create signing key",
description = "Create signing key for the currently logged in user.",
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
responses(
(status = OK, description = "Successfully created signing key"),
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn create_signing_key(
auth_session: AuthBackendType,
Json(key_request): Json<SigningKeyRequest>,
) -> Result<(), ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
// create Key Pair
let KeyPair { pk, sk } = KeyPair::generate_encrypted_keypair(Some(key_request.secret))?;
let public_key = pk.to_box()?.to_string();
let private_key = sk
.to_box(Some(&format!(
"{} {} - {}",
user.name,
user.surname,
Utc::now()
)))? // Optional comment about the key
.to_string();
// store keys in database
sql::update_private_key(backend.pool(), &user.user_id, private_key).await?;
sql::store_public_key(backend.pool(), &user.user_id, public_key).await?;
Ok(())
}
#[debug_handler]
#[utoipa::path(
put,
path = "/signature/key",
summary = "Verify signing key",
description = "Verify signing key for the currently logged in user.",
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
responses(
(status = OK, body = SigningKeyStatus, description = "Signature Status"),
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn verify_signing_key(
auth_session: AuthBackendType,
Json(key_request): Json<SigningKeyRequest>,
) -> Result<Json<SigningKeyStatus>, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
// get private key
let status = match key_request
.to_private_key(backend.pool(), &user.user_id)
.await?
{
Some(_key) => SigningKeyStatus {
configured: true,
verified: Some(true),
},
None => SigningKeyStatus {
configured: false,
verified: None,
},
};
Ok(Json(status))
}
#[debug_handler]
#[utoipa::path(
post,
path = "/signature/sign",
summary = "Sign something with your signing key",
description = "Sign something as the currently logged in user.",
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
responses(
(status = OK, description = "Successfully signed"),
),
security(
("user_auth" = ["write:signature",]),
),
tag = SIGNATURE_TAG)]
pub async fn sign(
auth_session: AuthBackendType,
Json(key_request): Json<SigningKeyRequest>,
) -> Result<Json<VerificationRequest>, ApiError> {
let backend = auth_session.backend();
let user = auth_session
.is_authenticated()
.ok_or(ApiError::AccessDenied)?;
// get private key
let private_key = key_request
.to_private_key(backend.pool(), &user.user_id)
.await?
.ok_or(ApiError::AccessDenied)?;
// get current public key
let public_key_id = sql::get_current_public_key(backend.pool(), &user.user_id)
.await?
.ok_or(ApiError::InternalError(
"No corresponding public key found".to_string(),
))?;
let data = SignatureTest {
value1: "blfjdljfa".to_string(),
value2: false,
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
};
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
let signature_box = minisign::sign(
None,
&private_key,
data_reader,
None,
Some(&serde_json::to_string(&public_key_id)?),
)?;
// We have a signature! Let's inspect it a little bit.
println!(
"Untrusted comment: [{}]",
signature_box.untrusted_comment().unwrap()
);
println!(
"Trusted comment: [{}]",
signature_box.trusted_comment().unwrap()
);
// store signature
// Converting the signature box to a string in order to save it is easy.
let signature = signature_box.into_string();
println!("BLA");
println!("{signature}");
Ok(Json(VerificationRequest {
signature,
signed_from: "".to_string(),
}))
}
#[debug_handler]
#[utoipa::path(
get,
path = "/signature/verify",
summary = "Verify a signature",
description = "Verify the signature.",
request_body(content = SigningKeyRequest, description = "Signing Key Request", content_type = "application/json"),
responses(
(status = OK, description = "Successfully signed"),
),
tag = SIGNATURE_TAG)]
pub async fn verify(
auth_session: AuthBackendType,
Json(verification_request): Json<VerificationRequest>,
) -> Result<(), ApiError> {
let backend = auth_session.backend();
// Now, let's verify the signature.
// Assuming we just loaded it into `signature_box_str`, get the box back.
let signature_box = SignatureBox::from_string(&verification_request.signature)?;
// get public key id from signature
let public_key_id: PublicKeyID = serde_json::from_str(&signature_box.untrusted_comment()?)?;
// Load the public key of the user that signed.
let public_key_str = sql::get_public_key_by_id(backend.pool(), public_key_id)
.await?
.key
.ok_or(ApiError::InternalError(
"No corresponding Public Key was found.".to_string(),
))?;
let public_key_box = PublicKeyBox::from_string(&public_key_str)?;
let public_key = public_key_box.into_public_key()?;
// And verify the data.
let data = SignatureTest {
value1: "blfjdljfa".to_string(),
value2: false,
value3: vec!["bladfa".to_string(), "noch bla".to_string()],
};
let data_reader = Cursor::new(data.calculate_hash().to_be_bytes());
let verified = minisign::verify(&public_key, &signature_box, data_reader, true, false, false);
match verified {
Ok(()) => println!("Success!"),
Err(_) => println!("Verification failed"),
};
Ok(())
}

View File

@ -1,6 +1,6 @@
use std::{
fmt::Display,
hash::{DefaultHasher, Hash, Hasher},
time::Duration,
};
use chrono::NaiveDateTime;
@ -10,7 +10,10 @@ use sqlx::PgPool;
use ts_rs::TS;
use utoipa::ToSchema;
use crate::{api::routes::signature::sql, errors::ApiError};
use crate::{
api::{backend::private_key_cache::PrivateKeyCache, routes::signature::sql},
errors::ApiError,
};
#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
#[ts(export)]
@ -32,7 +35,10 @@ pub struct PublicKeyID {
#[derive(Debug, Clone, Deserialize, TS, ToSchema)]
#[ts(export)]
pub struct SigningKeyRequest {
pub secret: String,
// if no secret is given, the key will be searched in the cache
pub secret: Option<String>,
// if ttl is set, the key is cached for ttl in seconds
pub ttl: Option<u32>,
}
impl SigningKeyRequest {
@ -40,19 +46,41 @@ impl SigningKeyRequest {
self,
pool: &PgPool,
user_id: &str,
private_key_cache: PrivateKeyCache,
) -> Result<Option<SecretKey>, ApiError> {
// try to get private key from database
let private_key = match sql::get_private_key(pool, user_id).await?.key {
Some(key) => key,
None => return Ok(None),
// get private key from database and decrypt it or get it from cache
// this depends on the request containing a secret
// if a secret and a TTL is given the decrypted private key is cached
let private_key = match self.secret {
Some(secret) => {
// try to get private key from database
let encrypted_private_key = match sql::get_private_key(pool, user_id).await?.key {
Some(key) => key,
None => return Ok(None),
};
// create MinSign Box from key string
let private_key_box = SecretKeyBox::from_string(&encrypted_private_key)?;
// and the box can be opened using the password to reveal the original secret key:
let private_key = private_key_box.into_secret_key(Some(secret))?;
// cache key if TTL is set
if let Some(ttl) = self.ttl {
let duration = Duration::from_secs(ttl.into());
private_key_cache
.add_key(user_id, &private_key, duration)
.await;
}
private_key
}
None => match private_key_cache.get_key(user_id).await {
Some(key) => key.secret_key(),
None => return Err(ApiError::InvalidCredentials),
},
};
// create MinSign Box from key string
let private_key_box = SecretKeyBox::from_string(&private_key)?;
// and the box can be opened using the password to reveal the original secret key:
let secret_key = private_key_box.into_secret_key(Some(self.secret))?;
Ok(Some(secret_key))
Ok(Some(private_key))
}
}

View File

@ -0,0 +1,4 @@
pub mod users_available_ad_get;
pub mod users_get;
pub mod users_post;
pub mod users_put;

View File

@ -0,0 +1,53 @@
use axum::{debug_handler, Extension, Json};
use axum_jwt_login::UserPermissions;
use crate::{
api::{
backend::{
ldap::{ActiveDirectoryUser, LDAPBackend},
ApiBackend,
},
description::USERS_TAG,
routes::{auth::models::Credentials, users::sql},
},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
get,
path = "/users/available_ad_users",
summary = "Get Active Directory Users",
description = "Get all Available Users from the Active Directory that are not already registered with this API.",
responses(
(status = OK, body = Vec<ActiveDirectoryUser>, description = "List of AD users"),
),
security(
("user_auth" = ["write:users",]),
),
tag = USERS_TAG)]
pub async fn get_ad_users(
Extension(backend): Extension<ApiBackend>,
Json(credentials): Json<Option<Credentials>>,
) -> Result<Json<Vec<ActiveDirectoryUser>>, ApiError> {
let api_user_ids: Vec<String> = sql::get_users(backend.pool(), None, None)
.await?
.iter()
.map(|user| user.id())
.collect();
let mut ldap = LDAPBackend::from_config(backend.config()).await?;
// bind to AD user if credentials are given
if let Some(credentials) = credentials {
ldap.ad_bind(&credentials.id, &credentials.password).await?;
}
let ad_users = ldap
.get_ad_user_list()
.await?
.into_iter()
.filter(|entry| !api_user_ids.contains(&entry.id))
.collect();
// disconnect from AD server
ldap.unbind().await;
Ok(Json(ad_users))
}

View File

@ -0,0 +1,26 @@
use axum::{debug_handler, Extension, Json};
use super::super::sql;
use crate::{
api::{backend::ApiBackend, description::USERS_TAG, routes::users::models::User},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
get,
path = "/users",
summary = "Get all Users",
description = "Get a list of all users.",
responses(
(status = OK, body = Vec<User>, description = "List of users"),
),
security(
("user_auth" = ["read:users",]),
),
tag = USERS_TAG)]
pub async fn get_users(
Extension(backend): Extension<ApiBackend>,
) -> Result<Json<Vec<User>>, ApiError> {
Ok(Json(sql::get_users(backend.pool(), None, None).await?))
}

View File

@ -0,0 +1,46 @@
use axum::{debug_handler, Extension, Json};
use super::super::sql;
use crate::{
api::{backend::ApiBackend, description::USERS_TAG, routes::users::models::User},
errors::ApiError,
utils::create_random,
};
#[debug_handler]
#[utoipa::path(
post,
path = "/users",
summary = "Create a new User",
description = "Creates a new user with the given information ",
request_body(content = User, description = "User details", content_type = "application/json"),
responses(
(status = OK, description = "User successfully created (Assigned Password in body)", body = String),
),
security(
("user_auth" = ["write:users",]),
),
tag = USERS_TAG)]
pub async fn create_user(
Extension(backend): Extension<ApiBackend>,
Json(user): Json<User>,
) -> Result<String, ApiError> {
// create password if not Active Directory user
let (password, hash) = match user.active_directory_auth {
true => (String::new(), None),
false => {
let salt = create_random(20);
let argon_config = argon2::Config::default();
let password = create_random(10);
let password_hash =
argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &argon_config)?;
(password, Some(password_hash))
}
};
// create user
sql::create_new_user(backend.pool(), &user, hash).await?;
// send created password back to frontend
Ok(password)
}

View File

@ -0,0 +1,33 @@
use axum::{debug_handler, Extension, Json};
use crate::{
api::{
backend::ApiBackend,
description::USERS_TAG,
routes::users::{models::User, sql},
},
errors::ApiError,
};
#[debug_handler]
#[utoipa::path(
put,
path = "/users",
summary = "Change User details",
description = "Update user information / permissions / groups ",
request_body(content = User, description = "User details", content_type = "application/json"),
responses(
(status = OK, description = "User successfully updated"),
),
security(
("user_auth" = ["write:users"]),
),
tag = USERS_TAG)]
pub async fn update_user(
Extension(backend): Extension<ApiBackend>,
Json(user): Json<User>,
) -> Result<(), ApiError> {
sql::update_user(backend.pool(), &user).await?;
Ok(())
}

View File

@ -1,23 +1,14 @@
use axum::{debug_handler, Extension, Json};
use axum_jwt_login::UserPermissions;
use models::User;
use permissions::{Permission, PermissionDetail};
use utoipa_axum::{router::OpenApiRouter, routes};
use crate::{
api::{
backend::{
ldap::{ActiveDirectoryUser, LDAPBackend},
ApiBackend,
},
description::USERS_TAG,
routes::auth::models::Credentials,
},
errors::ApiError,
permission_required,
utils::create_random,
};
use crate::permission_required;
use handlers::users_available_ad_get::*;
use handlers::users_get::*;
use handlers::users_post::*;
use handlers::users_put::*;
mod handlers;
pub mod models;
pub mod permissions;
pub mod sql;
@ -38,122 +29,3 @@ pub fn router() -> OpenApiRouter {
OpenApiRouter::new().merge(read).merge(write)
}
#[debug_handler]
#[utoipa::path(
get,
path = "/users",
summary = "Get all Users",
description = "Get a list of all users.",
responses(
(status = OK, body = Vec<User>, description = "List of users"),
),
security(
("user_auth" = ["read:users",]),
),
tag = USERS_TAG)]
pub async fn get_users(
Extension(backend): Extension<ApiBackend>,
) -> Result<Json<Vec<User>>, ApiError> {
Ok(Json(sql::get_users(backend.pool(), None, None).await?))
}
#[debug_handler]
#[utoipa::path(
put,
path = "/users",
summary = "Change User details",
description = "Update user information / permissions / groups ",
request_body(content = User, description = "User details", content_type = "application/json"),
responses(
(status = OK, description = "User successfully updated"),
),
security(
("user_auth" = ["write:users"]),
),
tag = USERS_TAG)]
pub async fn update_user(
Extension(backend): Extension<ApiBackend>,
Json(user): Json<User>,
) -> Result<(), ApiError> {
sql::update_user(backend.pool(), &user).await?;
Ok(())
}
#[debug_handler]
#[utoipa::path(
post,
path = "/users",
summary = "Create a new User",
description = "Creates a new user with the given information ",
request_body(content = User, description = "User details", content_type = "application/json"),
responses(
(status = OK, description = "User successfully created (Assigned Password in body)", body = String),
),
security(
("user_auth" = ["write:users",]),
),
tag = USERS_TAG)]
pub async fn create_user(
Extension(backend): Extension<ApiBackend>,
Json(user): Json<User>,
) -> Result<String, ApiError> {
// create password if not Active Directory user
let (password, hash) = match user.active_directory_auth {
true => (String::new(), None),
false => {
let salt = create_random(20);
let argon_config = argon2::Config::default();
let password = create_random(10);
let password_hash =
argon2::hash_encoded(password.as_bytes(), salt.as_bytes(), &argon_config)?;
(password, Some(password_hash))
}
};
// create user
sql::create_new_user(backend.pool(), &user, hash).await?;
// send created password back to frontend
Ok(password)
}
#[debug_handler]
#[utoipa::path(
get,
path = "/users/available_ad_users",
summary = "Get Active Directory Users",
description = "Get all Available Users from the Active Directory that are not already registered with this API.",
responses(
(status = OK, body = Vec<ActiveDirectoryUser>, description = "List of AD users"),
),
security(
("user_auth" = ["write:users",]),
),
tag = USERS_TAG)]
pub async fn get_ad_users(
Extension(backend): Extension<ApiBackend>,
Json(credentials): Json<Option<Credentials>>,
) -> Result<Json<Vec<ActiveDirectoryUser>>, ApiError> {
let api_user_ids: Vec<String> = sql::get_users(backend.pool(), None, None)
.await?
.iter()
.map(|user| user.id())
.collect();
let mut ldap = LDAPBackend::from_config(backend.config()).await?;
// bind to AD user if credentials are given
if let Some(credentials) = credentials {
ldap.ad_bind(&credentials.id, &credentials.password).await?;
}
let ad_users = ldap
.get_ad_user_list()
.await?
.into_iter()
.filter(|entry| !api_user_ids.contains(&entry.id))
.collect();
// disconnect from AD server
ldap.unbind().await;
Ok(Json(ad_users))
}

View File

@ -1,10 +1,12 @@
use std::fmt::{self, Display};
use axum::{
extract::multipart::MultipartError,
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use chrono::{NaiveDateTime, ParseError, ParseResult};
use error_stack::Context;
use minisign::PError;
use serde::Serialize;
@ -36,6 +38,9 @@ pub enum ApiError {
InvalidCredentials,
InternalError(String),
AccessDenied,
MultipartForm(StatusCode, String),
InvalidRequest(String),
FileNotFound,
}
impl ApiError {
@ -44,7 +49,7 @@ impl ApiError {
Self::InvalidCredentials => (StatusCode::UNAUTHORIZED, "Invalid credentials", None),
Self::SQLQueryError(error) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Invalid credentials",
"Error executing SQL request",
Some(error),
),
Self::InternalError(error) => (
@ -52,6 +57,13 @@ impl ApiError {
"Internal Server Error",
Some(error),
),
Self::InvalidRequest(s) => (
StatusCode::BAD_REQUEST,
"Request contains invalid or missing data",
Some(s),
),
Self::MultipartForm(c, s) => (*c, "Multipart Error", Some(s)),
Self::FileNotFound => (StatusCode::NOT_FOUND, "File not found", None),
Self::AccessDenied => (StatusCode::FORBIDDEN, "Access Denied", None), // ApiError::WrongCredentials => (StatusCode::UNAUTHORIZED, "Wrong credentials"),
// ApiError::MissingCredentials => (StatusCode::BAD_REQUEST, "Missing credentials"),
// ApiError::TokenCreation => (StatusCode::INTERNAL_SERVER_ERROR, "Token creation error"),
@ -123,6 +135,12 @@ impl From<serde_json::Error> for ApiError {
}
}
impl From<MultipartError> for ApiError {
fn from(value: MultipartError) -> Self {
Self::MultipartForm(value.status(), value.body_text())
}
}
impl Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let error_info = self.as_error_info().1;

View File

@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS public.draftfiles
(
"DraftID" timestamp without time zone NOT NULL,
"UserID" character varying(10) COLLATE pg_catalog."default" NOT NULL,
"ContentType" character varying(255) COLLATE pg_catalog."default" NOT NULL,
"Name" character varying COLLATE pg_catalog."default" NOT NULL,
"Hash" character varying COLLATE pg_catalog."default" NOT NULL,
"Data" bytea NOT NULL,
"Size" integer NOT NULL,
CONSTRAINT draftfiles_pkey PRIMARY KEY ("DraftID", "UserID", "Hash"),
CONSTRAINT "UserID" FOREIGN KEY ("UserID")
REFERENCES public.users ("UserID") MATCH SIMPLE
ON UPDATE NO ACTION
ON DELETE NO ACTION
NOT VALID
)
TABLESPACE pg_default;
ALTER TABLE IF EXISTS public.draftfiles
OWNER to postgres;