feat: add RBAC (read/write/admin) and persistent audit log

- Add Role enum to tokens: Read, Write, Admin (default: Read)
- Enforce role-based access in auth middleware (read-only tokens blocked from PUT/POST/DELETE)
- Add role field to token create/list/verify API
- Add persistent audit log (append-only JSONL) for all registry operations
- Audit logging across all registries: docker, npm, maven, pypi, cargo, raw

DevITWay
This commit is contained in:
2026-03-03 10:40:59 +00:00
parent f560e5f76b
commit 402d2321ef
10 changed files with 181 additions and 17 deletions

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
use crate::activity_log::{ActionType, ActivityEntry};
use crate::audit::AuditEntry;
use crate::AppState;
use axum::{
extract::{Path, State},
@@ -50,6 +51,7 @@ async fn download(
"cargo",
"LOCAL",
));
state.audit.log(AuditEntry::new("pull", "api", "", "cargo", ""));
(StatusCode::OK, data).into_response()
}
Err(_) => StatusCode::NOT_FOUND.into_response(),

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
use crate::activity_log::{ActionType, ActivityEntry};
use crate::audit::AuditEntry;
use crate::registry::docker_auth::DockerAuth;
use crate::storage::Storage;
use crate::validation::{validate_digest, validate_docker_name, validate_docker_reference};
@@ -488,6 +489,7 @@ async fn put_manifest(
"docker",
"LOCAL",
));
state.audit.log(AuditEntry::new("push", "api", &format!("{}:{}", name, reference), "docker", "manifest"));
state.repo_index.invalidate("docker");
let location = format!("/v2/{}/manifests/{}", name, reference);

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
use crate::activity_log::{ActionType, ActivityEntry};
use crate::audit::AuditEntry;
use crate::AppState;
use axum::{
body::Bytes,
@@ -42,6 +43,7 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
"maven",
"CACHE",
));
state.audit.log(AuditEntry::new("cache_hit", "api", "", "maven", ""));
return with_content_type(&path, data).into_response();
}
@@ -58,6 +60,7 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
"maven",
"PROXY",
));
state.audit.log(AuditEntry::new("proxy_fetch", "api", "", "maven", ""));
let storage = state.storage.clone();
let key_clone = key.clone();
@@ -103,6 +106,7 @@ async fn upload(
"maven",
"LOCAL",
));
state.audit.log(AuditEntry::new("push", "api", "", "maven", ""));
state.repo_index.invalidate("maven");
StatusCode::CREATED
}

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
use crate::activity_log::{ActionType, ActivityEntry};
use crate::audit::AuditEntry;
use crate::AppState;
use axum::{
body::Bytes,
@@ -48,6 +49,7 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
"npm",
"CACHE",
));
state.audit.log(AuditEntry::new("cache_hit", "api", "", "npm", ""));
}
return with_content_type(is_tarball, data).into_response();
}
@@ -67,6 +69,7 @@ async fn handle_request(State(state): State<Arc<AppState>>, Path(path): Path<Str
"npm",
"PROXY",
));
state.audit.log(AuditEntry::new("proxy_fetch", "api", "", "npm", ""));
}
let storage = state.storage.clone();

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
use crate::activity_log::{ActionType, ActivityEntry};
use crate::audit::AuditEntry;
use crate::AppState;
use axum::{
extract::{Path, State},
@@ -115,6 +116,7 @@ async fn download_file(
"pypi",
"CACHE",
));
state.audit.log(AuditEntry::new("cache_hit", "api", "", "pypi", ""));
let content_type = if filename.ends_with(".whl") {
"application/zip"
@@ -156,6 +158,7 @@ async fn download_file(
"pypi",
"PROXY",
));
state.audit.log(AuditEntry::new("proxy_fetch", "api", "", "pypi", ""));
// Cache in local storage
let storage = state.storage.clone();

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: MIT
use crate::activity_log::{ActionType, ActivityEntry};
use crate::audit::AuditEntry;
use crate::AppState;
use axum::{
body::Bytes,
@@ -35,6 +36,7 @@ async fn download(State(state): State<Arc<AppState>>, Path(path): Path<String>)
state
.activity
.push(ActivityEntry::new(ActionType::Pull, path, "raw", "LOCAL"));
state.audit.log(AuditEntry::new("pull", "api", "", "raw", ""));
// Guess content type from extension
let content_type = guess_content_type(&key);
@@ -72,6 +74,7 @@ async fn upload(
state
.activity
.push(ActivityEntry::new(ActionType::Push, path, "raw", "LOCAL"));
state.audit.log(AuditEntry::new("push", "api", "", "raw", ""));
StatusCode::CREATED.into_response()
}
Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),