From f3c96e912a36e99b7898bb7f482851855b42a097 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Mon, 29 Jun 2026 17:19:14 +0100 Subject: [PATCH 1/9] Added proposal for AuthN/Z Api Refactor Assisted-by: Claude Opus 4.6 Signed-off-by: Thomas Cooper --- .gitignore | 3 +- proposals/000-authorizer-api-refactor.md | 267 +++++++++++++++++++++++ 2 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 proposals/000-authorizer-api-refactor.md diff --git a/.gitignore b/.gitignore index 4952b217..f3ef5730 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea .claude/settings.local.json -.scratch/ \ No newline at end of file +.scratch/ +*.swp diff --git a/proposals/000-authorizer-api-refactor.md b/proposals/000-authorizer-api-refactor.md new file mode 100644 index 00000000..f9187769 --- /dev/null +++ b/proposals/000-authorizer-api-refactor.md @@ -0,0 +1,267 @@ +# xxx - Making the Authorizer API standalone + +The Kroxylicious authorizer API provides a general-purpose abstraction for access control decisions, deliberately designed to be agnostic of specific `Principal` and `ResourceType` implementations. +However, the API currently depends on `kroxylicious-api`, which transitively pulls in Kafka client libraries, Jackson, and compression codecs. +This makes it less appealing for non-Kroxylicious projects to reuse. +This proposal extracts the `Subject` and `Principal` concepts into a new lightweight module, `kroxylicious-authentication-api`, so that `kroxylicious-authorizer-api` can be consumed independently of `kroxylicious-api` module. + +## Current situation + +The `Authorizer` interface and `AuthorizeResult` record in `kroxylicious-authorizer-api` reference `io.kroxylicious.proxy.authentication.Subject`, which is defined in `kroxylicious-api`. +This means any project that wants to implement the `Authorizer` plugin interface must depend on `kroxylicious-api`, which transitively pulls in `kafka-clients`, `jackson-annotations`, and compression codec libraries (`zstd-jni`, `lz4-java`, `snappy-java`). + +The authorizer API's actual usage of `Subject` is narrow. +`Authorizer.authorize()` receives a `Subject` and passes it through to `AuthorizeResult`. +Implementations like `AclAuthorizer` call only `subject.principals()` and then `principal.name()` and `principal.getClass()` on each element. +None of the richer methods on the concrete `Subject` record (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) or the `User`-specific validation are used by the authorizer API itself. + +Despite this narrow usage, the module dependency graph forces consumers to accept a large transitive dependency tree: + +``` +kroxylicious-authorizer-api +├── kroxylicious-api (compile) +│ ├── jackson-annotations (compile) +│ └── kafka-clients (compile) +│ ├── zstd-jni (runtime) +│ ├── lz4-java (runtime) +│ ├── snappy-java (runtime) +│ └── slf4j-api (runtime) +└── ... +``` + +## Motivation + +### External reuse is blocked + +The authorizer API's general-purpose design, as described in [proposal 009][prop-9], was built for reuse: the `Authorizer` interface is agnostic of specific `Principal` and `ResourceType` implementations, and its asynchronous return type supports both in-process and networked policy decision points such as [OPA](https://www.openpolicyagent.org/) and [OpenFGA](https://openfga.dev/). + +This generality has attracted interest from other projects. +For example, [Apicurio Registry](https://www.apicur.io/registry/) would like to use the authorizer API as the basis for its [fine-grained authorization](https://github.com/Apicurio/apicurio-registry/issues/7724) implementation. +However, the dependency on `kroxylicious-api` makes this impractical. +To work on their [prototype implementation](https://github.com/Apicurio/apicurio-registry/pull/7829), Apicurio Registry have copied the Kroxylicious Authorizer API code into their own module and removed the dependent code. +Any non-Kroxylicious project that wants to implement or consume the `Authorizer` interface faces the same barrier. + +### The dependency cost is disproportionate to what is actually used + +As detailed in [Current situation](#current-situation) section, the authorizer API's usage of `Subject` is narrow: implementations only call `principals()`, `name()`, and `getClass()`. +Yet this narrow usage forces consumers to accept `kafka-clients`, `jackson-annotations`, and compression codec libraries as transitive dependencies. +The relative cost of importing the dependency does not match the value consumed. + +### Authentication concepts are misplaced in the module hierarchy + +`Subject` and `Principal` are general authentication concepts. +They represent identity, not proxy behaviour. +Their current placement in `kroxylicious-api` mixes identity types with proxy infrastructure. + +### The 0.x window makes this the right time + +The project is at version 0.x, where breaking changes can be expected and carry lower migration cost. +The authorizer API was introduced relatively recently, so external adoption is likely to be relatively minimal. +Post-1.0, this same change would require deprecation cycles, compatibility shims, and migration documentation. +Doing it now avoids most of that overhead. + +## Proposal + +Introduce a new module, `kroxylicious-authentication-api`, containing minimal interfaces for `Subject` and `Principal`, plus the `@Unique` annotation (which is used for marking `Principals` which should have only one instance per subject). +The existing concrete `Subject` record in `kroxylicious-api` is renamed to `ProxySubject` and implements the new `Subject` interface, and `kroxylicious-authorizer-api` switches its dependency from `kroxylicious-api` to the new module. + +### New module: `kroxylicious-authentication-api` + +A new module in the `io.kroxylicious.authentication` package containing three types: + +`Principal` interface: a single method, `String name()`. +The Javadoc contract (implementations must override `hashCode`/`equals` based on class and name) is carried forward from the existing `Principal`. + +`Subject` interface: a single method, `Set principals()`. +The wildcard return type is key: it allows the concrete `ProxySubject` record (whose accessors returns `Set`) to satisfy the interface via covariant return. + +`@Unique` annotation: `@Retention(RUNTIME)`, `@Target(TYPE)`. +Marks `Principal` implementations that should have at most one instance in a `Subject`. +This annotation is moved from the main `kroxylicious-api`, as external users of this API may also want to enforce this invariant. + +The module has no compile-scope dependencies beyond `spotbugs-annotations` (provided scope, for package-level null-safety annotations). +This means the transitive dependency tree for consumers of the authorizer API, which imports from this new module, becomes: + +``` +kroxylicious-authorizer-api +├── kroxylicious-authentication-api (compile) +│ └── spotbugs-annotations (provided) +└── ... +``` + +### Use of a new package + +The new types live in `io.kroxylicious.authentication`, not `io.kroxylicious.proxy.authentication`. +This avoids a split package, a situation where two Maven artifacts contribute types to the same Java package. +Split packages block JPMS adoption and can confuse IDEs and build tools even on the classpath. + +The package name also signals that these types are not proxy-specific. +They represent general authentication concepts that any project can use. + +### Changes to existing types in `kroxylicious-api` + +- `io.kroxylicious.proxy.authentication.Principal` is **deleted**. + This is a binary-incompatible change. + +- `io.kroxylicious.proxy.authentication.Subject` is **renamed** to `io.kroxylicious.proxy.authentication.ProxySubject` and adds `implements io.kroxylicious.authentication.Subject`. + The rename avoids ambiguity between the interface (`io.kroxylicious.authentication.Subject`) and the concrete record when both are in scope. + Its `Set principals` component and method type bounds change from the deleted proxy `Principal` to `io.kroxylicious.authentication.Principal`. + The file is renamed from `Subject.java` to `ProxySubject.java`, the test from `SubjectTest.java` to `ProxySubjectTest.java`, and all references are updated: `Subject.anonymous()` becomes `ProxySubject.anonymous()`, `new Subject(...)` becomes `new ProxySubject(...)`, etc. + This is a binary and source incompatible change for all code referencing the concrete type by name. + +- Because `ProxySubject` is used by several interfaces in `kroxylicious-api`, the following method signatures also change: + - `FilterContext.clientSaslAuthenticationSuccess(String, Subject)` to `FilterContext.clientSaslAuthenticationSuccess(String, ProxySubject)` + - `FilterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` + - `RouterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` + - `TransportSubjectBuilder.buildTransportSubject(Context)` return type changes from `CompletionStage` to `CompletionStage` + - `SaslSubjectBuilder.buildSaslSubject(Context)` return type changes from `CompletionStage` to `CompletionStage` + + These are all binary-incompatible changes. + +- The `@Unique` annotation is moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.authentication`. + The old annotation is deleted. + This is a binary-incompatible change: code compiled against the old annotation will not see it on types annotated with the new one. + This is mitigated by: + - The project being at version 0.x (pre-1.0 API stability). + - A `japicmp` exclusion documenting the intentional removal. + - The `@Unique` annotation having no known external consumers. + +- `User` and other types annotated with `@Unique` update their import to the new annotation. + `User`, `PrincipalFactory`, and test types (`FakeUniquePrincipal`, `FakeMultiplePrincipal`) add an explicit `import io.kroxylicious.authentication.Principal` since the same-package type no longer exists. + +- The `japicmp` configuration is updated with: + - `` entries for: the removed `Principal` class, the removed `Unique` annotation, the removed `Subject` class (renamed to `ProxySubject`), the changed `PrincipalFactory#newPrincipal` return type, and the changed method signatures in `TransportSubjectBuilder#buildTransportSubject`, `SaslSubjectBuilder#buildSaslSubject`, `FilterContext#clientSaslAuthenticationSuccess`, `FilterContext#authenticatedSubject`, and `RouterContext#authenticatedSubject`. + - An `` entry for `io.kroxylicious.proxy.authentication.Principal`, because `japicmp` cannot resolve old bytecode signatures that reference the deleted class without this. + +- The concrete `ProxySubject` record retains all its existing behaviour, including the `User`-principal validation in its constructor, and its `uniquePrincipalOfType`, `allPrincipalsOfType`, and `isAnonymous` methods. + +### Changes to `kroxylicious-authorizer-api` + +- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the old concrete record, now renamed to `ProxySubject`) to `io.kroxylicious.authentication.Subject` (the new interface). + +- The module's dependency on `kroxylicious-api` is replaced with a dependency on `kroxylicious-authentication-api`. + A test-scope dependency on `kroxylicious-api` is retained for tests that construct concrete `ProxySubject` instances. + +- This is a source-breaking change for `Authorizer` implementations: they must update the parameter type in their `authorize()` method from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.authentication.Subject`. + The fix is mechanical (change one import). + Callers of `authorize()` (such as `AuthorizationFilter`) are unaffected because the concrete `ProxySubject` implements the interface. + +### Changes to downstream modules + +Downstream changes follow two patterns: + +- **Rename**: all code referencing the concrete `Subject` type updates to `ProxySubject` — variable declarations, constructor calls, method signatures, test assertions (including `toString()` output and `Mockito.any()` matchers). +- **Re-import**: code referencing `Principal` or `@Unique` updates imports from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.authentication`. + +These changes are mechanical and affect most modules that interact with authentication types, including filters, runtime, integration tests, and microbenchmarks. + +#### Notable implications + +- **Wildcard return type in `Authorizer` implementations**: the `Subject` interface's `principals()` method returns `Set`. + Implementations that previously assigned the result to `Set` must change to `var` or `Set`. + +- **`kroxylicious-runtime` gains a direct dependency on `kroxylicious-authentication-api`**: its source directly references the authentication-api `Principal` type, so this must be an explicit compile-scope dependency. + +- **Maven dependency analyzer false positive**: the compiler needs `kroxylicious-authentication-api` on the classpath to resolve `ProxySubject`'s super-interface, but the bytecode doesn't directly reference authentication-api types. + This triggers Maven's analyzer. + Three modules (`kroxylicious-filter-test-support`, `kroxylicious-oauthbearer-validation`, `kroxylicious-sasl-inspection`) add `kroxylicious-authentication-api` as a compile-scope dependency with an `ignoredNonTestScopedDependencies` override to suppress the warning. + +- **Dependency enforcer allowlists**: `kroxylicious-authentication-api` must be added to `bannedDependencies` allowlists in the `kroxylicious-filters`, `kroxylicious-kms-providers`, and `kroxylicious-kubernetes` parent POMs. + +- **`@Unique` FQN in error message assertions**: tests that assert on the fully-qualified annotation name in error messages (e.g. in `ProxySubjectTest`, `AclAuthorizerServiceTest`) must update from `io.kroxylicious.proxy.authentication.Unique` to `io.kroxylicious.authentication.Unique`. + +### Modules not affected + +The following modules require no source or dependency changes: + +- `kroxylicious-annotations` +- `kroxylicious-app` +- `kroxylicious-certificate-test-support` +- `kroxylicious-docs` +- `kroxylicious-docs-tests` +- `kroxylicious-filter-archetype` +- `kroxylicious-integration-test-support` +- `kroxylicious-kafka-message-tools` +- `kroxylicious-kms` +- `kroxylicious-kms-test-support` +- `kroxylicious-kms-tls-support` +- `kroxylicious-krpc-plugin` +- `kroxylicious-openmessaging-benchmarks` +- `kroxylicious-systemtests` + +## Affected/not affected projects + +### Affected + +- `kroxylicious`: the main repository; all changes are within this repo. + +### Not affected + +- `kroxylicious-junit5-extension` +- `kroxylicious-operator` + +## Compatibility + +This proposal includes the following breaking changes: + +| Change | Kind | Impact | +|--------|------|--------| +| `io.kroxylicious.proxy.authentication.Principal` deleted | Binary-incompatible | Code compiled against the old interface must be recompiled. All implementations change to `io.kroxylicious.authentication.Principal`. | +| `@Unique` moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.authentication` | Binary-incompatible | Code compiled against the old annotation must be recompiled. No known external consumers. | +| `Subject` renamed to `ProxySubject` | Binary- and source-incompatible | All code referencing the concrete `Subject` type by name must be updated. | +| `ProxySubject` constructor and `PrincipalFactory` return type change from proxy `Principal` to authentication-api `Principal` | Binary-incompatible | Callers must be recompiled. The type bound is strictly widened so no source changes are needed at call sites. | +| `FilterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` | Binary-incompatible | Filter implementations and callers must be recompiled. Source-incompatible for implementations that declare the return type explicitly. | +| `RouterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` | Binary-incompatible | Router implementations and callers must be recompiled. Source-incompatible for implementations that declare the return type explicitly. | +| `FilterContext.clientSaslAuthenticationSuccess()` parameter changes from `Subject` to `ProxySubject` | Binary-incompatible | Filter implementations calling or implementing this method must be recompiled. | +| `TransportSubjectBuilder.buildTransportSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | Transport subject builder implementations must be recompiled. | +| `SaslSubjectBuilder.buildSaslSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | SASL subject builder implementations must be recompiled. | +| `Authorizer.authorize()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.authentication.Subject` (the new interface) | Binary- and source-incompatible | `Authorizer` implementations must be recompiled and must update one import. Two implementations exist in the codebase; the fix is mechanical. | +| `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.authentication.Subject` (the new interface) | Binary- and source-incompatible | Code creating or deconstructing `AuthorizeResult` instances must be recompiled. The record's canonical constructor and `subject()` accessor change type. Source fix is mechanical (change one import). | + +All other changes (adding a new module, adding dependency allowlist entries) are source- and binary-compatible. + +## Rejected alternatives + +### Extract concrete types into the new module + +Moving the concrete `Subject` record, `Principal` interface, `User`, `Unique`, `PrincipalFactory`, `UserFactory`, and `SubjectBuildingException` into a new module while keeping the existing package name `io.kroxylicious.proxy.authentication` would create a split package: two Maven artifacts contributing types to the same Java package. +Split packages block JPMS adoption, confuse build tooling, and are considered bad practice. +The interface extraction approach avoids this entirely by using a new package. + +### Include a concrete `Subject` implementation in the new module + +Providing a ready-made `Subject` implementation (e.g. `DefaultSubject`) in `kroxylicious-authentication-api` was considered so that external consumers wouldn't need to write their own. +This was deferred because: + +- The interface is a functional interface (`Subject` has one abstract method), so anonymous implementations are trivial: `() -> myPrincipalSet`. +- External consumers building production systems will likely want their own implementation with domain-specific validation or immutability guarantees. +- Adding a concrete implementation can be done later without breaking changes if there is demand. + +### Generalise the existing `Subject` record and ship it in `authentication-api` + +Rather than introducing a minimal `Subject` interface and renaming the existing concrete record to `ProxySubject`, an alternative would be to remove the `User`-principal validation from the existing `Subject` record and move it directly into `kroxylicious-authentication-api` as a general-purpose concrete type. +This would avoid the rename (no `ProxySubject`, no source-incompatible change for downstream code referencing `Subject` by name) and give external consumers a ready-made implementation. + +This was rejected for several reasons: + +1. **Split package or forced package rename for all consumers.** + If the record kept its `io.kroxylicious.proxy.authentication` package, two Maven artifacts would contribute types to the same package — a split package that blocks JPMS and confuses tooling. + If it moved to `io.kroxylicious.authentication`, every downstream reference would still need updating (the same source-incompatible cost as the rename to `ProxySubject`), but with the additional confusion of a type called `Subject` silently losing its proxy-specific validation. + +2. **The `User` validation is load-bearing within the proxy.** + The proxy's authentication pipeline relies on non-anonymous subjects containing exactly one `User` principal. + Removing this validation from the concrete type would push enforcement responsibility to every call site that constructs a subject within the proxy, creating a class of bugs where subjects without a `User` principal silently propagate through the pipeline. + The `ProxySubject` approach keeps this invariant co-located with the type, where it is easiest to maintain and hardest to forget. + +3. **It conflates two concerns with different stability requirements.** + The authentication-api module is intended to be a stable, minimal dependency for external consumers. + The concrete `Subject` record in `kroxylicious-api` carries proxy-specific behaviour (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`, `User` validation) that may evolve with the proxy. + Shipping a concrete implementation in the stable module locks in that behaviour and constrains future changes. + The interface approach decouples the contract (what external consumers depend on) from the implementation (what the proxy needs). + +4. **External consumers gain little.** + The `Subject` interface is a functional interface with a single `principals()` method, so external consumers can implement it trivially (`() -> myPrincipalSet`). + A generalized concrete record adds convenience, but at the cost of the issues above. + If demand for a concrete implementation materialises, it can be added later without breaking changes — the interface approach keeps this option open. + +[prop-9]: https://github.com/kroxylicious/design/blob/main/proposals/009-authorizer.md From 0d5ce3ca7d28ee076a5f03cbb0d7bf5d2b273c20 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 3 Jul 2026 14:31:22 +0100 Subject: [PATCH 2/9] Rename auth refactor proposal to use PR number Signed-off-by: Thomas Cooper --- ...{000-authorizer-api-refactor.md => 119-auth-api-refactor.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename proposals/{000-authorizer-api-refactor.md => 119-auth-api-refactor.md} (99%) diff --git a/proposals/000-authorizer-api-refactor.md b/proposals/119-auth-api-refactor.md similarity index 99% rename from proposals/000-authorizer-api-refactor.md rename to proposals/119-auth-api-refactor.md index f9187769..21218f69 100644 --- a/proposals/000-authorizer-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -1,4 +1,4 @@ -# xxx - Making the Authorizer API standalone +# 119 - Making the Authorizer API standalone The Kroxylicious authorizer API provides a general-purpose abstraction for access control decisions, deliberately designed to be agnostic of specific `Principal` and `ResourceType` implementations. However, the API currently depends on `kroxylicious-api`, which transitively pulls in Kafka client libraries, Jackson, and compression codecs. From 4661c3ba1e415e8bc31cec512dacd4e489b9a086 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 10 Jul 2026 11:55:57 +0100 Subject: [PATCH 3/9] Update proposal after review from Sam B Assisted-by: Claude Opus 4.6 Signed-off-by: Thomas Cooper --- proposals/119-auth-api-refactor.md | 79 +++++++++++++++--------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/proposals/119-auth-api-refactor.md b/proposals/119-auth-api-refactor.md index 21218f69..f0d12121 100644 --- a/proposals/119-auth-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -3,7 +3,7 @@ The Kroxylicious authorizer API provides a general-purpose abstraction for access control decisions, deliberately designed to be agnostic of specific `Principal` and `ResourceType` implementations. However, the API currently depends on `kroxylicious-api`, which transitively pulls in Kafka client libraries, Jackson, and compression codecs. This makes it less appealing for non-Kroxylicious projects to reuse. -This proposal extracts the `Subject` and `Principal` concepts into a new lightweight module, `kroxylicious-authentication-api`, so that `kroxylicious-authorizer-api` can be consumed independently of `kroxylicious-api` module. +This proposal extracts the `Subject` and `Principal` concepts into a new lightweight module, `kroxylicious-identity-api`, so that `kroxylicious-authorizer-api` can be consumed independently of `kroxylicious-api` module. ## Current situation @@ -53,27 +53,29 @@ The relative cost of importing the dependency does not match the value consumed. They represent identity, not proxy behaviour. Their current placement in `kroxylicious-api` mixes identity types with proxy infrastructure. -### The 0.x window makes this the right time +### The 0.x window reduces migration cost -The project is at version 0.x, where breaking changes can be expected and carry lower migration cost. -The authorizer API was introduced relatively recently, so external adoption is likely to be relatively minimal. +The preceding sections establish the substantive case for this change: external reuse is blocked, the dependency cost is disproportionate, and authentication concepts are misplaced. +The project's pre-1.0 status does not justify the change on its own. It should be noted that this would be the project's first breaking change and that track record of compatibility has value. +However, the 0.x window does significantly reduce the cost of making a change that is justified on its own merits. +The authorizer API was introduced relatively recently, so external adoption is likely to be minimal. Post-1.0, this same change would require deprecation cycles, compatibility shims, and migration documentation. -Doing it now avoids most of that overhead. +Given the demonstrated external demand and the narrow usage pattern, the migration cost is justified now in a way that would be harder to justify later. ## Proposal -Introduce a new module, `kroxylicious-authentication-api`, containing minimal interfaces for `Subject` and `Principal`, plus the `@Unique` annotation (which is used for marking `Principals` which should have only one instance per subject). +Introduce a new module, `kroxylicious-identity-api`, containing minimal interfaces for `Subject` and `Principal`, plus the `@Unique` annotation (which is used for marking `Principals` which should have only one instance per subject). The existing concrete `Subject` record in `kroxylicious-api` is renamed to `ProxySubject` and implements the new `Subject` interface, and `kroxylicious-authorizer-api` switches its dependency from `kroxylicious-api` to the new module. -### New module: `kroxylicious-authentication-api` +### New module: `kroxylicious-identity-api` -A new module in the `io.kroxylicious.authentication` package containing three types: +A new module in the `io.kroxylicious.identity` package containing three types: `Principal` interface: a single method, `String name()`. The Javadoc contract (implementations must override `hashCode`/`equals` based on class and name) is carried forward from the existing `Principal`. -`Subject` interface: a single method, `Set principals()`. -The wildcard return type is key: it allows the concrete `ProxySubject` record (whose accessors returns `Set`) to satisfy the interface via covariant return. +`Subject` interface: a single method, `Set principals()`. +This matches the original design from [proposal 009][prop-9], where the diversity of `Principal` implementations is handled by `Principal` being an interface. `@Unique` annotation: `@Retention(RUNTIME)`, `@Target(TYPE)`. Marks `Principal` implementations that should have at most one instance in a `Subject`. @@ -84,28 +86,28 @@ This means the transitive dependency tree for consumers of the authorizer API, w ``` kroxylicious-authorizer-api -├── kroxylicious-authentication-api (compile) +├── kroxylicious-identity-api (compile) │ └── spotbugs-annotations (provided) └── ... ``` ### Use of a new package -The new types live in `io.kroxylicious.authentication`, not `io.kroxylicious.proxy.authentication`. +The new types live in `io.kroxylicious.identity`, not `io.kroxylicious.proxy.authentication`. This avoids a split package, a situation where two Maven artifacts contribute types to the same Java package. Split packages block JPMS adoption and can confuse IDEs and build tools even on the classpath. The package name also signals that these types are not proxy-specific. -They represent general authentication concepts that any project can use. +They represent general identity concepts that any project can use. ### Changes to existing types in `kroxylicious-api` - `io.kroxylicious.proxy.authentication.Principal` is **deleted**. This is a binary-incompatible change. -- `io.kroxylicious.proxy.authentication.Subject` is **renamed** to `io.kroxylicious.proxy.authentication.ProxySubject` and adds `implements io.kroxylicious.authentication.Subject`. - The rename avoids ambiguity between the interface (`io.kroxylicious.authentication.Subject`) and the concrete record when both are in scope. - Its `Set principals` component and method type bounds change from the deleted proxy `Principal` to `io.kroxylicious.authentication.Principal`. +- `io.kroxylicious.proxy.authentication.Subject` is **renamed** to `io.kroxylicious.proxy.authentication.ProxySubject` and adds `implements io.kroxylicious.identity.Subject`. + The rename avoids ambiguity between the interface (`io.kroxylicious.identity.Subject`) and the concrete record when both are in scope. + Its `Set principals` component and method type bounds change from the deleted proxy `Principal` to `io.kroxylicious.identity.Principal`. The file is renamed from `Subject.java` to `ProxySubject.java`, the test from `SubjectTest.java` to `ProxySubjectTest.java`, and all references are updated: `Subject.anonymous()` becomes `ProxySubject.anonymous()`, `new Subject(...)` becomes `new ProxySubject(...)`, etc. This is a binary and source incompatible change for all code referencing the concrete type by name. @@ -118,7 +120,7 @@ They represent general authentication concepts that any project can use. These are all binary-incompatible changes. -- The `@Unique` annotation is moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.authentication`. +- The `@Unique` annotation is moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity`. The old annotation is deleted. This is a binary-incompatible change: code compiled against the old annotation will not see it on types annotated with the new one. This is mitigated by: @@ -127,7 +129,7 @@ They represent general authentication concepts that any project can use. - The `@Unique` annotation having no known external consumers. - `User` and other types annotated with `@Unique` update their import to the new annotation. - `User`, `PrincipalFactory`, and test types (`FakeUniquePrincipal`, `FakeMultiplePrincipal`) add an explicit `import io.kroxylicious.authentication.Principal` since the same-package type no longer exists. + `User`, `PrincipalFactory`, and test types (`FakeUniquePrincipal`, `FakeMultiplePrincipal`) add an explicit `import io.kroxylicious.identity.Principal` since the same-package type no longer exists. - The `japicmp` configuration is updated with: - `` entries for: the removed `Principal` class, the removed `Unique` annotation, the removed `Subject` class (renamed to `ProxySubject`), the changed `PrincipalFactory#newPrincipal` return type, and the changed method signatures in `TransportSubjectBuilder#buildTransportSubject`, `SaslSubjectBuilder#buildSaslSubject`, `FilterContext#clientSaslAuthenticationSuccess`, `FilterContext#authenticatedSubject`, and `RouterContext#authenticatedSubject`. @@ -137,12 +139,12 @@ They represent general authentication concepts that any project can use. ### Changes to `kroxylicious-authorizer-api` -- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the old concrete record, now renamed to `ProxySubject`) to `io.kroxylicious.authentication.Subject` (the new interface). +- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the old concrete record, now renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface). -- The module's dependency on `kroxylicious-api` is replaced with a dependency on `kroxylicious-authentication-api`. +- The module's dependency on `kroxylicious-api` is replaced with a dependency on `kroxylicious-identity-api`. A test-scope dependency on `kroxylicious-api` is retained for tests that construct concrete `ProxySubject` instances. -- This is a source-breaking change for `Authorizer` implementations: they must update the parameter type in their `authorize()` method from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.authentication.Subject`. +- This is a source-breaking change for `Authorizer` implementations: they must update the parameter type in their `authorize()` method from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. The fix is mechanical (change one import). Callers of `authorize()` (such as `AuthorizationFilter`) are unaffected because the concrete `ProxySubject` implements the interface. @@ -151,24 +153,21 @@ They represent general authentication concepts that any project can use. Downstream changes follow two patterns: - **Rename**: all code referencing the concrete `Subject` type updates to `ProxySubject` — variable declarations, constructor calls, method signatures, test assertions (including `toString()` output and `Mockito.any()` matchers). -- **Re-import**: code referencing `Principal` or `@Unique` updates imports from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.authentication`. +- **Re-import**: code referencing `Principal` or `@Unique` updates imports from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity`. These changes are mechanical and affect most modules that interact with authentication types, including filters, runtime, integration tests, and microbenchmarks. #### Notable implications -- **Wildcard return type in `Authorizer` implementations**: the `Subject` interface's `principals()` method returns `Set`. - Implementations that previously assigned the result to `Set` must change to `var` or `Set`. +- **`kroxylicious-runtime` gains a direct dependency on `kroxylicious-identity-api`**: its source directly references the identity-api `Principal` type, so this must be an explicit compile-scope dependency. -- **`kroxylicious-runtime` gains a direct dependency on `kroxylicious-authentication-api`**: its source directly references the authentication-api `Principal` type, so this must be an explicit compile-scope dependency. - -- **Maven dependency analyzer false positive**: the compiler needs `kroxylicious-authentication-api` on the classpath to resolve `ProxySubject`'s super-interface, but the bytecode doesn't directly reference authentication-api types. +- **Maven dependency analyzer false positive**: the compiler needs `kroxylicious-identity-api` on the classpath to resolve `ProxySubject`'s super-interface, but the bytecode doesn't directly reference identity-api types. This triggers Maven's analyzer. - Three modules (`kroxylicious-filter-test-support`, `kroxylicious-oauthbearer-validation`, `kroxylicious-sasl-inspection`) add `kroxylicious-authentication-api` as a compile-scope dependency with an `ignoredNonTestScopedDependencies` override to suppress the warning. + Three modules (`kroxylicious-filter-test-support`, `kroxylicious-oauthbearer-validation`, `kroxylicious-sasl-inspection`) add `kroxylicious-identity-api` as a compile-scope dependency with an `ignoredNonTestScopedDependencies` override to suppress the warning. -- **Dependency enforcer allowlists**: `kroxylicious-authentication-api` must be added to `bannedDependencies` allowlists in the `kroxylicious-filters`, `kroxylicious-kms-providers`, and `kroxylicious-kubernetes` parent POMs. +- **Dependency enforcer allowlists**: `kroxylicious-identity-api` must be added to `bannedDependencies` allowlists in the `kroxylicious-filters`, `kroxylicious-kms-providers`, and `kroxylicious-kubernetes` parent POMs. -- **`@Unique` FQN in error message assertions**: tests that assert on the fully-qualified annotation name in error messages (e.g. in `ProxySubjectTest`, `AclAuthorizerServiceTest`) must update from `io.kroxylicious.proxy.authentication.Unique` to `io.kroxylicious.authentication.Unique`. +- **`@Unique` FQN in error message assertions**: tests that assert on the fully-qualified annotation name in error messages (e.g. in `ProxySubjectTest`, `AclAuthorizerServiceTest`) must update from `io.kroxylicious.proxy.authentication.Unique` to `io.kroxylicious.identity.Unique`. ### Modules not affected @@ -206,17 +205,17 @@ This proposal includes the following breaking changes: | Change | Kind | Impact | |--------|------|--------| -| `io.kroxylicious.proxy.authentication.Principal` deleted | Binary-incompatible | Code compiled against the old interface must be recompiled. All implementations change to `io.kroxylicious.authentication.Principal`. | -| `@Unique` moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.authentication` | Binary-incompatible | Code compiled against the old annotation must be recompiled. No known external consumers. | +| `io.kroxylicious.proxy.authentication.Principal` deleted | Binary-incompatible | Code compiled against the old interface must be recompiled. All implementations change to `io.kroxylicious.identity.Principal`. | +| `@Unique` moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity` | Binary-incompatible | Code compiled against the old annotation must be recompiled. No known external consumers. | | `Subject` renamed to `ProxySubject` | Binary- and source-incompatible | All code referencing the concrete `Subject` type by name must be updated. | -| `ProxySubject` constructor and `PrincipalFactory` return type change from proxy `Principal` to authentication-api `Principal` | Binary-incompatible | Callers must be recompiled. The type bound is strictly widened so no source changes are needed at call sites. | +| `ProxySubject` constructor and `PrincipalFactory` return type change from proxy `Principal` to identity-api `Principal` | Binary-incompatible | Callers must be recompiled. The type bound is strictly widened so no source changes are needed at call sites. | | `FilterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` | Binary-incompatible | Filter implementations and callers must be recompiled. Source-incompatible for implementations that declare the return type explicitly. | | `RouterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` | Binary-incompatible | Router implementations and callers must be recompiled. Source-incompatible for implementations that declare the return type explicitly. | | `FilterContext.clientSaslAuthenticationSuccess()` parameter changes from `Subject` to `ProxySubject` | Binary-incompatible | Filter implementations calling or implementing this method must be recompiled. | | `TransportSubjectBuilder.buildTransportSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | Transport subject builder implementations must be recompiled. | | `SaslSubjectBuilder.buildSaslSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | SASL subject builder implementations must be recompiled. | -| `Authorizer.authorize()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.authentication.Subject` (the new interface) | Binary- and source-incompatible | `Authorizer` implementations must be recompiled and must update one import. Two implementations exist in the codebase; the fix is mechanical. | -| `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.authentication.Subject` (the new interface) | Binary- and source-incompatible | Code creating or deconstructing `AuthorizeResult` instances must be recompiled. The record's canonical constructor and `subject()` accessor change type. Source fix is mechanical (change one import). | +| `Authorizer.authorize()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface) | Binary- and source-incompatible | `Authorizer` implementations must be recompiled and must update one import. Two implementations exist in the codebase; the fix is mechanical. | +| `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface) | Binary- and source-incompatible | Code creating or deconstructing `AuthorizeResult` instances must be recompiled. The record's canonical constructor and `subject()` accessor change type. Source fix is mechanical (change one import). | All other changes (adding a new module, adding dependency allowlist entries) are source- and binary-compatible. @@ -230,23 +229,23 @@ The interface extraction approach avoids this entirely by using a new package. ### Include a concrete `Subject` implementation in the new module -Providing a ready-made `Subject` implementation (e.g. `DefaultSubject`) in `kroxylicious-authentication-api` was considered so that external consumers wouldn't need to write their own. +Providing a ready-made `Subject` implementation (e.g. `DefaultSubject`) in `kroxylicious-identity-api` was considered so that external consumers wouldn't need to write their own. This was deferred because: - The interface is a functional interface (`Subject` has one abstract method), so anonymous implementations are trivial: `() -> myPrincipalSet`. - External consumers building production systems will likely want their own implementation with domain-specific validation or immutability guarantees. - Adding a concrete implementation can be done later without breaking changes if there is demand. -### Generalise the existing `Subject` record and ship it in `authentication-api` +### Generalise the existing `Subject` record and ship it in `identity-api` -Rather than introducing a minimal `Subject` interface and renaming the existing concrete record to `ProxySubject`, an alternative would be to remove the `User`-principal validation from the existing `Subject` record and move it directly into `kroxylicious-authentication-api` as a general-purpose concrete type. +Rather than introducing a minimal `Subject` interface and renaming the existing concrete record to `ProxySubject`, an alternative would be to remove the `User`-principal validation from the existing `Subject` record and move it directly into `kroxylicious-identity-api` as a general-purpose concrete type. This would avoid the rename (no `ProxySubject`, no source-incompatible change for downstream code referencing `Subject` by name) and give external consumers a ready-made implementation. This was rejected for several reasons: 1. **Split package or forced package rename for all consumers.** If the record kept its `io.kroxylicious.proxy.authentication` package, two Maven artifacts would contribute types to the same package — a split package that blocks JPMS and confuses tooling. - If it moved to `io.kroxylicious.authentication`, every downstream reference would still need updating (the same source-incompatible cost as the rename to `ProxySubject`), but with the additional confusion of a type called `Subject` silently losing its proxy-specific validation. + If it moved to `io.kroxylicious.identity`, every downstream reference would still need updating (the same source-incompatible cost as the rename to `ProxySubject`), but with the additional confusion of a type called `Subject` silently losing its proxy-specific validation. 2. **The `User` validation is load-bearing within the proxy.** The proxy's authentication pipeline relies on non-anonymous subjects containing exactly one `User` principal. @@ -254,7 +253,7 @@ This was rejected for several reasons: The `ProxySubject` approach keeps this invariant co-located with the type, where it is easiest to maintain and hardest to forget. 3. **It conflates two concerns with different stability requirements.** - The authentication-api module is intended to be a stable, minimal dependency for external consumers. + The identity-api module is intended to be a stable, minimal dependency for external consumers. The concrete `Subject` record in `kroxylicious-api` carries proxy-specific behaviour (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`, `User` validation) that may evolve with the proxy. Shipping a concrete implementation in the stable module locks in that behaviour and constrains future changes. The interface approach decouples the contract (what external consumers depend on) from the implementation (what the proxy needs). From 70b6af854a482bedc120265b8b1326dcba159853 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 10 Jul 2026 14:59:28 +0100 Subject: [PATCH 4/9] Refactored the Subject interface to contain convenience menthods Assisted-by: Claude Opus 4.6 Signed-off-by: Thomas Cooper --- proposals/119-auth-api-refactor.md | 94 ++++++++++++++++++------------ 1 file changed, 56 insertions(+), 38 deletions(-) diff --git a/proposals/119-auth-api-refactor.md b/proposals/119-auth-api-refactor.md index f0d12121..201bb687 100644 --- a/proposals/119-auth-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -64,8 +64,12 @@ Given the demonstrated external demand and the narrow usage pattern, the migrati ## Proposal -Introduce a new module, `kroxylicious-identity-api`, containing minimal interfaces for `Subject` and `Principal`, plus the `@Unique` annotation (which is used for marking `Principals` which should have only one instance per subject). -The existing concrete `Subject` record in `kroxylicious-api` is renamed to `ProxySubject` and implements the new `Subject` interface, and `kroxylicious-authorizer-api` switches its dependency from `kroxylicious-api` to the new module. +Introduce a new module, `kroxylicious-identity-api`, containing the `Subject` and `Principal` interfaces and the `@Unique` annotation. +The `Subject` interface defines one abstract method (`principals()`) and provides default convenience methods (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) and a static factory (`anonymous()`), giving any implementation full subject-querying capability without additional dependencies. + +The existing concrete `Subject` record in `kroxylicious-api` is renamed to `ProxySubject` and implements the new `Subject` interface. +API surfaces that only consume subjects (`FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, `FilterContext.clientSaslAuthenticationSuccess()`) change their type to the `Subject` interface, while subject-constructing APIs (`TransportSubjectBuilder`, `SaslSubjectBuilder`) retain `ProxySubject`. +`kroxylicious-authorizer-api` switches its dependency from `kroxylicious-api` to the new module. ### New module: `kroxylicious-identity-api` @@ -74,12 +78,23 @@ A new module in the `io.kroxylicious.identity` package containing three types: `Principal` interface: a single method, `String name()`. The Javadoc contract (implementations must override `hashCode`/`equals` based on class and name) is carried forward from the existing `Principal`. -`Subject` interface: a single method, `Set principals()`. -This matches the original design from [proposal 009][prop-9], where the diversity of `Principal` implementations is handled by `Principal` being an interface. +`Subject` interface: one abstract method, `Set principals()`, plus default convenience methods and a static factory: + +- `

