ACL & quota admin APIs
Per-API reference — see the API support matrix for the generated version table.
kaas has no ACL store of its own: ACLs live inline on each principal's
KafkaUser CR (spec.authorization.acls — see
Kubernetes integration). The three ACL
admin APIs translate the AdminClient's int8-enum wire shape into that CR shape
and delegate to the ACL CR writer
(crates/kaas-broker/src/acl_cr_writer.rs); the operator's reconcile then
rebuilds /data/__cluster/acls.json and every broker's ACL engine hot-reloads
it. Runtime edits to git-managed KafkaUser CRs will show up as ArgoCD drift
until the next sync — the intentional trade for letting the admin protocol
reach the canonical store. Without a writer wired (dev mode), DescribeAcls
returns an empty set and CreateAcls/DeleteAcls report per-entry success
without persisting anything.
One cross-cutting honesty note: none of the three ACL handlers perform an
authorization check of their own — Apache requires DESCRIBE/ALTER on the
Cluster resource. On kaas, any client that clears the listener's
authentication gate can read and edit ACLs. The host field of a binding is
stored and round-tripped verbatim but ignored by ACL evaluation.
DescribeAcls
Lists ACL bindings matching a filter — kafka-acls.sh --list.
Versions: v0–v3 (flexible from v2).
Handling: the wire filter's ANY/UNKNOWN codes and null strings
collapse to wildcards; a MATCH pattern filter expands to literal + prefixed
per KIP-290; v0 (pre-KIP-290) pins the pattern filter to
literal so prefixed entries are never returned to a v0 client. The writer
lists every KafkaUser CR (skipping ones mid-deletion), expands each inline
ACL entry into one binding per operation, applies the filter, and the handler
folds the flat list back into Apache's per-resource shape — one resource row
per (type, name, pattern) with the matching ACLs inside. Filter errors
(resource types kaas can't express) answer INVALID_REQUEST (42); apiserver
failures answer UNKNOWN_SERVER_ERROR (-1).
Deviations from Apache 3.7:
- No
DESCRIBEauthorization on the Cluster resource (see the note above). - Resource types are limited to topic, group, cluster, and transactional-ID —
DELEGATION_TOKENandUSERfilters answerINVALID_REQUEST(42) (delegation tokens are a non-goal). - Dev mode answers an empty list rather than an error.
Source: crates/kaas-broker/src/handlers/acls.rs,
crates/kaas-broker/src/acl_cr_writer.rs,
crates/kaas-codec/src/api/acl_types.rs,
crates/kaas-codec/src/api/describe_acls.rs.
Verified by: scripts/kafka-acls.sh (list/add/list/remove round trip
against a temporary KafkaUser); enum-translation and grouping unit tests in
crates/kaas-broker/src/handlers/acls.rs; filter-matching unit tests in
crates/kaas-broker/src/acl_cr_writer.rs.
CreateAcls
Adds ACL bindings — kafka-acls.sh --add.
Versions: v0–v3 (flexible from v2).
Handling: per binding, the wire enums are validated (ANY/UNKNOWN
codes, and resource types kaas can't express, answer INVALID_REQUEST (42));
v0 bindings get literal pattern semantics. The principal must be of the form
User:<name> and a KafkaUser CR with that name must already exist —
kaas never auto-creates CRs from a runtime ACL write; both failures answer
INVALID_REQUEST (42). Creation is idempotent and coalescing: an existing
entry with the same resource, pattern, permission, and host absorbs the new
operation into its operations list (or no-ops when already present). The
write is a single Update with the read resourceVersion; a concurrent-edit
conflict surfaces as UNKNOWN_SERVER_ERROR (-1) and the AdminClient retries.
Deviations from Apache 3.7:
- Principals other than
User:(e.g.Group:) are rejected — kaas maps principals 1:1 ontoKafkaUserCRs. - An ACL for a principal with no KafkaUser CR is refused (
INVALID_REQUESTwithno KafkaUser CR for principal ...); Apache accepts ACLs for arbitrary principal strings. Create the KafkaUser first. - No
ALTERauthorization on the Cluster resource (see the page note). - Dev mode reports success without persisting.
Source: crates/kaas-broker/src/handlers/acls.rs,
crates/kaas-broker/src/acl_cr_writer.rs (create_acl),
crates/kaas-codec/src/api/create_acls.rs.
Verified by: scripts/kafka-acls.sh; unit tests in
crates/kaas-broker/src/handlers/acls.rs and
crates/kaas-broker/src/acl_cr_writer.rs (principal parsing, enum mapping);
end-to-end ACL enforcement in bins/kaas/tests/auth_smoke.rs
(acl_denies_unconfigured_topic).
DeleteAcls
Removes ACL bindings matching filters — kafka-acls.sh --remove.
Versions: v0–v3 (flexible from v2).
Handling: same filter translation as DescribeAcls (KIP-290 MATCH
expansion, v0 literal pinning). The writer walks every KafkaUser CR,
partitions each inline entry's operations into matched vs kept, rewrites the
CR when anything matched, and returns the flat list of removed bindings — one
per (entry, operation) pair — which the handler echoes as the per-filter
matching_acls. Entries whose operations are only partially matched are kept
with the remaining operations; entries emptied out are dropped. CRs
mid-deletion are skipped.
Deviations from Apache 3.7:
- No
ALTERauthorization on the Cluster resource (see the page note). - Dev mode reports success with zero matches, without touching anything.
Source: crates/kaas-broker/src/handlers/acls.rs,
crates/kaas-broker/src/acl_cr_writer.rs (delete_acls),
crates/kaas-codec/src/api/delete_acls.rs.
Verified by: scripts/kafka-acls.sh (remove-and-verify scenario);
filter-partition unit tests in crates/kaas-broker/src/acl_cr_writer.rs.
DescribeClientQuotas
Reads client quota entries (KIP-546) —
kafka-configs.sh --entity-type users --describe.
Versions: v0–v1 (flexible from v1).
Handling: authorizes DescribeConfigs on the Cluster resource (Apache's
mapping for quota describe; denial → CLUSTER_AUTHORIZATION_FAILED (31)).
kaas supports a single entity axis: user. An exact-match component describes
that user; ANY (or an empty component list) lists every user with a quota.
Values resolve runtime override first, CR-backed store second: overrides
installed by AlterClientQuotas shadow the quotas the
operator materialised into /data/__cluster/credentials.json from
KafkaUser.spec.quotas. Reported keys: producer_byte_rate,
consumer_byte_rate, request_percentage. With no quota enforcer wired
(auth disabled), the response is an empty success — indistinguishable on the
wire from "no quotas configured", mirroring Apache.
Deviations from Apache 3.7:
- Only the
userentity axis exists.client-id/ipcomponents, and theDEFAULTmatch type (<default>user entity), return an empty result rather than an error — kaas users are CR-instantiated, so there is no default entity. - Quota values are per-broker (KIP-13) — same
semantics as Apache, but worth restating: with N brokers the cluster-wide
ceiling is N × the reported value. The CR field names
(
producerMaxByteRatePerBroker) say so explicitly; the wire keys keep Apache's names.
Source: crates/kaas-broker/src/handlers/describe_client_quotas.rs,
crates/kaas-auth/src/quota.rs (describe_user_quota, list_user_quotas).
Verified by: scripts/kafka-configs.sh (quota scenarios 6–9); resolution-
order unit tests in crates/kaas-auth/src/quota.rs
(describe_user_quota_resolution_order).
AlterClientQuotas
Sets or removes client quota values (KIP-546) —
kafka-configs.sh --entity-type users --alter.
Versions: v0–v1 (flexible from v1).
Handling: authorizes AlterConfigs on the Cluster resource once for the
whole request (denial → per-entry CLUSTER_AUTHORIZATION_FAILED (31)). Each
entry must name exactly one user entity with an explicit name — anything
else answers INVALID_REQUEST (42). Ops merge onto the user's current
effective quotas with Apache semantics: a set replaces just the named key, a
remove drops just that key, unspecified keys are preserved. Supported keys are
producer_byte_rate, consumer_byte_rate, and request_percentage; an
unknown key answers INVALID_CONFIG (40). The merged result is installed as a
runtime override on the quota enforcer, live-updating any active token
bucket; a merge that empties every field clears the override, reverting the
user to the CR-backed value. With no enforcer wired (auth disabled) each entry
answers UNSUPPORTED_VERSION (35). validate_only skips the install.
Deviations from Apache 3.7:
- Alterations are not persisted. The override lives in the enforcer's
memory: it does not write back to the
KafkaUserCR, it is lost on broker restart, and it applies only on the broker that served the request — peers keep the store-backed value. Durable, cluster-wide quotas belong onKafkaUser.spec.quotas(see Kubernetes integration). Treat this API as a live-tuning knob, not a store. request_percentageis accepted, stored, and reported, but nothing enforces it — kaas throttles produce/fetch byte rates only, with no request-time CPU quota.- Entity axes other than a single named
userare rejected (INVALID_REQUEST), including the<default>entity.
Source: crates/kaas-broker/src/handlers/alter_client_quotas.rs,
crates/kaas-auth/src/quota.rs (set_user_quota).
Verified by: scripts/kafka-configs.sh (alter/describe/clear round trip);
set_user_quota_live_updates_existing_bucket and the debt-carry contention
test in crates/kaas-auth/src/quota.rs; enforcement end-to-end in
bins/kaas/tests/auth_smoke.rs (produce_exceeds_quota_returns_throttle).