From 942161c599f84108de44e971e391d4a4f1492fad Mon Sep 17 00:00:00 2001 From: SeanOMik Date: Mon, 12 Jun 2023 00:17:50 -0400 Subject: [PATCH] Implement user logging in with auth provided from configuration file --- Cargo.lock | 55 +++++++++- Cargo.toml | 1 + config-example.toml | 20 +++- src/auth/mod.rs | 1 + src/auth/static_driver.rs | 219 ++++++++++++++++++++++++++++++++++++++ src/config.rs | 9 ++ src/dto/mod.rs | 3 +- src/dto/user.rs | 24 ++++- src/main.rs | 21 +++- 9 files changed, 348 insertions(+), 5 deletions(-) create mode 100644 src/auth/static_driver.rs diff --git a/Cargo.lock b/Cargo.lock index 8e639cc..26bfbc2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -558,7 +558,7 @@ dependencies = [ "atomic", "pear", "serde", - "toml", + "toml 0.5.11", "uncased", "version_check", ] @@ -1280,6 +1280,7 @@ dependencies = [ "sqlx", "tokio", "tokio-util", + "toml 0.7.4", "tower-http", "tower-layer", "tracing", @@ -1698,6 +1699,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "serde_spanned" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2122,6 +2132,40 @@ dependencies = [ "serde", ] +[[package]] +name = "toml" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.4.13" @@ -2607,6 +2651,15 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +[[package]] +name = "winnow" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699" +dependencies = [ + "memchr", +] + [[package]] name = "yansi" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index ee6a0ef..6d891b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,3 +52,4 @@ rand = "0.8.5" bcrypt = "0.14.0" bitflags = "2.2.1" ldap3 = "0.11.1" +toml = "0.7.4" diff --git a/config-example.toml b/config-example.toml index 06335b0..fe3eea1 100644 --- a/config-example.toml +++ b/config-example.toml @@ -14,4 +14,22 @@ group_search_filter = "(&(objectclass=groupOfNames)(member=%d))" admin_filter = "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)" #login_attribute = "mail" -#display_name_attribute = "displayName" \ No newline at end of file +#display_name_attribute = "displayName" + +# Example of static auth + +[[static_auth.users]] +name = "admin" +password = "$2y$05$lZjROeq55JnpZlvRGJB4qOum6RXN1qgq586jar6W07tvzYRh7Ur1u" # test1234 + +[[static_auth.users]] +name = "guest" +password = "$2y$05$R2Inj/bckhXpi3kjJN0OxeQhSVExQUEhCq2XwzN3NTB4oLw8iNQQO" # guest1234 + +[[static_auth.acl]] +match = "account=admin" +permissions = [ "*" ] + +[[static_auth.acl]] +match = "account=guest,repository=public" +permissions = [ "pull" ] \ No newline at end of file diff --git a/src/auth/mod.rs b/src/auth/mod.rs index 8ef409e..ef4bc8d 100644 --- a/src/auth/mod.rs +++ b/src/auth/mod.rs @@ -1,4 +1,5 @@ pub mod ldap_driver; +pub mod static_driver; use std::{ops::Deref, sync::Arc}; diff --git a/src/auth/static_driver.rs b/src/auth/static_driver.rs new file mode 100644 index 0000000..14debe8 --- /dev/null +++ b/src/auth/static_driver.rs @@ -0,0 +1,219 @@ +use std::{path::Path, collections::HashMap, error::Error}; + +use anyhow::anyhow; +use async_trait::async_trait; +use serde::{de::{Visitor, MapAccess}, Deserialize, Deserializer}; +use toml::Table; +use tracing::{info, debug}; + +use crate::dto::{scope::Action, user::{Permission, RepositoryPermissions}, RepositoryVisibility}; + +use super::AuthDriver; + +enum PermissionMatch { + Account(String), + Repository(String) +} + +impl TryFrom<&str> for PermissionMatch { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + let (perm_type, perm_val) = value.split_once("=") + .ok_or(anyhow!("No delimiter found!"))?; + + match perm_type { + "account" => Ok(Self::Account(perm_val.to_string())), + "repository" => Ok(Self::Repository(perm_val.to_string())), + _ => Err(anyhow!("Unknown permission type '{}'", perm_type)) + } + } +} + +struct PermissionMatches(Vec); + +#[derive(Deserialize)] +struct UserEntry { + name: String, + #[serde(rename = "password")] + password_hash: String, +} + +struct Users(HashMap); + +struct UsersVisitor; + +impl<'de> Visitor<'de> for UsersVisitor { + type Value = Users; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a Scope in the format of `repository:samalba/my-app:pull,push`.") + } + + fn visit_map(self, mut access: M) -> Result + where + M: MapAccess<'de>, + { + let mut users = HashMap::new(); + + while let Some((key, value)) = access.next_entry()? { + users.insert(key, value); + } + + Ok(Users(users)) + } +} + +impl<'de> Deserialize<'de> for Users { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de> + { + deserializer.deserialize_map(UsersVisitor {}) + } +} + +struct AclPermissions(u32); + +impl AclPermissions { + fn has_permission(&self, perm: Permission) -> bool { + let perm = perm.bits(); + self.0 & perm == perm + } +} + +#[derive(Deserialize)] +struct AclEntry { + #[serde(rename = "match")] + matches: PermissionMatches, + #[serde(rename = "permissions")] + perms: AclPermissions, +} + +/// Auth from a configuration file +#[derive(Deserialize)] +pub struct StaticAuthDriver { + //users: Vec, + // email, password hash + #[serde(deserialize_with = "from_user_entries")] + users: HashMap, + acl: Vec, +} + +/// Custom deserializer to convert Vec into HashMap +fn from_user_entries<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let v: Vec = Deserialize::deserialize(deserializer)?; + + let mut map = HashMap::new(); + for entry in v.into_iter() { + map.insert(entry.name, entry.password_hash); + } + + Ok(map) +} + +impl StaticAuthDriver { + pub fn from_file

(path: P) -> anyhow::Result + where + P: AsRef + { + let content = std::fs::read_to_string(path)?; + let toml = toml::from_str::(&content)?; + let toml = toml.get("static_auth") + .ok_or(anyhow!("Missing `static_auth` at root of toml file!"))? + .as_table() + .unwrap() + .clone(); + + Ok(toml.try_into()?) + } +} + +#[async_trait] +impl AuthDriver for StaticAuthDriver { + async fn user_has_permission(&mut self, email: String, repository: String, permission: Permission, required_visibility: Option) -> anyhow::Result { + info!("TODO: StaticAuthDriver::user_has_permission"); + Ok(true) + } + + async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result { + if let Some(hash) = self.users.get(&email) { + Ok(bcrypt::verify(password, hash)?) + } else { + Ok(false) + } + } +} + +struct PermissionMatchesVisitor; + +impl<'de> Visitor<'de> for PermissionMatchesVisitor { + type Value = PermissionMatches; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("permission matches in the format of `account=guest,repository=public`.") + } + + fn visit_str(self, mut v: &str) -> Result + where + E: serde::de::Error + { + let matches: anyhow::Result> = v.split(",") + .map(|m| PermissionMatch::try_from(m)) + .collect(); + + match matches { + Ok(matches) => Ok(PermissionMatches(matches)), + Err(e) => Err(serde::de::Error::custom(format!("Failure to parse match! {:?}", e))), + } + } +} + +impl<'de> Deserialize<'de> for PermissionMatches { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de> + { + deserializer.deserialize_str(PermissionMatchesVisitor {}) + } +} + +struct AclPermissionsVisitor; + +impl<'de> Visitor<'de> for AclPermissionsVisitor { + type Value = AclPermissions; + + fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result { + formatter.write_str("a Scope in the format of `repository:samalba/my-app:pull,push`.") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de> + { + let mut bitset_raw = 0; + + while let Some(perm) = seq.next_element::()? { + let perm: &str = &perm; + let perm = Permission::try_from(perm) + .map_err(|e| serde::de::Error::custom(format!("Failure to parse match! {:?}", e)))?; + + let perm = perm.bits(); + bitset_raw |= perm; + } + + Ok(AclPermissions(bitset_raw)) + } +} + +impl<'de> Deserialize<'de> for AclPermissions { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de> + { + deserializer.deserialize_seq(AclPermissionsVisitor {}) + } +} \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 57c5029..54d029a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -31,6 +31,10 @@ fn default_display_name_attribute() -> String { #[derive(Deserialize, Clone)] pub struct Config { + /// The path that the configuration file was deserialized from + #[serde(skip)] + pub path: Option, + pub listen_address: String, pub listen_port: String, pub url: Option, @@ -63,11 +67,16 @@ impl Config { .join(Toml::file(format!("{}", path))); let mut config: Config = figment.extract()?; + + // Post process config options + if let Some(url) = config.url.as_mut() { if url.ends_with("/") { *url = url[..url.len() - 1].to_string(); } } + + config.path = Some(path); Ok(config) } diff --git a/src/dto/mod.rs b/src/dto/mod.rs index cc9886c..3d16ca0 100644 --- a/src/dto/mod.rs +++ b/src/dto/mod.rs @@ -24,8 +24,9 @@ impl Tag { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub enum RepositoryVisibility { + #[default] Private = 0, Public = 1 } diff --git a/src/dto/user.rs b/src/dto/user.rs index 6ba557a..f405261 100644 --- a/src/dto/user.rs +++ b/src/dto/user.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; +use anyhow::anyhow; use bitflags::bitflags; use chrono::{DateTime, Utc}; @@ -82,7 +83,22 @@ bitflags! { } } -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +impl TryFrom<&str> for Permission { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + match value { + "pull" => Ok(Self::PULL), + "push" => Ok(Self::PUSH), + "edit" => Ok(Self::EDIT), + "admin" => Ok(Self::ADMIN), + "*" => Ok(Self::ADMIN), + _ => Err(anyhow!("Unknown permission name '{}'!", value)), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)] pub struct RepositoryPermissions { perms: u32, visibility: RepositoryVisibility @@ -102,6 +118,12 @@ impl RepositoryPermissions { let perm = perm.bits(); self.perms & perm == perm } + + pub fn add_permission(&mut self, perm: Permission) { + let perm = perm.bits(); + + self.perms |= perm; + } } #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] diff --git a/src/main.rs b/src/main.rs index 729a814..9a5bb03 100644 --- a/src/main.rs +++ b/src/main.rs @@ -23,11 +23,13 @@ use tower_layer::Layer; use sqlx::sqlite::SqlitePoolOptions; use tokio::sync::Mutex; use tower_http::normalize_path::NormalizePathLayer; -use tracing::{debug, Level}; +use tracing::{debug, Level, info}; use app_state::AppState; use database::Database; +use crate::auth::static_driver::StaticAuthDriver; +use crate::dto::user::Permission; use crate::storage::StorageDriver; use crate::storage::filesystem::FilesystemDriver; @@ -73,6 +75,23 @@ async fn main() -> anyhow::Result<()> { .connect("test.db").await?; pool.create_schema().await?; + { + let mut driver = StaticAuthDriver::from_file(&config.path.clone().unwrap()).unwrap(); + + if driver.verify_user_login("admin".to_string(), "test1234".to_string()).await? { + info!("LOGGED IN!"); + + if driver.user_has_permission("admin".to_string(), "admin/alpine".to_string(), Permission::PULL, None).await? { + info!("user can do that!") + } else { + info!("user can not do that :(") + } + } else { + info!("not logged in :("); + } + + } + let storage_driver: Mutex> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs"))); // figure out the auth driver depending on whats specified in the config,