Skip to content

Conversation

@Eden-D-Zhang
Copy link
Contributor

@Eden-D-Zhang Eden-D-Zhang commented Nov 22, 2025

Description

This PR adds the initial implementation for the clp-credential-manager service.

Implemented:

  • Added the new clp-credential-manager crate with Axum server bootstrap, JSON logging, SQLx MySQL pool wiring, and shared service layer that wraps audit logging plus credential metadata CRUD.
  • Defined database access layer db::operations plus models/validation for IAM user and IAM role credentials, returning metadata only so secrets never leave storage, and expose REST handlers for GET/POST /credentials and GET/DELETE /credentials/:id.
  • Introduced an audit helper that emits structured JSON traces for every CRUD attempt.

TODO:

  • Implement temporary credential generation (using AWS STS) and caching logic.
  • Add JWT generation/validation with single-use enforcement.
  • Implement JWT/AWS credential exchange logic.
  • Add unit tests for all credential-manage operations.
  • Add HTTP API endpoints for token issuance and exchange.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

Summary by CodeRabbit

  • New Features
    • Introduced a new credential manager service component with REST API endpoints for managing credentials (create, list, retrieve, delete operations).
    • Added a health check endpoint for service monitoring.
    • Established infrastructure for token-related functionality (endpoints pending implementation).

✏️ Tip: You can customize this high-level summary in your review settings.

@Eden-D-Zhang Eden-D-Zhang requested a review from a team as a code owner November 22, 2025 17:40
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 22, 2025

Walkthrough

A new credential manager microservice component is introduced to the workspace. The implementation includes configuration management, MySQL-backed persistence, HTTP API endpoints for credential operations (list/create/get/delete), health checks, token placeholder routes, JWT utilities, error handling with type-mapped service errors, and structured audit logging with tracing integration.

Changes

Cohort / File(s) Summary
Workspace configuration
Cargo.toml
Adds new workspace member components/clp-credential-manager and ensures trailing comma formatting.
Component manifest
components/clp-credential-manager/Cargo.toml
Declares dependencies for runtime (tokio, tracing), web framework (axum, tower, tower-http), cryptography (jsonwebtoken, secrecy), serialization (serde, serde_json, serde_yaml), database (sqlx with MySQL), AWS SDKs, and development utilities.
Core service infrastructure
components/clp-credential-manager/src/config.rs, src/error.rs, src/service.rs, src/main.rs
Config module provides YAML-driven AppConfig with ServerConfig, DatabaseConfig, and JwtConfig; error module defines ServiceError enum with HTTP status mapping (ApiError) and IntoResponse trait; service module establishes CredentialManagerService and JwtManager; main.rs wires CLI parsing, tracing initialization, config loading, and Axum server startup.
Database layer
components/clp-credential-manager/src/db/mod.rs, src/db/operations.rs, src/db/schema.rs
mod.rs exposes operations and schema submodules; operations.rs implements async CRUD functions (list/get/create/delete credentials) with sqlx and specialized error mapping (1062 MySQL code → Conflict); schema.rs defines canonical table name and metadata column constants.
Data models
components/clp-credential-manager/src/models/credential.rs, src/models/token.rs, src/models/mod.rs
credential.rs introduces CredentialType enum with validation, CreateCredentialRequest with comprehensive validation, and CredentialMetadata structs with timestamp conversion; token.rs adds TokenSubject enum with Job/Service variants and constructors; mod.rs re-exports public types.
HTTP routes
components/clp-credential-manager/src/routes/mod.rs, src/routes/credentials.rs, src/routes/health.rs, src/routes/tokens.rs
mod.rs defines AppState type and build_router that merges all route submodules with TraceLayer; credentials.rs implements GET/POST/GET/:id/DELETE/:id endpoints; health.rs provides GET /health liveness check; tokens.rs scaffolds POST /credentials/issue-token and exchange-token with 501 placeholders.
Auditing
components/clp-credential-manager/src/audit.rs
Introduces AuditStatus enum, generic AuditEvent struct, and log_event function that serialize credential operations into JSON payloads with component, event_type, status, credentials, subject, source_ip, and metadata fields for tracing integration.

Sequence Diagrams

sequenceDiagram
    participant CLI as CLI
    participant Main as main.rs
    participant Tracer as Tracing
    participant Config as AppConfig
    participant Service as CredentialManagerService
    participant DB as MySqlPool
    participant Router as Axum Router
    participant Listener as TcpListener

    CLI->>Main: Start with config path
    Main->>Tracer: init_tracing()
    Main->>Config: load from YAML file
    Config-->>Main: AppConfig
    Main->>DB: MySqlConnectOptions → Pool
    DB-->>Main: Connected pool
    Main->>Service: new(config)
    Service->>DB: Test connection
    DB-->>Service: OK
    Service-->>Main: CredentialManagerService
    Main->>Router: build_router(service)
    Router-->>Main: Router with routes
    Main->>Listener: bind(socket_addr)
    Listener-->>Main: TcpListener
    Main->>Router: serve(listener)
    Router->>Main: Listening on address (logged)
    note over Router: Server ready
Loading
sequenceDiagram
    participant Client as HTTP Client
    participant Router as Axum Router
    participant Handler as Route Handler
    participant Service as CredentialManagerService
    participant DB as Database
    participant Audit as Audit Log

    Client->>Router: POST /credentials (CreateCredentialRequest)
    Router->>Handler: create_credential(request)
    Handler->>Service: create_credential(request)
    Service->>Service: validate()
    alt Validation fails
        Service-->>Handler: ServiceError::Validation
        Handler->>Audit: log_event(failure)
    else Validation succeeds
        Service->>DB: insert_credential()
        alt Insert succeeds
            DB-->>Service: CredentialMetadata
            Service->>Audit: log_event(success) with id/name
            Service-->>Handler: CredentialMetadata
        else Duplicate key (1062)
            DB-->>Service: ServiceError::Conflict
            Handler->>Audit: log_event(failure)
            Service-->>Handler: ServiceError::Conflict
        else Other DB error
            DB-->>Service: ServiceError::Database
            Handler->>Audit: log_event(failure)
        end
    end
    Handler->>Router: ApiError or JSON response
    Router-->>Client: HTTP 200 (JSON) or 4xx/5xx (ApiError)
