Compare commits

...

3 Commits

13 changed files with 398 additions and 316 deletions

1
Cargo.lock generated
View File

@ -321,6 +321,7 @@ dependencies = [
"js-sys", "js-sys",
"num-integer", "num-integer",
"num-traits", "num-traits",
"serde",
"time", "time",
"wasm-bindgen", "wasm-bindgen",
"winapi", "winapi",

View File

@ -14,7 +14,7 @@ uuid = { version = "1.3.1", features = [ "v4", "fast-rng" ] }
sqlx = { version = "0.6.3", features = [ "runtime-tokio-rustls", "sqlite" ] } sqlx = { version = "0.6.3", features = [ "runtime-tokio-rustls", "sqlite" ] }
bytes = "1.4.0" bytes = "1.4.0"
chrono = "0.4.23" chrono = { version = "0.4.23", features = [ "serde" ] }
tokio = { version = "1.21.2", features = [ "fs", "macros" ] } tokio = { version = "1.21.2", features = [ "fs", "macros" ] }
tokio-util = { version = "0.7.7", features = [ "io" ] } tokio-util = { version = "0.7.7", features = [ "io" ] }
@ -53,4 +53,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" lazy_static = "1.4.0"

View File

@ -1,8 +1,17 @@
use std::{sync::Arc, collections::{HashMap, BTreeMap}, time::SystemTime}; use std::{
collections::HashMap,
sync::Arc,
time::SystemTime,
};
use axum::{extract::{Query, State}, response::{IntoResponse, Response}, http::{StatusCode, header}, Form}; use axum::{
extract::{Query, State},
http::{header, StatusCode},
response::{IntoResponse, Response},
Form,
};
use axum_auth::AuthBasic; use axum_auth::AuthBasic;
use chrono::{DateTime, Utc, Duration}; use chrono::{DateTime, Days, Utc};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use tracing::{debug, error, info, span, Level}; use tracing::{debug, error, info, span, Level};
@ -12,12 +21,19 @@ use sha2::Sha256;
use rand::Rng; use rand::Rng;
use crate::{dto::{scope::Scope, user::TokenInfo}, app_state::AppState}; use crate::{database::Database, dto::scope::Action};
use crate::database::Database; use crate::{
app_state::AppState,
dto::{
scope::{Scope, ScopeType},
user::{AuthToken, TokenInfo},
RepositoryVisibility,
},
};
use crate::auth::auth_challenge_response; use crate::auth::auth_challenge_response;
#[derive(Deserialize, Debug)] #[derive(Debug)]
pub struct TokenAuthRequest { pub struct TokenAuthRequest {
user: Option<String>, user: Option<String>,
password: Option<String>, password: Option<String>,
@ -43,39 +59,43 @@ pub struct AuthResponse {
issued_at: String, issued_at: String,
} }
/// In the returned UserToken::user, only the username is specified fn create_jwt_token(jwt_key: String, account: Option<&str>, scopes: Vec<Scope>) -> anyhow::Result<TokenInfo> {
fn create_jwt_token(account: &str) -> anyhow::Result<TokenInfo> { let key: Hmac<Sha256> = Hmac::new_from_slice(jwt_key.as_bytes())?;
let key: Hmac<Sha256> = Hmac::new_from_slice(b"some-secret")?;
let now = chrono::offset::Utc::now(); let now = chrono::offset::Utc::now();
let now_secs = now.timestamp();
// Construct the claims for the token // Expire the token in a day
let mut claims = BTreeMap::new(); let expiration = now.checked_add_days(Days::new(1)).unwrap();
claims.insert("issuer", "orca-registry__DEV");
claims.insert("subject", &account);
//claims.insert("audience", auth.service);
let not_before = format!("{}", now_secs);
let issued_at = format!("{}", now_secs);
let expiration = format!("{}", now_secs + 86400); // 1 day
claims.insert("notbefore", &not_before);
claims.insert("issuedat", &issued_at);
claims.insert("expiration", &expiration); // TODO: 20 seconds expiry for testing
let issued_at = now;
let expiration = now + Duration::seconds(20);
// Create a randomized jwtid
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
let jwtid = format!("{}", rng.gen::<u64>()); let jwtid = format!("{}", rng.gen::<u64>());
claims.insert("jwtid", &jwtid);
let token_str = claims.sign_with_key(&key)?; // empty account if they are not authenticated
Ok(TokenInfo::new(token_str, expiration, issued_at)) let account = account.map(|a| a.to_string()).unwrap_or(String::new());
// Construct the claims for the token
// TODO: Verify the token!
let token = AuthToken::new(
String::from("orca-registry__DEV"),
account,
String::from("reg"),
expiration,
now.clone(),
now.clone(),
jwtid,
scopes,
);
let token_str = token.sign_with_key(&key)?;
Ok(TokenInfo::new(token_str, expiration, now))
} }
pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppState>>, Query(params): Query<HashMap<String, String>>, form: Option<Form<AuthForm>>) -> Result<Response, StatusCode> { pub async fn auth_basic_get(
basic_auth: Option<AuthBasic>,
state: State<Arc<AppState>>,
Query(params): Query<HashMap<String, String>>,
form: Option<Form<AuthForm>>,
) -> Result<Response, StatusCode> {
let mut auth = TokenAuthRequest { let mut auth = TokenAuthRequest {
user: None, user: None,
password: None, password: None,
@ -88,19 +108,32 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
let auth_method; let auth_method;
// Process all the scopes
if let Some(scope) = params.get("scope") {
// TODO: Handle multiple scopes
match Scope::try_from(&scope[..]) {
Ok(scope) => {
auth.scope.push(scope);
}
Err(_) => {
return Err(StatusCode::BAD_REQUEST);
}
}
}
// If BasicAuth is provided, set the fields to it // If BasicAuth is provided, set the fields to it
if let Some(AuthBasic((username, pass))) = basic_auth { if let Some(AuthBasic((username, pass))) = basic_auth {
auth.user = Some(username.clone()); auth.user = Some(username.clone());
auth.password = pass; auth.password = pass;
// I hate having to create this span here multiple times, but its the only // I hate having to create this span here multiple times, but its the only
// way I could think of // way I could think of
/* let span = span!(Level::DEBUG, "auth", username = auth.user.clone()); /* let span = span!(Level::DEBUG, "auth", username = auth.user.clone());
let _enter = span.enter(); let _enter = span.enter();
debug!("Read user authentication from an AuthBasic"); */ debug!("Read user authentication from an AuthBasic"); */
auth_method = "basic-auth"; auth_method = "basic-auth";
} }
// Username and password could be passed in forms // Username and password could be passed in forms
// If there was a way to also check if the Method was "POST", this is where // If there was a way to also check if the Method was "POST", this is where
// we would do it. // we would do it.
@ -114,14 +147,88 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
auth_method = "form"; auth_method = "form";
} else { } else {
info!("Auth failure! Auth was not provided in either AuthBasic or Form!"); // If no auth parameters were specified, check if the repository is public. if it is, respond with a token.
// Maybe BAD_REQUEST should be returned? let is_public_access = {
return Err(StatusCode::UNAUTHORIZED); let mut res = vec![];
for scope in auth.scope.iter() {
match scope.scope_type {
ScopeType::Repository => {
// check repository visibility
let database = &state.database;
match database.get_repository_visibility(&scope.name).await {
Ok(Some(RepositoryVisibility::Public)) => res.push(Ok(true)),
Ok(_) => res.push(Ok(false)),
Err(e) => {
error!(
"Failure to check repository visibility for {}! Err: {}",
scope.name, e
);
res.push(Err(StatusCode::INTERNAL_SERVER_ERROR));
}
}
}
_ => res.push(Ok(false)),
}
}
// merge the booleans into a single bool, respond with errors if there are any.
let res: Result<Vec<bool>, StatusCode> = res.into_iter().collect();
res?.iter().all(|b| *b)
};
if is_public_access {
for scope in auth.scope.iter_mut() {
// only retain Action::Pull
scope.actions.retain(|a| *a == Action::Pull);
}
let token = create_jwt_token(state.config.jwt_key.clone(), None, auth.scope)
.map_err(|_| {
error!("Failed to create jwt token!");
StatusCode::INTERNAL_SERVER_ERROR
})?;
let token_str = token.token;
let now_format = format!("{}", token.created_at.format("%+"));
let auth_response = AuthResponse {
token: token_str.clone(),
expires_in: 86400, // 1 day
issued_at: now_format,
};
let json_str = serde_json::to_string(&auth_response)
.map_err(|_| StatusCode::BAD_REQUEST)?;
debug!("Created anonymous token for public scopes!");
return Ok((
StatusCode::OK,
[
(header::CONTENT_TYPE, "application/json"),
(header::AUTHORIZATION, &format!("Bearer {}", token_str)),
],
json_str,
).into_response());
} else {
info!("Auth failure! Auth was not provided in either AuthBasic or Form!");
// Maybe BAD_REQUEST should be returned?
return Err(StatusCode::UNAUTHORIZED);
}
} }
// Create logging span for the rest of this request // Create logging span for the rest of this request
let span = span!(Level::DEBUG, "auth", username = auth.user.clone(), auth_method); let span = span!(
Level::DEBUG,
"auth",
username = auth.user.clone(),
auth_method
);
let _enter = span.enter(); let _enter = span.enter();
debug!("Parsed user auth request"); debug!("Parsed user auth request");
@ -131,8 +238,11 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
if let Some(account) = params.get("account") { if let Some(account) = params.get("account") {
if let Some(user) = &auth.user { if let Some(user) = &auth.user {
if account != user { if account != user {
error!("`user` and `account` are not the same!!! (user: {}, account: {})", user, account); error!(
"`user` and `account` are not the same!!! (user: {}, account: {})",
user, account
);
return Err(StatusCode::BAD_REQUEST); return Err(StatusCode::BAD_REQUEST);
} }
} }
@ -145,20 +255,6 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
auth.service = Some(service.clone()); auth.service = Some(service.clone());
} }
// Process all the scopes
if let Some(scope) = params.get("scope") {
// TODO: Handle multiple scopes
match Scope::try_from(&scope[..]) {
Ok(scope) => {
auth.scope.push(scope);
},
Err(_) => {
return Err(StatusCode::BAD_REQUEST);
}
}
}
// Get offline token and attempt to convert it to a boolean // Get offline token and attempt to convert it to a boolean
if let Some(offline_token) = params.get("offline_token") { if let Some(offline_token) = params.get("offline_token") {
if let Ok(b) = offline_token.parse::<bool>() { if let Ok(b) = offline_token.parse::<bool>() {
@ -175,19 +271,30 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
if let (Some(account), Some(password)) = (&auth.account, auth.password) { if let (Some(account), Some(password)) = (&auth.account, auth.password) {
// Ensure that the password is correct // Ensure that the password is correct
let mut auth_driver = state.auth_checker.lock().await; let mut auth_driver = state.auth_checker.lock().await;
if !auth_driver.verify_user_login(account.clone(), password).await if !auth_driver
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)? { .verify_user_login(account.clone(), password)
.await
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
{
debug!("Authentication failed, incorrect password!"); debug!("Authentication failed, incorrect password!");
// TODO: Multiple scopes
let scope = auth.scope
.first()
.and_then(|s| Some(s.clone()));
// TODO: Dont unwrap, find a way to return multiple scopes // 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(auth_challenge_response(
&state.config,
scope,
));
} }
drop(auth_driver); drop(auth_driver);
debug!("User password is correct"); debug!("User password is correct");
let now = SystemTime::now(); let now = SystemTime::now();
let token = create_jwt_token(account) let token = create_jwt_token(state.config.jwt_key.clone(), Some(account), vec![])
.map_err(|_| { .map_err(|_| {
error!("Failed to create jwt token!"); error!("Failed to create jwt token!");
@ -208,11 +315,18 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
issued_at: now_format, issued_at: now_format,
}; };
let json_str = serde_json::to_string(&auth_response) let json_str =
.map_err(|_| StatusCode::BAD_REQUEST)?; serde_json::to_string(&auth_response).map_err(|_| StatusCode::BAD_REQUEST)?;
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(|_| {
error!("Failed to store user token in database!"); error!("Failed to store user token in database!");
@ -223,14 +337,15 @@ pub async fn auth_basic_get(basic_auth: Option<AuthBasic>, state: State<Arc<AppS
return Ok(( return Ok((
StatusCode::OK, StatusCode::OK,
[ [
( header::CONTENT_TYPE, "application/json" ), (header::CONTENT_TYPE, "application/json"),
( header::AUTHORIZATION, &format!("Bearer {}", token_str) ) (header::AUTHORIZATION, &format!("Bearer {}", token_str)),
], ],
json_str json_str,
).into_response()); )
.into_response());
} }
info!("Auth failure! Not enough information given to create auth token!"); info!("Auth failure! Not enough information given to create auth token!");
// If we didn't get fields required to make a token, then the client did something bad // If we didn't get fields required to make a token, then the client did something bad
Err(StatusCode::UNAUTHORIZED) Err(StatusCode::UNAUTHORIZED)
} }

View File

@ -6,13 +6,12 @@ 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::app_state::AppState; use crate::app_state::AppState;
use crate::database::Database; use crate::database::Database;
use crate::dto::RepositoryVisibility; use crate::dto::RepositoryVisibility;
use crate::dto::digest::Digest; use crate::dto::digest::Digest;
use crate::dto::manifest::Manifest; use crate::dto::manifest::Manifest;
use crate::dto::user::{UserAuth, Permission}; use crate::dto::user::UserAuth;
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>>, auth: UserAuth, body: String) -> Result<Response, AppError> {
@ -20,10 +19,13 @@ pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>
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);
// anonymous users wouldn't be able to get to this point, so it should be safe to unwrap.
let user = auth.user.unwrap();
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, Some(user.email), 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,20 +59,7 @@ pub async fn upload_manifest_put(Path((name, reference)): Path<(String, String)>
} }
} }
pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>, auth: Option<UserAuth>) -> Result<Response, AppError> { pub async fn pull_manifest_get(Path((name, reference)): Path<(String, String)>, state: State<Arc<AppState>>) -> 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));
}
}
let database = &state.database; let database = &state.database;
let digest = match Digest::is_digest(&reference) { let digest = match Digest::is_digest(&reference) {
true => reference.clone(), true => reference.clone(),
@ -106,21 +95,8 @@ 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>>) -> Result<Response, AppError> {
// Check if the user has permission to pull, or that the repository is public debug!("start of head");
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));
}
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;
let digest = match Digest::is_digest(&reference) { let digest = match Digest::is_digest(&reference) {
@ -133,6 +109,7 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)
} }
} }
}; };
debug!("found digest: {}", digest);
let manifest_content = database.get_manifest(&name, &digest).await?; let manifest_content = database.get_manifest(&name, &digest).await?;
if manifest_content.is_none() { if manifest_content.is_none() {
@ -142,6 +119,8 @@ pub async fn manifest_exists_head(Path((name, reference)): Path<(String, String)
} }
let manifest_content = manifest_content.unwrap(); let manifest_content = manifest_content.unwrap();
debug!("got content");
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
[ [
@ -154,13 +133,7 @@ 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>>) -> 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));
}
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) {
true => { true => {

View File

@ -17,7 +17,7 @@ pub mod auth;
/// full endpoint: `/v2/` /// full endpoint: `/v2/`
pub async fn version_check(_state: State<Arc<AppState>>) -> Response { pub async fn version_check(_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" ),
] ]

View File

@ -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;

View File

@ -1,6 +1,6 @@
pub mod ldap_driver; pub mod ldap_driver;
use std::{ops::Deref, sync::Arc}; use std::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, Method}, middleware::Next, response::{Response, IntoResponse}};
@ -82,59 +82,8 @@ where
Ok(false) Ok(false)
} }
#[derive(Clone)]
pub struct AuthToken(pub String);
impl Deref for AuthToken {
type Target = String;
fn deref(&self) -> &Self::Target {
&self.0
}
}
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> {
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());
let auth = String::from(
request.headers().get(header::AUTHORIZATION)
.ok_or((StatusCode::UNAUTHORIZED, failure_headers.clone()))?
.to_str()
.map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))?
);
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) ),
};
// 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 middleware: {}", user.user.username);
request.extensions_mut().insert(user);
Ok(next.run(request).await)
} else {
let bearer = format!("Bearer realm=\"{}/auth\"", state.config.url());
Ok((
StatusCode::UNAUTHORIZED,
[
( header::WWW_AUTHENTICATE, bearer ),
( HeaderName::from_static("docker-distribution-api-version"), "registry/2.0".to_string() )
]
).into_response())
}
}
/// Creates a response with an Unauthorized (401) status code. /// Creates a response with an Unauthorized (401) status code.
/// The www-authenticate header is set to notify the client of where to authorize with. /// The www-authenticate header is set to notify the client of where to authorize with.
#[inline(always)] #[inline(always)]
@ -173,9 +122,17 @@ pub async fn check_auth<B>(State(state): State<Arc<AppState>>, auth: Option<User
// note: url is relative to /v2 // note: url is relative to /v2
let url = request.uri().to_string(); let url = request.uri().to_string();
if url == "/" && auth.is_none() { if url == "/" {
debug!("Responding to /v2/ with an auth challenge"); // if auth is none, then the client needs to authenticate
return Ok(auth_challenge_response(config, None)); if auth.is_none() {
debug!("Responding to /v2/ with an auth challenge");
return Ok(auth_challenge_response(config, None));
}
debug!("user is authed");
// the client is authenticating right now
return Ok(next.run(request).await);
} }
let url_split: Vec<&str> = url.split("/").skip(1).collect(); let url_split: Vec<&str> = url.split("/").skip(1).collect();
@ -216,14 +173,28 @@ pub async fn check_auth<B>(State(state): State<Arc<AppState>>, auth: Option<User
_ => None, _ => None,
}; };
match auth_checker.user_has_permission(auth.user.email.clone(), target_name.clone(), permission, vis).await { if let Some(user) = &auth.user {
Ok(false) => return Ok(auth_challenge_response(config, Some(scope))), match auth_checker.user_has_permission(user.email.clone(), target_name.clone(), permission, vis).await {
Ok(true) => { }, Ok(false) => return Ok(auth_challenge_response(config, Some(scope))),
Err(e) => { Ok(true) => { },
error!("Error when checking user permissions! {}", e); Err(e) => {
error!("Error when checking user permissions! {}", e);
return Err((StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new())); return Err((StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new()));
}, },
}
} else {
// anonymous users can ONLY pull from public repos
if permission != Permission::PULL {
return Ok(access_denied_response(config));
}
// ensure the repo is public
let database = &state.database;
if let Some(RepositoryVisibility::Private) = database.get_repository_visibility(&target_name).await
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new()))? {
return Ok(access_denied_response(config));
}
} }
} }
} else { } else {

View File

@ -1,4 +1,3 @@
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, Deserializer};
@ -71,6 +70,8 @@ pub struct Config {
pub database: DatabaseConfig, pub database: DatabaseConfig,
pub storage: StorageConfig, pub storage: StorageConfig,
pub tls: Option<TlsConfig>, pub tls: Option<TlsConfig>,
#[serde(skip)]
pub jwt_key: String,
} }
#[allow(dead_code)] #[allow(dead_code)]

