Implement user logging in with auth provided from configuration file

This commit is contained in:
SeanOMik 2023-06-12 00:17:50 -04:00
parent 279daed555
commit 942161c599
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
9 changed files with 348 additions and 5 deletions

55
Cargo.lock generated
View File

@ -558,7 +558,7 @@ dependencies = [
"atomic", "atomic",
"pear", "pear",
"serde", "serde",
"toml", "toml 0.5.11",
"uncased", "uncased",
"version_check", "version_check",
] ]
@ -1280,6 +1280,7 @@ dependencies = [
"sqlx", "sqlx",
"tokio", "tokio",
"tokio-util", "tokio-util",
"toml 0.7.4",
"tower-http", "tower-http",
"tower-layer", "tower-layer",
"tracing", "tracing",
@ -1698,6 +1699,15 @@ dependencies = [
"thiserror", "thiserror",
] ]
[[package]]
name = "serde_spanned"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -2122,6 +2132,40 @@ dependencies = [
"serde", "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]] [[package]]
name = "tower" name = "tower"
version = "0.4.13" version = "0.4.13"
@ -2607,6 +2651,15 @@ version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]]
name = "winnow"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "0.5.1" version = "0.5.1"

View File

@ -52,3 +52,4 @@ rand = "0.8.5"
bcrypt = "0.14.0" bcrypt = "0.14.0"
bitflags = "2.2.1" bitflags = "2.2.1"
ldap3 = "0.11.1" ldap3 = "0.11.1"
toml = "0.7.4"

View File

@ -15,3 +15,21 @@ group_search_filter = "(&(objectclass=groupOfNames)(member=%d))"
admin_filter = "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)" admin_filter = "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)"
#login_attribute = "mail" #login_attribute = "mail"
#display_name_attribute = "displayName" #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" ]

View File

@ -1,4 +1,5 @@
pub mod ldap_driver; pub mod ldap_driver;
pub mod static_driver;
use std::{ops::Deref, sync::Arc}; use std::{ops::Deref, sync::Arc};

219
src/auth/static_driver.rs Normal file
View File

@ -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<Self, Self::Error> {
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<PermissionMatch>);
#[derive(Deserialize)]
struct UserEntry {
name: String,
#[serde(rename = "password")]
password_hash: String,
}
struct Users(HashMap<String, String>);
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<M>(self, mut access: M) -> Result<Self::Value, M::Error>
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<D>(deserializer: D) -> Result<Self, D::Error>
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<UserEntry>,
// email, password hash
#[serde(deserialize_with = "from_user_entries")]
users: HashMap<String, String>,
acl: Vec<AclEntry>,
}
/// Custom deserializer to convert Vec<UserEntry> into HashMap<String, String>
fn from_user_entries<'de, D>(deserializer: D) -> Result<HashMap<String, String>, D::Error>
where
D: Deserializer<'de>,
{
let v: Vec<UserEntry> = 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<P>(path: P) -> anyhow::Result<Self>
where
P: AsRef<Path>
{
let content = std::fs::read_to_string(path)?;
let toml = toml::from_str::<Table>(&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<RepositoryVisibility>) -> anyhow::Result<bool> {
info!("TODO: StaticAuthDriver::user_has_permission");
Ok(true)
}
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool> {
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<E>(self, mut v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error
{
let matches: anyhow::Result<Vec<PermissionMatch>> = 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<D>(deserializer: D) -> Result<Self, D::Error>
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<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>
{
let mut bitset_raw = 0;
while let Some(perm) = seq.next_element::<String>()? {
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>
{
deserializer.deserialize_seq(AclPermissionsVisitor {})
}
}

View File

@ -31,6 +31,10 @@ fn default_display_name_attribute() -> String {
#[derive(Deserialize, Clone)] #[derive(Deserialize, Clone)]
pub struct Config { pub struct Config {
/// The path that the configuration file was deserialized from
#[serde(skip)]
pub path: Option<String>,
pub listen_address: String, pub listen_address: String,
pub listen_port: String, pub listen_port: String,
pub url: Option<String>, pub url: Option<String>,
@ -63,12 +67,17 @@ impl Config {
.join(Toml::file(format!("{}", path))); .join(Toml::file(format!("{}", path)));
let mut config: Config = figment.extract()?; let mut config: Config = figment.extract()?;
// Post process config options
if let Some(url) = config.url.as_mut() { if let Some(url) = config.url.as_mut() {
if url.ends_with("/") { if url.ends_with("/") {
*url = url[..url.len() - 1].to_string(); *url = url[..url.len() - 1].to_string();
} }
} }
config.path = Some(path);
Ok(config) Ok(config)
} }

View File

@ -24,8 +24,9 @@ impl Tag {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum RepositoryVisibility { pub enum RepositoryVisibility {
#[default]
Private = 0, Private = 0,
Public = 1 Public = 1
} }

View File

@ -1,5 +1,6 @@
use std::collections::HashMap; use std::collections::HashMap;
use anyhow::anyhow;
use bitflags::bitflags; use bitflags::bitflags;
use chrono::{DateTime, Utc}; 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<Self, Self::Error> {
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 { pub struct RepositoryPermissions {
perms: u32, perms: u32,
visibility: RepositoryVisibility visibility: RepositoryVisibility
@ -102,6 +118,12 @@ impl RepositoryPermissions {
let perm = perm.bits(); let perm = perm.bits();
self.perms & perm == perm 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)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]

View File

@ -23,11 +23,13 @@ use tower_layer::Layer;
use sqlx::sqlite::SqlitePoolOptions; use sqlx::sqlite::SqlitePoolOptions;
use tokio::sync::Mutex; use tokio::sync::Mutex;
use tower_http::normalize_path::NormalizePathLayer; use tower_http::normalize_path::NormalizePathLayer;
use tracing::{debug, Level}; use tracing::{debug, Level, info};
use app_state::AppState; use app_state::AppState;
use database::Database; use database::Database;
use crate::auth::static_driver::StaticAuthDriver;
use crate::dto::user::Permission;
use crate::storage::StorageDriver; use crate::storage::StorageDriver;
use crate::storage::filesystem::FilesystemDriver; use crate::storage::filesystem::FilesystemDriver;
@ -73,6 +75,23 @@ async fn main() -> anyhow::Result<()> {
.connect("test.db").await?; .connect("test.db").await?;
pool.create_schema().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<Box<dyn StorageDriver>> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs"))); let storage_driver: Mutex<Box<dyn StorageDriver>> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs")));
// figure out the auth driver depending on whats specified in the config, // figure out the auth driver depending on whats specified in the config,