Implement user logging in with auth provided from configuration file
This commit is contained in:
parent
279daed555
commit
942161c599
|
@ -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"
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -14,4 +14,22 @@ 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" ]
|
|
@ -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};
|
||||||
|
|
||||||
|
|
|
@ -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 {})
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,11 +67,16 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)]
|
||||||
|
|
21
src/main.rs
21
src/main.rs
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue