diff --git a/README.md b/README.md index c2da6e6c..ccebd607 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMFileSystem.java b/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMFileSystem.java index d1acf6e3..5eb05b98 100644 --- a/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMFileSystem.java +++ b/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMFileSystem.java @@ -84,7 +84,8 @@ public SCMFileSystem build(@NonNull Item owner, @NonNull SCM scm, @CheckForNull 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()); String projectPath = gitlabScmSource.getProjectPath(); return build(head, rev, gitLabApi, projectPath); } diff --git a/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMNavigator.java b/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMNavigator.java index 4b4091b1..a60739b6 100644 --- a/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMNavigator.java +++ b/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMNavigator.java @@ -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; @@ -198,7 +197,7 @@ public void setTraits(@CheckForNull SCMTrait[] traits) { private GitLabOwner getGitlabOwner(SCMNavigatorOwner owner) { if (gitlabOwner == null) { - getGitlabOwner(apiBuilder(owner, serverName)); + getGitlabOwner(apiBuilder(owner, serverName, credentialsId)); } return gitlabOwner; } @@ -235,7 +234,7 @@ protected String id() { 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 projects; if (gitlabOwner instanceof GitLabUser) { @@ -459,6 +458,7 @@ public static class DescriptorImpl extends SCMNavigatorDescriptor implements Ico public static FormValidation doCheckProjectOwner( @AncestorInPath SCMSourceOwner context, + @QueryParameter String credentialsId, @QueryParameter String projectOwner, @QueryParameter String serverName) { if (projectOwner.equals("")) { @@ -466,7 +466,7 @@ public static FormValidation doCheckProjectOwner( } 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) { @@ -552,9 +552,9 @@ public ListBoxModel doFillCredentialsIdItems( 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)); return result; } diff --git a/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java b/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java index 3e65766d..b61697fb 100644 --- a/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java +++ b/src/main/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSource.java @@ -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; @@ -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; @@ -203,7 +205,7 @@ public String getRemote() { protected Project getGitlabProject() { if (gitlabProject == null) { - getGitlabProject(apiBuilder(this.getOwner(), serverName)); + getGitlabProject(apiBuilder(this.getOwner(), serverName, credentialsId)); } return gitlabProject; } @@ -226,7 +228,7 @@ protected Project getGitlabProject(GitLabApi gitLabApi) { public HashMap getMembers() { HashMap 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()); } @@ -261,7 +263,7 @@ public void setTraits(List traits) { 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); getGitlabProject(gitLabApi); if (head instanceof BranchSCMHead) { listener.getLogger().format("Querying the current revision of branch %s...%n", head.getName()); @@ -323,7 +325,7 @@ protected void retrieve( @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)) { @@ -728,7 +730,7 @@ protected SCMProbe createProbe(@NonNull final SCMHead head, SCMRevision revision 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() { @@ -786,14 +788,19 @@ public void afterSave() { } } - public PersonalAccessToken credentials() { - return CredentialsMatchers.firstOrNull( - lookupCredentials( - PersonalAccessToken.class, - getOwner(), - Jenkins.getAuthentication(), - fromUri(getServerUrlFromName(serverName)).build()), - GitLabServer.CREDENTIALS_MATCHER); + public StandardCredentials credentials() { + List 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); } @Symbol("gitlab") @@ -865,13 +872,14 @@ public ListBoxModel doFillCredentialsIdItems( 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 gitLabServers = GitLabServers.get().getServers(); if (gitLabServers.size() == 0) { @@ -880,9 +888,9 @@ public long getProjectId( 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(); @@ -895,6 +903,7 @@ public long getProjectId( public ListBoxModel doFillProjectPathItems( @AncestorInPath SCMSourceOwner context, + @QueryParameter String credentialsId, @QueryParameter String serverName, @QueryParameter String projectOwner) { List gitLabServers = GitLabServers.get().getServers(); @@ -905,9 +914,9 @@ public ListBoxModel doFillProjectPathItems( try { GitLabApi gitLabApi; if (serverName.equals("")) { - 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 (projectOwner.equals("")) { diff --git a/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java b/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java index 49e83c2a..142e40c1 100644 --- a/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java +++ b/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabHelper.java @@ -1,11 +1,20 @@ 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 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; @@ -15,6 +24,7 @@ import java.util.Map; import java.util.regex.Pattern; import jenkins.model.Jenkins; +import org.apache.commons.lang3.StringUtils; import org.eclipse.jgit.annotations.NonNull; import org.gitlab4j.api.GitLabApi; import org.gitlab4j.api.ProxyClientConfig; @@ -22,10 +32,14 @@ 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); String serverUrl = server.getServerUrl(); String privateToken = getPrivateTokenAsPlainText(credentials); if (privateToken.equals(GitLabServer.EMPTY_TOKEN)) { @@ -106,6 +120,9 @@ public static String getPrivateTokenAsPlainText(StandardCredentials credentials) if (credentials instanceof PersonalAccessToken) { privateToken = ((PersonalAccessToken) credentials).getToken().getPlainText(); } + if (credentials instanceof GroupAccessToken) { + privateToken = ((GroupAccessToken) credentials).getToken().getPlainText(); + } if (credentials instanceof StringCredentials) { privateToken = ((StringCredentials) credentials).getSecret().getPlainText(); } @@ -148,4 +165,32 @@ public static UriTemplate commitUriTemplate(String serverNameOrUrl) { 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) { + return CredentialsMatchers.firstOrNull( + lookupCredentials( + StandardCredentials.class, + (ItemGroup) context, + ACL.SYSTEM, + fromUri(StringUtils.defaultIfBlank( + getServerUrlFromName(serverName), GitLabServer.GITLAB_SERVER_URL)) + .build()), + withId(credentialsId)); + } else { + return CredentialsMatchers.firstOrNull( + lookupCredentials( + StandardCredentials.class, + (Item) context, + ACL.SYSTEM, + fromUri(StringUtils.defaultIfBlank( + getServerUrlFromName(serverName), GitLabServer.GITLAB_SERVER_URL)) + .build()), + withId(credentialsId)); + } + } + + return null; + } } diff --git a/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabPipelineStatusNotifier.java b/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabPipelineStatusNotifier.java index b8154a30..38fe31c2 100644 --- a/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabPipelineStatusNotifier.java +++ b/src/main/java/io/jenkins/plugins/gitlabbranchsource/helpers/GitLabPipelineStatusNotifier.java @@ -207,7 +207,8 @@ private static void logComment(Run build, TaskListener listener) { 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); @@ -375,7 +376,8 @@ private static void sendNotifications(Run build, TaskListener listener, Bo } } 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) { @@ -476,7 +478,8 @@ public void onEnterWaiting(final Queue.WaitingItem wi) { Constants.CommitBuildState state = Constants.CommitBuildState.PENDING; try { - GitLabApi gitLabApi = GitLabHelper.apiBuilder(job, source.getServerName()); + GitLabApi gitLabApi = + GitLabHelper.apiBuilder(job, source.getServerName(), source.getCredentialsId()); // check are we still the task to set pending synchronized (resolving) { if (!nonce.equals(resolving.get(job))) { diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/action/GitlabAction.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/action/GitlabAction.java index aa3c2f15..2a679930 100644 --- a/src/main/java/io/jenkins/plugins/gitlabserverconfig/action/GitlabAction.java +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/action/GitlabAction.java @@ -51,7 +51,10 @@ public HttpResponse doServerList() { @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"); } @@ -62,7 +65,7 @@ public HttpResponse doProjectList( JSONArray servers = new JSONArray(); - GitLabApi gitLabApi = GitLabHelper.apiBuilder(context, server); + GitLabApi gitLabApi = GitLabHelper.apiBuilder(context, server, credentialsId); try { for (Project project : gitLabApi.getProjectApi().getUserProjects(owner, new ProjectFilter().withOwned(true))) { diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessToken.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessToken.java new file mode 100644 index 00000000..4d5ae193 --- /dev/null +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessToken.java @@ -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(); +} diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImpl.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImpl.java new file mode 100644 index 00000000..72f3a30e --- /dev/null +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImpl.java @@ -0,0 +1,115 @@ +package io.jenkins.plugins.gitlabserverconfig.credentials; + +import com.cloudbees.plugins.credentials.CredentialsDescriptor; +import com.cloudbees.plugins.credentials.CredentialsProvider; +import com.cloudbees.plugins.credentials.CredentialsScope; +import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; +import edu.umd.cs.findbugs.annotations.CheckForNull; +import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; +import hudson.Extension; +import hudson.util.FormValidation; +import hudson.util.Secret; +import jenkins.model.Jenkins; +import org.apache.commons.lang3.StringUtils; +import org.jenkinsci.Symbol; +import org.kohsuke.accmod.Restricted; +import org.kohsuke.accmod.restrictions.NoExternalUse; +import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; +import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; + +/** + * Default implementation of {@link GroupAccessToken} for use by {@link Jenkins} {@link + * CredentialsProvider} instances that store {@link Secret} locally. + */ +public class GroupAccessTokenImpl extends BaseStandardCredentials implements GroupAccessToken { + + /** + * Our token. + */ + @Nullable + private Secret token; + + /** + * Constructor. + * + * @param scope the credentials scope. + * @param id the credentials id. + * @param description the description of the token. + */ + @DataBoundConstructor + public GroupAccessTokenImpl( + @CheckForNull CredentialsScope scope, @CheckForNull String id, @CheckForNull String description) { + super(scope, id, description); + } + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public Secret getToken() { + return token; + } + + @DataBoundSetter + public void setToken(String token) { + this.token = Secret.fromString(token); + } + + @NonNull + @Override + public String getUsername() { + return "any-value-here-is-fine"; + } + + @NonNull + @Override + public Secret getPassword() { + return token; + } + + /** + * Our descriptor. + */ + @Extension + @Symbol("gitlabGroupAccessToken") + public static class DescriptorImpl extends CredentialsDescriptor { + + private static final int GITLAB_ACCESS_TOKEN_MINIMAL_LENGTH = 20; + + /** + * {@inheritDoc} + */ + @Override + @NonNull + public String getDisplayName() { + return Messages.GroupAccessTokenImpl_displayName(); + } + + /** + * Sanity check for a Gitlab access token. + * + * @param value the group access token. + * @return the results of the sanity check. + */ + @Restricted(NoExternalUse.class) // stapler + @SuppressWarnings("unused") + @RequirePOST + public FormValidation doCheckToken(@QueryParameter String value) { + Jenkins.get().checkPermission(CredentialsProvider.USE_OWN); + + Secret secret = Secret.fromString(value); + if (StringUtils.equals(value, secret.getPlainText())) { + if (value.length() < GITLAB_ACCESS_TOKEN_MINIMAL_LENGTH) { + return FormValidation.error(Messages.GroupAccessTokenImpl_tokenWrongLength()); + } + } else if (secret.getPlainText().length() < GITLAB_ACCESS_TOKEN_MINIMAL_LENGTH) { + return FormValidation.error(Messages.GroupAccessTokenImpl_tokenWrongLength()); + } + return FormValidation.ok(); + } + } +} diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessToken.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessToken.java index ec4b9a5c..e14fd8da 100644 --- a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessToken.java +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessToken.java @@ -1,10 +1,10 @@ package io.jenkins.plugins.gitlabserverconfig.credentials; -import com.cloudbees.plugins.credentials.common.StandardCredentials; +import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.util.Secret; -public interface PersonalAccessToken extends StandardCredentials { +public interface PersonalAccessToken extends StandardUsernamePasswordCredentials { /** * Returns the token. diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImpl.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImpl.java index 511aaf2e..1d63202e 100644 --- a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImpl.java +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImpl.java @@ -6,6 +6,7 @@ import com.cloudbees.plugins.credentials.impl.BaseStandardCredentials; import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; +import edu.umd.cs.findbugs.annotations.Nullable; import hudson.Extension; import hudson.util.FormValidation; import hudson.util.Secret; @@ -15,7 +16,9 @@ import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; import org.kohsuke.stapler.DataBoundConstructor; +import org.kohsuke.stapler.DataBoundSetter; import org.kohsuke.stapler.QueryParameter; +import org.kohsuke.stapler.interceptor.RequirePOST; /** * Default implementation of {@link PersonalAccessToken} for use by {@link Jenkins} {@link @@ -26,8 +29,8 @@ public class PersonalAccessTokenImpl extends BaseStandardCredentials implements /** * Our token. */ - @NonNull - private final Secret token; + @Nullable + private Secret token; /** * Constructor. @@ -35,16 +38,11 @@ public class PersonalAccessTokenImpl extends BaseStandardCredentials implements * @param scope the credentials scope. * @param id the credentials id. * @param description the description of the token. - * @param token the token itself (will be passed through {@link Secret#fromString(String)}) */ @DataBoundConstructor public PersonalAccessTokenImpl( - @CheckForNull CredentialsScope scope, - @CheckForNull String id, - @CheckForNull String description, - @NonNull String token) { + @CheckForNull CredentialsScope scope, @CheckForNull String id, @CheckForNull String description) { super(scope, id, description); - this.token = Secret.fromString(token); } /** @@ -56,6 +54,23 @@ public Secret getToken() { return token; } + @DataBoundSetter + public void setToken(String token) { + this.token = Secret.fromString(token); + } + + @NonNull + @Override + public String getUsername() { + return "any-value-here-is-fine"; + } + + @NonNull + @Override + public Secret getPassword() { + return token; + } + /** * Our descriptor. */ @@ -82,7 +97,10 @@ public String getDisplayName() { */ @Restricted(NoExternalUse.class) // stapler @SuppressWarnings("unused") + @RequirePOST public FormValidation doCheckToken(@QueryParameter String value) { + Jenkins.get().checkPermission(CredentialsProvider.USE_OWN); + Secret secret = Secret.fromString(value); if (StringUtils.equals(value, secret.getPlainText())) { if (value.length() < GITLAB_ACCESS_TOKEN_MINIMAL_LENGTH) { diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/helpers/GitLabCredentialMatcher.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/helpers/GitLabCredentialMatcher.java index cda8dbda..31ec4fe0 100644 --- a/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/helpers/GitLabCredentialMatcher.java +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/credentials/helpers/GitLabCredentialMatcher.java @@ -3,6 +3,7 @@ import com.cloudbees.plugins.credentials.Credentials; import com.cloudbees.plugins.credentials.CredentialsMatcher; import edu.umd.cs.findbugs.annotations.NonNull; +import io.jenkins.plugins.gitlabserverconfig.credentials.GroupAccessToken; import io.jenkins.plugins.gitlabserverconfig.credentials.PersonalAccessToken; import org.jenkinsci.plugins.plaincredentials.StringCredentials; @@ -13,7 +14,9 @@ public class GitLabCredentialMatcher implements CredentialsMatcher { @Override public boolean matches(@NonNull Credentials credentials) { try { - return credentials instanceof PersonalAccessToken || credentials instanceof StringCredentials; + return credentials instanceof PersonalAccessToken + || credentials instanceof GroupAccessToken + || credentials instanceof StringCredentials; } catch (Exception e) { return false; } diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/GitLabServer.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/GitLabServer.java index 9740bb5b..32b3b2ed 100644 --- a/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/GitLabServer.java +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/GitLabServer.java @@ -62,7 +62,7 @@ public class GitLabServer extends AbstractDescribableImpl { /** - * The credentials matcher for PersonalAccessToken and StringCredentials + * The credentials matcher for PersonalAccessToken, GroupAccessToken and StringCredentials */ public static final CredentialsMatcher CREDENTIALS_MATCHER = new GitLabCredentialMatcher(); /** @@ -268,7 +268,7 @@ public String getCredentialsId() { } /** - * Looks up for PersonalAccessToken and StringCredentials + * Looks up for PersonalAccessToken, GroupAccessToken and StringCredentials * * @return {@link StandardCredentials} */ diff --git a/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/helpers/GitLabPersonalAccessTokenCreator.java b/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/helpers/GitLabPersonalAccessTokenCreator.java index a58dd9b6..9429d706 100644 --- a/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/helpers/GitLabPersonalAccessTokenCreator.java +++ b/src/main/java/io/jenkins/plugins/gitlabserverconfig/servers/helpers/GitLabPersonalAccessTokenCreator.java @@ -188,8 +188,9 @@ public FormValidation doCreateTokenByPassword( private void createCredentials(@Nullable String serverUrl, String token, String username, String tokenName) { String url = defaultIfBlank(serverUrl, GitLabServer.GITLAB_SERVER_URL); String description = String.format("Auto Generated by %s server for %s user", url, username); - PersonalAccessToken credentials = - new PersonalAccessTokenImpl(CredentialsScope.GLOBAL, tokenName, description, token); + PersonalAccessTokenImpl credentials = + new PersonalAccessTokenImpl(CredentialsScope.GLOBAL, tokenName, description); + credentials.setToken(token); saveCredentials(url, credentials); } diff --git a/src/main/resources/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImpl/credentials.jelly b/src/main/resources/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImpl/credentials.jelly new file mode 100644 index 00000000..9fc86d0a --- /dev/null +++ b/src/main/resources/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImpl/credentials.jelly @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/main/resources/io/jenkins/plugins/gitlabserverconfig/credentials/Messages.properties b/src/main/resources/io/jenkins/plugins/gitlabserverconfig/credentials/Messages.properties index 2fcd55b6..12c67e2c 100644 --- a/src/main/resources/io/jenkins/plugins/gitlabserverconfig/credentials/Messages.properties +++ b/src/main/resources/io/jenkins/plugins/gitlabserverconfig/credentials/Messages.properties @@ -1,3 +1,6 @@ PersonalAccessTokenImpl.displayName=GitLab Personal Access Token PersonalAccessTokenImpl.tokenRequired=Token required PersonalAccessTokenImpl.tokenWrongLength=Token should be at least 20 characters long +GroupAccessTokenImpl.displayName=GitLab Group Access Token +GroupAccessTokenImpl.tokenRequired=Token required +GroupAccessTokenImpl.tokenWrongLength=Token should be at least 20 characters long diff --git a/src/test/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSourceTest.java b/src/test/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSourceTest.java index 9c261aaf..2933d0ba 100644 --- a/src/test/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSourceTest.java +++ b/src/test/java/io/jenkins/plugins/gitlabbranchsource/GitLabSCMSourceTest.java @@ -51,7 +51,7 @@ public void retrieveMRWithEmptyProjectSettings() throws GitLabApiException, IOEx Mockito.when(projectApi.getProject(any())).thenReturn(new Project()); try (MockedStatic utilities = Mockito.mockStatic(GitLabHelper.class)) { utilities - .when(() -> GitLabHelper.apiBuilder(any(AccessControlled.class), anyString())) + .when(() -> GitLabHelper.apiBuilder(any(AccessControlled.class), anyString(), anyString())) .thenReturn(gitLabApi); GitLabServers.get().addServer(new GitLabServer("", SERVER, "")); GitLabSCMSourceBuilder sb = diff --git a/src/test/java/io/jenkins/plugins/gitlabserverconfig/casc/ConfigurationAsCodeTest.java b/src/test/java/io/jenkins/plugins/gitlabserverconfig/casc/ConfigurationAsCodeTest.java index b82ad456..230ba3c2 100644 --- a/src/test/java/io/jenkins/plugins/gitlabserverconfig/casc/ConfigurationAsCodeTest.java +++ b/src/test/java/io/jenkins/plugins/gitlabserverconfig/casc/ConfigurationAsCodeTest.java @@ -16,6 +16,7 @@ import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; import io.jenkins.plugins.casc.model.CNode; +import io.jenkins.plugins.gitlabserverconfig.credentials.GroupAccessTokenImpl; import io.jenkins.plugins.gitlabserverconfig.credentials.PersonalAccessTokenImpl; import io.jenkins.plugins.gitlabserverconfig.servers.GitLabServer; import io.jenkins.plugins.gitlabserverconfig.servers.GitLabServers; @@ -41,12 +42,19 @@ public void should_support_configuration_as_code() { assertThat(server.isManageSystemHooks(), is(true)); assertThat(server.getHooksRootUrl(), is("https://jenkins.intranet/")); - List credentials = CredentialsProvider.lookupCredentials( + List personalCredentials = CredentialsProvider.lookupCredentials( PersonalAccessTokenImpl.class, j.jenkins, ACL.SYSTEM, Collections.emptyList()); - assertThat(credentials, hasSize(1)); - final PersonalAccessTokenImpl credential = credentials.get(0); - assertThat(credential.getToken().getPlainText(), is("glpat-XfsqZvVtAx5YCph5bq3r")); - assertThat(credential.getToken().getEncryptedValue(), is(not("glpat-XfsqZvVtAx5YCph5bq3r"))); + assertThat(personalCredentials, hasSize(1)); + final PersonalAccessTokenImpl personalCredential = personalCredentials.get(0); + assertThat(personalCredential.getToken().getPlainText(), is("glpat-XfsqZvVtAx5YCph5bq3r")); + assertThat(personalCredential.getToken().getEncryptedValue(), is(not("glpat-XfsqZvVtAx5YCph5bq3r"))); + + List groupCredentials = CredentialsProvider.lookupCredentials( + GroupAccessTokenImpl.class, j.jenkins, ACL.SYSTEM, Collections.emptyList()); + assertThat(groupCredentials, hasSize(1)); + final GroupAccessTokenImpl groupCredential = groupCredentials.get(0); + assertThat(groupCredential.getToken().getPlainText(), is("glgat-XfsqZvVtAx5YCph5bq3r")); + assertThat(groupCredential.getToken().getEncryptedValue(), is(not("glgat-XfsqZvVtAx5YCph5bq3r"))); } @Test diff --git a/src/test/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImplTest.java b/src/test/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImplTest.java new file mode 100644 index 00000000..4575f241 --- /dev/null +++ b/src/test/java/io/jenkins/plugins/gitlabserverconfig/credentials/GroupAccessTokenImplTest.java @@ -0,0 +1,56 @@ +package io.jenkins.plugins.gitlabserverconfig.credentials; + +import com.cloudbees.plugins.credentials.Credentials; +import com.cloudbees.plugins.credentials.CredentialsScope; +import hudson.model.AbstractProject; +import hudson.tasks.BuildStepDescriptor; +import hudson.tasks.Builder; +import org.junit.ClassRule; +import org.junit.Test; +import org.jvnet.hudson.test.JenkinsRule; +import org.jvnet.hudson.test.TestExtension; +import org.kohsuke.stapler.DataBoundConstructor; + +public class GroupAccessTokenImplTest { + + @ClassRule + public static JenkinsRule j = new JenkinsRule(); + + @Test + public void configRoundtrip() throws Exception { + GroupAccessTokenImpl expected = + new GroupAccessTokenImpl(CredentialsScope.GLOBAL, "magic-id", "configRoundtrip"); + expected.setToken("sAf_Xasnou47yxoAsC"); + CredentialsBuilder builder = new CredentialsBuilder(expected); + j.configRoundtrip(builder); + j.assertEqualDataBoundBeans(expected, builder.credentials); + } + + /** + * Helper for {@link #configRoundtrip()}. + */ + public static class CredentialsBuilder extends Builder { + + public final Credentials credentials; + + @DataBoundConstructor + public CredentialsBuilder(Credentials credentials) { + this.credentials = credentials; + } + + @TestExtension + public static class DescriptorImpl extends BuildStepDescriptor { + + @Override + public String getDisplayName() { + return "CredentialsBuilder"; + } + + @SuppressWarnings("rawtypes") + @Override + public boolean isApplicable(Class jobType) { + return true; + } + } + } +} diff --git a/src/test/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImplTest.java b/src/test/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImplTest.java index ee8871c2..d6581fbc 100644 --- a/src/test/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImplTest.java +++ b/src/test/java/io/jenkins/plugins/gitlabserverconfig/credentials/PersonalAccessTokenImplTest.java @@ -18,8 +18,9 @@ public class PersonalAccessTokenImplTest { @Test public void configRoundtrip() throws Exception { - PersonalAccessTokenImpl expected = new PersonalAccessTokenImpl( - CredentialsScope.GLOBAL, "magic-id", "configRoundtrip", "sAf_Xasnou47yxoAsC"); + PersonalAccessTokenImpl expected = + new PersonalAccessTokenImpl(CredentialsScope.GLOBAL, "magic-id", "configRoundtrip"); + expected.setToken("sAf_Xasnou47yxoAsC"); CredentialsBuilder builder = new CredentialsBuilder(expected); j.configRoundtrip(builder); j.assertEqualDataBoundBeans(expected, builder.credentials); diff --git a/src/test/resources/io/jenkins/plugins/gitlabserverconfig/casc/configuration-as-code.yml b/src/test/resources/io/jenkins/plugins/gitlabserverconfig/casc/configuration-as-code.yml index 9c37c139..b842fc1e 100644 --- a/src/test/resources/io/jenkins/plugins/gitlabserverconfig/casc/configuration-as-code.yml +++ b/src/test/resources/io/jenkins/plugins/gitlabserverconfig/casc/configuration-as-code.yml @@ -6,6 +6,10 @@ credentials: scope: SYSTEM id: "i<3GitLab" token: "glpat-XfsqZvVtAx5YCph5bq3r" + - gitlabGroupAccessToken: + scope: SYSTEM + id: "i<3GitLab" + token: "glgat-XfsqZvVtAx5YCph5bq3r" unclassified: gitLabServers: