/*- * -\-\- * Helios Services * -- * Copyright (C) 2016 Spotify AB * -- * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * -/-/- */ package com.spotify.helios.servicescommon; import com.google.common.collect.ImmutableList; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.regex.Pattern; import org.apache.curator.framework.api.ACLProvider; import org.apache.zookeeper.ZooDefs; import org.apache.zookeeper.data.ACL; import org.apache.zookeeper.data.Id; /** * A ZooKeeper ACLProvider that uses regular expression-based rules to determine what ACLs govern * created nodes. * * <p>The permissions for all rules that match a path apply. For example if there * are two rules: * <ol> * <li>"/foo.*", DELETE, usr1</li> * <li>"/foo/bar", CREATE, user1</li> * </ol> * * <p>Then ACLs will be set so that user1 can create and delete nodes on the /foo/bar node, but he * can not delete nodes under the /foo node. */ public class RuleBasedZooKeeperAclProvider implements ACLProvider { private final ImmutableList<ACL> defaultAcl; private final ImmutableList<Rule> rules; public static Builder builder() { return new Builder(); } private RuleBasedZooKeeperAclProvider(final ImmutableList<Rule> rules, final ImmutableList<ACL> defaultAcl) { this.rules = rules; this.defaultAcl = defaultAcl; } @Override public List<ACL> getDefaultAcl() { return defaultAcl; } @Override public List<ACL> getAclForPath(final String path) { // id -> permissions final Map<Id, Integer> matching = Maps.newHashMap(); for (final Rule rule : rules) { if (rule.matches(path)) { final int existingPerms = matching.containsKey(rule.id) ? matching.get(rule.id) : 0; matching.put(rule.id, rule.perms | existingPerms); } } if (matching.isEmpty()) { return null; } final List<ACL> acls = Lists.newArrayList(); for (final Map.Entry<Id, Integer> e : matching.entrySet()) { acls.add(new ACL(e.getValue(), e.getKey())); } return acls; } private static class Rule { private final Pattern pattern; private final Id id; private final int perms; Rule(final String regex, final int perms, final Id id) { this.pattern = Pattern.compile(regex); this.perms = perms; this.id = id; } boolean matches(final String path) { return pattern.matcher(path).matches(); } } public static class Builder { private final List<Rule> rules = Lists.newArrayList(); private ImmutableList<ACL> defaultAcl = ImmutableList.copyOf(ZooDefs.Ids.READ_ACL_UNSAFE); Builder defaultAcl(final ACL... acls) { return defaultAcl(Arrays.asList(acls)); } Builder defaultAcl(final List<ACL> defaultAcl) { this.defaultAcl = ImmutableList.copyOf(defaultAcl); return this; } Builder rule(final String pathRegex, final int permissions, final Id id) { rules.add(new Rule(pathRegex, permissions, id)); return this; } RuleBasedZooKeeperAclProvider build() { return new RuleBasedZooKeeperAclProvider(ImmutableList.copyOf(rules), defaultAcl); } } }