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 support for GroupAccessToken credentials, fix credential integration #479

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ credentials:
scope: SYSTEM
id: "i<3GitLab"
token: "glpat-XfsqZvVtAx5YCph5bq3r" # gitlab personal access token
- gitlabGroupAccessToken:
scope: SYSTEM
id: "i<3GitLab"
token: "glgat-XfsqZvVtAx5YCph5bq3r" # gitlab group access token

unclassified:
gitLabServers:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
public SCMFileSystem build(@NonNull SCMSource source, @NonNull SCMHead head, @CheckForNull SCMRevision rev)
throws IOException, InterruptedException {
GitLabSCMSource gitlabScmSource = (GitLabSCMSource) source;
GitLabApi gitLabApi = apiBuilder(source.getOwner(), gitlabScmSource.getServerName());
GitLabApi gitLabApi =
apiBuilder(source.getOwner(), gitlabScmSource.getServerName(), gitlabScmSource.getCredentialsId());

Check warning on line 88 in src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMFileSystem.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 87-88 are not covered by tests
String projectPath = gitlabScmSource.getProjectPath();
return build(head, rev, gitLabApi, projectPath);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
Expand Down Expand Up @@ -198,364 +197,365 @@

private GitLabOwner getGitlabOwner(SCMNavigatorOwner owner) {
if (gitlabOwner == null) {
getGitlabOwner(apiBuilder(owner, serverName));
getGitlabOwner(apiBuilder(owner, serverName, credentialsId));
}
return gitlabOwner;
}

private GitLabOwner getGitlabOwner(GitLabApi gitLabApi) {
if (gitlabOwner == null) {
gitlabOwner = GitLabOwner.fetchOwner(gitLabApi, projectOwner);
}
return gitlabOwner;
}

/**
* Sets the behavioral traits that are applied to this navigator and any {@link
* GitLabSCMSource} instances it discovers. The new traits will take affect on
* the next
* navigation through any of the {@link #visitSources(SCMSourceObserver)}
* overloads or {@link
* #visitSource(String, SCMSourceObserver)}.
*
* @param traits the new behavioral traits.
*/
@Override
public void setTraits(@CheckForNull List<SCMTrait<? extends SCMTrait<?>>> traits) {
this.traits = traits != null ? new ArrayList<>(traits) : new ArrayList<>();
}

@NonNull
@Override
protected String id() {
return getServerUrlFromName(serverName) + "::" + projectOwner;
}

@Override
public void visitSources(@NonNull final SCMSourceObserver observer) throws IOException, InterruptedException {
GitLabSCMNavigatorContext context = new GitLabSCMNavigatorContext().withTraits(traits);
try (GitLabSCMNavigatorRequest request = context.newRequest(this, observer)) {
GitLabApi gitLabApi = apiBuilder(observer.getContext(), serverName);
GitLabApi gitLabApi = apiBuilder(observer.getContext(), serverName, credentialsId);
getGitlabOwner(gitLabApi);
List<Project> projects;
if (gitlabOwner instanceof GitLabUser) {
// Even returns the group projects owned by the user
projects = gitLabApi.getProjectApi().getUserProjects(projectOwner, new ProjectFilter().withOwned(true));
} else {
isGroup = true;
GroupProjectsFilter groupProjectsFilter = new GroupProjectsFilter();
wantSubGroupProjects = request.wantSubgroupProjects();
groupProjectsFilter.withIncludeSubGroups(wantSubGroupProjects);
groupProjectsFilter.withShared(request.wantSharedProjects());
// If projectOwner is a subgroup, it will only return projects in the subgroup
projects = gitLabApi.getGroupApi().getProjects(projectOwner, groupProjectsFilter);
}
int count = 0;
observer.getListener().getLogger().format("%nChecking projects...%n");
StandardCredentials webHookCredentials = getWebHookCredentials(observer.getContext());
GitLabApi webhookGitLabApi = null;
String webHookUrl = null;
if (webHookCredentials != null) {
GitLabServer server = GitLabServers.get().findServer(serverName);
String serverUrl = getServerUrl(server);
webhookGitLabApi = new GitLabApi(
serverUrl, getPrivateTokenAsPlainText(webHookCredentials), null, getProxyConfig(serverUrl));
webHookUrl = GitLabHookCreator.getHookUrl(server, true);
}
projects = projects.stream().filter(Objects::nonNull).collect(Collectors.toList());
for (Project p : projects) {
count++;
String projectPathWithNamespace = p.getPathWithNamespace();
String projectOwner = getProjectOwnerFromNamespace(projectPathWithNamespace);
String projectName = getProjectName(gitLabApi, request.withProjectNamingStrategy(), p);
getNavigatorProjects().add(projectPathWithNamespace);
if (StringUtils.isEmpty(p.getDefaultBranch())) {
observer.getListener()
.getLogger()
.format(
"%nIgnoring project with empty repository %s%n",
HyperlinkNote.encodeTo(p.getWebUrl(), p.getName()));
continue;
}
if (p.getArchived() && context.isExcludeArchivedRepositories()) {
observer.getListener()
.getLogger()
.format(
"%nIgnoring archived project %s%n",
HyperlinkNote.encodeTo(p.getWebUrl(), p.getName()));
continue;
}
observer.getListener()
.getLogger()
.format("%nChecking project %s%n", HyperlinkNote.encodeTo(p.getWebUrl(), projectName));
try {
GitLabServer server = GitLabServers.get().findServer(serverName);
if (webhookGitLabApi != null && webHookUrl != null) {
String secretToken = server.getSecretTokenAsPlainText();
if (secretToken == null) {
// sending 'null' to GitLab will ignore the value, when we want to update it to be empty.
secretToken = "";
}
observer.getListener()
.getLogger()
.format(
"Web hook %s%n",
GitLabHookCreator.createWebHookWhenMissing(
webhookGitLabApi, projectPathWithNamespace, webHookUrl, secretToken));
}
} catch (GitLabApiException e) {
observer.getListener().getLogger().format("Cannot set web hook: %s%n", e.getReason());
}
if (request.process(
projectName,
name -> new GitLabSCMSourceBuilder(
getId() + "::" + projectPathWithNamespace,
serverName,
credentialsId,
projectOwner,
projectPathWithNamespace,
name)
.withTraits(traits)
.build(),
null,
(Witness) (name, isMatch) -> {
if (isMatch) {
observer.getListener().getLogger().format("Proposing %s%n", name);
} else {
observer.getListener().getLogger().format("Ignoring %s%n", name);
}
})) {
observer.getListener().getLogger().format("%n%d projects were processed (query complete)%n", count);
return;
}
}
observer.getListener().getLogger().format("%n%d projects were processed%n", count);
} catch (GitLabApiException | URISyntaxException e) {
LOGGER.log(Level.WARNING, "Exception caught:" + e, e);
throw new IOException("Failed to visit SCM source", e);
}
}

@NonNull
private String getProjectName(GitLabApi gitLabApi, int projectNamingStrategy, Project project)
throws URISyntaxException {
String fullPath = project.getPathWithNamespace();
String projectName;
switch (projectNamingStrategy) {
default:
// for legacy reasons default naming strategy is set to Full Project path
case 1:
projectName = fullPath;
break;
case 2:
// Project name
projectName = project.getNameWithNamespace()
.replace(
String.format("%s / ", getGitlabOwner(gitLabApi).getFullName()), "");
break;
case 3:
// Contextual project path
URI ownerPathUri = new URI(projectOwner);
URI fullPathUri = new URI(fullPath);
projectName = ownerPathUri.relativize(fullPathUri).toString();
break;
case 4:
// Simple project path
projectName = fullPath.substring(fullPath.lastIndexOf('/') + 1);
break;
}
return projectName;
}

private StandardCredentials getWebHookCredentials(SCMSourceOwner owner) {
StandardCredentials credentials = null;
GitLabServer server = GitLabServers.get().findServer(getServerName());
if (server == null) {
return null;
}
GitLabSCMNavigatorContext navigatorContext = new GitLabSCMNavigatorContext().withTraits(traits);
GitLabSCMSourceContext ctx =
new GitLabSCMSourceContext(null, SCMHeadObserver.none()).withTraits(navigatorContext.traits());
GitLabHookRegistration webhookMode = ctx.webhookRegistration();
switch (webhookMode) {
case DISABLE:
break;
case SYSTEM:
if (!server.isManageWebHooks()) {
break;
}
credentials = server.getCredentials(owner);
if (credentials == null) {
LOGGER.log(Level.WARNING, "No System credentials added, cannot create web hook");
}
break;
case ITEM:
credentials = credentials(owner);
if (credentials == null) {
LOGGER.log(Level.WARNING, "No Item credentials added, cannot create web hook");
}
break;
default:
return null;
}
return credentials;
}

@NonNull
@Override
protected List<Action> retrieveActions(
@NonNull SCMNavigatorOwner owner, SCMNavigatorEvent event, @NonNull TaskListener listener)
throws IOException, InterruptedException {
getGitlabOwner(owner);
String fullName = gitlabOwner.getFullName();
String webUrl = gitlabOwner.getWebUrl();
String avatarUrl = gitlabOwner.getAvatarUrl();
String description = null;
if (gitlabOwner instanceof GitLabGroup) {
description = ((GitLabGroup) gitlabOwner).getDescription();
}
List<Action> result = new ArrayList<>();
result.add(new ObjectMetadataAction(Util.fixEmpty(fullName), description, webUrl));
if (StringUtils.isNotBlank(avatarUrl)) {
result.add(new GitLabAvatar(avatarUrl));
}
result.add(GitLabLink.toGroup(webUrl));
if (StringUtils.isBlank(webUrl)) {
listener.getLogger().println("Web URL unspecified");
} else {
listener.getLogger()
.printf(
"%s URL: %s%n",
gitlabOwner.getWord(),
HyperlinkNote.encodeTo(webUrl, StringUtils.defaultIfBlank(fullName, webUrl)));
}
return result;
}

@Override
public void afterSave(@NonNull SCMNavigatorOwner owner) {
GitLabSCMNavigatorContext navigatorContext = new GitLabSCMNavigatorContext().withTraits(traits);
GitLabSCMSourceContext ctx =
new GitLabSCMSourceContext(null, SCMHeadObserver.none()).withTraits(navigatorContext.traits());
GitLabHookRegistration systemhookMode = ctx.systemhookRegistration();
GitLabHookCreator.register(owner, this, systemhookMode);
}

public StandardCredentials credentials(SCMSourceOwner owner) {
return CredentialsMatchers.firstOrNull(
lookupCredentials(
StandardCredentials.class,
owner,
Jenkins.getAuthentication(),
fromUri(getServerUrlFromName(serverName)).build()),
new GitLabCredentialMatcher());
}

@Symbol("gitlab")
@Extension
public static class DescriptorImpl extends SCMNavigatorDescriptor implements IconSpec {

@Inject
private GitLabSCMSource.DescriptorImpl delegate;

public static FormValidation doCheckProjectOwner(
@AncestorInPath SCMSourceOwner context,
@QueryParameter String credentialsId,
@QueryParameter String projectOwner,
@QueryParameter String serverName) {
if (projectOwner.equals("")) {
return FormValidation.ok();
}
GitLabApi gitLabApi = null;
try {
gitLabApi = apiBuilder(context, serverName);
gitLabApi = apiBuilder(context, serverName, credentialsId);
GitLabOwner gitLabOwner = GitLabOwner.fetchOwner(gitLabApi, projectOwner);
return FormValidation.ok(projectOwner + " is a valid " + gitLabOwner.getWord());
} catch (IllegalStateException e) {
return FormValidation.error(e, e.getMessage());
}
}

@NonNull
@Override
public String getDisplayName() {
return "GitLab Group";
}

@Override
public String getPronoun() {
return "GitLab Group";
}

@NonNull
@Override
public String getDescription() {
return "Scans a GitLab Group (or GitLab User) for all projects matching some defined markers.";
}

@Override
public String getIconClassName() {
return ICON_GITLAB;
}

@Override
public String getIconFilePathPattern() {
return iconFilePathPattern(getIconClassName());
}

@Override
public SCMNavigator newInstance(String name) {
GitLabSCMNavigator navigator = new GitLabSCMNavigator("");
navigator.setTraits(getTraitsDefaults());
return navigator;
}

public ListBoxModel doFillServerNameItems(
@AncestorInPath SCMSourceOwner context, @QueryParameter String serverName) {
if (context == null) {
if (!Jenkins.get().hasPermission(Jenkins.MANAGE)) {
// must have admin if you want the list without a context
ListBoxModel result = new ListBoxModel();
result.add(serverName);
return result;
}
} else {
if (!context.hasPermission(Item.EXTENDED_READ)) {
// must be able to read the configuration the list
ListBoxModel result = new ListBoxModel();
result.add(serverName);
return result;
}
}
return GitLabServers.get().getServerItems();
}

public ListBoxModel doFillCredentialsIdItems(
@AncestorInPath SCMSourceOwner context,
@QueryParameter String serverName,
@QueryParameter String credentialsId) {
StandardListBoxModel result = new StandardListBoxModel();
if (context == null) {
if (!Jenkins.get().hasPermission(Jenkins.MANAGE)) {
// must have admin if you want the list without a context
result.includeCurrentValue(credentialsId);
return result;
}
} else {
if (!context.hasPermission(Item.EXTENDED_READ)
&& !context.hasPermission(CredentialsProvider.USE_ITEM)) {
// must be able to read the configuration or use the item credentials if you
// want the list
result.includeCurrentValue(credentialsId);
return result;
}
}
result.includeEmptyValue();
result.includeMatchingAs(
context instanceof Queue.Task ? ((Queue.Task) context).getDefaultAuthentication() : ACL.SYSTEM,
context,
StandardUsernameCredentials.class,
StandardCredentials.class,
fromUri(getServerUrlFromName(serverName)).build(),
GitClient.CREDENTIALS_MATCHER);
CredentialsMatchers.anyOf(GitClient.CREDENTIALS_MATCHER, GitLabServer.CREDENTIALS_MATCHER));

Check warning on line 558 in src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMNavigator.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 200-558 are not covered by tests
return result;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardListBoxModel;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
Expand All @@ -31,6 +32,7 @@
import hudson.util.ListBoxModel;
import io.jenkins.plugins.gitlabbranchsource.helpers.GitLabAvatar;
import io.jenkins.plugins.gitlabbranchsource.helpers.GitLabLink;
import io.jenkins.plugins.gitlabserverconfig.credentials.GroupAccessToken;
import io.jenkins.plugins.gitlabserverconfig.credentials.PersonalAccessToken;
import io.jenkins.plugins.gitlabserverconfig.servers.GitLabServer;
import io.jenkins.plugins.gitlabserverconfig.servers.GitLabServers;
Expand Down Expand Up @@ -203,7 +205,7 @@

protected Project getGitlabProject() {
if (gitlabProject == null) {
getGitlabProject(apiBuilder(this.getOwner(), serverName));
getGitlabProject(apiBuilder(this.getOwner(), serverName, credentialsId));

Check warning on line 208 in src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 208 is not covered by tests
}
return gitlabProject;
}
Expand All @@ -226,7 +228,7 @@
public HashMap<String, AccessLevel> getMembers() {
HashMap<String, AccessLevel> members = new HashMap<>();
try {
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName);
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName, credentialsId);
for (Member m : gitLabApi.getProjectApi().getAllMembers(projectPath)) {
members.put(m.getUsername(), m.getAccessLevel());
}
Expand Down Expand Up @@ -261,7 +263,7 @@
protected SCMRevision retrieve(@NonNull SCMHead head, @NonNull TaskListener listener)
throws IOException, InterruptedException {
try {
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName);
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName, credentialsId);

Check warning on line 266 in src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 266 is not covered by tests
getGitlabProject(gitLabApi);
if (head instanceof BranchSCMHead) {
listener.getLogger().format("Querying the current revision of branch %s...%n", head.getName());
Expand Down Expand Up @@ -323,7 +325,7 @@
@NonNull TaskListener listener)
throws IOException, InterruptedException {
try {
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName);
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName, credentialsId);
getGitlabProject(gitLabApi);
GitLabSCMSourceContext ctx = new GitLabSCMSourceContext(criteria, observer).withTraits(getTraits());
try (GitLabSCMSourceRequest request = ctx.newRequest(this, listener)) {
Expand Down Expand Up @@ -728,72 +730,77 @@
if (builder == null) {
throw new AssertionError();
}
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName);
GitLabApi gitLabApi = apiBuilder(this.getOwner(), serverName, credentialsId);
getGitlabProject(gitLabApi);
final SCMFileSystem fs = builder.build(head, revision, gitLabApi, projectPath);
return new SCMProbe() {
@NonNull
@Override
public SCMProbeStat stat(@NonNull String path) throws IOException {
try {
return SCMProbeStat.fromType(fs.child(path).getType());
} catch (InterruptedException e) {
throw new IOException("Interrupted", e);
}
}

@Override
public void close() throws IOException {
Objects.requireNonNull(fs).close();
}

@Override
public String name() {
return head.getName();
}

@Override
public long lastModified() {
try {
return fs != null ? fs.lastModified() : 0;
} catch (IOException | InterruptedException e) {
return 0L;
}
}

@Override
public SCMFile getRoot() {
return fs != null ? fs.getRoot() : null;
}
};
} catch (InterruptedException e) {
throw new IOException(e);
}
}