Loading
sequenceDiagram
    participant Client as HTTP Client
    participant Router as Axum Router
    participant Handler as Route Handler
    participant Service as CredentialManagerService
    participant DB as Database
    participant Audit as Audit Log

    Client->>Router: GET /credentials/:id
    Router->>Handler: get_credential(id)
    Handler->>Service: get_credential(id)
    Service->>DB: sqlx SELECT by id
    alt Row found
        DB-->>Service: CredentialMetadataRow
        Service->>Service: Convert to CredentialMetadata
        Service->>Audit: log_event(success) with id/name
        Service-->>Handler: CredentialMetadata
        Handler-->>Client: HTTP 200 (JSON)
    else Row not found
        DB-->>Service: ServiceError::NotFound
        Service->>Audit: log_event(failure)
        Service-->>Handler: ServiceError::NotFound
        Handler-->>Client: HTTP 404 (ApiError)
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas requiring extra attention:

  • Database error mapping (src/db/operations.rs): MySQL-specific error code translation (1062 → Conflict) and RowNotFound handling; ensure all database failure modes propagate correctly.
  • Credential validation logic (src/models/credential.rs): Type-specific validation rules branching by CredentialType (IamUser, IamRole, Temporary) with field presence and constraint checks; verify all validation paths are comprehensive.
  • Service initialization flow (src/service.rs): CredentialManagerService construction with MySqlConnectOptions, pool creation, and JwtManager key derivation; verify connection pool cleanup and error recovery.
  • JWT configuration and key handling (src/service.rs): EncodingKey/DecodingKey construction from SecretString; ensure keys are properly protected and not logged.
  • HTTP error response mapping (src/error.rs): From to ApiError status code mapping; confirm all ServiceError variants map to appropriate HTTP statuses.
  • Audit event serialization (src/audit.rs): JSON payload construction with optional fields and serde handling; verify no sensitive credential data leaks into logs.
  • Configuration loading and defaults (src/config.rs): YAML parsing with fallback defaults (port 3306, TTL 3600s, etc.); confirm all required fields are validated.

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: scaffolding a credential manager service with base implementation, which aligns with the PR's objective of adding the initial implementation of the clp-credential-manager service.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2f79349 and ee65f25.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (17)
  • Cargo.toml (1 hunks)
  • components/clp-credential-manager/Cargo.toml (1 hunks)
  • components/clp-credential-manager/src/audit.rs (1 hunks)
  • components/clp-credential-manager/src/config.rs (1 hunks)
  • components/clp-credential-manager/src/db/mod.rs (1 hunks)
  • components/clp-credential-manager/src/db/operations.rs (1 hunks)
  • components/clp-credential-manager/src/db/schema.rs (1 hunks)
  • components/clp-credential-manager/src/error.rs (1 hunks)
  • components/clp-credential-manager/src/main.rs (1 hunks)
  • components/clp-credential-manager/src/models/credential.rs (1 hunks)
  • components/clp-credential-manager/src/models/mod.rs (1 hunks)
  • components/clp-credential-manager/src/models/token.rs (1 hunks)
  • components/clp-credential-manager/src/routes/credentials.rs (1 hunks)
  • components/clp-credential-manager/src/routes/health.rs (1 hunks)
  • components/clp-credential-manager/src/routes/mod.rs (1 hunks)
  • components/clp-credential-manager/src/routes/tokens.rs (1 hunks)
  • components/clp-credential-manager/src/service.rs (1 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-10-22T21:02:31.113Z
Learnt from: junhaoliao
Repo: y-scope/clp PR: 0
File: :0-0
Timestamp: 2025-10-22T21:02:31.113Z
Learning: Repository y-scope/clp: Maintain deterministic CI/builds for Rust; add a check to verify Cargo.lock is in sync with Cargo.toml without updating dependencies (non-mutating verification in clp-rust-checks workflow).

Applied to files:

  • components/clp-credential-manager/Cargo.toml
  • Cargo.toml
  • components/clp-credential-manager/src/main.rs
📚 Learning: 2025-10-22T21:01:31.391Z
Learnt from: junhaoliao
Repo: y-scope/clp PR: 0
File: :0-0
Timestamp: 2025-10-22T21:01:31.391Z
Learning: In y-scope/clp, for the clp-rust-checks workflow (GitHub Actions) and the Taskfile target deps:lock:check-rust, we should verify Rust Cargo.lock is in sync with Cargo.toml using a non-mutating method (e.g., cargo metadata --locked / cargo check --locked) to keep CI deterministic and avoid updating dependencies during validation.

Applied to files:

  • components/clp-credential-manager/Cargo.toml
📚 Learning: 2025-11-03T16:17:40.223Z
Learnt from: hoophalab
Repo: y-scope/clp PR: 1535
File: components/clp-rust-utils/src/clp_config/package/config.rs:47-61
Timestamp: 2025-11-03T16:17:40.223Z
Learning: In the y-scope/clp repository, the `ApiServer` struct in `components/clp-rust-utils/src/clp_config/package/config.rs` is a Rust-native configuration type and does not mirror any Python code, unlike other structs in the same file (Config, Database, ResultsCache, Package) which are mirrors of Python definitions.

Applied to files:

  • components/clp-credential-manager/src/config.rs
  • components/clp-credential-manager/src/main.rs
🧬 Code graph analysis (10)
components/clp-credential-manager/src/routes/mod.rs (3)
components/clp-credential-manager/src/routes/credentials.rs (1)
  • router (17-27)
components/clp-credential-manager/src/routes/health.rs (1)
  • router (6-8)
components/clp-credential-manager/src/routes/tokens.rs (1)
  • router (7-11)
components/clp-credential-manager/src/routes/tokens.rs (2)
components/clp-credential-manager/src/routes/credentials.rs (1)
  • router (17-27)
components/clp-credential-manager/src/routes/health.rs (1)
  • router (6-8)
components/clp-credential-manager/src/routes/health.rs (2)
components/clp-credential-manager/src/routes/credentials.rs (1)
  • router (17-27)
components/clp-credential-manager/src/routes/tokens.rs (1)
  • router (7-11)
components/clp-credential-manager/src/routes/credentials.rs (3)
components/clp-credential-manager/src/error.rs (3)
  • new (70-75)
  • from (44-58)
  • from (84-103)
components/clp-credential-manager/src/service.rs (6)
  • new (36-53)
  • new (184-199)
  • list_credentials (71-86)
  • create_credential (142-172)
  • get_credential (89-106)
  • delete_credential (109-133)
components/clp-credential-manager/src/db/operations.rs (4)
  • list_credentials (11-23)
  • create_credential (63-70)
  • get_credential (26-44)
  • delete_credential (47-55)
components/clp-credential-manager/src/models/credential.rs (2)
components/clp-credential-manager/src/error.rs (1)
  • fmt (124-126)
components/clp-credential-manager/src/db/operations.rs (2)
  • sqlx (18-18)
  • sqlx (33-33)
components/clp-credential-manager/src/service.rs (3)
components/clp-credential-manager/src/db/operations.rs (6)
  • sqlx (18-18)
  • sqlx (33-33)
  • list_credentials (11-23)
  • get_credential (26-44)
  • delete_credential (47-55)
  • create_credential (63-70)
components/clp-credential-manager/src/audit.rs (4)
  • success (42-53)
  • log_event (71-93)
  • failure (56-67)
  • as_str (19-24)
components/clp-credential-manager/src/models/credential.rs (1)
  • as_str (21-27)
components/clp-credential-manager/src/db/operations.rs (4)
components/clp-credential-manager/src/routes/credentials.rs (4)
  • list_credentials (31-36)
  • get_credential (53-59)
  • delete_credential (63-72)
  • create_credential (40-49)
components/clp-credential-manager/src/service.rs (4)
  • list_credentials (71-86)
  • get_credential (89-106)
  • delete_credential (109-133)
  • create_credential (142-172)
components/clp-credential-manager/src/models/credential.rs (2)
  • try_from (33-42)
  • try_from (167-180)
components/clp-credential-manager/src/error.rs (3)
  • db_err (50-50)
  • from (44-58)
  • from (84-103)
components/clp-credential-manager/src/db/mod.rs (2)
components/clp-credential-manager/src/db/operations.rs (4)
  • create_credential (63-70)
  • delete_credential (47-55)
  • get_credential (26-44)
  • list_credentials (11-23)
components/clp-credential-manager/src/service.rs (4)
  • create_credential (142-172)
  • delete_credential (109-133)
  • get_credential (89-106)
  • list_credentials (71-86)
components/clp-credential-manager/src/error.rs (2)
components/clp-credential-manager/src/db/operations.rs (3)
  • sqlx (18-18)
  • sqlx (33-33)
  • db_err (118-118)
components/clp-credential-manager/src/service.rs (2)
  • new (36-53)
  • new (184-199)
components/clp-credential-manager/src/main.rs (3)
components/clp-credential-manager/src/routes/mod.rs (1)
  • build_router (14-20)
components/clp-credential-manager/src/config.rs (1)
  • from_file (37-41)
components/clp-credential-manager/src/service.rs (2)
  • new (36-53)
  • new (184-199)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: package-image
  • GitHub Check: rust-checks
  • GitHub Check: lint-check (ubuntu-24.04)
  • GitHub Check: lint-check (macos-15)
  • GitHub Check: build (macos-15)
🔇 Additional comments (10)
components/clp-credential-manager/Cargo.toml (2)

1-28: Verify Cargo.lock is updated to maintain build determinism.

Per repository guidelines for y-scope/clp, Cargo.lock must remain synchronized with Cargo.toml changes to ensure deterministic CI builds. Verify that this PR includes an updated Cargo.lock file reflecting the new dependency set.

Based on learnings


4-4: Fix invalid Rust edition—2024 does not exist.

Rust currently supports editions 2015, 2018, and 2021. The edition must be set to "2021".

-edition = "2024"
+edition = "2021"
⛔ Skipped due to learnings
Learnt from: LinZhihao-723
Repo: y-scope/clp PR: 1448
File: components/clp-rust-utils/Cargo.toml:4-4
Timestamp: 2025-10-20T22:20:45.733Z
Learning: Rust edition 2024 is a valid edition value in Cargo.toml files. It was released with Rust 1.85 on February 20, 2025.
Learnt from: junhaoliao
Repo: y-scope/clp PR: 0
File: :0-0
Timestamp: 2025-10-22T21:01:31.391Z
Learning: In y-scope/clp, for the clp-rust-checks workflow (GitHub Actions) and the Taskfile target deps:lock:check-rust, we should verify Rust Cargo.lock is in sync with Cargo.toml using a non-mutating method (e.g., cargo metadata --locked / cargo check --locked) to keep CI deterministic and avoid updating dependencies during validation.
Learnt from: junhaoliao
Repo: y-scope/clp PR: 0
File: :0-0
Timestamp: 2025-10-22T21:02:31.113Z
Learning: Repository y-scope/clp: Maintain deterministic CI/builds for Rust; add a check to verify Cargo.lock is in sync with Cargo.toml without updating dependencies (non-mutating verification in clp-rust-checks workflow).
Cargo.toml (1)

4-6: Workspace membership registration looks good.

The new credential manager crate is correctly added to the workspace members list in alphabetical order, and the trailing comma maintains proper formatting.

components/clp-credential-manager/src/db/mod.rs (1)

1-4: Public db facade looks clean and cohesive

Re-exporting the core CRUD operations at the db module level gives a simple, well-scoped API for the service layer without leaking internal schema/operations details. No issues from a design or correctness perspective.

components/clp-credential-manager/src/config.rs (1)

17-41: Config loading and defaulting are consistent with the rest of the service

AppConfig::from_file cleanly lifts IO/YAML failures into ServiceError, ServerConfig provides sane defaults and a typed SocketAddr, and database/JWT configs are explicit with reasonable defaults for ports, pool size, and TTL. This should integrate smoothly with the service bootstrap paths you’ve wired in main.rs/service.rs.

Also applies to: 44-64, 83-94, 104-114

components/clp-credential-manager/src/routes/health.rs (1)

5-13: Health endpoint is simple and sufficient for readiness checks

The /health router and health_check handler are minimal, deterministic, and don’t depend on shared state or the DB, which is ideal for a basic liveness/readiness probe. No changes needed here.

components/clp-credential-manager/src/routes/mod.rs (1)

1-20: Router composition and shared AppState look good

Merging the sub-routers and applying TraceLayer::new_for_http() at the top level is a clean, idiomatic way to expose the service surface with shared AppState.

components/clp-credential-manager/src/models/mod.rs (1)

1-5: Model module wiring is cohesive

Re-exporting the credential and token types here gives a tidy public surface for consumers; this layout is clear and maintainable.

components/clp-credential-manager/src/models/token.rs (1)

1-23: TokenSubject modelling and helpers look appropriate

The tagged enum with snake_case variants, plus the job and service convenience constructors, gives a clear and robust way to represent token subjects for jobs vs services.

components/clp-credential-manager/src/routes/credentials.rs (1)

29-72: Concern Verified: CredentialMetadata safely excludes secret material

The CredentialMetadata struct correctly contains metadata only—id, name, type, description, durations, flags, and timestamps. No sensitive fields (keys, tokens, passwords) are present. Service layer methods (list_credentials, get_credential, create_credential) consistently return CredentialMetadata, and the TryFrom conversion implementation maps only non-sensitive fields. The struct is explicitly documented as public-facing, and the design properly isolates credential metadata from any internal secret values.

serde_yaml = "0.9.34"
sqlx = { version = "0.8.6", features = ["runtime-tokio", "mysql", "macros", "chrono"] }
thiserror = "2.0.17"
tokio = { version = "1.48.0", features = ["full"] }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Replace overly broad tokio "full" feature with explicitly needed features.

The "full" feature includes all runtime features (signal handling, process spawning, fs operations, net utilities, etc.), which increases compilation time and binary size. For a credential manager service using HTTP, async tasks, and time utilities, a more conservative set is preferable.

-tokio = { version = "1.48.0", features = ["full"] }
+tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "time", "sync"] }