Optional

uniquePrincipalOfType(Class

uniquePrincipalType)`: default method that returns the unique principal of a given `@Unique`-annotated type, or empty. Throws `IllegalArgumentException` if the type is not annotated with `@Unique`. +- `

Set

allPrincipalsOfType(Class

principalType)`: default method that returns all principals matching a given type. +- `boolean isAnonymous()`: default method that returns `true` when the principals set is empty. +- `static Subject anonymous()`: static factory method that returns a `Subject` with no principals. + +The interface retains the `@FunctionalInterface` annotation: default and static methods do not count toward the single abstract method requirement, so trivial implementations remain possible (`() -> myPrincipalSet`). + +Placing these methods on the interface is motivated by evidence from [Apicurio Registry's prototype][apicurio-pr], which copied the Kroxylicious `Subject` and re-implemented equivalent convenience methods (`principalOfType`, `isAnonymous`, `anonymous()`) for their `GrantsAuthorizer`. +This demonstrates that these methods are useful for working with `Subject`, not proxy-specific behaviour. +Providing them as defaults means external consumers get full subject-querying capability without writing boilerplate or duplicating logic. `@Unique` annotation: `@Retention(RUNTIME)`, `@Target(TYPE)`. -Marks `Principal` implementations that should have at most one instance in a `Subject`. -This annotation is moved from the main `kroxylicious-api`, as external users of this API may also want to enforce this invariant. +Marks `Principal` implementations that should have at most one instance in a `Subject`. +This annotation is moved from the main `kroxylicious-api` because the `uniquePrincipalOfType` default method on `Subject` directly depends on it: the method checks `@Unique` at runtime to validate that the requested principal type supports the "at most one" invariant. +Co-locating the annotation with the method that enforces it keeps the module self-contained. If `@Unique` remained in `kroxylicious-api`, the identity module would depend on the proxy module, defeating the purpose of the extraction. The module has no compile-scope dependencies beyond `spotbugs-annotations` (provided scope, for package-level null-safety annotations). This means the transitive dependency tree for consumers of the authorizer API, which imports from this new module, becomes: @@ -111,18 +126,18 @@ They represent general identity concepts that any project can use. The file is renamed from `Subject.java` to `ProxySubject.java`, the test from `SubjectTest.java` to `ProxySubjectTest.java`, and all references are updated: `Subject.anonymous()` becomes `ProxySubject.anonymous()`, `new Subject(...)` becomes `new ProxySubject(...)`, etc. This is a binary and source incompatible change for all code referencing the concrete type by name. -- Because `ProxySubject` is used by several interfaces in `kroxylicious-api`, the following method signatures also change: - - `FilterContext.clientSaslAuthenticationSuccess(String, Subject)` to `FilterContext.clientSaslAuthenticationSuccess(String, ProxySubject)` - - `FilterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` - - `RouterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` - - `TransportSubjectBuilder.buildTransportSubject(Context)` return type changes from `CompletionStage` to `CompletionStage` - - `SaslSubjectBuilder.buildSaslSubject(Context)` return type changes from `CompletionStage` to `CompletionStage` +- Because `Subject` is an interface with default convenience methods, API surfaces that only *consume* subjects use the interface type directly, while surfaces that *construct* subjects retain `ProxySubject` to preserve the `User`-principal validation in its constructor. The following method signatures change: + - `FilterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` (the existing concrete record) to `io.kroxylicious.identity.Subject` (the new interface). Source code retains the name `Subject` — only the import changes. + - `RouterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. + - `FilterContext.clientSaslAuthenticationSuccess(String, Subject)` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. + - `TransportSubjectBuilder.buildTransportSubject(Context)` return type changes from `CompletionStage` to `CompletionStage` — these construct subjects and need the concrete type's `User` validation. + - `SaslSubjectBuilder.buildSaslSubject(Context)` return type changes from `CompletionStage` to `CompletionStage`. - These are all binary-incompatible changes. + These are all binary-incompatible changes. For the consuming APIs (`FilterContext`, `RouterContext`), the source fix is a single import change — the type name `Subject` is preserved. - The `@Unique` annotation is moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity`. - The old annotation is deleted. - This is a binary-incompatible change: code compiled against the old annotation will not see it on types annotated with the new one. + The existing annotation is deleted. + This is a binary-incompatible change: code compiled against the existing annotation will not see it on types annotated with the new one. This is mitigated by: - The project being at version 0.x (pre-1.0 API stability). - A `japicmp` exclusion documenting the intentional removal. @@ -135,11 +150,11 @@ They represent general identity concepts that any project can use. - `` entries for: the removed `Principal` class, the removed `Unique` annotation, the removed `Subject` class (renamed to `ProxySubject`), the changed `PrincipalFactory#newPrincipal` return type, and the changed method signatures in `TransportSubjectBuilder#buildTransportSubject`, `SaslSubjectBuilder#buildSaslSubject`, `FilterContext#clientSaslAuthenticationSuccess`, `FilterContext#authenticatedSubject`, and `RouterContext#authenticatedSubject`. - An `` entry for `io.kroxylicious.proxy.authentication.Principal`, because `japicmp` cannot resolve old bytecode signatures that reference the deleted class without this. -- The concrete `ProxySubject` record retains all its existing behaviour, including the `User`-principal validation in its constructor, and its `uniquePrincipalOfType`, `allPrincipalsOfType`, and `isAnonymous` methods. +- The concrete `ProxySubject` record retains the `User`-principal validation and the `@Unique` cardinality check in its compact constructor. It inherits the `uniquePrincipalOfType`, `allPrincipalsOfType`, and `isAnonymous` default methods from the `Subject` interface. `ProxySubject.anonymous()` is retained as a static factory that returns a `ProxySubject` (with the constructor's validation invariants), while `Subject.anonymous()` is a separate static factory returning a lightweight anonymous `Subject` without proxy-specific validation. ### Changes to `kroxylicious-authorizer-api` -- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the old concrete record, now renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface). +- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the existing concrete record, renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface). - The module's dependency on `kroxylicious-api` is replaced with a dependency on `kroxylicious-identity-api`. A test-scope dependency on `kroxylicious-api` is retained for tests that construct concrete `ProxySubject` instances. @@ -150,20 +165,22 @@ They represent general identity concepts that any project can use. ### Changes to downstream modules -Downstream changes follow two patterns: +Downstream changes follow three patterns: -- **Rename**: all code referencing the concrete `Subject` type updates to `ProxySubject` — variable declarations, constructor calls, method signatures, test assertions (including `toString()` output and `Mockito.any()` matchers). -- **Re-import**: code referencing `Principal` or `@Unique` updates imports from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity`. +- **Re-import (Subject)**: code that references `Subject` in consuming positions (filter implementations reading `FilterContext.authenticatedSubject()`, router implementations reading `RouterContext.authenticatedSubject()`, authorizer implementations receiving a `Subject` parameter) updates the import from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. The type name in source code remains `Subject` — only the import statement changes. +- **Rename**: code that *constructs* subjects updates from `new Subject(...)` to `new ProxySubject(...)` and from `Subject.anonymous()` to `ProxySubject.anonymous()`. This affects subject builders, test setup code, and the runtime authentication pipeline. +- **Re-import (Principal and @Unique)**: code referencing `Principal` or `@Unique` updates imports from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity`. -These changes are mechanical and affect most modules that interact with authentication types, including filters, runtime, integration tests, and microbenchmarks. +Because `FilterContext.authenticatedSubject()` and `RouterContext.authenticatedSubject()` return `Subject` (the interface) rather than `ProxySubject`, most filter and router code only needs a re-import rather than a type-name change. +This reduces the blast radius of the rename: only code that constructs `ProxySubject` instances needs to use the new name. #### Notable implications - **`kroxylicious-runtime` gains a direct dependency on `kroxylicious-identity-api`**: its source directly references the identity-api `Principal` type, so this must be an explicit compile-scope dependency. -- **Maven dependency analyzer false positive**: the compiler needs `kroxylicious-identity-api` on the classpath to resolve `ProxySubject`'s super-interface, but the bytecode doesn't directly reference identity-api types. +- **Maven dependency analyzer false positive**: some modules need `kroxylicious-identity-api` on the classpath to resolve `ProxySubject`'s super-interface or `Subject` return types, but their bytecode may not directly reference identity-api types. This triggers Maven's analyzer. - Three modules (`kroxylicious-filter-test-support`, `kroxylicious-oauthbearer-validation`, `kroxylicious-sasl-inspection`) add `kroxylicious-identity-api` as a compile-scope dependency with an `ignoredNonTestScopedDependencies` override to suppress the warning. + Because several API surfaces (e.g. `FilterContext.authenticatedSubject()`) directly reference `io.kroxylicious.identity.Subject`, some modules will have genuine bytecode references that make the dependency required rather than a false positive. The exact set of modules needing an `ignoredNonTestScopedDependencies` override will be determined during implementation. - **Dependency enforcer allowlists**: `kroxylicious-identity-api` must be added to `bannedDependencies` allowlists in the `kroxylicious-filters`, `kroxylicious-kms-providers`, and `kroxylicious-kubernetes` parent POMs. @@ -205,13 +222,13 @@ This proposal includes the following breaking changes: | Change | Kind | Impact | |--------|------|--------| -| `io.kroxylicious.proxy.authentication.Principal` deleted | Binary-incompatible | Code compiled against the old interface must be recompiled. All implementations change to `io.kroxylicious.identity.Principal`. | -| `@Unique` moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity` | Binary-incompatible | Code compiled against the old annotation must be recompiled. No known external consumers. | +| `io.kroxylicious.proxy.authentication.Principal` deleted | Binary-incompatible | Code compiled against the existing interface must be recompiled. All implementations change to `io.kroxylicious.identity.Principal`. | +| `@Unique` moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity` | Binary-incompatible | Code compiled against the existing annotation must be recompiled. No known external consumers. | | `Subject` renamed to `ProxySubject` | Binary- and source-incompatible | All code referencing the concrete `Subject` type by name must be updated. | | `ProxySubject` constructor and `PrincipalFactory` return type change from proxy `Principal` to identity-api `Principal` | Binary-incompatible | Callers must be recompiled. The type bound is strictly widened so no source changes are needed at call sites. | -| `FilterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` | Binary-incompatible | Filter implementations and callers must be recompiled. Source-incompatible for implementations that declare the return type explicitly. | -| `RouterContext.authenticatedSubject()` return type changes from `Subject` to `ProxySubject` | Binary-incompatible | Router implementations and callers must be recompiled. Source-incompatible for implementations that declare the return type explicitly. | -| `FilterContext.clientSaslAuthenticationSuccess()` parameter changes from `Subject` to `ProxySubject` | Binary-incompatible | Filter implementations calling or implementing this method must be recompiled. | +| `FilterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject` | Binary-incompatible | Filter implementations and callers must be recompiled. Source fix is mechanical: change one import. | +| `RouterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject` | Binary-incompatible | Router implementations and callers must be recompiled. Source fix is mechanical: change one import. | +| `FilterContext.clientSaslAuthenticationSuccess()` parameter changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject` | Binary-incompatible | Filter implementations calling or implementing this method must be recompiled. Source fix is mechanical: change one import. | | `TransportSubjectBuilder.buildTransportSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | Transport subject builder implementations must be recompiled. | | `SaslSubjectBuilder.buildSaslSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | SASL subject builder implementations must be recompiled. | | `Authorizer.authorize()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface) | Binary- and source-incompatible | `Authorizer` implementations must be recompiled and must update one import. Two implementations exist in the codebase; the fix is mechanical. | @@ -232,13 +249,13 @@ The interface extraction approach avoids this entirely by using a new package. Providing a ready-made `Subject` implementation (e.g. `DefaultSubject`) in `kroxylicious-identity-api` was considered so that external consumers wouldn't need to write their own. This was deferred because: -- The interface is a functional interface (`Subject` has one abstract method), so anonymous implementations are trivial: `() -> myPrincipalSet`. +- The interface is a functional interface (`Subject` has one abstract method), so anonymous implementations are trivial: `() -> myPrincipalSet`. The default methods (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) and static factory (`Subject.anonymous()`) provide the most commonly needed querying behaviour to any implementation, reducing the incentive for a concrete type. - External consumers building production systems will likely want their own implementation with domain-specific validation or immutability guarantees. - Adding a concrete implementation can be done later without breaking changes if there is demand. ### Generalise the existing `Subject` record and ship it in `identity-api` -Rather than introducing a minimal `Subject` interface and renaming the existing concrete record to `ProxySubject`, an alternative would be to remove the `User`-principal validation from the existing `Subject` record and move it directly into `kroxylicious-identity-api` as a general-purpose concrete type. +Rather than introducing a `Subject` interface (with default convenience methods) and renaming the existing concrete record to `ProxySubject`, an alternative would be to remove the `User`-principal validation from the existing `Subject` record and move it directly into `kroxylicious-identity-api` as a general-purpose concrete type. This would avoid the rename (no `ProxySubject`, no source-incompatible change for downstream code referencing `Subject` by name) and give external consumers a ready-made implementation. This was rejected for several reasons: @@ -253,14 +270,15 @@ This was rejected for several reasons: The `ProxySubject` approach keeps this invariant co-located with the type, where it is easiest to maintain and hardest to forget. 3. **It conflates two concerns with different stability requirements.** - The identity-api module is intended to be a stable, minimal dependency for external consumers. - The concrete `Subject` record in `kroxylicious-api` carries proxy-specific behaviour (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`, `User` validation) that may evolve with the proxy. - Shipping a concrete implementation in the stable module locks in that behaviour and constrains future changes. - The interface approach decouples the contract (what external consumers depend on) from the implementation (what the proxy needs). - -4. **External consumers gain little.** - The `Subject` interface is a functional interface with a single `principals()` method, so external consumers can implement it trivially (`() -> myPrincipalSet`). - A generalized concrete record adds convenience, but at the cost of the issues above. + The identity-api module is intended to be a stable dependency for external consumers. + While the convenience methods (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) are default methods on the `Subject` interface, the proxy-specific `User`-principal validation and construction logic remain on `ProxySubject` in `kroxylicious-api`, where they may evolve with the proxy. + Shipping a concrete implementation with proxy-specific validation in the stable module would lock in that behaviour and constrain future changes. + The interface approach decouples the identity contract (what external consumers depend on, including standard querying behaviour via defaults) from the proxy-specific implementation (construction-time validation rules). + +4. **External consumers gain little beyond what the interface already provides.** + The `Subject` interface is a functional interface, so external consumers can implement it trivially (`() -> myPrincipalSet`), and the default methods provide the querying convenience (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) that any implementation needs. + A generalized concrete record in the stable module would add construction convenience at the cost of the issues above. If demand for a concrete implementation materialises, it can be added later without breaking changes — the interface approach keeps this option open. [prop-9]: https://github.com/kroxylicious/design/blob/main/proposals/009-authorizer.md +[apicurio-pr]: https://github.com/Apicurio/apicurio-registry/pull/7829 From 5b7235fc1d96e21ff3a4773443ccebbfdc286b70 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Fri, 31 Jul 2026 15:27:10 +0100 Subject: [PATCH 5/9] Switch to using a deprecation with bridge interface approach * Update to use the deprecation of the existing API with bridge interface which allows old implementation to contiue working until version 1.0. * Update the details of the various method signiture changes required. Assisted-By: Claude Opus 4.6 (1M context) Signed-off-by: Thomas Cooper --- proposals/119-auth-api-refactor.md | 241 ++++++++++++++++------------- 1 file changed, 133 insertions(+), 108 deletions(-) diff --git a/proposals/119-auth-api-refactor.md b/proposals/119-auth-api-refactor.md index 201bb687..664acb8d 100644 --- a/proposals/119-auth-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -3,7 +3,8 @@ The Kroxylicious authorizer API provides a general-purpose abstraction for access control decisions, deliberately designed to be agnostic of specific `Principal` and `ResourceType` implementations. However, the API currently depends on `kroxylicious-api`, which transitively pulls in Kafka client libraries, Jackson, and compression codecs. This makes it less appealing for non-Kroxylicious projects to reuse. -This proposal extracts the `Subject` and `Principal` concepts into a new lightweight module, `kroxylicious-identity-api`, so that `kroxylicious-authorizer-api` can be consumed independently of `kroxylicious-api` module. +This proposal extracts identity concepts into a new lightweight module, `kroxylicious-identity-api`, containing a `Principal` interface, a `Subject` record, a deprecated-at-birth `Identity` interface (to aid in the migration to the new module) and a `@SingularPrincipal` annotation. +The existing types in `kroxylicious-api` will be deprecated and gain super-types from the new module, enabling a phased migration where only the `Authorizer` API breaks immediately while `FilterContext` and other consuming APIs remain unchanged until version 1.0. ## Current situation @@ -45,7 +46,6 @@ Any non-Kroxylicious project that wants to implement or consume the `Authorizer` As detailed in [Current situation](#current-situation) section, the authorizer API's usage of `Subject` is narrow: implementations only call `principals()`, `name()`, and `getClass()`. Yet this narrow usage forces consumers to accept `kafka-clients`, `jackson-annotations`, and compression codec libraries as transitive dependencies. -The relative cost of importing the dependency does not match the value consumed. ### Authentication concepts are misplaced in the module hierarchy @@ -60,41 +60,57 @@ The project's pre-1.0 status does not justify the change on its own. It should b However, the 0.x window does significantly reduce the cost of making a change that is justified on its own merits. The authorizer API was introduced relatively recently, so external adoption is likely to be minimal. Post-1.0, this same change would require deprecation cycles, compatibility shims, and migration documentation. -Given the demonstrated external demand and the narrow usage pattern, the migration cost is justified now in a way that would be harder to justify later. +Given the demonstrated external demand and the narrow usage pattern, the migration cost seems justified now in a way that would be harder to justify later. ## Proposal -Introduce a new module, `kroxylicious-identity-api`, containing the `Subject` and `Principal` interfaces and the `@Unique` annotation. -The `Subject` interface defines one abstract method (`principals()`) and provides default convenience methods (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) and a static factory (`anonymous()`), giving any implementation full subject-querying capability without additional dependencies. +Introduce a new module, `kroxylicious-identity-api`, containing four types: a `Principal` interface, a `@SingularPrincipal` annotation, a deprecated-at-birth `Identity` bridge interface, and a `Subject` record. -The existing concrete `Subject` record in `kroxylicious-api` is renamed to `ProxySubject` and implements the new `Subject` interface. -API surfaces that only consume subjects (`FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, `FilterContext.clientSaslAuthenticationSuccess()`) change their type to the `Subject` interface, while subject-constructing APIs (`TransportSubjectBuilder`, `SaslSubjectBuilder`) retain `ProxySubject`. -`kroxylicious-authorizer-api` switches its dependency from `kroxylicious-api` to the new module. +The existing `Subject` record and `Principal` interface in `kroxylicious-api` will remain. +However, the existing `Principal` gains `extends io.kroxylicious.identity.Principal`, the existing `Subject` gains `implements io.kroxylicious.identity.Identity`, and both will be deprecated along with the existing `@Unique` annotation. + +`kroxylicious-authorizer-api` switches its dependency from `kroxylicious-api` to the new module and its method parameters change to use the `Identity` bridge interface. +This is the only immediately breaking change. + +API surfaces in `kroxylicious-api` that consume subjects (`FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, `FilterContext.clientSaslAuthenticationSuccess()`) are unchanged because the existing `Subject` record now implements `Identity` and flows into `Authorizer.authorize(Identity)` without conversion. +Subject-constructing APIs (`TransportSubjectBuilder`, `SaslSubjectBuilder`) are also unchanged. + +At 1.0, the deprecated bridge types are removed and all APIs migrate to using the new `Subject` record directly. ### New module: `kroxylicious-identity-api` -A new module in the `io.kroxylicious.identity` package containing three types: +We will create a new module in the `io.kroxylicious.identity` package containing four types: `Principal` interface: a single method, `String name()`. The Javadoc contract (implementations must override `hashCode`/`equals` based on class and name) is carried forward from the existing `Principal`. -`Subject` interface: one abstract method, `Set principals()`, plus default convenience methods and a static factory: +`@SingularPrincipal` annotation: `@Retention(RUNTIME)`, `@Target(TYPE)`. +This is a renamed version of the existing `@Unique` annotation, with a name that more clearly describes its purpose: marking `Principal` implementations that should have at most one instance in a subject. +This annotation is co-located in the identity module because the `uniquePrincipalOfType` default method on `Identity` will directly depend on it: the method will check `@SingularPrincipal` at runtime to validate that the requested principal type supports the "at most one" invariant. + +`Identity` interface: a deprecated-at-birth bridge interface with one abstract method, `Set principals()`, plus default convenience methods and a static factory: -- `

