Compare commits
1 Commits
main
...
feature/co
Author | SHA1 | Date |
---|---|---|
SeanOMik | 942161c599 |
|
@ -1,6 +0,0 @@
|
||||||
/target
|
|
||||||
.env
|
|
||||||
.vscode
|
|
||||||
test.db
|
|
||||||
/registry
|
|
||||||
config.toml
|
|
|
@ -1,5 +1,6 @@
|
||||||
/target
|
/target
|
||||||
.env
|
.env
|
||||||
.vscode
|
.vscode
|
||||||
|
test.db
|
||||||
/registry
|
/registry
|
||||||
config.toml
|
config.toml
|
|
@ -37,12 +37,6 @@ version = "1.0.70"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "arc-swap"
|
|
||||||
version = "1.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argmap"
|
name = "argmap"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
@ -119,9 +113,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "axum"
|
name = "axum"
|
||||||
version = "0.6.18"
|
version = "0.6.16"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
|
checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"axum-core",
|
"axum-core",
|
||||||
|
@ -190,26 +184,6 @@ dependencies = [
|
||||||
"syn 2.0.15",
|
"syn 2.0.15",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "axum-server"
|
|
||||||
version = "0.5.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "447f28c85900215cc1bea282f32d4a2f22d55c5a300afdfbc661c8d6a632e063"
|
|
||||||
dependencies = [
|
|
||||||
"arc-swap",
|
|
||||||
"bytes",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"http-body",
|
|
||||||
"hyper",
|
|
||||||
"pin-project-lite",
|
|
||||||
"rustls 0.21.5",
|
|
||||||
"rustls-pemfile",
|
|
||||||
"tokio",
|
|
||||||
"tokio-rustls 0.24.1",
|
|
||||||
"tower-service",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
|
@ -584,7 +558,7 @@ dependencies = [
|
||||||
"atomic",
|
"atomic",
|
||||||
"pear",
|
"pear",
|
||||||
"serde",
|
"serde",
|
||||||
"toml",
|
"toml 0.5.11",
|
||||||
"uncased",
|
"uncased",
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
@ -768,25 +742,6 @@ version = "0.3.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "h2"
|
|
||||||
version = "0.3.20"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
"fnv",
|
|
||||||
"futures-core",
|
|
||||||
"futures-sink",
|
|
||||||
"futures-util",
|
|
||||||
"http",
|
|
||||||
"indexmap",
|
|
||||||
"slab",
|
|
||||||
"tokio",
|
|
||||||
"tokio-util",
|
|
||||||
"tracing",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
|
@ -894,7 +849,6 @@ dependencies = [
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"h2",
|
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
|
@ -1302,7 +1256,6 @@ dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"axum-auth",
|
"axum-auth",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
"axum-server",
|
|
||||||
"bcrypt",
|
"bcrypt",
|
||||||
"bitflags 2.2.1",
|
"bitflags 2.2.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -1314,7 +1267,6 @@ dependencies = [
|
||||||
"hmac",
|
"hmac",
|
||||||
"jws",
|
"jws",
|
||||||
"jwt",
|
"jwt",
|
||||||
"lazy_static",
|
|
||||||
"ldap3",
|
"ldap3",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"qstring",
|
"qstring",
|
||||||
|
@ -1328,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",
|
||||||
|
@ -1620,18 +1573,6 @@ dependencies = [
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls"
|
|
||||||
version = "0.21.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "79ea77c539259495ce8ca47f53e66ae0330a8819f67e23ac96ca02f50e7b7d36"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"ring",
|
|
||||||
"rustls-webpki",
|
|
||||||
"sct",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-pemfile"
|
name = "rustls-pemfile"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
|
@ -1641,16 +1582,6 @@ dependencies = [
|
||||||
"base64 0.21.0",
|
"base64 0.21.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "rustls-webpki"
|
|
||||||
version = "0.101.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
|
|
||||||
dependencies = [
|
|
||||||
"ring",
|
|
||||||
"untrusted",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.12"
|
version = "1.0.12"
|
||||||
|
@ -1768,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"
|
||||||
|
@ -1917,7 +1857,7 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"paste",
|
"paste",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"rustls 0.20.8",
|
"rustls",
|
||||||
"rustls-pemfile",
|
"rustls-pemfile",
|
||||||
"sha2 0.10.6",
|
"sha2 0.10.6",
|
||||||
"smallvec",
|
"smallvec",
|
||||||
|
@ -1957,7 +1897,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-rustls 0.23.4",
|
"tokio-rustls",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -2153,21 +2093,11 @@ version = "0.23.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rustls 0.20.8",
|
"rustls",
|
||||||
"tokio",
|
"tokio",
|
||||||
"webpki",
|
"webpki",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tokio-rustls"
|
|
||||||
version = "0.24.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081"
|
|
||||||
dependencies = [
|
|
||||||
"rustls 0.21.5",
|
|
||||||
"tokio",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-stream"
|
name = "tokio-stream"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
|
@ -2202,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"
|
||||||
|
@ -2687,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"
|
||||||
|
|
|
@ -32,8 +32,7 @@ sha256 = "1.1.2"
|
||||||
pin-project-lite = "0.2.9"
|
pin-project-lite = "0.2.9"
|
||||||
anyhow = "1.0.70"
|
anyhow = "1.0.70"
|
||||||
async-stream = "0.3.5"
|
async-stream = "0.3.5"
|
||||||
axum = "0.6.18"
|
axum = "0.6.16"
|
||||||
axum-server = { version = "0.5.1", features = [ "tls-rustls" ] }
|
|
||||||
axum-macros = "0.3.7"
|
axum-macros = "0.3.7"
|
||||||
|
|
||||||
tower-http = { version = "0.4.0", features = [ "trace", "normalize-path" ] }
|
tower-http = { version = "0.4.0", features = [ "trace", "normalize-path" ] }
|
||||||
|
@ -53,4 +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"
|
||||||
lazy_static = "1.4.0"
|
toml = "0.7.4"
|
||||||
|
|
47
Dockerfile
47
Dockerfile
|
@ -1,47 +0,0 @@
|
||||||
FROM rust:alpine3.17 as builder
|
|
||||||
|
|
||||||
# update packages
|
|
||||||
RUN apk update
|
|
||||||
RUN apk add build-base openssl-dev ca-certificates
|
|
||||||
|
|
||||||
# create root application folder
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY ./ /app/src
|
|
||||||
|
|
||||||
# Install rust toolchains
|
|
||||||
RUN rustup toolchain install stable
|
|
||||||
RUN rustup default stable
|
|
||||||
|
|
||||||
WORKDIR /app/src
|
|
||||||
|
|
||||||
# Build dependencies only. Separate these for caches
|
|
||||||
RUN cargo install cargo-build-deps
|
|
||||||
RUN cargo build-deps --release
|
|
||||||
|
|
||||||
# Build the release executable.
|
|
||||||
RUN cargo build --release
|
|
||||||
|
|
||||||
# Runner stage. I tried using distroless (gcr.io/distroless/static-debian11), but the image was only ~3MBs smaller than
|
|
||||||
# alpine. I chose to use alpine since a user can easily be added to the image.
|
|
||||||
FROM alpine:3.17
|
|
||||||
|
|
||||||
ARG UNAME=orca-registry
|
|
||||||
ARG UID=1000
|
|
||||||
ARG GID=1000
|
|
||||||
|
|
||||||
# Add user and copy the executable from the build stage.
|
|
||||||
RUN adduser --disabled-password --gecos "" $UNAME -s -G $GID -u $UID
|
|
||||||
COPY --from=builder --chown=$UID:$GID /app/src/target/release/orca-registry /app/orca-registry
|
|
||||||
|
|
||||||
RUN mkdir /data && \
|
|
||||||
chown -R $UID:$GID /data && \
|
|
||||||
chown -R $UID:$GID /app
|
|
||||||
|
|
||||||
USER $UNAME
|
|
||||||
|
|
||||||
WORKDIR /app/
|
|
||||||
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
ENTRYPOINT [ "/app/orca-registry" ]
|
|
46
README.md
46
README.md
|
@ -1,44 +1,4 @@
|
||||||
# Orca registry
|
# Docker registry
|
||||||
Orca is a pure-rust implementation of a Docker Registry.
|
Docker registry written in Rust.
|
||||||
|
|
||||||
Note: Orca is still in early development ([status](#status)).
|
This follows the [docker registry spec](https://docs.docker.com/registry/spec/api/).
|
||||||
|
|
||||||
## Features
|
|
||||||
* Low resource consumption
|
|
||||||
* Easy to deploy
|
|
||||||
* Single application and executable
|
|
||||||
|
|
||||||
## Status
|
|
||||||
The project is still in early development, use at your own risk. Although the registry does work, and you can push and pull images from it, there is no simple way to modify user permissions and to add users to the registry. Currently, the only way to add a user and, modify their permissions, is to edit the sqlite database.
|
|
||||||
|
|
||||||
### Adding users
|
|
||||||
These instructions are assuming the user is stored in the database, if you use LDAP auth, users are created automatically and you don't need all this.
|
|
||||||
|
|
||||||
> Note: These instructions are subject to change or quickly become outdated without notes in the instructions.
|
|
||||||
|
|
||||||
1. Open the sqlite database in an editor.
|
|
||||||
|
|
||||||
2. Create a bcrypt password hash for the new user:
|
|
||||||
```shell
|
|
||||||
$ htpasswd -nB
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Insert the new user's email, password hash into the `user_logins` table. The salt is not used, so you can put whatever there
|
|
||||||
> WARNING: Ensure that the username is all lowercase!!!
|
|
||||||
```sql
|
|
||||||
INSERT INTO user_logins (email, password_hash, password_salt) VALUES ("example@email.com", "some password", "random salt")
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Insert the new user into another table, `users` so the registry knows the source of the user
|
|
||||||
> WARNING: Ensure that the username is all lowercase!!!
|
|
||||||
```sql
|
|
||||||
INSERT INTO users (username, email, login_source) VALUES ("example", "example@email.com", 0)
|
|
||||||
```
|
|
||||||
a `login_source` of `0` means database
|
|
||||||
|
|
||||||
1. Give the user registry permissions
|
|
||||||
> WARNING: Ensure that the username is all lowercase!!!
|
|
||||||
```sql
|
|
||||||
INSERT INTO user_registry_permissions (email, user_type) VALUES ("example@email.com", 1)
|
|
||||||
```
|
|
||||||
a `user_type` of `1` means admin, they have permission for all image repositories.
|
|
|
@ -2,27 +2,34 @@ listen_address = "127.0.0.1"
|
||||||
listen_port = "3000"
|
listen_port = "3000"
|
||||||
url = "http://localhost:3000/"
|
url = "http://localhost:3000/"
|
||||||
|
|
||||||
# error, warn, info, debug, trace
|
[ldap]
|
||||||
log_level = "debug"
|
connection_url = "ldap://localhost:389"
|
||||||
|
bind_dn = "cn=admin,dc=planetexpress,dc=com"
|
||||||
|
bind_password = "GoodNewsEveryone"
|
||||||
|
user_base_dn = "ou=people,dc=planetexpress,dc=com"
|
||||||
|
group_base_dn = "ou=people,dc=planetexpress,dc=com"
|
||||||
|
|
||||||
[storage]
|
user_search_filter = "(&(objectClass=person)(mail=%s))"
|
||||||
driver = "filesystem"
|
group_search_filter = "(&(objectclass=groupOfNames)(member=%d))"
|
||||||
path = "/app/blobs"
|
|
||||||
|
|
||||||
[database]
|
admin_filter = "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)"
|
||||||
type = "sqlite"
|
|
||||||
path = "/app/orca.db"
|
|
||||||
|
|
||||||
#[ldap]
|
|
||||||
#connection_url = "ldap://localhost:389"
|
|
||||||
#bind_dn = "cn=admin,dc=planetexpress,dc=com"
|
|
||||||
#bind_password = "GoodNewsEveryone"
|
|
||||||
#user_base_dn = "ou=people,dc=planetexpress,dc=com"
|
|
||||||
#group_base_dn = "ou=people,dc=planetexpress,dc=com"
|
|
||||||
#
|
|
||||||
#user_search_filter = "(&(objectClass=person)(mail=%s))"
|
|
||||||
#group_search_filter = "(&(objectclass=groupOfNames)(member=%d))"
|
|
||||||
#
|
|
||||||
#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,7 +0,0 @@
|
||||||
--- Creates a regular user with the password 'test'
|
|
||||||
INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('test', 'test@example.com', 0);
|
|
||||||
INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('test@example.com', '$2y$05$k3gn.RxGxh59NhtyyiWPeeQ2J9kqVaImiL3GPuBjMsiJ51Bn3js.K', 'x5ECk0jUmOSfBWxW52wsyO');
|
|
||||||
INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('test@example.com', 0);
|
|
||||||
|
|
||||||
-- example of giving this user pull access to a repository
|
|
||||||
--INSERT OR IGNORE INTO user_repo_permissions (email, repository_name, repository_permissions) VALUES ('test@example.com', 'admin/alpine', 1);
|
|
14
docs/todo.md
14
docs/todo.md
|
@ -2,19 +2,9 @@
|
||||||
- [x] Simple auth
|
- [x] Simple auth
|
||||||
- [x] ldap auth
|
- [x] ldap auth
|
||||||
- [ ] permission stuff
|
- [ ] permission stuff
|
||||||
- [ ] simple way to define users and their permissions through a "users.toml"
|
- [ ] Only allow users to create repositories if its the same name as their username, or if they're an admin
|
||||||
- [x] Only allow users to create repositories if its the same name as their username, or if they're an admin
|
- [ ] Only allow users to pull from their own repositories
|
||||||
- [x] Only allow users to pull from their own repositories
|
|
||||||
- [ ] token expiry
|
|
||||||
- [ ] postgresql
|
- [ ] postgresql
|
||||||
- [ ] prometheus metrics
|
- [ ] prometheus metrics
|
||||||
- [ ] simple webui for managing the registry
|
|
||||||
- [x] streaming layer bytes into providers
|
- [x] streaming layer bytes into providers
|
||||||
- [x] streaming layer bytes from providers
|
- [x] streaming layer bytes from providers
|
||||||
- [ ] better client error messages
|
|
||||||
- [ ] fix repository list
|
|
||||||
- [ ] its not responding with anything
|
|
||||||
- [ ] make sure private repositories dont show up
|
|
||||||
- [x] fix pulling from public repositories when not logged in
|
|
||||||
- [ ] database table for orca related info (version, etc.)
|
|
||||||
- [ ] only execute sql schemas if this table is missing or not updated
|
|
|
@ -15,7 +15,7 @@ use rand::Rng;
|
||||||
use crate::{dto::{scope::Scope, user::TokenInfo}, app_state::AppState};
|
use crate::{dto::{scope::Scope, user::TokenInfo}, app_state::AppState};
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
||||||
use crate::auth::auth_challenge_response;
|
use crate::auth::unauthenticated_response;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct TokenAuthRequest {
|
pub struct TokenAuthRequest {
|
||||||
|
@ -179,8 +179,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? {
|
||||||
debug!("Authentication failed, incorrect password!");
|
debug!("Authentication failed, incorrect password!");
|
||||||
|
|
||||||
// TODO: Dont unwrap, find a way to return multiple scopes
|
return Ok(unauthenticated_response(&state.config));
|
||||||
return Ok(auth_challenge_response(&state.config, Some(auth.scope.first().unwrap().clone())));
|
|
||||||
}
|
}
|
||||||
drop(auth_driver);
|
drop(auth_driver);
|
||||||
|
|
||||||
|
@ -188,11 +187,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
||||||
|
|
||||||
let now = SystemTime::now();
|
let now = SystemTime::now();
|
||||||
let token = create_jwt_token(account)
|
let token = create_jwt_token(account)
|
||||||
.map_err(|_| {
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
error!("Failed to create jwt token!");
|
|
||||||
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
let token_str = token.token;
|
let token_str = token.token;
|
||||||
|
|
||||||
debug!("Created jwt token");
|
debug!("Created jwt token");
|
||||||
|
@ -213,11 +208,7 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
|
||||||
|
|
||||||
let database = &state.database;
|
let database = &state.database;
|
||||||
database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await
|
database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await
|
||||||
.map_err(|_| {
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
error!("Failed to store user token in database!");
|
|
||||||
|
|
||||||
StatusCode::INTERNAL_SERVER_ERROR
|
|
||||||
})?;
|
|
||||||
drop(database);
|
drop(database);
|
||||||
|
|
||||||
return Ok((
|
return Ok((
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::Extension;
|
||||||
use axum::body::StreamBody;
|
use axum::body::StreamBody;
|
||||||
use axum::extract::{State, Path};
|
use axum::extract::{State, Path};
|
||||||
use axum::http::{StatusCode, header, HeaderName};
|
use axum::http::{StatusCode, header, HeaderName};
|
||||||
|
@ -7,9 +8,19 @@ 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::unauthenticated_response;
|
||||||
|
use crate::dto::RepositoryVisibility;
|
||||||
|
use crate::dto::user::{Permission, UserAuth};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
pub async fn digest_exists_head(Path((_name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
pub async fn digest_exists_head(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
||||||
|
// Check if the user has permission to pull, or that the repository is public
|
||||||
|
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? {
|
||||||
|
return Ok(unauthenticated_response(&state.config));
|
||||||
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
|
|
||||||
if storage.has_digest(&layer_digest).await? {
|
if storage.has_digest(&layer_digest).await? {
|
||||||
|
@ -27,7 +38,14 @@ pub async fn digest_exists_head(Path((_name, layer_digest)): Path<(String, Strin
|
||||||
Ok(StatusCode::NOT_FOUND.into_response())
|
Ok(StatusCode::NOT_FOUND.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn pull_digest_get(Path((_name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
pub async fn pull_digest_get(Path((name, layer_digest)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
||||||
|
// Check if the user has permission to pull, or that the repository is public
|
||||||
|
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? {
|
||||||
|
return Ok(unauthenticated_response(&state.config));
|
||||||
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
|
|
||||||
if let Some(len) = storage.digest_length(&layer_digest).await? {
|
if let Some(len) = storage.digest_length(&layer_digest).await? {
|
||||||
|
|
|
@ -36,7 +36,7 @@ pub async fn list_repositories(Query(params): Query<ListRepositoriesParams>, sta
|
||||||
let last_repo = repos.last().and_then(|s| Some(s.clone()));
|
let last_repo = repos.last().and_then(|s| Some(s.clone()));
|
||||||
|
|
||||||
// Construct the link header
|
// Construct the link header
|
||||||
let url = &state.config.url();
|
let url = &state.config.get_url();
|
||||||
let mut url = format!("<{}/v2/_catalog?n={}", url, limit);
|
let mut url = format!("<{}/v2/_catalog?n={}", url, limit);
|
||||||
if let Some(last_repo) = last_repo {
|
if let Some(last_repo) = last_repo {
|
||||||
url += &format!("&limit={}", last_repo);
|
url += &format!("&limit={}", last_repo);
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::Extension;
|
||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::response::{Response, IntoResponse};
|
use axum::response::{Response, IntoResponse};
|
||||||
use axum::http::{StatusCode, HeaderName, header};
|
use axum::http::{StatusCode, HeaderName, header};
|
||||||
use tracing::log::warn;
|
use tracing::log::warn;
|
||||||
use tracing::{debug, info};
|
use tracing::{debug, info};
|
||||||
|
|
||||||
use crate::auth::access_denied_response;
|
use crate::auth::unauthenticated_response;
|
||||||
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;
|
||||||
|
@ -15,7 +16,13 @@ use crate::dto::manifest::Manifest;
|
||||||
use crate::dto::user::{UserAuth, Permission};
|
use crate::dto::user::{UserAuth, Permission};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth, body: String) -> Result<Response, AppError> {
|
pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>, body: String) -> Result<Response, AppError> {
|
||||||
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
||||||
|
return Ok(unauthenticated_response(&state.config));
|
||||||
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
// Calculate the sha256 digest for the manifest.
|
// Calculate the sha256 digest for the manifest.
|
||||||
let calculated_hash = sha256::digest(body.clone());
|
let calculated_hash = sha256::digest(body.clone());
|
||||||
let calculated_digest = format!("sha256:{}", calculated_hash);
|
let calculated_digest = format!("sha256:{}", calculated_hash);
|
||||||
|
@ -23,7 +30,7 @@ pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>
|
||||||
let database = &state.database;
|
let database = &state.database;
|
||||||
|
|
||||||
// Create the image repository and save the image manifest. This repository will be private by default
|
// Create the image repository and save the image manifest. This repository will be private by default
|
||||||
database.save_repository(&name, RepositoryVisibility::Private, Some(auth.user.email), None).await?;
|
database.save_repository(&name, RepositoryVisibility::Private, None).await?;
|
||||||
database.save_manifest(&name, &calculated_digest, &body).await?;
|
database.save_manifest(&name, &calculated_digest, &body).await?;
|
||||||
|
|
||||||
// If the reference is not a digest, then it must be a tag name.
|
// If the reference is not a digest, then it must be a tag name.
|
||||||
|
@ -57,19 +64,13 @@ 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>>, auth: Option<UserAuth>) -> Result<Response, AppError> {
|
pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
||||||
// 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
|
||||||
if let Some(auth) = auth {
|
|
||||||
let mut 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? {
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? {
|
||||||
return Ok(access_denied_response(&state.config));
|
return Ok(unauthenticated_response(&state.config));
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let database = &state.database;
|
|
||||||
if database.get_repository_visibility(&name).await? != Some(RepositoryVisibility::Public) {
|
|
||||||
return Ok(access_denied_response(&state.config));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
let database = &state.database;
|
let database = &state.database;
|
||||||
let digest = match Digest::is_digest(&reference) {
|
let digest = match Digest::is_digest(&reference) {
|
||||||
|
@ -106,20 +107,13 @@ pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>,
|
||||||
).into_response())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> {
|
pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
||||||
// 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
|
||||||
if let Some(auth) = auth {
|
|
||||||
let mut 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? {
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PULL, Some(RepositoryVisibility::Public)).await? {
|
||||||
return Ok(access_denied_response(&state.config));
|
return Ok(unauthenticated_response(&state.config));
|
||||||
}
|
}
|
||||||
drop(auth_driver);
|
drop(auth_driver);
|
||||||
} else {
|
|
||||||
let database = &state.database;
|
|
||||||
if database.get_repository_visibility(&name).await? != Some(RepositoryVisibility::Public) {
|
|
||||||
return Ok(access_denied_response(&state.config));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the digest from the reference path.
|
// Get the digest from the reference path.
|
||||||
let database = &state.database;
|
let database = &state.database;
|
||||||
|
@ -154,10 +148,10 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)
|
||||||
).into_response())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: UserAuth) -> Result<Response, AppError> {
|
pub async fn delete_manifest(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
||||||
let mut 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? {
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
||||||
return Ok(access_denied_response(&state.config));
|
return Ok(unauthenticated_response(&state.config));
|
||||||
}
|
}
|
||||||
drop(auth_driver);
|
drop(auth_driver);
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::Extension;
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::http::{StatusCode, HeaderName};
|
use axum::http::{StatusCode, HeaderName};
|
||||||
|
@ -13,13 +14,13 @@ pub mod tags;
|
||||||
pub mod catalog;
|
pub mod catalog;
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
|
|
||||||
|
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
|
||||||
/// full endpoint: `/v2/`
|
/// full endpoint: `/v2/`
|
||||||
pub async fn version_check(_state: State<Arc<AppState>>) -> Response {
|
pub async fn version_check(Extension(_auth): Extension<UserAuth>, _state: State<Arc<AppState>>) -> Response {
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::OK,
|
||||||
[
|
[( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )]
|
||||||
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" ),
|
|
||||||
]
|
|
||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
|
@ -37,7 +37,7 @@ pub async fn list_tags(Path((name, )): Path<(String, )>, Query(params): Query<Li
|
||||||
let last_tag = tags.last();
|
let last_tag = tags.last();
|
||||||
|
|
||||||
// Construct the link header
|
// Construct the link header
|
||||||
let url = &state.config.url();
|
let url = &state.config.get_url();
|
||||||
let mut url = format!("<{}/v2/{}/tags/list?n={}", url, name, limit);
|
let mut url = format!("<{}/v2/{}/tags/list?n={}", url, name, limit);
|
||||||
if let Some(last_tag) = last_tag {
|
if let Some(last_tag) = last_tag {
|
||||||
url += &format!("&limit={}", last_tag.name);
|
url += &format!("&limit={}", last_tag.name);
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||||
use std::io::ErrorKind;
|
use std::io::ErrorKind;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use axum::Extension;
|
||||||
use axum::http::{StatusCode, header, HeaderName};
|
use axum::http::{StatusCode, header, HeaderName};
|
||||||
use axum::extract::{Path, BodyStream, State, Query};
|
use axum::extract::{Path, BodyStream, State, Query};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
@ -11,11 +12,15 @@ use futures::StreamExt;
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
use crate::auth::unauthenticated_response;
|
||||||
use crate::byte_stream::ByteStream;
|
use crate::byte_stream::ByteStream;
|
||||||
|
use crate::dto::user::{UserAuth, Permission};
|
||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
/// Starting an upload
|
/// Starting an upload
|
||||||
pub async fn start_upload_post(Path((name, )): Path<(String, )>) -> Result<Response, AppError> {
|
pub async fn start_upload_post(Path((name, )): Path<(String, )>, Extension(auth): Extension<UserAuth>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
||||||
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
|
if auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
||||||
debug!("Upload requested");
|
debug!("Upload requested");
|
||||||
let uuid = uuid::Uuid::new_v4();
|
let uuid = uuid::Uuid::new_v4();
|
||||||
|
|
||||||
|
@ -28,9 +33,18 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>) -> Result<Respo
|
||||||
StatusCode::ACCEPTED,
|
StatusCode::ACCEPTED,
|
||||||
[ (header::LOCATION, location) ]
|
[ (header::LOCATION, location) ]
|
||||||
).into_response());
|
).into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(unauthenticated_response(&state.config))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, mut body: BodyStream) -> Result<Response, AppError> {
|
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) -> Result<Response, AppError> {
|
||||||
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
||||||
|
return Ok(unauthenticated_response(&state.config));
|
||||||
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
let current_size = storage.digest_length(&layer_uuid).await?;
|
let current_size = storage.digest_length(&layer_uuid).await?;
|
||||||
|
|
||||||
|
@ -71,7 +85,7 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
|
||||||
(0, written_size)
|
(0, written_size)
|
||||||
};
|
};
|
||||||
|
|
||||||
let full_uri = format!("{}/v2/{}/blobs/uploads/{}", state.config.url(), name, layer_uuid);
|
let full_uri = format!("{}/v2/{}/blobs/uploads/{}", state.config.get_url(), name, layer_uuid);
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::ACCEPTED,
|
StatusCode::ACCEPTED,
|
||||||
[
|
[
|
||||||
|
@ -83,7 +97,13 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
|
||||||
).into_response())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, String)>, Query(query): Query<HashMap<String, String>>, state: State<Arc<AppState>>, body: Bytes) -> Result<Response, AppError> {
|
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) -> Result<Response, AppError> {
|
||||||
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
||||||
|
return Ok(unauthenticated_response(&state.config));
|
||||||
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
let digest = query.get("digest").unwrap();
|
let digest = query.get("digest").unwrap();
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
|
@ -106,7 +126,13 @@ pub async fn finish_chunked_upload_put(Path((name, layer_uuid)): Path<(String, S
|
||||||
).into_response())
|
).into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn cancel_upload_delete(Path((_name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
pub async fn cancel_upload_delete(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
||||||
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
||||||
|
return Ok(unauthenticated_response(&state.config));
|
||||||
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
storage.delete_digest(&layer_uuid).await?;
|
storage.delete_digest(&layer_uuid).await?;
|
||||||
|
|
||||||
|
@ -114,7 +140,13 @@ pub async fn cancel_upload_delete(Path((_name, layer_uuid)): Path<(String, Strin
|
||||||
Ok(StatusCode::OK.into_response())
|
Ok(StatusCode::OK.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>) -> Result<Response, AppError> {
|
pub async fn check_upload_status_get(Path((name, layer_uuid)): Path<(String, String)>, state: State<Arc<AppState>>, Extension(auth): Extension<UserAuth>) -> Result<Response, AppError> {
|
||||||
|
let mut auth_driver = state.auth_checker.lock().await;
|
||||||
|
if !auth_driver.user_has_permission(auth.user.username, name.clone(), Permission::PUSH, None).await? {
|
||||||
|
return Ok(unauthenticated_response(&state.config));
|
||||||
|
}
|
||||||
|
drop(auth_driver);
|
||||||
|
|
||||||
let storage = state.storage.lock().await;
|
let storage = state.storage.lock().await;
|
||||||
let ending = storage.digest_length(&layer_uuid).await?.unwrap_or(0);
|
let ending = storage.digest_length(&layer_uuid).await?.unwrap_or(0);
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use ldap3::{LdapConnAsync, Ldap, Scope, SearchEntry};
|
||||||
use sqlx::{Pool, Sqlite};
|
use sqlx::{Pool, Sqlite};
|
||||||
use tracing::{debug, warn};
|
use tracing::{debug, warn};
|
||||||
|
|
||||||
use crate::{config::LdapConnectionConfig, dto::{user::{Permission, LoginSource, RegistryUserType, self}, RepositoryVisibility}, database::Database};
|
use crate::{config::LdapConnectionConfig, dto::{user::{Permission, LoginSource, RegistryUserType}, RepositoryVisibility}, database::Database};
|
||||||
|
|
||||||
use super::AuthDriver;
|
use super::AuthDriver;
|
||||||
|
|
||||||
|
@ -60,7 +60,6 @@ impl AuthDriver for LdapAuthDriver {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
} else {
|
} else {
|
||||||
debug!("LDAP is falling back to database");
|
debug!("LDAP is falling back to database");
|
||||||
|
|
||||||
// fall back to database auth since this user might be local
|
// fall back to database auth since this user might be local
|
||||||
self.database.user_has_permission(email, repository, permission, required_visibility).await
|
self.database.user_has_permission(email, repository, permission, required_visibility).await
|
||||||
}
|
}
|
||||||
|
|
131
src/auth/mod.rs
131
src/auth/mod.rs
|
@ -1,12 +1,14 @@
|
||||||
pub mod ldap_driver;
|
pub mod ldap_driver;
|
||||||
|
pub mod static_driver;
|
||||||
|
|
||||||
use std::{ops::Deref, sync::Arc};
|
use std::{ops::Deref, sync::Arc};
|
||||||
|
|
||||||
use axum::{extract::State, http::{StatusCode, HeaderMap, header, HeaderName, Request, Method}, middleware::Next, response::{Response, IntoResponse}};
|
use axum::{extract::State, http::{StatusCode, HeaderMap, header, HeaderName, Request}, middleware::Next, response::{Response, IntoResponse}};
|
||||||
|
|
||||||
use tracing::{debug, warn, error};
|
use sqlx::{Pool, Sqlite};
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType, UserAuth}, RepositoryVisibility, scope::{Scope, ScopeType, Action}}, config::Config};
|
use crate::{app_state::AppState, dto::{user::{Permission, RegistryUserType}, RepositoryVisibility}, config::Config};
|
||||||
use crate::database::Database;
|
use crate::database::Database;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
@ -23,36 +25,19 @@ pub trait AuthDriver: Send + Sync {
|
||||||
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool>;
|
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implement AuthDriver for anything the implements Database
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<T> AuthDriver for T
|
impl AuthDriver for Pool<Sqlite> {
|
||||||
where
|
|
||||||
T: Database + Send + Sync
|
|
||||||
{
|
|
||||||
async fn user_has_permission(&mut self, email: 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> {
|
||||||
match self.get_repository_owner(&repository).await? {
|
let allowed_to = {
|
||||||
Some(owner) if owner == email => return Ok(true),
|
|
||||||
Some(_other_owner) => {
|
|
||||||
match self.get_user_registry_type(email.clone()).await? {
|
match self.get_user_registry_type(email.clone()).await? {
|
||||||
Some(RegistryUserType::Admin) => return Ok(true),
|
Some(RegistryUserType::Admin) => true,
|
||||||
_ => {
|
_ => {
|
||||||
return Ok(check_user_permissions(self, email, repository, permission, required_visibility).await?);
|
check_user_permissions(self, email, repository, permission, required_visibility).await?
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
};
|
||||||
None => {
|
|
||||||
// If the repository does not exist, see if its the per-user repositories and autocreate it.
|
|
||||||
if let Some(user) = self.get_user(email.clone()).await? {
|
|
||||||
let username = user.username.to_lowercase();
|
|
||||||
if repository.starts_with(&username) {
|
|
||||||
self.save_repository(&repository, RepositoryVisibility::Private, Some(email), None).await?;
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(false)
|
Ok(allowed_to)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool> {
|
async fn verify_user_login(&mut self, email: String, password: String) -> anyhow::Result<bool> {
|
||||||
|
@ -96,7 +81,7 @@ impl Deref for AuthToken {
|
||||||
type Rejection = (StatusCode, HeaderMap);
|
type Rejection = (StatusCode, HeaderMap);
|
||||||
|
|
||||||
pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Request<B>, next: Next<B>) -> Result<Response, Rejection> {
|
pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Request<B>, next: Next<B>) -> Result<Response, Rejection> {
|
||||||
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url());
|
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url());
|
||||||
let mut failure_headers = HeaderMap::new();
|
let mut failure_headers = HeaderMap::new();
|
||||||
failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap());
|
failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap());
|
||||||
failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap());
|
failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap());
|
||||||
|
@ -124,7 +109,7 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
|
||||||
|
|
||||||
Ok(next.run(request).await)
|
Ok(next.run(request).await)
|
||||||
} else {
|
} else {
|
||||||
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url());
|
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url());
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
[
|
[
|
||||||
|
@ -135,100 +120,14 @@ pub async fn require_auth<B>(State(state): State<Arc<AppState>>, mut request: Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates a response with an Unauthorized (401) status code.
|
|
||||||
/// The www-authenticate header is set to notify the client of where to authorize with.
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn auth_challenge_response(config: &Config, scope: Option<Scope>) -> Response {
|
pub fn unauthenticated_response(config: &Config) -> Response {
|
||||||
let bearer = match scope {
|
let bearer = format!("Bearer realm=\"{}/auth\"", config.get_url());
|
||||||
Some(scope) => format!("Bearer realm=\"{}/auth\",scope=\"{}\"", config.url(), scope),
|
|
||||||
None => format!("Bearer realm=\"{}/auth\"", config.url())
|
|
||||||
};
|
|
||||||
debug!("responding with www-authenticate header of: \"{}\"", bearer);
|
|
||||||
|
|
||||||
(
|
(
|
||||||
StatusCode::UNAUTHORIZED,
|
StatusCode::UNAUTHORIZED,
|
||||||
[
|
[
|
||||||
( header::WWW_AUTHENTICATE, bearer ),
|
( header::WWW_AUTHENTICATE, bearer ),
|
||||||
( header::CONTENT_TYPE, "application/json".to_string() ),
|
|
||||||
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
|
|
||||||
],
|
|
||||||
//"{\"errors\":[{\"code\":\"UNAUTHORIZED\",\"message\":\"access to the requested resource is not authorized\",\"detail\":[{\"Type\":\"repository\",\"Name\":\"samalba/my-app\",\"Action\":\"pull\"},{\"Type\":\"repository\",\"Name\":\"samalba/my-app\",\"Action\":\"push\"}]}]}"
|
|
||||||
).into_response()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Creates a response with a Forbidden (403) status code.
|
|
||||||
/// No other headers are set.
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn access_denied_response(_config: &Config) -> Response {
|
|
||||||
(
|
|
||||||
StatusCode::FORBIDDEN,
|
|
||||||
[
|
|
||||||
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
|
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
|
||||||
]
|
]
|
||||||
).into_response()
|
).into_response()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_auth<B>(State(state): State<Arc<AppState>>, auth: Option<UserAuth>, request: Request<B>, next: Next<B>) -> Result<Response, Rejection> {
|
|
||||||
let config = &state.config;
|
|
||||||
// note: url is relative to /v2
|
|
||||||
let url = request.uri().to_string();
|
|
||||||
|
|
||||||
if url == "/" && auth.is_none() {
|
|
||||||
debug!("Responding to /v2/ with an auth challenge");
|
|
||||||
return Ok(auth_challenge_response(config, None));
|
|
||||||
}
|
|
||||||
|
|
||||||
let url_split: Vec<&str> = url.split("/").skip(1).collect();
|
|
||||||
let target_name = url_split[0].replace("%2F", "/");
|
|
||||||
let target_type = url_split[1];
|
|
||||||
|
|
||||||
// check if the request is targeting something inside an image repository
|
|
||||||
if target_type == "blobs" || target_type == "uploads" || target_type == "manifests" {
|
|
||||||
let scope_actions: &[Action] = match request.method().clone() {
|
|
||||||
Method::GET | Method::HEAD => &[Action::Pull],
|
|
||||||
Method::POST | Method::PATCH | Method::PUT => &[Action::Pull, Action::Push],
|
|
||||||
_ => &[],
|
|
||||||
};
|
|
||||||
let scope = Scope::new(ScopeType::Repository, target_name.clone(), scope_actions);
|
|
||||||
|
|
||||||
// respond with an auth challenge if there is no auth header.
|
|
||||||
//if !headers.contains_key(header::AUTHORIZATION) && auth.is_none() {
|
|
||||||
if auth.is_none() {
|
|
||||||
debug!("User is not authenticated, sending challenge");
|
|
||||||
return Ok(auth_challenge_response(config, Some(scope)));
|
|
||||||
}
|
|
||||||
let auth = auth.unwrap();
|
|
||||||
|
|
||||||
let mut auth_checker = state.auth_checker.lock().await;
|
|
||||||
|
|
||||||
// Check permission for each action
|
|
||||||
for action in scope_actions {
|
|
||||||
// action to permission
|
|
||||||
let permission = match action {
|
|
||||||
Action::Pull => Permission::PULL,
|
|
||||||
Action::Push => Permission::PUSH,
|
|
||||||
_ => Permission::NONE,
|
|
||||||
};
|
|
||||||
|
|
||||||
// get optional required visibility from action
|
|
||||||
let vis = match action {
|
|
||||||
Action::Pull => Some(RepositoryVisibility::Public),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
match auth_checker.user_has_permission(auth.user.email.clone(), target_name.clone(), permission, vis).await {
|
|
||||||
Ok(false) => return Ok(auth_challenge_response(config, Some(scope))),
|
|
||||||
Ok(true) => { },
|
|
||||||
Err(e) => {
|
|
||||||
error!("Error when checking user permissions! {}", e);
|
|
||||||
|
|
||||||
return Err((StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new()));
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
warn!("Unhandled auth check for '{target_type}'!!"); // TODO
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(next.run(request).await)
|
|
||||||
}
|
|
|
@ -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 {})
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,8 +1,6 @@
|
||||||
use anyhow::anyhow;
|
|
||||||
use figment::{Figment, providers::{Env, Toml, Format}};
|
use figment::{Figment, providers::{Env, Toml, Format}};
|
||||||
use figment_cliarg_provider::FigmentCliArgsProvider;
|
use figment_cliarg_provider::FigmentCliArgsProvider;
|
||||||
use serde::{Deserialize, Deserializer};
|
use serde::Deserialize;
|
||||||
use tracing::Level;
|
|
||||||
|
|
||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
|
@ -31,46 +29,16 @@ fn default_display_name_attribute() -> String {
|
||||||
"displayName".to_string()
|
"displayName".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct FilesystemDriverConfig {
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
#[serde(tag = "driver", rename_all = "snake_case")]
|
|
||||||
pub enum StorageConfig {
|
|
||||||
Filesystem(FilesystemDriverConfig),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct SqliteDbConfig {
|
|
||||||
pub path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
pub struct TlsConfig {
|
|
||||||
pub enable: bool,
|
|
||||||
pub key: String,
|
|
||||||
pub cert: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Clone)]
|
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
|
||||||
pub enum DatabaseConfig {
|
|
||||||
Sqlite(SqliteDbConfig),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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,
|
||||||
url: Option<String>,
|
pub url: Option<String>,
|
||||||
#[serde(deserialize_with = "serialize_log_level", default = "default_log_level")]
|
|
||||||
pub log_level: Level,
|
|
||||||
pub ldap: Option<LdapConnectionConfig>,
|
pub ldap: Option<LdapConnectionConfig>,
|
||||||
pub database: DatabaseConfig,
|
|
||||||
pub storage: StorageConfig,
|
|
||||||
pub tls: Option<TlsConfig>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
@ -99,45 +67,24 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url(&self) -> String {
|
pub fn get_url(&self) -> String {
|
||||||
match &self.url {
|
match &self.url {
|
||||||
Some(u) => u.clone(),
|
Some(u) => u.clone(),
|
||||||
None => format!("http://{}:{}", self.listen_address, self.listen_port)
|
None => format!("http://{}:{}", self.listen_address, self.listen_port)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_log_level() -> Level {
|
|
||||||
Level::INFO
|
|
||||||
}
|
|
||||||
|
|
||||||
fn serialize_log_level<'de, D>(deserializer: D) -> Result<Level, D::Error>
|
|
||||||
where D: Deserializer<'de> {
|
|
||||||
let s = String::deserialize(deserializer)?.to_lowercase();
|
|
||||||
let s = s.as_str();
|
|
||||||
|
|
||||||
match s {
|
|
||||||
"error" => Ok(Level::ERROR),
|
|
||||||
"warn" => Ok(Level::WARN),
|
|
||||||
"info" => Ok(Level::INFO),
|
|
||||||
"debug" => Ok(Level::DEBUG),
|
|
||||||
"trace" => Ok(Level::TRACE),
|
|
||||||
_ => Err(serde::de::Error::custom(format!("Unknown log level: '{}'", s))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* fn<'de, D> serialize_log_level(D) -> Result<Level, D::Error>
|
|
||||||
where D: Deserializer<'de>
|
|
||||||
{
|
|
||||||
|
|
||||||
} */
|
|
||||||
//fn serialize_log_level() -> Level
|
|
|
@ -1,6 +1,6 @@
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use sqlx::{Sqlite, Pool};
|
use sqlx::{Sqlite, Pool};
|
||||||
use tracing::{debug, warn};
|
use tracing::debug;
|
||||||
|
|
||||||
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
|
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone};
|
||||||
|
|
||||||
|
@ -42,9 +42,8 @@ pub trait Database {
|
||||||
|
|
||||||
async fn has_repository(&self, repository: &str) -> anyhow::Result<bool>;
|
async fn has_repository(&self, repository: &str) -> anyhow::Result<bool>;
|
||||||
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>>;
|
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>>;
|
||||||
async fn get_repository_owner(&self, repository: &str) -> anyhow::Result<Option<String>>;
|
|
||||||
/// Create a repository
|
/// Create a repository
|
||||||
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owner_email: Option<String>, owning_project: Option<String>) -> anyhow::Result<()>;
|
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option<String>) -> anyhow::Result<()>;
|
||||||
/// List all repositories.
|
/// List all repositories.
|
||||||
/// If limit is not specified, a default limit of 1000 will be returned.
|
/// If limit is not specified, a default limit of 1000 will be returned.
|
||||||
async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> anyhow::Result<Vec<String>>;
|
async fn list_repositories(&self, limit: Option<u32>, last_repo: Option<String>) -> anyhow::Result<Vec<String>>;
|
||||||
|
@ -53,7 +52,6 @@ pub trait Database {
|
||||||
/// User stuff
|
/// User stuff
|
||||||
async fn does_user_exist(&self, email: String) -> anyhow::Result<bool>;
|
async fn does_user_exist(&self, email: String) -> anyhow::Result<bool>;
|
||||||
async fn create_user(&self, email: String, username: String, login_source: LoginSource) -> anyhow::Result<User>;
|
async fn create_user(&self, email: String, username: String, login_source: LoginSource) -> anyhow::Result<User>;
|
||||||
async fn get_user(&self, email: String) -> anyhow::Result<Option<User>>;
|
|
||||||
async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> anyhow::Result<()>;
|
async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> anyhow::Result<()>;
|
||||||
async fn set_user_registry_type(&self, email: String, user_type: RegistryUserType) -> anyhow::Result<()>;
|
async fn set_user_registry_type(&self, email: String, user_type: RegistryUserType) -> anyhow::Result<()>;
|
||||||
async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool>;
|
async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool>;
|
||||||
|
@ -256,7 +254,7 @@ impl Database for Pool<Sqlite> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>> {
|
async fn get_repository_visibility(&self, repository: &str) -> anyhow::Result<Option<RepositoryVisibility>> {
|
||||||
let row: (u32, ) = match sqlx::query_as("SELECT visibility FROM repositories WHERE name = ?")
|
let row: (u32, ) = match sqlx::query_as("SELECT visibility FROM repositories WHERE 'name' = ?")
|
||||||
.bind(repository)
|
.bind(repository)
|
||||||
.fetch_one(self).await {
|
.fetch_one(self).await {
|
||||||
Ok(row) => row,
|
Ok(row) => row,
|
||||||
|
@ -273,42 +271,29 @@ impl Database for Pool<Sqlite> {
|
||||||
Ok(Some(RepositoryVisibility::try_from(row.0)?))
|
Ok(Some(RepositoryVisibility::try_from(row.0)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_repository_owner(&self, repository: &str) -> anyhow::Result<Option<String>> {
|
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option<String>) -> anyhow::Result<()> {
|
||||||
let row: (String, ) = match sqlx::query_as("SELECT owner_email FROM repositories WHERE name = ?")
|
|
||||||
.bind(repository)
|
|
||||||
.fetch_one(self).await {
|
|
||||||
Ok(row) => row,
|
|
||||||
Err(e) => match e {
|
|
||||||
sqlx::Error::RowNotFound => {
|
|
||||||
return Ok(None)
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
debug!("here's the error: {:?}", e);
|
|
||||||
return Err(anyhow::Error::new(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(row.0))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owner_email: Option<String>, owning_project: Option<String>) -> anyhow::Result<()> {
|
|
||||||
// ensure that the repository was not already created
|
// ensure that the repository was not already created
|
||||||
if self.has_repository(repository).await? {
|
if self.has_repository(repository).await? {
|
||||||
debug!("Skipping creation of repository since it already exists");
|
debug!("repo exists");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
debug!("repo does not exist");
|
||||||
|
|
||||||
// unwrap None values to empty for inserting into database
|
match owning_project {
|
||||||
let owner_email = owner_email.unwrap_or(String::new());
|
Some(owner) => {
|
||||||
let owning_project = owning_project.unwrap_or(String::new());
|
sqlx::query("INSERT INTO repositories (name, visibility, owning_project) VALUES (?, ?, ?)")
|
||||||
|
|
||||||
sqlx::query("INSERT INTO repositories (name, visibility, owner_email, owning_project) VALUES (?, ?, ?, ?)")
|
|
||||||
.bind(repository)
|
.bind(repository)
|
||||||
.bind(visibility as u32)
|
.bind(visibility as u32)
|
||||||
.bind(owner_email)
|
.bind(owner)
|
||||||
.bind(owning_project)
|
|
||||||
.execute(self).await?;
|
.execute(self).await?;
|
||||||
|
},
|
||||||
|
None => {
|
||||||
|
sqlx::query("INSERT INTO repositories (name, visibility) VALUES (?, ?)")
|
||||||
|
.bind(repository)
|
||||||
|
.bind(visibility as u32)
|
||||||
|
.execute(self).await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -368,25 +353,6 @@ impl Database for Pool<Sqlite> {
|
||||||
Ok(User::new(username, email, login_source))
|
Ok(User::new(username, email, login_source))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn get_user(&self, email: String) -> anyhow::Result<Option<User>> {
|
|
||||||
let email = email.to_lowercase();
|
|
||||||
let row: (String, u32) = match sqlx::query_as("SELECT username, login_source FROM users WHERE email = ?")
|
|
||||||
.bind(email.clone())
|
|
||||||
.fetch_one(self).await {
|
|
||||||
Ok(row) => row,
|
|
||||||
Err(e) => match e {
|
|
||||||
sqlx::Error::RowNotFound => {
|
|
||||||
return Ok(None)
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
return Err(anyhow::Error::new(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Some(User::new(row.0, email, LoginSource::try_from(row.1)?)))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> anyhow::Result<()> {
|
async fn add_user_auth(&self, email: String, password_hash: String, password_salt: String) -> anyhow::Result<()> {
|
||||||
let email = email.to_lowercase();
|
let email = email.to_lowercase();
|
||||||
sqlx::query("INSERT INTO user_logins (email, password_hash, password_salt) VALUES (?, ?, ?)")
|
sqlx::query("INSERT INTO user_logins (email, password_hash, password_salt) VALUES (?, ?, ?)")
|
||||||
|
@ -410,20 +376,9 @@ impl Database for Pool<Sqlite> {
|
||||||
|
|
||||||
async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool> {
|
async fn verify_user_login(&self, email: String, password: String) -> anyhow::Result<bool> {
|
||||||
let email = email.to_lowercase();
|
let email = email.to_lowercase();
|
||||||
|
let row: (String, ) = sqlx::query_as("SELECT password_hash FROM users WHERE email = ?")
|
||||||
let row: (String,) = match sqlx::query_as("SELECT password_hash FROM user_logins WHERE email = ?")
|
|
||||||
.bind(email)
|
.bind(email)
|
||||||
.fetch_one(self).await {
|
.fetch_one(self).await?;
|
||||||
Ok(row) => row,
|
|
||||||
Err(e) => match e {
|
|
||||||
sqlx::Error::RowNotFound => {
|
|
||||||
return Ok(false)
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
return Err(anyhow::Error::new(e));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(bcrypt::verify(password, &row.0)?)
|
Ok(bcrypt::verify(password, &row.0)?)
|
||||||
}
|
}
|
||||||
|
@ -451,8 +406,6 @@ impl Database for Pool<Sqlite> {
|
||||||
async fn get_user_repo_permissions(&self, email: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>> {
|
async fn get_user_repo_permissions(&self, email: String, repository: String) -> anyhow::Result<Option<RepositoryPermissions>> {
|
||||||
let email = email.to_lowercase();
|
let email = email.to_lowercase();
|
||||||
|
|
||||||
debug!("email: {email}, repo: {repository}");
|
|
||||||
|
|
||||||
let row: (u32, ) = match sqlx::query_as("SELECT repository_permissions FROM user_repo_permissions WHERE email = ? AND repository_name = ?")
|
let row: (u32, ) = match sqlx::query_as("SELECT repository_permissions FROM user_repo_permissions WHERE email = ? AND repository_name = ?")
|
||||||
.bind(email.clone())
|
.bind(email.clone())
|
||||||
.bind(repository.clone())
|
.bind(repository.clone())
|
||||||
|
@ -470,17 +423,13 @@ impl Database for Pool<Sqlite> {
|
||||||
|
|
||||||
let vis = match self.get_repository_visibility(&repository).await? {
|
let vis = match self.get_repository_visibility(&repository).await? {
|
||||||
Some(v) => v,
|
Some(v) => v,
|
||||||
None => {
|
None => return Ok(None),
|
||||||
warn!("Failure to find visibility for repository '{}'", repository);
|
|
||||||
return Ok(None)
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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 = match self.get_user_registry_usertype(email).await? {
|
let utype = match self.get_user_registry_usertype(email).await? {
|
||||||
Some(t) => t,
|
Some(t) => t,
|
||||||
// assume a regular user is their type is not found
|
None => return Ok(None),
|
||||||
None => RegistryUserType::Regular,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if utype == RegistryUserType::Admin {
|
if utype == RegistryUserType::Admin {
|
||||||
|
@ -550,7 +499,7 @@ impl Database for Pool<Sqlite> {
|
||||||
|
|
||||||
let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).single(), Utc.timestamp_millis_opt(created_at).single());
|
let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).single(), Utc.timestamp_millis_opt(created_at).single());
|
||||||
if let (Some(expiry), Some(created_at)) = (expiry, created_at) {
|
if let (Some(expiry), Some(created_at)) = (expiry, created_at) {
|
||||||
let user = User::new(user_row.0, email, LoginSource::try_from(user_row.1)?);
|
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);
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,6 @@ CREATE TABLE IF NOT EXISTS projects (
|
||||||
CREATE TABLE IF NOT EXISTS repositories (
|
CREATE TABLE IF NOT EXISTS repositories (
|
||||||
name TEXT NOT NULL UNIQUE PRIMARY KEY,
|
name TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||||
owning_project TEXT,
|
owning_project TEXT,
|
||||||
owner_email TEXT,
|
|
||||||
-- 0 = private, 1 = public
|
-- 0 = private, 1 = public
|
||||||
visibility INTEGER NOT NULL
|
visibility INTEGER NOT NULL
|
||||||
);
|
);
|
||||||
|
@ -71,7 +70,6 @@ CREATE TABLE IF NOT EXISTS user_tokens (
|
||||||
created_at BIGINT NOT NULL
|
created_at BIGINT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- create admin user (password is 'admin')
|
-- create admin user
|
||||||
INSERT OR IGNORE INTO users (username, email, login_source) VALUES ('admin', 'admin@example.com', 0);
|
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', '$2y$05$v9ND7dQKvfkOtY4XpnKVaOpvV0F5RDnW1Ec.nfkZ0vmEjLX5D5S8e', 'x5ECk0jUmOSfBWxW52wsyO');
|
INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2b$12$x5ECk0jUmOSfBWxW52wsyOmFxNZkwc2J9FH225if4eBnQYUvYLYYq', 'x5ECk0jUmOSfBWxW52wsyO');
|
||||||
INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('admin@example.com', 1);
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, de::Visitor};
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug)]
|
||||||
pub enum ScopeType {
|
pub enum ScopeType {
|
||||||
#[default]
|
#[default]
|
||||||
Unknown,
|
Unknown,
|
||||||
|
@ -19,7 +19,7 @@ impl fmt::Display for ScopeType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug)]
|
||||||
pub enum Action {
|
pub enum Action {
|
||||||
#[default]
|
#[default]
|
||||||
None,
|
None,
|
||||||
|
@ -37,23 +37,13 @@ impl fmt::Display for Action {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug)]
|
||||||
pub struct Scope {
|
pub struct Scope {
|
||||||
scope_type: ScopeType,
|
scope_type: ScopeType,
|
||||||
path: String,
|
path: String,
|
||||||
actions: Vec<Action>,
|
actions: Vec<Action>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Scope {
|
|
||||||
pub fn new(scope_type: ScopeType, path: String, actions: &[Action]) -> Self {
|
|
||||||
Self {
|
|
||||||
scope_type,
|
|
||||||
path,
|
|
||||||
actions: actions.to_vec(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for Scope {
|
impl fmt::Display for Scope {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
let actions = self.actions
|
let actions = self.actions
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
use std::{collections::HashMap, sync::Arc};
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use anyhow::anyhow;
|
||||||
use axum::{http::{StatusCode, header, HeaderName, HeaderMap, request::Parts}, extract::FromRequestParts};
|
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use crate::{app_state::AppState, database::Database};
|
|
||||||
|
|
||||||
use super::RepositoryVisibility;
|
use super::RepositoryVisibility;
|
||||||
|
|
||||||
|
@ -77,58 +73,9 @@ impl UserAuth {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
impl FromRequestParts<Arc<AppState>> for UserAuth {
|
|
||||||
type Rejection = (StatusCode, HeaderMap);
|
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &Arc<AppState>) -> Result<Self, Self::Rejection> {
|
|
||||||
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url());
|
|
||||||
let mut failure_headers = HeaderMap::new();
|
|
||||||
failure_headers.append(header::WWW_AUTHENTICATE, bearer.parse().unwrap());
|
|
||||||
failure_headers.append(HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".parse().unwrap());
|
|
||||||
|
|
||||||
debug!("starting UserAuth request parts");
|
|
||||||
|
|
||||||
let auth = String::from(
|
|
||||||
parts.headers
|
|
||||||
.get(header::AUTHORIZATION)
|
|
||||||
.ok_or((StatusCode::UNAUTHORIZED, failure_headers.clone()))?
|
|
||||||
.to_str()
|
|
||||||
.map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))?
|
|
||||||
);
|
|
||||||
|
|
||||||
debug!("got auth header");
|
|
||||||
|
|
||||||
let token = match auth.split_once(' ') {
|
|
||||||
Some((auth, token)) if auth == "Bearer" => token,
|
|
||||||
// This line would allow empty tokens
|
|
||||||
//_ if auth == "Bearer" => Ok(AuthToken(None)),
|
|
||||||
_ => return Err( (StatusCode::UNAUTHORIZED, failure_headers) ),
|
|
||||||
};
|
|
||||||
|
|
||||||
debug!("got token");
|
|
||||||
|
|
||||||
// If the token is not valid, return an unauthorized response
|
|
||||||
let database = &state.database;
|
|
||||||
if let Ok(Some(user)) = database.verify_user_token(token.to_string()).await {
|
|
||||||
debug!("Authenticated user through request extractor: {}", user.user.username);
|
|
||||||
|
|
||||||
Ok(user)
|
|
||||||
} else {
|
|
||||||
debug!("Failure to verify user token, responding with auth realm");
|
|
||||||
|
|
||||||
Err((
|
|
||||||
StatusCode::UNAUTHORIZED,
|
|
||||||
failure_headers
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||||
pub struct Permission: u32 {
|
pub struct Permission: u32 {
|
||||||
const NONE = 0b0000;
|
|
||||||
const PULL = 0b0001;
|
const PULL = 0b0001;
|
||||||
const PUSH = 0b0010;
|
const PUSH = 0b0010;
|
||||||
const EDIT = 0b0111;
|
const EDIT = 0b0111;
|
||||||
|
@ -136,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
|
||||||
|
@ -156,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)]
|
||||||
|
|
82
src/main.rs
82
src/main.rs
|
@ -9,7 +9,6 @@ mod auth;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
@ -19,35 +18,30 @@ use axum::middleware::Next;
|
||||||
use axum::response::Response;
|
use axum::response::Response;
|
||||||
use axum::{Router, routing};
|
use axum::{Router, routing};
|
||||||
use axum::ServiceExt;
|
use axum::ServiceExt;
|
||||||
use axum_server::tls_rustls::RustlsConfig;
|
|
||||||
use lazy_static::lazy_static;
|
|
||||||
use regex::Regex;
|
|
||||||
use tokio::fs::File;
|
|
||||||
use tower_layer::Layer;
|
use tower_layer::Layer;
|
||||||
|
|
||||||
use sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode};
|
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, info};
|
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;
|
||||||
|
|
||||||
use crate::config::{Config, DatabaseConfig, StorageConfig};
|
use crate::config::Config;
|
||||||
|
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
lazy_static! {
|
|
||||||
static ref REGISTRY_URL_REGEX: Regex = regex::Regex::new(r"/v2/([\w\-_./]+)/(blobs|tags|manifests)").unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Encode the 'name' path parameter in the url
|
/// Encode the 'name' path parameter in the url
|
||||||
async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Result<Response, StatusCode> {
|
async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Result<Response, StatusCode> {
|
||||||
// Attempt to find the name using regex in the url
|
// Attempt to find the name using regex in the url
|
||||||
let regex = ®ISTRY_URL_REGEX;
|
let regex = regex::Regex::new(r"/v2/([\w/]+)/(blobs|tags|manifests)")
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||||
let captures = match regex.captures(request.uri().path()) {
|
let captures = match regex.captures(request.uri().path()) {
|
||||||
Some(captures) => captures,
|
Some(captures) => captures,
|
||||||
None => return Ok(next.run(request).await),
|
None => return Ok(next.run(request).await),
|
||||||
|
@ -69,34 +63,36 @@ async fn change_request_paths<B>(mut request: Request<B>, next: Next<B>) -> Resu
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
|
tracing_subscriber::fmt()
|
||||||
|
.with_max_level(Level::DEBUG)
|
||||||
|
.init();
|
||||||
|
|
||||||
let config = Config::new()
|
let config = Config::new()
|
||||||
.expect("Failure to parse config!");
|
.expect("Failure to parse config!");
|
||||||
|
|
||||||
tracing_subscriber::fmt()
|
|
||||||
.with_max_level(config.log_level)
|
|
||||||
.init();
|
|
||||||
|
|
||||||
let sqlite_config = match &config.database {
|
|
||||||
DatabaseConfig::Sqlite(sqlite) => sqlite,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create a database file if it doesn't exist already
|
|
||||||
if !Path::new(&sqlite_config.path).exists() {
|
|
||||||
File::create(&sqlite_config.path).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let connection_options = SqliteConnectOptions::from_str(&format!("sqlite://{}", &sqlite_config.path))?
|
|
||||||
.journal_mode(SqliteJournalMode::Wal);
|
|
||||||
let pool = SqlitePoolOptions::new()
|
let pool = SqlitePoolOptions::new()
|
||||||
.max_connections(15)
|
.max_connections(15)
|
||||||
.connect_with(connection_options).await?;
|
.connect("test.db").await?;
|
||||||
pool.create_schema().await?;
|
pool.create_schema().await?;
|
||||||
|
|
||||||
let storage_driver: Mutex<Box<dyn StorageDriver>> = match &config.storage {
|
{
|
||||||
StorageConfig::Filesystem(fs) => {
|
let mut driver = StaticAuthDriver::from_file(&config.path.clone().unwrap()).unwrap();
|
||||||
Mutex::new(Box::new(FilesystemDriver::new(&fs.path)))
|
|
||||||
|
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,
|
// figure out the auth driver depending on whats specified in the config,
|
||||||
// the fallback is a database auth driver.
|
// the fallback is a database auth driver.
|
||||||
|
@ -112,11 +108,10 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let app_addr = SocketAddr::from_str(&format!("{}:{}", config.listen_address, config.listen_port))?;
|
let app_addr = SocketAddr::from_str(&format!("{}:{}", config.listen_address, config.listen_port))?;
|
||||||
|
|
||||||
let tls_config = config.tls.clone();
|
|
||||||
let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver));
|
let state = Arc::new(AppState::new(pool, storage_driver, config, auth_driver));
|
||||||
|
|
||||||
//let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::require_auth);
|
|
||||||
let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::check_auth);
|
let auth_middleware = axum::middleware::from_fn_with_state(state.clone(), auth::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()
|
||||||
|
@ -150,23 +145,10 @@ async fn main() -> anyhow::Result<()> {
|
||||||
|
|
||||||
let layered_app = NormalizePathLayer::trim_trailing_slash().layer(path_middleware.layer(app));
|
let layered_app = NormalizePathLayer::trim_trailing_slash().layer(path_middleware.layer(app));
|
||||||
|
|
||||||
match tls_config {
|
debug!("Starting http server, listening on {}", app_addr);
|
||||||
Some(tls) if tls.enable => {
|
|
||||||
info!("Starting https server, listening on {}", app_addr);
|
|
||||||
|
|
||||||
let config = RustlsConfig::from_pem_file(&tls.cert, &tls.key).await?;
|
|
||||||
|
|
||||||
axum_server::bind_rustls(app_addr, config)
|
|
||||||
.serve(layered_app.into_make_service())
|
|
||||||
.await?;
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
info!("Starting http server, listening on {}", app_addr);
|
|
||||||
axum::Server::bind(&app_addr)
|
axum::Server::bind(&app_addr)
|
||||||
.serve(layered_app.into_make_service())
|
.serve(layered_app.into_make_service())
|
||||||
.await?;
|
.await?;
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
|
@ -25,11 +25,6 @@ impl FilesystemDriver {
|
||||||
fn get_digest_path(&self, digest: &str) -> String {
|
fn get_digest_path(&self, digest: &str) -> String {
|
||||||
format!("{}/{}", self.storage_path, digest)
|
format!("{}/{}", self.storage_path, digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_storage_path(&self) -> std::io::Result<()>
|
|
||||||
{
|
|
||||||
std::fs::create_dir_all(&self.storage_path)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
|
@ -45,8 +40,6 @@ impl StorageDriver for FilesystemDriver {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn save_digest_stream(&self, digest: &str, mut stream: ByteStream, append: bool) -> anyhow::Result<usize> {
|
async fn save_digest_stream(&self, digest: &str, mut stream: ByteStream, append: bool) -> anyhow::Result<usize> {
|
||||||
self.ensure_storage_path()?;
|
|
||||||
|
|
||||||
let path = self.get_digest_path(digest);
|
let path = self.get_digest_path(digest);
|
||||||
let mut file = fs::OpenOptions::new()
|
let mut file = fs::OpenOptions::new()
|
||||||
.write(true)
|
.write(true)
|
||||||
|
|
Loading…
Reference in New Issue