Compare commits
1 Commits
main
...
feature/co
Author | SHA1 | Date |
---|---|---|
SeanOMik | 942161c599 |
|
@ -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"
|
||||
|
|
|
@ -52,3 +52,4 @@ rand = "0.8.5"
|
|||
bcrypt = "0.14.0"
|
||||
bitflags = "2.2.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)"
|
||||
#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 static_driver;
|
||||
|
||||
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)]
|
||||
pub struct Config {
|
||||
/// The path that the configuration file was deserialized from
|
||||
#[serde(skip)]
|
||||
pub path: Option<String>,
|
||||
|
||||
pub listen_address: String,
|
||||
pub listen_port: String,
|
||||
pub url: Option<String>,
|
||||
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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<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 {
|
||||
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)]
|
||||
|
|
21
src/main.rs
21
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<Box<dyn StorageDriver>> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs")));
|
||||
|
||||
// figure out the auth driver depending on whats specified in the config,
|
||||
|
|
Loading…
Reference in New Issue