Optional

uniquePrincipalOfType(Class

uniquePrincipalType)`: default method that returns the unique principal of a given `@Unique`-annotated type, or empty. Throws `IllegalArgumentException` if the type is not annotated with `@Unique`. +- `

Optional

uniquePrincipalOfType(Class

uniquePrincipalType)`: default method that returns the unique principal of a given `@SingularPrincipal`-annotated type, or empty. Throws `IllegalArgumentException` if the type is not annotated with `@SingularPrincipal`. - `

Set

allPrincipalsOfType(Class

principalType)`: default method that returns all principals matching a given type. - `boolean isAnonymous()`: default method that returns `true` when the principals set is empty. -- `static Subject anonymous()`: static factory method that returns a `Subject` with no principals. +- `static Identity anonymous()`: static factory method that returns an `Identity` with no principals, backed by a lightweight private implementation. -The interface retains the `@FunctionalInterface` annotation: default and static methods do not count toward the single abstract method requirement, so trivial implementations remain possible (`() -> myPrincipalSet`). +The `Identity` interface is annotated `@Deprecated` from its introduction. +It exists solely as a bridge type so that both the existing `Subject` record (in `kroxylicious-api`) and the new `Subject` record (in this module) can be passed to `Authorizer.authorize()`. +The wildcard return type on `principals()` (`Set`) is necessary so the existing `Subject` record's `Set` accessor satisfies the interface through covariant return types, given that the existing `Principal` gains `extends io.kroxylicious.identity.Principal`. +The `Identity` interface will be removed at 1.0. -Placing these methods on the interface is motivated by evidence from [Apicurio Registry's prototype][apicurio-pr], which copied the Kroxylicious `Subject` and re-implemented equivalent convenience methods (`principalOfType`, `isAnonymous`, `anonymous()`) for their `GrantsAuthorizer`. -This demonstrates that these methods are useful for working with `Subject`, not proxy-specific behaviour. -Providing them as defaults means external consumers get full subject-querying capability without writing boilerplate or duplicating logic. +Placing the convenience methods on `Identity` is motivated by evidence from [Apicurio Registry's prototype][apicurio-pr], which copied the Kroxylicious `Subject` and re-implemented equivalent convenience methods (`principalOfType`, `isAnonymous`, `anonymous()`) for their `GrantsAuthorizer`. +This demonstrates that these methods are useful for working with subjects, not proxy-specific behaviour. +Providing them as defaults means all implementations of `Identity`, including both the existing and new `Subject` types, get full subject-querying capability without writing boilerplate or duplicating logic. -`@Unique` annotation: `@Retention(RUNTIME)`, `@Target(TYPE)`. -Marks `Principal` implementations that should have at most one instance in a `Subject`. -This annotation is moved from the main `kroxylicious-api` because the `uniquePrincipalOfType` default method on `Subject` directly depends on it: the method checks `@Unique` at runtime to validate that the requested principal type supports the "at most one" invariant. -Co-locating the annotation with the method that enforces it keeps the module self-contained. If `@Unique` remained in `kroxylicious-api`, the identity module would depend on the proxy module, defeating the purpose of the extraction. +`Subject` record: a concrete record implementing the `Identity` interface. +Its constructor validates `@SingularPrincipal` uniqueness: if a `Principal` implementation is annotated with `@SingularPrincipal`, the constructor rejects any principal set containing more than one instance of that type. +This is the intended final type for all consumers of the identity API. +External consumers such as Apicurio should target this type directly. + +The `Subject` record has its own `static Subject anonymous()` factory method that returns a `Subject` with no principals. +This is separate from `Identity.anonymous()` because static methods on interfaces are not inherited in Java — when `Identity` is removed at 1.0, `Subject.anonymous()` must already exist for code that has migrated to the new type. The module has no compile-scope dependencies beyond `spotbugs-annotations` (provided scope, for package-level null-safety annotations). This means the transitive dependency tree for consumers of the authorizer API, which imports from this new module, becomes: @@ -117,74 +133,63 @@ They represent general identity concepts that any project can use. ### Changes to existing types in `kroxylicious-api` -- `io.kroxylicious.proxy.authentication.Principal` is **deleted**. - This is a binary-incompatible change. - -- `io.kroxylicious.proxy.authentication.Subject` is **renamed** to `io.kroxylicious.proxy.authentication.ProxySubject` and adds `implements io.kroxylicious.identity.Subject`. - The rename avoids ambiguity between the interface (`io.kroxylicious.identity.Subject`) and the concrete record when both are in scope. - Its `Set principals` component and method type bounds change from the deleted proxy `Principal` to `io.kroxylicious.identity.Principal`. - The file is renamed from `Subject.java` to `ProxySubject.java`, the test from `SubjectTest.java` to `ProxySubjectTest.java`, and all references are updated: `Subject.anonymous()` becomes `ProxySubject.anonymous()`, `new Subject(...)` becomes `new ProxySubject(...)`, etc. - This is a binary and source incompatible change for all code referencing the concrete type by name. - -- Because `Subject` is an interface with default convenience methods, API surfaces that only *consume* subjects use the interface type directly, while surfaces that *construct* subjects retain `ProxySubject` to preserve the `User`-principal validation in its constructor. The following method signatures change: - - `FilterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` (the existing concrete record) to `io.kroxylicious.identity.Subject` (the new interface). Source code retains the name `Subject` — only the import changes. - - `RouterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. - - `FilterContext.clientSaslAuthenticationSuccess(String, Subject)` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. - - `TransportSubjectBuilder.buildTransportSubject(Context)` return type changes from `CompletionStage` to `CompletionStage` — these construct subjects and need the concrete type's `User` validation. - - `SaslSubjectBuilder.buildSaslSubject(Context)` return type changes from `CompletionStage` to `CompletionStage`. - - These are all binary-incompatible changes. For the consuming APIs (`FilterContext`, `RouterContext`), the source fix is a single import change — the type name `Subject` is preserved. +- `io.kroxylicious.proxy.authentication.Principal` gains `extends io.kroxylicious.identity.Principal`. + This is both source- and binary-compatible: the existing `Principal` already declares `String name()`, matching the new super-interface's single method. + The existing `Principal` is deprecated. -- The `@Unique` annotation is moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity`. - The existing annotation is deleted. - This is a binary-incompatible change: code compiled against the existing annotation will not see it on types annotated with the new one. - This is mitigated by: - - The project being at version 0.x (pre-1.0 API stability). - - A `japicmp` exclusion documenting the intentional removal. - - The `@Unique` annotation having no known external consumers. +- `io.kroxylicious.proxy.authentication.Subject` gains `implements io.kroxylicious.identity.Identity`. + Adding the super-interface is binary-compatible, and the existing `principals()` accessor satisfies `Identity`'s `Set principals()` through covariant return types. + However, the existing `Subject`'s convenience methods (`uniquePrincipalOfType`, `allPrincipalsOfType`) must have their type parameter bounds widened from `

