[Feature-12040][api][ui] Add authorization management of read and write permissions for project center (#12048)
[Feature-12040][api][ui] Add authorization management of read and write permissions for project center
This commit is contained in:
parent
4a13148bfc
commit
dc8d18cf87
|
|
@ -174,6 +174,43 @@ public class ProjectController extends BaseController {
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* query project with authorized level list paging
|
||||
*
|
||||
* @param userId user id
|
||||
* @param loginUser login user
|
||||
* @param searchVal search value
|
||||
* @param pageSize page size
|
||||
* @param pageNo page number
|
||||
* @return project list which with the login user's authorized level
|
||||
*/
|
||||
@Operation(summary = "queryProjectWithAuthorizedLevelListPaging", description = "QUERY_PROJECT_WITH_AUTH_LEVEL_LIST_PAGING_NOTES")
|
||||
@Parameters({
|
||||
@Parameter(name = "userId", description = "USER_ID", schema = @Schema(implementation = int.class, example = "100")),
|
||||
@Parameter(name = "searchVal", description = "SEARCH_VAL", schema = @Schema(implementation = String.class)),
|
||||
@Parameter(name = "pageSize", description = "PAGE_SIZE", required = true, schema = @Schema(implementation = int.class, example = "10")),
|
||||
@Parameter(name = "pageNo", description = "PAGE_NO", required = true, schema = @Schema(implementation = int.class, example = "1"))
|
||||
})
|
||||
@GetMapping(value = "/project-with-authorized-level-list-paging")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@ApiException(LOGIN_USER_QUERY_PROJECT_LIST_PAGING_ERROR)
|
||||
@AccessLogAnnotation(ignoreRequestArgs = "loginUser")
|
||||
public Result queryProjectWithAuthorizedLevelListPaging(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
|
||||
@RequestParam("userId") Integer userId,
|
||||
@RequestParam(value = "searchVal", required = false) String searchVal,
|
||||
@RequestParam("pageSize") Integer pageSize,
|
||||
@RequestParam("pageNo") Integer pageNo) {
|
||||
|
||||
Result result = checkPageParams(pageNo, pageSize);
|
||||
if (!result.checkResult()) {
|
||||
return result;
|
||||
}
|
||||
searchVal = ParameterUtils.handleEscapes(searchVal);
|
||||
result = projectService.queryProjectWithAuthorizedLevelListPaging(userId, loginUser, pageSize, pageNo,
|
||||
searchVal);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* delete project by code
|
||||
*
|
||||
|
|
@ -234,6 +271,26 @@ public class ProjectController extends BaseController {
|
|||
return projectService.queryAuthorizedProject(loginUser, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* query all project with authorized level
|
||||
*
|
||||
* @param loginUser login user
|
||||
* @param userId user id
|
||||
* @return All projects with users' authorized level for them
|
||||
*/
|
||||
@Operation(summary = "queryProjectWithAuthorizedLevel", description = "QUERY_PROJECT_AUTHORIZED_LEVEL")
|
||||
@Parameters({
|
||||
@Parameter(name = "userId", description = "USER_ID", schema = @Schema(implementation = int.class, example = "100"))
|
||||
})
|
||||
@GetMapping(value = "/project-with-authorized-level")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@ApiException(QUERY_AUTHORIZED_PROJECT)
|
||||
@AccessLogAnnotation(ignoreRequestArgs = "loginUser")
|
||||
public Result queryProjectWithAuthorizedLevel(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
|
||||
@RequestParam("userId") Integer userId) {
|
||||
return projectService.queryProjectWithAuthorizedLevel(loginUser, userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* query authorized user
|
||||
*
|
||||
|
|
|
|||
|
|
@ -214,6 +214,54 @@ public class UsersController extends BaseController {
|
|||
return returnDataList(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* revoke project By Id
|
||||
*
|
||||
* @param loginUser login user
|
||||
* @param userId user id
|
||||
* @param projectIds project id array
|
||||
* @return revoke result code
|
||||
*/
|
||||
@Operation(summary = "revokeProjectById", description = "REVOKE_PROJECT_NOTES")
|
||||
@Parameters({
|
||||
@Parameter(name = "userId", description = "USER_ID", required = true, schema = @Schema(implementation = int.class, example = "100")),
|
||||
@Parameter(name = "projectIds", description = "PROJECT_IDS", required = true, schema = @Schema(implementation = String.class))
|
||||
})
|
||||
@PostMapping(value = "/revoke-project-by-id")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@ApiException(REVOKE_PROJECT_ERROR)
|
||||
@AccessLogAnnotation
|
||||
public Result revokeProjectById(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
|
||||
@RequestParam(value = "userId") int userId,
|
||||
@RequestParam(value = "projectIds") String projectIds) {
|
||||
Map<String, Object> result = usersService.revokeProjectById(loginUser, userId, projectIds);
|
||||
return returnDataList(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* grant project with read permission
|
||||
*
|
||||
* @param loginUser login user
|
||||
* @param userId user id
|
||||
* @param projectIds project id array
|
||||
* @return grant result code
|
||||
*/
|
||||
@Operation(summary = "grantProjectWithReadPerm", description = "GRANT_PROJECT_WITH_READ_PERM_NOTES")
|
||||
@Parameters({
|
||||
@Parameter(name = "userId", description = "USER_ID", required = true, schema = @Schema(implementation = int.class, example = "100")),
|
||||
@Parameter(name = "projectIds", description = "PROJECT_IDS", required = true, schema = @Schema(implementation = String.class))
|
||||
})
|
||||
@PostMapping(value = "/grant-project-with-read-perm")
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@ApiException(GRANT_PROJECT_ERROR)
|
||||
@AccessLogAnnotation
|
||||
public Result grantProjectWithReadPerm(@Parameter(hidden = true) @RequestAttribute(value = Constants.SESSION_USER) User loginUser,
|
||||
@RequestParam(value = "userId") int userId,
|
||||
@RequestParam(value = "projectIds") String projectIds) {
|
||||
Map<String, Object> result = usersService.grantProjectWithReadPerm(loginUser, userId, projectIds);
|
||||
return returnDataList(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* grant project
|
||||
*
|
||||
|
|
|
|||
|
|
@ -280,6 +280,8 @@ public enum Status {
|
|||
|
||||
USER_NO_OPERATION_PERM(30001, "user has no operation privilege", "当前用户没有操作权限"),
|
||||
USER_NO_OPERATION_PROJECT_PERM(30002, "user {0} is not has project {1} permission", "当前用户[{0}]没有[{1}]项目的操作权限"),
|
||||
USER_NO_WRITE_PROJECT_PERM(30003, "user [{0}] does not have write permission for project [{1}]",
|
||||
"当前用户[{0}]没有[{1}]项目的写权限"),
|
||||
|
||||
PROCESS_INSTANCE_NOT_EXIST(50001, "process instance {0} does not exist", "工作流实例[{0}]不存在"),
|
||||
PROCESS_INSTANCE_EXIST(50002, "process instance {0} already exists", "工作流实例[{0}]已存在"),
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ public class ResourcePermissionCheckServiceImpl
|
|||
}
|
||||
List<Resource> ownResourceList = resourceMapper.queryResourceListAuthored(userId, -1);
|
||||
relationResources.addAll(ownResourceList);
|
||||
return ownResourceList.stream().map(Resource::getId).collect(toSet());
|
||||
return relationResources.stream().map(Resource::getId).collect(toSet());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -81,6 +81,10 @@ public interface ProjectService {
|
|||
*/
|
||||
boolean hasProjectAndPerm(User loginUser, Project project, Result result, String permission);
|
||||
|
||||
boolean hasProjectAndWritePerm(User loginUser, Project project, Result result);
|
||||
|
||||
boolean hasProjectAndWritePerm(User loginUser, Project project, Map<String, Object> result);
|
||||
|
||||
/**
|
||||
* admin can view all projects
|
||||
*
|
||||
|
|
@ -92,6 +96,19 @@ public interface ProjectService {
|
|||
*/
|
||||
Result queryProjectListPaging(User loginUser, Integer pageSize, Integer pageNo, String searchVal);
|
||||
|
||||
/**
|
||||
* admin can view all projects
|
||||
*
|
||||
* @param userId user id
|
||||
* @param loginUser login user
|
||||
* @param searchVal search value
|
||||
* @param pageSize page size
|
||||
* @param pageNo page number
|
||||
* @return project list which with the login user's authorized level
|
||||
*/
|
||||
Result queryProjectWithAuthorizedLevelListPaging(Integer userId, User loginUser, Integer pageSize, Integer pageNo,
|
||||
String searchVal);
|
||||
|
||||
/**
|
||||
* delete project by code
|
||||
*
|
||||
|
|
@ -131,6 +148,13 @@ public interface ProjectService {
|
|||
*/
|
||||
Result queryAuthorizedProject(User loginUser, Integer userId);
|
||||
|
||||
/**
|
||||
* query all project with authorized level
|
||||
* @param loginUser login user
|
||||
* @return project list
|
||||
*/
|
||||
Result queryProjectWithAuthorizedLevel(User loginUser, Integer userId);
|
||||
|
||||
/**
|
||||
* query authorized user
|
||||
*
|
||||
|
|
|
|||
|
|
@ -195,7 +195,9 @@ public interface ResourcesService {
|
|||
/**
|
||||
* updateProcessInstance resource
|
||||
*
|
||||
* @param resourceId resource id
|
||||
* @param loginUser login user
|
||||
* @param fullName full name
|
||||
* @param tenantCode tenantCode
|
||||
* @param content content
|
||||
* @return update result cod
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -153,6 +153,16 @@ public interface UsersService {
|
|||
*/
|
||||
Map<String, Object> grantProject(User loginUser, int userId, String projectIds);
|
||||
|
||||
/**
|
||||
* grant project with read permission
|
||||
*
|
||||
* @param loginUser login user
|
||||
* @param userId user id
|
||||
* @param projectIds project id array
|
||||
* @return grant result code
|
||||
*/
|
||||
Map<String, Object> grantProjectWithReadPerm(User loginUser, int userId, String projectIds);
|
||||
|
||||
/**
|
||||
* grant project by code
|
||||
*
|
||||
|
|
@ -163,6 +173,15 @@ public interface UsersService {
|
|||
*/
|
||||
Map<String, Object> grantProjectByCode(User loginUser, int userId, long projectCode);
|
||||
|
||||
/**
|
||||
* revoke the project permission for specified user by id
|
||||
* @param loginUser Login user
|
||||
* @param userId User id
|
||||
* @param projectIds project id array
|
||||
* @return
|
||||
*/
|
||||
Map<String, Object> revokeProjectById(User loginUser, int userId, String projectIds);
|
||||
|
||||
/**
|
||||
* revoke the project permission for specified user.
|
||||
* @param loginUser Login user
|
||||
|
|
|
|||
|
|
@ -261,10 +261,12 @@ public class ProcessDefinitionServiceImpl extends BaseServiceImpl implements Pro
|
|||
String otherParamsJson,
|
||||
ProcessExecutionTypeEnum executionType) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, WORKFLOW_CREATE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
if (checkDescriptionLength(description)) {
|
||||
|
|
@ -754,12 +756,14 @@ public class ProcessDefinitionServiceImpl extends BaseServiceImpl implements Pro
|
|||
String otherParamsJson,
|
||||
ProcessExecutionTypeEnum executionType) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, WORKFLOW_UPDATE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (checkDescriptionLength(description)) {
|
||||
logger.warn("Parameter description is too long.");
|
||||
putMsg(result, Status.DESCRIPTION_TOO_LONG_ERROR);
|
||||
|
|
@ -2404,12 +2408,14 @@ public class ProcessDefinitionServiceImpl extends BaseServiceImpl implements Pro
|
|||
public Map<String, Object> deleteProcessDefinitionVersion(User loginUser, long projectCode, long code,
|
||||
int version) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, VERSION_DELETE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
|
||||
ProcessDefinition processDefinition = processDefinitionMapper.queryByCode(code);
|
||||
|
||||
if (processDefinition == null || projectCode != processDefinition.getProjectCode()) {
|
||||
|
|
@ -2466,10 +2472,11 @@ public class ProcessDefinitionServiceImpl extends BaseServiceImpl implements Pro
|
|||
String scheduleJson,
|
||||
ProcessExecutionTypeEnum executionType) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, WORKFLOW_CREATE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
if (checkDescriptionLength(description)) {
|
||||
|
|
@ -2613,10 +2620,11 @@ public class ProcessDefinitionServiceImpl extends BaseServiceImpl implements Pro
|
|||
String otherParamsJson,
|
||||
ProcessExecutionTypeEnum executionType) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, WORKFLOW_UPDATE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
if (checkDescriptionLength(description)) {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ package org.apache.dolphinscheduler.api.service.impl;
|
|||
import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.PROJECT;
|
||||
import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.PROJECT_CREATE;
|
||||
import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.PROJECT_DELETE;
|
||||
import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.PROJECT_UPDATE;
|
||||
|
||||
import org.apache.dolphinscheduler.api.enums.Status;
|
||||
import org.apache.dolphinscheduler.api.exceptions.ServiceException;
|
||||
|
|
@ -256,6 +255,60 @@ public class ProjectServiceImpl extends BaseServiceImpl implements ProjectServic
|
|||
return checkResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasProjectAndWritePerm(User loginUser, Project project, Result result) {
|
||||
boolean checkResult = false;
|
||||
if (project == null) {
|
||||
logger.error("Project does not exist.");
|
||||
putMsg(result, Status.PROJECT_NOT_FOUND, "");
|
||||
} else {
|
||||
// case 1: user is admin
|
||||
if (loginUser.getUserType() == UserType.ADMIN_USER) {
|
||||
return true;
|
||||
}
|
||||
// case 2: user is project owner
|
||||
if (project.getUserId().equals(loginUser.getId())) {
|
||||
return true;
|
||||
}
|
||||
// case 3: check user permission level
|
||||
ProjectUser projectUser = projectUserMapper.queryProjectRelation(project.getId(), loginUser.getId());
|
||||
if (projectUser == null || projectUser.getPerm() != Constants.DEFAULT_ADMIN_PERMISSION) {
|
||||
putMsg(result, Status.USER_NO_WRITE_PROJECT_PERM, loginUser.getUserName(), project.getCode());
|
||||
checkResult = false;
|
||||
} else {
|
||||
checkResult = true;
|
||||
}
|
||||
}
|
||||
return checkResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasProjectAndWritePerm(User loginUser, Project project, Map<String, Object> result) {
|
||||
boolean checkResult = false;
|
||||
if (project == null) {
|
||||
logger.error("Project does not exist.");
|
||||
putMsg(result, Status.PROJECT_NOT_FOUND, "");
|
||||
} else {
|
||||
// case 1: user is admin
|
||||
if (loginUser.getUserType() == UserType.ADMIN_USER) {
|
||||
return true;
|
||||
}
|
||||
// case 2: user is project owner
|
||||
if (project.getUserId().equals(loginUser.getId())) {
|
||||
return true;
|
||||
}
|
||||
// case 3: check user permission level
|
||||
ProjectUser projectUser = projectUserMapper.queryProjectRelation(project.getId(), loginUser.getId());
|
||||
if (projectUser == null || projectUser.getPerm() != Constants.DEFAULT_ADMIN_PERMISSION) {
|
||||
putMsg(result, Status.USER_NO_WRITE_PROJECT_PERM, loginUser.getUserName(), project.getCode());
|
||||
checkResult = false;
|
||||
} else {
|
||||
checkResult = true;
|
||||
}
|
||||
}
|
||||
return checkResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasProjectAndPerm(User loginUser, Project project, Result result, String permission) {
|
||||
boolean checkResult = false;
|
||||
|
|
@ -310,6 +363,57 @@ public class ProjectServiceImpl extends BaseServiceImpl implements ProjectServic
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* admin can view all projects
|
||||
*
|
||||
* @param userId user id
|
||||
* @param loginUser login user
|
||||
* @param searchVal search value
|
||||
* @param pageSize page size
|
||||
* @param pageNo page number
|
||||
* @return project list which with the login user's authorized level
|
||||
*/
|
||||
@Override
|
||||
public Result queryProjectWithAuthorizedLevelListPaging(Integer userId, User loginUser, Integer pageSize,
|
||||
Integer pageNo, String searchVal) {
|
||||
Result result = new Result();
|
||||
PageInfo<Project> pageInfo = new PageInfo<>(pageNo, pageSize);
|
||||
Page<Project> page = new Page<>(pageNo, pageSize);
|
||||
Set<Integer> allProjectIds = resourcePermissionCheckService
|
||||
.userOwnedResourceIdsAcquisition(AuthorizationType.PROJECTS, loginUser.getId(), logger);
|
||||
Set<Integer> userProjectIds = resourcePermissionCheckService
|
||||
.userOwnedResourceIdsAcquisition(AuthorizationType.PROJECTS, userId, logger);
|
||||
if (allProjectIds.isEmpty()) {
|
||||
result.setData(pageInfo);
|
||||
putMsg(result, Status.SUCCESS);
|
||||
return result;
|
||||
}
|
||||
IPage<Project> projectIPage =
|
||||
projectMapper.queryProjectListPaging(page, new ArrayList<>(allProjectIds), searchVal);
|
||||
|
||||
List<Project> projectList = projectIPage.getRecords();
|
||||
|
||||
for (Project project : projectList) {
|
||||
if (userProjectIds.contains(project.getId())) {
|
||||
ProjectUser projectUser = projectUserMapper.queryProjectRelation(project.getId(), userId);
|
||||
if (projectUser == null) {
|
||||
// in this case, the user is the project owner, maybe it's better to set it to ALL_PERMISSION.
|
||||
project.setPerm(Constants.DEFAULT_ADMIN_PERMISSION);
|
||||
} else {
|
||||
project.setPerm(projectUser.getPerm());
|
||||
}
|
||||
} else {
|
||||
project.setPerm(0);
|
||||
}
|
||||
}
|
||||
|
||||
pageInfo.setTotal((int) projectIPage.getTotal());
|
||||
pageInfo.setTotalList(projectList);
|
||||
result.setData(pageInfo);
|
||||
putMsg(result, Status.SUCCESS);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* delete project by code
|
||||
*
|
||||
|
|
@ -322,6 +426,11 @@ public class ProjectServiceImpl extends BaseServiceImpl implements ProjectServic
|
|||
Result result = new Result();
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
|
||||
boolean hasProjectAndWritePerm = hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
|
||||
checkProjectAndAuth(result, loginUser, project, project == null ? 0L : project.getCode(), PROJECT_DELETE);
|
||||
if (result.getCode() != Status.SUCCESS.getCode()) {
|
||||
return result;
|
||||
|
|
@ -386,8 +495,8 @@ public class ProjectServiceImpl extends BaseServiceImpl implements ProjectServic
|
|||
}
|
||||
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
boolean hasProjectAndPerm = hasProjectAndPerm(loginUser, project, result, PROJECT_UPDATE);
|
||||
if (!hasProjectAndPerm) {
|
||||
boolean hasProjectAndWritePerm = hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
Project tempProject = projectMapper.queryByName(projectName);
|
||||
|
|
@ -417,6 +526,47 @@ public class ProjectServiceImpl extends BaseServiceImpl implements ProjectServic
|
|||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* query all project with authorized level
|
||||
* @param loginUser login user
|
||||
* @return project list
|
||||
*/
|
||||
@Override
|
||||
public Result queryProjectWithAuthorizedLevel(User loginUser, Integer userId) {
|
||||
Result result = new Result();
|
||||
|
||||
Set<Integer> projectIds = resourcePermissionCheckService
|
||||
.userOwnedResourceIdsAcquisition(AuthorizationType.PROJECTS, loginUser.getId(), logger);
|
||||
List<Project> projectList = projectMapper.listAuthorizedProjects(
|
||||
loginUser.getUserType().equals(UserType.ADMIN_USER) ? 0 : loginUser.getId(),
|
||||
new ArrayList<>(projectIds));
|
||||
|
||||
List<Project> unauthorizedProjectsList = new ArrayList<>();
|
||||
List<Project> authedProjectList = new ArrayList<>();
|
||||
Set<Project> projectSet;
|
||||
if (projectList != null && !projectList.isEmpty()) {
|
||||
projectSet = new HashSet<>(projectList);
|
||||
authedProjectList = projectMapper.queryAuthedProjectListByUserId(userId);
|
||||
unauthorizedProjectsList = getUnauthorizedProjects(projectSet, authedProjectList);
|
||||
}
|
||||
|
||||
for (int i = 0; i < authedProjectList.size(); i++) {
|
||||
authedProjectList.get(i).setPerm(7);
|
||||
}
|
||||
|
||||
for (int i = 0; i < unauthorizedProjectsList.size(); i++) {
|
||||
unauthorizedProjectsList.get(i).setPerm(0);
|
||||
}
|
||||
|
||||
List<Project> joined = new ArrayList<>();
|
||||
joined.addAll(authedProjectList);
|
||||
joined.addAll(unauthorizedProjectsList);
|
||||
|
||||
result.setData(joined);
|
||||
putMsg(result, Status.SUCCESS);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* query unauthorized project
|
||||
*
|
||||
|
|
|
|||
|
|
@ -143,10 +143,11 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe
|
|||
long projectCode,
|
||||
String taskDefinitionJson) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, TASK_DEFINITION_CREATE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -285,10 +286,11 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe
|
|||
String taskDefinitionJsonObj,
|
||||
String upstreamCodes) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, TASK_DEFINITION_CREATE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
ProcessDefinition processDefinition = processDefinitionMapper.queryByCode(processDefinitionCode);
|
||||
|
|
@ -426,10 +428,16 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe
|
|||
/**
|
||||
* Whether task definition can be deleted or not
|
||||
*/
|
||||
private void taskCanDeleteValid(User user, TaskDefinition taskDefinition) {
|
||||
private void taskCanDeleteValid(User user, TaskDefinition taskDefinition, User loginUser) {
|
||||
// check user access for project
|
||||
Project project = projectMapper.queryByCode(taskDefinition.getProjectCode());
|
||||
projectService.checkProjectAndAuthThrowException(user, project, TASK_DEFINITION_DELETE);
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
throw new ServiceException(Status.TASK_DEFINE_STATE_ONLINE, taskDefinition.getCode());
|
||||
}
|
||||
|
||||
// Whether task relation workflow is online
|
||||
if (processService.isTaskOnline(taskDefinition.getCode()) && taskDefinition.getFlag() == Flag.YES) {
|
||||
|
|
@ -466,7 +474,7 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe
|
|||
throw new ServiceException(Status.TASK_DEFINE_NOT_EXIST, taskCode);
|
||||
}
|
||||
|
||||
this.taskCanDeleteValid(loginUser, taskDefinition);
|
||||
this.taskCanDeleteValid(loginUser, taskDefinition, loginUser);
|
||||
int delete = taskDefinitionMapper.deleteByCode(taskCode);
|
||||
if (delete <= 0) {
|
||||
throw new ServiceException(Status.DELETE_TASK_DEFINE_BY_CODE_MSG_ERROR, taskDefinition.getCode());
|
||||
|
|
@ -683,11 +691,13 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe
|
|||
private TaskDefinitionLog updateTask(User loginUser, long projectCode, long taskCode, String taskDefinitionJsonObj,
|
||||
Map<String, Object> result) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
result.putAll(projectService.checkProjectAndAuth(loginUser, project, projectCode, TASK_DEFINITION_UPDATE));
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
|
||||
// check if user have write perm for project
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TaskDefinition taskDefinition = taskDefinitionMapper.queryByCode(taskCode);
|
||||
if (taskDefinition == null) {
|
||||
logger.error("Task definition does not exist, taskDefinitionCode:{}.", taskCode);
|
||||
|
|
@ -943,10 +953,11 @@ public class TaskDefinitionServiceImpl extends BaseServiceImpl implements TaskDe
|
|||
@Override
|
||||
public Map<String, Object> deleteByCodeAndVersion(User loginUser, long projectCode, long taskCode, int version) {
|
||||
Project project = projectMapper.queryByCode(projectCode);
|
||||
// check user access for project
|
||||
// check if user have write perm for project
|
||||
Map<String, Object> result =
|
||||
projectService.checkProjectAndAuth(loginUser, project, projectCode, TASK_DEFINITION_DELETE);
|
||||
if (result.get(Constants.STATUS) != Status.SUCCESS) {
|
||||
boolean hasProjectAndWritePerm = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
if (!hasProjectAndWritePerm) {
|
||||
return result;
|
||||
}
|
||||
TaskDefinition taskDefinition = taskDefinitionMapper.queryByCode(taskCode);
|
||||
|
|
|
|||
|
|
@ -533,6 +533,97 @@ public class UsersServiceImpl extends BaseServiceImpl implements UsersService {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* revoke the project permission for specified user by id
|
||||
* @param loginUser Login user
|
||||
* @param userId User id
|
||||
* @param projectIds project id array
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = RuntimeException.class)
|
||||
public Map<String, Object> revokeProjectById(User loginUser, int userId, String projectIds) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put(Constants.STATUS, false);
|
||||
|
||||
if (resourcePermissionCheckService.functionDisabled()) {
|
||||
putMsg(result, Status.FUNCTION_DISABLED);
|
||||
return result;
|
||||
}
|
||||
// 1. only admin can operate
|
||||
if (this.check(result, !this.isAdmin(loginUser), Status.USER_NO_OPERATION_PERM)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 2. check if user is existed
|
||||
User user = this.userMapper.selectById(userId);
|
||||
if (user == null) {
|
||||
this.putMsg(result, Status.USER_NOT_EXIST, userId);
|
||||
return result;
|
||||
}
|
||||
|
||||
Arrays.stream(projectIds.split(",")).distinct().forEach(projectId -> {
|
||||
// 3. check if project is existed
|
||||
Project project = this.projectMapper.queryDetailById(Integer.parseInt(projectId));
|
||||
if (project == null) {
|
||||
this.putMsg(result, Status.PROJECT_NOT_FOUND, Integer.parseInt(projectId));
|
||||
} else {
|
||||
// 4. delete the relationship between project and user
|
||||
this.projectUserMapper.deleteProjectRelation(project.getId(), user.getId());
|
||||
}
|
||||
});
|
||||
|
||||
this.putMsg(result, Status.SUCCESS);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* grant project with read permission
|
||||
*
|
||||
* @param loginUser login user
|
||||
* @param userId user id
|
||||
* @param projectIds project id array
|
||||
* @return grant result code
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = RuntimeException.class)
|
||||
public Map<String, Object> grantProjectWithReadPerm(User loginUser, int userId, String projectIds) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put(Constants.STATUS, false);
|
||||
|
||||
if (resourcePermissionCheckService.functionDisabled()) {
|
||||
putMsg(result, Status.FUNCTION_DISABLED);
|
||||
return result;
|
||||
}
|
||||
// check exist
|
||||
User tempUser = userMapper.selectById(userId);
|
||||
if (tempUser == null) {
|
||||
putMsg(result, Status.USER_NOT_EXIST, userId);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (check(result, StringUtils.isEmpty(projectIds), Status.SUCCESS)) {
|
||||
return result;
|
||||
}
|
||||
Arrays.stream(projectIds.split(Constants.COMMA)).distinct().forEach(projectId -> {
|
||||
ProjectUser projectUserOld = projectUserMapper.queryProjectRelation(Integer.parseInt(projectId), userId);
|
||||
if (projectUserOld != null) {
|
||||
projectUserMapper.deleteProjectRelation(Integer.parseInt(projectId), userId);
|
||||
}
|
||||
Date now = new Date();
|
||||
ProjectUser projectUser = new ProjectUser();
|
||||
projectUser.setUserId(userId);
|
||||
projectUser.setProjectId(Integer.parseInt(projectId));
|
||||
projectUser.setPerm(Constants.READ_PERMISSION);
|
||||
projectUser.setCreateTime(now);
|
||||
projectUser.setUpdateTime(now);
|
||||
projectUserMapper.insert(projectUser);
|
||||
});
|
||||
putMsg(result, Status.SUCCESS);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* grant project
|
||||
*
|
||||
|
|
@ -559,13 +650,15 @@ public class UsersServiceImpl extends BaseServiceImpl implements UsersService {
|
|||
return result;
|
||||
}
|
||||
|
||||
projectUserMapper.deleteProjectRelation(0, userId);
|
||||
|
||||
if (check(result, StringUtils.isEmpty(projectIds), Status.SUCCESS)) {
|
||||
logger.warn("Parameter projectIds is empty.");
|
||||
return result;
|
||||
}
|
||||
Arrays.stream(projectIds.split(",")).distinct().forEach(projectId -> {
|
||||
ProjectUser projectUserOld = projectUserMapper.queryProjectRelation(Integer.parseInt(projectId), userId);
|
||||
if (projectUserOld != null) {
|
||||
projectUserMapper.deleteProjectRelation(Integer.parseInt(projectId), userId);
|
||||
}
|
||||
Date now = new Date();
|
||||
ProjectUser projectUser = new ProjectUser();
|
||||
projectUser.setUserId(userId);
|
||||
|
|
|
|||
|
|
@ -779,6 +779,7 @@ public class ProcessDefinitionServiceTest extends BaseServiceTestTool {
|
|||
Mockito.when(projectMapper.queryByCode(projectCode)).thenReturn(getProject(projectCode));
|
||||
Mockito.when(projectService.checkProjectAndAuth(user, project, projectCode, WORKFLOW_UPDATE))
|
||||
.thenReturn(result);
|
||||
Mockito.when(projectService.hasProjectAndWritePerm(user, project, result)).thenReturn(true);
|
||||
|
||||
try {
|
||||
processDefinitionService.updateProcessDefinition(user, projectCode, "test", 1,
|
||||
|
|
|
|||
|
|
@ -22,11 +22,14 @@ import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationCon
|
|||
import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.PROJECT_CREATE;
|
||||
import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.PROJECT_DELETE;
|
||||
import static org.apache.dolphinscheduler.api.constants.ApiFuncIdentificationConstant.PROJECT_UPDATE;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
|
||||
import org.apache.dolphinscheduler.api.enums.Status;
|
||||
import org.apache.dolphinscheduler.api.permission.ResourcePermissionCheckService;
|
||||
import org.apache.dolphinscheduler.api.service.impl.BaseServiceImpl;
|
||||
import org.apache.dolphinscheduler.api.service.impl.ProjectServiceImpl;
|
||||
import org.apache.dolphinscheduler.api.utils.PageInfo;
|
||||
import org.apache.dolphinscheduler.api.utils.Result;
|
||||
import org.apache.dolphinscheduler.common.constants.Constants;
|
||||
import org.apache.dolphinscheduler.common.enums.AuthorizationType;
|
||||
|
|
@ -62,6 +65,9 @@ import org.mockito.quality.Strictness;
|
|||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
|
||||
/**
|
||||
* project service test
|
||||
**/
|
||||
|
|
@ -206,6 +212,66 @@ public class ProjectServiceTest {
|
|||
Assertions.assertTrue(checkResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryProjectWithAuthorizedLevelListPaging() {
|
||||
IPage<Project> page = new Page<>(1, 10);
|
||||
page.setTotal(1L);
|
||||
page.setRecords(getList());
|
||||
|
||||
User loginUser = getLoginUser();
|
||||
Integer pageSize = 10;
|
||||
Integer pageNo = 1;
|
||||
String searchVal = "testVal";
|
||||
Result result = new Result();
|
||||
Mockito.when(projectMapper.queryProjectListPaging(any(Page.class), Mockito.anyList(), eq(searchVal)))
|
||||
.thenReturn(page);
|
||||
|
||||
Set<Integer> allProjectIds = new HashSet();
|
||||
allProjectIds.add(1);
|
||||
Mockito.when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.PROJECTS,
|
||||
loginUser.getId(), projectLogger)).thenReturn(allProjectIds);
|
||||
|
||||
// SUCCESS
|
||||
result = projectService.queryProjectWithAuthorizedLevelListPaging(loginUser.getId(), loginUser, pageSize,
|
||||
pageNo, searchVal);
|
||||
logger.info(result.toString());
|
||||
PageInfo<Project> pageInfo = (PageInfo<Project>) result.getData();
|
||||
Assertions.assertTrue(CollectionUtils.isNotEmpty(pageInfo.getTotalList()));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHasProjectAndWritePerm() {
|
||||
|
||||
// Mockito.when(projectUserMapper.queryProjectRelation(1, 1)).thenReturn(getProjectUser());
|
||||
User loginUser = getLoginUser();
|
||||
Project project = getProject();
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
// not exist user
|
||||
User tempUser = new User();
|
||||
tempUser.setId(Integer.MAX_VALUE);
|
||||
tempUser.setUserType(UserType.GENERAL_USER);
|
||||
Mockito.when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.PROJECTS,
|
||||
new Object[]{project.getId()},
|
||||
tempUser.getId(), null, baseServiceLogger)).thenReturn(true);
|
||||
boolean checkResult = projectService.hasProjectAndWritePerm(tempUser, project, result);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertFalse(checkResult);
|
||||
|
||||
// success
|
||||
result = new HashMap<>();
|
||||
project.setUserId(1);
|
||||
loginUser.setUserType(UserType.ADMIN_USER);
|
||||
Mockito.when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.PROJECTS,
|
||||
new Object[]{project.getId()},
|
||||
loginUser.getId(), null, baseServiceLogger)).thenReturn(true);
|
||||
Mockito.when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.PROJECTS,
|
||||
new Object[]{project.getId()},
|
||||
0, baseServiceLogger)).thenReturn(true);
|
||||
checkResult = projectService.hasProjectAndWritePerm(loginUser, project, result);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertTrue(checkResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteProject() {
|
||||
User loginUser = getLoginUser();
|
||||
|
|
@ -213,10 +279,10 @@ public class ProjectServiceTest {
|
|||
Mockito.when(resourcePermissionCheckService.operationPermissionCheck(AuthorizationType.PROJECTS,
|
||||
new Object[]{1}, loginUser.getId(),
|
||||
PROJECT_DELETE, baseServiceLogger)).thenReturn(true);
|
||||
// PROJECT_NOT_FOUNT
|
||||
// PROJECT_NOT_FOUND
|
||||
Result result = projectService.deleteProject(loginUser, 11L);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertTrue(Status.PROJECT_NOT_EXIST.getCode() == result.getCode());
|
||||
Assertions.assertTrue(Status.PROJECT_NOT_FOUND.getCode() == result.getCode());
|
||||
loginUser.setId(2);
|
||||
// USER_NO_OPERATION_PROJECT_PERM
|
||||
Mockito.when(resourcePermissionCheckService.resourcePermissionCheck(AuthorizationType.PROJECTS, new Object[]{1},
|
||||
|
|
@ -224,7 +290,7 @@ public class ProjectServiceTest {
|
|||
baseServiceLogger)).thenReturn(true);
|
||||
result = projectService.deleteProject(loginUser, 1L);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertTrue(Status.USER_NO_OPERATION_PROJECT_PERM.getCode() == result.getCode());
|
||||
Assertions.assertTrue(Status.USER_NO_WRITE_PROJECT_PERM.getCode() == result.getCode());
|
||||
|
||||
// DELETE_PROJECT_ERROR_DEFINES_NOT_NULL
|
||||
Mockito.when(processDefinitionMapper.queryAllDefinitionList(1L)).thenReturn(getProcessDefinitions());
|
||||
|
|
@ -418,6 +484,40 @@ public class ProjectServiceTest {
|
|||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryProjectWithAuthorizedLevel() {
|
||||
Set<Integer> set = new HashSet();
|
||||
set.add(1);
|
||||
// test admin user
|
||||
User loginUser = new User();
|
||||
loginUser.setUserType(UserType.ADMIN_USER);
|
||||
loginUser.setId(1);
|
||||
List<Integer> list = new ArrayList<>(1);
|
||||
list.add(1);
|
||||
Mockito.when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.PROJECTS,
|
||||
loginUser.getId(), projectLogger)).thenReturn(set);
|
||||
Mockito.when(projectMapper.listAuthorizedProjects(
|
||||
loginUser.getUserType().equals(UserType.ADMIN_USER) ? 0 : loginUser.getId(), list))
|
||||
.thenReturn(getList());
|
||||
Result result = projectService.queryProjectWithAuthorizedLevel(loginUser, 2);
|
||||
logger.info(result.toString());
|
||||
List<Project> projects = (List<Project>) result.getData();
|
||||
Assertions.assertTrue(CollectionUtils.isNotEmpty(projects));
|
||||
|
||||
// test non-admin user
|
||||
loginUser.setId(2);
|
||||
loginUser.setUserType(UserType.GENERAL_USER);
|
||||
Mockito.when(resourcePermissionCheckService.userOwnedResourceIdsAcquisition(AuthorizationType.PROJECTS,
|
||||
loginUser.getId(), projectLogger)).thenReturn(set);
|
||||
Mockito.when(projectMapper.listAuthorizedProjects(
|
||||
loginUser.getUserType().equals(UserType.ADMIN_USER) ? 0 : loginUser.getId(), list))
|
||||
.thenReturn(getList());
|
||||
result = projectService.queryProjectWithAuthorizedLevel(loginUser, 3);
|
||||
logger.info(result.toString());
|
||||
projects = (List<Project>) result.getData();
|
||||
Assertions.assertTrue(CollectionUtils.isNotEmpty(projects));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testQueryUnauthorizedProject() {
|
||||
Set<Integer> set = new HashSet();
|
||||
|
|
|
|||
|
|
@ -141,10 +141,6 @@ public class TaskDefinitionServiceImplTest {
|
|||
+ "\\\\\\\"failedNode\\\\\\\":[\\\\\\\"\\\\\\\"]}\\\",\\\"dependence\\\":{}}\",\"flag\":0,\"taskPriority\":0,"
|
||||
+ "\"workerGroup\":\"default\",\"failRetryTimes\":0,\"failRetryInterval\":0,\"timeoutFlag\":0,"
|
||||
+ "\"timeoutNotifyStrategy\":0,\"timeout\":0,\"delayTime\":0,\"resourceIds\":\"\"}]";
|
||||
List<TaskDefinitionLog> taskDefinitions = JSONUtils.toList(createTaskDefinitionJson, TaskDefinitionLog.class);
|
||||
Mockito.when(processService.saveTaskDefine(user, PROJECT_CODE, taskDefinitions, Boolean.TRUE))
|
||||
.thenReturn(1);
|
||||
Mockito.when(taskPluginManager.checkTaskParameters(Mockito.any())).thenReturn(true);
|
||||
Map<String, Object> relation = taskDefinitionService
|
||||
.createTaskDefinition(user, PROJECT_CODE, createTaskDefinitionJson);
|
||||
Assertions.assertEquals(Status.SUCCESS, relation.get(Constants.STATUS));
|
||||
|
|
@ -166,8 +162,7 @@ public class TaskDefinitionServiceImplTest {
|
|||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
putMsg(result, Status.SUCCESS, PROJECT_CODE);
|
||||
Mockito.when(projectService.checkProjectAndAuth(user, project, PROJECT_CODE, TASK_DEFINITION_UPDATE))
|
||||
.thenReturn(result);
|
||||
Mockito.when(projectService.hasProjectAndWritePerm(user, project, new HashMap<>())).thenReturn(true);
|
||||
|
||||
Mockito.when(processService.isTaskOnline(TASK_CODE)).thenReturn(Boolean.FALSE);
|
||||
Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(new TaskDefinition());
|
||||
|
|
@ -212,6 +207,7 @@ public class TaskDefinitionServiceImplTest {
|
|||
// error delete single task definition object
|
||||
Mockito.when(taskDefinitionMapper.queryByCode(TASK_CODE)).thenReturn(getTaskDefinition());
|
||||
Mockito.when(taskDefinitionMapper.deleteByCode(TASK_CODE)).thenReturn(0);
|
||||
Mockito.when(projectService.hasProjectAndWritePerm(user, project, new HashMap<>())).thenReturn(true);
|
||||
exception = Assertions.assertThrows(ServiceException.class,
|
||||
() -> taskDefinitionService.deleteTaskDefinitionByCode(user, TASK_CODE));
|
||||
Assertions.assertEquals(Status.DELETE_TASK_DEFINE_BY_CODE_MSG_ERROR.getCode(),
|
||||
|
|
|
|||
|
|
@ -369,6 +369,27 @@ public class UsersServiceTest {
|
|||
Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrantProjectWithReadPerm() {
|
||||
String projectIds = "100000,120000";
|
||||
User loginUser = new User();
|
||||
int userId = 3;
|
||||
|
||||
// user not exist
|
||||
loginUser.setId(1);
|
||||
loginUser.setUserType(UserType.ADMIN_USER);
|
||||
when(userMapper.selectById(userId)).thenReturn(null);
|
||||
Map<String, Object> result = usersService.grantProjectWithReadPerm(loginUser, userId, projectIds);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertEquals(Status.USER_NOT_EXIST, result.get(Constants.STATUS));
|
||||
|
||||
// SUCCESS
|
||||
when(userMapper.selectById(userId)).thenReturn(getUser());
|
||||
result = usersService.grantProjectWithReadPerm(loginUser, userId, projectIds);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrantProjectByCode() {
|
||||
// Mock Project, User
|
||||
|
|
@ -440,6 +461,31 @@ public class UsersServiceTest {
|
|||
Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRevokeProjectById() {
|
||||
Mockito.when(this.userMapper.selectById(1)).thenReturn(this.getUser());
|
||||
|
||||
String projectId = "100000";
|
||||
|
||||
// user no permission
|
||||
User loginUser = new User();
|
||||
Map<String, Object> result = this.usersService.revokeProjectById(loginUser, 1, projectId);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertEquals(Status.USER_NO_OPERATION_PERM, result.get(Constants.STATUS));
|
||||
|
||||
// user not exist
|
||||
loginUser.setUserType(UserType.ADMIN_USER);
|
||||
result = this.usersService.revokeProjectById(loginUser, 2, projectId);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertEquals(Status.USER_NOT_EXIST, result.get(Constants.STATUS));
|
||||
|
||||
// success
|
||||
Mockito.when(this.projectMapper.queryByCode(Mockito.anyLong())).thenReturn(new Project());
|
||||
result = this.usersService.revokeProjectById(loginUser, 1, projectId);
|
||||
logger.info(result.toString());
|
||||
Assertions.assertEquals(Status.SUCCESS, result.get(Constants.STATUS));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGrantResources() {
|
||||
String resourceIds = "100000,120000";
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
|||
public interface ProjectUserMapper extends BaseMapper<ProjectUser> {
|
||||
|
||||
/**
|
||||
* delte prject user relation
|
||||
* delete project user relation
|
||||
*
|
||||
* @param projectId projectId
|
||||
* @param userId userId
|
||||
|
|
|
|||
|
|
@ -95,6 +95,9 @@ export const COLUMN_WIDTH_CONFIG = {
|
|||
tag: {
|
||||
width: 160
|
||||
},
|
||||
checkbox:{
|
||||
width: 20
|
||||
},
|
||||
copy: {
|
||||
width: 50
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ export default {
|
|||
delete: 'Delete',
|
||||
confirm: 'Confirm',
|
||||
cancel: 'Cancel',
|
||||
delete_confirm: 'Delete?'
|
||||
delete_confirm: 'Delete?',
|
||||
authorize_level:'Authorize Level',
|
||||
no_permission: 'No Permission',
|
||||
read_permission: 'Read Permission',
|
||||
all_permission: 'All Permission',
|
||||
},
|
||||
workflow: {
|
||||
on_line: 'Online',
|
||||
|
|
|
|||
|
|
@ -150,6 +150,9 @@ export default {
|
|||
datasource: 'Datasource',
|
||||
udf: 'UDF Function',
|
||||
namespace: 'Namespace',
|
||||
revoke_auth: 'Revoke',
|
||||
grant_read: 'Grant Read',
|
||||
grant_all:'Grant All',
|
||||
authorize_project: 'Project Authorize',
|
||||
authorize_resource: 'Resource Authorize',
|
||||
authorize_namespace: 'Namespace Authorize',
|
||||
|
|
|
|||
|
|
@ -36,7 +36,11 @@ export default {
|
|||
delete: '删除',
|
||||
confirm: '确定',
|
||||
cancel: '取消',
|
||||
delete_confirm: '确定删除吗?'
|
||||
delete_confirm: '确定删除吗?',
|
||||
authorize_level:'权限等级',
|
||||
no_permission: '无权限',
|
||||
read_permission: '读权限',
|
||||
all_permission: '所有权限',
|
||||
},
|
||||
workflow: {
|
||||
on_line: '线上',
|
||||
|
|
|
|||
|
|
@ -148,6 +148,9 @@ export default {
|
|||
datasource: '数据源',
|
||||
udf: 'UDF函数',
|
||||
namespace: '命名空间',
|
||||
revoke_auth: '撤销权限',
|
||||
grant_read: '授予读权限',
|
||||
grant_all:'授予所有权限',
|
||||
authorize_project: '项目授权',
|
||||
authorize_resource: '资源授权',
|
||||
authorize_namespace: '命名空间授权',
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
*/
|
||||
|
||||
import { axios } from '@/service/service'
|
||||
import { ListReq, ProjectsReq, UserIdReq, UpdateProjectsReq } from './types'
|
||||
import { ListReq, ListIdReq, ProjectsReq, UserIdReq, UpdateProjectsReq } from './types'
|
||||
|
||||
export function queryProjectListPaging(params: ListReq): any {
|
||||
return axios({
|
||||
|
|
@ -26,6 +26,14 @@ export function queryProjectListPaging(params: ListReq): any {
|
|||
})
|
||||
}
|
||||
|
||||
export function queryProjectWithAuthorizedLevelListPaging(params: ListIdReq): any {
|
||||
return axios({
|
||||
url: '/projects/project-with-authorized-level-list-paging',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function createProject(data: ProjectsReq): any {
|
||||
return axios({
|
||||
url: '/projects',
|
||||
|
|
@ -64,6 +72,14 @@ export function queryUnauthorizedProject(params: UserIdReq): any {
|
|||
})
|
||||
}
|
||||
|
||||
export function queryProjectWithAuthorizedLevel(params: UserIdReq): any {
|
||||
return axios({
|
||||
url: '/projects/project-with-authorized-level',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export function queryProjectByCode(code: number): any {
|
||||
return axios({
|
||||
url: `/projects/${code}`,
|
||||
|
|
|
|||
|
|
@ -21,6 +21,13 @@ interface ListReq {
|
|||
searchVal?: string
|
||||
}
|
||||
|
||||
interface ListIdReq {
|
||||
userId?: number
|
||||
pageNo: number
|
||||
pageSize: number
|
||||
searchVal?: string
|
||||
}
|
||||
|
||||
interface ProjectsReq {
|
||||
description?: string
|
||||
projectName: string
|
||||
|
|
@ -59,6 +66,7 @@ interface ProjectRes {
|
|||
|
||||
export {
|
||||
ListReq,
|
||||
ListIdReq,
|
||||
ProjectsReq,
|
||||
UserIdReq,
|
||||
UpdateProjectsReq,
|
||||
|
|
|
|||
|
|
@ -97,6 +97,14 @@ export function grantResource(data: GrantResourceReq) {
|
|||
})
|
||||
}
|
||||
|
||||
export function revokeProjectById(data: GrantProject) {
|
||||
return axios({
|
||||
url: '/users/revoke-project-by-id',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function grantProject(data: GrantProject) {
|
||||
return axios({
|
||||
url: '/users/grant-project',
|
||||
|
|
@ -105,6 +113,14 @@ export function grantProject(data: GrantProject) {
|
|||
})
|
||||
}
|
||||
|
||||
export function grantProjectWithReadPerm(data: GrantProject) {
|
||||
return axios({
|
||||
url: 'users/grant-project-with-read-perm',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export function grantProjectByCode(data: ProjectCodeReq & UserIdReq): any {
|
||||
return axios({
|
||||
url: '/users/grant-project-by-code',
|
||||
|
|
|
|||
|
|
@ -15,19 +15,26 @@
|
|||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { defineComponent, PropType, toRefs, watch } from 'vue'
|
||||
import { defineComponent, PropType, toRefs, watch} from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
NInput,
|
||||
NButton,
|
||||
NIcon,
|
||||
NTransfer,
|
||||
NSpace,
|
||||
NRadioGroup,
|
||||
NRadioButton,
|
||||
NTreeSelect
|
||||
NTreeSelect,
|
||||
NDataTable,
|
||||
NPagination
|
||||
} from 'naive-ui'
|
||||
import { useAuthorize } from './use-authorize'
|
||||
import Modal from '@/components/modal'
|
||||
import styles from '../index.module.scss'
|
||||
import type { TAuthType } from '../types'
|
||||
import { useColumns } from './use-columns'
|
||||
import { SearchOutlined } from '@vicons/antd'
|
||||
|
||||
const props = {
|
||||
show: {
|
||||
|
|
@ -43,14 +50,13 @@ const props = {
|
|||
default: 'auth_project'
|
||||
}
|
||||
}
|
||||
|
||||
export const AuthorizeModal = defineComponent({
|
||||
name: 'authorize-project-modal',
|
||||
props,
|
||||
emits: ['cancel'],
|
||||
setup(props, ctx) {
|
||||
const { t } = useI18n()
|
||||
const { state, onInit, onSave } = useAuthorize()
|
||||
const { state, onInit, onSave, getProjects, revokeProjectByIdRequest, grantProjectRequest, grantProjectWithReadPermRequest, requestData, handleChangePageSize } = useAuthorize()
|
||||
const onCancel = () => {
|
||||
ctx.emit('cancel')
|
||||
}
|
||||
|
|
@ -59,6 +65,20 @@ export const AuthorizeModal = defineComponent({
|
|||
if (result) onCancel()
|
||||
}
|
||||
|
||||
const onRevokeProject = () => {
|
||||
revokeProjectByIdRequest(props.userId, state.projectIds)
|
||||
}
|
||||
const onGrantReadPerm = () => {
|
||||
grantProjectWithReadPermRequest(props.userId, state.projectIds)
|
||||
}
|
||||
const onGrantAllPerm = () => {
|
||||
grantProjectRequest(props.userId, state.projectIds)
|
||||
}
|
||||
|
||||
const { columnsRef } = useColumns()
|
||||
const handleCheck = (rowKeys: Array<number>) => {
|
||||
state.projectIds = rowKeys.join()
|
||||
}
|
||||
watch(
|
||||
() => props.show,
|
||||
() => {
|
||||
|
|
@ -70,14 +90,23 @@ export const AuthorizeModal = defineComponent({
|
|||
|
||||
return {
|
||||
t,
|
||||
columnsRef,
|
||||
rowKey: (row: any) => row.id,
|
||||
...toRefs(state),
|
||||
onCancel,
|
||||
onConfirm
|
||||
onConfirm,
|
||||
getProjects,
|
||||
handleCheck,
|
||||
requestData,
|
||||
handleChangePageSize,
|
||||
onRevokeProject,
|
||||
onGrantReadPerm,
|
||||
onGrantAllPerm
|
||||
}
|
||||
},
|
||||
render(props: { type: TAuthType }) {
|
||||
render(props: { type: TAuthType, userId: number }) {
|
||||
const { t } = this
|
||||
const { type } = props
|
||||
const { type, userId } = props
|
||||
return (
|
||||
<Modal
|
||||
show={this.show}
|
||||
|
|
@ -89,13 +118,54 @@ export const AuthorizeModal = defineComponent({
|
|||
cancelClassName='btn-cancel'
|
||||
>
|
||||
{type === 'authorize_project' && (
|
||||
<NTransfer
|
||||
|
||||
<NSpace vertical>
|
||||
<NSpace>
|
||||
<NButton size='small' type='primary' onClick={this.onRevokeProject}>
|
||||
{t('security.user.revoke_auth')}
|
||||
</NButton>
|
||||
<NButton size='small' type='primary' onClick={this.onGrantReadPerm}>
|
||||
{t('security.user.grant_read')}
|
||||
</NButton>
|
||||
<NButton size='small' type='primary' onClick={this.onGrantAllPerm}>
|
||||
{t('security.user.grant_all')}
|
||||
</NButton>
|
||||
<NInput
|
||||
size='small'
|
||||
placeholder={t('project.list.project_tips')}
|
||||
clearable
|
||||
v-model:value={this.searchVal}
|
||||
/>
|
||||
{/* <NButton size='small' type='primary' onClick={this.handleSearch}> */}
|
||||
<NButton size='small' type='primary' onClick={() => this.getProjects(userId)}>
|
||||
<NIcon>
|
||||
<SearchOutlined />
|
||||
</NIcon>
|
||||
</NButton>
|
||||
</NSpace>
|
||||
<NDataTable
|
||||
virtualScroll
|
||||
options={this.unauthorizedProjects}
|
||||
filterable
|
||||
v-model={[this.authorizedProjects, 'value']}
|
||||
class={styles.transfer}
|
||||
row-class-name='items'
|
||||
columns={this.columnsRef.columns}
|
||||
data={this.projectWithAuthorizedLevel}
|
||||
loading={this.loading}
|
||||
max-height="250"
|
||||
row-key={this.rowKey}
|
||||
on-update:checked-row-keys={this.handleCheck}
|
||||
/>
|
||||
<div class={styles.pagination}>
|
||||
<NPagination
|
||||
v-model:page={this.pagination.page}
|
||||
v-model:page-size={this.pagination.pageSize}
|
||||
page-count={this.pagination.totalPage}
|
||||
show-size-picker
|
||||
page-sizes={[5, 10]}
|
||||
show-quick-jumper
|
||||
onUpdatePage={this.requestData}
|
||||
onUpdatePageSize={this.handleChangePageSize}
|
||||
/>
|
||||
</div>
|
||||
</NSpace>
|
||||
)}
|
||||
{type === 'authorize_datasource' && (
|
||||
<NTransfer
|
||||
|
|
|
|||
|
|
@ -16,8 +16,7 @@
|
|||
*/
|
||||
import { reactive } from 'vue'
|
||||
import {
|
||||
queryAuthorizedProject,
|
||||
queryUnauthorizedProject
|
||||
queryProjectWithAuthorizedLevelListPaging
|
||||
} from '@/service/modules/projects'
|
||||
import {
|
||||
authedDatasource,
|
||||
|
|
@ -36,17 +35,22 @@ import {
|
|||
import {
|
||||
grantProject,
|
||||
grantResource,
|
||||
grantProjectWithReadPerm,
|
||||
grantDataSource,
|
||||
grantUDFFunc,
|
||||
grantNamespaceFunc
|
||||
grantNamespaceFunc,
|
||||
revokeProjectById,
|
||||
} from '@/service/modules/users'
|
||||
import utils from '@/utils'
|
||||
import type { TAuthType, IResourceOption, IOption } from '../types'
|
||||
import type { TAuthType, IResourceOption, IOption, IRecord } from '../types'
|
||||
|
||||
export function useAuthorize() {
|
||||
const state = reactive({
|
||||
saving: false,
|
||||
loading: false,
|
||||
projectIds: '',
|
||||
currentRecord: {} as IRecord | null,
|
||||
projectWithAuthorizedLevel: [],
|
||||
authorizedProjects: [] as number[],
|
||||
unauthorizedProjects: [] as IOption[],
|
||||
authorizedDatasources: [] as number[],
|
||||
|
|
@ -59,26 +63,69 @@ export function useAuthorize() {
|
|||
fileResources: [] as IResourceOption[],
|
||||
udfResources: [] as IResourceOption[],
|
||||
authorizedFileResources: [] as number[],
|
||||
authorizedUdfResources: [] as number[]
|
||||
authorizedUdfResources: [] as number[],
|
||||
pagination: {
|
||||
pageSize: 5,
|
||||
page: 1,
|
||||
totalPage: 0
|
||||
},
|
||||
searchVal: '',
|
||||
userId: 0
|
||||
})
|
||||
|
||||
const getProjects = async (userId: number) => {
|
||||
if (state.loading) return
|
||||
state.loading = true
|
||||
const projects = await Promise.all([
|
||||
queryAuthorizedProject({ userId }),
|
||||
queryUnauthorizedProject({ userId })
|
||||
])
|
||||
if (userId) {
|
||||
state.userId = userId
|
||||
}
|
||||
|
||||
const projectsList = await queryProjectWithAuthorizedLevelListPaging({
|
||||
userId,
|
||||
searchVal: state.searchVal,
|
||||
pageSize: state.pagination.pageSize,
|
||||
pageNo: state.pagination.page
|
||||
})
|
||||
state.loading = false
|
||||
state.authorizedProjects = projects[0].map(
|
||||
(item: { name: string; id: number }) => item.id
|
||||
)
|
||||
state.unauthorizedProjects = [...projects[0], ...projects[1]].map(
|
||||
(item: { name: string; id: number }) => ({
|
||||
label: item.name,
|
||||
value: item.id
|
||||
})
|
||||
)
|
||||
if (!projectsList) throw Error()
|
||||
state.pagination.totalPage = projectsList.totalPage
|
||||
state.projectWithAuthorizedLevel = projectsList.totalList
|
||||
return state.projectWithAuthorizedLevel
|
||||
}
|
||||
|
||||
const requestData = async (page: number) => {
|
||||
state.pagination.page = page
|
||||
await getProjects(state.userId)
|
||||
}
|
||||
|
||||
const handleChangePageSize = async (pageSize: number) => {
|
||||
state.pagination.page = 1
|
||||
state.pagination.pageSize = pageSize
|
||||
await getProjects(state.userId)
|
||||
}
|
||||
|
||||
const revokeProjectByIdRequest = async (userId: number, projectIds: string) => {
|
||||
await revokeProjectById({
|
||||
userId,
|
||||
projectIds: projectIds
|
||||
})
|
||||
await getProjects(userId)
|
||||
}
|
||||
|
||||
const grantProjectRequest = async (userId: number, projectIds: string) => {
|
||||
await grantProject({
|
||||
userId,
|
||||
projectIds: projectIds
|
||||
})
|
||||
await getProjects(userId)
|
||||
}
|
||||
|
||||
const grantProjectWithReadPermRequest = async (userId: number, projectIds: string) => {
|
||||
await grantProjectWithReadPerm({
|
||||
userId,
|
||||
projectIds: projectIds
|
||||
})
|
||||
await getProjects(userId)
|
||||
}
|
||||
|
||||
const getDatasources = async (userId: number) => {
|
||||
|
|
@ -216,12 +263,6 @@ export function useAuthorize() {
|
|||
const onSave = async (type: TAuthType, userId: number) => {
|
||||
if (state.saving) return false
|
||||
state.saving = true
|
||||
if (type === 'authorize_project') {
|
||||
await grantProject({
|
||||
userId,
|
||||
projectIds: state.authorizedProjects.join(',')
|
||||
})
|
||||
}
|
||||
if (type === 'authorize_datasource') {
|
||||
await grantDataSource({
|
||||
userId,
|
||||
|
|
@ -281,5 +322,5 @@ export function useAuthorize() {
|
|||
return true
|
||||
}
|
||||
|
||||
return { state, onInit, onSave }
|
||||
return { state, onInit, onSave, getProjects, revokeProjectByIdRequest, grantProjectRequest, grantProjectWithReadPermRequest, requestData, handleChangePageSize }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
||||
* contributor license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright ownership.
|
||||
* The ASF licenses this file to You under the Apache License, Version 2.0
|
||||
* (the "License"); you may not use this file except in compliance with
|
||||
* the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import {
|
||||
COLUMN_WIDTH_CONFIG,
|
||||
calculateTableWidth,
|
||||
DefaultTableWidth
|
||||
} from '@/common/column-width-config'
|
||||
import type { TableColumns } from '../types'
|
||||
|
||||
// const { t } = useI18n()
|
||||
const PERM_LIST = [
|
||||
{
|
||||
label: 'project.list.no_permission',
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
label: 'project.list.read_permission',
|
||||
value: 2
|
||||
},
|
||||
{
|
||||
label: 'project.list.all_permission',
|
||||
value: 7
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
export function useColumns() {
|
||||
const { t } = useI18n()
|
||||
|
||||
const columnsRef = ref({
|
||||
columns: [] as TableColumns,
|
||||
tableWidth: DefaultTableWidth
|
||||
})
|
||||
|
||||
const createColumns = () => {
|
||||
const columns: any = [
|
||||
{
|
||||
type: 'selection',
|
||||
key: 'selection',
|
||||
...COLUMN_WIDTH_CONFIG['checkbox']
|
||||
},
|
||||
{
|
||||
title: t('project.list.project_name'),
|
||||
key: 'name',
|
||||
...COLUMN_WIDTH_CONFIG['size']
|
||||
},
|
||||
{
|
||||
title: t('project.list.authorize_level'),
|
||||
key: 'perm',
|
||||
render: (record: any):any => {
|
||||
return PERM_LIST.filter(item => item.value == record.perm).map(item => t(item.label))
|
||||
},
|
||||
...COLUMN_WIDTH_CONFIG['index']
|
||||
}
|
||||
]
|
||||
columnsRef.value = {
|
||||
columns,
|
||||
tableWidth: calculateTableWidth(columns)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
createColumns()
|
||||
})
|
||||
|
||||
watch(useI18n().locale, () => {
|
||||
createColumns()
|
||||
})
|
||||
|
||||
return {
|
||||
columnsRef,
|
||||
createColumns
|
||||
}
|
||||
}
|
||||
|
|
@ -18,3 +18,8 @@
|
|||
.transfer {
|
||||
width: 100%;
|
||||
}
|
||||
.pagination {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue