initial
This commit is contained in:
		
						commit
						4ebb2a21d2
					
				
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					/target
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
 | 
					config.toml
 | 
				
			||||||
 | 
					*.log
 | 
				
			||||||
 | 
					*.db
 | 
				
			||||||
 | 
					/bindings
 | 
				
			||||||
 | 
					.DS_Store
 | 
				
			||||||
							
								
								
									
										18
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								.vscode/settings.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					    "cSpell.words": [
 | 
				
			||||||
 | 
					        "apikey",
 | 
				
			||||||
 | 
					        "axum",
 | 
				
			||||||
 | 
					        "chrono",
 | 
				
			||||||
 | 
					        "color",
 | 
				
			||||||
 | 
					        "Conn",
 | 
				
			||||||
 | 
					        "dotenv",
 | 
				
			||||||
 | 
					        "hmac",
 | 
				
			||||||
 | 
					        "oneshot",
 | 
				
			||||||
 | 
					        "openapi",
 | 
				
			||||||
 | 
					        "recv",
 | 
				
			||||||
 | 
					        "repr",
 | 
				
			||||||
 | 
					        "Servable",
 | 
				
			||||||
 | 
					        "sqlx",
 | 
				
			||||||
 | 
					        "utoipa"
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										9
									
								
								ActiveDirecotry/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								ActiveDirecotry/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					FROM ubuntu:22.04
 | 
				
			||||||
 | 
					ENV DEBIAN_FRONTEND noninteractive
 | 
				
			||||||
 | 
					# Install Required packages
 | 
				
			||||||
 | 
					RUN apt-get update && apt-get -y install samba krb5-config winbind smbclient iproute2 openssl \
 | 
				
			||||||
 | 
					    && rm /etc/krb5.conf \
 | 
				
			||||||
 | 
					    && mkdir -p /opt/ad-scripts
 | 
				
			||||||
 | 
					WORKDIR /opt/ad-scripts
 | 
				
			||||||
 | 
					COPY *.sh /opt/ad-scripts
 | 
				
			||||||
 | 
					CMD chmod +x /opt/ad-scripts/*.sh && ./samba-ad-setup.sh && ./samba-ad-run.sh
 | 
				
			||||||
							
								
								
									
										17
									
								
								ActiveDirecotry/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								ActiveDirecotry/docker-compose.yaml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
				
			|||||||
 | 
					# https://github.com/amardeep2006/ldap-activedirectory-samba/
 | 
				
			||||||
 | 
					version: "3.4"
 | 
				
			||||||
 | 
					services:
 | 
				
			||||||
 | 
					  ldap.example.org:
 | 
				
			||||||
 | 
					    restart: "no"
 | 
				
			||||||
 | 
					    hostname: "ldap.example.org"
 | 
				
			||||||
 | 
					    build:
 | 
				
			||||||
 | 
					      context: .
 | 
				
			||||||
 | 
					      dockerfile: Dockerfile
 | 
				
			||||||
 | 
					    privileged: true
 | 
				
			||||||
 | 
					    expose:
 | 
				
			||||||
 | 
					      - "636"
 | 
				
			||||||
 | 
					    ports:
 | 
				
			||||||
 | 
					      - "636:636"      
 | 
				
			||||||
 | 
					    environment:
 | 
				
			||||||
 | 
					      SMB_ADMIN_PASSWORD: "admin123!"
 | 
				
			||||||
 | 
					      AD_DOMAIN: "DEV-AD"
 | 
				
			||||||
							
								
								
									
										10
									
								
								ActiveDirecotry/samba-ad-run.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								ActiveDirecotry/samba-ad-run.sh
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					[ -f /var/lib/samba/.setup ] || {
 | 
				
			||||||
 | 
					    >&2 echo "[ERROR] Samba is not setup yet, which should happen automatically. Look for errors!"
 | 
				
			||||||
 | 
					    exit 127
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					samba -i -s /var/lib/samba/private/smb.conf
 | 
				
			||||||
							
								
								
									
										31
									
								
								ActiveDirecotry/samba-ad-setup.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ActiveDirecotry/samba-ad-setup.sh
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
				
			|||||||
 | 
					#!/bin/bash
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					set -e
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					info () {
 | 
				
			||||||
 | 
					    echo "[INFO] $@"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					info "Running setup"
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					# Check if samba is setup
 | 
				
			||||||
 | 
					[ -f /var/lib/samba/.setup ] && info "Already setup..." && exit 0
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					info "Provisioning domain controller..."
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					info "Given admin password: ${SMB_ADMIN_PASSWORD}"
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					rm /etc/samba/smb.conf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Setting up Samba AD server 
 | 
				
			||||||
 | 
					samba-tool domain provision\
 | 
				
			||||||
 | 
					 --server-role=dc\
 | 
				
			||||||
 | 
					 --use-rfc2307\
 | 
				
			||||||
 | 
					 --dns-backend=SAMBA_INTERNAL\
 | 
				
			||||||
 | 
					 --realm=$(hostname | cut -d '.' -f 2-)\
 | 
				
			||||||
 | 
					 --domain=${AD_DOMAIN}\
 | 
				
			||||||
 | 
					 --adminpass=${SMB_ADMIN_PASSWORD}
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					mv /etc/samba/smb.conf /var/lib/samba/private/smb.conf
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					touch /var/lib/samba/.setup
 | 
				
			||||||
							
								
								
									
										3600
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3600
									
								
								Cargo.lock
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										66
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								Cargo.toml
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,66 @@
 | 
				
			|||||||
 | 
					[package]
 | 
				
			||||||
 | 
					name = "axum-api-test"
 | 
				
			||||||
 | 
					version = "0.1.0"
 | 
				
			||||||
 | 
					edition = "2021"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[dependencies]
 | 
				
			||||||
 | 
					# Logging
 | 
				
			||||||
 | 
					# ========================================
 | 
				
			||||||
 | 
					tracing = "0.1.41"
 | 
				
			||||||
 | 
					tracing-subscriber = { version = "0.3.19", features = [
 | 
				
			||||||
 | 
					    "local-time",
 | 
				
			||||||
 | 
					    "env-filter",
 | 
				
			||||||
 | 
					] }
 | 
				
			||||||
 | 
					tracing-appender = "0.2.3"
 | 
				
			||||||
 | 
					tracing-rolling-file = "0.1.2"
 | 
				
			||||||
 | 
					error-stack = "0.5.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# CLI
 | 
				
			||||||
 | 
					# ========================================
 | 
				
			||||||
 | 
					dotenv = "0.15"
 | 
				
			||||||
 | 
					clap = { version = "4.5.23", features = ["derive"] }
 | 
				
			||||||
 | 
					config = "0.15.4"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# User Authentication
 | 
				
			||||||
 | 
					# ========================================
 | 
				
			||||||
 | 
					uuid = { version = "1.11.0", features = ["v4"] }
 | 
				
			||||||
 | 
					sha2 = "0.10.8"
 | 
				
			||||||
 | 
					hmac = "0.12.1"
 | 
				
			||||||
 | 
					# axum-jwt-login = { path = "../axum-login-jwt" }
 | 
				
			||||||
 | 
					axum-jwt-login = { version = "0.1.0", registry = "kellnr" }
 | 
				
			||||||
 | 
					rust-argon2 = "2.1.0"
 | 
				
			||||||
 | 
					rand = "0.8.5"
 | 
				
			||||||
 | 
					ldap3 = "0.11.5"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Service
 | 
				
			||||||
 | 
					# ========================================
 | 
				
			||||||
 | 
					windows-service = "0.7.0"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					axum = { version = "0.8.1", features = ["macros"] }
 | 
				
			||||||
 | 
					strum = { version = "0.26", features = ["derive"] }
 | 
				
			||||||
 | 
					# utoipa = { version = "5.3.0", features = ["axum_extras"] }
 | 
				
			||||||
 | 
					utoipa = { git = "https://github.com/juhaku/utoipa", features = [
 | 
				
			||||||
 | 
					    "axum_extras",
 | 
				
			||||||
 | 
					] }
 | 
				
			||||||
 | 
					# utoipa-axum = "0.1.3"
 | 
				
			||||||
 | 
					utoipa-axum = { git = "https://github.com/juhaku/utoipa" }
 | 
				
			||||||
 | 
					# utoipa-swagger-ui = { version = "8.1.0", features = ["axum"] }
 | 
				
			||||||
 | 
					# utoipa-redoc = { version = "*", features = ["axum"] }
 | 
				
			||||||
 | 
					# utoipa-scalar = { version = "*", features = ["axum"] }
 | 
				
			||||||
 | 
					utoipa-swagger-ui = { git = "https://github.com/juhaku/utoipa", features = [
 | 
				
			||||||
 | 
					    "axum",
 | 
				
			||||||
 | 
					] }
 | 
				
			||||||
 | 
					utoipa-redoc = { git = "https://github.com/juhaku/utoipa", features = ["axum"] }
 | 
				
			||||||
 | 
					utoipa-scalar = { git = "https://github.com/juhaku/utoipa", features = [
 | 
				
			||||||
 | 
					    "axum",
 | 
				
			||||||
 | 
					] }
 | 
				
			||||||
 | 
					ts-rs = { version = "10.1.0", features = ["chrono-impl"] }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Utilities
 | 
				
			||||||
 | 
					# ========================================
 | 
				
			||||||
 | 
					serde = { version = "1.0.216", features = ["derive"] }
 | 
				
			||||||
 | 
					tokio = { version = "1.42.0", features = ["full"] }
 | 
				
			||||||
 | 
					tokio-util = { version = "0.7.13", features = ["rt"] }
 | 
				
			||||||
 | 
					once_cell = "1.20.2"
 | 
				
			||||||
 | 
					sqlx = { version = "0.8.3", features = ["runtime-tokio", "postgres", "chrono"] }
 | 
				
			||||||
 | 
					chrono = { version = "0.4.39", features = ["serde"] }
 | 
				
			||||||
							
								
								
									
										206
									
								
								src/api/backend/ldap.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								src/api/backend/ldap.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,206 @@
 | 
				
			|||||||
 | 
					use std::collections::HashMap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use ldap3::{Ldap, LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use ts_rs::TS;
 | 
				
			||||||
 | 
					use utoipa::ToSchema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    config::{Configuration, LDAP},
 | 
				
			||||||
 | 
					    errors::ApiError,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
 | 
				
			||||||
 | 
					#[ts(export)]
 | 
				
			||||||
 | 
					pub struct ActiveDirectoryUser {
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub surname: String,
 | 
				
			||||||
 | 
					    pub email: String,
 | 
				
			||||||
 | 
					    pub id: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub struct LDAPBackend {
 | 
				
			||||||
 | 
					    connection: Ldap,
 | 
				
			||||||
 | 
					    config: LDAP,
 | 
				
			||||||
 | 
					    bound: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl LDAPBackend {
 | 
				
			||||||
 | 
					    pub async fn from_config(config: &Configuration) -> Result<Self, ApiError> {
 | 
				
			||||||
 | 
					        // create connection to LDAP server
 | 
				
			||||||
 | 
					        let ldap_settings = LdapConnSettings::new().set_no_tls_verify(config.ldap.skip_tls_verify);
 | 
				
			||||||
 | 
					        let (connection, ldap) = LdapConnAsync::with_settings(ldap_settings, &config.ldap.server)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| ApiError::InternalError(format!("LDAP Server connection: {e}")))?;
 | 
				
			||||||
 | 
					        ldap3::drive!(connection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(Self {
 | 
				
			||||||
 | 
					            config: config.ldap.clone(),
 | 
				
			||||||
 | 
					            connection: ldap,
 | 
				
			||||||
 | 
					            bound: false,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn unbind(&mut self) {
 | 
				
			||||||
 | 
					        let _ = self.connection.unbind().await;
 | 
				
			||||||
 | 
					        self.bound = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn ad_bind(&mut self, user: &str, password: &str) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					        self.connection
 | 
				
			||||||
 | 
					            .simple_bind(&format!("{}\\{user}", self.config.ad_domain), &password)
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|_e| ApiError::InvalidCredentials)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.bound = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn get_user_detail(
 | 
				
			||||||
 | 
					        &mut self,
 | 
				
			||||||
 | 
					        user_id: &str,
 | 
				
			||||||
 | 
					    ) -> Result<HashMap<String, Vec<String>>, ApiError> {
 | 
				
			||||||
 | 
					        // check if elevated user has to be used
 | 
				
			||||||
 | 
					        if self.config.elevated_search {
 | 
				
			||||||
 | 
					            let id = self.config.elevated_user_id.clone();
 | 
				
			||||||
 | 
					            let password = self.config.elevated_user_pw.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.ad_bind(&id, &password).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        let (search_result, _) = self
 | 
				
			||||||
 | 
					            .connection
 | 
				
			||||||
 | 
					            .search(
 | 
				
			||||||
 | 
					                &self.config.user_search_base,
 | 
				
			||||||
 | 
					                Scope::Subtree,
 | 
				
			||||||
 | 
					                // "(objectClass=*)",
 | 
				
			||||||
 | 
					                &format!("(&(ObjectCategory=Person)(sAMAccountName={user_id}))"),
 | 
				
			||||||
 | 
					                vec!["givenName", "sn", "mail"],
 | 
				
			||||||
 | 
					                // vec!["*"],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))?
 | 
				
			||||||
 | 
					            .success()
 | 
				
			||||||
 | 
					            .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let attributes = if let Some(entry) = search_result.first().cloned() {
 | 
				
			||||||
 | 
					            SearchEntry::construct(entry).attrs
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            HashMap::new()
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(attributes)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn get_ad_user_list(&mut self) -> Result<Vec<ActiveDirectoryUser>, ApiError> {
 | 
				
			||||||
 | 
					        // check if elevated user has to be used
 | 
				
			||||||
 | 
					        if self.config.elevated_search {
 | 
				
			||||||
 | 
					            let id = self.config.elevated_user_id.clone();
 | 
				
			||||||
 | 
					            let password = self.config.elevated_user_pw.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            self.ad_bind(&id, &password).await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (search_result, _) = self
 | 
				
			||||||
 | 
					            .connection
 | 
				
			||||||
 | 
					            .search(
 | 
				
			||||||
 | 
					                &self.config.user_search_base,
 | 
				
			||||||
 | 
					                Scope::Subtree,
 | 
				
			||||||
 | 
					                &format!("(&(ObjectCategory=Person))"),
 | 
				
			||||||
 | 
					                vec!["sAMAccountName", "givenName", "sn", "mail"],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))?
 | 
				
			||||||
 | 
					            .success()
 | 
				
			||||||
 | 
					            .map_err(|e| ApiError::InternalError(format!("LDAP search error: {e}")))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let result = search_result
 | 
				
			||||||
 | 
					            .into_iter()
 | 
				
			||||||
 | 
					            .map(|entry| {
 | 
				
			||||||
 | 
					                let user_info = SearchEntry::construct(entry).attrs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                ActiveDirectoryUser {
 | 
				
			||||||
 | 
					                    name: user_info
 | 
				
			||||||
 | 
					                        .get("givenName")
 | 
				
			||||||
 | 
					                        .map(|v| v.first().cloned())
 | 
				
			||||||
 | 
					                        .flatten()
 | 
				
			||||||
 | 
					                        .unwrap_or_default(),
 | 
				
			||||||
 | 
					                    surname: user_info
 | 
				
			||||||
 | 
					                        .get("sn")
 | 
				
			||||||
 | 
					                        .map(|v| v.first().cloned())
 | 
				
			||||||
 | 
					                        .flatten()
 | 
				
			||||||
 | 
					                        .unwrap_or_default(),
 | 
				
			||||||
 | 
					                    email: user_info
 | 
				
			||||||
 | 
					                        .get("mail")
 | 
				
			||||||
 | 
					                        .map(|v| v.first().cloned())
 | 
				
			||||||
 | 
					                        .flatten()
 | 
				
			||||||
 | 
					                        .unwrap_or_default(),
 | 
				
			||||||
 | 
					                    id: user_info
 | 
				
			||||||
 | 
					                        .get("sAMAccountName")
 | 
				
			||||||
 | 
					                        .map(|v| v.first().cloned())
 | 
				
			||||||
 | 
					                        .flatten()
 | 
				
			||||||
 | 
					                        .unwrap_or_default(),
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .collect();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(result)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use ldap3::{exop::WhoAmI, LdapConnAsync, LdapConnSettings, Scope, SearchEntry};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn test_ldap() {
 | 
				
			||||||
 | 
					        let (connection, mut ldap) = LdapConnAsync::with_settings(
 | 
				
			||||||
 | 
					            LdapConnSettings::new().set_no_tls_verify(true),
 | 
				
			||||||
 | 
					            "ldaps://localhost:636",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap();
 | 
				
			||||||
 | 
					        ldap3::drive!(connection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // bind to server
 | 
				
			||||||
 | 
					        let res = ldap
 | 
				
			||||||
 | 
					            // .simple_bind("cn=read-only-admin,dc=example,dc=com", "password")
 | 
				
			||||||
 | 
					            .simple_bind(
 | 
				
			||||||
 | 
					                // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local",
 | 
				
			||||||
 | 
					                // "MYLAB\\A0H67123",
 | 
				
			||||||
 | 
					                // "cn=read-only-admin,dc=example,dc=com",
 | 
				
			||||||
 | 
					                // "DEV-AD\\Administrator",
 | 
				
			||||||
 | 
					                // "admin123!",
 | 
				
			||||||
 | 
					                "DEV-AD\\einstein",
 | 
				
			||||||
 | 
					                "einstein",
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        println!("{res:?}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let (res, re) = ldap
 | 
				
			||||||
 | 
					            .search(
 | 
				
			||||||
 | 
					                // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local",
 | 
				
			||||||
 | 
					                "OU=DevUsers,DC=example,DC=org",
 | 
				
			||||||
 | 
					                Scope::Subtree,
 | 
				
			||||||
 | 
					                "(objectClass=*)",
 | 
				
			||||||
 | 
					                // "(&(ObjectCategory=Person)(sAMAccountName=A0H67123))",
 | 
				
			||||||
 | 
					                vec!["givenName", "sn"],
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .await
 | 
				
			||||||
 | 
					            .unwrap()
 | 
				
			||||||
 | 
					            .success()
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for entry in res {
 | 
				
			||||||
 | 
					            println!("{:?}", SearchEntry::construct(entry));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let test = ldap.extended(WhoAmI).await.unwrap().0;
 | 
				
			||||||
 | 
					        println!("{test:?}");
 | 
				
			||||||
 | 
					        // let whoami: ldap3::exop::WhoAmIResp = test.parse();
 | 
				
			||||||
 | 
					        // println!("{whoami:?}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let _ = ldap.unbind().await;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										70
									
								
								src/api/backend/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								src/api/backend/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,70 @@
 | 
				
			|||||||
 | 
					use axum_jwt_login::{AuthBackend, UserPermissions};
 | 
				
			||||||
 | 
					use ldap::LDAPBackend;
 | 
				
			||||||
 | 
					use sqlx::PgPool;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    api::routes::users::{models::UserStatus, sql::get_users},
 | 
				
			||||||
 | 
					    config::Configuration,
 | 
				
			||||||
 | 
					    errors::ApiError,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::routes::{auth::models::Credentials, users::models::User};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod ldap;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct ApiBackend {
 | 
				
			||||||
 | 
					    pool: PgPool,
 | 
				
			||||||
 | 
					    config: Configuration,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ApiBackend {
 | 
				
			||||||
 | 
					    pub fn new(pool: PgPool, config: &Configuration) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            pool,
 | 
				
			||||||
 | 
					            config: config.clone(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn pool(&self) -> &PgPool {
 | 
				
			||||||
 | 
					        &self.pool
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn config(&self) -> &Configuration {
 | 
				
			||||||
 | 
					        &self.config
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl AuthBackend<User> for ApiBackend {
 | 
				
			||||||
 | 
					    type Credentials = Credentials;
 | 
				
			||||||
 | 
					    type Error = ApiError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn authenticate(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        Credentials { id, password }: Self::Credentials,
 | 
				
			||||||
 | 
					    ) -> Result<Option<User>, Self::Error> {
 | 
				
			||||||
 | 
					        // get user from Database
 | 
				
			||||||
 | 
					        let user = get_users(&self.pool, Some(UserStatus::Active), Some(id)).await?;
 | 
				
			||||||
 | 
					        let user = user.first().ok_or(ApiError::InvalidCredentials)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // authenticate user
 | 
				
			||||||
 | 
					        if user.active_directory_auth {
 | 
				
			||||||
 | 
					            // authenticate against LDAP (AD) server
 | 
				
			||||||
 | 
					            // --
 | 
				
			||||||
 | 
					            let mut ldap = LDAPBackend::from_config(&self.config).await?;
 | 
				
			||||||
 | 
					            ldap.ad_bind(&user.id(), &password).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // update user information
 | 
				
			||||||
 | 
					            let details = ldap.get_user_detail(&user.id()).await?;
 | 
				
			||||||
 | 
					            user.update_with_ad_details(&self.pool, details).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // terminate connection
 | 
				
			||||||
 | 
					            ldap.unbind().await;
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            argon2::verify_encoded(&user.password, password.as_bytes())
 | 
				
			||||||
 | 
					                .map_err(|_| ApiError::InvalidCredentials)?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(Some(user.clone()))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										41
									
								
								src/api/description.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/api/description.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,41 @@
 | 
				
			|||||||
 | 
					use utoipa::{
 | 
				
			||||||
 | 
					    openapi::security::{ApiKey, ApiKeyValue, HttpAuthScheme, HttpBuilder, SecurityScheme},
 | 
				
			||||||
 | 
					    Modify, OpenApi,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const AUTH_TAG: &str = "Authentication";
 | 
				
			||||||
 | 
					pub const USERS_TAG: &str = "Users";
 | 
				
			||||||
 | 
					pub const ORDER_TAG: &str = "order";
 | 
				
			||||||
 | 
					pub const API_KEY_TAG: &str = "API Keys";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(OpenApi)]
 | 
				
			||||||
 | 
					#[openapi(
 | 
				
			||||||
 | 
					    modifiers(&SecurityAddon),
 | 
				
			||||||
 | 
					    tags(
 | 
				
			||||||
 | 
					        (name = AUTH_TAG, description = "API Authentication endpoints"),
 | 
				
			||||||
 | 
					        (name = ORDER_TAG, description = "Order API endpoints")
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub struct ApiDocumentation;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct SecurityAddon;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Modify for SecurityAddon {
 | 
				
			||||||
 | 
					    fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) {
 | 
				
			||||||
 | 
					        if let Some(components) = openapi.components.as_mut() {
 | 
				
			||||||
 | 
					            components.add_security_scheme(
 | 
				
			||||||
 | 
					                "api_key",
 | 
				
			||||||
 | 
					                SecurityScheme::ApiKey(ApiKey::Header(ApiKeyValue::new("x-api-key"))),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					            components.add_security_scheme(
 | 
				
			||||||
 | 
					                "user_auth",
 | 
				
			||||||
 | 
					                SecurityScheme::Http(
 | 
				
			||||||
 | 
					                    HttpBuilder::new()
 | 
				
			||||||
 | 
					                        .scheme(HttpAuthScheme::Bearer)
 | 
				
			||||||
 | 
					                        .bearer_format("JWT")
 | 
				
			||||||
 | 
					                        .build(),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										202
									
								
								src/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										202
									
								
								src/api/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,202 @@
 | 
				
			|||||||
 | 
					use std::{future::IntoFuture, net::Ipv4Addr, time::Duration};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum::http::HeaderName;
 | 
				
			||||||
 | 
					use axum_jwt_login::{AuthSessionWithApiKeyBuilder, JWTKeyPair};
 | 
				
			||||||
 | 
					use backend::ApiBackend;
 | 
				
			||||||
 | 
					use error_stack::{Report, ResultExt};
 | 
				
			||||||
 | 
					use routes::api_keys::models::ApiKey;
 | 
				
			||||||
 | 
					use sqlx::PgPool;
 | 
				
			||||||
 | 
					use tokio::net::TcpListener;
 | 
				
			||||||
 | 
					use tokio_util::sync::CancellationToken;
 | 
				
			||||||
 | 
					use tracing::{info, level_filters::LevelFilter, Level};
 | 
				
			||||||
 | 
					use tracing_rolling_file::{RollingConditionBase, RollingFileAppenderBase};
 | 
				
			||||||
 | 
					use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::APP_NAME;
 | 
				
			||||||
 | 
					use crate::{config::Configuration, errors::AppError, ROOT_PATH};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod backend;
 | 
				
			||||||
 | 
					mod description;
 | 
				
			||||||
 | 
					pub mod routes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn start(
 | 
				
			||||||
 | 
					    config: &Configuration,
 | 
				
			||||||
 | 
					    cancellation_token: CancellationToken,
 | 
				
			||||||
 | 
					) -> Result<(), Report<AppError>> {
 | 
				
			||||||
 | 
					    // Set Report Colour Mode to NONE
 | 
				
			||||||
 | 
					    Report::set_color_mode(error_stack::fmt::ColorMode::None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let level_filter = LevelFilter::from_level(match config.debug {
 | 
				
			||||||
 | 
					        true => Level::DEBUG,
 | 
				
			||||||
 | 
					        false => Level::INFO,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Prepare logging to file
 | 
				
			||||||
 | 
					    let file_appender = RollingFileAppenderBase::new(
 | 
				
			||||||
 | 
					        ROOT_PATH.with_file_name(format!("{}.log", env!("CARGO_PKG_NAME"))),
 | 
				
			||||||
 | 
					        RollingConditionBase::new().max_size(1024 * 1024 * 2),
 | 
				
			||||||
 | 
					        5,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .change_context(AppError)?;
 | 
				
			||||||
 | 
					    let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // prepare initialization of logging
 | 
				
			||||||
 | 
					    let log_layers = tracing_subscriber::registry().with(level_filter).with(
 | 
				
			||||||
 | 
					        fmt::Layer::default()
 | 
				
			||||||
 | 
					            .with_target(false)
 | 
				
			||||||
 | 
					            .with_ansi(false)
 | 
				
			||||||
 | 
					            .with_writer(non_blocking),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // also log to console in debug mode
 | 
				
			||||||
 | 
					    #[cfg(debug_assertions)]
 | 
				
			||||||
 | 
					    let stdout_log = tracing_subscriber::fmt::layer().pretty();
 | 
				
			||||||
 | 
					    #[cfg(debug_assertions)]
 | 
				
			||||||
 | 
					    let log_layers = log_layers.with(stdout_log);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize logging
 | 
				
			||||||
 | 
					    log_layers.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize database pool
 | 
				
			||||||
 | 
					    let pool = PgPool::connect(&config.database_url)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .change_context(AppError)?;
 | 
				
			||||||
 | 
					    // Get API Keys from database
 | 
				
			||||||
 | 
					    let api_keys = ApiKey::get_all_with_secret_attached(&pool, config)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Initialize API Backend
 | 
				
			||||||
 | 
					    let backend = ApiBackend::new(pool.clone(), config);
 | 
				
			||||||
 | 
					    // Create Session storage for API
 | 
				
			||||||
 | 
					    let session = AuthSessionWithApiKeyBuilder::new()
 | 
				
			||||||
 | 
					        .backend(backend)
 | 
				
			||||||
 | 
					        .api_header_name(HeaderName::from_static("x-api-key"))
 | 
				
			||||||
 | 
					        .initial_api_keys(api_keys)
 | 
				
			||||||
 | 
					        .jwt_key_pair(JWTKeyPair::from_secret(&config.token_secret))
 | 
				
			||||||
 | 
					        .session_length(Duration::from_secs(config.session_length))
 | 
				
			||||||
 | 
					        .build()
 | 
				
			||||||
 | 
					        .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Create routes
 | 
				
			||||||
 | 
					    let router = routes::create_routes(session);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Define listener
 | 
				
			||||||
 | 
					    let port = config.port;
 | 
				
			||||||
 | 
					    let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, port))
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Serve API
 | 
				
			||||||
 | 
					    let server = axum::serve(listener, router.into_make_service())
 | 
				
			||||||
 | 
					        .with_graceful_shutdown(shutdown_signal(cancellation_token));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tokio::spawn(server.into_future());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    info!("{APP_NAME} up and running on port '{port}'.");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn shutdown_signal(stop_signal: CancellationToken) {
 | 
				
			||||||
 | 
					    // Wait for shutdown event.
 | 
				
			||||||
 | 
					    stop_signal.cancelled().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    info!("Shutting down {APP_NAME}...");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // Set Report Colour Mode to NONE
 | 
				
			||||||
 | 
					// Report::set_color_mode(error_stack::fmt::ColorMode::None);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // Enable the `INFO` level for anything in `darl` as default
 | 
				
			||||||
 | 
					// let level_filter =
 | 
				
			||||||
 | 
					//     filter::Targets::new().with_target(env!("CARGO_PKG_NAME"), DEFAULT_LOG_LEVEL_FILTER);
 | 
				
			||||||
 | 
					// let (level_filter, tracing_target_reload_handle) = reload::Layer::new(level_filter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // Prepare logging to file
 | 
				
			||||||
 | 
					// let file_appender = RollingFileAppenderBase::new(
 | 
				
			||||||
 | 
					//     ROOT_PATH.with_file_name(format!("{}.log", env!("CARGO_PKG_NAME"))),
 | 
				
			||||||
 | 
					//     RollingConditionBase::new().max_size(1024 * 1024 * 2),
 | 
				
			||||||
 | 
					//     5,
 | 
				
			||||||
 | 
					// )
 | 
				
			||||||
 | 
					// .change_context(ServiceError::Starting)?;
 | 
				
			||||||
 | 
					// let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // Prepare live logging to config webserver
 | 
				
			||||||
 | 
					// let (log_receiver, live_log_layer) = LiveLogLayer::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // prepare initialization of logging
 | 
				
			||||||
 | 
					// let log_layers = tracing_subscriber::registry()
 | 
				
			||||||
 | 
					//     .with(level_filter)
 | 
				
			||||||
 | 
					//     // .with(filter::LevelFilter::DEBUG)
 | 
				
			||||||
 | 
					//     .with(live_log_layer)
 | 
				
			||||||
 | 
					//     .with(
 | 
				
			||||||
 | 
					//         fmt::Layer::default()
 | 
				
			||||||
 | 
					//             .with_target(false)
 | 
				
			||||||
 | 
					//             .with_ansi(false)
 | 
				
			||||||
 | 
					//             .with_writer(non_blocking),
 | 
				
			||||||
 | 
					//     );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // also log to console in debug mode
 | 
				
			||||||
 | 
					// #[cfg(debug_assertions)]
 | 
				
			||||||
 | 
					// let stdout_log = tracing_subscriber::fmt::layer().pretty();
 | 
				
			||||||
 | 
					// #[cfg(debug_assertions)]
 | 
				
			||||||
 | 
					// let log_layers = log_layers.with(stdout_log);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // Initialize logging
 | 
				
			||||||
 | 
					// log_layers.init();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // Initialize local database
 | 
				
			||||||
 | 
					// let local_database = LocalDatabase::init()
 | 
				
			||||||
 | 
					//     .await
 | 
				
			||||||
 | 
					//     .change_context(ServiceError::Starting)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // Load configuration from config files
 | 
				
			||||||
 | 
					// let (config, external_database) = Configuration::initialize(&local_database)
 | 
				
			||||||
 | 
					//     .await
 | 
				
			||||||
 | 
					//     .change_context(ServiceError::Starting)?;
 | 
				
			||||||
 | 
					// let standalone_external_db = StandaloneExternalDatabase::from(&external_database);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // change log level to configured value
 | 
				
			||||||
 | 
					// if let Err(error) = tracing_target_reload_handle.modify(|filter| {
 | 
				
			||||||
 | 
					//     *filter = filter::Targets::new().with_target(env!("CARGO_PKG_NAME"), &config.log_level)
 | 
				
			||||||
 | 
					// }) {
 | 
				
			||||||
 | 
					//     error!("{error}");
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // prepare and start connections
 | 
				
			||||||
 | 
					// let connections = MachineConnections::init(&local_database, &standalone_external_db, &config)
 | 
				
			||||||
 | 
					//     .await
 | 
				
			||||||
 | 
					//     .change_context(ServiceError::Starting)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // start config server
 | 
				
			||||||
 | 
					// ConfigServer::start(
 | 
				
			||||||
 | 
					//     config.extended_config.webserver.config_server_port,
 | 
				
			||||||
 | 
					//     config.opc_configuration.clone(),
 | 
				
			||||||
 | 
					//     &local_database,
 | 
				
			||||||
 | 
					//     standalone_external_db,
 | 
				
			||||||
 | 
					//     connections,
 | 
				
			||||||
 | 
					//     tracing_target_reload_handle,
 | 
				
			||||||
 | 
					//     log_receiver,
 | 
				
			||||||
 | 
					// )
 | 
				
			||||||
 | 
					// .await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // start webserver
 | 
				
			||||||
 | 
					// WebServer::start(&config, &local_database).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // initialize Logging to external database
 | 
				
			||||||
 | 
					// external_database.start_writer().await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// info!("{APP_NAME} service is now running...");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// // block thread
 | 
				
			||||||
 | 
					// loop {
 | 
				
			||||||
 | 
					//     // Poll shutdown event.
 | 
				
			||||||
 | 
					//     if (stop_signal.recv().await).is_some() {
 | 
				
			||||||
 | 
					//         // Break the loop either upon stop or channel disconnect
 | 
				
			||||||
 | 
					//         info!("Shutting down {APP_NAME} service");
 | 
				
			||||||
 | 
					//         break;
 | 
				
			||||||
 | 
					//     };
 | 
				
			||||||
 | 
					// }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Ok(())
 | 
				
			||||||
							
								
								
									
										145
									
								
								src/api/routes/api_keys/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/api/routes/api_keys/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,145 @@
 | 
				
			|||||||
 | 
					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,
 | 
				
			||||||
 | 
					    permission_required,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::AuthBackendType;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod models;
 | 
				
			||||||
 | 
					pub mod sql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// expose the OpenAPI to parent module
 | 
				
			||||||
 | 
					pub fn router() -> OpenApiRouter {
 | 
				
			||||||
 | 
					    let read = OpenApiRouter::new()
 | 
				
			||||||
 | 
					        .routes(routes!(get_api_keys))
 | 
				
			||||||
 | 
					        .route_layer(permission_required!(Permission::Read(
 | 
				
			||||||
 | 
					            PermissionDetail::APIKeys
 | 
				
			||||||
 | 
					        )));
 | 
				
			||||||
 | 
					    let write = OpenApiRouter::new()
 | 
				
			||||||
 | 
					        .routes(routes!(create_api_key, update_api_key, delete_api_key))
 | 
				
			||||||
 | 
					        .route_layer(permission_required!(Permission::Write(
 | 
				
			||||||
 | 
					            PermissionDetail::APIKeys
 | 
				
			||||||
 | 
					        )));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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(), ¶ms.key_id).await?;
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										143
									
								
								src/api/routes/api_keys/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								src/api/routes/api_keys/models.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,143 @@
 | 
				
			|||||||
 | 
					use std::collections::HashSet;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum_jwt_login::ApiKey as ApiKeyTrait;
 | 
				
			||||||
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
 | 
					use hmac::{Hmac, Mac};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use sha2::Sha512;
 | 
				
			||||||
 | 
					use sqlx::PgPool;
 | 
				
			||||||
 | 
					use ts_rs::TS;
 | 
				
			||||||
 | 
					use utoipa::ToSchema;
 | 
				
			||||||
 | 
					use uuid::Uuid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    api::routes::users::permissions::{Permission, PermissionContainer},
 | 
				
			||||||
 | 
					    config::Configuration,
 | 
				
			||||||
 | 
					    errors::ApiError,
 | 
				
			||||||
 | 
					    utils::create_random,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const KEY_LENGTH: usize = 40;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
 | 
				
			||||||
 | 
					#[ts(export)]
 | 
				
			||||||
 | 
					pub struct ApiKey {
 | 
				
			||||||
 | 
					    pub id: String,
 | 
				
			||||||
 | 
					    #[serde(skip)] // Don't leak Hash
 | 
				
			||||||
 | 
					    pub hash: Vec<u8>,
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub auth_required: bool,
 | 
				
			||||||
 | 
					    #[schema(inline)]
 | 
				
			||||||
 | 
					    pub permissions: PermissionContainer,
 | 
				
			||||||
 | 
					    #[serde(skip)] // Don't leak secret
 | 
				
			||||||
 | 
					    pub api_config_secret: Option<String>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    #[schema(value_type = String, read_only)]
 | 
				
			||||||
 | 
					    pub creation_date: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    #[schema(value_type = String, read_only)]
 | 
				
			||||||
 | 
					    pub last_change: Option<NaiveDateTime>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ApiKey {
 | 
				
			||||||
 | 
					    pub async fn get_all_with_secret_attached(
 | 
				
			||||||
 | 
					        pool: &PgPool,
 | 
				
			||||||
 | 
					        config: &Configuration,
 | 
				
			||||||
 | 
					    ) -> Result<Vec<Self>, ApiError> {
 | 
				
			||||||
 | 
					        let mut keys = super::sql::get_api_keys(pool).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        for key in keys.iter_mut() {
 | 
				
			||||||
 | 
					            key.api_config_secret = Some(config.token_secret.clone());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(keys)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn create(
 | 
				
			||||||
 | 
					        name: &str,
 | 
				
			||||||
 | 
					        requires_auth: bool,
 | 
				
			||||||
 | 
					        permissions: HashSet<Permission>,
 | 
				
			||||||
 | 
					        config: &Configuration,
 | 
				
			||||||
 | 
					    ) -> Result<(String, Self), ApiError> {
 | 
				
			||||||
 | 
					        // create uuid
 | 
				
			||||||
 | 
					        let uuid = Uuid::new_v4().simple();
 | 
				
			||||||
 | 
					        // create API Key secret part
 | 
				
			||||||
 | 
					        let key: String = create_random(KEY_LENGTH);
 | 
				
			||||||
 | 
					        // calculate API Key signature
 | 
				
			||||||
 | 
					        let mut mac = Hmac::<Sha512>::new_from_slice(config.token_secret.as_bytes())?;
 | 
				
			||||||
 | 
					        mac.update(key.as_bytes());
 | 
				
			||||||
 | 
					        let signature = mac.finalize();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Return Api Key Secret and Api Key
 | 
				
			||||||
 | 
					        Ok((
 | 
				
			||||||
 | 
					            key,
 | 
				
			||||||
 | 
					            Self {
 | 
				
			||||||
 | 
					                name: name.to_string(),
 | 
				
			||||||
 | 
					                auth_required: requires_auth,
 | 
				
			||||||
 | 
					                id: uuid.to_string(),
 | 
				
			||||||
 | 
					                hash: signature.into_bytes().as_slice().to_vec(),
 | 
				
			||||||
 | 
					                permissions: PermissionContainer(permissions),
 | 
				
			||||||
 | 
					                api_config_secret: Some(config.token_secret.clone()),
 | 
				
			||||||
 | 
					                creation_date: None,
 | 
				
			||||||
 | 
					                last_change: None,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn validate(&self, key_secret_part: &str) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					        // calculate API Key signature
 | 
				
			||||||
 | 
					        let mut mac = Hmac::<Sha512>::new_from_slice(
 | 
				
			||||||
 | 
					            self.api_config_secret
 | 
				
			||||||
 | 
					                .clone()
 | 
				
			||||||
 | 
					                .ok_or(ApiError::InternalError(
 | 
				
			||||||
 | 
					                    "Missing API Config Secret".to_string(),
 | 
				
			||||||
 | 
					                ))?
 | 
				
			||||||
 | 
					                .as_bytes(),
 | 
				
			||||||
 | 
					        )?;
 | 
				
			||||||
 | 
					        mac.update(key_secret_part.as_bytes());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        match mac.verify_slice(&self.hash) {
 | 
				
			||||||
 | 
					            Ok(_) => Ok(()),
 | 
				
			||||||
 | 
					            Err(_e) => Err(ApiError::AccessDenied),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ApiKeyTrait for ApiKey {
 | 
				
			||||||
 | 
					    type Permission = Permission;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn id(&self) -> String {
 | 
				
			||||||
 | 
					        self.id.clone()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn validate(&self, key_secret_part: &str) -> bool {
 | 
				
			||||||
 | 
					        self.validate(key_secret_part).is_ok()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn requires_user_auth(&self) -> bool {
 | 
				
			||||||
 | 
					        self.auth_required
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn get_permissions(&self) -> HashSet<Self::Permission> {
 | 
				
			||||||
 | 
					        self.permissions.0.clone()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use std::collections::HashSet;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use crate::config::Configuration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use super::ApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn test_api_key_creation_and_validation() {
 | 
				
			||||||
 | 
					        let config = Configuration {
 | 
				
			||||||
 | 
					            token_secret: "abcdefghijk".to_string(),
 | 
				
			||||||
 | 
					            ..Default::default()
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let (key, api_key) =
 | 
				
			||||||
 | 
					            ApiKey::create("name", true, HashSet::new(), &config).expect("Error creating API Key");
 | 
				
			||||||
 | 
					        println!("{key}, {api_key:?}");
 | 
				
			||||||
 | 
					        assert_eq!(Ok(()), api_key.validate(&key));
 | 
				
			||||||
 | 
					        assert_ne!(Ok(()), api_key.validate("124"));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										135
									
								
								src/api/routes/api_keys/sql.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/api/routes/api_keys/sql.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,135 @@
 | 
				
			|||||||
 | 
					use sqlx::{PgPool, Postgres, Transaction};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::errors::ApiError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::models::ApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn get_api_keys(pool: &PgPool) -> Result<Vec<ApiKey>, ApiError> {
 | 
				
			||||||
 | 
					    Ok(sqlx::query_as!(
 | 
				
			||||||
 | 
					        ApiKey,
 | 
				
			||||||
 | 
					        r#"SELECT
 | 
				
			||||||
 | 
					            APIKEYS."KeyID" as id,
 | 
				
			||||||
 | 
					            APIKEYS."Name" as name,
 | 
				
			||||||
 | 
					            APIKEYS."Hash" as hash,
 | 
				
			||||||
 | 
					            APIKEYS."UserAuthRequired" as auth_required,
 | 
				
			||||||
 | 
					            APIKEYS."CreationDate" as "creation_date?",
 | 
				
			||||||
 | 
					            APIKEYS."LastChanged" as "last_change?",
 | 
				
			||||||
 | 
					            NULL as api_config_secret,
 | 
				
			||||||
 | 
					            array_remove(ARRAY_AGG(APIKEY_PERMISSIONS."Permission"), NULL) AS permissions
 | 
				
			||||||
 | 
					        FROM
 | 
				
			||||||
 | 
					            apikeys
 | 
				
			||||||
 | 
					        LEFT JOIN APIKEY_PERMISSIONS ON APIKEY_PERMISSIONS."KeyID" = APIKEYS."KeyID"
 | 
				
			||||||
 | 
					        GROUP BY APIKEYS."KeyID""#
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool)
 | 
				
			||||||
 | 
					    .await?)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn create_api_key(pool: &PgPool, api_key: &ApiKey) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    // start transaction
 | 
				
			||||||
 | 
					    let mut transaction = pool.begin().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // create api_key
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"INSERT INTO apikeys
 | 
				
			||||||
 | 
					        ("KeyID", "Name", "UserAuthRequired", "Hash") 
 | 
				
			||||||
 | 
					        VALUES ($1, $2, $3, $4)"#,
 | 
				
			||||||
 | 
					        api_key.id,
 | 
				
			||||||
 | 
					        api_key.name,
 | 
				
			||||||
 | 
					        api_key.auth_required,
 | 
				
			||||||
 | 
					        api_key.hash,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut *transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update permissions
 | 
				
			||||||
 | 
					    update_permissions(&mut transaction, &api_key).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // commit transaction
 | 
				
			||||||
 | 
					    transaction.commit().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn update_api_key(pool: &PgPool, api_key: &ApiKey) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    // start transaction
 | 
				
			||||||
 | 
					    let mut transaction = pool.begin().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // create api_key
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"UPDATE apikeys SET
 | 
				
			||||||
 | 
					            "Name" = $2,
 | 
				
			||||||
 | 
					            "UserAuthRequired" = $3
 | 
				
			||||||
 | 
					        WHERE "KeyID" = $1"#,
 | 
				
			||||||
 | 
					        api_key.id,
 | 
				
			||||||
 | 
					        api_key.name,
 | 
				
			||||||
 | 
					        api_key.auth_required,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut *transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update permissions
 | 
				
			||||||
 | 
					    update_permissions(&mut transaction, &api_key).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // commit transaction
 | 
				
			||||||
 | 
					    transaction.commit().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn update_permissions(
 | 
				
			||||||
 | 
					    transaction: &mut Transaction<'static, Postgres>,
 | 
				
			||||||
 | 
					    api_key: &ApiKey,
 | 
				
			||||||
 | 
					) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    // delete all permissions for API Key
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"DELETE FROM apikey_permissions
 | 
				
			||||||
 | 
					        WHERE "KeyID" = $1"#,
 | 
				
			||||||
 | 
					        api_key.id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut **transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // insert permissions
 | 
				
			||||||
 | 
					    for permission in &api_key.permissions.0 {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            r#"INSERT INTO apikey_permissions
 | 
				
			||||||
 | 
					            ("KeyID", "Permission") 
 | 
				
			||||||
 | 
					            VALUES ($1, $2)"#,
 | 
				
			||||||
 | 
					            api_key.id,
 | 
				
			||||||
 | 
					            permission.to_string()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(&mut **transaction)
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn delete_api_key(pool: &PgPool, key_id: &str) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    // start transaction
 | 
				
			||||||
 | 
					    let mut transaction = pool.begin().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Delete permissions
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"DELETE FROM apikey_permissions
 | 
				
			||||||
 | 
					        WHERE "KeyID" = $1"#,
 | 
				
			||||||
 | 
					        key_id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut *transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Delete API Key
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"DELETE FROM apikeys
 | 
				
			||||||
 | 
					        WHERE "KeyID" = $1"#,
 | 
				
			||||||
 | 
					        key_id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut *transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // commit transaction
 | 
				
			||||||
 | 
					    transaction.commit().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										62
									
								
								src/api/routes/auth/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										62
									
								
								src/api/routes/auth/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,62 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// expose the OpenAPI to parent module
 | 
				
			||||||
 | 
					pub fn router() -> OpenApiRouter {
 | 
				
			||||||
 | 
					    OpenApiRouter::new()
 | 
				
			||||||
 | 
					        .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(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/api/routes/auth/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/api/routes/auth/models.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					use utoipa::ToSchema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Deserialize, ToSchema)]
 | 
				
			||||||
 | 
					pub struct Credentials {
 | 
				
			||||||
 | 
					    pub id: String,
 | 
				
			||||||
 | 
					    pub password: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										64
									
								
								src/api/routes/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/api/routes/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,64 @@
 | 
				
			|||||||
 | 
					pub mod api_keys;
 | 
				
			||||||
 | 
					pub mod auth;
 | 
				
			||||||
 | 
					pub mod users;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use api_keys::models::ApiKey as APIKey;
 | 
				
			||||||
 | 
					use axum::{Extension, Router};
 | 
				
			||||||
 | 
					use axum_jwt_login::AuthSessionWithApiKey;
 | 
				
			||||||
 | 
					use users::models::User;
 | 
				
			||||||
 | 
					use utoipa::OpenApi;
 | 
				
			||||||
 | 
					use utoipa_axum::router::OpenApiRouter;
 | 
				
			||||||
 | 
					use utoipa_redoc::{Redoc, Servable};
 | 
				
			||||||
 | 
					use utoipa_scalar::{Scalar, Servable as ScalarServable};
 | 
				
			||||||
 | 
					use utoipa_swagger_ui::SwaggerUi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{backend::ApiBackend, description::ApiDocumentation};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const API_BASE: &str = "/api";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type AuthBackendType = AuthSessionWithApiKey<User, ApiBackend, APIKey>;
 | 
				
			||||||
 | 
					#[macro_export]
 | 
				
			||||||
 | 
					macro_rules! login_required {
 | 
				
			||||||
 | 
					    () => {
 | 
				
			||||||
 | 
					        axum_jwt_login::login_required!(User, ApiBackend, APIKey)
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					#[macro_export]
 | 
				
			||||||
 | 
					macro_rules! permission_required {
 | 
				
			||||||
 | 
					    ($($perm:expr),+ $(,)?) => {
 | 
				
			||||||
 | 
					        axum_jwt_login::permission_required!(
 | 
				
			||||||
 | 
					            User,
 | 
				
			||||||
 | 
					            ApiBackend,
 | 
				
			||||||
 | 
					            crate::api::routes::APIKey,
 | 
				
			||||||
 | 
					            $($perm),+
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn create_routes(session: AuthBackendType) -> Router {
 | 
				
			||||||
 | 
					    let backend = session.backend();
 | 
				
			||||||
 | 
					    let (router, api) = OpenApiRouter::with_openapi(ApiDocumentation::openapi())
 | 
				
			||||||
 | 
					        // .routes(routes!(health))
 | 
				
			||||||
 | 
					        .nest(API_BASE, auth::router())
 | 
				
			||||||
 | 
					        .nest(API_BASE, users::router())
 | 
				
			||||||
 | 
					        .nest(API_BASE, api_keys::router())
 | 
				
			||||||
 | 
					        //         .nest(
 | 
				
			||||||
 | 
					        //             "/api/order",
 | 
				
			||||||
 | 
					        //             // order::router().route_layer(crate::login_required!(AuthenticationBackend<ApiKey>)),
 | 
				
			||||||
 | 
					        //             order::router().route_layer(login_required!(User, auth_backend::AuthenticationBackend)),
 | 
				
			||||||
 | 
					        //         )
 | 
				
			||||||
 | 
					        //         .routes(routes!(
 | 
				
			||||||
 | 
					        //             inner::secret_handlers::get_secret,
 | 
				
			||||||
 | 
					        //             inner::secret_handlers::post_secret
 | 
				
			||||||
 | 
					        //         ))
 | 
				
			||||||
 | 
					        //         .nest("/api", users::router())
 | 
				
			||||||
 | 
					        //         // .layer(auth_layer)
 | 
				
			||||||
 | 
					        .layer(session.into_layer())
 | 
				
			||||||
 | 
					        .layer(Extension(backend))
 | 
				
			||||||
 | 
					        .split_for_parts();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    router
 | 
				
			||||||
 | 
					        .merge(SwaggerUi::new("/swagger-ui").url("/apidoc/openapi.json", api.clone()))
 | 
				
			||||||
 | 
					        .merge(Redoc::with_url("/redoc", api.clone()))
 | 
				
			||||||
 | 
					        .merge(Scalar::with_url("/scalar", api))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										159
									
								
								src/api/routes/users/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/api/routes/users/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,159 @@
 | 
				
			|||||||
 | 
					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,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod models;
 | 
				
			||||||
 | 
					pub mod permissions;
 | 
				
			||||||
 | 
					pub mod sql;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// expose the OpenAPI to parent module
 | 
				
			||||||
 | 
					pub fn router() -> OpenApiRouter {
 | 
				
			||||||
 | 
					    let read = OpenApiRouter::new()
 | 
				
			||||||
 | 
					        .routes(routes!(get_users))
 | 
				
			||||||
 | 
					        .route_layer(permission_required!(Permission::Read(
 | 
				
			||||||
 | 
					            PermissionDetail::Users
 | 
				
			||||||
 | 
					        )));
 | 
				
			||||||
 | 
					    let write = OpenApiRouter::new()
 | 
				
			||||||
 | 
					        .routes(routes!(create_user, update_user))
 | 
				
			||||||
 | 
					        .routes(routes!(get_ad_users))
 | 
				
			||||||
 | 
					        .route_layer(permission_required!(Permission::Write(
 | 
				
			||||||
 | 
					            PermissionDetail::Users
 | 
				
			||||||
 | 
					        )));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										118
									
								
								src/api/routes/users/models.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								src/api/routes/users/models.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,118 @@
 | 
				
			|||||||
 | 
					use std::collections::{HashMap, HashSet};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum_jwt_login::UserPermissions;
 | 
				
			||||||
 | 
					use chrono::NaiveDateTime;
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use sqlx::PgPool;
 | 
				
			||||||
 | 
					use ts_rs::TS;
 | 
				
			||||||
 | 
					use utoipa::ToSchema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::errors::ApiError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{
 | 
				
			||||||
 | 
					    permissions::{Permission, PermissionContainer},
 | 
				
			||||||
 | 
					    sql,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy, Serialize, Deserialize, sqlx::Type, TS, ToSchema)]
 | 
				
			||||||
 | 
					#[repr(i16)]
 | 
				
			||||||
 | 
					pub enum UserStatus {
 | 
				
			||||||
 | 
					    Deleted = -1,
 | 
				
			||||||
 | 
					    Deactivated = 0,
 | 
				
			||||||
 | 
					    Active = 1,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
 | 
				
			||||||
 | 
					pub struct GroupContainer(pub HashSet<i32>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<Option<Vec<i32>>> for GroupContainer {
 | 
				
			||||||
 | 
					    fn from(value: Option<Vec<i32>>) -> Self {
 | 
				
			||||||
 | 
					        let set = match value {
 | 
				
			||||||
 | 
					            Some(values) => HashSet::from_iter(values),
 | 
				
			||||||
 | 
					            None => HashSet::new(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self(set)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
 | 
				
			||||||
 | 
					#[ts(export)]
 | 
				
			||||||
 | 
					pub struct User {
 | 
				
			||||||
 | 
					    pub user_id: String,
 | 
				
			||||||
 | 
					    pub active_directory_auth: bool,
 | 
				
			||||||
 | 
					    #[serde(skip)] // do not leak password hashes
 | 
				
			||||||
 | 
					    pub password: String,
 | 
				
			||||||
 | 
					    pub name: String,
 | 
				
			||||||
 | 
					    pub surname: String,
 | 
				
			||||||
 | 
					    pub email: String,
 | 
				
			||||||
 | 
					    #[schema(inline)]
 | 
				
			||||||
 | 
					    pub groups: GroupContainer,
 | 
				
			||||||
 | 
					    #[schema(inline)]
 | 
				
			||||||
 | 
					    pub group_permissions: PermissionContainer,
 | 
				
			||||||
 | 
					    #[schema(inline)]
 | 
				
			||||||
 | 
					    pub permissions: PermissionContainer,
 | 
				
			||||||
 | 
					    pub status_flag: UserStatus,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    #[schema(value_type = String, read_only)]
 | 
				
			||||||
 | 
					    pub creation_date: NaiveDateTime,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    #[schema(value_type = String, read_only)]
 | 
				
			||||||
 | 
					    pub last_change: NaiveDateTime,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl UserPermissions for User {
 | 
				
			||||||
 | 
					    type Error = ApiError;
 | 
				
			||||||
 | 
					    type Permission = Permission;
 | 
				
			||||||
 | 
					    type Id = String;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn id(&self) -> Self::Id {
 | 
				
			||||||
 | 
					        self.user_id.clone()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn get_user_permissions(&self) -> Result<HashSet<Self::Permission>, Self::Error> {
 | 
				
			||||||
 | 
					        Ok(self.permissions.0.iter().cloned().collect())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    fn get_group_permissions(&self) -> Result<HashSet<Self::Permission>, Self::Error> {
 | 
				
			||||||
 | 
					        Ok(self.group_permissions.0.iter().cloned().collect())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl User {
 | 
				
			||||||
 | 
					    pub async fn update_with_ad_details(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        pool: &PgPool,
 | 
				
			||||||
 | 
					        details: HashMap<String, Vec<String>>,
 | 
				
			||||||
 | 
					    ) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					        let create_error = |key: &str| {
 | 
				
			||||||
 | 
					            ApiError::InternalError(format!(
 | 
				
			||||||
 | 
					                "Update User Information: Information '{key}' is not present"
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // get field values
 | 
				
			||||||
 | 
					        let name = details
 | 
				
			||||||
 | 
					            .get("givenName")
 | 
				
			||||||
 | 
					            .ok_or(create_error("Name"))?
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					            .ok_or(create_error("Name"))?;
 | 
				
			||||||
 | 
					        let surname = details
 | 
				
			||||||
 | 
					            .get("sn")
 | 
				
			||||||
 | 
					            .ok_or(create_error("Surname"))?
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					            .ok_or(create_error("Surname"))?;
 | 
				
			||||||
 | 
					        let email = details
 | 
				
			||||||
 | 
					            .get("mail")
 | 
				
			||||||
 | 
					            .ok_or(create_error("Email"))?
 | 
				
			||||||
 | 
					            .first()
 | 
				
			||||||
 | 
					            .ok_or(create_error("Email"))?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if name != &self.name || surname != &self.surname || email != &self.email {
 | 
				
			||||||
 | 
					            let mut transaction = pool.begin().await?;
 | 
				
			||||||
 | 
					            sql::update_user_details(&mut transaction, &self.id(), name, surname, email).await?;
 | 
				
			||||||
 | 
					            transaction.commit().await?;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										102
									
								
								src/api/routes/users/permissions.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										102
									
								
								src/api/routes/users/permissions.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,102 @@
 | 
				
			|||||||
 | 
					use std::{collections::HashSet, convert::Infallible, fmt::Display, str::FromStr};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use strum::EnumString;
 | 
				
			||||||
 | 
					use ts_rs::TS;
 | 
				
			||||||
 | 
					use utoipa::ToSchema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, TS, ToSchema)]
 | 
				
			||||||
 | 
					pub enum Permission {
 | 
				
			||||||
 | 
					    Read(PermissionDetail),
 | 
				
			||||||
 | 
					    Write(PermissionDetail),
 | 
				
			||||||
 | 
					    None,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl FromStr for Permission {
 | 
				
			||||||
 | 
					    type Err = Infallible;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn from_str(s: &str) -> Result<Self, Self::Err> {
 | 
				
			||||||
 | 
					        if let Some((permission, detail)) = s.split_once(':') {
 | 
				
			||||||
 | 
					            match permission {
 | 
				
			||||||
 | 
					                "Read" => Ok(Self::Read(
 | 
				
			||||||
 | 
					                    PermissionDetail::from_str(detail).unwrap_or_default(),
 | 
				
			||||||
 | 
					                )),
 | 
				
			||||||
 | 
					                "Write" => Ok(Self::Write(
 | 
				
			||||||
 | 
					                    PermissionDetail::from_str(detail).unwrap_or_default(),
 | 
				
			||||||
 | 
					                )),
 | 
				
			||||||
 | 
					                _ => Ok(Self::None),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Ok(Self::None)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Display for Permission {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        let string = match self {
 | 
				
			||||||
 | 
					            Permission::Read(detail) => format!("Read:{detail}"),
 | 
				
			||||||
 | 
					            Permission::Write(detail) => format!("Write:{detail}"),
 | 
				
			||||||
 | 
					            Permission::None => "None".to_string(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        f.write_str(&string)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(
 | 
				
			||||||
 | 
					    Debug,
 | 
				
			||||||
 | 
					    Default,
 | 
				
			||||||
 | 
					    Clone,
 | 
				
			||||||
 | 
					    Copy,
 | 
				
			||||||
 | 
					    Serialize,
 | 
				
			||||||
 | 
					    Deserialize,
 | 
				
			||||||
 | 
					    PartialEq,
 | 
				
			||||||
 | 
					    Eq,
 | 
				
			||||||
 | 
					    Hash,
 | 
				
			||||||
 | 
					    EnumString,
 | 
				
			||||||
 | 
					    strum::Display,
 | 
				
			||||||
 | 
					    TS,
 | 
				
			||||||
 | 
					    ToSchema,
 | 
				
			||||||
 | 
					)]
 | 
				
			||||||
 | 
					pub enum PermissionDetail {
 | 
				
			||||||
 | 
					    Users,
 | 
				
			||||||
 | 
					    APIKeys,
 | 
				
			||||||
 | 
					    #[default]
 | 
				
			||||||
 | 
					    None,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Serialize, Deserialize, TS, ToSchema)]
 | 
				
			||||||
 | 
					pub struct PermissionContainer(pub HashSet<Permission>);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<Option<Vec<String>>> for PermissionContainer {
 | 
				
			||||||
 | 
					    fn from(value: Option<Vec<String>>) -> Self {
 | 
				
			||||||
 | 
					        let set = match value {
 | 
				
			||||||
 | 
					            Some(values) => HashSet::from_iter(
 | 
				
			||||||
 | 
					                values
 | 
				
			||||||
 | 
					                    .iter()
 | 
				
			||||||
 | 
					                    .map(|s| Permission::from_str(s).unwrap_or(Permission::None)),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            None => HashSet::new(),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Self(set)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)]
 | 
				
			||||||
 | 
					mod test {
 | 
				
			||||||
 | 
					    use std::str::FromStr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use crate::api::routes::users::permissions::PermissionDetail;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use super::Permission;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[tokio::test]
 | 
				
			||||||
 | 
					    async fn test_permissions() {
 | 
				
			||||||
 | 
					        let permission = Permission::Read(PermissionDetail::Users);
 | 
				
			||||||
 | 
					        assert_eq!("Read:Users", permission.to_string());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let parsed = Permission::from_str("Write:Users").unwrap();
 | 
				
			||||||
 | 
					        assert_eq!(parsed, Permission::Write(PermissionDetail::Users));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										184
									
								
								src/api/routes/users/sql.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										184
									
								
								src/api/routes/users/sql.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,184 @@
 | 
				
			|||||||
 | 
					use sqlx::{PgPool, Postgres, Transaction};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::errors::ApiError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::models::{User, UserStatus};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn get_users(
 | 
				
			||||||
 | 
					    pool: &PgPool,
 | 
				
			||||||
 | 
					    filter_status: Option<UserStatus>,
 | 
				
			||||||
 | 
					    filter_id: Option<String>,
 | 
				
			||||||
 | 
					) -> Result<Vec<User>, ApiError> {
 | 
				
			||||||
 | 
					    Ok(sqlx::query_as!(
 | 
				
			||||||
 | 
					        User,
 | 
				
			||||||
 | 
					        r#"SELECT
 | 
				
			||||||
 | 
					            USERS."UserID" as user_id,
 | 
				
			||||||
 | 
					            USERS."ActiveDirectoryAuth" as active_directory_auth,
 | 
				
			||||||
 | 
					            USERS."Password" as password,
 | 
				
			||||||
 | 
					            USERS."Name" as name,
 | 
				
			||||||
 | 
					            USERS."Surname" as surname,
 | 
				
			||||||
 | 
					            USERS."Email" as email,
 | 
				
			||||||
 | 
					            USERS."StatusFlag" as "status_flag: UserStatus",
 | 
				
			||||||
 | 
					            USERS."CreationDate" as "creation_date",
 | 
				
			||||||
 | 
					            USERS."LastChanged" as "last_change",
 | 
				
			||||||
 | 
					            array_remove(ARRAY_AGG(USERS_GROUPS."GroupID"), NULL) AS groups,
 | 
				
			||||||
 | 
					            array_remove(ARRAY_AGG(USER_PERMISSIONS."Permission"), NULL) AS permissions,
 | 
				
			||||||
 | 
					            array_remove(ARRAY_AGG(GROUP_PERMISSIONS."Permission"), NULL) AS group_permissions
 | 
				
			||||||
 | 
					        FROM
 | 
				
			||||||
 | 
					            users
 | 
				
			||||||
 | 
					        LEFT JOIN PUBLIC.USER_PERMISSIONS ON USER_PERMISSIONS."UserID" = USERS."UserID"
 | 
				
			||||||
 | 
					        LEFT JOIN USERS_GROUPS ON USERS."UserID" = USERS_GROUPS."UserID"
 | 
				
			||||||
 | 
					        LEFT JOIN GROUP_PERMISSIONS ON GROUP_PERMISSIONS."GroupID" = USERS_GROUPS."GroupID"
 | 
				
			||||||
 | 
					        WHERE
 | 
				
			||||||
 | 
					            ($1::smallint IS NULL OR USERS."StatusFlag" = $1)
 | 
				
			||||||
 | 
					            AND ($2::varchar IS NULL OR USERS."UserID" = $2)
 | 
				
			||||||
 | 
					        GROUP BY USERS."UserID""#,
 | 
				
			||||||
 | 
					        filter_status.map(|s| s as i16),
 | 
				
			||||||
 | 
					        filter_id
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .fetch_all(pool)
 | 
				
			||||||
 | 
					    .await?)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn update_user(pool: &PgPool, user: &User) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    let mut transaction = pool.begin().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update general stuff
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"UPDATE users SET
 | 
				
			||||||
 | 
					            "ActiveDirectoryAuth" = $2,
 | 
				
			||||||
 | 
					            "StatusFlag" = $3,
 | 
				
			||||||
 | 
					            "LastChanged" = NOW()
 | 
				
			||||||
 | 
					        WHERE "UserID" = $1"#,
 | 
				
			||||||
 | 
					        user.user_id,
 | 
				
			||||||
 | 
					        user.active_directory_auth,
 | 
				
			||||||
 | 
					        user.status_flag as i16,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut *transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // update user information
 | 
				
			||||||
 | 
					    update_user_details(
 | 
				
			||||||
 | 
					        &mut transaction,
 | 
				
			||||||
 | 
					        &user.user_id,
 | 
				
			||||||
 | 
					        &user.name,
 | 
				
			||||||
 | 
					        &user.surname,
 | 
				
			||||||
 | 
					        &user.email,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					    // update permissions
 | 
				
			||||||
 | 
					    update_user_permissions(&mut transaction, &user).await?;
 | 
				
			||||||
 | 
					    // update groups
 | 
				
			||||||
 | 
					    update_user_groups(&mut transaction, user).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // commit transaction
 | 
				
			||||||
 | 
					    transaction.commit().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn update_user_details<'a>(
 | 
				
			||||||
 | 
					    pool: &'a mut Transaction<'static, Postgres>,
 | 
				
			||||||
 | 
					    user_id: &String,
 | 
				
			||||||
 | 
					    name: &String,
 | 
				
			||||||
 | 
					    surname: &String,
 | 
				
			||||||
 | 
					    email: &String,
 | 
				
			||||||
 | 
					) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    // let test = **pool;
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"UPDATE users SET
 | 
				
			||||||
 | 
					            "Name" = $2,
 | 
				
			||||||
 | 
					            "Surname" = $3,
 | 
				
			||||||
 | 
					            "Email" = $4
 | 
				
			||||||
 | 
					        WHERE "UserID" = $1"#,
 | 
				
			||||||
 | 
					        user_id,
 | 
				
			||||||
 | 
					        name,
 | 
				
			||||||
 | 
					        surname,
 | 
				
			||||||
 | 
					        email
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut **pool)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn update_user_permissions<'a>(
 | 
				
			||||||
 | 
					    pool: &'a mut Transaction<'static, Postgres>,
 | 
				
			||||||
 | 
					    user: &User,
 | 
				
			||||||
 | 
					) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    // delete all permissions for user
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"DELETE FROM user_permissions
 | 
				
			||||||
 | 
					        WHERE "UserID" = $1"#,
 | 
				
			||||||
 | 
					        user.user_id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut **pool)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // insert permissions
 | 
				
			||||||
 | 
					    for permission in &user.permissions.0 {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            r#"INSERT INTO user_permissions
 | 
				
			||||||
 | 
					            ("UserID", "Permission") 
 | 
				
			||||||
 | 
					            VALUES ($1, $2)"#,
 | 
				
			||||||
 | 
					            user.user_id,
 | 
				
			||||||
 | 
					            permission.to_string()
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(&mut **pool)
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn update_user_groups<'a>(
 | 
				
			||||||
 | 
					    pool: &'a mut Transaction<'static, Postgres>,
 | 
				
			||||||
 | 
					    user: &User,
 | 
				
			||||||
 | 
					) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    // delete all permissions for user
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"DELETE FROM users_groups
 | 
				
			||||||
 | 
					        WHERE "UserID" = $1"#,
 | 
				
			||||||
 | 
					        user.user_id,
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut **pool)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // insert permissions
 | 
				
			||||||
 | 
					    for group_id in &user.groups.0 {
 | 
				
			||||||
 | 
					        sqlx::query!(
 | 
				
			||||||
 | 
					            r#"INSERT INTO users_groups
 | 
				
			||||||
 | 
					            ("UserID", "GroupID") 
 | 
				
			||||||
 | 
					            VALUES ($1, $2)"#,
 | 
				
			||||||
 | 
					            user.user_id,
 | 
				
			||||||
 | 
					            group_id
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .execute(&mut **pool)
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn create_new_user(
 | 
				
			||||||
 | 
					    pool: &PgPool,
 | 
				
			||||||
 | 
					    user: &User,
 | 
				
			||||||
 | 
					    password: Option<String>,
 | 
				
			||||||
 | 
					) -> Result<(), ApiError> {
 | 
				
			||||||
 | 
					    let mut transaction = pool.begin().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sqlx::query!(
 | 
				
			||||||
 | 
					        r#"INSERT INTO users
 | 
				
			||||||
 | 
					        ("UserID", "Password") 
 | 
				
			||||||
 | 
					        VALUES ($1, $2)"#,
 | 
				
			||||||
 | 
					        user.user_id,
 | 
				
			||||||
 | 
					        password
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    .execute(&mut *transaction)
 | 
				
			||||||
 | 
					    .await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // commit transaction
 | 
				
			||||||
 | 
					    transaction.commit().await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										69
									
								
								src/authentication/api_key.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								src/authentication/api_key.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					use rand::{distributions::Alphanumeric, Rng};
 | 
				
			||||||
 | 
					use sha2::{Digest, Sha512};
 | 
				
			||||||
 | 
					use uuid::Uuid;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) const KEY_LENGTH: usize = 40;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub trait ApiKeyInfo {
 | 
				
			||||||
 | 
					    fn id(&self) -> String;
 | 
				
			||||||
 | 
					    fn validate(&self, hash: &str) -> bool;
 | 
				
			||||||
 | 
					    fn requires_auth(&self) -> bool;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ApiKeyInfo for ApiKey {
 | 
				
			||||||
 | 
					    fn id(&self) -> String {
 | 
				
			||||||
 | 
					        self.id.to_string()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn validate(&self, hash: &str) -> bool {
 | 
				
			||||||
 | 
					        let mut sha512 = Sha512::new();
 | 
				
			||||||
 | 
					        sha512.update(hash);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        format!("{:X}", sha512.finalize()) == self.hash
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn requires_auth(&self) -> bool {
 | 
				
			||||||
 | 
					        self.auth_required
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Hash, PartialEq, Eq)]
 | 
				
			||||||
 | 
					pub(crate) struct ApiKey {
 | 
				
			||||||
 | 
					    pub(crate) id: String,
 | 
				
			||||||
 | 
					    pub(crate) key: Option<String>,
 | 
				
			||||||
 | 
					    pub(crate) hash: String,
 | 
				
			||||||
 | 
					    pub(crate) name: String,
 | 
				
			||||||
 | 
					    pub(crate) auth_required: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ApiKey {
 | 
				
			||||||
 | 
					    pub(crate) fn create(name: &str, requires_auth: bool) -> Self {
 | 
				
			||||||
 | 
					        let uuid = Uuid::new_v4().simple();
 | 
				
			||||||
 | 
					        let key: String = create_random(KEY_LENGTH);
 | 
				
			||||||
 | 
					        let mut sha512 = Sha512::new();
 | 
				
			||||||
 | 
					        sha512.update(key.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ApiKey {
 | 
				
			||||||
 | 
					            id: uuid.to_string(),
 | 
				
			||||||
 | 
					            key: Some(key.to_owned()),
 | 
				
			||||||
 | 
					            hash: format!("{:X}", sha512.finalize()),
 | 
				
			||||||
 | 
					            auth_required: requires_auth,
 | 
				
			||||||
 | 
					            name: name.to_string(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub(crate) fn validate(&self, password: &str) -> bool {
 | 
				
			||||||
 | 
					        let mut sha512 = Sha512::new();
 | 
				
			||||||
 | 
					        sha512.update(password);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        format!("{:X}", sha512.finalize()) == self.hash
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub(crate) fn create_random(len: usize) -> String {
 | 
				
			||||||
 | 
					    rand::thread_rng()
 | 
				
			||||||
 | 
					        .sample_iter(&Alphanumeric)
 | 
				
			||||||
 | 
					        .take(len)
 | 
				
			||||||
 | 
					        .map(char::from)
 | 
				
			||||||
 | 
					        .collect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										27
									
								
								src/authentication/extract.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/authentication/extract.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,27 @@
 | 
				
			|||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    async_trait,
 | 
				
			||||||
 | 
					    extract::FromRequestParts,
 | 
				
			||||||
 | 
					    http::{request::Parts, StatusCode},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::AuthenticationBackend;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[async_trait]
 | 
				
			||||||
 | 
					impl<S, T> FromRequestParts<S> for AuthenticationBackend<T>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    S: Send + Sync,
 | 
				
			||||||
 | 
					    T: Send + Sync + Clone + 'static,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    type Rejection = (StatusCode, &'static str);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
 | 
				
			||||||
 | 
					        parts
 | 
				
			||||||
 | 
					            .extensions
 | 
				
			||||||
 | 
					            .get::<AuthenticationBackend<T>>()
 | 
				
			||||||
 | 
					            .cloned()
 | 
				
			||||||
 | 
					            .ok_or((
 | 
				
			||||||
 | 
					                StatusCode::INTERNAL_SERVER_ERROR,
 | 
				
			||||||
 | 
					                "Can't extract auth session. Is `AuthenticationLayer` enabled?",
 | 
				
			||||||
 | 
					            ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										112
									
								
								src/authentication/jwt.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/authentication/jwt.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					use std::time::Duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use chrono::{DateTime, TimeZone, Utc};
 | 
				
			||||||
 | 
					use jsonwebtoken::{decode, Algorithm, DecodingKey, EncodingKey, Header, Validation};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::user::User;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Session length in seconds -> 12 hours
 | 
				
			||||||
 | 
					pub(crate) const DEFAULT_SESSION_LENGTH: std::time::Duration = Duration::from_secs(12 * 3600);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone)]
 | 
				
			||||||
 | 
					pub struct JwtAuthorizer {
 | 
				
			||||||
 | 
					    encoding_key: EncodingKey,
 | 
				
			||||||
 | 
					    decoding_key: DecodingKey,
 | 
				
			||||||
 | 
					    algorithm: Algorithm,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Here we've implemented `Debug` manually to avoid accidentally logging the keys.
 | 
				
			||||||
 | 
					impl std::fmt::Debug for JwtAuthorizer {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        f.debug_struct("JwtAuthorizer")
 | 
				
			||||||
 | 
					            .field("encoding_key", &"[redacted]")
 | 
				
			||||||
 | 
					            .field("decoding_key", &"[redacted]")
 | 
				
			||||||
 | 
					            .field("algorithm", &self.algorithm)
 | 
				
			||||||
 | 
					            .finish()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl JwtAuthorizer {
 | 
				
			||||||
 | 
					    pub fn new(encoding_key: EncodingKey, decoding_key: DecodingKey, algorithm: Algorithm) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            encoding_key,
 | 
				
			||||||
 | 
					            decoding_key,
 | 
				
			||||||
 | 
					            algorithm,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn encode(&self, user: User) -> Result<String, jsonwebtoken::errors::Error> {
 | 
				
			||||||
 | 
					        let claims = RegisteredClaims::new(user);
 | 
				
			||||||
 | 
					        jsonwebtoken::encode(&Header::default(), &claims, &self.encoding_key)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn decode(&self, token: &str) -> Result<User, jsonwebtoken::errors::Error> {
 | 
				
			||||||
 | 
					        decode::<RegisteredClaims<User>>(
 | 
				
			||||||
 | 
					            &token,
 | 
				
			||||||
 | 
					            &self.decoding_key,
 | 
				
			||||||
 | 
					            &Validation::new(self.algorithm),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .map(|data| data.claims.data)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Claims mentioned in the JWT specifications.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// <https://www.rfc-editor.org/rfc/rfc7519#section-4.1>
 | 
				
			||||||
 | 
					#[derive(Deserialize, Serialize, Clone, Debug)]
 | 
				
			||||||
 | 
					pub struct RegisteredClaims<T> {
 | 
				
			||||||
 | 
					    #[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			||||||
 | 
					    pub iss: Option<String>,
 | 
				
			||||||
 | 
					    #[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			||||||
 | 
					    pub sub: Option<String>,
 | 
				
			||||||
 | 
					    #[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			||||||
 | 
					    pub exp: Option<NumericDate>,
 | 
				
			||||||
 | 
					    #[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			||||||
 | 
					    pub nbf: Option<NumericDate>,
 | 
				
			||||||
 | 
					    #[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			||||||
 | 
					    pub iat: Option<NumericDate>,
 | 
				
			||||||
 | 
					    #[serde(skip_serializing_if = "Option::is_none")]
 | 
				
			||||||
 | 
					    pub jti: Option<String>,
 | 
				
			||||||
 | 
					    pub data: T,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T> RegisteredClaims<T> {
 | 
				
			||||||
 | 
					    pub fn new(data: T) -> Self {
 | 
				
			||||||
 | 
					        let now = Utc::now();
 | 
				
			||||||
 | 
					        let exp = now + DEFAULT_SESSION_LENGTH;
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            iss: Some(env!("CARGO_PKG_NAME").to_string()),
 | 
				
			||||||
 | 
					            sub: Some("User token".to_string()),
 | 
				
			||||||
 | 
					            exp: Some(exp.into()),
 | 
				
			||||||
 | 
					            nbf: Some(now.into()),
 | 
				
			||||||
 | 
					            iat: Some(now.into()),
 | 
				
			||||||
 | 
					            jti: None,
 | 
				
			||||||
 | 
					            data,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// The number of seconds from 1970-01-01T00:00:00Z UTC until the specified UTC date/time ignoring leap seconds.
 | 
				
			||||||
 | 
					/// (<https://www.rfc-editor.org/rfc/rfc7519#section-2>)
 | 
				
			||||||
 | 
					#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, Debug)]
 | 
				
			||||||
 | 
					pub struct NumericDate(pub i64);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// accesses the underlying value
 | 
				
			||||||
 | 
					impl From<NumericDate> for i64 {
 | 
				
			||||||
 | 
					    fn from(t: NumericDate) -> Self {
 | 
				
			||||||
 | 
					        t.0
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<DateTime<Utc>> for NumericDate {
 | 
				
			||||||
 | 
					    fn from(t: DateTime<Utc>) -> Self {
 | 
				
			||||||
 | 
					        Self(t.timestamp())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<NumericDate> for DateTime<Utc> {
 | 
				
			||||||
 | 
					    fn from(t: NumericDate) -> Self {
 | 
				
			||||||
 | 
					        Utc.timestamp_opt(t.0, 0).unwrap()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										191
									
								
								src/authentication/layer.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/authentication/layer.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,191 @@
 | 
				
			|||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    future::Future,
 | 
				
			||||||
 | 
					    pin::Pin,
 | 
				
			||||||
 | 
					    task::{Context, Poll},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    extract::Request,
 | 
				
			||||||
 | 
					    http::{self, header::AUTHORIZATION, HeaderMap, HeaderName, Response},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use tower_layer::Layer;
 | 
				
			||||||
 | 
					use tower_service::Service;
 | 
				
			||||||
 | 
					use tracing::Instrument;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{api_key::ApiKeyInfo, AuthenticationBackend, AuthenticationData, UserAuthData};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct AuthenticationLayer<T> {
 | 
				
			||||||
 | 
					    backend: AuthenticationBackend<T>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T> AuthenticationLayer<T> {
 | 
				
			||||||
 | 
					    pub fn new(backend: AuthenticationBackend<T>) -> Self {
 | 
				
			||||||
 | 
					        Self { backend }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<S, T> Layer<S> for AuthenticationLayer<T>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    T: Clone,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    type Service = AuthenticationService<S, T>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn layer(&self, service: S) -> Self::Service {
 | 
				
			||||||
 | 
					        AuthenticationService {
 | 
				
			||||||
 | 
					            backend: self.backend.clone(),
 | 
				
			||||||
 | 
					            service,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This service implements the Log behavior
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct AuthenticationService<S, T> {
 | 
				
			||||||
 | 
					    backend: AuthenticationBackend<T>,
 | 
				
			||||||
 | 
					    service: S,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<ReqBody, ResBody, S, T> Service<Request<ReqBody>> for AuthenticationService<S, T>
 | 
				
			||||||
 | 
					where
 | 
				
			||||||
 | 
					    S: Service<Request<ReqBody>, Response = Response<ResBody>> + Clone + Send + 'static,
 | 
				
			||||||
 | 
					    S::Future: Send + 'static,
 | 
				
			||||||
 | 
					    ReqBody: Send + 'static,
 | 
				
			||||||
 | 
					    ResBody: Default + Send,
 | 
				
			||||||
 | 
					    T: ApiKeyInfo + Send + Sync + Clone + 'static,
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    type Response = S::Response;
 | 
				
			||||||
 | 
					    type Error = S::Error;
 | 
				
			||||||
 | 
					    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #[inline]
 | 
				
			||||||
 | 
					    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
 | 
				
			||||||
 | 
					        self.service.poll_ready(cx)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
 | 
				
			||||||
 | 
					        let span = tracing::info_span!("call", user.id = tracing::field::Empty);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let mut backend = self.backend.clone();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Because the inner service can panic until ready, we need to ensure we only
 | 
				
			||||||
 | 
					        // use the ready service.
 | 
				
			||||||
 | 
					        //
 | 
				
			||||||
 | 
					        // See: https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services
 | 
				
			||||||
 | 
					        let clone = self.service.clone();
 | 
				
			||||||
 | 
					        let mut inner = std::mem::replace(&mut self.service, clone);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Box::pin(
 | 
				
			||||||
 | 
					            async move {
 | 
				
			||||||
 | 
					                // check for api key if api key requirement is enabled
 | 
				
			||||||
 | 
					                let bearer_required = if let Some(ref header) = backend.api_key_header {
 | 
				
			||||||
 | 
					                    match extract_api_key(req.headers(), header) {
 | 
				
			||||||
 | 
					                        Some((id, hash)) => {
 | 
				
			||||||
 | 
					                            // get api key information
 | 
				
			||||||
 | 
					                            if let Some(key) = backend.session_store.get_api_key(&id).await {
 | 
				
			||||||
 | 
					                                // validate api key
 | 
				
			||||||
 | 
					                                if !key.validate(&hash) {
 | 
				
			||||||
 | 
					                                    tracing::error!("API Key invalid");
 | 
				
			||||||
 | 
					                                    let mut res = Response::default();
 | 
				
			||||||
 | 
					                                    *res.status_mut() = http::StatusCode::UNAUTHORIZED;
 | 
				
			||||||
 | 
					                                    return Ok(res);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                // check if further authentication is required
 | 
				
			||||||
 | 
					                                if key.requires_auth() {
 | 
				
			||||||
 | 
					                                    true
 | 
				
			||||||
 | 
					                                } else {
 | 
				
			||||||
 | 
					                                    // add API Key to authenticated information
 | 
				
			||||||
 | 
					                                    backend.authenticated_user =
 | 
				
			||||||
 | 
					                                        Some(AuthenticationData::ApiKey(key));
 | 
				
			||||||
 | 
					                                    false
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            } else {
 | 
				
			||||||
 | 
					                                let mut res = Response::default();
 | 
				
			||||||
 | 
					                                *res.status_mut() = http::StatusCode::UNAUTHORIZED;
 | 
				
			||||||
 | 
					                                return Ok(res);
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        None => {
 | 
				
			||||||
 | 
					                            let mut res = Response::default();
 | 
				
			||||||
 | 
					                            *res.status_mut() = http::StatusCode::UNAUTHORIZED;
 | 
				
			||||||
 | 
					                            return Ok(res);
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    true
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Authenticate with bearer if required
 | 
				
			||||||
 | 
					                if bearer_required {
 | 
				
			||||||
 | 
					                    // try to extract Authentication Bearer from headers
 | 
				
			||||||
 | 
					                    let bearer = extract_bearer(req.headers());
 | 
				
			||||||
 | 
					                    // check if bearer is present
 | 
				
			||||||
 | 
					                    if let Some(bearer) = bearer {
 | 
				
			||||||
 | 
					                        // check if bearer belongs to active session
 | 
				
			||||||
 | 
					                        if backend.session_store.includes(&bearer).await {
 | 
				
			||||||
 | 
					                            // try to decode bearer
 | 
				
			||||||
 | 
					                            match backend.jwt_authorizer.decode(&bearer) {
 | 
				
			||||||
 | 
					                                Ok(user) => {
 | 
				
			||||||
 | 
					                                    backend.authenticated_user =
 | 
				
			||||||
 | 
					                                        Some(AuthenticationData::User(UserAuthData {
 | 
				
			||||||
 | 
					                                            user,
 | 
				
			||||||
 | 
					                                            token: bearer,
 | 
				
			||||||
 | 
					                                        }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                    // tracing::Span::current().record("user.id", user.id().to_string());
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                                Err(err) => {
 | 
				
			||||||
 | 
					                                    // remove token from session storage
 | 
				
			||||||
 | 
					                                    backend.session_store.remove(&bearer).await;
 | 
				
			||||||
 | 
					                                    // log error
 | 
				
			||||||
 | 
					                                    tracing::error!("Error decoding JWT token: {err}");
 | 
				
			||||||
 | 
					                                    // return internal server error
 | 
				
			||||||
 | 
					                                    let mut res = Response::default();
 | 
				
			||||||
 | 
					                                    *res.status_mut() = http::StatusCode::INTERNAL_SERVER_ERROR;
 | 
				
			||||||
 | 
					                                    return Ok(res);
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                req.extensions_mut().insert(backend);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                inner.call(req).await
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            .instrument(span),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn extract_bearer(headers: &HeaderMap) -> Option<String> {
 | 
				
			||||||
 | 
					    // Check that its a well-formed bearer and return
 | 
				
			||||||
 | 
					    headers
 | 
				
			||||||
 | 
					        .get(AUTHORIZATION)
 | 
				
			||||||
 | 
					        .map(|header| {
 | 
				
			||||||
 | 
					            let split = header.to_str().unwrap_or("").split_once(' ');
 | 
				
			||||||
 | 
					            let bearer = match split {
 | 
				
			||||||
 | 
					                // Found proper bearer
 | 
				
			||||||
 | 
					                Some((name, contents)) if name == "Bearer" => Some(contents.to_string()),
 | 
				
			||||||
 | 
					                // Found nothing
 | 
				
			||||||
 | 
					                _ => None,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            bearer
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn extract_api_key(headers: &HeaderMap, name: &HeaderName) -> Option<(String, String)> {
 | 
				
			||||||
 | 
					    // Check that its a well-formed bearer and return
 | 
				
			||||||
 | 
					    headers
 | 
				
			||||||
 | 
					        .get(name)
 | 
				
			||||||
 | 
					        .map(|header| {
 | 
				
			||||||
 | 
					            let split = header.to_str().unwrap_or("").split_once(".");
 | 
				
			||||||
 | 
					            let api_key = match split {
 | 
				
			||||||
 | 
					                Some((id, hash)) => Some((id.to_string(), hash.to_string())),
 | 
				
			||||||
 | 
					                _ => None,
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            api_key
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										76
									
								
								src/authentication/middleware.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/authentication/middleware.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,76 @@
 | 
				
			|||||||
 | 
					/// Login predicate middleware.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Requires that the user is authenticated.
 | 
				
			||||||
 | 
					#[macro_export]
 | 
				
			||||||
 | 
					macro_rules! login_required {
 | 
				
			||||||
 | 
					    ($backend_type:ty) => {{
 | 
				
			||||||
 | 
					        async fn is_authenticated(
 | 
				
			||||||
 | 
					            auth_session: $crate::axum_test::AuthenticationBackend<$backend_type>,
 | 
				
			||||||
 | 
					        ) -> bool {
 | 
				
			||||||
 | 
					            auth_session.is_authenticated()
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $crate::predicate_required!(is_authenticated, axum::http::StatusCode::UNAUTHORIZED)
 | 
				
			||||||
 | 
					    }};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Permission predicate middleware.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Requires that the specified permissions, either user or group or both, are
 | 
				
			||||||
 | 
					/// all assigned to the user.
 | 
				
			||||||
 | 
					#[macro_export]
 | 
				
			||||||
 | 
					macro_rules! permission_required {
 | 
				
			||||||
 | 
					    ($backend_type:ty, login_url = $login_url:expr, redirect_field = $redirect_field:expr, $($perm:expr),+ $(,)?) => {{
 | 
				
			||||||
 | 
					        use $crate::AuthzBackend;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        async fn is_authorized(auth_session: $crate::axum_test::AuthSession<$backend_type>) -> bool {
 | 
				
			||||||
 | 
					            if let Some(ref user) = auth_session.user {
 | 
				
			||||||
 | 
					                let mut has_all_permissions = true;
 | 
				
			||||||
 | 
					                $(
 | 
				
			||||||
 | 
					                    has_all_permissions = has_all_permissions &&
 | 
				
			||||||
 | 
					                        auth_session.backend.has_perm(user, $perm.into()).await.unwrap_or(false);
 | 
				
			||||||
 | 
					                )+
 | 
				
			||||||
 | 
					                has_all_permissions
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					                false
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        $crate::predicate_required!(
 | 
				
			||||||
 | 
					            is_authorized,
 | 
				
			||||||
 | 
					            login_url = $login_url,
 | 
				
			||||||
 | 
					            redirect_field = $redirect_field
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Predicate middleware.
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// Can be specified with a login URL and next redirect field or an alternative
 | 
				
			||||||
 | 
					/// which implements [`IntoResponse`](axum::response::IntoResponse).
 | 
				
			||||||
 | 
					///
 | 
				
			||||||
 | 
					/// When the predicate passes, the request processes normally. On failure,
 | 
				
			||||||
 | 
					/// either a redirect to the specified login URL is issued or the alternative is
 | 
				
			||||||
 | 
					/// used as the response.
 | 
				
			||||||
 | 
					#[macro_export]
 | 
				
			||||||
 | 
					macro_rules! predicate_required {
 | 
				
			||||||
 | 
					    ($predicate:expr, $alternative:expr) => {{
 | 
				
			||||||
 | 
					        use axum::{
 | 
				
			||||||
 | 
					            extract::Request,
 | 
				
			||||||
 | 
					            middleware::{from_fn, Next},
 | 
				
			||||||
 | 
					            response::IntoResponse,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        from_fn(
 | 
				
			||||||
 | 
					            |auth_session: $crate::axum_test::AuthenticationBackend<_>,
 | 
				
			||||||
 | 
					             req: Request,
 | 
				
			||||||
 | 
					             next: Next| async move {
 | 
				
			||||||
 | 
					                if $predicate(auth_session).await {
 | 
				
			||||||
 | 
					                    next.run(req).await
 | 
				
			||||||
 | 
					                } else {
 | 
				
			||||||
 | 
					                    $alternative.into_response()
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										123
									
								
								src/authentication/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								src/authentication/mod.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,123 @@
 | 
				
			|||||||
 | 
					pub mod api_key;
 | 
				
			||||||
 | 
					mod extract;
 | 
				
			||||||
 | 
					mod jwt;
 | 
				
			||||||
 | 
					pub mod layer;
 | 
				
			||||||
 | 
					mod middleware;
 | 
				
			||||||
 | 
					mod sessions;
 | 
				
			||||||
 | 
					mod user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// use api_key::ApiKey;
 | 
				
			||||||
 | 
					use axum::http::HeaderName;
 | 
				
			||||||
 | 
					use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey};
 | 
				
			||||||
 | 
					use jwt::JwtAuthorizer;
 | 
				
			||||||
 | 
					use sessions::SessionStore;
 | 
				
			||||||
 | 
					use user::User;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct AuthenticationBackend<T> {
 | 
				
			||||||
 | 
					    sql_pool: (),
 | 
				
			||||||
 | 
					    session_store: SessionStore<T>,
 | 
				
			||||||
 | 
					    jwt_authorizer: JwtAuthorizer,
 | 
				
			||||||
 | 
					    api_key_header: Option<HeaderName>,
 | 
				
			||||||
 | 
					    authenticated_user: Option<AuthenticationData<T>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T> AuthenticationBackend<T> {
 | 
				
			||||||
 | 
					    pub fn is_authenticated(&self) -> bool {
 | 
				
			||||||
 | 
					        self.authenticated_user.is_some()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub enum AuthenticationData<T> {
 | 
				
			||||||
 | 
					    User(UserAuthData),
 | 
				
			||||||
 | 
					    ApiKey(T),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct UserAuthData {
 | 
				
			||||||
 | 
					    user: User,
 | 
				
			||||||
 | 
					    token: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Clone, Default)]
 | 
				
			||||||
 | 
					pub struct AuthenticationBackendBuilder<ApiKey> {
 | 
				
			||||||
 | 
					    pool: Option<()>,
 | 
				
			||||||
 | 
					    api_header_key: Option<HeaderName>,
 | 
				
			||||||
 | 
					    loaded_api_keys: Option<Vec<ApiKey>>,
 | 
				
			||||||
 | 
					    jwt_decode_key: Option<DecodingKey>,
 | 
				
			||||||
 | 
					    jwt_encoding_key: Option<EncodingKey>,
 | 
				
			||||||
 | 
					    jwt_algorithm: Option<Algorithm>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<ApiKey> AuthenticationBackendBuilder<ApiKey> {
 | 
				
			||||||
 | 
					    pub fn new() -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            pool: None,
 | 
				
			||||||
 | 
					            api_header_key: None,
 | 
				
			||||||
 | 
					            loaded_api_keys: None,
 | 
				
			||||||
 | 
					            jwt_decode_key: None,
 | 
				
			||||||
 | 
					            jwt_encoding_key: None,
 | 
				
			||||||
 | 
					            jwt_algorithm: None,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    pub fn pool(self, pool: ()) -> Self {
 | 
				
			||||||
 | 
					        let mut me = self;
 | 
				
			||||||
 | 
					        me.pool = Some(pool);
 | 
				
			||||||
 | 
					        me
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn use_api_key(self, key_header_name: HeaderName) -> Self {
 | 
				
			||||||
 | 
					        let mut me = self;
 | 
				
			||||||
 | 
					        me.api_header_key = Some(key_header_name);
 | 
				
			||||||
 | 
					        me
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn api_keys(self, keys: Vec<ApiKey>) -> Self {
 | 
				
			||||||
 | 
					        let mut me = self;
 | 
				
			||||||
 | 
					        me.loaded_api_keys = Some(keys);
 | 
				
			||||||
 | 
					        me
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn jwt_decoding_key(self, key: DecodingKey) -> Self {
 | 
				
			||||||
 | 
					        let mut me = self;
 | 
				
			||||||
 | 
					        me.jwt_decode_key = Some(key);
 | 
				
			||||||
 | 
					        me
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn jwt_encoding_key(self, key: EncodingKey) -> Self {
 | 
				
			||||||
 | 
					        let mut me = self;
 | 
				
			||||||
 | 
					        me.jwt_encoding_key = Some(key);
 | 
				
			||||||
 | 
					        me
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn jwt_algorithm(self, algorithm: Algorithm) -> Self {
 | 
				
			||||||
 | 
					        let mut me = self;
 | 
				
			||||||
 | 
					        me.jwt_algorithm = Some(algorithm);
 | 
				
			||||||
 | 
					        me
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn build(self) -> Result<AuthenticationBackend<ApiKey>, String> {
 | 
				
			||||||
 | 
					        if self.pool.is_none() {
 | 
				
			||||||
 | 
					            return Err("Missing Pool".to_string());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if self.jwt_decode_key.is_none() {
 | 
				
			||||||
 | 
					            return Err("Missing JWT Decoding key".to_string());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        if self.jwt_encoding_key.is_none() {
 | 
				
			||||||
 | 
					            return Err("Missing JWT Encoding key".to_string());
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(AuthenticationBackend {
 | 
				
			||||||
 | 
					            sql_pool: self.pool.unwrap(),
 | 
				
			||||||
 | 
					            session_store: SessionStore::new(self.loaded_api_keys),
 | 
				
			||||||
 | 
					            jwt_authorizer: JwtAuthorizer::new(
 | 
				
			||||||
 | 
					                self.jwt_encoding_key.unwrap(),
 | 
				
			||||||
 | 
					                self.jwt_decode_key.unwrap(),
 | 
				
			||||||
 | 
					                self.jwt_algorithm.unwrap_or(Algorithm::RS256),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            api_key_header: self.api_header_key,
 | 
				
			||||||
 | 
					            authenticated_user: None,
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										54
									
								
								src/authentication/sessions.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/authentication/sessions.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,54 @@
 | 
				
			|||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    collections::{HashMap, HashSet},
 | 
				
			||||||
 | 
					    sync::Arc,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use tokio::sync::Mutex;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// use super::ApiKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone)]
 | 
				
			||||||
 | 
					pub struct SessionStore<T> {
 | 
				
			||||||
 | 
					    active_sessions: Arc<Mutex<HashSet<String>>>,
 | 
				
			||||||
 | 
					    active_api_keys: Option<Arc<Mutex<HashMap<String, T>>>>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T> SessionStore<T> {
 | 
				
			||||||
 | 
					    pub fn new(initial_api_keys: Option<Vec<T>>) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            active_sessions: Arc::new(Mutex::new(HashSet::new())),
 | 
				
			||||||
 | 
					            active_api_keys: None, //initial_api_keys.map(|keys| {
 | 
				
			||||||
 | 
					                                   //     Arc::new(Mutex::new(HashMap::from_iter(
 | 
				
			||||||
 | 
					                                   //         keys.into_iter().map(|key| (key.id.clone(), key)),
 | 
				
			||||||
 | 
					                                   //     )))
 | 
				
			||||||
 | 
					                                   // }),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn insert(&self, token: impl Into<String>) {
 | 
				
			||||||
 | 
					        let mut sessions = self.active_sessions.lock().await;
 | 
				
			||||||
 | 
					        sessions.insert(token.into());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn includes(&self, token: &str) -> bool {
 | 
				
			||||||
 | 
					        let sessions = self.active_sessions.lock().await;
 | 
				
			||||||
 | 
					        sessions.contains(token)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn remove(&self, token: &str) -> bool {
 | 
				
			||||||
 | 
					        let mut sessions = self.active_sessions.lock().await;
 | 
				
			||||||
 | 
					        sessions.remove(token)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn get_api_key(&self, key: &str) -> Option<T>
 | 
				
			||||||
 | 
					    where
 | 
				
			||||||
 | 
					        T: Clone,
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        if let Some(ref key_store) = self.active_api_keys {
 | 
				
			||||||
 | 
					            let lock = key_store.lock().await;
 | 
				
			||||||
 | 
					            lock.get(key).cloned()
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            None
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/authentication/user.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/authentication/user.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Clone, Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct User {}
 | 
				
			||||||
							
								
								
									
										310
									
								
								src/cli.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										310
									
								
								src/cli.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,310 @@
 | 
				
			|||||||
 | 
					use std::{collections::HashSet, path::PathBuf};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use clap::{command, Parser, Subcommand};
 | 
				
			||||||
 | 
					use error_stack::{Report, ResultExt};
 | 
				
			||||||
 | 
					use sqlx::PgPool;
 | 
				
			||||||
 | 
					use tokio::sync::mpsc::UnboundedReceiver;
 | 
				
			||||||
 | 
					use tokio_util::sync::CancellationToken;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::{
 | 
				
			||||||
 | 
					    api::{
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        routes::api_keys::{models::ApiKey, sql::create_api_key},
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    config::Configuration,
 | 
				
			||||||
 | 
					    errors::AppError,
 | 
				
			||||||
 | 
					    APP_NAME, ROOT_PATH,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Parser, Debug)]
 | 
				
			||||||
 | 
					#[command(name = APP_NAME, version = "1.0", about = "A tool with subcommands")]
 | 
				
			||||||
 | 
					pub struct Cli {
 | 
				
			||||||
 | 
					    #[arg(short, long, value_name = "FILE", help = "Configuration file location")]
 | 
				
			||||||
 | 
					    config: Option<PathBuf>,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Subcommands for specific actions
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    command: Option<Commands>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Subcommand)]
 | 
				
			||||||
 | 
					enum Commands {
 | 
				
			||||||
 | 
					    Serve {
 | 
				
			||||||
 | 
					        /// lists test values
 | 
				
			||||||
 | 
					        #[arg(short, long)]
 | 
				
			||||||
 | 
					        port: Option<u16>,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    Migrate,
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    Service(ServiceCommands),
 | 
				
			||||||
 | 
					    #[command(subcommand)]
 | 
				
			||||||
 | 
					    Create(CreateCommands),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Subcommand)]
 | 
				
			||||||
 | 
					enum ServiceCommands {
 | 
				
			||||||
 | 
					    Install,
 | 
				
			||||||
 | 
					    Uninstall,
 | 
				
			||||||
 | 
					    Run,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Subcommand)]
 | 
				
			||||||
 | 
					enum CreateCommands {
 | 
				
			||||||
 | 
					    ApiKey {
 | 
				
			||||||
 | 
					        /// name of API Key
 | 
				
			||||||
 | 
					        #[arg(short, long)]
 | 
				
			||||||
 | 
					        name: String,
 | 
				
			||||||
 | 
					        /// name of API Key
 | 
				
			||||||
 | 
					        #[arg(short, long, default_value = "true")]
 | 
				
			||||||
 | 
					        requires_auth: bool,
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Cli {
 | 
				
			||||||
 | 
					    pub fn config(&self) -> PathBuf {
 | 
				
			||||||
 | 
					        match self.config.as_ref() {
 | 
				
			||||||
 | 
					            Some(path) => path.clone(),
 | 
				
			||||||
 | 
					            None => ROOT_PATH.with_file_name("config.toml"),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub async fn handle(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        config: &mut Configuration,
 | 
				
			||||||
 | 
					    ) -> Result<DaemonStatus, Report<AppError>> {
 | 
				
			||||||
 | 
					        match &self.command {
 | 
				
			||||||
 | 
					            Some(command) => match command {
 | 
				
			||||||
 | 
					                Commands::Serve { port } => {
 | 
				
			||||||
 | 
					                    if let Some(port) = port {
 | 
				
			||||||
 | 
					                        config.port = *port;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    start_service(config, None).await
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Commands::Migrate => {
 | 
				
			||||||
 | 
					                    let pool = PgPool::connect(&config.database_url)
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                        .change_context(AppError)?;
 | 
				
			||||||
 | 
					                    sqlx::migrate!("src/migrations")
 | 
				
			||||||
 | 
					                        .run(&pool)
 | 
				
			||||||
 | 
					                        .await
 | 
				
			||||||
 | 
					                        .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    Ok(DaemonStatus::NotRunning)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Commands::Service(service_commands) => match service_commands {
 | 
				
			||||||
 | 
					                    ServiceCommands::Install => {
 | 
				
			||||||
 | 
					                        // TODO do things
 | 
				
			||||||
 | 
					                        Ok(DaemonStatus::NotRunning)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    ServiceCommands::Uninstall => {
 | 
				
			||||||
 | 
					                        // TODO do things
 | 
				
			||||||
 | 
					                        Ok(DaemonStatus::NotRunning)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                    ServiceCommands::Run => {
 | 
				
			||||||
 | 
					                        // TODO do things
 | 
				
			||||||
 | 
					                        // create shutdown signals
 | 
				
			||||||
 | 
					                        // let (shutdown_send, mut shutdown_recv) = mpsc::unbounded_channel();
 | 
				
			||||||
 | 
					                        // Register generated `ffi_service_main` with the system and start the service, blocking
 | 
				
			||||||
 | 
					                        // this thread until the service is stopped.
 | 
				
			||||||
 | 
					                        #[cfg(windows)]
 | 
				
			||||||
 | 
					                        let _ = service_dispatcher::start(env!("CARGO_PKG_NAME"), ffi_service_main);
 | 
				
			||||||
 | 
					                        Ok(DaemonStatus::NotRunning)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					                Commands::Create(create_commands) => match create_commands {
 | 
				
			||||||
 | 
					                    CreateCommands::ApiKey {
 | 
				
			||||||
 | 
					                        name,
 | 
				
			||||||
 | 
					                        requires_auth,
 | 
				
			||||||
 | 
					                    } => {
 | 
				
			||||||
 | 
					                        // create API Key
 | 
				
			||||||
 | 
					                        let (key_secret, key) =
 | 
				
			||||||
 | 
					                            ApiKey::create(name, *requires_auth, HashSet::new(), config)
 | 
				
			||||||
 | 
					                                .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // Add API Key to Database
 | 
				
			||||||
 | 
					                        let pool = PgPool::connect(&config.database_url)
 | 
				
			||||||
 | 
					                            .await
 | 
				
			||||||
 | 
					                            .change_context(AppError)?;
 | 
				
			||||||
 | 
					                        create_api_key(&pool, &key).await.change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // print API key secret to console
 | 
				
			||||||
 | 
					                        println!("Created API Key: {}.{key_secret}", key.id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        Ok(DaemonStatus::NotRunning)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            None => start_service(config, None).await,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum DaemonStatus {
 | 
				
			||||||
 | 
					    NotRunning,
 | 
				
			||||||
 | 
					    Running((CancellationToken, Option<UnboundedReceiver<()>>)),
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn start_service(
 | 
				
			||||||
 | 
					    config: &Configuration,
 | 
				
			||||||
 | 
					    shutdown_signal: Option<UnboundedReceiver<()>>,
 | 
				
			||||||
 | 
					) -> Result<DaemonStatus, Report<AppError>> {
 | 
				
			||||||
 | 
					    // create cancellation token
 | 
				
			||||||
 | 
					    let cancellation_token = CancellationToken::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    api::start(config, cancellation_token.clone()).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(DaemonStatus::Running((cancellation_token, shutdown_signal)))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(windows)]
 | 
				
			||||||
 | 
					mod windows {
 | 
				
			||||||
 | 
					    use error_stack::{Report, ResultExt};
 | 
				
			||||||
 | 
					    use std::{ffi::OsString, thread, time::Duration};
 | 
				
			||||||
 | 
					    use windows_service::{
 | 
				
			||||||
 | 
					        service::{
 | 
				
			||||||
 | 
					            ServiceAccess, ServiceErrorControl, ServiceInfo, ServiceStartType, ServiceState,
 | 
				
			||||||
 | 
					            ServiceType,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        service_manager::{ServiceManager, ServiceManagerAccess},
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    use crate::errors::AppError;
 | 
				
			||||||
 | 
					    use crate::APP_NAME;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn install() -> Result<(), Report<AppError>> {
 | 
				
			||||||
 | 
					        let manager_access = ServiceManagerAccess::CONNECT | ServiceManagerAccess::CREATE_SERVICE;
 | 
				
			||||||
 | 
					        let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
 | 
				
			||||||
 | 
					            .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // This example installs the service defined in `examples/ping_service.rs`.
 | 
				
			||||||
 | 
					        // In the real world code you would set the executable path to point to your own binary
 | 
				
			||||||
 | 
					        // that implements windows service.
 | 
				
			||||||
 | 
					        let service_binary_path = ::std::env::current_exe()
 | 
				
			||||||
 | 
					            .change_context(AppError)?
 | 
				
			||||||
 | 
					            .with_file_name(format!("{}.exe", env!("CARGO_PKG_NAME")));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let service_info = ServiceInfo {
 | 
				
			||||||
 | 
					            name: OsString::from(env!("CARGO_PKG_NAME")),
 | 
				
			||||||
 | 
					            display_name: OsString::from(&format!("{APP_NAME}")),
 | 
				
			||||||
 | 
					            service_type: ServiceType::OWN_PROCESS,
 | 
				
			||||||
 | 
					            start_type: ServiceStartType::AutoStart,
 | 
				
			||||||
 | 
					            error_control: ServiceErrorControl::Normal,
 | 
				
			||||||
 | 
					            executable_path: service_binary_path,
 | 
				
			||||||
 | 
					            launch_arguments: vec!["service run".into()],
 | 
				
			||||||
 | 
					            dependencies: vec![],
 | 
				
			||||||
 | 
					            account_name: None, // run as System
 | 
				
			||||||
 | 
					            account_password: None,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					        let service = service_manager
 | 
				
			||||||
 | 
					            .create_service(&service_info, ServiceAccess::CHANGE_CONFIG)
 | 
				
			||||||
 | 
					            .into_report()
 | 
				
			||||||
 | 
					            .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        service
 | 
				
			||||||
 | 
					            .set_description(format!("{APP_NAME}"))
 | 
				
			||||||
 | 
					            .into_report()
 | 
				
			||||||
 | 
					            .change_context(AppError)?;
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn remove() -> Result<(), Report<AppError>> {
 | 
				
			||||||
 | 
					        let manager_access = ServiceManagerAccess::CONNECT;
 | 
				
			||||||
 | 
					        let service_manager = ServiceManager::local_computer(None::<&str>, manager_access)
 | 
				
			||||||
 | 
					            .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let service_access =
 | 
				
			||||||
 | 
					            ServiceAccess::QUERY_STATUS | ServiceAccess::STOP | ServiceAccess::DELETE;
 | 
				
			||||||
 | 
					        let service = service_manager
 | 
				
			||||||
 | 
					            .open_service(env!("CARGO_PKG_NAME"), service_access)
 | 
				
			||||||
 | 
					            .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let service_status = service.query_status().change_context(AppError)?;
 | 
				
			||||||
 | 
					        if service_status.current_state != ServiceState::Stopped {
 | 
				
			||||||
 | 
					            service.stop().change_context(AppError)?;
 | 
				
			||||||
 | 
					            // Wait for service to stop
 | 
				
			||||||
 | 
					            thread::sleep(Duration::from_secs(1));
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        service.delete().into_report().change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Service entry function which is called on background thread by the system with service
 | 
				
			||||||
 | 
					    // parameters. There is no stdout or stderr at this point so make sure to configure the log
 | 
				
			||||||
 | 
					    // output to file if needed.
 | 
				
			||||||
 | 
					    pub fn service_main(_arguments: Vec<OsString>) {
 | 
				
			||||||
 | 
					        if let Err(_e) = run_service() {
 | 
				
			||||||
 | 
					            // Handle the error, by logging or something.
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn run_service() -> Result<(), Report<AppError>> {
 | 
				
			||||||
 | 
					        // Create a channel to be able to poll a stop event from the service worker loop.
 | 
				
			||||||
 | 
					        let (shutdown_tx, shutdown_rx) = tokio::sync::mpsc::channel(1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Define system service event handler that will be receiving service events.
 | 
				
			||||||
 | 
					        let event_handler = move |control_event| -> ServiceControlHandlerResult {
 | 
				
			||||||
 | 
					            match control_event {
 | 
				
			||||||
 | 
					                // Notifies a service to report its current status information to the service
 | 
				
			||||||
 | 
					                // control manager. Always return NoError even if not implemented.
 | 
				
			||||||
 | 
					                ServiceControl::Interrogate => ServiceControlHandlerResult::NoError,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                // Handle stop
 | 
				
			||||||
 | 
					                ServiceControl::Stop => {
 | 
				
			||||||
 | 
					                    shutdown_tx.try_send(()).unwrap();
 | 
				
			||||||
 | 
					                    ServiceControlHandlerResult::NoError
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                _ => ServiceControlHandlerResult::NotImplemented,
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Register system service event handler.
 | 
				
			||||||
 | 
					        // The returned status handle should be used to report service status changes to the system.
 | 
				
			||||||
 | 
					        let status_handle =
 | 
				
			||||||
 | 
					            service_control_handler::register(env!("CARGO_PKG_NAME"), event_handler)
 | 
				
			||||||
 | 
					                .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Tell the system that service is running
 | 
				
			||||||
 | 
					        status_handle
 | 
				
			||||||
 | 
					            .set_service_status(ServiceStatus {
 | 
				
			||||||
 | 
					                service_type: ServiceType::OWN_PROCESS,
 | 
				
			||||||
 | 
					                current_state: ServiceState::Running,
 | 
				
			||||||
 | 
					                controls_accepted: ServiceControlAccept::STOP,
 | 
				
			||||||
 | 
					                exit_code: ServiceExitCode::Win32(0),
 | 
				
			||||||
 | 
					                checkpoint: 0,
 | 
				
			||||||
 | 
					                wait_hint: Duration::default(),
 | 
				
			||||||
 | 
					                process_id: None,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // run service - blocking thread
 | 
				
			||||||
 | 
					        let rt = tokio::runtime::Builder::new_current_thread()
 | 
				
			||||||
 | 
					            .enable_all()
 | 
				
			||||||
 | 
					            .build()
 | 
				
			||||||
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        rt.block_on(async move {
 | 
				
			||||||
 | 
					            if let Err(e) = execute(shutdown_rx).await {
 | 
				
			||||||
 | 
					                error!("{e:?}");
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Tell the system that service has stopped.
 | 
				
			||||||
 | 
					        status_handle
 | 
				
			||||||
 | 
					            .set_service_status(ServiceStatus {
 | 
				
			||||||
 | 
					                service_type: ServiceType::OWN_PROCESS,
 | 
				
			||||||
 | 
					                current_state: ServiceState::Stopped,
 | 
				
			||||||
 | 
					                controls_accepted: ServiceControlAccept::empty(),
 | 
				
			||||||
 | 
					                exit_code: ServiceExitCode::Win32(0),
 | 
				
			||||||
 | 
					                checkpoint: 0,
 | 
				
			||||||
 | 
					                wait_hint: Duration::default(),
 | 
				
			||||||
 | 
					                process_id: None,
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .into_report()
 | 
				
			||||||
 | 
					            .change_context(ServiceError::Starting)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										88
									
								
								src/config.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								src/config.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,88 @@
 | 
				
			|||||||
 | 
					use std::path::PathBuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use config::{Config, Environment, File};
 | 
				
			||||||
 | 
					use error_stack::{Report, ResultExt};
 | 
				
			||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::errors::AppError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn default_port() -> u16 {
 | 
				
			||||||
 | 
					    8080
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Deserialize, Default, Clone)]
 | 
				
			||||||
 | 
					#[allow(unused)]
 | 
				
			||||||
 | 
					pub struct ConfigInfo {
 | 
				
			||||||
 | 
					    pub location: Option<String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Deserialize, Default, Clone)]
 | 
				
			||||||
 | 
					pub struct LDAP {
 | 
				
			||||||
 | 
					    pub server: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub skip_tls_verify: bool,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub ad_domain: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub user_search_base: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub elevated_search: bool,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub elevated_user_id: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub elevated_user_pw: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Deserialize, Default, Clone)]
 | 
				
			||||||
 | 
					#[allow(unused)]
 | 
				
			||||||
 | 
					pub struct Configuration {
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub config: ConfigInfo,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub debug: bool,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub token_secret: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub session_length: u64,
 | 
				
			||||||
 | 
					    #[serde(default = "default_port")]
 | 
				
			||||||
 | 
					    pub port: u16,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    #[serde(alias = "DATABASE_URL")]
 | 
				
			||||||
 | 
					    pub database_url: String,
 | 
				
			||||||
 | 
					    #[serde(default)]
 | 
				
			||||||
 | 
					    pub ldap: LDAP,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Configuration {
 | 
				
			||||||
 | 
					    pub fn new(location: PathBuf) -> Result<Self, Report<AppError>> {
 | 
				
			||||||
 | 
					        let s = Config::builder()
 | 
				
			||||||
 | 
					            .add_source(File::from(location.clone()))
 | 
				
			||||||
 | 
					            .add_source(Environment::default())
 | 
				
			||||||
 | 
					            .set_override("config.location", location.to_str().unwrap_or("default"))
 | 
				
			||||||
 | 
					            .change_context(AppError)?
 | 
				
			||||||
 | 
					            .build()
 | 
				
			||||||
 | 
					            .change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let settings = s.try_deserialize().change_context(AppError)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok(settings)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn check(&self) -> Result<(), Report<AppError>> {
 | 
				
			||||||
 | 
					        // check if elevated LDAP user is set if activated
 | 
				
			||||||
 | 
					        if self.ldap.elevated_search {
 | 
				
			||||||
 | 
					            if self.ldap.elevated_user_id == "" {
 | 
				
			||||||
 | 
					                return Err(Report::new(AppError).attach_printable(
 | 
				
			||||||
 | 
					                    "LDAP: Search with elevated user is activated but no user is configured",
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if self.ldap.elevated_user_pw == "" {
 | 
				
			||||||
 | 
					                return Err(Report::new(AppError).attach_printable(
 | 
				
			||||||
 | 
					                    "LDAP: Search with elevated user is activated but no password is configured",
 | 
				
			||||||
 | 
					                ));
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										124
									
								
								src/errors.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/errors.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
				
			|||||||
 | 
					use std::fmt::{self, Display};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    http::StatusCode,
 | 
				
			||||||
 | 
					    response::{IntoResponse, Response},
 | 
				
			||||||
 | 
					    Json,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use error_stack::Context;
 | 
				
			||||||
 | 
					use serde::Serialize;
 | 
				
			||||||
 | 
					use sha2::digest::InvalidLength;
 | 
				
			||||||
 | 
					use tracing::error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::api::{backend::ApiBackend, routes::users::models::User};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug)]
 | 
				
			||||||
 | 
					pub struct AppError;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl fmt::Display for AppError {
 | 
				
			||||||
 | 
					    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
 | 
				
			||||||
 | 
					        fmt.write_str("Error on executing Application")
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Context for AppError {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, Serialize)]
 | 
				
			||||||
 | 
					pub struct ErrorInfo<'a> {
 | 
				
			||||||
 | 
					    error_message: &'a str,
 | 
				
			||||||
 | 
					    debug_info: Option<&'a String>,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Debug, PartialEq)]
 | 
				
			||||||
 | 
					pub enum ApiError {
 | 
				
			||||||
 | 
					    SQLQueryError(String),
 | 
				
			||||||
 | 
					    InvalidCredentials,
 | 
				
			||||||
 | 
					    InternalError(String),
 | 
				
			||||||
 | 
					    AccessDenied,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl ApiError {
 | 
				
			||||||
 | 
					    fn as_error_info(&self) -> (StatusCode, ErrorInfo) {
 | 
				
			||||||
 | 
					        let (status, error_message, debug_info) = match self {
 | 
				
			||||||
 | 
					            Self::InvalidCredentials => (StatusCode::UNAUTHORIZED, "Invalid credentials", None),
 | 
				
			||||||
 | 
					            Self::SQLQueryError(error) => (
 | 
				
			||||||
 | 
					                StatusCode::INTERNAL_SERVER_ERROR,
 | 
				
			||||||
 | 
					                "Invalid credentials",
 | 
				
			||||||
 | 
					                Some(error),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Self::InternalError(error) => (
 | 
				
			||||||
 | 
					                StatusCode::INTERNAL_SERVER_ERROR,
 | 
				
			||||||
 | 
					                "Internal Server Error",
 | 
				
			||||||
 | 
					                Some(error),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            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"),
 | 
				
			||||||
 | 
					                                                                                  // ApiError::InvalidToken => (StatusCode::BAD_REQUEST, "Invalid token"),
 | 
				
			||||||
 | 
					                                                                                  // ApiError::InvalidApiKey => (StatusCode::BAD_REQUEST, "Invalid api key"),
 | 
				
			||||||
 | 
					                                                                                  // ApiError::InternalServerError => {
 | 
				
			||||||
 | 
					                                                                                  //     (StatusCode::INTERNAL_SERVER_ERROR, "Unspecified error")
 | 
				
			||||||
 | 
					                                                                                  // }
 | 
				
			||||||
 | 
					                                                                                  // ApiError::AccessDenied => (StatusCode::UNAUTHORIZED, "Access denied"),
 | 
				
			||||||
 | 
					                                                                                  // ApiError::InvalidPermissions => (StatusCode::BAD_REQUEST, "Invalid permissions"),
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            status,
 | 
				
			||||||
 | 
					            ErrorInfo {
 | 
				
			||||||
 | 
					                error_message,
 | 
				
			||||||
 | 
					                debug_info,
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl IntoResponse for ApiError {
 | 
				
			||||||
 | 
					    fn into_response(self) -> Response {
 | 
				
			||||||
 | 
					        let (status, error_info) = self.as_error_info();
 | 
				
			||||||
 | 
					        let body = Json(error_info);
 | 
				
			||||||
 | 
					        (status, body).into_response()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<sqlx::Error> for ApiError {
 | 
				
			||||||
 | 
					    fn from(error: sqlx::Error) -> Self {
 | 
				
			||||||
 | 
					        error!("{error}");
 | 
				
			||||||
 | 
					        Self::SQLQueryError(error.to_string())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<axum_jwt_login::Error<User, ApiBackend>> for ApiError {
 | 
				
			||||||
 | 
					    fn from(value: axum_jwt_login::Error<User, ApiBackend>) -> Self {
 | 
				
			||||||
 | 
					        match value {
 | 
				
			||||||
 | 
					            axum_jwt_login::Error::Jwt(error) => ApiError::InternalError(error.to_string()),
 | 
				
			||||||
 | 
					            axum_jwt_login::Error::Backend(error) => error,
 | 
				
			||||||
 | 
					            _ => unreachable!(),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<argon2::Error> for ApiError {
 | 
				
			||||||
 | 
					    fn from(_: argon2::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::InvalidCredentials
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<InvalidLength> for ApiError {
 | 
				
			||||||
 | 
					    fn from(value: InvalidLength) -> Self {
 | 
				
			||||||
 | 
					        Self::InternalError(format!("Invalid HMac Key length: {value}"))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Display for ApiError {
 | 
				
			||||||
 | 
					    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
				
			||||||
 | 
					        let error_info = self.as_error_info().1;
 | 
				
			||||||
 | 
					        f.write_str(&format!(
 | 
				
			||||||
 | 
					            "{}: {}",
 | 
				
			||||||
 | 
					            error_info.error_message,
 | 
				
			||||||
 | 
					            error_info.debug_info.unwrap_or(&"".to_string())
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl std::error::Error for ApiError {}
 | 
				
			||||||
							
								
								
									
										55
									
								
								src/ldap_test.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/ldap_test.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
				
			|||||||
 | 
					use ldap3::exop::WhoAmI;
 | 
				
			||||||
 | 
					use ldap3::{LdapConnAsync, Scope, SearchEntry};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub async fn try_authentication() -> ldap3::result::Result<()> {
 | 
				
			||||||
 | 
					    println!("Hello, world!");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // let (conn, mut ldap) = LdapConnAsync::new("ldap://ldap.forumsys.com:389").await?;
 | 
				
			||||||
 | 
					    let (conn, mut ldap) = LdapConnAsync::new("ldap://192.168.10.200:389").await?;
 | 
				
			||||||
 | 
					    ldap3::drive!(conn);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let res = ldap
 | 
				
			||||||
 | 
					        // .simple_bind("cn=read-only-admin,dc=example,dc=com", "password")
 | 
				
			||||||
 | 
					        .simple_bind(
 | 
				
			||||||
 | 
					            // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local",
 | 
				
			||||||
 | 
					            "MYLAB\\A0H67123",
 | 
				
			||||||
 | 
					            "Passwort123",
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await?;
 | 
				
			||||||
 | 
					    println!("{res:?}");
 | 
				
			||||||
 | 
					    // let (res, re) = ldap
 | 
				
			||||||
 | 
					    //     .search(
 | 
				
			||||||
 | 
					    //         "ou=mathematicians,dc=example,dc=com",
 | 
				
			||||||
 | 
					    //         Scope::Subtree,
 | 
				
			||||||
 | 
					    //         "(objectClass=*)",
 | 
				
			||||||
 | 
					    //         vec!["*"],
 | 
				
			||||||
 | 
					    //     )
 | 
				
			||||||
 | 
					    //     .await?
 | 
				
			||||||
 | 
					    //     .success()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // for entry in res {
 | 
				
			||||||
 | 
					    //     println!("{:?}", SearchEntry::construct(entry));
 | 
				
			||||||
 | 
					    // }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let (res, re) = ldap
 | 
				
			||||||
 | 
					        .search(
 | 
				
			||||||
 | 
					            // "CN=Abel Austin,OU=Accounting,OU=Mylab Users,DC=mylab,DC=local",
 | 
				
			||||||
 | 
					            "OU=Mylab Users,DC=mylab,DC=local",
 | 
				
			||||||
 | 
					            Scope::Subtree,
 | 
				
			||||||
 | 
					            // "(objectClass=*)",
 | 
				
			||||||
 | 
					            "(&(ObjectCategory=Person)(sAMAccountName=A0H67123))",
 | 
				
			||||||
 | 
					            vec!["givenName", "sn"],
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await?
 | 
				
			||||||
 | 
					        .success()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    for entry in res {
 | 
				
			||||||
 | 
					        println!("{:?}", SearchEntry::construct(entry));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let test = ldap.extended(WhoAmI).await?.0;
 | 
				
			||||||
 | 
					    let whoami: ldap3::exop::WhoAmIResp = test.parse();
 | 
				
			||||||
 | 
					    println!("{whoami:?}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(ldap.unbind().await?)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										96
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/main.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
				
			|||||||
 | 
					use std::{path::PathBuf, time::Duration};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use clap::Parser;
 | 
				
			||||||
 | 
					use error_stack::Report;
 | 
				
			||||||
 | 
					use errors::AppError;
 | 
				
			||||||
 | 
					use once_cell::sync::Lazy;
 | 
				
			||||||
 | 
					use tokio::signal;
 | 
				
			||||||
 | 
					use tokio_util::task::TaskTracker;
 | 
				
			||||||
 | 
					use tracing::{error, info};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub mod api;
 | 
				
			||||||
 | 
					mod cli;
 | 
				
			||||||
 | 
					mod config;
 | 
				
			||||||
 | 
					mod errors;
 | 
				
			||||||
 | 
					mod utils;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub static ROOT_PATH: Lazy<PathBuf> = Lazy::new(|| match std::env::current_exe() {
 | 
				
			||||||
 | 
					    #[cfg(not(debug_assertions))]
 | 
				
			||||||
 | 
					    Ok(path) => path,
 | 
				
			||||||
 | 
					    #[cfg(debug_assertions)]
 | 
				
			||||||
 | 
					    Ok(_) => PathBuf::from("./"),
 | 
				
			||||||
 | 
					    Err(e) => {
 | 
				
			||||||
 | 
					        error!("Exiting on error for getting root path: {e:?}");
 | 
				
			||||||
 | 
					        panic!("Exiting on error for getting root path: {e:?}");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub const APP_NAME: &str = "Generic API Service";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[tokio::main]
 | 
				
			||||||
 | 
					async fn main() -> Result<(), Report<AppError>> {
 | 
				
			||||||
 | 
					    dotenv::dotenv().ok();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // prepare CLI
 | 
				
			||||||
 | 
					    let cli = cli::Cli::parse();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // load config file
 | 
				
			||||||
 | 
					    let mut config = config::Configuration::new(cli.config())?;
 | 
				
			||||||
 | 
					    config.check()?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    println!("{config:?}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // handle CLI input
 | 
				
			||||||
 | 
					    let daemon = cli.handle(&mut config).await?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match daemon {
 | 
				
			||||||
 | 
					        cli::DaemonStatus::NotRunning => {}
 | 
				
			||||||
 | 
					        cli::DaemonStatus::Running((cancellation_token, shutdown_signal)) => {
 | 
				
			||||||
 | 
					            // create task tracker
 | 
				
			||||||
 | 
					            let tracker = TaskTracker::new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // terminate signal
 | 
				
			||||||
 | 
					            #[cfg(unix)]
 | 
				
			||||||
 | 
					            let terminate = async {
 | 
				
			||||||
 | 
					                signal::unix::signal(signal::unix::SignalKind::terminate())
 | 
				
			||||||
 | 
					                    .expect("Failed to install termination signal handler")
 | 
				
			||||||
 | 
					                    .recv()
 | 
				
			||||||
 | 
					                    .await;
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					            #[cfg(not(unix))]
 | 
				
			||||||
 | 
					            let terminate = std::future::pending::<()>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            let shutdown_signal = async {
 | 
				
			||||||
 | 
					                match shutdown_signal {
 | 
				
			||||||
 | 
					                    Some(mut signal) => signal.recv().await,
 | 
				
			||||||
 | 
					                    None => std::future::pending::<Option<()>>().await,
 | 
				
			||||||
 | 
					                };
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // wait for shutdown signals
 | 
				
			||||||
 | 
					            tokio::select! {
 | 
				
			||||||
 | 
					                _ = signal::ctrl_c() => {},
 | 
				
			||||||
 | 
					                _ = terminate => {},
 | 
				
			||||||
 | 
					                _ = shutdown_signal => {},
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // send shutdown signal to application and wait
 | 
				
			||||||
 | 
					            cancellation_token.cancel();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // close task tracker
 | 
				
			||||||
 | 
					            tracker.close();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            // Wait for everything to finish.
 | 
				
			||||||
 | 
					            tracker.wait().await;
 | 
				
			||||||
 | 
					            tokio::time::sleep(Duration::from_millis(200)).await;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            info!("{APP_NAME} gracefully shut down.");
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // let _ = axum_test::start_axum().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // let _ = ldap_test::try_authentication().await.unwrap();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(())
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										37
									
								
								src/migrations/01_api_keys.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/migrations/01_api_keys.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,37 @@
 | 
				
			|||||||
 | 
					-- Table: public.apikeys
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					-- DROP TABLE IF EXISTS public.apikeys;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					CREATE TABLE IF NOT EXISTS public.apikeys
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					    "KeyID" character varying(32) COLLATE pg_catalog."default" NOT NULL,
 | 
				
			||||||
 | 
					    "Name" character varying(255) COLLATE pg_catalog."default" NOT NULL,
 | 
				
			||||||
 | 
					    "UserAuthRequired" boolean NOT NULL DEFAULT true,
 | 
				
			||||||
 | 
					    "Hash" bytea NOT NULL,
 | 
				
			||||||
 | 
					    "CreationDate" timestamp without time zone NOT NULL DEFAULT now(),
 | 
				
			||||||
 | 
					    "LastChanged" timestamp without time zone NOT NULL DEFAULT now(),
 | 
				
			||||||
 | 
					    CONSTRAINT apikeys_pkey PRIMARY KEY ("KeyID")
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TABLESPACE pg_default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE IF EXISTS public.apikeys
 | 
				
			||||||
 | 
					    OWNER to postgres;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON COLUMN public.apikeys."KeyID"
 | 
				
			||||||
 | 
					    IS 'UUID of API Key';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON COLUMN public.apikeys."Name"
 | 
				
			||||||
 | 
					    IS 'Name/Description of API Key';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON COLUMN public.apikeys."Hash"
 | 
				
			||||||
 | 
					    IS 'Hashed value of API Key';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON COLUMN public.apikeys."UserAuthRequired"
 | 
				
			||||||
 | 
					    IS 'Indication if this api key requires additional user authentication';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON COLUMN public.apikeys."CreationDate"
 | 
				
			||||||
 | 
					    IS 'Time of creation';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON COLUMN public.apikeys."LastChanged"
 | 
				
			||||||
 | 
					    IS 'Time of last modification';
 | 
				
			||||||
							
								
								
									
										19
									
								
								src/migrations/02_users.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								src/migrations/02_users.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
				
			|||||||
 | 
					CREATE TABLE public.users
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					    "UserID" character varying(10)[] NOT NULL,
 | 
				
			||||||
 | 
					    "ActiveDirectoryAuth" boolean NOT NULL DEFAULT false,
 | 
				
			||||||
 | 
					    "Name" character varying(250) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    "Surname" character varying(250) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    "Email" character varying(500) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    "Password" character varying(255) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    "CreationDate" timestamp without time zone NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					    "LastChanged" timestamp without time zone NOT NULL DEFAULT NOW(),
 | 
				
			||||||
 | 
					    "StatusFlag" smallint NOT NULL,
 | 
				
			||||||
 | 
					    PRIMARY KEY ("UserID")
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE IF EXISTS public.users
 | 
				
			||||||
 | 
					    OWNER to postgres;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON TABLE public.users
 | 
				
			||||||
 | 
					    IS 'Table containing user information';
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/migrations/03_groups.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/migrations/03_groups.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
				
			|||||||
 | 
					CREATE TABLE public.groups
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					    "GroupID" serial NOT NULL,
 | 
				
			||||||
 | 
					    "GroupName" character varying(255) NOT NULL DEFAULT '',
 | 
				
			||||||
 | 
					    PRIMARY KEY ("GroupID")
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE IF EXISTS public.groups
 | 
				
			||||||
 | 
					    OWNER to postgres;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON TABLE public.groups
 | 
				
			||||||
 | 
					    IS 'Group information';
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/migrations/04_user_permissions.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/migrations/04_user_permissions.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					CREATE TABLE IF NOT EXISTS public.user_permissions
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					    "UserID" character varying(10) COLLATE pg_catalog."default" NOT NULL,
 | 
				
			||||||
 | 
					    "Permission" character varying(250) COLLATE pg_catalog."default" NOT NULL,
 | 
				
			||||||
 | 
					    CONSTRAINT "DistinctUserPermission" UNIQUE ("UserID", "Permission"),
 | 
				
			||||||
 | 
					    CONSTRAINT "UserID" FOREIGN KEY ("UserID")
 | 
				
			||||||
 | 
					        REFERENCES public.users ("UserID") MATCH SIMPLE
 | 
				
			||||||
 | 
					        ON UPDATE NO ACTION
 | 
				
			||||||
 | 
					        ON DELETE NO ACTION
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TABLESPACE pg_default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE IF EXISTS public.user_permissions
 | 
				
			||||||
 | 
					    OWNER to postgres;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON TABLE public.user_permissions
 | 
				
			||||||
 | 
					    IS 'Contains the permissions for every user';
 | 
				
			||||||
							
								
								
									
										18
									
								
								src/migrations/05_group_permssions.sql
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								src/migrations/05_group_permssions.sql
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					CREATE TABLE IF NOT EXISTS public.group_permissions
 | 
				
			||||||
 | 
					(
 | 
				
			||||||
 | 
					    "GroupID" integer NOT NULL,
 | 
				
			||||||
 | 
					    "Permission" character varying(250) COLLATE pg_catalog."default" NOT NULL,
 | 
				
			||||||
 | 
					    CONSTRAINT "DistinctGroupPermission" UNIQUE ("GroupID", "Permission"),
 | 
				
			||||||
 | 
					    CONSTRAINT "GroupID" FOREIGN KEY ("GroupID")
 | 
				
			||||||
 | 
					        REFERENCES public.groups ("GroupID") MATCH SIMPLE
 | 
				
			||||||
 | 
					        ON UPDATE NO ACTION
 | 
				
			||||||
 | 
					        ON DELETE NO ACTION
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					TABLESPACE pg_default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ALTER TABLE IF EXISTS public.group_permissions
 | 
				
			||||||
 | 
					    OWNER to postgres;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					COMMENT ON TABLE public.group_permissions
 | 
				
			||||||
 | 
					    IS 'Group -> Permission relation';
 | 
				
			||||||
							
								
								
									
										6
									
								
								src/utils.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/utils.rs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					pub(crate) fn create_random(len: usize) -> String {
 | 
				
			||||||
 | 
					    rand::Rng::sample_iter(rand::thread_rng(), &rand::distributions::Alphanumeric)
 | 
				
			||||||
 | 
					        .take(len)
 | 
				
			||||||
 | 
					        .map(char::from)
 | 
				
			||||||
 | 
					        .collect()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue
	
	Block a user