` to `

`. + Without this change, the existing methods and the `Identity` default methods would have the same erasure but different type parameter bounds, producing a name clash compilation error (JLS §8.4.8.1 requires identical bounds for a valid override; a subtype relationship between bounds is not sufficient). + Widening the bounds is both source- and binary-compatible for callers: the erased method signature is unchanged, and any type argument that satisfied the narrower bound also satisfies the wider one. + `isAnonymous()` has no type parameters and overrides the `Identity` default cleanly. + The existing `Subject` is deprecated. + Its constructor will check both `@SingularPrincipal` and `@Unique` for the cardinality invariant, so that principals annotated with either annotation are validated during the transition period. + This avoids silently dropping enforcement for any external `Principal` implementations still annotated with `@Unique`. + The dual check is removed at 1.0 along with the rest of the deprecated `Subject` record; the new `Subject` in `kroxylicious-identity-api` only checks `@SingularPrincipal`. + The existing `Subject` also retains its `User`-principal validation. -- `User` and other types annotated with `@Unique` update their import to the new annotation. - `User`, `PrincipalFactory`, and test types (`FakeUniquePrincipal`, `FakeMultiplePrincipal`) add an explicit `import io.kroxylicious.identity.Principal` since the same-package type no longer exists. +- `io.kroxylicious.proxy.authentication.Unique` is deprecated. -- The `japicmp` configuration is updated with: - - `` entries for: the removed `Principal` class, the removed `Unique` annotation, the removed `Subject` class (renamed to `ProxySubject`), the changed `PrincipalFactory#newPrincipal` return type, and the changed method signatures in `TransportSubjectBuilder#buildTransportSubject`, `SaslSubjectBuilder#buildSaslSubject`, `FilterContext#clientSaslAuthenticationSuccess`, `FilterContext#authenticatedSubject`, and `RouterContext#authenticatedSubject`. - - An `` entry for `io.kroxylicious.proxy.authentication.Principal`, because `japicmp` cannot resolve old bytecode signatures that reference the deleted class without this. +- `User` and other types annotated with `@Unique` switch to the new `@SingularPrincipal` annotation from `io.kroxylicious.identity`. -- The concrete `ProxySubject` record retains the `User`-principal validation and the `@Unique` cardinality check in its compact constructor. It inherits the `uniquePrincipalOfType`, `allPrincipalsOfType`, and `isAnonymous` default methods from the `Subject` interface. `ProxySubject.anonymous()` is retained as a static factory that returns a `ProxySubject` (with the constructor's validation invariants), while `Subject.anonymous()` is a separate static factory returning a lightweight anonymous `Subject` without proxy-specific validation. +- API surfaces in `kroxylicious-api` that consume or produce the existing `Subject` (`FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, `FilterContext.clientSaslAuthenticationSuccess()`, `TransportSubjectBuilder.buildTransportSubject()`, and `SaslSubjectBuilder.buildSaslSubject()`) will remain unchanged. + Their signatures continue to use the existing `io.kroxylicious.proxy.authentication.Subject` record. + Since that record now implements `Identity`, returned subjects flow into `Authorizer.authorize(Identity)` without conversion, preserving existing contracts for filter and router plugin authors. ### Changes to `kroxylicious-authorizer-api` -- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the existing concrete record, renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface). +- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the existing concrete record) to `io.kroxylicious.identity.Identity` (the new bridge interface). - The module's dependency on `kroxylicious-api` is replaced with a dependency on `kroxylicious-identity-api`. - A test-scope dependency on `kroxylicious-api` is retained for tests that construct concrete `ProxySubject` instances. + A test-scope dependency on `kroxylicious-identity-api` is sufficient for tests that construct `io.kroxylicious.identity.Subject` record instances. -- This is a source-breaking change for `Authorizer` implementations: they must update the parameter type in their `authorize()` method from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. - The fix is mechanical (change one import). - Callers of `authorize()` (such as `AuthorizationFilter`) are unaffected because the concrete `ProxySubject` implements the interface. +- This is a binary-incompatible change. + However, it is source-compatible for callers: since the existing `Subject` record now implements `Identity`, code that passes an existing `Subject` to `authorize()` compiles without changes. + `Authorizer` implementations must update the parameter type in their `authorize()` method from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Identity`. + The fix is mechanical (change one import and the parameter type). ### Changes to downstream modules -Downstream changes follow three patterns: +The only downstream modules that need source changes are those containing `Authorizer` implementations, which must update their `authorize()` method signature from the existing `Subject` to `Identity`. +Two such implementations exist in the codebase. -- **Re-import (Subject)**: code that references `Subject` in consuming positions (filter implementations reading `FilterContext.authenticatedSubject()`, router implementations reading `RouterContext.authenticatedSubject()`, authorizer implementations receiving a `Subject` parameter) updates the import from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. The type name in source code remains `Subject` — only the import statement changes. -- **Rename**: code that *constructs* subjects updates from `new Subject(...)` to `new ProxySubject(...)` and from `Subject.anonymous()` to `ProxySubject.anonymous()`. This affects subject builders, test setup code, and the runtime authentication pipeline. -- **Re-import (Principal and @Unique)**: code referencing `Principal` or `@Unique` updates imports from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity`. +Additionally, the existing `Subject`'s `uniquePrincipalOfType()` method checks `isAnnotationPresent(Unique.class)` at runtime. +Since `User` switches from `@Unique` to `@SingularPrincipal`, this method must be updated to accept both annotations during the transition period, mirroring the dual check described for the constructor. -Because `FilterContext.authenticatedSubject()` and `RouterContext.authenticatedSubject()` return `Subject` (the interface) rather than `ProxySubject`, most filter and router code only needs a re-import rather than a type-name change. -This reduces the blast radius of the rename: only code that constructs `ProxySubject` instances needs to use the new name. +`PrincipalEntityNameMapper` in `kroxylicious-entity-isolation` also checks `isAnnotationPresent(Unique.class)` in its constructor to validate that a principal type supports the "at most one" invariant. +This check must be updated to accept both `@Unique` and `@SingularPrincipal`, otherwise passing `User` (which now carries `@SingularPrincipal`) as the principal type would be rejected. -#### Notable implications - -- **`kroxylicious-runtime` gains a direct dependency on `kroxylicious-identity-api`**: its source directly references the identity-api `Principal` type, so this must be an explicit compile-scope dependency. +All other modules (including those that use `FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, or any other API surface in `kroxylicious-api`) require no immediate source changes. +These modules will see deprecation warnings for usages of the existing `Subject`, `Principal`, and `@Unique`, encouraging migration to the new types, but compilation is unaffected. -- **Maven dependency analyzer false positive**: some modules need `kroxylicious-identity-api` on the classpath to resolve `ProxySubject`'s super-interface or `Subject` return types, but their bytecode may not directly reference identity-api types. - This triggers Maven's analyzer. - Because several API surfaces (e.g. `FilterContext.authenticatedSubject()`) directly reference `io.kroxylicious.identity.Subject`, some modules will have genuine bytecode references that make the dependency required rather than a false positive. The exact set of modules needing an `ignoredNonTestScopedDependencies` override will be determined during implementation. +#### Notable implications - **Dependency enforcer allowlists**: `kroxylicious-identity-api` must be added to `bannedDependencies` allowlists in the `kroxylicious-filters`, `kroxylicious-kms-providers`, and `kroxylicious-kubernetes` parent POMs. -- **`@Unique` FQN in error message assertions**: tests that assert on the fully-qualified annotation name in error messages (e.g. in `ProxySubjectTest`, `AclAuthorizerServiceTest`) must update from `io.kroxylicious.proxy.authentication.Unique` to `io.kroxylicious.identity.Unique`. +- **`PrincipalEntityNameMapper` dual annotation check**: `PrincipalEntityNameMapper` in `kroxylicious-entity-isolation` validates principal types at construction time using `isAnnotationPresent(Unique.class)`. + Since `User` now carries `@SingularPrincipal` instead of `@Unique`, this check must accept both annotations during the transition period. + The dual check is removed at 1.0 when `@Unique` is deleted. ### Modules not affected @@ -205,6 +210,9 @@ The following modules require no source or dependency changes: - `kroxylicious-openmessaging-benchmarks` - `kroxylicious-systemtests` +Additionally, all filter and router modules that only consume `Subject` through `FilterContext.authenticatedSubject()` or `RouterContext.authenticatedSubject()` are unaffected. +The exact set of affected modules beyond the `Authorizer` implementations will be confirmed during implementation. + ## Affected/not affected projects ### Affected @@ -222,63 +230,80 @@ This proposal includes the following breaking changes: | Change | Kind | Impact | |--------|------|--------| -| `io.kroxylicious.proxy.authentication.Principal` deleted | Binary-incompatible | Code compiled against the existing interface must be recompiled. All implementations change to `io.kroxylicious.identity.Principal`. | -| `@Unique` moved from `io.kroxylicious.proxy.authentication` to `io.kroxylicious.identity` | Binary-incompatible | Code compiled against the existing annotation must be recompiled. No known external consumers. | -| `Subject` renamed to `ProxySubject` | Binary- and source-incompatible | All code referencing the concrete `Subject` type by name must be updated. | -| `ProxySubject` constructor and `PrincipalFactory` return type change from proxy `Principal` to identity-api `Principal` | Binary-incompatible | Callers must be recompiled. The type bound is strictly widened so no source changes are needed at call sites. | -| `FilterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject` | Binary-incompatible | Filter implementations and callers must be recompiled. Source fix is mechanical: change one import. | -| `RouterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject` | Binary-incompatible | Router implementations and callers must be recompiled. Source fix is mechanical: change one import. | -| `FilterContext.clientSaslAuthenticationSuccess()` parameter changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject` | Binary-incompatible | Filter implementations calling or implementing this method must be recompiled. Source fix is mechanical: change one import. | -| `TransportSubjectBuilder.buildTransportSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | Transport subject builder implementations must be recompiled. | -| `SaslSubjectBuilder.buildSaslSubject()` return type changes from `CompletionStage` to `CompletionStage` | Binary-incompatible | SASL subject builder implementations must be recompiled. | -| `Authorizer.authorize()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface) | Binary- and source-incompatible | `Authorizer` implementations must be recompiled and must update one import. Two implementations exist in the codebase; the fix is mechanical. | -| `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.proxy.authentication.Subject` (now renamed to `ProxySubject`) to `io.kroxylicious.identity.Subject` (the new interface) | Binary- and source-incompatible | Code creating or deconstructing `AuthorizeResult` instances must be recompiled. The record's canonical constructor and `subject()` accessor change type. Source fix is mechanical (change one import). | - -All other changes (adding a new module, adding dependency allowlist entries) are source- and binary-compatible. +| `Authorizer.authorize()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Identity` | Binary- and source-incompatible | `Authorizer` implementations must update their method signature. Two implementations exist in the codebase. Callers are unaffected because the existing `Subject` record implements `Identity`. | +| `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Identity` | Binary- and source-incompatible | Code creating or deconstructing `AuthorizeResult` instances must be recompiled. Source fix is mechanical (change one import). | +| `User` annotation changes from `@Unique` to `@SingularPrincipal` | Binary-incompatible | Code compiled against `@Unique` on `User` must be recompiled. No source changes required at call sites since the annotation is not referenced directly by consumers. | -## Rejected alternatives +The following changes are source- and binary-compatible: -### Extract concrete types into the new module +- Adding `extends io.kroxylicious.identity.Principal` to the existing `Principal` interface. +- Adding `implements io.kroxylicious.identity.Identity` to the existing `Subject` record, together with widening the type parameter bounds on its `uniquePrincipalOfType` and `allPrincipalsOfType` methods from `

