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

JENKINS-41348# BlueOcean specific CredentialsProvider #768

Merged
merged 21 commits into from
Feb 10, 2017
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
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
Prev Previous commit
Next Next commit
Create domain specification for blueocean credential provider proxy
  • Loading branch information
Vivek Pandey authored and Vivek Pandey committed Feb 7, 2017
commit 8aff779e2312107668375ef3404f325e1ed8f3bf
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
package io.jenkins.blueocean.blueocean_git_pipeline;

import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.cloudbees.plugins.credentials.domains.DomainSpecification;
import com.google.common.collect.ImmutableList;
import hudson.model.Cause;
import hudson.model.TopLevelItem;
import hudson.model.User;
Expand All @@ -17,7 +14,6 @@
import io.jenkins.blueocean.rest.model.BluePipeline;
import io.jenkins.blueocean.rest.model.BlueScmConfig;
import io.jenkins.blueocean.service.embedded.rest.AbstractPipelineCreateRequestImpl;
import java.util.Collections;
import jenkins.branch.BranchSource;
import jenkins.branch.MultiBranchProjectDescriptor;
import jenkins.model.Jenkins;
Expand Down Expand Up @@ -76,10 +72,12 @@ public BluePipeline create(Reachable parent) throws IOException {
ErrorMessage.Error.ErrorCodes.INVALID.toString(),
"No domain in user credentials found for credentialId: "+ scmConfig.getCredentialId())));
}
project.addProperty(
new BlueOceanCredentialsProvider.FolderPropertyImpl(authenticatedUser.getId(),
scmConfig.getCredentialId(), new Domain("blue-ocean-proxy", "Blue Ocean Proxy domain", /** TODO insert specification here **/
Collections.<DomainSpecification>emptyList())));
if(domain.test(new BlueOceanDomainRequirement())) { //this is blueocean specific domain
project.addProperty(
new BlueOceanCredentialsProvider.FolderPropertyImpl(authenticatedUser.getId(),
scmConfig.getCredentialId(),
BlueOceanCredentialsProvider.createDomain(sourceUri)));
}
}

String credentialId = StringUtils.defaultString(scmConfig.getCredentialId());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.cloudbees.hudson.plugins.folder.AbstractFolder;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import hudson.model.Cause;
import hudson.model.TopLevelItem;
import hudson.model.User;
Expand Down Expand Up @@ -79,12 +81,24 @@ public BluePipeline create(Reachable parent) throws IOException {
if(credentialId != null) {
validateCredentialId(credentialId, (AbstractFolder) item);

((OrganizationFolder) item)
.addProperty(
new BlueOceanCredentialsProvider.FolderPropertyImpl(
authenticatedUser.getId(), credentialId,
GithubCredentialsDomain(apiUrl)
));
//Find domain attached to this credentialId, if present check if it's BlueOcean specific domain then
//add the properties otherwise simply use it
Domain domain = CredentialsUtils.findDomain(credentialId, authenticatedUser);
if(domain == null){ //this should not happen since validateCredentialId found the credential
throw new ServiceException.BadRequestExpception(
new ErrorMessage(400, "Failed to create pipeline")
.add(new ErrorMessage.Error("scm.credentialId",
ErrorMessage.Error.ErrorCodes.INVALID.toString(),
"No domain in user credentials found for credentialId: "+ scmConfig.getCredentialId())));
}
if(domain.test(new DomainRequirement())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a bug, you should be testing against your BlueOceanDomainRequirement not DomainRequirement

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes it is, good catch.

((OrganizationFolder) item)
.addProperty(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does addProperty handle duplicates correctly?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, will add validation of this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No duplicate test needed, this code creates a new org folder and is add property right after creation.

new BlueOceanCredentialsProvider.FolderPropertyImpl(
authenticatedUser.getId(), credentialId,
BlueOceanCredentialsProvider.createDomain(apiUrl)
));
}
}
GitHubSCMNavigator gitHubSCMNavigator = new GitHubSCMNavigator(apiUrl, orgName, credentialId, credentialId);
if (sb.length() > 0) {
Expand All @@ -98,9 +112,6 @@ public BluePipeline create(Reachable parent) throws IOException {
return new GithubOrganizationFolder(organizationFolder, parent.getLink());
}
} catch (Exception e){
if(e instanceof ServiceException){
throw e;
}
String msg = String.format("Error creating pipeline %s: %s",getName(),e.getMessage());
logger.error(msg, e);
if(item != null) {
Expand All @@ -112,6 +123,9 @@ public BluePipeline create(Reachable parent) throws IOException {

}
}
if(e instanceof ServiceException){
throw e;
}
throw new ServiceException.UnexpectedErrorException(msg, e);
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
import hudson.model.User;
import io.jenkins.blueocean.rest.impl.pipeline.PipelineBaseTest;
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanCredentialsProvider;
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanDomainRequirement;
import io.jenkins.blueocean.rest.impl.pipeline.credential.BlueOceanDomainSpecification;
import io.jenkins.blueocean.rest.impl.pipeline.credential.CredentialsUtils;
import jenkins.branch.OrganizationFolder;
import jenkins.model.Jenkins;
import org.jenkinsci.plugins.github_branch_source.Connector;
import org.junit.Assert;
import org.junit.Test;
Expand Down Expand Up @@ -89,37 +93,52 @@ public void shouldFindUserStoreCredential() throws IOException {
break;
}
}

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


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

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

//create org folder and attach user and credential id to it
OrganizationFolder organizationFolder = j.createProject(OrganizationFolder.class, "demo");
AbstractFolderProperty prop = new BlueOceanCredentialsProvider.FolderPropertyImpl(user.getId(), credential.getId(), "github-domain"
AbstractFolderProperty prop = new BlueOceanCredentialsProvider.FolderPropertyImpl(user.getId(), credential.getId(),
BlueOceanCredentialsProvider.createDomain("https://api.github.com"));

);
organizationFolder.addProperty(prop);

// lookup for created credential id in system store, it should resolve to previously created user store credential
StandardCredentials c = Connector.lookupScanCredentials(organizationFolder, null, credential.getId());
StandardCredentials c = Connector.lookupScanCredentials(organizationFolder, "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());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a test where there is a credential with the same ID in the system store, verifying that your proxy store is a higher ordinal and hence will get asked first


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

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

//it must not be found
//it must resolve to system credential
c = Connector.lookupScanCredentials(organizationFolder, null, credential.getId());
assertNull(c);
assertEquals("System Github Access Token", c.getDescription());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,14 @@
import com.cloudbees.hudson.plugins.folder.AbstractFolderPropertyDescriptor;
import com.cloudbees.plugins.credentials.Credentials;
import com.cloudbees.plugins.credentials.CredentialsMatcher;
import com.cloudbees.plugins.credentials.CredentialsMatchers;
import com.cloudbees.plugins.credentials.CredentialsProvider;
import com.cloudbees.plugins.credentials.CredentialsScope;
import com.cloudbees.plugins.credentials.CredentialsStore;
import com.cloudbees.plugins.credentials.CredentialsStoreAction;
import com.cloudbees.plugins.credentials.common.IdCredentials;
import com.cloudbees.plugins.credentials.domains.Domain;
import com.cloudbees.plugins.credentials.domains.DomainRequirement;
import com.google.common.collect.ImmutableSet;
import edu.umd.cs.findbugs.annotations.NonNull;
import hudson.Extension;
import hudson.model.Describable;
import hudson.model.Item;
import hudson.model.ItemGroup;
import hudson.model.ModelObject;
import hudson.model.TopLevelItem;
Expand All @@ -28,8 +23,6 @@
import io.jenkins.blueocean.rest.impl.pipeline.Messages;
import net.sf.json.JSONObject;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContext;
import org.acegisecurity.context.SecurityContextHolder;
import org.kohsuke.stapler.DataBoundConstructor;
import org.kohsuke.stapler.StaplerRequest;

Expand All @@ -42,7 +35,8 @@
import java.util.List;
import java.util.Set;

import static com.cloudbees.plugins.credentials.CredentialsMatchers.*;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.filter;
import static com.cloudbees.plugins.credentials.CredentialsMatchers.withId;

/**
* {@link CredentialsProvider} to serve credentials stored in user store.
Expand All @@ -58,21 +52,21 @@
public class BlueOceanCredentialsProvider extends CredentialsProvider {
private static final BlueOceanDomainRequirement PROXY_REQUIREMENT = new BlueOceanDomainRequirement();

@NonNull
@Nonnull
@Override
public <C extends Credentials> List<C> getCredentials(@NonNull Class<C> type,
@edu.umd.cs.findbugs.annotations.Nullable ItemGroup itemGroup,
@edu.umd.cs.findbugs.annotations.Nullable
public <C extends Credentials> List<C> getCredentials(@Nonnull Class<C> type,
@Nonnull ItemGroup itemGroup,
@Nonnull
Authentication authentication) {
return getCredentials(type, itemGroup, authentication, Collections.<DomainRequirement>emptyList());
}

@NonNull
public <C extends Credentials> List<C> getCredentials(@NonNull final Class<C> type,
@edu.umd.cs.findbugs.annotations.Nullable ItemGroup itemGroup,
@edu.umd.cs.findbugs.annotations.Nullable
@Nonnull
public <C extends Credentials> List<C> getCredentials(@Nonnull final Class<C> type,
@Nullable ItemGroup itemGroup,
@Nullable
Authentication authentication,
@NonNull List<DomainRequirement> domainRequirements) {
@Nonnull List<DomainRequirement> domainRequirements) {
final List<C> result = new ArrayList<>();
final FolderPropertyImpl prop = propertyOf(itemGroup);
if (prop != null && prop.domain.test(domainRequirements)) {
Expand Down Expand Up @@ -315,4 +309,15 @@ public boolean updateCredentials(@Nonnull Domain domain, @Nonnull Credentials cu
}
}
}

/**
* Creates a domain specific to {@link BlueOceanCredentialsProvider}
*
* @param uri repo URL
* @return {@link Domain} instance
*/
public static @Nonnull Domain createDomain(@Nonnull String uri){
return new Domain("blueocean-folder-credential-domain", Messages.BlueOceanCredentialsProvider_DomainDescription(),
CredentialsUtils.generateDomainSpecifications(uri));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,14 @@
import hudson.model.User;
import io.jenkins.blueocean.commons.ServiceException;
import jenkins.model.Jenkins;
import org.apache.commons.lang3.StringUtils;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
Expand Down Expand Up @@ -92,6 +94,21 @@ public boolean apply(@Nullable Credentials input) {
return null;
}

/**
* Get all domains this user has access to
*/
public static @Nonnull Iterable<Domain> getUserDomains(@Nonnull User user){
List<Domain> domains = new ArrayList<>();
for(final CredentialsStore store: findUserStores(user)) {
domains.addAll(store.getDomains());
}
for(final CredentialsStore store: CredentialsProvider.lookupStores(Jenkins.getInstance())){
domains.addAll(store.getDomains());
}
return domains;
}


public static @CheckForNull <C extends Credentials> C findCredential(@Nonnull String id, @Nonnull Class<C> type){
if(User.current() == null){
throw new ServiceException.UnauthorizedException("No authenticated user found. Please login");
Expand Down Expand Up @@ -128,33 +145,46 @@ public boolean apply(@Nullable Credentials input) {
}

private static @CheckForNull Iterable<CredentialsStore> findUserStores(User user){
return Iterables.filter(CredentialsProvider.lookupStores(user), new Predicate<CredentialsStore>() {
@Override
public boolean apply(@Nullable CredentialsStore s) {
return s!= null && s.hasPermission(CredentialsProvider.CREATE) && s.hasPermission(CredentialsProvider.UPDATE);
}
});
List<CredentialsStore> stores = new ArrayList<>();

//First user store
for (CredentialsStore store : CredentialsProvider.lookupStores(user)) {
stores.add(store);
}

//then system store
for (CredentialsStore store : CredentialsProvider.lookupStores(Jenkins.getInstance())) {
stores.add(store);
}
return stores;

}

private static List<DomainSpecification> generateDomainSpecifications(@Nullable URI uri){
if (uri == null) {
public static List<DomainSpecification> generateDomainSpecifications(@Nullable String uriStr){
if (StringUtils.isBlank(uriStr)) {
return Collections.emptyList();
}

List<DomainSpecification> domainSpecifications = new ArrayList<>();
try {
URI uri = new URI(uriStr);

// XXX: UriRequirementBuilder.fromUri() maps "" path to "/", so need to take care of it here
String path = uri.getRawPath() == null ? null : (uri.getRawPath().trim().isEmpty() ? "/" : uri.getRawPath());
// XXX: UriRequirementBuilder.fromUri() maps "" path to "/", so need to take care of it here
String path = uri.getRawPath() == null ? null : (uri.getRawPath().trim().isEmpty() ? "/" : uri.getRawPath());

domainSpecifications.add(new PathSpecification(path, "", false));
if (uri.getPort() != -1) {
domainSpecifications.add(new HostnamePortSpecification(uri.getHost() + ":" + uri.getPort(), null));
} else {
domainSpecifications.add(new HostnameSpecification(uri.getHost(), null));
domainSpecifications.add(new PathSpecification(path, "", false));
if (uri.getPort() != -1) {
domainSpecifications.add(new HostnamePortSpecification(uri.getHost() + ":" + uri.getPort(), null));
} else {
domainSpecifications.add(new HostnameSpecification(uri.getHost(), null));
}
domainSpecifications.add(new SchemeSpecification(uri.getScheme()));
} catch (URISyntaxException e) {
// TODO: handle git repo of form: [user@]host.xz:path/to/repo.git/, when URIRequirementBuilder.fromUri() supports it
// for now, we are returning empty list to match with URIRequirementBuilder.fromUri()
return domainSpecifications;
}
domainSpecifications.add(new SchemeSpecification(uri.getScheme()));
return domainSpecifications;
return Collections.emptyList();
}

private static @Nonnull Domain findOrCreateDomain(@Nonnull CredentialsStore store,
Expand All @@ -165,7 +195,7 @@ private static List<DomainSpecification> generateDomainSpecifications(@Nullable
Domain domain = store.getDomainByName(domainName);
if (domain == null) { //create new one
boolean result = store.addDomain(new Domain(domainName,
"Github Domain to store personal access token", domainSpecifications)
domainName+" to store credentials by BlueOcean", domainSpecifications)
);
if (!result) {
throw new ServiceException.BadRequestExpception("Failed to create credential domain: " + domainName);
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
BlueOceanCredentialsProvider.DisplayName=BlueOcean Folder Credentials
BlueOceanCredentialsProvider.DisplayName=BlueOcean Folder Credentials
BlueOceanCredentialsProvider.DomainDescription=Blue Ocean Folder Credentials domain