Skip to content

Commit

Permalink
Finish delete and update repo, and delete user
Browse files Browse the repository at this point in the history
We finish the APIs for deleting and updating repository. For the
deleting user API, we now will remove all the ssh-keys uploaded by the
user, but not the repositories.

Besides, we find that we should not trust the data from the client side,
such as the `userId` from request body, we must get this data from the
database to check if the operation is allowed.

See #20 and #32.
  • Loading branch information
Kaiser-Yang committed Sep 25, 2024
1 parent ca8da81 commit a6a7747
Show file tree
Hide file tree
Showing 17 changed files with 329 additions and 58 deletions.
4 changes: 4 additions & 0 deletions src/main/java/edu/cmipt/gcs/constant/ApiPathConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ public class ApiPathConstant {
public static final String REPOSITORY_API_PREFIX = ALL_API_PREFIX + "/repository";
public static final String REPOSITORY_CREATE_REPOSITORY_API_PATH =
REPOSITORY_API_PREFIX + "/create";
public static final String REPOSITORY_DELETE_REPOSITORY_API_PATH =
REPOSITORY_API_PREFIX + "/delete";
public static final String REPOSITORY_UPDATE_REPOSITORY_API_PATH =
REPOSITORY_API_PREFIX + "/update";

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";
Expand Down
107 changes: 106 additions & 1 deletion src/main/java/edu/cmipt/gcs/controller/RepositoryController.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,28 +8,36 @@
import edu.cmipt.gcs.exception.GenericException;
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.service.RepositoryService;
import edu.cmipt.gcs.util.JwtUtil;
import edu.cmipt.gcs.validation.group.CreateGroup;

import edu.cmipt.gcs.validation.group.UpdateGroup;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@Tag(name = "Repository", description = "Repository Related APIs")
public class RepositoryController {
private static final Logger logger = LoggerFactory.getLogger(SshKeyController.class);
@Autowired private RepositoryService repositoryService;

@PostMapping(ApiPathConstant.REPOSITORY_CREATE_REPOSITORY_API_PATH)
Expand All @@ -49,6 +57,9 @@ public class RepositoryController {
public void createRepository(
@Validated(CreateGroup.class) @RequestBody RepositoryDTO repository,
@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) {
if (repository.isPrivate() != null && repository.isPrivate()) {
throw new GenericException(ErrorCodeEnum.OPERATION_NOT_IMPLEMENTED, "private repository is not implemented");
}
String userId = JwtUtil.getId(accessToken);
RepositoryPO repositoryPO = new RepositoryPO(repository, userId);
QueryWrapper<RepositoryPO> queryWrapper = new QueryWrapper<>();
Expand All @@ -61,4 +72,98 @@ public void createRepository(
throw new GenericException(ErrorCodeEnum.REPOSITORY_CREATE_FAILED, repository);
}
}

@DeleteMapping(ApiPathConstant.REPOSITORY_DELETE_REPOSITORY_API_PATH)
@Operation(
summary = "Delete a repository",
description = "Delete a repository with the given id",
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 = "id",
description = "Repository id",
required = true,
in = ParameterIn.QUERY,
schema = @Schema(implementation = Long.class)
)
})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Repository deleted successfully"),
@ApiResponse(responseCode = "403", description = "Access denied"),
@ApiResponse(responseCode = "404", description = "Repository not found")
})
public void deleteRepository(
@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken,
@RequestParam("id") Long id
) {
var repository = repositoryService.getById(id);
if (repository == null) {
throw new GenericException(ErrorCodeEnum.REPOSITORY_NOT_FOUND, id);
}
String userId = JwtUtil.getId(accessToken);
if (!userId.equals(repository.getUserId().toString())) {
logger.info("User[{}] tried to delete repository of user[{}]", userId, repository.getUserId());
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
if (!repositoryService.removeById(id)) {
throw new GenericException(ErrorCodeEnum.REPOSITORY_DELETE_FAILED, id);
}
}

@PostMapping(ApiPathConstant.REPOSITORY_UPDATE_REPOSITORY_API_PATH)
@Operation(
summary = "Update a repository",
description = "Update a repository with the given information",
tags = {"Repository", "Post Method"}
)
@Parameter(
name = HeaderParameter.ACCESS_TOKEN,
description = "Access token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class)
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "Repository updated successfully"),
@ApiResponse(responseCode = "403", description = "Access denied"),
@ApiResponse(responseCode = "404", description = "Repository not found"),
@ApiResponse(responseCode = "501", description = "Update repository name is not implemented")
})
public ResponseEntity<RepositoryVO> updateRepository(
@Validated(UpdateGroup.class) @RequestBody RepositoryDTO repository,
@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken
) {
Long id = null;
try {
id = Long.valueOf(repository.id());
} catch (NumberFormatException e) {
logger.error(e.getMessage());
throw new GenericException(ErrorCodeEnum.MESSAGE_CONVERSION_ERROR);
}
var repositoryPO = repositoryService.getById(id);
if (repositoryPO == null) {
throw new GenericException(ErrorCodeEnum.REPOSITORY_NOT_FOUND, id);
}
String userId = JwtUtil.getId(accessToken);
if (!userId.equals(repositoryPO.getUserId().toString())) {
logger.info("User[{}] tried to update repository of user[{}]", userId, repositoryPO.getUserId());
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
if (repository.repositoryName() != null &&
!repository.repositoryName().equals(repositoryService.getById(id).getRepositoryName())) {
throw new GenericException(ErrorCodeEnum.OPERATION_NOT_IMPLEMENTED, "update repository name is not implemented");
}
if (!repositoryService.updateById(new RepositoryPO(repository))) {
throw new GenericException(ErrorCodeEnum.REPOSITORY_UPDATE_FAILED, repository);
}
return ResponseEntity.ok().body(new RepositoryVO(repositoryService.getById(id)));
}
}
30 changes: 23 additions & 7 deletions src/main/java/edu/cmipt/gcs/controller/SshKeyController.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public class SshKeyController {
public void uploadSshKey(
@Validated(CreateGroup.class) @RequestBody SshKeyDTO sshKeyDTO,
@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) {
if (!sshKeyService.save(new SshKeyPO(sshKeyDTO))) {
if (!sshKeyService.save(new SshKeyPO(sshKeyDTO, JwtUtil.getId(accessToken)))) {
throw new GenericException(ErrorCodeEnum.SSH_KEY_UPLOAD_FAILED, sshKeyDTO);
}
}
Expand Down Expand Up @@ -101,14 +101,13 @@ public void uploadSshKey(
public void deleteSshKey(
@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken,
@RequestParam("id") Long id) {
var res = sshKeyService.getById(id);
if (res == null) {
var sshKeyPO = sshKeyService.getById(id);
if (sshKeyPO == null) {
throw new GenericException(ErrorCodeEnum.SSH_KEY_NOT_FOUND, id);
}
String idInToken = JwtUtil.getId(accessToken);
String idInRes = res.getUserId().toString();
if (!idInRes.equals(idInToken)) {
logger.info("User[{}] tried to get SSH key of user[{}]", idInToken, idInRes);
if (!idInToken.equals(sshKeyPO.getUserId().toString())) {
logger.info("User[{}] tried to get SSH key of user[{}]", idInToken, sshKeyPO.getUserId());
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
if (!sshKeyService.removeById(id)) {
Expand Down Expand Up @@ -137,7 +136,24 @@ public void deleteSshKey(
content = @Content(schema = @Schema(implementation = ErrorVO.class)))
})
public ResponseEntity<SshKeyVO> updateSshKey(
@Validated(UpdateGroup.class) @RequestBody SshKeyDTO sshKeyDTO) {
@Validated(UpdateGroup.class) @RequestBody SshKeyDTO sshKeyDTO,
@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken) {
Long id = null;
try {
id = Long.valueOf(sshKeyDTO.id());
} catch (NumberFormatException e) {
logger.error(e.getMessage());
throw new GenericException(ErrorCodeEnum.MESSAGE_CONVERSION_ERROR);
}
var sshKeyPO = sshKeyService.getById(id);
if (sshKeyPO == null) {
throw new GenericException(ErrorCodeEnum.SSH_KEY_NOT_FOUND, id);
}
String idInToken = JwtUtil.getId(accessToken);
if (!idInToken.equals(sshKeyPO.getUserId().toString())) {
logger.info("User[{}] tried to update SSH key of user[{}]", idInToken, sshKeyPO.getUserId());
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
if (!sshKeyService.updateById(new SshKeyPO(sshKeyDTO))) {
throw new GenericException(ErrorCodeEnum.SSH_KEY_UPDATE_FAILED, sshKeyDTO);
}
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,14 @@ public enum ErrorCodeEnum {
REPOSITORYDTO_WATCHER_MIN("RepositoryDTO.watcher.Min"),

REPOSITORYNAME_PATTERN_MISMATCH("REPOSITORYNAME_PATTERN_MISMATCH"),
REPOSITORY_NOT_FOUND("REPOSITORY_NOT_FOUND"),
REPOSITORY_ALREADY_EXISTS("REPOSITORY_ALREADY_EXISTS"),
REPOSITORY_CREATE_FAILED("REPOSITORY_CREATE_FAILED"),
REPOSITORY_UPDATE_FAILED("REPOSITORY_UPDATE_FAILED"),
REPOSITORY_DELETE_FAILED("REPOSITORY_DELETE_FAILED"),

SSHKEYDTO_ID_NULL("SshKeyDTO.id.Null"),
SSHKEYDTO_ID_NOTNULL("SshKeyDTO.id.NotNull"),
SSHKEYDTO_USERID_NOTBLANK("SshKeyDTO.userId.NotBlank"),
SSHKEYDTO_NAME_NOTBLANK("SshKeyDTO.name.NotBlank"),
SSHKEYDTO_NAME_SIZE("SshKeyDTO.name.Size"),
SSHKEYDTO_PUBLICKEY_NOTBLANK("SshKeyDTO.publicKey.NotBlank"),
Expand All @@ -62,7 +64,9 @@ public enum ErrorCodeEnum {
SSH_KEY_UPLOAD_FAILED("SSH_KEY_UPLOAD_FAILED"),
SSH_KEY_UPDATE_FAILED("SSH_KEY_UPDATE_FAILED"),
SSH_KEY_DELETE_FAILED("SSH_KEY_DELETE_FAILED"),
SSH_KEY_NOT_FOUND("SSH_KEY_NOT_FOUND");
SSH_KEY_NOT_FOUND("SSH_KEY_NOT_FOUND"),

OPERATION_NOT_IMPLEMENTED("OPERATION_NOT_IMPLEMENTED");

// code means the error code in the message.properties
private String code;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,13 @@ public ResponseEntity<ErrorVO> handleGenericException(
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new ErrorVO(e.getCode(), e.getMessage()));
case USER_NOT_FOUND:
case SSH_KEY_NOT_FOUND:
case REPOSITORY_NOT_FOUND:
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorVO(e.getCode(), e.getMessage()));
case OPERATION_NOT_IMPLEMENTED:
return ResponseEntity.status(HttpStatus.NOT_IMPLEMENTED)
.body(new ErrorVO(e.getCode(), e.getMessage()));
default:
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(new ErrorVO(e.getCode(), e.getMessage()));
Expand Down
29 changes: 11 additions & 18 deletions src/main/java/edu/cmipt/gcs/filter/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -198,26 +198,15 @@ private void authorize(HttpServletRequest request, String accessToken, String re
// pass
} else if (request.getRequestURI()
.equals(ApiPathConstant.SSH_KEY_UPLOAD_SSH_KEY_API_PATH)) {
String idInToken = JwtUtil.getId(accessToken);
String idInBody = getFromRequestBody(request, "userId");
if (!idInToken.equals(idInBody)) {
logger.info(
"User[{}] tried to upload SSH key of user[{}]",
idInToken,
idInBody);
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
// pass
} else if (request.getRequestURI()
.equals(ApiPathConstant.SSH_KEY_UPDATE_SSH_KEY_API_PATH)) {
String idInToken = JwtUtil.getId(accessToken);
String idInBody = getFromRequestBody(request, "userId");
if (!idInToken.equals(idInBody)) {
logger.info(
"User[{}] tried to update SSH key of user[{}]",
idInToken,
idInBody);
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
// this will be checked in controller
// because we must query the database to get the user id of the ssh key
} else if (request.getRequestURI()
.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 {
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
Expand All @@ -242,6 +231,10 @@ private void authorize(HttpServletRequest request, String accessToken, String re
.equals(ApiPathConstant.SSH_KEY_DELETE_SSH_KEY_API_PATH)) {
// this will be checked in controller
// because we must query the database to get the user id of the ssh key
} else if (request.getRequestURI()
.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 {
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryPO.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,8 @@ public RepositoryPO(RepositoryDTO repositoryDTO, String userId) {
this.fork = repositoryDTO.fork();
this.watcher = repositoryDTO.watcher();
}

public RepositoryPO(RepositoryDTO repositoryDTO) {
this(repositoryDTO, null);
}
}
2 changes: 2 additions & 0 deletions src/main/java/edu/cmipt/gcs/pojo/repository/RepositoryVO.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ public record RepositoryVO(
@Schema(description = "Repository Name") String repositoryName,
@Schema(description = "Repository Description") String repositoryDescription,
@Schema(description = "Whether or Not Private Repo") Boolean isPrivate,
@Schema(description = "Owner ID") Long userId,
@Schema(description = "Star Count") Integer star,
@Schema(description = "Fork Count") Integer fork,
@Schema(description = "Watcher Count") Integer watcher) {
Expand All @@ -16,6 +17,7 @@ public RepositoryVO(RepositoryPO repositoryPO) {
repositoryPO.getRepositoryName(),
repositoryPO.getRepositoryDescription(),
repositoryPO.getIsPrivate(),
repositoryPO.getUserId(),
repositoryPO.getStar(),
repositoryPO.getFork(),
repositoryPO.getWatcher());
Expand Down
5 changes: 0 additions & 5 deletions src/main/java/edu/cmipt/gcs/pojo/ssh/SshKeyDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,6 @@ public record SshKeyDTO(
groups = UpdateGroup.class,
message = "SSHKEYDTO_ID_NOTNULL {SshKeyDTO.id.NotNull}")
String id,
@Schema(description = "User ID")
@NotBlank(
groups = {CreateGroup.class, UpdateGroup.class},
message = "SSHKEYDTO_USERID_NOTBLANK {SshKeyDTO.userId.NotBlank}")
String userId,
@Schema(description = "Name", example = "My SSH Key")
@NotBlank(
groups = {CreateGroup.class, UpdateGroup.class},
Expand Down
8 changes: 6 additions & 2 deletions src/main/java/edu/cmipt/gcs/pojo/ssh/SshKeyPO.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,22 @@ public class SshKeyPO {
private LocalDateTime gmtUpdated;
@TableLogic private LocalDateTime gmtDeleted;

public SshKeyPO(SshKeyDTO sshKeyDTO) {
public SshKeyPO(SshKeyDTO sshKeyDTO, String userId) {
try {
this.id = Long.valueOf(sshKeyDTO.id());
} catch (NumberFormatException e) {
this.id = null;
}
try {
this.userId = Long.valueOf(sshKeyDTO.userId());
this.userId = Long.valueOf(userId);
} catch (NumberFormatException e) {
this.userId = null;
}
this.name = sshKeyDTO.name();
this.publicKey = sshKeyDTO.publicKey();
}

public SshKeyPO(SshKeyDTO sshKeyDTO) {
this(sshKeyDTO, null);
}
}
Loading

0 comments on commit a6a7747

Please sign in to comment.