From 8da4c4278a87ced1420fc6e7a9ea2c567e4e7b97 Mon Sep 17 00:00:00 2001 From: devitway Date: Tue, 3 Mar 2026 11:03:40 +0000 Subject: [PATCH] style: cargo fmt DevITWay --- CHANGELOG.md.bak | 414 +++++++++++++++++++ nora-registry/src/audit.rs | 6 +- nora-registry/src/auth.rs | 20 +- nora-registry/src/gc.rs | 5 +- nora-registry/src/main.rs | 2 +- nora-registry/src/registry/cargo_registry.rs | 4 +- nora-registry/src/registry/docker.rs | 13 +- nora-registry/src/registry/maven.rs | 12 +- nora-registry/src/registry/npm.rs | 8 +- nora-registry/src/registry/pypi.rs | 8 +- nora-registry/src/registry/raw.rs | 8 +- nora-registry/src/tokens.rs | 24 +- 12 files changed, 492 insertions(+), 32 deletions(-) create mode 100644 CHANGELOG.md.bak diff --git a/CHANGELOG.md.bak b/CHANGELOG.md.bak new file mode 100644 index 0000000..02e5119 --- /dev/null +++ b/CHANGELOG.md.bak @@ -0,0 +1,414 @@ +# Changelog + +All notable changes to NORA will be documented in this file. + +--- + +## [0.2.18] - 2026-01-31 + +### Changed +- Logo styling refinements + +--- + +## [0.2.17] - 2026-01-31 + +### Added +- Copyright headers to all source files (Volkov Pavel | DevITWay) +- SPDX-License-Identifier: MIT in all .rs files + +--- + +## [0.2.16] - 2026-01-31 + +### Changed +- N○RA branding: stylized O logo across dashboard +- Fixed O letter alignment in logo + +--- + +## [0.2.15] - 2026-01-31 + +### Fixed +- Code formatting (cargo fmt) + +--- + +## [0.2.14] - 2026-01-31 + +### Fixed +- Docker dashboard now shows actual image size from manifest layers (config + layers sum) +- Previously showed only manifest file size (~500 B instead of actual image size) + +--- + +## [0.2.13] - 2026-01-31 + +### Fixed +- npm dashboard now shows correct version count and package sizes +- Parses metadata.json for versions, dist.unpackedSize, and time.modified +- Previously showed 0 versions / 0 B for all packages + +--- + +## [0.2.12] - 2026-01-30 + +### Added + +#### Configurable Rate Limiting +- Rate limits now configurable via `config.toml` and environment variables +- New config section `[rate_limit]` with parameters: `auth_rps`, `auth_burst`, `upload_rps`, `upload_burst`, `general_rps`, `general_burst` +- Environment variables: `NORA_RATE_LIMIT_{AUTH|UPLOAD|GENERAL}_{RPS|BURST}` + +#### Secrets Provider Architecture +- Trait-based secrets management (`SecretsProvider` trait) +- ENV provider as default (12-Factor App pattern) +- Protected secrets with `zeroize` (memory zeroed on drop) +- Redacted Debug impl prevents secret leakage in logs +- New config section `[secrets]` with `provider` and `clear_env` options + +#### Docker Image Metadata +- Support for image metadata retrieval + +#### Documentation +- Bilingual onboarding guide (EN/RU) + +--- + +## [0.2.11] - 2026-01-26 + +### Added +- Internationalization (i18n) support +- PyPI registry proxy +- UI improvements + +--- + +## [0.2.10] - 2026-01-26 + +### Changed +- Dark theme applied to all UI pages + +--- + +## [0.2.9] - 2026-01-26 + +### Changed +- Version bump release + +--- + +## [0.2.8] - 2026-01-26 + +### Added +- Dashboard endpoint added to OpenAPI documentation + +--- + +## [0.2.7] - 2026-01-26 + +### Added +- Dynamic version display in UI sidebar + +--- + +## [0.2.6] - 2026-01-26 + +### Added + +#### Dashboard Metrics +- Global stats panel: downloads, uploads, artifacts, cache hit rate, storage +- Extended registry cards with artifact count, size, counters +- Activity log (last 20 events) + +#### UI +- Dark theme (bg: #0f172a, cards: #1e293b) + +--- + +## [0.2.5] - 2026-01-26 + +### Fixed +- Docker push/pull: added PATCH endpoint for chunked uploads + +--- + +## [0.2.4] - 2026-01-26 + +### Fixed +- Rate limiting: health/metrics endpoints now exempt +- Increased upload rate limits for Docker parallel requests + +--- + +## [0.2.0] - 2026-01-25 + +### Added + +#### UI: SVG Brand Icons +- Replaced emoji icons with proper SVG brand icons (Simple Icons style) +- Docker, Maven, npm, Cargo, PyPI icons now render as scalable vector graphics +- Consistent icon styling across dashboard, sidebar, and detail pages + +#### Testing Infrastructure +- Unit tests for LocalStorage (8 tests): put/get, list, stat, health_check +- Unit tests for S3Storage with wiremock HTTP mocking (11 tests) +- Integration tests for auth/htpasswd (7 tests) +- Token lifecycle tests (11 tests) +- Validation tests (21 tests) +- **Total: 75 tests passing** + +#### Security: Input Validation (`validation.rs`) +- Path traversal protection: rejects `../`, `..\\`, null bytes, absolute paths +- Docker image name validation per OCI distribution spec +- Content digest validation (`sha256:[64 hex]`, `sha512:[128 hex]`) +- Docker tag/reference validation +- Storage key length limits (max 1024 chars) + +#### Security: Rate Limiting (`rate_limit.rs`) +- Auth endpoints: 1 req/sec, burst 5 (brute-force protection) +- Upload endpoints: 10 req/sec, burst 20 +- General endpoints: 100 req/sec, burst 200 +- Uses `tower_governor` 0.8 with `PeerIpKeyExtractor` + +#### Observability: Request ID Tracking (`request_id.rs`) +- `X-Request-ID` header added to all responses +- Accepts upstream request ID or generates UUID v4 +- Tracing spans include request_id for log correlation + +#### CLI: Migrate Command (`migrate.rs`) +- `nora migrate --from local --to s3` - migrate between storage backends +- `--dry-run` flag for preview without copying +- Progress bar with indicatif +- Skips existing files in destination +- Summary statistics (migrated, skipped, failed, bytes) + +#### Error Handling (`error.rs`) +- `AppError` enum with `IntoResponse` for Axum +- Automatic conversion from `StorageError` and `ValidationError` +- JSON error responses with request_id support + +### Changed +- `StorageError` now uses `thiserror` derive macro +- `TokenError` now uses `thiserror` derive macro +- Storage wrapper validates keys before delegating to backend +- Docker registry handlers validate name, digest, reference inputs +- Body size limit set to 100MB default via `DefaultBodyLimit` + +### Dependencies Added +- `thiserror = "2"` - typed error handling +- `tower_governor = "0.8"` - rate limiting +- `governor = "0.10"` - rate limiting backend +- `tempfile = "3"` (dev) - temporary directories for tests +- `wiremock = "0.6"` (dev) - HTTP mocking for S3 tests + +### Files Added +- `src/validation.rs` - input validation module +- `src/migrate.rs` - storage migration module +- `src/error.rs` - application error types +- `src/request_id.rs` - request ID middleware +- `src/rate_limit.rs` - rate limiting configuration + +--- + +## [0.1.0] - 2026-01-24 + +### Added +- Multi-protocol support: Docker Registry v2, Maven, npm, Cargo, PyPI +- Web UI dashboard +- Swagger UI (`/api-docs`) +- Storage backends: Local filesystem, S3-compatible +- Smart proxy/cache for Maven and npm +- Health checks (`/health`, `/ready`) +- Basic authentication (htpasswd with bcrypt) +- API tokens (revocable, per-user) +- Prometheus metrics (`/metrics`) +- JSON structured logging +- Environment variable configuration +- Graceful shutdown (SIGTERM/SIGINT) +- Backup/restore commands + +--- + +# Журнал изменений (RU) + +Все значимые изменения NORA документируются в этом файле. + +--- + +## [0.2.12] - 2026-01-30 + +### Добавлено + +#### Настраиваемый Rate Limiting +- Rate limits настраиваются через `config.toml` и переменные окружения +- Новая секция `[rate_limit]` с параметрами: `auth_rps`, `auth_burst`, `upload_rps`, `upload_burst`, `general_rps`, `general_burst` +- Переменные окружения: `NORA_RATE_LIMIT_{AUTH|UPLOAD|GENERAL}_{RPS|BURST}` + +#### Архитектура Secrets Provider +- Trait-based управление секретами (`SecretsProvider` trait) +- ENV provider по умолчанию (12-Factor App паттерн) +- Защищённые секреты с `zeroize` (память обнуляется при drop) +- Redacted Debug impl предотвращает утечку секретов в логи +- Новая секция `[secrets]` с опциями `provider` и `clear_env` + +#### Docker Image Metadata +- Поддержка получения метаданных образов + +#### Документация +- Двуязычный onboarding guide (EN/RU) + +--- + +## [0.2.11] - 2026-01-26 + +### Добавлено +- Поддержка интернационализации (i18n) +- PyPI registry proxy +- Улучшения UI + +--- + +## [0.2.10] - 2026-01-26 + +### Изменено +- Тёмная тема применена ко всем страницам UI + +--- + +## [0.2.9] - 2026-01-26 + +### Изменено +- Релиз с обновлением версии + +--- + +## [0.2.8] - 2026-01-26 + +### Добавлено +- Dashboard endpoint добавлен в OpenAPI документацию + +--- + +## [0.2.7] - 2026-01-26 + +### Добавлено +- Динамическое отображение версии в сайдбаре UI + +--- + +## [0.2.6] - 2026-01-26 + +### Добавлено + +#### Dashboard Metrics +- Глобальная панель статистики: downloads, uploads, artifacts, cache hit rate, storage +- Расширенные карточки реестров с количеством артефактов, размером, счётчиками +- Лог активности (последние 20 событий) + +#### UI +- Тёмная тема (bg: #0f172a, cards: #1e293b) + +--- + +## [0.2.5] - 2026-01-26 + +### Исправлено +- Docker push/pull: добавлен PATCH endpoint для chunked uploads + +--- + +## [0.2.4] - 2026-01-26 + +### Исправлено +- Rate limiting: health/metrics endpoints теперь исключены +- Увеличены лимиты upload для параллельных Docker запросов + +--- + +## [0.2.0] - 2026-01-25 + +### Добавлено + +#### UI: SVG иконки брендов +- Эмоджи заменены на SVG иконки брендов (стиль Simple Icons) +- Docker, Maven, npm, Cargo, PyPI теперь отображаются как векторная графика +- Единый стиль иконок на дашборде, сайдбаре и страницах деталей + +#### Тестовая инфраструктура +- Unit-тесты для LocalStorage (8 тестов): put/get, list, stat, health_check +- Unit-тесты для S3Storage с HTTP-мокированием wiremock (11 тестов) +- Интеграционные тесты auth/htpasswd (7 тестов) +- Тесты жизненного цикла токенов (11 тестов) +- Тесты валидации (21 тест) +- **Всего: 75 тестов проходят** + +#### Безопасность: Валидация ввода (`validation.rs`) +- Защита от path traversal: отклоняет `../`, `..\\`, null-байты, абсолютные пути +- Валидация имён Docker-образов по спецификации OCI distribution +- Валидация дайджестов (`sha256:[64 hex]`, `sha512:[128 hex]`) +- Валидация тегов и ссылок Docker +- Ограничение длины ключей хранилища (макс. 1024 символа) + +#### Безопасность: Rate Limiting (`rate_limit.rs`) +- Auth endpoints: 1 req/sec, burst 5 (защита от брутфорса) +- Upload endpoints: 10 req/sec, burst 20 +- Общие endpoints: 100 req/sec, burst 200 +- Использует `tower_governor` 0.8 с `PeerIpKeyExtractor` + +#### Наблюдаемость: Отслеживание Request ID (`request_id.rs`) +- Заголовок `X-Request-ID` добавляется ко всем ответам +- Принимает upstream request ID или генерирует UUID v4 +- Tracing spans включают request_id для корреляции логов + +#### CLI: Команда миграции (`migrate.rs`) +- `nora migrate --from local --to s3` - миграция между storage backends +- Флаг `--dry-run` для предпросмотра без копирования +- Прогресс-бар с indicatif +- Пропуск существующих файлов в destination +- Итоговая статистика (migrated, skipped, failed, bytes) + +#### Обработка ошибок (`error.rs`) +- Enum `AppError` с `IntoResponse` для Axum +- Автоматическая конверсия из `StorageError` и `ValidationError` +- JSON-ответы об ошибках с поддержкой request_id + +### Изменено +- `StorageError` теперь использует макрос `thiserror` +- `TokenError` теперь использует макрос `thiserror` +- Storage wrapper валидирует ключи перед делегированием backend +- Docker registry handlers валидируют name, digest, reference +- Лимит размера body установлен в 100MB через `DefaultBodyLimit` + +### Добавлены зависимости +- `thiserror = "2"` - типизированная обработка ошибок +- `tower_governor = "0.8"` - rate limiting +- `governor = "0.10"` - backend для rate limiting +- `tempfile = "3"` (dev) - временные директории для тестов +- `wiremock = "0.6"` (dev) - HTTP-мокирование для S3 тестов + +### Добавлены файлы +- `src/validation.rs` - модуль валидации ввода +- `src/migrate.rs` - модуль миграции хранилища +- `src/error.rs` - типы ошибок приложения +- `src/request_id.rs` - middleware для request ID +- `src/rate_limit.rs` - конфигурация rate limiting + +--- + +## [0.1.0] - 2026-01-24 + +### Добавлено +- Мульти-протокольная поддержка: Docker Registry v2, Maven, npm, Cargo, PyPI +- Web UI дашборд +- Swagger UI (`/api-docs`) +- Storage backends: локальная файловая система, S3-совместимое хранилище +- Умный прокси/кэш для Maven и npm +- Health checks (`/health`, `/ready`) +- Базовая аутентификация (htpasswd с bcrypt) +- API токены (отзываемые, per-user) +- Prometheus метрики (`/metrics`) +- JSON структурированное логирование +- Конфигурация через переменные окружения +- Graceful shutdown (SIGTERM/SIGINT) +- Команды backup/restore diff --git a/nora-registry/src/audit.rs b/nora-registry/src/audit.rs index 2667c15..fd15dcc 100644 --- a/nora-registry/src/audit.rs +++ b/nora-registry/src/audit.rs @@ -45,11 +45,7 @@ pub struct AuditLog { 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) - { + let writer = match OpenOptions::new().create(true).append(true).open(&path) { Ok(f) => { info!(path = %path.display(), "Audit log initialized"); Mutex::new(Some(f)) diff --git a/nora-registry/src/auth.rs b/nora-registry/src/auth.rs index 66a4e00..492472b 100644 --- a/nora-registry/src/auth.rs +++ b/nora-registry/src/auth.rs @@ -13,8 +13,8 @@ use std::collections::HashMap; use std::path::Path; use std::sync::Arc; -use crate::AppState; use crate::tokens::Role; +use crate::AppState; /// Htpasswd-based authentication #[derive(Clone)] @@ -247,12 +247,18 @@ async fn create_token( }; let role = match req.role.as_str() { - "read" => Role::Read, - "write" => Role::Write, - "admin" => Role::Admin, - _ => return (StatusCode::BAD_REQUEST, "Invalid role. Use: read, write, admin").into_response(), - }; - match token_store.create_token(&req.username, req.ttl_days, req.description, role) { + "read" => Role::Read, + "write" => Role::Write, + "admin" => Role::Admin, + _ => { + return ( + StatusCode::BAD_REQUEST, + "Invalid role. Use: read, write, admin", + ) + .into_response() + } + }; + match token_store.create_token(&req.username, req.ttl_days, req.description, role) { Ok(token) => Json(CreateTokenResponse { token, expires_in_days: req.ttl_days, diff --git a/nora-registry/src/gc.rs b/nora-registry/src/gc.rs index cb4da3b..6bb9aef 100644 --- a/nora-registry/src/gc.rs +++ b/nora-registry/src/gc.rs @@ -29,7 +29,10 @@ pub async fn run_gc(storage: &Storage, dry_run: bool) -> GcResult { // 2. Collect all referenced digests from manifests let referenced = collect_referenced_digests(storage).await; - info!("Found {} referenced digests from manifests", referenced.len()); + info!( + "Found {} referenced digests from manifests", + referenced.len() + ); // 3. Find orphans let mut orphan_keys: Vec = Vec::new(); diff --git a/nora-registry/src/main.rs b/nora-registry/src/main.rs index e2764ed..32bfc55 100644 --- a/nora-registry/src/main.rs +++ b/nora-registry/src/main.rs @@ -8,11 +8,11 @@ mod backup; mod config; mod dashboard_metrics; mod error; +mod gc; mod health; mod metrics; mod migrate; mod openapi; -mod gc; mod rate_limit; mod registry; mod repo_index; diff --git a/nora-registry/src/registry/cargo_registry.rs b/nora-registry/src/registry/cargo_registry.rs index 22000dc..70a76eb 100644 --- a/nora-registry/src/registry/cargo_registry.rs +++ b/nora-registry/src/registry/cargo_registry.rs @@ -51,7 +51,9 @@ async fn download( "cargo", "LOCAL", )); - state.audit.log(AuditEntry::new("pull", "api", "", "cargo", "")); + state + .audit + .log(AuditEntry::new("pull", "api", "", "cargo", "")); (StatusCode::OK, data).into_response() } Err(_) => StatusCode::NOT_FOUND.into_response(), diff --git a/nora-registry/src/registry/docker.rs b/nora-registry/src/registry/docker.rs index c9da929..afbab3e 100644 --- a/nora-registry/src/registry/docker.rs +++ b/nora-registry/src/registry/docker.rs @@ -312,7 +312,10 @@ async fn upload_blob( StatusCode::CREATED, [ (header::LOCATION, location), - (HeaderName::from_static("docker-content-digest"), digest.to_string()), + ( + HeaderName::from_static("docker-content-digest"), + digest.to_string(), + ), ], ) .into_response() @@ -489,7 +492,13 @@ async fn put_manifest( "docker", "LOCAL", )); - state.audit.log(AuditEntry::new("push", "api", &format!("{}:{}", name, reference), "docker", "manifest")); + state.audit.log(AuditEntry::new( + "push", + "api", + &format!("{}:{}", name, reference), + "docker", + "manifest", + )); state.repo_index.invalidate("docker"); let location = format!("/v2/{}/manifests/{}", name, reference); diff --git a/nora-registry/src/registry/maven.rs b/nora-registry/src/registry/maven.rs index 373bb8d..3dc00b3 100644 --- a/nora-registry/src/registry/maven.rs +++ b/nora-registry/src/registry/maven.rs @@ -43,7 +43,9 @@ async fn download(State(state): State>, Path(path): Path) "maven", "CACHE", )); - state.audit.log(AuditEntry::new("cache_hit", "api", "", "maven", "")); + state + .audit + .log(AuditEntry::new("cache_hit", "api", "", "maven", "")); return with_content_type(&path, data).into_response(); } @@ -60,7 +62,9 @@ async fn download(State(state): State>, Path(path): Path) "maven", "PROXY", )); - state.audit.log(AuditEntry::new("proxy_fetch", "api", "", "maven", "")); + state + .audit + .log(AuditEntry::new("proxy_fetch", "api", "", "maven", "")); let storage = state.storage.clone(); let key_clone = key.clone(); @@ -106,7 +110,9 @@ async fn upload( "maven", "LOCAL", )); - state.audit.log(AuditEntry::new("push", "api", "", "maven", "")); + state + .audit + .log(AuditEntry::new("push", "api", "", "maven", "")); state.repo_index.invalidate("maven"); StatusCode::CREATED } diff --git a/nora-registry/src/registry/npm.rs b/nora-registry/src/registry/npm.rs index 7cd7614..6927d44 100644 --- a/nora-registry/src/registry/npm.rs +++ b/nora-registry/src/registry/npm.rs @@ -49,7 +49,9 @@ async fn handle_request(State(state): State>, Path(path): Path>, Path(path): Path>, Path(path): Path) state .activity .push(ActivityEntry::new(ActionType::Pull, path, "raw", "LOCAL")); - state.audit.log(AuditEntry::new("pull", "api", "", "raw", "")); + state + .audit + .log(AuditEntry::new("pull", "api", "", "raw", "")); // Guess content type from extension let content_type = guess_content_type(&key); @@ -74,7 +76,9 @@ async fn upload( state .activity .push(ActivityEntry::new(ActionType::Push, path, "raw", "LOCAL")); - state.audit.log(AuditEntry::new("push", "api", "", "raw", "")); + state + .audit + .log(AuditEntry::new("push", "api", "", "raw", "")); StatusCode::CREATED.into_response() } Err(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(), diff --git a/nora-registry/src/tokens.rs b/nora-registry/src/tokens.rs index 940da1c..bee4b12 100644 --- a/nora-registry/src/tokens.rs +++ b/nora-registry/src/tokens.rs @@ -40,7 +40,6 @@ impl Role { } } - /// API Token metadata stored on disk #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TokenInfo { @@ -260,7 +259,9 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let store = TokenStore::new(temp_dir.path()); - let token = store.create_token("testuser", 30, None, Role::Write).unwrap(); + let token = store + .create_token("testuser", 30, None, Role::Write) + .unwrap(); let (user, role) = store.verify_token(&token).unwrap(); assert_eq!(user, "testuser"); @@ -291,7 +292,9 @@ mod tests { let store = TokenStore::new(temp_dir.path()); // Create token and manually set it as expired - let token = store.create_token("testuser", 1, None, Role::Write).unwrap(); + let token = store + .create_token("testuser", 1, None, Role::Write) + .unwrap(); let token_hash = hash_token(&token); let file_path = temp_dir.path().join(format!("{}.json", &token_hash[..16])); @@ -330,7 +333,9 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let store = TokenStore::new(temp_dir.path()); - let token = store.create_token("testuser", 30, None, Role::Write).unwrap(); + let token = store + .create_token("testuser", 30, None, Role::Write) + .unwrap(); let token_hash = hash_token(&token); let hash_prefix = &token_hash[..16]; @@ -375,7 +380,9 @@ mod tests { let temp_dir = TempDir::new().unwrap(); let store = TokenStore::new(temp_dir.path()); - let token = store.create_token("testuser", 30, None, Role::Write).unwrap(); + let token = store + .create_token("testuser", 30, None, Role::Write) + .unwrap(); // First verification store.verify_token(&token).unwrap(); @@ -391,7 +398,12 @@ mod tests { let store = TokenStore::new(temp_dir.path()); store - .create_token("testuser", 30, Some("CI/CD Pipeline".to_string()), Role::Admin) + .create_token( + "testuser", + 30, + Some("CI/CD Pipeline".to_string()), + Role::Admin, + ) .unwrap(); let tokens = store.list_tokens("testuser");