diff --git a/.yaak/yaak.fl_1twfKS3Z0A.yaml b/.yaak/yaak.fl_1twfKS3Z0A.yaml new file mode 100644 index 0000000..c0c1460 --- /dev/null +++ b/.yaak/yaak.fl_1twfKS3Z0A.yaml @@ -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 diff --git a/.yaak/yaak.rq_1YHYKIkG8x.yaml b/.yaak/yaak.rq_1YHYKIkG8x.yaml new file mode 100644 index 0000000..2fdfb8d --- /dev/null +++ b/.yaak/yaak.rq_1YHYKIkG8x.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_A81dnTkhL5.yaml b/.yaak/yaak.rq_A81dnTkhL5.yaml new file mode 100644 index 0000000..cfc213f --- /dev/null +++ b/.yaak/yaak.rq_A81dnTkhL5.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_Eb5hJAiaKG.yaml b/.yaak/yaak.rq_Eb5hJAiaKG.yaml new file mode 100644 index 0000000..7c7d0af --- /dev/null +++ b/.yaak/yaak.rq_Eb5hJAiaKG.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_FJT5RHtZ2B.yaml b/.yaak/yaak.rq_FJT5RHtZ2B.yaml new file mode 100644 index 0000000..2d5eb04 --- /dev/null +++ b/.yaak/yaak.rq_FJT5RHtZ2B.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_FmsM38yvHM.yaml b/.yaak/yaak.rq_FmsM38yvHM.yaml new file mode 100644 index 0000000..056b2ea --- /dev/null +++ b/.yaak/yaak.rq_FmsM38yvHM.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_G6OxWcR5j2.yaml b/.yaak/yaak.rq_G6OxWcR5j2.yaml new file mode 100644 index 0000000..a9cf390 --- /dev/null +++ b/.yaak/yaak.rq_G6OxWcR5j2.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_SFA4nX8S0k.yaml b/.yaak/yaak.rq_SFA4nX8S0k.yaml new file mode 100644 index 0000000..0028e5d --- /dev/null +++ b/.yaak/yaak.rq_SFA4nX8S0k.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_eFkZk6Z6QS.yaml b/.yaak/yaak.rq_eFkZk6Z6QS.yaml new file mode 100644 index 0000000..c6792de --- /dev/null +++ b/.yaak/yaak.rq_eFkZk6Z6QS.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_gU10vzCIxt.yaml b/.yaak/yaak.rq_gU10vzCIxt.yaml new file mode 100644 index 0000000..29afc82 --- /dev/null +++ b/.yaak/yaak.rq_gU10vzCIxt.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_lGuHoFSNGa.yaml b/.yaak/yaak.rq_lGuHoFSNGa.yaml new file mode 100644 index 0000000..10cce97 --- /dev/null +++ b/.yaak/yaak.rq_lGuHoFSNGa.yaml @@ -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: [] diff --git a/.yaak/yaak.rq_t5k4XC8Poe.yaml b/.yaak/yaak.rq_t5k4XC8Poe.yaml new file mode 100644 index 0000000..aea10f0 --- /dev/null +++ b/.yaak/yaak.rq_t5k4XC8Poe.yaml @@ -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: [] diff --git a/.yaak/yaak.wk_SlydsyH2WI.yaml b/.yaak/yaak.wk_SlydsyH2WI.yaml new file mode 100644 index 0000000..a3df92d --- /dev/null +++ b/.yaak/yaak.wk_SlydsyH2WI.yaml @@ -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 diff --git a/src/api/backend/mod.rs b/src/api/backend/mod.rs index 8cff223..6e1749c 100644 --- a/src/api/backend/mod.rs +++ b/src/api/backend/mod.rs @@ -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 for ApiBackend { diff --git a/src/api/backend/private_key_cache.rs b/src/api/backend/private_key_cache.rs new file mode 100644 index 0000000..71c00d7 --- /dev/null +++ b/src/api/backend/private_key_cache.rs @@ -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>>, +} + +#[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 { + let lock = self.cache.lock().await; + lock.get(user_id).cloned() + } +} diff --git a/src/api/description.rs b/src/api/description.rs index 600fdbd..2da9e97 100644 --- a/src/api/description.rs +++ b/src/api/description.rs @@ -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") ), )] diff --git a/src/api/routes/api_keys/handlers/apikeys_delete.rs b/src/api/routes/api_keys/handlers/apikeys_delete.rs new file mode 100644 index 0000000..b458027 --- /dev/null +++ b/src/api/routes/api_keys/handlers/apikeys_delete.rs @@ -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, + Query(params): Query, +) -> Result<(), ApiError> { + sql::delete_api_key(backend.pool(), ¶ms.key_id).await?; + Ok(()) +} diff --git a/src/api/routes/api_keys/handlers/apikeys_get.rs b/src/api/routes/api_keys/handlers/apikeys_get.rs new file mode 100644 index 0000000..031592f --- /dev/null +++ b/src/api/routes/api_keys/handlers/apikeys_get.rs @@ -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, description = "List of API Keys"), + ), + security( + ("user_auth" = ["read:apikeys",]), + ), + tag = API_KEY_TAG)] +pub async fn get_api_keys( + Extension(backend): Extension, +) -> Result>, ApiError> { + Ok(Json(sql::get_api_keys(backend.pool()).await?)) +} diff --git a/src/api/routes/api_keys/handlers/apikeys_post.rs b/src/api/routes/api_keys/handlers/apikeys_post.rs new file mode 100644 index 0000000..5891cef --- /dev/null +++ b/src/api/routes/api_keys/handlers/apikeys_post.rs @@ -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, +) -> Result { + 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) +} diff --git a/src/api/routes/api_keys/handlers/apikeys_put.rs b/src/api/routes/api_keys/handlers/apikeys_put.rs new file mode 100644 index 0000000..0f81165 --- /dev/null +++ b/src/api/routes/api_keys/handlers/apikeys_put.rs @@ -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, + Json(api_key): Json, +) -> Result<(), ApiError> { + sql::update_api_key(backend.pool(), &api_key).await?; + Ok(()) +} diff --git a/src/api/routes/api_keys/handlers/mod.rs b/src/api/routes/api_keys/handlers/mod.rs new file mode 100644 index 0000000..91750b0 --- /dev/null +++ b/src/api/routes/api_keys/handlers/mod.rs @@ -0,0 +1,4 @@ +pub mod apikeys_delete; +pub mod apikeys_get; +pub mod apikeys_post; +pub mod apikeys_put; diff --git a/src/api/routes/api_keys/mod.rs b/src/api/routes/api_keys/mod.rs index 1e1bb3e..114cc63 100644 --- a/src/api/routes/api_keys/mod.rs +++ b/src/api/routes/api_keys/mod.rs @@ -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, description = "List of API Keys"), - ), - security( - ("user_auth" = ["read:apikeys",]), - ), - tag = API_KEY_TAG)] -pub async fn get_api_keys( - Extension(backend): Extension, -) -> Result>, 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, -) -> Result { - 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, - Json(api_key): Json, -) -> 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, - Query(params): Query, -) -> Result<(), ApiError> { - sql::delete_api_key(backend.pool(), ¶ms.key_id).await?; - Ok(()) -} diff --git a/src/api/routes/auth/handlers/login_post.rs b/src/api/routes/auth/handlers/login_post.rs new file mode 100644 index 0000000..c1a9247 --- /dev/null +++ b/src/api/routes/auth/handlers/login_post.rs @@ -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, +) -> Result { + 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) +} diff --git a/src/api/routes/auth/handlers/logout_post.rs b/src/api/routes/auth/handlers/logout_post.rs new file mode 100644 index 0000000..7934b74 --- /dev/null +++ b/src/api/routes/auth/handlers/logout_post.rs @@ -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(()) +} diff --git a/src/api/routes/auth/handlers/mod.rs b/src/api/routes/auth/handlers/mod.rs new file mode 100644 index 0000000..d7dd078 --- /dev/null +++ b/src/api/routes/auth/handlers/mod.rs @@ -0,0 +1,2 @@ +pub mod login_post; +pub mod logout_post; diff --git a/src/api/routes/auth/mod.rs b/src/api/routes/auth/mod.rs index b6f82e7..2ef4d56 100644 --- a/src/api/routes/auth/mod.rs +++ b/src/api/routes/auth/mod.rs @@ -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, -) -> Result { - 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(()) -} diff --git a/src/api/routes/files/handlers/files_draft_file_get.rs b/src/api/routes/files/handlers/files_draft_file_get.rs new file mode 100644 index 0000000..9a4cbac --- /dev/null +++ b/src/api/routes/files/handlers/files_draft_file_get.rs @@ -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, +) -> Result { + 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), + } +} diff --git a/src/api/routes/files/handlers/files_draft_get.rs b/src/api/routes/files/handlers/files_draft_get.rs new file mode 100644 index 0000000..8adf191 --- /dev/null +++ b/src/api/routes/files/handlers/files_draft_get.rs @@ -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, +) -> Result>, 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?, + )) +} diff --git a/src/api/routes/files/handlers/files_draft_post.rs b/src/api/routes/files/handlers/files_draft_post.rs new file mode 100644 index 0000000..ad11a03 --- /dev/null +++ b/src/api/routes/files/handlers/files_draft_post.rs @@ -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, + #[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 = None; + let mut draft_id: Option = None; + + let mut content_type: Option = None; + let mut file_name: Option = None; + let mut bytes: Option = None; + let mut size: Option = 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(), + )) + } +} diff --git a/src/api/routes/files/handlers/mod.rs b/src/api/routes/files/handlers/mod.rs new file mode 100644 index 0000000..d68a319 --- /dev/null +++ b/src/api/routes/files/handlers/mod.rs @@ -0,0 +1,3 @@ +pub mod files_draft_file_get; +pub mod files_draft_get; +pub mod files_draft_post; diff --git a/src/api/routes/files/mod.rs b/src/api/routes/files/mod.rs new file mode 100644 index 0000000..4fe3275 --- /dev/null +++ b/src/api/routes/files/mod.rs @@ -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 + // ))) +} diff --git a/src/api/routes/files/models.rs b/src/api/routes/files/models.rs new file mode 100644 index 0000000..721de08 --- /dev/null +++ b/src/api/routes/files/models.rs @@ -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}") + } +} diff --git a/src/api/routes/files/sql.rs b/src/api/routes/files/sql.rs new file mode 100644 index 0000000..a357977 --- /dev/null +++ b/src/api/routes/files/sql.rs @@ -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, 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, 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) +} diff --git a/src/api/routes/mod.rs b/src/api/routes/mod.rs index 89036f1..6d5067a 100644 --- a/src/api/routes/mod.rs +++ b/src/api/routes/mod.rs @@ -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)), diff --git a/src/api/routes/signature/handlers/mod.rs b/src/api/routes/signature/handlers/mod.rs new file mode 100644 index 0000000..aee71fe --- /dev/null +++ b/src/api/routes/signature/handlers/mod.rs @@ -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; diff --git a/src/api/routes/signature/handlers/signature_key_get.rs b/src/api/routes/signature/handlers/signature_key_get.rs new file mode 100644 index 0000000..b89db3a --- /dev/null +++ b/src/api/routes/signature/handlers/signature_key_get.rs @@ -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, 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, + })) +} diff --git a/src/api/routes/signature/handlers/signature_key_post.rs b/src/api/routes/signature/handlers/signature_key_post.rs new file mode 100644 index 0000000..083f8c3 --- /dev/null +++ b/src/api/routes/signature/handlers/signature_key_post.rs @@ -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, +) -> 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(()) +} diff --git a/src/api/routes/signature/handlers/signature_key_put.rs b/src/api/routes/signature/handlers/signature_key_put.rs new file mode 100644 index 0000000..3bd9408 --- /dev/null +++ b/src/api/routes/signature/handlers/signature_key_put.rs @@ -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, +) -> Result, 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)) +} diff --git a/src/api/routes/signature/handlers/signature_sign_post.rs b/src/api/routes/signature/handlers/signature_sign_post.rs new file mode 100644 index 0000000..51d6830 --- /dev/null +++ b/src/api/routes/signature/handlers/signature_sign_post.rs @@ -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, +) -> Result, 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(), + })) +} diff --git a/src/api/routes/signature/handlers/signature_verify_get.rs b/src/api/routes/signature/handlers/signature_verify_get.rs new file mode 100644 index 0000000..617b573 --- /dev/null +++ b/src/api/routes/signature/handlers/signature_verify_get.rs @@ -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, +) -> 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(()) +} diff --git a/src/api/routes/signature/mod.rs b/src/api/routes/signature/mod.rs index 5a95f7a..055d9d0 100644 --- a/src/api/routes/signature/mod.rs +++ b/src/api/routes/signature/mod.rs @@ -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, 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, -) -> 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, -) -> Result, 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, -) -> Result, 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, -) -> 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(()) -} diff --git a/src/api/routes/signature/models.rs b/src/api/routes/signature/models.rs index a63ec95..00cdb10 100644 --- a/src/api/routes/signature/models.rs +++ b/src/api/routes/signature/models.rs @@ -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, + // if ttl is set, the key is cached for ttl in seconds + pub ttl: Option, } impl SigningKeyRequest { @@ -40,19 +46,41 @@ impl SigningKeyRequest { self, pool: &PgPool, user_id: &str, + private_key_cache: PrivateKeyCache, ) -> Result, 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)) } } diff --git a/src/api/routes/users/handlers/mod.rs b/src/api/routes/users/handlers/mod.rs new file mode 100644 index 0000000..142a72d --- /dev/null +++ b/src/api/routes/users/handlers/mod.rs @@ -0,0 +1,4 @@ +pub mod users_available_ad_get; +pub mod users_get; +pub mod users_post; +pub mod users_put; diff --git a/src/api/routes/users/handlers/users_available_ad_get.rs b/src/api/routes/users/handlers/users_available_ad_get.rs new file mode 100644 index 0000000..6d0d461 --- /dev/null +++ b/src/api/routes/users/handlers/users_available_ad_get.rs @@ -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, description = "List of AD users"), + ), + security( + ("user_auth" = ["write:users",]), + ), + tag = USERS_TAG)] +pub async fn get_ad_users( + Extension(backend): Extension, + Json(credentials): Json>, +) -> Result>, ApiError> { + let api_user_ids: Vec = 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)) +} diff --git a/src/api/routes/users/handlers/users_get.rs b/src/api/routes/users/handlers/users_get.rs new file mode 100644 index 0000000..b76d793 --- /dev/null +++ b/src/api/routes/users/handlers/users_get.rs @@ -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, description = "List of users"), + ), + security( + ("user_auth" = ["read:users",]), + ), + tag = USERS_TAG)] +pub async fn get_users( + Extension(backend): Extension, +) -> Result>, ApiError> { + Ok(Json(sql::get_users(backend.pool(), None, None).await?)) +} diff --git a/src/api/routes/users/handlers/users_post.rs b/src/api/routes/users/handlers/users_post.rs new file mode 100644 index 0000000..81369db --- /dev/null +++ b/src/api/routes/users/handlers/users_post.rs @@ -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, + Json(user): Json, +) -> Result { + // 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) +} diff --git a/src/api/routes/users/handlers/users_put.rs b/src/api/routes/users/handlers/users_put.rs new file mode 100644 index 0000000..65e26ae --- /dev/null +++ b/src/api/routes/users/handlers/users_put.rs @@ -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, + Json(user): Json, +) -> Result<(), ApiError> { + sql::update_user(backend.pool(), &user).await?; + + Ok(()) +} diff --git a/src/api/routes/users/mod.rs b/src/api/routes/users/mod.rs index 18ad969..8f28c82 100644 --- a/src/api/routes/users/mod.rs +++ b/src/api/routes/users/mod.rs @@ -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, description = "List of users"), - ), - security( - ("user_auth" = ["read:users",]), - ), - tag = USERS_TAG)] -pub async fn get_users( - Extension(backend): Extension, -) -> Result>, 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, - Json(user): Json, -) -> 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, - Json(user): Json, -) -> Result { - // 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, description = "List of AD users"), - ), - security( - ("user_auth" = ["write:users",]), - ), - tag = USERS_TAG)] -pub async fn get_ad_users( - Extension(backend): Extension, - Json(credentials): Json>, -) -> Result>, ApiError> { - let api_user_ids: Vec = 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)) -} diff --git a/src/errors.rs b/src/errors.rs index 7aa0666..b5686b8 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -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 for ApiError { } } +impl From 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; diff --git a/src/migrations/07_draft_files.sql b/src/migrations/07_draft_files.sql new file mode 100644 index 0000000..05eb528 --- /dev/null +++ b/src/migrations/07_draft_files.sql @@ -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; \ No newline at end of file