implement ldap support

This commit is contained in:
SeanOMik 2023-05-28 23:47:22 -04:00
parent d98a8e3790
commit be59462741
Signed by: SeanOMik
GPG Key ID: 568F326C7EB33ACB
16 changed files with 697 additions and 125 deletions

339
Cargo.lock generated
View File

@ -100,7 +100,7 @@ version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.1.19",
"libc", "libc",
"winapi", "winapi",
] ]
@ -357,6 +357,16 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "core-foundation"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "core-foundation-sys" name = "core-foundation-sys"
version = "0.8.3" version = "0.8.3"
@ -503,12 +513,42 @@ version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91"
[[package]]
name = "errno"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1"
dependencies = [
"errno-dragonfly",
"libc",
"winapi",
]
[[package]]
name = "errno-dragonfly"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf"
dependencies = [
"cc",
"libc",
]
[[package]] [[package]]
name = "event-listener" name = "event-listener"
version = "2.5.3" version = "2.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
[[package]]
name = "fastrand"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
dependencies = [
"instant",
]
[[package]] [[package]]
name = "figment" name = "figment"
version = "0.10.8" version = "0.10.8"
@ -551,6 +591,21 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "foreign-types"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
dependencies = [
"foreign-types-shared",
]
[[package]]
name = "foreign-types-shared"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
[[package]] [[package]]
name = "form_urlencoded" name = "form_urlencoded"
version = "1.1.0" version = "1.1.0"
@ -723,6 +778,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "hermit-abi"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286"
[[package]] [[package]]
name = "hex" name = "hex"
version = "0.4.3" version = "0.4.3"
@ -869,6 +930,17 @@ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "io-lifetimes"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
dependencies = [
"hermit-abi 0.3.1",
"libc",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "itertools" name = "itertools"
version = "0.10.5" version = "0.10.5"
@ -930,10 +1002,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "lber"
version = "0.2.137" version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" checksum = "b5d85f5e00e12cb50c70c3b1c1f0daff6546eb4c608b44d0a990e38a539e0446"
dependencies = [
"bytes",
"nom",
]
[[package]]
name = "ldap3"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5cfbd3c59ca16d6671b002b8b3dd013cd825d9c77a1664a3135194d3270511e"
dependencies = [
"async-trait",
"bytes",
"futures",
"futures-util",
"lazy_static",
"lber",
"log",
"native-tls",
"nom",
"percent-encoding",
"thiserror",
"tokio",
"tokio-native-tls",
"tokio-stream",
"tokio-util",
"url",
]
[[package]]
name = "libc"
version = "0.2.144"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1"
[[package]] [[package]]
name = "libsqlite3-sys" name = "libsqlite3-sys"
@ -955,6 +1061,12 @@ dependencies = [
"cc", "cc",
] ]
[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4"
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.9" version = "0.4.9"
@ -1007,7 +1119,25 @@ dependencies = [
"libc", "libc",
"log", "log",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "windows-sys 0.42.0",
]
[[package]]
name = "native-tls"
version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e"
dependencies = [
"lazy_static",
"libc",
"log",
"openssl",
"openssl-probe",
"openssl-sys",
"schannel",
"security-framework",
"security-framework-sys",
"tempfile",
] ]
[[package]] [[package]]
@ -1055,7 +1185,7 @@ version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5"
dependencies = [ dependencies = [
"hermit-abi", "hermit-abi 0.1.19",
"libc", "libc",
] ]
@ -1071,6 +1201,50 @@ version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
[[package]]
name = "openssl"
version = "0.10.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01b8574602df80f7b85fdfc5392fa884a4e3b3f4f35402c070ab34c3d3f78d56"
dependencies = [
"bitflags 1.3.2",
"cfg-if",
"foreign-types",
"libc",
"once_cell",
"openssl-macros",
"openssl-sys",
]
[[package]]
name = "openssl-macros"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
dependencies = [
"proc-macro2",
"quote 1.0.26",
"syn 2.0.15",
]
[[package]]
name = "openssl-probe"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-sys"
version = "0.9.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e17f59264b2809d77ae94f0e1ebabc434773f370d6ca667bd223ea10e06cc7e"
dependencies = [
"cc",
"libc",
"pkg-config",
"vcpkg",
]
[[package]] [[package]]
name = "orca-registry" name = "orca-registry"
version = "0.1.0" version = "0.1.0"
@ -1093,6 +1267,7 @@ dependencies = [
"hmac", "hmac",
"jws", "jws",
"jwt", "jwt",
"ldap3",
"pin-project-lite", "pin-project-lite",
"qstring", "qstring",
"rand", "rand",
@ -1371,6 +1546,20 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "rustix"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03"
dependencies = [
"bitflags 1.3.2",
"errno",
"io-lifetimes",
"libc",
"linux-raw-sys",
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "rustls" name = "rustls"
version = "0.20.8" version = "0.20.8"
@ -1404,6 +1593,15 @@ version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "schannel"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3"
dependencies = [
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "scopeguard" name = "scopeguard"
version = "1.1.0" version = "1.1.0"
@ -1426,6 +1624,29 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "security-framework"
version = "2.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.147" version = "1.0.147"
@ -1739,6 +1960,19 @@ dependencies = [
"unicode-xid", "unicode-xid",
] ]
[[package]]
name = "tempfile"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95"
dependencies = [
"cfg-if",
"fastrand",
"redox_syscall",
"rustix",
"windows-sys 0.42.0",
]
[[package]] [[package]]
name = "termcolor" name = "termcolor"
version = "1.1.3" version = "1.1.3"
@ -1819,7 +2053,7 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
"socket2", "socket2",
"tokio-macros", "tokio-macros",
"windows-sys", "windows-sys 0.42.0",
] ]
[[package]] [[package]]
@ -1833,6 +2067,16 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "tokio-native-tls"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
dependencies = [
"native-tls",
"tokio",
]
[[package]] [[package]]
name = "tokio-rustls" name = "tokio-rustls"
version = "0.23.4" version = "0.23.4"
@ -1866,6 +2110,7 @@ dependencies = [
"futures-sink", "futures-sink",
"pin-project-lite", "pin-project-lite",
"tokio", "tokio",
"tracing",
] ]
[[package]] [[package]]
@ -2245,13 +2490,37 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
dependencies = [ dependencies = [
"windows_aarch64_gnullvm", "windows_aarch64_gnullvm 0.42.0",
"windows_aarch64_msvc", "windows_aarch64_msvc 0.42.0",
"windows_i686_gnu", "windows_i686_gnu 0.42.0",
"windows_i686_msvc", "windows_i686_msvc 0.42.0",
"windows_x86_64_gnu", "windows_x86_64_gnu 0.42.0",
"windows_x86_64_gnullvm", "windows_x86_64_gnullvm 0.42.0",
"windows_x86_64_msvc", "windows_x86_64_msvc 0.42.0",
]
[[package]]
name = "windows-sys"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
dependencies = [
"windows_aarch64_gnullvm 0.48.0",
"windows_aarch64_msvc 0.48.0",
"windows_i686_gnu 0.48.0",
"windows_i686_msvc 0.48.0",
"windows_x86_64_gnu 0.48.0",
"windows_x86_64_gnullvm 0.48.0",
"windows_x86_64_msvc 0.48.0",
] ]
[[package]] [[package]]
@ -2260,42 +2529,84 @@ version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.42.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.42.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
[[package]]
name = "windows_i686_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.42.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
[[package]]
name = "windows_i686_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.42.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
[[package]] [[package]]
name = "windows_x86_64_gnullvm" name = "windows_x86_64_gnullvm"
version = "0.42.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.42.0" version = "0.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
[[package]] [[package]]
name = "yansi" name = "yansi"
version = "0.5.1" version = "0.5.1"

