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}