All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.wl4g.infra.common.minio.v8_4.MinioAdminClient Maven / Gradle / Ivy

The newest version!
/*
 * MinIO Java SDK for Amazon S3 Compatible Cloud Storage,
 * (C) 2021 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
 *
 *     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 com.wl4g.infra.common.minio.v8_4;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.type.MapType;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import io.minio.Digest;
import io.minio.MinioProperties;
import io.minio.S3Escaper;
import io.minio.Signer;
import io.minio.Time;
import io.minio.credentials.Credentials;
import io.minio.credentials.Provider;
import io.minio.credentials.StaticProvider;
import io.minio.http.HttpUtils;
import io.minio.http.Method;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.time.ZonedDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
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 okhttp3.ResponseBody;
import org.bouncycastle.crypto.InvalidCipherTextException;

/** Client to perform MinIO administration operations. */
public class MinioAdminClient {
    private enum Command {
        ADD_USER("add-user"), USER_INFO("user-info"), LIST_USERS("list-users"), REMOVE_USER("remove-user"), ADD_CANNED_POLICY(
                "add-canned-policy"), SET_USER_OR_GROUP_POLICY("set-user-or-group-policy"), LIST_CANNED_POLICIES(
                        "list-canned-policies"), REMOVE_CANNED_POLICY("remove-canned-policy"), SET_BUCKET_QUOTA(
                                "set-bucket-quota"), GET_BUCKET_QUOTA("get-bucket-quota");
        private final String value;

        private Command(String value) {
            this.value = value;
        }

        public String toString() {
            return this.value;
        }
    }

    private static final long DEFAULT_CONNECTION_TIMEOUT = TimeUnit.MINUTES.toMillis(1);
    private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.parse("application/octet-stream");
    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();

    private String userAgent = MinioProperties.INSTANCE.getDefaultUserAgent();
    private PrintWriter traceStream;

    private HttpUrl baseUrl;
    private String region;
    private Provider provider;
    private OkHttpClient httpClient;

    private MinioAdminClient(HttpUrl baseUrl, String region, Provider provider, OkHttpClient httpClient) {
        this.baseUrl = baseUrl;
        this.region = region;
        this.provider = provider;
        this.httpClient = httpClient;
    }

    private Credentials getCredentials() {
        Credentials creds = provider.fetch();
        if (creds == null)
            throw new RuntimeException("Credential provider returns null credential");
        return creds;
    }

    private Response execute(Method method, Command command, Multimap queryParamMap, byte[] body)
            throws InvalidKeyException, IOException, NoSuchAlgorithmException {
        Credentials creds = getCredentials();

        HttpUrl.Builder urlBuilder = this.baseUrl.newBuilder().host(this.baseUrl.host()).addEncodedPathSegments(
                S3Escaper.encodePath("minio/admin/v3/" + command.toString()));
        if (queryParamMap != null) {
            for (Map.Entry entry : queryParamMap.entries()) {
                urlBuilder.addEncodedQueryParameter(S3Escaper.encode(entry.getKey()), S3Escaper.encode(entry.getValue()));
            }
        }
        HttpUrl url = urlBuilder.build();

        Request.Builder requestBuilder = new Request.Builder();
        requestBuilder.url(url);
        requestBuilder.header("Host", HttpUtils.getHostHeader(url));
        requestBuilder.header("Accept-Encoding", "identity"); // Disable default
                                                              // gzip
                                                              // compression.
        requestBuilder.header("User-Agent", this.userAgent);
        requestBuilder.header("x-amz-date", ZonedDateTime.now().format(Time.AMZ_DATE_FORMAT));
        if (creds.sessionToken() != null) {
            requestBuilder.header("X-Amz-Security-Token", creds.sessionToken());
        }
        if (body == null && (method != Method.GET && method != Method.HEAD)) {
            body = HttpUtils.EMPTY_BODY;
        }
        if (body != null) {
            requestBuilder.header("x-amz-content-sha256", Digest.sha256Hash(body, body.length));
            requestBuilder.method(method.toString(), RequestBody.create(body, DEFAULT_MEDIA_TYPE));
        } else {
            requestBuilder.header("x-amz-content-sha256", Digest.ZERO_SHA256_HASH);
        }
        Request request = requestBuilder.build();

        request = Signer.signV4S3(request, region, creds.accessKey(), creds.secretKey(), request.header("x-amz-content-sha256"));

        PrintWriter traceStream = this.traceStream;
        if (traceStream != null) {
            StringBuilder traceBuilder = new StringBuilder();
            traceBuilder.append("---------START-HTTP---------\n");
            String encodedPath = request.url().encodedPath();
            String encodedQuery = request.url().encodedQuery();
            if (encodedQuery != null)
                encodedPath += "?" + encodedQuery;
            traceBuilder.append(request.method()).append(" ").append(encodedPath).append(" HTTP/1.1\n");
            traceBuilder
                    .append(request.headers().toString().replaceAll("Signature=([0-9a-f]+)", "Signature=*REDACTED*").replaceAll(
                            "Credential=([^/]+)", "Credential=*REDACTED*"));
            if (body != null)
                traceBuilder.append("\n").append(new String(body, StandardCharsets.UTF_8));
            traceStream.println(traceBuilder.toString());
        }

        OkHttpClient httpClient = this.httpClient;
        Response response = httpClient.newCall(request).execute();

        if (traceStream != null) {
            String trace = response.protocol().toString().toUpperCase(Locale.US) + " " + response.code() + "\n"
                    + response.headers();
            traceStream.println(trace);
            ResponseBody responseBody = response.peekBody(1024 * 1024);
            traceStream.println(responseBody.string());
            traceStream.println("----------END-HTTP----------");
        }

        if (response.isSuccessful())
            return response;

        throw new RuntimeException("Request failed with response: " + response.body().string());
    }

    /**
     * Adds a user with the specified access and secret key.
     *
     * @param accessKey
     *            Access key.
     * @param status
     *            Status.
     * @param secretKey
     *            Secret key.
     * @param policyName
     *            Policy name.
     * @param memberOf
     *            List of group.
     * @throws NoSuchAlgorithmException
     *             thrown to indicate missing of MD5 or SHA-256 digest library.
     * @throws InvalidKeyException
     *             thrown to indicate missing of HMAC SHA-256 library.
     * @throws IOException
     *             thrown to indicate I/O error on MinIO REST operation.
     * @throws InvalidCipherTextException
     *             thrown to indicate data cannot be encrypted/decrypted.
     */
    public void addUser(
            @Nonnull String accessKey,
            @Nonnull UserInfo.Status status,
            @Nullable String secretKey,
            @Nullable String policyName,
            @Nullable List memberOf)
            throws NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidCipherTextException {
        if (accessKey == null || accessKey.isEmpty()) {
            throw new IllegalArgumentException("access key must be provided");
        }
        UserInfo userInfo = new UserInfo(status, secretKey, policyName, memberOf);

        Credentials creds = getCredentials();
        try (Response response = execute(Method.PUT, Command.ADD_USER, ImmutableMultimap.of("accessKey", accessKey),
                Crypto.encrypt(creds.secretKey(), OBJECT_MAPPER.writeValueAsBytes(userInfo)))) {
        }
    }

    /**
     * Obtains user info for a specified MinIO user.
     *
     * @param accessKey
     *            Access Key.
     * @return user info for the specified accessKey.
     * @throws NoSuchAlgorithmException
     *             thrown to indicate missing of MD5 or SHA-256 digest library.
     * @throws InvalidKeyException
     *             thrown to indicate missing of HMAC SHA-256 library.
     * @throws IOException
     *             thrown to indicate I/O error on MinIO REST operation.
     */
    public UserInfo getUserInfo(String accessKey) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
        try (Response response = execute(Method.GET, Command.USER_INFO, ImmutableMultimap.of("accessKey", accessKey), null)) {
            byte[] jsonData = response.body().bytes();
            return OBJECT_MAPPER.readValue(jsonData, UserInfo.class);
        }
    }

    /**
     * Obtains a list of all MinIO users.
     *
     * @return List of all users.
     * @throws NoSuchAlgorithmException
     *             thrown to indicate missing of MD5 or SHA-256 digest library.
     * @throws InvalidKeyException
     *             thrown to indicate missing of HMAC SHA-256 library.
     * @throws IOException
     *             thrown to indicate I/O error on MinIO REST operation.
     * @throws InvalidCipherTextException
     *             thrown to indicate data cannot be encrypted/decrypted.
     */
    public Map listUsers()
            throws NoSuchAlgorithmException, InvalidKeyException, IOException, InvalidCipherTextException {
        try (Response response = execute(Method.GET, Command.LIST_USERS, null, null)) {
            Credentials creds = getCredentials();
            byte[] jsonData = Crypto.decrypt(creds.secretKey(), response.body().bytes());
            MapType mapType = OBJECT_MAPPER.getTypeFactory().constructMapType(HashMap.class, String.class, UserInfo.class);
            return OBJECT_MAPPER.readValue(jsonData, mapType);
        }
    }

    /**
     * Deletes a user by it's access key
     *
     * @param accessKey
     *            Access Key.
     * @throws NoSuchAlgorithmException
     *             thrown to indicate missing of MD5 or SHA-256 digest library.
     * @throws InvalidKeyException
     *             thrown to indicate missing of HMAC SHA-256 library.
     * @throws IOException
     *             thrown to indicate I/O error on MinIO REST operation.
     */
    public void deleteUser(@Nonnull String accessKey) throws NoSuchAlgorithmException, InvalidKeyException, IOException {
        if (accessKey == null || accessKey.isEmpty()) {
            throw new IllegalArgumentException("access key must be provided");
        }

        try (Response response = execute(Method.DELETE, Command.REMOVE_USER, ImmutableMultimap.of("accessKey", accessKey),
                null)) {
        }
    }

    /**
     * set bucket quota size
     *
     * @param bucketName
     *            bucketName
     * @param size
     *            the capacity of the bucket
     * @param unit
     *            the quota unit of the size argument
     * @throws NoSuchAlgorithmException
     *             thrown to indicate missing of MD5 or SHA-256 digest library.
     * @throws InvalidKeyException
     *             thrown to indicate missing of HMAC SHA-256 library.
     * @throws IOException
     *             thrown to indicate I/O error on MinIO REST operation.
     */
    public void setBucketQuota(@Nonnull String bucketName, long size, @Nonnull QuotaUnit unit)
            throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        Map quotaEntity = new HashMap<>();
        if (size > 0) {
            quotaEntity.put("quotatype", "hard");
        }
        quotaEntity.put("quota", unit.toBytes(size));
        try (Response response = execute(Method.PUT, Command.SET_BUCKET_QUOTA, ImmutableMultimap.of("bucket", bucketName),
                OBJECT_MAPPER.writeValueAsBytes(quotaEntity))) {
        }
    }

    /**
     * get bucket quota size
     *
     * @param bucketName
     *            bucketName
     * @return bytes of bucket
     * @throws IOException
     * @throws NoSuchAlgorithmException
     * @throws InvalidKeyException
     */
    public long getBucketQuota(String bucketName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        try (Response response = execute(Method.GET, Command.GET_BUCKET_QUOTA, ImmutableMultimap.of("bucket", bucketName),
                null)) {
            MapType mapType = OBJECT_MAPPER.getTypeFactory().constructMapType(HashMap.class, String.class, JsonNode.class);
            return OBJECT_MAPPER.> readValue(response.body().bytes(), mapType)
                    .entrySet()
                    .stream()
                    .filter(entry -> "quota".equals(entry.getKey()))
                    .findFirst()
                    .map(entry -> Long.valueOf(entry.getValue().toString()))
                    .orElseThrow(() -> new IllegalArgumentException("found not quota"));
        }
    }

    /**
     * Reset bucket quota
     *
     * @param bucketName
     *            bucketName
     * @throws NoSuchAlgorithmException
     *             thrown to indicate missing of MD5 or SHA-256 digest library.
     * @throws InvalidKeyException
     *             thrown to indicate missing of HMAC SHA-256 library.
     * @throws IOException
     *             thrown to indicate I/O error on MinIO REST operation.
     */
    public void clearBucketQuota(@Nonnull String bucketName) throws IOException, NoSuchAlgorithmException, InvalidKeyException {
        setBucketQuota(bucketName, 0, QuotaUnit.KB);
    }

    /**
     * Creates a policy.
     *
     * 
     * Example:{@code
     * // Assume policyJson contains below JSON string;
     * // {
     * //     "Statement": [
     * //         {
     * //             "Action": "s3:GetObject",
     * //             "Effect": "Allow",
     * //             "Principal": "*",
     * //             "Resource": "arn:aws:s3:::my-bucketname/myobject*"
     * //         }
     * //     ],
     * //     "Version": "2012-10-17"
     * // }
     * //
     * client.addCannedPolicy("my-policy-name", policyJson);
     * }
     * 
* * @param name * Policy name. * @param policy * Policy as JSON string. * @throws NoSuchAlgorithmException * thrown to indicate missing of MD5 or SHA-256 digest library. * @throws InvalidKeyException * thrown to indicate missing of HMAC SHA-256 library. * @throws IOException * thrown to indicate I/O error on MinIO REST operation. */ public void addCannedPolicy(@Nonnull String name, @Nonnull String policy) throws NoSuchAlgorithmException, InvalidKeyException, IOException { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("name must be provided"); } if (policy == null || policy.isEmpty()) { throw new IllegalArgumentException("policy must be provided"); } try (Response response = execute(Method.PUT, Command.ADD_CANNED_POLICY, ImmutableMultimap.of("name", name), policy.getBytes(StandardCharsets.UTF_8))) { } } /** * Sets a policy to a given user or group. * * @param userOrGroupName * User/Group name. * @param isGroup * Flag to denote userOrGroupName is a group name. * @param policyName * Policy name. * @throws NoSuchAlgorithmException * thrown to indicate missing of MD5 or SHA-256 digest library. * @throws InvalidKeyException * thrown to indicate missing of HMAC SHA-256 library. * @throws IOException * thrown to indicate I/O error on MinIO REST operation. */ public void setPolicy(@Nonnull String userOrGroupName, boolean isGroup, @Nonnull String policyName) throws NoSuchAlgorithmException, InvalidKeyException, IOException { if (userOrGroupName == null || userOrGroupName.isEmpty()) { throw new IllegalArgumentException("user/group name must be provided"); } if (policyName == null || policyName.isEmpty()) { throw new IllegalArgumentException("policy name must be provided"); } try (Response response = execute(Method.PUT, Command.SET_USER_OR_GROUP_POLICY, ImmutableMultimap.of("userOrGroup", userOrGroupName, "isGroup", String.valueOf(isGroup), "policyName", policyName), null)) { } } /** * Lists all configured canned policies. * * @return Map of policies, keyed by their name, with their actual policy as * their value. * @throws NoSuchAlgorithmException * thrown to indicate missing of MD5 or SHA-256 digest library. * @throws InvalidKeyException * thrown to indicate missing of HMAC SHA-256 library. * @throws IOException * thrown to indicate I/O error on MinIO REST operation. */ public Map listCannedPolicies() throws NoSuchAlgorithmException, InvalidKeyException, IOException { try (Response response = execute(Method.GET, Command.LIST_CANNED_POLICIES, null, null)) { MapType mapType = OBJECT_MAPPER.getTypeFactory().constructMapType(HashMap.class, String.class, JsonNode.class); HashMap policies = new HashMap<>(); OBJECT_MAPPER.> readValue(response.body().bytes(), mapType) .forEach((key, value) -> policies.put(key, value.toString())); return policies; } } /** * Removes canned policy by name. * * @param name * Policy name. * @throws NoSuchAlgorithmException * thrown to indicate missing of MD5 or SHA-256 digest library. * @throws InvalidKeyException * thrown to indicate missing of HMAC SHA-256 library. * @throws IOException * thrown to indicate I/O error on MinIO REST operation. */ public void removeCannedPolicy(@Nonnull String name) throws NoSuchAlgorithmException, InvalidKeyException, IOException { if (name == null || name.isEmpty()) { throw new IllegalArgumentException("name must be provided"); } try (Response response = execute(Method.DELETE, Command.REMOVE_CANNED_POLICY, ImmutableMultimap.of("name", name), null)) { } } /** * Sets HTTP connect, write and read timeouts. A value of 0 means no * timeout, otherwise values must be between 1 and Integer.MAX_VALUE when * converted to milliseconds. * *
     * Example:{@code
     * minioClient.setTimeout(TimeUnit.SECONDS.toMillis(10), TimeUnit.SECONDS.toMillis(10),
     *     TimeUnit.SECONDS.toMillis(30));
     * }
     * 
* * @param connectTimeout * HTTP connect timeout in milliseconds. * @param writeTimeout * HTTP write timeout in milliseconds. * @param readTimeout * HTTP read timeout in milliseconds. */ public void setTimeout(long connectTimeout, long writeTimeout, long readTimeout) { this.httpClient = HttpUtils.setTimeout(this.httpClient, connectTimeout, writeTimeout, readTimeout); } /** * Ignores check on server certificate for HTTPS connection. * *
     * Example:{@code
     * client.ignoreCertCheck();
     * }
     * 
* * @throws KeyManagementException * thrown to indicate key management error. * @throws NoSuchAlgorithmException * thrown to indicate missing of SSL library. */ @SuppressFBWarnings(value = "SIC", justification = "Should not be used in production anyways.") public void ignoreCertCheck() throws KeyManagementException, NoSuchAlgorithmException { this.httpClient = HttpUtils.disableCertCheck(this.httpClient); } /** * Sets application's name/version to user agent. For more information about * user agent refer #rfc2616. * * @param name * Your application name. * @param version * Your application version. */ public void setAppInfo(String name, String version) { if (name == null || version == null) return; this.userAgent = MinioProperties.INSTANCE.getDefaultUserAgent() + " " + name.trim() + "/" + version.trim(); } /** * Enables HTTP call tracing and written to traceStream. * * @param traceStream * {@link OutputStream} for writing HTTP call tracing. * @see #traceOff */ public void traceOn(OutputStream traceStream) { if (traceStream == null) throw new IllegalArgumentException("trace stream must be provided"); this.traceStream = new PrintWriter(new OutputStreamWriter(traceStream, StandardCharsets.UTF_8), true); } /** * Disables HTTP call tracing previously enabled. * * @see #traceOn * @throws IOException * upon connection error */ public void traceOff() throws IOException { this.traceStream = null; } public static Builder builder() { return new Builder(); } /** Argument builder of {@link MinioAdminClient}. */ public static final class Builder { private HttpUrl baseUrl; private String region = ""; private Provider provider; private OkHttpClient httpClient; public Builder endpoint(String endpoint) { this.baseUrl = HttpUtils.getBaseUrl(endpoint); return this; } public Builder endpoint(String endpoint, int port, boolean secure) { HttpUrl url = HttpUtils.getBaseUrl(endpoint); if (port < 1 || port > 65535) { throw new IllegalArgumentException("port must be in range of 1 to 65535"); } this.baseUrl = url.newBuilder().port(port).scheme(secure ? "https" : "http").build(); return this; } public Builder endpoint(HttpUrl url) { HttpUtils.validateNotNull(url, "url"); HttpUtils.validateUrl(url); this.baseUrl = url; return this; } public Builder endpoint(URL url) { HttpUtils.validateNotNull(url, "url"); return endpoint(HttpUrl.get(url)); } public Builder region(String region) { HttpUtils.validateNotNull(region, "region"); this.region = region; return this; } public Builder credentials(String accessKey, String secretKey) { this.provider = new StaticProvider(accessKey, secretKey, null); return this; } public Builder credentialsProvider(Provider provider) { HttpUtils.validateNotNull(provider, "credential provider"); this.provider = provider; return this; } public Builder httpClient(OkHttpClient httpClient) { HttpUtils.validateNotNull(httpClient, "http client"); this.httpClient = httpClient; return this; } public MinioAdminClient build() { HttpUtils.validateNotNull(baseUrl, "base url"); HttpUtils.validateNotNull(provider, "credential provider"); if (httpClient == null) { httpClient = HttpUtils.newDefaultHttpClient(DEFAULT_CONNECTION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT, DEFAULT_CONNECTION_TIMEOUT); } return new MinioAdminClient(baseUrl, region, provider, httpClient); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy