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/IFileStorageServiceinterfaces 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,Existsagainst the Swift Object Storage REST API, using the same logical object name generated byAttachmentService— 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. asaio/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 Unauthorizedresponse 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 onX-Container-Read). GetUrlreturns 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 (localorswift).- Factory/DI resolves the correct provider without conditional logic scattered across the codebase (today's DI registration in
InfrastructureServiceRegistration.cswiresLocalFileStorageServiceas a singleton directly — this becomes provider-selectable). - Both
localandswiftremain 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.