diff --git a/README-zh.md b/README-zh.md index 26dcb9a..4593099 100644 --- a/README-zh.md +++ b/README-zh.md @@ -191,13 +191,16 @@ repo gitolite-admin RW+ = gcs repo testing R = @all -include "gitolite.d/*.conf" +include "gitolite.d/user/*.conf" +include "gitolite.d/repository/*.conf" @all_public_repo = repo @all_public_repo R = @all ``` -7. 你需要保证 `/home/gcs/gitolite-admin/conf/gitolite.d` 目录存在。该目录用来管理用户对私有仓库的权限。 +7. 你需要保证 `/home/gcs/gitolite-admin/conf/gitolite.d/user` 与 +`home/gcs/gitolite-admin/conf/gitolite.d/repository`目录存在。第一个目录用来管理私有仓库的权限, +第二个目录用于实现合作者功能。 ### 修改 `sudo` 配置 你需要保证你运行 `Java` 程序的用户能够在执行 `sudo -u rm` diff --git a/database/constraint/all_column_constraint.sql b/database/constraint/all_column_constraint.sql index 5f6789a..7b5c049 100644 --- a/database/constraint/all_column_constraint.sql +++ b/database/constraint/all_column_constraint.sql @@ -13,3 +13,7 @@ ADD CONSTRAINT pk_user_star_repository PRIMARY KEY (id); -- The constraint of the primary key and unique key is added to the table. ALTER TABLE ONLY public.t_ssh_key ADD CONSTRAINT pk_ssh_key PRIMARY KEY (id); + +-- The constraint of t_user_collaborate_repository is added to the table. +ALTER TABLE ONLY public.t_user_collaborate_repository +ADD CONSTRAINT pk_user_collaborate_repository PRIMARY KEY (id); diff --git a/database/table/t_user_collaborate_repository.sql b/database/table/t_user_collaborate_repository.sql new file mode 100644 index 0000000..6ab8e73 --- /dev/null +++ b/database/table/t_user_collaborate_repository.sql @@ -0,0 +1,18 @@ +CREATE TABLE public.t_user_collaborate_repository ( + id bigint NOT NULL, + collaborator_id bigint NOT NULL, + repository_id bigint NOT NULL, + gmt_created timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + gmt_updated timestamp without time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + gmt_deleted timestamp without time zone +); + +COMMENT ON TABLE public.t_user_collaborate_repository IS 'Table for collaboration relationship.'; + +COMMENT ON COLUMN public.t_user_collaborate_repository.id IS 'Primary key of the collaboration relationship table.'; +COMMENT ON COLUMN public.t_user_collaborate_repository.collaborator_id IS 'ID of the collaborator.'; +COMMENT ON COLUMN public.t_user_collaborate_repository.repository_id IS 'ID of the repository.'; +COMMENT ON COLUMN public.t_user_collaborate_repository.gmt_created IS 'Timestamp when the relationship was created.'; +COMMENT ON COLUMN public.t_user_collaborate_repository.gmt_updated IS 'Timestamp when the relationship was last updated.'; +COMMENT ON COLUMN public.t_user_collaborate_repository.gmt_deleted IS 'Timestamp when the relationship was deleted. +If set to NULL, it indicates that the repository has not been deleted.'; diff --git a/database/trigger/all_table_trigger.sql b/database/trigger/all_table_trigger.sql index 11f168c..91c13b4 100644 --- a/database/trigger/all_table_trigger.sql +++ b/database/trigger/all_table_trigger.sql @@ -19,3 +19,8 @@ FOR EACH ROW EXECUTE FUNCTION public.update_gmt_updated_column(); CREATE TRIGGER update_t_ssh_key_gmt_updated BEFORE UPDATE ON public.t_ssh_key FOR EACH ROW EXECUTE FUNCTION public.update_gmt_updated_column(); + +-- The trigger of the t_user_collaborate_repository table is added. +CREATE TRIGGER update_t_user_collaborate_repository_gmt_updated +BEFORE UPDATE ON public.t_user_collaborate_repository +FOR EACH ROW EXECUTE FUNCTION public.update_gmt_updated_column(); diff --git a/prepare_dev.sh b/prepare_dev.sh index 8b23714..b2dfc45 100644 --- a/prepare_dev.sh +++ b/prepare_dev.sh @@ -30,13 +30,15 @@ sudo su -c "/home/git/bin/gitolite setup -pk /home/git/$USER.pub" git rm -rf /home/"$USER"/gitolite-admin GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git clone \ ssh://git@localhost:22/gitolite-admin /home/"$USER"/gitolite-admin -mkdir -p /home/"$USER"/gitolite-admin/conf/gitolite.d +mkdir -p /home/"$USER"/gitolite-admin/conf/gitolite.d/user +mkdir -p /home/"$USER"/gitolite-admin/conf/gitolite.d/repository echo " repo gitolite-admin RW+ = $USER repo testing R = @all -include \"gitolite.d/*.conf\" +include \"gitolite.d/user/*.conf\" +include \"gitolite.d/repository/*.conf\" @all_public_repo = repo @all_public_repo R = @all" > /home/"$USER"/gitolite-admin/conf/gitolite.conf diff --git a/script/deploy_helper.py b/script/deploy_helper.py index 0ace678..217cf63 100644 --- a/script/deploy_helper.py +++ b/script/deploy_helper.py @@ -406,16 +406,21 @@ def init_gitolite(config): f"{config.serviceUserHomeDirectory}/gitolite-admin\" {config.serviceUser}") log_debug(f"Clone gitolite-admin command: {command}") command_checker(os.system(command), f"Failed to clone gitolite-admin") - command = (f"su -c 'mkdir -p {config.serviceUserHomeDirectory}/gitolite-admin/conf/gitolite.d' " + command = (f"su -c 'mkdir -p {config.serviceUserHomeDirectory}/gitolite-admin/conf/gitolite.d/user' " f"{config.serviceUser}") log_debug(f"Create usr directory command: {command}") command_checker(os.system(command), f"Failed to create usr directory in gitolite-admin/conf") + command = (f"su -c 'mkdir -p {config.serviceUserHomeDirectory}/gitolite-admin/conf/gitolite.d/repository' " + f"{config.serviceUser}") + log_debug(f"Create repository directory command: {command}") + command_checker(os.system(command), f"Failed to create repository directory in gitolite-admin/conf") content = f''' repo gitolite-admin RW+ = {config.serviceUser} repo testing R = @all -include "gitolite.d/*.conf" +include "gitolite.d/user/*.conf" +include "gitolite.d/repository/*.conf" @all_public_repo = repo @all_public_repo R = @all diff --git a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java index 26bc661..93f0f71 100644 --- a/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java +++ b/src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java @@ -40,6 +40,18 @@ public class ApiPathConstant { REPOSITORY_API_PREFIX + "/update"; public static final String REPOSITORY_CHECK_REPOSITORY_NAME_VALIDITY_API_PATH = REPOSITORY_API_PREFIX + "/repository-name"; + public static final String REPOSITORY_PAGE_COLLABORATOR_API_PATH = + REPOSITORY_API_PREFIX + "/page/collaborator"; + public static final String REPOSITORY_ADD_COLLABORATOR_API_PREFIX = + REPOSITORY_API_PREFIX + "/add-collaborator"; + public static final String REPOSITORY_ADD_COLLABORATOR_BY_NAME_API_PATH = + REPOSITORY_ADD_COLLABORATOR_API_PREFIX + "/name"; + public static final String REPOSITORY_ADD_COLLABORATOR_BY_EMAIL_API_PATH = + REPOSITORY_ADD_COLLABORATOR_API_PREFIX + "/email"; + public static final String REPOSITORY_ADD_COLLABORATOR_BY_ID_API_PATH = + REPOSITORY_ADD_COLLABORATOR_API_PREFIX + "/id"; + public static final String REPOSITORY_REMOVE_COLLABORATION_API_PATH = + REPOSITORY_API_PREFIX + "/remove-collaborator"; public static final String SSH_KEY_API_PREFIX = ALL_API_PREFIX + "/ssh"; public static final String SSH_KEY_UPLOAD_SSH_KEY_API_PATH = SSH_KEY_API_PREFIX + "/upload"; diff --git a/src/main/java/edu/cmipt/gcs/constant/GitConstant.java b/src/main/java/edu/cmipt/gcs/constant/GitConstant.java index 2b425a5..2ff46c1 100644 --- a/src/main/java/edu/cmipt/gcs/constant/GitConstant.java +++ b/src/main/java/edu/cmipt/gcs/constant/GitConstant.java @@ -23,6 +23,8 @@ public class GitConstant { public static String GITOLITE_USER_CONF_DIR_PATH; + public static String GITOLITE_REPOSITORY_CONF_DIR_PATH; + public static String GITOLITE_KEY_DIR_PATH; @Value("${git.user.name}") @@ -53,7 +55,9 @@ public void setGITOLITE_ADMIN_REPOSITORY_PATH(String gitoliteAdminRepositoryPath GitConstant.GITOLITE_CONF_FILE_PATH = Paths.get(GITOLITE_CONF_DIR_PATH, "gitolite.conf").toString(); GitConstant.GITOLITE_USER_CONF_DIR_PATH = - Paths.get(GITOLITE_CONF_DIR_PATH, "gitolite.d").toString(); + Paths.get(GITOLITE_CONF_DIR_PATH, "gitolite.d", "user").toString(); + GitConstant.GITOLITE_REPOSITORY_CONF_DIR_PATH = + Paths.get(GITOLITE_CONF_DIR_PATH, "gitolite.d", "repository").toString(); GitConstant.GITOLITE_KEY_DIR_PATH = Paths.get(gitoliteAdminRepositoryPath, "keydir").toString(); } diff --git a/src/main/java/edu/cmipt/gcs/controller/RepositoryController.java b/src/main/java/edu/cmipt/gcs/controller/RepositoryController.java index 557c44d..c61f7a9 100644 --- a/src/main/java/edu/cmipt/gcs/controller/RepositoryController.java +++ b/src/main/java/edu/cmipt/gcs/controller/RepositoryController.java @@ -1,16 +1,22 @@ package edu.cmipt.gcs.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import edu.cmipt.gcs.constant.ApiPathConstant; import edu.cmipt.gcs.constant.HeaderParameter; import edu.cmipt.gcs.constant.ValidationConstant; import edu.cmipt.gcs.enumeration.ErrorCodeEnum; import edu.cmipt.gcs.exception.GenericException; +import edu.cmipt.gcs.pojo.collaboration.UserCollaborateRepositoryPO; import edu.cmipt.gcs.pojo.repository.RepositoryDTO; import edu.cmipt.gcs.pojo.repository.RepositoryPO; import edu.cmipt.gcs.pojo.repository.RepositoryVO; +import edu.cmipt.gcs.pojo.user.UserPO; +import edu.cmipt.gcs.pojo.user.UserVO; import edu.cmipt.gcs.service.RepositoryService; +import edu.cmipt.gcs.service.UserCollaborateRepositoryService; +import edu.cmipt.gcs.service.UserService; import edu.cmipt.gcs.util.JwtUtil; import edu.cmipt.gcs.validation.group.CreateGroup; import edu.cmipt.gcs.validation.group.UpdateGroup; @@ -41,12 +47,16 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @Validated @RestController @Tag(name = "Repository", description = "Repository Related APIs") public class RepositoryController { private static final Logger logger = LoggerFactory.getLogger(SshKeyController.class); @Autowired private RepositoryService repositoryService; + @Autowired private UserService userService; + @Autowired private UserCollaborateRepositoryService userCollaborateRepositoryService; @PostMapping(ApiPathConstant.REPOSITORY_CREATE_REPOSITORY_API_PATH) @Operation( @@ -226,4 +236,299 @@ public void checkRepositoryNameValidity( throw new GenericException(ErrorCodeEnum.REPOSITORY_ALREADY_EXISTS, repositoryName); } } + + @PostMapping(ApiPathConstant.REPOSITORY_ADD_COLLABORATOR_BY_NAME_API_PATH) + @Operation( + summary = "Add a collaborator by names", + description = "Add a collaborator to the repository", + tags = {"Repository", "Post Method"}) + @Parameters({ + @Parameter( + name = HeaderParameter.ACCESS_TOKEN, + description = "Access token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class)), + @Parameter( + name = "repositoryId", + description = "Repository ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)), + @Parameter( + name = "collaboratorName", + description = "Collaborator's name", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = String.class)) + }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Collaborator added successfully"), + @ApiResponse(responseCode = "403", description = "Access denied"), + @ApiResponse(responseCode = "404", description = "Collaborator or repository not found") + }) + public void addCollaboratorByName( + @RequestParam("repositoryId") Long repositoryId, + @RequestParam("collaboratorName") String collaboratorName, + @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username", collaboratorName); + if (!userService.exists(queryWrapper)) { + throw new GenericException(ErrorCodeEnum.USER_NOT_FOUND, collaboratorName); + } + Long userId = userService.getOne(queryWrapper).getId(); + addCollaboratorById(repositoryId, userId, accessToken); + } + + @PostMapping(ApiPathConstant.REPOSITORY_ADD_COLLABORATOR_BY_EMAIL_API_PATH) + @Operation( + summary = "Add a collaborator by email", + description = "Add a collaborator to the repository", + tags = {"Repository", "Post Method"}) + @Parameters({ + @Parameter( + name = HeaderParameter.ACCESS_TOKEN, + description = "Access token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class)), + @Parameter( + name = "repositoryId", + description = "Repository ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)), + @Parameter( + name = "collaboratorEmail", + description = "Collaborator's email", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = String.class)) + }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Collaborator added successfully"), + @ApiResponse(responseCode = "403", description = "Access denied"), + @ApiResponse(responseCode = "404", description = "Collaborator or repository not found") + }) + public void addCollaboratorByEmail( + @RequestParam("repositoryId") Long repositoryId, + @RequestParam("collaboratorEmail") String collaboratorEmail, + @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("email", collaboratorEmail); + if (!userService.exists(queryWrapper)) { + throw new GenericException(ErrorCodeEnum.USER_NOT_FOUND, collaboratorEmail); + } + Long userId = userService.getOne(queryWrapper).getId(); + addCollaboratorById(repositoryId, userId, accessToken); + } + + @PostMapping(ApiPathConstant.REPOSITORY_ADD_COLLABORATOR_BY_ID_API_PATH) + @Operation( + summary = "Add a collaborator by id", + description = "Add a collaborator to the repository", + tags = {"Repository", "Post Method"}) + @Parameters({ + @Parameter( + name = HeaderParameter.ACCESS_TOKEN, + description = "Access token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class)), + @Parameter( + name = "repositoryId", + description = "Repository ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)), + @Parameter( + name = "collaboratorId", + description = "Collaborator's ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)) + }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Collaborator added successfully"), + @ApiResponse(responseCode = "403", description = "Access denied"), + @ApiResponse(responseCode = "404", description = "Collaborator or repository not found") + }) + public void addCollaboratorById( + @RequestParam("repositoryId") Long repositoryId, + @RequestParam("collaboratorId") Long collaboratorId, + @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { + if (userService.getById(collaboratorId) == null) { + throw new GenericException(ErrorCodeEnum.USER_NOT_FOUND, collaboratorId); + } + RepositoryPO repository = repositoryService.getById(repositoryId); + if (repository == null) { + throw new GenericException(ErrorCodeEnum.REPOSITORY_NOT_FOUND, repositoryId); + } + Long idInToken = Long.valueOf(JwtUtil.getId(accessToken)); + Long repositoryUserId = repository.getUserId(); + if (!idInToken.equals(repositoryUserId)) { + logger.error( + "User[{}] tried to add collaborator to repository[{}] whose creator is [{}]", + idInToken, + repositoryId, + repositoryUserId); + throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); + } + if (collaboratorId.equals(repositoryUserId)) { + logger.error( + "User[{}] tried to add himself to repository[{}]", + collaboratorId, + repositoryId); + throw new GenericException(ErrorCodeEnum.ILLOGICAL_OPERATION); + } + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("collaborator_id", collaboratorId); + queryWrapper.eq("repository_id", repositoryId); + if (userCollaborateRepositoryService.exists(queryWrapper)) { + logger.error( + "Collaborator[{}] already exists in repository[{}]", + collaboratorId, + repositoryId); + throw new GenericException( + ErrorCodeEnum.COLLABORATION_ALREADY_EXISTS, collaboratorId, repositoryId); + } + if (!userCollaborateRepositoryService.save( + new UserCollaborateRepositoryPO(collaboratorId, repositoryId))) { + logger.error( + "Failed to add collaborator[{}] to repository[{}]", + collaboratorId, + repositoryId); + throw new GenericException( + ErrorCodeEnum.COLLABORATION_ADD_FAILED, collaboratorId, repositoryId); + } + } + + @DeleteMapping(ApiPathConstant.REPOSITORY_REMOVE_COLLABORATION_API_PATH) + @Operation( + summary = "Remove a collaboration relationship", + description = + "Remove a collaboration relationship between a collaborator and a repository", + tags = {"Repository", "Delete Method"}) + @Parameters({ + @Parameter( + name = HeaderParameter.ACCESS_TOKEN, + description = "Access token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class)), + @Parameter( + name = "repositoryId", + description = "Repository's ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)), + @Parameter( + name = "collaboratorId", + description = "Collaborator's ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)) + }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Relationship removed successfully"), + @ApiResponse(responseCode = "404", description = "Collaboration not found"), + @ApiResponse(responseCode = "403", description = "Access denied"), + }) + public void removeCollaboration( + @RequestParam("repositoryId") Long repositoryId, + @RequestParam("collaboratorId") Long collaboratorId, + @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("collaborator_id", collaboratorId); + queryWrapper.eq("repository_id", repositoryId); + UserCollaborateRepositoryPO userCollaborateRepositoryPO = + userCollaborateRepositoryService.getOne(queryWrapper); + if (userCollaborateRepositoryPO == null) { + throw new GenericException( + ErrorCodeEnum.COLLABORATION_NOT_FOUND, collaboratorId, repositoryId); + } + Long idInToken = Long.valueOf(JwtUtil.getId(accessToken)); + Long repositoryUserId = repositoryService.getById(repositoryId).getUserId(); + if (!idInToken.equals(repositoryUserId)) { + logger.error( + "User[{}] tried to remove collaborator from repository[{}] whose creator is" + + " [{}]", + idInToken, + repositoryId, + repositoryUserId); + throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); + } + if (!userCollaborateRepositoryService.removeById(userCollaborateRepositoryPO.getId())) { + logger.error( + "Failed to remove collaborator[{}] from repository[{}]", + collaboratorId, + repositoryId); + throw new GenericException( + ErrorCodeEnum.COLLABORATION_REMOVE_FAILED, collaboratorId, repositoryId); + } + } + + @GetMapping(ApiPathConstant.REPOSITORY_PAGE_COLLABORATOR_API_PATH) + @Operation( + summary = "Page collaborators", + description = "Page collaborators of the repository", + tags = {"Repository", "Get Method"}) + @Parameters({ + @Parameter( + name = "repositoryId", + description = "Repository ID", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Long.class)), + @Parameter( + name = "page", + description = "Page number", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Integer.class)), + @Parameter( + name = "size", + description = "Page size", + required = true, + in = ParameterIn.QUERY, + schema = @Schema(implementation = Integer.class)), + @Parameter( + name = "accessToken", + description = "Access token", + required = true, + in = ParameterIn.HEADER, + schema = @Schema(implementation = String.class)) + }) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "Collaborators paged successfully"), + @ApiResponse(responseCode = "404", description = "Repository not found") + }) + public List pageCollaborator( + @RequestParam("repositoryId") Long repositoryId, + @RequestParam("page") Integer page, + @RequestParam("size") Integer size, + @RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) { + RepositoryPO repository = repositoryService.getById(repositoryId); + if (repository == null) { + throw new GenericException(ErrorCodeEnum.REPOSITORY_NOT_FOUND, repositoryId); + } + Long idInToken = Long.valueOf(JwtUtil.getId(accessToken)); + Long userId = repository.getUserId(); + List collaboratorList = + userCollaborateRepositoryService.listCollaboratorsByRepositoryId( + repositoryId, new Page<>(page, size)); + // only the creator and collaborators of the repository can page collaborators of a private + // repository + if (repository.getIsPrivate() + && !idInToken.equals(userId) + && collaboratorList.stream().noneMatch(user -> user.getId().equals(idInToken))) { + logger.error( + "User[{}] tried to page collaborators of repository[{}] whose creator is [{}]", + idInToken, + repositoryId, + userId); + throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); + } + return collaboratorList.stream().map(UserVO::new).toList(); + } } diff --git a/src/main/java/edu/cmipt/gcs/controller/SshKeyController.java b/src/main/java/edu/cmipt/gcs/controller/SshKeyController.java index 2916660..fac149c 100644 --- a/src/main/java/edu/cmipt/gcs/controller/SshKeyController.java +++ b/src/main/java/edu/cmipt/gcs/controller/SshKeyController.java @@ -40,7 +40,6 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; -import java.util.stream.Collectors; @RestController @Tag(name = "SSH", description = "SSH APIs") @@ -209,6 +208,6 @@ public List pageSshKey( wrapper.eq("user_id", userId); return sshKeyService.list(new Page<>(page, size), wrapper).stream() .map(SshKeyVO::new) - .collect(Collectors.toList()); + .toList(); } } diff --git a/src/main/java/edu/cmipt/gcs/controller/UserController.java b/src/main/java/edu/cmipt/gcs/controller/UserController.java index d658f8f..d663925 100644 --- a/src/main/java/edu/cmipt/gcs/controller/UserController.java +++ b/src/main/java/edu/cmipt/gcs/controller/UserController.java @@ -47,7 +47,6 @@ import org.springframework.web.bind.annotation.RestController; import java.util.List; -import java.util.stream.Collectors; @Validated @RestController @@ -238,7 +237,7 @@ public List pageUserRepository( wrapper.eq("user_id", userId); return repositoryService.list(new Page<>(page, size), wrapper).stream() .map(RepositoryVO::new) - .collect(Collectors.toList()); + .toList(); } @GetMapping(ApiPathConstant.USER_CHECK_EMAIL_VALIDITY_API_PATH) diff --git a/src/main/java/edu/cmipt/gcs/dao/UserCollaborateRepositoryMapper.java b/src/main/java/edu/cmipt/gcs/dao/UserCollaborateRepositoryMapper.java new file mode 100644 index 0000000..228edd4 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/dao/UserCollaborateRepositoryMapper.java @@ -0,0 +1,7 @@ +package edu.cmipt.gcs.dao; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; + +import edu.cmipt.gcs.pojo.collaboration.UserCollaborateRepositoryPO; + +public interface UserCollaborateRepositoryMapper extends BaseMapper {} diff --git a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java index c8d5214..07cc41f 100644 --- a/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java +++ b/src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java @@ -54,6 +54,11 @@ public enum ErrorCodeEnum { REPOSITORY_UPDATE_FAILED("REPOSITORY_UPDATE_FAILED"), REPOSITORY_DELETE_FAILED("REPOSITORY_DELETE_FAILED"), + COLLABORATION_ADD_FAILED("COLLABORATION_ADD_FAILED"), + COLLABORATION_REMOVE_FAILED("COLLABORATION_REMOVE_FAILED"), + COLLABORATION_ALREADY_EXISTS("COLLABORATION_ALREADY_EXISTS"), + COLLABORATION_NOT_FOUND("COLLABORATION_NOT_FOUND"), + SSHKEYDTO_ID_NULL("SshKeyDTO.id.Null"), SSHKEYDTO_ID_NOTNULL("SshKeyDTO.id.NotNull"), SSHKEYDTO_NAME_NOTBLANK("SshKeyDTO.name.NotBlank"), @@ -68,7 +73,9 @@ public enum ErrorCodeEnum { OPERATION_NOT_IMPLEMENTED("OPERATION_NOT_IMPLEMENTED"), - SERVER_ERROR("SERVER_ERROR"); + SERVER_ERROR("SERVER_ERROR"), + + ILLOGICAL_OPERATION("ILLOGICAL_OPERATION"); // code means the error code in the message.properties private String code; diff --git a/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java b/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java index 80d6007..e12d835 100644 --- a/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java +++ b/src/main/java/edu/cmipt/gcs/filter/JwtFilter.java @@ -170,6 +170,10 @@ private void authorize(HttpServletRequest request, String accessToken, String re "User[{}] tried to get SSH key of user[{}]", idInToken, idInParam); throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); } + } else if (request.getRequestURI() + .equals(ApiPathConstant.REPOSITORY_PAGE_COLLABORATOR_API_PATH)) { + // this will be checked in controller + // because we must query the database to get the user id of the repository } else { throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); } @@ -209,6 +213,10 @@ private void authorize(HttpServletRequest request, String accessToken, String re .equals(ApiPathConstant.REPOSITORY_UPDATE_REPOSITORY_API_PATH)) { // this will be checked in controller // because we must query the database to get the user id of the repository + } else if (request.getRequestURI() + .startsWith(ApiPathConstant.REPOSITORY_ADD_COLLABORATOR_API_PREFIX)) { + // this will be checked in controller + // because we must query the database to get the user id of the repository } else { throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); } @@ -237,6 +245,10 @@ private void authorize(HttpServletRequest request, String accessToken, String re .equals(ApiPathConstant.REPOSITORY_DELETE_REPOSITORY_API_PATH)) { // this will be checked in controller // because we must query the database to get the user id of the repository + } else if (request.getRequestURI() + .startsWith(ApiPathConstant.REPOSITORY_REMOVE_COLLABORATION_API_PATH)) { + // this will be checked in controller + // because we must query the database to get the user id of the repository } else { throw new GenericException(ErrorCodeEnum.ACCESS_DENIED); } diff --git a/src/main/java/edu/cmipt/gcs/pojo/collaboration/UserCollaborateRepositoryPO.java b/src/main/java/edu/cmipt/gcs/pojo/collaboration/UserCollaborateRepositoryPO.java new file mode 100644 index 0000000..f1c7d81 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/pojo/collaboration/UserCollaborateRepositoryPO.java @@ -0,0 +1,26 @@ +package edu.cmipt.gcs.pojo.collaboration; + +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.time.LocalDateTime; + +@Data +@AllArgsConstructor +@TableName("t_user_collaborate_repository") +public class UserCollaborateRepositoryPO { + private Long id; + private Long collaboratorId; + private Long repositoryId; + private LocalDateTime gmtCreated; + private LocalDateTime gmtUpdated; + @TableLogic private LocalDateTime gmtDeleted; + + public UserCollaborateRepositoryPO(Long collaboratorId, Long repositoryId) { + this.collaboratorId = collaboratorId; + this.repositoryId = repositoryId; + } +} diff --git a/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java b/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java index deb6bfa..faa3160 100644 --- a/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java +++ b/src/main/java/edu/cmipt/gcs/service/RepositoryServiceImpl.java @@ -34,6 +34,7 @@ public boolean save(RepositoryPO repositoryPO) { return false; } if (!GitoliteUtil.createRepository( + repositoryPO.getId(), repositoryPO.getRepositoryName(), repositoryPO.getUserId(), repositoryPO.getIsPrivate())) { diff --git a/src/main/java/edu/cmipt/gcs/service/UserCollaborateRepositoryService.java b/src/main/java/edu/cmipt/gcs/service/UserCollaborateRepositoryService.java new file mode 100644 index 0000000..41d5735 --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/service/UserCollaborateRepositoryService.java @@ -0,0 +1,13 @@ +package edu.cmipt.gcs.service; + +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; + +import edu.cmipt.gcs.pojo.collaboration.UserCollaborateRepositoryPO; +import edu.cmipt.gcs.pojo.user.UserPO; + +import java.util.List; + +public interface UserCollaborateRepositoryService extends IService { + List listCollaboratorsByRepositoryId(Long repositoryId, Page page); +} diff --git a/src/main/java/edu/cmipt/gcs/service/UserCollaborateRepositoryServiceImpl.java b/src/main/java/edu/cmipt/gcs/service/UserCollaborateRepositoryServiceImpl.java new file mode 100644 index 0000000..915d7fa --- /dev/null +++ b/src/main/java/edu/cmipt/gcs/service/UserCollaborateRepositoryServiceImpl.java @@ -0,0 +1,84 @@ +package edu.cmipt.gcs.service; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; + +import edu.cmipt.gcs.dao.UserCollaborateRepositoryMapper; +import edu.cmipt.gcs.enumeration.ErrorCodeEnum; +import edu.cmipt.gcs.exception.GenericException; +import edu.cmipt.gcs.pojo.collaboration.UserCollaborateRepositoryPO; +import edu.cmipt.gcs.pojo.user.UserPO; +import edu.cmipt.gcs.util.GitoliteUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.Serializable; +import java.util.List; + +@Service +public class UserCollaborateRepositoryServiceImpl + extends ServiceImpl + implements UserCollaborateRepositoryService { + private static final Logger logger = + LoggerFactory.getLogger(UserCollaborateRepositoryServiceImpl.class); + + @Autowired RepositoryService repositoryService; + @Autowired UserService userService; + + @Override + @Transactional + public boolean save(UserCollaborateRepositoryPO userCollaborateRepository) { + if (!super.save(userCollaborateRepository)) { + logger.error("Failed to save user collaborate repository to database"); + return false; + } + Long repositoryId = userCollaborateRepository.getRepositoryId(); + Long collaboratorId = userCollaborateRepository.getCollaboratorId(); + Long repositoryUserId = repositoryService.getById(repositoryId).getUserId(); + if (!GitoliteUtil.addCollaborator(repositoryUserId, repositoryId, collaboratorId)) { + logger.error("Failed to add collaborator to gitolite"); + throw new GenericException( + ErrorCodeEnum.COLLABORATION_ADD_FAILED, collaboratorId, repositoryId); + } + return true; + } + + @Override + @Transactional + public boolean removeById(Serializable id) { + var userCollaborateRepository = super.getById(id); + Long repositoryId = userCollaborateRepository.getRepositoryId(); + Long collaboratorId = userCollaborateRepository.getCollaboratorId(); + Long repositoryUserId = repositoryService.getById(repositoryId).getUserId(); + if (!super.removeById(id)) { + logger.error("Failed to remove user collaborate repository from database"); + return false; + } + if (!GitoliteUtil.removeCollaborator(repositoryUserId, repositoryId, collaboratorId)) { + logger.error("Failed to remove collaborator from gitolite"); + throw new GenericException( + ErrorCodeEnum.COLLABORATION_REMOVE_FAILED, collaboratorId, repositoryId); + } + return true; + } + + @Override + public List listCollaboratorsByRepositoryId(Long repositoryId, Page page) { + QueryWrapper queryWrapper = new QueryWrapper<>(); + List collaboratorIds = + super.listObjs( + new QueryWrapper() + .eq("repository_id", repositoryId) + .select("collaborator_id")); + if (collaboratorIds == null || collaboratorIds.isEmpty()) { + return List.of(); + } + queryWrapper.in("id", collaboratorIds); + return userService.list(page, queryWrapper); + } +} diff --git a/src/main/java/edu/cmipt/gcs/util/GitoliteUtil.java b/src/main/java/edu/cmipt/gcs/util/GitoliteUtil.java index 0a8524c..bb39ef4 100644 --- a/src/main/java/edu/cmipt/gcs/util/GitoliteUtil.java +++ b/src/main/java/edu/cmipt/gcs/util/GitoliteUtil.java @@ -15,7 +15,7 @@ public class GitoliteUtil { private static final Logger logger = LoggerFactory.getLogger(GitoliteUtil.class); public static synchronized boolean initUserConfig(Long userId) { - String userFileName = new StringBuilder().append(userId).append(".conf").toString(); + var userFileName = new StringBuilder().append(userId).append(".conf").toString(); var userConfPath = Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName); if (Files.exists(userConfPath)) { logger.error("Duplicate user file"); @@ -41,7 +41,7 @@ public static synchronized boolean initUserConfig(Long userId) { Files.write(Paths.get(GitConstant.GITOLITE_CONF_FILE_PATH), lines); String message = "Add user " + userId; Path[] files = { - Paths.get("conf", "gitolite.d", userFileName), + Paths.get("conf", "gitolite.d", "user", userFileName), Paths.get("conf", "gitolite.conf") }; if (!GitoliteUtil.commitAndPush(message, files)) { @@ -60,27 +60,29 @@ public static synchronized boolean initUserConfig(Long userId) { } public static synchronized boolean addSshKey(Long sshKeyId, String key, Long userId) { - var sshKeyPath = Paths.get(GitConstant.GITOLITE_KEY_DIR_PATH, sshKeyId + ".pub"); + var sshKeyFileName = new StringBuilder().append(sshKeyId).append(".pub").toString(); + var sshKeyPath = Paths.get(GitConstant.GITOLITE_KEY_DIR_PATH, sshKeyFileName); if (Files.exists(sshKeyPath)) { logger.error("Duplicate SSH file"); return false; } try { + var userFileName = new StringBuilder().append(userId).append(".conf").toString(); Files.writeString(sshKeyPath, key); List lines = Files.readAllLines( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf")); + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName)); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); if (line.startsWith("@%d_ssh_key".formatted(userId))) { lines.set(i, line + ' ' + sshKeyId); Files.write( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf"), + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName), lines); String message = "Add ssh key " + sshKeyId; Path[] files = { - Paths.get("keydir", sshKeyId + ".pub"), - Paths.get("conf", "gitolite.d", userId + ".conf") + Paths.get("keydir", sshKeyFileName), + Paths.get("conf", "gitolite.d", "user", userFileName) }; if (!GitoliteUtil.commitAndPush(message, files)) { logger.error("Failed to commit and push"); @@ -98,7 +100,8 @@ public static synchronized boolean addSshKey(Long sshKeyId, String key, Long use } public static synchronized boolean removeSshKey(Long sshKeyId, Long userId) { - var sshKeyPath = Paths.get(GitConstant.GITOLITE_KEY_DIR_PATH, sshKeyId + ".pub"); + var sshKeyFileName = new StringBuilder().append(sshKeyId).append(".pub").toString(); + var sshKeyPath = Paths.get(GitConstant.GITOLITE_KEY_DIR_PATH, sshKeyFileName); if (!Files.exists(sshKeyPath)) { logger.warn("Trying to remove a non-existent SSH key file: {}", sshKeyPath); return true; @@ -110,20 +113,21 @@ public static synchronized boolean removeSshKey(Long sshKeyId, Long userId) { return false; } try { + var userFileName = new StringBuilder().append(userId).append(".conf").toString(); List lines = Files.readAllLines( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf")); + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName)); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); if (line.startsWith("@%d_ssh_key".formatted(userId))) { lines.set(i, line.replace(" " + sshKeyId, "")); Files.write( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf"), + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName), lines); String message = "Remove ssh key " + sshKeyId; Path[] files = { - Paths.get("keydir", sshKeyId + ".pub"), - Paths.get("conf", "gitolite.d", userId + ".conf") + Paths.get("keydir", sshKeyFileName), + Paths.get("conf", "gitolite.d", "user", userFileName) }; if (!GitoliteUtil.commitAndPush(message, files)) { logger.error("Failed to commit and push"); @@ -141,7 +145,8 @@ public static synchronized boolean removeSshKey(Long sshKeyId, Long userId) { } public static synchronized boolean updateSshKey(Long sshKeyId, String key) { - var sshKeyPath = Paths.get(GitConstant.GITOLITE_KEY_DIR_PATH, sshKeyId + ".pub"); + var sshKeyFileName = new StringBuilder().append(sshKeyId).append(".pub").toString(); + var sshKeyPath = Paths.get(GitConstant.GITOLITE_KEY_DIR_PATH, sshKeyFileName); if (!Files.exists(sshKeyPath)) { logger.error("Trying to update a non-existent SSH key file: {}", sshKeyPath); return false; @@ -149,7 +154,7 @@ public static synchronized boolean updateSshKey(Long sshKeyId, String key) { try { Files.writeString(sshKeyPath, key); String message = "Update ssh key " + sshKeyId; - Path[] files = {Paths.get("keydir", sshKeyId + ".pub")}; + Path[] files = {Paths.get("keydir", sshKeyFileName)}; if (!GitoliteUtil.commitAndPush(message, files)) { logger.error("Failed to commit and push"); return false; @@ -162,21 +167,42 @@ public static synchronized boolean updateSshKey(Long sshKeyId, String key) { } public static synchronized boolean createRepository( - String repositoryName, Long userId, boolean isPrivate) { + Long repositoryId, String repositoryName, Long userId, boolean isPrivate) { + var userFileName = new StringBuilder().append(userId).append(".conf").toString(); + var repositoryFileName = + new StringBuilder().append(repositoryId).append(".conf").toString(); + var repositoryConfPath = + Paths.get(GitConstant.GITOLITE_REPOSITORY_CONF_DIR_PATH, repositoryFileName); + if (Files.exists(repositoryConfPath)) { + logger.error("Duplicate repository file"); + return false; + } try { + Files.createFile(repositoryConfPath); + String content = + """ + @%d_repo_collaborator = + repo %d/%s + RW+ = @%d_repo_collaborator + """ + .formatted(repositoryId, userId, repositoryName, repositoryId); + Files.writeString(repositoryConfPath, content); List lines = Files.readAllLines( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf")); + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName)); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); if (line.startsWith( "@%d_%s_repo".formatted(userId, isPrivate ? "private" : "public"))) { lines.set(i, line + ' ' + userId + '/' + repositoryName); Files.write( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf"), + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName), lines); String message = "Add repository " + userId + '/' + repositoryName; - Path[] files = {Paths.get("conf", "gitolite.d", userId + ".conf")}; + Path[] files = { + Paths.get("conf", "gitolite.d", "user", userFileName), + Paths.get("conf", "gitolite.d", "repository", repositoryFileName) + }; if (!GitoliteUtil.commitAndPush(message, files)) { logger.error("Failed to commit and push"); return false; @@ -196,20 +222,21 @@ public static synchronized boolean createRepository( public static synchronized boolean removeRepository( String repositoryName, Long userId, boolean isPrivate) { + var userFileName = new StringBuilder().append(userId).append(".conf").toString(); try { List lines = Files.readAllLines( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf")); + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName)); for (int i = 0; i < lines.size(); i++) { String line = lines.get(i); if (line.startsWith( "@%d_%s_repo".formatted(userId, isPrivate ? "private" : "public"))) { lines.set(i, line.replace(" " + userId + '/' + repositoryName, "")); Files.write( - Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userId + ".conf"), + Paths.get(GitConstant.GITOLITE_USER_CONF_DIR_PATH, userFileName), lines); String message = "Remove repository " + userId + '/' + repositoryName; - Path[] files = {Paths.get("conf", "gitolite.d", userId + ".conf")}; + Path[] files = {Paths.get("conf", "gitolite.d", "repository", userFileName)}; if (!GitoliteUtil.commitAndPush(message, files)) { logger.error("Failed to commit and push"); } @@ -246,6 +273,87 @@ public static synchronized boolean removeRepository( return false; } + public static synchronized boolean addCollaborator( + Long repositoryOwnerId, Long repositoryId, Long collaboratorId) { + var repositoryFileName = + new StringBuilder().append(repositoryId).append(".conf").toString(); + var repositoryConfPath = + Paths.get(GitConstant.GITOLITE_REPOSITORY_CONF_DIR_PATH, repositoryFileName); + if (!Files.exists(repositoryConfPath)) { + logger.error("Repository file does not exist"); + return false; + } + try { + List lines = Files.readAllLines(repositoryConfPath); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + if (line.startsWith("@%d_repo_collaborator".formatted(repositoryId))) { + lines.set(i, line + " @%d_ssh_key".formatted(collaboratorId)); + Files.write(repositoryConfPath, lines); + String message = + "Add collaborator " + collaboratorId + " to repository " + repositoryId; + Path[] files = { + Paths.get("conf", "gitolite.d", "repository", repositoryFileName) + }; + if (!GitoliteUtil.commitAndPush(message, files)) { + logger.error("Failed to commit and push"); + return false; + } + return true; + } + } + } catch (Exception e) { + logger.error(e.getMessage()); + return false; + } + logger.error( + "Can not find @{}_repo_collaborator in repository configuration" + .formatted(repositoryId)); + return false; + } + + public static synchronized boolean removeCollaborator( + Long repositoryOwnerId, Long repositoryId, Long collaboratorId) { + var repositoryFileName = + new StringBuilder().append(repositoryId).append(".conf").toString(); + var repositoryConfPath = + Paths.get(GitConstant.GITOLITE_REPOSITORY_CONF_DIR_PATH, repositoryFileName); + if (!Files.exists(repositoryConfPath)) { + logger.error("Repository file does not exist"); + return false; + } + try { + List lines = Files.readAllLines(repositoryConfPath); + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + if (line.startsWith("@%d_repo_collaborator".formatted(repositoryId))) { + lines.set(i, line.replace(" @%d_ssh_key".formatted(collaboratorId), "")); + Files.write(repositoryConfPath, lines); + String message = + "Remove collaborator " + + collaboratorId + + " from repository " + + repositoryId; + Path[] files = { + Paths.get("conf", "gitolite.d", "repository", repositoryFileName) + }; + if (!GitoliteUtil.commitAndPush(message, files)) { + logger.error("Failed to commit and push"); + return false; + } + return true; + } + } + } catch (Exception e) { + logger.error(e.getMessage()); + return false; + } + logger.error( + "Can not find @{}_repo_collaborator in repository configuration" + .formatted(repositoryId)); + return false; + } + private static synchronized boolean commitAndPush(String message, Path... files) { if (files.length == 0) { logger.error("No files to commit"); diff --git a/src/main/resources/message/message.properties b/src/main/resources/message/message.properties index c81685f..c13aa1f 100644 --- a/src/main/resources/message/message.properties +++ b/src/main/resources/message/message.properties @@ -44,6 +44,11 @@ REPOSITORY_CREATE_FAILED=Repository create failed: {} REPOSITORY_UPDATE_FAILED=Repository update failed: {} REPOSITORY_DELETE_FAILED=Repository delete failed: {} +COLLABORATION_ADD_FAILED=Collaborator add failed: collaborator{}, repository{} +COLLABORATION_REMOVE_FAILED=Collaborator remove failed: collaborator{}, repository{} +COLLABORATION_ALREADY_EXISTS=Collaborator already exists: collaborator{}, repository{} +COLLABORATION_NOT_FOUND=Collaborator not found: collaborator{}, repository{} + SshKeyDTO.id.Null=SSH key id must be null when creating a new SSH key SshKeyDTO.id.NotNull=SSH key id cannot be null SshKeyDTO.name.NotBlank=SSH key name cannot be blank @@ -59,3 +64,5 @@ SSH_KEY_NOT_FOUND=SSH key not found: {} OPERATION_NOT_IMPLEMENTED=Operation not implemented: {} SERVER_ERROR=Internal server error, try again later + +ILLOGICAL_OPERATION=Illogical operation, please check diff --git a/src/test/java/edu/cmipt/gcs/controller/RepositoryControllerTest.java b/src/test/java/edu/cmipt/gcs/controller/RepositoryControllerTest.java index 3043d91..fc39a62 100644 --- a/src/test/java/edu/cmipt/gcs/controller/RepositoryControllerTest.java +++ b/src/test/java/edu/cmipt/gcs/controller/RepositoryControllerTest.java @@ -114,6 +114,46 @@ public void testUpdateRepositoryValid() throws Exception { @Test @Order(Ordered.HIGHEST_PRECEDENCE + 2) + public void testAddCollaboratorByIdValid() throws Exception { + mvc.perform( + post(ApiPathConstant.REPOSITORY_ADD_COLLABORATOR_BY_ID_API_PATH) + .header(HeaderParameter.ACCESS_TOKEN, TestConstant.ACCESS_TOKEN) + .param("repositoryId", TestConstant.REPOSITORY_ID) + .param("collaboratorId", TestConstant.OTHER_ID)) + .andExpect(status().isOk()); + } + + @Test + @Order(Ordered.HIGHEST_PRECEDENCE + 3) + public void testPageCollaboratorValid() throws Exception { + mvc.perform( + get(ApiPathConstant.REPOSITORY_PAGE_COLLABORATOR_API_PATH) + .header(HeaderParameter.ACCESS_TOKEN, TestConstant.ACCESS_TOKEN) + .param("repositoryId", TestConstant.REPOSITORY_ID) + .param("page", "1") + .param("size", "10")) + .andExpectAll( + status().isOk(), + jsonPath("$").isArray(), + jsonPath("$.length()").value(1), + jsonPath("$[0].id").value(TestConstant.OTHER_ID), + jsonPath("$[0].username").value(TestConstant.OTHER_USERNAME), + jsonPath("$[0].email").value(TestConstant.OTHER_EMAIL)); + } + + @Test + @Order(Ordered.HIGHEST_PRECEDENCE + 4) + public void testRemoveCollaborationValid() throws Exception { + mvc.perform( + delete(ApiPathConstant.REPOSITORY_REMOVE_COLLABORATION_API_PATH) + .header(HeaderParameter.ACCESS_TOKEN, TestConstant.ACCESS_TOKEN) + .param("repositoryId", TestConstant.REPOSITORY_ID) + .param("collaboratorId", TestConstant.OTHER_ID)) + .andExpect(status().isOk()); + } + + @Test + @Order(Ordered.HIGHEST_PRECEDENCE + 5) public void testDeleteRepositoryValid() throws Exception { mvc.perform( delete(ApiPathConstant.REPOSITORY_DELETE_REPOSITORY_API_PATH)