ACL in Apache ZooKeeper

Apache ZooKeeper is very often compared to distributed file system. Because each file system has a feature to deal with file permissions, ZooKeeper, as a kind of file system, can't be different.

As you can deduce, this article covers ACL part of Apache ZooKeeper. In the first part we can see which permissions and how they can be set to given zNode. The second part, as in previous articles, shows the use of them through several test cases written with ZooKeeper's Java API.

ACL in Apache ZooKeeper

Before listing possible permissions, we should accentuate that even if ZooKeeper is considered as file system, it has one difference regarding to standard file system. Rather than limiting the file permissions to one of 3 possible scopes (file's owner, group or world), it associates the limitation to a set of ids. This set defines in its own associated permissions.

These ids are specified in the form of "scheme:id". We can observe that in org.apache.zookeeper.data.Id class and some of its predefined objects:

public final Id ANYONE_ID_UNSAFE = new Id("world", "anyone");
public final Id AUTH_IDS = new Id("auth", "");

Each Id is used to specify the familly of permissions. The ANYONE_ID_UNSAFE applies to all users, authentified or not. The AUTH_IDS marks given set of permissions as applicable only for authentified user. Permissions can also be associated to specified ip address.

Regarding to permissions, there are several supported entries:

ACL example with Apache ZooKeeper Java API

After this short intrudction to permissions in Apache ZooKeeper, we can see how manipulate them through test cases using Java API:

@Test(expected = KeeperException.NoAuthException.class)
public void should_not_be_able_to_get_znode_content_for_other_user_than_its_creator() throws KeeperException, InterruptedException, IOException {
  zooKeeper.addAuthInfo("digest", "bartosz:my_password".getBytes());
  zooKeeper.create(ONLY_OWNER, "zNode owned by its creator".getBytes(), 
    ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);

  // open new session and try to get the file
  ZooKeeper newSession = new ZooKeeper("127.0.0.1:2181", CONNECTION_TIMEOUT,
          (event) -> System.out.println("Processing event " + event));
  newSession.addAuthInfo("digest", "other_user:my_password".getBytes());
  while (newSession.getState() == ZooKeeper.States.CONNECTING) {
  }
  newSession.getData(ONLY_OWNER, false, new Stat());
}

@Test
public void should_be_able_to_get_znode_content_when_new_session_is_created_for_the_same_user_as_znode_creator()
        throws KeeperException, InterruptedException, IOException {
  String zNodeContent = "zNode owned by its creator";
  zooKeeper.addAuthInfo("digest", "bartosz:my_password".getBytes());
  zooKeeper.create(ONLY_OWNER_BIS, zNodeContent.getBytes(), 
    ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.PERSISTENT);

  // open new session and try to get the file
  ZooKeeper newSession = new ZooKeeper("127.0.0.1:2181", CONNECTION_TIMEOUT, 
    (event) -> System.out.println("Processing event " + event));
  newSession.addAuthInfo("digest", "bartosz:my_password".getBytes());
  while (newSession.getState() == ZooKeeper.States.CONNECTING) {
  }

  String content = new String(newSession.getData(ONLY_OWNER_BIS, false, new Stat()));

  assertThat(content).isEqualTo(zNodeContent);
}

@Test
public void should_correctly_get_znode_with_authentication_ip() throws KeeperException, InterruptedException, IOException {
  String zNodeContent = "zNode owned by its creator";
  zooKeeper.addAuthInfo("ip", "127.0.0.1".getBytes());
  zooKeeper.create(OWNER_IP, zNodeContent.getBytes(), LOCALHOST_IP_AUTH, CreateMode.PERSISTENT);

  // open new session and try to get the file
  ZooKeeper newSession = new ZooKeeper("127.0.0.1:2181", CONNECTION_TIMEOUT, 
    (event) -> System.out.println("Processing event " + event));
  newSession.addAuthInfo("ip", "127.0.0.1".getBytes());
  while (newSession.getState() == ZooKeeper.States.CONNECTING) {
  }

  String content = new String(newSession.getData(OWNER_IP, false, new Stat()));

  assertThat(content).isEqualTo(zNodeContent);
}

 /**
  * IP authentication is resolved by Apache ZooKeeper. So calling the same node with 2 different authentication
  * modes (one IP, other digest), should work.
  *
  * The authentication against ACL is made by {@link IPAuthenticationProvider#handleAuthentication(ServerCnxn, byte[])} where
  * IP is retrieved by:
  * <pre>
  * String id = cnxn.getRemoteSocketAddress().getAddress().getHostAddress();
  * cnxn.addAuthInfo(new Id(getScheme(), id));
  * </pre>
  */
@Test
public void should_get_node_even_if_uses_different_auth_information() throws KeeperException, InterruptedException, IOException {
  zooKeeper.addAuthInfo("ip", "127.0.0.1".getBytes());
  zooKeeper.create(OWNER_IP_DIGEST, "X".getBytes(), LOCALHOST_IP_AUTH, CreateMode.PERSISTENT);

  // open new session and try to get the file
  ZooKeeper newSession = new ZooKeeper("127.0.0.1:2181", CONNECTION_TIMEOUT, 
    (event) -> System.out.println("Processing event " + event));
  newSession.addAuthInfo("digest", "a:b".getBytes());
  while (newSession.getState() == ZooKeeper.States.CONNECTING) {
  }

  assertThat(newSession.getData(OWNER_IP_DIGEST, false, new Stat())).isEqualTo("X".getBytes());
}

@Test(expected = KeeperException.NoAuthException.class)
public void should_fail_on_getting_znode_when_is_owned_by_other_ip_address() throws KeeperException, InterruptedException, IOException {
  ArrayList<ACL> otherIp = new ArrayList<>(
          Collections.singletonList(new ACL(ZooDefs.Perms.ALL, new Id("ip", "1.0.0.1"))));
  zooKeeper.addAuthInfo("ip", "127.0.0.1".getBytes());
  zooKeeper.create(OWNER_IP_FAILURE, "X".getBytes(), otherIp, CreateMode.PERSISTENT);

  // open new session and try to get the file
  ZooKeeper newSession = new ZooKeeper("127.0.0.1:2181", CONNECTION_TIMEOUT, 
    (event) -> System.out.println("Processing event " + event));
  newSession.addAuthInfo("ip", "127.0.0.1".getBytes());
  while (newSession.getState() == ZooKeeper.States.CONNECTING) {
  }

  newSession.getData(OWNER_IP_FAILURE, false, new Stat());
}

@Test
public void should_allow_only_reading_znode_for_public() throws KeeperException, InterruptedException, IOException {
  ArrayList<ACL> worldReadOnlyACL = new ArrayList<>(
      Collections.singletonList(new ACL(ZooDefs.Perms.READ, new Id("world", "anyone"))));
  zooKeeper.addAuthInfo("digest", "bartosz:pass".getBytes());
  zooKeeper.create(OWNER_IP_READ_ONLY, "X".getBytes(), worldReadOnlyACL, CreateMode.PERSISTENT);

  // open new session and try to get the file
  ZooKeeper newSession = new ZooKeeper("127.0.0.1:2181", CONNECTION_TIMEOUT, 
    (event) -> System.out.println("Processing event " + event));
  newSession.addAuthInfo("digest", "bartosz2:pass2".getBytes());
  while (newSession.getState() == ZooKeeper.States.CONNECTING) {
  }

  assertThat(newSession.getData(OWNER_IP_READ_ONLY, false, new Stat())).isEqualTo("X".getBytes());
  newSession.setData(OWNER_IP_READ_ONLY, "Z".getBytes(), ALL_VERSIONS);
}

@Test(expected = KeeperException.NoAuthException.class)
public void should_fail_on_modyfing_read_only_node() throws KeeperException, InterruptedException, IOException {
  ArrayList<ACL> readOnlyACL = new ArrayList<>(
          Collections.singletonList(new ACL(ZooDefs.Perms.READ, new Id("world", "anyone"))));
  zooKeeper.addAuthInfo("digest", "bartosz:pass".getBytes());
  zooKeeper.create(OWNER_IP_READ_ONLY_FAILING, "X".getBytes(), readOnlyACL, CreateMode.PERSISTENT);

  // open new session and try to set new file content
  ZooKeeper newSession = new ZooKeeper("127.0.0.1:2181", CONNECTION_TIMEOUT, 
    (event) -> System.out.println("Processing event " + event));
  newSession.addAuthInfo("digest", "bartosz2:pass2".getBytes());
  while (newSession.getState() == ZooKeeper.States.CONNECTING) {
  }

  newSession.setData(OWNER_IP_READ_ONLY_FAILING, "Z".getBytes(), ALL_VERSIONS);
}


@Test(expected = KeeperException.InvalidACLException.class)
public void should_fail_on_setting_creator_acl_without_auth_information() throws KeeperException, InterruptedException {
  zooKeeper.create("/failing_node", "Test".getBytes(), ZooDefs.Ids.CREATOR_ALL_ACL, CreateMode.EPHEMERAL);
}

Permissions in Apache ZooKeeper can be managed through schema and id. This pair has associated permissions mask describing if a zNode is read-only or not. We saw also that several authentication modes exist: digest (login-password combination) or ip (access limited to specific IP address).

If you liked it, you should read:

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!