diff --git a/others/powerjob-mysql.sql b/others/powerjob-mysql.sql index 27568b18..7801a2e4 100644 --- a/others/powerjob-mysql.sql +++ b/others/powerjob-mysql.sql @@ -1,6 +1,6 @@ /* 官方 SQL 仅基于特定版本(MySQL8)导出,不一定兼容其他数据库,也不一定兼容其他版本。此 SQL 仅供参考。 - 如果您的数据库无法使用此 SQL,建议使用 SpringDataJPA 自带的建表能力,先在开发环境直连测试库自动建表,然后自行导出相关的 SQL 即可 + 如果您的数据库无法使用此 SQL,建议使用 SpringDataJPA 自带的建表能力,先在开发环境直连测试库自动建表,然后自行导出相关的 SQL 即可。 Navicat Premium Data Transfer @@ -8,13 +8,13 @@ Source Server Type : MySQL Source Server Version : 80300 (8.3.0) Source Host : localhost:3306 - Source Schema : powerjob4 + Source Schema : powerjob500 Target Server Type : MySQL Target Server Version : 80300 (8.3.0) File Encoding : 65001 - Date: 02/03/2024 18:51:36 + Date: 17/02/2024 22:20:07 */ SET NAMES utf8mb4; @@ -27,10 +27,16 @@ DROP TABLE IF EXISTS `app_info`; CREATE TABLE `app_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `app_name` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, `current_server` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, `gmt_create` datetime(6) DEFAULT NULL, `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `namespace_id` bigint DEFAULT NULL, `password` varchar(255) DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `title` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uidx01_app_info` (`app_name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; @@ -89,13 +95,11 @@ CREATE TABLE `instance_info` ( DROP TABLE IF EXISTS `job_info`; CREATE TABLE `job_info` ( `id` bigint NOT NULL AUTO_INCREMENT, - `advanced_runtime_config` varchar(255) DEFAULT NULL, `alarm_config` varchar(255) DEFAULT NULL, `app_id` bigint DEFAULT NULL, `concurrency` int DEFAULT NULL, `designated_workers` varchar(255) DEFAULT NULL, `dispatch_strategy` int DEFAULT NULL, - `dispatch_strategy_config` varchar(255) DEFAULT NULL, `execute_type` int DEFAULT NULL, `extra` varchar(255) DEFAULT NULL, `gmt_create` datetime(6) DEFAULT NULL, @@ -125,6 +129,27 @@ CREATE TABLE `job_info` ( KEY `idx01_job_info` (`app_id`,`status`,`time_expression_type`,`next_trigger_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +-- ---------------------------- +-- Table structure for namespace +-- ---------------------------- +DROP TABLE IF EXISTS `namespace`; +CREATE TABLE `namespace` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `code` varchar(255) DEFAULT NULL, + `creator` bigint DEFAULT NULL, + `dept` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `modifier` bigint DEFAULT NULL, + `name` varchar(255) DEFAULT NULL, + `status` int DEFAULT NULL, + `tags` varchar(255) DEFAULT NULL, + `token` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_namespace` (`code`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + -- ---------------------------- -- Table structure for oms_lock -- ---------------------------- @@ -138,6 +163,40 @@ CREATE TABLE `oms_lock` ( `ownerip` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uidx01_oms_lock` (`lock_name`) +) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for powerjob_files +-- ---------------------------- +DROP TABLE IF EXISTS `powerjob_files`; +CREATE TABLE `powerjob_files` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID', + `bucket` varchar(255) NOT NULL COMMENT '分桶', + `name` varchar(255) NOT NULL COMMENT '文件名称', + `version` varchar(255) NOT NULL COMMENT '版本', + `meta` varchar(255) DEFAULT NULL COMMENT '元数据', + `length` bigint NOT NULL COMMENT '长度', + `status` int NOT NULL COMMENT '状态', + `data` longblob NOT NULL COMMENT '文件内容', + `extra` varchar(255) DEFAULT NULL COMMENT '其他信息', + `gmt_create` datetime NOT NULL COMMENT '创建时间', + `gmt_modified` datetime DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for pwjb_user_info +-- ---------------------------- +DROP TABLE IF EXISTS `pwjb_user_info`; +CREATE TABLE `pwjb_user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `password` varchar(255) DEFAULT NULL, + `username` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- @@ -154,24 +213,61 @@ CREATE TABLE `server_info` ( KEY `idx01_server_info` (`gmt_modified`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +-- ---------------------------- +-- Table structure for sundry +-- ---------------------------- +DROP TABLE IF EXISTS `sundry`; +CREATE TABLE `sundry` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` varchar(255) DEFAULT NULL, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `pkey` varchar(255) DEFAULT NULL, + `skey` varchar(255) DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `uidx01_sundry` (`pkey`,`skey`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + -- ---------------------------- -- Table structure for user_info -- ---------------------------- DROP TABLE IF EXISTS `user_info`; CREATE TABLE `user_info` ( `id` bigint NOT NULL AUTO_INCREMENT, + `account_type` varchar(255) DEFAULT NULL, `email` varchar(255) DEFAULT NULL, `extra` varchar(255) DEFAULT NULL, `gmt_create` datetime(6) DEFAULT NULL, `gmt_modified` datetime(6) DEFAULT NULL, + `nick` varchar(255) DEFAULT NULL, + `origin_username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, `phone` varchar(255) DEFAULT NULL, + `token_login_verify_info` varchar(255) DEFAULT NULL, `username` varchar(255) DEFAULT NULL, `web_hook` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`), - KEY `uidx01_user_info` (`username`), + UNIQUE KEY `uidx01_user_name` (`username`), KEY `uidx02_user_info` (`email`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ---------------------------- +-- Table structure for user_role +-- ---------------------------- +DROP TABLE IF EXISTS `user_role`; +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) DEFAULT NULL, + `gmt_create` datetime(6) DEFAULT NULL, + `gmt_modified` datetime(6) DEFAULT NULL, + `role` int DEFAULT NULL, + `scope` int DEFAULT NULL, + `target` bigint DEFAULT NULL, + `user_id` bigint DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `uidx01_user_id` (`user_id`) +) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- ---------------------------- -- Table structure for workflow_info diff --git a/others/sql/upgrade/v4.3.x-v5.0.x.sql b/others/sql/upgrade/v4.3.x-v5.0.x.sql new file mode 100644 index 00000000..ee9f2a12 --- /dev/null +++ b/others/sql/upgrade/v4.3.x-v5.0.x.sql @@ -0,0 +1,88 @@ +-- Upgrade SQL FROM 4.1.x to 4.2.x +-- ---------------------------- +-- Table change for app_info +-- ---------------------------- +SET FOREIGN_KEY_CHECKS=0; + +ALTER TABLE `app_info` ADD COLUMN `creator` bigint NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `extra` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `modifier` bigint NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `namespace_id` bigint NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `tags` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `app_info` ADD COLUMN `title` varchar(255) NULL DEFAULT NULL; + +-- ---------------------------- +-- Table change for user_info +-- ---------------------------- +ALTER TABLE `user_info` ADD COLUMN `account_type` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD COLUMN `nick` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD COLUMN `origin_username` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD COLUMN `token_login_verify_info` varchar(255) NULL DEFAULT NULL; +ALTER TABLE `user_info` ADD UNIQUE INDEX `uidx01_user_name`(`username` ASC) USING BTREE; + +-- ---------------------------- +-- new table 'namespace' +-- ---------------------------- +CREATE TABLE `namespace` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `code` varchar(255) NULL DEFAULT NULL, + `creator` bigint NULL DEFAULT NULL, + `dept` varchar(255) NULL DEFAULT NULL, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `modifier` bigint NULL DEFAULT NULL, + `name` varchar(255) NULL DEFAULT NULL, + `status` int NULL DEFAULT NULL, + `tags` varchar(255) NULL DEFAULT NULL, + `token` varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uidx01_namespace`(`code` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + + +-- ---------------------------- +-- new table 'pwjb_user_info' +-- ---------------------------- +CREATE TABLE `pwjb_user_info` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `password` varchar(255) NULL DEFAULT NULL, + `username` varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uidx01_username`(`username` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- new table 'sundry' +-- ---------------------------- +CREATE TABLE `sundry` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `content` varchar(255) NULL DEFAULT NULL, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `pkey` varchar(255) NULL DEFAULT NULL, + `skey` varchar(255) NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `uidx01_sundry`(`pkey` ASC, `skey` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; + + +-- ---------------------------- +-- new table 'user_role' +-- ---------------------------- +CREATE TABLE `user_role` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `extra` varchar(255) NULL DEFAULT NULL, + `gmt_create` datetime(6) NULL DEFAULT NULL, + `gmt_modified` datetime(6) NULL DEFAULT NULL, + `role` int NULL DEFAULT NULL, + `scope` int NULL DEFAULT NULL, + `target` bigint NULL DEFAULT NULL, + `user_id` bigint NULL DEFAULT NULL, + PRIMARY KEY (`id`) USING BTREE, + INDEX `uidx01_user_id`(`user_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic; \ No newline at end of file diff --git a/pom.xml b/pom.xml index f9bf1b28..df64de5a 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ tech.powerjob powerjob - 4.3.9 + 5.0.0-beta2 pom powerjob http://www.powerjob.tech diff --git a/powerjob-client/pom.xml b/powerjob-client/pom.xml index ad852573..5f779289 100644 --- a/powerjob-client/pom.xml +++ b/powerjob-client/pom.xml @@ -5,18 +5,18 @@ powerjob tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 powerjob-client - 4.3.9 + 5.0.0-beta2 jar 5.9.1 1.2.83 - 4.3.9 + 5.0.0-beta2 3.2.4 diff --git a/powerjob-common/pom.xml b/powerjob-common/pom.xml index 16765564..86b1fa7f 100644 --- a/powerjob-common/pom.xml +++ b/powerjob-common/pom.xml @@ -5,12 +5,12 @@ powerjob tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 powerjob-common - 4.3.9 + 5.0.0-beta2 jar diff --git a/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java b/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java index f589e869..4d40d323 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/OmsConstant.java @@ -30,4 +30,6 @@ public class OmsConstant { public static final String HTTP_HEADER_CONTENT_TYPE = "Content-Type"; public static final String JSON_MEDIA_TYPE = "application/json; charset=utf-8"; + + public static final String NULL = "null"; } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java b/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java index d9ecda6d..3ce68960 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/exception/PowerJobException.java @@ -1,13 +1,20 @@ package tech.powerjob.common.exception; +import lombok.Getter; +import lombok.Setter; + /** * PowerJob 运行时异常 * * @author tjq * @since 2020/5/26 */ +@Setter +@Getter public class PowerJobException extends RuntimeException { + protected String code; + public PowerJobException() { } diff --git a/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java b/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java index 03e1dc6b..5cd90519 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/response/ResultDTO.java @@ -8,6 +8,11 @@ import org.apache.commons.lang3.exception.ExceptionUtils; /** * The result object returned by the request + *

+ * 低版本由于 Jackson 序列化配置问题,导致无法在此对象上新增任何字段了,否则会报错 com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException: Unrecognized field "code" (class tech.powerjob.common.response.ObjectResultDTO), not marked as ignorable (3 known properties: "data", "success", "message"]) + * at [Source: (String)"{"success":true,"code":null,"data":2,"message":null}"; line: 1, column: 28] (through reference chain: tech.powerjob.common.response.ObjectResultDTO["code"]) + *

+ * 短期内所有的新增字段需求,都通过新对象继承实现 * * @author tjq * @since 2020/3/30 @@ -17,9 +22,9 @@ import org.apache.commons.lang3.exception.ExceptionUtils; @ToString public class ResultDTO implements PowerSerializable { - private boolean success; - private T data; - private String message; + protected boolean success; + protected T data; + protected String message; public static ResultDTO success(T data) { ResultDTO r = new ResultDTO<>(); diff --git a/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java b/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java index 498d5b73..60507c6d 100644 --- a/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java +++ b/powerjob-common/src/main/java/tech/powerjob/common/utils/CommonUtils.java @@ -7,6 +7,7 @@ import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import java.util.Collection; +import java.util.Date; import java.util.UUID; import java.util.function.Supplier; @@ -147,6 +148,13 @@ public class CommonUtils { return OmsConstant.NONE; } + public static String formatTime(Date date) { + if (date == null) { + return OmsConstant.NONE; + } + return formatTime(date.getTime()); + } + /** * 格式化字符串,如果是 null 或空则显示 N/A * @param str 字符串 diff --git a/powerjob-official-processors/pom.xml b/powerjob-official-processors/pom.xml index 7a8d5550..4425c658 100644 --- a/powerjob-official-processors/pom.xml +++ b/powerjob-official-processors/pom.xml @@ -5,12 +5,12 @@ powerjob tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 powerjob-official-processors - 4.3.9 + 5.0.0-beta2 jar @@ -20,7 +20,7 @@ 5.9.1 1.2.13 - 4.3.9 + 5.0.0-beta2 2.2.224 8.0.28 5.3.31 diff --git a/powerjob-remote/pom.xml b/powerjob-remote/pom.xml index c1629f5d..296125b4 100644 --- a/powerjob-remote/pom.xml +++ b/powerjob-remote/pom.xml @@ -5,7 +5,7 @@ powerjob tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 pom diff --git a/powerjob-remote/powerjob-remote-benchmark/pom.xml b/powerjob-remote/powerjob-remote-benchmark/pom.xml index 230a6e27..afb25789 100644 --- a/powerjob-remote/powerjob-remote-benchmark/pom.xml +++ b/powerjob-remote/powerjob-remote-benchmark/pom.xml @@ -5,7 +5,7 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 @@ -21,8 +21,8 @@ 1.2.13 2.7.18 - 4.3.9 - 4.3.9 + 5.0.0-beta2 + 5.0.0-beta2 3.9.0 4.2.9 diff --git a/powerjob-remote/powerjob-remote-framework/pom.xml b/powerjob-remote/powerjob-remote-framework/pom.xml index a244e551..64369ab8 100644 --- a/powerjob-remote/powerjob-remote-framework/pom.xml +++ b/powerjob-remote/powerjob-remote-framework/pom.xml @@ -5,11 +5,11 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 - 4.3.9 + 5.0.0-beta2 powerjob-remote-framework @@ -17,7 +17,7 @@ 8 UTF-8 - 4.3.9 + 5.0.0-beta2 0.10.2 diff --git a/powerjob-remote/powerjob-remote-impl-akka/pom.xml b/powerjob-remote/powerjob-remote-impl-akka/pom.xml index 4a9b4a23..7bb8b017 100644 --- a/powerjob-remote/powerjob-remote-impl-akka/pom.xml +++ b/powerjob-remote/powerjob-remote-impl-akka/pom.xml @@ -5,19 +5,19 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 powerjob-remote-impl-akka - 4.3.9 + 5.0.0-beta2 8 8 UTF-8 - 4.3.9 + 5.0.0-beta2 2.6.13 diff --git a/powerjob-remote/powerjob-remote-impl-http/pom.xml b/powerjob-remote/powerjob-remote-impl-http/pom.xml index 19e3cce4..efdbee20 100644 --- a/powerjob-remote/powerjob-remote-impl-http/pom.xml +++ b/powerjob-remote/powerjob-remote-impl-http/pom.xml @@ -5,12 +5,12 @@ powerjob-remote tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 powerjob-remote-impl-http - 4.3.9 + 5.0.0-beta2 8 @@ -18,7 +18,7 @@ UTF-8 4.3.7 - 4.3.9 + 5.0.0-beta2 diff --git a/powerjob-server/pom.xml b/powerjob-server/pom.xml index 70bf2ee4..e74a1876 100644 --- a/powerjob-server/pom.xml +++ b/powerjob-server/pom.xml @@ -5,12 +5,12 @@ powerjob tech.powerjob - 4.3.9 + 5.0.0-beta2 4.0.0 powerjob-server - 4.3.9 + 5.0.0-beta2 pom @@ -22,6 +22,7 @@ powerjob-server-migrate powerjob-server-core powerjob-server-monitor + powerjob-server-auth @@ -50,12 +51,12 @@ 3.0.10 9.2.1 - 4.3.9 - 4.3.9 - 4.3.9 + 5.0.0-beta2 + 5.0.0-beta2 + 5.0.0-beta2 1.6.14 3.17.1 - 1.12.665 + 8.5.2 4.4 @@ -96,6 +97,11 @@ powerjob-server-migrate ${project.version} + + tech.powerjob + powerjob-server-auth + ${project.version} + tech.powerjob powerjob-server-starter @@ -114,13 +120,12 @@ aliyun-sdk-oss ${aliyun-sdk-oss.version} - + - com.amazonaws - aws-java-sdk-s3 - ${aws-java-sdk-s3.version} + io.minio + minio + ${minio.version} - org.apache.commons diff --git a/powerjob-server/powerjob-server-auth/pom.xml b/powerjob-server/powerjob-server-auth/pom.xml new file mode 100644 index 00000000..1da713a0 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/pom.xml @@ -0,0 +1,58 @@ + + + + + tech.powerjob + powerjob-server + 5.0.0-beta2 + + + 4.0.0 + + powerjob-server-auth + ${project.parent.version} + + + 8 + 8 + UTF-8 + 0.11.5 + 1.1.86 + + + + + tech.powerjob + powerjob-server-persistence + provided + + + io.jsonwebtoken + jjwt-api + ${jjwt.version} + + + io.jsonwebtoken + jjwt-impl + ${jjwt.version} + runtime + + + io.jsonwebtoken + jjwt-jackson + ${jjwt.version} + runtime + + + + com.aliyun + dingtalk + ${dingtalk.version} + + + + + + \ No newline at end of file diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/LoginUserHolder.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/LoginUserHolder.java new file mode 100644 index 00000000..7cf324a6 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/LoginUserHolder.java @@ -0,0 +1,48 @@ +package tech.powerjob.server.auth; + +/** + * LoginUserHolder + * + * @author tjq + * @since 2023/4/16 + */ +public class LoginUserHolder { + + private static final ThreadLocal TL = new ThreadLocal<>(); + + public static PowerJobUser get() { + return TL.get(); + } + + public static void set(PowerJobUser powerJobUser) { + TL.set(powerJobUser); + } + + public static void clean() { + TL.remove(); + } + + /** + * 获取用户名 + * @return 存在则返回常规用户名,否则返回 unknown + */ + public static String getUserName() { + PowerJobUser powerJobUser = get(); + if (powerJobUser != null) { + return powerJobUser.getUsername(); + } + return "UNKNOWN"; + } + + /** + * 获取用户ID + * @return 存在则返回,否则返回 null + */ + public static Long getUserId() { + PowerJobUser powerJobUser = get(); + if (powerJobUser != null) { + return powerJobUser.getId(); + } + return null; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java new file mode 100644 index 00000000..e6fabb4a --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Permission.java @@ -0,0 +1,40 @@ +package tech.powerjob.server.auth; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 权限 + * + * @author tjq + * @since 2023/3/20 + */ +@Getter +@AllArgsConstructor +public enum Permission { + + /** + * 不需要权限 + */ + NONE(1), + /** + * 读权限,查看控制台数据 + */ + READ(10), + /** + * 写权限,新增/修改任务等 + */ + WRITE(20), + /** + * 运维权限,比如任务的执行 + */ + OPS(30), + /** + * 超级权限 + */ + SU(100) + ; + + + private int v; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/PowerJobUser.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/PowerJobUser.java new file mode 100644 index 00000000..c0a750c1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/PowerJobUser.java @@ -0,0 +1,44 @@ +package tech.powerjob.server.auth; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.io.Serializable; + +/** + * PowerJob 的 登陆用户 + * + * @author tjq + * @since 2023/3/20 + */ +@Getter +@Setter +@ToString +public class PowerJobUser implements Serializable { + + private Long id; + + private String username; + + /** + * 手机号 + */ + private String phone; + /** + * 邮箱地址 + */ + private String email; + /** + * webHook + */ + private String webHook; + /** + * 扩展字段 + */ + private String extra; + + /* ************** 以上为数据库字段 ************** */ + + private String jwtToken; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java new file mode 100644 index 00000000..9b0280ab --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/Role.java @@ -0,0 +1,53 @@ +package tech.powerjob.server.auth; + +import com.google.common.collect.Sets; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.util.Set; + +import static tech.powerjob.server.auth.Permission.*; + +/** + * 角色 + * PowerJob 采用 RBAC 实现权限,出于实际需求的考虑,不决定采用动态权限模型。因此 RBAC 中的角色和权限均在此处定义。 + * 如果有自定义诉求,可以修改 Role 的定义 + * + * @author tjq + * @since 2023/3/20 + */ +@Getter +@AllArgsConstructor +public enum Role { + + /** + * 观察者,默认只读权限 + */ + OBSERVER(10, Sets.newHashSet(READ)), + /** + * 技术质量,读 + 操作权限 + */ + QA(20, Sets.newHashSet(READ, OPS)), + /** + * 开发者,读 + 编辑 + 操作权限 + */ + DEVELOPER(30, Sets.newHashSet(READ, WRITE, OPS)), + /** + * 管理员 + */ + ADMIN(40, Sets.newHashSet(READ, WRITE, OPS, SU)) + ; + + private final int v; + + private final Set permissions; + + public static Role of(int vv) { + for (Role role : values()) { + if (vv == role.v) { + return role; + } + } + throw new IllegalArgumentException("unknown role: " + vv); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java new file mode 100644 index 00000000..cec6f559 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/RoleScope.java @@ -0,0 +1,40 @@ +package tech.powerjob.server.auth; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 权限范围 + * + * @author tjq + * @since 2023/9/3 + */ +@Getter +@AllArgsConstructor +public enum RoleScope { + + /** + * NAMESPACE 权限 + */ + NAMESPACE(1), + /** + * APP 级别权限 + */ + APP(10), + /** + * 全局权限 + */ + GLOBAL(666) + ; + + private final int v; + + public static RoleScope of(int vv) { + for (RoleScope rs : values()) { + if (vv == rs.v) { + return rs; + } + } + throw new IllegalArgumentException("unknown RoleScope: " + vv); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java new file mode 100644 index 00000000..e4983c9d --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthConstants.java @@ -0,0 +1,52 @@ +package tech.powerjob.server.auth.common; + +/** + * 常量 + * + * @author tjq + * @since 2024/2/11 + */ +public class AuthConstants { + + /* ********** 账号体系唯一标识,推荐开发者接入第三方登录体系时也使用4位编码,便于前端统一做样式 ********** */ + /** + * PowerJob自建账号体系 + */ + public static final String ACCOUNT_TYPE_POWER_JOB = "PWJB"; + /** + * 钉钉 + */ + public static final String ACCOUNT_TYPE_DING = "DING"; + /** + * 企业微信(预留,蹲一个 contributor) + */ + public static final String ACCOUNT_TYPE_WX = "QYWX"; + /** + * 飞书(预留,蹲一个 contributor +1) + */ + public static final String ACCOUNT_LARK = "LARK"; + + public static final String PARAM_KEY_USERNAME = "username"; + public static final String PARAM_KEY_PASSWORD = "password"; + /** + * 前端参数-密码加密类型,官方版本出于成本未进行前后端传输的对称加密,接入方有需求可自行实现,此处定义加密协议字段 + */ + public static final String PARAM_KEY_ENCRYPTION = "encryption"; + + /* ********** 账号体系 ********** */ + + /** + * JWT key + * 前端 header 默认首字母大写,保持一致方便处理 + */ + public static final String JWT_NAME = "Power_jwt"; + + /** + * 前端跳转到指定页面指令 + */ + public static final String FE_REDIRECT_KEY = "FE-REDIRECT:"; + + public static final String TIPS_NO_PERMISSION_TO_SEE = "NO_PERMISSION_TO_SEE"; + + public static final Long GLOBAL_ADMIN_TARGET_ID = 1L; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthErrorCode.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthErrorCode.java new file mode 100644 index 00000000..8d51f25d --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/AuthErrorCode.java @@ -0,0 +1,36 @@ +package tech.powerjob.server.auth.common; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 鉴权错误信息 + * + * @author tjq + * @since 2024/2/11 + */ +@Getter +@AllArgsConstructor +public enum AuthErrorCode { + + USER_NOT_LOGIN("-100", "UserNotLoggedIn"), + USER_NOT_EXIST("-101", "UserNotExist"), + USER_AUTH_FAILED("-102", "UserAuthFailed"), + + + NO_PERMISSION("-200", "NoPermission"), + + /** + * 无效请求,一般是参数问题 + */ + INVALID_REQUEST("-300", "INVALID_REQUEST"), + + INCORRECT_PASSWORD("-400", "INCORRECT_PASSWORD"), + + INVALID_TOKEN("-401", "INVALID_TOKEN"), + + ; + + private final String code; + private final String msg; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/PowerJobAuthException.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/PowerJobAuthException.java new file mode 100644 index 00000000..024dd15f --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/PowerJobAuthException.java @@ -0,0 +1,23 @@ +package tech.powerjob.server.auth.common; + +import lombok.Getter; +import tech.powerjob.common.exception.PowerJobException; + +/** + * 鉴权相关错误 + * + * @author tjq + * @since 2024/2/10 + */ +@Getter +public class PowerJobAuthException extends PowerJobException { + + public PowerJobAuthException(AuthErrorCode errorCode) { + this(errorCode, null); + } + + public PowerJobAuthException(AuthErrorCode errorCode, String extraMsg) { + super(extraMsg == null ? errorCode.getMsg() : errorCode.getMsg().concat(":").concat(extraMsg)); + this.code = errorCode.getCode(); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/utils/HttpServletUtils.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/utils/HttpServletUtils.java new file mode 100644 index 00000000..7c232ca1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/common/utils/HttpServletUtils.java @@ -0,0 +1,27 @@ +package tech.powerjob.server.auth.common.utils; + +import tech.powerjob.common.OmsConstant; + +import javax.servlet.http.HttpServletRequest; + +/** + * HttpServletUtils + * + * @author tjq + * @since 2024/2/12 + */ +public class HttpServletUtils { + + public static String fetchFromHeader(String key, HttpServletRequest httpServletRequest) { + // header、cookie 都能获取 + String v = httpServletRequest.getHeader(key); + + // 解决 window.localStorage.getItem 为 null 的问题 + if (OmsConstant.NULL.equalsIgnoreCase(v) || "undefined".equalsIgnoreCase(v)) { + return null; + } + + return v; + } + +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java new file mode 100644 index 00000000..98bfce2e --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermission.java @@ -0,0 +1,45 @@ +package tech.powerjob.server.auth.interceptor; + +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * API 权限 + * + * @author tjq + * @since 2023/3/20 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface ApiPermission { + /** + * API 名称 + * @return 空使用服务.方法名代替 + */ + String name() default ""; + + RoleScope roleScope() default RoleScope.APP; + + /** + * 需要的权限 + * @return 权限 + */ + Permission requiredPermission() default Permission.SU; + + /** + * 固定权限不支持的场景,需要使用动态权限 + * @return 动态权限 + */ + Class dynamicPermissionPlugin() default EmptyPlugin.class; + + /** + * 新增场景,需要授权插件执行授权 + * @return 授权插件 + */ + Class grandPermissionPlugin() default EmptyPlugin.class; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java new file mode 100644 index 00000000..e0179fe2 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/ApiPermissionAspect.java @@ -0,0 +1,65 @@ +package tech.powerjob.server.auth.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.AfterReturning; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.annotation.Pointcut; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; + +/** + * ApiPermission 切面 + * 主要用于执行授权插件,完成创建后授权 + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +@Aspect +@Component +public class ApiPermissionAspect { + + @Pointcut("@annotation(ApiPermission)") + public void apiPermissionPointcut() { + // 定义切入点 + } + + /** + * 后置返回 + * 如果第一个参数为JoinPoint,则第二个参数为返回值的信息 + * 如果第一个参数不为JoinPoint,则第一个参数为returning中对应的参数 + * returning:限定了只有目标方法返回值与通知方法参数类型匹配时才能执行后置返回通知,否则不执行, + * 参数为Object类型将匹配任何目标返回值 + * After注解标注的方法会在目标方法执行后运行,无论目标方法是正常完成还是抛出异常。它相当于finally块,因为它总是执行,所以适用于释放资源等清理活动。@After注解不能访问目标方法的返回值。 + * AfterReturning注解标注的方法仅在目标方法成功执行后(即正常返回)运行。它可以访问目标方法的返回值。使用@AfterReturning可以在方法正常返回后执行一些逻辑,比如对返回值进行处理或验证。 + */ + @AfterReturning(value = "apiPermissionPointcut()", returning = "result") + public void doAfterReturningAdvice1(JoinPoint joinPoint, Object result) { + + // 入参 + Object[] args = joinPoint.getArgs(); + + // 获取目标方法 + MethodSignature signature = (MethodSignature) joinPoint.getSignature(); + Method method = signature.getMethod(); + + ApiPermission annotationAnno = AnnotationUtils.getAnnotation(method, ApiPermission.class); + + assert annotationAnno != null; + + Class grandPermissionPluginClz = annotationAnno.grandPermissionPlugin(); + + try { + GrantPermissionPlugin grandPermissionPlugin = grandPermissionPluginClz.getDeclaredConstructor().newInstance(); + grandPermissionPlugin.grant(args, result, method, joinPoint.getTarget()); + } catch (Exception e) { + log.error("[ApiPermissionAspect] process ApiPermission grant failed", e); + ExceptionUtils.rethrow(e); + } + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/DynamicPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/DynamicPermissionPlugin.java new file mode 100644 index 00000000..504deb6e --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/DynamicPermissionPlugin.java @@ -0,0 +1,15 @@ +package tech.powerjob.server.auth.interceptor; + +import tech.powerjob.server.auth.Permission; + +import javax.servlet.http.HttpServletRequest; + +/** + * 动态权限 + * + * @author tjq + * @since 2023/9/3 + */ +public interface DynamicPermissionPlugin { + Permission calculate(HttpServletRequest request, Object handler); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/EmptyPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/EmptyPlugin.java new file mode 100644 index 00000000..95d00b8f --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/EmptyPlugin.java @@ -0,0 +1,24 @@ +package tech.powerjob.server.auth.interceptor; + +import tech.powerjob.server.auth.Permission; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; + +/** + * 空 + * + * @author tjq + * @since 2024/2/12 + */ +public class EmptyPlugin implements DynamicPermissionPlugin, GrantPermissionPlugin { + @Override + public Permission calculate(HttpServletRequest request, Object handler) { + return null; + } + + @Override + public void grant(Object[] args, Object result, Method method, Object originBean) { + + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/GrantPermissionPlugin.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/GrantPermissionPlugin.java new file mode 100644 index 00000000..34d085ad --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/GrantPermissionPlugin.java @@ -0,0 +1,21 @@ +package tech.powerjob.server.auth.interceptor; + +import java.lang.reflect.Method; + +/** + * 授予权限插件 + * + * @author tjq + * @since 2024/2/11 + */ +public interface GrantPermissionPlugin { + + /** + * 授权 + * @param args 入参 + * @param result 响应 + * @param method 被调用方法 + * @param originBean 原始对象 + */ + void grant(Object[] args, Object result, Method method, Object originBean); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java new file mode 100644 index 00000000..6774fcad --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/interceptor/PowerJobAuthInterceptor.java @@ -0,0 +1,135 @@ +package tech.powerjob.server.auth.interceptor; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.springframework.lang.NonNull; +import org.springframework.stereotype.Component; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.HandlerInterceptor; +import tech.powerjob.common.exception.ImpossibleException; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.common.utils.HttpServletUtils; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.common.Loggers; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.lang.reflect.Method; +import java.util.Optional; + +/** + * login auth and permission check + * + * @author tjq + * @since 2023/3/25 + */ +@Slf4j +@Component +public class PowerJobAuthInterceptor implements HandlerInterceptor { + @Resource + private PowerJobLoginService powerJobLoginService; + @Resource + private PowerJobPermissionService powerJobPermissionService; + + @Override + public boolean preHandle(@NonNull HttpServletRequest request,@NonNull HttpServletResponse response,@NonNull Object handler) throws Exception { + if (!(handler instanceof HandlerMethod)) { + return true; + } + HandlerMethod handlerMethod = (HandlerMethod) handler; + final Method method = handlerMethod.getMethod(); + final ApiPermission apiPermissionAnno = method.getAnnotation(ApiPermission.class); + + // 无注解代表不需要权限,无需登陆直接访问 + if (apiPermissionAnno == null) { + return true; + } + + // 尝试直接解析登陆 + final Optional loginUserOpt = powerJobLoginService.ifLogin(request); + + // 未登录直接报错,返回固定状态码,前端拦截后跳转到登录页 + if (!loginUserOpt.isPresent()) { + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN); + } + + // 登陆用户进行权限校验 + final PowerJobUser powerJobUser = loginUserOpt.get(); + + // 写入上下文 + LoginUserHolder.set(powerJobUser); + + Permission requiredPermission = parsePermission(request, handler, apiPermissionAnno); + RoleScope roleScope = apiPermissionAnno.roleScope(); + Long targetId = null; + + if (RoleScope.NAMESPACE.equals(roleScope)) { + + final String namespaceIdStr = HttpServletUtils.fetchFromHeader("NamespaceId", request); + if (StringUtils.isNotEmpty(namespaceIdStr)) { + targetId = Long.valueOf(namespaceIdStr); + } + } + + if (RoleScope.APP.equals(roleScope)) { + final String appIdStr = HttpServletUtils.fetchFromHeader("AppId", request); + if (StringUtils.isNotEmpty(appIdStr)) { + targetId = Long.valueOf(appIdStr); + } + } + + + final boolean hasPermission = powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, targetId, requiredPermission); + if (hasPermission) { + return true; + } + + final String resourceName = parseResourceName(apiPermissionAnno, handlerMethod); + Loggers.WEB.info("[PowerJobAuthInterceptor] user[{}] has no permission to access: {}", powerJobUser.getUsername(), resourceName); + + throw new PowerJobException("Permission denied!"); + } + + @Override + public void afterCompletion(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull Object handler, Exception ex) throws Exception { + LoginUserHolder.clean(); + } + + private static String parseResourceName(ApiPermission apiPermission, HandlerMethod handlerMethod) { + final String name = apiPermission.name(); + if (StringUtils.isNotEmpty(name)) { + return name; + } + try { + final String clzName = handlerMethod.getBean().getClass().getSimpleName(); + final String methodName = handlerMethod.getMethod().getName(); + return String.format("%s_%s", clzName, methodName); + } catch (Exception ignore) { + } + return "UNKNOWN"; + } + + private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) { + Class dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin(); + if (EmptyPlugin.class.equals(dynamicPermissionPlugin)) { + return apiPermission.requiredPermission(); + } + try { + DynamicPermissionPlugin dynamicPermission = dynamicPermissionPlugin.getDeclaredConstructor().newInstance(); + return dynamicPermission.calculate(request, handler); + } catch (Throwable t) { + log.error("[PowerJobAuthService] process dynamicPermissionPlugin failed!", t); + ExceptionUtils.rethrow(t); + } + throw new ImpossibleException(); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/JwtService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/JwtService.java new file mode 100644 index 00000000..b6ea29a8 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/JwtService.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.jwt; + +import java.util.Map; + +/** + * JWT 服务 + * + * @author tjq + * @since 2023/3/20 + */ +public interface JwtService { + + String build(Map body, String extraSk); + + Map parse(String jwt, String extraSk); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/SecretProvider.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/SecretProvider.java new file mode 100644 index 00000000..349f393e --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/SecretProvider.java @@ -0,0 +1,13 @@ +package tech.powerjob.server.auth.jwt; + +/** + * JWT 安全性的核心 + * 对安全性有要求的接入方,可以自行重新该方法,自定义自己的安全 token 生成策略 + * + * @author tjq + * @since 2023/3/20 + */ +public interface SecretProvider { + + String fetchSecretKey(); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java new file mode 100644 index 00000000..e5ae481f --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/DefaultSecretProvider.java @@ -0,0 +1,47 @@ +package tech.powerjob.server.auth.jwt.impl; + +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; +import tech.powerjob.server.auth.jwt.SecretProvider; +import tech.powerjob.server.common.utils.DigestUtils; + +import javax.annotation.Resource; + +/** + * PowerJob 默认实现 + * + * @author tjq + * @since 2023/3/20 + */ +@Slf4j +@Component +public class DefaultSecretProvider implements SecretProvider { + + @Resource + private Environment environment; + + private static final String PROPERTY_KEY = "spring.datasource.core.jdbc-url"; + + @Override + public String fetchSecretKey() { + + // 考虑到大部分用户都是开箱即用,此处还是提供一个相对安全的默认实现。JDBC URL 部署时必会改,skey 不固定,更安全 + try { + String propertyValue = environment.getProperty(PROPERTY_KEY); + if (StringUtils.isNotEmpty(propertyValue)) { + String md5 = DigestUtils.md5(propertyValue); + + log.debug("[DefaultSecretProvider] propertyValue: {} ==> md5: {}", propertyValue, md5); + + if (StringUtils.isNotEmpty(md5)) { + return md5; + } + } + } catch (Exception ignore) { + } + + return "ZQQZJ"; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/JwtServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/JwtServiceImpl.java new file mode 100644 index 00000000..15cb49bb --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/jwt/impl/JwtServiceImpl.java @@ -0,0 +1,92 @@ +package tech.powerjob.server.auth.jwt.impl; + +import com.google.common.collect.Maps; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.powerjob.server.auth.jwt.JwtService; +import tech.powerjob.server.auth.jwt.SecretProvider; + +import javax.annotation.Resource; +import java.security.Key; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +/** + * JWT 默认实现 + * + * @author tjq + * @since 2023/3/20 + */ +@Service +public class JwtServiceImpl implements JwtService { + + @Resource + private SecretProvider secretProvider; + + /** + * JWT 客户端过期时间 + */ + @Value("${oms.auth.security.jwt.expire-seconds:604800}") + private int jwtExpireTime; + + /** + * GoodSong + */ + private static final String BASE_SECURITY = + "CengMengXiangZhangJianZouTianYa" + + "KanYiKanShiJieDeFanHua" + + "NianShaoDeXinZongYouXieQingKuang" + + "RuJinWoSiHaiWeiJia" + ; + + @Override + public String build(Map body, String extraSk) { + + final String secret = fetchSk(extraSk); + return innerBuild(secret, jwtExpireTime, body); + } + + static String innerBuild(String secret, int expireSeconds, Map body) { + JwtBuilder jwtBuilder = Jwts.builder() + .setHeaderParam("typ", "JWT") + .addClaims(body) + .setSubject("PowerJob") + .setExpiration(new Date(System.currentTimeMillis() + 1000L * expireSeconds)) + .setId(UUID.randomUUID().toString()) + .signWith(genSecretKey(secret), SignatureAlgorithm.HS256); + return jwtBuilder.compact(); + } + + @Override + public Map parse(String jwt, String extraSk) { + return innerParse(fetchSk(extraSk), jwt); + } + + private String fetchSk(String extraSk) { + if (StringUtils.isEmpty(extraSk)) { + return secretProvider.fetchSecretKey(); + } + return secretProvider.fetchSecretKey().concat(extraSk); + } + + static Map innerParse(String secret, String jwtStr) { + final Jws claimsJws = Jwts.parserBuilder() + .setSigningKey(genSecretKey(secret)) + .build() + .parseClaimsJws(jwtStr); + Map ret = Maps.newHashMap(); + ret.putAll(claimsJws.getBody()); + return ret; + } + + private static Key genSecretKey(String secret) { + byte[] keyBytes = Decoders.BASE64.decode(BASE_SECURITY.concat(secret)); + return Keys.hmacShaKeyFor(keyBytes); + } + +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java new file mode 100644 index 00000000..0ecf38a1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/LoginTypeInfo.java @@ -0,0 +1,30 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serializable; + +/** + * 登录类型描述 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class LoginTypeInfo implements Serializable { + + /** + * 登录类型,唯一标识 + */ + private String type; + /** + * 描述名称,前端展示用 + */ + private String name; + /** + * 展示用的 ICON + */ + private String iconUrl; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginRequest.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginRequest.java new file mode 100644 index 00000000..52f74efc --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginRequest.java @@ -0,0 +1,24 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.servlet.http.HttpServletRequest; + +/** + * 第三方登录请求 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class ThirdPartyLoginRequest { + + /** + * 原始参数,给第三方登录方式一个服务端和前端交互的数据通道。PowerJob 本身不感知其中的内容 + */ + private String originParams; + + private transient HttpServletRequest httpServletRequest; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java new file mode 100644 index 00000000..16dd819b --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyLoginService.java @@ -0,0 +1,42 @@ +package tech.powerjob.server.auth.login; + +import javax.servlet.http.HttpServletRequest; + +/** + * 第三方登录服务 + * + * @author tjq + * @since 2024/2/10 + */ +public interface ThirdPartyLoginService { + + /** + * 登陆服务的类型 + * @return 登陆服务类型,比如 PowerJob / DingTalk + */ + LoginTypeInfo loginType(); + + /** + * 生成登陆的重定向 URL + * @param httpServletRequest http请求 + * @return 重定向地址 + */ + String generateLoginUrl(HttpServletRequest httpServletRequest); + + /** + * 执行第三方登录 + * @param loginRequest 上下文 + * @return 登录地址 + */ + ThirdPartyUser login(ThirdPartyLoginRequest loginRequest); + + /** + * JWT 登录的回调校验 + * @param username 用户名称 + * @param tokenLoginVerifyInfo 二次校验信息 + * @return 是否通过 + */ + default boolean tokenLoginVerify(String username, TokenLoginVerifyInfo tokenLoginVerifyInfo) { + return true; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java new file mode 100644 index 00000000..f34604a1 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/ThirdPartyUser.java @@ -0,0 +1,47 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 第三方用户 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class ThirdPartyUser { + + /** + * 用户的唯一标识,用于关联到 PowerJob 的 username + */ + private String username; + /** + * JWT 登录的二次校验配置 + * 可空,空则代表放弃二次校验(会出现第三方登录改了密码当 PowerJob JWT 登录依然可用的情况) + */ + private TokenLoginVerifyInfo tokenLoginVerifyInfo; + + /* ******** 以下全部选填即可,只是方便数据同步,后续都可以去 PowerJob 控制台更改 ******** */ + /** + * 用户昵称 + */ + private String nick; + /** + * 手机号 + */ + private String phone; + /** + * 邮箱地址 + */ + private String email; + /** + * web 回调地址 + */ + private String webHook; + /** + * 扩展字段 + */ + private String extra; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java new file mode 100644 index 00000000..e60f0077 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/TokenLoginVerifyInfo.java @@ -0,0 +1,31 @@ +package tech.powerjob.server.auth.login; + +import lombok.Data; + +import java.io.Serializable; + +/** + * JWT 登录时的校验信息 + * + * @author tjq + * @since 2024/2/16 + */ +@Data +public class TokenLoginVerifyInfo implements Serializable { + + /** + * 加密 token 部分,比如密码的 md5,会直接写入 JWT 下发给前端 + * 如果需要使用 JWT 二次校验,则该参数必须存在 + */ + private String encryptedToken; + + /** + * 补充信息,用于二次校验 + */ + private String additionalInfo; + + /** + * 依然是预留字段,第三方实现自用即可 + */ + private String extra; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java new file mode 100644 index 00000000..c9a646c3 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/DingTalkLoginService.java @@ -0,0 +1,142 @@ +package tech.powerjob.server.auth.login.impl; + +import com.aliyun.dingtalkcontact_1_0.models.GetUserHeaders; +import com.aliyun.dingtalkcontact_1_0.models.GetUserResponseBody; +import com.aliyun.dingtalkoauth2_1_0.models.GetUserTokenRequest; +import com.aliyun.dingtalkoauth2_1_0.models.GetUserTokenResponse; +import com.aliyun.teaopenapi.models.Config; +import com.aliyun.teautil.models.RuntimeOptions; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.login.*; +import tech.powerjob.server.common.Loggers; + +import javax.servlet.http.HttpServletRequest; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 钉钉账号体系登录第三方网站 + * PowerJob 官方支持钉钉账号体系登录原因: + * 1. 钉钉作为当下用户体量最大的企业级办公软件,覆盖率足够高,提供钉钉支持能让更多开发者开箱即用 + * 2. 钉钉的 API 设计和 PowerJob 设想一致,算是个最佳实践,其他企业内部的账号体系可参考这套流程进行接入 + * - PowerJob 重定向到第三方账号体系登陆页 -> 第三方完成登陆 -> 跳转回调 PowerJob auth 接口 -> PowerJob 解析回调登陆信息,完整用户关联 + * + * @author tjq + * @since 2023/3/26 + */ +@Service +public class DingTalkLoginService implements ThirdPartyLoginService { + + /* + 配置示例 + oms.auth.dingtalk.appkey=dinggzqqzqqzqqzqq + oms.auth.dingtalk.appSecret=iY-FS8mzqqzqq_xEizqqzqqzqqzqqzqqzqqYEbkZOal + oms.auth.dingtalk.callbackUrl=http://localhost:7700 + */ + + /** + * 钉钉应用 AppKey + */ + @Value("${oms.auth.dingtalk.appkey:#{null}}") + private String dingTalkAppKey; + /** + * 钉钉应用 AppSecret + */ + @Value("${oms.auth.dingtalk.appSecret:#{null}}") + private String dingTalkAppSecret; + /** + * 回调地址,powerjob 前端控制台地址,即 powerjob-console 地址 + * 比如本地调试时为 LocalDemoCallbackUrl + * 部署后则为 demoCallBackUrl + */ + @Value("${oms.auth.dingtalk.callbackUrl:#{null}}") + private String dingTalkCallbackUrl; + + @Override + public LoginTypeInfo loginType() { + return new LoginTypeInfo() + .setType(AuthConstants.ACCOUNT_TYPE_DING) + .setName("DingTalk") + ; + } + + @Override + @SneakyThrows + public String generateLoginUrl(HttpServletRequest httpServletRequest) { + if (StringUtils.isAnyEmpty(dingTalkAppKey, dingTalkAppSecret, dingTalkCallbackUrl)) { + throw new IllegalArgumentException("please config 'oms.auth.dingtalk.appkey', 'oms.auth.dingtalk.appSecret' and 'oms.auth.dingtalk.callbackUrl' in properties!"); + } + + String urlString = URLEncoder.encode(dingTalkCallbackUrl, StandardCharsets.UTF_8.name()); + String url = "https://login.dingtalk.com/oauth2/auth?" + + "redirect_uri=" + urlString + + "&response_type=code" + + "&client_id=" + dingTalkAppKey + + "&scope=openid" + + "&state=" + AuthConstants.ACCOUNT_TYPE_DING + + "&prompt=consent"; + Loggers.WEB.info("[DingTalkBizLoginService] login url: {}", url); + return url; + } + + @Override + @SneakyThrows + public ThirdPartyUser login(ThirdPartyLoginRequest loginRequest) { + try { + com.aliyun.dingtalkoauth2_1_0.Client client = authClient(); + GetUserTokenRequest getUserTokenRequest = new GetUserTokenRequest() + //应用基础信息-应用信息的AppKey,请务必替换为开发的应用AppKey + .setClientId(dingTalkAppKey) + //应用基础信息-应用信息的AppSecret,,请务必替换为开发的应用AppSecret + .setClientSecret(dingTalkAppSecret) + .setCode(loginRequest.getHttpServletRequest().getParameter("authCode")) + .setGrantType("authorization_code"); + GetUserTokenResponse getUserTokenResponse = client.getUserToken(getUserTokenRequest); + //获取用户个人 token + String accessToken = getUserTokenResponse.getBody().getAccessToken(); + // 查询钉钉用户 + final GetUserResponseBody dingUser = getUserinfo(accessToken); + // 将钉钉用户的唯一ID 和 PowerJob 账户体系的唯一键 username 关联 + if (dingUser != null) { + ThirdPartyUser bizUser = new ThirdPartyUser(); + bizUser.setUsername(dingUser.getUnionId()); + bizUser.setNick(dingUser.getNick()); + bizUser.setPhone(dingUser.getMobile()); + bizUser.setEmail(dingUser.getEmail()); + return bizUser; + } + } catch (Exception e) { + Loggers.WEB.error("[DingTalkBizLoginService] login by dingTalk failed!", e); + throw e; + } + throw new PowerJobException("login from dingTalk failed!"); + } + + /* 以下代码均拷自钉钉官网示例 */ + + private static com.aliyun.dingtalkoauth2_1_0.Client authClient() throws Exception { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + return new com.aliyun.dingtalkoauth2_1_0.Client(config); + } + private static com.aliyun.dingtalkcontact_1_0.Client contactClient() throws Exception { + Config config = new Config(); + config.protocol = "https"; + config.regionId = "central"; + return new com.aliyun.dingtalkcontact_1_0.Client(config); + } + + private GetUserResponseBody getUserinfo(String accessToken) throws Exception { + com.aliyun.dingtalkcontact_1_0.Client client = contactClient(); + GetUserHeaders getUserHeaders = new GetUserHeaders(); + getUserHeaders.xAcsDingtalkAccessToken = accessToken; + //获取用户个人信息,如需获取当前授权人的信息,unionId参数必须传me + return client.getUserWithOptions("me", getUserHeaders, new RuntimeOptions()).getBody(); + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java new file mode 100644 index 00000000..5116e38d --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/login/impl/PwjbAccountLoginService.java @@ -0,0 +1,121 @@ +package tech.powerjob.server.auth.login.impl; + +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.login.*; +import tech.powerjob.server.common.Loggers; +import tech.powerjob.server.common.utils.DigestUtils; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; + +import javax.annotation.Resource; +import javax.servlet.http.HttpServletRequest; +import java.util.Map; +import java.util.Optional; + +/** + * PowerJob 自带的登陆服务 + * 和应用主框架无关,依然属于第三方登录体系 + * + * @author tjq + * @since 2023/3/20 + */ +@Service +public class PwjbAccountLoginService implements ThirdPartyLoginService { + + @Resource + private PwjbUserInfoRepository pwjbUserInfoRepository; + + + @Override + public LoginTypeInfo loginType() { + return new LoginTypeInfo() + .setType(AuthConstants.ACCOUNT_TYPE_POWER_JOB) + .setName("PowerJob Account") + ; + } + + @Override + public String generateLoginUrl(HttpServletRequest httpServletRequest) { + // 前端实现跳转,服务端返回特殊指令 + return AuthConstants.FE_REDIRECT_KEY.concat("powerjobLogin"); + } + + @Override + public ThirdPartyUser login(ThirdPartyLoginRequest loginRequest) { + final String loginInfo = loginRequest.getOriginParams(); + if (StringUtils.isEmpty(loginInfo)) { + throw new IllegalArgumentException("can't find login Info"); + } + + Map loginInfoMap = JsonUtils.parseMap(loginInfo); + + final String username = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_USERNAME); + final String password = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_PASSWORD); + final String encryption = MapUtils.getString(loginInfoMap, AuthConstants.PARAM_KEY_ENCRYPTION); + + Loggers.WEB.debug("[PowerJobLoginService] username: {}, password: {}, encryption: {}", username, password, encryption); + + if (StringUtils.isAnyEmpty(username, password)) { + Loggers.WEB.debug("[PowerJobLoginService] username or password is empty, login failed!"); + throw new PowerJobAuthException(AuthErrorCode.INVALID_REQUEST); + } + + final Optional userInfoOpt = pwjbUserInfoRepository.findByUsername(username); + if (!userInfoOpt.isPresent()) { + Loggers.WEB.debug("[PowerJobLoginService] can't find user by username: {}", username); + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_EXIST); + } + + final PwjbUserInfoDO dbUser = userInfoOpt.get(); + + if (DigestUtils.rePassword(password, username).equals(dbUser.getPassword())) { + ThirdPartyUser bizUser = new ThirdPartyUser(); + bizUser.setUsername(username); + + // 回填第一次创建的信息 + String extra = dbUser.getExtra(); + if (StringUtils.isNotEmpty(extra)) { + ThirdPartyUser material = JsonUtils.parseObjectIgnoreException(extra, ThirdPartyUser.class); + if (material != null) { + bizUser.setEmail(material.getEmail()); + bizUser.setNick(material.getNick()); + bizUser.setPhone(material.getPhone()); + bizUser.setWebHook(material.getWebHook()); + } + } + + // 下发加密的密码作为 JWT 的一部分,方便处理改密码后失效的场景 + TokenLoginVerifyInfo tokenLoginVerifyInfo = new TokenLoginVerifyInfo(); + tokenLoginVerifyInfo.setEncryptedToken(dbUser.getPassword()); + bizUser.setTokenLoginVerifyInfo(tokenLoginVerifyInfo); + + return bizUser; + } + + Loggers.WEB.debug("[PowerJobLoginService] user[{}]'s password is incorrect, login failed!", username); + throw new PowerJobException("password is incorrect"); + } + + @Override + public boolean tokenLoginVerify(String username, TokenLoginVerifyInfo tokenLoginVerifyInfo) { + + if (tokenLoginVerifyInfo == null) { + return false; + } + + final Optional userInfoOpt = pwjbUserInfoRepository.findByUsername(username); + if (userInfoOpt.isPresent()) { + String dbPassword = userInfoOpt.get().getPassword(); + return StringUtils.equals(dbPassword, tokenLoginVerifyInfo.getEncryptedToken()); + } + + return false; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java new file mode 100644 index 00000000..a72bd06c --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/LoginRequest.java @@ -0,0 +1,32 @@ +package tech.powerjob.server.auth.service.login; + +import lombok.Data; +import lombok.experimental.Accessors; + +import javax.servlet.http.HttpServletRequest; + +/** + * 执行登录的请求 + * + * @author tjq + * @since 2024/2/10 + */ +@Data +@Accessors(chain = true) +public class LoginRequest { + + /** + * 登录类型 + */ + private String loginType; + + /** + * 原始参数,给第三方登录方式一个服务端和前端交互的数据通道。PowerJob 本身不感知其中的内容 + */ + private String originParams; + + /** + * http原始请求,第三方回调参数传递类型无法枚举,直接传递 HttpServletRequest 满足扩展性要求 + */ + private transient HttpServletRequest httpServletRequest; +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/PowerJobLoginService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/PowerJobLoginService.java new file mode 100644 index 00000000..f932b5be --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/PowerJobLoginService.java @@ -0,0 +1,48 @@ +package tech.powerjob.server.auth.service.login; + +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.login.LoginTypeInfo; + +import javax.servlet.http.HttpServletRequest; +import java.util.List; +import java.util.Optional; + +/** + * PowerJob 登录服务 + * + * @author tjq + * @since 2024/2/10 + */ +public interface PowerJobLoginService { + + /** + * 获取全部可登录的类型 + * @return 全部可登录类型 + */ + List fetchSupportLoginTypes(); + + + /** + * 获取第三方登录链接 + * @param loginType 登录类型 + * @param httpServletRequest http请求 + * @return 重定向地址 + */ + String fetchThirdPartyLoginUrl(String loginType, HttpServletRequest httpServletRequest); + + /** + * 执行真正的登录请求,底层调用第三方登录服务完成登录 + * @param loginRequest 登录请求 + * @return 登录完成的 PowerJobUser + * @throws PowerJobAuthException 鉴权失败抛出异常 + */ + PowerJobUser doLogin(LoginRequest loginRequest) throws PowerJobAuthException; + + /** + * 从 JWT 信息中解析用户登录信息 + * @param httpServletRequest httpServletRequest + * @return PowerJob 用户 + */ + Optional ifLogin(HttpServletRequest httpServletRequest); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java new file mode 100644 index 00000000..b7da1439 --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/login/impl/PowerJobLoginServiceImpl.java @@ -0,0 +1,236 @@ +package tech.powerjob.server.auth.service.login.impl; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import lombok.Data; +import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.common.utils.HttpServletUtils; +import tech.powerjob.server.auth.jwt.JwtService; +import tech.powerjob.server.auth.login.*; +import tech.powerjob.server.auth.service.login.LoginRequest; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.common.Loggers; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; + +import javax.servlet.http.HttpServletRequest; +import java.io.Serializable; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * PowerJob 登录服务 + * + * @author tjq + * @since 2024/2/10 + */ +@Slf4j +@Service +public class PowerJobLoginServiceImpl implements PowerJobLoginService { + + private final JwtService jwtService; + private final UserInfoRepository userInfoRepository; + private final Map code2ThirdPartyLoginService; + + @Autowired + public PowerJobLoginServiceImpl(JwtService jwtService, UserInfoRepository userInfoRepository, List thirdPartyLoginServices) { + + this.jwtService = jwtService; + this.userInfoRepository = userInfoRepository; + + code2ThirdPartyLoginService = Maps.newHashMap(); + thirdPartyLoginServices.forEach(s -> { + code2ThirdPartyLoginService.put(s.loginType().getType(), s); + log.info("[PowerJobLoginService] register ThirdPartyLoginService: {}", s.loginType()); + }); + } + + @Override + public List fetchSupportLoginTypes() { + return Lists.newArrayList(code2ThirdPartyLoginService.values()).stream().map(ThirdPartyLoginService::loginType).collect(Collectors.toList()); + } + + @Override + public String fetchThirdPartyLoginUrl(String type, HttpServletRequest httpServletRequest) { + final ThirdPartyLoginService thirdPartyLoginService = fetchBizLoginService(type); + return thirdPartyLoginService.generateLoginUrl(httpServletRequest); + } + + @Override + public PowerJobUser doLogin(LoginRequest loginRequest) throws PowerJobAuthException { + final String loginType = loginRequest.getLoginType(); + final ThirdPartyLoginService thirdPartyLoginService = fetchBizLoginService(loginType); + + ThirdPartyLoginRequest thirdPartyLoginRequest = new ThirdPartyLoginRequest() + .setOriginParams(loginRequest.getOriginParams()) + .setHttpServletRequest(loginRequest.getHttpServletRequest()); + + final ThirdPartyUser bizUser = thirdPartyLoginService.login(thirdPartyLoginRequest); + + String dbUserName = String.format("%s_%s", loginType, bizUser.getUsername()); + Optional powerJobUserOpt = userInfoRepository.findByUsername(dbUserName); + + // 如果不存在用户,先同步创建用户 + if (!powerJobUserOpt.isPresent()) { + UserInfoDO newUser = new UserInfoDO(); + newUser.setUsername(dbUserName); + // 写入账号体系类型 + newUser.setAccountType(loginType); + newUser.setOriginUsername(bizUser.getUsername()); + + newUser.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo())); + + // 同步素材 + newUser.setEmail(bizUser.getEmail()); + newUser.setPhone(bizUser.getPhone()); + newUser.setNick(bizUser.getNick()); + newUser.setWebHook(bizUser.getWebHook()); + newUser.setExtra(bizUser.getExtra()); + + Loggers.WEB.info("[PowerJobLoginService] sync user to PowerJobUserSystem: {}", dbUserName); + userInfoRepository.saveAndFlush(newUser); + + powerJobUserOpt = userInfoRepository.findByUsername(dbUserName); + } else { + + // 更新二次校验的 TOKEN 信息 + UserInfoDO dbUserInfoDO = powerJobUserOpt.get(); + + dbUserInfoDO.setTokenLoginVerifyInfo(JsonUtils.toJSONString(bizUser.getTokenLoginVerifyInfo())); + dbUserInfoDO.setGmtModified(new Date()); + + userInfoRepository.saveAndFlush(dbUserInfoDO); + } + + PowerJobUser ret = new PowerJobUser(); + + // 理论上 100% 存在 + if (powerJobUserOpt.isPresent()) { + final UserInfoDO dbUser = powerJobUserOpt.get(); + BeanUtils.copyProperties(dbUser, ret); + ret.setUsername(dbUserName); + } + + fillJwt(ret, Optional.ofNullable(bizUser.getTokenLoginVerifyInfo()).map(TokenLoginVerifyInfo::getEncryptedToken).orElse(null)); + + return ret; + } + + @Override + public Optional ifLogin(HttpServletRequest httpServletRequest) { + final Optional jwtBodyOpt = parseJwt(httpServletRequest); + if (!jwtBodyOpt.isPresent()) { + return Optional.empty(); + } + + JwtBody jwtBody = jwtBodyOpt.get(); + + Optional dbUserInfoOpt = userInfoRepository.findByUsername(jwtBody.getUsername()); + if (!dbUserInfoOpt.isPresent()) { + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_EXIST); + } + + UserInfoDO dbUser = dbUserInfoOpt.get(); + + PowerJobUser powerJobUser = new PowerJobUser(); + + String tokenLoginVerifyInfoStr = dbUser.getTokenLoginVerifyInfo(); + TokenLoginVerifyInfo tokenLoginVerifyInfo = Optional.ofNullable(tokenLoginVerifyInfoStr).map(x -> JsonUtils.parseObjectIgnoreException(x, TokenLoginVerifyInfo.class)).orElse(new TokenLoginVerifyInfo()); + + // DB 中的 encryptedToken 存在,代表需要二次校验 + if (StringUtils.isNotEmpty(tokenLoginVerifyInfo.getEncryptedToken())) { + if (!StringUtils.equals(jwtBody.getEncryptedToken(), tokenLoginVerifyInfo.getEncryptedToken())) { + throw new PowerJobAuthException(AuthErrorCode.INVALID_TOKEN); + } + + ThirdPartyLoginService thirdPartyLoginService = code2ThirdPartyLoginService.get(dbUser.getAccountType()); + boolean tokenLoginVerifyOk = thirdPartyLoginService.tokenLoginVerify(dbUser.getOriginUsername(), tokenLoginVerifyInfo); + + if (!tokenLoginVerifyOk) { + throw new PowerJobAuthException(AuthErrorCode.USER_AUTH_FAILED); + } + } + + BeanUtils.copyProperties(dbUser, powerJobUser); + + // 兼容某些直接通过 ifLogin 判断登录的场景 + LoginUserHolder.set(powerJobUser); + + return Optional.of(powerJobUser); + } + + private ThirdPartyLoginService fetchBizLoginService(String loginType) { + final ThirdPartyLoginService loginService = code2ThirdPartyLoginService.get(loginType); + if (loginService == null) { + throw new PowerJobAuthException(AuthErrorCode.INVALID_REQUEST, "can't find ThirdPartyLoginService by type: " + loginType); + } + return loginService; + } + + private void fillJwt(PowerJobUser powerJobUser, String encryptedToken) { + + // 不能下发 userId,容易被轮询爆破 + JwtBody jwtBody = new JwtBody(); + jwtBody.setUsername(powerJobUser.getUsername()); + if (StringUtils.isNotEmpty(encryptedToken)) { + jwtBody.setEncryptedToken(encryptedToken); + } + + Map jwtMap = JsonUtils.parseMap(JsonUtils.toJSONString(jwtBody)); + + powerJobUser.setJwtToken(jwtService.build(jwtMap, null)); + } + + @SneakyThrows + private Optional parseJwt(HttpServletRequest httpServletRequest) { + // header、cookie 都能获取 + String jwtStr = HttpServletUtils.fetchFromHeader(AuthConstants.JWT_NAME, httpServletRequest); + + /* + + 开发阶段跨域无法简单传输 cookies,暂时采取 header 方案传输 JWT + + if (StringUtils.isEmpty(jwtStr)) { + for (Cookie cookie : Optional.ofNullable(httpServletRequest.getCookies()).orElse(new Cookie[]{})) { + if (cookie.getName().equals(AuthConstants.JWT_NAME)) { + jwtStr = cookie.getValue(); + } + } + } + */ + + if (StringUtils.isEmpty(jwtStr)) { + return Optional.empty(); + } + final Map jwtBodyMap = jwtService.parse(jwtStr, null); + + if (MapUtils.isEmpty(jwtBodyMap)) { + return Optional.empty(); + } + + return Optional.ofNullable(JsonUtils.parseObject(JsonUtils.toJSONString(jwtBodyMap), JwtBody.class)); + } + + @Data + static class JwtBody implements Serializable { + + private String username; + + private String encryptedToken; + } +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java new file mode 100644 index 00000000..db534abd --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionService.java @@ -0,0 +1,63 @@ +package tech.powerjob.server.auth.service.permission; + +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; + +import java.util.List; +import java.util.Map; + +/** + * PowerJob 鉴权服务 + * + * @author tjq + * @since 2024/2/11 + */ +public interface PowerJobPermissionService { + + + /** + * 判断用户是否有访问权限 + * @param userId userId + * @param roleScope 权限范围 + * @param target 权限目标ID + * @param permission 要求的权限 + * @return 是否有权限 + */ + boolean hasPermission(Long userId, RoleScope roleScope, Long target, Permission permission); + + /** + * 授予用户角色 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param userId 用户ID + * @param role 角色 + * @param extra 其他 + */ + void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra); + + /** + * 回收用户角色 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param userId 用户ID + * @param role 角色 + */ + void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role); + + /** + * 获取有相关权限的用户 + * @param roleScope 角色范围 + * @param target 目标 + * @return 角色对应的用户列表 + */ + Map> fetchUserWithPermissions(RoleScope roleScope, Long target); + + /** + * 获取用户有权限的目标 + * @param roleScope 角色范围 + * @param userId 用户ID + * @return result + */ + Map> fetchUserHadPermissionTargets(RoleScope roleScope, Long userId); +} diff --git a/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java new file mode 100644 index 00000000..dcbad2da --- /dev/null +++ b/powerjob-server/powerjob-server-auth/src/main/java/tech/powerjob/server/auth/service/permission/PowerJobPermissionServiceImpl.java @@ -0,0 +1,177 @@ +package tech.powerjob.server.auth.service.permission; + +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.collect.Multimap; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.UserRoleDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.persistence.remote.repository.UserRoleRepository; + +import javax.annotation.Resource; +import java.util.*; + +/** + * PowerJobPermissionService + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +@Service +public class PowerJobPermissionServiceImpl implements PowerJobPermissionService { + + @Resource + private AppInfoRepository appInfoRepository; + @Resource + private UserRoleRepository userRoleRepository; + + @Override + public boolean hasPermission(Long userId, RoleScope roleScope, Long target, Permission requiredPermission) { + final List userRoleList = Optional.ofNullable(userRoleRepository.findAllByUserId(userId)).orElse(Collections.emptyList()); + + Multimap appId2Role = ArrayListMultimap.create(); + Multimap namespaceId2Role = ArrayListMultimap.create(); + + List globalRoles = Lists.newArrayList(); + + for (UserRoleDO userRole : userRoleList) { + final Role role = Role.of(userRole.getRole()); + + // 处理全局权限 + if (RoleScope.GLOBAL.getV() == userRole.getScope()) { + if (Role.ADMIN.equals(role)) { + return true; + } + globalRoles.add(role); + } + + if (RoleScope.NAMESPACE.getV() == userRole.getScope()) { + namespaceId2Role.put(userRole.getTarget(), role); + } + if (RoleScope.APP.getV() == userRole.getScope()) { + appId2Role.put(userRole.getTarget(), role); + } + } + + // 前置判断需要的权限(新增场景还没有 appId or namespaceId) + if (requiredPermission == Permission.NONE) { + return true; + } + + // 检验全局穿透权限 + for (Role role : globalRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + // 无超级管理员权限,校验普通权限 + if (RoleScope.APP.equals(roleScope)) { + return checkAppPermission(target, requiredPermission, appId2Role, namespaceId2Role); + } + + if (RoleScope.NAMESPACE.equals(roleScope)) { + return checkNamespacePermission(target, requiredPermission, namespaceId2Role); + } + + return false; + } + + @Override + public void grantRole(RoleScope roleScope, Long target, Long userId, Role role, String extra) { + + UserRoleDO userRoleDO = new UserRoleDO(); + userRoleDO.setGmtCreate(new Date()); + userRoleDO.setGmtModified(new Date()); + userRoleDO.setExtra(extra); + + userRoleDO.setScope(roleScope.getV()); + userRoleDO.setTarget(target); + userRoleDO.setUserId(userId); + userRoleDO.setRole(role.getV()); + + userRoleRepository.saveAndFlush(userRoleDO); + log.info("[PowerJobPermissionService] [grantPermission] saveAndFlush userRole successfully: {}", userRoleDO); + } + + @Override + public void retrieveRole(RoleScope roleScope, Long target, Long userId, Role role) { + List originUserRole = userRoleRepository.findAllByScopeAndTargetAndRoleAndUserId(roleScope.getV(), target, role.getV(), userId); + log.info("[PowerJobPermissionService] [retrievePermission] origin rule: {}", originUserRole); + Optional.ofNullable(originUserRole).orElse(Collections.emptyList()).forEach(r -> { + userRoleRepository.deleteById(r.getId()); + log.info("[PowerJobPermissionService] [retrievePermission] delete UserRole: {}", r); + }); + } + + @Override + public Map> fetchUserWithPermissions(RoleScope roleScope, Long target) { + List permissionUserList = userRoleRepository.findAllByScopeAndTarget(roleScope.getV(), target); + Map> ret = Maps.newHashMap(); + Optional.ofNullable(permissionUserList).orElse(Collections.emptyList()).forEach(userRoleDO -> { + Role role = Role.of(userRoleDO.getRole()); + List userIds = ret.computeIfAbsent(role, ignore -> Lists.newArrayList()); + userIds.add(userRoleDO.getUserId()); + }); + return ret; + } + + @Override + public Map> fetchUserHadPermissionTargets(RoleScope roleScope, Long userId) { + + Map> ret = Maps.newHashMap(); + List userRoleDOList = userRoleRepository.findAllByUserIdAndScope(userId, roleScope.getV()); + + Optional.ofNullable(userRoleDOList).orElse(Collections.emptyList()).forEach(r -> { + Role role = Role.of(r.getRole()); + List targetIds = ret.computeIfAbsent(role, ignore -> Lists.newArrayList()); + targetIds.add(r.getTarget()); + }); + + return ret; + } + + private boolean checkAppPermission(Long targetId, Permission requiredPermission, Multimap appId2Role, Multimap namespaceId2Role) { + + final Collection appRoles = appId2Role.get(targetId); + for (Role role : appRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + // 校验 namespace 穿透权限 + Optional appInfoOpt = appInfoRepository.findById(targetId); + if (!appInfoOpt.isPresent()) { + throw new IllegalArgumentException("can't find appInfo by appId in permission check: " + targetId); + } + Long namespaceId = Optional.ofNullable(appInfoOpt.get().getNamespaceId()).orElse(-1L); + Collection namespaceRoles = namespaceId2Role.get(namespaceId); + for (Role role : namespaceRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + return false; + } + + private boolean checkNamespacePermission(Long targetId, Permission requiredPermission, Multimap namespaceId2Role) { + Collection namespaceRoles = namespaceId2Role.get(targetId); + for (Role role : namespaceRoles) { + if (role.getPermissions().contains(requiredPermission)) { + return true; + } + } + + return false; + } + +} diff --git a/powerjob-server/powerjob-server-common/pom.xml b/powerjob-server/powerjob-server-common/pom.xml index 72d44c41..3cc6c82d 100644 --- a/powerjob-server/powerjob-server-common/pom.xml +++ b/powerjob-server/powerjob-server-common/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java new file mode 100644 index 00000000..20b0e58c --- /dev/null +++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/Loggers.java @@ -0,0 +1,18 @@ +package tech.powerjob.server.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 统一定义日志 + * + * @author tjq + * @since 2023/3/25 + */ +public class Loggers { + + /** + * Web 层统一日志 + */ + public static final Logger WEB = LoggerFactory.getLogger("P_SERVER_LOGGER_WEB"); +} diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java index 89a89730..eb2051a8 100644 --- a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java +++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/SJ.java @@ -3,6 +3,8 @@ package tech.powerjob.server.common; import com.google.common.base.Joiner; import com.google.common.base.Splitter; +import java.util.Map; + /** * Splitter & Joiner * @@ -16,4 +18,9 @@ public class SJ { public static final Joiner MONITOR_JOINER = Joiner.on("|").useForNull("-"); + private static final Splitter.MapSplitter MAP_SPLITTER = Splitter.onPattern(";").withKeyValueSeparator(":"); + + public static Map splitKvString(String kvString) { + return MAP_SPLITTER.split(kvString); + } } diff --git a/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java new file mode 100644 index 00000000..9a6bbee8 --- /dev/null +++ b/powerjob-server/powerjob-server-common/src/main/java/tech/powerjob/server/common/utils/DigestUtils.java @@ -0,0 +1,41 @@ +package tech.powerjob.server.common.utils; + +import lombok.SneakyThrows; + +import java.math.BigInteger; +import java.security.MessageDigest; + +/** + * 加密工具 + * + * @author tjq + * @since 2023/3/25 + */ +public class DigestUtils { + + /** + * 32位小写 md5 + * @param input 输入 + * @return md5 + */ + @SneakyThrows + public static String md5(String input) { + MessageDigest md5 = MessageDigest.getInstance("MD5"); + md5.update(input.getBytes()); + byte[] byteArray = md5.digest(); + + BigInteger bigInt = new BigInteger(1, byteArray); + // 参数16表示16进制 + StringBuilder result = new StringBuilder(bigInt.toString(16)); + // 不足32位高位补零 + while(result.length() < 32) { + result.insert(0, "0"); + } + return result.toString(); + } + + public static String rePassword(String password, String salt) { + String f1 = String.format("%s_%s_z", salt, password); + return String.format("%s_%s_b", salt, md5(f1)); + } +} diff --git a/powerjob-server/powerjob-server-core/pom.xml b/powerjob-server/powerjob-server-core/pom.xml index 426bd20b..d8fbfc46 100644 --- a/powerjob-server/powerjob-server-core/pom.xml +++ b/powerjob-server/powerjob-server-core/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java index 6acea5bd..4087438c 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/service/UserService.java @@ -1,14 +1,13 @@ package tech.powerjob.server.core.service; -import tech.powerjob.server.persistence.remote.model.UserInfoDO; -import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import com.google.common.base.Splitter; import com.google.common.collect.Lists; -import org.springframework.stereotype.Service; import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import javax.annotation.Resource; -import java.util.Date; import java.util.List; import java.util.Set; import java.util.stream.Collectors; @@ -25,16 +24,6 @@ public class UserService { @Resource private UserInfoRepository userInfoRepository; - /** - * 保存/修改 用户 - * @param userInfoDO user - */ - public void save(UserInfoDO userInfoDO) { - userInfoDO.setGmtCreate(new Date()); - userInfoDO.setGmtModified(userInfoDO.getGmtCreate()); - userInfoRepository.saveAndFlush(userInfoDO); - } - /** * 根据用户ID字符串获取用户信息详细列表 * @param userIds 逗号分割的用户ID信息 diff --git a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java index d583f69b..75568e76 100644 --- a/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java +++ b/powerjob-server/powerjob-server-core/src/main/java/tech/powerjob/server/core/uid/IdGenerateService.java @@ -1,7 +1,6 @@ package tech.powerjob.server.core.uid; import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import tech.powerjob.server.remote.server.self.ServerInfoService; @@ -22,7 +21,7 @@ public class IdGenerateService { private static final int DATA_CENTER_ID = 0; public IdGenerateService(ServerInfoService serverInfoService) { - long id = serverInfoService.fetchServiceInfo().getId(); + long id = serverInfoService.fetchCurrentServerInfo().getId(); snowFlakeIdGenerator = new SnowFlakeIdGenerator(DATA_CENTER_ID, id); log.info("[IdGenerateService] initialize IdGenerateService successfully, ID:{}", id); } diff --git a/powerjob-server/powerjob-server-extension/pom.xml b/powerjob-server/powerjob-server-extension/pom.xml index ad4d772f..01a8cd5c 100644 --- a/powerjob-server/powerjob-server-extension/pom.xml +++ b/powerjob-server/powerjob-server-extension/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-migrate/pom.xml b/powerjob-server/powerjob-server-migrate/pom.xml index a8bf4916..e1fbbbb7 100644 --- a/powerjob-server/powerjob-server-migrate/pom.xml +++ b/powerjob-server/powerjob-server-migrate/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-monitor/pom.xml b/powerjob-server/powerjob-server-monitor/pom.xml index 012e8121..e72663b5 100644 --- a/powerjob-server/powerjob-server-monitor/pom.xml +++ b/powerjob-server/powerjob-server-monitor/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-persistence/pom.xml b/powerjob-server/powerjob-server-persistence/pom.xml index 5956e0ee..ec5011ed 100644 --- a/powerjob-server/powerjob-server-persistence/pom.xml +++ b/powerjob-server/powerjob-server-persistence/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 @@ -41,8 +41,8 @@ aliyun-sdk-oss - com.amazonaws - aws-java-sdk-s3 + io.minio + minio diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java index 31d0bbbf..3f4fe41d 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/QueryConvertUtils.java @@ -86,7 +86,7 @@ public class QueryConvertUtils { }; } - private static String convertLikeParams(Object o) { + public static String convertLikeParams(Object o) { String s = (String) o; if (!s.startsWith("%")) { s = "%" + s; diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java index 7395af38..fe073946 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/AppInfoDO.java @@ -22,7 +22,14 @@ public class AppInfoDO { @GenericGenerator(name = "native", strategy = "native") private Long id; + private String appName; + + /** + * 描述 + */ + private String title; + /** * 应用分组密码 */ @@ -35,7 +42,24 @@ public class AppInfoDO { */ private String currentServer; + /** + * 命名空间ID,外键关联 + */ + private Long namespaceId; + /** + * 管理标签 + */ + private String tags; + /** + * 扩展字段 + */ + private String extra; + private Date gmtCreate; private Date gmtModified; + + private Long creator; + + private Long modifier; } diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/NamespaceDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/NamespaceDO.java new file mode 100644 index 00000000..35d01bf8 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/NamespaceDO.java @@ -0,0 +1,65 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * 命名空间,用于组织管理 App + * + * @author tjq + * @since 2023/9/3 + */ +@Data +@Entity +@Table(uniqueConstraints = {@UniqueConstraint(name = "uidx01_namespace", columnNames = {"code"})}) +public class NamespaceDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + /** + * 空间唯一标识 + */ + private String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + private String name; + + /** + * 鉴权 token,后续 OpenAPI 调用需要 + */ + private String token; + + private Integer status; + + /** + * 部门,组织架构相关属性。 + * 预留数据库字段方便基于组织架构二次开发 + */ + private String dept; + + /** + * 标签,扩展性之王,多值逗号分割 + */ + private String tags; + + /** + * 扩展字段 + */ + private String extra; + + private Date gmtCreate; + + private Date gmtModified; + + private Long creator; + + private Long modifier; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/PwjbUserInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/PwjbUserInfoDO.java new file mode 100644 index 00000000..81418271 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/PwjbUserInfoDO.java @@ -0,0 +1,36 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * PowerJob 自建登录体系的用户表,只存储使用 PowerJob 自带登录方式登录的用户信息 + * + * @author tjq + * @since 2024/2/13 + */ +@Data +@Entity +@Table(uniqueConstraints = { + @UniqueConstraint(name = "uidx01_username", columnNames = {"username"}) +}) +public class PwjbUserInfoDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + private String username; + + private String password; + + private String extra; + + private Date gmtCreate; + + private Date gmtModified; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java new file mode 100644 index 00000000..b72cd64f --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/SundryDO.java @@ -0,0 +1,49 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * 杂项 + * KKV 表存一些配置数据 + * + * @author tjq + * @since 2024/2/15 + */ +@Data +@Entity +@NoArgsConstructor +@Table(uniqueConstraints = {@UniqueConstraint(name = "uidx01_sundry", columnNames = {"pkey", "skey"})}) +public class SundryDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + /** + * PKEY + */ + private String pkey; + /** + * SKEY + */ + private String skey; + /** + * 内容 + */ + private String content; + + /** + * 其他参数 + */ + private String extra; + + private Date gmtCreate; + + private Date gmtModified; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java index e19bef8c..d0c5db1d 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserInfoDO.java @@ -8,16 +8,20 @@ import java.util.Date; /** * 用户信息表 + * PowerJob 自身维护的全部用户体系数据 + * 5.0.0 可能不兼容改动:为了支持第三方登录,需要通过 username 与第三方登录系统做匹配,该列需要声明为唯一索引,确保全局唯一 * * @author tjq * @since 2020/4/12 */ @Data @Entity -@Table(indexes = { - @Index(name = "uidx01_user_info", columnList = "username"), - @Index(name = "uidx02_user_info", columnList = "email") -}) +@Table(uniqueConstraints = { + @UniqueConstraint(name = "uidx01_user_name", columnNames = {"username"}) +}, + indexes = { + @Index(name = "uidx02_user_info", columnList = "email") + }) public class UserInfoDO { @Id @@ -25,7 +29,17 @@ public class UserInfoDO { @GenericGenerator(name = "native", strategy = "native") private Long id; + /** + * 账号类型 + */ + private String accountType; + private String username; + /** + * since 5.0.0 + * 昵称(第三方登陆的 username 很难识别,方便后续展示引入 nick) + */ + private String nick; private String password; /** @@ -40,11 +54,23 @@ public class UserInfoDO { * webHook */ private String webHook; + /** - * 扩展字段 + * JWT 登录的二次校验信息 + */ + private String tokenLoginVerifyInfo; + + /** + * 扩展字段 for 第三方 + * PowerJob 内部不允许使用该字段 */ private String extra; + /** + * 原始账号 username + */ + private String originUsername; + private Date gmtCreate; private Date gmtModified; diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java new file mode 100644 index 00000000..22d7b629 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/model/UserRoleDO.java @@ -0,0 +1,53 @@ +package tech.powerjob.server.persistence.remote.model; + +import lombok.Data; +import org.hibernate.annotations.GenericGenerator; + +import javax.persistence.*; +import java.util.Date; + +/** + * 用户角色表 + * + * @author tjq + * @since 2023/3/20 + */ +@Data +@Entity +@Table(indexes = { + @Index(name = "uidx01_user_id", columnList = "userId") +}) +public class UserRoleDO { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO, generator = "native") + @GenericGenerator(name = "native", strategy = "native") + private Long id; + + /** + * 授予角色的用户ID + */ + private Long userId; + + /** + * 权限范围,namespace 还是 app + */ + private Integer scope; + /** + * 和 scope 一起组成授权目标,比如某个 app 或 某个 namespace + */ + private Long target; + + /** + * 角色,比如 Observer + */ + private Integer role; + /** + * 扩展字段 + */ + private String extra; + + private Date gmtCreate; + + private Date gmtModified; +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java index a7e6d09d..ec98e8e9 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/AppInfoRepository.java @@ -1,12 +1,14 @@ package tech.powerjob.server.persistence.remote.repository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import tech.powerjob.server.persistence.remote.model.AppInfoDO; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import java.util.Collection; import java.util.List; import java.util.Optional; @@ -16,7 +18,7 @@ import java.util.Optional; * @author tjq * @since 2020/4/1 */ -public interface AppInfoRepository extends JpaRepository { +public interface AppInfoRepository extends JpaRepository, JpaSpecificationExecutor { Optional findByAppName(String appName); @@ -31,4 +33,8 @@ public interface AppInfoRepository extends JpaRepository { @Query(value = "select id from AppInfoDO where currentServer = :currentServer") List listAppIdByCurrentServer(@Param("currentServer")String currentServer); + List findAllByNamespaceId(Long namespaceId); + + + List findAllByIdIn(Collection ids); } diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/NamespaceRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/NamespaceRepository.java new file mode 100644 index 00000000..ff776903 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/NamespaceRepository.java @@ -0,0 +1,22 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; + +import java.util.Collection; +import java.util.List; +import java.util.Optional; + +/** + * 命名空间 + * + * @author tjq + * @since 2023/9/3 + */ +public interface NamespaceRepository extends JpaRepository, JpaSpecificationExecutor { + + Optional findByCode(String code); + + List findAllByIdIn(Collection ids); +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/PwjbUserInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/PwjbUserInfoRepository.java new file mode 100644 index 00000000..68c02bb0 --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/PwjbUserInfoRepository.java @@ -0,0 +1,17 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; + +import java.util.Optional; + +/** + * PwjbUserInfoRepository + * + * @author tjq + * @since 2024/2/13 + */ +public interface PwjbUserInfoRepository extends JpaRepository { + + Optional findByUsername(String username); +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java new file mode 100644 index 00000000..62e47a5a --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/SundryRepository.java @@ -0,0 +1,20 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import tech.powerjob.server.persistence.remote.model.SundryDO; + +import java.util.List; +import java.util.Optional; + +/** + * SundryRepository + * + * @author tjq + * @since 2024/2/15 + */ +public interface SundryRepository extends JpaRepository { + + List findAllByPkey(String pkey); + + Optional findByPkeyAndSkey(String pkey, String skey); +} diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java index 2a11d207..3e70cef4 100644 --- a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserInfoRepository.java @@ -4,6 +4,7 @@ import tech.powerjob.server.persistence.remote.model.UserInfoDO; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; +import java.util.Optional; /** * 用户信息表数据库访问层 @@ -13,6 +14,8 @@ import java.util.List; */ public interface UserInfoRepository extends JpaRepository { + Optional findByUsername(String username); + List findByUsernameLike(String username); List findByIdIn(List userIds); diff --git a/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java new file mode 100644 index 00000000..fb4c69cf --- /dev/null +++ b/powerjob-server/powerjob-server-persistence/src/main/java/tech/powerjob/server/persistence/remote/repository/UserRoleRepository.java @@ -0,0 +1,23 @@ +package tech.powerjob.server.persistence.remote.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import tech.powerjob.server.persistence.remote.model.UserRoleDO; + +import java.util.List; + +/** + * UserRoleRepository + * + * @author tjq + * @since 2023/3/20 + */ +public interface UserRoleRepository extends JpaRepository { + + List findAllByUserId(Long userId); + + List findAllByScopeAndTarget(Integer scope, Long target); + + List findAllByScopeAndTargetAndRoleAndUserId(Integer scope, Long target, Integer role, Long userId); + + List findAllByUserIdAndScope(Long userId, Integer scope); +} diff --git a/powerjob-server/powerjob-server-remote/pom.xml b/powerjob-server/powerjob-server-remote/pom.xml index 5254a96e..852d6887 100644 --- a/powerjob-server/powerjob-server-remote/pom.xml +++ b/powerjob-server/powerjob-server-remote/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 diff --git a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java index 5c799a7a..1e119ec6 100644 --- a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java +++ b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoService.java @@ -14,6 +14,13 @@ public interface ServerInfoService { * fetch current server info * @return ServerInfo */ - ServerInfo fetchServiceInfo(); + ServerInfo fetchCurrentServerInfo(); + + /** + * fetch schedule server info + * @param appId appId + * @return ServerInfo + */ + ServerInfo fetchAppServerInfo(Long appId); } diff --git a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java index 8a205e49..21af89c8 100644 --- a/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java +++ b/powerjob-server/powerjob-server-remote/src/main/java/tech/powerjob/server/remote/server/self/ServerInfoServiceImpl.java @@ -15,6 +15,7 @@ import tech.powerjob.server.common.module.ServerInfo; import tech.powerjob.server.extension.LockService; import tech.powerjob.server.persistence.remote.model.ServerInfoDO; import tech.powerjob.server.persistence.remote.repository.ServerInfoRepository; +import tech.powerjob.server.remote.server.redirector.DesignateServer; import java.util.Date; import java.util.List; @@ -138,7 +139,13 @@ public class ServerInfoServiceImpl implements ServerInfoService { } @Override - public ServerInfo fetchServiceInfo() { + public ServerInfo fetchCurrentServerInfo() { + return serverInfo; + } + + @Override + @DesignateServer + public ServerInfo fetchAppServerInfo(Long appId) { return serverInfo; } } diff --git a/powerjob-server/powerjob-server-starter/pom.xml b/powerjob-server/powerjob-server-starter/pom.xml index b2d32bd1..128ffd38 100644 --- a/powerjob-server/powerjob-server-starter/pom.xml +++ b/powerjob-server/powerjob-server-starter/pom.xml @@ -5,7 +5,7 @@ powerjob-server tech.powerjob - 4.3.9 + 5.0.0-beta2 ../pom.xml 4.0.0 @@ -43,6 +43,10 @@ tech.powerjob powerjob-server-core + + tech.powerjob + powerjob-server-auth + tech.powerjob powerjob-server-migrate diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/ModifyOrCreateDynamicPermission.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/ModifyOrCreateDynamicPermission.java new file mode 100644 index 00000000..44962eaa --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/ModifyOrCreateDynamicPermission.java @@ -0,0 +1,46 @@ +package tech.powerjob.server.auth.plugin; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StreamUtils; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.interceptor.DynamicPermissionPlugin; + +import javax.servlet.http.HttpServletRequest; +import java.util.Map; + +/** + * 针对 namespace 和 app 两大鉴权纬度,创建不需要任何权限,但任何修改操作都需要 WRITE 权限 + * 创建不需要权限,修改需要校验权限 + * + * @author tjq + * @since 2023/9/3 + */ +@Slf4j +public class ModifyOrCreateDynamicPermission implements DynamicPermissionPlugin { + @Override + public Permission calculate(HttpServletRequest request, Object handler) { + + try { + //获取请求body + byte[] bodyBytes = StreamUtils.copyToByteArray(request.getInputStream()); + String body = new String(bodyBytes, request.getCharacterEncoding()); + + Map inputParams = JsonUtils.parseMap(body); + + Object id = inputParams.get("id"); + + // 创建,不需要权限 + if (id == null) { + return Permission.NONE; + } + + return Permission.WRITE; + } catch (Exception e) { + log.error("[ModifyOrCreateDynamicPermission] check permission failed, please fix the bug!!!", e); + } + + // 异常情况先放行,不影响功能使用,后续修复 BUG + return Permission.NONE; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveAppGrantPermissionPlugin.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveAppGrantPermissionPlugin.java new file mode 100644 index 00000000..4bffe6fd --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveAppGrantPermissionPlugin.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.plugin; + +import tech.powerjob.server.auth.RoleScope; + +/** + * desc + * + * @author tjq + * @since 2024/2/11 + */ +public class SaveAppGrantPermissionPlugin extends SaveGrantPermissionPlugin { + @Override + protected RoleScope fetchRuleScope() { + return RoleScope.APP; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java new file mode 100644 index 00000000..766d6854 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveGrantPermissionPlugin.java @@ -0,0 +1,76 @@ +package tech.powerjob.server.auth.plugin; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.collections4.MapUtils; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.GrantPermissionPlugin; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.common.utils.SpringUtils; + +import java.lang.reflect.Method; +import java.util.Map; + +/** + * WEB 类保存&修改一体型请求-授权插件 + * + * @author tjq + * @since 2024/2/11 + */ +@Slf4j +public abstract class SaveGrantPermissionPlugin implements GrantPermissionPlugin { + + private static final String KEY_ID = "id"; + + @Override + public void grant(Object[] args, Object result, Method method, Object originBean) { + + if (args == null || args.length != 1) { + throw new IllegalArgumentException("[GrantPermission] args not match, maybe there has some bug"); + } + + // 理论上不可能,前置已完成判断 + PowerJobUser powerJobUser = LoginUserHolder.get(); + if (powerJobUser == null) { + throw new IllegalArgumentException("[GrantPermission] user not login, can't grant permission"); + } + + // 解析ID,非空代表更新,不授权 + Map saveRequest = JsonUtils.parseMap(JsonUtils.toJSONString(args[0])); + Long id = MapUtils.getLong(saveRequest, KEY_ID); + if (id != null) { + return; + } + + if (!(result instanceof ResultDTO)) { + throw new IllegalArgumentException("[GrantPermission] result not instanceof ResultDTO, maybe there has some bug"); + } + + ResultDTO resultDTO = (ResultDTO) result; + + if (!resultDTO.isSuccess()) { + log.warn("[GrantPermission] result not success, skip grant permission!"); + return; + } + + Map saveResult = JsonUtils.parseMap(JsonUtils.toJSONString(resultDTO.getData())); + Long savedId = MapUtils.getLong(saveResult, KEY_ID); + if (savedId == null) { + throw new IllegalArgumentException("[GrantPermission] result success but id not exits, maybe there has some bug, please fix it!!!"); + } + + PowerJobPermissionService powerJobPermissionService = SpringUtils.getBean(PowerJobPermissionService.class); + + Map extra = Maps.newHashMap(); + extra.put("source", "SaveGrantPermissionPlugin"); + + powerJobPermissionService.grantRole(fetchRuleScope(), savedId, powerJobUser.getId(), Role.ADMIN, JsonUtils.toJSONString(extra)); + } + + protected abstract RoleScope fetchRuleScope(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveNamespaceGrantPermissionPlugin.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveNamespaceGrantPermissionPlugin.java new file mode 100644 index 00000000..955ded5a --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/plugin/SaveNamespaceGrantPermissionPlugin.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.auth.plugin; + +import tech.powerjob.server.auth.RoleScope; + +/** + * namespace 授权插件 + * + * @author tjq + * @since 2024/2/11 + */ +public class SaveNamespaceGrantPermissionPlugin extends SaveGrantPermissionPlugin { + @Override + protected RoleScope fetchRuleScope() { + return RoleScope.NAMESPACE; + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/WebAuthService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/WebAuthService.java new file mode 100644 index 00000000..f579ea19 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/WebAuthService.java @@ -0,0 +1,56 @@ +package tech.powerjob.server.auth.service; + +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import java.util.List; +import java.util.Map; + +/** + * Web Auth 服务 + * 写在 starter 包下,抽取 controller 的公共逻辑 + * (powerjob 的 service/core 包核心处理调度核心逻辑,admin 部分代码收口在 stater 包) + * + * @author tjq + * @since 2024/2/12 + */ +public interface WebAuthService { + + /** + * 对当前登录用户授予角色 + * @param roleScope 角色范围 + * @param target 目标 + * @param role 角色 + * @param extra 其他信息 + */ + void grantRole2LoginUser(RoleScope roleScope, Long target, Role role, String extra); + + /** + * 处理授权 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param componentUserRoleInfo 人员角色信息 + */ + void processPermissionOnSave(RoleScope roleScope, Long target, ComponentUserRoleInfo componentUserRoleInfo); + + /** + * 获取目标相关权限人员列表 + * @param roleScope 权限范围 + * @param target 权限目标 + * @return ComponentUserRoleInfo + */ + ComponentUserRoleInfo fetchComponentUserRoleInfo(RoleScope roleScope, Long target); + + /** + * 判断当前用户是否有权限 + * @param roleScope 权限范围 + * @param target 权限目标 + * @param permission 要求的权限 + * @return 是否有权限 + */ + boolean hasPermission(RoleScope roleScope, Long target, Permission permission); + + Map> fetchMyPermissionTargets(RoleScope roleScope); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java new file mode 100644 index 00000000..9804f4f3 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/auth/service/impl/WebAuthServiceImpl.java @@ -0,0 +1,114 @@ +package tech.powerjob.server.auth.service.impl; + +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.*; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import javax.annotation.Resource; +import java.util.*; + +/** + * WebAuthService + * + * @author tjq + * @since 2024/2/12 + */ +@Slf4j +@Service +public class WebAuthServiceImpl implements WebAuthService { + + @Resource + private PowerJobPermissionService powerJobPermissionService; + + + @Override + public void grantRole2LoginUser(RoleScope roleScope, Long target, Role role, String extra) { + Long userId = LoginUserHolder.getUserId(); + if (userId == null) { + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN); + } + powerJobPermissionService.grantRole(roleScope, target, userId, role, extra); + } + + @Override + public void processPermissionOnSave(RoleScope roleScope, Long target, ComponentUserRoleInfo o) { + ComponentUserRoleInfo componentUserRoleInfo = Optional.ofNullable(o).orElse(new ComponentUserRoleInfo()); + + Map> role2Uids = powerJobPermissionService.fetchUserWithPermissions(roleScope, target); + diffGrant(roleScope, target, Role.OBSERVER, componentUserRoleInfo.getObserver(), role2Uids); + diffGrant(roleScope, target, Role.QA, componentUserRoleInfo.getQa(), role2Uids); + diffGrant(roleScope, target, Role.DEVELOPER, componentUserRoleInfo.getDeveloper(), role2Uids); + diffGrant(roleScope, target, Role.ADMIN, componentUserRoleInfo.getAdmin(), role2Uids); + } + + @Override + public ComponentUserRoleInfo fetchComponentUserRoleInfo(RoleScope roleScope, Long target) { + Map> role2Uids = powerJobPermissionService.fetchUserWithPermissions(roleScope, target); + return new ComponentUserRoleInfo() + .setObserver(role2Uids.getOrDefault(Role.OBSERVER, Collections.emptyList())) + .setQa(role2Uids.getOrDefault(Role.QA, Collections.emptyList())) + .setDeveloper(role2Uids.getOrDefault(Role.DEVELOPER, Collections.emptyList())) + .setAdmin(role2Uids.getOrDefault(Role.ADMIN, Collections.emptyList())); + } + + @Override + public boolean hasPermission(RoleScope roleScope, Long target, Permission permission) { + + PowerJobUser powerJobUser = LoginUserHolder.get(); + if (powerJobUser == null) { + return false; + } + + return powerJobPermissionService.hasPermission(powerJobUser.getId(), roleScope, target, permission); + } + + @Override + public Map> fetchMyPermissionTargets(RoleScope roleScope) { + + PowerJobUser powerJobUser = LoginUserHolder.get(); + if (powerJobUser == null) { + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN); + } + + // 展示不考虑穿透权限的问题(即拥有 namespace 权限也可以看到全部的 apps) + return powerJobPermissionService.fetchUserHadPermissionTargets(roleScope, powerJobUser.getId()); + } + + private void diffGrant(RoleScope roleScope, Long target, Role role, List uids, Map> originRole2Uids) { + + Set originUids = Sets.newHashSet(Optional.ofNullable(originRole2Uids.get(role)).orElse(Collections.emptyList())); + Set currentUids = Sets.newHashSet(Optional.ofNullable(uids).orElse(Collections.emptyList())); + + Map extraInfo = Maps.newHashMap(); + extraInfo.put("grantor", LoginUserHolder.getUserName()); + extraInfo.put("source", "diffGrant"); + String extra = JsonUtils.toJSONString(extraInfo); + + Set allIds = Sets.newHashSet(originUids); + allIds.addAll(currentUids); + + Set allIds2 = Sets.newHashSet(allIds); + + // 在 originUids 不在 currentUids,需要取消授权 + allIds.removeAll(currentUids); + allIds.forEach(cancelPermissionUid -> { + powerJobPermissionService.retrieveRole(roleScope, target, cancelPermissionUid, role); + log.info("[WebAuthService] [diffGrant] cancelPermission: roleScope={},target={},uid={},role={}", roleScope, target, cancelPermissionUid, role); + }); + + // 在 currentUids 当不在 orignUids,需要增加授权 + allIds2.removeAll(originUids); + allIds2.forEach(addPermissionUid -> { + powerJobPermissionService.grantRole(roleScope, target, addPermissionUid, role, extra); + log.info("[WebAuthService] [diffGrant] grantPermission: roleScope={},target={},uid={},role={},extra={}", roleScope, target, addPermissionUid, role, extra); + }); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java new file mode 100644 index 00000000..e41136fb --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/CachingRequestBodyFilter.java @@ -0,0 +1,96 @@ +package tech.powerjob.server.config; + +import org.springframework.stereotype.Component; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import java.io.*; + +/** + * 解决 HttpServletRequest 只能被读取一次的问题,方便全局日志 & 鉴权,切面提前读取数据 + * 在请求进入Servlet容器之前,先经过Filter的过滤器链。在请求进入Controller之前,先经过 HandlerInterceptor 的拦截器链。Filter 一定先于 HandlerInterceptor 执行 + * + * @author tjq + * @since 2024/2/11 + */ +@Component +public class CachingRequestBodyFilter implements Filter { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + if (request instanceof HttpServletRequest) { + CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper((HttpServletRequest) request); + chain.doFilter(wrappedRequest, response); + } else { + chain.doFilter(request, response); + } + } + + // Implement other required methods like init() and destroy() if necessary + + + public static class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final String body; + + public CustomHttpServletRequestWrapper(HttpServletRequest request) throws IOException { + super(request); + StringBuilder stringBuilder = new StringBuilder(); + BufferedReader bufferedReader = null; + try { + InputStream inputStream = request.getInputStream(); + if (inputStream != null) { + bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); + char[] charBuffer = new char[128]; + int bytesRead = -1; + while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { + stringBuilder.append(charBuffer, 0, bytesRead); + } + } + } finally { + if (bufferedReader != null) { + bufferedReader.close(); + } + } + body = stringBuilder.toString(); + } + + @Override + public ServletInputStream getInputStream() throws IOException { + final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); + + return new ServletInputStream() { + public int read() throws IOException { + return byteArrayInputStream.read(); + } + + @Override + public boolean isFinished() { + return byteArrayInputStream.available() == 0; + } + + @Override + public boolean isReady() { + return true; + } + + @Override + public void setReadListener(ReadListener readListener) { + throw new UnsupportedOperationException("Not implemented"); + } + }; + } + + @Override + public BufferedReader getReader() throws IOException { + return new BufferedReader(new InputStreamReader(this.getInputStream())); + } + + public String getBody() { + return this.body; + } + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java index 5f4467e1..44c11726 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/SwaggerConfig.java @@ -41,7 +41,7 @@ public class SwaggerConfig { return new OpenAPI() .info(new Info().title("PowerJob") .description("Distributed scheduling and computing framework.") - .version(serverInfoService.fetchServiceInfo().getVersion()) + .version(serverInfoService.fetchCurrentServerInfo().getVersion()) .contact(contact) .license(new License().name("Apache License 2.0").url("https://github.com/PowerJob/PowerJob/blob/master/LICENSE"))); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java index c1e819ad..28427a0e 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/config/WebConfig.java @@ -3,9 +3,13 @@ package tech.powerjob.server.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.socket.config.annotation.EnableWebSocket; import org.springframework.web.socket.server.standard.ServerEndpointExporter; +import tech.powerjob.server.auth.interceptor.PowerJobAuthInterceptor; + +import javax.annotation.Resource; /** * CORS @@ -16,6 +20,10 @@ import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration @EnableWebSocket public class WebConfig implements WebMvcConfigurer { + + @Resource + private PowerJobAuthInterceptor powerJobAuthInterceptor; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") @@ -26,4 +34,19 @@ public class WebConfig implements WebMvcConfigurer { public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } + + @Override + public void addInterceptors(InterceptorRegistry registry) { + /* + 可以添加多个拦截器 + addPathPatterns("/**") 表示对所有请求都拦截 + .excludePathPatterns("/base/index") 表示排除对/base/index请求的拦截 + 多个拦截器可以设置order顺序,值越小,preHandle越先执行,postHandle和afterCompletion越后执行 + order默认的值是0,如果只添加一个拦截器,可以不显示设置order的值 + */ + registry.addInterceptor(powerJobAuthInterceptor) + .addPathPatterns("/**") + .excludePathPatterns("/css/**", "/js/**", "/images/**", "/img/**", "/fonts/**", "/favicon.ico") + .order(0); + } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java new file mode 100644 index 00000000..130b18fd --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/NewSystemInitializer.java @@ -0,0 +1,102 @@ +package tech.powerjob.server.initializer; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.CommandLineRunner; +import org.springframework.stereotype.Component; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.extension.LockService; +import tech.powerjob.server.persistence.remote.model.SundryDO; +import tech.powerjob.server.persistence.remote.repository.SundryRepository; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.Optional; +import java.util.function.Consumer; + +/** + * 新系统初始化器 + * + * @author tjq + * @since 2023/9/5 + */ +@Slf4j +@Component +public class NewSystemInitializer implements CommandLineRunner { + + + private static final String LOCK_PREFIX = "sys_init_lock_"; + + private static final int MAX_LOCK_TIME = 5000; + + + @Resource + private LockService lockService; + @Resource + private SundryRepository sundryRepository; + @Resource + private SystemInitializeService systemInitializeService; + + private static final String SUNDRY_PKEY = "sys_initialize"; + + @Override + public void run(String... args) throws Exception { + initSystemAdmin(); + initDefaultNamespace(); + } + + private void initSystemAdmin() { + clusterInit(SystemInitializeService.GOAL_INIT_ADMIN, Void -> systemInitializeService.initAdmin()); + } + + private void initDefaultNamespace() { + clusterInit(SystemInitializeService.GOAL_INIT_NAMESPACE, Void -> systemInitializeService.initNamespace()); + } + + private void clusterInit(String name, Consumer initFunc) { + + Optional sundryOpt = sundryRepository.findByPkeyAndSkey(SUNDRY_PKEY, name); + if (sundryOpt.isPresent()) { + log.info("[NewSystemInitializer] already initialized, skip: {}", name); + return; + } + + String lockName = LOCK_PREFIX.concat(name); + + while (true) { + try { + + boolean lockStatus = lockService.tryLock(lockName, MAX_LOCK_TIME); + + // 无论是否拿到锁,都重现检测一次,如果已完成初始化,则直接 return + Optional sundryOpt2 = sundryRepository.findByPkeyAndSkey(SUNDRY_PKEY, name); + if (sundryOpt2.isPresent()) { + log.info("[NewSystemInitializer] other server finished initialize, skip process: {}", name); + break; + } + + if (!lockStatus) { + CommonUtils.easySleep(277); + continue; + } + + log.info("[NewSystemInitializer] try to initialize: {}", name); + initFunc.accept(null); + log.info("[NewSystemInitializer] initialize [{}] successfully!", name); + + // 写入初始化成功标记 + SundryDO sundryDO = new SundryDO(); + sundryDO.setPkey(SUNDRY_PKEY); + sundryDO.setSkey(name); + sundryDO.setContent("A"); + sundryDO.setGmtCreate(new Date()); + sundryRepository.saveAndFlush(sundryDO); + log.info("[NewSystemInitializer] write initialized tag successfully: {}", sundryDO); + + break; + } finally { + lockService.unlock(lockName); + } + } + + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java new file mode 100644 index 00000000..e5ec2ceb --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeService.java @@ -0,0 +1,24 @@ +package tech.powerjob.server.initializer; + +/** + * 系统初始化服务 + * + * @author tjq + * @since 2024/2/15 + */ +public interface SystemInitializeService { + + String GOAL_INIT_ADMIN = "goal_init_admin"; + String GOAL_INIT_NAMESPACE = "goal_init_namespace"; + + + /** + * 初始化超级管理员 + */ + void initAdmin(); + + /** + * 初始化 namespace + */ + void initNamespace(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java new file mode 100644 index 00000000..00a717c7 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/initializer/SystemInitializeServiceImpl.java @@ -0,0 +1,105 @@ +package tech.powerjob.server.initializer; + +import com.google.common.collect.Maps; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.service.login.LoginRequest; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.auth.service.permission.PowerJobPermissionService; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.NamespaceWebService; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; +import javax.transaction.Transactional; +import java.util.Map; + +/** + * 初始化 PowerJob 首次部署相关的内容 + * 为了可维护性足够高,统一使用 WEB 请求进行初始化,不直接操作底层,防止后续内部逻辑变更后出现问题 + * + * @author tjq + * @since 2024/2/15 + */ +@Slf4j +@Service +public class SystemInitializeServiceImpl implements SystemInitializeService { + + @Value("${oms.auth.initiliaze.admin.password:#{null}}") + private String defaultAdminPassword; + @Resource + private PwjbUserWebService pwjbUserWebService; + @Resource + private NamespaceWebService namespaceWebService; + @Resource + private PowerJobLoginService powerJobLoginService; + @Resource + private PowerJobPermissionService powerJobPermissionService; + + private static final String SYSTEM_ADMIN_NAME = "ADMIN"; + + + private static final String SYSTEM_DEFAULT_NAMESPACE = "default_namespace"; + + @Override + @Transactional(rollbackOn = Exception.class) + public void initAdmin() { + + String username = SYSTEM_ADMIN_NAME; + String password = StringUtils.isEmpty(defaultAdminPassword) ? RandomStringUtils.randomAlphabetic(8) : defaultAdminPassword; + + // STEP1: 创建 PWJB 用户 + ModifyUserInfoRequest createUser = new ModifyUserInfoRequest(); + createUser.setUsername(username); + createUser.setNick(username); + createUser.setPassword(password); + + log.info("[SystemInitializeService] [S1] create default PWJB user by request: {}", createUser); + PwjbUserInfoDO savedPwjbUser = pwjbUserWebService.save(createUser); + log.info("[SystemInitializeService] [S1] create default PWJB user successfully: {}", savedPwjbUser); + + Map params = Maps.newHashMap(); + params.put(AuthConstants.PARAM_KEY_USERNAME, username); + params.put(AuthConstants.PARAM_KEY_PASSWORD, password); + + // STEP2: 创建 USER 对象 + LoginRequest loginRequest = new LoginRequest() + .setLoginType(AuthConstants.ACCOUNT_TYPE_POWER_JOB) + .setOriginParams(JsonUtils.toJSONString(params)); + log.info("[SystemInitializeService] [S2] createPowerJobUser user by request: {}", loginRequest); + PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginRequest); + log.info("[SystemInitializeService] [S2] createPowerJobUser successfully: {}", powerJobUser); + + // STEP3: 授予全局管理员权限 + powerJobPermissionService.grantRole(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, powerJobUser.getId(), Role.ADMIN, null); + log.info("[SystemInitializeService] [S3] GRANT ADMIN successfully!"); + + // 循环10遍,强提醒用户,第一次使用必须更改 admin 密码 + for (int i = 0; i < 10; i++) { + log.warn("[SystemInitializeService] The system has automatically created a super administrator account[username={},password={}], please log in and change the password immediately!", username, password); + } + } + + @Override + @Transactional(rollbackOn = Exception.class) + public void initNamespace() { + ModifyNamespaceRequest saveNamespaceReq = new ModifyNamespaceRequest(); + saveNamespaceReq.setName(SYSTEM_DEFAULT_NAMESPACE); + saveNamespaceReq.setCode(SYSTEM_DEFAULT_NAMESPACE); + + log.info("[SystemInitializeService] create default namespace by request: {}", saveNamespaceReq); + NamespaceDO savedNamespaceDO = namespaceWebService.save(saveNamespaceReq); + log.info("[SystemInitializeService] create default namespace successfully: {}", savedNamespaceDO); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java index 6a4aa54a..cd335752 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/support/ServerInfoAwareProcessor.java @@ -19,7 +19,7 @@ import java.util.List; public class ServerInfoAwareProcessor { public ServerInfoAwareProcessor(ServerInfoService serverInfoService, List awareList) { - final ServerInfo serverInfo = serverInfoService.fetchServiceInfo(); + final ServerInfo serverInfo = serverInfoService.fetchCurrentServerInfo(); log.info("[ServerInfoAwareProcessor] current server info: {}", serverInfo); awareList.forEach(aware -> { aware.setServerInfo(serverInfo); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java index 0730312f..4903e3cd 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/ControllerExceptionHandler.java @@ -1,7 +1,5 @@ package tech.powerjob.server.web; -import tech.powerjob.common.exception.PowerJobException; -import tech.powerjob.common.response.ResultDTO; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.exception.ExceptionUtils; import org.springframework.http.converter.HttpMessageNotReadableException; @@ -10,6 +8,9 @@ import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.web.response.WebResultDTO; /** * 统一处理 web 层异常信息 @@ -23,11 +24,16 @@ public class ControllerExceptionHandler { @ResponseBody @ExceptionHandler(Exception.class) - public ResultDTO exceptionHandler(Exception e) { + public WebResultDTO exceptionHandler(Exception e) { + + WebResultDTO ret = new WebResultDTO<>(ResultDTO.failed(ExceptionUtils.getMessage(e))); // 不是所有异常都需要打印完整堆栈,后续可以定义内部的Exception,便于判断 - if (e instanceof IllegalArgumentException || e instanceof PowerJobException) { - log.warn("[ControllerException] http request failed, message is {}.", e.getMessage()); + if (e instanceof PowerJobException) { + ret.setCode(((PowerJobException) e).getCode()); + log.warn("[ControllerException] PowerJobException, message is {}.", e.getMessage()); + } else if (e instanceof IllegalArgumentException) { + log.warn("[ControllerException] http request failed due to IllegalArgument, message is {}.", e.getMessage()); } else if (e instanceof HttpMessageNotReadableException || e instanceof MethodArgumentTypeMismatchException) { log.warn("[ControllerException] invalid http request params, exception is {}.", e.getMessage()); } else if (e instanceof HttpRequestMethodNotSupportedException) { @@ -35,6 +41,7 @@ public class ControllerExceptionHandler { } else { log.error("[ControllerException] http request failed.", e); } - return ResultDTO.failed(ExceptionUtils.getMessage(e)); + + return ret; } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java index f50e9a90..7d2c8ecc 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AppInfoController.java @@ -1,26 +1,50 @@ package tech.powerjob.server.web.controller; -import lombok.RequiredArgsConstructor; -import tech.powerjob.common.exception.PowerJobException; -import tech.powerjob.common.response.ResultDTO; -import tech.powerjob.server.persistence.remote.model.AppInfoDO; -import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; -import tech.powerjob.server.core.service.AppInfoService; -import tech.powerjob.server.web.request.AppAssertRequest; -import tech.powerjob.server.web.request.ModifyAppInfoRequest; import com.google.common.collect.Lists; -import lombok.Data; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.BeanUtils; +import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.plugin.ModifyOrCreateDynamicPermission; +import tech.powerjob.server.auth.plugin.SaveAppGrantPermissionPlugin; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.persistence.PageResult; +import tech.powerjob.server.persistence.QueryConvertUtils; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.web.converter.NamespaceConverter; +import tech.powerjob.server.web.request.AppAssertRequest; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; +import tech.powerjob.server.web.request.ModifyAppInfoRequest; +import tech.powerjob.server.web.request.QueryAppInfoRequest; +import tech.powerjob.server.web.response.AppInfoVO; +import tech.powerjob.server.web.response.NamespaceBaseVO; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.service.NamespaceWebService; +import tech.powerjob.server.web.service.UserWebService; -import javax.annotation.Resource; -import java.util.Date; -import java.util.List; -import java.util.Objects; +import javax.persistence.criteria.Predicate; +import java.util.*; import java.util.stream.Collectors; /** @@ -35,14 +59,17 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class AppInfoController { - private final AppInfoService appInfoService; + private final WebAuthService webAuthService; + + private final UserWebService userWebService; private final AppInfoRepository appInfoRepository; - private static final int MAX_APP_NUM = 200; + private final NamespaceWebService namespaceWebService; @PostMapping("/save") - public ResultDTO saveAppInfo(@RequestBody ModifyAppInfoRequest req) { + @ApiPermission(name = "App-Save", roleScope = RoleScope.APP, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveAppGrantPermissionPlugin.class) + public ResultDTO saveAppInfo(@RequestBody ModifyAppInfoRequest req) { req.valid(); AppInfoDO appInfoDO; @@ -51,59 +78,154 @@ public class AppInfoController { if (id == null) { appInfoDO = new AppInfoDO(); appInfoDO.setGmtCreate(new Date()); - }else { + appInfoDO.setCreator(LoginUserHolder.getUserId()); + } else { appInfoDO = appInfoRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("can't find appInfo by id:" + id)); - // 对比密码 - if (!Objects.equals(req.getOldPassword(), appInfoDO.getPassword())) { - throw new PowerJobException("The password is incorrect."); + // 不允许修改 appName + if (!appInfoDO.getAppName().equalsIgnoreCase(req.getAppName())) { + throw new IllegalArgumentException("NOT_ALLOW_CHANGE_THE_APP_NAME"); } } - BeanUtils.copyProperties(req, appInfoDO); + + appInfoDO.setAppName(req.getAppName()); + appInfoDO.setTitle(req.getTitle()); + appInfoDO.setPassword(req.getPassword()); + appInfoDO.setNamespaceId(req.getNamespaceId()); + appInfoDO.setTags(req.getTags()); + appInfoDO.setExtra(req.getExtra()); + appInfoDO.setGmtModified(new Date()); + appInfoDO.setModifier(LoginUserHolder.getUserId()); - appInfoRepository.saveAndFlush(appInfoDO); - return ResultDTO.success(null); - } + AppInfoDO savedAppInfo = appInfoRepository.saveAndFlush(appInfoDO); - @PostMapping("/assert") - public ResultDTO assertApp(@RequestBody AppAssertRequest request) { - return ResultDTO.success(appInfoService.assertApp(request.getAppName(), request.getPassword())); + // 重现授权 + webAuthService.processPermissionOnSave(RoleScope.APP, savedAppInfo.getId(), req.getComponentUserRoleInfo()); + + return ResultDTO.success(convert(Lists.newArrayList(savedAppInfo), false).get(0)); } @GetMapping("/delete") + @ApiPermission(name = "App-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.SU) public ResultDTO deleteAppInfo(Long appId) { appInfoRepository.deleteById(appId); return ResultDTO.success(null); } - @GetMapping("/list") - public ResultDTO> listAppInfo(@RequestParam(required = false) String condition) { - List result; - Pageable limit = PageRequest.of(0, MAX_APP_NUM); - if (StringUtils.isEmpty(condition)) { - result = appInfoRepository.findAll(limit).getContent(); - }else { - result = appInfoRepository.findByAppNameLike("%" + condition + "%", limit).getContent(); + @PostMapping("/list") + @ApiPermission(name = "App-List", roleScope = RoleScope.APP, requiredPermission = Permission.NONE) + public ResultDTO> listAppInfoByQuery(@RequestBody QueryAppInfoRequest queryAppInfoRequest) { + + Pageable pageable = PageRequest.of(queryAppInfoRequest.getIndex(), queryAppInfoRequest.getPageSize()); + + // 相关权限(先查处关联 ids) + Set queryAppIds; + Boolean showMyRelated = queryAppInfoRequest.getShowMyRelated(); + if (BooleanUtils.isTrue(showMyRelated)) { + Set targetIds = Sets.newHashSet(); + webAuthService.fetchMyPermissionTargets(RoleScope.APP).values().forEach(targetIds::addAll); + queryAppIds = targetIds; + } else { + queryAppIds = Collections.emptySet(); } - return ResultDTO.success(convert(result)); + + Specification specification = (root, query, criteriaBuilder) -> { + List predicates = Lists.newArrayList(); + + Long appId = queryAppInfoRequest.getAppId(); + Long namespaceId = queryAppInfoRequest.getNamespaceId(); + + if (appId != null) { + predicates.add(criteriaBuilder.equal(root.get("id"), appId)); + } + + if (namespaceId != null) { + predicates.add(criteriaBuilder.equal(root.get("namespaceId"), namespaceId)); + } + + if (StringUtils.isNotEmpty(queryAppInfoRequest.getAppNameLike())) { + predicates.add(criteriaBuilder.like(root.get("appName"), QueryConvertUtils.convertLikeParams(queryAppInfoRequest.getAppNameLike()))); + } + + if (StringUtils.isNotEmpty(queryAppInfoRequest.getTagLike())) { + predicates.add(criteriaBuilder.like(root.get("tags"), QueryConvertUtils.convertLikeParams(queryAppInfoRequest.getTagLike()))); + } + + if (!queryAppIds.isEmpty()) { + predicates.add(criteriaBuilder.in(root.get("id")).value(queryAppIds)); + } + + return query.where(predicates.toArray(new Predicate[0])).getRestriction(); + }; + + Page pageAppInfoResult = appInfoRepository.findAll(specification, pageable); + + PageResult pageRet = new PageResult<>(pageAppInfoResult); + + List appInfoDos = pageAppInfoResult.get().collect(Collectors.toList()); + pageRet.setData(convert(appInfoDos, true)); + + return ResultDTO.success(pageRet); } - private static List convert(List data) { + @PostMapping("/becomeAdmin") + @ApiPermission(name = "App-BecomeAdmin", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.NONE) + public ResultDTO becomeAdminByAppNameAndPassword(@RequestBody AppAssertRequest appAssertRequest) { + String appName = appAssertRequest.getAppName(); + Optional appInfoOpt = appInfoRepository.findByAppName(appName); + if (!appInfoOpt.isPresent()) { + throw new IllegalArgumentException("can't find app by appName: " + appName); + } + if (!StringUtils.equals(appInfoOpt.get().getPassword(), appAssertRequest.getPassword())) { + throw new PowerJobAuthException(AuthErrorCode.INCORRECT_PASSWORD); + } + + Map extra = Maps.newHashMap(); + extra.put("source", "becomeAdminByAppNameAndPassword"); + + webAuthService.grantRole2LoginUser(RoleScope.APP, appInfoOpt.get().getId(), Role.ADMIN, JsonUtils.toJSONString(extra)); + + return ResultDTO.success(null); + } + + private List convert(List data, boolean fillDetail) { if (CollectionUtils.isEmpty(data)) { return Lists.newLinkedList(); } - return data.stream().map(appInfoDO -> { + + return data.parallelStream().map(appInfoDO -> { AppInfoVO appInfoVO = new AppInfoVO(); BeanUtils.copyProperties(appInfoDO, appInfoVO); + + appInfoVO.setGmtCreateStr(CommonUtils.formatTime(appInfoDO.getGmtCreate())); + appInfoVO.setGmtModifiedStr(CommonUtils.formatTime(appInfoDO.getGmtModified())); + + if (fillDetail) { + // 人员面板 + ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.APP, appInfoDO.getId()); + appInfoVO.setComponentUserRoleInfo(componentUserRoleInfo); + + // 密码 + boolean hasPermission = webAuthService.hasPermission(RoleScope.APP, appInfoDO.getId(), Permission.READ); + appInfoVO.setPassword(hasPermission ? appInfoDO.getPassword() : AuthConstants.TIPS_NO_PERMISSION_TO_SEE); + + // namespace + Optional namespaceOpt = namespaceWebService.findById(appInfoDO.getNamespaceId()); + if (namespaceOpt.isPresent()) { + NamespaceBaseVO baseNamespace = NamespaceConverter.do2BaseVo(namespaceOpt.get()); + appInfoVO.setNamespace(baseNamespace); + appInfoVO.setNamespaceName(baseNamespace.getName()); + } + + // user 信息 + appInfoVO.setCreatorShowName(userWebService.fetchBaseUserInfo(appInfoDO.getCreator()).map(UserBaseVO::getShowName).orElse(null)); + appInfoVO.setModifierShowName(userWebService.fetchBaseUserInfo(appInfoDO.getModifier()).map(UserBaseVO::getShowName).orElse(null)); + + } + return appInfoVO; }).collect(Collectors.toList()); } - @Data - private static class AppInfoVO { - private Long id; - private String appName; - } - } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java new file mode 100644 index 00000000..b9198764 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/AuthController.java @@ -0,0 +1,123 @@ +package tech.powerjob.server.web.controller; + +import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.common.utils.CollectionUtils; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.login.LoginTypeInfo; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.auth.service.login.LoginRequest; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import javax.annotation.Resource; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Optional; + +/** + * 登录 & 权限相关 + * + * @author tjq + * @since 2023/4/16 + */ +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Resource + private WebAuthService webAuthService; + @Resource + private PowerJobLoginService powerJobLoginService; + + @GetMapping("/supportLoginTypes") + public ResultDTO> listSupportLoginTypes() { + return ResultDTO.success(powerJobLoginService.fetchSupportLoginTypes()); + } + + @GetMapping("/thirdPartyLoginUrl") + public ResultDTO getThirdPartyLoginUrl(String type, HttpServletRequest request) { + String url = powerJobLoginService.fetchThirdPartyLoginUrl(type, request); + return ResultDTO.success(url); + } + + /** + * 第三方账号体系回调登录接口,eg, 接受钉钉登录回调 + * @param httpServletRequest 请求 + * @param httpServletResponse 响应 + * @return 登录结果 + */ + @RequestMapping(value = "/thirdPartyLoginCallback", method = {RequestMethod.GET, RequestMethod.POST}) + public ResultDTO loginCallback(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { + + LoginRequest loginContext = new LoginRequest().setHttpServletRequest(httpServletRequest); + + // 常见登录组件的标准规范(钉钉、企业微信、飞书),第三方原样透传。开发者在对接第三方登录体系时,可能需要修改此处,将 type 回填 + final String state = httpServletRequest.getParameter("state"); + loginContext.setLoginType(state); + + final PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginContext); + fillJwt4LoginUser(powerJobUser, httpServletResponse); + + return ResultDTO.success(powerJobUser); + } + + /** + * 第三方账号体系直接登录接口,eg, 接受 PowerJob 自带账号密码体系的登录请求 + * @param loginRequest 登录请求 + * @param httpServletResponse 响应 + * @return 登录结果 + */ + @PostMapping("/thirdPartyLoginDirect") + public ResultDTO selfLogin(@RequestBody LoginRequest loginRequest, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) { + loginRequest.setHttpServletRequest(httpServletRequest); + try { + final PowerJobUser powerJobUser = powerJobLoginService.doLogin(loginRequest); + if (powerJobUser == null) { + return ResultDTO.failed("USER_NOT_FOUND"); + } + fillJwt4LoginUser(powerJobUser, httpServletResponse); + return ResultDTO.success(powerJobUser); + } catch (Exception e) { + return ResultDTO.failed(e.getMessage()); + } + } + + @GetMapping(value = "/ifLogin") + public ResultDTO ifLogin(HttpServletRequest httpServletRequest) { + final Optional powerJobUser = powerJobLoginService.ifLogin(httpServletRequest); + return powerJobUser.map(ResultDTO::success).orElseGet(() -> ResultDTO.success(null)); + } + + /* ****************** 授权相关 ****************** */ + + @GetMapping("/listGlobalAdmin") + public ResultDTO> listGlobalAdmin() { + // 全局只设置超级管理员权限 + ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID); + return ResultDTO.success(componentUserRoleInfo.getAdmin()); + } + + @PostMapping("/saveGlobalAdmin") + @ApiPermission(name = "Auth-SaveGlobalAdmin", roleScope = RoleScope.GLOBAL, requiredPermission = Permission.SU) + public ResultDTO saveGlobalAdmin(@RequestBody ComponentUserRoleInfo componentUserRoleInfo) { + + if (CollectionUtils.isEmpty(componentUserRoleInfo.getAdmin())) { + throw new IllegalArgumentException("At least one super administrator is required!"); + } + + webAuthService.processPermissionOnSave(RoleScope.GLOBAL, AuthConstants.GLOBAL_ADMIN_TARGET_ID, componentUserRoleInfo); + + return ResultDTO.success(null); + } + + private void fillJwt4LoginUser(PowerJobUser powerJobUser, HttpServletResponse httpServletResponse) { + httpServletResponse.addCookie(new Cookie(AuthConstants.JWT_NAME, powerJobUser.getJwtToken())); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java index 02acd5d0..361a881f 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/ContainerController.java @@ -4,11 +4,13 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.time.DateFormatUtils; import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import tech.powerjob.common.OmsConstant; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.common.constants.ContainerSourceType; import tech.powerjob.server.common.constants.SwitchableStatus; import tech.powerjob.server.common.utils.OmsFileUtils; @@ -39,22 +41,25 @@ import java.util.stream.Collectors; @RequestMapping("/container") public class ContainerController { - - private final int port; - private final ContainerService containerService; private final AppInfoRepository appInfoRepository; private final ContainerInfoRepository containerInfoRepository; - public ContainerController(@Value("${server.port}") int port, ContainerService containerService, AppInfoRepository appInfoRepository, ContainerInfoRepository containerInfoRepository) { - this.port = port; + public ContainerController(ContainerService containerService, AppInfoRepository appInfoRepository, ContainerInfoRepository containerInfoRepository) { this.containerService = containerService; this.appInfoRepository = appInfoRepository; this.containerInfoRepository = containerInfoRepository; } + /** + * 暴露给 worker 的下载端口,制品本身 version 不可枚举,不单独鉴权 + * 如果对此有安全性需求,可自行实现加密鉴权逻辑,或者干脆走自己的下载通道下载制品 + * @param version 容器版本 + * @param response 响应 + * @throws IOException 异常 + */ @GetMapping("/downloadJar") public void downloadJar(String version, HttpServletResponse response) throws IOException { File file = containerService.fetchContainerJarFile(version); @@ -66,12 +71,14 @@ public class ContainerController { } @PostMapping("/downloadContainerTemplate") + @ApiPermission(name = "Container-DownloadContainerTemplate", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public void downloadContainerTemplate(@RequestBody GenerateContainerTemplateRequest req, HttpServletResponse response) throws IOException { File zipFile = ContainerTemplateGenerator.generate(req.getGroup(), req.getArtifact(), req.getName(), req.getPackageName(), req.getJavaVersion()); OmsFileUtils.file2HttpResponse(zipFile, response); } @PostMapping("/jarUpload") + @ApiPermission(name = "Container-JarUpload", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO fileUpload(@RequestParam("file") MultipartFile file) throws Exception { if (file == null || file.isEmpty()) { return ResultDTO.failed("empty file"); @@ -80,6 +87,7 @@ public class ContainerController { } @PostMapping("/save") + @ApiPermission(name = "Container-Save", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO saveContainer(@RequestBody SaveContainerInfoRequest request) { request.valid(); @@ -93,12 +101,14 @@ public class ContainerController { } @GetMapping("/delete") + @ApiPermission(name = "Container-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO deleteContainer(Long appId, Long containerId) { containerService.delete(appId, containerId); return ResultDTO.success(null); } @GetMapping("/list") + @ApiPermission(name = "Container-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> listContainers(Long appId) { List res = containerInfoRepository.findByAppIdAndStatusNot(appId, SwitchableStatus.DELETED.getV()) .stream().map(ContainerController::convert).collect(Collectors.toList()); @@ -106,6 +116,7 @@ public class ContainerController { } @GetMapping("/listDeployedWorker") + @ApiPermission(name = "Container-ListDeployedWorker", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO listDeployedWorker(Long appId, Long containerId, HttpServletResponse response) { AppInfoDO appInfoDO = appInfoRepository.findById(appId).orElseThrow(() -> new IllegalArgumentException("can't find app by id:" + appId)); String targetServer = appInfoDO.getCurrentServer(); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java index 56ff5ea0..fb1cfa00 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/InstanceController.java @@ -3,6 +3,9 @@ package tech.powerjob.server.web.controller; import tech.powerjob.common.OmsConstant; import tech.powerjob.common.enums.InstanceStatus; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.common.utils.OmsFileUtils; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.StringPage; @@ -46,8 +49,6 @@ import java.util.stream.Collectors; @RequestMapping("/instance") public class InstanceController { - - @Resource private InstanceService instanceService; @Resource @@ -59,18 +60,21 @@ public class InstanceController { private InstanceInfoRepository instanceInfoRepository; @GetMapping("/stop") + @ApiPermission(name = "Instance-Stop", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO stopInstance(Long appId,Long instanceId) { instanceService.stopInstance(appId,instanceId); return ResultDTO.success(null); } @GetMapping("/retry") + @ApiPermission(name = "Instance-Retry", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO retryInstance(String appId, Long instanceId) { instanceService.retryInstance(Long.valueOf(appId), instanceId); return ResultDTO.success(null); } @GetMapping("/detail") + @ApiPermission(name = "Instance-Detail", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO getInstanceDetail(Long appId, Long instanceId) { QueryInstanceDetailRequest queryInstanceDetailRequest = new QueryInstanceDetailRequest(); queryInstanceDetailRequest.setAppId(appId); @@ -97,11 +101,13 @@ public class InstanceController { } @GetMapping("/log") + @ApiPermission(name = "Instance-Log", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO getInstanceLog(Long appId, Long instanceId, Long index) { return ResultDTO.success(instanceLogService.fetchInstanceLog(appId, instanceId, index)); } @GetMapping("/downloadLogUrl") + @ApiPermission(name = "Instance-FetchDownloadLogUrl", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO getDownloadUrl(Long appId, Long instanceId) { return ResultDTO.success(instanceLogService.fetchDownloadUrl(appId, instanceId)); } @@ -133,6 +139,7 @@ public class InstanceController { } @PostMapping("/list") + @ApiPermission(name = "Instance-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> list(@RequestBody QueryInstanceRequest request) { Sort sort = Sort.by(Sort.Direction.DESC, "gmtModified"); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java index aaa3a9f1..ee8276ff 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/JobController.java @@ -3,6 +3,9 @@ package tech.powerjob.server.web.controller; import org.apache.commons.lang3.StringUtils; import tech.powerjob.common.request.http.SaveJobInfoRequest; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.common.constants.SwitchableStatus; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.remote.model.JobInfoDO; @@ -39,39 +42,46 @@ public class JobController { private JobInfoRepository jobInfoRepository; @PostMapping("/save") + @ApiPermission(name = "Job-Save", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO saveJobInfo(@RequestBody SaveJobInfoRequest request) { jobService.saveJob(request); return ResultDTO.success(null); } @PostMapping("/copy") + @ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO copyJob(String jobId) { return ResultDTO.success(JobInfoVO.from(jobService.copyJob(Long.valueOf(jobId)))); } @GetMapping("/export") + @ApiPermission(name = "Job-Export", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO exportJob(String jobId) { return ResultDTO.success(jobService.exportJob(Long.valueOf(jobId))); } @GetMapping("/disable") + @ApiPermission(name = "Job-Disable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO disableJob(String jobId) { jobService.disableJob(Long.valueOf(jobId)); return ResultDTO.success(null); } @GetMapping("/delete") + @ApiPermission(name = "Job-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO deleteJob(String jobId) { jobService.deleteJob(Long.valueOf(jobId)); return ResultDTO.success(null); } @GetMapping("/run") + @ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO runImmediately(String appId, String jobId, @RequestParam(required = false) String instanceParams) { return ResultDTO.success(jobService.runJob(Long.valueOf(appId), Long.valueOf(jobId), instanceParams, 0L)); } @PostMapping("/list") + @ApiPermission(name = "Job-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> listJobs(@RequestBody QueryJobInfoRequest request) { Sort sort = Sort.by(Sort.Direction.ASC, "id"); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java new file mode 100644 index 00000000..11cbcd9e --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/NamespaceController.java @@ -0,0 +1,117 @@ +package tech.powerjob.server.web.controller; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.BeanUtils; +import org.springframework.data.domain.Page; +import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthConstants; +import tech.powerjob.server.auth.interceptor.ApiPermission; +import tech.powerjob.server.auth.plugin.ModifyOrCreateDynamicPermission; +import tech.powerjob.server.auth.plugin.SaveNamespaceGrantPermissionPlugin; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.persistence.PageResult; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.converter.NamespaceConverter; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; +import tech.powerjob.server.web.response.NamespaceBaseVO; +import tech.powerjob.server.web.response.NamespaceVO; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.service.NamespaceWebService; +import tech.powerjob.server.web.service.UserWebService; + +import javax.annotation.Resource; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 命名空间 Controller + * + * @author tjq + * @since 2023/9/3 + */ +@Slf4j +@RestController +@RequestMapping("/namespace") +public class NamespaceController { + + @Resource + private WebAuthService webAuthService; + @Resource + private UserWebService userWebService; + @Resource + private NamespaceWebService namespaceWebService; + + @ResponseBody + @PostMapping("/save") + @ApiPermission(name = "Namespace-Save", roleScope = RoleScope.NAMESPACE, dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class, grandPermissionPlugin = SaveNamespaceGrantPermissionPlugin.class) + public ResultDTO save(@RequestBody ModifyNamespaceRequest req) { + + NamespaceDO savedNamespace = namespaceWebService.save(req); + return ResultDTO.success(NamespaceConverter.do2BaseVo(savedNamespace)); + } + + @DeleteMapping("/delete") + @ApiPermission(name = "Namespace-Delete", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.SU) + public ResultDTO deleteNamespace(Long id) { + namespaceWebService.delete(id); + return ResultDTO.success(null); + } + + @PostMapping("/list") + @ApiPermission(name = "Namespace-List", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE) + public ResultDTO> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) { + + Page namespacePageResult = namespaceWebService.list(queryNamespaceRequest); + + PageResult ret = new PageResult<>(namespacePageResult); + ret.setData(namespacePageResult.get().map(x -> { + NamespaceVO detailVo = new NamespaceVO(); + NamespaceBaseVO baseVO = NamespaceConverter.do2BaseVo(x); + BeanUtils.copyProperties(baseVO, detailVo); + + fillDetail(x, detailVo); + return detailVo; + }).collect(Collectors.toList())); + + return ResultDTO.success(ret); + } + + @PostMapping("/listAll") + @ApiPermission(name = "Namespace-ListAll", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.NONE) + public ResultDTO> listAll() { + // 数量应该不是很多,先简单处理,不查询精简对象 + List namespaceRepositoryAll = namespaceWebService.listAll(); + List namespaceBaseVOList = namespaceRepositoryAll.stream().map(nd -> { + NamespaceBaseVO nv = new NamespaceBaseVO(); + nv.setId(nd.getId()); + nv.setCode(nd.getCode()); + nv.setName(nd.getName()); + nv.genShowName(); + return nv; + }).collect(Collectors.toList()); + return ResultDTO.success(namespaceBaseVOList); + } + + private void fillDetail(NamespaceDO namespaceDO, NamespaceVO namespaceVO) { + + Long namespaceId = namespaceVO.getId(); + + // 权限用户关系 + ComponentUserRoleInfo componentUserRoleInfo = webAuthService.fetchComponentUserRoleInfo(RoleScope.NAMESPACE, namespaceId); + namespaceVO.setComponentUserRoleInfo(componentUserRoleInfo); + + // 有权限用户填充 token + boolean hasPermission = webAuthService.hasPermission(RoleScope.NAMESPACE, namespaceId, Permission.READ); + namespaceVO.setToken(hasPermission ? namespaceDO.getToken() : AuthConstants.TIPS_NO_PERMISSION_TO_SEE); + + // 用户信息 + namespaceVO.setCreatorShowName(userWebService.fetchBaseUserInfo(namespaceDO.getCreator()).map(UserBaseVO::getShowName).orElse(null)); + namespaceVO.setModifierShowName(userWebService.fetchBaseUserInfo(namespaceDO.getModifier()).map(UserBaseVO::getShowName).orElse(null)); + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java new file mode 100644 index 00000000..af397b9e --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/PwjbUserInfoController.java @@ -0,0 +1,45 @@ +package tech.powerjob.server.web.controller; + +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; + +/** + * PowerJob 自带的登录体系 + * (同样视为第三方服务,与主框架没有任何关系) + * + * @author tjq + * @since 2024/2/13 + */ +@RestController +@RequestMapping("/pwjbUser") +public class PwjbUserInfoController { + + @Resource + private PwjbUserWebService pwjbUserWebService; + + /** + * 创建第三方登录体系(PowerJob) 的账户,不允许修改 + * @param request 请求(此处复用了主框架请求,便于用户一次性把所有参数都填入) + * @return 创建结果 + */ + @PostMapping("/create") + public ResultDTO save(@RequestBody ModifyUserInfoRequest request) { + pwjbUserWebService.save(request); + return ResultDTO.success(null); + } + + @PostMapping("/changePassword") + public ResultDTO changePassword(@RequestBody ChangePasswordRequest changePasswordRequest) { + + pwjbUserWebService.changePassword(changePasswordRequest); + return ResultDTO.success(null); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java index 5864fecc..728454c0 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/SystemInfoController.java @@ -12,6 +12,8 @@ import tech.powerjob.common.enums.InstanceStatus; import tech.powerjob.common.response.ResultDTO; import tech.powerjob.server.common.constants.SwitchableStatus; import tech.powerjob.server.common.module.WorkerInfo; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; import tech.powerjob.server.persistence.remote.repository.InstanceInfoRepository; import tech.powerjob.server.persistence.remote.repository.JobInfoRepository; import tech.powerjob.server.remote.server.self.ServerInfoService; @@ -21,6 +23,7 @@ import tech.powerjob.server.web.response.WorkerStatusVO; import java.util.Date; import java.util.List; +import java.util.Optional; import java.util.TimeZone; import java.util.stream.Collectors; @@ -36,6 +39,8 @@ import java.util.stream.Collectors; @RequiredArgsConstructor public class SystemInfoController { + private final AppInfoRepository appInfoRepository; + private final JobInfoRepository jobInfoRepository; private final InstanceInfoRepository instanceInfoRepository; @@ -56,6 +61,14 @@ public class SystemInfoController { SystemOverviewVO overview = new SystemOverviewVO(); + Optional appInfoOpt = appInfoRepository.findById(appId); + if (appInfoOpt.isPresent()) { + AppInfoDO appInfo = appInfoOpt.get(); + + overview.setAppId(appId); + overview.setAppName(appInfo.getAppName()); + } + // 总任务数量 overview.setJobCount(jobInfoRepository.countByAppIdAndStatusNot(appId, SwitchableStatus.DELETED.getV())); // 运行任务数 @@ -69,7 +82,8 @@ public class SystemInfoController { // 服务器时间 overview.setServerTime(DateFormatUtils.format(new Date(), OmsConstant.TIME_PATTERN)); - overview.setServerInfo(serverInfoService.fetchServiceInfo()); + overview.setWebServerInfo(serverInfoService.fetchCurrentServerInfo()); + overview.setScheduleServerInfo(serverInfoService.fetchAppServerInfo(appId)); return ResultDTO.success(overview); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java index 029c653f..95e31f42 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/UserInfoController.java @@ -1,21 +1,38 @@ package tech.powerjob.server.web.controller; -import tech.powerjob.common.response.ResultDTO; -import org.springframework.beans.BeanUtils; -import tech.powerjob.server.persistence.remote.model.UserInfoDO; -import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; -import tech.powerjob.server.core.service.UserService; -import tech.powerjob.server.web.request.ModifyUserInfoRequest; import com.google.common.collect.Lists; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import org.springframework.util.CollectionUtils; +import com.google.common.collect.Maps; +import com.google.common.collect.Sets; +import lombok.SneakyThrows; import org.apache.commons.lang3.StringUtils; +import org.springframework.beans.BeanUtils; +import org.springframework.util.CollectionUtils; import org.springframework.web.bind.annotation.*; +import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.PowerJobUser; +import tech.powerjob.server.auth.Role; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.auth.service.login.PowerJobLoginService; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; +import tech.powerjob.server.web.converter.NamespaceConverter; +import tech.powerjob.server.web.converter.UserConverter; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.response.AppBaseVO; +import tech.powerjob.server.web.response.NamespaceBaseVO; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.response.UserDetailVO; import javax.annotation.Resource; -import java.util.List; +import javax.servlet.http.HttpServletRequest; +import java.util.*; import java.util.stream.Collectors; /** @@ -27,22 +44,63 @@ import java.util.stream.Collectors; @RestController @RequestMapping("/user") public class UserInfoController { - - @Resource - private UserService userService; @Resource private UserInfoRepository userInfoRepository; + @Resource + private PowerJobLoginService powerJobLoginService; + @Resource + private WebAuthService webAuthService; + @Resource + private NamespaceRepository namespaceRepository; + @Resource + private AppInfoRepository appInfoRepository; + + @SneakyThrows + @PostMapping("/modify") + public ResultDTO modifyUser(@RequestBody ModifyUserInfoRequest modifyUserInfoRequest, HttpServletRequest httpServletRequest) { + + Optional powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest); + if (!powerJobUserOpt.isPresent()) { + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN); + } + + Long userId = modifyUserInfoRequest.getId(); + Optional userOpt = userInfoRepository.findById(userId); + if (!userOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by userId:" + userId); + } + + if (!Objects.equals(powerJobUserOpt.get().getId(), userId)) { + throw new IllegalAccessException("no permission to change others user info"); + } + + UserInfoDO dbUser = userOpt.get(); + + // 拷入允许修改的内容 + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getNick())) { + dbUser.setNick(modifyUserInfoRequest.getNick()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getPhone())) { + dbUser.setPhone(modifyUserInfoRequest.getPhone()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getEmail())) { + dbUser.setEmail(modifyUserInfoRequest.getEmail()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getWebHook())) { + dbUser.setWebHook(modifyUserInfoRequest.getWebHook()); + } + if (StringUtils.isNotEmpty(modifyUserInfoRequest.getExtra())) { + dbUser.setExtra(modifyUserInfoRequest.getExtra()); + } + + dbUser.setGmtModified(new Date()); + userInfoRepository.saveAndFlush(dbUser); - @PostMapping("save") - public ResultDTO save(@RequestBody ModifyUserInfoRequest request) { - UserInfoDO userInfoDO = new UserInfoDO(); - BeanUtils.copyProperties(request, userInfoDO); - userService.save(userInfoDO); return ResultDTO.success(null); } - @GetMapping("list") - public ResultDTO> list(@RequestParam(required = false) String name) { + @GetMapping("/list") + public ResultDTO> list(@RequestParam(required = false) String name) { List result; if (StringUtils.isEmpty(name)) { @@ -53,18 +111,76 @@ public class UserInfoController { return ResultDTO.success(convert(result)); } - private static List convert(List data) { + @GetMapping("/detail") + public ResultDTO getUserDetail(HttpServletRequest httpServletRequest) { + Optional powerJobUserOpt = powerJobLoginService.ifLogin(httpServletRequest); + if (!powerJobUserOpt.isPresent()) { + throw new PowerJobAuthException(AuthErrorCode.USER_NOT_LOGIN); + } + Optional userinfoDoOpt = userInfoRepository.findById(powerJobUserOpt.get().getId()); + if (!userinfoDoOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by id: " + powerJobUserOpt.get().getId()); + } + UserDetailVO userDetailVO = new UserDetailVO(); + BeanUtils.copyProperties(userinfoDoOpt.get(), userDetailVO); + userDetailVO.genShowName(); + + // 权限信息 + Map> globalPermissions = webAuthService.fetchMyPermissionTargets(RoleScope.GLOBAL); + userDetailVO.setGlobalRoles(globalPermissions.keySet().stream().map(Enum::name).collect(Collectors.toList())); + + Map> namespacePermissions = webAuthService.fetchMyPermissionTargets(RoleScope.NAMESPACE); + List nsList = namespaceRepository.findAllByIdIn(mergeIds(namespacePermissions)); + Map id2NamespaceDo = Maps.newHashMap(); + nsList.forEach(x -> id2NamespaceDo.put(x.getId(), x)); + Map> role2NamespaceBaseVo = Maps.newHashMap(); + namespacePermissions.forEach((k, v) -> { + List namespaceBaseVOS = Lists.newArrayList(); + role2NamespaceBaseVo.put(k.name(), namespaceBaseVOS); + v.forEach(nId -> { + NamespaceDO namespaceDO = id2NamespaceDo.get(nId); + if (namespaceDO == null) { + return; + } + NamespaceBaseVO namespaceBaseVO = NamespaceConverter.do2BaseVo(namespaceDO); + namespaceBaseVOS.add(namespaceBaseVO); + }); + }); + userDetailVO.setRole2NamespaceList(role2NamespaceBaseVo); + + Map> appPermissions = webAuthService.fetchMyPermissionTargets(RoleScope.APP); + List appList = appInfoRepository.findAllByIdIn(mergeIds(appPermissions)); + Map id2AppInfo = Maps.newHashMap(); + appList.forEach(x -> id2AppInfo.put(x.getId(), x)); + Map> role2AppBaseVo = Maps.newHashMap(); + appPermissions.forEach((k, v) -> { + List appBaseVOS = Lists.newArrayList(); + role2AppBaseVo.put(k.name(), appBaseVOS); + v.forEach(nId -> { + AppInfoDO appInfoDO = id2AppInfo.get(nId); + if (appInfoDO == null) { + return; + } + AppBaseVO appBaseVO = new AppBaseVO(); + BeanUtils.copyProperties(appInfoDO, appBaseVO); + appBaseVOS.add(appBaseVO); + }); + }); + userDetailVO.setRole2AppList(role2AppBaseVo); + + return ResultDTO.success(userDetailVO); + } + + private static List convert(List data) { if (CollectionUtils.isEmpty(data)) { return Lists.newLinkedList(); } - return data.stream().map(x -> new UserItemVO(x.getId(), x.getUsername())).collect(Collectors.toList()); + return data.stream().map(UserConverter::do2BaseVo).collect(Collectors.toList()); } - @Getter - @NoArgsConstructor - @AllArgsConstructor - public static final class UserItemVO { - private Long id; - private String username; + private static Set mergeIds(Map> map) { + Set ids = Sets.newHashSet(); + map.values().forEach(ids::addAll); + return ids; } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java index c78c6965..93b217cb 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowController.java @@ -1,26 +1,28 @@ package tech.powerjob.server.web.controller; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.web.bind.annotation.*; import tech.powerjob.common.request.http.SaveWorkflowNodeRequest; import tech.powerjob.common.request.http.SaveWorkflowRequest; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.core.workflow.WorkflowService; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.remote.model.WorkflowInfoDO; import tech.powerjob.server.persistence.remote.model.WorkflowNodeInfoDO; import tech.powerjob.server.persistence.remote.repository.WorkflowInfoRepository; -import tech.powerjob.server.core.workflow.WorkflowService; import tech.powerjob.server.web.request.QueryWorkflowInfoRequest; import tech.powerjob.server.web.response.WorkflowInfoVO; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Sort; -import org.apache.commons.lang3.StringUtils; -import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import java.text.ParseException; import java.util.List; -import java.util.function.LongToDoubleFunction; import java.util.stream.Collectors; /** @@ -40,34 +42,40 @@ public class WorkflowController { private WorkflowInfoRepository workflowInfoRepository; @PostMapping("/save") + @ApiPermission(name = "Workflow-Save", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO save(@RequestBody SaveWorkflowRequest req) throws ParseException { return ResultDTO.success(workflowService.saveWorkflow(req)); } @PostMapping("/copy") + @ApiPermission(name = "Workflow-Copy", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO copy(Long workflowId, Long appId) { return ResultDTO.success(workflowService.copyWorkflow(workflowId,appId)); } @GetMapping("/disable") + @ApiPermission(name = "Workflow-Disable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO disableWorkflow(Long workflowId, Long appId) { workflowService.disableWorkflow(workflowId, appId); return ResultDTO.success(null); } @GetMapping("/enable") + @ApiPermission(name = "Workflow-Enable", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO enableWorkflow(Long workflowId, Long appId) { workflowService.enableWorkflow(workflowId, appId); return ResultDTO.success(null); } @GetMapping("/delete") + @ApiPermission(name = "Workflow-Delete", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO deleteWorkflow(Long workflowId, Long appId) { workflowService.deleteWorkflow(workflowId, appId); return ResultDTO.success(null); } @PostMapping("/list") + @ApiPermission(name = "Workflow-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> list(@RequestBody QueryWorkflowInfoRequest req) { Sort sort = Sort.by(Sort.Direction.DESC, "gmtCreate"); @@ -89,6 +97,7 @@ public class WorkflowController { } @GetMapping("/run") + @ApiPermission(name = "Workflow-Run", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO runWorkflow(Long workflowId, Long appId, @RequestParam(required = false,defaultValue = "0") Long delay, @RequestParam(required = false) String initParams @@ -97,12 +106,14 @@ public class WorkflowController { } @GetMapping("/fetch") + @ApiPermission(name = "Workflow-Fetch", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO fetchWorkflow(Long workflowId, Long appId) { WorkflowInfoDO workflowInfoDO = workflowService.fetchWorkflow(workflowId, appId); return ResultDTO.success(WorkflowInfoVO.from(workflowInfoDO)); } @PostMapping("/saveNode") + @ApiPermission(name = "Workflow-SaveNode", roleScope = RoleScope.APP, requiredPermission = Permission.WRITE) public ResultDTO> addWorkflowNode(@RequestBody List request) { return ResultDTO.success(workflowService.saveWorkflowNode(request)); } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java index 944737dd..0f80016d 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/controller/WorkflowInstanceController.java @@ -2,6 +2,9 @@ package tech.powerjob.server.web.controller; import tech.powerjob.common.enums.WorkflowInstanceStatus; import tech.powerjob.common.response.ResultDTO; +import tech.powerjob.server.auth.Permission; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.persistence.PageResult; import tech.powerjob.server.persistence.remote.model.WorkflowInstanceInfoDO; import tech.powerjob.server.persistence.remote.repository.WorkflowInstanceInfoRepository; @@ -38,18 +41,21 @@ public class WorkflowInstanceController { private WorkflowInstanceInfoRepository workflowInstanceInfoRepository; @GetMapping("/stop") + @ApiPermission(name = "WorkflowInstance-Stop", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO stopWfInstance(Long wfInstanceId, Long appId) { workflowInstanceService.stopWorkflowInstanceEntrance(wfInstanceId, appId); return ResultDTO.success(null); } @RequestMapping("/retry") + @ApiPermission(name = "WorkflowInstance-Retry", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO retryWfInstance(Long wfInstanceId, Long appId) { workflowInstanceService.retryWorkflowInstance(wfInstanceId, appId); return ResultDTO.success(null); } @RequestMapping("/markNodeAsSuccess") + @ApiPermission(name = "WorkflowInstance-MarkNodeAsSuccess", roleScope = RoleScope.APP, requiredPermission = Permission.OPS) public ResultDTO markNodeAsSuccess(Long wfInstanceId, Long appId, Long nodeId) { workflowInstanceService.markNodeAsSuccess(appId, wfInstanceId, nodeId); return ResultDTO.success(null); @@ -57,12 +63,14 @@ public class WorkflowInstanceController { @GetMapping("/info") + @ApiPermission(name = "WorkflowInstance-Info", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO getInfo(Long wfInstanceId, Long appId) { WorkflowInstanceInfoDO wfInstanceDO = workflowInstanceService.fetchWfInstance(wfInstanceId, appId); return ResultDTO.success(WorkflowInstanceInfoVO.from(wfInstanceDO, cacheService.getWorkflowName(wfInstanceDO.getWorkflowId()))); } @PostMapping("/list") + @ApiPermission(name = "WorkflowInstance-List", roleScope = RoleScope.APP, requiredPermission = Permission.READ) public ResultDTO> listWfInstance(@RequestBody QueryWorkflowInstanceRequest req) { Sort sort = Sort.by(Sort.Direction.DESC, "gmtModified"); PageRequest pageable = PageRequest.of(req.getIndex(), req.getPageSize(), sort); diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java new file mode 100644 index 00000000..b727d9e9 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/NamespaceConverter.java @@ -0,0 +1,28 @@ +package tech.powerjob.server.web.converter; + +import org.springframework.beans.BeanUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.response.NamespaceBaseVO; + +/** + * NamespaceConverter + * + * @author tjq + * @since 2023/9/4 + */ +public class NamespaceConverter { + + public static NamespaceBaseVO do2BaseVo(NamespaceDO d) { + NamespaceBaseVO v = new NamespaceBaseVO(); + BeanUtils.copyProperties(d, v); + v.setGmtCreateStr(CommonUtils.formatTime(d.getGmtCreate())); + v.setGmtModifiedStr(CommonUtils.formatTime(d.getGmtModified())); + v.setStatusStr(SwitchableStatus.of(d.getStatus()).name()); + + v.genShowName(); + return v; + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/UserConverter.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/UserConverter.java new file mode 100644 index 00000000..77876d93 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/converter/UserConverter.java @@ -0,0 +1,26 @@ +package tech.powerjob.server.web.converter; + +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.web.response.UserBaseVO; + +/** + * UserConverter + * + * @author tjq + * @since 2023/9/4 + */ +public class UserConverter { + + public static UserBaseVO do2BaseVo(UserInfoDO x) { + + UserBaseVO userBaseVO = new UserBaseVO(); + + userBaseVO.setId(x.getId()); + userBaseVO.setUsername(x.getUsername()); + userBaseVO.setNick(x.getNick()); + + userBaseVO.genShowName(); + return userBaseVO; + } + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ChangePasswordRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ChangePasswordRequest.java new file mode 100644 index 00000000..60b5ddb7 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ChangePasswordRequest.java @@ -0,0 +1,23 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +import java.io.Serializable; + +/** + * 修改密码 + * + * @author tjq + * @since 2024/2/13 + */ +@Data +public class ChangePasswordRequest implements Serializable { + + private String username; + + private String oldPassword; + + private String newPassword; + + private String newPassword2; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ComponentUserRoleInfo.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ComponentUserRoleInfo.java new file mode 100644 index 00000000..0b2fd925 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ComponentUserRoleInfo.java @@ -0,0 +1,34 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; +import lombok.experimental.Accessors; + +import java.util.List; + +/** + * 组件上的用户角色信息 + * + * @author tjq + * @since 2024/2/12 + */ +@Data +@Accessors(chain = true) +public class ComponentUserRoleInfo { + /** + * 观察者 + */ + private List observer; + /** + * 测试 + */ + private List qa; + /** + * 开发者 + */ + private List developer; + /** + * 管理员 + */ + private List admin; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/GrantPermissionRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/GrantPermissionRequest.java new file mode 100644 index 00000000..cc2b71d6 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/GrantPermissionRequest.java @@ -0,0 +1,31 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * 授权请求 + * + * @author tjq + * @since 2024/2/12 + */ +@Data +public class GrantPermissionRequest implements Serializable { + + /** + * 目标ID + */ + private Long targetId; + + /** + * 授予的角色 + */ + private Integer role; + + /** + * 授予的用户IDS + */ + private List userIds; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java index 2b5aca6f..4669cf62 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyAppInfoRequest.java @@ -15,14 +15,37 @@ import org.apache.commons.lang3.StringUtils; public class ModifyAppInfoRequest { private Long id; - private String oldPassword; private String appName; + + private Long namespaceId; + + private String oldPassword; private String password; + /** + * 描述 + */ + private String title; + + /** + * 管理标签 + */ + private String tags; + /** + * 扩展字段 + */ + private String extra; + + private ComponentUserRoleInfo componentUserRoleInfo; + public void valid() { CommonUtils.requireNonNull(appName, "appName can't be empty"); if (StringUtils.containsWhitespace(appName)) { throw new PowerJobException("appName can't contains white space!"); } + CommonUtils.requireNonNull(password, "password can't be empty"); + + // 后续版本强制要求设置 namespace,方便统一管理 + CommonUtils.requireNonNull(namespaceId, "namespace can't be empty"); } } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java new file mode 100644 index 00000000..1edb7eb3 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyNamespaceRequest.java @@ -0,0 +1,53 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; +import org.apache.commons.lang3.StringUtils; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.common.utils.CommonUtils; + +/** + * ModifyNamespaceRequest + * + * @author tjq + * @since 2023/9/3 + */ +@Data +public class ModifyNamespaceRequest { + + private Long id; + + /** + * 空间唯一标识 + */ + private String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + private String name; + + private String dept; + + /** + * 标签,扩展性之王,多值逗号分割 + */ + private String tags; + + private Integer status; + + /** + * 扩展字段 + */ + private String extra; + /** + * 权限表单 + */ + private ComponentUserRoleInfo componentUserRoleInfo; + + public void valid() { + CommonUtils.requireNonNull(code, "namespace code can't be empty"); + if (StringUtils.containsWhitespace(code)) { + throw new PowerJobException("namespace code can't contains white space!"); + } + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java index db43e346..428f5c84 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/ModifyUserInfoRequest.java @@ -14,6 +14,7 @@ public class ModifyUserInfoRequest { private Long id; private String username; + private String nick; private String password; private String webHook; @@ -25,4 +26,6 @@ public class ModifyUserInfoRequest { * 邮箱地址 */ private String email; + + private String extra; } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java new file mode 100644 index 00000000..44f2886a --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryAppInfoRequest.java @@ -0,0 +1,44 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +/** + * 查询应用信息 + * + * @author tjq + * @since 2024/2/11 + */ +@Data +public class QueryAppInfoRequest { + + /** + * appId 精确查旋 + */ + private Long appId; + /** + * namespaceId + */ + private Long namespaceId; + /** + * 任务名称 + */ + private String appNameLike; + + private String tagLike; + + /** + * 查询与我相关的任务(我有直接权限的) + */ + private Boolean showMyRelated; + + /* ****************** 分页参数 ****************** */ + /** + * 当前页码 + */ + private Integer index; + /** + * 页大小 + */ + private Integer pageSize; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java new file mode 100644 index 00000000..57f4d8f3 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/request/QueryNamespaceRequest.java @@ -0,0 +1,35 @@ +package tech.powerjob.server.web.request; + +import lombok.Data; + +/** + * 查询 namespace 请求 + * + * @author tjq + * @since 2024/2/11 + */ +@Data +public class QueryNamespaceRequest { + + /** + * code 模糊查询 + */ + private String codeLike; + + /** + * 名称模糊查询 + */ + private String nameLike; + + private String tagLike; + + /* ****************** 分页参数 ****************** */ + /** + * 当前页码 + */ + private Integer index = 0; + /** + * 页大小 + */ + private Integer pageSize = 10; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppBaseVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppBaseVO.java new file mode 100644 index 00000000..2c0f778f --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppBaseVO.java @@ -0,0 +1,27 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; + +/** + * AppBaseVO + * + * @author tjq + * @since 2024/2/13 + */ +@Getter +@Setter +public class AppBaseVO implements Serializable { + + protected Long id; + + protected String appName; + + protected Long namespaceId; + /** + * 描述 + */ + protected String title; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppInfoVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppInfoVO.java new file mode 100644 index 00000000..9e08f1f2 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/AppInfoVO.java @@ -0,0 +1,47 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +import java.util.Date; + +/** + * AppInfoVO + * + * @author tjq + * @since 2024/2/12 + */ +@Getter +@Setter +@ToString +public class AppInfoVO extends AppBaseVO { + + private String password; + + private String tags; + + private String extra; + + private ComponentUserRoleInfo componentUserRoleInfo; + + private Date gmtCreate; + + private String gmtCreateStr; + + private Date gmtModified; + + private String gmtModifiedStr; + + private String creatorShowName; + + private String modifierShowName; + + /** + * Namespace Info + */ + private NamespaceBaseVO namespace; + + private String namespaceName; +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java new file mode 100644 index 00000000..dbfa6ce5 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceBaseVO.java @@ -0,0 +1,50 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; + +import java.io.Serializable; +import java.util.Date; + +/** + * namespace 基本 VO 对象,用于列表渲染 + * + * @author tjq + * @since 2024/2/12 + */ +@Getter +@Setter +public class NamespaceBaseVO implements Serializable { + + protected Long id; + + /** + * 空间唯一标识 + */ + protected String code; + + /** + * 空间名称,比如中文描述(XX部门XX空间) + */ + protected String name; + + private Integer status; + private String statusStr; + + private Date gmtCreate; + + private String gmtCreateStr; + + private Date gmtModified; + + private String gmtModifiedStr; + + /** + * 前端名称(拼接 code + name,更容易辨认) + */ + protected String showName; + + public void genShowName() { + showName = String.format("%s(%s)", name, code); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceVO.java new file mode 100644 index 00000000..f9bc1b83 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/NamespaceVO.java @@ -0,0 +1,39 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import tech.powerjob.server.web.request.ComponentUserRoleInfo; + +/** + * 基础版本的命名空间 + * + * @author tjq + * @since 2023/9/3 + */ +@Getter +@Setter +@ToString +public class NamespaceVO extends NamespaceBaseVO { + + private String dept; + private String tags; + + /** + * 扩展字段 + */ + private String extra; + + /** + * 访问 token + * 仅拥有当前 namespace 权限的访问者可见 + */ + private String token; + + private ComponentUserRoleInfo componentUserRoleInfo; + + private String creatorShowName; + + private String modifierShowName; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java index 0f4f9526..178550d2 100644 --- a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/SystemOverviewVO.java @@ -1,8 +1,6 @@ package tech.powerjob.server.web.response; -import lombok.AllArgsConstructor; import lombok.Data; -import lombok.Getter; import tech.powerjob.server.common.module.ServerInfo; /** @@ -14,6 +12,10 @@ import tech.powerjob.server.common.module.ServerInfo; @Data public class SystemOverviewVO { + private Long appId; + + private String appName; + private long jobCount; private long runningInstanceCount; private long failedInstanceCount; @@ -26,5 +28,12 @@ public class SystemOverviewVO { */ private String serverTime; - private ServerInfo serverInfo; + /** + * 处理当前 WEB 服务的 server 信息 + */ + private ServerInfo webServerInfo; + /** + * 调度服务器信息 + */ + private ServerInfo scheduleServerInfo; } diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserBaseVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserBaseVO.java new file mode 100644 index 00000000..1b0ee206 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserBaseVO.java @@ -0,0 +1,36 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; +import org.apache.commons.lang3.StringUtils; + +/** + * 用户基础信息 + * + * @author tjq + * @since 2023/9/3 + */ +@Getter +@Setter +@NoArgsConstructor +public class UserBaseVO { + protected Long id; + protected String username; + protected String nick; + + /** + * 前端展示名称,更容易辨认 + */ + protected String showName; + + public void genShowName() { + + if (StringUtils.isEmpty(nick)) { + showName = username; + } else { + showName = String.format("%s (%s)", nick, username); + } + } + +} \ No newline at end of file diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserDetailVO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserDetailVO.java new file mode 100644 index 00000000..7b83e496 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/UserDetailVO.java @@ -0,0 +1,62 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; + +import java.util.List; +import java.util.Map; + +/** + * 用户详细信息 + * + * @author tjq + * @since 2024/2/13 + */ +@Getter +@Setter +@ToString +public class UserDetailVO extends UserBaseVO { + + /** + * 账户类型 + */ + private String accountType; + /** + * 密码 + */ + private String password; + + /** + * 手机号 + */ + private String phone; + /** + * 邮箱地址 + */ + private String email; + /** + * webHook + */ + private String webHook; + + private String originUsername; + /** + * 扩展字段 + */ + private String extra; + + /** + * 拥有的全局权限 + */ + private List globalRoles; + /** + * 拥有的 namespace 权限 + */ + private Map> role2NamespaceList; + /** + * 拥有的 app 权限 + */ + private Map> role2AppList; + +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WebResultDTO.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WebResultDTO.java new file mode 100644 index 00000000..272c4d39 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/response/WebResultDTO.java @@ -0,0 +1,27 @@ +package tech.powerjob.server.web.response; + +import lombok.Getter; +import lombok.Setter; +import tech.powerjob.common.response.ResultDTO; + +/** + * WEB 请求结果 + * + * @author tjq + * @since 2024/2/18 + */ +@Getter +@Setter +public class WebResultDTO extends ResultDTO { + + private String code; + + public WebResultDTO() { + } + + public WebResultDTO(ResultDTO res) { + this.success = res.isSuccess(); + this.data = res.getData(); + this.message = res.getMessage(); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java new file mode 100644 index 00000000..07f9d218 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/NamespaceWebService.java @@ -0,0 +1,28 @@ +package tech.powerjob.server.web.service; + +import org.springframework.data.domain.Page; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; + +import java.util.List; +import java.util.Optional; + +/** + * namespace web 服务 + * + * @author tjq + * @since 2024/2/15 + */ +public interface NamespaceWebService { + + NamespaceDO save(ModifyNamespaceRequest req); + + void delete(Long id); + + Optional findById(Long id); + + Page list(QueryNamespaceRequest queryNamespaceRequest); + + List listAll(); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java new file mode 100644 index 00000000..36e6ea14 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/PwjbUserWebService.java @@ -0,0 +1,18 @@ +package tech.powerjob.server.web.service; + +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; + +/** + * PwjbUserWebService + * + * @author tjq + * @since 2024/2/15 + */ +public interface PwjbUserWebService { + + PwjbUserInfoDO save(ModifyUserInfoRequest request); + + void changePassword(ChangePasswordRequest changePasswordRequest); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/UserWebService.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/UserWebService.java new file mode 100644 index 00000000..c6fe2ccc --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/UserWebService.java @@ -0,0 +1,16 @@ +package tech.powerjob.server.web.service; + +import tech.powerjob.server.web.response.UserBaseVO; + +import java.util.Optional; + +/** + * 用户 WEB 服务 + * + * @author tjq + * @since 2024/2/17 + */ +public interface UserWebService { + + Optional fetchBaseUserInfo(Long userId); +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java new file mode 100644 index 00000000..42266fc5 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/NamespaceWebServiceImpl.java @@ -0,0 +1,155 @@ +package tech.powerjob.server.web.service.impl; + +import com.google.common.collect.Lists; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import tech.powerjob.common.exception.PowerJobException; +import tech.powerjob.server.auth.LoginUserHolder; +import tech.powerjob.server.auth.RoleScope; +import tech.powerjob.server.auth.service.WebAuthService; +import tech.powerjob.server.common.SJ; +import tech.powerjob.server.common.constants.SwitchableStatus; +import tech.powerjob.server.persistence.QueryConvertUtils; +import tech.powerjob.server.persistence.remote.model.AppInfoDO; +import tech.powerjob.server.persistence.remote.model.NamespaceDO; +import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; +import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; +import tech.powerjob.server.web.request.ModifyNamespaceRequest; +import tech.powerjob.server.web.request.QueryNamespaceRequest; +import tech.powerjob.server.web.service.NamespaceWebService; + +import javax.annotation.Resource; +import javax.persistence.criteria.Predicate; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Collectors; + +/** + * NamespaceWebService + * + * @author tjq + * @since 2024/2/15 + */ +@Service +public class NamespaceWebServiceImpl implements NamespaceWebService { + + @Resource + private WebAuthService webAuthService; + @Resource + private AppInfoRepository appInfoRepository; + @Resource + private NamespaceRepository namespaceRepository; + + @Override + public NamespaceDO save(ModifyNamespaceRequest req) { + req.valid(); + + Long id = req.getId(); + NamespaceDO namespaceDO; + + boolean isCreate = id == null; + + if (isCreate) { + namespaceDO = new NamespaceDO(); + namespaceDO.setGmtCreate(new Date()); + + // code 单独拷贝 + namespaceDO.setCode(req.getCode()); + // 创建时生成 token + namespaceDO.setToken(UUID.randomUUID().toString()); + namespaceDO.setCreator(LoginUserHolder.getUserId()); + + } else { + namespaceDO = fetchById(id); + namespaceDO.setModifier(LoginUserHolder.getUserId()); + + if (!namespaceDO.getCode().equalsIgnoreCase(req.getCode())) { + throw new IllegalArgumentException("NOT_ALLOW_CHANGE_THE_NAMESPACE_CODE"); + } + } + + // 拷贝通用变更属性(code 不允许更改) + namespaceDO.setTags(req.getTags()); + namespaceDO.setName(req.getName()); + namespaceDO.setExtra(req.getExtra()); + namespaceDO.setStatus(Optional.ofNullable(req.getStatus()).orElse(SwitchableStatus.ENABLE.getV())); + + namespaceDO.setGmtModified(new Date()); + NamespaceDO savedNamespace = namespaceRepository.save(namespaceDO); + + // 授权 + webAuthService.processPermissionOnSave(RoleScope.NAMESPACE, savedNamespace.getId(), req.getComponentUserRoleInfo()); + + return savedNamespace; + } + + @Override + public void delete(Long id) { + List appInfosInNamespace = appInfoRepository.findAllByNamespaceId(id); + if (CollectionUtils.isNotEmpty(appInfosInNamespace)) { + List relatedApps = appInfosInNamespace.stream().map(AppInfoDO::getAppName).collect(Collectors.toList()); + throw new PowerJobException("Unable to delete due to associated apps: " + SJ.COMMA_JOINER.join(relatedApps)); + } + + namespaceRepository.deleteById(id); + } + + @Override + public Optional findById(Long id) { + if (id == null) { + return Optional.empty(); + } + return namespaceRepository.findById(id); + } + + @Override + public Page list(QueryNamespaceRequest queryNamespaceRequest) { + String codeLike = queryNamespaceRequest.getCodeLike(); + String nameLike = queryNamespaceRequest.getNameLike(); + String tagLike = queryNamespaceRequest.getTagLike(); + + Pageable pageable = PageRequest.of(queryNamespaceRequest.getIndex(), queryNamespaceRequest.getPageSize()); + Specification specification = (root, query, cb) -> { + + List predicates = Lists.newArrayList(); + + if (StringUtils.isNotEmpty(codeLike)) { + predicates.add(cb.like(root.get("code"), QueryConvertUtils.convertLikeParams(codeLike))); + } + + if (StringUtils.isNotEmpty(nameLike)) { + predicates.add(cb.like(root.get("name"), QueryConvertUtils.convertLikeParams(nameLike))); + } + if (StringUtils.isNotEmpty(tagLike)) { + predicates.add(cb.like(root.get("tags"), QueryConvertUtils.convertLikeParams(tagLike))); + } + + if (predicates.isEmpty()) { + return null; + } + return query.where(predicates.toArray(new Predicate[0])).getRestriction(); + }; + + return namespaceRepository.findAll(specification, pageable); + } + + @Override + public List listAll() { + return namespaceRepository.findAll(); + } + + private NamespaceDO fetchById(Long id) { + Optional namespaceDoOpt = namespaceRepository.findById(id); + if (!namespaceDoOpt.isPresent()) { + throw new IllegalArgumentException("can't find namespace by id: " + id); + } + return namespaceDoOpt.get(); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java new file mode 100644 index 00000000..104783c6 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/PwjbUserWebServiceImplImpl.java @@ -0,0 +1,99 @@ +package tech.powerjob.server.web.service.impl; + +import com.google.common.collect.Sets; +import lombok.SneakyThrows; +import org.apache.commons.lang3.StringUtils; +import org.springframework.stereotype.Service; +import tech.powerjob.common.serialize.JsonUtils; +import tech.powerjob.common.utils.CommonUtils; +import tech.powerjob.server.auth.common.AuthErrorCode; +import tech.powerjob.server.auth.common.PowerJobAuthException; +import tech.powerjob.server.common.utils.DigestUtils; +import tech.powerjob.server.persistence.remote.model.PwjbUserInfoDO; +import tech.powerjob.server.persistence.remote.repository.PwjbUserInfoRepository; +import tech.powerjob.server.web.request.ChangePasswordRequest; +import tech.powerjob.server.web.request.ModifyUserInfoRequest; +import tech.powerjob.server.web.service.PwjbUserWebService; + +import javax.annotation.Resource; +import java.util.Date; +import java.util.Optional; +import java.util.Set; + +/** + * PwjbUserWebService + * + * @author tjq + * @since 2024/2/15 + */ +@Service +public class PwjbUserWebServiceImplImpl implements PwjbUserWebService { + + @Resource + private PwjbUserInfoRepository pwjbUserInfoRepository; + + private static final Set NOT_ALLOWED_CHANGE_PASSWORD_ACCOUNTS = Sets.newHashSet("powerjob_trial_account"); + + @Override + @SneakyThrows + public PwjbUserInfoDO save(ModifyUserInfoRequest request) { + String username = request.getUsername(); + CommonUtils.requireNonNull(username, "userName can't be null or empty!"); + CommonUtils.requireNonNull(request.getPassword(), "password can't be null or empty!"); + + Optional oldUserOpt = pwjbUserInfoRepository.findByUsername(username); + if (oldUserOpt.isPresent()) { + throw new IllegalArgumentException("username already exist, please change one!"); + } + + PwjbUserInfoDO pwjbUserInfoDO = new PwjbUserInfoDO(); + + pwjbUserInfoDO.setUsername(username); + pwjbUserInfoDO.setGmtCreate(new Date()); + pwjbUserInfoDO.setGmtModified(new Date()); + + // 二次加密密码 + final String password = request.getPassword(); + if (StringUtils.isNotEmpty(password)) { + pwjbUserInfoDO.setPassword(DigestUtils.rePassword(password, pwjbUserInfoDO.getUsername())); + } + + // 其他参数存入 extra,在回调创建真正的内部 USER 时回填 + ModifyUserInfoRequest cpRequest = JsonUtils.parseObject(JsonUtils.toJSONString(request), ModifyUserInfoRequest.class); + cpRequest.setPassword(null); + cpRequest.setUsername(null); + cpRequest.setNick(null); + pwjbUserInfoDO.setExtra(JsonUtils.toJSONString(cpRequest)); + + return pwjbUserInfoRepository.save(pwjbUserInfoDO); + } + + @Override + public void changePassword(ChangePasswordRequest changePasswordRequest) { + if (!StringUtils.equals(changePasswordRequest.getNewPassword(), changePasswordRequest.getNewPassword2())) { + throw new IllegalArgumentException("Inconsistent passwords"); + } + + String username = changePasswordRequest.getUsername(); + Optional userOpt = pwjbUserInfoRepository.findByUsername(username); + if (!userOpt.isPresent()) { + throw new IllegalArgumentException("can't find user by username: " + username); + } + + PwjbUserInfoDO dbUser = userOpt.get(); + String oldPasswordInDb = dbUser.getPassword(); + String oldPasswordInReq = DigestUtils.rePassword(changePasswordRequest.getOldPassword(), dbUser.getUsername()); + if (!StringUtils.equals(oldPasswordInDb, oldPasswordInReq)) { + throw new PowerJobAuthException(AuthErrorCode.INCORRECT_PASSWORD); + } + + // 测试账号特殊处理 + if (NOT_ALLOWED_CHANGE_PASSWORD_ACCOUNTS.contains(username)) { + throw new IllegalArgumentException("this account not allowed change the password"); + } + + dbUser.setPassword(DigestUtils.rePassword(changePasswordRequest.getNewPassword(), dbUser.getUsername())); + dbUser.setGmtModified(new Date()); + pwjbUserInfoRepository.saveAndFlush(dbUser); + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/UserWebServiceImpl.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/UserWebServiceImpl.java new file mode 100644 index 00000000..fd44bce3 --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/impl/UserWebServiceImpl.java @@ -0,0 +1,58 @@ +package tech.powerjob.server.web.service.impl; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import org.springframework.stereotype.Service; +import tech.powerjob.server.persistence.remote.model.UserInfoDO; +import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; +import tech.powerjob.server.web.converter.UserConverter; +import tech.powerjob.server.web.response.UserBaseVO; +import tech.powerjob.server.web.service.UserWebService; + +import javax.annotation.Resource; +import java.util.Optional; +import java.util.concurrent.TimeUnit; + +/** + * UserWebService + * + * @author tjq + * @since 2024/2/17 + */ +@Service +public class UserWebServiceImpl implements UserWebService { + + /** + * 展示用的 user 查询缓存,对延迟不敏感 + */ + private final Cache userCache4Show = CacheBuilder.newBuilder() + .softValues() + .maximumSize(256) + .expireAfterWrite(3, TimeUnit.MINUTES) + .build(); + + @Resource + private UserInfoRepository userInfoRepository; + + @Override + public Optional fetchBaseUserInfo(Long userId) { + + if (userId == null) { + return Optional.empty(); + } + + try { + UserInfoDO userInfoDO = userCache4Show.get(userId, () -> { + Optional userInfoOpt = userInfoRepository.findById(userId); + if (userInfoOpt.isPresent()) { + return userInfoOpt.get(); + } + throw new IllegalArgumentException("can't find user by userId: " + userId); + }); + + return Optional.of(UserConverter.do2BaseVo(userInfoDO)); + } catch (Exception e) { + return Optional.empty(); + } + } +} diff --git a/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java new file mode 100644 index 00000000..e1f263ce --- /dev/null +++ b/powerjob-server/powerjob-server-starter/src/main/java/tech/powerjob/server/web/service/z-package-info.java @@ -0,0 +1,9 @@ +/** + * 处理 WEB 服务的 service 层 + * 如果有共用逻辑可以单独抽成 service,否则直接写在 controller 即可。PowerJob 的 WEB 领域模型不复杂,没必要过度封装。 + * LESS IS MORE + * + * @author tjq + * @since 2024/2/15 + */ +package tech.powerjob.server.web.service; \ No newline at end of file diff --git a/powerjob-server/powerjob-server-starter/src/main/resources/application.properties b/powerjob-server/powerjob-server-starter/src/main/resources/application.properties index 1b74735b..911527e7 100644 --- a/powerjob-server/powerjob-server-starter/src/main/resources/application.properties +++ b/powerjob-server/powerjob-server-starter/src/main/resources/application.properties @@ -19,4 +19,7 @@ oms.transporter.main.protocol=HTTP oms.akka.port=10086 oms.http.port=10010 # Prefix for all tables. Default empty string. Config if you have needs, i.e. pj_ -oms.table-prefix= \ No newline at end of file +oms.table-prefix= + +###### PowerJob User and Permission Configuration Configuration ###### +oms.auth.initiliaze.admin.password=powerjob_admin \ No newline at end of file diff --git a/powerjob-server/powerjob-server-starter/src/main/resources/static/img/banner.f4c75b86.jpg b/powerjob-server/powerjob-server-starter/src/main/resources/static/img/banner.f4c75b86.jpg deleted file mode 100644 index 5ea2145a..00000000 Binary files a/powerjob-server/powerjob-server-starter/src/main/resources/static/img/banner.f4c75b86.jpg and /dev/null differ diff --git a/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html b/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html index 1cef0c30..73bc0011 100644 --- a/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html +++ b/powerjob-server/powerjob-server-starter/src/main/resources/static/index.html @@ -6,7 +6,7 @@ PowerJob - +