feat: [auth] grant permission

This commit is contained in:
tjq 2024-02-12 11:11:06 +08:00
parent 3fdcc1e599
commit 841c7891c8
33 changed files with 422 additions and 101 deletions

View File

@ -1,13 +1,20 @@
package tech.powerjob.common.exception; package tech.powerjob.common.exception;
import lombok.Getter;
import lombok.Setter;
/** /**
* PowerJob 运行时异常 * PowerJob 运行时异常
* *
* @author tjq * @author tjq
* @since 2020/5/26 * @since 2020/5/26
*/ */
@Setter
@Getter
public class PowerJobException extends RuntimeException { public class PowerJobException extends RuntimeException {
protected String code;
public PowerJobException() { public PowerJobException() {
} }

View File

@ -18,6 +18,7 @@ import org.apache.commons.lang3.exception.ExceptionUtils;
public class ResultDTO<T> implements PowerSerializable { public class ResultDTO<T> implements PowerSerializable {
private boolean success; private boolean success;
private String code;
private T data; private T data;
private String message; private String message;

View File

@ -21,4 +21,16 @@ public class LoginUserHolder {
public static void clean() { public static void clean() {
TL.remove(); TL.remove();
} }
/**
* 获取用户名
* @return 存在则返回常规用户名否则返回 unknown
*/
public static String getUserName() {
PowerJobUser powerJobUser = get();
if (powerJobUser != null) {
return powerJobUser.getUsername();
}
return "UNKNOWN";
}
} }

View File

@ -12,17 +12,12 @@ import tech.powerjob.common.exception.PowerJobException;
@Getter @Getter
public class PowerJobAuthException extends PowerJobException { public class PowerJobAuthException extends PowerJobException {
private final String code;
private final String msg;
public PowerJobAuthException(AuthErrorCode errorCode) { public PowerJobAuthException(AuthErrorCode errorCode) {
this.code = errorCode.getCode(); this(errorCode, null);
this.msg = errorCode.getMsg();
} }
public PowerJobAuthException(AuthErrorCode errorCode, String extraMsg) { public PowerJobAuthException(AuthErrorCode errorCode, String extraMsg) {
super(extraMsg == null ? errorCode.getMsg() : errorCode.getMsg().concat(":").concat(extraMsg));
this.code = errorCode.getCode(); this.code = errorCode.getCode();
this.msg = errorCode.getMsg().concat(":").concat(extraMsg);
} }
} }

View File

@ -2,10 +2,6 @@ package tech.powerjob.server.auth.interceptor;
import tech.powerjob.server.auth.Permission; import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.dp.DynamicPermission;
import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission;
import tech.powerjob.server.auth.interceptor.gp.EmptyGrantPermissionPlugin;
import tech.powerjob.server.auth.interceptor.gp.GrantPermissionPlugin;
import java.lang.annotation.ElementType; import java.lang.annotation.ElementType;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
@ -39,11 +35,11 @@ public @interface ApiPermission {
* 固定权限不支持的场景需要使用动态权限 * 固定权限不支持的场景需要使用动态权限
* @return 动态权限 * @return 动态权限
*/ */
Class<? extends DynamicPermission> dynamicPermissionPlugin() default EmptyDynamicPermission.class; Class<? extends DynamicPermissionPlugin> dynamicPermissionPlugin() default EmptyPlugin.class;
/** /**
* 新增场景需要授权插件执行授权 * 新增场景需要授权插件执行授权
* @return 授权插件 * @return 授权插件
*/ */
Class<? extends GrantPermissionPlugin> grandPermissionPlugin() default EmptyGrantPermissionPlugin.class; Class<? extends GrantPermissionPlugin> grandPermissionPlugin() default EmptyPlugin.class;
} }

View File

@ -9,7 +9,6 @@ import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import tech.powerjob.server.auth.interceptor.gp.GrantPermissionPlugin;
import java.lang.reflect.Method; import java.lang.reflect.Method;

View File

@ -1,4 +1,4 @@
package tech.powerjob.server.auth.interceptor.dp; package tech.powerjob.server.auth.interceptor;
import tech.powerjob.server.auth.Permission; import tech.powerjob.server.auth.Permission;
@ -10,6 +10,6 @@ import javax.servlet.http.HttpServletRequest;
* @author tjq * @author tjq
* @since 2023/9/3 * @since 2023/9/3
*/ */
public interface DynamicPermission { public interface DynamicPermissionPlugin {
Permission calculate(HttpServletRequest request, Object handler); Permission calculate(HttpServletRequest request, Object handler);
} }

View File

@ -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) {
}
}

