JanssenProject / JanssenProject/jans
feat(jans-cedarling): expose current policy store ID
- Dominant language
- Java
- Stars
- 647
- Forks
- 173
- Avg merge
- 1d 18h
- Merged PRs (30d)
- 110
Description
# Feature: expose current policy store ID
### Problem
Cedarling knows the ID of the policy store it loaded (`PolicyStoreWithID.id`, `jans-cedarling/cedarling/src/common/policy_store.rs:111`), but the type is `pub(crate)`. The ID is only visible indirectly, inside decision log entries (`policystore_id`, `cedarling/src/authz/mod.rs:1120`).
Library and binding users have no way to answer "which policy store is this instance running right now?" — needed for diagnostics, multi-tenant routing, and confirming a background refresh actually picked up a new store.
### Proposal
Public getter returning the ID of the **currently active** store, `None`/null when the store carries no ID.
- Core: `Cedarling::policy_store_id(&self) -> Option` in `cedarling/src/lib.rs`, backed by `Authz::policy_store_id()` reading `config.policy_store.id`. Mirror in `cedarling/src/blocking.rs`. **Not** gated behind the `tools` feature.
- Bindings (all of them, same semantics):
- uniffi (Kotlin/Swift/Java): `Option`
- WASM: `Option` → `string | undefined`
- Python: `Optional[str]` + `.pyi` stub
- Go: bridge returns `String` (`""` = unset, rust2go has no `Option`); Go wrapper `PolicyStoreID() (string, bool)`. Requires `internal/gen.go` regen + `.so` rebuild.
- C: `cedarling_get_policy_store_id(u64 instance_id, char** out_id) -> c_int`, `*out_id = NULL` **with `Success`** when unset (plain `NULL` return would be ambiguous with error). Free via `cedarling_free_string`. Header regen.
### Pitfalls to handle
1. **ID format differs per source.** New format (`.cjar`/directory) → hex 8–64 chars from `metadata.json`, but the field is `#[serde(default)]` and can legitimately be `""`. Legacy Agama JSON/YAML/Lock-Server → ID is the **map key** of `policy_stores`, not hex. Document as an opaque, source-dependent string.
2. **Two ID fields disagree.** Legacy path (`init/policy_store.rs:91`) sets `PolicyStoreWithID.id = key` but leaves `metadata.policy_store.id = String::new()`. Fix: populate metadata ID from the same key. `PolicyStoreWithID.id` stays the single source of truth (it is what the log entry uses).
3. **Store is swapped at runtime.** `Cedarling.authz` is an `ArcSwap` and the refresh worker publishes a rebuilt `Authz`. The getter must read through `authz.load()` on every call — never cache at construction. The ID read after `authorize()` may already be a different one; for per-decision correlation the log entry's `policystore_id` is authoritative. Hot reload to a different ID: just report the new ID, no warn, no rejection.
4. **Don't make `PolicyStoreWithID` / `PolicyStoreMetadata` public** just for this — `Option` keeps the public type surface unchanged.
### Tests
- `cedarling/src/tests/policy_store_loader.rs`: ID present for `.cjar` + directory; ID equals `policy_stores` key for Agama JSON/YAML; `None` when `metadata.json` omits `id`.
- `cedarling/src/init/policy_store_refresh.rs` tests: after a swap to a store with a different ID, getter returns the **new** ID.
- Negative: new-format store with malformed `id` fails with `ValidationError::InvalidPolicyStoreId` specifically.
- Per-binding smoke tests next to the existing `total_issuers` tests.
### Docs
- `docs/cedarling/reference/cedarling-interfaces.md` — add to each language's interface table.
- `docs/cedarling/reference/cedarling-policy-store.md` — ID semantics per source format + hot-reload caveat.
Contributor guide
Assessment
This issue has not been assessed yet.