|
|
@@ -15,6 +15,7 @@ How to install, configure, and integrate smartbotic-database into your C++ proje
|
|
|
- [Upgrading from Legacy Packages](#upgrading-from-legacy-packages)
|
|
|
- [Drop-in configuration (conf.d)](#drop-in-configuration-confd)
|
|
|
- [Eviction & Memory Pressure](#eviction--memory-pressure)
|
|
|
+- [Access Policy — row and column level security](#access-policy--row-and-column-level-security-270)
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -394,6 +395,106 @@ this name is always a subset of what the view allows.
|
|
|
> reader; the protection is against someone reading data files, snapshots, or
|
|
|
> backups, not against a client.
|
|
|
|
|
|
+### Access Policy — row and column level security (2.7.0+)
|
|
|
+
|
|
|
+Per-project row- and column-level access control. **Off by default**: a project
|
|
|
+with no policy behaves exactly as it did before 2.7.0, so upgrading changes
|
|
|
+nothing until you turn it on.
|
|
|
+
|
|
|
+> Before 2.7.0 there was no access control beyond authentication. Every API-key
|
|
|
+> holder had identical, complete access. Views were never a boundary - see the
|
|
|
+> note under "Why AND-merge filters".
|
|
|
+
|
|
|
+#### The model
|
|
|
+
|
|
|
+- **Principal** = the *name* of the API key the request arrived with. Keys became
|
|
|
+ named in 2.7.0:
|
|
|
+ ```json
|
|
|
+ "auth": { "required": true, "keys": [
|
|
|
+ { "name": "shadowman", "key": "<base64>" },
|
|
|
+ { "name": "callerai", "key": "<base64>" } ] }
|
|
|
+ ```
|
|
|
+ A bare string key still works and maps to the reserved principal `unnamed`. A
|
|
|
+ request with no usable token is the reserved principal `anonymous` - which is
|
|
|
+ grantable, so the local CLI keeps working by explicit policy rather than by an
|
|
|
+ implicit hole. `anonymous` and `unnamed` are refused as key names.
|
|
|
+- **Policy** = one record per (project, principal) granting `read`/`write` per
|
|
|
+ collection or file type, with an optional column `mask` and row predicate.
|
|
|
+- **admin** grants everything within a project plus the right to edit policy.
|
|
|
+
|
|
|
+#### Turning it on safely
|
|
|
+
|
|
|
+Enabling is **refused** unless some policy in the project has `admin: true` -
|
|
|
+lockout is prevented structurally, not by care. Removing the last admin of a
|
|
|
+secured project is refused for the same reason.
|
|
|
+
|
|
|
+Always arm a live project in **audit** mode first. Audit evaluates every policy,
|
|
|
+logs what it *would* deny, and allows the request:
|
|
|
+
|
|
|
+```bash
|
|
|
+smartbotic-db-cli policy-set acme ops '{"admin":true}'
|
|
|
+smartbotic-db-cli policy-set acme svc \
|
|
|
+ '{"collections":{"users":{"read":true,"mask":["ssn"],
|
|
|
+ "row":[{"field":"tenant","op":0,"value":"acme"}]}}}'
|
|
|
+
|
|
|
+smartbotic-db-cli security-set acme on audit # watch the log
|
|
|
+smartbotic-db-cli security-set acme on enforce # then commit
|
|
|
+smartbotic-db-cli security acme # show current state
|
|
|
+smartbotic-db-cli policies acme # list principals
|
|
|
+```
|
|
|
+
|
|
|
+Once enforcing, the project is **deny-by-default**: a principal with no matching
|
|
|
+rule gets nothing.
|
|
|
+
|
|
|
+#### What enforcement covers
|
|
|
+
|
|
|
+Every RPC that reads or writes user data. Notably including the ones that are
|
|
|
+easy to forget:
|
|
|
+
|
|
|
+- **`Subscribe`** is filtered per event, since an empty collection list means
|
|
|
+ "everything" and there is no single name to authorise up front. Masked columns
|
|
|
+ are stripped from event payloads.
|
|
|
+- **Version history** (`GetVersionHistory`, `GetDocumentVersion`,
|
|
|
+ `RestoreVersion`, `RestoreToDate`) - it returns previous document bodies, so
|
|
|
+ leaving it open would bypass a column mask entirely.
|
|
|
+- **Enumeration and counts** - `ListCollections`, `ListViews`, `ListFiles`,
|
|
|
+ `Count`, `GetCollectionInfo`, `GetMemoryStats` filter or refuse. A name or a
|
|
|
+ document count is information about data you may not read. `ListFiles` also
|
|
|
+ recomputes `total_count`, which would otherwise report how many files exist in
|
|
|
+ types you have no grant for.
|
|
|
+- **Batch and set operations**, which are ordinary reads and writes.
|
|
|
+
|
|
|
+Semantics worth knowing:
|
|
|
+
|
|
|
+- A **column mask** removes fields from every document returned, and **filtering
|
|
|
+ on a masked field is refused** - `salary > 100000` would otherwise leak a value
|
|
|
+ you cannot read.
|
|
|
+- A **write touching a masked field is denied**, not silently dropped. Silent
|
|
|
+ field loss is worse than an error.
|
|
|
+- A **row predicate** hides documents outright: `Get` reports not-found rather
|
|
|
+ than "forbidden", because the difference is itself information.
|
|
|
+- **System collections** (`_policies`, `_views`, …) require admin. `_policies`
|
|
|
+ describes the whole access model.
|
|
|
+
|
|
|
+#### Denials are visible to your code
|
|
|
+
|
|
|
+`get`, `exists`, `find`, `findWithMetrics` and `count` **throw** on a denial
|
|
|
+rather than returning empty. This is deliberate: a denial that looked like "no
|
|
|
+data" would leave your application unable to tell "you may not see this" from
|
|
|
+"there is nothing to see", and it would silently take the wrong branch.
|
|
|
+
|
|
|
+#### Limits
|
|
|
+
|
|
|
+- **Service-wide operations cannot be expressed per project.** `GetStats`,
|
|
|
+ `SetReadOnly`, `CreateProject` and `DropProject` require admin of *some*
|
|
|
+ secured project; `ListProjects` filters. The real boundary for operator
|
|
|
+ surfaces is **listener separation** - do not expose an admin listener publicly.
|
|
|
+- `HealthCheck` and `GetReadOnlyStatus` are intentionally open; they expose no
|
|
|
+ user data.
|
|
|
+- **Auth requires TLS.** gRPC aborts if an auth processor is attached to insecure
|
|
|
+ credentials, so `auth.required: true` with `tls.enabled: false` is refused at
|
|
|
+ startup.
|
|
|
+
|
|
|
### Collection Configuration
|
|
|
|
|
|
Per-collection runtime settings. Currently supports timestamp precision; extensible for future knobs.
|