` to `

` (required to avoid a name clash with the `Identity` default methods; see [Changes to existing types](#changes-to-existing-types-in-kroxylicious-api)). +- Introducing the new `kroxylicious-identity-api` module. -Moving the concrete `Subject` record, `Principal` interface, `User`, `Unique`, `PrincipalFactory`, `UserFactory`, and `SubjectBuildingException` into a new module while keeping the existing package name `io.kroxylicious.proxy.authentication` would create a split package: two Maven artifacts contributing types to the same Java package. -Split packages block JPMS adoption, confuse build tooling, and are considered bad practice. -The interface extraction approach avoids this entirely by using a new package. +The existing `Subject`, `Principal`, and `@Unique` are deprecated. +This generates compiler warnings but requires no immediate source changes. -### Include a concrete `Subject` implementation in the new module +## 1.0 cleanup -Providing a ready-made `Subject` implementation (e.g. `DefaultSubject`) in `kroxylicious-identity-api` was considered so that external consumers wouldn't need to write their own. -This was deferred because: +At 1.0, the deprecated bridge types are removed and all APIs migrate to the types in `kroxylicious-identity-api`: -- The interface is a functional interface (`Subject` has one abstract method), so anonymous implementations are trivial: `() -> myPrincipalSet`. The default methods (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) and static factory (`Subject.anonymous()`) provide the most commonly needed querying behaviour to any implementation, reducing the incentive for a concrete type. -- External consumers building production systems will likely want their own implementation with domain-specific validation or immutability guarantees. -- Adding a concrete implementation can be done later without breaking changes if there is demand. +- The deprecated `Identity` interface is removed from `kroxylicious-identity-api`. +- The deprecated `Subject` record, `Principal` interface, and `@Unique` annotation are removed from `kroxylicious-api`. +- `Authorizer.authorize()` parameter type changes from `io.kroxylicious.identity.Identity` to `io.kroxylicious.identity.Subject` (the record). +- `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.identity.Identity` to `io.kroxylicious.identity.Subject`. +- `FilterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. +- `RouterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. +- `FilterContext.clientSaslAuthenticationSuccess()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. +- `TransportSubjectBuilder.buildTransportSubject()` return type changes from `CompletionStage` to `CompletionStage`. +- `SaslSubjectBuilder.buildSaslSubject()` return type changes from `CompletionStage` to `CompletionStage`. + +By this point, the deprecated types will have been available for at least one release cycle, giving consumers time to migrate. +The deprecation warnings serve as documentation of the migration path. + +## Rejected alternatives + +### Extract concrete types into the new module + +Moving the concrete `Subject` record, `Principal` interface, `User`, `@Unique`, `PrincipalFactory`, `UserFactory`, and `SubjectBuildingException` into a new module while keeping the existing package name `io.kroxylicious.proxy.authentication` would create a split package: two Maven artifacts contributing types to the same Java package. +Split packages block JPMS adoption, confuse build tooling, and are considered bad practice. +The current approach avoids this entirely by using a new package (`io.kroxylicious.identity`) for the new types while keeping the existing types in their original package until they are removed at 1.0. ### Generalise the existing `Subject` record and ship it in `identity-api` -Rather than introducing a `Subject` interface (with default convenience methods) and renaming the existing concrete record to `ProxySubject`, an alternative would be to remove the `User`-principal validation from the existing `Subject` record and move it directly into `kroxylicious-identity-api` as a general-purpose concrete type. -This would avoid the rename (no `ProxySubject`, no source-incompatible change for downstream code referencing `Subject` by name) and give external consumers a ready-made implementation. +Rather than introducing a new `Subject` record in `kroxylicious-identity-api` and keeping the existing record in `kroxylicious-api` (deprecated), an alternative would be to remove the `User`-principal validation from the existing `Subject` record and move it directly into `kroxylicious-identity-api` as a general-purpose concrete type. This was rejected for several reasons: 1. **Split package or forced package rename for all consumers.** If the record kept its `io.kroxylicious.proxy.authentication` package, two Maven artifacts would contribute types to the same package — a split package that blocks JPMS and confuses tooling. - If it moved to `io.kroxylicious.identity`, every downstream reference would still need updating (the same source-incompatible cost as the rename to `ProxySubject`), but with the additional confusion of a type called `Subject` silently losing its proxy-specific validation. + If it moved to `io.kroxylicious.identity`, every downstream reference would need updating immediately, with no deprecation path. 2. **The `User` validation is load-bearing within the proxy.** The proxy's authentication pipeline relies on non-anonymous subjects containing exactly one `User` principal. - Removing this validation from the concrete type would push enforcement responsibility to every call site that constructs a subject within the proxy, creating a class of bugs where subjects without a `User` principal silently propagate through the pipeline. - The `ProxySubject` approach keeps this invariant co-located with the type, where it is easiest to maintain and hardest to forget. - -3. **It conflates two concerns with different stability requirements.** - The identity-api module is intended to be a stable dependency for external consumers. - While the convenience methods (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) are default methods on the `Subject` interface, the proxy-specific `User`-principal validation and construction logic remain on `ProxySubject` in `kroxylicious-api`, where they may evolve with the proxy. - Shipping a concrete implementation with proxy-specific validation in the stable module would lock in that behaviour and constrain future changes. - The interface approach decouples the identity contract (what external consumers depend on, including standard querying behaviour via defaults) from the proxy-specific implementation (construction-time validation rules). - -4. **External consumers gain little beyond what the interface already provides.** - The `Subject` interface is a functional interface, so external consumers can implement it trivially (`() -> myPrincipalSet`), and the default methods provide the querying convenience (`uniquePrincipalOfType`, `allPrincipalsOfType`, `isAnonymous`) that any implementation needs. - A generalized concrete record in the stable module would add construction convenience at the cost of the issues above. - If demand for a concrete implementation materialises, it can be added later without breaking changes — the interface approach keeps this option open. + Removing this validation from the existing record would push enforcement responsibility to every call site that constructs a subject within the proxy, creating a class of bugs where subjects without a `User` principal silently propagate through the pipeline. + The existing `Subject` retains this invariant while the new `Subject` record in `kroxylicious-identity-api` uses the more general `@SingularPrincipal` validation, which is appropriate for external consumers with different principal types. + +### Subject-as-interface with `ProxySubject` rename + +The original version of this proposal used a `Subject` interface (rather than a record) as the primary type in `kroxylicious-identity-api`, renamed the existing `Subject` record to `ProxySubject`, and changed the return types of `FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, and other API surfaces to use the new interface. +All breaking changes were applied in a single release with no deprecation period. + +This was rejected for several reasons: + +1. **Larger blast radius.** + Changing `FilterContext.authenticatedSubject()` and `RouterContext.authenticatedSubject()` to return a new interface type would break every filter and router plugin that references the return type. + `FilterContext` has real external adoption, and this is a higher bar than the authorizer API. + +2. **`ProxySubject` rename forces source-incompatible changes across all downstream modules.** + Every module that constructs a `Subject` would need to change to `new ProxySubject(...)` and `ProxySubject.anonymous()`, increasing the migration cost and the size of the diff. + +3. **An interface is harder to make safe for authorizer implementations.** + Making `Subject` an interface requires every consumer to provide their own implementation, making it harder to enforce `equals`/`hashCode`/`toString` contracts and `@SingularPrincipal` uniqueness invariants. + A concrete record with constructor validation ensures that all `Authorizer` implementations receive subjects with consistent, tested behaviour — particularly important given that [broken access control is #1 on the OWASP top ten](https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/). + +4. **The phased deprecation approach achieves the same end state with lower immediate migration cost.** + The `Identity` bridge interface is deprecated at birth and carries the compatibility cost for one release cycle. + The end state (a concrete `Subject` record as the primary type, no bridge interface) is the same, but the migration path avoids breaking widely-adopted API surfaces until 1.0. [prop-9]: https://github.com/kroxylicious/design/blob/main/proposals/009-authorizer.md [apicurio-pr]: https://github.com/Apicurio/apicurio-registry/pull/7829 From 63ad9928e300e666416dc4304dc763ddf651ca80 Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Wed, 5 Aug 2026 10:53:10 +0100 Subject: [PATCH 6/9] Restructure proposal per TomBentley's review feedback - Rewrote proposal section using a code format rather than long desciriptions - Split it into Phase 1 (initial changes) and Phase 2 (1.0 cleanup) - Split the compatibility section into each phase's impact subsections - Removed modules-not-affected list and affected/not-affected projects - Expanded split package rationale with JPMS explanation - Add @Deprecated(since, forRemoval) to code examples - Deferred the User @Unique annotation change to @SingularPrincipal until 1.0 - Named kroxylicious-authorizer-acl explicitly in impact table - Fix various wording and formatting issues Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Thomas Cooper --- proposals/119-auth-api-refactor.md | 361 ++++++++++++++--------------- 1 file changed, 175 insertions(+), 186 deletions(-) diff --git a/proposals/119-auth-api-refactor.md b/proposals/119-auth-api-refactor.md index 664acb8d..cd58ee33 100644 --- a/proposals/119-auth-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -1,15 +1,15 @@ # 119 - Making the Authorizer API standalone The Kroxylicious authorizer API provides a general-purpose abstraction for access control decisions, deliberately designed to be agnostic of specific `Principal` and `ResourceType` implementations. -However, the API currently depends on `kroxylicious-api`, which transitively pulls in Kafka client libraries, Jackson, and compression codecs. +However, the API currently depends on `kroxylicious-api`, which transitively pulls in Kafka client libraries, Jackson and compression codecs. This makes it less appealing for non-Kroxylicious projects to reuse. -This proposal extracts identity concepts into a new lightweight module, `kroxylicious-identity-api`, containing a `Principal` interface, a `Subject` record, a deprecated-at-birth `Identity` interface (to aid in the migration to the new module) and a `@SingularPrincipal` annotation. +This proposal introduces new identity types in a lightweight module, `kroxylicious-identity-api`, containing a `Principal` interface, a `Subject` record, a deprecated-at-birth `Identity` interface (to bridge the migration) and a `@SingularPrincipal` annotation. The existing types in `kroxylicious-api` will be deprecated and gain super-types from the new module, enabling a phased migration where only the `Authorizer` API breaks immediately while `FilterContext` and other consuming APIs remain unchanged until version 1.0. ## Current situation The `Authorizer` interface and `AuthorizeResult` record in `kroxylicious-authorizer-api` reference `io.kroxylicious.proxy.authentication.Subject`, which is defined in `kroxylicious-api`. -This means any project that wants to implement the `Authorizer` plugin interface must depend on `kroxylicious-api`, which transitively pulls in `kafka-clients`, `jackson-annotations`, and compression codec libraries (`zstd-jni`, `lz4-java`, `snappy-java`). +This means any project that wants to implement the `Authorizer` plugin interface must depend on `kroxylicious-api`, which transitively pulls in `kafka-clients`, `jackson-annotations` and compression codec libraries (`zstd-jni`, `lz4-java`, `snappy-java`). The authorizer API's actual usage of `Subject` is narrow. `Authorizer.authorize()` receives a `Subject` and passes it through to `AuthorizeResult`. @@ -44,8 +44,8 @@ Any non-Kroxylicious project that wants to implement or consume the `Authorizer` ### The dependency cost is disproportionate to what is actually used -As detailed in [Current situation](#current-situation) section, the authorizer API's usage of `Subject` is narrow: implementations only call `principals()`, `name()`, and `getClass()`. -Yet this narrow usage forces consumers to accept `kafka-clients`, `jackson-annotations`, and compression codec libraries as transitive dependencies. +As detailed in [Current situation](#current-situation) section, the authorizer API's usage of `Subject` is narrow: implementations only call `principals()`, `name()` and `getClass()`. +Yet this narrow usage forces consumers to accept `kafka-clients`, `jackson-annotations` and compression codec libraries as transitive dependencies. ### Authentication concepts are misplaced in the module hierarchy @@ -55,217 +55,208 @@ Their current placement in `kroxylicious-api` mixes identity types with proxy in ### The 0.x window reduces migration cost -The preceding sections establish the substantive case for this change: external reuse is blocked, the dependency cost is disproportionate, and authentication concepts are misplaced. -The project's pre-1.0 status does not justify the change on its own. It should be noted that this would be the project's first breaking change and that track record of compatibility has value. +The preceding sections establish the substantive case for this change: external reuse is blocked, the dependency cost is disproportionate and authentication concepts are misplaced. +The project's pre-1.0 status does not justify the change on its own. +It should be noted that this would be the project's first breaking change and that track record of compatibility has value. However, the 0.x window does significantly reduce the cost of making a change that is justified on its own merits. The authorizer API was introduced relatively recently, so external adoption is likely to be minimal. -Post-1.0, this same change would require deprecation cycles, compatibility shims, and migration documentation. +Post-1.0, this same change would require deprecation cycles, compatibility shims and migration documentation. Given the demonstrated external demand and the narrow usage pattern, the migration cost seems justified now in a way that would be harder to justify later. ## Proposal -Introduce a new module, `kroxylicious-identity-api`, containing four types: a `Principal` interface, a `@SingularPrincipal` annotation, a deprecated-at-birth `Identity` bridge interface, and a `Subject` record. - -The existing `Subject` record and `Principal` interface in `kroxylicious-api` will remain. -However, the existing `Principal` gains `extends io.kroxylicious.identity.Principal`, the existing `Subject` gains `implements io.kroxylicious.identity.Identity`, and both will be deprecated along with the existing `@Unique` annotation. - -`kroxylicious-authorizer-api` switches its dependency from `kroxylicious-api` to the new module and its method parameters change to use the `Identity` bridge interface. -This is the only immediately breaking change. - -API surfaces in `kroxylicious-api` that consume subjects (`FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, `FilterContext.clientSaslAuthenticationSuccess()`) are unchanged because the existing `Subject` record now implements `Identity` and flows into `Authorizer.authorize(Identity)` without conversion. -Subject-constructing APIs (`TransportSubjectBuilder`, `SaslSubjectBuilder`) are also unchanged. - -At 1.0, the deprecated bridge types are removed and all APIs migrate to using the new `Subject` record directly. - -### New module: `kroxylicious-identity-api` - -We will create a new module in the `io.kroxylicious.identity` package containing four types: - -`Principal` interface: a single method, `String name()`. -The Javadoc contract (implementations must override `hashCode`/`equals` based on class and name) is carried forward from the existing `Principal`. - -`@SingularPrincipal` annotation: `@Retention(RUNTIME)`, `@Target(TYPE)`. -This is a renamed version of the existing `@Unique` annotation, with a name that more clearly describes its purpose: marking `Principal` implementations that should have at most one instance in a subject. -This annotation is co-located in the identity module because the `uniquePrincipalOfType` default method on `Identity` will directly depend on it: the method will check `@SingularPrincipal` at runtime to validate that the requested principal type supports the "at most one" invariant. - -`Identity` interface: a deprecated-at-birth bridge interface with one abstract method, `Set principals()`, plus default convenience methods and a static factory: - -- `

