docs: add code review process and baseline review of all 19 modules
Establishes a per-module code review workflow under code-reviews/ and
records the 2026-05-16 baseline review (commit 9c60592): 241 findings
across all src/ modules (6 Critical, 46 High, 100 Medium, 89 Low).
This is the clean starting point for remediation work.
This commit is contained in:
394
code-reviews/ConfigurationDatabase/findings.md
Normal file
394
code-reviews/ConfigurationDatabase/findings.md
Normal file
@@ -0,0 +1,394 @@
|
||||
# Code Review — ConfigurationDatabase
|
||||
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Module | `src/ScadaLink.ConfigurationDatabase` |
|
||||
| Design doc | `docs/requirements/Component-ConfigurationDatabase.md` |
|
||||
| Status | Reviewed |
|
||||
| Last reviewed | 2026-05-16 |
|
||||
| Reviewer | claude-agent |
|
||||
| Commit reviewed | `9c60592` |
|
||||
| Open findings | 11 |
|
||||
|
||||
## Summary
|
||||
|
||||
The ConfigurationDatabase module is a focused, conventional EF Core data-access layer:
|
||||
a single `ScadaLinkDbContext`, Fluent API entity configurations, eight repository
|
||||
implementations of Commons-defined interfaces, an `IAuditService` implementation, an
|
||||
`IInstanceLocator`, environment-aware migration handling, and design-time tooling
|
||||
support. Overall structure adheres well to the design doc and the CLAUDE.md "Code
|
||||
Organization" decisions — POCO entities and interfaces live in Commons, EF mappings and
|
||||
implementations live here, Fluent API only, and optimistic concurrency is correctly
|
||||
applied to `DeploymentRecord` via `rowversion`. The module is generally healthy.
|
||||
|
||||
The main themes across findings are: (1) a genuine logic bug in
|
||||
`GetTemplateWithChildrenAsync`, which loads child templates and then discards them, so
|
||||
the method does not deliver what its name implies; (2) secret-bearing columns (SMTP
|
||||
credentials, external-system auth config, database connection strings) persisted in
|
||||
plaintext with no encryption-at-rest; (3) a hardcoded SQL `sa` connection string with a
|
||||
password literal embedded in `DesignTimeDbContextFactory`; (4) the no-arg
|
||||
`AddConfigurationDatabase()` overload, which silently registers nothing, making a
|
||||
misconfigured central node fail late and opaquely; and (5) audit-trail robustness gaps —
|
||||
`AuditService` can throw on serializing entities with navigation cycles, rolling back
|
||||
the whole business operation, and the design doc's claim that audit `Id` is `Long/GUID`
|
||||
disagrees with the `int` entity. Test coverage is good for the repositories that have
|
||||
tests (Security, CentralUI, audit, concurrency, seed data, data protection) but several
|
||||
repositories (`TemplateEngineRepository`, `DeploymentManagerRepository`,
|
||||
`ExternalSystemRepository`, `InboundApiRepository`, `NotificationRepository`,
|
||||
`SiteRepository`, `InstanceLocator`) have little or no direct coverage.
|
||||
|
||||
## Checklist coverage
|
||||
|
||||
| # | Category | Examined | Notes |
|
||||
|---|----------|----------|-------|
|
||||
| 1 | Correctness & logic bugs | ✓ | `GetTemplateWithChildrenAsync` discards loaded children (CD-001); `GetApprovedKeysForMethodAsync` CSV parsing is brittle (CD-008). |
|
||||
| 2 | Akka.NET conventions | ✓ | No actors in this module; data-access layer only. No issues found. |
|
||||
| 3 | Concurrency & thread safety | ✓ | DbContext correctly scoped; optimistic concurrency on `DeploymentRecord` correct. Repositories hold no shared mutable state. No issues found. |
|
||||
| 4 | Error handling & resilience | ✓ | `WaitForDatabaseReadyAsync` is sound. No-arg DI overload fails late and silently (CD-003); audit JSON serialization failure handling (CD-007). |
|
||||
| 5 | Security | ✓ | Hardcoded `sa` credential literal (CD-002); SMTP/DB-connection/auth secrets stored unencrypted (CD-004). |
|
||||
| 6 | Performance & resource management | ✓ | `GetAllTemplatesAsync` / `GetTemplateTreeAsync` eager-load multiple collections without `AsSplitQuery` (CD-009). No N+1 in audited paths. |
|
||||
| 7 | Design-document adherence | ✓ | Audit `Id` type mismatch vs design doc (CD-005); seed data uses `HasData` consistent with design. |
|
||||
| 8 | Code organization & conventions | ✓ | Mostly clean. `Grpc*` address columns unbounded (CD-006); inconsistent null-guard on injected context (CD-011). |
|
||||
| 9 | Testing coverage | ✓ | Several repositories and `InstanceLocator` lack direct tests (CD-010). |
|
||||
| 10 | Documentation & comments | ✓ | `DeploymentManagerRepository` "WP-24 stub" XML comment is stale; noted in module context but not raised as a standalone finding. No issues found beyond items above. |
|
||||
|
||||
## Findings
|
||||
|
||||
### ConfigurationDatabase-001 — `GetTemplateWithChildrenAsync` loads child templates then discards them
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | High |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs:30-41` |
|
||||
|
||||
**Description**
|
||||
|
||||
`GetTemplateWithChildrenAsync` queries for all templates whose `ParentTemplateId`
|
||||
equals the requested id, assigns the result to the local variable `children`, and
|
||||
then returns `template` — the `children` list is never used, attached to the returned
|
||||
object, or otherwise exposed. The method is therefore behaviourally identical to
|
||||
`GetTemplateByIdAsync` but issues an extra database round-trip. Any caller relying on
|
||||
the method name to obtain a template with its derived/child templates populated will
|
||||
silently receive a template with no children, leading to incorrect template-resolution
|
||||
or UI behaviour with no error.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either populate the children onto the returned aggregate (e.g. project into a result
|
||||
type that carries the children, or load them into a navigation collection that is
|
||||
actually returned), or remove the dead query and the misleading method if children are
|
||||
not in fact needed. If the navigation does not exist on the `Template` entity, add an
|
||||
explicit result tuple/DTO so the loaded data reaches the caller.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-002 — Hardcoded `sa` connection string with embedded password literal
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/DesignTimeDbContextFactory.cs:21-22` |
|
||||
|
||||
**Description**
|
||||
|
||||
`DesignTimeDbContextFactory` falls back to a literal connection string
|
||||
`"Server=localhost,1433;Database=ScadaLink_Config;User Id=sa;Password=YourPassword;TrustServerCertificate=True"`
|
||||
when no configured connection string is found. Embedding a credential literal (even a
|
||||
placeholder) in source code is a poor pattern: it is committed to version control,
|
||||
encourages copy-paste of `sa`/`TrustServerCertificate=True` into real environments, and
|
||||
the fallback can mask a genuine misconfiguration during `dotnet ef` operations by
|
||||
silently pointing tooling at an unintended database.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Remove the hardcoded fallback. If no connection string is resolved from configuration
|
||||
or environment, throw a clear `InvalidOperationException` instructing the developer to
|
||||
set `ScadaLink:Database:ConfigurationDb` (or an environment variable). At minimum, read
|
||||
the design-time connection string from an environment variable rather than a literal,
|
||||
and never use `sa`.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-003 — No-arg `AddConfigurationDatabase()` silently registers nothing
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/ServiceCollectionExtensions.cs:44-49` |
|
||||
|
||||
**Description**
|
||||
|
||||
The parameterless `AddConfigurationDatabase()` overload is a deliberate no-op "retained
|
||||
for backward compatibility during migration." If a central node is wired up with this
|
||||
overload by mistake, no `ScadaLinkDbContext`, repositories, `IAuditService`, or
|
||||
`IInstanceLocator` are registered. The failure does not surface at startup; it surfaces
|
||||
much later as opaque DI resolution exceptions the first time any consumer requests a
|
||||
repository — far from the actual misconfiguration. The XML comment also refers to
|
||||
"Phase 0 stubs," which is stale relative to the current state of the module.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Either delete the no-op overload now that the connection-string overload exists, or
|
||||
mark it `[Obsolete]` with an error-level message so misuse is a compile-time failure.
|
||||
If a true "site node" no-op is genuinely required, give it an explicit, self-documenting
|
||||
name (e.g. `AddConfigurationDatabaseNoOp()`), and remove the stale "Phase 0" wording.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-004 — Secret-bearing columns stored in plaintext with no protection
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Security |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/NotificationConfiguration.cs:56-57`, `src/ScadaLink.ConfigurationDatabase/Configurations/ExternalSystemConfiguration.cs:25-26,75-77` |
|
||||
|
||||
**Description**
|
||||
|
||||
`SmtpConfiguration.Credentials`, `ExternalSystemDefinition.AuthConfiguration`, and
|
||||
`DatabaseConnectionDefinition.ConnectionString` all hold authentication secrets (SMTP
|
||||
OAuth2 client secrets / passwords, external-system API keys or Basic Auth credentials,
|
||||
and database passwords respectively). They are mapped as ordinary string columns and
|
||||
persisted verbatim. Anyone with read access to the configuration database — including
|
||||
audit-log JSON if these entities are serialized into `AfterStateJson` — obtains the
|
||||
plaintext secrets. The design doc does not call out encryption-at-rest for these
|
||||
fields, so the design is also silent on a real risk.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Apply encryption to these fields, e.g. an EF Core value converter backed by ASP.NET
|
||||
Data Protection (the module already configures `IDataProtectionKeyContext`), or rely on
|
||||
SQL Server Always Encrypted / column encryption. Separately, ensure `IAuditService`
|
||||
callers never pass these secret-bearing entities (or that the serializer redacts the
|
||||
fields) so secrets do not leak into `AuditLogEntry.AfterStateJson`. Update the design
|
||||
doc to state the chosen at-rest protection.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-005 — Audit `Id` type disagrees with the design doc
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Design-document adherence |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/AuditConfiguration.cs:11` (entity `src/ScadaLink.Commons/Entities/Audit/AuditLogEntry.cs`) |
|
||||
|
||||
**Description**
|
||||
|
||||
The design doc's Audit Entry Schema table specifies `Id` as `Long / GUID`, and notes
|
||||
the audit table is append-only and retained indefinitely. The actual `AuditLogEntry`
|
||||
entity uses an `int` identity key. For a never-purged, append-only table that
|
||||
accumulates one row per save operation across the system lifetime, a 32-bit identity
|
||||
risks overflow over a long deployment horizon, and the code drifts from the documented
|
||||
schema.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Change `AuditLogEntry.Id` to `long` (and the corresponding migration column to
|
||||
`bigint`) to match the design doc and remove the overflow risk, or — if `int` is
|
||||
intentional — update the design doc's schema table to say `int` and justify it.
|
||||
Resolve the discrepancy in one direction.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-006 — `Site.GrpcNodeAAddress` / `GrpcNodeBAddress` columns are unbounded
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Configurations/SiteConfiguration.cs:24-25` |
|
||||
|
||||
**Description**
|
||||
|
||||
`SiteConfiguration` explicitly sets `HasMaxLength(500)` for `NodeAAddress` and
|
||||
`NodeBAddress`, but the entity also has `GrpcNodeAAddress` and `GrpcNodeBAddress`
|
||||
(added per the gRPC streaming design decision) which are not configured at all. With no
|
||||
length set, EF Core maps them to `nvarchar(max)`. This is inconsistent with the sibling
|
||||
address columns, wastes the opportunity to constrain input, and `nvarchar(max)` columns
|
||||
cannot be indexed and have different storage/performance characteristics.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add `builder.Property(s => s.GrpcNodeAAddress).HasMaxLength(500);` and the same for
|
||||
`GrpcNodeBAddress`, matching the existing `NodeAAddress`/`NodeBAddress` mapping, and
|
||||
generate a migration to alter the column types.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-007 — `AuditService` does not handle JSON-serialization failure of arbitrary `afterState`
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Medium |
|
||||
| Category | Error handling & resilience |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Services/AuditService.cs:28-30` |
|
||||
|
||||
**Description**
|
||||
|
||||
`LogAsync` serializes the caller-supplied `afterState` object with
|
||||
`JsonSerializer.Serialize(afterState)` using default options. EF entity POCOs commonly
|
||||
have navigation properties; serializing an entity that has loaded navigations (e.g. a
|
||||
`Template` with `Attributes`/`Scripts`, or any entity with a cycle) will throw
|
||||
`JsonException` for a reference cycle or produce a very large payload. Because audit
|
||||
writes are designed to commit in the same transaction as the change, a serialization
|
||||
exception thrown here will roll back the *entire* business operation — a template
|
||||
update fails because its audit entry could not be serialized. This couples audit
|
||||
robustness to the shape of every entity passed in.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Configure `JsonSerializerOptions` with `ReferenceHandler.IgnoreCycles` (or
|
||||
`Preserve`) and a sensible `MaxDepth`, and consider serializing a projected
|
||||
DTO/snapshot rather than the live tracked entity. Decide explicitly whether an audit
|
||||
serialization failure should fail the operation or be logged and degraded gracefully,
|
||||
and document that decision against the design doc's transactional-guarantee section.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-008 — `GetApprovedKeysForMethodAsync` CSV parsing silently drops malformed ids
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Correctness & logic bugs |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/InboundApiRepository.cs:46-58` |
|
||||
|
||||
**Description**
|
||||
|
||||
`ApiMethod.ApprovedApiKeyIds` is stored as a comma-separated string of integer ids.
|
||||
`GetApprovedKeysForMethodAsync` splits it, maps each token with
|
||||
`int.TryParse(...) ? id : -1`, then filters with `id > 0`. Any token that fails to
|
||||
parse, or a legitimately negative/zero id, is silently discarded. If `ApprovedApiKeyIds`
|
||||
becomes corrupt (e.g. a stray name instead of an id), the method quietly returns fewer
|
||||
approved keys than expected, which for an API-key authorization path means a method may
|
||||
unexpectedly reject a key that should be approved. Storing a relational many-to-many as
|
||||
a CSV string in a column is itself fragile (no FK integrity, no cascade on key delete).
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Short term: log a warning when a token fails to parse instead of silently dropping it,
|
||||
so corruption is observable. Longer term: replace the CSV column with a proper join
|
||||
table (`ApiMethodApprovedKey`) with foreign keys to `ApiMethod` and `ApiKey`, which
|
||||
gives referential integrity and correct cascade behaviour when an API key is deleted.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-009 — Multi-collection eager loads issue cartesian-product queries
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Performance & resource management |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs:43-51,53-61`, `src/ScadaLink.ConfigurationDatabase/Repositories/CentralUiRepository.cs:45-55` |
|
||||
|
||||
**Description**
|
||||
|
||||
`GetAllTemplatesAsync`, `GetTemplatesComposingAsync`, and `GetTemplateTreeAsync` each
|
||||
`Include` three-to-four sibling collections (`Attributes`, `Alarms`, `Scripts`,
|
||||
`Compositions`) in a single query. EF Core's default single-query strategy produces a
|
||||
cartesian-product join across those collections, so a template with N attributes, M
|
||||
alarms, and K scripts yields N×M×K rows that EF must then de-duplicate. For templates
|
||||
with many members this materially inflates the result set and query time.
|
||||
`GetInstanceByIdAsync`/`GetAllInstancesAsync` have the same shape with three
|
||||
collections.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add `.AsSplitQuery()` to these multi-collection-include queries (or set
|
||||
`UseQuerySplittingBehavior(QuerySplittingBehavior.SplitQuery)` globally in
|
||||
`AddConfigurationDatabase`) so each collection is loaded with a separate query and the
|
||||
cartesian explosion is avoided.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-010 — Several repositories and `InstanceLocator` lack direct test coverage
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Testing coverage |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/TemplateEngineRepository.cs`, `Repositories/DeploymentManagerRepository.cs`, `Repositories/ExternalSystemRepository.cs`, `Repositories/InboundApiRepository.cs`, `Repositories/NotificationRepository.cs`, `Repositories/SiteRepository.cs`, `Services/InstanceLocator.cs` |
|
||||
|
||||
**Description**
|
||||
|
||||
The test project covers `SecurityRepository`, `CentralUiRepository`, `AuditService`,
|
||||
optimistic concurrency, seed data, and Data Protection persistence. There are no direct
|
||||
tests for `TemplateEngineRepository` (the largest repository, and the one with the
|
||||
CD-001 bug, which a test would have caught), `DeploymentManagerRepository` (including
|
||||
its `Local`-then-stub delete fallback and the `DeleteInstanceAsync`
|
||||
restrict-FK-cleanup logic), `ExternalSystemRepository`, `InboundApiRepository` (notably
|
||||
`GetApprovedKeysForMethodAsync` CSV parsing — CD-008), `NotificationRepository`,
|
||||
`SiteRepository` (including its stub-attach delete path), or `InstanceLocator`.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Add repository-level tests using the existing `SqliteTestHelper` pattern, covering at
|
||||
minimum: CRUD round-trips, the stub-attach delete fallbacks in
|
||||
`DeploymentManagerRepository`/`SiteRepository`, `DeleteInstanceAsync`'s explicit
|
||||
deployment-record cleanup, `GetApprovedKeysForMethodAsync` with valid/malformed CSV,
|
||||
and `InstanceLocator.GetSiteIdForInstanceAsync` for found/not-found cases.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
|
||||
### ConfigurationDatabase-011 — Inconsistent constructor null-guarding across repositories/services
|
||||
|
||||
| | |
|
||||
|--|--|
|
||||
| Severity | Low |
|
||||
| Category | Code organization & conventions |
|
||||
| Status | Open |
|
||||
| Location | `src/ScadaLink.ConfigurationDatabase/Repositories/ExternalSystemRepository.cs:11-14`, `Repositories/InboundApiRepository.cs:11-14`, `Repositories/NotificationRepository.cs:11-14`, `Services/InstanceLocator.cs:13-16` |
|
||||
|
||||
**Description**
|
||||
|
||||
`SecurityRepository`, `CentralUiRepository`, `TemplateEngineRepository`,
|
||||
`DeploymentManagerRepository`, `SiteRepository`, and `AuditService` all guard their
|
||||
injected `ScadaLinkDbContext` with `?? throw new ArgumentNullException(...)`.
|
||||
`ExternalSystemRepository`, `InboundApiRepository`, `NotificationRepository`, and
|
||||
`InstanceLocator` assign the constructor argument directly with no guard. This is a
|
||||
minor consistency/maintainability issue: although the DI container will not normally
|
||||
supply null, the divergence makes the codebase look unfinished and means a future
|
||||
hand-constructed instance fails with a less informative `NullReferenceException` later.
|
||||
|
||||
**Recommendation**
|
||||
|
||||
Apply the same `?? throw new ArgumentNullException(nameof(context))` guard in the four
|
||||
inconsistent constructors so all data-access types behave uniformly.
|
||||
|
||||
**Resolution**
|
||||
|
||||
_Unresolved._
|
||||
Reference in New Issue
Block a user