feat: add namespace to manage app

This commit is contained in:
tjq 2023-09-04 00:20:06 +08:00
parent 70e3afec1a
commit 0da830a6bc
24 changed files with 566 additions and 42 deletions

View File

@ -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 字符串

View File

@ -0,0 +1,22 @@
package tech.powerjob.server.auth;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 权限范围
*
* @author tjq
* @since 2023/9/3
*/
@Getter
@AllArgsConstructor
public enum RoleScope {
NAMESPACE(1),
APP(10)
;
private final int v;
}

View File

@ -1,6 +1,8 @@
package tech.powerjob.server.auth.anno;
package tech.powerjob.server.auth.interceptor;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.interceptor.dp.DynamicPermission;
import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
@ -26,5 +28,11 @@ public @interface ApiPermission {
* 需要的权限
* @return 权限
*/
Permission requiredPermission();
Permission requiredPermission() default Permission.GLOBAL_SU;
/**
* 固定权限不支持的场景需要使用动态权限
* @return 动态权限
*/
Class<? extends DynamicPermission> dynamicPermissionPlugin() default EmptyDynamicPermission.class;
}

View File

@ -9,7 +9,6 @@ import tech.powerjob.common.Loggers;
import tech.powerjob.common.exception.PowerJobException;
import tech.powerjob.server.auth.LoginUserHolder;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.anno.ApiPermission;
import tech.powerjob.server.auth.service.PowerJobAuthService;
import javax.annotation.Resource;
@ -59,7 +58,7 @@ public class PowerJobAuthInterceptor implements HandlerInterceptor {
// 写入上下文
LoginUserHolder.set(powerJobUser);
final boolean hasPermission = powerJobAuthService.hasPermission(request, powerJobUser, apiPermissionAnno);
final boolean hasPermission = powerJobAuthService.hasPermission(request, handler, powerJobUser, apiPermissionAnno);
if (hasPermission) {
return true;
}

View File

@ -0,0 +1,15 @@
package tech.powerjob.server.auth.interceptor.dp;
import tech.powerjob.server.auth.Permission;
import javax.servlet.http.HttpServletRequest;
/**
* 动态权限
*
* @author tjq
* @since 2023/9/3
*/
public interface DynamicPermission {
Permission calculate(HttpServletRequest request, Object handler);
}

View File

@ -0,0 +1,18 @@
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

@ -0,0 +1,19 @@
package tech.powerjob.server.auth.interceptor.dp;
import tech.powerjob.server.auth.Permission;
import javax.servlet.http.HttpServletRequest;
/**
* 创建不需要权限修改需要校验权限
*
* @author tjq
* @since 2023/9/3
*/
public class ModifyOrCreateDynamicPermission implements DynamicPermission {
@Override
public Permission calculate(HttpServletRequest request, Object handler) {
// TODO: 动态权限判断新建不需要权限
return Permission.WRITE;
}
}

View File

@ -2,7 +2,7 @@ package tech.powerjob.server.auth.service;
import tech.powerjob.server.auth.LoginContext;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.anno.ApiPermission;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
@ -43,9 +43,10 @@ public interface PowerJobAuthService {
/**
* 判断用户是否有访问权限
* @param request 上下文请求
* @param handler hander
* @param user 用户
* @param apiPermission 权限描述
* @return true or false
*/
boolean hasPermission(HttpServletRequest request, PowerJobUser user, ApiPermission apiPermission);
boolean hasPermission(HttpServletRequest request, Object handler, PowerJobUser user, ApiPermission apiPermission);
}

View File

@ -4,21 +4,25 @@ 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.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tech.powerjob.common.Loggers;
import tech.powerjob.server.auth.LoginContext;
import tech.powerjob.server.auth.Permission;
import tech.powerjob.server.auth.PowerJobUser;
import tech.powerjob.server.auth.Role;
import tech.powerjob.server.auth.anno.ApiPermission;
import tech.powerjob.common.exception.ImpossibleException;
import tech.powerjob.server.auth.*;
import tech.powerjob.server.auth.interceptor.ApiPermission;
import tech.powerjob.server.auth.interceptor.dp.DynamicPermission;
import tech.powerjob.server.auth.interceptor.dp.EmptyDynamicPermission;
import tech.powerjob.server.auth.jwt.JwtService;
import tech.powerjob.server.auth.login.BizLoginService;
import tech.powerjob.server.auth.login.BizUser;
import tech.powerjob.server.persistence.remote.model.AppInfoDO;
import tech.powerjob.server.persistence.remote.model.UserInfoDO;
import tech.powerjob.server.persistence.remote.model.UserRoleDO;
import tech.powerjob.server.persistence.remote.repository.AppInfoRepository;
import tech.powerjob.server.persistence.remote.repository.UserInfoRepository;
import tech.powerjob.server.persistence.remote.repository.UserRoleRepository;
@ -32,10 +36,12 @@ import java.util.*;
* @author tjq
* @since 2023/3/21
*/
@Slf4j
@Service
public class PowerJobAuthServiceImpl implements PowerJobAuthService {
private final JwtService jwtService;
private final AppInfoRepository appInfoRepository;
private final UserInfoRepository userInfoRepository;
private final UserRoleRepository userRoleRepository;
private final Map<String, BizLoginService> type2LoginService = Maps.newHashMap();
@ -45,8 +51,9 @@ public class PowerJobAuthServiceImpl implements PowerJobAuthService {
private static final String KEY_USERNAME = "userName";
@Autowired
public PowerJobAuthServiceImpl(List<BizLoginService> loginServices, JwtService jwtService, UserInfoRepository userInfoRepository, UserRoleRepository userRoleRepository) {
public PowerJobAuthServiceImpl(List<BizLoginService> loginServices, JwtService jwtService, AppInfoRepository appInfoRepository, UserInfoRepository userInfoRepository, UserRoleRepository userRoleRepository) {
this.jwtService = jwtService;
this.appInfoRepository = appInfoRepository;
this.userInfoRepository = userInfoRepository;
this.userRoleRepository = userRoleRepository;
loginServices.forEach(k -> type2LoginService.put(k.type(), k));
@ -109,33 +116,52 @@ public class PowerJobAuthServiceImpl implements PowerJobAuthService {
}
@Override
public boolean hasPermission(HttpServletRequest request, PowerJobUser user, ApiPermission apiPermission) {
public boolean hasPermission(HttpServletRequest request, Object handler, PowerJobUser user, ApiPermission apiPermission) {
final List<UserRoleDO> userRoleList = Optional.ofNullable(userRoleRepository.findAllByUserId(user.getId())).orElse(Collections.emptyList());
Multimap<String, Role> appId2Role = ArrayListMultimap.create();
Multimap<Long, Role> appId2Role = ArrayListMultimap.create();
Multimap<Long, Role> namespaceId2Role = ArrayListMultimap.create();
for (UserRoleDO userRole : userRoleList) {
if (userRole.getRole().equalsIgnoreCase(String.valueOf(Role.GOD.getV()))) {
if (Objects.equals(Role.GOD.getV(), userRole.getRole())) {
return true;
}
// 除了上帝角色其他任何角色都是 roleId_appId 的形式
final String[] split = userRole.getRole().split("_");
final Role role = Role.of(Integer.parseInt(split[0]));
appId2Role.put(split[1], role);
final Role role = Role.of(userRole.getRole());
if (Objects.equals(userRole.getScope(), RoleScope.NAMESPACE.getV())) {
namespaceId2Role.put(userRole.getTarget(), role);
}
if (Objects.equals(userRole.getScope(), RoleScope.APP.getV())) {
appId2Role.put(userRole.getTarget(), role);
}
}
// 无超级管理员权限校验普通权限
final String appId = request.getHeader("appId");
if (StringUtils.isEmpty(appId)) {
throw new IllegalArgumentException("can't find appId in header, please login again!");
final String appIdStr = request.getHeader("appId");
if (StringUtils.isEmpty(appIdStr)) {
throw new IllegalArgumentException("can't find appId in header, please refresh and try again!");
}
final Permission requiredPermission = apiPermission.requiredPermission();
Long appId = Long.valueOf(appIdStr);
final Collection<Role> roleCollection = appId2Role.get(appId);
for (Role role : roleCollection) {
final Permission requiredPermission = parsePermission(request, handler, apiPermission);
final Collection<Role> appRoles = appId2Role.get(appId);
for (Role role : appRoles) {
if (role.getPermissions().contains(requiredPermission)) {
return true;
}
}
// 校验 namespace 穿透权限
Optional<AppInfoDO> appInfoOpt = appInfoRepository.findById(appId);
if (!appInfoOpt.isPresent()) {
throw new IllegalArgumentException("can't find appInfo by appId in permission check: " + appId);
}
Long namespaceId = Optional.ofNullable(appInfoOpt.get().getNamespaceId()).orElse(-1L);
Collection<Role> namespaceRoles = namespaceId2Role.get(namespaceId);
for (Role role : namespaceRoles) {
if (role.getPermissions().contains(requiredPermission)) {
return true;
}
@ -184,4 +210,19 @@ public class PowerJobAuthServiceImpl implements PowerJobAuthService {
powerJobUser.setJwtToken(jwtService.build(jwtMap, null));
}
private static Permission parsePermission(HttpServletRequest request, Object handler, ApiPermission apiPermission) {
Class<? extends DynamicPermission> dynamicPermissionPlugin = apiPermission.dynamicPermissionPlugin();
if (EmptyDynamicPermission.class.equals(dynamicPermissionPlugin)) {
return apiPermission.requiredPermission();
}
try {
DynamicPermission 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();
}
}

View File

@ -13,7 +13,7 @@ import lombok.Getter;
@AllArgsConstructor
public enum SwitchableStatus {
/**
*
* 启用
*/
ENABLE(1),
DISABLE(2),

View File

@ -22,6 +22,11 @@ public class AppInfoDO {
@GenericGenerator(name = "native", strategy = "native")
private Long id;
/**
* 命名空间ID外键关联
*/
private Long namespaceId;
private String appName;
/**
* 应用分组密码

View File

@ -0,0 +1,54 @@
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 extra;
private Date gmtCreate;
private Date gmtModified;
private String creator;
private String modifier;
}

View File

@ -28,10 +28,20 @@ public class UserRoleDO {
* 授予角色的用户ID
*/
private Long userId;
/**
* 角色${role}_${appId}比如 Observer_277
* 权限范围namespace 还是 app
*/
private String role;
private Integer scope;
/**
* scope 一起组成授权目标比如某个 app 某个 namespace
*/
private Long target;
/**
* 角色比如 Observer
*/
private Integer role;
/**
* 扩展字段
*/

View File

@ -0,0 +1,13 @@
package tech.powerjob.server.persistence.remote.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import tech.powerjob.server.persistence.remote.model.NamespaceDO;
/**
* 命名空间
*
* @author tjq
* @since 2023/9/3
*/
public interface NamespaceRepository extends JpaRepository<NamespaceDO, Long> {
}

View File

@ -14,4 +14,6 @@ import java.util.List;
public interface UserRoleRepository extends JpaRepository<UserRoleDO, Long> {
List<UserRoleDO> findAllByUserId(Long userId);
List<UserRoleDO> findAllByScopeAndTarget(Integer scope, Long target);
}

View File

@ -0,0 +1,126 @@
package tech.powerjob.server.web.controller;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.powerjob.common.response.ResultDTO;
import tech.powerjob.server.auth.Permission;
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.dp.ModifyOrCreateDynamicPermission;
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.UserInfoRepository;
import tech.powerjob.server.persistence.remote.repository.UserRoleRepository;
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.response.NamespaceBaseVO;
import tech.powerjob.server.web.response.NamespaceDetailVO;
import tech.powerjob.server.web.response.UserBaseVO;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
/**
* 命名空间 Controller
*
* @author tjq
* @since 2023/9/3
*/
@Slf4j
@RestController
@RequestMapping("/namespace")
public class NamespaceController {
@Resource
private NamespaceRepository namespaceRepository;
@Resource
private UserInfoRepository userInfoRepository;
@Resource
private UserRoleRepository userRoleRepository;
@PostMapping("/save")
@ApiPermission(name = "Namespace-Save", dynamicPermissionPlugin = ModifyOrCreateDynamicPermission.class)
public ResultDTO<Void> save(ModifyNamespaceRequest req) {
req.valid();
Long id = req.getId();
NamespaceDO namespaceDO;
if (id == null) {
namespaceDO = new NamespaceDO();
namespaceDO.setGmtCreate(new Date());
// code 单独拷贝
namespaceDO.setCode(req.getCode());
// 创建时生成 token
namespaceDO.setToken(UUID.randomUUID().toString());
} else {
namespaceDO = fetchById(id);
}
// 拷贝通用变更属性code 不允许更改
namespaceDO.setName(req.getName());
namespaceDO.setExtra(req.getExtra());
namespaceDO.setStatus(req.getStatus());
namespaceDO.setGmtModified(new Date());
namespaceRepository.save(namespaceDO);
return ResultDTO.success(null);
}
@GetMapping("/listAll")
public ResultDTO<List<NamespaceBaseVO>> listAllNamespace() {
List<NamespaceDO> allDos = namespaceRepository.findAll();
return ResultDTO.success(allDos.stream().map(NamespaceConverter::do2BaseVo).collect(Collectors.toList()));
}
@GetMapping("/detail")
@ApiPermission(name = "Namespace-DetailInfo", 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) {
Optional<NamespaceDO> namespaceDoOpt = namespaceRepository.findById(id);
if (!namespaceDoOpt.isPresent()) {
throw new IllegalArgumentException("can't find namespace by id: " + id);
}
return namespaceDoOpt.get();
}
}

View File

@ -1,9 +1,6 @@
package tech.powerjob.server.web.controller;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;
@ -12,7 +9,9 @@ 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.repository.UserInfoRepository;
import tech.powerjob.server.web.converter.UserConverter;
import tech.powerjob.server.web.request.ModifyUserInfoRequest;
import tech.powerjob.server.web.response.UserBaseVO;
import javax.annotation.Resource;
import java.util.List;
@ -41,7 +40,7 @@ public class UserInfoController {
}
@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;
if (StringUtils.isEmpty(name)) {
@ -52,18 +51,10 @@ public class UserInfoController {
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)) {
return Lists.newLinkedList();
}
return data.stream().map(x -> new UserItemVO(x.getId(), x.getUsername())).collect(Collectors.toList());
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
public static final class UserItemVO {
private Long id;
private String username;
return data.stream().map(UserConverter::do2BaseVo).collect(Collectors.toList());
}
}

View File

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

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

@ -15,6 +15,7 @@ import org.apache.commons.lang3.StringUtils;
public class ModifyAppInfoRequest {
private Long id;
private Long namespaceId;
private String oldPassword;
private String appName;
private String password;

View File

@ -0,0 +1,42 @@
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 Integer status;
/**
* 扩展字段
*/
private String extra;
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!");
}
}
}

View File

@ -0,0 +1,52 @@
package tech.powerjob.server.web.response;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import java.io.Serializable;
import java.util.Date;
/**
* 基础版本的命名空间
*
* @author tjq
* @since 2023/9/3
*/
@Getter
@Setter
@ToString
public class NamespaceBaseVO implements Serializable {
private Long id;
/**
* 空间唯一标识
*/
private String code;
/**
* 空间名称比如中文描述XX部门XX空间
*/
private String name;
private Integer status;
private String statusStr;
/**
* 扩展字段
*/
private String extra;
private Date gmtCreate;
private String gmtCreateStr;
private Date gmtModified;
private String gmtModifiedStr;
private String creator;
private String modifier;
}

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;
}