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