Optional

uniquePrincipalOfType(Class

uniquePrincipalType)`: default method that returns the unique principal of a given `@SingularPrincipal`-annotated type, or empty. Throws `IllegalArgumentException` if the type is not annotated with `@SingularPrincipal`. -- `

Set

allPrincipalsOfType(Class

principalType)`: default method that returns all principals matching a given type. -- `boolean isAnonymous()`: default method that returns `true` when the principals set is empty. -- `static Identity anonymous()`: static factory method that returns an `Identity` with no principals, backed by a lightweight private implementation. - -The `Identity` interface is annotated `@Deprecated` from its introduction. -It exists solely as a bridge type so that both the existing `Subject` record (in `kroxylicious-api`) and the new `Subject` record (in this module) can be passed to `Authorizer.authorize()`. -The wildcard return type on `principals()` (`Set`) is necessary so the existing `Subject` record's `Set` accessor satisfies the interface through covariant return types, given that the existing `Principal` gains `extends io.kroxylicious.identity.Principal`. -The `Identity` interface will be removed at 1.0. - -Placing the convenience methods on `Identity` is motivated by evidence from [Apicurio Registry's prototype][apicurio-pr], which copied the Kroxylicious `Subject` and re-implemented equivalent convenience methods (`principalOfType`, `isAnonymous`, `anonymous()`) for their `GrantsAuthorizer`. -This demonstrates that these methods are useful for working with subjects, not proxy-specific behaviour. -Providing them as defaults means all implementations of `Identity`, including both the existing and new `Subject` types, get full subject-querying capability without writing boilerplate or duplicating logic. - -`Subject` record: a concrete record implementing the `Identity` interface. -Its constructor validates `@SingularPrincipal` uniqueness: if a `Principal` implementation is annotated with `@SingularPrincipal`, the constructor rejects any principal set containing more than one instance of that type. -This is the intended final type for all consumers of the identity API. -External consumers such as Apicurio should target this type directly. - -The `Subject` record has its own `static Subject anonymous()` factory method that returns a `Subject` with no principals. -This is separate from `Identity.anonymous()` because static methods on interfaces are not inherited in Java — when `Identity` is removed at 1.0, `Subject.anonymous()` must already exist for code that has migrated to the new type. - -The module has no compile-scope dependencies beyond `spotbugs-annotations` (provided scope, for package-level null-safety annotations). -This means the transitive dependency tree for consumers of the authorizer API, which imports from this new module, becomes: - -``` -kroxylicious-authorizer-api -├── kroxylicious-identity-api (compile) -│ └── spotbugs-annotations (provided) -└── ... -``` +To decouple the authorizer API from `kroxylicious-api`, we introduce new `Principal`, `Subject` and related types in a new zero-dependency module (`kroxylicious-identity-api`), in a new package (`io.kroxylicious.identity`). +The existing types in `kroxylicious-api` are retained and deprecated. +They are part of public APIs (`FilterContext`, `RouterContext`, `TransportSubjectBuilder`, etc.), so removing them outright would break multiple plugin surfaces simultaneously. +Instead, we introduce a deprecated-at-birth bridge interface, `Identity`, that both the existing and new `Subject` types implement. +This allows the authorizer API to switch to `Identity` immediately, breaking only `Authorizer` implementations (of which there are two in the codebase), while other public APIs continue returning the existing `Subject` unchanged. +This gives consumers of the API time to migrate. +When Kroxylicious 1.0 is released, the bridge types will be removed and all APIs migrated to the new types. ### Use of a new package -The new types live in `io.kroxylicious.identity`, not `io.kroxylicious.proxy.authentication`. -This avoids a split package, a situation where two Maven artifacts contribute types to the same Java package. -Split packages block JPMS adoption and can confuse IDEs and build tools even on the classpath. - -The package name also signals that these types are not proxy-specific. -They represent general identity concepts that any project can use. - -### Changes to existing types in `kroxylicious-api` - -- `io.kroxylicious.proxy.authentication.Principal` gains `extends io.kroxylicious.identity.Principal`. - This is both source- and binary-compatible: the existing `Principal` already declares `String name()`, matching the new super-interface's single method. - The existing `Principal` is deprecated. - -- `io.kroxylicious.proxy.authentication.Subject` gains `implements io.kroxylicious.identity.Identity`. - Adding the super-interface is binary-compatible, and the existing `principals()` accessor satisfies `Identity`'s `Set principals()` through covariant return types. - However, the existing `Subject`'s convenience methods (`uniquePrincipalOfType`, `allPrincipalsOfType`) must have their type parameter bounds widened from `

