foundry

This week in Foundry development: whose authority is it anyway?

  • foundry
  • folio
  • library-identity
  • architecture

Engineering checkpoint, 25 October 2026: the authorization boundary, administration API and Admin screens are source-complete and locally qualified. Release and hosted adoption are a separate workstream, underway at this checkpoint. This post reports the completed source work, not a completed hosted rollout.

Last time, we described expanding Foundry towards a broader FOLIO distribution for practical library and collections integration. This week, tracing real workflows brought us to a more fundamental question: when a user starts work that passes between services, whose authority does the system exercise?

An import may legitimately create records that its initiating user cannot create directly. That requires distinguishing the user’s identity, permission to start the workflow, and authority entrusted to its components. It also raises an awkward question: how much of the application understands the machinery carrying that authority?

Replacing authorization machinery does not automatically repair the boundaries around it. That applies to Foundry as much as to the systems we integrate.

Three questions, different responsibilities

ResponsibilityQuestionImport example
AuthenticationWho is making this request, and what verifies that identity?A verified staff identity; later, a calling workload.
AuthorizationMay this subject perform this operation on this resource in this context?May this staff member start an import?
DelegationWhat authority may another component exercise on someone’s behalf, with what limits?May the import workflow create Inventory records without giving the user general direct-write access?

Foundry also checks Workspace admission independently: successful authentication does not mean a person belongs to the Workspace. Membership administration and application permission administration are separate responsibilities.

Delegation needs authorization too. It is more than forwarding a user’s identity, and need not be limited to the user’s direct privileges. “Alice caused this action” does not mean “Alice personally holds every privilege required to execute it.”

What brought us here

Data Import exposed a practical credential-size problem. We traced the native path rather than assuming that a larger HTTP header limit was the answer. Okapi registers compact per-handler SYS# permission references; Foundry had not implemented that compatibility mechanism. An isolated native proof reduced a delegated token from 7,601 to 417 bytes, preserving the actor, allowing one declared operation and denying an undeclared one.

That was our integration gap, not evidence that upstream lacked a solution. Nor was the isolated proof a completed import or proof of every delegated permission. Compact references solve transport size; they do not settle delegation lifetime, caller binding or revocation.

Role administration exposed a different problem. Against the selected native provider, controlled overlapping assignment requests both reported success while losing a grant. A concurrent revoke/add could restore the revoked grant. This was stored-state readback, not just a stale permission cache. It establishes a failure mechanism in a local reproduction, not a claim that a customer incident occurred.

Those findings made authorization a practical engineering priority rather than an abstract redesign exercise.

The public qualification summary records the retained evidence and exact source revisions. Data Import completion and a replacement delegation protocol remain outside this delivery.

Original FOLIO: delegation exists, but its representation travels

The original design does distinguish human assignments from module execution authority. Descriptor modulePermissions are an explicit delegation mechanism. The criticism is not that FOLIO forgot delegation.

The following UML-style component sketch simplifies the inspected native path. Arrows indicate dependencies or carried context, not every physical network hop.

flowchart TB
UI["Users / Settings UI"] -->|Native administration| P["mod-permissions: definitions, bundles, assignments"]
L["Login mechanism"] -->|User credential| O["Okapi"]
O -->|Validate and authorize| T["mod-authtoken"]
T -->|Resolve grants| P
T -->|Actor and module execution credential| O
O --> A["Application module A"]
A -->|Forward credential| O
O --> B["Application module B"]
A --> Q["Some native queues/events: credential and permission context"]
Q --> B
Native delegation is explicit; some consumers carry its credential representation across process and time boundaries.

The inspected native module token combines these claims:

.put("tenant", tenant)
.put("sub", username)
.put("module", moduleName)
.put("user_id", userId)
.put("extra_permissions", permissionList);

Source: pinned mod-authtoken ModuleToken.

Combining claims in a token is not inherently wrong. The coupling becomes significant when modules persist that token into queued work, retain permission snapshots or inspect claims themselves. A credential is then also application continuation context. Browser sessions, human membership and background work have different lifetimes; changing one cannot safely be assumed to change the others.

This is an architectural dependency we can demonstrate in inspected consumers. Attributing every part of it to the earliest FOLIO design would require historical evidence we have not gathered.

Eureka: replacement machinery, provider-aware consumers

Eureka substantially changes authorization through roles, capabilities, policies and Keycloak-backed UMA decisions. Its sidecars distinguish service credentials from user context. Describing that as merely cosmetic would be unfair.

However, the inspected Users UI selects a different workflow when the roles interface is available:

if (props.stripes.hasInterface('roles')) {
  return undefined;
}
return `perms/users/${pathComponents.id}`;

Source: ui-users 13.0.2, UserRecordContainer.

The alternate workflow checks and creates Keycloak authentication accounts and selects a user-update path based on their existence. This is ui-users, not a claim that mod-users itself owns that discovery switch.

