001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.hadoop.security.token;
020
021import com.google.common.collect.Maps;
022import org.apache.commons.codec.binary.Base64;
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025import org.apache.hadoop.classification.InterfaceAudience;
026import org.apache.hadoop.classification.InterfaceStability;
027import org.apache.hadoop.conf.Configuration;
028import org.apache.hadoop.io.*;
029import org.apache.hadoop.util.ReflectionUtils;
030
031import java.io.*;
032import java.util.Arrays;
033import java.util.Map;
034import java.util.ServiceLoader;
035
036/**
037 * The client-side form of the token.
038 */
039@InterfaceAudience.Public
040@InterfaceStability.Evolving
041public class Token<T extends TokenIdentifier> implements Writable {
042  public static final Log LOG = LogFactory.getLog(Token.class);
043  
044  private static Map<Text, Class<? extends TokenIdentifier>> tokenKindMap;
045  
046  private byte[] identifier;
047  private byte[] password;
048  private Text kind;
049  private Text service;
050  private TokenRenewer renewer;
051  
052  /**
053   * Construct a token given a token identifier and a secret manager for the
054   * type of the token identifier.
055   * @param id the token identifier
056   * @param mgr the secret manager
057   */
058  public Token(T id, SecretManager<T> mgr) {
059    password = mgr.createPassword(id);
060    identifier = id.getBytes();
061    kind = id.getKind();
062    service = new Text();
063  }
064 
065  /**
066   * Construct a token from the components.
067   * @param identifier the token identifier
068   * @param password the token's password
069   * @param kind the kind of token
070   * @param service the service for this token
071   */
072  public Token(byte[] identifier, byte[] password, Text kind, Text service) {
073    this.identifier = (identifier == null)? new byte[0] : identifier;
074    this.password = (password == null)? new byte[0] : password;
075    this.kind = (kind == null)? new Text() : kind;
076    this.service = (service == null)? new Text() : service;
077  }
078
079  /**
080   * Default constructor
081   */
082  public Token() {
083    identifier = new byte[0];
084    password = new byte[0];
085    kind = new Text();
086    service = new Text();
087  }
088
089  /**
090   * Clone a token.
091   * @param other the token to clone
092   */
093  public Token(Token<T> other) {
094    this.identifier = other.identifier;
095    this.password = other.password;
096    this.kind = other.kind;
097    this.service = other.service;
098  }
099
100  /**
101   * Get the token identifier's byte representation
102   * @return the token identifier's byte representation
103   */
104  public byte[] getIdentifier() {
105    return identifier;
106  }
107  
108  private static Class<? extends TokenIdentifier>
109      getClassForIdentifier(Text kind) {
110    Class<? extends TokenIdentifier> cls = null;
111    synchronized (Token.class) {
112      if (tokenKindMap == null) {
113        tokenKindMap = Maps.newHashMap();
114        for (TokenIdentifier id : ServiceLoader.load(TokenIdentifier.class)) {
115          tokenKindMap.put(id.getKind(), id.getClass());
116        }
117      }
118      cls = tokenKindMap.get(kind);
119    }
120    if (cls == null) {
121      LOG.warn("Cannot find class for token kind " + kind);
122      return null;
123    }
124    return cls;
125  }
126  
127  /**
128   * Get the token identifier object, or null if it could not be constructed
129   * (because the class could not be loaded, for example).
130   * @return the token identifier, or null
131   * @throws IOException 
132   */
133  @SuppressWarnings("unchecked")
134  public T decodeIdentifier() throws IOException {
135    Class<? extends TokenIdentifier> cls = getClassForIdentifier(getKind());
136    if (cls == null) {
137      return null;
138    }
139    TokenIdentifier tokenIdentifier = ReflectionUtils.newInstance(cls, null);
140    ByteArrayInputStream buf = new ByteArrayInputStream(identifier);
141    DataInputStream in = new DataInputStream(buf);  
142    tokenIdentifier.readFields(in);
143    in.close();
144    return (T) tokenIdentifier;
145  }
146  
147  /**
148   * Get the token password/secret
149   * @return the token password/secret
150   */
151  public byte[] getPassword() {
152    return password;
153  }
154  
155  /**
156   * Get the token kind
157   * @return the kind of the token
158   */
159  public synchronized Text getKind() {
160    return kind;
161  }
162
163  /**
164   * Set the token kind. This is only intended to be used by services that
165   * wrap another service's token.
166   * @param newKind
167   */
168  @InterfaceAudience.Private
169  public synchronized void setKind(Text newKind) {
170    kind = newKind;
171    renewer = null;
172  }
173
174  /**
175   * Get the service on which the token is supposed to be used
176   * @return the service name
177   */
178  public Text getService() {
179    return service;
180  }
181  
182  /**
183   * Set the service on which the token is supposed to be used
184   * @param newService the service name
185   */
186  public void setService(Text newService) {
187    service = newService;
188  }
189
190  /**
191   * Indicates whether the token is a clone.  Used by HA failover proxy
192   * to indicate a token should not be visible to the user via
193   * UGI.getCredentials()
194   */
195  @InterfaceAudience.Private
196  @InterfaceStability.Unstable
197  public static class PrivateToken<T extends TokenIdentifier> extends Token<T> {
198    public PrivateToken(Token<T> token) {
199      super(token);
200    }
201  }
202
203  @Override
204  public void readFields(DataInput in) throws IOException {
205    int len = WritableUtils.readVInt(in);
206    if (identifier == null || identifier.length != len) {
207      identifier = new byte[len];
208    }
209    in.readFully(identifier);
210    len = WritableUtils.readVInt(in);
211    if (password == null || password.length != len) {
212      password = new byte[len];
213    }
214    in.readFully(password);
215    kind.readFields(in);
216    service.readFields(in);
217  }
218
219  @Override
220  public void write(DataOutput out) throws IOException {
221    WritableUtils.writeVInt(out, identifier.length);
222    out.write(identifier);
223    WritableUtils.writeVInt(out, password.length);
224    out.write(password);
225    kind.write(out);
226    service.write(out);
227  }
228
229  /**
230   * Generate a string with the url-quoted base64 encoded serialized form
231   * of the Writable.
232   * @param obj the object to serialize
233   * @return the encoded string
234   * @throws IOException
235   */
236  private static String encodeWritable(Writable obj) throws IOException {
237    DataOutputBuffer buf = new DataOutputBuffer();
238    obj.write(buf);
239    Base64 encoder = new Base64(0, null, true);
240    byte[] raw = new byte[buf.getLength()];
241    System.arraycopy(buf.getData(), 0, raw, 0, buf.getLength());
242    return encoder.encodeToString(raw);
243  }
244  
245  /**
246   * Modify the writable to the value from the newValue
247   * @param obj the object to read into
248   * @param newValue the string with the url-safe base64 encoded bytes
249   * @throws IOException
250   */
251  private static void decodeWritable(Writable obj, 
252                                     String newValue) throws IOException {
253    Base64 decoder = new Base64(0, null, true);
254    DataInputBuffer buf = new DataInputBuffer();
255    byte[] decoded = decoder.decode(newValue);
256    buf.reset(decoded, decoded.length);
257    obj.readFields(buf);
258  }
259
260  /**
261   * Encode this token as a url safe string
262   * @return the encoded string
263   * @throws IOException
264   */
265  public String encodeToUrlString() throws IOException {
266    return encodeWritable(this);
267  }
268  
269  /**
270   * Decode the given url safe string into this token.
271   * @param newValue the encoded string
272   * @throws IOException
273   */
274  public void decodeFromUrlString(String newValue) throws IOException {
275    decodeWritable(this, newValue);
276  }
277  
278  @SuppressWarnings("unchecked")
279  @Override
280  public boolean equals(Object right) {
281    if (this == right) {
282      return true;
283    } else if (right == null || getClass() != right.getClass()) {
284      return false;
285    } else {
286      Token<T> r = (Token<T>) right;
287      return Arrays.equals(identifier, r.identifier) &&
288             Arrays.equals(password, r.password) &&
289             kind.equals(r.kind) &&
290             service.equals(r.service);
291    }
292  }
293  
294  @Override
295  public int hashCode() {
296    return WritableComparator.hashBytes(identifier, identifier.length);
297  }
298  
299  private static void addBinaryBuffer(StringBuilder buffer, byte[] bytes) {
300    for (int idx = 0; idx < bytes.length; idx++) {
301      // if not the first, put a blank separator in
302      if (idx != 0) {
303        buffer.append(' ');
304      }
305      String num = Integer.toHexString(0xff & bytes[idx]);
306      // if it is only one digit, add a leading 0.
307      if (num.length() < 2) {
308        buffer.append('0');
309      }
310      buffer.append(num);
311    }
312  }
313  
314  private void identifierToString(StringBuilder buffer) {
315    T id = null;
316    try {
317      id = decodeIdentifier();
318    } catch (IOException e) {
319      // handle in the finally block
320    } finally {
321      if (id != null) {
322        buffer.append("(").append(id).append(")");
323      } else {
324        addBinaryBuffer(buffer, identifier);
325      }
326    }
327  }
328
329  @Override
330  public String toString() {
331    StringBuilder buffer = new StringBuilder();
332    buffer.append("Kind: ");
333    buffer.append(kind.toString());
334    buffer.append(", Service: ");
335    buffer.append(service.toString());
336    buffer.append(", Ident: ");
337    identifierToString(buffer);
338    return buffer.toString();
339  }
340  
341  private static ServiceLoader<TokenRenewer> renewers =
342      ServiceLoader.load(TokenRenewer.class);
343
344  private synchronized TokenRenewer getRenewer() throws IOException {
345    if (renewer != null) {
346      return renewer;
347    }
348    renewer = TRIVIAL_RENEWER;
349    synchronized (renewers) {
350      for (TokenRenewer canidate : renewers) {
351        if (canidate.handleKind(this.kind)) {
352          renewer = canidate;
353          return renewer;
354        }
355      }
356    }
357    LOG.warn("No TokenRenewer defined for token kind " + this.kind);
358    return renewer;
359  }
360
361  /**
362   * Is this token managed so that it can be renewed or cancelled?
363   * @return true, if it can be renewed and cancelled.
364   */
365  public boolean isManaged() throws IOException {
366    return getRenewer().isManaged(this);
367  }
368
369  /**
370   * Renew this delegation token
371   * @return the new expiration time
372   * @throws IOException
373   * @throws InterruptedException
374   */
375  public long renew(Configuration conf
376                    ) throws IOException, InterruptedException {
377    return getRenewer().renew(this, conf);
378  }
379  
380  /**
381   * Cancel this delegation token
382   * @throws IOException
383   * @throws InterruptedException
384   */
385  public void cancel(Configuration conf
386                     ) throws IOException, InterruptedException {
387    getRenewer().cancel(this, conf);
388  }
389  
390  /**
391   * A trivial renewer for token kinds that aren't managed. Sub-classes need
392   * to implement getKind for their token kind.
393   */
394  @InterfaceAudience.LimitedPrivate({"HDFS", "MapReduce"})
395  @InterfaceStability.Evolving
396  public static class TrivialRenewer extends TokenRenewer {
397    
398    // define the kind for this renewer
399    protected Text getKind() {
400      return null;
401    }
402
403    @Override
404    public boolean handleKind(Text kind) {
405      return kind.equals(getKind());
406    }
407
408    @Override
409    public boolean isManaged(Token<?> token) {
410      return false;
411    }
412
413    @Override
414    public long renew(Token<?> token, Configuration conf) {
415      throw new UnsupportedOperationException("Token renewal is not supported "+
416                                              " for " + token.kind + " tokens");
417    }
418
419    @Override
420    public void cancel(Token<?> token, Configuration conf) throws IOException,
421        InterruptedException {
422      throw new UnsupportedOperationException("Token cancel is not supported " +
423          " for " + token.kind + " tokens");
424    }
425
426  }
427  private static final TokenRenewer TRIVIAL_RENEWER = new TrivialRenewer();
428}