mirror of
https://github.com/getnora-io/nora.git
synced 2026-04-12 23:10:32 +00:00
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:
77
nora-registry/src/audit.rs
Normal file
77
nora-registry/src/audit.rs
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (c) 2026 Volkov Pavel | DevITWay
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//! Persistent audit log — append-only JSONL file
|
||||
//!
|
||||
//! Records who/when/what for every registry operation.
|
||||
//! File: {storage_path}/audit.jsonl
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::fs::{self, OpenOptions};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use tracing::{info, warn};
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct AuditEntry {
|
||||
pub ts: DateTime<Utc>,
|
||||
pub action: String,
|
||||
pub actor: String,
|
||||
pub artifact: String,
|
||||
pub registry: String,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
impl AuditEntry {
|
||||
pub fn new(action: &str, actor: &str, artifact: &str, registry: &str, detail: &str) -> Self {
|
||||
Self {
|
||||
ts: Utc::now(),
|
||||
action: action.to_string(),
|
||||
actor: actor.to_string(),
|
||||
artifact: artifact.to_string(),
|
||||
registry: registry.to_string(),
|
||||
detail: detail.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AuditLog {
|
||||
path: PathBuf,
|
||||
writer: Mutex<Option<fs::File>>,
|
||||
}
|
||||
|
||||
impl AuditLog {
|
||||
pub fn new(storage_path: &str) -> Self {
|
||||
let path = PathBuf::from(storage_path).join("audit.jsonl");
|
||||
let writer = match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
{
|
||||
Ok(f) => {
|
||||
info!(path = %path.display(), "Audit log initialized");
|
||||
Mutex::new(Some(f))
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(path = %path.display(), error = %e, "Failed to open audit log, auditing disabled");
|
||||
Mutex::new(None)
|
||||
}
|
||||
};
|
||||
Self { path, writer }
|
||||
}
|
||||
|
||||
pub fn log(&self, entry: AuditEntry) {
|
||||
if let Some(ref mut file) = *self.writer.lock() {
|
||||
if let Ok(json) = serde_json::to_string(&entry) {
|
||||
let _ = writeln!(file, "{}", json);
|
||||
let _ = file.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &PathBuf {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user