` to `

`. - Without this change, the existing methods and the `Identity` default methods would have the same erasure but different type parameter bounds, producing a name clash compilation error (JLS §8.4.8.1 requires identical bounds for a valid override; a subtype relationship between bounds is not sufficient). - Widening the bounds is both source- and binary-compatible for callers: the erased method signature is unchanged, and any type argument that satisfied the narrower bound also satisfies the wider one. - `isAnonymous()` has no type parameters and overrides the `Identity` default cleanly. - The existing `Subject` is deprecated. - Its constructor will check both `@SingularPrincipal` and `@Unique` for the cardinality invariant, so that principals annotated with either annotation are validated during the transition period. - This avoids silently dropping enforcement for any external `Principal` implementations still annotated with `@Unique`. - The dual check is removed at 1.0 along with the rest of the deprecated `Subject` record; the new `Subject` in `kroxylicious-identity-api` only checks `@SingularPrincipal`. - The existing `Subject` also retains its `User`-principal validation. - -- `io.kroxylicious.proxy.authentication.Unique` is deprecated. - -- `User` and other types annotated with `@Unique` switch to the new `@SingularPrincipal` annotation from `io.kroxylicious.identity`. - -- API surfaces in `kroxylicious-api` that consume or produce the existing `Subject` (`FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, `FilterContext.clientSaslAuthenticationSuccess()`, `TransportSubjectBuilder.buildTransportSubject()`, and `SaslSubjectBuilder.buildSaslSubject()`) will remain unchanged. - Their signatures continue to use the existing `io.kroxylicious.proxy.authentication.Subject` record. - Since that record now implements `Identity`, returned subjects flow into `Authorizer.authorize(Identity)` without conversion, preserving existing contracts for filter and router plugin authors. +The new types live in a new `io.kroxylicious.identity` package, not the existing `io.kroxylicious.proxy.authentication`. -### Changes to `kroxylicious-authorizer-api` +If the new module used the existing package, both `kroxylicious-api` and `kroxylicious-identity-api` would contribute types to `io.kroxylicious.proxy.authentication`: +This would be a split package. +Split packages are incompatible with the [Java Platform Module System (JPMS)][jpms], Java's built-in module system which requires each package to belong to exactly one module. +They can also confuse IDEs and build tools even on the classpath. -- `Authorizer.authorize()` and `AuthorizeResult`'s `subject` component change their type from `io.kroxylicious.proxy.authentication.Subject` (the existing concrete record) to `io.kroxylicious.identity.Identity` (the new bridge interface). +Using a distinct package avoids this problem entirely. +The name `io.kroxylicious.identity` also signals that these types are not proxy-specific. +They represent general identity concepts that any project can use. -- The module's dependency on `kroxylicious-api` is replaced with a dependency on `kroxylicious-identity-api`. - A test-scope dependency on `kroxylicious-identity-api` is sufficient for tests that construct `io.kroxylicious.identity.Subject` record instances. +### Phase 1: Initial changes + +#### What changes + +New `kroxylicious-identity-api` module with package `io.kroxylicious.identity`: + +```java +package io.kroxylicious.identity; + +// Carried forward from io.kroxylicious.proxy.authentication.Principal +interface Principal { + String name(); + // Implementations must override hashCode/equals based on class and name +} + +// Replaces @Unique with a clearer name +@Retention(RUNTIME) @Target(TYPE) +@interface SingularPrincipal { } + +// Bridge interface: both the existing and new Subject implement this, +// allowing either to be passed to Authorizer.authorize(). +/** @deprecated Use {@link Subject} directly. Will be removed at 1.0. */ +@Deprecated(since = "0.x.0", forRemoval = true) +interface Identity { + Set principals(); + default

Optional

uniquePrincipalOfType(Class

type) { /* checks @SingularPrincipal */ } + default

Set

allPrincipalsOfType(Class

type) { ... } + default boolean isAnonymous() { ... } + static Identity anonymous() { ... } +} + +// Intended final type for all consumers. +// Has its own anonymous() factory because static methods on interfaces +// are not inherited. Subject.anonymous() must exist before Identity is removed in 1.0. +record Subject(Set principals) implements Identity { + Subject { /* validates @SingularPrincipal uniqueness */ } + static Subject anonymous() { ... } +} +``` -- This is a binary-incompatible change. - However, it is source-compatible for callers: since the existing `Subject` record now implements `Identity`, code that passes an existing `Subject` to `authorize()` compiles without changes. - `Authorizer` implementations must update the parameter type in their `authorize()` method from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Identity`. - The fix is mechanical (change one import and the parameter type). +In `kroxylicious-api` the existing types are deprecated in place and the existing `Subject` now implements the new `Identity` interface: + +```java +package io.kroxylicious.proxy.authentication; + +/** @deprecated Use {@link io.kroxylicious.identity.Principal} instead. */ +@Deprecated(since = "0.x.0", forRemoval = true) +interface Principal extends io.kroxylicious.identity.Principal { + // Adding extends is source- and binary-compatible (name() already declared) +} + +/** @deprecated Use {@link io.kroxylicious.identity.Subject} instead. */ +@Deprecated(since = "0.x.0", forRemoval = true) +record Subject(Set principals) implements Identity { + // These methods originally had bounds

. + // Identity's defaults have bounds

. + // Both erase to the same JVM signature, but Java requires identical + // bounds for a valid override (JLS §8.4.8.1), not just a subtype + // relationship. Without changing the bounds, compilation fails with + // a name clash. The change is safe: since old.Principal extends + // new.Principal, any type that satisfied the old bound also satisfies + // the new one, so callers are unaffected. + @Override

Optional

uniquePrincipalOfType(Class

type) { ... } + @Override

Set

allPrincipalsOfType(Class

type) { ... } +} + +/** @deprecated Use {@link io.kroxylicious.identity.SingularPrincipal} instead. */ +@Deprecated(since = "0.x.0", forRemoval = true) +@interface Unique { } +``` -### Changes to downstream modules +In `kroxylicious-authorizer-api` the dependency switches from `kroxylicious-api` to `kroxylicious-identity-api`: -The only downstream modules that need source changes are those containing `Authorizer` implementations, which must update their `authorize()` method signature from the existing `Subject` to `Identity`. -Two such implementations exist in the codebase. +```java +package io.kroxylicious.authorizer.service; -Additionally, the existing `Subject`'s `uniquePrincipalOfType()` method checks `isAnnotationPresent(Unique.class)` at runtime. -Since `User` switches from `@Unique` to `@SingularPrincipal`, this method must be updated to accept both annotations during the transition period, mirroring the dual check described for the constructor. +interface Authorizer { + CompletionStage authorize(Identity subject, List actions); + // ^^^^^^^^ was: Subject +} -`PrincipalEntityNameMapper` in `kroxylicious-entity-isolation` also checks `isAnnotationPresent(Unique.class)` in its constructor to validate that a principal type supports the "at most one" invariant. -This check must be updated to accept both `@Unique` and `@SingularPrincipal`, otherwise passing `User` (which now carries `@SingularPrincipal`) as the principal type would be rejected. +record AuthorizeResult( + Identity subject, // was: Subject + List allowed, + List denied) { ... } +``` -All other modules (including those that use `FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, or any other API surface in `kroxylicious-api`) require no immediate source changes. -These modules will see deprecation warnings for usages of the existing `Subject`, `Principal`, and `@Unique`, encouraging migration to the new types, but compilation is unaffected. +In `kroxylicious-authorizer-acl` and other `Authorizer` implementations: -#### Notable implications +```java +// Mechanical signature change: Subject to Identity +CompletionStage authorize(Identity subject, List actions) { ... } +``` -- **Dependency enforcer allowlists**: `kroxylicious-identity-api` must be added to `bannedDependencies` allowlists in the `kroxylicious-filters`, `kroxylicious-kms-providers`, and `kroxylicious-kubernetes` parent POMs. +#### Impact -- **`PrincipalEntityNameMapper` dual annotation check**: `PrincipalEntityNameMapper` in `kroxylicious-entity-isolation` validates principal types at construction time using `isAnnotationPresent(Unique.class)`. - Since `User` now carries `@SingularPrincipal` instead of `@Unique`, this check must accept both annotations during the transition period. - The dual check is removed at 1.0 when `@Unique` is deleted. +Breaking changes: -### Modules not affected +| Change | Kind | Who must act | +|--------------------------------------------------------------|---------------------|--------------| +| `Authorizer.authorize()` parameter: `Subject` to `Identity` | Binary-incompatible | `Authorizer` implementations must update method signature. Two exist in the codebase (`AclAuthorizer` in `kroxylicious-authorizer-acl`, `SimpleAuthorizer` test in `kroxylicious-authorization`), and no usages outside the project are known. Callers are unaffected as the existing `Subject` will now implement `Identity`. | +| `AuthorizeResult.subject` component: `Subject` to `Identity` | Binary-incompatible | Code creating or deconstructing `AuthorizeResult` must be recompiled. The source code fix is mechanical. | -The following modules require no source or dependency changes: +Compatible changes (no action required): -- `kroxylicious-annotations` -- `kroxylicious-app` -- `kroxylicious-certificate-test-support` -- `kroxylicious-docs` -- `kroxylicious-docs-tests` -- `kroxylicious-filter-archetype` -- `kroxylicious-integration-test-support` -- `kroxylicious-kafka-message-tools` -- `kroxylicious-kms` -- `kroxylicious-kms-test-support` -- `kroxylicious-kms-tls-support` -- `kroxylicious-krpc-plugin` -- `kroxylicious-openmessaging-benchmarks` -- `kroxylicious-systemtests` +- Adding `extends io.kroxylicious.identity.Principal` to the existing `Principal` interface +- Adding `implements Identity` to the existing `Subject` record and widening type parameter bounds +- Introducing `kroxylicious-identity-api` as a new module +- `kroxylicious-identity-api` must be added to `bannedDependencies` allowlists in relevant parent POMs -Additionally, all filter and router modules that only consume `Subject` through `FilterContext.authenticatedSubject()` or `RouterContext.authenticatedSubject()` are unaffected. -The exact set of affected modules beyond the `Authorizer` implementations will be confirmed during implementation. +All other modules (including those that use `FilterContext.authenticatedSubject()` or `RouterContext.authenticatedSubject()`) require no source changes. +These modules will see compile-time deprecation warnings for usages of the existing `Subject`, `Principal` and `@Unique`, visible to developers during builds but not to end users. -## Affected/not affected projects +### Phase 2: 1.0 cleanup -### Affected +#### What changes -- `kroxylicious`: the main repository; all changes are within this repo. +In `kroxylicious-identity-api`: -### Not affected +```java +package io.kroxylicious.identity; -- `kroxylicious-junit5-extension` -- `kroxylicious-operator` +// Identity interface: removed (bridge no longer needed) +``` -## Compatibility +In `kroxylicious-api`: -This proposal includes the following breaking changes: +```java +package io.kroxylicious.proxy.authentication; -| Change | Kind | Impact | -|--------|------|--------| -| `Authorizer.authorize()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Identity` | Binary- and source-incompatible | `Authorizer` implementations must update their method signature. Two implementations exist in the codebase. Callers are unaffected because the existing `Subject` record implements `Identity`. | -| `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Identity` | Binary- and source-incompatible | Code creating or deconstructing `AuthorizeResult` instances must be recompiled. Source fix is mechanical (change one import). | -| `User` annotation changes from `@Unique` to `@SingularPrincipal` | Binary-incompatible | Code compiled against `@Unique` on `User` must be recompiled. No source changes required at call sites since the annotation is not referenced directly by consumers. | +// Subject record: removed +// Principal interface: removed +// @Unique annotation: removed +// User switches from using @Unique to @SingularPrincipal +``` -The following changes are source- and binary-compatible: +In `kroxylicious-authorizer-api`: -- Adding `extends io.kroxylicious.identity.Principal` to the existing `Principal` interface. -- Adding `implements io.kroxylicious.identity.Identity` to the existing `Subject` record, together with widening the type parameter bounds on its `uniquePrincipalOfType` and `allPrincipalsOfType` methods from `

` to `

