Rust implementation of OCI Distribution Spec with granular access control
1use base64::{prelude::BASE64_STANDARD, Engine};
2use std::sync::Arc;
3
4use crate::metrics;
5use crate::permissions::{has_permission, Action};
6use crate::response::unauthorized;
7use crate::state::{self, User};
8use axum::{
9 body::Body,
10 extract::State,
11 http::{HeaderMap, Response},
12};
13
14fn parse_auth_header(headers: &HeaderMap) -> Option<User> {
15 let auth_header = headers.get("authorization")?;
16 let auth_str = auth_header.to_str().ok()?;
17 let auth_decoded_vec = BASE64_STANDARD
18 .decode(auth_str.trim_start_matches("Basic "))
19 .ok()?;
20 let decoded = String::from_utf8(auth_decoded_vec).ok()?;
21
22 let parts: Vec<&str> = decoded.split(':').collect();
23 if parts.len() == 2 {
24 Some(User {
25 username: parts[0].to_string(),
26 password: parts[1].to_string(),
27 permissions: vec![],
28 })
29 } else {
30 None
31 }
32}
33
34/// Authenticate user from headers and return User object
35pub async fn authenticate_user(state: &Arc<state::App>, headers: &HeaderMap) -> Result<User, ()> {
36 let user = parse_auth_header(headers).ok_or(())?;
37
38 let users = state.users.lock().await;
39 for u in users.iter() {
40 if u.username == user.username && u.password == user.password {
41 return Ok(u.clone());
42 }
43 }
44
45 metrics::AUTH_FAILURES_TOTAL.inc();
46 Err(())
47}
48
49/// Check if authenticated user has permission for the action
50pub async fn check_permission(
51 state: &Arc<state::App>,
52 headers: &HeaderMap,
53 repository: &str,
54 tag: Option<&str>,
55 action: Action,
56) -> Result<User, ()> {
57 // First authenticate
58 let user = authenticate_user(state, headers).await?;
59
60 // Then check permission
61 if has_permission(&user, repository, tag, action) {
62 Ok(user)
63 } else {
64 log::warn!(
65 "User {} denied {} access to {}/{}",
66 user.username,
67 action.as_str(),
68 repository,
69 tag.unwrap_or("*")
70 );
71 metrics::PERMISSION_DENIALS_TOTAL.inc();
72 Err(())
73 }
74}
75
76pub(crate) async fn get(State(data): State<Arc<state::App>>, headers: HeaderMap) -> Response<Body> {
77 log::info!("Incoming request headers: {:?}", headers);
78
79 match authenticate_user(&data, &headers).await {
80 Ok(user) => {
81 log::info!("User {} authenticated successfully", user.username);
82 Response::builder()
83 .status(200)
84 .body(Body::from("200 OK"))
85 .unwrap()
86 }
87 Err(_) => {
88 log::warn!("Authentication failed");
89 unauthorized(&data.args.host)
90 }
91 }
92}