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}