CodamAIDocs
Topicdone

Storage layout and tenant isolation in storage

How the storage path is built, how tenants are separated in the file system, and how a file is stored atomically.

Variants
MULTI: path with tenantSINGLEsystem model without tenanttenant missing → rejectedatomic storingdirectory permissions

What this is about

The content of every file lives as a file of its own in the file storage, usually a directory on a volume. The path to it is built in a fixed way. The separation of tenants follows from it too: every tenant has its own subdirectory.

The path

Structure
Request
<basePath>/[<tenant>/]<model path>/<fileId>
Example, operating mode MULTI, tenant t1
/files/t1/fileasset/910e2c19-d5d1-4dae-8229-ea5fcbcc6194
PartfromExample
basePathconfiguration codamai.cdms.persistence.file.basePath/files/
tenanttenant of the request, only when needed (see below)t1
model pathfrom the API path of the model, fixed at generationfileasset/, company/logo/
fileIdUUID that the server assigns at the first save910e2c19-…

No part of the path comes directly from the user. The display name Contract.pdf is only in the record. CDMS checks the tenant before it goes into the path: letters, digits, ., _ and - are allowed, but no ...

When the tenant is in the path

With or without a tenant directory?
Operating modeLevel of the modelPath
MULTItenant or user<basePath>/<tenant>/<model>/<fileId>
MULTIsystem<basePath>/<model>/<fileId>, shared by all tenants
SINGLEall<basePath>/<model>/<fileId>, one shared directory

The user level does not separate further in the storage than the tenant level. Who may see which file of a tenant is decided by the permissions and the owner filter when the record is read. See Model levels: system, tenant, user.

A directory tree

flowchart TB
    B["/files/"] --> T1["t1/"]
    B --> T2["t2/"]
    B --> S["country-flags/ (system model)"]
    T1 --> T1F["fileasset/"]
    T1F --> F1["910e2c19-… (current)"]
    T1F --> F1V["910e2c19-….1790000100000 (version)"]
    T1 --> T1L["company/logo/"]
    T1L --> L1["3b7a…"]
    T2 --> T2F["fileasset/"]
    T2F --> F2["5c01…"]
    S --> S1["a9e2…"]

Tenant isolation

The separation lies in the path alone. A request from tenant t2 only looks under /files/t2/. Even with the fileId of a file of t1, it finds nothing there.

What the path separation achieves (MULTI)
SituationResult
t1 writes, t1 readsfound
t1 writes, t2 reads the same fileIdnot found, different directory
tenant model, request without tenantrejected, CDMS_FILE_TENANT_REQUIRED
tenant with disallowed charactersrejected, CDMS_FILE_TENANT_INVALID
system model, any tenantfound, shared directory

The database has already separated before that: a request does not even find the record of a foreign file. See Tenant isolation in CDMS.

Storing atomically

An upload first arrives as a temporary file, often on a different file system than the storage. If CDMS copied it directly to the target, a concurrent download could get a half-written file. Therefore:

  1. 1
    CDMS→File storage
    for audited models: renames the previous content into a version
  2. 2
    CDMS→File storage
    writes the content under a private name in the target directory: <fileId>.staging-<random>
    That can take a while for large files. Nobody reads this name.
  3. 3
    CDMS→File storage
    renames the temporary file to <fileId> in one step
    Result: There is never half-written content under <fileId>.

Directory permissions

CDMS creates new directories with the permissions rwxr-x---: the application user may do everything, its group may read, everyone else nothing. So tenant data stays closed even to other processes on the same server. A backup that runs under the same group can read. CDMS does not create the base directory itself, see Storage backends.

Pitfalls

What comes next

Sources in the code and the knowledge base
  • CDMS/cdms-localfs-storage – FileUtils.getPathForCurrentTenant, stagingPath; LocalFSFileController.saveFile (publish); docs/02-path-layout-and-tenant-separation.md; docs/adr/ADR-003, ADR-004, ADR-013, ADR-014, ADR-016, ADR-017
  • CDMS/cdms-generator – DtoMetaProcessor.getFilePath (model path from the API path)
  • CDMS/cdms-integrationtest – AbstractRecursiveFileDeleteTest (storageRoot with and without tenant segment)
Search