Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Project Access Tokens #1018

Merged
merged 18 commits into from
Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/main/java/org/gitlab4j/api/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -919,6 +919,28 @@ public String toString() {
}
}

/** Enum to use for specifying the project token scope. */
public enum ProjectAccessTokenScope {
API, READ_API, READ_REGISTRY, WRITE_REGISTRY, READ_REPOSITORY, WRITE_REPOSITORY, CREATE_RUNNER;

private static JacksonJsonEnumHelper<ProjectAccessTokenScope> enumHelper = new JacksonJsonEnumHelper<>(ProjectAccessTokenScope.class);

@JsonCreator
public static ProjectAccessTokenScope forValue(String value) {
return enumHelper.forValue(value);
}

@JsonValue
public String toValue() {
return (enumHelper.toString(this));
}

@Override
public String toString() {
return (enumHelper.toString(this));
}
}

/** Enum for the build_git_strategy of the project instance. */
enum SquashOption {

Expand Down
114 changes: 89 additions & 25 deletions src/main/java/org/gitlab4j/api/ProjectApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,31 +38,7 @@
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.gitlab4j.api.GitLabApi.ApiVersion;
import org.gitlab4j.api.models.AccessLevel;
import org.gitlab4j.api.models.AccessRequest;
import org.gitlab4j.api.models.ApprovalRule;
import org.gitlab4j.api.models.ApprovalRuleParams;
import org.gitlab4j.api.models.AuditEvent;
import org.gitlab4j.api.models.Badge;
import org.gitlab4j.api.models.CustomAttribute;
import org.gitlab4j.api.models.Event;
import org.gitlab4j.api.models.FileUpload;
import org.gitlab4j.api.models.Issue;
import org.gitlab4j.api.models.Member;
import org.gitlab4j.api.models.Namespace;
import org.gitlab4j.api.models.Project;
import org.gitlab4j.api.models.ProjectApprovalsConfig;
import org.gitlab4j.api.models.ProjectFetches;
import org.gitlab4j.api.models.ProjectFilter;
import org.gitlab4j.api.models.ProjectGroupsFilter;
import org.gitlab4j.api.models.ProjectGroup;
import org.gitlab4j.api.models.ProjectHook;
import org.gitlab4j.api.models.ProjectUser;
import org.gitlab4j.api.models.PushRules;
import org.gitlab4j.api.models.RemoteMirror;
import org.gitlab4j.api.models.Snippet;
import org.gitlab4j.api.models.Variable;
import org.gitlab4j.api.models.Visibility;
import org.gitlab4j.api.models.*;
bensofficial marked this conversation as resolved.
Show resolved Hide resolved
import org.gitlab4j.api.utils.ISO8601;

/**
Expand Down Expand Up @@ -3901,4 +3877,92 @@ public RemoteMirror updateRemoteMirror(Object projectIdOrPath, Long mirrorId, Bo
"projects", getProjectIdOrPath(projectIdOrPath), "remote_mirrors", mirrorId);
return (response.readEntity(RemoteMirror.class));
}

/**
* Lists the projects access tokens for the project.
*
* @param projectIdOrPath the project in the form of a Long(ID), String(path), or Project instance
* @return the list of ProjectAccessTokens. The token and lastUsedAt attribute of each object is unset.
* @throws GitLabApiException if any exception occurs
*/
public List<ProjectAccessToken> listProjectAccessTokens(Object projectIdOrPath) throws GitLabApiException {
Response response = get(Response.Status.OK, null, "projects", getProjectIdOrPath(projectIdOrPath), "access_tokens");
return (response.readEntity(new GenericType<List<ProjectAccessToken>>() { }));
}

/**
* Gets the specific project access token.
*
* @param projectIdOrPath the project in the form of a Long(ID), String(path), or Project instance
* @param tokenId the id of the token
* @return the ProjectAccessToken. The token attribute of the object is unset.
* @throws GitLabApiException if any exception occurs
*/
// TODO
public ProjectAccessToken getProjectAccessToken(Object projectIdOrPath, Long tokenId) throws GitLabApiException {
Response response = get(Response.Status.OK, null, "projects", getProjectIdOrPath(projectIdOrPath), "access_tokens", tokenId);
return (response.readEntity(ProjectAccessToken.class));
}

/**
* Creates a new project access token.
*
* @param projectIdOrPath the project in the form of a Long(ID), String(path), or Project instance
* @param name the name of the token
* @param scopes the scope of the token
* @param expiresAt the date when the token should expire
* @param accessLevel The access level of the token is optional. It can either be 10, 20, 30, 40, or 50.
* @return the newly created ProjectAccessToken. The lastUsedAt attribute of each object is unset.
* @throws GitLabApiException if any exception occurs
*/
public ProjectAccessToken createProjectAccessToken(Object projectIdOrPath, String name, List<Constants.ProjectAccessTokenScope> scopes, Date expiresAt, Long accessLevel) throws GitLabApiException {
GitLabApiForm formData = new GitLabApiForm()
.withParam("name", name, true)
.withParam("expires_at", expiresAt, true)
.withParam("scopes", scopes, true)
.withParam("access_level", accessLevel, false);
Response response = post(Response.Status.CREATED, formData,
"projects", getProjectIdOrPath(projectIdOrPath), "access_tokens");
return (response.readEntity(ProjectAccessToken.class));
}

/**
* Creates a new project access token.
* The default value for the accessLevel is used.
*
* @param projectIdOrPath the project in the form of a Long(ID), String(path), or Project instance
* @param name the name of the token
* @param scopes the scope of the token
* @param expiresAt the date when the token should expire
* @return the newly created ProjectAccessToken. The lastUsedAt attribute of each object is unset.
* @throws GitLabApiException if any exception occurs
*/
public ProjectAccessToken createProjectAccessToken(Object projectIdOrPath, String name, List<Constants.ProjectAccessTokenScope> scopes, Date expiresAt) throws GitLabApiException {
GitLabApiForm formData = new GitLabApiForm()
.withParam("name", name, true)
.withParam("expires_at", ISO8601.dateOnly(expiresAt), true)
.withParam("scopes", scopes, true)
.withParam("access_level", (Object) null, false);
Response response = post(Response.Status.CREATED, formData,
"projects", getProjectIdOrPath(projectIdOrPath), "access_tokens");
return (response.readEntity(ProjectAccessToken.class));
}

// TODO
bensofficial marked this conversation as resolved.
Show resolved Hide resolved
public ProjectAccessToken rotateProjectAccessToken(Object projectIdOrPath, Long tokenId) throws GitLabApiException {
Response response = post(Response.Status.OK, (Object) null, "projects", getProjectIdOrPath(projectIdOrPath), "access_tokens", tokenId, "rotate");
return (response.readEntity(ProjectAccessToken.class));
}

/**
* Revokes the project access token.
*
* @param projectIdOrPath the project in the form of a Long(ID), String(path), or Project instance
* @param tokenId the id of the token, which should be revoked
* @throws GitLabApiException if any exception occurs
*/
public void revokeProjectAccessToken(Object projectIdOrPath, Long tokenId) throws GitLabApiException {
delete(Response.Status.NO_CONTENT, null, "projects", getProjectIdOrPath(projectIdOrPath), "access_tokens", tokenId);
}

}
118 changes: 118 additions & 0 deletions src/main/java/org/gitlab4j/api/models/ProjectAccessToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package org.gitlab4j.api.models;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.gitlab4j.api.Constants;
import org.gitlab4j.api.utils.JacksonJson;