View File

@ -51,3 +51,4 @@ sha2 = "0.10.6"
rand = "0.8.5" rand = "0.8.5"
bcrypt = "0.14.0" bcrypt = "0.14.0"
bitflags = "2.2.1" bitflags = "2.2.1"
ldap3 = "0.11.1"

View File

@ -8,5 +8,7 @@ pkgs.mkShell {
postgresql postgresql
sqlite sqlite
diesel-cli diesel-cli
openssl
pkg-config
]; ];
} }

View File

@ -15,7 +15,7 @@ use rand::Rng;
use crate::{dto::{scope::Scope, user::{UserAuth, TokenInfo}}, app_state::AppState}; use crate::{dto::{scope::Scope, user::{UserAuth, TokenInfo}}, app_state::AppState};
use crate::database::Database; use crate::database::Database;
use crate::auth_storage::{unauthenticated_response, AuthDriver}; use crate::auth::{unauthenticated_response, AuthDriver};
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct TokenAuthRequest { pub struct TokenAuthRequest {
@ -167,7 +167,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
if let (Some(account), Some(password)) = (&auth.account, auth.password) { if let (Some(account), Some(password)) = (&auth.account, auth.password) {
// Ensure that the password is correct // Ensure that the password is correct
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.verify_user_login(account.clone(), password).await.unwrap() { if !auth_driver.verify_user_login(account.clone(), password).await.unwrap() {
debug!("Authentication failed, incorrect password!"); debug!("Authentication failed, incorrect password!");

View File

@ -8,14 +8,14 @@ use axum::response::{IntoResponse, Response};
use tokio_util::io::ReaderStream; use tokio_util::io::ReaderStream;
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::auth_storage::{unauthenticated_response, AuthDriver}; use crate::auth::{unauthenticated_response, AuthDriver};
use crate::database::Database; use crate::database::Database;
use crate::dto::RepositoryVisibility; use crate::dto::RepositoryVisibility;
use crate::dto::user::{Permission, RegistryUserType, UserAuth}; use crate::dto::user::{Permission, RegistryUserType, UserAuth};
pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response { pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }
@ -40,7 +40,7 @@ pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String
pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response { pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }

View File

@ -7,7 +7,7 @@ use axum::http::{StatusCode, HeaderMap, HeaderName, header};
use tracing::log::warn; use tracing::log::warn;
use tracing::{debug, info}; use tracing::{debug, info};
use crate::auth_storage::{unauthenticated_response, AuthDriver}; use crate::auth::{unauthenticated_response, AuthDriver};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::database::Database; use crate::database::Database;
use crate::dto::RepositoryVisibility; use crate::dto::RepositoryVisibility;
@ -16,7 +16,7 @@ use crate::dto::manifest::Manifest;
use crate::dto::user::{UserAuth, Permission}; use crate::dto::user::{UserAuth, Permission};
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>, body: String) -> Response { pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>, body: String) -> Response {
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }
@ -65,7 +65,7 @@ pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>
pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response { pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }
@ -108,7 +108,7 @@ pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>,
pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response { pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
// Check if the user has permission to pull, or that the repository is public // Check if the user has permission to pull, or that the repository is public
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }
@ -148,7 +148,7 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)
} }
pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, headers: HeaderMap, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response { pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, headers: HeaderMap, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }

View File

@ -14,7 +14,7 @@ pub mod tags;
pub mod catalog; pub mod catalog;
pub mod auth; pub mod auth;
use crate::auth_storage::AuthToken; use crate::auth::AuthToken;
use crate::dto::user::UserAuth; use crate::dto::user::UserAuth;
/// https://docs.docker.com/registry/spec/api/#api-version-check /// https://docs.docker.com/registry/spec/api/#api-version-check

View File

@ -12,14 +12,14 @@ use futures::StreamExt;
use tracing::{debug, warn}; use tracing::{debug, warn};
use crate::app_state::AppState; use crate::app_state::AppState;
use crate::auth_storage::{unauthenticated_response, AuthDriver}; use crate::auth::{unauthenticated_response, AuthDriver};
use crate::byte_stream::ByteStream; use crate::byte_stream::ByteStream;
use crate::database::Database; use crate::database::Database;
use crate::dto::user::{UserAuth, Permission, RegistryUser, RegistryUserType}; use crate::dto::user::{UserAuth, Permission, RegistryUser, RegistryUserType};
/// Starting an upload /// Starting an upload
pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>) -> Response { pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>) -> Response {
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() { if auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
debug!("Upload requested"); debug!("Upload requested");
let uuid = uuid::Uuid::new_v4(); let uuid = uuid::Uuid::new_v4();
@ -39,7 +39,7 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth)
} }
pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>, mut body: BodyStream) -> Response { pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>, mut body: BodyStream) -> Response {
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }
@ -98,7 +98,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
} }
pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<HashMap<String, String>>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>, body: Bytes) -> Response { pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<HashMap<String, String>>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>, body: Bytes) -> Response {
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }
@ -127,7 +127,7 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S
} }
pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response { pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }
@ -141,7 +141,7 @@ pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String
} }
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response { pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Response {
let auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() { if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await.unwrap() {
return unauthenticated_response(&state.config); return unauthenticated_response(&state.config);
} }

View File

@ -1,6 +1,6 @@
use sqlx::{Sqlite, Pool}; use sqlx::{Sqlite, Pool};
use crate::auth_storage::AuthDriver; use crate::auth::AuthDriver;
use crate::storage::StorageDriver; use crate::storage::StorageDriver;
use crate::config::Config; use crate::config::Config;

145
src/auth/ldap_driver.rs Normal file
View File

@ -0,0 +1,145 @@
use std::{slice::Iter, iter::Peekable};
use async_trait::async_trait;
use ldap3::{LdapConnAsync, Ldap, Scope, asn1::PL, ResultEntry, SearchEntry};
use sqlx::{Pool, Sqlite};
use tracing::{debug, warn};
use crate::{config::LdapConnectionConfig, dto::{user::{Permission, LoginSource}, RepositoryVisibility}, database::Database};
use super::AuthDriver;
pub struct LdapAuthDriver {
ldap_config: LdapConnectionConfig,
ldap: Ldap,
database: Pool<Sqlite>,
}
impl LdapAuthDriver {
pub async fn new(config: LdapConnectionConfig, database: Pool<Sqlite>) -> anyhow::Result<Self> {
debug!("connecting to ldap");
let (conn, ldap) = LdapConnAsync::new(&config.connection_url).await?;
ldap3::drive!(conn);
debug!("Created ldap connection!");
Ok(Self {
ldap_config: config,
ldap,
database,
})
}
async fn bind(&mut self) -> anyhow::Result<()> {
let res = self.ldap.simple_bind(&self.ldap_config.bind_dn, &self.ldap_config.bind_password).await?;
res.success()?;
Ok(())
}
/* pub async fn verify_login(&mut self, username: &str, password: &str) -> anyhow::Result<bool> {
self.bind().await?;
let filter = self.ldap_config.user_search_filter.replace("%s", &username);
let res = self.ldap.search(&self.ldap_config.user_base_dn, Scope::Subtree, &filter,
vec!["userPassword", "uid", "cn", "mail", "displayName"]).await?;
let (entries, _res) = res.success()?;
let entries: Vec<SearchEntry> = entries
.into_iter()
.map(|e| SearchEntry::construct(e))
.collect();
if entries.is_empty() {
Ok(false)
} else if entries.len() > 1 {
warn!("Got multiple DNs for user ({}), unsure which one to use!!", username);
Ok(false)
} else {
let entry = entries.first().unwrap();
let res = self.ldap.simple_bind(&entry.dn, password).await?;
if res.rc == 0 {
Ok(true)
} else if res.rc == 49 {
warn!("User failed to auth (invalidCredentials, rc=49)!");
Ok(false)
} else {
// this would fail, its just here to propagate the error down
res.success()?;
Ok(false)
}
}
} */
}
#[async_trait]
impl AuthDriver for LdapAuthDriver {
async fn user_has_permission(&mut self, email: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool> {
self.bind().await?;
// Send a request to LDAP to check if the user is an admin
let filter = format!("(&({}={}){})", &self.ldap_config.login_attribute, email, &self.ldap_config.admin_filter);
let res = self.ldap.search(&self.ldap_config.group_base_dn, Scope::Subtree, &filter, vec!["*"]).await?;
let (entries, _res) = res.success()?;
let entries: Vec<SearchEntry> = entries
.into_iter()
.map(|e| SearchEntry::construct(e))
.collect();
if entries.len() > 0 {
Ok(true)
} else {
debug!("LDAP is falling back to database");
// fall back to database auth since this user might be local
self.database.user_has_permission(email, repository, permission, required_visibility).await
}
}
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool> {
self.bind().await?;
let filter = self.ldap_config.user_search_filter.replace("%s", &email);
let res = self.ldap.search(&self.ldap_config.user_base_dn, Scope::Subtree, &filter,
vec!["userPassword", "uid", "cn", "mail", "displayName"]).await?;
let (entries, _res) = res.success()?;
let entries: Vec<SearchEntry> = entries
.into_iter()
.map(|e| SearchEntry::construct(e))
.collect();
if entries.is_empty() {
Ok(false)
} else if entries.len() > 1 {
warn!("Got multiple DNs for user ({}), unsure which one to use!!", email);
Ok(false)
} else {
let entry = entries.first().unwrap();
let res = self.ldap.simple_bind(&entry.dn, &password).await?;
if res.rc == 0 {
// The user was authenticated through ldap!
// Check if the user is stored in the database, if not, add it.
let database = &self.database;
if !database.does_user_exist(email.clone()).await? {
let display_name = entry.attrs.get(&self.ldap_config.display_name_attribute).unwrap().first().unwrap().clone();
database.create_user(email, display_name, LoginSource::LDAP).await?;
}
Ok(true)
} else if res.rc == 49 {
warn!("User failed to auth (invalidCredentials, rc=49)!");
Ok(false)
} else {
// this would fail, its just here to propagate the error down
res.success()?;
Ok(false)
}
}
}
}

View File

@ -1,3 +1,5 @@
pub mod ldap_driver;
use std::{collections::HashSet, ops::Deref, sync::Arc}; use std::{collections::HashSet, ops::Deref, sync::Arc};
use axum::{extract::{State, Path}, http::{StatusCode, HeaderMap, header, HeaderName, Request}, middleware::Next, response::{Response, IntoResponse}}; use axum::{extract::{State, Path}, http::{StatusCode, HeaderMap, header, HeaderName, Request}, middleware::Next, response::{Response, IntoResponse}};
@ -18,60 +20,51 @@ pub trait AuthDriver: Send + Sync {
/// * `repository`: Name of the repository. /// * `repository`: Name of the repository.
/// * `permissions`: Permission to check for. /// * `permissions`: Permission to check for.
/// * `required_visibility`: Specified if there is a specific visibility of the repository that will give the user permission. /// * `required_visibility`: Specified if there is a specific visibility of the repository that will give the user permission.
async fn user_has_permission(&self, username: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool>; async fn user_has_permission(&mut self, email: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool>;
async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool>; async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool>;
} }
#[async_trait] #[async_trait]
impl AuthDriver for Pool<Sqlite> { impl AuthDriver for Pool<Sqlite> {
async fn user_has_permission(&self, username: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool> { async fn user_has_permission(&mut self, email: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool> {
let allowed_to = { let allowed_to = {
match self.get_user_registry_type(username.clone()).await? { match self.get_user_registry_type(email.clone()).await? {
Some(RegistryUserType::Admin) => true, Some(RegistryUserType::Admin) => true,
_ => { _ => {
if let Some(perms) = self.get_user_repo_permissions(username, repository.clone()).await? { check_user_permissions(self, email, repository, permission, required_visibility).await?
if perms.has_permission(permission) {
return Ok(true);
}
}
if let Some(vis) = required_visibility {
if let Some(repo_vis) = self.get_repository_visibility(&repository).await? {
if vis == repo_vis {
return Ok(true);
}
}
}
false
} }
/* match database.get_user_repo_permissions(username, repository).await.unwrap() {
Some(perms) => if perms.has_permission(permission) { true } else { false },
_ => false,
} */
} }
}; };
Ok(allowed_to) Ok(allowed_to)
} }
async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool> { async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool> {
Database::verify_user_login(self, username, password).await Database::verify_user_login(self, email, password).await
} }
} }
/// Temporary struct for storing auth information in memory. // This ONLY checks permissions, does not check user type
/* pub struct MemoryAuthStorage { pub async fn check_user_permissions<D>(database: &D, email: String, repository: String, permission: Permission, required_visibility: Option<RepositoryVisibility>) -> anyhow::Result<bool>
pub valid_tokens: HashSet<String>, where
} D: Database
{
impl MemoryAuthStorage { if let Some(perms) = database.get_user_repo_permissions(email, repository.clone()).await? {
pub fn new() -> Self { if perms.has_permission(permission) {
Self { return Ok(true);
valid_tokens: HashSet::new(),
} }
} }
} */
if let Some(vis) = required_visibility {
if let Some(repo_vis) = database.get_repository_visibility(&repository).await? {
if vis == repo_vis {
return Ok(true);
}
}
}
Ok(false)
}
#[derive(Clone)] #[derive(Clone)]
pub struct AuthToken(pub String); pub struct AuthToken(pub String);

View File

@ -4,11 +4,37 @@ use serde::Deserialize;
use std::env; use std::env;
#[derive(Deserialize)] #[derive(Deserialize, Clone)]
pub struct LdapConnectionConfig {
pub connection_url: String,
pub bind_dn: String,
pub bind_password: String,
pub user_base_dn: String,
pub group_base_dn: String,
pub user_search_filter: String,
pub group_search_filter: String,
pub admin_filter: String,
#[serde(default = "default_login_attribute")]
pub login_attribute: String,
#[serde(default = "default_display_name_attribute")]
pub display_name_attribute: String,
}
fn default_login_attribute() -> String {
"mail".to_string()
}
fn default_display_name_attribute() -> String {
"displayName".to_string()
}
#[derive(Deserialize, Clone)]
pub struct Config { pub struct Config {
pub listen_address: String, pub listen_address: String,
pub listen_port: String, pub listen_port: String,
pub url: Option<String>, pub url: Option<String>,
pub ldap: Option<LdapConnectionConfig>,
} }
#[allow(dead_code)] #[allow(dead_code)]

View File

@ -4,7 +4,7 @@ use tracing::debug;
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone}; use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
use crate::dto::{Tag, user::{User, RepositoryPermissions, RegistryUserType, Permission, UserAuth, TokenInfo}, RepositoryVisibility}; use crate::dto::{Tag, user::{User, RepositoryPermissions, RegistryUserType, Permission, UserAuth, TokenInfo, LoginSource}, RepositoryVisibility};
#[async_trait] #[async_trait]
pub trait Database { pub trait Database {
@ -50,12 +50,14 @@ pub trait Database {
/// User stuff /// User stuff
async fn create_user(&self, username: String, email: String, password_hash: String, password_salt: String) -> sqlx::Result<User>; async fn does_user_exist(&self, email: String) -> sqlx::Result<bool>;
async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool>; async fn create_user(&self, email: String, username: String, login_source: LoginSource) -> sqlx::Result<User>;
async fn get_user_registry_type(&self, username: String) -> anyhow::Result<Option<RegistryUserType>>; async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> sqlx::Result<()>;
async fn get_user_repo_permissions(&self, username: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>>; async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool>;
async fn get_user_registry_usertype(&self, username: String) -> anyhow::Result<Option<RegistryUserType>>; async fn get_user_registry_type(&self, email: String) -> anyhow::Result<Option<RegistryUserType>>;
async fn store_user_token(&self, token: String, username: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()>; async fn get_user_repo_permissions(&self, email: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>>;
async fn get_user_registry_usertype(&self, email: String) -> anyhow::Result<Option<RegistryUserType>>;
async fn store_user_token(&self, token: String, email: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()>;
async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>>; async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>>;
} }
@ -233,8 +235,7 @@ impl Database for Pool<Sqlite> {
} }
async fn has_repository(&self, repository: &str) -> sqlx::Result<bool> { async fn has_repository(&self, repository: &str) -> sqlx::Result<bool> {
debug!("before query ig"); let row: (u32, ) = match sqlx::query_as("SELECT COUNT(1) FROM repositories WHERE \"name\" = ?")
let row: (u32, ) = match sqlx::query_as("SELECT COUNT(1) repositories WHERE 'name' = ?")
.bind(repository) .bind(repository)
.fetch_one(self).await { .fetch_one(self).await {
Ok(row) => row, Ok(row) => row,
@ -321,33 +322,61 @@ impl Database for Pool<Sqlite> {
Ok(repos) Ok(repos)
} }
async fn create_user(&self, username: String, email: String, password_hash: String, password_salt: String) -> sqlx::Result<User> { async fn does_user_exist(&self, email: String) -> sqlx::Result<bool> {
let row: (u32, ) = match sqlx::query_as("SELECT COUNT(1) FROM users WHERE \"email\" = ?")
.bind(email)
.fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(false)
},
_ => {
return Err(e);
}
}
};
Ok(row.0 > 0)
}
async fn create_user(&self, email: String, username: String, login_source: LoginSource) -> sqlx::Result<User> {
let username = username.to_lowercase(); let username = username.to_lowercase();
let email = email.to_lowercase(); let email = email.to_lowercase();
sqlx::query("INSERT INTO users (username, email, password_hash, password_salt) VALUES (?, ?, ?, ?)") sqlx::query("INSERT INTO users (username, email, login_source) VALUES (?, ?, ?)")
.bind(username.clone()) .bind(username.clone())
.bind(email.clone())
.bind(login_source as u32)
.execute(self).await?;
Ok(User::new(username, email, login_source))
}
async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> sqlx::Result<()> {
let email = email.to_lowercase();
sqlx::query("INSERT INTO user_logins (email, password_hash, password_salt) VALUES (?, ?, ?)")
.bind(email.clone()) .bind(email.clone())
.bind(password_hash) .bind(password_hash)
.bind(password_salt) .bind(password_salt)
.execute(self).await?; .execute(self).await?;
Ok(User::new(username, email)) Ok(())
} }
async fn verify_user_login(&self, username: String, password: String) -> anyhow::Result<bool> { async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool> {
let username = username.to_lowercase(); let email = email.to_lowercase();
let row: (String, ) = sqlx::query_as("SELECT password_hash FROM users WHERE username = ?") let row: (String, ) = sqlx::query_as("SELECT password_hash FROM users WHERE email = ?")
.bind(username) .bind(email)
.fetch_one(self).await?; .fetch_one(self).await?;
Ok(bcrypt::verify(password, &row.0)?) Ok(bcrypt::verify(password, &row.0)?)
} }
async fn get_user_registry_type(&self, username: String) -> anyhow::Result<Option<RegistryUserType>> { async fn get_user_registry_type(&self, email: String) -> anyhow::Result<Option<RegistryUserType>> {
let username = username.to_lowercase(); let email = email.to_lowercase();
let row: (u32, ) = match sqlx::query_as("SELECT user_type FROM user_registry_permissions WHERE username = ?") let row: (u32, ) = match sqlx::query_as("SELECT user_type FROM user_registry_permissions WHERE email = ?")
.bind(username) .bind(email)
.fetch_one(self).await { .fetch_one(self).await {
Ok(row) => row, Ok(row) => row,
Err(e) => match e { Err(e) => match e {
@ -363,11 +392,11 @@ impl Database for Pool<Sqlite> {
Ok(Some(RegistryUserType::try_from(row.0)?)) Ok(Some(RegistryUserType::try_from(row.0)?))
} }
async fn get_user_repo_permissions(&self, username: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>> { async fn get_user_repo_permissions(&self, email: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>> {
let username = username.to_lowercase(); let email = email.to_lowercase();
let row: (u32, ) = match sqlx::query_as("SELECT repository_permissions FROM user_repo_permissions WHERE username = ? AND repository_name = ?") let row: (u32, ) = match sqlx::query_as("SELECT repository_permissions FROM user_repo_permissions WHERE email = ? AND repository_name = ?")
.bind(username.clone()) .bind(email.clone())
.bind(repository.clone()) .bind(repository.clone())
.fetch_one(self).await { .fetch_one(self).await {
Ok(row) => row, Ok(row) => row,
@ -384,7 +413,7 @@ impl Database for Pool<Sqlite> {
let vis = self.get_repository_visibility(&repository).await?.unwrap(); let vis = self.get_repository_visibility(&repository).await?.unwrap();
// Also get the user type for the registry, if its admin return admin repository permissions // Also get the user type for the registry, if its admin return admin repository permissions
let utype = self.get_user_registry_usertype(username).await?.unwrap(); // unwrap should be safe let utype = self.get_user_registry_usertype(email).await?.unwrap(); // unwrap should be safe
if utype == RegistryUserType::Admin { if utype == RegistryUserType::Admin {
Ok(Some(RepositoryPermissions::new(Permission::ADMIN.bits(), vis))) Ok(Some(RepositoryPermissions::new(Permission::ADMIN.bits(), vis)))
} else { } else {
@ -392,22 +421,22 @@ impl Database for Pool<Sqlite> {
} }
} }
async fn get_user_registry_usertype(&self, username: String) -> anyhow::Result<Option<RegistryUserType>> { async fn get_user_registry_usertype(&self, email: String) -> anyhow::Result<Option<RegistryUserType>> {
let username = username.to_lowercase(); let email = email.to_lowercase();
let row: (u32, ) = sqlx::query_as("SELECT user_type FROM user_registry_permissions WHERE username = ?") let row: (u32, ) = sqlx::query_as("SELECT user_type FROM user_registry_permissions WHERE email = ?")
.bind(username) .bind(email)
.fetch_one(self).await?; .fetch_one(self).await?;
Ok(Some(RegistryUserType::try_from(row.0)?)) Ok(Some(RegistryUserType::try_from(row.0)?))
} }
async fn store_user_token(&self, token: String, username: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()> { async fn store_user_token(&self, token: String, email: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()> {
let username = username.to_lowercase(); let email = email.to_lowercase();
let expiry = expiry.timestamp(); let expiry = expiry.timestamp();
let created_at = created_at.timestamp(); let created_at = created_at.timestamp();
sqlx::query("INSERT INTO user_tokens (token, username, expiry, created_at) VALUES (?, ?, ?, ?)") sqlx::query("INSERT INTO user_tokens (token, email, expiry, created_at) VALUES (?, ?, ?, ?)")
.bind(token) .bind(token)
.bind(username) .bind(email)
.bind(expiry) .bind(expiry)
.bind(created_at) .bind(created_at)
.execute(self).await?; .execute(self).await?;
@ -416,18 +445,42 @@ impl Database for Pool<Sqlite> {
} }
async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>> { async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>> {
let token_row: (String, i64, i64, ) = sqlx::query_as("SELECT username, expiry, created_at FROM user_tokens WHERE token = ?") let token_row: (String, i64, i64,) = match sqlx::query_as("SELECT email, expiry, created_at FROM user_tokens WHERE token = ?")
.bind(token.clone()) .bind(token.clone())
.fetch_one(self).await?; .fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(None)
},
_ => {
return Err(anyhow::Error::new(e));
}
}
};
let (username, expiry, created_at) = (token_row.0, token_row.1, token_row.2); let (email, expiry, created_at) = (token_row.0, token_row.1, token_row.2);
let user_row: (String, ) = sqlx::query_as("SELECT email FROM users WHERE username = ?") let user_row: (String, u32) = match sqlx::query_as("SELECT username, login_source FROM users WHERE email = ?")
.bind(username.clone()) .bind(email.clone())
.fetch_one(self).await?; .fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(None)
},
_ => {
return Err(anyhow::Error::new(e));
}
}
};
/* let user_row: (String, u32) = sqlx::query_as("SELECT email, login_source FROM users WHERE email = ?")
.bind(email.clone())
.fetch_one(self).await?; */
let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).unwrap(), Utc.timestamp_millis_opt(created_at).unwrap()); let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).unwrap(), Utc.timestamp_millis_opt(created_at).unwrap());
let user = User::new(username, user_row.0); let user = User::new(email, user_row.0, LoginSource::try_from(user_row.1)?);
let token = TokenInfo::new(token, expiry, created_at); let token = TokenInfo::new(token, expiry, created_at);
let auth = UserAuth::new(user, token); let auth = UserAuth::new(user, token);

View File

@ -35,8 +35,14 @@ CREATE TABLE IF NOT EXISTS manifest_layers (
); );
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
username TEXT NOT NULL UNIQUE PRIMARY KEY, email TEXT NOT NULL UNIQUE PRIMARY KEY,
email TEXT NOT NULL, username TEXT NOT NULL,
-- 0 = local, 1 = ldap
login_source BIGINT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_logins (
email TEXT NOT NULL UNIQUE PRIMARY KEY,
-- bcrypt hashed password -- bcrypt hashed password
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
-- the salt generated along side the password hash -- the salt generated along side the password hash
@ -44,13 +50,13 @@ CREATE TABLE IF NOT EXISTS users (
); );
CREATE TABLE IF NOT EXISTS user_registry_permissions ( CREATE TABLE IF NOT EXISTS user_registry_permissions (
username TEXT NOT NULL UNIQUE PRIMARY KEY, email TEXT NOT NULL UNIQUE PRIMARY KEY,
-- 0 = regular user, 1 = admin -- 0 = regular user, 1 = admin
user_type INTEGER NOT NULL user_type INTEGER NOT NULL
); );
CREATE TABLE IF NOT EXISTS user_repo_permissions ( CREATE TABLE IF NOT EXISTS user_repo_permissions (
username TEXT NOT NULL UNIQUE PRIMARY KEY, email TEXT NOT NULL UNIQUE PRIMARY KEY,
-- name of repository that this user has these permissions in -- name of repository that this user has these permissions in
repository_name TEXT NOT NULL, repository_name TEXT NOT NULL,
-- bitwised integer storing permissions -- bitwised integer storing permissions
@ -59,10 +65,11 @@ CREATE TABLE IF NOT EXISTS user_repo_permissions (
CREATE TABLE IF NOT EXISTS user_tokens ( CREATE TABLE IF NOT EXISTS user_tokens (
token TEXT NOT NULL UNIQUE PRIMARY KEY, token TEXT NOT NULL UNIQUE PRIMARY KEY,
username TEXT NOT NULL, email TEXT NOT NULL,
expiry BIGINT NOT NULL, expiry BIGINT NOT NULL,
created_at BIGINT NOT NULL created_at BIGINT NOT NULL
); );
-- create admin user -- create admin user
INSERT OR IGNORE INTO users (username, email, password_hash, password_salt) VALUES ('admin', 'admin@example.com', '$2b$12$x5ECk0jUmOSfBWxW52wsyOmFxNZkwc2J9FH225if4eBnQYUvYLYYq', 'x5ECk0jUmOSfBWxW52wsyO'); INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('admin', 'admin@example.com', 0);
INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2b$12$x5ECk0jUmOSfBWxW52wsyOmFxNZkwc2J9FH225if4eBnQYUvYLYYq', 'x5ECk0jUmOSfBWxW52wsyO');

View File

@ -5,17 +5,37 @@ use chrono::{DateTime, Utc};
use super::RepositoryVisibility; use super::RepositoryVisibility;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LoginSource {
Database = 0,
LDAP = 1
}
impl TryFrom<u32> for LoginSource {
type Error = anyhow::Error;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
0 => Ok(Self::Database),
1 => Ok(Self::LDAP),
_ => Err(anyhow::anyhow!("Invalid value for LoginSource: `{}`", value)),
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct User { pub struct User {
pub username: String, pub username: String,
pub email: String, pub email: String,
pub source: LoginSource,
} }
impl User { impl User {
pub fn new(username: String, email: String) -> Self { pub fn new(username: String, email: String, source: LoginSource) -> Self {
Self { Self {
username, username,
email, email,
source,
} }
} }
} }

View File

@ -6,13 +6,13 @@ mod storage;
mod byte_stream; mod byte_stream;
mod config; mod config;
mod query; mod query;
mod auth_storage; mod auth;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::str::FromStr; use std::str::FromStr;
use std::sync::Arc; use std::sync::Arc;
use auth_storage::AuthDriver; use auth::{AuthDriver, ldap_driver::LdapAuthDriver};
use axum::http::{Request, StatusCode, header, HeaderName}; use axum::http::{Request, StatusCode, header, HeaderName};
use axum::middleware::Next; use axum::middleware::Next;
use axum::response::{Response, IntoResponse}; use axum::response::{Response, IntoResponse};
@ -29,10 +29,11 @@ use tracing::{debug, Level};
use app_state::AppState; use app_state::AppState;
use database::Database; use database::Database;
use crate::dto::user::Permission;
use crate::storage::StorageDriver; use crate::storage::StorageDriver;
use crate::storage::filesystem::FilesystemDriver; use crate::storage::filesystem::FilesystemDriver;
use crate::config::Config; use crate::config::{Config, LdapConnectionConfig};
use tower_http::trace::TraceLayer; use tower_http::trace::TraceLayer;
@ -60,6 +61,12 @@ async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Resp
#[tokio::main] #[tokio::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
tracing_subscriber::fmt()
.with_max_level(Level::DEBUG)
.init();
let config = Config::new().expect("Failure to parse config!");
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()
.max_connections(15) .max_connections(15)
.connect("test.db").await.unwrap(); .connect("test.db").await.unwrap();
@ -67,18 +74,25 @@ async fn main() -> std::io::Result<()> {
pool.create_schema().await.unwrap(); pool.create_schema().await.unwrap();
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")));
let auth_driver: Mutex<Box<dyn AuthDriver>> = Mutex::new(Box::new(pool.clone()));
// figure out the auth driver depending on whats specified in the config,
// the fallback is a database auth driver.
let auth_driver: Mutex<Box<dyn AuthDriver>> = match config.ldap.clone() {
Some(ldap) => {
let ldap_driver = LdapAuthDriver::new(ldap, pool.clone()).await.unwrap();
Mutex::new(Box::new(ldap_driver))
},
None => {
Mutex::new(Box::new(pool.clone()))
}
};
let config = Config::new().expect("Failure to parse config!");
let app_addr = SocketAddr::from_str(&format!("{}:{}", config.listen_address, config.listen_port)).unwrap(); let app_addr = SocketAddr::from_str(&format!("{}:{}", config.listen_address, config.listen_port)).unwrap();
let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver)); let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver));
tracing_subscriber::fmt() let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::require_auth);
.with_max_level(Level::DEBUG)
.init();
let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth_storage::require_auth);
let path_middleware = axum::middleware::from_fn(change_request_paths); let path_middleware = axum::middleware::from_fn(change_request_paths);
let app = Router::new() let app = Router::new()