Skip to content

Commit

Permalink
[SECURITY-714]
Browse files Browse the repository at this point in the history
Remove BlueOcean Credentials Providers

Signed-off-by: Olivier Lamy <[email protected]>
  • Loading branch information
olamy committed May 9, 2022
1 parent dc939fa commit 70fe479
Show file tree
Hide file tree
Showing 13 changed files with 351 additions and 46 deletions.
2 changes: 1 addition & 1 deletion acceptance-tests/runner/scripts/args.sh
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PLUGINS=""
AGGREGATOR_DIR=""
DEV_JENKINS=false
PROFILES="-P runTests"
JENKINS_JAVA_OPTS="-Djava.util.logging.config.file=./logging.properties"
JENKINS_JAVA_OPTS="-Djava.util.logging.config.file=./logging.properties -Dio.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider.enabled=true"
TEST_TO_RUN=""

for i in "$@"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,22 +23,34 @@
*/
package io.jenkins.blueocean.blueocean_git_pipeline;

import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.common.StandardCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernameCredentials;
import com.cloudbees.plugins.credentials.common.StandardUsernamePasswordCredentials;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import hudson.Extension;
import hudson.model.Item;
import hudson.model.User;
import hudson.remoting.Base64;
import hudson.security.ACL;
import hudson.security.ACLContext;
import io.jenkins.blueocean.commons.ServiceException;
import io.jenkins.blueocean.credential.CredentialsUtils;
import io.jenkins.blueocean.rest.impl.pipeline.ScmContentProvider;
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanDomainRequirement;
import io.jenkins.blueocean.rest.impl.pipeline.scm.GitContent;

import java.io.IOException;
import java.util.List;
import java.util.Locale;
import javax.annotation.Nonnull;

import io.jenkins.blueocean.ssh.UserSSHKeyManager;
import jenkins.branch.MultiBranchProject;
import jenkins.model.Jenkins;
import jenkins.plugins.git.GitSCMSource;
import jenkins.scm.api.SCMSource;
import net.sf.json.JSONObject;
Expand Down Expand Up @@ -105,7 +117,7 @@ public static void setType(@Nonnull ReadSaveType type) {

static GitReadSaveRequest makeSaveRequest(
Item item, String branch, String commitMessage,
String sourceBranch, String filePath, byte[] contents) {
String sourceBranch, String filePath, byte[] contents, User user) {
String defaultBranch = "master";
GitSCMSource gitSource = null;
if (item instanceof MultiBranchProject<?, ?>) {
Expand All @@ -116,6 +128,21 @@ static GitReadSaveRequest makeSaveRequest(
}
}
}
if (gitSource != null) {
// this part is only used for authenticated user and we do not expose anything
// as this has already been created when using the wizard
gitSource = new GitSCMSource(gitSource.getRemote()) {
@Override
protected StandardUsernameCredentials getCredentials() {
User current = User.current();
if (current == null) {
return super.getCredentials();
} else {
return UserSSHKeyManager.getOrCreate(current);
}
}
};
}

return new GitBareRepoReadSaveRequest(
gitSource,
Expand All @@ -135,7 +162,8 @@ private GitReadSaveRequest makeSaveRequest(Item item, StaplerRequest req) {
req.getParameter("commitMessage"),
ObjectUtils.defaultIfNull(req.getParameter("sourceBranch"), branch),
req.getParameter("path"),
Base64.decode(req.getParameter("contents"))
Base64.decode(req.getParameter("contents")),
User.current()
);
}

Expand All @@ -147,7 +175,8 @@ private GitReadSaveRequest makeSaveRequest(Item item, JSONObject json) {
content.getString("message"),
content.has("sourceBranch") ? content.getString("sourceBranch") : branch,
content.getString("path"),
Base64.decode(content.getString("base64Data"))
Base64.decode(content.getString("base64Data")),
User.current()
);
}