@Override
public void afterSave() {
GitLabServer server = GitLabServers.get().findServer(getServerName());
// Only register webhooks in the case webhooks wants to be managed in
// the jenkins instance.
if (server != null && server.isManageWebHooks()) {
GitLabSCMSourceContext ctx = new GitLabSCMSourceContext(null, SCMHeadObserver.none())
.withTraits(
new GitLabSCMNavigatorContext().withTraits(traits).traits());
GitLabHookRegistration webhookMode = ctx.webhookRegistration();
GitLabHookRegistration systemhookMode = ctx.systemhookRegistration();
GitLabHookCreator.register(this, webhookMode, systemhookMode);
}
}

public PersonalAccessToken credentials() {
return CredentialsMatchers.firstOrNull(
lookupCredentials(
PersonalAccessToken.class,
getOwner(),
Jenkins.getAuthentication(),
fromUri(getServerUrlFromName(serverName)).build()),
GitLabServer.CREDENTIALS_MATCHER);
public StandardCredentials credentials() {
List<StandardCredentials> list = new ArrayList<>();
list.addAll(lookupCredentials(
PersonalAccessToken.class,
getOwner(),
Jenkins.getAuthentication(),
fromUri(getServerUrlFromName(serverName)).build()));
list.addAll(lookupCredentials(
GroupAccessToken.class,
getOwner(),
Jenkins.getAuthentication(),
fromUri(getServerUrlFromName(serverName)).build()));
return CredentialsMatchers.firstOrNull(list, GitLabServer.CREDENTIALS_MATCHER);

Check warning on line 803 in src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 733-803 are not covered by tests
}

@Symbol("gitlab")
Expand Down Expand Up @@ -865,13 +872,14 @@
context,
StandardUsernameCredentials.class,
fromUri(getServerUrlFromName(serverName)).build(),
GitClient.CREDENTIALS_MATCHER);
CredentialsMatchers.anyOf(GitClient.CREDENTIALS_MATCHER, GitLabServer.CREDENTIALS_MATCHER));
return result;
}

