From 62de71640c028f7ea376e806bfc0879972248590 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Tue, 17 Feb 2026 17:10:33 +0100 Subject: [PATCH 1/2] added JWT claims (preferred_username, groups) for Status Dashboard rbac auth --- Cargo.toml | 1 + doc/modules/sd.md | 29 ++++++++++-- doc/reporter.md | 46 ++++++++++++++---- src/bin/reporter.rs | 6 ++- src/config.rs | 17 +++++++ src/sd.rs | 32 +++++++++++-- tests/integration_sd.rs | 100 ++++++++++++++++++++++++++++++++++++++-- 7 files changed, 207 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9a43c57..89167bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,6 +45,7 @@ tracing-subscriber = { version = "~0.3", features = ["env-filter"] } uuid = { version = "~1.3", features = ["v4", "fast-rng"] } [dev-dependencies] +base64 = "~0.21" mockito = "~1.0" serial_test = "3.3.1" tempfile = "~3.5" diff --git a/doc/modules/sd.md b/doc/modules/sd.md index 5227292..ea18c24 100644 --- a/doc/modules/sd.md +++ b/doc/modules/sd.md @@ -93,18 +93,33 @@ Key: `(component_name, sorted_attributes)` → Value: `component_id` #### `build_auth_headers` ```rust -pub fn build_auth_headers(secret: Option<&str>) -> HeaderMap +pub fn build_auth_headers( + secret: Option<&str>, + preferred_username: Option<&str>, + group: Option<&str>, +) -> HeaderMap ``` Generates HMAC-JWT authorization headers for Status Dashboard API. - Creates Bearer token using HMAC-SHA256 signing - Returns empty HeaderMap if no secret provided (optional auth) +- Optionally includes `preferred_username` claim for audit logging +- Optionally includes `groups` array claim with single group for authorization **Example**: ```rust -let headers = build_auth_headers(Some("my-secret")); -// Headers contain: Authorization: Bearer eyJ... +// With claims +let headers = build_auth_headers( + Some("status-dashboard-secret"), + Some("operator-sd"), + Some("sd-operators"), +); +// JWT payload: {"preferred_username": "operator-sd", "groups": ["sd-operators"]} + +// Without claims (backward compatible) +let headers = build_auth_headers(Some("my-secret"), None, None); +// JWT payload: {} ``` ### Component Management @@ -184,8 +199,12 @@ use cloudmon_metrics::sd::{ Component, ComponentAttribute, }; -// Build auth headers -let headers = build_auth_headers(config.secret.as_deref()); +// Build auth headers with optional claims +let headers = build_auth_headers( + config.secret.as_deref(), + config.jwt_preferred_username.as_deref(), + config.jwt_group.as_deref(), +); // Fetch and cache components let components = fetch_components(&client, &url, &headers).await?; diff --git a/doc/reporter.md b/doc/reporter.md index 45e96d2..7302586 100644 --- a/doc/reporter.md +++ b/doc/reporter.md @@ -138,19 +138,35 @@ Incidents are created with static, secure payloads: ### 5. Authentication -The reporter uses HMAC-JWT for authentication (unchanged from V1): +The reporter uses HMAC-JWT for authentication with optional claims: ```rust -// Generate HMAC-JWT token -let headers = build_auth_headers(secret.as_deref()); +// Generate HMAC-JWT token with optional claims +let headers = build_auth_headers( + secret.as_deref(), + preferred_username.as_deref(), + group.as_deref(), +); // Headers contain: Authorization: Bearer ``` **Token Format**: - Algorithm: HMAC-SHA256 -- Claims: `{"stackmon": "dummy"}` +- Claims (when configured): + - `preferred_username`: User identifier for audit logging + - `groups`: Array containing single group for authorization - Optional: No secret = no auth header (for environments without auth) +**Example JWT Payload** (with all claims configured): +```json +{ + "preferred_username": "operator-sd", + "groups": ["sd-operators"] +} +``` + +**Backward Compatibility**: If `jwt_preferred_username` and `jwt_group` are not configured, the JWT payload will be empty (same behavior as before). + ## Module Structure The Status Dashboard integration is consolidated in `src/sd.rs`: @@ -166,7 +182,11 @@ pub struct IncidentData { title, description, impact, components, start_date, sy pub type ComponentCache = HashMap<(String, Vec), u32>; // Authentication -pub fn build_auth_headers(secret: Option<&str>) -> HeaderMap +pub fn build_auth_headers( + secret: Option<&str>, + preferred_username: Option<&str>, + group: Option<&str>, +) -> HeaderMap // V2 API Functions pub async fn fetch_components(...) -> Result> @@ -194,12 +214,16 @@ convertor: status_dashboard: url: "https://dashboard.example.com" secret: "your-jwt-secret" + jwt_preferred_username: "operator-sd" # Optional: user identifier for JWT + jwt_group: "sd-operators" # Optional: group for authorization ``` -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|---------------------------------------| -| `url` | string | Yes | - | Status Dashboard API URL | -| `secret` | string | No | - | JWT signing secret for authentication | +| Property | Type | Required | Default | Description | +|------------------------|--------|----------|---------|--------------------------------------------------| +| `url` | string | Yes | - | Status Dashboard API URL | +| `secret` | string | No | - | JWT signing secret for authentication | +| `jwt_preferred_username` | string | No | - | Username claim for JWT (audit logging) | +| `jwt_group` | string | No | - | Group claim for JWT (placed into `groups` array) | ### Health Query Configuration @@ -282,7 +306,9 @@ spec: Override configuration: ```bash -MP_STATUS_DASHBOARD__SECRET=new-secret \ +MP_STATUS_DASHBOARD__SECRET=status-dashboard-secret \ +MP_STATUS_DASHBOARD__JWT_PREFERRED_USERNAME=operator-sd \ +MP_STATUS_DASHBOARD__JWT_GROUP=sd-operators \ MP_CONVERTOR__URL=http://convertor-svc:3005 \ cloudmon-metrics-reporter --config config.yaml ``` diff --git a/src/bin/reporter.rs b/src/bin/reporter.rs index f6895d7..442427a 100644 --- a/src/bin/reporter.rs +++ b/src/bin/reporter.rs @@ -121,7 +121,11 @@ async fn metric_watcher(config: &Config) { // Build authorization headers using status_dashboard module (T021, T022, T023 - US3) // VERIFIED: Existing HMAC-JWT mechanism works unchanged with V2 endpoints - let headers = build_auth_headers(sdb_config.secret.as_deref()); + let headers = build_auth_headers( + sdb_config.secret.as_deref(), + sdb_config.jwt_preferred_username.as_deref(), + sdb_config.jwt_group.as_deref(), + ); // Initialize component ID cache at startup with retry logic (T024, T025, T026, T027) // Per FR-006: 3 retry attempts with 60-second delays diff --git a/src/config.rs b/src/config.rs index 350b2cc..ac032e4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -33,7 +33,20 @@ //! expressions: //! - expression: 'a + b-c && d-e' //! weight: 1 +//! status_dashboard: +//! url: 'https://status-dashboard.example.com' +//! secret: 'status-dashboard-jwt-secret' +//! jwt_preferred_username: 'operator-sd' +//! jwt_group: 'sd-operators' //! ``` +//! +//! # Environment variables +//! Configuration can be overridden with environment variables using the `MP_` prefix +//! and `__` as separator for nested values. Examples: +//! - `MP_STATUS_DASHBOARD__SECRET` - JWT signing secret +//! - `MP_STATUS_DASHBOARD__JWT_PREFERRED_USERNAME` - JWT preferred_username claim +//! - `MP_STATUS_DASHBOARD__JWT_GROUP` - JWT group claim (will be placed into groups array) +//! use glob::glob; @@ -177,6 +190,10 @@ pub struct StatusDashboardConfig { pub url: String, /// JWT token signature secret pub secret: Option, + /// JWT token preferred_username claim + pub jwt_preferred_username: Option, + /// JWT token group claim (will be placed into "groups" array in JWT payload) + pub jwt_group: Option, } /// Health metrics query configuration diff --git a/src/sd.rs b/src/sd.rs index bfae872..4101be2 100644 --- a/src/sd.rs +++ b/src/sd.rs @@ -8,8 +8,9 @@ use hmac::{Hmac, Mac}; use jwt::SignWithKey; use reqwest::header::HeaderMap; use serde::{Deserialize, Serialize}; +use serde_json; use sha2::Sha256; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; /// Component attribute (key-value pair) for identifying components #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] @@ -65,15 +66,38 @@ pub type ComponentCache = HashMap<(String, Vec), u32>; /// /// # Arguments /// * `secret` - Optional HMAC secret for JWT signing +/// * `preferred_username` - Optional preferred_username claim for JWT +/// * `group` - Optional group claim for JWT (will be placed into "groups" array in JWT payload) /// /// # Returns /// HeaderMap with Authorization header if secret provided, empty otherwise -pub fn build_auth_headers(secret: Option<&str>) -> HeaderMap { +pub fn build_auth_headers( + secret: Option<&str>, + preferred_username: Option<&str>, + group: Option<&str>, +) -> HeaderMap { let mut headers = HeaderMap::new(); if let Some(secret) = secret { let key: Hmac = Hmac::new_from_slice(secret.as_bytes()).unwrap(); - let mut claims = BTreeMap::new(); - claims.insert("stackmon", "dummy"); + + // Build claims as a JSON Value to support complex types + let mut claims_map = serde_json::Map::new(); + + // Add preferred_username if provided + if let Some(username) = preferred_username { + claims_map.insert( + "preferred_username".to_string(), + serde_json::Value::String(username.to_string()), + ); + } + + // Add group as array if provided (Status Dashboard expects "groups" claim name) + if let Some(group_value) = group { + let groups_json = vec![serde_json::Value::String(group_value.to_string())]; + claims_map.insert("groups".to_string(), serde_json::Value::Array(groups_json)); + } + + let claims = serde_json::Value::Object(claims_map); let token_str = claims.sign_with_key(&key).unwrap(); let bearer = format!("Bearer {}", token_str); headers.insert(reqwest::header::AUTHORIZATION, bearer.parse().unwrap()); diff --git a/tests/integration_sd.rs b/tests/integration_sd.rs index 860acea..3b3abe1 100644 --- a/tests/integration_sd.rs +++ b/tests/integration_sd.rs @@ -473,17 +473,109 @@ fn test_multiple_components_same_name() { /// Test build_auth_headers - verify JWT token generation #[test] fn test_build_auth_headers() { - // Test with secret - let headers = build_auth_headers(Some("test-secret")); + // Test with secret only (no claims) - should not panic + let headers = build_auth_headers(Some("test-secret"), None, None); assert!(headers.contains_key(reqwest::header::AUTHORIZATION)); let auth_value = headers.get(reqwest::header::AUTHORIZATION).unwrap(); let auth_str = auth_value.to_str().unwrap(); assert!(auth_str.starts_with("Bearer ")); - // Test without secret (optional auth) - let headers_empty = build_auth_headers(None); + // Test without secret (optional auth) - should not panic + let headers_empty = build_auth_headers(None, None, None); assert!(!headers_empty.contains_key(reqwest::header::AUTHORIZATION)); + + // Test with secret and claims - should not panic + let headers_with_claims = build_auth_headers( + Some("test-secret"), + Some("operator-sd"), + Some("sd-operators"), + ); + assert!(headers_with_claims.contains_key(reqwest::header::AUTHORIZATION)); + + // Test with only preferred_username (no group) - should not panic + let headers_username_only = build_auth_headers(Some("test-secret"), Some("operator-sd"), None); + assert!(headers_username_only.contains_key(reqwest::header::AUTHORIZATION)); + + // Test with only group (no preferred_username) - should not panic + let headers_group_only = build_auth_headers(Some("test-secret"), None, Some("sd-operators")); + assert!(headers_group_only.contains_key(reqwest::header::AUTHORIZATION)); +} + +/// Test build_auth_headers with claims - verify JWT payload structure +#[test] +fn test_build_auth_headers_with_claims() { + use base64::{engine::general_purpose, Engine as _}; + + // Generate token with all claims + let headers = build_auth_headers( + Some("test-secret"), + Some("operator-sd"), + Some("sd-operators"), + ); + + let auth_value = headers.get(reqwest::header::AUTHORIZATION).unwrap(); + let auth_str = auth_value.to_str().unwrap(); + let token = &auth_str[7..]; + + let parts: Vec<&str> = token.split('.').collect(); + assert_eq!(parts.len(), 3, "JWT should have 3 parts"); + + let payload_decoded = general_purpose::URL_SAFE_NO_PAD + .decode(parts[1]) + .expect("Failed to decode JWT payload"); + let payload_str = String::from_utf8(payload_decoded).expect("Failed to parse payload as UTF-8"); + let payload: serde_json::Value = + serde_json::from_str(&payload_str).expect("Failed to parse payload as JSON"); + + assert_eq!( + payload.get("preferred_username").and_then(|v| v.as_str()), + Some("operator-sd"), + "preferred_username should be 'operator-sd'" + ); + + // Verify groups claim is an array with single element + let groups = payload.get("groups").expect("groups claim should exist"); + assert!(groups.is_array(), "groups should be an array"); + let groups_array = groups.as_array().expect("groups should be array"); + assert_eq!(groups_array.len(), 1, "groups array should have 1 element"); + assert_eq!( + groups_array[0].as_str(), + Some("sd-operators"), + "groups[0] should be 'sd-operators'" + ); +} + +/// Test build_auth_headers without claims - verify empty payload is valid JWT +#[test] +fn test_build_auth_headers_without_claims() { + use base64::{engine::general_purpose, Engine as _}; + + // Generate token without claims (backward compatibility) + let headers = build_auth_headers(Some("test-secret"), None, None); + + let auth_value = headers.get(reqwest::header::AUTHORIZATION).unwrap(); + let auth_str = auth_value.to_str().unwrap(); + let token = &auth_str[7..]; // Remove "Bearer " prefix + + // Split JWT into parts + let parts: Vec<&str> = token.split('.').collect(); + assert_eq!(parts.len(), 3, "JWT should have 3 parts"); + + // Decode payload (second part) + let payload_decoded = general_purpose::URL_SAFE_NO_PAD + .decode(parts[1]) + .expect("Failed to decode JWT payload"); + let payload_str = String::from_utf8(payload_decoded).expect("Failed to parse payload as UTF-8"); + let payload: serde_json::Value = + serde_json::from_str(&payload_str).expect("Failed to parse payload as JSON"); + + // Verify payload is empty object (no claims) + assert!(payload.is_object(), "payload should be an object"); + assert!( + payload.as_object().unwrap().is_empty(), + "payload should be empty when no claims provided" + ); } /// Test create_incident failure - verify error handling when API returns error From 741e6c8cfe649f8ac1b6ca2e9b872ae307b5cc13 Mon Sep 17 00:00:00 2001 From: Ilia Bakhterev Date: Thu, 19 Feb 2026 20:10:21 +0100 Subject: [PATCH 2/2] variables renamed --- doc/api/authentication.md | 8 +++--- doc/architecture/overview.md | 4 +-- doc/config.md | 12 ++++----- doc/configuration/examples.md | 2 +- doc/configuration/overview.md | 4 +-- doc/configuration/schema.md | 4 +-- doc/guides/deployment.md | 8 +++--- doc/modules/api.md | 2 +- doc/modules/config.md | 4 +-- doc/modules/sd.md | 6 ++--- doc/reporter.md | 28 ++++++++++----------- specs/003-sd-api-v2-migration/quickstart.md | 2 +- src/bin/reporter.rs | 6 ++--- src/config.rs | 26 +++++++++---------- src/sd.rs | 10 ++++++-- tests/integration_e2e_reporter.rs | 2 +- tests/integration_sd.rs | 21 ++++++++-------- 17 files changed, 78 insertions(+), 71 deletions(-) diff --git a/doc/api/authentication.md b/doc/api/authentication.md index a6c5fed..188e3fb 100644 --- a/doc/api/authentication.md +++ b/doc/api/authentication.md @@ -26,7 +26,7 @@ The JWT token contains a simple claim structure: **Signing Process:** -1. The shared secret is loaded from configuration (`status_dashboard.secret`) +1. The shared secret is loaded from configuration (`status_dashboard.jwt_secret`) 2. An HMAC-SHA256 key is created from the secret bytes 3. Claims are signed with the key to produce the JWT token 4. The token is included in the `Authorization` header as a Bearer token @@ -51,7 +51,7 @@ status_dashboard: The secret can also be set via environment variable: ```bash -export MP_STATUS_DASHBOARD__SECRET="your-shared-secret-key" +export MP_STATUS_DASHBOARD__JWT_SECRET="your-shared-secret-key" ``` Environment variables are merged with the configuration file, with environment variables taking precedence. @@ -68,7 +68,7 @@ Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdGFja21vbiI6ImR1b ### Request Flow -1. **Configuration Load:** The reporter reads the `status_dashboard.secret` from configuration +1. **Configuration Load:** The reporter reads the `status_dashboard.jwt_secret` from configuration 2. **Token Generation:** If a secret is configured, a JWT token is generated at startup 3. **Request Authentication:** All POST requests to `/v1/component_status` include the Bearer token 4. **Server Validation:** The status-dashboard validates the token signature using the same shared secret @@ -86,7 +86,7 @@ On the server side (status-dashboard), tokens should be validated by: ### Secret Management - **Never commit secrets** to version control -- Use environment variables (`MP_STATUS_DASHBOARD__SECRET`) in production +- Use environment variables (`MP_STATUS_DASHBOARD__JWT_SECRET`) in production - Rotate secrets periodically - Use strong, randomly-generated secrets (minimum 32 characters recommended) diff --git a/doc/architecture/overview.md b/doc/architecture/overview.md index 6b10383..11bf219 100644 --- a/doc/architecture/overview.md +++ b/doc/architecture/overview.md @@ -256,8 +256,8 @@ src/ ```bash # Environment variable example -MP_STATUS_DASHBOARD__SECRET=my-jwt-secret -# Translates to: status_dashboard.secret = "my-jwt-secret" +MP_STATUS_DASHBOARD__JWT_SECRET=my-jwt-secret +# Translates to: status_dashboard.jwt_secret = "my-jwt-secret" ``` ## Security Considerations diff --git a/doc/config.md b/doc/config.md index 4fc5a18..45890e8 100644 --- a/doc/config.md +++ b/doc/config.md @@ -34,7 +34,7 @@ environments: status_dashboard: url: "https://status.cloudmon.com" - secret: "dev" + jwt_secret: "dev" flag_metrics: ### Comp1 @@ -94,13 +94,13 @@ Configures URL and JWT secret for communication with the status dashboard. ```yaml status_dashboard: url: "https://status-dashboard.example.com" - secret: "your-jwt-secret" + jwt_secret: "your-jwt-secret" ``` -| Property | Type | Required | Default | Description | -|----------|--------|----------|---------|---------------------------------------| -| `url` | string | Yes | - | Status Dashboard API URL | -| `secret` | string | No | - | JWT signing secret for authentication | +| Property | Type | Required | Default | Description | +|--------------|--------|----------|---------|---------------------------------------| +| `url` | string | Yes | - | Status Dashboard API URL | +| `jwt_secret` | string | No | - | JWT signing secret for authentication | ## health_query diff --git a/doc/configuration/examples.md b/doc/configuration/examples.md index 367a2a9..779d771 100644 --- a/doc/configuration/examples.md +++ b/doc/configuration/examples.md @@ -596,7 +596,7 @@ server: status_dashboard: url: "https://status.example.com" - # Secret should be set via MP_STATUS_DASHBOARD__SECRET environment variable + # Secret should be set via MP_STATUS_DASHBOARD__JWT_SECRET environment variable metric_templates: api_latency: diff --git a/doc/configuration/overview.md b/doc/configuration/overview.md index d2672a6..92d9832 100644 --- a/doc/configuration/overview.md +++ b/doc/configuration/overview.md @@ -57,8 +57,8 @@ export MP_DATASOURCE__URL="http://graphite.example.com:8080" # Override server.port export MP_SERVER__PORT=3005 -# Set status_dashboard.secret (sensitive values) -export MP_STATUS_DASHBOARD__SECRET="your-jwt-secret" +# Set status_dashboard.jwt_secret (sensitive values) +export MP_STATUS_DASHBOARD__JWT_SECRET="your-jwt-secret" ``` **Best Practice**: Use environment variables for sensitive values like secrets and for deployment-specific overrides in containerized environments. diff --git a/doc/configuration/schema.md b/doc/configuration/schema.md index c2a45de..48f2342 100644 --- a/doc/configuration/schema.md +++ b/doc/configuration/schema.md @@ -195,12 +195,12 @@ Optional status dashboard integration. | Property | Type | Required | Description | |----------|------|----------|-------------| | `url` | string | Yes | Status dashboard URL | -| `secret` | string | No | JWT signing secret | +| `jwt_secret` | string | No | JWT signing secret | ```yaml status_dashboard: url: "https://status.example.com" - secret: "your-jwt-secret" # Use MP_STATUS_DASHBOARD__SECRET env var instead + jwt_secret: "your-jwt-secret" # Use MP_STATUS_DASHBOARD__JWT_SECRET env var instead ``` ## Comparison Operators diff --git a/doc/guides/deployment.md b/doc/guides/deployment.md index ac74ee4..912ca19 100644 --- a/doc/guides/deployment.md +++ b/doc/guides/deployment.md @@ -94,7 +94,7 @@ docker run -d \ --network host \ -v $(pwd)/config:/cloudmon/config:ro \ -e RUST_LOG=info \ - -e MP_STATUS_DASHBOARD__SECRET=your-jwt-secret \ + -e MP_STATUS_DASHBOARD__JWT_SECRET=your-jwt-secret \ metrics-processor:latest \ /cloudmon/cloudmon-metrics-reporter ``` @@ -130,7 +130,7 @@ services: - ./config:/cloudmon/config:ro environment: - RUST_LOG=info - - MP_STATUS_DASHBOARD__SECRET=${STATUS_DASHBOARD_SECRET} + - MP_STATUS_DASHBOARD__JWT_SECRET=${STATUS_DASHBOARD_SECRET} depends_on: convertor: condition: service_healthy @@ -343,7 +343,7 @@ spec: env: - name: RUST_LOG value: "info" - - name: MP_STATUS_DASHBOARD__SECRET + - name: MP_STATUS_DASHBOARD__JWT_SECRET valueFrom: secretKeyRef: name: metrics-processor-secrets @@ -462,7 +462,7 @@ Override configuration values using environment variables prefixed with `MP_`: ```bash # Override status dashboard secret -export MP_STATUS_DASHBOARD__SECRET=production-secret +export MP_STATUS_DASHBOARD__JWT_SECRET=production-secret # Override datasource URL export MP_DATASOURCE__URL=https://graphite-prod.example.com diff --git a/doc/modules/api.md b/doc/modules/api.md index 0e66a8d..3222649 100644 --- a/doc/modules/api.md +++ b/doc/modules/api.md @@ -127,7 +127,7 @@ The `AppState` contains: ## Authentication -Currently, the API does not implement authentication. The `status_dashboard.secret` configuration option suggests JWT-based authentication may be planned for integration with status dashboard services. +Currently, the API does not implement authentication. The `status_dashboard.jwt_secret` configuration option suggests JWT-based authentication may be planned for integration with status dashboard services. ## Integration with Other Modules diff --git a/doc/modules/config.md b/doc/modules/config.md index c8b9962..46aefdf 100644 --- a/doc/modules/config.md +++ b/doc/modules/config.md @@ -94,7 +94,7 @@ Environment variables use `MP_` prefix with `__` as sublevel separator: |---------------------|-------------| | `MP_DATASOURCE__URL` | `datasource.url` | | `MP_SERVER__PORT` | `server.port` | -| `MP_STATUS_DASHBOARD__SECRET` | `status_dashboard.secret` | +| `MP_STATUS_DASHBOARD__JWT_SECRET` | `status_dashboard.jwt_secret` | ```rust Environment::with_prefix("MP") @@ -163,7 +163,7 @@ health_metrics: status_dashboard: url: 'https://status.example.com' - secret: ${MP_STATUS_DASHBOARD__SECRET} + jwt_secret: ${MP_STATUS_DASHBOARD__JWT_SECRET} ``` ### Modular Configuration (conf.d) diff --git a/doc/modules/sd.md b/doc/modules/sd.md index ea18c24..e03a21e 100644 --- a/doc/modules/sd.md +++ b/doc/modules/sd.md @@ -201,9 +201,9 @@ use cloudmon_metrics::sd::{ // Build auth headers with optional claims let headers = build_auth_headers( - config.secret.as_deref(), - config.jwt_preferred_username.as_deref(), - config.jwt_group.as_deref(), + config.jwt_secret.as_deref(), + config.claim_preferred_username.as_deref(), + config.claim_group.as_deref(), ); // Fetch and cache components diff --git a/doc/reporter.md b/doc/reporter.md index 7302586..3c087d0 100644 --- a/doc/reporter.md +++ b/doc/reporter.md @@ -160,12 +160,12 @@ let headers = build_auth_headers( **Example JWT Payload** (with all claims configured): ```json { - "preferred_username": "operator-sd", - "groups": ["sd-operators"] + "preferred_username": "sd-username", + "groups": ["operators-sd-group", "group_1", "group_2"] } ``` -**Backward Compatibility**: If `jwt_preferred_username` and `jwt_group` are not configured, the JWT payload will be empty (same behavior as before). +**Backward Compatibility**: If `claim_preferred_username` and `claim_group` are not configured, the JWT payload will be empty (same behavior as before). ## Module Structure @@ -213,17 +213,17 @@ convertor: ```yaml status_dashboard: url: "https://dashboard.example.com" - secret: "your-jwt-secret" - jwt_preferred_username: "operator-sd" # Optional: user identifier for JWT - jwt_group: "sd-operators" # Optional: group for authorization + jwt_secret: "your-jwt-secret" + claim_preferred_username: "sd-username" # Optional: user identifier for JWT + claim_group: "operators-sd-group" # Optional: group for authorization ``` | Property | Type | Required | Default | Description | |------------------------|--------|----------|---------|--------------------------------------------------| | `url` | string | Yes | - | Status Dashboard API URL | -| `secret` | string | No | - | JWT signing secret for authentication | -| `jwt_preferred_username` | string | No | - | Username claim for JWT (audit logging) | -| `jwt_group` | string | No | - | Group claim for JWT (placed into `groups` array) | +| `jwt_secret` | string | No | - | JWT signing secret for authentication | +| `claim_preferred_username` | string | No | - | Username claim for JWT (audit logging) | +| `claim_group` | string | No | - | Group claim for JWT (placed into `groups` array) | ### Health Query Configuration @@ -306,9 +306,9 @@ spec: Override configuration: ```bash -MP_STATUS_DASHBOARD__SECRET=status-dashboard-secret \ -MP_STATUS_DASHBOARD__JWT_PREFERRED_USERNAME=operator-sd \ -MP_STATUS_DASHBOARD__JWT_GROUP=sd-operators \ +MP_STATUS_DASHBOARD__JWT_SECRET=status-dashboard-secret \ +MP_STATUS_DASHBOARD__CLAIM_PREFERRED_USERNAME=sd-username \ +MP_STATUS_DASHBOARD__CLAIM_GROUP=operators-sd-group \ MP_CONVERTOR__URL=http://convertor-svc:3005 \ cloudmon-metrics-reporter --config config.yaml ``` @@ -417,7 +417,7 @@ When the reporter decides to create an incident, it logs all the information nee ### Authentication Failures **Cause**: Invalid JWT secret -**Solution**: Update `status_dashboard.secret` in configuration +**Solution**: Update `status_dashboard.jwt_secret` in configuration ## Use Cases @@ -456,7 +456,7 @@ curl http://localhost:3005/v1/health?service=api&environment=prod&from=2024-01-0 ### "Dashboard authentication failed" **Cause**: Invalid JWT secret -**Solution**: Ensure `status_dashboard.secret` matches dashboard configuration +**Solution**: Ensure `status_dashboard.jwt_secret` matches dashboard configuration ### "No services being polled" diff --git a/specs/003-sd-api-v2-migration/quickstart.md b/specs/003-sd-api-v2-migration/quickstart.md index 51f1389..1c8815c 100644 --- a/specs/003-sd-api-v2-migration/quickstart.md +++ b/specs/003-sd-api-v2-migration/quickstart.md @@ -45,7 +45,7 @@ No configuration changes required. Existing `config.yaml` is compatible: ```yaml status_dashboard: url: "https://status-dashboard.example.com" - secret: "your-hmac-secret" # Optional, for auth + jwt_secret: "your-hmac-secret" # Optional, for auth environments: - name: production diff --git a/src/bin/reporter.rs b/src/bin/reporter.rs index 442427a..947884d 100644 --- a/src/bin/reporter.rs +++ b/src/bin/reporter.rs @@ -122,9 +122,9 @@ async fn metric_watcher(config: &Config) { // Build authorization headers using status_dashboard module (T021, T022, T023 - US3) // VERIFIED: Existing HMAC-JWT mechanism works unchanged with V2 endpoints let headers = build_auth_headers( - sdb_config.secret.as_deref(), - sdb_config.jwt_preferred_username.as_deref(), - sdb_config.jwt_group.as_deref(), + sdb_config.jwt_secret.as_deref(), + sdb_config.claim_preferred_username.as_deref(), + sdb_config.claim_group.as_deref(), ); // Initialize component ID cache at startup with retry logic (T024, T025, T026, T027) diff --git a/src/config.rs b/src/config.rs index ac032e4..605ce77 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,17 +35,17 @@ //! weight: 1 //! status_dashboard: //! url: 'https://status-dashboard.example.com' -//! secret: 'status-dashboard-jwt-secret' -//! jwt_preferred_username: 'operator-sd' -//! jwt_group: 'sd-operators' +//! jwt_secret: 'status-dashboard-jwt-secret' +//! claim_preferred_username: 'operator-sd' +//! claim_group: 'sd-operators' //! ``` //! //! # Environment variables //! Configuration can be overridden with environment variables using the `MP_` prefix //! and `__` as separator for nested values. Examples: -//! - `MP_STATUS_DASHBOARD__SECRET` - JWT signing secret -//! - `MP_STATUS_DASHBOARD__JWT_PREFERRED_USERNAME` - JWT preferred_username claim -//! - `MP_STATUS_DASHBOARD__JWT_GROUP` - JWT group claim (will be placed into groups array) +//! - `MP_STATUS_DASHBOARD__JWT_SECRET` - JWT signing secret +//! - `MP_STATUS_DASHBOARD__CLAIM_PREFERRED_USERNAME` - JWT preferred_username claim +//! - `MP_STATUS_DASHBOARD__CLAIM_GROUP` - JWT group claim (will be placed into groups array) //! use glob::glob; @@ -113,7 +113,7 @@ impl Config { } // merge environment variables (subelements separated by "__") - // MP_STATUS_DASHBOARD__SECRET goes to status_dashboard.secret + // MP_STATUS_DASHBOARD__JWT_SECRET goes to status_dashboard.jwt_secret s = s.add_source( Environment::with_prefix("MP") .prefix_separator("_") @@ -189,11 +189,11 @@ pub struct StatusDashboardConfig { /// Status dashboard URL pub url: String, /// JWT token signature secret - pub secret: Option, + pub jwt_secret: Option, /// JWT token preferred_username claim - pub jwt_preferred_username: Option, + pub claim_preferred_username: Option, /// JWT token group claim (will be placed into "groups" array in JWT payload) - pub jwt_group: Option, + pub claim_group: Option, } /// Health metrics query configuration @@ -331,12 +331,12 @@ mod test { config_file.write_all(CONFIG_STR1.as_bytes()).unwrap(); - env::set_var("MP_STATUS_DASHBOARD__SECRET", "val"); + env::set_var("MP_STATUS_DASHBOARD__JWT_SECRET", "val"); let _config = config::Config::new(config_file.path().to_str().unwrap()).unwrap(); - assert_eq!(_config.status_dashboard.unwrap().secret.unwrap(), "val"); + assert_eq!(_config.status_dashboard.unwrap().jwt_secret.unwrap(), "val"); // Clean up to avoid affecting other tests - env::remove_var("MP_STATUS_DASHBOARD__SECRET"); + env::remove_var("MP_STATUS_DASHBOARD__JWT_SECRET"); } /// Test merging of the config with conf.d elements diff --git a/src/sd.rs b/src/sd.rs index 4101be2..b5f4a7e 100644 --- a/src/sd.rs +++ b/src/sd.rs @@ -12,6 +12,9 @@ use serde_json; use sha2::Sha256; use std::collections::HashMap; +const CLAIM_PREFERRED_USERNAME: &str = "preferred_username"; +const CLAIM_GROUP: &str = "group"; + /// Component attribute (key-value pair) for identifying components #[derive(Clone, Deserialize, Serialize, Debug, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct ComponentAttribute { @@ -86,7 +89,7 @@ pub fn build_auth_headers( // Add preferred_username if provided if let Some(username) = preferred_username { claims_map.insert( - "preferred_username".to_string(), + CLAIM_PREFERRED_USERNAME.to_string(), serde_json::Value::String(username.to_string()), ); } @@ -94,7 +97,10 @@ pub fn build_auth_headers( // Add group as array if provided (Status Dashboard expects "groups" claim name) if let Some(group_value) = group { let groups_json = vec![serde_json::Value::String(group_value.to_string())]; - claims_map.insert("groups".to_string(), serde_json::Value::Array(groups_json)); + claims_map.insert( + CLAIM_GROUP.to_string(), + serde_json::Value::Array(groups_json), + ); } let claims = serde_json::Value::Object(claims_map); diff --git a/tests/integration_e2e_reporter.rs b/tests/integration_e2e_reporter.rs index be7205a..b75020d 100644 --- a/tests/integration_e2e_reporter.rs +++ b/tests/integration_e2e_reporter.rs @@ -786,7 +786,7 @@ server: status_dashboard: url: 'http://localhost:{}' - secret: 'test-secret-key' + jwt_secret: 'test-secret-key' metric_templates: api_down: diff --git a/tests/integration_sd.rs b/tests/integration_sd.rs index 3b3abe1..7ee264c 100644 --- a/tests/integration_sd.rs +++ b/tests/integration_sd.rs @@ -488,17 +488,18 @@ fn test_build_auth_headers() { // Test with secret and claims - should not panic let headers_with_claims = build_auth_headers( Some("test-secret"), - Some("operator-sd"), - Some("sd-operators"), + Some("sd-username"), + Some("operators-sd-group"), ); assert!(headers_with_claims.contains_key(reqwest::header::AUTHORIZATION)); // Test with only preferred_username (no group) - should not panic - let headers_username_only = build_auth_headers(Some("test-secret"), Some("operator-sd"), None); + let headers_username_only = build_auth_headers(Some("test-secret"), Some("sd-username"), None); assert!(headers_username_only.contains_key(reqwest::header::AUTHORIZATION)); // Test with only group (no preferred_username) - should not panic - let headers_group_only = build_auth_headers(Some("test-secret"), None, Some("sd-operators")); + let headers_group_only = + build_auth_headers(Some("test-secret"), None, Some("operators-sd-group")); assert!(headers_group_only.contains_key(reqwest::header::AUTHORIZATION)); } @@ -510,8 +511,8 @@ fn test_build_auth_headers_with_claims() { // Generate token with all claims let headers = build_auth_headers( Some("test-secret"), - Some("operator-sd"), - Some("sd-operators"), + Some("sd-username"), + Some("operators-sd-group"), ); let auth_value = headers.get(reqwest::header::AUTHORIZATION).unwrap(); @@ -530,8 +531,8 @@ fn test_build_auth_headers_with_claims() { assert_eq!( payload.get("preferred_username").and_then(|v| v.as_str()), - Some("operator-sd"), - "preferred_username should be 'operator-sd'" + Some("sd-username"), + "preferred_username should be 'sd-username'" ); // Verify groups claim is an array with single element @@ -541,8 +542,8 @@ fn test_build_auth_headers_with_claims() { assert_eq!(groups_array.len(), 1, "groups array should have 1 element"); assert_eq!( groups_array[0].as_str(), - Some("sd-operators"), - "groups[0] should be 'sd-operators'" + Some("operators-sd-group"), + "groups[0] should be 'operators-sd-group'" ); }