1 /** 2 * Licensed to the Apache Software Foundation (ASF) under one 3 * or more contributor license agreements. See the NOTICE file 4 * distributed with this work for additional information 5 * regarding copyright ownership. The ASF licenses this file 6 * to you under the Apache License, Version 2.0 (the 7 * "License"); you may not use this file except in compliance 8 * with the License. You may obtain a copy of the License at 9 * 10 * http://www.apache.org/licenses/LICENSE-2.0 11 * 12 * Unless required by applicable law or agreed to in writing, software 13 * distributed under the License is distributed on an "AS IS" BASIS, 14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 * See the License for the specific language governing permissions and 16 * limitations under the License. 17 */ 18 19 package org.apache.hadoop.security; 20 21 import java.io.IOException; 22 import java.util.ArrayList; 23 import java.util.List; 24 import java.util.regex.Matcher; 25 import java.util.regex.Pattern; 26 27 import org.apache.hadoop.conf.Configuration; 28 import org.apache.hadoop.security.authentication.util.KerberosUtil; 29 30 /** 31 * This class implements parsing and handling of Kerberos principal names. In 32 * particular, it splits them apart and translates them down into local 33 * operating system names. 34 */ 35 public class KerberosName { 36 /** The first component of the name */ 37 private final String serviceName; 38 /** The second component of the name. It may be null. */ 39 private final String hostName; 40 /** The realm of the name. */ 41 private final String realm; 42 43 /** 44 * A pattern that matches a Kerberos name with at most 2 components. 45 */ 46 private static final Pattern nameParser = 47 Pattern.compile("([^/@]*)(/([^/@]*))?@([^/@]*)"); 48 49 /** 50 * A pattern that matches a string with out '$' and then a single 51 * parameter with $n. 52 */ 53 private static Pattern parameterPattern = 54 Pattern.compile("([^$]*)(\\$(\\d*))?"); 55 56 /** 57 * A pattern for parsing a auth_to_local rule. 58 */ 59 private static final Pattern ruleParser = 60 Pattern.compile("\\s*((DEFAULT)|(RULE:\\[(\\d*):([^\\]]*)](\\(([^)]*)\\))?"+ 61 "(s/([^/]*)/([^/]*)/(g)?)?))"); 62 63 /** 64 * A pattern that recognizes simple/non-simple names. 65 */ 66 private static final Pattern nonSimplePattern = Pattern.compile("[/@]"); 67 68 /** 69 * The list of translation rules. 70 */ 71 private static List<Rule> rules; 72 73 private static String defaultRealm; 74 75 static { 76 try { 77 defaultRealm = KerberosUtil.getDefaultRealm(); 78 } catch (Exception ke) { 79 if(UserGroupInformation.isSecurityEnabled()) 80 throw new IllegalArgumentException("Can't get Kerberos configuration",ke); 81 else 82 defaultRealm=""; 83 } 84 } 85 86 /** 87 * Create a name from the full Kerberos principal name. 88 * @param name 89 */ KerberosName(String name)90 public KerberosName(String name) { 91 Matcher match = nameParser.matcher(name); 92 if (!match.matches()) { 93 if (name.contains("@")) { 94 throw new IllegalArgumentException("Malformed Kerberos name: " + name); 95 } else { 96 serviceName = name; 97 hostName = null; 98 realm = null; 99 } 100 } else { 101 serviceName = match.group(1); 102 hostName = match.group(3); 103 realm = match.group(4); 104 } 105 } 106 107 /** 108 * Get the configured default realm. 109 * @return the default realm from the krb5.conf 110 */ getDefaultRealm()111 public String getDefaultRealm() { 112 return defaultRealm; 113 } 114 115 /** 116 * Put the name back together from the parts. 117 */ 118 @Override toString()119 public String toString() { 120 StringBuilder result = new StringBuilder(); 121 result.append(serviceName); 122 if (hostName != null) { 123 result.append('/'); 124 result.append(hostName); 125 } 126 if (realm != null) { 127 result.append('@'); 128 result.append(realm); 129 } 130 return result.toString(); 131 } 132 133 /** 134 * Get the first component of the name. 135 * @return the first section of the Kerberos principal name 136 */ getServiceName()137 public String getServiceName() { 138 return serviceName; 139 } 140 141 /** 142 * Get the second component of the name. 143 * @return the second section of the Kerberos principal name, and may be null 144 */ getHostName()145 public String getHostName() { 146 return hostName; 147 } 148 149 /** 150 * Get the realm of the name. 151 * @return the realm of the name, may be null 152 */ getRealm()153 public String getRealm() { 154 return realm; 155 } 156 157 /** 158 * An encoding of a rule for translating kerberos names. 159 */ 160 private static class Rule { 161 private final boolean isDefault; 162 private final int numOfComponents; 163 private final String format; 164 private final Pattern match; 165 private final Pattern fromPattern; 166 private final String toPattern; 167 private final boolean repeat; 168 Rule()169 Rule() { 170 isDefault = true; 171 numOfComponents = 0; 172 format = null; 173 match = null; 174 fromPattern = null; 175 toPattern = null; 176 repeat = false; 177 } 178 Rule(int numOfComponents, String format, String match, String fromPattern, String toPattern, boolean repeat)179 Rule(int numOfComponents, String format, String match, String fromPattern, 180 String toPattern, boolean repeat) { 181 isDefault = false; 182 this.numOfComponents = numOfComponents; 183 this.format = format; 184 this.match = match == null ? null : Pattern.compile(match); 185 this.fromPattern = 186 fromPattern == null ? null : Pattern.compile(fromPattern); 187 this.toPattern = toPattern; 188 this.repeat = repeat; 189 } 190 191 @Override toString()192 public String toString() { 193 StringBuilder buf = new StringBuilder(); 194 if (isDefault) { 195 buf.append("DEFAULT"); 196 } else { 197 buf.append("RULE:["); 198 buf.append(numOfComponents); 199 buf.append(':'); 200 buf.append(format); 201 buf.append(']'); 202 if (match != null) { 203 buf.append('('); 204 buf.append(match); 205 buf.append(')'); 206 } 207 if (fromPattern != null) { 208 buf.append("s/"); 209 buf.append(fromPattern); 210 buf.append('/'); 211 buf.append(toPattern); 212 buf.append('/'); 213 if (repeat) { 214 buf.append('g'); 215 } 216 } 217 } 218 return buf.toString(); 219 } 220 221 /** 222 * Replace the numbered parameters of the form $n where n is from 1 to 223 * the length of params. Normal text is copied directly and $n is replaced 224 * by the corresponding parameter. 225 * @param format the string to replace parameters again 226 * @param params the list of parameters 227 * @return the generated string with the parameter references replaced. 228 * @throws BadFormatString 229 */ replaceParameters(String format, String[] params)230 static String replaceParameters(String format, 231 String[] params) throws BadFormatString { 232 Matcher match = parameterPattern.matcher(format); 233 int start = 0; 234 StringBuilder result = new StringBuilder(); 235 while (start < format.length() && match.find(start)) { 236 result.append(match.group(1)); 237 String paramNum = match.group(3); 238 if (paramNum != null) { 239 try { 240 int num = Integer.parseInt(paramNum); 241 if (num < 0 || num > params.length) { 242 throw new BadFormatString("index " + num + " from " + format + 243 " is outside of the valid range 0 to " + 244 (params.length - 1)); 245 } 246 result.append(params[num]); 247 } catch (NumberFormatException nfe) { 248 throw new BadFormatString("bad format in username mapping in " + 249 paramNum, nfe); 250 } 251 252 } 253 start = match.end(); 254 } 255 return result.toString(); 256 } 257 258 /** 259 * Replace the matches of the from pattern in the base string with the value 260 * of the to string. 261 * @param base the string to transform 262 * @param from the pattern to look for in the base string 263 * @param to the string to replace matches of the pattern with 264 * @param repeat whether the substitution should be repeated 265 * @return 266 */ replaceSubstitution(String base, Pattern from, String to, boolean repeat)267 static String replaceSubstitution(String base, Pattern from, String to, 268 boolean repeat) { 269 Matcher match = from.matcher(base); 270 if (repeat) { 271 return match.replaceAll(to); 272 } else { 273 return match.replaceFirst(to); 274 } 275 } 276 277 /** 278 * Try to apply this rule to the given name represented as a parameter 279 * array. 280 * @param params first element is the realm, second and later elements are 281 * are the components of the name "a/b@FOO" -> {"FOO", "a", "b"} 282 * @return the short name if this rule applies or null 283 * @throws IOException throws if something is wrong with the rules 284 */ apply(String[] params)285 String apply(String[] params) throws IOException { 286 String result = null; 287 if (isDefault) { 288 if (defaultRealm.equals(params[0])) { 289 result = params[1]; 290 } 291 } else if (params.length - 1 == numOfComponents) { 292 String base = replaceParameters(format, params); 293 if (match == null || match.matcher(base).matches()) { 294 if (fromPattern == null) { 295 result = base; 296 } else { 297 result = replaceSubstitution(base, fromPattern, toPattern, repeat); 298 } 299 } 300 } 301 if (result != null && nonSimplePattern.matcher(result).find()) { 302 throw new NoMatchingRule("Non-simple name " + result + 303 " after auth_to_local rule " + this); 304 } 305 return result; 306 } 307 } 308 parseRules(String rules)309 static List<Rule> parseRules(String rules) { 310 List<Rule> result = new ArrayList<Rule>(); 311 String remaining = rules.trim(); 312 while (remaining.length() > 0) { 313 Matcher matcher = ruleParser.matcher(remaining); 314 if (!matcher.lookingAt()) { 315 throw new IllegalArgumentException("Invalid rule: " + remaining); 316 } 317 if (matcher.group(2) != null) { 318 result.add(new Rule()); 319 } else { 320 result.add(new Rule(Integer.parseInt(matcher.group(4)), 321 matcher.group(5), 322 matcher.group(7), 323 matcher.group(9), 324 matcher.group(10), 325 "g".equals(matcher.group(11)))); 326 } 327 remaining = remaining.substring(matcher.end()); 328 } 329 return result; 330 } 331 332 /** 333 * Set the static configuration to get the rules. 334 * @param conf the new configuration 335 * @throws IOException 336 */ setConfiguration(Configuration conf)337 public static void setConfiguration(Configuration conf) throws IOException { 338 String ruleString = conf.get("hadoop.security.auth_to_local", "DEFAULT"); 339 rules = parseRules(ruleString); 340 } 341 342 /** 343 * Set the rules. 344 * @param ruleString the rules string. 345 */ setRules(String ruleString)346 public static void setRules(String ruleString) { 347 rules = parseRules(ruleString); 348 } 349 350 @SuppressWarnings("serial") 351 public static class BadFormatString extends IOException { BadFormatString(String msg)352 BadFormatString(String msg) { 353 super(msg); 354 } BadFormatString(String msg, Throwable err)355 BadFormatString(String msg, Throwable err) { 356 super(msg, err); 357 } 358 } 359 360 @SuppressWarnings("serial") 361 public static class NoMatchingRule extends IOException { NoMatchingRule(String msg)362 NoMatchingRule(String msg) { 363 super(msg); 364 } 365 } 366 367 /** 368 * Get the translation of the principal name into an operating system 369 * user name. 370 * @return the short name 371 * @throws IOException 372 */ getShortName()373 public String getShortName() throws IOException { 374 String[] params; 375 if (hostName == null) { 376 // if it is already simple, just return it 377 if (realm == null) { 378 return serviceName; 379 } 380 params = new String[]{realm, serviceName}; 381 } else { 382 params = new String[]{realm, serviceName, hostName}; 383 } 384 for(Rule r: rules) { 385 String result = r.apply(params); 386 if (result != null) { 387 return result; 388 } 389 } 390 throw new NoMatchingRule("No rules applied to " + toString()); 391 } 392 printRules()393 public static void printRules() throws IOException { 394 int i = 0; 395 for(Rule r: rules) { 396 System.out.println(++i + " " + r); 397 } 398 } 399 main(String[] args)400 public static void main(String[] args) throws Exception { 401 for(String arg: args) { 402 KerberosName name = new KerberosName(arg); 403 System.out.println("Name: " + name + " to " + name.getShortName()); 404 } 405 } 406 }