From c56ca7879332050cc8ffca56d6adadb61dba3e6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E5=88=9D995?= <2308850421@qq.com> Date: Sun, 3 Sep 2023 01:14:21 +0000 Subject: [PATCH 1/2] =?UTF-8?q?:sparkles:=20=E6=94=AF=E6=8C=81S3=E5=AD=98?= =?UTF-8?q?=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/storage/impl/MinIoStorage.java | 8 +- .../common/storage/impl/S3Storage.java | 192 ++++++++++++++++++ .../setting/StorageSettingConverter.java | 2 +- 3 files changed, 197 insertions(+), 5 deletions(-) create mode 100644 eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java diff --git a/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/MinIoStorage.java b/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/MinIoStorage.java index 584177f6..5e637eec 100644 --- a/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/MinIoStorage.java +++ b/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/MinIoStorage.java @@ -67,8 +67,8 @@ public class MinIoStorage extends AbstractStorage { .credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey()).build(); createBucket(this.minioClient, minioConfig); } catch (Exception e) { - log.error("Create bucket excception: {}", e.getMessage(), e); - throw new StorageProviderException("Create bucket excception", e); + log.error("Create bucket exception: {}", e.getMessage(), e); + throw new StorageProviderException("Create bucket exception", e); } } @@ -100,7 +100,7 @@ public class MinIoStorage extends AbstractStorage { + SEPARATOR + URLEncoder.encode(key, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); } catch (Exception e) { - log.error("minio download exception: {}", e.getMessage(), e); + log.error("minio upload exception: {}", e.getMessage(), e); throw new StorageProviderException("minio upload exception", e); } } @@ -117,7 +117,7 @@ public class MinIoStorage extends AbstractStorage { return downloadUrl.replace(minioConfig.getEndpoint(), minioConfig.getDomain()); } catch (Exception e) { log.error("minio download exception: {}", e.getMessage(), e); - throw new StorageProviderException("minio upload exception", e); + throw new StorageProviderException("minio download exception", e); } } diff --git a/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java b/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java new file mode 100644 index 00000000..45c04722 --- /dev/null +++ b/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java @@ -0,0 +1,192 @@ +/* + * eiam-common - Employee Identity and Access Management + * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package cn.topiam.employee.common.storage.impl; + +import cn.topiam.employee.common.storage.AbstractStorage; +import cn.topiam.employee.common.storage.StorageConfig; +import cn.topiam.employee.common.storage.StorageProviderException; +import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.waiters.WaiterResponse; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.*; +import software.amazon.awssdk.services.s3.waiters.S3Waiter; + +import java.io.InputStream; +import java.net.URI; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * S3 协议实现 + * + * @author TopIAM + * Created by support@topiam.cn on 2023/08/29 22:30 + */ +@Slf4j +public class S3Storage extends AbstractStorage { + + private final S3Client s3Client; + + private final StorageConfig.Config config; + + public S3Storage(StorageConfig config) { + super(config); + // 创建连接 + + // 凭证 + AwsBasicCredentials creds; + this.config = config.getConfig(); + try { + // 阿里云 + if (this.config instanceof AliYunOssStorage.Config) { + String accessKeyId = ((AliYunOssStorage.Config) this.config).getAccessKeyId(); + String accessKeySecret = ((AliYunOssStorage.Config) this.config) + .getAccessKeySecret(); + String endpoint = ((AliYunOssStorage.Config) this.config).getEndpoint(); + creds = AwsBasicCredentials.create(accessKeyId, accessKeySecret); + this.s3Client = S3Client.builder() + .serviceConfiguration(b -> b.checksumValidationEnabled(false)) + .credentialsProvider(StaticCredentialsProvider.create(creds)) + .endpointOverride(new URI(endpoint)).build(); + } + // MiniO + else if (this.config instanceof MinIoStorage.Config) { + String accessKey = ((MinIoStorage.Config) this.config).getAccessKey(); + String secretKey = ((MinIoStorage.Config) this.config).getSecretKey(); + String endpoint = ((MinIoStorage.Config) this.config).getEndpoint(); + creds = AwsBasicCredentials.create(accessKey, secretKey); + this.s3Client = S3Client.builder() + .serviceConfiguration(b -> b.checksumValidationEnabled(false)) + .credentialsProvider(StaticCredentialsProvider.create(creds)) + .endpointOverride(new URI(endpoint)).build(); + } + // 七牛云 + else if (this.config instanceof QiNiuKodoStorage.Config) { + String accessKey = ((QiNiuKodoStorage.Config) this.config).getAccessKey(); + String secretKey = ((QiNiuKodoStorage.Config) this.config).getSecretKey(); + String domain = this.config.getDomain(); + creds = AwsBasicCredentials.create(accessKey, secretKey); + this.s3Client = S3Client.builder() + .serviceConfiguration(b -> b.checksumValidationEnabled(false)) + .credentialsProvider(StaticCredentialsProvider.create(creds)) + .endpointOverride(new URI(domain)).build(); + } + // 腾讯云 + else if (this.config instanceof TencentCosStorage.Config) { + String secretId = ((TencentCosStorage.Config) this.config).getSecretId(); + String secretKey = ((TencentCosStorage.Config) this.config).getSecretKey(); + String domain = this.config.getDomain(); + String region = ((TencentCosStorage.Config) this.config).getRegion(); + creds = AwsBasicCredentials.create(secretId, secretKey); + this.s3Client = S3Client.builder() + .serviceConfiguration(b -> b.checksumValidationEnabled(false)) + .credentialsProvider(StaticCredentialsProvider.create(creds)) + .region(Region.of(region)).endpointOverride(new URI(domain)).build(); + } + // 错误 + else { + throw new StorageProviderException("s3Client initialize exception"); + } + createBucket(getBucket()); + } catch (Exception e) { + log.error("s3Client initialize exception: {}", e.getMessage(), e); + throw new StorageProviderException("s3Client initialize exception", e); + } + } + + private void createBucket(String bucket) throws Exception { + try { + // 创建bucket + S3Waiter s3Waiter = this.s3Client.waiter(); + CreateBucketRequest bucketRequest = CreateBucketRequest.builder().bucket(bucket).build(); + // 获取bucket是否存在 + this.s3Client.createBucket(bucketRequest); + HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder().bucket(bucket).build(); + WaiterResponse waiterResponse = s3Waiter + .waitUntilBucketExists(bucketRequestWait); + waiterResponse.matched().response().ifPresent(System.out::println); + } catch (Exception e) { + log.error("create bucket exception: {}", e.getMessage(), e); + throw new StorageProviderException("create bucket exception", e); + } + } + + @Override + public String upload(@NotNull String fileName, + InputStream inputStream) throws StorageProviderException { + try { + super.upload(fileName, inputStream); + String bucket = getBucket(); + String key = this.config.getLocation() + SEPARATOR + getFileName(fileName); + // 写object + PutObjectRequest putOb = PutObjectRequest.builder().bucket(bucket).key(key).build(); + this.s3Client.putObject(putOb, + RequestBody.fromInputStream(inputStream, inputStream.readAllBytes().length)); + return this.config.getDomain() + SEPARATOR + bucket + SEPARATOR + + URLEncoder.encode(key, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); + } catch (Exception e) { + log.error("s3Client upload exception: {}", e.getMessage(), e); + throw new StorageProviderException("s3Client upload exception", e); + } + } + + @Override + public String download(String path) throws StorageProviderException { + try { + super.download(path); + String bucket = getBucket(); + GetUrlRequest request = GetUrlRequest.builder().bucket(bucket).key(path).build(); + + URL url = this.s3Client.utilities().getUrl(request); + return url.toString(); + } catch (Exception e) { + log.error("s3Client download exception: {}", e.getMessage(), e); + throw new StorageProviderException("s3Client download exception", e); + } + } + + private String getBucket() { + String bucket = ""; + if (this.config instanceof AliYunOssStorage.Config) { + bucket = ((AliYunOssStorage.Config) this.config).getBucket(); + } + // MiniO + else if (this.config instanceof MinIoStorage.Config) { + bucket = ((MinIoStorage.Config) this.config).getBucket(); + } + // 七牛云 + else if (this.config instanceof QiNiuKodoStorage.Config) { + bucket = ((QiNiuKodoStorage.Config) this.config).getBucket(); + } + // 腾讯云 + else if (this.config instanceof TencentCosStorage.Config) { + bucket = ((TencentCosStorage.Config) this.config).getBucket(); + } + // 错误 + else { + throw new StorageProviderException("getBucket exception"); + } + return bucket; + } +} diff --git a/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java b/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java index edd6c36a..f934ed0a 100644 --- a/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java +++ b/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java @@ -45,7 +45,7 @@ import jakarta.validation.ValidationException; import static cn.topiam.employee.core.setting.constant.StorageProviderSettingConstants.STORAGE_PROVIDER_KEY; /** - * 消息设置转换器 + * 对象存储设置转换器 * * @author TopIAM * Created by support@topiam.cn on 2021/10/1 23:18 From 264febf4b3fec253a62429abe3f70baea0b42948 Mon Sep 17 00:00:00 2001 From: kay <729048330@qq.com> Date: Mon, 4 Sep 2023 13:19:57 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E5=AD=98=E5=82=A8=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=94=AF=E6=8C=81S3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/storage/enums/StorageProvider.java | 6 +- .../common/storage/impl/S3Storage.java | 239 +++++++++--------- .../pages/setting/Storage/StorageProvider.tsx | 8 + .../setting/Storage/components/S3/index.tsx | 120 +++++++++ .../src/pages/setting/Storage/constant.ts | 1 + .../pages/setting/Storage/locales/zh-CN.ts | 25 ++ .../setting/StorageSettingConverter.java | 20 +- .../controller/AppAccountController.java | 105 ++++++++ .../portal/converter/AppAccountConverter.java | 51 ++++ .../pojo/request/AppAccountRequest.java | 55 ++++ .../portal/service/AppAccountService.java | 53 ++++ .../service/impl/AppAccountServiceImpl.java | 134 ++++++++++ 12 files changed, 692 insertions(+), 125 deletions(-) create mode 100644 eiam-console/src/main/console-fe/src/pages/setting/Storage/components/S3/index.tsx create mode 100644 eiam-portal/src/main/java/cn/topiam/employee/portal/controller/AppAccountController.java create mode 100644 eiam-portal/src/main/java/cn/topiam/employee/portal/converter/AppAccountConverter.java create mode 100644 eiam-portal/src/main/java/cn/topiam/employee/portal/pojo/request/AppAccountRequest.java create mode 100644 eiam-portal/src/main/java/cn/topiam/employee/portal/service/AppAccountService.java create mode 100644 eiam-portal/src/main/java/cn/topiam/employee/portal/service/impl/AppAccountServiceImpl.java diff --git a/eiam-common/src/main/java/cn/topiam/employee/common/storage/enums/StorageProvider.java b/eiam-common/src/main/java/cn/topiam/employee/common/storage/enums/StorageProvider.java index 9ea064fc..3d8ae95d 100644 --- a/eiam-common/src/main/java/cn/topiam/employee/common/storage/enums/StorageProvider.java +++ b/eiam-common/src/main/java/cn/topiam/employee/common/storage/enums/StorageProvider.java @@ -51,7 +51,11 @@ public enum StorageProvider implements Serializable { /** * minio */ - MINIO("minio", "minio", MinIoStorage.class); + MINIO("minio", "minio", MinIoStorage.class), + /** + * S3 + */ + S3("s3", "s3", S3Storage.class); /** * code diff --git a/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java b/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java index 45c04722..192d9445 100644 --- a/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java +++ b/eiam-common/src/main/java/cn/topiam/employee/common/storage/impl/S3Storage.java @@ -17,25 +17,36 @@ */ package cn.topiam.employee.common.storage.impl; +import java.io.InputStream; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.time.Duration; + +import org.apache.commons.lang3.StringUtils; +import org.hibernate.validator.constraints.URL; +import org.jetbrains.annotations.NotNull; + +import cn.topiam.employee.common.jackjson.encrypt.JsonPropertyEncrypt; import cn.topiam.employee.common.storage.AbstractStorage; import cn.topiam.employee.common.storage.StorageConfig; import cn.topiam.employee.common.storage.StorageProviderException; + +import lombok.Data; +import lombok.EqualsAndHashCode; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; + +import jakarta.validation.constraints.NotEmpty; import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; import software.amazon.awssdk.core.sync.RequestBody; -import software.amazon.awssdk.core.waiters.WaiterResponse; import software.amazon.awssdk.regions.Region; import software.amazon.awssdk.services.s3.S3Client; import software.amazon.awssdk.services.s3.model.*; -import software.amazon.awssdk.services.s3.waiters.S3Waiter; - -import java.io.InputStream; -import java.net.URI; -import java.net.URL; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; +import software.amazon.awssdk.services.s3.presigner.S3Presigner; +import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest; +import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest; +import static cn.topiam.employee.common.constant.StorageConstants.URL_REGEXP; /** * S3 协议实现 @@ -46,86 +57,55 @@ import java.nio.charset.StandardCharsets; @Slf4j public class S3Storage extends AbstractStorage { - private final S3Client s3Client; + private final S3Client s3Client; - private final StorageConfig.Config config; + private final S3Presigner s3Presigner; + + private final Config s3Config; public S3Storage(StorageConfig config) { super(config); - // 创建连接 - - // 凭证 - AwsBasicCredentials creds; - this.config = config.getConfig(); - try { - // 阿里云 - if (this.config instanceof AliYunOssStorage.Config) { - String accessKeyId = ((AliYunOssStorage.Config) this.config).getAccessKeyId(); - String accessKeySecret = ((AliYunOssStorage.Config) this.config) - .getAccessKeySecret(); - String endpoint = ((AliYunOssStorage.Config) this.config).getEndpoint(); - creds = AwsBasicCredentials.create(accessKeyId, accessKeySecret); - this.s3Client = S3Client.builder() - .serviceConfiguration(b -> b.checksumValidationEnabled(false)) - .credentialsProvider(StaticCredentialsProvider.create(creds)) - .endpointOverride(new URI(endpoint)).build(); - } - // MiniO - else if (this.config instanceof MinIoStorage.Config) { - String accessKey = ((MinIoStorage.Config) this.config).getAccessKey(); - String secretKey = ((MinIoStorage.Config) this.config).getSecretKey(); - String endpoint = ((MinIoStorage.Config) this.config).getEndpoint(); - creds = AwsBasicCredentials.create(accessKey, secretKey); - this.s3Client = S3Client.builder() - .serviceConfiguration(b -> b.checksumValidationEnabled(false)) - .credentialsProvider(StaticCredentialsProvider.create(creds)) - .endpointOverride(new URI(endpoint)).build(); - } - // 七牛云 - else if (this.config instanceof QiNiuKodoStorage.Config) { - String accessKey = ((QiNiuKodoStorage.Config) this.config).getAccessKey(); - String secretKey = ((QiNiuKodoStorage.Config) this.config).getSecretKey(); - String domain = this.config.getDomain(); - creds = AwsBasicCredentials.create(accessKey, secretKey); - this.s3Client = S3Client.builder() - .serviceConfiguration(b -> b.checksumValidationEnabled(false)) - .credentialsProvider(StaticCredentialsProvider.create(creds)) - .endpointOverride(new URI(domain)).build(); - } - // 腾讯云 - else if (this.config instanceof TencentCosStorage.Config) { - String secretId = ((TencentCosStorage.Config) this.config).getSecretId(); - String secretKey = ((TencentCosStorage.Config) this.config).getSecretKey(); - String domain = this.config.getDomain(); - String region = ((TencentCosStorage.Config) this.config).getRegion(); - creds = AwsBasicCredentials.create(secretId, secretKey); - this.s3Client = S3Client.builder() - .serviceConfiguration(b -> b.checksumValidationEnabled(false)) - .credentialsProvider(StaticCredentialsProvider.create(creds)) - .region(Region.of(region)).endpointOverride(new URI(domain)).build(); - } - // 错误 - else { - throw new StorageProviderException("s3Client initialize exception"); - } - createBucket(getBucket()); - } catch (Exception e) { - log.error("s3Client initialize exception: {}", e.getMessage(), e); - throw new StorageProviderException("s3Client initialize exception", e); - } + // 获取客户端 + this.s3Config = (Config) this.config.getConfig(); + this.s3Client = getS3Client(); + this.s3Presigner = getS3Presigner(); + createBucket(); } - private void createBucket(String bucket) throws Exception { + private S3Client getS3Client() { + return S3Client.builder().serviceConfiguration(b -> b.checksumValidationEnabled(false)) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials + .create(s3Config.getAccessKeyId(), s3Config.getSecretAccessKey()))) + .region(getRegion()).endpointOverride(URI.create(s3Config.getEndpoint())).build(); + } + + private S3Presigner getS3Presigner() { + return S3Presigner.builder().region(getRegion()) + .endpointOverride(URI.create(s3Config.getEndpoint())) + .credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials + .create(s3Config.getAccessKeyId(), s3Config.getSecretAccessKey()))) + .build(); + } + + /** + * 创建Bucket + */ + protected void createBucket() { try { - // 创建bucket - S3Waiter s3Waiter = this.s3Client.waiter(); - CreateBucketRequest bucketRequest = CreateBucketRequest.builder().bucket(bucket).build(); // 获取bucket是否存在 - this.s3Client.createBucket(bucketRequest); - HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder().bucket(bucket).build(); - WaiterResponse waiterResponse = s3Waiter - .waitUntilBucketExists(bucketRequestWait); - waiterResponse.matched().response().ifPresent(System.out::println); + HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder() + .bucket(this.s3Config.getBucket()).build(); + s3Client.headBucket(bucketRequestWait); + } catch (S3Exception se) { + if (se.statusCode() == 404) { + // 创建bucket + CreateBucketRequest bucketRequest = CreateBucketRequest.builder() + .bucket(this.s3Config.getBucket()).build(); + this.s3Client.createBucket(bucketRequest); + } else { + log.error("查询bucket是否存在异常:[{}]", se.getMessage(), se); + throw se; + } } catch (Exception e) { log.error("create bucket exception: {}", e.getMessage(), e); throw new StorageProviderException("create bucket exception", e); @@ -136,57 +116,76 @@ public class S3Storage extends AbstractStorage { public String upload(@NotNull String fileName, InputStream inputStream) throws StorageProviderException { try { - super.upload(fileName, inputStream); - String bucket = getBucket(); - String key = this.config.getLocation() + SEPARATOR + getFileName(fileName); - // 写object - PutObjectRequest putOb = PutObjectRequest.builder().bucket(bucket).key(key).build(); - this.s3Client.putObject(putOb, - RequestBody.fromInputStream(inputStream, inputStream.readAllBytes().length)); - return this.config.getDomain() + SEPARATOR + bucket + SEPARATOR + String key = s3Config.getLocation() + SEPARATOR + getFileName(fileName); + PutObjectRequest putOb = PutObjectRequest.builder().bucket(s3Config.getBucket()) + .key(key).build(); + this.s3Client.putObject(putOb, RequestBody.fromBytes(inputStream.readAllBytes())); + return this.s3Config.getDomain() + SEPARATOR + URLEncoder.encode(key, StandardCharsets.UTF_8).replaceAll("\\+", "%20"); } catch (Exception e) { - log.error("s3Client upload exception: {}", e.getMessage(), e); - throw new StorageProviderException("s3Client upload exception", e); + log.error("[{}] upload exception: {}", this.config.getProvider(), e.getMessage(), e); + throw new StorageProviderException("upload exception", e); } } @Override public String download(String path) throws StorageProviderException { try { - super.download(path); - String bucket = getBucket(); - GetUrlRequest request = GetUrlRequest.builder().bucket(bucket).key(path).build(); - - URL url = this.s3Client.utilities().getUrl(request); - return url.toString(); + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(this.s3Config.getBucket()).key(path).build(); + GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder() + .signatureDuration(Duration.ofSeconds(EXPIRY_SECONDS)) + .getObjectRequest(getObjectRequest).build(); + PresignedGetObjectRequest presignedGetObjectRequest = s3Presigner + .presignGetObject(getObjectPresignRequest); + String downloadUrl = presignedGetObjectRequest.url().toString(); + return downloadUrl.replace(this.s3Config.getEndpoint(), this.s3Config.getDomain()); } catch (Exception e) { - log.error("s3Client download exception: {}", e.getMessage(), e); - throw new StorageProviderException("s3Client download exception", e); + log.error("[{}] download exception: {}", this.config.getProvider(), e.getMessage(), e); + throw new StorageProviderException("download exception", e); } } - private String getBucket() { - String bucket = ""; - if (this.config instanceof AliYunOssStorage.Config) { - bucket = ((AliYunOssStorage.Config) this.config).getBucket(); + private Region getRegion() { + if (StringUtils.isNotBlank(s3Config.getRegion())) { + return Region.of(s3Config.getRegion()); } - // MiniO - else if (this.config instanceof MinIoStorage.Config) { - bucket = ((MinIoStorage.Config) this.config).getBucket(); - } - // 七牛云 - else if (this.config instanceof QiNiuKodoStorage.Config) { - bucket = ((QiNiuKodoStorage.Config) this.config).getBucket(); - } - // 腾讯云 - else if (this.config instanceof TencentCosStorage.Config) { - bucket = ((TencentCosStorage.Config) this.config).getBucket(); - } - // 错误 - else { - throw new StorageProviderException("getBucket exception"); - } - return bucket; + return Region.AWS_GLOBAL; + } + + @EqualsAndHashCode(callSuper = true) + @Data + public static class Config extends StorageConfig.Config { + + /** + * AccessKeyId + */ + @NotEmpty(message = "AccessKeyId不能为空") + private String accessKeyId; + + /** + * SecretAccessKey + */ + @JsonPropertyEncrypt + @NotEmpty(message = "SecretAccessKey不能为空") + private String secretAccessKey; + + /** + * endpoint + */ + @URL(message = "Endpoint格式不正确", regexp = URL_REGEXP) + @NotEmpty(message = "Endpoint不能为空") + private String endpoint; + + /** + * bucket + */ + @NotEmpty(message = "Bucket不能为空") + private String bucket; + + /** + * Region + */ + private String region; } } diff --git a/eiam-console/src/main/console-fe/src/pages/setting/Storage/StorageProvider.tsx b/eiam-console/src/main/console-fe/src/pages/setting/Storage/StorageProvider.tsx index 67847f09..60519887 100644 --- a/eiam-console/src/main/console-fe/src/pages/setting/Storage/StorageProvider.tsx +++ b/eiam-console/src/main/console-fe/src/pages/setting/Storage/StorageProvider.tsx @@ -32,6 +32,7 @@ import AliCloudOss from './components/AliCloud'; import MinIO from './components/MinIo'; import QiQiuKodo from './components/QiNiu'; import TencentCos from './components/Tencent'; +import S3 from './components/S3'; import { Container } from '@/components/Container'; import { useIntl } from '@umijs/max'; @@ -239,12 +240,19 @@ const Storage = () => { id: 'pages.setting.storage_provider.provider.minio', }), }, + { + value: OssProvider.S3, + label: intl.formatMessage({ + id: 'pages.setting.storage_provider.provider.s3', + }), + }, ]} /> {provider === OssProvider.ALIYUN_OSS && } {provider === OssProvider.TENCENT_COS && } {provider === OssProvider.QINIU_KODO && } {provider === OssProvider.MINIO && } + {provider === OssProvider.S3 && } )} diff --git a/eiam-console/src/main/console-fe/src/pages/setting/Storage/components/S3/index.tsx b/eiam-console/src/main/console-fe/src/pages/setting/Storage/components/S3/index.tsx new file mode 100644 index 00000000..23580e02 --- /dev/null +++ b/eiam-console/src/main/console-fe/src/pages/setting/Storage/components/S3/index.tsx @@ -0,0 +1,120 @@ +/* + * eiam-console - Employee Identity and Access Management + * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +import { ProFormText } from '@ant-design/pro-components'; +import { useIntl } from '@umijs/max'; + +export default () => { + const intl = useIntl(); + return ( + <> + + + + + + + + ); +}; diff --git a/eiam-console/src/main/console-fe/src/pages/setting/Storage/constant.ts b/eiam-console/src/main/console-fe/src/pages/setting/Storage/constant.ts index 041547e3..bee29a72 100644 --- a/eiam-console/src/main/console-fe/src/pages/setting/Storage/constant.ts +++ b/eiam-console/src/main/console-fe/src/pages/setting/Storage/constant.ts @@ -26,6 +26,7 @@ export enum OssProvider { QINIU_KODO = 'qiniu_kodo', LOCAL = 'local', MINIO = 'minio', + S3 = 's3', } export enum Language { ZH = 'zh', diff --git a/eiam-console/src/main/console-fe/src/pages/setting/Storage/locales/zh-CN.ts b/eiam-console/src/main/console-fe/src/pages/setting/Storage/locales/zh-CN.ts index 39699ffb..90318df8 100644 --- a/eiam-console/src/main/console-fe/src/pages/setting/Storage/locales/zh-CN.ts +++ b/eiam-console/src/main/console-fe/src/pages/setting/Storage/locales/zh-CN.ts @@ -97,4 +97,29 @@ export default { 'pages.setting.storage_provider.minio.endpoint.rule.0.message': 'MinIO Endpoint为必填项', 'pages.setting.storage_provider.minio.bucket.placeholder': '请输入MinIO Bucket', 'pages.setting.storage_provider.minio.bucket.rule.0.message': 'MinIO Bucket为必填项', + 'pages.setting.storage_provider.provider.s3': 'S3', + 'pages.setting.storage_provider.provider.s3.endpoint': 'S3 域名', + 'pages.setting.storage_provider.provider.s3.endpoint.placeholder': + '请输入 S3 域名', + 'pages.setting.storage_provider.provider.qiniu_kodo.endpoint.rule.0.message': + '七牛云Kodo S3 域名为必填项', + 'pages.setting.storage_provider.provider.s3.domain': '外链域名', + 'pages.setting.storage_provider.provider.s3.domain.placeholder': + '请输入S3 外链域名', + 'pages.setting.storage_provider.provider.s3.domain.rule.0.message': + 'S3 外链域名为必填项', + 'pages.setting.storage_provider.provider.s3.access_key_id.placeholder': + '请输入S3 AccessKeyId', + 'pages.setting.storage_provider.provider.s3.access_key_id.rule.0.message': + 'S3 AccessKeyId为必填项', + 'pages.setting.storage_provider.provider.s3.secret_access_key.placeholder': + '请输入S3 SecretAccessKey', + 'pages.setting.storage_provider.provider.s3.secret_access_key.rule.0.message': + 'S3 SecretAccessKey为必填项', + 'pages.setting.storage_provider.provider.s3.region.placeholder': + '请输入S3 Region', + 'pages.setting.storage_provider.provider.s3.bucket.placeholder': + '请输入S3 Bucket', + 'pages.setting.storage_provider.provider.s3.bucket.rule.0.message': + 'S3 Bucket为必填项', }; diff --git a/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java b/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java index f934ed0a..273d247f 100644 --- a/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java +++ b/eiam-console/src/main/java/cn/topiam/employee/console/converter/setting/StorageSettingConverter.java @@ -33,10 +33,7 @@ import cn.topiam.employee.common.jackjson.encrypt.EncryptionModule; import cn.topiam.employee.common.storage.StorageConfig; import cn.topiam.employee.common.storage.StorageProviderException; import cn.topiam.employee.common.storage.enums.StorageProvider; -import cn.topiam.employee.common.storage.impl.AliYunOssStorage; -import cn.topiam.employee.common.storage.impl.MinIoStorage; -import cn.topiam.employee.common.storage.impl.QiNiuKodoStorage; -import cn.topiam.employee.common.storage.impl.TencentCosStorage; +import cn.topiam.employee.common.storage.impl.*; import cn.topiam.employee.console.pojo.result.setting.StorageProviderConfigResult; import cn.topiam.employee.console.pojo.save.setting.StorageConfigSaveParam; import cn.topiam.employee.support.validation.ValidationUtils; @@ -120,6 +117,21 @@ public interface StorageSettingConverter { unencryptedConfig.setSecretKey(param.getConfig().getString("secretKey")); checkStorage(MinIoStorage::new, unencryptedConfig); } + //S3 + else if (provider.equals(StorageProvider.S3)) { + S3Storage.Config config = objectMapper.readValue(param.getConfig().toJSONString(), + S3Storage.Config.class); + config.setEndpoint(getUrl(config.getEndpoint())); + config.setDomain(getUrl(config.getDomain())); + builder.config(config); + validateEntity(ValidationUtils.validateEntity(config)); + + S3Storage.Config unencryptedConfig = new S3Storage.Config(); + BeanUtils.copyProperties(config, unencryptedConfig); + unencryptedConfig + .setSecretAccessKey(param.getConfig().getString("secretAccessKey")); + checkStorage(S3Storage::new, unencryptedConfig); + } entity.setName(STORAGE_PROVIDER_KEY); // 指定序列化输入的类型 objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(), diff --git a/eiam-portal/src/main/java/cn/topiam/employee/portal/controller/AppAccountController.java b/eiam-portal/src/main/java/cn/topiam/employee/portal/controller/AppAccountController.java new file mode 100644 index 00000000..687503f4 --- /dev/null +++ b/eiam-portal/src/main/java/cn/topiam/employee/portal/controller/AppAccountController.java @@ -0,0 +1,105 @@ +/* + * eiam-portal - Employee Identity and Access Management + * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package cn.topiam.employee.portal.controller; + +import org.springframework.http.MediaType; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import cn.topiam.employee.application.AppAccount; +import cn.topiam.employee.audit.annotation.Audit; +import cn.topiam.employee.audit.event.type.EventType; +import cn.topiam.employee.portal.pojo.request.AppAccountRequest; +import cn.topiam.employee.portal.service.AppAccountService; +import cn.topiam.employee.support.lock.Lock; +import cn.topiam.employee.support.preview.Preview; +import cn.topiam.employee.support.result.ApiRestResult; + +import lombok.AllArgsConstructor; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import static cn.topiam.employee.common.constant.AppConstants.APP_PATH; + +/** + * 应用账户资源 + * + * @author TopIAM + * Created by support@topiam.cn on 2022/6/4 21:06 + */ +@Validated +@Tag(name = "应用账户") +@RestController +@AllArgsConstructor +@RequestMapping(value = APP_PATH + "/account", produces = MediaType.APPLICATION_JSON_VALUE) +public class AppAccountController { + + /** + * 获取应用账户列表 + * + * @param appId {@link String} + * @return {@link } + */ + @Operation(summary = "获取应用账户") + @GetMapping("/appId/{appId}") + public ApiRestResult getAppAccountList(@PathVariable String appId) { + AppAccount appAccount = appAccountService.getAppAccount(Long.valueOf(appId)); + return ApiRestResult.ok(appAccount); + } + + /** + * 创建应用账户 + * + * @param param {@link AppAccountRequest} + * @return {@link Boolean} + */ + @Lock + @Preview + @Operation(summary = "创建应用账户") + @Audit(type = EventType.ADD_APP_ACCOUNT) + @PostMapping(value = "/create") + @PreAuthorize(value = "authenticated and @sae.hasAuthority(T(cn.topiam.employee.support.security.userdetails.UserType).ADMIN)") + public ApiRestResult createAppAccount(@RequestBody @Validated AppAccountRequest param) { + return ApiRestResult. builder().result(appAccountService.createAppAccount(param)) + .build(); + } + + /** + * 删除应用账户 + * + * @param id {@link String} + * @return {@link Boolean} + */ + @Lock + @Preview + @Operation(summary = "删除应用账户") + @Audit(type = EventType.DELETE_APP_ACCOUNT) + @DeleteMapping(value = "/delete/{id}") + @PreAuthorize(value = "authenticated and @sae.hasAuthority(T(cn.topiam.employee.support.security.userdetails.UserType).ADMIN)") + public ApiRestResult deleteAppAccount(@PathVariable(value = "id") String id) { + return ApiRestResult. builder().result(appAccountService.deleteAppAccount(id)) + .build(); + } + + /** + * AppAccountService + */ + private final AppAccountService appAccountService; + +} diff --git a/eiam-portal/src/main/java/cn/topiam/employee/portal/converter/AppAccountConverter.java b/eiam-portal/src/main/java/cn/topiam/employee/portal/converter/AppAccountConverter.java new file mode 100644 index 00000000..1c6b4598 --- /dev/null +++ b/eiam-portal/src/main/java/cn/topiam/employee/portal/converter/AppAccountConverter.java @@ -0,0 +1,51 @@ +/* + * eiam-portal - Employee Identity and Access Management + * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package cn.topiam.employee.portal.converter; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; + +import cn.topiam.employee.common.entity.app.AppAccountEntity; +import cn.topiam.employee.portal.pojo.request.AppAccountRequest; + +/** + * 应用账户映射 + * + * @author TopIAM + * Created by support@topiam.cn on 2023/8/25 21:08 + */ +@Mapper(componentModel = "spring") +public interface AppAccountConverter { + + /** + * 应用账户新增参数转换应用账户实体 + * + * @param param {@link AppAccountRequest} + * @return {@link AppAccountEntity} + */ + @Mapping(target = "userId", ignore = true) + @Mapping(target = "deleted", ignore = true) + @Mapping(target = "remark", ignore = true) + @Mapping(target = "id", ignore = true) + @Mapping(target = "updateTime", ignore = true) + @Mapping(target = "updateBy", ignore = true) + @Mapping(target = "createTime", ignore = true) + @Mapping(target = "createBy", ignore = true) + AppAccountEntity appAccountRequestConvertToEntity(AppAccountRequest param); + +} diff --git a/eiam-portal/src/main/java/cn/topiam/employee/portal/pojo/request/AppAccountRequest.java b/eiam-portal/src/main/java/cn/topiam/employee/portal/pojo/request/AppAccountRequest.java new file mode 100644 index 00000000..120f91fe --- /dev/null +++ b/eiam-portal/src/main/java/cn/topiam/employee/portal/pojo/request/AppAccountRequest.java @@ -0,0 +1,55 @@ +/* + * eiam-portal - Employee Identity and Access Management + * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package cn.topiam.employee.portal.pojo.request; + +import lombok.Data; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; + +/** + * AppAccountRequest 应用账户新增入参 + * + * @author TopIAM + * Created by support@topiam.cn on 2023/8/25 22:13 + */ +@Data +@Schema(description = "应用账户新增入参") +public class AppAccountRequest { + + /** + * 应用ID + */ + @Schema(description = "应用ID") + @NotNull(message = "应用ID不能为空") + private Long appId; + + /** + * 账户名称 + */ + @Schema(description = "账户名称") + @NotBlank(message = "账户名称不能为空") + private String account; + + /** + * 账户密码 + */ + @Schema(description = "账户密码") + private String password; +} diff --git a/eiam-portal/src/main/java/cn/topiam/employee/portal/service/AppAccountService.java b/eiam-portal/src/main/java/cn/topiam/employee/portal/service/AppAccountService.java new file mode 100644 index 00000000..782085a0 --- /dev/null +++ b/eiam-portal/src/main/java/cn/topiam/employee/portal/service/AppAccountService.java @@ -0,0 +1,53 @@ +/* + * eiam-portal - Employee Identity and Access Management + * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package cn.topiam.employee.portal.service; + +import cn.topiam.employee.application.AppAccount; +import cn.topiam.employee.portal.pojo.request.AppAccountRequest; + +/** + * 应用账户 + * + * @author TopIAM + * Created by support@topiam.cn on 2023/8/25 21:07 + */ +public interface AppAccountService { + + /** + * 新增应用账户 + * + * @param param {@link AppAccountRequest} + * @return {@link Boolean} + */ + Boolean createAppAccount(AppAccountRequest param); + + /** + * 删除应用账户 + * + * @param id {@link String} + * @return {@link Boolean} + */ + Boolean deleteAppAccount(String id); + + /** + * 获取应用账户 + * @param appId {@link Long} + * @return {@link AppAccount} + */ + AppAccount getAppAccount(Long appId); +} diff --git a/eiam-portal/src/main/java/cn/topiam/employee/portal/service/impl/AppAccountServiceImpl.java b/eiam-portal/src/main/java/cn/topiam/employee/portal/service/impl/AppAccountServiceImpl.java new file mode 100644 index 00000000..b9f6488a --- /dev/null +++ b/eiam-portal/src/main/java/cn/topiam/employee/portal/service/impl/AppAccountServiceImpl.java @@ -0,0 +1,134 @@ +/* + * eiam-portal - Employee Identity and Access Management + * Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package cn.topiam.employee.portal.service.impl; + +import java.nio.charset.StandardCharsets; +import java.util.Optional; + +import org.apache.commons.codec.binary.Base64; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.alibaba.excel.util.StringUtils; + +import cn.topiam.employee.application.AppAccount; +import cn.topiam.employee.audit.context.AuditContext; +import cn.topiam.employee.audit.entity.Target; +import cn.topiam.employee.audit.enums.TargetType; +import cn.topiam.employee.common.entity.app.AppAccountEntity; +import cn.topiam.employee.common.exception.app.AppAccountExistException; +import cn.topiam.employee.common.jackjson.encrypt.EncryptContextHelp; +import cn.topiam.employee.common.repository.app.AppAccountRepository; +import cn.topiam.employee.portal.converter.AppAccountConverter; +import cn.topiam.employee.portal.pojo.request.AppAccountRequest; +import cn.topiam.employee.portal.service.AppAccountService; +import cn.topiam.employee.support.exception.TopIamException; +import cn.topiam.employee.support.security.util.SecurityUtils; + +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +/** + * 应用账户 + * + * @author TopIAM + * Created by support@topiam.cn on 2023/8/25 21:07 + */ +@Service +@Slf4j +@AllArgsConstructor +public class AppAccountServiceImpl implements AppAccountService { + + /** + * 新增应用账户 + * + * @param param {@link AppAccountRequest} + * @return {@link Boolean} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean createAppAccount(AppAccountRequest param) { + Optional optional = appAccountRepository + .findByAppIdAndUserId(param.getAppId(), Long.valueOf(SecurityUtils.getCurrentUserId())); + if (optional.isPresent()) { + throw new AppAccountExistException(); + } + AppAccountEntity entity = appAccountConverter.appAccountRequestConvertToEntity(param); + //密码不为空 + if (!StringUtils.isBlank(param.getPassword())) { + Base64 base64 = new Base64(); + String password = new String(base64.decode(param.getPassword()), + StandardCharsets.UTF_8); + entity.setPassword(EncryptContextHelp.encrypt(password)); + } + appAccountRepository.save(entity); + AuditContext.setTarget( + Target.builder().id(entity.getUserId().toString()).type(TargetType.USER).build(), + Target.builder().id(entity.getAccount()).type(TargetType.APPLICATION_ACCOUNT).build(), + Target.builder().id(entity.getAppId().toString()).type(TargetType.APPLICATION).build()); + return true; + } + + /** + * 删除应用账户 + * + * @param id {@link Long} + * @return {@link String} + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Boolean deleteAppAccount(String id) { + Optional optional = appAccountRepository.findById(Long.valueOf(id)); + //管理员不存在 + if (optional.isEmpty()) { + AuditContext.setContent("删除失败,应用账户不存在"); + log.warn(AuditContext.getContent()); + throw new TopIamException(AuditContext.getContent()); + } + appAccountRepository.deleteById(Long.valueOf(id)); + AuditContext.setTarget( + Target.builder().id(optional.get().getId().toString()).type(TargetType.USER).build(), + Target.builder().id(optional.get().getAppId().toString()).type(TargetType.APPLICATION) + .build()); + return true; + } + + @Override + public AppAccount getAppAccount(Long appId) { + Optional optional = appAccountRepository.findByAppIdAndUserId(appId, + Long.valueOf(SecurityUtils.getCurrentUserId())); + if (optional.isPresent()) { + AppAccountEntity entity = optional.get(); + AppAccount account = new AppAccount(); + account.setAppId(entity.getAppId()); + account.setAccount(entity.getAccount()); + return account; + } + return null; + } + + /** + * AppAccountConverter + */ + private final AppAccountConverter appAccountConverter; + + /** + * AppAccountRepository + */ + private final AppAccountRepository appAccountRepository; +}