View File

@ -1,19 +1,21 @@
use async_trait::async_trait; use async_trait::async_trait;
use rand::{Rng, distributions::Alphanumeric};
use sqlx::{Sqlite, Pool}; use sqlx::{Sqlite, Pool};
use tracing::{debug, warn}; use tracing::{debug, warn};
use chrono::{DateTime, Utc, NaiveDateTime, TimeZone}; use chrono::{DateTime, Utc, NaiveDateTime};
use crate::dto::{Tag, user::{User, RepositoryPermissions, RegistryUserType, Permission, UserAuth, TokenInfo, LoginSource}, RepositoryVisibility}; use crate::dto::{Tag, user::{User, RepositoryPermissions, RegistryUserType, Permission, UserAuth, LoginSource}, RepositoryVisibility};
#[async_trait] #[async_trait]
pub trait Database { pub trait Database {
// Digest related functions // Digest related functions
/// Create the tables in the database /// Create the tables in the database
async fn create_schema(&self) -> anyhow::Result<()>; async fn create_schema(&self) -> anyhow::Result<()>;
async fn get_jwt_secret(&self) -> anyhow::Result<String>;
// Tag related functions // Tag related functions
/// Get tags associated with a repository /// Get tags associated with a repository
@ -61,20 +63,67 @@ pub trait Database {
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>>;
async fn get_user_registry_usertype(&self, email: String) -> anyhow::Result<Option<RegistryUserType>>; async fn get_user_registry_usertype(&self, email: String) -> anyhow::Result<Option<RegistryUserType>>;
async fn store_user_token(&self, token: String, email: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()>; async fn store_user_token(&self, token: String, email: String, expiry: DateTime<Utc>, created_at: DateTime<Utc>) -> anyhow::Result<()>;
#[deprecated = "Tokens are now verified using a secret"]
async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>>; async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>>;
} }
#[async_trait] #[async_trait]
impl Database for Pool<Sqlite> { impl Database for Pool<Sqlite> {
async fn create_schema(&self) -> anyhow::Result<()> { async fn create_schema(&self) -> anyhow::Result<()> {
let orca_version = "0.1.0";
let schema_version = "0.0.1";
let row: Option<(u32, )> = match sqlx::query_as("SELECT COUNT(1) FROM orca WHERE \"schema_version\" = ?")
.bind(schema_version)
.fetch_one(self).await {
Ok(row) => Some(row),
Err(e) => match e {
sqlx::Error::RowNotFound => {
None
},
// ignore no such table errors
sqlx::Error::Database(b) if b.message().starts_with("no such table") => None,
_ => {
return Err(anyhow::Error::new(e));
}
}
};
sqlx::query(include_str!("schemas/schema.sql")) sqlx::query(include_str!("schemas/schema.sql"))
.execute(self).await?; .execute(self).await?;
debug!("Created database schema"); debug!("Created database schema");
if row.is_none() || row.unwrap().0 == 0 {
let jwt_sec: String = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(16)
.map(char::from)
.collect();
// create schema
// TODO: Check if needed
/* sqlx::query(include_str!("schemas/schema.sql"))
.execute(self).await?;
debug!("Created database schema"); */
sqlx::query("INSERT INTO orca(orca_version, schema_version, jwt_secret) VALUES (?, ?, ?)")
.bind(orca_version)
.bind(schema_version)
.bind(jwt_sec)
.execute(self).await?;
debug!("Inserted information about orca!");
}
Ok(()) Ok(())
} }
async fn get_jwt_secret(&self) -> anyhow::Result<String> {
let rows: (String, ) = sqlx::query_as("SELECT jwt_secret FROM orca WHERE id = (SELECT max(id) FROM orca)")
.fetch_one(self).await?;
Ok(rows.0)
}
async fn link_manifest_layer(&self, manifest_digest: &str, layer_digest: &str) -> anyhow::Result<()> { async fn link_manifest_layer(&self, manifest_digest: &str, layer_digest: &str) -> anyhow::Result<()> {
sqlx::query("INSERT INTO manifest_layers(manifest, layer_digest) VALUES (?, ?)") sqlx::query("INSERT INTO manifest_layers(manifest, layer_digest) VALUES (?, ?)")
.bind(manifest_digest) .bind(manifest_digest)
@ -369,6 +418,7 @@ impl Database for Pool<Sqlite> {
} }
async fn get_user(&self, email: String) -> anyhow::Result<Option<User>> { async fn get_user(&self, email: String) -> anyhow::Result<Option<User>> {
debug!("getting user");
let email = email.to_lowercase(); let email = email.to_lowercase();
let row: (String, u32) = match sqlx::query_as("SELECT username, login_source FROM users WHERE email = ?") let row: (String, u32) = match sqlx::query_as("SELECT username, login_source FROM users WHERE email = ?")
.bind(email.clone()) .bind(email.clone())
@ -513,50 +563,7 @@ impl Database for Pool<Sqlite> {
Ok(()) Ok(())
} }
async fn verify_user_token(&self, token: String) -> anyhow::Result<Option<UserAuth>> { async fn verify_user_token(&self, _token: String) -> anyhow::Result<Option<UserAuth>> {
let token_row: (String, i64, i64,) = match sqlx::query_as("SELECT email, expiry, created_at FROM user_tokens WHERE token = ?") panic!("ERR: Database::verify_user_token is deprecated!")
.bind(token.clone())
.fetch_one(self).await {
Ok(row) => row,
Err(e) => match e {
sqlx::Error::RowNotFound => {
return Ok(None)
},
_ => {
return Err(anyhow::Error::new(e));
}
}
};
let (email, expiry, created_at) = (token_row.0, token_row.1, token_row.2);
let user_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));
}
}
};
/* let user_row: (String, u32) = sqlx::query_as("SELECT email, login_source FROM users WHERE email = ?")
.bind(email.clone())
.fetch_one(self).await?; */
let (expiry, created_at) = (Utc.timestamp_millis_opt(expiry).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 token = TokenInfo::new(token, expiry, created_at);
let auth = UserAuth::new(user, token);
Ok(Some(auth))
} else {
Ok(None)
}
} }
} }

