/*-
* -\-\-
* 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);
}
}
}