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 }