View File

@ -1,3 +1,10 @@
CREATE TABLE IF NOT EXISTS orca (
id INTEGER PRIMARY KEY AUTOINCREMENT,
orca_version TEXT NOT NULL,
schema_version TEXT NOT NULL,
jwt_secret TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS projects ( CREATE TABLE IF NOT EXISTS projects (
name TEXT NOT NULL UNIQUE PRIMARY KEY, name TEXT NOT NULL UNIQUE PRIMARY KEY,
-- 0 = private, 1 = public -- 0 = private, 1 = public

View File

@ -1,12 +1,13 @@
use anyhow::anyhow; use anyhow::anyhow;
use serde::{Deserialize, de::Visitor}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum ScopeType { pub enum ScopeType {
#[default] #[default]
Unknown, Unknown,
#[serde(rename = "repository")]
Repository, Repository,
} }
@ -19,11 +20,13 @@ impl fmt::Display for ScopeType {
} }
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum Action { pub enum Action {
#[default] #[default]
None, None,
#[serde(rename = "push")]
Push, Push,
#[serde(rename = "pull")]
Pull, Pull,
} }
@ -37,18 +40,19 @@ impl fmt::Display for Action {
} }
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Scope { pub struct Scope {
scope_type: ScopeType, #[serde(rename = "type")]
path: String, pub scope_type: ScopeType,
actions: Vec<Action>, pub name: String,
pub actions: Vec<Action>,
} }
impl Scope { impl Scope {
pub fn new(scope_type: ScopeType, path: String, actions: &[Action]) -> Self { pub fn new(scope_type: ScopeType, path: String, actions: &[Action]) -> Self {
Self { Self {
scope_type, scope_type,
path, name: path,
actions: actions.to_vec(), actions: actions.to_vec(),
} }
} }
@ -62,7 +66,7 @@ impl fmt::Display for Scope {
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(","); .join(",");
write!(f, "{}:{}:{}", self.scope_type, self.path, actions) write!(f, "{}:{}:{}", self.scope_type, self.name, actions)
} }
} }
@ -93,7 +97,7 @@ impl TryFrom<&str> for Scope {
Ok(Scope { Ok(Scope {
scope_type, scope_type,
path: String::from(path), name: String::from(path),
actions actions
}) })
} else { } else {
@ -101,68 +105,4 @@ impl TryFrom<&str> for Scope {
//Err(serde::de::Error::custom("Malformed scope string!")) //Err(serde::de::Error::custom("Malformed scope string!"))
} }
} }
}
pub struct ScopeVisitor {
}
impl<'de> Visitor<'de> for ScopeVisitor {
type Value = Scope;
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_str<E>(self, val: &str) -> Result<Self::Value, E>
where
E: serde::de::Error {
println!("Start of visit_str!");
let res = match Scope::try_from(val) {
Ok(val) => Ok(val),
Err(e) => Err(serde::de::Error::custom(format!("{}", e)))
};
res
/* let splits: Vec<&str> = val.split(":").collect();
if splits.len() == 3 {
let scope_type = match splits[0] {
"repository" => ScopeType::Repository,
_ => {
return Err(serde::de::Error::custom(format!("Invalid scope type: `{}`!", splits[0])));
}
};
let path = splits[1];
let actions: Result<Vec<Action>, E> = splits[2]
.split(",")
.map(|a| match a {
"pull" => Ok(Action::Pull),
"push" => Ok(Action::Push),
_ => Err(serde::de::Error::custom(format!("Invalid action: `{}`!", a))),
}).collect();
let actions = actions?;
Ok(Scope {
scope_type,
path: String::from(path),
actions
})
} else {
Err(serde::de::Error::custom("Malformed scope string!"))
} */
}
}
impl<'de> Deserialize<'de> for Scope {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de> {
deserializer.deserialize_str(ScopeVisitor {})
}
} }

