Skip to content

Commit

Permalink
fix: scope issue
Browse files Browse the repository at this point in the history
  • Loading branch information
PThorpe92 committed Aug 19, 2024
1 parent 094e637 commit da785a0
Show file tree
Hide file tree
Showing 10 changed files with 165 additions and 175 deletions.
16 changes: 8 additions & 8 deletions migrations/01_createtables.sql
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ CREATE TABLE IF NOT EXISTS blobs (
ref_count INTEGER NOT NULL DEFAULT 0,
chunk_count INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id)
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS tags (
Expand All @@ -25,8 +25,8 @@ CREATE TABLE IF NOT EXISTS tags (
repository_id INTEGER NOT NULL,
tag TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id),
FOREIGN KEY (manifest_id) REFERENCES manifests(id),
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE,
FOREIGN KEY (manifest_id) REFERENCES manifests(id) ON DELETE CASCADE,
UNIQUE (repository_id, tag)
);

Expand All @@ -39,7 +39,7 @@ CREATE TABLE IF NOT EXISTS manifests (
size INTEGER NOT NULL,
schema_version INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (repository_id) REFERENCES repositories(id)
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS manifest_layers (
Expand All @@ -50,7 +50,7 @@ CREATE TABLE IF NOT EXISTS manifest_layers (
size INTEGER NOT NULL,
media_type TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (manifest_id) REFERENCES manifests(id)
FOREIGN KEY (manifest_id) REFERENCES manifests(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS uploads (
Expand Down Expand Up @@ -78,8 +78,8 @@ CREATE TABLE IF NOT EXISTS repository_scopes (
push BOOLEAN NOT NULL DEFAULT FALSE,
pull BOOLEAN NOT NULL DEFAULT FALSE,
del BOOLEAN NOT NULL DEFAULT FALSE,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (repository_id) REFERENCES repositories(id)
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY (repository_id) REFERENCES repositories(id) ON DELETE CASCADE
);

CREATE TABLE IF NOT EXISTS clients (
Expand All @@ -97,7 +97,7 @@ CREATE TABLE IF NOT EXISTS tokens (
token TEXT NOT NULL UNIQUE,
client_id TEXT UNIQUE,
expires TIMESTAMP NOT NULL DEFAULT (datetime('now', '+1 day')),
FOREIGN KEY (client_id) REFERENCES clients(client_id),
FOREIGN KEY (client_id) REFERENCES clients(client_id) ON DELETE CASCADE,
FOREIGN KEY (account) REFERENCES users(email)
);

Expand Down
34 changes: 26 additions & 8 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::fmt::Formatter;
use std::{fmt::Formatter, str::FromStr};

use super::UserScope;
use crate::{
Expand Down Expand Up @@ -243,11 +243,17 @@ pub async fn check_scope_middleware(req: Request, next: Next) -> Result<Response
match Action::from_request(&req) {
Some(required_scope) => {
let repo_name = get_repo_name_from_path(&req).unwrap_or("*");
if let Some(scopes) = claims.scopes.0.iter().find(|(r, _)| r.eq(&repo_name)) {
if scopes.1.contains(&required_scope) {
info!("user has required scope: {}", required_scope);
return Ok(next.run(req).await);
}
if claims.scopes.is_allowed(repo_name, required_scope) {
info!("user has required scope: {}", required_scope);
return Ok(next.run(req).await);
} else {
info!("user does not have required scope: {}", required_scope);
return Err((
StatusCode::UNAUTHORIZED,
auth_response_headers(&req),
"requested unauthorized scope",
)
.into_response());
}
}
None => {
Expand Down Expand Up @@ -360,10 +366,16 @@ pub async fn auth_token_get(
headers: HeaderMap,
req: Request,
) -> impl IntoResponse {
let scope = params.scope.unwrap_or_default();
if let Ok(auth) = check_auth_headers(&headers, &mut conn).await {
if let Some(ref claims) = auth.claims {
if claims.is_valid() && claims.scopes.is_allowed(&scope) {
let requested = get_requested_scope(&req);
let scope = UserScope::from_str(&requested).unwrap();
if claims.is_valid()
&& claims.scopes.is_allowed(
scope.0.keys().next().unwrap(),
*scope.0.values().next().unwrap(),
)
{
return (
StatusCode::OK,
serde_json::to_string(&TokenResponse {
Expand All @@ -372,6 +384,12 @@ pub async fn auth_token_get(
.unwrap(),
)
.into_response();
} else {
tracing::error!(
"invalid claims or scope: {:?} valid? : {:?}",
claims,
claims.is_valid()
);
}
}
}
Expand Down
12 changes: 2 additions & 10 deletions src/blobs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use crate::{
codes::{Code, ErrorResponse},
database::DbConn,
storage_driver::{Backend, StorageError},
util::{parse_content_length, parse_content_range, strip_sha_header},
util::{parse_content_length, parse_content_range},
};
use axum::{
extract::{Path, Query, Request},
Expand All @@ -27,14 +27,10 @@ pub async fn get_blob(
DbConn(mut conn): DbConn,
Extension(blob_storage): Extension<Arc<Backend>>,
) -> impl IntoResponse {
let digest = strip_sha_header(&digest);
match blob_storage.read_blob(&mut conn, &name, &digest).await {
Ok(data) => {
let mut headers = HeaderMap::new();
headers.insert(
"Docker-Content-Digest",
format!("sha256:{}", digest).parse().unwrap(),
);
headers.insert("Docker-Content-Digest", digest.parse().unwrap());
(headers, data).into_response()
}
Err(_) => ErrorResponse::from_code(&Code::BlobUnknown, String::from("blob not found"))
Expand All @@ -48,7 +44,6 @@ pub async fn check_blob(
DbConn(mut conn): DbConn,
) -> impl IntoResponse {
debug!("HEAD /v2/{}/blobs/{}", name, digest);
let digest = strip_sha_header(&digest);
let exists = sqlx::query!("SELECT COUNT(*) as count from blobs join repositories r on r.id = (select id from repositories where name = ?) AND digest = ?", name, digest)
.fetch_one(&mut *conn)
.await
Expand All @@ -71,7 +66,6 @@ pub async fn delete_blob(
storage: Extension<Arc<Backend>>,
) -> impl IntoResponse {
debug!("DELETE /v2/{}/blobs/{}", name, digest);
let digest = strip_sha_header(&digest);
if sqlx::query!("SELECT COUNT(*) as count from blobs join repositories r on r.id = (select id from repositories where name = ?) AND digest = ?", name, digest)
.fetch_one(&mut *conn)
.await
Expand Down Expand Up @@ -147,7 +141,6 @@ pub async fn put_upload_session_blob(
match upload_chunk(&name, &session_id, cloned, &mut conn, request).await {
Ok(result_digest) => {
let digest = query.digest.unwrap();
let result_digest = format!("sha256:{}", result_digest);
if !result_digest.eq(&digest) {
error!("{} did not match {}", result_digest, digest);
let code = crate::codes::Code::DigestInvalid;
Expand Down Expand Up @@ -182,7 +175,6 @@ async fn finish_upload_session(
.combine_chunks(conn, name, session_id)
.await
.unwrap();
let digest = format!("sha256:{}", digest);
let location = format!("/v2/{}/blobs/{}", name, digest);
let mut return_headers = HeaderMap::new();
return_headers.insert(LOCATION, location.parse().unwrap());
Expand Down
1 change: 0 additions & 1 deletion src/content_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,6 @@ pub async fn get_tags_list(
match query.fetch_all(&mut *conn).await {
Ok(rows) => {
let tags: Vec<String> = rows.into_iter().map(|row| row.get(0)).collect();

let mut headers = HeaderMap::new();
if let Some(limit) = n {
if tags.len() == limit {
Expand Down
5 changes: 3 additions & 2 deletions src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,9 @@ pub async fn migrate(
pub async fn drop_tables(pool: &mut SqliteConnection) -> Result<(), sqlx::Error> {
let mut tx = pool.begin().await?;
for table in TABLES {
tx.execute(sqlx::query(&format!("DROP TABLE {};", table)))
.await?;
let _ = tx
.execute(sqlx::query(&format!("DROP TABLE {};", table)))
.await;
}
Ok(())
}
Expand Down
99 changes: 60 additions & 39 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,41 @@ pub fn set_env() {
JWT_SECRET.set(jwt_secret).unwrap();
}

#[derive(serde::Serialize, PartialEq, Eq, serde::Deserialize, Clone, Debug)]
#[derive(serde::Serialize, PartialEq, Eq, serde::Deserialize, Clone, Copy, Debug)]
pub enum Action {
Pull,
Push,
Delete,
}

impl PartialOrd for Action {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}

impl Ord for Action {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
match (self, other) {
(Action::Pull, Action::Pull) => std::cmp::Ordering::Equal,
(Action::Pull, Action::Push) => std::cmp::Ordering::Less,
(Action::Pull, Action::Delete) => std::cmp::Ordering::Less,
(Action::Push, Action::Pull) => std::cmp::Ordering::Greater,
(Action::Push, Action::Push) => std::cmp::Ordering::Equal,
(Action::Push, Action::Delete) => std::cmp::Ordering::Less,
(Action::Delete, Action::Pull) => std::cmp::Ordering::Greater,
(Action::Delete, Action::Push) => std::cmp::Ordering::Greater,
(Action::Delete, Action::Delete) => std::cmp::Ordering::Equal,
}
}
}

impl Action {
pub fn check_permission(&self, actions: &[Action]) -> bool {
// self here is the users available scope
// actions is the requested scope
pub fn check_permission(&self, requested: Action) -> bool {
match self {
Action::Pull => actions.contains(&Action::Pull),
Action::Push => actions.contains(&Action::Push),
Action::Delete => true,
Action::Delete => true, // Delete can perform any action
Action::Push => requested.eq(&Action::Pull) || requested.eq(&Action::Push),
Action::Pull => requested.eq(&Action::Pull),
}
}

Expand All @@ -67,7 +88,7 @@ impl Action {
Method::PUT | Method::POST | Method::PATCH => Some(Self::Push),
Method::DELETE => Some(Self::Delete),
_ => {
if path.contains("blobs") {
if path.starts_with("/v2/") {
Some(Self::Pull)
} else {
None
Expand Down Expand Up @@ -111,29 +132,25 @@ impl std::str::FromStr for Action {
}
pub type Repo = String;
#[derive(serde::Serialize, Default, serde::Deserialize, Clone, Debug)]
pub struct UserScope(HashMap<Repo, Vec<Action>>);
pub struct UserScope(HashMap<Repo, Action>);

impl UserScope {
pub fn is_allowed(&self, requested: &str) -> bool {
// this is going to be called on the claims scope injected into the request by the
// middleware before it's assigned a scoped token
tracing::info!("checking scope: {}", requested);
let requested = UserScope::from_str(requested).unwrap_or_default();
for (repo, requested_actions) in requested.0.iter() {
if repo == "*" {
return self.0.values().next().is_some_and(|avail| {
avail
.iter()
.all(|ax| ax.check_permission(requested_actions))
});
pub fn is_allowed(&self, repo: &str, action: Action) -> bool {
tracing::info!("checking scope: {} {}", repo, action);
if repo == "*" {
if self
.0
.values()
.any(|available_action| !available_action.check_permission(action))
{
return false;
}
if self.0.get(repo).is_some_and(|avail| {
avail
.iter()
.any(|ax| !ax.check_permission(requested_actions))
}) {
} else if let Some(available_action) = self.0.get(repo) {
if !available_action.check_permission(action) {
return false;
}
} else {
return false;
}
true
}
Expand All @@ -149,11 +166,15 @@ impl FromStr for UserScope {
return Err(format!("invalid scope: {}", scope));
}
let repo = parts[1];
let actions = parts[2].parse::<Action>().unwrap();
let action = parts[2].parse::<Action>().unwrap();
scopes
.entry(repo.to_string())
.or_insert_with(Vec::new)
.extend_from_slice(&actions.to_vec());
.and_modify(|existing_action| {
if action > *existing_action {
*existing_action = action;
}
})
.or_insert(action);
}
tracing::info!("parsed scopes: {:?}", scopes);
Ok(UserScope(scopes))
Expand All @@ -174,14 +195,14 @@ pub async fn get_user_scopes(conn: &mut SqliteConnection, user_id: &str) -> User
.expect("unable to fetch user scopes");
let mut scopes = UserScope::default();
for row in rows {
scopes.0.insert(
row.name,
Vec::from([
if row.pull { Action::Pull } else { continue },
if row.push { Action::Push } else { continue },
if row.del { Action::Delete } else { continue },
]),
);
let highest_action = if row.del {
Action::Delete
} else if row.push {
Action::Push
} else {
Action::Pull
};
scopes.0.insert(row.name, highest_action);
}
scopes
}
Expand All @@ -191,7 +212,7 @@ pub async fn get_admin_scopes(conn: &mut SqliteConnection) -> UserScope {
UserScope(
repos
.into_iter()
.map(|row| (row, Vec::from([Action::Pull, Action::Push, Action::Delete])))
.map(|row| (row, Action::Delete))
.collect::<HashMap<Repo, _>>(),
)
}
Expand All @@ -200,7 +221,7 @@ pub async fn default_public_scopes(conn: &mut SqliteConnection) -> UserScope {
let repos = database::get_repositories(conn, true).await;
let mut scopes = HashMap::new();
for repo in repos {
scopes.insert(repo, vec![Action::Pull]);
scopes.insert(repo, Action::Pull);
}
UserScope(scopes)
}
3 changes: 1 addition & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ async fn main() {
.expect("unable to bind to port");

let routes = Router::new()
.route("/v2/", get(get_v2))
.route("/auth/login", post(login_user))
.route("/auth/token", get(auth_token_get))
.route("/auth/register", post(register_user))
Expand All @@ -152,6 +151,7 @@ async fn main() {
.route("/users", get(get_users))
.route("/users/:email", delete(delete_user))
.route("/users/:email/tokens", post(generate_token))
.route("/v2/", get(get_v2))
.route("/v2/:name/blobs/:digest", put(put_upload_blob))
.route("/v2/:name/blobs/:digest", get(get_blob))
.route("/v2/:name/blobs/:digest", head(check_blob))
Expand All @@ -167,7 +167,6 @@ async fn main() {
.route("/v2/:name/blobs/:digest", delete(delete_blob))
.route("/v2/:name/tags/list", get(get_tags_list))
.route("/v2/:name/manifests/:reference", get(get_manifest))
.route("/v2/:name/manifests/:reference", head(get_manifest))
.route("/v2/:name/manifests/:reference", put(push_manifest))
.route("/v2/:name/manifests/:reference", delete(delete_manifest))
.layer(from_fn(check_scope_middleware))
Expand Down
Loading

0 comments on commit da785a0

Please sign in to comment.