Attachments: EPIC 2 — Cloud Object Storage Migration - JU-DEV-Bootcamps/ERAS GitHub Wiki

Description: Move binary storage off the application server by implementing an object storage provider (OpenStack Swift) behind the extended IFileStorageService contract, with private-by-default containers and time-limited signed download URLs. Fully new work — no cloud provider integration exists today.

Acceptance Criteria:

  • Files can be stored/retrieved from an OpenStack Swift cluster using the same AttachmentService/IFileStorageService interfaces from Epic 1.
  • Storage provider is switchable per environment via configuration only.
  • No container is publicly readable; all downloads happen via short-lived Temporary URLs (TempURL).
  • Encryption at rest is provided by Swift's server-side (proxy-level) encryption middleware, which — unlike S3's SSE — applies transparently to every object in an encrypted container regardless of upload path, requiring no per-request header or client cooperation (see US 2.1).

User Story 2.1 — OpenStack Swift Storage Provider Implementation

Description: As a system, I want a SwiftFileStorageProvider implementing the extended IFileStorageService, so attachments can be stored in an OpenStack Swift object storage cluster instead of local disk.

Acceptance Criteria:

  • Implements SaveAsync, ReadAsync/GetUrl, DeleteAsync, Exists against the Swift Object Storage REST API, using the same logical object name generated by AttachmentService — the existing {entityType}/{entityId}/{uuid}.{ext} key scheme from Ticket 1.3 maps directly onto Swift's pseudo-folder object naming within a single container.
  • Keystone endpoint, project/tenant, and credentials are configured via environment variables, never hardcoded (token handling itself is covered by US 2.2).
  • Passes the shared provider contract test suite (Task 1.2.2).
  • Encryption at rest relies on Swift's at-rest encryption middleware (backed by a key manager such as Barbican), enabled at the cluster/container level — no per-object or per-request configuration is required from the application.

Tasks:

  • Task 2.1.1 — Implement provider methods against the Swift Object Storage REST API Description: Implement PUT/GET/DELETE/HEAD calls against the Swift endpoint returned by the Keystone service catalog. There's no official first-party .NET SDK for Swift the way there is for S3, so this is a thin REST client over HttpClient (or a maintained community OpenStack client library, if one meets the team's support bar). AC: Integration tests pass against a local Swift test cluster (e.g. a saio/all-in-one Swift Docker image) or an equivalent Swift-compatible test double.
  • Task 2.1.2 — Externalize Keystone and container configuration AC: No secret, project name, or container name appears in source code; documented in deployment config.
  • Task 2.1.3 — Verify at-rest encryption is enabled on the target container Description: Confirm the Swift cluster's encryption middleware and key manager are active for the attachments container. Since Swift encrypts transparently at the proxy level, there's no per-object flag for the app to set — this task is about verification, not application code. AC: Cluster-level verification (or a test confirming ciphertext at the storage backend) confirms objects written to the container are encrypted at rest, independent of which client or flow wrote them.

User Story 2.2 — Keystone Authentication and Token Lifecycle Management

Description: As a system, I want the Swift provider to authenticate against OpenStack Keystone and keep its access token valid transparently, so uploads/downloads never fail due to an expired or missing token — a concern that doesn't exist with S3's long-lived IAM credentials and has no equivalent in the original proposal.

Acceptance Criteria:

  • The provider obtains a Keystone token (via application credentials, or another supported method) before the first request and caches it in memory.
  • The token is proactively refreshed before expiry, and transparently re-acquired on a 401 Unauthorized response instead of surfacing the failure to the caller.
  • Credentials used to obtain tokens are never logged or exposed in error messages or exceptions.

Tasks:

  • Task 2.2.1 — Implement Keystone token acquisition and in-memory caching AC: A token is requested once and reused across subsequent calls until it nears expiry.
  • Task 2.2.2 — Implement automatic token refresh and 401 retry AC: A request made with an expired/revoked token transparently re-authenticates and retries exactly once before failing.

User Story 2.3 — Restrict Container Access and Generate Temporary Download URLs

Description: As a security-conscious system owner, I want containers private by default and downloads served only via short-lived Temporary URLs (TempURL), so attachments can't be accessed via guessable or leaked direct links. Since encryption at rest is handled transparently by Swift's middleware (US 2.1), no client-side decryption step is needed for a valid Temporary URL.

Acceptance Criteria:

  • Containers have no public-read ACL by default (no .r:* grant on X-Container-Read).
  • GetUrl returns a Swift Temporary URL (TempURL), HMAC-signed with the account/container TempURL key, with a configurable expiration.
  • Direct, permanent object URLs are never exposed to clients.
  • The TempURL secret key is stored and rotated as a managed secret, not hardcoded, with a documented rotation procedure.

Tasks:

  • Task 2.3.1 — Configure container ACLs for private-by-default access AC: Fetching an object via its raw URL without a valid TempURL signature fails with 401/403.
  • Task 2.3.2 — Implement Temporary URL generation with configurable TTL Description: Compute the HMAC signature required by Swift's TempURL middleware (method, path, expiry, secret key) to build a time-limited download URL. AC: URL expires after the configured TTL; TTL is environment-configurable; an expired TempURL is rejected by the cluster.

User Story 2.4 — Configurable Provider Selection

Description: As a developer, I want to switch the active storage provider per environment through configuration, so local development doesn't require cloud access while production uses OpenStack Swift.

Acceptance Criteria:

  • STORAGE_PROVIDER (or equivalent) environment variable selects the active implementation at boot (local or swift).
  • Factory/DI resolves the correct provider without conditional logic scattered across the codebase (today's DI registration in InfrastructureServiceRegistration.cs wires LocalFileStorageService as a singleton directly — this becomes provider-selectable).
  • Both local and swift remain fully functional and tested.

Tasks:

  • Task 2.4.1 — Implement provider factory resolved at application boot AC: Switching the env var alone changes provider behavior with no code changes.
  • Task 2.4.2 — Document environment configuration for both providers AC: A new developer can configure either provider following docs alone.