public long getProjectId(
@AncestorInPath SCMSourceOwner context,
@QueryParameter String projectPath,
@QueryParameter String credentialsId,
@QueryParameter String serverName) {
List<GitLabServer> gitLabServers = GitLabServers.get().getServers();
if (gitLabServers.size() == 0) {
Expand All @@ -880,34 +888,35 @@
try {
GitLabApi gitLabApi;
if (StringUtils.isBlank(serverName)) {
gitLabApi = apiBuilder(context, gitLabServers.get(0).getName());
gitLabApi = apiBuilder(context, gitLabServers.get(0).getName(), credentialsId);
} else {
gitLabApi = apiBuilder(context, serverName);
gitLabApi = apiBuilder(context, serverName, credentialsId);
}
if (StringUtils.isNotBlank(projectPath)) {
return gitLabApi.getProjectApi().getProject(projectPath).getId();
}
} catch (GitLabApiException e) {
return -1;
}
return -1;
}

public ListBoxModel doFillProjectPathItems(
@AncestorInPath SCMSourceOwner context,
@QueryParameter String credentialsId,
@QueryParameter String serverName,
@QueryParameter String projectOwner) {
List<GitLabServer> gitLabServers = GitLabServers.get().getServers();
if (gitLabServers.size() == 0) {
return new StandardListBoxModel().includeEmptyValue();
}
ListBoxModel result = new ListBoxModel();
try {
GitLabApi gitLabApi;
if (serverName.equals("")) {
gitLabApi = apiBuilder(context, gitLabServers.get(0).getName());
gitLabApi = apiBuilder(context, gitLabServers.get(0).getName(), credentialsId);

Check warning on line 917 in src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 891-917 are not covered by tests
} else {
gitLabApi = apiBuilder(context, serverName);
gitLabApi = apiBuilder(context, serverName, credentialsId);
}

if (projectOwner.equals("")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
package io.jenkins.plugins.gitlabbranchsource.helpers;

import static com.cloudbees.plugins.credentials.CredentialsMatchers.withId;
import static com.cloudbees.plugins.credentials.CredentialsProvider.lookupCredentials;
import static com.cloudbees.plugins.credentials.domains.URIRequirementBuilder.fromUri;
import static org.apache.commons.lang.StringUtils.defaultIfBlank;

import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.damnhandy.uri.template.UriTemplate;
import com.damnhandy.uri.template.UriTemplateBuilder;
import com.damnhandy.uri.template.impl.Operator;
import hudson.ProxyConfiguration;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.security.ACL;
import hudson.security.AccessControlled;
import io.jenkins.plugins.gitlabserverconfig.credentials.GroupAccessToken;
import io.jenkins.plugins.gitlabserverconfig.credentials.PersonalAccessToken;
import io.jenkins.plugins.gitlabserverconfig.servers.GitLabServer;
import io.jenkins.plugins.gitlabserverconfig.servers.GitLabServers;
Expand All @@ -15,17 +25,22 @@
import java.util.Map;
import java.util.regex.Pattern;
import jenkins.model.Jenkins;
import org.apache.commons.lang.StringUtils;
import org.eclipse.jgit.annotations.NonNull;
import org.gitlab4j.api.GitLabApi;
import org.gitlab4j.api.ProxyClientConfig;
import org.jenkinsci.plugins.plaincredentials.StringCredentials;

public class GitLabHelper {

public static GitLabApi apiBuilder(AccessControlled context, String serverName) {
public static GitLabApi apiBuilder(AccessControlled context, String serverName, String credentialsId) {
return apiBuilder(context, serverName, getCredential(credentialsId, serverName, context));
}

public static GitLabApi apiBuilder(AccessControlled context, String serverName, StandardCredentials credential) {
GitLabServer server = GitLabServers.get().findServer(serverName);
if (server != null) {
StandardCredentials credentials = server.getCredentials(context);
StandardCredentials credentials = credential != null ? credential : server.getCredentials(context);

Check warning on line 43 in src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 43 is only partially covered, one branch is missing
String serverUrl = server.getServerUrl();
String privateToken = getPrivateTokenAsPlainText(credentials);
if (privateToken.equals(GitLabServer.EMPTY_TOKEN)) {
Expand Down Expand Up @@ -106,6 +121,9 @@
if (credentials instanceof PersonalAccessToken) {
privateToken = ((PersonalAccessToken) credentials).getToken().getPlainText();
}
if (credentials instanceof GroupAccessToken) {
privateToken = ((GroupAccessToken) credentials).getToken().getPlainText();

Check warning on line 125 in src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 124-125 are not covered by tests
}
if (credentials instanceof StringCredentials) {
privateToken = ((StringCredentials) credentials).getSecret().getPlainText();
}
Expand Down Expand Up @@ -148,4 +166,32 @@
public static String[] splitPath(String path) {
return path.split(Operator.PATH.getSeparator());
}

public static StandardCredentials getCredential(String credentialsId, String serverName, AccessControlled context) {
if (StringUtils.isNotBlank(credentialsId)) {
if (context instanceof ItemGroup) {

Check warning on line 172 in src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Partially covered line

Line 172 is only partially covered, one branch is missing
return CredentialsMatchers.firstOrNull(
lookupCredentials(
StandardCredentials.class,
(ItemGroup) context,
ACL.SYSTEM,
fromUri(defaultIfBlank(
getServerUrlFromName(serverName), GitLabServer.GITLAB_SERVER_URL))
.build()),
withId(credentialsId));
} else {
return CredentialsMatchers.firstOrNull(
lookupCredentials(
StandardCredentials.class,
(Item) context,
ACL.SYSTEM,
fromUri(defaultIfBlank(
getServerUrlFromName(serverName), GitLabServer.GITLAB_SERVER_URL))
.build()),
withId(credentialsId));

Check warning on line 191 in src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 183-191 are not covered by tests
}
}

return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,274 +207,277 @@
String suffix = " - [Details](" + url + ")";
SCMRevision revision = SCMRevisionAction.getRevision(source, build);
try {
GitLabApi gitLabApi = GitLabHelper.apiBuilder(build.getParent(), source.getServerName());
GitLabApi gitLabApi =
GitLabHelper.apiBuilder(build.getParent(), source.getServerName(), source.getCredentialsId());
String sudoUsername = sourceContext.getSudoUser();
if (!sudoUsername.isEmpty()) {
gitLabApi.sudo(sudoUsername);
}
try {
EnvVars envVars = build.getEnvironment(listener);
final String buildName = "**" + getStatusName(sourceContext, build, envVars, revision) + ":** ";
final String hash;
if (revision instanceof BranchSCMRevision) {
hash = ((BranchSCMRevision) revision).getHash();
gitLabApi
.getCommitsApi()
.addComment(source.getProjectPath(), hash, symbol + buildName + note + suffix);
} else if (revision instanceof MergeRequestSCMRevision) {
MergeRequestSCMHead head = (MergeRequestSCMHead) revision.getHead();
gitLabApi
.getNotesApi()
.createMergeRequestNote(
source.getProjectPath(),
Long.valueOf(head.getId()),
symbol + buildName + note + suffix);
} else if (revision instanceof GitTagSCMRevision) {
hash = ((GitTagSCMRevision) revision).getHash();
gitLabApi
.getCommitsApi()
.addComment(source.getProjectPath(), hash, symbol + buildName + note + suffix);
}
} catch (IOException | InterruptedException e) {
LOGGER.log(
Level.INFO,
"Could not send status notification for " + build.getFullDisplayName() + " to "
+ source.getServerName(),
e);
}
} catch (GitLabApiException e) {
LOGGER.log(Level.WARNING, "Exception caught:" + e, e);
}
}

/**
* Retrieves the source project ID for a merge request
*/
static Long getSourceProjectId(Job job, GitLabApi gitLabApi, String projectPath) {
LOGGER.log(Level.INFO, "Getting source project ID from MR");
Matcher m = MERGE_REQUEST_JOB_NAME_FORMAT.matcher(job.getName());
if (!m.matches()) {
LOGGER.log(
Level.WARNING,
String.format(
"Job name does not match expected format: [%s], [%s]",
job.getName(), MERGE_REQUEST_JOB_NAME_FORMAT.pattern()));
return null;
}

Long mrId = Long.parseLong(m.group(1));
MergeRequest mr;
try {
mr = gitLabApi.getMergeRequestApi().getMergeRequest(projectPath, mrId);
} catch (GitLabApiException e) {
if (!e.getMessage().contains(("Cannot transition status"))) {
LOGGER.log(Level.WARNING, String.format("Exception caught: %s", e.getMessage()));
}
return null;
}
Long sourceProjectId = mr.getSourceProjectId();
LOGGER.log(Level.INFO, "Got source project ID from MR: {0}", String.valueOf(sourceProjectId));

return sourceProjectId;
}

/**
* Sends notifications to GitLab on Checkout (for the "In Progress" Status).
*/
private static void sendNotifications(Run<?, ?> build, TaskListener listener, Boolean useResult) {
GitLabSCMSource source = getSource(build);
if (source == null) {
return;
}
final GitLabSCMSourceContext sourceContext = getSourceContext(build, source);
if (sourceContext.notificationsDisabled()) {
return;
}
String url = getRootUrl(build);
if (url.isEmpty()) {
listener.getLogger()
.println(
"Can not determine Jenkins root URL. Commit status notifications are disabled until a root URL is"
+ " configured in Jenkins global configuration.");
return;
}
Result result = null;
if (useResult) {
result = build.getResult();
LOGGER.log(Level.FINE, String.format("Result: %s", result));
}

CommitStatus status = new CommitStatus();
Constants.CommitBuildState state;
status.setTargetUrl(url);

if (Result.SUCCESS.equals(result)) {
status.setDescription(build.toString() + ": This commit looks good.");
status.setStatus("SUCCESS");
state = Constants.CommitBuildState.SUCCESS;
} else if (Result.UNSTABLE.equals(result)) {
status.setDescription(build.toString() + ": This commit is unstable with partial failure.");
if (sourceContext.getMarkUnstableAsSuccess()) {
status.setStatus("SUCCESS");
state = Constants.CommitBuildState.SUCCESS;
} else {
status.setStatus("FAILED");
state = Constants.CommitBuildState.FAILED;
}
} else if (Result.FAILURE.equals(result)) {
status.setDescription(build.toString() + ": There was a failure building this commit.");
status.setStatus("FAILED");
state = Constants.CommitBuildState.FAILED;
} else if (result != null) { // ABORTED, NOT_BUILT.
status.setDescription(build.toString() + ": Something is wrong with the build of this commit.");
status.setStatus("CANCELED");
state = Constants.CommitBuildState.CANCELED;
} else {
status.setDescription(build.toString() + ": Build started...");
status.setStatus("RUNNING");
state = Constants.CommitBuildState.RUNNING;
}

final SCMRevision revision = SCMRevisionAction.getRevision(source, build);
String hash;
if (revision instanceof BranchSCMRevision) {
listener.getLogger()
.format(
"[GitLab Pipeline Status] Notifying branch build status: %s %s%n",
status.getStatus(), status.getDescription());
hash = ((BranchSCMRevision) revision).getHash();
} else if (revision instanceof MergeRequestSCMRevision) {
listener.getLogger()
.format(
"[GitLab Pipeline Status] Notifying merge request build status: %s %s%n",
status.getStatus(), status.getDescription());
hash = ((MergeRequestSCMRevision) revision).getOrigin().getHash();
} else if (revision instanceof GitTagSCMRevision) {
listener.getLogger()
.format(
"[GitLab Pipeline Status] Notifying tag build status: %s %s%n",
status.getStatus(), status.getDescription());
hash = ((GitTagSCMRevision) revision).getHash();
} else {
return;
}
try {
EnvVars envVars = build.getEnvironment(listener);
status.setName(getStatusName(sourceContext, build, envVars, revision));
status.setRef(getRevisionRef(revision));

final JobScheduledListener jsl =
ExtensionList.lookup(QueueListener.class).get(JobScheduledListener.class);
if (jsl != null) {
// we are setting the status, so don't let the queue listener background thread
// change it to pending
synchronized (jsl.resolving) {
jsl.resolving.remove(build.getParent());
}
}
try {
GitLabApi gitLabApi = GitLabHelper.apiBuilder(build.getParent(), source.getServerName());
GitLabApi gitLabApi =
GitLabHelper.apiBuilder(build.getParent(), source.getServerName(), source.getCredentialsId());
LOGGER.log(Level.FINE, String.format("Notifiying commit: %s", hash));

if (revision instanceof MergeRequestSCMRevision) {
Long projectId = getSourceProjectId(build.getParent(), gitLabApi, source.getProjectPath());
status.setRef(((MergeRequestSCMRevision) revision)
.getOrigin()
.getHead()
.getName());
gitLabApi.getCommitsApi().addCommitStatus(projectId, hash, state, status);
} else {
gitLabApi.getCommitsApi().addCommitStatus(source.getProjectPath(), hash, state, status);
}

listener.getLogger().format("[GitLab Pipeline Status] Notified%n");
} catch (GitLabApiException e) {
if (!e.getMessage().contains(("Cannot transition status"))) {
LOGGER.log(Level.WARNING, String.format("Exception caught: %s", e.getMessage()));
}
}
} catch (IOException | InterruptedException e) {
LOGGER.log(
Level.INFO,
"Could not send status notification for " + build.getFullDisplayName() + " to "
+ source.getServerName(),
e);
}
}

@Extension
public static class JobScheduledListener extends QueueListener {

private final AtomicLong nonce = new AtomicLong();
private final Map<Job, Long> resolving = new HashMap<>();

/**
* Manages the GitLab Commit Pending Status.
*/
@Override
public void onEnterWaiting(final Queue.WaitingItem wi) {
if (!(wi.task instanceof Job)) {
return;
}
final Job<?, ?> job = (Job) wi.task;
LOGGER.log(Level.FINE, String.format("QueueListener: Waiting > %s", job.getFullDisplayName()));
final SCMSource src = SCMSource.SourceByItem.findSource(job);
if (!(src instanceof GitLabSCMSource)) {
return;
}
final GitLabSCMSource source = (GitLabSCMSource) src;
final GitLabSCMSourceContext sourceContext =
new GitLabSCMSourceContext(null, SCMHeadObserver.none()).withTraits((source.getTraits()));
if (sourceContext.notificationsDisabled()) {
return;
}
final SCMHead head = SCMHead.HeadByItem.findHead(job);
if (head == null) {
return;
}
final Long nonce = this.nonce.incrementAndGet();
synchronized (resolving) {
resolving.put(job, nonce);
}
// prevent delays in the queue when updating GitLab
Computer.threadPoolForRemoting.submit(() -> {
try (ACLContext ctx = ACL.as(Tasks.getAuthenticationOf(wi.task))) {
final TaskListener listener = new LogTaskListener(LOGGER, Level.INFO);
final SCMRevision revision = source.fetch(head, listener);
String hash;
final CommitStatus status = new CommitStatus();
if (revision instanceof BranchSCMRevision) {
LOGGER.log(Level.INFO, "Notifying branch pending build {0}", job.getFullName());
hash = ((BranchSCMRevision) revision).getHash();
} else if (revision instanceof MergeRequestSCMRevision) {
LOGGER.log(Level.INFO, "Notifying merge request pending build {0}", job.getFullName());
hash = ((MergeRequestSCMRevision) revision).getOrigin().getHash();
} else if (revision instanceof GitTagSCMRevision) {
LOGGER.log(Level.INFO, "Notifying tag pending build {0}", job.getFullName());
hash = ((GitTagSCMRevision) revision).getHash();
} else {
return;
}
Computer c = Computer.currentComputer();
Node n = c == null ? null : c.getNode();
EnvVars envVars = job.getEnvironment(n, listener);
status.setName(getStatusName(sourceContext, job, envVars, revision));
status.setRef(getRevisionRef(revision));

String url;
try {
url = DisplayURLProvider.get().getJobURL(job);
} catch (IllegalStateException e) {
// no root url defined, cannot notify, let's get out of here
return;
}
status.setTargetUrl(url);
status.setDescription(job.getFullName() + ": Build queued...");
status.setStatus("PENDING");

Constants.CommitBuildState state = Constants.CommitBuildState.PENDING;
try {
GitLabApi gitLabApi = GitLabHelper.apiBuilder(job, source.getServerName());
GitLabApi gitLabApi =
GitLabHelper.apiBuilder(job, source.getServerName(), source.getCredentialsId());

Check warning on line 480 in src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabPipelineStatusNotifier.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered lines

Lines 210-480 are not covered by tests
// check are we still the task to set pending
synchronized (resolving) {
if (!nonce.equals(resolving.get(job))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@

@RequirePOST
public HttpResponse doProjectList(
@AncestorInPath SCMSourceOwner context, @QueryParameter String server, @QueryParameter String owner) {
@AncestorInPath SCMSourceOwner context,
@QueryParameter String server,
@QueryParameter String credentialsId,
@QueryParameter String owner) {
if (!Jenkins.get().hasPermission(Jenkins.MANAGE)) {
return HttpResponses.errorJSON("no permission to get Gitlab server list");
}
Expand All @@ -62,7 +65,7 @@

JSONArray servers = new JSONArray();

GitLabApi gitLabApi = GitLabHelper.apiBuilder(context, server);
GitLabApi gitLabApi = GitLabHelper.apiBuilder(context, server, credentialsId);

Check warning on line 68 in src/main/java/io/jenkins/plugins/gitlabserverconfig/action/GitlabAction.java

View check run for this annotation

ci.jenkins.io / Code Coverage

Not covered line

Line 68 is not covered by tests
try {
for (Project project :
gitLabApi.getProjectApi().getUserProjects(owner, new ProjectFilter().withOwned(true))) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.jenkins.plugins.gitlabserverconfig.credentials;

import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.util.Secret;

public interface GroupAccessToken extends StandardUsernamePasswordCredentials {

/**
* Returns the token.
*
* @return the token.
*/
@NonNull
Secret getToken();
}
Loading