org.apache.hadoop.fs.s3a.auth.MarshalledCredentials Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of hadoop-aws Show documentation
Show all versions of hadoop-aws Show documentation
This module contains code to support integration with Amazon Web Services.
It also declares the dependencies needed to work with AWS services.
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.hadoop.fs.s3a.auth;
import java.io.DataInput;
import java.io.DataOutput;
import java.io.IOException;
import java.io.Serializable;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import org.apache.hadoop.thirdparty.com.google.common.annotations.VisibleForTesting;
import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.s3a.S3AUtils;
import org.apache.hadoop.fs.s3a.auth.delegation.DelegationTokenIOException;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.Writable;
import static java.util.Objects.requireNonNull;
import static org.apache.commons.lang3.StringUtils.isNotEmpty;
import static org.apache.hadoop.fs.s3a.Constants.ACCESS_KEY;
import static org.apache.hadoop.fs.s3a.Constants.SECRET_KEY;
import static org.apache.hadoop.fs.s3a.Constants.SESSION_TOKEN;
/**
* Stores the credentials for a session or for a full login.
* This structure is {@link Writable}, so can be marshalled inside a
* delegation token.
*
* The class is designed so that keys inside are kept non-null; to be
* unset just set them to the empty string. This is to simplify marshalling.
*
* Important: Add no references to any AWS SDK class, to
* ensure it can be safely deserialized whenever the relevant token
* identifier of a token type declared in this JAR is examined.
*/
@InterfaceAudience.Private
public final class MarshalledCredentials implements Writable, Serializable {
/**
* Error text on invalid non-empty credentials: {@value}.
*/
@VisibleForTesting
public static final String INVALID_CREDENTIALS
= "Invalid AWS credentials";
/**
* How long can any of the secrets be: {@value}.
* This is much longer than the current tokens, but leaves space for
* future enhancements.
*/
private static final int MAX_SECRET_LENGTH = 8192;
private static final long serialVersionUID = 8444610385533920692L;
/**
* Access key of IAM account.
*/
private String accessKey = "";
/**
* Secret key of IAM account.
*/
private String secretKey = "";
/**
* Optional session token.
* If non-empty: the credentials can be converted into
* session credentials.
*/
private String sessionToken = "";
/**
* ARN of a role. Purely for diagnostics.
*/
private String roleARN = "";
/**
* Expiry time milliseconds in UTC; the {@code Java.Util.Date} value.
* 0 means "does not expire/unknown".
*/
private long expiration;
/**
* Constructor.
*/
public MarshalledCredentials() {
}
/**
* Create from a set of properties.
* No expiry time is expected/known here.
* @param accessKey access key
* @param secretKey secret key
* @param sessionToken session token
*/
public MarshalledCredentials(
final String accessKey,
final String secretKey,
final String sessionToken) {
this();
this.accessKey = requireNonNull(accessKey);
this.secretKey = requireNonNull(secretKey);
this.sessionToken = sessionToken == null ? "" : sessionToken;
}
public String getAccessKey() {
return accessKey;
}
public String getSecretKey() {
return secretKey;
}
public String getSessionToken() {
return sessionToken;
}
/**
* Expiration; will be 0 for none known.
* @return any expiration timestamp
*/
public long getExpiration() {
return expiration;
}
public void setExpiration(final long expiration) {
this.expiration = expiration;
}
/**
* Get a temporal representing the time of expiration, if there
* is one.
* This is here to wrap up expectations about timestamps and zones.
* @return the expiration time.
*/
public Optional getExpirationDateTime() {
return expiration == 0
? Optional.empty()
: Optional.of(
OffsetDateTime.ofInstant(
new Date(expiration).toInstant(),
ZoneOffset.UTC));
}
public String getRoleARN() {
return roleARN;
}
public void setRoleARN(String roleARN) {
this.roleARN = requireNonNull(roleARN);
}
public void setAccessKey(final String accessKey) {
this.accessKey = requireNonNull(accessKey, "access key");
}
public void setSecretKey(final String secretKey) {
this.secretKey = requireNonNull(secretKey, "secret key");
}
public void setSessionToken(final String sessionToken) {
this.sessionToken = requireNonNull(sessionToken, "session token");
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
MarshalledCredentials that = (MarshalledCredentials) o;
return expiration == that.expiration &&
Objects.equals(accessKey, that.accessKey) &&
Objects.equals(secretKey, that.secretKey) &&
Objects.equals(sessionToken, that.sessionToken) &&
Objects.equals(roleARN, that.roleARN);
}
@Override
public int hashCode() {
return Objects.hash(accessKey, secretKey, sessionToken, roleARN,
expiration);
}
/**
* String value MUST NOT include any secrets.
* @return a string value for logging.
*/
@Override
public String toString() {
if (isEmpty()) {
return "Empty credentials";
}
String validity = isValid(CredentialTypeRequired.AnyNonEmpty)
? "valid"
: "invalid";
if (!hasSessionToken()) {
// full credentials have the simplest string value.
return "full credentials (" + validity + ")";
} else {
// session/role credentials may have an expiry and role ARN.
return String.format("session credentials, expiry %s; %s(%s)",
getExpirationDateTime()
.map(x -> x.format(DateTimeFormatter.ISO_DATE_TIME))
.orElse("unknown"),
(isNotEmpty(roleARN)
? ("role \"" + roleARN + "\" ")
: ""),
validity);
}
}
/**
* Is this empty: does it contain any credentials at all?
* This test returns true if either the access key or secret key is empty.
* @return true if there are no credentials.
*/
public boolean isEmpty() {
return !(isNotEmpty(accessKey) && isNotEmpty(secretKey));
}
/**
* Is this a valid set of credentials tokens?
* @param required credential type required.
* @return true if the requirements are met.
*/
public boolean isValid(final CredentialTypeRequired required) {
if (accessKey == null || secretKey == null || sessionToken == null) {
// null fields are not permitted, empty is OK for marshalling around.
return false;
}
// now look at whether values are set/unset.
boolean hasAccessAndSecretKeys = isNotEmpty(accessKey)
&& isNotEmpty(secretKey);
boolean hasSessionToken = hasSessionToken();
switch (required) {
case AnyIncludingEmpty:
// this is simplest.
return true;
case Empty:
// empty. ignore session value if the other keys are unset.
return !hasAccessAndSecretKeys;
case AnyNonEmpty:
// just look for the access key and secret key being non-empty
return hasAccessAndSecretKeys;
case FullOnly:
return hasAccessAndSecretKeys && !hasSessionToken;
case SessionOnly:
return hasAccessAndSecretKeys && hasSessionToken();
// this is here to keep the IDE quiet
default:
return false;
}
}
/**
* Does this set of credentials have a session token.
* @return true if there's a session token.
*/
public boolean hasSessionToken() {
return isNotEmpty(sessionToken);
}
/**
* Write the token.
* Only works if valid.
* @param out stream to serialize to.
* @throws IOException if the serialization failed.
*/
@Override
public void write(DataOutput out) throws IOException {
validate("Writing " + this + ": ",
CredentialTypeRequired.AnyIncludingEmpty);
Text.writeString(out, accessKey);
Text.writeString(out, secretKey);
Text.writeString(out, sessionToken);
Text.writeString(out, roleARN);
out.writeLong(expiration);
}
/**
* Read in the fields.
* @throws IOException IO problem
*/
@Override
public void readFields(DataInput in) throws IOException {
accessKey = Text.readString(in, MAX_SECRET_LENGTH);
secretKey = Text.readString(in, MAX_SECRET_LENGTH);
sessionToken = Text.readString(in, MAX_SECRET_LENGTH);
roleARN = Text.readString(in, MAX_SECRET_LENGTH);
expiration = in.readLong();
}
/**
* Verify that a set of credentials is valid.
* @throws DelegationTokenIOException if they aren't
* @param message message to prefix errors;
* @param typeRequired credential type required.
*/
public void validate(final String message,
final CredentialTypeRequired typeRequired) throws IOException {
if (!isValid(typeRequired)) {
throw new DelegationTokenIOException(message
+ buildInvalidCredentialsError(typeRequired));
}
}
/**
* Build an error string for when the credentials do not match
* those required.
* @param typeRequired credential type required.
* @return an error string.
*/
public String buildInvalidCredentialsError(
final CredentialTypeRequired typeRequired) {
if (isEmpty()) {
return " " + MarshalledCredentialBinding.NO_AWS_CREDENTIALS;
} else {
return " " + INVALID_CREDENTIALS
+ " in " + toString() + " required: " + typeRequired;
}
}
/**
* Patch a configuration with the secrets.
* This does not set any per-bucket options (it doesn't know the bucket...).
* Warning: once done the configuration must be considered sensitive.
* @param config configuration to patch
*/
public void setSecretsInConfiguration(Configuration config) {
config.set(ACCESS_KEY, accessKey);
config.set(SECRET_KEY, secretKey);
S3AUtils.setIfDefined(config, SESSION_TOKEN, sessionToken,
"session credentials");
}
/**
* Return a set of empty credentials.
* These can be marshalled, but not used for login.
* @return a new set of credentials.
*/
public static MarshalledCredentials empty() {
return new MarshalledCredentials("", "", "");
}
/**
* Enumeration of credential types for use in validation methods.
*/
public enum CredentialTypeRequired {
/** No entry at all. */
Empty("None"),
/** Any credential type including "unset". */
AnyIncludingEmpty("Full, Session or None"),
/** Any credential type is OK. */
AnyNonEmpty("Full or Session"),
/** The credentials must be session or role credentials. */
SessionOnly("Session"),
/** Full credentials are required. */
FullOnly("Full");
private final String text;
CredentialTypeRequired(final String text) {
this.text = text;
}
public String getText() {
return text;
}
@Override
public String toString() {
return getText();
}
}
}