flowchart TB
U["ui-users"] --> D{"roles interface available?"}
D -->|No| N["Native permission workflow"]
D -->|Yes| R["Role workflow with auth-account lifecycle"]
R -->|Check/create auth account; update user| A["mod-users-keycloak"]
R -->|Role assignments| M["mod-roles-keycloak"]
A --> K["Keycloak"]
M --> K
S["Module sidecar"] -->|UMA decisions and token integration| K
S --> B["Application module"]
classDef coupled fill:#fff0d6,stroke:#a66c00,color:#222;
class R coupled;
Discovery selects a workflow that knows provider-specific account semantics. The browser uses FOLIO APIs; it does not directly call Keycloak Admin REST in these flows.

For example, the inspected hook calls:

await tenantApi.get(`users-keycloak/auth-users/${userId}`);

Source: Keycloak existence hook. The role workflow also creates missing accounts.

Interface discovery is useful. But discovering a new interface is not proof that a new realization satisfies the previous contract. Here the consumer learns another workflow, including provider lifecycle. The API hides transport mechanics while exposing provider semantics.

Using Keycloak for both authentication and authorization is not itself a separation-of-responsibility failure. The question is whether consumers must understand that choice. We are not claiming FOLIO’s historical replaceability ambition was never tested; we are identifying a concrete limit of substitution in the inspected implementation.

Foundry: move provider variation behind the contract

Foundry retains Zitadel authentication, independent membership admission and stable staff actors. It separates the decision consumers need from the protocol used to obtain and propagate it.

The delivered decision port includes:

Evaluation evaluate(AccessRequest request, Authentication verifiedPrincipal);

The request describes subject, action, resource and context. The result exposes a decision or typed failure, without a native signing session, token or HTTP response. Credential propagation is separate. Installed operation policy is bound by trusted code; a request cannot submit its own replacement policy.

The recorded qualification uses real native modules and production-rendered Envoy, and checks that non-adapter production consumers no longer depend on native protocol implementation classes. This adopts AuthZEN’s information model; it does not claim full public AuthZEN protocol conformance.

flowchart TB
I["OIDC authentication: currently Zitadel"] -->|Verified identity| C["Foundry consumers / gateway"]
W["Workspace membership"] -->|Independent admission| C
C --> D["Authorization decision contract"]
C --> M["Role administration contract"]
D --> A["Native adapter: grants, evaluation, protocol"]
M --> A
A --> P["mod-permissions: single native grant authority"]
A --> T["Native credential propagation"]
T --> F["Unmodified FOLIO modules: domain checks retained"]
D -.-> K["Candidate Keycloak authorization adapter: unproved"]
M -.-> K
T -.-> X["Managed delegation: unselected / unproved"]
classDef delivered fill:#e0f2e5,stroke:#287a42,color:#222;
classDef contained fill:#fff0d6,stroke:#a66c00,color:#222;
classDef pending fill:#eee,stroke:#777,stroke-dasharray:5 5,color:#222;
class D,M delivered;
class A,P,T contained;
class K,X pending;
Green: source-complete and locally qualified contracts. Amber: retained native implementation and limitations. Grey: unproved future candidates. Existing native UIs retain their own compatibility paths; they are not all converted to neutral consumers.

The fresh integrated qualification executed 70 normal planner/executor actions, 20 real-browser checks and four native setup replays. It included real Zitadel login, native Users and Settings, Admin role edits and assignments, membership separation, denials, revocation and permission-service outage/recovery. Replaying setup preserved edited roles, direct grants, empty/revoked assignments and the explicitly removed bootstrap Administrator assignment. Required source pipelines passed, and owned test resources were removed.

Foundry Staff access separates direct roles, other direct privileges and observed effective access, and warns that roles do not grant Workspace admission.
Staff access from the 24 October local UI qualification. The actor and definitions are test fixtures, not customer data or deployed release versions. Stored assignments and observed cached access are deliberately presented separately. The later integrated run qualified the same UI with real Zitadel.

Are we adopting the roles interface?

We are adopting application-role concepts and Foundry-owned role administration, not currently advertising Eureka’s roles interface. Those are different decisions.

A role is a useful institutional bundle of privileges. In the selected native profile, mutable named permission sets and their assignments provide that backing, including nesting. Foundry administration operates on that same authority. It does not create a second grant store.

Advertising Eureka’s interface would tell consumers that its associated contracts are supported. In the inspected Users UI, it also selects the Keycloak-aware workflow. Returning plausible /roles data while omitting that lifecycle would be false compatibility, particularly for our independently authenticated Zitadel users.

This is not an argument that every roles interface must be Keycloak-specific. It is a decision about the actual consumer contract we inspected. We should advertise an interface only after qualifying the behavior it triggers.

Nor are we keeping mod-permissions merely because we have declined that interface. We retain it because it already owns useful native definitions, institutional bundles, assignments and expansion. It is a provider choice behind the boundary. A future provider could implement Foundry’s supported role semantics without forcing the core UI to know its account machinery.