View File

@ -1,4 +1,4 @@
package tech.powerjob.server.auth.interceptor.gp; package tech.powerjob.server.auth.interceptor;
import java.lang.reflect.Method; import java.lang.reflect.Method;

View File

@ -48,7 +48,7 @@ public class PowerJobAuthInterceptor implements HandlerInterceptor {
// 尝试直接解析登陆 // 尝试直接解析登陆
final Optional<PowerJobUser> loginUserOpt = powerJobLoginService.ifLogin(request); final Optional<PowerJobUser> loginUserOpt = powerJobLoginService.ifLogin(request);
// 未登录前先使用302重定向到登录页面 // 未登录前先使用302重定向到登录页面 TODO: 前端登录还是服务端直接跳转有待考虑
if (!loginUserOpt.isPresent()) { if (!loginUserOpt.isPresent()) {
response.setStatus(302); response.setStatus(302);
response.setHeader("location", request.getContextPath() + "/login"); response.setHeader("location", request.getContextPath() + "/login");

View File

@ -1,18 +0,0 @@
package tech.powerjob.server.auth.interceptor.dp;
import tech.powerjob.server.auth.Permission;
import javax.servlet.http.HttpServletRequest;
/**
* NotUseDynamicPermission
*
* @author tjq
* @since 2023/9/3
*/
public class EmptyDynamicPermission implements DynamicPermission {
@Override
public Permission calculate(HttpServletRequest request, Object handler) {
return null;
}
}

View File

@ -1,16 +0,0 @@
package tech.powerjob.server.auth.interceptor.gp;
import java.lang.reflect.Method;
/**
* do nothing
*
* @author tjq
* @since 2024/2/11
*/
public class EmptyGrantPermissionPlugin implements GrantPermissionPlugin {
@Override
public void grant(Object[] args, Object result, Method method, Object originBean) {
}
}

View File

@ -1,6 +1,8 @@
package tech.powerjob.server.auth.service.permission; package tech.powerjob.server.auth.service.permission;
import tech.powerjob.server.auth.PowerJobUser; import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.auth.interceptor.ApiPermission;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -22,4 +24,14 @@ public interface PowerJobPermissionService {
* @return true or false * @return true or false
*/ */
boolean hasPermission(HttpServletRequest request, Object handler, PowerJobUser user, ApiPermission apiPermission); boolean hasPermission(HttpServletRequest request, Object handler, PowerJobUser user, ApiPermission apiPermission);
/**
* 授予用户权限
* @param roleScope 权限范围
* @param target 权限目标
* @param userId 用户ID
* @param role 角色
* @param extra 其他
*/
void grantPermission(RoleScope roleScope, Long target, Long userId, Role role, String extra);
} }

View File

@ -13,8 +13,8 @@ import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.Role; import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.auth.interceptor.dp.DynamicPermission; import tech.powerjob.server.auth.interceptor.DynamicPermissionPlugin;
import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission; import tech.powerjob.server.auth.interceptor.EmptyPlugin;
import tech.powerjob.server.persistence.remote.model.AppInfoDO; import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import tech.powerjob.server.persistence.remote.model.UserRoleDO; import tech.powerjob.server.persistence.remote.model.UserRoleDO;
import tech.powerjob.server.persistence.remote.repository.AppInfoRepository; import tech.powerjob.server.persistence.remote.repository.AppInfoRepository;
@ -93,6 +93,23 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService
return false; return false;
} }
@Override
public void grantPermission(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] saveAndFlush userRole successfully: {}", userRoleDO);
}
private boolean checkAppPermission(HttpServletRequest request, Permission requiredPermission, Multimap<Long, Role> appId2Role, Multimap<Long, Role> namespaceId2Role) { private boolean checkAppPermission(HttpServletRequest request, Permission requiredPermission, Multimap<Long, Role> appId2Role, Multimap<Long, Role> namespaceId2Role) {
final String appIdStr = request.getHeader("appId"); final String appIdStr = request.getHeader("appId");
if (StringUtils.isEmpty(appIdStr)) { if (StringUtils.isEmpty(appIdStr)) {
@ -144,12 +161,12 @@ public class PowerJobPermissionServiceImpl implements PowerJobPermissionService
private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) { private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) {
Class<? extends DynamicPermission> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin(); Class<? extends DynamicPermissionPlugin> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin();
if (EmptyDynamicPermission.class.equals(dynamicPermissionPlugin)) { if (EmptyPlugin.class.equals(dynamicPermissionPlugin)) {
return apiPermission.requiredPermission(); return apiPermission.requiredPermission();
} }
try { try {
DynamicPermission dynamicPermission = dynamicPermissionPlugin.getDeclaredConstructor().newInstance(); DynamicPermissionPlugin dynamicPermission = dynamicPermissionPlugin.getDeclaredConstructor().newInstance();
return dynamicPermission.calculate(request, handler); return dynamicPermission.calculate(request, handler);
} catch (Throwable t) { } catch (Throwable t) {
log.error("[PowerJobAuthService] process dynamicPermissionPlugin failed!", t); log.error("[PowerJobAuthService] process dynamicPermissionPlugin failed!", t);

View File

@ -39,6 +39,12 @@ public class NamespaceDO {
private Integer status; private Integer status;
/**
* 部门组织架构相关属性
* 预留数据库字段方便基于组织架构二次开发
*/
private String dept;
/** /**
* 标签扩展性之王多值逗号分割 * 标签扩展性之王多值逗号分割
*/ */

View File

@ -1,6 +1,7 @@
package tech.powerjob.server.persistence.remote.repository; package tech.powerjob.server.persistence.remote.repository;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
import tech.powerjob.server.persistence.remote.model.NamespaceDO; import tech.powerjob.server.persistence.remote.model.NamespaceDO;
import java.util.Optional; import java.util.Optional;
@ -11,7 +12,7 @@ import java.util.Optional;
* @author tjq * @author tjq
* @since 2023/9/3 * @since 2023/9/3
*/ */
public interface NamespaceRepository extends JpaRepository<NamespaceDO, Long> { public interface NamespaceRepository extends JpaRepository<NamespaceDO, Long>, JpaSpecificationExecutor<NamespaceDO> {
Optional<NamespaceDO> findByCode(String code); Optional<NamespaceDO> findByCode(String code);
} }

View File

@ -0,0 +1,43 @@
package tech.powerjob.server.auth.dp;
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.Role;
import tech.powerjob.server.auth.interceptor.DynamicPermissionPlugin;
import tech.powerjob.server.web.request.GrantPermissionRequest;
import javax.servlet.http.HttpServletRequest;
/**
* 授权动态权限计算
* 授予权限要低于或等于授权人自身的权限
*
* @author tjq
* @since 2024/2/12
*/
@Slf4j
public class GrantDynamicPermission 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());
GrantPermissionRequest grantPermissionRequest = JsonUtils.parseObject(body, GrantPermissionRequest.class);
Role role = Role.of(grantPermissionRequest.getRole());
switch (role) {
case OBSERVER: return Permission.READ;
case QA: return Permission.OPS;
case DEVELOPER: return Permission.WRITE;
}
} catch (Exception e) {
log.error("[GrantDynamicPermission] check permission failed, please fix the bug!!!", e);
}
return Permission.SU;
}
}

View File

@ -1,9 +1,10 @@
package tech.powerjob.server.auth.interceptor.dp; package tech.powerjob.server.auth.dp;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
import tech.powerjob.common.serialize.JsonUtils; import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.server.auth.Permission; import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.interceptor.DynamicPermissionPlugin;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import java.util.Map; import java.util.Map;
@ -16,7 +17,7 @@ import java.util.Map;
* @since 2023/9/3 * @since 2023/9/3
*/ */
@Slf4j @Slf4j
public class ModifyOrCreateDynamicPermission implements DynamicPermission { public class ModifyOrCreateDynamicPermission implements DynamicPermissionPlugin {
@Override @Override
public Permission calculate(HttpServletRequest request, Object handler) { public Permission calculate(HttpServletRequest request, Object handler) {
@ -36,7 +37,7 @@ public class ModifyOrCreateDynamicPermission implements DynamicPermission {
return Permission.WRITE; return Permission.WRITE;
} catch (Exception e) { } catch (Exception e) {
log.error("[ModifyOrCreateDynamicPermission] check permission failed, please fix the bug!!!"); log.error("[ModifyOrCreateDynamicPermission] check permission failed, please fix the bug!!!", e);
} }
// 异常情况先放行不影响功能使用后续修复 BUG // 异常情况先放行不影响功能使用后续修复 BUG

View File

@ -1,4 +1,4 @@
package tech.powerjob.server.auth.interceptor.gp; package tech.powerjob.server.auth.gp;
import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.RoleScope;

View File

@ -1,4 +1,4 @@
package tech.powerjob.server.auth.interceptor.gp; package tech.powerjob.server.auth.gp;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.MapUtils; import org.apache.commons.collections4.MapUtils;
@ -8,6 +8,7 @@ import tech.powerjob.server.auth.LoginUserHolder;
import tech.powerjob.server.auth.PowerJobUser; import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.Role; import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.RoleScope;
import tech.powerjob.server.auth.interceptor.GrantPermissionPlugin;
import tech.powerjob.server.common.utils.SpringUtils; import tech.powerjob.server.common.utils.SpringUtils;
import tech.powerjob.server.persistence.remote.model.UserRoleDO; import tech.powerjob.server.persistence.remote.model.UserRoleDO;
import tech.powerjob.server.persistence.remote.repository.UserRoleRepository; import tech.powerjob.server.persistence.remote.repository.UserRoleRepository;

View File

@ -1,4 +1,4 @@
package tech.powerjob.server.auth.interceptor.gp; package tech.powerjob.server.auth.gp;
import tech.powerjob.server.auth.RoleScope; import tech.powerjob.server.auth.RoleScope;

View File

@ -1,7 +1,5 @@
package tech.powerjob.server.web; package tech.powerjob.server.web;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.common.response.ResultDTO;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils; import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.converter.HttpMessageNotReadableException; import org.springframework.http.converter.HttpMessageNotReadableException;
@ -10,6 +8,8 @@ import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseBody;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.common.response.ResultDTO;
/** /**
* 统一处理 web 层异常信息 * 统一处理 web 层异常信息
@ -25,8 +25,13 @@ public class ControllerExceptionHandler {
@ExceptionHandler(Exception.class) @ExceptionHandler(Exception.class)
public ResultDTO<Void> exceptionHandler(Exception e) { public ResultDTO<Void> exceptionHandler(Exception e) {
ResultDTO<Void> ret = ResultDTO.failed(ExceptionUtils.getMessage(e));
// 不是所有异常都需要打印完整堆栈后续可以定义内部的Exception便于判断 // 不是所有异常都需要打印完整堆栈后续可以定义内部的Exception便于判断
if (e instanceof IllegalArgumentException || e instanceof PowerJobException) { if (e instanceof PowerJobException) {
ret.setCode(((PowerJobException) e).getCode());
log.warn("[ControllerException] PowerJobException, message is {}.", ExceptionUtils.getMessage(e));
} else if (e instanceof IllegalArgumentException) {
log.warn("[ControllerException] http request failed, message is {}.", e.getMessage()); log.warn("[ControllerException] http request failed, message is {}.", e.getMessage());
} else if (e instanceof HttpMessageNotReadableException || e instanceof MethodArgumentTypeMismatchException) { } else if (e instanceof HttpMessageNotReadableException || e instanceof MethodArgumentTypeMismatchException) {
log.warn("[ControllerException] invalid http request params, exception is {}.", e.getMessage()); log.warn("[ControllerException] invalid http request params, exception is {}.", e.getMessage());
@ -35,6 +40,7 @@ public class ControllerExceptionHandler {
} else { } else {
log.error("[ControllerException] http request failed.", e); log.error("[ControllerException] http request failed.", e);
} }
return ResultDTO.failed(ExceptionUtils.getMessage(e));
return ret;
} }
} }

View File

@ -1,18 +1,26 @@
package tech.powerjob.server.web.controller; package tech.powerjob.server.web.controller;
import com.google.common.collect.Maps;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.response.ResultDTO; import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.PowerJobUser; import tech.powerjob.common.serialize.JsonUtils;
import tech.powerjob.server.auth.*;
import tech.powerjob.server.auth.common.AuthConstants; import tech.powerjob.server.auth.common.AuthConstants;
import tech.powerjob.server.auth.dp.GrantDynamicPermission;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.auth.login.LoginTypeInfo; import tech.powerjob.server.auth.login.LoginTypeInfo;
import tech.powerjob.server.auth.service.login.LoginRequest; import tech.powerjob.server.auth.service.login.LoginRequest;
import tech.powerjob.server.auth.service.login.PowerJobLoginService; import tech.powerjob.server.auth.service.login.PowerJobLoginService;
import tech.powerjob.server.auth.service.permission.PowerJobPermissionService;
import tech.powerjob.server.web.request.GrantPermissionRequest;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.Cookie; import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
/** /**
@ -27,6 +35,8 @@ public class AuthController {
@Resource @Resource
private PowerJobLoginService powerJobLoginService; private PowerJobLoginService powerJobLoginService;
@Resource
private PowerJobPermissionService powerJobPermissionService;
@GetMapping("/supportLoginTypes") @GetMapping("/supportLoginTypes")
public ResultDTO<List<LoginTypeInfo>> listSupportLoginTypes() { public ResultDTO<List<LoginTypeInfo>> listSupportLoginTypes() {
@ -90,4 +100,34 @@ public class AuthController {
private void fillJwt4LoginUser(PowerJobUser powerJobUser, HttpServletResponse httpServletResponse) { private void fillJwt4LoginUser(PowerJobUser powerJobUser, HttpServletResponse httpServletResponse) {
httpServletResponse.addCookie(new Cookie(AuthConstants.JWT_NAME, powerJobUser.getJwtToken())); httpServletResponse.addCookie(new Cookie(AuthConstants.JWT_NAME, powerJobUser.getJwtToken()));
} }
/* 授权相关 */
@PostMapping("/grantApp")
@ApiPermission(name = "Auth-GrantAppPermission", roleScope = RoleScope.APP, dynamicPermissionPlugin = GrantDynamicPermission.class)
public ResultDTO<Void> grantAppPermission(GrantPermissionRequest grantPermissionRequest) {
grantPermission(RoleScope.APP, grantPermissionRequest);
return ResultDTO.success(null);
}
@PostMapping("/grantNamespace")
@ApiPermission(name = "Auth-GrantNamespacePermission", roleScope = RoleScope.NAMESPACE, dynamicPermissionPlugin = GrantDynamicPermission.class)
public ResultDTO<Void> grantNamespacePermission(GrantPermissionRequest grantPermissionRequest) {
grantPermission(RoleScope.NAMESPACE, grantPermissionRequest);
return ResultDTO.success(null);
}
private void grantPermission(RoleScope roleScope, GrantPermissionRequest grantPermissionRequest) {
Role role = Role.of(grantPermissionRequest.getRole());
Optional.ofNullable(grantPermissionRequest.getUserIds()).orElse(Collections.emptyList()).forEach(uid -> {
// 记录授权人信息
Map<String, Object> extraInfo = Maps.newHashMap();
extraInfo.put("grantor", LoginUserHolder.getUserName());
String extra = JsonUtils.toJSONString(extraInfo);
powerJobPermissionService.grantPermission(roleScope, grantPermissionRequest.getTargetId(), uid, role, extra);
});
}
} }

View File

@ -1,25 +1,43 @@
package tech.powerjob.server.web.controller; package tech.powerjob.server.web.controller;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
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.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import tech.powerjob.common.response.ResultDTO; import tech.powerjob.common.response.ResultDTO;
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.RoleScope;
import tech.powerjob.server.auth.dp.ModifyOrCreateDynamicPermission;
import tech.powerjob.server.auth.gp.SaveNamespaceGrantPermissionPlugin;
import tech.powerjob.server.auth.interceptor.ApiPermission; import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.auth.interceptor.dp.ModifyOrCreateDynamicPermission;
import tech.powerjob.server.auth.interceptor.gp.SaveNamespaceGrantPermissionPlugin;
import tech.powerjob.server.common.constants.SwitchableStatus; import tech.powerjob.server.common.constants.SwitchableStatus;
import tech.powerjob.server.persistence.PageResult;
import tech.powerjob.server.persistence.QueryConvertUtils;
import tech.powerjob.server.persistence.remote.model.NamespaceDO; import tech.powerjob.server.persistence.remote.model.NamespaceDO;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.model.UserRoleDO;
import tech.powerjob.server.persistence.remote.repository.NamespaceRepository; import tech.powerjob.server.persistence.remote.repository.NamespaceRepository;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import tech.powerjob.server.persistence.remote.repository.UserRoleRepository;
import tech.powerjob.server.web.converter.NamespaceConverter; import tech.powerjob.server.web.converter.NamespaceConverter;
import tech.powerjob.server.web.converter.UserConverter;
import tech.powerjob.server.web.request.ModifyNamespaceRequest; import tech.powerjob.server.web.request.ModifyNamespaceRequest;
import tech.powerjob.server.web.request.QueryNamespaceRequest; import tech.powerjob.server.web.request.QueryNamespaceRequest;
import tech.powerjob.server.web.response.NamespaceBaseVO; import tech.powerjob.server.web.response.NamespaceBaseVO;
import tech.powerjob.server.web.response.NamespaceDetailVO;
import tech.powerjob.server.web.response.UserBaseVO;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.Date; import javax.persistence.criteria.Predicate;
import java.util.List; import java.util.*;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -35,6 +53,10 @@ public class NamespaceController {
@Resource @Resource
private NamespaceRepository namespaceRepository; private NamespaceRepository namespaceRepository;
@Resource
private UserInfoRepository userInfoRepository;
@Resource
private UserRoleRepository userRoleRepository;
@ResponseBody @ResponseBody
@PostMapping("/save") @PostMapping("/save")
@ -53,9 +75,11 @@ public class NamespaceController {
namespaceDO.setCode(req.getCode()); namespaceDO.setCode(req.getCode());
// 创建时生成 token // 创建时生成 token
namespaceDO.setToken(UUID.randomUUID().toString()); namespaceDO.setToken(UUID.randomUUID().toString());
namespaceDO.setCreator(LoginUserHolder.getUserName());
} else { } else {
namespaceDO = fetchById(id); namespaceDO = fetchById(id);
namespaceDO.setModifier(LoginUserHolder.getUserName());
} }
// 拷贝通用变更属性code 不允许更改 // 拷贝通用变更属性code 不允许更改
@ -69,9 +93,69 @@ public class NamespaceController {
} }
@PostMapping("/list") @PostMapping("/list")
public ResultDTO<List<NamespaceBaseVO>> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) { public ResultDTO<PageResult<NamespaceBaseVO>> listNamespace(@RequestBody QueryNamespaceRequest queryNamespaceRequest) {
List<NamespaceDO> allDos = namespaceRepository.findAll();
return ResultDTO.success(allDos.stream().map(NamespaceConverter::do2BaseVo).collect(Collectors.toList())); String codeLike = queryNamespaceRequest.getCodeLike();
String nameLike = queryNamespaceRequest.getNameLike();
String tagLike = queryNamespaceRequest.getTagLike();
Pageable pageable = PageRequest.of(queryNamespaceRequest.getIndex(), queryNamespaceRequest.getPageSize());
Specification<NamespaceDO> specification = (root, query, cb) -> {
List<Predicate> 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();
};
Page<NamespaceDO> namespacePageResult = namespaceRepository.findAll(specification, pageable);
PageResult<NamespaceBaseVO> ret = new PageResult<>(namespacePageResult);
ret.setData(namespacePageResult.get().map(NamespaceConverter::do2BaseVo).collect(Collectors.toList()));
return ResultDTO.success(ret);
}
@GetMapping("/detail")
@ApiPermission(name = "Namespace-GetDetail", roleScope = RoleScope.NAMESPACE, requiredPermission = Permission.READ)
public ResultDTO<NamespaceDetailVO> queryNamespaceDetail(Long id) {
NamespaceDO namespaceDO = fetchById(id);
NamespaceDetailVO namespaceDetailVO = new NamespaceDetailVO();
// 拷贝基础字段
NamespaceBaseVO namespaceBaseVO = NamespaceConverter.do2BaseVo(namespaceDO);
BeanUtils.copyProperties(namespaceBaseVO, namespaceDetailVO);
// 处理 token
namespaceDetailVO.setToken(namespaceDO.getToken());
// 处理权限视图
Map<String, List<UserBaseVO>> privilegedUsers = Maps.newHashMap();
namespaceDetailVO.setPrivilegedUsers(privilegedUsers);
List<UserRoleDO> permissionUserList = userRoleRepository.findAllByScopeAndTarget(RoleScope.NAMESPACE.getV(), namespaceDO.getId());
permissionUserList.forEach(r -> {
Role role = Role.of(r.getRole());
List<UserBaseVO> userBaseVOList = privilegedUsers.computeIfAbsent(role.name(), ignore -> Lists.newArrayList());
Optional<UserInfoDO> userInfoDoOpt = userInfoRepository.findById(r.getUserId());
userInfoDoOpt.ifPresent(userInfoDO -> userBaseVOList.add(UserConverter.do2BaseVo(userInfoDO)));
});
return ResultDTO.success(namespaceDetailVO);
} }
private NamespaceDO fetchById(Long id) { private NamespaceDO fetchById(Long id) {

View File

@ -1,18 +1,17 @@
package tech.powerjob.server.web.controller; package tech.powerjob.server.web.controller;
import tech.powerjob.common.response.ResultDTO; import com.google.common.collect.Lists;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils; 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.core.service.UserService;
import tech.powerjob.server.persistence.remote.model.UserInfoDO; import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository; import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import tech.powerjob.server.core.service.UserService; import tech.powerjob.server.web.converter.UserConverter;
import tech.powerjob.server.web.request.ModifyUserInfoRequest; import tech.powerjob.server.web.request.ModifyUserInfoRequest;
import com.google.common.collect.Lists; import tech.powerjob.server.web.response.UserBaseVO;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.util.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource; import javax.annotation.Resource;
import java.util.List; import java.util.List;
@ -27,7 +26,6 @@ import java.util.stream.Collectors;
@RestController @RestController
@RequestMapping("/user") @RequestMapping("/user")
public class UserInfoController { public class UserInfoController {
@Resource @Resource
private UserService userService; private UserService userService;
@Resource @Resource
@ -42,7 +40,7 @@ public class UserInfoController {
} }
@GetMapping("list") @GetMapping("list")
public ResultDTO<List<UserItemVO>> list(@RequestParam(required = false) String name) { public ResultDTO<List<UserBaseVO>> list(@RequestParam(required = false) String name) {
List<UserInfoDO> result; List<UserInfoDO> result;
if (StringUtils.isEmpty(name)) { if (StringUtils.isEmpty(name)) {
@ -53,18 +51,10 @@ public class UserInfoController {
return ResultDTO.success(convert(result)); return ResultDTO.success(convert(result));
} }
private static List<UserItemVO> convert(List<UserInfoDO> data) { private static List<UserBaseVO> convert(List<UserInfoDO> data) {
if (CollectionUtils.isEmpty(data)) { if (CollectionUtils.isEmpty(data)) {
return Lists.newLinkedList(); 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;
} }
} }

View File

@ -0,0 +1,22 @@
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());
return userBaseVO;
}
}

View File

@ -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<Long> userIds;
}

View File

@ -26,6 +26,8 @@ public class ModifyNamespaceRequest {
*/ */
private String name; private String name;
private String dept;
/** /**
* 标签扩展性之王多值逗号分割 * 标签扩展性之王多值逗号分割
*/ */

View File

@ -29,6 +29,7 @@ public class QueryAppInfoRequest {
*/ */
private Boolean showMyRelated; private Boolean showMyRelated;
/* ****************** 分页参数 ****************** */
/** /**
* 当前页码 * 当前页码
*/ */

View File

@ -14,10 +14,22 @@ public class QueryNamespaceRequest {
/** /**
* code 模糊查询 * code 模糊查询
*/ */
private String code; private String codeLike;
/** /**
* 名称模糊查询 * 名称模糊查询
*/ */
private String name; private String nameLike;
private String tagLike;
/* ****************** 分页参数 ****************** */
/**
* 当前页码
*/
private Integer index = 0;
/**
* 页大小
*/
private Integer pageSize = 10;
} }

View File

@ -30,6 +30,9 @@ public class NamespaceBaseVO implements Serializable {
*/ */
private String name; private String name;
private String dept;
private String tags;
private Integer status; private Integer status;
private String statusStr; private String statusStr;

View File

@ -0,0 +1,29 @@
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 2023/9/3
*/
@Getter
@Setter
@ToString(callSuper = true)
public class NamespaceDetailVO extends NamespaceBaseVO {
/**
* 访问 token
*/
private String token;
/**
* 有权限的用户
*/
private Map<String, List<UserBaseVO>> privilegedUsers;
}

View File

@ -0,0 +1,20 @@
package tech.powerjob.server.web.response;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
/**
* 用户基础信息
*
* @author tjq
* @since 2023/9/3
*/
@Getter
@Setter
@NoArgsConstructor
public class UserBaseVO {
private Long id;
private String username;
private String nick;
}