Java tutorial
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you 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 org.wso2.andes.server.security.access.config; import org.apache.commons.lang.BooleanUtils; import org.apache.commons.lang.StringUtils; import org.wso2.andes.exchange.ExchangeDefaults; import org.wso2.andes.server.logging.actors.CurrentActor; import org.wso2.andes.server.security.Result; import org.wso2.andes.server.security.access.ObjectProperties; import org.wso2.andes.server.security.access.ObjectType; import org.wso2.andes.server.security.access.Operation; import org.wso2.andes.server.security.access.Permission; import org.wso2.andes.server.security.access.logging.AccessControlMessages; import javax.security.auth.Subject; import java.security.Principal; import java.util.*; /** * Models the rule configuration for the access control plugin. * * The access control rule definitions are loaded from an external configuration file, passed in as the * target to the {@link load(ConfigurationFile)} method. The file specified */ public class RuleSet { private static final String AT = "@"; private static final String SLASH = "/"; public static final String DEFAULT_ALLOW = "defaultallow"; public static final String DEFAULT_DENY = "defaultdeny"; public static final String TRANSITIVE = "transitive"; public static final String EXPAND = "expand"; public static final String AUTONUMBER = "autonumber"; public static final String CONTROLLED = "controlled"; public static final String VALIDATE = "validate"; public static final List<String> CONFIG_PROPERTIES = Arrays.asList(DEFAULT_ALLOW, DEFAULT_DENY, TRANSITIVE, EXPAND, AUTONUMBER, CONTROLLED); private static final Integer _increment = 10; private final Map<String, List<String>> _aclGroups = new HashMap<String, List<String>>(); private final SortedMap<Integer, Rule> _rules = new TreeMap<Integer, Rule>(); private final Map<Subject, Map<Operation, Map<ObjectType, List<Rule>>>> _cache = new WeakHashMap<Subject, Map<Operation, Map<ObjectType, List<Rule>>>>(); private final Map<String, Boolean> _config = new HashMap<String, Boolean>(); public RuleSet() { // set some default configuration properties configure(DEFAULT_DENY, Boolean.TRUE); configure(TRANSITIVE, Boolean.TRUE); } /** * Clear the contents, including acl groups, rules and configuration. */ public void clear() { _rules.clear(); _cache.clear(); _config.clear(); _aclGroups.clear(); } public int getRuleCount() { return _rules.size(); } /** * Filtered rules list based on a subject and operation. * * Allows only enabled rules with identity equal to all, the same, or a group with identity as a member, * and operation is either all or the same operation. */ public List<Rule> getRules(final Subject subject, final Operation operation, final ObjectType objectType) { final Map<ObjectType, List<Rule>> objects = getObjectToRuleCache(subject, operation); // Lookup object type rules for the operation if (!objects.containsKey(objectType)) { final Set<Principal> principals = subject.getPrincipals(); boolean controlled = false; List<Rule> filtered = new LinkedList<Rule>(); for (Rule rule : _rules.values()) { final Action ruleAction = rule.getAction(); if (rule.isEnabled() && (ruleAction.getOperation() == Operation.ALL || ruleAction.getOperation() == operation) && (ruleAction.getObjectType() == ObjectType.ALL || ruleAction.getObjectType() == objectType)) { controlled = true; if (isRelevant(principals, rule)) { filtered.add(rule); } } } // Return null if there are no rules at all for this operation and object type if (filtered.isEmpty() && controlled == false) { filtered = null; } // Save the rules we selected objects.put(objectType, filtered); } // Return the cached rules return objects.get(objectType); } public boolean isValidNumber(Integer number) { return !_rules.containsKey(number); } public void grant(Integer number, String identity, Permission permission, Operation operation) { Action action = new Action(operation); addRule(number, identity, permission, action); } public void grant(Integer number, String identity, Permission permission, Operation operation, ObjectType object, ObjectProperties properties) { Action action = new Action(operation, object, properties); addRule(number, identity, permission, action); } public boolean ruleExists(String identity, Action action) { for (Rule rule : _rules.values()) { if (rule.getIdentity().equals(identity) && rule.getAction().equals(action)) { return true; } } return false; } private Permission noLog(Permission permission) { switch (permission) { case ALLOW: case ALLOW_LOG: return Permission.ALLOW; case DENY: case DENY_LOG: default: return Permission.DENY; } } // TODO make this work when group membership is not known at file parse time public void addRule(Integer number, String identity, Permission permission, Action action) { _cache.clear(); if (!action.isAllowed()) { throw new IllegalArgumentException("Action is not allowd: " + action); } if (ruleExists(identity, action)) { return; } // expand actions - possibly multiply number by if (isSet(EXPAND)) { if (action.getOperation() == Operation.CREATE && action.getObjectType() == ObjectType.TOPIC) { addRule(null, identity, noLog(permission), new Action(Operation.BIND, ObjectType.EXCHANGE, new ObjectProperties("amq.topic", action.getProperties().get(ObjectProperties.Property.NAME)))); ObjectProperties topicProperties = new ObjectProperties(); topicProperties.put(ObjectProperties.Property.DURABLE, true); addRule(null, identity, permission, new Action(Operation.CREATE, ObjectType.QUEUE, topicProperties)); return; } if (action.getOperation() == Operation.DELETE && action.getObjectType() == ObjectType.TOPIC) { addRule(null, identity, noLog(permission), new Action(Operation.UNBIND, ObjectType.EXCHANGE, new ObjectProperties("amq.topic", action.getProperties().get(ObjectProperties.Property.NAME)))); ObjectProperties topicProperties = new ObjectProperties(); topicProperties.put(ObjectProperties.Property.DURABLE, true); addRule(null, identity, permission, new Action(Operation.DELETE, ObjectType.QUEUE, topicProperties)); return; } } // transitive action dependencies if (isSet(TRANSITIVE)) { if (action.getOperation() == Operation.CREATE && action.getObjectType() == ObjectType.QUEUE) { ObjectProperties exchProperties = new ObjectProperties(action.getProperties()); exchProperties.setName(ExchangeDefaults.DEFAULT_EXCHANGE_NAME); exchProperties.put(ObjectProperties.Property.ROUTING_KEY, action.getProperties().get(ObjectProperties.Property.NAME)); addRule(null, identity, noLog(permission), new Action(Operation.BIND, ObjectType.EXCHANGE, exchProperties)); if (action.getProperties().isSet(ObjectProperties.Property.AUTO_DELETE)) { addRule(null, identity, noLog(permission), new Action(Operation.DELETE, ObjectType.QUEUE, action.getProperties())); } } else if (action.getOperation() == Operation.DELETE && action.getObjectType() == ObjectType.QUEUE) { ObjectProperties exchProperties = new ObjectProperties(action.getProperties()); exchProperties.setName(ExchangeDefaults.DEFAULT_EXCHANGE_NAME); exchProperties.put(ObjectProperties.Property.ROUTING_KEY, action.getProperties().get(ObjectProperties.Property.NAME)); addRule(null, identity, noLog(permission), new Action(Operation.UNBIND, ObjectType.EXCHANGE, exchProperties)); } else if (action.getOperation() != Operation.ACCESS && action.getObjectType() != ObjectType.VIRTUALHOST) { addRule(null, identity, noLog(permission), new Action(Operation.ACCESS, ObjectType.VIRTUALHOST)); } } // set rule number if needed Rule rule = new Rule(number, identity, action, permission); if (rule.getNumber() == null) { if (_rules.isEmpty()) { rule.setNumber(0); } else { rule.setNumber(_rules.lastKey() + _increment); } } // save rule _cache.remove(identity); _rules.put(rule.getNumber(), rule); } public void enableRule(int ruleNumber) { _rules.get(Integer.valueOf(ruleNumber)).enable(); } public void disableRule(int ruleNumber) { _rules.get(Integer.valueOf(ruleNumber)).disable(); } public boolean addGroup(String group, List<String> constituents) { _cache.clear(); if (_aclGroups.containsKey(group)) { // cannot redefine return false; } else { _aclGroups.put(group, new ArrayList<String>()); } for (String name : constituents) { if (name.equalsIgnoreCase(group)) { // recursive definition return false; } if (!checkName(name)) { // invalid name return false; } if (_aclGroups.containsKey(name)) { // is a group _aclGroups.get(group).addAll(_aclGroups.get(name)); } else { // is a user if (!isvalidUserName(name)) { // invalid username return false; } _aclGroups.get(group).add(name); } } return true; } /** Return true if the name is well-formed (contains legal characters). */ protected boolean checkName(String name) { for (int i = 0; i < name.length(); i++) { Character c = name.charAt(i); if (!Character.isLetterOrDigit(c) && c != '-' && c != '_' && c != '@' && c != '.' && c != '/') { return false; } } return true; } /** Returns true if a username has the name[@domain][/realm] format */ protected boolean isvalidUserName(String name) { // check for '@' and '/' in namne int atPos = name.indexOf(AT); int slashPos = name.indexOf(SLASH); boolean atFound = atPos != StringUtils.INDEX_NOT_FOUND && atPos == name.lastIndexOf(AT); boolean slashFound = slashPos != StringUtils.INDEX_NOT_FOUND && slashPos == name.lastIndexOf(SLASH); // must be at least one character after '@' or '/' if (atFound && atPos > name.length() - 2) { return false; } if (slashFound && slashPos > name.length() - 2) { return false; } // must be at least one character between '@' and '/' if (atFound && slashFound) { return (atPos < (slashPos - 1)); } // otherwise all good return true; } // C++ broker authorise function prototype // virtual bool authorise(const std::string& id, const Action& action, const ObjectType& objType, // const std::string& name, std::map<Property, std::string>* params=0); // Possibly add a String name paramater? /** * Check the authorisation granted to a particular identity for an operation on an object type with * specific properties. * * Looks up the entire ruleset, which may be cached, for the user and operation and goes through the rules * in order to find the first one that matches. Either defers if there are no rules, returns the result of * the first match found, or denies access if there are no matching rules. Normally, it would be expected * to have a default deny or allow rule at the end of an access configuration however. */ public Result check(Subject subject, Operation operation, ObjectType objectType, ObjectProperties properties) { // Create the action to check Action action = new Action(operation, objectType, properties); // get the list of rules relevant for this request List<Rule> rules = getRules(subject, operation, objectType); if (rules == null) { if (isSet(CONTROLLED)) { // Abstain if there are no rules for this operation return Result.ABSTAIN; } else { return getDefault(); } } // Iterate through a filtered set of rules dealing with this identity and operation for (Rule current : rules) { // Check if action matches if (action.matches(current.getAction())) { Permission permission = current.getPermission(); switch (permission) { case ALLOW_LOG: CurrentActor.get().message(AccessControlMessages.ALLOWED(action.getOperation().toString(), action.getObjectType().toString(), action.getProperties().toString())); case ALLOW: return Result.ALLOWED; case DENY_LOG: CurrentActor.get().message(AccessControlMessages.DENIED(action.getOperation().toString(), action.getObjectType().toString(), action.getProperties().toString())); case DENY: return Result.DENIED; } return Result.DENIED; } } // Defer to the next plugin of this type, if it exists return Result.DEFER; } /** Default deny. */ public Result getDefault() { if (isSet(DEFAULT_ALLOW)) { return Result.ALLOWED; } if (isSet(DEFAULT_DENY)) { return Result.DENIED; } return Result.ABSTAIN; } /** * Check if a configuration property is set. */ protected boolean isSet(String key) { return BooleanUtils.isTrue(_config.get(key)); } /** * Configure properties for the plugin instance. * * @param properties */ public void configure(Map<String, Boolean> properties) { _config.putAll(properties); } /** * Configure a single property for the plugin instance. * * @param key * @param value */ public void configure(String key, Boolean value) { _config.put(key, value); } private boolean isRelevant(final Set<Principal> principals, final Rule rule) { if (rule.getIdentity().equalsIgnoreCase(Rule.ALL)) { return true; } else { for (Iterator<Principal> iterator = principals.iterator(); iterator.hasNext();) { final Principal principal = iterator.next(); if (rule.getIdentity().equalsIgnoreCase(principal.getName()) || (_aclGroups.containsKey(rule.getIdentity()) && _aclGroups.get(rule.getIdentity()).contains(principal.getName()))) { return true; } } } return false; } private Map<ObjectType, List<Rule>> getObjectToRuleCache(final Subject subject, final Operation operation) { // Lookup identity in cache and create empty operation map if required Map<Operation, Map<ObjectType, List<Rule>>> operations = _cache.get(subject); if (operations == null) { operations = new EnumMap<Operation, Map<ObjectType, List<Rule>>>(Operation.class); _cache.put(subject, operations); } // Lookup operation and create empty object type map if required Map<ObjectType, List<Rule>> objects = operations.get(operation); if (objects == null) { objects = new EnumMap<ObjectType, List<Rule>>(ObjectType.class); operations.put(operation, objects); } return objects; } }