That is legitimate so long as we describe the supported profile honestly: native additive/nested grants are not full contextual policy evaluation, role activation or separation of duties. Unsupported semantics must remain explicit. We do not promise drop-in compatibility with every Eureka UI.

What we fixed, what we contain

ConcernCurrent engineering positionLimit
Native protocol exposed to Foundry consumersDecision and propagation separated; implementation details adapter-private.One qualified native realization is not proof of universal substitution.
Trusting caller-supplied operation policyDecision bound to trusted tenant/action/resource and installed operation.This does not prove every possible future consumer.
Concurrent native administrationCross-replica write coordination, provider isolation and concurrent-write behavior locally qualified.Native implementation remains unsafe if bypassed; isolation is part of the solution.
New Foundry editsVersion checks for role edits; explicit grant/revoke operations.Legacy native whole-list forms lack edit-base versions and can overwrite later edits.
RevocationNative visibility measured and limitations recorded.No immediate-revocation guarantee.
Delegated credentials and queued workNative behavior retained behind the adapter.Managed invocation lifetime, caller binding and async revocation are not solved here.
sequenceDiagram
participant C as Foundry API / native UI
participant B as Controlled administration boundary
participant G as PostgreSQL coordination
participant P as Native provider
C->>B: Requested mutation
B->>B: Verify identity, admission, tenant and operation permission
B->>G: Reserve tenant write ownership
B->>B: Check edit version where supported
B->>G: Record dispatch
B->>P: Execute with genuine caller context
P->>P: Apply native record and grant policy
P-->>B: Native result
alt Confirmed completion
  B->>G: Record result and release
  B-->>C: Result
else Ambiguous outcome
  B->>G: Keep further writes blocked
  B-->>C: Explicit unresolved outcome
end
Note over C,P: Legacy stale forms remain a separate limitation.
Qualified administration boundary: Foundry coordinates execution; the native module owns grant decisions. Async native lifecycle jobs require completion tracking beyond an initial HTTP response.

A significant correction during this work was removing our own duplicated grant-policy checks. The first implementation had started interpreting native grant closure and assignment rules inside Foundry. That would have created two policy owners—the very coupling we wanted to avoid. The final implementation preserves actual caller identity and native credentials, lets the native module decide record/grant policy, and keeps Foundry’s tenant-wide concurrency coordination and version mechanics separate.

That means accepting a real native limitation rather than quietly claiming to repair it. The selected mod-permissions permits a caller with whole-record permission-user replacement authority to add grants through self-replacement that its item-level grant operation refuses. We treat that operation permission, perms.users.item.put, as fully trusted grant-administrator authority. Limited managers use explicit grant/revoke operations without it; the native Users permission editor requires the broader trust. This is a restriction on who may receive that authority, not a fix to the native behavior. Legacy stale whole-list forms can also restore an intervening revocation despite serialized execution.

Containment is engineering when it changes dependencies and enforces an operating envelope. It becomes concealment when the wrapper advertises guarantees the provider cannot supply. Foundry also acquires real maintenance obligations for its coordination and compatibility code; adaptation is not free.

We have not modified mod-permissions or proved every backend is unreachable through every possible bypass. Specific provider-isolation paths have real network evidence. That is narrower than a claim of complete installation-wide isolation.

Replaceability has to earn its name

We can distinguish three claims:

  1. Dependency isolation: consumers do not depend on provider implementation details. We have demonstrated this for the extracted Foundry boundary.
  2. Semantic substitution: another provider satisfies the same supported behavior, including denials, errors and unsupported features. This remains unproved.
  3. Operational replacement: identifiers, assignments, queued credentials and rollback survive a real transition. This is not current delivery.

A useful future test would put Keycloak authorization behind these contracts while leaving Zitadel authentication and the core consumer workflow unchanged. Any required realms, clients or subject representations would belong to adapter lifecycle. Richer policy semantics would need explicit feature support rather than silent flattening into grant lists.

That is a design test, not a selected implementation or a promise that Keycloak will fit without cost. It would be stronger evidence than declaring an interface “pluggable.” We want provider variation behind the contract, rather than making every consumer learn the next provider.

Evidence and limits

Our public evidence summary identifies the source commits, qualification results and retained limitations. It is a selected, sanitized extract of the internal receipts, not a complete independently reproducible test package. The upstream links above pin the exact source inspected rather than making claims about every current or future FOLIO version.

The integrated source proof is complete. Hosted adoption, complete imports, managed delegation, a second authorization provider and customer migration are separate outcomes. Native cache timing is eventual: the integrated run observed staff revocation after about 57 seconds, an observation rather than a guaranteed bound. Gateway denial does not cancel work already executing inside a module.

The useful claim is not that Foundry has abolished authorization debt. We have identified specific failures, separated responsibilities, changed enforceable behavior and kept remaining limits visible. That is the standard by which this work should be judged.