Skip to content

Commit

Permalink
add IAM AWS credential provider (#1038)
Browse files Browse the repository at this point in the history
  • Loading branch information
balamurugana authored Sep 7, 2020
1 parent 8c153fb commit 3e5ba0c
Show file tree
Hide file tree
Showing 9 changed files with 293 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import io.minio.errors.XmlParserException;
import java.io.IOException;
import java.security.ProviderException;
import java.util.Arrays;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
Expand Down Expand Up @@ -47,14 +48,13 @@ public synchronized Credentials fetch() {

try (Response response = httpClient.newCall(getRequest()).execute()) {
if (!response.isSuccessful()) {
throw new IllegalStateException(
"STS service failed with HTTP status code " + response.code());
throw new ProviderException("STS service failed with HTTP status code " + response.code());
}

credentials = parseResponse(response);
return credentials;
} catch (XmlParserException | IOException e) {
throw new IllegalStateException("Unable to parse STS response", e);
throw new ProviderException("Unable to parse STS response", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.io.IOException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.ProviderException;
import java.time.ZonedDateTime;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
Expand Down Expand Up @@ -123,7 +124,7 @@ protected Request getRequest() {
secretKey,
contentSha256);
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new IllegalStateException("Signature calculation failed", e);
throw new ProviderException("Signature calculation failed", e);
}
}

Expand Down
9 changes: 5 additions & 4 deletions api/src/main/java/io/minio/credentials/AwsConfigProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import java.io.Reader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.ProviderException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
Expand Down Expand Up @@ -73,7 +74,7 @@ public Credentials fetch() {
Map<String, Properties> result = unmarshal(new InputStreamReader(is, StandardCharsets.UTF_8));
Properties values = result.get(profile);
if (values == null) {
throw new IllegalStateException(
throw new ProviderException(
"Profile " + profile + " does not exist in AWS credential file");
}

Expand All @@ -82,18 +83,18 @@ public Credentials fetch() {
String sessionToken = values.getProperty("aws_session_token");

if (accessKey == null) {
throw new IllegalStateException(
throw new ProviderException(
"Access key does not exist in profile " + profile + " in AWS credential file");
}

if (secretKey == null) {
throw new IllegalStateException(
throw new ProviderException(
"Secret key does not exist in profile " + profile + " in AWS credential file");
}

return new Credentials(accessKey, secretKey, sessionToken, null);
} catch (IOException e) {
throw new IllegalStateException("Unable to read AWS credential file", e);
throw new ProviderException("Unable to read AWS credential file", e);
}
}

Expand Down
7 changes: 4 additions & 3 deletions api/src/main/java/io/minio/credentials/ChainedProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package io.minio.credentials;

import java.security.ProviderException;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nonnull;
Expand All @@ -40,7 +41,7 @@ public synchronized Credentials fetch() {
try {
credentials = currentProvider.fetch();
return credentials;
} catch (IllegalStateException e) {
} catch (ProviderException e) {
// Ignore and fallback to iteration.
}
}
Expand All @@ -50,11 +51,11 @@ public synchronized Credentials fetch() {
credentials = provider.fetch();
currentProvider = provider;
return credentials;
} catch (IllegalStateException e) {
} catch (ProviderException e) {
// Ignore and continue to next iteration.
}
}

throw new IllegalStateException("All providers fail to fetch credentials");
throw new ProviderException("All providers fail to fetch credentials");
}
}
231 changes: 231 additions & 0 deletions api/src/main/java/io/minio/credentials/IamAwsProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
/*
* MinIO Java SDK for Amazon S3 Compatible Cloud Storage, (C) 2020 MinIO, Inc.
*
* Licensed 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
*
* https://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 io.minio.credentials;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.minio.messages.ResponseDate;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.security.ProviderException;
import java.util.Arrays;
import java.util.Objects;
import javax.annotation.Nullable;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Protocol;
import okhttp3.Request;
import okhttp3.Response;

/**
* Credential provider using <a
* href="http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html">IAM roles
* for Amazon EC2</a>.
*/
public class IamAwsProvider extends EnvironmentProvider {
// Custom endpoint to fetch IAM role credentials.
private final HttpUrl customEndpoint;
private final OkHttpClient httpClient;
private final ObjectMapper mapper;
private Credentials credentials;

public IamAwsProvider(@Nullable String customEndpoint, @Nullable OkHttpClient customHttpClient) {
this.customEndpoint =
(customEndpoint != null)
? Objects.requireNonNull(HttpUrl.parse(customEndpoint), "Invalid custom endpoint")
: null;
// HTTP/1.1 is only supported in default client because of HTTP/2 in OkHttpClient cause 5
// minutes timeout on program exit.
this.httpClient =
(customHttpClient != null)
? customHttpClient
: new OkHttpClient().newBuilder().protocols(Arrays.asList(Protocol.HTTP_1_1)).build();
this.mapper = new ObjectMapper();
this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
this.mapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
}

private void checkLoopbackHost(HttpUrl url) {
try {
for (InetAddress addr : InetAddress.getAllByName(url.host())) {
if (!addr.isLoopbackAddress()) {
throw new ProviderException(url.host() + " is not loopback only host");
}
}
} catch (UnknownHostException e) {
throw new ProviderException("Host in " + url + " is not loopback address");
}
}

private Credentials fetchCredentials(String tokenFile) {
HttpUrl url = this.customEndpoint;
if (url == null) {
String region = getProperty("AWS_REGION");
url =
HttpUrl.parse(
(region == null)
? "https://sts.amazonaws.com"
: "https://sts." + region + ".amazonaws.com");
}

Provider provider =
new WebIdentityProvider(
() -> {
try {
byte[] data = Files.readAllBytes(Paths.get(tokenFile));
return new Jwt(new String(data, StandardCharsets.UTF_8), 0);
} catch (IOException e) {
throw new ProviderException("Error in reading file " + tokenFile, e);
}
},
url.toString(),
null,
null,
getProperty("AWS_ROLE_ARN"),
getProperty("AWS_ROLE_SESSION_NAME"),
httpClient);
credentials = provider.fetch();
return credentials;
}

private Credentials fetchCredentials(HttpUrl url) {
try (Response response =
httpClient.newCall(new Request.Builder().url(url).method("GET", null).build()).execute()) {
if (!response.isSuccessful()) {
throw new ProviderException(url + " failed with HTTP status code " + response.code());
}

EcsCredentials creds = mapper.readValue(response.body().charStream(), EcsCredentials.class);
if (!"Success".equals(creds.code())) {
throw new ProviderException(url + " failed with message " + creds.message());
}
return creds.toCredentials();
} catch (IOException e) {
throw new ProviderException("Unable to parse response", e);
}
}

private String getIamRoleName(HttpUrl url) {
String[] roleNames = null;
try (Response response =
httpClient.newCall(new Request.Builder().url(url).method("GET", null).build()).execute()) {
if (!response.isSuccessful()) {
throw new ProviderException(url + " failed with HTTP status code " + response.code());
}

roleNames = response.body().string().split("\\R");
} catch (IOException e) {
throw new ProviderException("Unable to parse response", e);
}

if (roleNames.length == 0) {
throw new ProviderException("No IAM roles attached to EC2 service " + url);
}

return roleNames[0];
}

private HttpUrl getIamRoleNamedUrl() {
HttpUrl url = this.customEndpoint;
if (url == null) {
url = HttpUrl.parse("http://169.254.169.254/latest/meta-data/iam/security-credentials/");
} else {
url =
new HttpUrl.Builder()
.scheme(url.scheme())
.host(url.host())
.addPathSegments("latest/meta-data/iam/security-credentials/")
.build();
}

String roleName = getIamRoleName(url);
return url.newBuilder().addPathSegment(roleName).build();
}

@Override
public synchronized Credentials fetch() {
if (credentials != null && !credentials.isExpired()) {
return credentials;
}

HttpUrl url = this.customEndpoint;
String tokenFile = getProperty("AWS_WEB_IDENTITY_TOKEN_FILE");
if (tokenFile != null) {
credentials = fetchCredentials(tokenFile);
return credentials;
}

if (getProperty("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != null) {
if (url == null) {
url =
new HttpUrl.Builder()
.scheme("http")
.host("169.254.170.2")
.addPathSegments(getProperty("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI"))
.build();
}
} else if (getProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI") != null) {
if (url == null) {
url = HttpUrl.parse(getProperty("AWS_CONTAINER_CREDENTIALS_FULL_URI"));
}
checkLoopbackHost(url);
} else {
url = getIamRoleNamedUrl();
}

credentials = fetchCredentials(url);
return credentials;
}

public static class EcsCredentials {
@JsonProperty("AccessKeyID")
private String accessKey;

@JsonProperty("SecretAccessKey")
private String secretKey;

@JsonProperty("Token")
private String sessionToken;

@JsonProperty("Expiration")
private ResponseDate expiration;

@JsonProperty("Code")
private String code;

@JsonProperty("Message")
private String message;

public String code() {
return this.code;
}

public String message() {
return this.message;
}

public Credentials toCredentials() {
return new Credentials(accessKey, secretKey, sessionToken, expiration);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Paths;
import java.security.ProviderException;
import java.util.Locale;
import java.util.Map;
import javax.annotation.Nullable;
Expand Down Expand Up @@ -84,26 +85,26 @@ public Credentials fetch() {
mapper.readValue(new InputStreamReader(is, StandardCharsets.UTF_8), McConfig.class);
Map<String, String> values = config.get(alias);
if (values == null) {
throw new IllegalStateException(
throw new ProviderException(
"Alias " + alias + " does not exist in MinioClient configuration file");
}

String accessKey = values.get("accessKey");
String secretKey = values.get("secretKey");

if (accessKey == null) {
throw new IllegalStateException(
throw new ProviderException(
"Access key does not exist in alias " + alias + " in MinioClient configuration file");
}

if (secretKey == null) {
throw new IllegalStateException(
throw new ProviderException(
"Secret key does not exist in alias " + alias + " in MinioClient configuration file");
}

return new Credentials(accessKey, secretKey, null, null);
} catch (IOException e) {
throw new IllegalStateException("Unable to read MinioClient configuration file", e);
throw new ProviderException("Unable to read MinioClient configuration file", e);
}
}

Expand Down
3 changes: 2 additions & 1 deletion examples/MinioClientWithClientGrantsProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import io.minio.credentials.Jwt;
import io.minio.credentials.Provider;
import java.io.IOException;
import java.security.ProviderException;
import java.util.Objects;
import javax.annotation.Nonnull;
import okhttp3.FormBody;
Expand Down Expand Up @@ -61,7 +62,7 @@ static Jwt getJwt(
.withFieldVisibility(JsonAutoDetect.Visibility.ANY));
return mapper.readValue(response.body().charStream(), Jwt.class);
} catch (IOException e) {
throw new IllegalStateException(e);
throw new ProviderException(e);
}
}

Expand Down
Loading

0 comments on commit 3e5ba0c

Please sign in to comment.