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