View File

@ -4,11 +4,15 @@ use async_trait::async_trait;
use axum::{http::{StatusCode, header, HeaderName, HeaderMap, request::Parts}, extract::FromRequestParts}; 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 hmac::{Hmac, digest::KeyInit};
use jwt::VerifyWithKey;
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tracing::debug; use tracing::debug;
use crate::{app_state::AppState, database::Database}; use crate::{app_state::AppState, database::Database};
use super::RepositoryVisibility; use super::{RepositoryVisibility, scope::Scope};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum LoginSource { pub enum LoginSource {
@ -45,6 +49,50 @@ impl User {
} }
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AuthToken {
#[serde(rename = "iss")]
pub issuer: String,
#[serde(rename = "sub")]
pub subject: String,
#[serde(rename = "aud")]
pub audience: String,
#[serde(rename = "exp")]
#[serde(with = "chrono::serde::ts_seconds")]
pub expiration: DateTime<Utc>,
#[serde(rename = "nbf")]
#[serde(with = "chrono::serde::ts_seconds")]
pub not_before: DateTime<Utc>,
#[serde(rename = "iat")]
#[serde(with = "chrono::serde::ts_seconds")]
pub issued_at: DateTime<Utc>,
#[serde(rename = "jti")]
pub jwt_id: String,
pub access: Vec<Scope>,
}
impl AuthToken {
pub fn new(issuer: String, subject: String, audience: String, expiration: DateTime<Utc>, not_before: DateTime<Utc>, issued_at: DateTime<Utc>, jwt_id: String, access: Vec<Scope>) -> Self {
Self {
issuer,
subject,
audience,
expiration,
not_before,
issued_at,
jwt_id,
access
}
}
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct TokenInfo { pub struct TokenInfo {
pub token: String, pub token: String,
@ -64,12 +112,12 @@ impl TokenInfo {
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct UserAuth { pub struct UserAuth {
pub user: User, pub user: Option<User>,
pub token: TokenInfo, pub token: AuthToken,
} }
impl UserAuth { impl UserAuth {
pub fn new(user: User, token: TokenInfo) -> Self { pub fn new(user: Option<User>, token: AuthToken) -> Self {
Self { Self {
user, user,
token, token,
@ -87,8 +135,6 @@ impl FromRequestParts<Arc<AppState>> for UserAuth {
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());
debug!("starting UserAuth request parts");
let auth = String::from( let auth = String::from(
parts.headers parts.headers
.get(header::AUTHORIZATION) .get(header::AUTHORIZATION)
@ -97,8 +143,6 @@ impl FromRequestParts<Arc<AppState>> for UserAuth {
.map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))? .map_err(|_| (StatusCode::UNAUTHORIZED, failure_headers.clone()))?
); );
debug!("got auth header");
let token = match auth.split_once(' ') { let token = match auth.split_once(' ') {
Some((auth, token)) if auth == "Bearer" => token, Some((auth, token)) if auth == "Bearer" => token,
// This line would allow empty tokens // This line would allow empty tokens
@ -106,22 +150,42 @@ impl FromRequestParts<Arc<AppState>> for UserAuth {
_ => return Err( (StatusCode::UNAUTHORIZED, failure_headers) ), _ => return Err( (StatusCode::UNAUTHORIZED, failure_headers) ),
}; };
debug!("got token");
// If the token is not valid, return an unauthorized response // If the token is not valid, return an unauthorized response
let database = &state.database; let jwt_key: Hmac<Sha256> = Hmac::new_from_slice(state.config.jwt_key.as_bytes())
if let Ok(Some(user)) = database.verify_user_token(token.to_string()).await { .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, HeaderMap::new()) )?;
debug!("Authenticated user through request extractor: {}", user.user.username);
Ok(user) match VerifyWithKey::<AuthToken>::verify_with_key(token, &jwt_key) {
} else { Ok(token) => {
debug!("Failure to verify user token, responding with auth realm"); // attempt to get the user
if !token.subject.is_empty() {
let database = &state.database;
if let Ok(Some(user)) = database.get_user(token.subject.clone()).await {
return Ok(UserAuth::new(Some(user), token));
} else {
debug!("failure to get user from token: {:?}", token);
}
} else {
return Ok(UserAuth::new(None, token));
}
Err(( /* let database = &state.database;
StatusCode::UNAUTHORIZED, if let Ok(user) = database.get_user(token.subject.clone()).await {
failure_headers return Ok(UserAuth::new(user, token));
)) } else {
debug!("failure to get user from token: {:?}", token);
} */
},
Err(e) => {
debug!("Failure to verify user token: '{}'", e);
}
} }
debug!("Failure to verify user token, responding with auth realm");
Err((
StatusCode::UNAUTHORIZED,
failure_headers
))
} }
} }

View File

@ -69,7 +69,7 @@ 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<()> {
let config = Config::new() let mut config = Config::new()
.expect("Failure to parse config!"); .expect("Failure to parse config!");
tracing_subscriber::fmt() tracing_subscriber::fmt()
@ -92,6 +92,9 @@ async fn main() -> anyhow::Result<()> {
.connect_with(connection_options).await?; .connect_with(connection_options).await?;
pool.create_schema().await?; pool.create_schema().await?;
// set jwt key
config.jwt_key = pool.get_jwt_secret().await?;
let storage_driver: Mutex<Box<dyn StorageDriver>> = match &config.storage { let storage_driver: Mutex<Box<dyn StorageDriver>> = match &config.storage {
StorageConfig::Filesystem(fs) => { StorageConfig::Filesystem(fs) => {
Mutex::new(Box::new(FilesystemDriver::new(&fs.path))) Mutex::new(Box::new(FilesystemDriver::new(&fs.path)))