Skip to content

Commit

Permalink
Finish the APIs related with ssh-key
Browse files Browse the repository at this point in the history
We finish the APIs related with ssh-key, including the following APIs:
* Upload a ssh-key
* Delete a ssh-key
* Get ssh-keys with pagination
* Update a ssh-key

We use spring transaction to ensure the atomicity of the operations.

See #32.
  • Loading branch information
Kaiser-Yang committed Sep 25, 2024
1 parent 38ef416 commit 0633edb
Show file tree
Hide file tree
Showing 28 changed files with 732 additions and 42 deletions.
1 change: 1 addition & 0 deletions README-zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
| `deployLogLevel` | `string` | `"info"` | 部署脚本的日志级别。 |
| `skipTest` | `bool` | `true` | 是否跳过测试。 |
| `gitUserName` | `string` | `"git"` | 用于保存 `git` 仓库的用户名。 |
| `gitHomeDirectory` | `string` | `"/home/git"` | `git` 用户的家目录。 |
| `gitUserPassword` | `string` | `"git"` | 用于保存 `git` 仓库的用户密码。 |
| `gitRepositoryDirectory` | `string` | `"/home/git/repository"` | `git` 仓库存放目录。不要使用 `~`|
| `gitServerDomain` | `string` | `"localhost"` | 服务器域名。 |
Expand Down
1 change: 1 addition & 0 deletions config_default.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"deployLogLevel": "info",
"skipTest": true,
"gitUserName": "git",
"gitHomeDirectory": "/home/git",
"gitUserPassword": "git",
"gitRepositoryDirectory": "/home/git/repository",
"gitServerDomain": "localhost",
Expand Down
6 changes: 5 additions & 1 deletion database/constraint/all_column_constraint.sql
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ ADD CONSTRAINT pk_user_table PRIMARY KEY (id);

-- The constraint of t_user_star_repository is added to the table.
ALTER TABLE ONLY public.t_user_star_repository
ADD CONSTRAINT pk_user_star_repository PRIMARY KEY (id);
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);
19 changes: 19 additions & 0 deletions database/table/t_ssh_key.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE public.t_ssh_key (
id bigint NOT NULL,
user_id bigint NOT NULL,
name character varying(255) NOT NULL,
public_key character varying(4096) 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_ssh_key IS 'Table for storing ssh public key.';
COMMENT ON COLUMN public.t_ssh_key.id IS 'Primary key of the ssh_key table.';
COMMENT ON COLUMN public.t_ssh_key.user_id IS 'ID of the user who owns the ssh key.';
COMMENT ON COLUMN public.t_ssh_key.name IS 'Name of the ssh key.';
COMMENT ON COLUMN public.t_ssh_key.public_key IS 'Public key of the ssh key.';
COMMENT ON COLUMN public.t_ssh_key.gmt_created IS 'Timestamp when the ssh_key record was created.';
COMMENT ON COLUMN public.t_ssh_key.gmt_updated IS 'Timestamp when the ssh_key record was last updated.';
COMMENT ON COLUMN public.t_ssh_key.gmt_deleted IS 'Timestamp when the ssh_key record was deleted.
If set to NULL, it indicates that the ssh_key record has not been deleted.';
2 changes: 1 addition & 1 deletion database/table/t_user.sql
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,4 @@ COMMENT ON COLUMN public.t_user.user_password IS 'Password of the user, stored a
COMMENT ON COLUMN public.t_user.gmt_created IS 'Timestamp when the user record was created.';
COMMENT ON COLUMN public.t_user.gmt_updated IS 'Timestamp when the user record was last updated.';
COMMENT ON COLUMN public.t_user.gmt_deleted IS 'Timestamp when the user record was deleted.
If set to NULL, it indicates that the user information not been deleted.';
If set to NULL, it indicates that the user information has not been deleted.';
3 changes: 2 additions & 1 deletion database/table/t_user_star_repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ COMMENT ON COLUMN public.t_user_star_repository.user_id IS 'ID of the user who s
COMMENT ON COLUMN public.t_user_star_repository.repository_id IS 'ID of the repository that has been starred.';
COMMENT ON COLUMN public.t_user_star_repository.gmt_created IS 'Timestamp when the relationship was created.';
COMMENT ON COLUMN public.t_user_star_repository.gmt_updated IS 'Timestamp when the relationship was last updated.';
COMMENT ON COLUMN public.t_user_star_repository.gmt_deleted IS 'Timestamp when the relationship was deleted. If set to NULL, it indicates that this relationship has not been deleted.';
COMMENT ON COLUMN public.t_user_star_repository.gmt_deleted IS 'Timestamp when the relationship was deleted.
If set to NULL, it indicates that this relationship has not been deleted.';
5 changes: 5 additions & 0 deletions database/trigger/all_table_trigger.sql
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,8 @@ FOR EACH ROW EXECUTE FUNCTION public.update_gmt_updated_column();
CREATE TRIGGER update_t_user_repository_gmt_updated
BEFORE UPDATE ON public.t_user_repository
FOR EACH ROW EXECUTE FUNCTION public.update_gmt_updated_column();

-- The trigger of the t_ssh_key table is added.
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();
5 changes: 3 additions & 2 deletions script/deploy_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ def write_other_config(config):
"frontEndUrl": "front-end.url",
"gitServerDomain": "git.server.domain",
"gitUserName": "git.user.name",
"gitHomeDirectory": "git.home.directory",
"gitRepositoryDirectory": "git.repository.directory",
"gitRepositorySuffix": "git.repository.suffix",
}
Expand Down Expand Up @@ -376,8 +377,8 @@ def deploy_on_ubuntu(config):
command_checker(res, message)
create_or_update_user(config.gitUserName, config.gitUserPassword)
create_or_update_user(config.serviceUser, config.serviceUserPassword)
# let the service user can use git command as the git user without password
sudoers_entry = f"{config.serviceUser} ALL=(git) NOPASSWD: /usr/bin/git"
# let the service user can use git and tee commands as the git user without password
sudoers_entry = f"{config.serviceUser} ALL=(git) NOPASSWD: /usr/bin/git, /usr/bin/tee"
res = subprocess.run(f"echo '{sudoers_entry}' | {sudo_cmd} tee /etc/sudoers.d/{config.serviceUser}", shell=True);
command_checker(res.returncode, f"Failed to create /etc/sudoers.d/{config.serviceUser}")
res = subprocess.run(f"{sudo_cmd} chmod 440 /etc/sudoers.d/{config.serviceUser}", shell=True)
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/edu/cmipt/gcs/GcsApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@MapperScan("edu.cmipt.gcs.dao")
@EnableTransactionManagement
public class GcsApplication {

public static void main(String[] args) {
Expand Down
11 changes: 11 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,4 +32,15 @@ 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 SSH_KEY_API_PREFIX = ALL_API_PREFIX + "/ssh";
public static final String SSH_KEY_UPLOAD_SSH_KEY_API_PATH =
SSH_KEY_API_PREFIX + "/upload";
public static final String SSH_KEY_UPDATE_SSH_KEY_API_PATH =
SSH_KEY_API_PREFIX + "/update";
public static final String SSH_KEY_DELETE_SSH_KEY_API_PATH =
SSH_KEY_API_PREFIX + "/delete";
public static final String SSH_KEY_PAGE_SSH_KEY_API_PATH =
SSH_KEY_API_PREFIX + "/page";

}
44 changes: 44 additions & 0 deletions src/main/java/edu/cmipt/gcs/constant/GitConstant.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package edu.cmipt.gcs.constant;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class GitConstant {
public static String GIT_USER_NAME;

public static String GIT_HOME_DIRECTORY;

public static String GIT_REPOSITORY_DIRECTORY;

public static String GIT_REPOSITORY_SUFFIX;

public static String GIT_SERVER_DOMAIN;

public static final String SSH_KEY_PREFIX = "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ";

@Value("${git.user.name}")
public void setGIT_USER_NAME(String gitUserName) {
GitConstant.GIT_USER_NAME = gitUserName;
}

@Value("${git.home.directory}")
public void setGIT_HOME_DIRECTORY(String gitHomeDirectory) {
GitConstant.GIT_HOME_DIRECTORY = gitHomeDirectory;
}

@Value("${git.repository.directory}")
public void setGIT_REPOSITORY_DIRECTORY(String gitRepositoryDirectory) {
GitConstant.GIT_REPOSITORY_DIRECTORY = gitRepositoryDirectory;
}

@Value("${git.repository.suffix}")
public void setGIT_REPOSITORY_SUFFIX(String gitRepositorySuffix) {
GitConstant.GIT_REPOSITORY_SUFFIX = gitRepositorySuffix;
}

@Value("${git.server.domain}")
public void setGIT_SERVER_DOMAIN(String gitServerDomain) {
GitConstant.GIT_SERVER_DOMAIN = gitServerDomain;
}
}
1 change: 1 addition & 0 deletions src/main/java/edu/cmipt/gcs/constant/HeaderParameter.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
public class HeaderParameter {
public static final String ACCESS_TOKEN = "Access-Token";
public static final String REFRESH_TOKEN = "Refresh-Token";
public static final String SSH_KEY = "Ssh-Key";
}
6 changes: 6 additions & 0 deletions src/main/java/edu/cmipt/gcs/constant/ValidationConstant.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,10 @@ public class ValidationConstant {
public static final int MAX_REPOSITORY_DESCRIPTION_LENGTH = 255;
// the length will be checked by @Size
public static final String REPOSITORY_NAME_PATTERN = "^[a-zA-Z0-9_-]*$";

public static final int MIN_SSH_KEY_NAME_LENGTH = 1;
public static final int MAX_SSH_KEY_NAME_LENGTH = 255;

public static final int MIN_SSH_KEY_PUBLICKEY_LENGTH = 1;
public static final int MAX_SSH_KEY_PUBLICKEY_LENGTH = 4096;
}
180 changes: 180 additions & 0 deletions src/main/java/edu/cmipt/gcs/controller/SshKeyController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
package edu.cmipt.gcs.controller;

import edu.cmipt.gcs.constant.ApiPathConstant;
import edu.cmipt.gcs.constant.HeaderParameter;
import edu.cmipt.gcs.enumeration.ErrorCodeEnum;
import edu.cmipt.gcs.exception.GenericException;
import edu.cmipt.gcs.pojo.error.ErrorVO;
import edu.cmipt.gcs.pojo.ssh.SshKeyDTO;
import edu.cmipt.gcs.pojo.ssh.SshKeyPO;
import edu.cmipt.gcs.pojo.ssh.SshKeyVO;
import edu.cmipt.gcs.service.SshKeyService;
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.Content;
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 java.util.List;
import java.util.stream.Collectors;

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.GetMapping;
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;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;

@RestController
@Tag(name = "SSH", description = "SSH APIs")
public class SshKeyController {
private static final Logger logger = LoggerFactory.getLogger(SshKeyController.class);

@Autowired private SshKeyService sshKeyService;

@PostMapping(ApiPathConstant.SSH_KEY_UPLOAD_SSH_KEY_API_PATH)
@Operation(
summary = "Upload SSH key",
description = "Upload SSH key with the given information",
tags = {"SSH", "Post Method"})
@Parameters({
@Parameter(
name = HeaderParameter.ACCESS_TOKEN,
description = "Access token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class))})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "SSH key uploaded successfully"),
@ApiResponse(
responseCode = "400",
description = "SSH key upload failed",
content = @Content(schema = @Schema(implementation = ErrorVO.class))),
@ApiResponse(responseCode = "500", description = "Internal server error")
})
public void uploadSshKey(@Validated(CreateGroup.class) @RequestBody SshKeyDTO sshKeyDTO,
@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken){
if (!sshKeyService.save(new SshKeyPO(sshKeyDTO))) {
throw new GenericException(ErrorCodeEnum.SSH_KEY_UPLOAD_FAILED, sshKeyDTO);
}
}

@DeleteMapping(ApiPathConstant.SSH_KEY_DELETE_SSH_KEY_API_PATH)
@Operation(
summary = "Delete SSH key",
description = "Delete SSH key with the given information",
tags = {"SSH", "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 = "SSH key ID",
required = true,
in = ParameterIn.QUERY,
schema = @Schema(implementation = Long.class))})
@ApiResponse(responseCode = "200", description = "SSH key deleted successfully")
public void deleteSshKey(@RequestHeader(HeaderParameter.ACCESS_TOKEN) String accessToken,
@RequestParam("id") Long id) {
var res = sshKeyService.getById(id);
if (res == 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);
throw new GenericException(ErrorCodeEnum.ACCESS_DENIED);
}
if (!sshKeyService.removeById(id)) {
throw new GenericException(ErrorCodeEnum.SSH_KEY_DELETE_FAILED, id);
}
}

@PostMapping(ApiPathConstant.SSH_KEY_UPDATE_SSH_KEY_API_PATH)
@Operation(
summary = "Update SSH key",
description = "Update SSH key with the given information",
tags = {"SSH", "Post Method"})
@Parameters({
@Parameter(
name = HeaderParameter.ACCESS_TOKEN,
description = "Access token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class))})
@ApiResponses({
@ApiResponse(responseCode = "200", description = "SSH key updated successfully"),
@ApiResponse(
responseCode = "400",
description = "SSH key update failed",
content = @Content(schema = @Schema(implementation = ErrorVO.class)))})
public ResponseEntity<SshKeyVO> updateSshKey(
@Validated(UpdateGroup.class) @RequestBody SshKeyDTO sshKeyDTO
) {
if (!sshKeyService.updateById(new SshKeyPO(sshKeyDTO))) {
throw new GenericException(ErrorCodeEnum.SSH_KEY_UPDATE_FAILED, sshKeyDTO);
}
return ResponseEntity.ok().body(new SshKeyVO(sshKeyService.getById(Long.valueOf(sshKeyDTO.id()))));
}

@GetMapping(ApiPathConstant.SSH_KEY_PAGE_SSH_KEY_API_PATH)
@Operation(
summary = "Page SSH key",
description = "Page SSH key with the given information",
tags = {"SSH", "Get Method"})
@Parameters({
@Parameter(
name = HeaderParameter.ACCESS_TOKEN,
description = "Access token",
required = true,
in = ParameterIn.HEADER,
schema = @Schema(implementation = String.class)),
@Parameter(
name = "id",
description = "User ID",
required = true,
in = ParameterIn.QUERY,
schema = @Schema(implementation = Long.class)),
@Parameter(
name = "page",
description = "Page number",
example = "1",
required = true,
in = ParameterIn.QUERY,
schema = @Schema(implementation = Integer.class)),
@Parameter(
name = "size",
description = "Page size",
example = "10",
required = true,
in = ParameterIn.QUERY,
schema = @Schema(implementation = Integer.class))})
@ApiResponse(responseCode = "200", description = "SSH key paged successfully")
public List<SshKeyVO> pageSshKey(@RequestParam("id") Long userId,
@RequestParam("page") Integer page, @RequestParam("size") Integer size) {
QueryWrapper<SshKeyPO> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
return sshKeyService.list(new Page<>(page, size), wrapper).stream().map(SshKeyVO::new).collect(Collectors.toList());
}
}
7 changes: 7 additions & 0 deletions src/main/java/edu/cmipt/gcs/dao/SshKeyMapper.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.cmipt.gcs.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;

import edu.cmipt.gcs.pojo.ssh.SshKeyPO;

public interface SshKeyMapper extends BaseMapper<SshKeyPO> {}
15 changes: 14 additions & 1 deletion src/main/java/edu/cmipt/gcs/enumeration/ErrorCodeEnum.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,20 @@ public enum ErrorCodeEnum {

REPOSITORYNAME_PATTERN_MISMATCH("REPOSITORYNAME_PATTERN_MISMATCH"),
REPOSITORY_ALREADY_EXISTS("REPOSITORY_ALREADY_EXISTS"),
REPOSITORY_CREATE_FAILED("REPOSITORY_CREATE_FAILED");
REPOSITORY_CREATE_FAILED("REPOSITORY_CREATE_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"),
SSHKEYDTO_PUBLICKEY_SIZE("SshKeyDTO.publicKey.Size"),

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");

// 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 @@ -94,6 +94,8 @@ public ResponseEntity<ErrorVO> handleJsonParseException(
@ExceptionHandler(Exception.class)
public void handleException(Exception e) {
logger.error(e.getMessage());
// TODO: use logger to log the exception
e.printStackTrace();
}

private ResponseEntity<ErrorVO> handleValidationException(
Expand Down
Loading

0 comments on commit 0633edb

Please sign in to comment.