/* * 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.apache.ambari.server.controller; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.lang.StringUtils; /** * AuthToLocalBuilder helps to create auth_to_local rules for use in configuration files like * core-site.xml. No duplicate rules will be generated. * <p/> * Allows previously existing rules to be added verbatim. Also allows new rules to be generated * based on a principal and local username. For each principal added to the builder, generate * a rule conforming to one of the following formats: * <p/> * Qualified Principal (the principal contains a user and host): * RULE:[2:$1@$0](PRIMARY@REALM)s/.*\/LOCAL_USERNAME/ * <p/> * Unqualified Principal (only user is specified): * RULE:[1:$1@$0](PRIMARY@REALM)s/.*\/LOCAL_USERNAME/ * <p/> * Additionally, for each realm included in the rule set, generate a default realm rule * in the format: RULE:[1:$1@$0](.*@REALM)s/@.{@literal *}// * <p/> * Ordering guarantees for the generated rule string are as follows: * <ul> * <li>Rules with the same expected component count are ordered according to match component count</li> * <li>Rules with different expected component count are ordered according to the default string ordering</li> * <li>Rules in the form of .*@REALM are ordered after all other rules with the same expected component count</li> * </ul> */ public class AuthToLocalBuilder implements Cloneable { public static final ConcatenationType DEFAULT_CONCATENATION_TYPE = ConcatenationType.NEW_LINES; /** * Ordered set of rules which have been added to the builder. */ private Set<Rule> setRules = new TreeSet<>(); /** * The default realm. */ private final String defaultRealm; /** * A set of additional realm names to reference when generating rules. */ private final Set<String> additionalRealms; /** * A flag indicating whether case insensitive support to the local username has been requested. This will append an //L switch to the generic realm rule */ private boolean caseInsensitiveUser; /** * Constructs a new AuthToLocalBuilder. * * @param defaultRealm a String declaring the default realm * @param additionalRealms a String containing a comma-delimited list of realm names * to incorporate into the generated rule set * @param caseInsensitiveUserSupport true indicating that case-insensitivity should be enabled; * false otherwise */ public AuthToLocalBuilder(String defaultRealm, String additionalRealms, boolean caseInsensitiveUserSupport) { this(defaultRealm, splitDelimitedString(additionalRealms), caseInsensitiveUserSupport); } /** * Constructs a new AuthToLocalBuilder. * * @param defaultRealm a String declaring the default realm * @param additionalRealms a collection of Strings declaring the set of realm names to * incorporate into the generated rule set * @param caseInsensitiveUserSupport true indicating that case-insensitivity should be enabled; * false otherwise */ public AuthToLocalBuilder(String defaultRealm, Collection<String> additionalRealms, boolean caseInsensitiveUserSupport) { this.defaultRealm = defaultRealm; this.additionalRealms = (additionalRealms == null) ? Collections.<String>emptySet() : Collections.unmodifiableSet(new HashSet<>(additionalRealms)); this.caseInsensitiveUser = caseInsensitiveUserSupport; } @Override public Object clone() throws CloneNotSupportedException { AuthToLocalBuilder copy = (AuthToLocalBuilder) super.clone(); /* **** Copy mutable members **** */ copy.setRules = new TreeSet<>(setRules); return copy; } /** * Add existing rules from the given authToLocal configuration property. * The rules are added verbatim. * * @param authToLocalRules config property value containing the existing rules */ public void addRules(String authToLocalRules) { if (!StringUtils.isEmpty(authToLocalRules)) { String[] rules = authToLocalRules.split("RULE:|DEFAULT"); for (String r : rules) { r = r.trim(); if (!r.isEmpty()) { Rule rule = createRule(r); setRules.add(rule); } } } } /** * Adds a rule for the given principal and local user. * The principal must contain a realm component. * <p/> * The supplied principal is parsed to determine if it is qualified or unqualified and stored * accordingly so that when the mapping rules are generated the appropriate rule is generated. * <p/> * If a principal is added that yields a duplicate primary principal value (relative to the set of * qualified or unqualified rules), that later entry will overwrite the older entry, allowing for * only one mapping rule. * <p/> * If the principal does not match one of the two expected patterns, it will be ignored. * * @param principal a string containing the full principal * @param localUsername a string declaring that local username to map the principal to * @throws IllegalArgumentException if the provided principal doesn't contain a realm element */ public void addRule(String principal, String localUsername) { if (!StringUtils.isEmpty(principal) && !StringUtils.isEmpty(localUsername)) { Principal p = new Principal(principal); if (p.getRealm() == null) { throw new IllegalArgumentException( "Attempted to add a rule for a principal with no realm: " + principal); } Rule rule = createHostAgnosticRule(p, localUsername); setRules.add(rule); } } /** * Generates the auth_to_local rules used by configuration settings such as core-site/auth_to_local. * <p/> * Each rule is concatenated using the default ConcatenationType, like calling * {@link #generate(ConcatenationType)} with {@link #DEFAULT_CONCATENATION_TYPE} * * @return a string containing the generated auth-to-local rule set */ public String generate() { return generate(null); } /** * Generates the auth_to_local rules used by configuration settings such as core-site/auth_to_local. * <p/> * Each rule is concatenated using the specified * {@link org.apache.ambari.server.controller.AuthToLocalBuilder.ConcatenationType}. * If the concatenation type is <code>null</code>, the default concatenation type is assumed - * see {@link #DEFAULT_CONCATENATION_TYPE}. * * @param concatenationType the concatenation type to use to generate the rule set string * @return a string containing the generated auth-to-local rule set */ public String generate(ConcatenationType concatenationType) { StringBuilder builder = new StringBuilder(); // ensure that a default rule is added for this realm if (!StringUtils.isEmpty(defaultRealm)) { // Remove existing default rule.... this is in the event we are switching case sensitivity... setRules.remove(createDefaultRealmRule(defaultRealm, !caseInsensitiveUser)); // Add (new) default rule.... setRules.add(createDefaultRealmRule(defaultRealm, caseInsensitiveUser)); } // ensure that a default realm rule is added for the specified additional realms for (String additionalRealm : additionalRealms) { // Remove existing default rule.... this is in the event we are switching case sensitivity... setRules.remove(createDefaultRealmRule(additionalRealm, !caseInsensitiveUser)); // Add (new) default rule.... setRules.add(createDefaultRealmRule(additionalRealm, caseInsensitiveUser)); } if (concatenationType == null) { concatenationType = DEFAULT_CONCATENATION_TYPE; } for (Rule rule : setRules) { appendRule(builder, rule.toString(), concatenationType); } appendRule(builder, "DEFAULT", concatenationType); return builder.toString(); } /** * Append a rule to the given string builder. * * @param stringBuilder string builder to which rule is added * @param rule rule to add * @param concatenationType the concatenation type to use to generate the rule set string */ private void appendRule(StringBuilder stringBuilder, String rule, ConcatenationType concatenationType) { if (stringBuilder.length() > 0) { switch (concatenationType) { case NEW_LINES: stringBuilder.append('\n'); break; case NEW_LINES_ESCAPED: stringBuilder.append("\\\n"); break; case SPACES: stringBuilder.append(" "); break; default: throw new UnsupportedOperationException(String.format("The auth-to-local rule concatenation type is not supported: %s", concatenationType.name())); } } stringBuilder.append(rule); } /** * Create a rule that expects 2 components in the principal and ignores hostname in the comparison. * * @param principal principal * @param localUser local user * @return a new rule that ignores hostname in the comparison */ private Rule createHostAgnosticRule(Principal principal, String localUser) { List<String> principalComponents = principal.getComponents(); int componentCount = principalComponents.size(); return new Rule(principal, componentCount, 1, String.format( "RULE:[%d:$1@$0](%s@%s)s/.*/%s/", componentCount, principal.getComponent(1), principal.getRealm(), localUser)); } /** * Create a default rule for a realm which matches all principals with 1 component and the same realm. * * @param realm realm that the rule is being created for * @param caseInsensitive true if the rule should be case-insensitive; otherwise false * @return a new default realm rule */ private Rule createDefaultRealmRule(String realm, boolean caseInsensitive) { String caseSensitivityRule = caseInsensitive ? "/L" : ""; return new Rule(new Principal(String.format(".*@%s", realm)), 1, 1, String.format("RULE:[1:$1@$0](.*@%s)s/@.*//" + caseSensitivityRule, realm)); } /** * Create a rule from an existing string representation. * * @param rule string representation of a rule * @return a new rule which matches the provided string representation */ private Rule createRule(String rule) { return new Rule(rule.startsWith("RULE:") ? rule : String.format("RULE:%s", rule)); } /** * Given a comma or line delimited list of strings, returns a collection of non-empty strings. * * @param string a string to split * @return an array of non-empty strings or null if the source string is empty or null */ private static Collection<String> splitDelimitedString(String string) { Collection<String> collection = null; if (!StringUtils.isEmpty(string)) { collection = new HashSet<>(); for (String realm : string.split("\\s*(?:\\r?\\n|,)\\s*")) { realm = realm.trim(); if (!realm.isEmpty()) { collection.add(realm); } } } return collection; } /** * Rule implementation. */ private static class Rule implements Comparable<Rule> { /** * pattern used to parse existing rules */ private static final Pattern PATTERN_RULE_PARSE = Pattern.compile("RULE:\\s*\\[\\s*(\\d)\\s*:\\s*(.+?)(?:@(.+?))??\\s*\\]\\s*\\((.+?)\\)\\s*s/(.*?)/(.*?)/([a-zA-Z]*)(?:.|\n)*"); /** * associated principal */ private Principal principal; /** * string representation of the rule */ private String rule; /** * expected component count */ private int expectedComponentCount; /** * number of components being matched in the rule */ private int matchComponentCount; /** * Constructor. * * @param principal principal * @param expectedComponentCount number of components needed by a principal to match * @param matchComponentCount number of components which are included in the rule evaluation * @param rule string representation of the rule */ public Rule(Principal principal, int expectedComponentCount, int matchComponentCount, String rule) { this.principal = principal; this.expectedComponentCount = expectedComponentCount; this.matchComponentCount = matchComponentCount; this.rule = rule; } /** * Constructor. * * @param rule string representation of the rule */ public Rule(String rule) { //this.rule = rule; Matcher m = PATTERN_RULE_PARSE.matcher(rule); if (!m.matches()) { throw new IllegalArgumentException("Invalid rule: " + rule); } expectedComponentCount = Integer.valueOf(m.group(1)); String matchPattern = m.group(2); matchComponentCount = (matchPattern.startsWith("$") ? matchPattern.substring(1) : matchPattern). split("\\$").length; String patternRealm = m.group(3); principal = new Principal(m.group(4)); String replacementPattern = m.group(5); String replacementReplacement = m.group(6); String replacementModifier = m.group(7); if (patternRealm != null) { this.rule = String.format("RULE:[%d:%s@%s](%s)s/%s/%s/%s", expectedComponentCount, matchPattern, patternRealm, principal.toString(), replacementPattern, replacementReplacement, replacementModifier); } else { this.rule = String.format("RULE:[%d:%s](%s)s/%s/%s/%s", expectedComponentCount, matchPattern, principal.toString(), replacementPattern, replacementReplacement, replacementModifier); } } /** * Get the associated principal. * * @return associated principal */ public Principal getPrincipal() { return principal; } /** * Get the expected component count. This specified the number of components * that a principal must contain to match this rule. * * @return the expected component count */ public int getExpectedComponentCount() { return expectedComponentCount; } /** * Get the match component count. This is the number of components that are evaluated * when attempting to match a principal to the rule. * * @return the match component count */ public int getMatchComponentCount() { return matchComponentCount; } /** * String representation of the rule in the form * RULE:[componentCount:matchString](me@foo.com)s/pattern/localUser/ * * @return string representation of the rule */ @Override public String toString() { return rule; } /** * Compares rules. * <p/> * For rules with different expected component counts, the default string comparison is used. * For rules with the same expected component count rules are ordered so that rules with a higher * match component count occur first. * <p/> * For rules with the same expected component count, default realm rules in the form of * .*@myRealm.com are ordered last. * * @param other the other rule to compare * @return a negative integer, zero, or a positive integer as this object is less than, * equal to, or greater than the specified object */ @Override public int compareTo(Rule other) { int retVal = expectedComponentCount - other.getExpectedComponentCount(); if (retVal == 0) { retVal = other.getMatchComponentCount() - matchComponentCount; if (retVal == 0) { Principal otherPrincipal = other.getPrincipal(); if (principal.equals(otherPrincipal)) { retVal = rule.compareTo(other.rule); } else { // check for wildcard realms '.*' String realm = principal.getRealm(); String otherRealm = otherPrincipal.getRealm(); retVal = compareValueWithWildcards(realm, otherRealm); if (retVal == 0) { for (int i = 1; i <= matchComponentCount; i++) { // check for wildcard component String component1 = principal.getComponent(1); String otherComponent1 = otherPrincipal.getComponent(1); retVal = compareValueWithWildcards(component1, otherComponent1); if (retVal != 0) { break; } } } } } } return retVal; } @Override public boolean equals(Object o) { return this == o || o instanceof Rule && rule.equals(((Rule) o).rule); } @Override public int hashCode() { return rule.hashCode(); } /** * Compares 2 strings for use in compareTo methods but orders <code>null</code>s first and wildcards last. * <p/> * Rules: * <ul> * <li><code>null</code> is ordered before any other string except for <code>null</code>, which is considered be equal</li> * <li><code>.*</code> is ordered after any other string except for <code>.*</code>, which is considered equal</li> * <li>All other values are order based on the result of {@link String#compareTo(String)}</li> * </ul> * * @param s1 the first string to be compared. * @param s2 the second string to be compared. * @return a negative integer, zero, or a positive integer as the first argument is less than, * equal to, or greater than the second. * @see Comparable#compareTo(Object) */ private int compareValueWithWildcards(String s1, String s2) { if (s1 == null) { if (s2 == null) { return 0; } else { return -1; } } else if (s2 == null) { return 1; } else if (s1.equals(s2)) { return 0; } else if (s1.equals(".*")) { return 1; } else if (s2.equals(".*")) { return -1; } else { return s1.compareTo(s2); } } } /** * Principal implementation. */ private static class Principal { /** * principal pattern which allows for null realm */ private static final Pattern p = Pattern.compile("([^@]+)(?:@(.*))?"); /** * string representation */ private String principal; /** * associated realm */ private String realm; /** * list of components in the principal not including the realm */ private List<String> components; /** * Constructor. * * @param principal string representation of the principal */ public Principal(String principal) { this.principal = principal; Matcher m = p.matcher(principal); if (m.matches()) { String allComponents = m.group(1); if (allComponents == null) { components = Collections.emptyList(); } else { allComponents = allComponents.startsWith("/") ? allComponents.substring(1) : allComponents; components = Arrays.asList(allComponents.split("/")); } realm = m.group(2); } else { throw new IllegalArgumentException("Invalid Principal: " + principal); } } /** * Get all of the components which make up the principal. * * @return list of principal components */ public List<String> getComponents() { return components; } /** * Get the component at the specified location. * Uses the range 1-n to match the notation used in the rule. * * @param position position of the component in the range 1-n * @return the component at the specified location or null */ public String getComponent(int position) { if (position > components.size()) { return null; } else { return components.get(position - 1); } } /** * Get the associated realm. * * @return the associated realm */ public String getRealm() { return realm; } @Override public String toString() { return principal; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } Principal principal1 = (Principal) o; return components.equals(principal1.components) && principal.equals(principal1.principal) && !(realm != null ? !realm.equals(principal1.realm) : principal1.realm != null); } @Override public int hashCode() { int result = principal.hashCode(); result = 31 * result + (realm != null ? realm.hashCode() : 0); result = 31 * result + components.hashCode(); return result; } } /** * ConcatenationType is an enumeration of auth-to-local rule concatenation types. */ public enum ConcatenationType { /** * Each rule is appended to the set of rules on a new line (<code>\n</code>) */ NEW_LINES, /** * Each rule is appended to the set of rules on a new line, escaped using a \ (<code>\\n</code>) */ NEW_LINES_ESCAPED, /** * Each rule is appended to the set of rules using a space - the ruleset exists on a single line */ SPACES; /** * Translate a string declaring a concatenation type to the enumerated value. * <p/> * If the string value is <code>null</code> or empty, return the default type - {@link #NEW_LINES}. * * @param value a value to translate * @return a ConcatenationType */ public static ConcatenationType translate(String value) { if (value != null) { value = value.trim(); if (!value.isEmpty()) { return valueOf(value.toUpperCase()); } } return DEFAULT_CONCATENATION_TYPE; } } }