Expand All @@ -165,7 +194,8 @@ public Object getContent(@Nonnull StaplerRequest req, @Nonnull Item item) {
final byte[] reqData = r.read();
String encoded = Base64.encode(reqData);

final GitContent content = new GitContent(r.filePath, user.getId(), r.gitSource.getRemote(), r.filePath, 0, "sha", encoded, "", r.branch, r.sourceBranch, true, "");
final GitContent content = new GitContent(r.filePath, user.getId(), r.gitSource.getRemote(), r.filePath, 0,
"sha", encoded, "", r.branch, r.sourceBranch, true, "");
final GitFile gitFile = new GitFile(content);
return gitFile;
} catch (ServiceException.UnauthorizedException e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,27 @@
package io.jenkins.blueocean.blueocean_git_pipeline;

import com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.SystemCredentialsProvider;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.KeyPair;
import hudson.model.User;
import io.jenkins.blueocean.commons.MapsHelper;
import io.jenkins.blueocean.rest.impl.pipeline.PipelineBaseTest;
import io.jenkins.blueocean.ssh.UserSSHKeyManager;
import io.jenkins.blueocean.test.ssh.SSHServer;
import jenkins.model.Jenkins;
import jenkins.plugins.git.AbstractGitSCMSource;
import jenkins.plugins.git.GitSCMSource;
import jenkins.plugins.git.GitSampleRepoRule;
import jenkins.scm.impl.mock.AbstractSampleDVCSRepoRule;
import jenkins.scm.impl.mock.AbstractSampleRepoRule;
import org.apache.commons.io.FileUtils;
import org.apache.sshd.common.util.OsUtils;
import org.jenkinsci.plugins.scriptsecurity.scripts.ScriptApproval;
import org.jenkinsci.plugins.workflow.multibranch.WorkflowMultiBranchProject;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
Expand All @@ -52,7 +60,9 @@
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.logging.Logger;

import static org.junit.Assert.*;
Expand Down Expand Up @@ -82,6 +92,7 @@ public GitReadSaveTest() {
@Override
public void setup() throws Exception {
super.setup();
setupUserCredentials();
setupScm();
}

Expand All @@ -93,6 +104,11 @@ private String getOrgName() {
private static final String branchPipelineScript = "pipeline { stage('Build 2') { steps { echo 'build' } } }";
private static final String newPipelineScript = "pipeline { stage('Build 3') { steps { echo 'build' } } }";

private void setupUserCredentials() throws Exception {
User user = login();
UserSSHKeyManager.getOrCreate(user);
}

private void setupScm() throws Exception {
// create git repo
repoWithJenkinsfiles.init();
Expand Down Expand Up @@ -331,6 +347,36 @@ private void testGitReadWrite(final @Nonnull GitReadSaveService.ReadSaveType typ

assertEquals(jobName, r.get("name"));

// here we have an automatic ssh key authz being created for the user
// we copy this credentials at system level and tell the job to use it
BasicSSHUserPrivateKey sshUserPrivateKey = UserSSHKeyManager.getOrCreate(user);

BasicSSHUserPrivateKey.DirectEntryPrivateKeySource keySource =
new BasicSSHUserPrivateKey.DirectEntryPrivateKeySource(sshUserPrivateKey.getPrivateKey());
String id = UUID.randomUUID().toString();
BasicSSHUserPrivateKey key = new BasicSSHUserPrivateKey( CredentialsScope.SYSTEM, id, user.getId(), keySource,
null, id);


Iterable<CredentialsStore> stores = CredentialsProvider.lookupStores(Jenkins.get());
stores.forEach( credentialsStore -> {
if (credentialsStore instanceof SystemCredentialsProvider.StoreImpl) {
SystemCredentialsProvider.StoreImpl sysStore = (SystemCredentialsProvider.StoreImpl)credentialsStore;
try {
sysStore.addCredentials(null, key);
} catch (IOException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
});

WorkflowMultiBranchProject item = (WorkflowMultiBranchProject) Jenkins.get().getItem( jobName);
item.getSCMSources().stream().forEach( scmSource -> {
String credId = ((AbstractGitSCMSource)scmSource).getCredentialsId();
LOGGER.debug( "credId: {}", credId );
((GitSCMSource)scmSource).setCredentialsId(key.getId());
});

String urlJobPrefix = "/organizations/" + getOrgName() + "/pipelines/" + jobName;

r = new RequestBuilder(baseUrl)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -295,61 +295,66 @@ public void shouldSucceedForAuthedUserWithCredentialCreatedAndCredentialIdMissin

@Test
public void shouldFindUserStoreCredential() throws IOException {
//add username password credential to user's credential store in user domain and in USER scope
User user = login();
CredentialsStore store=null;
for(CredentialsStore s: CredentialsProvider.lookupStores(user)){
if(s.hasPermission(CredentialsProvider.CREATE) && s.hasPermission(CredentialsProvider.UPDATE)){
store = s;
break;
System.setProperty(io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider.class.getName() + ".enabled", "true");
try {
//add username password credential to user's credential store in user domain and in USER scope
User user = login();
CredentialsStore store = null;
for (CredentialsStore s : CredentialsProvider.lookupStores(user)) {
if (s.hasPermission(CredentialsProvider.CREATE) && s.hasPermission(CredentialsProvider.UPDATE)) {
store = s;
break;
}
}
}

assertNotNull(store);
store.addDomain(new Domain("github-domain",
assertNotNull(store);
store.addDomain(new Domain("github-domain",
"GitHub Domain to store personal access token",
Collections.<DomainSpecification>singletonList(new BlueOceanDomainSpecification())));


Domain domain = store.getDomainByName("github-domain");
StandardUsernamePasswordCredentials credential = new UsernamePasswordCredentialsImpl(CredentialsScope.USER,
Domain domain = store.getDomainByName("github-domain");
StandardUsernamePasswordCredentials credential = new UsernamePasswordCredentialsImpl(CredentialsScope.USER,
"github", "GitHub Access Token", user.getId(), "12345");
store.addCredentials(domain, credential);
store.addCredentials(domain, credential);

//create another credentials with same id in system store with different description
for(CredentialsStore s: CredentialsProvider.lookupStores(Jenkins.get())){
s.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl(CredentialsScope.USER,
//create another credentials with same id in system store with different description
for (CredentialsStore s : CredentialsProvider.lookupStores(Jenkins.get())) {
s.addCredentials(Domain.global(), new UsernamePasswordCredentialsImpl(CredentialsScope.USER,
"github", "System GitHub Access Token", user.getId(), "12345"));
}
}

WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "demo");
AbstractFolderProperty prop = new BlueOceanCredentialsProvider.FolderPropertyImpl(user.getId(), credential.getId(),
WorkflowMultiBranchProject mp = j.jenkins.createProject(WorkflowMultiBranchProject.class, "demo");
AbstractFolderProperty prop = new BlueOceanCredentialsProvider.FolderPropertyImpl(user.getId(), credential.getId(),
BlueOceanCredentialsProvider.createDomain("https://api.github.com"));

mp.addProperty(prop);
mp.addProperty(prop);

// lookup for created credential id in system store, it should resolve to previously created user store credential
StandardCredentials c = Connector.lookupScanCredentials((Item)mp, "https://api.github.com", credential.getId());
assertEquals("GitHub Access Token", c.getDescription());
// lookup for created credential id in system store, it should resolve to previously created user store credential
StandardCredentials c = Connector.lookupScanCredentials((Item) mp, "https://api.github.com", credential.getId());
assertEquals("GitHub Access Token", c.getDescription());

assertNotNull(c);
assertTrue(c instanceof StandardUsernamePasswordCredentials);
StandardUsernamePasswordCredentials usernamePasswordCredentials = (StandardUsernamePasswordCredentials) c;
assertEquals(credential.getId(), usernamePasswordCredentials.getId());
assertEquals(credential.getPassword().getPlainText(),usernamePasswordCredentials.getPassword().getPlainText());
assertEquals(credential.getUsername(),usernamePasswordCredentials.getUsername());
assertNotNull(c);
assertTrue(c instanceof StandardUsernamePasswordCredentials);
StandardUsernamePasswordCredentials usernamePasswordCredentials = (StandardUsernamePasswordCredentials) c;
assertEquals(credential.getId(), usernamePasswordCredentials.getId());
assertEquals(credential.getPassword().getPlainText(), usernamePasswordCredentials.getPassword().getPlainText());
assertEquals(credential.getUsername(), usernamePasswordCredentials.getUsername());

//check the domain
Domain d = CredentialsUtils.findDomain(credential.getId(), user);
assertNotNull(d);
assertTrue(d.test(new BlueOceanDomainRequirement()));
//check the domain
Domain d = CredentialsUtils.findDomain(credential.getId(), user);
assertNotNull(d);
assertTrue(d.test(new BlueOceanDomainRequirement()));

//now remove this property
mp.getProperties().remove(prop);
//now remove this property
mp.getProperties().remove(prop);

//it must resolve to system credential
c = Connector.lookupScanCredentials((Item)mp, null, credential.getId());
assertEquals("System GitHub Access Token", c.getDescription());
//it must resolve to system credential
c = Connector.lookupScanCredentials((Item) mp, null, credential.getId());
assertEquals("System GitHub Access Token", c.getDescription());
} finally {
System.setProperty(io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider.class.getName() + ".enabled", "false");
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import io.jenkins.blueocean.credential.CredentialsUtils;
import io.jenkins.blueocean.pipeline.credential.Messages;
import jenkins.model.Jenkins;
import jenkins.util.SystemProperties;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.userdetails.UsernameNotFoundException;
Expand Down Expand Up @@ -68,12 +69,25 @@ public <C extends Credentials> List<C> getCredentials(@Nonnull Class<C> type,
return getCredentials(type, itemGroup, authentication, Collections.<DomainRequirement>emptyList());
}

private static boolean IsSystemPropertyEnabled() {
return SystemProperties.getBoolean( BlueOceanCredentialsProvider.class.getName() + ".enabled" );
}

@Override
public boolean isEnabled(Object context)
{
return IsSystemPropertyEnabled() && super.isEnabled(context);
}

@Nonnull
public <C extends Credentials> List<C> getCredentials(@Nonnull final Class<C> type,
@Nullable ItemGroup itemGroup,
@Nullable
Authentication authentication,
@Nonnull List<DomainRequirement> domainRequirements) {
if (!IsSystemPropertyEnabled()) {
return Collections.emptyList();
}
final List<C> result = new ArrayList<>();
final FolderPropertyImpl prop = propertyOf(itemGroup);
if (prop != null && prop.domain.test(domainRequirements)) {
Expand All @@ -99,14 +113,16 @@ public <C extends Credentials> List<C> getCredentials(@Nonnull final Class<C> ty
return result;
}


@Nonnull
@Override
public <C extends IdCredentials> ListBoxModel getCredentialIds(@Nonnull Class<C> type,
@Nullable ItemGroup itemGroup,
@Nullable Authentication authentication,
@Nonnull List<DomainRequirement> domainRequirements,
@Nonnull CredentialsMatcher matcher) {
if (!IsSystemPropertyEnabled()) {
return new ListBoxModel();
}
ListBoxModel result = new ListBoxModel();
FolderPropertyImpl prop = propertyOf(itemGroup);
if (prop != null && prop.domain.test(domainRequirements)) {
Expand All @@ -123,6 +139,9 @@ public String getDisplayName() {

@Override
public CredentialsStore getStore(@CheckForNull ModelObject object) {
if (!IsSystemPropertyEnabled()) {
return null;
}
FolderPropertyImpl property = propertyOf(object);
return property != null ? property.getStore() : null;
}
Expand Down Expand Up @@ -286,6 +305,9 @@ public boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission perm
@Nonnull
@Override
public List<Credentials> getCredentials(@Nonnull Domain domain) {
if(!IsSystemPropertyEnabled()) {
return Collections.emptyList();
}
final List<Credentials> result = new ArrayList<>(1);
if (domain.equals(FolderPropertyImpl.this.domain)) {
final User proxyUser = User.get(getUser(), false, Collections.emptyMap());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.jenkins.blueocean.scm.api;

import com.cloudbees.plugins.credentials.domains.Domain;
import hudson.ExtensionList;
import hudson.model.Cause;
import hudson.model.Failure;
import hudson.model.TaskListener;
Expand Down Expand Up @@ -218,6 +219,7 @@ private void assignCredentialToProject(BlueScmConfig scmConfig, MultiBranchProje
if (StringUtils.isEmpty(scmConfig.getUri())) {
throw new ServiceException.BadRequestException("uri not specified");
}
// ExtensionList.lookupSingleton(BlueOceanCredentialsProvider.class).isEnabled( null ) ??
if(domain.test(new BlueOceanDomainRequirement())) { //this is blueocean specific domain
project.addProperty(
new BlueOceanCredentialsProvider.FolderPropertyImpl(authenticatedUser.getId(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
BlueOceanCredentialsProvider.DisplayName=BlueOcean Folder Credentials
BlueOceanCredentialsProvider.DomainDescription=Blue Ocean Folder Credentials domain
BlueOceanCredentialsProvider.DisplayName=BlueOcean Folder Credentials (Disabled per default)
BlueOceanCredentialsProvider.DomainDescription=Blue Ocean Folder Credentials domain
Loading

0 comments on commit 70fe479

Please sign in to comment.