diff --git a/api/src/main/java/io/minio/Digest.java b/api/src/main/java/io/minio/Digest.java index 7108bf826..baa101015 100644 --- a/api/src/main/java/io/minio/Digest.java +++ b/api/src/main/java/io/minio/Digest.java @@ -28,7 +28,7 @@ import java.util.Locale; /** Various global static functions used. */ -class Digest { +public class Digest { /** Private constructor. */ private Digest() {} diff --git a/api/src/main/java/io/minio/MinioClient.java b/api/src/main/java/io/minio/MinioClient.java index 806cc48f7..942274f08 100644 --- a/api/src/main/java/io/minio/MinioClient.java +++ b/api/src/main/java/io/minio/MinioClient.java @@ -1033,7 +1033,13 @@ protected Response execute( Credentials creds = (provider == null) ? null : provider.fetch(); Request request = createRequest(url, method, headerMap, body, length, creds); if (creds != null) { - request = Signer.signV4(request, region, creds.accessKey(), creds.secretKey()); + request = + Signer.signV4( + request, + region, + creds.accessKey(), + creds.secretKey(), + request.header("x-amz-content-sha256")); } if (this.traceStream != null) { diff --git a/api/src/main/java/io/minio/Signer.java b/api/src/main/java/io/minio/Signer.java index e7154cd2c..52b0be286 100644 --- a/api/src/main/java/io/minio/Signer.java +++ b/api/src/main/java/io/minio/Signer.java @@ -37,7 +37,7 @@ import okhttp3.Request; /** Amazon AWS S3 signature V4 signer. */ -class Signer { +public class Signer { // // Excerpts from @lsegal - https://github.com/aws/aws-sdk-js/issues/659#issuecomment-120477258 // @@ -115,7 +115,7 @@ class Signer { * @param secretKey Secret Key string. * @param prevSignature Previous signature of chunk upload. */ - public Signer( + private Signer( Request request, String contentSha256, ZonedDateTime date, @@ -303,9 +303,9 @@ public static String getChunkSeedSignature(Request request, String region, Strin } /** Returns signed request object for given request, region, access key and secret key. */ - public static Request signV4(Request request, String region, String accessKey, String secretKey) + public static Request signV4( + Request request, String region, String accessKey, String secretKey, String contentSha256) throws NoSuchAlgorithmException, InvalidKeyException { - String contentSha256 = request.header("x-amz-content-sha256"); ZonedDateTime date = ZonedDateTime.parse(request.header("x-amz-date"), Time.AMZ_DATE_FORMAT); Signer signer = new Signer(request, contentSha256, date, region, accessKey, secretKey, null); diff --git a/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java b/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java new file mode 100644 index 000000000..2bf92ff7f --- /dev/null +++ b/api/src/main/java/io/minio/credentials/AssumeRoleProvider.java @@ -0,0 +1,172 @@ +/* + * 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 io.minio.Digest; +import io.minio.Signer; +import io.minio.Time; +import io.minio.Xml; +import io.minio.errors.XmlParserException; +import java.io.IOException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.ZonedDateTime; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Path; +import org.simpleframework.xml.Root; + +/** + * Credential provider using AssumeRole + * API. + */ +public class AssumeRoleProvider implements Provider { + public static final int DEFAULT_DURATION_SECONDS = (int) TimeUnit.HOURS.toSeconds(1); + private final HttpUrl stsEndpoint; + private final String accessKey; + private final String secretKey; + private final String region; + private final OkHttpClient httpClient; + private final String contentSha256; + private final Request request; + private Credentials credentials; + + public AssumeRoleProvider( + @Nonnull String stsEndpoint, + @Nonnull String accessKey, + @Nonnull String secretKey, + @Nullable Integer durationSeconds, + @Nullable String policy, + @Nullable String region, + @Nullable String roleArn, + @Nullable String roleSessionName, + @Nullable String externalId, + @Nullable OkHttpClient customHttpClient) + throws NoSuchAlgorithmException { + stsEndpoint = Objects.requireNonNull(stsEndpoint, "STS endpoint cannot be empty"); + this.stsEndpoint = Objects.requireNonNull(HttpUrl.parse(stsEndpoint), "Invalid STS endpoint"); + accessKey = Objects.requireNonNull(accessKey, "Access key must not be null"); + if (accessKey.isEmpty()) { + throw new IllegalArgumentException("Access key must not be empty"); + } + this.accessKey = accessKey; + this.secretKey = Objects.requireNonNull(secretKey, "Secret key must not be null"); + this.region = (region != null) ? region : ""; + this.httpClient = (customHttpClient != null) ? customHttpClient : new OkHttpClient(); + + if (externalId != null && (externalId.length() < 2 || externalId.length() > 1224)) { + throw new IllegalArgumentException("Length of ExternalId must be in between 2 and 1224"); + } + + durationSeconds = + (durationSeconds != null && durationSeconds > DEFAULT_DURATION_SECONDS) + ? durationSeconds + : DEFAULT_DURATION_SECONDS; + + HttpUrl.Builder urlBuilder = + this.stsEndpoint + .newBuilder() + .addQueryParameter("Action", "AssumeRole") + .addQueryParameter("Version", "2011-06-15") + .addQueryParameter("DurationSeconds", String.valueOf(durationSeconds)); + + if (roleArn != null) { + urlBuilder.addQueryParameter("RoleArn", roleArn); + } + + if (roleSessionName != null) { + urlBuilder.addQueryParameter("RoleSessionName", roleSessionName); + } + + if (policy != null) { + urlBuilder.addQueryParameter("Policy", policy); + } + + if (externalId != null) { + urlBuilder.addQueryParameter("ExternalId", externalId); + } + + String data = urlBuilder.build().encodedQuery(); + this.contentSha256 = Digest.sha256Hash(data); + this.request = + new Request.Builder() + .url(this.stsEndpoint) + .method( + "POST", + RequestBody.create(MediaType.parse("application/x-www-form-urlencoded"), data)) + .build(); + } + + @Override + public synchronized Credentials fetch() { + if (credentials != null && !credentials.isExpired()) { + return credentials; + } + + try { + Request request = + Signer.signV4( + this.request + .newBuilder() + .header("x-amz-date", ZonedDateTime.now().format(Time.AMZ_DATE_FORMAT)) + .build(), + region, + accessKey, + secretKey, + contentSha256); + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new IllegalStateException( + "STS service failed with HTTP status code " + response.code()); + } + + AssumeRoleResponse result = + Xml.unmarshal(AssumeRoleResponse.class, response.body().charStream()); + credentials = result.credentials(); + return credentials; + } + } catch (XmlParserException | IOException e) { + throw new IllegalStateException("Unable to parse STS response", e); + } catch (NoSuchAlgorithmException | InvalidKeyException e) { + throw new IllegalStateException("Signature calculation failed", e); + } + } + + /** Object representation of response XML of AssumeRole API. */ + @Root(name = "AssumeRoleResponse", strict = false) + @Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/") + public static class AssumeRoleResponse { + @Path(value = "AssumeRoleResult") + @Element(name = "Credentials") + private Credentials credentials; + + public Credentials credentials() { + return credentials; + } + } +} diff --git a/examples/MinioClientWithAssumeRoleProvider.java b/examples/MinioClientWithAssumeRoleProvider.java new file mode 100644 index 000000000..b3c31c0e0 --- /dev/null +++ b/examples/MinioClientWithAssumeRoleProvider.java @@ -0,0 +1,74 @@ +/* + * 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. + */ + +import io.minio.MinioClient; +import io.minio.StatObjectArgs; +import io.minio.StatObjectResponse; +import io.minio.credentials.AssumeRoleProvider; +import io.minio.credentials.Provider; + +public class MinioClientWithAssumeRoleProvider { + public static void main(String[] args) throws Exception { + // STS endpoint usually point to MinIO server. + String stsEndpoint = "http://STS-HOST:STS-PORT/"; + + // Access key to fetch credentials from STS endpoint. + String accessKey = "YOUR-ACCESSKEY"; + + // Secret key to fetch credentials from STS endpoint. + String secretKey = "YOUR-SECRETACCESSKEY"; + + // Role ARN if available. + String roleArn = "ROLE-ARN"; + + // Role session name if available. + String roleSessionName = "ROLE-SESSION-NAME"; + + // External ID if available. + String externalId = "EXTERNAL-ID"; + + // Policy if available. + String policy = "POLICY"; + + // Region if available. + String region = "REGION"; + + Provider provider = + new AssumeRoleProvider( + stsEndpoint, + accessKey, + secretKey, + null, + policy, + region, + roleArn, + roleSessionName, + externalId, + null); + + MinioClient minioClient = + MinioClient.builder() + .endpoint("https://MINIO-HOST:MINIO-PORT") + .credentialsProvider(provider) + .build(); + + // Get information of an object. + StatObjectResponse stat = + minioClient.statObject( + StatObjectArgs.builder().bucket("my-bucketname").object("my-objectname").build()); + System.out.println(stat); + } +}