diff --git a/README.md b/README.md index 2d791c880..e4427f56f 100644 --- a/README.md +++ b/README.md @@ -155,6 +155,10 @@ The full API Reference is available here. * [SetBucketPolicy.java](https://github.com/minio/minio-java/tree/master/examples/SetBucketPolicy.java) * [GetBucketPolicy.Java](https://github.com/minio/minio-java/tree/master/examples/GetBucketPolicy.java) +#### Full Examples: STS Operations +* [ClientGrants.java](https://github.com/minio/minio-java/tree/master/examples/ClientGrants.java) +* [WebIdentity.java](https://github.com/minio/minio-java/tree/master/examples/WebIdentity.java) + #### Full Examples: Server Side Encryption * [CopyObjectEncrypted.java](https://github.com/minio/minio-java/tree/master/examples/CopyObjectEncrypted.java) * [CopyObjectEncryptedKms.java](https://github.com/minio/minio-java/tree/master/examples/CopyObjectEncryptedKms.java) diff --git a/api/src/main/java/io/minio/MinioClient.java b/api/src/main/java/io/minio/MinioClient.java index 42f1df754..b5396f421 100644 --- a/api/src/main/java/io/minio/MinioClient.java +++ b/api/src/main/java/io/minio/MinioClient.java @@ -28,6 +28,8 @@ import com.google.common.collect.Multimaps; import com.google.common.io.ByteStreams; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import io.minio.credentials.Provider; +import io.minio.credentials.StaticProvider; import io.minio.errors.BucketPolicyTooLargeException; import io.minio.errors.ErrorResponseException; import io.minio.errors.InsufficientDataException; @@ -47,6 +49,7 @@ import io.minio.messages.CopyObjectResult; import io.minio.messages.CopyPartResult; import io.minio.messages.CreateBucketConfiguration; +import io.minio.messages.Credentials; import io.minio.messages.DeleteError; import io.minio.messages.DeleteMarker; import io.minio.messages.DeleteObject; @@ -234,8 +237,7 @@ public class MinioClient { private boolean isAcceleratedHost; private boolean isDualStackHost; private boolean useVirtualStyle; - private String accessKey; - private String secretKey; + private Provider provider; private OkHttpClient httpClient; private MinioClient( @@ -245,8 +247,7 @@ private MinioClient( boolean isAcceleratedHost, boolean isDualStackHost, boolean useVirtualStyle, - String accessKey, - String secretKey, + Provider provider, OkHttpClient httpClient) { this.baseUrl = baseUrl; this.region = region; @@ -254,8 +255,7 @@ private MinioClient( this.isAcceleratedHost = isAcceleratedHost; this.isDualStackHost = isDualStackHost; this.useVirtualStyle = useVirtualStyle; - this.accessKey = accessKey; - this.secretKey = secretKey; + this.provider = provider; this.httpClient = httpClient; } @@ -267,8 +267,7 @@ private MinioClient(MinioClient client) { this.isAcceleratedHost = client.isAcceleratedHost; this.isDualStackHost = client.isDualStackHost; this.useVirtualStyle = client.useVirtualStyle; - this.accessKey = client.accessKey; - this.secretKey = client.secretKey; + this.provider = client.provider; this.httpClient = client.httpClient; } @@ -890,7 +889,8 @@ protected Request createRequest( String sha256Hash = null; String md5Hash = null; - if (this.accessKey != null && this.secretKey != null) { + Credentials creds = provider != null ? provider.fetch() : null; + if (creds != null && !creds.isEmpty()) { if (url.isHttps()) { // Fix issue #415: No need to compute sha256 if endpoint scheme is HTTPS. sha256Hash = "UNSIGNED-PAYLOAD"; @@ -924,6 +924,10 @@ protected Request createRequest( requestBuilder.header("x-amz-content-sha256", sha256Hash); } + if (creds != null && creds.sessionToken() != null) { + requestBuilder.header("X-Amz-Security-Token", creds.sessionToken()); + } + ZonedDateTime date = ZonedDateTime.now(); requestBuilder.header("x-amz-date", date.format(Time.AMZ_DATE_FORMAT)); @@ -1016,8 +1020,9 @@ protected Response execute( HttpUrl url = buildUrl(method, bucketName, objectName, region, queryParamMap); Request request = createRequest(url, method, headerMap, body, length); - if (this.accessKey != null && this.secretKey != null) { - request = Signer.signV4(request, region, accessKey, secretKey); + Credentials creds = provider != null ? provider.fetch() : null; + if (creds != null && !creds.isEmpty()) { + request = Signer.signV4(request, region, creds.accessKey(), creds.secretKey()); } if (this.traceStream != null) { @@ -1193,7 +1198,7 @@ protected String getRegion(String bucketName, String region) return this.region; } - if (!isAwsHost || bucketName == null || this.accessKey == null) { + if (!isAwsHost || bucketName == null || this.provider == null) { return US_EAST_1; } @@ -2575,6 +2580,9 @@ public String getPresignedObjectUrl(GetPresignedObjectUrlArgs args) String region = getRegion(args.bucket(), args.region()); HttpUrl url = buildUrl(args.method(), args.bucket(), args.object(), region, queryParams); Request request = createRequest(url, args.method(), null, body, 0); + Credentials creds = provider != null ? provider.fetch() : null; + String accessKey = creds != null ? creds.accessKey() : null; + String secretKey = creds != null ? creds.secretKey() : null; url = Signer.presignV4(request, region, accessKey, secretKey, args.expiry()); return url.toString(); } @@ -2856,7 +2864,13 @@ public Map getPresignedPostFormData(PostPolicy policy) InternalException, InvalidBucketNameException, InvalidExpiresRangeException, InvalidKeyException, InvalidResponseException, IOException, NoSuchAlgorithmException, ServerException, XmlParserException { - return policy.formData(this.accessKey, this.secretKey, getRegion(policy.bucket(), null)); + + if (provider == null) { + throw new IllegalArgumentException("credentials provider cannot be null"); + } + + Credentials creds = provider.fetch(); + return policy.formData(creds.accessKey(), creds.secretKey(), getRegion(policy.bucket(), null)); } /** @@ -7768,8 +7782,6 @@ public static Builder builder() { public static final class Builder { HttpUrl baseUrl; String region; - String accessKey; - String secretKey; OkHttpClient httpClient; boolean isAwsHost; boolean isAwsChinaHost; @@ -7777,6 +7789,7 @@ public static final class Builder { boolean isDualStackHost; boolean useVirtualStyle; String regionInUrl; + Provider provider; public Builder() {} @@ -8010,8 +8023,12 @@ public Builder region(String region) { } public Builder credentials(String accessKey, String secretKey) { - this.accessKey = accessKey; - this.secretKey = secretKey; + this.provider = new StaticProvider(accessKey, secretKey); + return this; + } + + public Builder credentialsProvider(Provider provider) { + this.provider = provider; return this; } @@ -8053,8 +8070,7 @@ public MinioClient build() { isAcceleratedHost, isDualStackHost, useVirtualStyle, - accessKey, - secretKey, + provider, httpClient); } } diff --git a/api/src/main/java/io/minio/S3Escaper.java b/api/src/main/java/io/minio/S3Escaper.java index 225cd08ea..1e9d64d78 100644 --- a/api/src/main/java/io/minio/S3Escaper.java +++ b/api/src/main/java/io/minio/S3Escaper.java @@ -19,9 +19,14 @@ import com.google.common.escape.Escaper; import com.google.common.net.UrlEscapers; -class S3Escaper { +public class S3Escaper { + private static final Escaper ESCAPER = UrlEscapers.urlPathSegmentEscaper(); + private S3Escaper() { + throw new IllegalAccessError(); + } + /** Returns S3 encoded string. */ public static String encode(String str) { if (str == null) { @@ -50,23 +55,23 @@ public static String encode(String str) { /** Returns S3 encoded string of given path where multiple '/' are trimmed. */ public static String encodePath(String path) { - StringBuffer encodedPathBuf = new StringBuffer(); + final StringBuilder encodedPath = new StringBuilder(); for (String pathSegment : path.split("/")) { if (!pathSegment.isEmpty()) { - if (encodedPathBuf.length() > 0) { - encodedPathBuf.append("/"); + if (encodedPath.length() > 0) { + encodedPath.append("/"); } - encodedPathBuf.append(S3Escaper.encode(pathSegment)); + encodedPath.append(S3Escaper.encode(pathSegment)); } } if (path.startsWith("/")) { - encodedPathBuf.insert(0, "/"); + encodedPath.insert(0, "/"); } if (path.endsWith("/")) { - encodedPathBuf.append("/"); + encodedPath.append("/"); } - return encodedPathBuf.toString(); + return encodedPath.toString(); } } diff --git a/api/src/main/java/io/minio/credentials/AwsEnvironmentProvider.java b/api/src/main/java/io/minio/credentials/AwsEnvironmentProvider.java new file mode 100644 index 000000000..edc2fba46 --- /dev/null +++ b/api/src/main/java/io/minio/credentials/AwsEnvironmentProvider.java @@ -0,0 +1,72 @@ +/* + * 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.messages.Credentials; +import io.minio.messages.ResponseDate; +import java.time.ZonedDateTime; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import javax.annotation.Nonnull; + +@SuppressWarnings("unused") +public class AwsEnvironmentProvider extends EnvironmentProvider { + + private static final List ACCESS_KEY_ALIASES = + Arrays.asList("AWS_ACCESS_KEY_ID", "AWS_ACCESS_KEY"); + private static final List SECRET_KEY_ALIASES = + Arrays.asList("AWS_SECRET_ACCESS_KEY", "AWS_SECRET_KEY"); + private static final String SESSION_TOKEN_ALIAS = "AWS_SESSION_TOKEN"; + + private Credentials credentials; + + public AwsEnvironmentProvider() { + credentials = readCredentials(); + } + + @Override + public Credentials fetch() { + if (!isExpired(credentials)) { + return credentials; + } + // avoid race conditions with credentials rewriting + synchronized (this) { + if (isExpired(credentials)) { + credentials = readCredentials(); + } + } + return credentials; + } + + private Credentials readCredentials() { + final String accessKey = readFirst(ACCESS_KEY_ALIASES); + final String secretKey = readFirst(SECRET_KEY_ALIASES); + final ZonedDateTime lifeTime = ZonedDateTime.now().plus(REFRESHED_AFTER); + final String sessionToken = readProperty(SESSION_TOKEN_ALIAS); + return new Credentials(accessKey, secretKey, new ResponseDate(lifeTime), sessionToken); + } + + private String readFirst(@Nonnull Collection propertyKeys) { + for (String propertyKey : propertyKeys) { + final String value = readProperty(propertyKey); + if (value != null) { + return value; + } + } + throw new IllegalStateException("Can't find env variables for " + propertyKeys); + } +} diff --git a/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java b/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java new file mode 100644 index 000000000..4aadf2f4a --- /dev/null +++ b/api/src/main/java/io/minio/credentials/ClientGrantsProvider.java @@ -0,0 +1,88 @@ +/* + * 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.Xml; +import io.minio.errors.InvalidResponseException; +import io.minio.errors.XmlParserException; +import io.minio.messages.AssumeRoleWithClientGrantsResponse; +import io.minio.messages.ClientGrantsToken; +import io.minio.messages.Credentials; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import okhttp3.Response; +import okhttp3.ResponseBody; + +@SuppressWarnings("unused") +public class ClientGrantsProvider extends StsProvider { + + private Credentials credentials; + private final Supplier tokenProducer; + + public ClientGrantsProvider( + @Nonnull String stsEndpoint, @Nonnull Supplier tokenProducer) { + super(stsEndpoint); + this.tokenProducer = Objects.requireNonNull(tokenProducer, "Token producer must not be null"); + } + + /** + * Returns a pointer to a new, temporary credentials, obtained via STS assume role with client + * grants api. + * + * @return temporary credentials to access minio api. + */ + @Override + public Credentials fetch() { + if (credentials != null && !isExpired(credentials)) { + return credentials; + } + synchronized (this) { + if (credentials == null || isExpired(credentials)) { + try (Response response = callSecurityTokenService()) { + final ResponseBody body = response.body(); + if (body == null) { + // should not happen + throw new IllegalStateException("Received empty response"); + } + credentials = + Xml.unmarshal(AssumeRoleWithClientGrantsResponse.class, body.charStream()) + .credentials(); + } catch (XmlParserException | IOException | InvalidResponseException e) { + throw new IllegalStateException("Failed to process STS call", e); + } + } + } + return credentials; + } + + @Override + protected Map queryParams() { + final ClientGrantsToken grantsToken = tokenProducer.get(); + final Map queryParamenters = new HashMap<>(); + queryParamenters.put("Action", "AssumeRoleWithClientGrants"); + queryParamenters.put("DurationSeconds", tokenDuration(grantsToken.expiredAfter())); + queryParamenters.put("Token", grantsToken.token()); + queryParamenters.put("Version", "2011-06-15"); + if (grantsToken.policy() != null) { + queryParamenters.put("Policy", grantsToken.policy()); + } + return queryParamenters; + } +} diff --git a/api/src/main/java/io/minio/credentials/EnvironmentProvider.java b/api/src/main/java/io/minio/credentials/EnvironmentProvider.java new file mode 100644 index 000000000..93c94208e --- /dev/null +++ b/api/src/main/java/io/minio/credentials/EnvironmentProvider.java @@ -0,0 +1,43 @@ +/* + * 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 java.time.Duration; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public abstract class EnvironmentProvider implements Provider { + + // it's ok to re-read values from env.variables every 5 min. + protected static final Duration REFRESHED_AFTER = Duration.ofMinutes(5); + + /** + * Method used to read system/env properties. If property not found through system properties it + * will search the property in environment properties. + * + * @param propertyName name of the property to retrieve. + * @return property value. + * @throws NullPointerException if {@literal propertyName} is null. + */ + @Nullable + protected String readProperty(@Nonnull String propertyName) { + final String systemProperty = System.getProperty(propertyName); + if (systemProperty != null) { + return systemProperty; + } + return System.getenv(propertyName); + } +} diff --git a/api/src/main/java/io/minio/credentials/MinioEnvironmentProvider.java b/api/src/main/java/io/minio/credentials/MinioEnvironmentProvider.java new file mode 100644 index 000000000..bee7c734f --- /dev/null +++ b/api/src/main/java/io/minio/credentials/MinioEnvironmentProvider.java @@ -0,0 +1,55 @@ +/* + * 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.messages.Credentials; +import io.minio.messages.ResponseDate; +import java.time.ZonedDateTime; + +@SuppressWarnings("unused") +public class MinioEnvironmentProvider extends EnvironmentProvider { + + private static final String ACCESS_KEY_ALIAS = "MINIO_ACCESS_KEY"; + private static final String SECRET_KEY_ALIAS = "MINIO_SECRET_KEY"; + + private Credentials credentials; + + public MinioEnvironmentProvider() { + credentials = readCredentials(); + } + + @Override + public Credentials fetch() { + if (!isExpired(credentials)) { + return credentials; + } + // avoid race conditions with credentials rewriting + synchronized (this) { + if (isExpired(credentials)) { + credentials = readCredentials(); + } + } + return credentials; + } + + private Credentials readCredentials() { + final String accessKey = readProperty(ACCESS_KEY_ALIAS); + final String secretKey = readProperty(SECRET_KEY_ALIAS); + final ZonedDateTime lifeTime = ZonedDateTime.now().plus(REFRESHED_AFTER); + //noinspection ConstantConditions + return new Credentials(accessKey, secretKey, new ResponseDate(lifeTime), null); + } +} diff --git a/api/src/main/java/io/minio/credentials/Provider.java b/api/src/main/java/io/minio/credentials/Provider.java new file mode 100644 index 000000000..72f25222a --- /dev/null +++ b/api/src/main/java/io/minio/credentials/Provider.java @@ -0,0 +1,45 @@ +/* + * 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.messages.Credentials; +import java.time.Duration; +import java.time.ZonedDateTime; +import javax.annotation.Nullable; + +/** + * This component allows {@link io.minio.MinioClient} to fetch valid (not expired) credentials. + * Note: any provider implementation should cache valid credentials and control it's lifetime to + * prevent unnesessary computation logic of repeatedly called {@link #fetch()}, while holding a + * valid {@link Credentials} instance. + */ +public interface Provider { + + /** + * @return a valid (not expired) {@link Credentials} instance for {@link io.minio.MinioClient}. + */ + Credentials fetch(); + + default boolean isExpired(@Nullable Credentials credentials) { + if (credentials == null || credentials.expiredAt() == null || credentials.isEmpty()) { + return false; + } + // fair enough amount of time to execute the call to avoid situations when the check returns ok + // and credentials + // expire immediately after that. + return ZonedDateTime.now().plus(Duration.ofSeconds(30)).isAfter(credentials.expiredAt()); + } +} diff --git a/api/src/main/java/io/minio/credentials/StaticProvider.java b/api/src/main/java/io/minio/credentials/StaticProvider.java new file mode 100644 index 000000000..b4d263b43 --- /dev/null +++ b/api/src/main/java/io/minio/credentials/StaticProvider.java @@ -0,0 +1,34 @@ +/* + * 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.messages.Credentials; +import javax.annotation.Nonnull; + +@SuppressWarnings("unused") +public class StaticProvider implements Provider { + + private final Credentials credentials; + + public StaticProvider(@Nonnull String accessKey, @Nonnull String secretKey) { + this.credentials = new Credentials(accessKey, secretKey, null, null); + } + + @Override + public Credentials fetch() { + return credentials; + } +} diff --git a/api/src/main/java/io/minio/credentials/StsProvider.java b/api/src/main/java/io/minio/credentials/StsProvider.java new file mode 100644 index 000000000..cd18c15fa --- /dev/null +++ b/api/src/main/java/io/minio/credentials/StsProvider.java @@ -0,0 +1,82 @@ +/* + * 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 static io.minio.S3Escaper.encode; + +import io.minio.errors.InvalidResponseException; +import io.minio.http.Method; +import java.io.IOException; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import okhttp3.HttpUrl; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public abstract class StsProvider implements Provider { + + private static final int MINIMUM_TOKEN_DURATION = 900; + private static final int MAXIMUM_TOKEN_DURATION = 43200; + private static final RequestBody EMPTY_BODY = + RequestBody.create(MediaType.parse("application/octet-stream"), new byte[] {}); + + private final HttpUrl endpoint; + private final OkHttpClient httpClient = new OkHttpClient(); + + public StsProvider(@Nonnull String stsEndpoint) { + this.endpoint = + HttpUrl.parse(Objects.requireNonNull(stsEndpoint, "STS endpoint cannot be empty")); + } + + protected Response callSecurityTokenService() throws IOException, InvalidResponseException { + final Map queryParams = queryParams(); + final HttpUrl.Builder url = endpoint.newBuilder(); + for (Map.Entry entry : queryParams.entrySet()) { + url.addEncodedQueryParameter(encode(entry.getKey()), encode(entry.getValue())); + } + final Request request = + new Request.Builder() + .url(url.build()) + // Disable default gzip compression by okhttp library. + .header("Accept-Encoding", "identity") + .method(Method.POST.toString(), EMPTY_BODY) + .build(); + + final Response response = httpClient.newCall(request).execute(); + if (response.isSuccessful()) { + return response; + } + final String body = response.body() != null ? response.body().string() : null; + final String contentType = response.headers().get("content-type"); + throw new InvalidResponseException(response.code(), contentType, body); + } + + protected String tokenDuration(long requiredSeconds) { + if (requiredSeconds < MINIMUM_TOKEN_DURATION) { + return String.valueOf(MINIMUM_TOKEN_DURATION); + } else if (requiredSeconds > MAXIMUM_TOKEN_DURATION) { + return String.valueOf(MAXIMUM_TOKEN_DURATION); + } + return String.valueOf(requiredSeconds); + } + + /** @return specific for concrete method query parameters. */ + protected abstract Map queryParams(); +} diff --git a/api/src/main/java/io/minio/credentials/WebIdentityProvider.java b/api/src/main/java/io/minio/credentials/WebIdentityProvider.java new file mode 100644 index 000000000..fd368ec7b --- /dev/null +++ b/api/src/main/java/io/minio/credentials/WebIdentityProvider.java @@ -0,0 +1,88 @@ +/* + * 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.Xml; +import io.minio.errors.InvalidResponseException; +import io.minio.errors.XmlParserException; +import io.minio.messages.AssumeRoleWithWebIdentityResponse; +import io.minio.messages.Credentials; +import io.minio.messages.WebIdentityToken; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.function.Supplier; +import javax.annotation.Nonnull; +import okhttp3.Response; +import okhttp3.ResponseBody; + +@SuppressWarnings("unused") +public class WebIdentityProvider extends StsProvider { + + private Credentials credentials; + private final Supplier tokenProducer; + + public WebIdentityProvider( + @Nonnull String stsEndpoint, @Nonnull Supplier tokenProducer) { + super(stsEndpoint); + this.tokenProducer = Objects.requireNonNull(tokenProducer, "Token producer must not be null"); + } + + /** + * Returns a pointer to a new, temporary credentials, obtained via STS assume role with web + * identity api. + * + * @return temporary credentials to access minio api. + */ + @Override + public Credentials fetch() { + if (credentials != null && !isExpired(credentials)) { + return credentials; + } + synchronized (this) { + if (credentials == null || isExpired(credentials)) { + try (Response response = callSecurityTokenService()) { + final ResponseBody body = response.body(); + if (body == null) { + // should not happen + throw new IllegalStateException("Received empty response"); + } + credentials = + Xml.unmarshal(AssumeRoleWithWebIdentityResponse.class, body.charStream()) + .credentials(); + } catch (XmlParserException | IOException | InvalidResponseException e) { + throw new IllegalStateException("Failed to process STS call", e); + } + } + } + return credentials; + } + + @Override + protected Map queryParams() { + final WebIdentityToken grantsToken = tokenProducer.get(); + final Map queryParamenters = new HashMap<>(); + queryParamenters.put("Action", "AssumeRoleWithWebIdentity"); + queryParamenters.put("DurationSeconds", tokenDuration(grantsToken.expiredAfter())); + queryParamenters.put("WebIdentityToken", grantsToken.token()); + queryParamenters.put("Version", "2011-06-15"); + if (grantsToken.policy() != null) { + queryParamenters.put("Policy", grantsToken.policy()); + } + return queryParamenters; + } +} diff --git a/api/src/main/java/io/minio/messages/AssumeRoleWithClientGrantsResponse.java b/api/src/main/java/io/minio/messages/AssumeRoleWithClientGrantsResponse.java new file mode 100644 index 000000000..872bcf562 --- /dev/null +++ b/api/src/main/java/io/minio/messages/AssumeRoleWithClientGrantsResponse.java @@ -0,0 +1,34 @@ +/* + * 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.messages; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Path; +import org.simpleframework.xml.Root; + +@Root(name = "AssumeRoleWithClientGrantsResponse", strict = false) +@Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/") +public class AssumeRoleWithClientGrantsResponse { + + @Path(value = "AssumeRoleWithClientGrantsResult") + @Element(name = "Credentials") + private Credentials credentials; + + public Credentials credentials() { + return credentials; + } +} diff --git a/api/src/main/java/io/minio/messages/AssumeRoleWithWebIdentityResponse.java b/api/src/main/java/io/minio/messages/AssumeRoleWithWebIdentityResponse.java new file mode 100644 index 000000000..7e1ddf33e --- /dev/null +++ b/api/src/main/java/io/minio/messages/AssumeRoleWithWebIdentityResponse.java @@ -0,0 +1,34 @@ +/* + * 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.messages; + +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Path; +import org.simpleframework.xml.Root; + +@Root(name = "AssumeRoleWithWebIdentityResponse", strict = false) +@Namespace(reference = "https://sts.amazonaws.com/doc/2011-06-15/") +public class AssumeRoleWithWebIdentityResponse { + + @Path(value = "AssumeRoleWithWebIdentityResult") + @Element(name = "Credentials") + private Credentials credentials; + + public Credentials credentials() { + return credentials; + } +} diff --git a/api/src/main/java/io/minio/messages/ClientGrantsToken.java b/api/src/main/java/io/minio/messages/ClientGrantsToken.java new file mode 100644 index 000000000..d13da2f56 --- /dev/null +++ b/api/src/main/java/io/minio/messages/ClientGrantsToken.java @@ -0,0 +1,41 @@ +/* + * 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.messages; + +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ClientGrantsToken extends StsRequestToken { + + private final String jwtAccessToken; + private final long expiredAfter; + + public ClientGrantsToken( + @Nonnull String jwtAccessToken, long expiredAfter, @Nullable String policy) { + super(policy); + this.jwtAccessToken = Objects.requireNonNull(jwtAccessToken); + this.expiredAfter = expiredAfter; + } + + public String token() { + return jwtAccessToken; + } + + public long expiredAfter() { + return expiredAfter; + } +} diff --git a/api/src/main/java/io/minio/messages/Credentials.java b/api/src/main/java/io/minio/messages/Credentials.java new file mode 100644 index 000000000..7f6af5d4d --- /dev/null +++ b/api/src/main/java/io/minio/messages/Credentials.java @@ -0,0 +1,73 @@ +/* + * 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.messages; + +import java.time.ZonedDateTime; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.simpleframework.xml.Element; +import org.simpleframework.xml.Root; + +@Root(name = "Credentials", strict = false) +public class Credentials { + + @Element(name = "AccessKeyId") + private final String accessKey; + + @Element(name = "SecretAccessKey") + private final String secretKey; + + @Element(name = "Expiration") + private final ResponseDate expiredAt; + + @Element(name = "SessionToken") + private final String sessionToken; + + public Credentials( + @Nonnull @Element(name = "AccessKeyId") String accessKey, + @Nonnull @Element(name = "SecretAccessKey") String secretKey, + @Nullable @Element(name = "Expiration") ResponseDate expiredAt, + @Nullable @Element(name = "SessionToken") String sessionToken) { + this.accessKey = Objects.requireNonNull(accessKey, "AccessKey must not be null"); + this.secretKey = Objects.requireNonNull(secretKey, "SecretKey must not be null"); + if (accessKey.isEmpty() || secretKey.isEmpty()) { + throw new IllegalArgumentException("AccessKey and SecretKey must not be empty"); + } + this.sessionToken = sessionToken; + this.expiredAt = expiredAt; + } + + public String accessKey() { + return accessKey; + } + + public String secretKey() { + return secretKey; + } + + public ZonedDateTime expiredAt() { + return expiredAt.zonedDateTime(); + } + + public String sessionToken() { + return sessionToken; + } + + public boolean isEmpty() { + return accessKey == null || secretKey == null; + } +} diff --git a/api/src/main/java/io/minio/messages/ResponseDate.java b/api/src/main/java/io/minio/messages/ResponseDate.java index a4d562474..9e3f426ac 100644 --- a/api/src/main/java/io/minio/messages/ResponseDate.java +++ b/api/src/main/java/io/minio/messages/ResponseDate.java @@ -68,7 +68,7 @@ public ResponseDate read(InputNode node) throws Exception { } @Override - public void write(OutputNode node, ResponseDate amzDate) throws Exception { + public void write(OutputNode node, ResponseDate amzDate) { node.setValue(amzDate.toString()); } } diff --git a/api/src/main/java/io/minio/messages/StsRequestToken.java b/api/src/main/java/io/minio/messages/StsRequestToken.java new file mode 100644 index 000000000..930d04ae1 --- /dev/null +++ b/api/src/main/java/io/minio/messages/StsRequestToken.java @@ -0,0 +1,44 @@ +/* + * 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.messages; + +import javax.annotation.Nullable; + +/** + * Base class for STS Requests. There are four types of sts requests: + * + *
    + *
  • 1. Client grants (with specific params: DurationSeconds, Token, Policy) + *
  • 2. Web identity (with specific params: DurationSeconds, WebIdentityToken, Policy) + *
  • 3. Assume Role (with specific params: DurationSeconds, AUTHPARAMS, Policy) + *
  • 4. Ad/Ldap (with specific params: LDAPUsername, LDAPPassword, Policy) + *
+ * + * other parameters (like Version or Action) is static and handled by concrete {@link + * io.minio.credentials.Provider}. + */ +public class StsRequestToken { + + private final String policy; + + public StsRequestToken(@Nullable String policy) { + this.policy = policy; + } + + public String policy() { + return policy; + } +} diff --git a/api/src/main/java/io/minio/messages/WebIdentityToken.java b/api/src/main/java/io/minio/messages/WebIdentityToken.java new file mode 100644 index 000000000..3d7acc7a2 --- /dev/null +++ b/api/src/main/java/io/minio/messages/WebIdentityToken.java @@ -0,0 +1,41 @@ +/* + * 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.messages; + +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class WebIdentityToken extends StsRequestToken { + + private final String jwtAccessToken; + private final long expiredAfter; + + public WebIdentityToken( + @Nonnull String jwtAccessToken, long expiredAfter, @Nullable String policy) { + super(policy); + this.jwtAccessToken = Objects.requireNonNull(jwtAccessToken); + this.expiredAfter = expiredAfter; + } + + public String token() { + return jwtAccessToken; + } + + public long expiredAfter() { + return expiredAfter; + } +} diff --git a/examples/ClientGrants.java b/examples/ClientGrants.java new file mode 100644 index 000000000..a283c850d --- /dev/null +++ b/examples/ClientGrants.java @@ -0,0 +1,133 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.VisibilityChecker; +import io.minio.MinioClient; +import io.minio.credentials.ClientGrantsProvider; +import io.minio.credentials.Provider; +import io.minio.messages.Bucket; +import io.minio.messages.ClientGrantsToken; +import java.beans.ConstructorProperties; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import okhttp3.FormBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class ClientGrants { + + private static final String POLICY = + new StringBuilder() + .append("{\n") + .append(" \"Statement\": [\n") + .append(" " + " {\n") + .append(" \"Action\": [\n") + .append(" \"s3:GetBucketLocation\",\n") + .append(" \"s3:ListBucket\"\n") + .append(" ],\n") + .append(" \"Effect\": \"Allow\",\n") + .append(" \"Principal\": \"*\",\n") + .append(" \"Resource\": \"arn:aws:s3:::test\"\n") + .append(" }\n") + .append(" ],\n") + .append(" \"Version\": \"2012-10-17\"\n") + .append("}\n") + .toString(); + + static class JwtToken { + + @JsonProperty("access_token") + private final String accessToken; + + @JsonProperty("expires_in") + private final long expiredAfter; + + @ConstructorProperties({"access_token", "expires_in"}) + public JwtToken(String accessToken, long expiredAfter) { + this.accessToken = accessToken; + this.expiredAfter = expiredAfter; + } + } + + static ClientGrantsToken getTokenAndExpiry( + @Nonnull String clientId, + @Nonnull String clientSecret, + @Nonnull String idpClientId, + @Nonnull String idpEndpoint) { + Objects.requireNonNull(clientId, "Client id must not be null"); + Objects.requireNonNull(clientSecret, "ClientSecret must not be null"); + + final RequestBody requestBody = + new FormBody.Builder() + .add("username", clientId) + .add("password", clientSecret) + .add("grant_type", "password") + .add("client_id", idpClientId) + .build(); + + final Request request = new Request.Builder().url(idpEndpoint).post(requestBody).build(); + + final OkHttpClient client = new OkHttpClient(); + try (Response response = client.newCall(request).execute()) { + final ObjectMapper mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.setVisibility( + VisibilityChecker.Std.defaultInstance() + .withFieldVisibility(JsonAutoDetect.Visibility.ANY)); + + final JwtToken jwtToken = + mapper.readValue(Objects.requireNonNull(response.body()).charStream(), JwtToken.class); + return new ClientGrantsToken(jwtToken.accessToken, jwtToken.expiredAfter, POLICY); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) throws Exception { + final String clientId = "user"; + final String clientSecret = "password"; + final String idpEndpoint = + "http://idp-host:idp-port/auth/realms/master/protocol/openid-connect/token"; + // STS endpoint usually points to MinIO endpoint in case of MinIO + final String stsEndpoint = "http://sts-host:sts-port/"; + // client id for minio on idp + final String idpClientId = "minio-client-id"; + + final Provider credentialsProvider = + new ClientGrantsProvider( + stsEndpoint, () -> getTokenAndExpiry(clientId, clientSecret, idpClientId, idpEndpoint)); + + final MinioClient minioClient = + MinioClient.builder() + .endpoint("http://minio-host:minio-port") + .credentialsProvider(credentialsProvider) + .build(); + + final List buckets = minioClient.listBuckets(); + for (Bucket bucket : buckets) { + System.out.print(bucket.name() + " created at "); + System.out.println(bucket.creationDate()); + } + } +} diff --git a/examples/WebIdentity.java b/examples/WebIdentity.java new file mode 100644 index 000000000..8afd87695 --- /dev/null +++ b/examples/WebIdentity.java @@ -0,0 +1,115 @@ +/* + * 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 com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.introspect.VisibilityChecker; +import io.minio.MinioClient; +import io.minio.credentials.Provider; +import io.minio.credentials.WebIdentityProvider; +import io.minio.messages.Bucket; +import io.minio.messages.WebIdentityToken; +import java.beans.ConstructorProperties; +import java.io.IOException; +import java.util.List; +import java.util.Objects; +import javax.annotation.Nonnull; +import okhttp3.FormBody; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; + +public class WebIdentity { + + static class JwtToken { + + @JsonProperty("access_token") + private final String accessToken; + + @JsonProperty("expires_in") + private final long expiredAfter; + + @ConstructorProperties({"access_token", "expires_in"}) + public JwtToken(String accessToken, long expiredAfter) { + this.accessToken = accessToken; + this.expiredAfter = expiredAfter; + } + } + + static WebIdentityToken getTokenAndExpiry( + @Nonnull String clientId, + @Nonnull String clientSecret, + @Nonnull String idpClientId, + @Nonnull String idpEndpoint) { + Objects.requireNonNull(clientId, "Client id must not be null"); + Objects.requireNonNull(clientSecret, "ClientSecret must not be null"); + + final RequestBody requestBody = + new FormBody.Builder() + .add("username", clientId) + .add("password", clientSecret) + .add("grant_type", "password") + .add("client_id", idpClientId) + .build(); + + final Request request = new Request.Builder().url(idpEndpoint).post(requestBody).build(); + + final OkHttpClient client = new OkHttpClient(); + try (Response response = client.newCall(request).execute()) { + final ObjectMapper mapper = new ObjectMapper(); + mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + mapper.setVisibility( + VisibilityChecker.Std.defaultInstance() + .withFieldVisibility(JsonAutoDetect.Visibility.ANY)); + + final JwtToken jwtToken = + mapper.readValue(Objects.requireNonNull(response.body()).charStream(), JwtToken.class); + return new WebIdentityToken(jwtToken.accessToken, jwtToken.expiredAfter, null); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static void main(String[] args) throws Exception { + final String clientId = "user"; + final String clientSecret = "password"; + final String idpEndpoint = + "http://idp-host:idp-port/auth/realms/master/protocol/openid-connect/token"; + // STS endpoint usually points to MinIO endpoint in case of MinIO + final String stsEndpoint = "http://sts-host:sts-port/"; + // client id for minio on idp + final String idpClientId = "minio-client-id"; + + final Provider credentialsProvider = + new WebIdentityProvider( + stsEndpoint, () -> getTokenAndExpiry(clientId, clientSecret, idpClientId, idpEndpoint)); + + final MinioClient minioClient = + MinioClient.builder() + .endpoint("http://minio-host:minio-port") + .credentialsProvider(credentialsProvider) + .build(); + + final List buckets = minioClient.listBuckets(); + for (Bucket bucket : buckets) { + System.out.print(bucket.name() + " created at "); + System.out.println(bucket.creationDate()); + } + } +}