` (required to avoid a name clash with the `Identity` default methods; see [Changes to existing types](#changes-to-existing-types-in-kroxylicious-api)). -- Introducing the new `kroxylicious-identity-api` module. +```java +interface Authorizer { + CompletionStage authorize(Subject subject, List actions); + // ^^^^^^^ Identity changed to the new Subject +} -The existing `Subject`, `Principal`, and `@Unique` are deprecated. -This generates compiler warnings but requires no immediate source changes. +record AuthorizeResult( + Subject subject, // Identity changed to the new Subject + ...) { ... } +``` -## 1.0 cleanup +In `kroxylicious-api` (consuming APIs migrate to new types): -At 1.0, the deprecated bridge types are removed and all APIs migrate to the types in `kroxylicious-identity-api`: +```java +FilterContext.authenticatedSubject() // returns io.kroxylicious.identity.Subject +RouterContext.authenticatedSubject() // returns io.kroxylicious.identity.Subject +FilterContext.clientSaslAuthenticationSuccess() // accepts io.kroxylicious.identity.Subject +TransportSubjectBuilder.buildTransportSubject() // returns CompletionStage +SaslSubjectBuilder.buildSaslSubject() // returns CompletionStage +``` -- The deprecated `Identity` interface is removed from `kroxylicious-identity-api`. -- The deprecated `Subject` record, `Principal` interface, and `@Unique` annotation are removed from `kroxylicious-api`. -- `Authorizer.authorize()` parameter type changes from `io.kroxylicious.identity.Identity` to `io.kroxylicious.identity.Subject` (the record). -- `AuthorizeResult`'s `subject` component type changes from `io.kroxylicious.identity.Identity` to `io.kroxylicious.identity.Subject`. -- `FilterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. -- `RouterContext.authenticatedSubject()` return type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. -- `FilterContext.clientSaslAuthenticationSuccess()` parameter type changes from `io.kroxylicious.proxy.authentication.Subject` to `io.kroxylicious.identity.Subject`. -- `TransportSubjectBuilder.buildTransportSubject()` return type changes from `CompletionStage` to `CompletionStage`. -- `SaslSubjectBuilder.buildSaslSubject()` return type changes from `CompletionStage` to `CompletionStage`. +#### Impact +All types migrate to `io.kroxylicious.identity`. +Filter, router and authorizer plugin authors update imports. By this point, the deprecated types will have been available for at least one release cycle, giving consumers time to migrate. -The deprecation warnings serve as documentation of the migration path. ## Rejected alternatives ### Extract concrete types into the new module -Moving the concrete `Subject` record, `Principal` interface, `User`, `@Unique`, `PrincipalFactory`, `UserFactory`, and `SubjectBuildingException` into a new module while keeping the existing package name `io.kroxylicious.proxy.authentication` would create a split package: two Maven artifacts contributing types to the same Java package. -Split packages block JPMS adoption, confuse build tooling, and are considered bad practice. +Moving the concrete `Subject` record, `Principal` interface, `User`, `@Unique`, `PrincipalFactory`, `UserFactory` and `SubjectBuildingException` into a new module while keeping the existing package name `io.kroxylicious.proxy.authentication` would create a split package: two Maven artifacts contributing types to the same Java package. +Split packages block (JPMS)[jpms] adoption, confuse build tooling and are considered bad practice. The current approach avoids this entirely by using a new package (`io.kroxylicious.identity`) for the new types while keeping the existing types in their original package until they are removed at 1.0. ### Generalise the existing `Subject` record and ship it in `identity-api` @@ -274,36 +265,34 @@ Rather than introducing a new `Subject` record in `kroxylicious-identity-api` an This was rejected for several reasons: -1. **Split package or forced package rename for all consumers.** - If the record kept its `io.kroxylicious.proxy.authentication` package, two Maven artifacts would contribute types to the same package — a split package that blocks JPMS and confuses tooling. - If it moved to `io.kroxylicious.identity`, every downstream reference would need updating immediately, with no deprecation path. +1. If the record kept its `io.kroxylicious.proxy.authentication` package, two Maven artifacts would contribute types to the same package. + This is a split package that blocks JPMS and confuses tooling. + If it moved to `io.kroxylicious.identity`, every downstream reference would need updating immediately with no deprecation path. -2. **The `User` validation is load-bearing within the proxy.** - The proxy's authentication pipeline relies on non-anonymous subjects containing exactly one `User` principal. +2. The proxy's authentication pipeline relies on non-anonymous subjects containing exactly one `User` principal. Removing this validation from the existing record would push enforcement responsibility to every call site that constructs a subject within the proxy, creating a class of bugs where subjects without a `User` principal silently propagate through the pipeline. The existing `Subject` retains this invariant while the new `Subject` record in `kroxylicious-identity-api` uses the more general `@SingularPrincipal` validation, which is appropriate for external consumers with different principal types. ### Subject-as-interface with `ProxySubject` rename -The original version of this proposal used a `Subject` interface (rather than a record) as the primary type in `kroxylicious-identity-api`, renamed the existing `Subject` record to `ProxySubject`, and changed the return types of `FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()`, and other API surfaces to use the new interface. +The original version of this proposal used a `Subject` interface (rather than a record) as the primary type in `kroxylicious-identity-api`, renamed the existing `Subject` record to `ProxySubject`, and changed the return types of `FilterContext.authenticatedSubject()`, `RouterContext.authenticatedSubject()` and other API surfaces to use the new interface. All breaking changes were applied in a single release with no deprecation period. This was rejected for several reasons: -1. **Larger blast radius.** - Changing `FilterContext.authenticatedSubject()` and `RouterContext.authenticatedSubject()` to return a new interface type would break every filter and router plugin that references the return type. - `FilterContext` has real external adoption, and this is a higher bar than the authorizer API. +1. Changing `FilterContext.authenticatedSubject()` and `RouterContext.authenticatedSubject()` to return a new interface type would break every filter and router plugin that references the return type. + These are public APIs with a wider surface area than the authorizer API, so breaking them without a deprecation path is a higher bar. -2. **`ProxySubject` rename forces source-incompatible changes across all downstream modules.** - Every module that constructs a `Subject` would need to change to `new ProxySubject(...)` and `ProxySubject.anonymous()`, increasing the migration cost and the size of the diff. +2. Every module that constructs a `Subject` would need to change to `new ProxySubject(...)` and `ProxySubject.anonymous()`, increasing the migration cost and the size of the diff. -3. **An interface is harder to make safe for authorizer implementations.** - Making `Subject` an interface requires every consumer to provide their own implementation, making it harder to enforce `equals`/`hashCode`/`toString` contracts and `@SingularPrincipal` uniqueness invariants. - A concrete record with constructor validation ensures that all `Authorizer` implementations receive subjects with consistent, tested behaviour — particularly important given that [broken access control is #1 on the OWASP top ten](https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/). +3. Making `Subject` an interface requires every consumer to provide their own implementation, making it harder to enforce `equals`/`hashCode`/`toString` contracts and `@SingularPrincipal` uniqueness invariants. + A concrete record with constructor validation ensures that all `Authorizer` implementations receive subjects with consistent, tested behaviour. + This is particularly important given that [broken access control is #1 on the OWASP top ten](https://owasp.org/Top10/2025/A01_2025-Broken_Access_Control/). -4. **The phased deprecation approach achieves the same end state with lower immediate migration cost.** +4. The phased deprecation approach achieves the same end state with lower immediate migration cost. The `Identity` bridge interface is deprecated at birth and carries the compatibility cost for one release cycle. - The end state (a concrete `Subject` record as the primary type, no bridge interface) is the same, but the migration path avoids breaking widely-adopted API surfaces until 1.0. + The end state (a concrete `Subject` record as the primary type, no bridge interface) is the same, but the migration path avoids breaking public API surfaces until 1.0. [prop-9]: https://github.com/kroxylicious/design/blob/main/proposals/009-authorizer.md [apicurio-pr]: https://github.com/Apicurio/apicurio-registry/pull/7829 +[jpms]: https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/module/package-summary.html From 7b8ea30d058a63a276d8a128dcdb6f7fe76f929b Mon Sep 17 00:00:00 2001 From: Thomas Cooper Date: Wed, 5 Aug 2026 17:46:32 +0100 Subject: [PATCH 7/9] Address k-wall's review feedback - Add User and PrincipalFactory signature changes to Phase 2 - Add testing section for AuthorizeResult with both Subject types - Add OpenRewrite migration tooling note to Phase 2 impact Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Thomas Cooper --- proposals/119-auth-api-refactor.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/proposals/119-auth-api-refactor.md b/proposals/119-auth-api-refactor.md index cd58ee33..d0c4fc17 100644 --- a/proposals/119-auth-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -216,10 +216,14 @@ In `kroxylicious-api`: ```java package io.kroxylicious.proxy.authentication; -// Subject record: removed -// Principal interface: removed -// @Unique annotation: removed -// User switches from using @Unique to @SingularPrincipal +// Subject record, Principal interface, @Unique annotation: removed + +@SingularPrincipal // was: @Unique +record User(String name) implements io.kroxylicious.identity.Principal { } // was: Principal + +interface PrincipalFactory

{ // was: Principal + P newPrincipal(String name); +} ``` In `kroxylicious-authorizer-api`: @@ -250,6 +254,12 @@ SaslSubjectBuilder.buildSaslSubject() // returns CompletionStage Date: Thu, 6 Aug 2026 16:04:24 +0100 Subject: [PATCH 8/9] Added clarifying comments and text after Tom Bentley's review Signed-off-by: Thomas Cooper --- proposals/119-auth-api-refactor.md | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/proposals/119-auth-api-refactor.md b/proposals/119-auth-api-refactor.md index d0c4fc17..95e59965 100644 --- a/proposals/119-auth-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -111,7 +111,11 @@ interface Principal { @Deprecated(since = "0.x.0", forRemoval = true) interface Identity { Set principals(); - default

Optional

uniquePrincipalOfType(Class

type) { /* checks @SingularPrincipal */ } + default

Optional

uniquePrincipalOfType(Class

type) { + // Throws IllegalArgumentException if type does not carry @SingularPrincipal. + // Does NOT check the old @Unique annotation. + // Returns the single principal of the given type, or empty if none. + } default

Set

allPrincipalsOfType(Class

type) { ... } default boolean isAnonymous() { ... } static Identity anonymous() { ... } @@ -121,7 +125,10 @@ interface Identity { // Has its own anonymous() factory because static methods on interfaces // are not inherited. Subject.anonymous() must exist before Identity is removed in 1.0. record Subject(Set principals) implements Identity { - Subject { /* validates @SingularPrincipal uniqueness */ } + Subject { + // Validates that at most one principal of each @SingularPrincipal-annotated + // type is present. Throws IllegalArgumentException on violation. + } static Subject anonymous() { ... } } ``` @@ -140,7 +147,9 @@ interface Principal extends io.kroxylicious.identity.Principal { /** @deprecated Use {@link io.kroxylicious.identity.Subject} instead. */ @Deprecated(since = "0.x.0", forRemoval = true) record Subject(Set principals) implements Identity { - // These methods originally had bounds

. + // Constructor continues to validate @Unique (not the new @SingularPrincipal). + + // The methods below originally had bounds

. // Identity's defaults have bounds

. // Both erase to the same JVM signature, but Java requires identical // bounds for a valid override (JLS §8.4.8.1), not just a subtype @@ -148,6 +157,9 @@ record Subject(Set principals) implements Identity { // a name clash. The change is safe: since old.Principal extends // new.Principal, any type that satisfied the old bound also satisfies // the new one, so callers are unaffected. + + // Overrides still check @Unique (not the new @SingularPrincipal), + // matching the constructor's validation. @Override

Optional

uniquePrincipalOfType(Class

type) { ... } @Override

Set

allPrincipalsOfType(Class

type) { ... } } @@ -251,9 +263,12 @@ SaslSubjectBuilder.buildSaslSubject() // returns CompletionStage Date: Mon, 10 Aug 2026 16:49:54 +0100 Subject: [PATCH 9/9] Update proposal to cover validation of old and new principal uniqueness annotations - Add the new singular principal annotation as a meta-annotaion on the old Unique annotation so that the identity module doesn't need to import Unique. - Add shared tooling in the identity-api module to check for principal uniqueness that can be used by both the old and new Subject records. - Added notes on the other locations these shared checks will need to be invoked. Assisted-By: Claude Opus 4.6 (1M context) Signed-off-by: Thomas Cooper --- proposals/119-auth-api-refactor.md | 42 +++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/proposals/119-auth-api-refactor.md b/proposals/119-auth-api-refactor.md index 95e59965..857ebe71 100644 --- a/proposals/119-auth-api-refactor.md +++ b/proposals/119-auth-api-refactor.md @@ -105,6 +105,25 @@ interface Principal { @Retention(RUNTIME) @Target(TYPE) @interface SingularPrincipal { } +// Shared validation utility. Both the existing and new Subject delegate their +// constructor uniqueness checks here. Uses one-level meta-annotation scanning +// so that types annotated with @Unique (which carries @SingularPrincipal as a +// meta-annotation) are recognised without this module importing @Unique. +// Deprecated at birth: at 1.0, the meta-annotation scanning will no longer be +// needed and this validation should be inlined into Subject directly. +/** @deprecated Transitional utility. Will be removed at 1.0. */ +@Deprecated(since = "0.x.0", forRemoval = true) +final class SingularPrincipals { + static boolean isSingular(Class type) { + // Returns true if type is annotated with @SingularPrincipal directly, + // or if any of its annotations are themselves annotated with @SingularPrincipal. + } + static void validateUniqueness(Set principals) { + // Groups principals by class, throws IllegalArgumentException if any + // singular principal type has more than one instance. + } +} + // Bridge interface: both the existing and new Subject implement this, // allowing either to be passed to Authorizer.authorize(). /** @deprecated Use {@link Subject} directly. Will be removed at 1.0. */ @@ -112,8 +131,9 @@ interface Principal { interface Identity { Set principals(); default

Optional

uniquePrincipalOfType(Class

type) { - // Throws IllegalArgumentException if type does not carry @SingularPrincipal. - // Does NOT check the old @Unique annotation. + // Uses SingularPrincipals.isSingular() to accept types annotated with + // @SingularPrincipal directly or via meta-annotation (e.g. @Unique). + // Throws IllegalArgumentException if the type is not a singular principal type. // Returns the single principal of the given type, or empty if none. } default

Set

allPrincipalsOfType(Class

type) { ... } @@ -126,8 +146,8 @@ interface Identity { // are not inherited. Subject.anonymous() must exist before Identity is removed in 1.0. record Subject(Set principals) implements Identity { Subject { - // Validates that at most one principal of each @SingularPrincipal-annotated - // type is present. Throws IllegalArgumentException on violation. + // Delegates to SingularPrincipals.validateUniqueness() to validate that + // at most one principal of each singular type is present. } static Subject anonymous() { ... } } @@ -147,7 +167,9 @@ interface Principal extends io.kroxylicious.identity.Principal { /** @deprecated Use {@link io.kroxylicious.identity.Subject} instead. */ @Deprecated(since = "0.x.0", forRemoval = true) record Subject(Set principals) implements Identity { - // Constructor continues to validate @Unique (not the new @SingularPrincipal). + // Constructor delegates to SingularPrincipals.validateUniqueness(), + // the same shared utility used by the new Subject. + // Both @Unique and @SingularPrincipal are recognised via the utility. // The methods below originally had bounds

. // Identity's defaults have bounds

. @@ -158,14 +180,17 @@ record Subject(Set principals) implements Identity { // new.Principal, any type that satisfied the old bound also satisfies // the new one, so callers are unaffected. - // Overrides still check @Unique (not the new @SingularPrincipal), - // matching the constructor's validation. + // Override uses SingularPrincipals.isSingular(), accepting types + // annotated with either @Unique or @SingularPrincipal. @Override

Optional

uniquePrincipalOfType(Class

type) { ... } @Override

Set

allPrincipalsOfType(Class

type) { ... } } /** @deprecated Use {@link io.kroxylicious.identity.SingularPrincipal} instead. */ @Deprecated(since = "0.x.0", forRemoval = true) +@SingularPrincipal // Meta-annotation: allows the new module's SingularPrincipals + // utility to recognise @Unique-annotated types via a one-level + // meta-annotation scan, without importing @Unique. @interface Unique { } ``` @@ -207,6 +232,7 @@ Compatible changes (no action required): - Adding `implements Identity` to the existing `Subject` record and widening type parameter bounds - Introducing `kroxylicious-identity-api` as a new module - `kroxylicious-identity-api` must be added to `bannedDependencies` allowlists in relevant parent POMs +- `PrincipalEntityNameMapper` in the entity isolation filter switches from checking `@Unique` directly to using `SingularPrincipals.isSingular()`, widening acceptance to `@SingularPrincipal`-annotated types All other modules (including those that use `FilterContext.authenticatedSubject()` or `RouterContext.authenticatedSubject()`) require no source changes. These modules will see compile-time deprecation warnings for usages of the existing `Subject`, `Principal` and `@Unique`, visible to developers during builds but not to end users. @@ -221,6 +247,8 @@ In `kroxylicious-identity-api`: package io.kroxylicious.identity; // Identity interface: removed (bridge no longer needed) +// SingularPrincipals utility: removed (meta-annotation scanning no longer needed, +// uniqueness validation inlined into Subject) ``` In `kroxylicious-api`: