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
|
||||
.env
|
||||
.vscode
|
||||
test.db
|
||||
/registry
|
||||
config.toml
|
|
@ -37,12 +37,6 @@ version = "1.0.70"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6"
|
||||
|
||||
[[package]]
|
||||
name = "argmap"
|
||||
version = "1.1.2"
|
||||
|
@ -119,9 +113,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
|||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.6.18"
|
||||
version = "0.6.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
|
||||
checksum = "113713495a32dd0ab52baf5c10044725aa3aec00b31beda84218e469029b72a3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
|
@ -190,26 +184,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "base64"
|
||||
version = "0.13.1"
|
||||
|
@ -584,7 +558,7 @@ dependencies = [
|
|||
"atomic",
|
||||
"pear",
|
||||
"serde",
|
||||
"toml",
|
||||
"toml 0.5.11",
|
||||
"uncased",
|
||||
"version_check",
|
||||
]
|
||||
|
@ -768,25 +742,6 @@ version = "0.3.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.3"
|
||||
|
@ -894,7 +849,6 @@ dependencies = [
|
|||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"h2",
|
||||
"http",
|
||||
"http-body",
|
||||
"httparse",
|
||||
|
@ -1302,7 +1256,6 @@ dependencies = [
|
|||
"axum",
|
||||
"axum-auth",
|
||||
"axum-macros",
|
||||
"axum-server",
|
||||
"bcrypt",
|
||||
"bitflags 2.2.1",
|
||||
"bytes",
|
||||
|
@ -1314,7 +1267,6 @@ dependencies = [
|
|||
"hmac",
|
||||
"jws",
|
||||
"jwt",
|
||||
"lazy_static",
|
||||
"ldap3",
|
||||
"pin-project-lite",
|
||||
"qstring",
|
||||
|
@ -1328,6 +1280,7 @@ dependencies = [
|
|||
"sqlx",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml 0.7.4",
|
||||
"tower-http",
|
||||
"tower-layer",
|
||||
"tracing",
|
||||
|
@ -1620,18 +1573,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustls-pemfile"
|
||||
version = "1.0.2"
|
||||
|
@ -1641,16 +1582,6 @@ dependencies = [
|
|||
"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]]
|
||||
name = "rustversion"
|
||||
version = "1.0.12"
|
||||
|
@ -1768,6 +1699,15 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93107647184f6027e3b7dcb2e11034cf95ffa1e3a682c67951963ac69c1c007d"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_urlencoded"
|
||||
version = "0.7.1"
|
||||
|
@ -1917,7 +1857,7 @@ dependencies = [
|
|||
"once_cell",
|
||||
"paste",
|
||||
"percent-encoding",
|
||||
"rustls 0.20.8",
|
||||
"rustls",
|
||||
"rustls-pemfile",
|
||||
"sha2 0.10.6",
|
||||
"smallvec",
|
||||
|
@ -1957,7 +1897,7 @@ checksum = "804d3f245f894e61b1e6263c84b23ca675d96753b5abfd5cc8597d86806e8024"
|
|||
dependencies = [
|
||||
"once_cell",
|
||||
"tokio",
|
||||
"tokio-rustls 0.23.4",
|
||||
"tokio-rustls",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -2153,21 +2093,11 @@ version = "0.23.4"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59"
|
||||
dependencies = [
|
||||
"rustls 0.20.8",
|
||||
"rustls",
|
||||
"tokio",
|
||||
"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]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.12"
|
||||
|
@ -2202,6 +2132,40 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6135d499e69981f9ff0ef2167955a5333c35e36f6937d382974566b3d5b94ec"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_datetime"
|
||||
version = "0.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a76a9312f5ba4c2dec6b9161fdf25d87ad8a09256ccea5a556fef03c706a10f"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2380d56e8670370eee6566b0bfd4265f65b3f432e8c6d85623f728d4fa31f739"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower"
|
||||
version = "0.4.13"
|
||||
|
@ -2687,6 +2651,15 @@ version = "0.48.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.4.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61de7bac303dc551fe038e2b3cef0f571087a47571ea6e79a87692ac99b99699"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
|
|
|
@ -32,8 +32,7 @@ sha256 = "1.1.2"
|
|||
pin-project-lite = "0.2.9"
|
||||
anyhow = "1.0.70"
|
||||
async-stream = "0.3.5"
|
||||
axum = "0.6.18"
|
||||
axum-server = { version = "0.5.1", features = [ "tls-rustls" ] }
|
||||
axum = "0.6.16"
|
||||
axum-macros = "0.3.7"
|
||||
|
||||
tower-http = { version = "0.4.0", features = [ "trace", "normalize-path" ] }
|
||||
|
@ -53,4 +52,4 @@ rand = "0.8.5"
|
|||
bcrypt = "0.14.0"
|
||||
bitflags = "2.2.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
|
||||
Orca is a pure-rust implementation of a Docker Registry.
|
||||
# Docker registry
|
||||
Docker registry written in Rust.
|
||||
|
||||
Note: Orca is still in early development ([status](#status)).
|
||||
|
||||
## 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.
|
||||
This follows the [docker registry spec](https://docs.docker.com/registry/spec/api/).
|
|
@ -2,27 +2,34 @@ listen_address = "127.0.0.1"
|
|||
listen_port = "3000"
|
||||
url = "http://localhost:3000/"
|
||||
|
||||
# error, warn, info, debug, trace
|
||||
log_level = "debug"
|
||||
[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"
|
||||
|
||||
[storage]
|
||||
driver = "filesystem"
|
||||
path = "/app/blobs"
|
||||
user_search_filter = "(&(objectClass=person)(mail=%s))"
|
||||
group_search_filter = "(&(objectclass=groupOfNames)(member=%d))"
|
||||
|
||||
[database]
|
||||
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)"
|
||||
admin_filter = "(memberOf=cn=admin_staff,ou=people,dc=planetexpress,dc=com)"
|
||||
#login_attribute = "mail"
|
||||
#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] ldap auth
|
||||
- [ ] permission stuff
|
||||
- [ ] simple way to define users and their permissions through a "users.toml"
|
||||
- [x] 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 pull from their own repositories
|
||||
- [ ] token expiry
|
||||
- [ ] 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
|
||||
- [ ] postgresql
|
||||
- [ ] prometheus metrics
|
||||
- [ ] simple webui for managing the registry
|
||||
- [x] streaming layer bytes into 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::database::Database;
|
||||
|
||||
use crate::auth::auth_challenge_response;
|
||||
use crate::auth::unauthenticated_response;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
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)? {
|
||||
debug!("Authentication failed, incorrect password!");
|
||||
|
||||
// TODO: Dont unwrap, find a way to return multiple scopes
|
||||
return Ok(auth_challenge_response(&state.config, Some(auth.scope.first().unwrap().clone())));
|
||||
return Ok(unauthenticated_response(&state.config));
|
||||
}
|
||||
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 token = create_jwt_token(account)
|
||||
.map_err(|_| {
|
||||
error!("Failed to create jwt token!");
|
||||
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
let token_str = token.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;
|
||||
database.store_user_token(token_str.clone(), account.clone(), token.expiry, token.created_at).await
|
||||
.map_err(|_| {
|
||||
error!("Failed to store user token in database!");
|
||||
|
||||
StatusCode::INTERNAL_SERVER_ERROR
|
||||
})?;
|
||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
||||
drop(database);
|
||||
|
||||
return Ok((
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::Extension;
|
||||
use axum::body::StreamBody;
|
||||
use axum::extract::{State, Path};
|
||||
use axum::http::{StatusCode, header, HeaderName};
|
||||
|
@ -7,9 +8,19 @@ use axum::response::{IntoResponse, Response};
|
|||
use tokio_util::io::ReaderStream;
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::unauthenticated_response;
|
||||
use crate::dto::RepositoryVisibility;
|
||||
use crate::dto::user::{Permission, UserAuth};
|
||||
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;
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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()));
|
||||
|
||||
// Construct the link header
|
||||
let url = &state.config.url();
|
||||
let url = &state.config.get_url();
|
||||
let mut url = format!("<{}/v2/_catalog?n={}", url, limit);
|
||||
if let Some(last_repo) = last_repo {
|
||||
url += &format!("&limit={}", last_repo);
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::Extension;
|
||||
use axum::extract::{Path, State};
|
||||
use axum::response::{Response, IntoResponse};
|
||||
use axum::http::{StatusCode, HeaderName, header};
|
||||
use tracing::log::warn;
|
||||
use tracing::{debug, info};
|
||||
|
||||
use crate::auth::access_denied_response;
|
||||
use crate::auth::unauthenticated_response;
|
||||
use crate::app_state::AppState;
|
||||
use crate::database::Database;
|
||||
use crate::dto::RepositoryVisibility;
|
||||
|
@ -15,7 +16,13 @@ use crate::dto::manifest::Manifest;
|
|||
use crate::dto::user::{UserAuth, Permission};
|
||||
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.
|
||||
let calculated_hash = sha256::digest(body.clone());
|
||||
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;
|
||||
|
||||
// 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?;
|
||||
|
||||
// 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
|
||||
if let Some(auth) = auth {
|
||||
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(access_denied_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));
|
||||
}
|
||||
return Ok(unauthenticated_response(&state.config));
|
||||
}
|
||||
drop(auth_driver);
|
||||
|
||||
let database = &state.database;
|
||||
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())
|
||||
}
|
||||
|
||||
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
|
||||
if let Some(auth) = auth {
|
||||
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(access_denied_response(&state.config));
|
||||
return Ok(unauthenticated_response(&state.config));
|
||||
}
|
||||
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.
|
||||
let database = &state.database;
|
||||
|
@ -154,10 +148,10 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)
|
|||
).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;
|
||||
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);
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use axum::Extension;
|
||||
use axum::extract::State;
|
||||
use axum::response::{IntoResponse, Response};
|
||||
use axum::http::{StatusCode, HeaderName};
|
||||
|
@ -13,13 +14,13 @@ pub mod tags;
|
|||
pub mod catalog;
|
||||
pub mod auth;
|
||||
|
||||
use crate::dto::user::UserAuth;
|
||||
|
||||
/// https://docs.docker.com/registry/spec/api/#api-version-check
|
||||
/// 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,
|
||||
[
|
||||
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" ),
|
||||
]
|
||||
StatusCode::OK,
|
||||
[( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0" )]
|
||||
).into_response()
|
||||
}
|
|
@ -37,7 +37,7 @@ pub async fn list_tags(Path((name, )): Path<(String, )>, Query(params): Query<Li
|
|||
let last_tag = tags.last();
|
||||
|
||||
// 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);
|
||||
if let Some(last_tag) = last_tag {
|
||||
url += &format!("&limit={}", last_tag.name);
|
||||
|
|
|
@ -2,6 +2,7 @@ use std::collections::HashMap;
|
|||
use std::io::ErrorKind;
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::Extension;
|
||||
use axum::http::{StatusCode, header, HeaderName};
|
||||
use axum::extract::{Path, BodyStream, State, Query};
|
||||
use axum::response::{IntoResponse, Response};
|
||||
|
@ -11,11 +12,15 @@ use futures::StreamExt;
|
|||
use tracing::{debug, warn};
|
||||
|
||||
use crate::app_state::AppState;
|
||||
use crate::auth::unauthenticated_response;
|
||||
use crate::byte_stream::ByteStream;
|
||||
use crate::dto::user::{UserAuth, Permission};
|
||||
use crate::error::AppError;
|
||||
|
||||
/// 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");
|
||||
let uuid = uuid::Uuid::new_v4();
|
||||
|
||||
|
@ -28,9 +33,18 @@ pub async fn start_upload_post(Path((name, )): Path<(String, )>) -> Result<Respo
|
|||
StatusCode::ACCEPTED,
|
||||
[ (header::LOCATION, location) ]
|
||||
).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 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)
|
||||
};
|
||||
|
||||
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((
|
||||
StatusCode::ACCEPTED,
|
||||
[
|
||||
|
@ -83,7 +97,13 @@ pub async fn chunked_upload_layer_patch(Path((name, layer_uuid)): Path<(String,
|
|||
).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 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())
|
||||
}
|
||||
|
||||
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;
|
||||
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())
|
||||
}
|
||||
|
||||
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 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 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;
|
||||
|
||||
|
@ -60,7 +60,6 @@ impl AuthDriver for LdapAuthDriver {
|
|||
Ok(true)
|
||||
} else {
|
||||
debug!("LDAP is falling back to database");
|
||||
|
||||
// fall back to database auth since this user might be local
|
||||
self.database.user_has_permission(email, repository, permission, required_visibility).await
|
||||
}
|
||||
|
|
131
src/auth/mod.rs
131
src/auth/mod.rs
|
@ -1,12 +1,14 @@
|
|||
pub mod ldap_driver;
|
||||
pub mod static_driver;
|
||||
|
||||
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 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>;
|
||||
}
|
||||
|
||||
// Implement AuthDriver for anything the implements Database
|
||||
#[async_trait]
|
||||
impl<T> AuthDriver for T
|
||||
where
|
||||
T: Database + Send + Sync
|
||||
{
|
||||
impl AuthDriver for Pool<Sqlite> {
|
||||
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? {
|
||||
Some(owner) if owner == email => return Ok(true),
|
||||
Some(_other_owner) => {
|
||||
let allowed_to = {
|
||||
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> {
|
||||
|
@ -96,7 +81,7 @@ impl Deref for AuthToken {
|
|||
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> {
|
||||
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();
|
||||
failure_headers.append(header::WWW_AUTHENTICATE, bearer.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)
|
||||
} else {
|
||||
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url());
|
||||
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.get_url());
|
||||
Ok((
|
||||
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)]
|
||||
pub fn auth_challenge_response(config: &Config, scope: Option<Scope>) -> Response {
|
||||
let bearer = match scope {
|
||||
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);
|
||||
|
||||
pub fn unauthenticated_response(config: &Config) -> Response {
|
||||
let bearer = format!("Bearer realm=\"{}/auth\"", config.get_url());
|
||||
(
|
||||
StatusCode::UNAUTHORIZED,
|
||||
[
|
||||
( 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() )
|
||||
]
|
||||
).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_cliarg_provider::FigmentCliArgsProvider;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use tracing::Level;
|
||||
use serde::Deserialize;
|
||||
|
||||
use std::env;
|
||||
|
||||
|
@ -31,46 +29,16 @@ fn default_display_name_attribute() -> 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)]
|
||||
pub struct Config {
|
||||
/// The path that the configuration file was deserialized from
|
||||
#[serde(skip)]
|
||||
pub path: Option<String>,
|
||||
|
||||
pub listen_address: String,
|
||||
pub listen_port: String,
|
||||
url: Option<String>,
|
||||
#[serde(deserialize_with = "serialize_log_level", default = "default_log_level")]
|
||||
pub log_level: Level,
|
||||
pub url: Option<String>,
|
||||
pub ldap: Option<LdapConnectionConfig>,
|
||||
pub database: DatabaseConfig,
|
||||
pub storage: StorageConfig,
|
||||
pub tls: Option<TlsConfig>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
|
@ -99,45 +67,24 @@ impl Config {
|
|||
.join(Toml::file(format!("{}", path)));
|
||||
|
||||
let mut config: Config = figment.extract()?;
|
||||
|
||||
// Post process config options
|
||||
|
||||
if let Some(url) = config.url.as_mut() {
|
||||
if url.ends_with("/") {
|
||||
*url = url[..url.len() - 1].to_string();
|
||||
}
|
||||
}
|
||||
|
||||
config.path = Some(path);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
pub fn url(&self) -> String {
|
||||
pub fn get_url(&self) -> String {
|
||||
match &self.url {
|
||||
Some(u) => u.clone(),
|
||||
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 sqlx::{Sqlite, Pool};
|
||||
use tracing::{debug, warn};
|
||||
use tracing::debug;
|
||||
|
||||
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 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
|
||||
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.
|
||||
/// 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>>;
|
||||
|
@ -53,7 +52,6 @@ pub trait Database {
|
|||
/// User stuff
|
||||
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 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 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>;
|
||||
|
@ -256,7 +254,7 @@ impl Database for Pool<Sqlite> {
|
|||
}
|
||||
|
||||
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)
|
||||
.fetch_one(self).await {
|
||||
Ok(row) => row,
|
||||
|
@ -273,42 +271,29 @@ impl Database for Pool<Sqlite> {
|
|||
Ok(Some(RepositoryVisibility::try_from(row.0)?))
|
||||
}
|
||||
|
||||
async fn get_repository_owner(&self, repository: &str) -> anyhow::Result<Option<String>> {
|
||||
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<()> {
|
||||
async fn save_repository(&self, repository: &str, visibility: RepositoryVisibility, owning_project: Option<String>) -> anyhow::Result<()> {
|
||||
// ensure that the repository was not already created
|
||||
if self.has_repository(repository).await? {
|
||||
debug!("Skipping creation of repository since it already exists");
|
||||
debug!("repo exists");
|
||||
return Ok(());
|
||||
}
|
||||
debug!("repo does not exist");
|
||||
|
||||
// unwrap None values to empty for inserting into database
|
||||
let owner_email = owner_email.unwrap_or(String::new());
|
||||
let owning_project = owning_project.unwrap_or(String::new());
|
||||
|
||||
sqlx::query("INSERT INTO repositories (name, visibility, owner_email, owning_project) VALUES (?, ?, ?, ?)")
|
||||
match owning_project {
|
||||
Some(owner) => {
|
||||
sqlx::query("INSERT INTO repositories (name, visibility, owning_project) VALUES (?, ?, ?)")
|
||||
.bind(repository)
|
||||
.bind(visibility as u32)
|
||||
.bind(owner_email)
|
||||
.bind(owning_project)
|
||||
.bind(owner)
|
||||
.execute(self).await?;
|
||||
},
|
||||
None => {
|
||||
sqlx::query("INSERT INTO repositories (name, visibility) VALUES (?, ?)")
|
||||
.bind(repository)
|
||||
.bind(visibility as u32)
|
||||
.execute(self).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -368,25 +353,6 @@ impl Database for Pool<Sqlite> {
|
|||
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<()> {
|
||||
let email = email.to_lowercase();
|
||||
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> {
|
||||
let email = email.to_lowercase();
|
||||
|
||||
let row: (String,) = match sqlx::query_as("SELECT password_hash FROM user_logins WHERE email = ?")
|
||||
let row: (String, ) = sqlx::query_as("SELECT password_hash FROM users WHERE email = ?")
|
||||
.bind(email)
|
||||
.fetch_one(self).await {
|
||||
Ok(row) => row,
|
||||
Err(e) => match e {
|
||||
sqlx::Error::RowNotFound => {
|
||||
return Ok(false)
|
||||
},
|
||||
_ => {
|
||||
return Err(anyhow::Error::new(e));
|
||||
}
|
||||
}
|
||||
};
|
||||
.fetch_one(self).await?;
|
||||
|
||||
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>> {
|
||||
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 = ?")
|
||||
.bind(email.clone())
|
||||
.bind(repository.clone())
|
||||
|
@ -470,17 +423,13 @@ impl Database for Pool<Sqlite> {
|
|||
|
||||
let vis = match self.get_repository_visibility(&repository).await? {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
warn!("Failure to find visibility for repository '{}'", repository);
|
||||
return Ok(None)
|
||||
},
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
// 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? {
|
||||
Some(t) => t,
|
||||
// assume a regular user is their type is not found
|
||||
None => RegistryUserType::Regular,
|
||||
None => return Ok(None),
|
||||
};
|
||||
|
||||
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());
|
||||
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 auth = UserAuth::new(user, token);
|
||||
|
||||
|
|
|
@ -7,7 +7,6 @@ CREATE TABLE IF NOT EXISTS projects (
|
|||
CREATE TABLE IF NOT EXISTS repositories (
|
||||
name TEXT NOT NULL UNIQUE PRIMARY KEY,
|
||||
owning_project TEXT,
|
||||
owner_email TEXT,
|
||||
-- 0 = private, 1 = public
|
||||
visibility INTEGER NOT NULL
|
||||
);
|
||||
|
@ -71,7 +70,6 @@ CREATE TABLE IF NOT EXISTS user_tokens (
|
|||
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 user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2y$05$v9ND7dQKvfkOtY4XpnKVaOpvV0F5RDnW1Ec.nfkZ0vmEjLX5D5S8e', 'x5ECk0jUmOSfBWxW52wsyO');
|
||||
INSERT OR IGNORE INTO user_registry_permissions (email, user_type) VALUES ('admin@example.com', 1);
|
||||
INSERT OR IGNORE INTO user_logins (email, password_hash, password_salt) VALUES ('admin@example.com', '$2b$12$x5ECk0jUmOSfBWxW52wsyOmFxNZkwc2J9FH225if4eBnQYUvYLYYq', 'x5ECk0jUmOSfBWxW52wsyO');
|
|
@ -24,8 +24,9 @@ impl Tag {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
|
||||
pub enum RepositoryVisibility {
|
||||
#[default]
|
||||
Private = 0,
|
||||
Public = 1
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use serde::{Deserialize, de::Visitor};
|
|||
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Default, Debug)]
|
||||
pub enum ScopeType {
|
||||
#[default]
|
||||
Unknown,
|
||||
|
@ -19,7 +19,7 @@ impl fmt::Display for ScopeType {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Default, Debug)]
|
||||
pub enum Action {
|
||||
#[default]
|
||||
None,
|
||||
|
@ -37,23 +37,13 @@ impl fmt::Display for Action {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
#[derive(Default, Debug)]
|
||||
pub struct Scope {
|
||||
scope_type: ScopeType,
|
||||
path: String,
|
||||
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 {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let actions = self.actions
|
||||
|
|
|
@ -1,12 +1,8 @@
|
|||
use std::{collections::HashMap, sync::Arc};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use async_trait::async_trait;
|
||||
use axum::{http::{StatusCode, header, HeaderName, HeaderMap, request::Parts}, extract::FromRequestParts};
|
||||
use anyhow::anyhow;
|
||||
use bitflags::bitflags;
|
||||
use chrono::{DateTime, Utc};
|
||||
use tracing::debug;
|
||||
|
||||
use crate::{app_state::AppState, database::Database};
|
||||
|
||||
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! {
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct Permission: u32 {
|
||||
const NONE = 0b0000;
|
||||
const PULL = 0b0001;
|
||||
const PUSH = 0b0010;
|
||||
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 {
|
||||
perms: u32,
|
||||
visibility: RepositoryVisibility
|
||||
|
@ -156,6 +118,12 @@ impl RepositoryPermissions {
|
|||
let perm = perm.bits();
|
||||
self.perms & perm == perm
|
||||
}
|
||||
|
||||
pub fn add_permission(&mut self, perm: Permission) {
|
||||
let perm = perm.bits();
|
||||
|
||||
self.perms |= perm;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
|
|
82
src/main.rs
82
src/main.rs
|
@ -9,7 +9,6 @@ mod auth;
|
|||
mod error;
|
||||
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -19,35 +18,30 @@ use axum::middleware::Next;
|
|||
use axum::response::Response;
|
||||
use axum::{Router, routing};
|
||||
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 sqlx::sqlite::{SqlitePoolOptions, SqliteConnectOptions, SqliteJournalMode};
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use tokio::sync::Mutex;
|
||||
use tower_http::normalize_path::NormalizePathLayer;
|
||||
use tracing::{debug, info};
|
||||
use tracing::{debug, Level, info};
|
||||
|
||||
use app_state::AppState;
|
||||
use database::Database;
|
||||
|
||||
use crate::auth::static_driver::StaticAuthDriver;
|
||||
use crate::dto::user::Permission;
|
||||
use crate::storage::StorageDriver;
|
||||
use crate::storage::filesystem::FilesystemDriver;
|
||||
|
||||
use crate::config::{Config, DatabaseConfig, StorageConfig};
|
||||
use crate::config::Config;
|
||||
|
||||
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
|
||||
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
|
||||
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()) {
|
||||
Some(captures) => captures,
|
||||
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]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
tracing_subscriber::fmt()
|
||||
.with_max_level(Level::DEBUG)
|
||||
.init();
|
||||
|
||||
let config = Config::new()
|
||||
.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()
|
||||
.max_connections(15)
|
||||
.connect_with(connection_options).await?;
|
||||
.connect("test.db").await?;
|
||||
pool.create_schema().await?;
|
||||
|
||||
let storage_driver: Mutex<Box<dyn StorageDriver>> = match &config.storage {
|
||||
StorageConfig::Filesystem(fs) => {
|
||||
Mutex::new(Box::new(FilesystemDriver::new(&fs.path)))
|
||||
{
|
||||
let mut driver = StaticAuthDriver::from_file(&config.path.clone().unwrap()).unwrap();
|
||||
|
||||
if driver.verify_user_login("admin".to_string(), "test1234".to_string()).await? {
|
||||
info!("LOGGED IN!");
|
||||
|
||||
if driver.user_has_permission("admin".to_string(), "admin/alpine".to_string(), Permission::PULL, None).await? {
|
||||
info!("user can do that!")
|
||||
} else {
|
||||
info!("user can not do that :(")
|
||||
}
|
||||
};
|
||||
} else {
|
||||
info!("not logged in :(");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let storage_driver: Mutex<Box<dyn StorageDriver>> = Mutex::new(Box::new(FilesystemDriver::new("registry/blobs")));
|
||||
|
||||
// figure out the auth driver depending on whats specified in the config,
|
||||
// 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 tls_config = config.tls.clone();
|
||||
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 app = Router::new()
|
||||
|
@ -150,23 +145,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
let layered_app = NormalizePathLayer::trim_trailing_slash().layer(path_middleware.layer(app));
|
||||
|
||||
match tls_config {
|
||||
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);
|
||||
debug!("Starting http server, listening on {}", app_addr);
|
||||
axum::Server::bind(&app_addr)
|
||||
.serve(layered_app.into_make_service())
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
|
@ -25,11 +25,6 @@ impl FilesystemDriver {
|
|||
fn get_digest_path(&self, digest: &str) -> String {
|
||||
format!("{}/{}", self.storage_path, digest)
|
||||
}
|
||||
|
||||
fn ensure_storage_path(&self) -> std::io::Result<()>
|
||||
{
|
||||
std::fs::create_dir_all(&self.storage_path)
|
||||
}
|
||||
}
|
||||
|
||||
#[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> {
|
||||
self.ensure_storage_path()?;
|
||||
|
||||
let path = self.get_digest_path(digest);
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.write(true)
|
||||
|
|
Loading…
Reference in New Issue