Support custom ZooKeeper ACLs when creating a cluster - #224
Conversation
Add HelixAdmin#addCluster(String, boolean, List<ACL>) so callers can create a cluster whose root znode is owned by a specific ZooKeeper identity instead of the client default ACL. The overload is a default method that throws UnsupportedOperationException so existing HelixAdmin implementations keep compiling, and ZKHelixAdmin routes the two argument version through it with a null ACL. A null or empty ACL preserves the previous behavior exactly. ZooKeeper does not propagate ACLs to children, so only the cluster root carries the supplied ACL. Because ZooKeeper checks the DELETE permission on the parent znode, this is still enough to stop a foreign session from removing the cluster or its top level znodes, but the nodes underneath keep the default open ACL. The added tests document that boundary against a real ZooKeeper server. Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
Adds an ACL-aware cluster creation API so callers can create the cluster root znode with a caller-specified ZooKeeper ACL (while leaving child znodes on the existing default behavior), and verifies the behavior with new integration tests against the embedded ZooKeeper.
Changes:
- Added
HelixAdmin#addCluster(String, boolean, List<ACL>)as a new overload for ACL-aware cluster creation. - Implemented the new overload in
ZKHelixAdmin, routing the existing 2-arg overload through it. - Added test coverage in
TestZkHelixAdminto validate ACL application and enforcement boundaries.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| helix-core/src/main/java/org/apache/helix/HelixAdmin.java | Introduces the new addCluster(..., List<ACL>) API contract (default method) for ACL-aware cluster creation. |
| helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixAdmin.java | Implements the ACL-aware overload and routes the existing overload through it with null. |
| helix-core/src/test/java/org/apache/helix/manager/zk/TestZkHelixAdmin.java | Adds integration tests for applying ACLs on the cluster root and validating enforcement behavior. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| private static ZooKeeper rawZooKeeper(Object helixZkClient) { | ||
| return ((ZkConnection) ((org.apache.helix.zookeeper.zkclient.ZkClient) helixZkClient) | ||
| .getConnection()).getZookeeper(); | ||
| } |
There was a problem hiding this comment.
Fixed in fdedb6f — rawZooKeeper now takes HelixZkClient instead of Object, so both call sites are type checked. The cast to the concrete ZkClient/ZkConnection is still needed because getConnection()/getZookeeper() are not on the interface, but it is now isolated to this one helper.
| default boolean addCluster(String clusterName, boolean recreateIfExists, List<ACL> acl) { | ||
| throw new UnsupportedOperationException("addCluster with ACL is not implemented."); | ||
| } |
There was a problem hiding this comment.
Good catch, fixed in fdedb6f. The default method now delegates to addCluster(String, boolean) when the ACL is null or empty, and only throws UnsupportedOperationException when a caller actually asks for custom ACLs. Javadoc updated with an @throws to match.
The default addCluster(String, boolean, List<ACL>) threw UnsupportedOperationException unconditionally, which contradicted its own javadoc and broke non ZK HelixAdmin implementations that pass a null or empty ACL. It now delegates to addCluster(String, boolean) in that case and only throws when a caller actually asks for custom ACLs. Also type rawZooKeeper's parameter as HelixZkClient instead of Object so the test helper does not silently accept an unrelated type. Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (3)
helix-core/src/test/java/org/apache/helix/manager/zk/TestZkHelixAdmin.java:447
- Avoid relying on the platform default charset when generating digest auth credentials; the default can vary across environments and make this test brittle. Use an explicit charset (UTF-8) when calling getBytes().
final byte[] credentials = (owner + ":" + password).getBytes();
helix-core/src/test/java/org/apache/helix/manager/zk/TestZkHelixAdmin.java:484
- Minor grammar: use “non-empty” instead of “non empty” in the error message for consistency with standard usage.
Assert.fail("Expected the delete of a non empty cluster root to be rejected");
helix-core/src/main/java/org/apache/helix/HelixAdmin.java:125
- Minor grammar in Javadoc: use “non-empty” instead of “non empty”.
* @throws UnsupportedOperationException if a non empty ACL is supplied and the implementation
Use StandardCharsets.UTF_8 when converting the digest credentials to bytes so the test does not depend on the platform default charset, and hyphenate "non-empty" in the javadoc and the test failure message. Co-authored-by: Copilot <[email protected]>
ZooKeeper does not inherit ACLs, so protecting only /{clusterName} left every
node below it with the ZkClient default OPEN_ACL_UNSAFE. That blocked adding or
removing top level znodes but still allowed any session to read, overwrite and
delete cluster state, and even rewrite child ACLs to lock the owner out.
Thread the ACL through createZKPaths so every node created by addCluster carries
it. Behavior is unchanged when the ACL is null or empty.
Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (3)
helix-core/src/main/java/org/apache/helix/manager/zk/ZKHelixAdmin.java:1684
- If createZKPaths(clusterName, acl) throws (e.g., because the supplied ACL doesn’t grant CREATE/WRITE on some path), addCluster currently returns false but leaves a partially-created cluster root behind. That can make subsequent addCluster(..., recreateIfExists=false) calls incorrectly return true just because the root exists, and it leaves garbage znodes in ZooKeeper. Consider best-effort cleanup of the root on failure so the operation is closer to atomic.
try {
createZKPaths(clusterName, acl);
} catch (Exception e) {
logger.error("Error creating cluster:" + clusterName, e);
return false;
}
helix-core/src/test/java/org/apache/helix/manager/zk/TestZkHelixAdmin.java:570
- This test only deletes the digest-protected cluster inside the try block. If any assertion fails before the deleteRecursively call, the finally block closes the authorized client without cleaning up, leaving a cluster that the suite’s global unauthenticated _gZkClient may not be able to remove later. Please do best-effort cleanup in finally (before closing the authorized client) to keep the test isolated and avoid leaking protected znodes across failures.
} finally {
if (unauthorizedClient != null) {
unauthorizedClient.close();
}
authorizedClient.close();
helix-core/src/main/java/org/apache/helix/HelixAdmin.java:125
- The PR description’s caveat says only the cluster root znode carries the supplied ACL and children keep the default open ACL, but the API Javadoc here (and the ZKHelixAdmin implementation/tests) indicate the ACL is applied to the root and all metadata nodes created by addCluster. Please align the PR description (or, if root-only was intended, adjust the implementation/Javadoc) so the documented security boundary is consistent.
* @param acl ACLs applied to the cluster root node ("/{clusterName}") and to every cluster
* metadata node created underneath it by this call. If null or empty, the default ACL
* of the underlying metadata store client is used, making this equivalent to
* {@link #addCluster(String, boolean)}. ZooKeeper does not propagate ACLs to children,
* so nodes created after this call (resources, instances, live instances, ...) are
* NOT covered and keep the client default ACL. The ACL is only applied when the nodes
* are created by this call; the ACL of a pre-existing cluster is left untouched unless
* recreateIfExists is true.
Use MasterSlaveSMD.name instead of repeating the literal, and hoist the digest scheme into a named constant. ZooKeeper exposes scheme names only through AuthenticationProvider#getScheme, so there is no upstream constant to reuse. Co-authored-by: Copilot <[email protected]>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 3 out of 3 changed files in this pull request and generated no new comments.
Suppressed comments (1)
helix-core/src/main/java/org/apache/helix/HelixAdmin.java:125
- The PR description states that ZooKeeper ACLs do not propagate so “only the cluster root carries the supplied ACL” and that deeper ACLs would require a ZK client change. However, this new API contract/Javadoc (and ZKHelixAdmin implementation) applies the provided ACL to all metadata nodes created by this call (and their parents when createParents=true). Please reconcile the PR description/scope notes with the actual behavior so callers aren’t misled about what is and isn’t protected.
* Add a cluster whose metadata store nodes are created with the given ACLs
* @param clusterName
* @param recreateIfExists If the cluster already exists, it will delete it and recreate
* @param acl ACLs applied to the cluster root node ("/{clusterName}") and to every cluster
* metadata node created underneath it by this call. If null or empty, the default ACL
* of the underlying metadata store client is used, making this equivalent to
* {@link #addCluster(String, boolean)}. ZooKeeper does not propagate ACLs to children,
* so nodes created after this call (resources, instances, live instances, ...) are
* NOT covered and keep the client default ACL. The ACL is only applied when the nodes
* are created by this call; the ACL of a pre-existing cluster is left untouched unless
* recreateIfExists is true.
Description
Adds
HelixAdmin#addCluster(String clusterName, boolean recreateIfExists, List<ACL> acl)so callers can create a cluster whose metadata nodes are owned by a specific ZooKeeper identity instead of the client default ACL.defaultmethod, so existingHelixAdminimplementations keep compiling. It delegates toaddCluster(String, boolean)for a null/empty ACL and otherwise throwsUnsupportedOperationException.ZKHelixAdminimplements it and routes the existing two-argument version through it with anullACL.nullor empty ACL preserves the previous behavior exactly.The ACL is applied to the cluster root and to every cluster metadata node created by
addCluster(IDEALSTATES,CONFIGS/*,PROPERTYSTORE,LIVEINSTANCES,INSTANCES,EXTERNALVIEW,STATEMODELDEFS,CONTROLLER/*). ZooKeeper has no ACL inheritance, so each node has to be created with the ACL explicitly — applying it only to the root would block adding/removing top-level znodes while leaving all cluster state readable, writable and deletable by any session.Usage
The
Idscheme and format are determined by the authentication provider the ZooKeeper ensemble is running, and the client that callsaddClustermust itself authenticate as a principal the ACL grantsCREATEto.Service and group identities (
x509)With the stock
X509AuthenticationProvider, the id is the certificate subject DN as returned byX500Principal#getName(for exampleCN=my-service,OU=my-team,O=my-org,C=US); ids that are not valid DNs are rejected withInvalidACLException. Deployments that run a custom provider use whatever identity that provider extracts from the certificate, so take the exact string from your ZooKeeper operators rather than assuming a format.Digest identities
Prefer sourcing ACLs from configuration over hardcoding them, so identities can be rotated without a code change.
Caveats
addClustercreates. Nodes written afterwards (resources, instances, live instances, ...) go through other code paths, andZkClient.createPersistent(path, createParents)hardcodesIds.OPEN_ACL_UNSAFE. Covering those means an ACL on the ZkClient itself, which is out of scope here.CREATEon the root, cluster creation fails partway through and leaves an incomplete cluster behind.recreateIfExists=false,addClusterreturnstruefor an existing cluster and the ACL is skipped; usesetACLto change ACLs in place.skipACL=yes. Check with whoever operates the ensemble before relying on this.Tests
Added to
TestZkHelixAdmin, all run against the embedded real ZooKeeper server (no mocks):testAddClusterWithAcl— the root and all 15 cluster metadata nodes carry the supplied ACL, and a node created afteraddClusterdoes not.testAddClusterWithoutAclKeepsDefaultAcl— the two-argument path is unchanged.testAddClusterAclEnforcement— creates the cluster under a digest ACL, then opens a second unauthenticated ZooKeeper session and verifies it cannot read the root ACL, delete the root or its top-level children, read or overwrite the cluster config, inject a resource, or rewrite a child ACL to grant itself access.mvn -pl helix-core test -Dtest=TestZkHelixAdmin→ 25/25 pass.