import java.util.Date;
import java.util.List;

public class ProjectAccessToken {
bensofficial marked this conversation as resolved.
Show resolved Hide resolved
private Long userId;
private List<Constants.ProjectAccessTokenScope> scopes;
private String name;
@JsonProperty("expires_at")
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
private Date expiredAt;
private Long id;
private Boolean active;
private Date createdAt;
private Boolean revoked;
private Long accessLevel;
private Date lastUsedAt;
private String token;

public Long getUserId() {
return userId;
}

public void setUserId(Long userId) {
this.userId = userId;
}

public List<Constants.ProjectAccessTokenScope> getScopes() {
return scopes;
}

public void setScopes(List<Constants.ProjectAccessTokenScope> scopes) {
this.scopes = scopes;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public Date getExpiredAt() {
return expiredAt;
}

public void setExpiredAt(Date expiredAt) {
this.expiredAt = expiredAt;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public Boolean isActive() {
return active;
}

public void setActive(Boolean active) {
this.active = active;
}

public Date getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}

public Boolean isRevoked() {
return revoked;
}

public void setRevoked(Boolean revoked) {
this.revoked = revoked;
}

public Long getAccessLevel() {
return accessLevel;
}

public void setAccessLevel(Long accessLevel) {
this.accessLevel = accessLevel;
}

public Date getLastUsedAt() {
return lastUsedAt;
}

public void setLastUsedAt(Date lastUsedAt) {
this.lastUsedAt = lastUsedAt;
}

public String getToken() {
return token;
}

public void setToken(String token) {
this.token = token;
}

@Override
public String toString() {
return JacksonJson.toJsonString(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ private static void seedData() throws GitLabApiException {

GitLabApi gitLabApi = GitLabApi.oauth2Login(TEST_HOST_URL, username, password, null, null, true);

// If the tester user doen't exists, create it
// If the tester user doesn't exist, create it
Optional<User> optionalUser = gitLabApi.getUserApi().getOptionalUser(TEST_LOGIN_USERNAME);
if (!optionalUser.isPresent()) {
User userSettings = new User()
Expand All @@ -185,7 +185,7 @@ private static void seedData() throws GitLabApiException {
// so use OAUTH2 to get the GitLabApi instance
gitLabApi = GitLabApi.oauth2Login(TEST_HOST_URL, TEST_LOGIN_USERNAME, TEST_LOGIN_PASSWORD, null, null, true);

// Create the sudo as user if it does not exists
// Create the sudo as user if it does not exist
username = HelperUtils.getProperty(SUDO_AS_USERNAME_KEY, "user1");
optionalUser = gitLabApi.getUserApi().getOptionalUser(username);
if (!optionalUser.isPresent()) {
Expand Down
79 changes: 60 additions & 19 deletions src/test/java/org/gitlab4j/api/TestProjectApi.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,31 +23,17 @@

package org.gitlab4j.api;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.time.Instant;
import java.util.*;
import java.util.stream.Stream;

import javax.ws.rs.core.Response;

import org.gitlab4j.api.models.AccessLevel;
import org.gitlab4j.api.models.AccessRequest;
import org.gitlab4j.api.models.Group;
import org.gitlab4j.api.models.Member;
import org.gitlab4j.api.models.Project;
import org.gitlab4j.api.models.ProjectFetches;
import org.gitlab4j.api.models.ProjectFilter;
import org.gitlab4j.api.models.User;
import org.gitlab4j.api.models.Variable;
import org.gitlab4j.api.models.Visibility;
import org.gitlab4j.api.models.*;
import org.gitlab4j.api.utils.ISO8601;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -884,4 +870,59 @@ public void testTriggerHousekeeping() throws GitLabApiException {
}
}
}

@Test
public void testCreateProjectAccessToken() throws GitLabApiException {
final String tokenName = "token-" + HelperUtils.getRandomInt(1000);;
final List<Constants.ProjectAccessTokenScope> scopes = Arrays.asList(Constants.ProjectAccessTokenScope.READ_API, Constants.ProjectAccessTokenScope.READ_REPOSITORY);
final Date expiresAt = Date.from(Instant.now().plusSeconds(48*60*60));
final int size = gitLabApi.getProjectApi().listProjectAccessTokens(testProject.getId()).size() + 1;
assertNotNull(testProject);

ProjectAccessToken token = gitLabApi.getProjectApi().createProjectAccessToken(testProject.getId(), tokenName, scopes, expiresAt);

assertEquals(size, gitLabApi.getProjectApi().listProjectAccessTokens(testProject.getId()).size());
assertNotNull(token.getCreatedAt());
assertEquals(ISO8601.dateOnly(expiresAt), ISO8601.dateOnly(token.getExpiredAt()));
assertNotNull(token.getId());
assertEquals(tokenName, token.getName());
assertFalse(token.isRevoked());
assertEquals(scopes, token.getScopes());
assertNotNull(token.getToken());
assertNotEquals(token.getToken(), "");
assertNotNull(token.getUserId());
// unset
assertNull(token.getLastUsedAt());

gitLabApi.getProjectApi().revokeProjectAccessToken(testProject.getId(), token.getId());
assertFalse(gitLabApi.getProjectApi().listProjectAccessTokens(testProject.getId()).stream().filter(t -> !t.isRevoked()).anyMatch(t -> t.getId().equals(token.getId())));
}

@Test
public void testRotateProjectAccessToken() throws GitLabApiException {
final String tokenName = "token-" + HelperUtils.getRandomInt(1000);;
final List<Constants.ProjectAccessTokenScope> scopes = Arrays.asList(Constants.ProjectAccessTokenScope.READ_API, Constants.ProjectAccessTokenScope.READ_REPOSITORY);
final Date expiresAt = Date.from(Instant.now().plusSeconds(7*24*60*60));
assertNotNull(testProject);

ProjectAccessToken rotatedToken = gitLabApi.getProjectApi().createProjectAccessToken(testProject.getId(), tokenName, scopes, expiresAt);
ProjectAccessToken token = gitLabApi.getProjectApi().rotateProjectAccessToken(testProject.getId(), rotatedToken.getId());
rotatedToken = gitLabApi.getProjectApi().getProjectAccessToken(testProject.getId(), rotatedToken.getId());

assertNotNull(token.getCreatedAt());
assertEquals(ISO8601.dateOnly(expiresAt), ISO8601.dateOnly(token.getExpiredAt()));
assertNotNull(token.getId());
assertNotEquals(rotatedToken.getId(), token.getId());
assertEquals(tokenName, token.getName());
assertFalse(token.isRevoked());
assertTrue(rotatedToken.isRevoked());
assertEquals(scopes, token.getScopes());
assertNotNull(token.getToken());
assertNotEquals(token.getToken(), "");
assertNotEquals(rotatedToken.getToken(), token.getToken());
assertNotNull(token.getUserId());

gitLabApi.getProjectApi().revokeProjectAccessToken(testProject.getId(), token.getId());
assertFalse(gitLabApi.getProjectApi().listProjectAccessTokens(testProject.getId()).stream().filter(t -> !t.isRevoked()).anyMatch(t -> t.getId().equals(token.getId())));
}
}
Loading