If additional tokio features are required based on the actual service implementation, adjust accordingly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
tokio = { version = "1.48.0", features = ["full"] }
tokio = { version = "1.48.0", features = ["rt-multi-thread", "macros", "time", "sync"] }
🤖 Prompt for AI Agents
In components/clp-credential-manager/Cargo.toml around line 18, replace the
overly broad tokio feature = ["full"] with an explicit, minimal set required by
this service (e.g., runtime, macros and time utilities for async tasks and
timers, plus any IO/net/sync features used by your HTTP client/server); update
the features list to include only what the credential manager actually needs
(for example rt-multi-thread, macros, time and any of net/io-util/sync needed),
and run a build to add any missing features iteratively until compilation
succeeds.

Comment on lines +70 to +93
/// Emits the provided audit event to the tracing ecosystem using the `audit` target.
pub fn log_event(event: AuditEvent<'_>) {
let subject = event
.subject
.and_then(|subject| serde_json::to_value(subject).ok())
.unwrap_or(Value::Null);
let source_ip = event
.source_ip
.map(|addr| addr.ip().to_string())
.unwrap_or_default();
let payload = json!({
"component": COMPONENT_NAME,
"event_type": event.event_type,
"status": event.status.as_str(),
"credential_id": event.credential_id,
"credential_name": event.credential_name,
"subject": subject,
"source_ip": if source_ip.is_empty() { Value::Null } else { Value::String(source_ip) },
"error": event.error,
"metadata": event.metadata.unwrap_or(Value::Null),
});

info!(target = "audit", %payload);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Avoid double‑encoding JSON in audit logs for easier downstream processing

Given that the global tracing setup uses JSON formatting, logging a serde_json::Value via info!(target = "audit", %payload) will produce a JSON log whose payload field is itself a JSON string. That’s workable, but downstream consumers need to parse JSON twice. You may want instead to log the fields as structured attributes (e.g., info!(target = "audit", component = COMPONENT_NAME, event_type = event.event_type, status = event.status.as_str(), credential_id = event.credential_id, ...)) so the tracing backend emits a single-layer JSON object.

🤖 Prompt for AI Agents
In components/clp-credential-manager/src/audit.rs around lines 70–93, the
current code builds a serde_json::Value payload and logs it with info!(target =
"audit", %payload), which double‑encodes JSON under the global JSON tracing
formatter; instead, log structured fields directly so the tracing layer emits a
single JSON object. Replace the single %payload argument with named fields
(component, event_type, status, credential_id, credential_name, subject,
source_ip, error, metadata), converting Option/IpAddr to strings or
serde_json::Value as needed (use Value::Null for missing subject/metadata and an
empty string or null for missing source_ip), and pass them as structured key =
value pairs to info!(target = "audit", ...). Ensure no one call formats the
entire payload as a string.

Comment on lines +10 to +126
/// Returns all credential metadata sorted alphabetically by name.
pub async fn list_credentials(pool: &MySqlPool) -> ServiceResult<Vec<CredentialMetadata>> {
let query = format!(
"SELECT {columns} FROM {table} ORDER BY name",
columns = AWS_CREDENTIAL_METADATA_COLUMNS,
table = AWS_CREDENTIALS_TABLE
);

let rows = sqlx::query_as::<_, CredentialMetadataRow>(&query)
.fetch_all(pool)
.await?;

rows.into_iter().map(CredentialMetadata::try_from).collect()
}

/// Fetches a single credential row by ID.
pub async fn get_credential(pool: &MySqlPool, id: i64) -> ServiceResult<CredentialMetadata> {
let query = format!(
"SELECT {columns} FROM {table} WHERE id = ?",
columns = AWS_CREDENTIAL_METADATA_COLUMNS,
table = AWS_CREDENTIALS_TABLE
);

let row = sqlx::query_as::<_, CredentialMetadataRow>(&query)
.bind(id)
.fetch_optional(pool)
.await?;

match row {
Some(row) => CredentialMetadata::try_from(row),
None => Err(ServiceError::NotFound(format!(
"credential with id `{id}` was not found"
))),
}
}

/// Deletes the credential row and returns the affected row count.
pub async fn delete_credential(pool: &MySqlPool, id: i64) -> ServiceResult<u64> {
let query = format!(
"DELETE FROM {table} WHERE id = ?",
table = AWS_CREDENTIALS_TABLE
);

let result = sqlx::query(&query).bind(id).execute(pool).await?;
Ok(result.rows_affected())
}

/// Persists a new credential then fetches the stored metadata.
///
/// # Errors
///
/// * Propagates validation/persistence failures from [`insert_credential`].
/// * Propagates errors from [`get_credential`] when reloading the inserted row.
pub async fn create_credential(
pool: &MySqlPool,
request: &CreateCredentialRequest,
) -> ServiceResult<CredentialMetadata> {
let result = insert_credential(pool, request).await?;
let inserted_id = result.last_insert_id() as i64;
get_credential(pool, inserted_id).await
}

/// Raw insert helper that maps low-level driver errors into [`ServiceError`] variants.
async fn insert_credential(
pool: &MySqlPool,
request: &CreateCredentialRequest,
) -> ServiceResult<MySqlQueryResult> {
let query = format!(
"INSERT INTO {table} (name, credential_type, access_key_id, secret_access_key, role_arn, \
external_id, description, default_session_duration_seconds, transient, created_by) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
table = AWS_CREDENTIALS_TABLE
);

let name = request.name.as_str();
let credential_type = request.credential_type.as_str();
let access_key_id = request.access_key_id.as_deref();
let secret_access_key = request
.secret_access_key
.as_ref()
.map(|secret| secret.expose_secret().to_owned());
let role_arn = request.role_arn.as_deref();
let external_id = request.external_id.as_deref();
let description = request.description.as_deref();
let default_duration = request.default_session_duration_seconds.unwrap_or(3600);
let transient = request.transient.unwrap_or(false);
let created_by = request.created_by.as_deref();

sqlx::query(&query)
.bind(name)
.bind(credential_type)
.bind(access_key_id)
.bind(secret_access_key.as_deref())
.bind(role_arn)
.bind(external_id)
.bind(description)
.bind(default_duration)
.bind(transient)
.bind(created_by)
.execute(pool)
.await
.map_err(|err| map_insert_error(err, name))
}

/// Converts insert failures into domain-specific conflicts when possible.
fn map_insert_error(err: sqlx::Error, name: &str) -> ServiceError {
let is_duplicate = err
.as_database_error()
.and_then(|db_err| db_err.try_downcast_ref::<sqlx::mysql::MySqlDatabaseError>())
.is_some_and(|mysql_err| mysql_err.number() == 1062);

if is_duplicate {
return ServiceError::Conflict(format!("credential with name `{}` already exists", name));
}

ServiceError::from(err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Metadata‑only queries and insert mapping look good; consider static SQL for compile‑time checks

The operations correctly:

  • Restrict SELECTs to metadata columns (no secrets are read back).
  • Use bound parameters for all user input, avoiding SQL injection.
  • Map duplicate‑name insert failures to a domain‑level Conflict.

One improvement to consider is replacing the format!‑built SQL strings with static query strings and sqlx macros (e.g., query_as!), which would give you compile‑time validation of column sets and types and avoid string allocation at runtime. That’s especially helpful as the schema evolves.

Comment on lines +1 to +7
/// Canonical table name for the unified AWS credentials store.
pub const AWS_CREDENTIALS_TABLE: &str = "clp_aws_credentials";

/// Columns selected by metadata queries to avoid leaking secrets.
pub const AWS_CREDENTIAL_METADATA_COLUMNS: &str = "id, name, credential_type, description, \n \
default_session_duration_seconds, transient, \
created_at, updated_at, last_used_at";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Column list constant embeds a newline/indent; harmless but slightly surprising

The AWS_CREDENTIAL_METADATA_COLUMNS string literal includes a \n and indentation spaces, which is fine for SQL (whitespace is ignored) but might be unexpected if this constant is ever reused in a non-SQL context. Consider collapsing this to a single-line literal or using concat! for readability and to avoid embedded formatting artefacts.

🤖 Prompt for AI Agents
In components/clp-credential-manager/src/db/schema.rs around lines 1 to 7, the
AWS_CREDENTIAL_METADATA_COLUMNS constant embeds a `\n` and indentation spaces
which can introduce unexpected whitespace if reused; replace the
multi-line/escaped-newline string with a single-line literal containing the
comma-separated column names (or use concat! to build a single-line string) so
the constant contains no embedded newlines or leading spaces, preserving the
same column list but collapsing it into a clean single-line value for
readability and reuse.

Comment on lines +43 to +58
impl From<sqlx::Error> for ServiceError {
fn from(err: sqlx::Error) -> Self {
if matches!(err, sqlx::Error::RowNotFound) {
return ServiceError::NotFound("requested record was not found".to_owned());
}

if let Some(db_err) = err.as_database_error() {
if let Some(mysql_err) = db_err.try_downcast_ref::<sqlx::mysql::MySqlDatabaseError>() {
if mysql_err.number() == 1062 {
return ServiceError::Conflict(mysql_err.message().to_owned());
}
}
}

ServiceError::Database(err)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

Avoid leaking low-level backend errors to API clients; consider more user-facing messages and statuses

Right now ServiceError::Conflict and ServiceError::Database (and the Jwt/Io/Yaml variants) are turned into ApiError with source.to_string() (or mysql_err.message()) exposed directly to callers. For a credential service, this can leak internal schema details (constraint/index names, SQL text, filesystem paths, JWT library wording, etc.) and couples the public API tightly to backend implementations.

It may be safer to:

  • Normalise DB conflicts and generic DB errors into short, user-facing strings, and keep the original sqlx::Error only in logs/tracing.
  • Map common JWT failures (invalid/expired token) to 401 UNAUTHORIZED (or 400 BAD_REQUEST) with a generic message instead of 500.

For example:

impl From<ServiceError> for ApiError {
    fn from(err: ServiceError) -> Self {
        match err {
-           ServiceError::Validation(msg) => ApiError::new(StatusCode::BAD_REQUEST, msg),
-           ServiceError::Conflict(msg) => ApiError::new(StatusCode::CONFLICT, msg),
-           ServiceError::NotFound(msg) => ApiError::new(StatusCode::NOT_FOUND, msg),
-           ServiceError::Config(msg) => ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, msg),
-           ServiceError::Database(source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
-           }
-           ServiceError::Jwt(source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
-           }
-           ServiceError::Io(source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
-           }
-           ServiceError::Yaml(source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
-           }
+           ServiceError::Validation(msg) => ApiError::new(StatusCode::BAD_REQUEST, msg),
+           ServiceError::Conflict(_msg) => {
+               ApiError::new(StatusCode::CONFLICT, "resource already exists")
+           }
+           ServiceError::NotFound(msg) => ApiError::new(StatusCode::NOT_FOUND, msg),
+           ServiceError::Config(_msg) => {
+               ApiError::internal("service configuration error")
+           }
+           ServiceError::Database(_source) => {
+               ApiError::internal("database operation failed")
+           }
+           ServiceError::Jwt(_source) => {
+               ApiError::new(StatusCode::UNAUTHORIZED, "invalid or expired token")
+           }
+           ServiceError::Io(_source) => ApiError::internal("internal I/O error"),
+           ServiceError::Yaml(_source) => ApiError::internal("configuration parse error"),
        }
    }
}

Similarly, you could stop propagating the raw MySQL error message for code 1062 in the From<sqlx::Error> impl and instead use a generic conflict message or have the higher-level operation craft a domain-specific one.

Also applies to: 83-102


🏁 Script executed:

#!/bin/bash
# Locate and examine the ApiError implementation and From<ServiceError> conversion
rg -n "struct ApiError|impl From<ServiceError>" components/clp-credential-manager/src/error.rs -A 30

Length of output: 1937


🏁 Script executed:

#!/bin/bash
# Search for ApiError definition in the entire module
fd -e rs -x rg -l "struct ApiError|impl ApiError" {} \; | head -5

Length of output: 48


🏁 Script executed:

#!/bin/bash
# Examine the full error.rs file to understand the complete error handling structure
cat components/clp-credential-manager/src/error.rs

Length of output: 3708



🏁 Script executed:

#!/bin/bash
# Find endpoints/handlers that use ServiceError or ApiError
rg -n "ServiceError|ApiError" components/clp-credential-manager/src/ --type rs -B 2 | grep -E "(fn |pub async|ServiceError|ApiError)" | head -40

Length of output: 26


🏁 Script executed:

#!/bin/bash
# Check if there are any HTTP handlers returning ServiceError
fd -e rs -path "*/src/*" | xargs grep -l "handler\|route\|endpoint" | head -5

Length of output: 168


Sanitize backend error messages before exposing to API clients; map JWT errors to 401/400

The current implementation leaks low-level backend details to API callers, creating an information disclosure vulnerability:

  • Lines 90–91 (ServiceError::Database): Raw sqlx::Error messages expose database schema details, query structure, or connection issues.
  • Line 87 (ServiceError::Conflict): Raw MySQL error from line 50 exposes constraint/index names (e.g., "Duplicate entry for unique key 'email'").
  • Lines 93–94 (ServiceError::Jwt): JWT library errors with 500 status leak token parsing internals; should return 401 or 400 with a generic message.
  • Lines 96–99 (ServiceError::Io, Yaml): File paths, YAML parse locations, and system-level details exposed via .to_string().

Replace raw error messages with generic, user-facing strings; log original errors server-side:

impl From<ServiceError> for ApiError {
    fn from(err: ServiceError) -> Self {
        match err {
            ServiceError::Validation(msg) => ApiError::new(StatusCode::BAD_REQUEST, msg),
-           ServiceError::Conflict(msg) => ApiError::new(StatusCode::CONFLICT, msg),
+           ServiceError::Conflict(_) => {
+               ApiError::new(StatusCode::CONFLICT, "resource already exists")
+           }
            ServiceError::NotFound(msg) => ApiError::new(StatusCode::NOT_FOUND, msg),
            ServiceError::Config(_msg) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, msg)
+               ApiError::internal("service configuration error")
            }
            ServiceError::Database(_source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
+               ApiError::internal("database operation failed")
            }
            ServiceError::Jwt(_source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
+               ApiError::new(StatusCode::UNAUTHORIZED, "invalid or expired token")
            }
-           ServiceError::Io(_source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
-           }
+           ServiceError::Io(_) => ApiError::internal("internal I/O error"),
-           ServiceError::Yaml(_source) => {
-               ApiError::new(StatusCode::INTERNAL_SERVER_ERROR, source.to_string())
-           }
+           ServiceError::Yaml(_) => ApiError::internal("configuration parse error"),
        }
    }
}

Also update line 50 to use a generic conflict message instead of propagating mysql_err.message().

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In components/clp-credential-manager/src/error.rs around lines 43–58 (and
related ServiceError variants referenced in the review), the code currently
returns raw backend error strings to API clients; change it to return generic,
user-facing messages and log the original error server-side. Specifically:
replace returning ServiceError::Database(err) with a generic
ServiceError::Database("internal database error".to_owned()) while logging the
original err (e.g., log::error!("database error: {:?}", err)); replace the MySQL
duplicate-key branch to return ServiceError::Conflict("resource
conflict".to_owned()) and log mysql_err; ensure ServiceError::Jwt maps token
validation/parsing failures to appropriate client-level errors (401 Unauthorized
for invalid/missing token or 400 Bad Request for malformed token) using generic
messages like "invalid authentication token" and log the detailed jwt error;
likewise replace ServiceError::Io and ServiceError::Yaml to return generic
messages ("internal IO error", "invalid configuration") and log the underlying
errors; keep logs detailed but never expose underlying messages in the returned
ServiceError strings.

Comment on lines +1 to +7
use axum::{
Json,
Router,
extract::{Path, State},
http::StatusCode,
routing::get,
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Missing post import will break compilation

The router uses post(create_credential) but post is not imported from axum::routing, so this module will not compile.

-use axum::{
-    Json,
-    Router,
-    extract::{Path, State},
-    http::StatusCode,
-    routing::get,
-};
+use axum::{
+    Json,
+    Router,
+    extract::{Path, State},
+    http::StatusCode,
+    routing::{get, post},
+};

🏁 Script executed:

#!/bin/bash
# Verify the actual contents of credentials.rs and check for post import and usage
cat components/clp-credential-manager/src/routes/credentials.rs

Length of output: 1938


🏁 Script executed:

#!/bin/bash
# Search for post( usage in the credentials.rs file
rg -n "post\s*\(" components/clp-credential-manager/src/routes/credentials.rs

Length of output: 61


Missing post and delete imports will break compilation

The router uses .post(create_credential) and .delete(delete_credential) but neither post nor delete are imported from axum::routing, causing compilation to fail.

 use axum::{
     Json,
     Router,
     extract::{Path, State},
     http::StatusCode,
-    routing::get,
+    routing::{delete, get, post},
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use axum::{
Json,
Router,
extract::{Path, State},
http::StatusCode,
routing::get,
};
use axum::{
Json,
Router,
extract::{Path, State},
http::StatusCode,
routing::{delete, get, post},
};
🤖 Prompt for AI Agents
In components/clp-credential-manager/src/routes/credentials.rs around lines 1 to
7, the router uses .post(create_credential) and .delete(delete_credential) but
only get is imported from axum::routing; add the missing routing imports so
compilation succeeds: update the use axum::routing import to include post and
delete (e.g., import routing::{get, post, delete}) or add separate imports for
post and delete so the router functions resolve.

Comment on lines +16 to +27
/// Registers CRUD endpoints for long-term credentials.
pub fn router() -> Router<AppState> {
Router::new()
.route(
"/credentials",
get(list_credentials).post(create_credential),
)
.route(
"/credentials/:id",
get(get_credential).delete(delete_credential),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Router shape is sensible; consider 201 for resource creation

The CRUD paths and method mapping look good. For POST /credentials, you may want to return 201 Created (and possibly a Location header) instead of 200 OK to better reflect that a new resource was created.

🤖 Prompt for AI Agents
In components/clp-credential-manager/src/routes/credentials.rs around lines 16
to 27, the POST /credentials route currently leads to a 200 OK response; update
the create_credential handler to return 201 Created and set a Location header
pointing to the new credential resource (e.g. /credentials/{id}) when creation
succeeds, and ensure any testing/response types are adjusted accordingly so
callers receive the status and header instead of 200.

Comment on lines +1 to +26
use axum::{Json, Router, http::StatusCode, routing::post};
use serde::Serialize;

use super::AppState;

/// Temporary placeholder router until token issuance/exchange is implemented.
pub fn router() -> Router<AppState> {
Router::new()
.route("/credentials/issue-token", post(not_implemented))
.route("/credentials/exchange-token", post(not_implemented))
}

#[derive(Serialize)]
struct PlaceholderResponse {
message: &'static str,
}

/// Returns a 501 response indicating the endpoint is still under construction.
async fn not_implemented() -> (StatusCode, Json<PlaceholderResponse>) {
(
StatusCode::NOT_IMPLEMENTED,
Json(PlaceholderResponse {
message: "token endpoints are not implemented yet",
}),
)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Placeholder token routes are fine; consider future response-shape alignment

Using 501 with a small JSON payload is reasonable for a scaffold. Once the real token logic is added, it would be good to align these endpoints with the service’s standard JSON envelope / error type so clients see a consistent response shape across all routes.

🤖 Prompt for AI Agents
In components/clp-credential-manager/src/routes/tokens.rs around lines 1 to 26,
the placeholder endpoints currently return a bare JSON message which will break
client expectations; change the response to use the service’s standard JSON
envelope/error type (import the shared response or error struct used elsewhere
in the project), adapt PlaceholderResponse to that type (populate the usual
fields such as message, error code or details as required by the envelope), keep
returning StatusCode::NOT_IMPLEMENTED, and update the function signature and
Json wrapper to use the shared type so the endpoints match the application's
consistent response shape.

Comment on lines +36 to +53
pub async fn new(config: &AppConfig) -> ServiceResult<Self> {
let db_config = &config.database;
let options = sqlx::mysql::MySqlConnectOptions::new()
.host(&db_config.host)
.port(db_config.port)
.database(&db_config.name)
.username(&db_config.user)
.password(db_config.password.expose_secret());

let pool = MySqlPoolOptions::new()
.max_connections(db_config.max_connections)
.connect_with(options)
.await?;

let jwt = JwtManager::new(&config.jwt)?;

Ok(Self { db_pool: pool, jwt })
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

DB pool setup is fine; optional hardening via explicit timeouts

The database pool initialisation is straightforward and matches the config structure. For resilience, you may eventually want to surface connect_timeout / acquire_timeout (and related options) from MySqlPoolOptions into your config so a misbehaving database doesn’t block startup or request handling longer than intended.

🤖 Prompt for AI Agents
In components/clp-credential-manager/src/service.rs around lines 36 to 53, the
DB pool is created without any timeout or lifetime settings; update the code and
config to accept/connect pool timeouts and lifetimes (e.g. connect_timeout,
acquire_timeout, max_lifetime, idle_timeout) and apply them to MySqlPoolOptions
when building the pool. Add corresponding optional Duration or integer fields to
AppConfig.database (with sensible defaults), convert them to std::time::Duration
(e.g. Duration::from_secs) and call the matching MySqlPoolOptions methods
(connect_timeout, acquire_timeout, max_lifetime, idle_timeout) before
connect_with so the service won’t block indefinitely on DB operations.

Comment on lines +70 to +172
/// Fetches every credential metadata row and records the attempt in audit logs.
pub async fn list_credentials(&self) -> ServiceResult<Vec<CredentialMetadata>> {
match db::list_credentials(self.db_pool()).await {
Ok(credentials) => {
let mut event = AuditEvent::success("credential_list");
event.metadata = Some(json!({ "count": credentials.len() }));
audit::log_event(event);
Ok(credentials)
}
Err(err) => {
let error_message = err.to_string();
let event = AuditEvent::failure("credential_list", &error_message);
audit::log_event(event);
Err(err)
}
}
}

/// Retrieves a single credential and enriches audit records with identifiers.
pub async fn get_credential(&self, id: i64) -> ServiceResult<CredentialMetadata> {
match db::get_credential(self.db_pool(), id).await {
Ok(credential) => {
let mut event = AuditEvent::success("credential_get");
event.credential_id = Some(credential.id);
event.credential_name = Some(&credential.name);
audit::log_event(event);
Ok(credential)
}
Err(err) => {
let error_message = err.to_string();
let mut event = AuditEvent::failure("credential_get", &error_message);
event.credential_id = Some(id);
audit::log_event(event);
Err(err)
}
}
}

/// Attempts to delete a credential and distinguishes between success and missing rows.
pub async fn delete_credential(&self, id: i64) -> ServiceResult<()> {
let rows = db::delete_credential(self.db_pool(), id).await;
match rows {
Ok(rows_affected) if rows_affected > 0 => {
let mut event = AuditEvent::success("credential_delete");
event.credential_id = Some(id);
audit::log_event(event);
Ok(())
}
Ok(_) => {
let error_message = format!("credential with id `{id}` was not found");
let mut event = AuditEvent::failure("credential_delete", &error_message);
event.credential_id = Some(id);
audit::log_event(event);
Err(ServiceError::NotFound(error_message))
}
Err(err) => {
let error_message = err.to_string();
let mut event = AuditEvent::failure("credential_delete", &error_message);
event.credential_id = Some(id);
audit::log_event(event);
Err(err)
}
}
}

/// Validates, persists, and audits a new credential request.
///
/// # Errors
///
/// * Returns [`ServiceError::Validation`] when the input fails
/// [`CreateCredentialRequest::validate`].
/// * Propagates persistence failures from [`db::create_credential`].
pub async fn create_credential(
&self,
request: CreateCredentialRequest,
) -> ServiceResult<CredentialMetadata> {
if let Err(err) = request.validate() {
let error_message = err.to_string();
let event = AuditEvent::failure("credential_create", &error_message);
audit::log_event(event);
return Err(err);
}

match db::create_credential(self.db_pool(), &request).await {
Ok(created) => {
let mut event = AuditEvent::success("credential_create");
event.credential_id = Some(created.id);
event.credential_name = Some(&created.name);
event.metadata = Some(json!({
"credential_type": created.credential_type.as_str(),
"transient": created.transient,
}));
audit::log_event(event);
Ok(created)
}
Err(err) => {
let error_message = err.to_string();
let event = AuditEvent::failure("credential_create", &error_message);
audit::log_event(event);
Err(err)
}
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Audit usage is consistent; ensure error strings don’t leak sensitive or low‑level details

The CRUD methods cleanly integrate with the audit helper and avoid logging secrets, which is good. The failure paths, however, funnel err.to_string() directly into AuditEvent::failure, which means whatever Display for ServiceError (and any wrapped causes) emits will end up in audit logs. It’s worth double‑checking that these messages are suitably high‑level and do not expose SQL snippets, connection details, or other sensitive internals; if they do, consider mapping errors to coarser reason codes or sanitised messages before logging.

🤖 Prompt for AI Agents
components/clp-credential-manager/src/service.rs lines 70-172: the review warns
that calling err.to_string() and stuffing that into AuditEvent::failure may leak
sensitive/low-level details; update all failure branches to map ServiceError
(and underlying errors) to a sanitized high-level message or reason code before
passing to AuditEvent::failure (e.g., match on error variants and produce short
labels like "validation_failed", "not_found", "database_error", or include only
the public error message for validation errors), use that sanitized_message for
the audit event while still returning the original err to the caller so behavior
is unchanged.

@Eden-D-Zhang Eden-D-Zhang changed the base branch from main to s3-credentials-manager November 23, 2025 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant