diff --git a/adminapi/src/main/java/io/minio/admin/AddServiceAccountResp.java b/adminapi/src/main/java/io/minio/admin/AddServiceAccountResp.java new file mode 100644 index 000000000..c48c6f606 --- /dev/null +++ b/adminapi/src/main/java/io/minio/admin/AddServiceAccountResp.java @@ -0,0 +1,38 @@ +/* + * 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 io.minio.admin; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.minio.credentials.Credentials; + +/** + * add service account response. + * + *

* @see user-commands.go + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class AddServiceAccountResp { + @JsonProperty("credentials") + private Credentials credentials; + + public Credentials credentials() { + return credentials; + } +} diff --git a/adminapi/src/main/java/io/minio/admin/GetServiceAccountInfoResp.java b/adminapi/src/main/java/io/minio/admin/GetServiceAccountInfoResp.java new file mode 100644 index 000000000..1694bf113 --- /dev/null +++ b/adminapi/src/main/java/io/minio/admin/GetServiceAccountInfoResp.java @@ -0,0 +1,75 @@ +/* + * 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 io.minio.admin; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * service account info. + * + *

* @see user-commands.go + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class GetServiceAccountInfoResp { + @JsonProperty("parentUser") + private String parentUser; + + @JsonProperty("accountStatus") + private String accountStatus; + + @JsonProperty("impliedPolicy") + private boolean impliedPolicy; + + @JsonProperty("policy") + private String policy; + + @JsonProperty("name") + private String name; + + @JsonProperty("description") + private String description; + + @JsonProperty("expiration") + private String expiration; + + public String parentUser() { + return parentUser; + } + + public String accountStatus() { + return accountStatus; + } + + public boolean impliedPolicy() { + return impliedPolicy; + } + + public String description() { + return description; + } + + public String name() { + return name; + } + + public String expiration() { + return expiration; + } +} diff --git a/adminapi/src/main/java/io/minio/admin/ListServiceAccountResp.java b/adminapi/src/main/java/io/minio/admin/ListServiceAccountResp.java new file mode 100644 index 000000000..addc940f8 --- /dev/null +++ b/adminapi/src/main/java/io/minio/admin/ListServiceAccountResp.java @@ -0,0 +1,49 @@ +/* + * 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 io.minio.admin; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +/** list service account response. */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class ListServiceAccountResp { + @JsonProperty("accounts") + private List accounts; + + public List accounts() { + return accounts; + } + + public static class ListServiceAccountInfo { + @JsonProperty("accessKey") + private String accessKey; + + @JsonProperty("expiration") + private String expiration; + + public String expiration() { + return expiration; + } + + public String accessKey() { + return accessKey; + } + } +} diff --git a/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java b/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java index ea4bc8adf..7f8e9f236 100644 --- a/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java +++ b/adminapi/src/main/java/io/minio/admin/MinioAdminClient.java @@ -53,6 +53,7 @@ import java.util.Locale; import java.util.Map; import java.util.concurrent.TimeUnit; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.annotation.Nullable; import okhttp3.HttpUrl; @@ -81,7 +82,12 @@ private enum Command { ADD_UPDATE_REMOVE_GROUP("update-group-members"), GROUP_INFO("group"), LIST_GROUPS("groups"), - INFO("info"); + INFO("info"), + ADD_SERVICE_ACCOUNT("add-service-account"), + UPDATE_SERVICE_ACCOUNT("update-service-account"), + LIST_SERVICE_ACCOUNTS("list-service-accounts"), + DELETE_SERVICE_ACCOUNT("delete-service-account"), + INFO_SERVICE_ACCOUNT("info-service-account"); private final String value; private Command(String value) { @@ -97,6 +103,9 @@ public String toString() { private static final MediaType DEFAULT_MEDIA_TYPE = MediaType.parse("application/octet-stream"); private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private static final Pattern SERVICE_ACCOUNT_NAME_REGEX = + Pattern.compile("^(?!-)(?!_)[a-z_\\d-]{1,31}(?} - 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. @@ -338,7 +347,7 @@ public void addUpdateGroup( * Obtains group info for a specified MinIO group. * * @param group Group name. - * @return group info for the specified group. + * @return {@link GroupInfo} - group info for the specified 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. @@ -355,7 +364,7 @@ public GroupInfo getGroupInfo(String group) /** * Obtains a list of all MinIO groups. * - * @return List of all groups. + * @return {@link List} - List of all groups. * @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. @@ -542,7 +551,8 @@ public void setPolicy( /** * Lists all configured canned policies. * - * @return Map of policies, keyed by their name, with their actual policy as their value. + * @return {@link Map} - 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. @@ -587,7 +597,7 @@ public void removeCannedPolicy(@Nonnull String name) /** * Get server/cluster data usage info * - * @return DataUsageInfo object + * @return {@link DataUsageInfo} - DataUsageInfo object * @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. @@ -602,7 +612,7 @@ public DataUsageInfo getDataUsageInfo() /** * Obtains admin info for the Minio server. * - * @return admin info for the Minio server. + * @return {@link Message} - admin info for the Minio server. * @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. @@ -613,6 +623,219 @@ public Message getServerInfo() throws IOException, NoSuchAlgorithmException, Inv } } + /** + * Creates a new service account belonging to the user sending. + * + * @param accessKey Access key. + * @param secretKey Secret key. + * @param targetUser Target user. + * @param policy Policy as map . + * @param name Service account name. + * @param description Description for this access key. + * @param expiration Expiry time. + * @return {@link Credentials} - Service account 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. + * @throws InvalidCipherTextException thrown to indicate data cannot be encrypted/decrypted. + */ + public Credentials addServiceAccount( + @Nonnull String accessKey, + @Nonnull String secretKey, + @Nullable String targetUser, + @Nullable Map policy, + @Nullable String name, + @Nullable String description, + @Nullable ZonedDateTime expiration) + throws NoSuchAlgorithmException, InvalidKeyException, IOException, + InvalidCipherTextException { + if (accessKey == null || accessKey.isEmpty()) { + throw new IllegalArgumentException("access key must be provided"); + } + if (secretKey == null || secretKey.isEmpty()) { + throw new IllegalArgumentException("secret key must be provided"); + } + if (name != null && !SERVICE_ACCOUNT_NAME_REGEX.matcher(name).find()) { + throw new IllegalArgumentException( + "name must contain non-empty alphanumeric, underscore and hyphen characters not longer than 32 characters"); + } + if (description != null && description.length() > 256) { + throw new IllegalArgumentException("description must be at most 256 characters long"); + } + + Map serviceAccount = new HashMap<>(); + serviceAccount.put("accessKey", accessKey); + serviceAccount.put("secretKey", secretKey); + if (targetUser != null && !targetUser.isEmpty()) { + serviceAccount.put("targetUser", targetUser); + } + if (policy != null && !policy.isEmpty()) { + serviceAccount.put("policy", policy); + } + if (name != null && !name.isEmpty()) { + serviceAccount.put("name", name); + } + if (description != null && !description.isEmpty()) { + serviceAccount.put("description", description); + } + if (expiration != null) { + serviceAccount.put("expiration", expiration.format(Time.EXPIRATION_DATE_FORMAT)); + } + + Credentials creds = getCredentials(); + try (Response response = + execute( + Method.PUT, + Command.ADD_SERVICE_ACCOUNT, + null, + Crypto.encrypt(creds.secretKey(), OBJECT_MAPPER.writeValueAsBytes(serviceAccount)))) { + byte[] jsonData = Crypto.decrypt(creds.secretKey(), response.body().bytes()); + return OBJECT_MAPPER.readValue(jsonData, AddServiceAccountResp.class).credentials(); + } + } + + /** + * Edit an existing service account. + * + * @param accessKey Access key. + * @param newSecretKey New secret key. + * @param newPolicy New policy as JSON string . + * @param newStatus New service account status. + * @param newName New service account name. + * @param newDescription New description. + * @param newExpiration New expiry time. + * @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 updateServiceAccount( + @Nonnull String accessKey, + @Nullable String newSecretKey, + @Nullable Map newPolicy, + @Nullable boolean newStatus, + @Nullable String newName, + @Nullable String newDescription, + @Nullable ZonedDateTime newExpiration) + throws NoSuchAlgorithmException, InvalidKeyException, IOException, + InvalidCipherTextException { + if (accessKey == null || accessKey.isEmpty()) { + throw new IllegalArgumentException("access key must be provided"); + } + if (newName != null && !SERVICE_ACCOUNT_NAME_REGEX.matcher(newName).find()) { + throw new IllegalArgumentException( + "new name must contain non-empty alphanumeric, underscore and hyphen characters not longer than 32 characters"); + } + if (newDescription != null && newDescription.length() > 256) { + throw new IllegalArgumentException("new description must be at most 256 characters long"); + } + + Map serviceAccount = new HashMap<>(); + if (newSecretKey != null && !newSecretKey.isEmpty()) { + serviceAccount.put("newSecretKey", newSecretKey); + } + if (newPolicy != null && !newPolicy.isEmpty()) { + serviceAccount.put("newPolicy", newPolicy); + } + serviceAccount.put("newStatus", newStatus ? "on" : "off"); + if (newName != null && !newName.isEmpty()) { + serviceAccount.put("newName", newName); + } + if (newDescription != null && !newDescription.isEmpty()) { + serviceAccount.put("newDescription", newDescription); + } + if (newExpiration != null) { + serviceAccount.put("newExpiration", newExpiration.format(Time.EXPIRATION_DATE_FORMAT)); + } + + Credentials creds = getCredentials(); + try (Response response = + execute( + Method.POST, + Command.UPDATE_SERVICE_ACCOUNT, + ImmutableMultimap.of("accessKey", accessKey), + Crypto.encrypt(creds.secretKey(), OBJECT_MAPPER.writeValueAsBytes(serviceAccount)))) {} + } + + /** + * Deletes a service account 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 deleteServiceAccount(@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.DELETE_SERVICE_ACCOUNT, + ImmutableMultimap.of("accessKey", accessKey), + null)) {} + } + + /** + * Obtains a list of minio service account by user name. + * + * @param username user name. + * @return {@link ListServiceAccountResp} - List of minio service account. + * @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 ListServiceAccountResp listServiceAccount(@Nonnull String username) + throws NoSuchAlgorithmException, InvalidKeyException, IOException, + InvalidCipherTextException { + if (username == null || username.isEmpty()) { + throw new IllegalArgumentException("user name must be provided"); + } + + try (Response response = + execute( + Method.GET, + Command.LIST_SERVICE_ACCOUNTS, + ImmutableMultimap.of("user", username), + null)) { + Credentials creds = getCredentials(); + byte[] jsonData = Crypto.decrypt(creds.secretKey(), response.body().bytes()); + return OBJECT_MAPPER.readValue(jsonData, ListServiceAccountResp.class); + } + } + + /** + * Obtains service account info for a specified MinIO user. + * + * @param accessKey Access Key. + * @return {@link GetServiceAccountInfoResp} - Service account 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. + * @throws InvalidCipherTextException thrown to indicate data cannot be encrypted/decrypted. + */ + public GetServiceAccountInfoResp getServiceAccountInfo(@Nonnull String accessKey) + throws NoSuchAlgorithmException, InvalidKeyException, IOException, + InvalidCipherTextException { + if (accessKey == null || accessKey.isEmpty()) { + throw new IllegalArgumentException("access key must be provided"); + } + try (Response response = + execute( + Method.GET, + Command.INFO_SERVICE_ACCOUNT, + ImmutableMultimap.of("accessKey", accessKey), + null)) { + Credentials creds = getCredentials(); + byte[] jsonData = Crypto.decrypt(creds.secretKey(), response.body().bytes()); + return OBJECT_MAPPER.readValue(jsonData, GetServiceAccountInfoResp.class); + } + } + /** * 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. diff --git a/api/src/main/java/io/minio/credentials/Credentials.java b/api/src/main/java/io/minio/credentials/Credentials.java index e9b589765..fb28da816 100644 --- a/api/src/main/java/io/minio/credentials/Credentials.java +++ b/api/src/main/java/io/minio/credentials/Credentials.java @@ -16,6 +16,8 @@ package io.minio.credentials; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; import io.minio.messages.ResponseDate; import java.time.Duration; import java.time.ZonedDateTime; @@ -27,24 +29,29 @@ /** Object representation of credentials access key, secret key and session token. */ @Root(name = "Credentials", strict = false) +@JsonIgnoreProperties(ignoreUnknown = true) public class Credentials { @Element(name = "AccessKeyId") + @JsonProperty("accessKey") private final String accessKey; @Element(name = "SecretAccessKey") + @JsonProperty("secretKey") private final String secretKey; @Element(name = "SessionToken") + @JsonProperty("sessionToken") private final String sessionToken; @Element(name = "Expiration") + @JsonProperty("expiration") private final ResponseDate expiration; public Credentials( - @Nonnull @Element(name = "AccessKeyId") String accessKey, - @Nonnull @Element(name = "SecretAccessKey") String secretKey, - @Nullable @Element(name = "SessionToken") String sessionToken, - @Nullable @Element(name = "Expiration") ResponseDate expiration) { + @Nonnull @Element(name = "AccessKeyId") @JsonProperty("accessKey") String accessKey, + @Nonnull @Element(name = "SecretAccessKey") @JsonProperty("secretKey") String secretKey, + @Nullable @Element(name = "SessionToken") @JsonProperty("sessionToken") String sessionToken, + @Nullable @Element(name = "Expiration") @JsonProperty("expiration") ResponseDate expiration) { 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()) {