From 5475273a6388d770f555328d0ed3d2c745cfd3eb Mon Sep 17 00:00:00 2001 From: Christopher Williams Date: Tue, 11 Dec 2018 20:37:29 -0500 Subject: [PATCH] refactor: simplify auth flow to avoid Github API calls as much as possible Attempts to handle use cases that don't require repository permission lookups first to avoid hitting the Github API. When we do need to look at repository permissions, still attempts to use a global cache of public/private flags for repos so if any user has pulled that repo's info, and it's a public repo we can resolve read related permissions without hitting the API. When we need to load the details for a repository, load the user's full listing of repositories en masse/batch. Whenever we load a repo store the public/private fag in global cache. Improve the test suite to handle many of the typical cases. --- .../plugins/GithubAuthenticationToken.java | 252 ++++++------ ...ithubRequireOrganizationMembershipACL.java | 138 +++---- ...bRequireOrganizationMembershipACLTest.java | 362 ++++++++++-------- 3 files changed, 400 insertions(+), 352 deletions(-) diff --git a/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java b/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java index 84eb2543..40280707 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java +++ b/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java @@ -68,6 +68,7 @@ of this software and associated documentation files (the "Software"), to deal import java.util.logging.Logger; import javax.annotation.Nonnull; +import javax.annotation.Nullable; /** @@ -95,7 +96,28 @@ public class GithubAuthenticationToken extends AbstractAuthenticationToken { private static final Cache> userOrganizationCache = CacheBuilder.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); - private static final Cache> repositoriesByUserCache = + /** + * This is a double-layered cached. The first mapping is from github username + * to a secondary cache of repositories. This is so we can mass populate + * the initial set of repos a user is a collaborator on at once. + * + * The secondary layer is from repository names (full names) to rights the + * user has for that repo. Here we may add single entries occasionally, and this + * is primarily about adding entries for public repos that they're not explicitly + * a collaborator on (or updating a given repo's entry) + * + * We could make this a single layer since this token object should be per-user, + * but I'm unsure of how long it actually lives in memory. + */ + private static final Cache> repositoriesByUserCache = + CacheBuilder.newBuilder().expireAfterWrite(24, CACHE_EXPIRY).build(); + + /** + * Here we keep a global cache of whether repos are public or private, since that + * can be shared across users (and public repos are global read/pull, so we + * can avoid asking for user repos if the repo is known to be public and they want read rights) + */ + private static final Cache repositoriesPublicStatusCache = CacheBuilder.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); private static final Cache usersByIdCache = @@ -107,20 +129,6 @@ public class GithubAuthenticationToken extends AbstractAuthenticationToken { private static final Cache>> userTeamsCache = CacheBuilder.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); - /** - * This cache is for repositories and is explicitly _not_ static because we - * want to store repo information per-user (and this class should be per-user). - * We potentially could hold a separe static cache for public repo info - * that applies to all users, but it wouldn't be able to contain user-specific - * details like exact permissions (read/write/admin). - * - * This representation of the repo holds details on whether the repo is - * public/private, as well as whether the current user has pull/push/admin - * access. - */ - private final Cache repositoryCache = - CacheBuilder.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); - private final List authorities = new ArrayList(); private static final GithubUser UNKNOWN_USER = new GithubUser(null); @@ -149,7 +157,7 @@ static class RepoRights { public final boolean hasPushAccess; public final boolean isPrivate; - public RepoRights(GHRepository repo) { + public RepoRights(@Nullable GHRepository repo) { if (repo != null) { this.hasAdminAccess = repo.hasAdminAccess(); this.hasPullAccess = repo.hasPullAccess(); @@ -197,36 +205,34 @@ public GithubAuthenticationToken(final String accessToken, final String githubSe this.userName = this.me.getLogin(); authorities.add(SecurityRealm.AUTHENTICATED_AUTHORITY); + + // This stuff only really seems useful if *not* using GithubAuthorizationStrategy + // but instead using matrix so org/team can be granted rights Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { throw new IllegalStateException("Jenkins not started"); } - if(jenkins.getSecurityRealm() instanceof GithubSecurityRealm) { - if(myRealm == null) { + if (jenkins.getSecurityRealm() instanceof GithubSecurityRealm) { + if (myRealm == null) { myRealm = (GithubSecurityRealm) jenkins.getSecurityRealm(); } - //Search for scopes that allow fetching team membership. This is documented online. - //https://developer.github.com/v3/orgs/#list-your-organizations - //https://developer.github.com/v3/orgs/teams/#list-user-teams - if(myRealm.hasScope("read:org") || myRealm.hasScope("admin:org") || myRealm.hasScope("user") || myRealm.hasScope("repo")) { - try{ - Set myOrgs = userOrganizationCache.get(getName(), new Callable>() { - @Override - public Set call() throws Exception { - return getGitHub().getMyOrganizations().keySet(); - } - }); - - Map> myTeams = userTeamsCache.get(getName(), new Callable>>() { + // Search for scopes that allow fetching team membership. This is documented online. + // https://developer.github.com/v3/orgs/#list-your-organizations + // https://developer.github.com/v3/orgs/teams/#list-user-teams + if (myRealm.hasScope("read:org") || myRealm.hasScope("admin:org") || myRealm.hasScope("user") || myRealm.hasScope("repo")) { + try { + Set myOrgs = getUserOrgs(); + + Map> myTeams = userTeamsCache.get(this.userName, new Callable>>() { @Override public Map> call() throws Exception { return getGitHub().getMyTeams(); } }); - //fetch organization-only memberships (i.e.: groups without teams) - for(String orgLogin : myOrgs){ - if(!myTeams.containsKey(orgLogin)){ + // fetch organization-only memberships (i.e.: groups without teams) + for (String orgLogin : myOrgs) { + if (!myTeams.containsKey(orgLogin)) { myTeams.put(orgLogin, Collections.emptySet()); } } @@ -242,7 +248,7 @@ public Map> call() throws Exception { } } catch (ExecutionException e) { throw new RuntimeException("authorization failed for user = " - + getName(), e); + + this.userName, e); } } } @@ -254,6 +260,7 @@ public Map> call() throws Exception { public static void clearCaches() { userOrganizationCache.invalidateAll(); repositoriesByUserCache.invalidateAll(); + repositoriesPublicStatusCache.invalidateAll(); usersByIdCache.invalidateAll(); usersByTokenCache.invalidateAll(); userTeamsCache.invalidateAll(); @@ -263,7 +270,7 @@ public static void clearCaches() { * Gets the OAuth access token, so that it can be persisted and used elsewhere. * @return accessToken */ - public String getAccessToken() { + String getAccessToken() { return accessToken; } @@ -271,11 +278,11 @@ public String getAccessToken() { * Gets the Github server used for this token * @return githubServer */ - public String getGithubServer() { + String getGithubServer() { return githubServer; } - public GitHub getGitHub() throws IOException { + GitHub getGitHub() throws IOException { if (this.gh == null) { String host; @@ -320,6 +327,7 @@ public GrantedAuthority[] getAuthorities() { return authorities.toArray(new GrantedAuthority[authorities.size()]); } + @Override public Object getCredentials() { return ""; // do not expose the credential } @@ -328,6 +336,7 @@ public Object getCredentials() { * Returns the login name in GitHub. * @return principal */ + @Override public String getPrincipal() { return this.userName; } @@ -344,103 +353,115 @@ public GHMyself getMyself() throws IOException { } /** - * For some reason I can't get the github api to tell me for the current - * user the groups to which he belongs. - * - * So this is a slightly larger consideration. If the authenticated user is - * part of any team within the organization then they have permission. - * - * It caches user organizations for 24 hours for faster web navigation. - * - * @param candidateName name of the candidate - * @param organization name of the organization - * @return has organization permission + * Wraps grabbing a user's github orgs with our caching + * @return the Set of org names current user is a member of + * @throws ExecutionException if the api call somehow blows up when lazy loading */ - public boolean hasOrganizationPermission(String candidateName, - String organization) { - try { - Set v = userOrganizationCache.get(candidateName,new Callable>() { - @Override - public Set call() throws Exception { - return getGitHub().getMyOrganizations().keySet(); - } - }); + @Nonnull + private Set getUserOrgs() throws ExecutionException { + return userOrganizationCache.get(this.userName, new Callable>() { + @Override + public Set call() throws Exception { + return getGitHub().getMyOrganizations().keySet(); + } + }); + } - return v.contains(organization); + @Nonnull + boolean isMemberOfAnyOrganizationInList(@Nonnull Collection organizations) { + try { + Set userOrgs = getUserOrgs(); + for (String orgName : organizations) { + if (userOrgs.contains(orgName)) { + return true; + } + } + return false; } catch (ExecutionException e) { throw new RuntimeException("authorization failed for user = " - + candidateName, e); + + this.userName, e); } } - public boolean hasRepositoryPermission(String repositoryName, Permission permission) { + @Nonnull + boolean hasRepositoryPermission(@Nonnull String repositoryName, @Nonnull Permission permission) { LOGGER.log(Level.FINEST, "Checking for permission: " + permission + " on repo: " + repositoryName + " for user: " + this.userName); - boolean isRepoOfMine = myRepositories().contains(repositoryName); - if (isRepoOfMine) { - return true; + boolean isReadPermission = isReadRelatedPermission(permission); + if (isReadPermission) { + // here we do a 2-pass system since public repos are global read, so if *any* user has retrieved tha info + // for the repo, we can use it here to possibly skip loading the full repo details for the user. + Boolean isPublic = repositoriesPublicStatusCache.getIfPresent(repositoryName); + if (isPublic != null && isPublic.booleanValue()) { + return true; + } } - // This is not my repository, nor is it a repository of an organization I belong to. - // Check what rights I have on the github repo. + // repo is not public (or we don't yet know) so load it up... RepoRights repository = loadRepository(repositoryName); - if (repository == null) { - return false; - } // let admins do anything if (repository.hasAdminAccess()) { return true; } - // WRITE or READ can Read/Build/View Workspace - if (permission.equals(Item.DISCOVER) || - permission.equals(Item.READ) || - permission.equals(Item.BUILD) || - permission.equals(Item.WORKSPACE)) { - return repository.hasPullAccess() || repository.hasPushAccess(); + // WRITE or READ (or public repo) can Read/Build/View Workspace + if (isReadPermission) { + return !repository.isPrivate() || repository.hasPullAccess() || repository.hasPushAccess(); } // WRITE can cancel builds or view config if (permission.equals(Item.CANCEL) || permission.equals(Item.EXTENDED_READ)) { return repository.hasPushAccess(); } - // Need ADMIN rights to do rest: configure, create, delete, discover, wipeout + // Need ADMIN rights to do rest: configure, create, delete, wipeout return false; } - public Set myRepositories() { + @Nonnull + private boolean isReadRelatedPermission(@Nonnull Permission permission) { + return permission.equals(Item.DISCOVER) || + permission.equals(Item.READ) || + permission.equals(Item.BUILD) || + permission.equals(Item.WORKSPACE); + } + + /** + * Returns a mapping from repo names to repo rights for the current user + * @return [description] + */ + @Nonnull + private Cache myRepositories() { try { - return repositoriesByUserCache.get(getName(), - new Callable>() { + return repositoriesByUserCache.get(this.userName, + new Callable>() { @Override - public Set call() throws Exception { + public Cache call() throws Exception { // listRepositories returns all repos owned by user, where they are a collaborator, // and any user has access through org membership List userRepositoryList = getMyself().listRepositories(100).asList(); // use max page size of 100 to limit API calls - return listToNames(userRepositoryList); + // Now we want to cache each repo's rights too + Cache repoNameToRightsCache = + CacheBuilder.newBuilder().expireAfterWrite(1, CACHE_EXPIRY).build(); + for (GHRepository repo : userRepositoryList) { + RepoRights rights = new RepoRights(repo); + String repositoryName = repo.getFullName(); + // store in user's repo cache + repoNameToRightsCache.put(repositoryName, rights); + // store public/private flag in our global cache + repositoriesPublicStatusCache.put(repositoryName, !rights.isPrivate()); + } + return repoNameToRightsCache; } } ); } catch (ExecutionException e) { LOGGER.log(Level.SEVERE, "an exception was thrown", e); throw new RuntimeException("authorization failed for user = " - + getName(), e); - } - } - - public Set listToNames(Collection respositories) throws IOException { - Set names = new HashSet(); - for (GHRepository repository : respositories) { - names.add(repository.getFullName()); + + this.userName, e); } - return names; - } - - public boolean isPublicRepository(String repositoryName) { - RepoRights repository = loadRepository(repositoryName); - return repository != null && !repository.isPrivate(); } private static final Logger LOGGER = Logger .getLogger(GithubAuthenticationToken.class.getName()); - public GHUser loadUser(String username) throws IOException { + @Nullable + GHUser loadUser(@Nonnull String username) throws IOException { GithubUser user; try { user = usersByIdCache.getIfPresent(username); @@ -457,7 +478,7 @@ public GHUser loadUser(String username) throws IOException { return user != null ? user.user : null; } - public GHMyself loadMyself(String token) throws IOException { + private GHMyself loadMyself(@Nonnull String token) throws IOException { GithubMyself me; try { me = usersByTokenCache.getIfPresent(token); @@ -465,6 +486,9 @@ public GHMyself loadMyself(String token) throws IOException { GHMyself ghMyself = getGitHub().getMyself(); me = new GithubMyself(ghMyself); usersByTokenCache.put(token, me); + // Also stick into usersByIdCache (to have latest copy) + String username = ghMyself.getLogin(); + usersByIdCache.put(username, new GithubUser(ghMyself)); } } catch (IOException e) { LOGGER.log(Level.FINEST, e.getMessage(), e); @@ -474,7 +498,8 @@ public GHMyself loadMyself(String token) throws IOException { return me.me; } - public GHOrganization loadOrganization(String organization) { + @Nullable + GHOrganization loadOrganization(@Nonnull String organization) { try { if (gh != null && isAuthenticated()) return getGitHub().getOrganization(organization); @@ -484,17 +509,22 @@ public GHOrganization loadOrganization(String organization) { return null; } - public RepoRights loadRepository(final String repositoryName) { + @Nonnull + private RepoRights loadRepository(@Nonnull final String repositoryName) { try { if (gh != null && isAuthenticated() && (myRealm.hasScope("repo") || myRealm.hasScope("public_repo"))) { - return repositoryCache.get(repositoryName, - new Callable() { - @Override - public RepoRights call() throws Exception { - GHRepository repo = getGitHub().getRepository(repositoryName); - return new RepoRights(repo); - } - } + Cache repoNameToRightsCache = myRepositories(); + return repoNameToRightsCache.get(repositoryName, + new Callable() { + @Override + public RepoRights call() throws Exception { + GHRepository repo = getGitHub().getRepository(repositoryName); + RepoRights rights = new RepoRights(repo); + // store public/private flag in our cache + repositoriesPublicStatusCache.put(repositoryName, !rights.isPrivate()); + return rights; + } + } ); } } catch (Exception e) { @@ -503,10 +533,11 @@ public RepoRights call() throws Exception { "Looks like a bad GitHub URL OR the Jenkins user {0} does not have access to the repository {1}. May need to add 'repo' or 'public_repo' to the list of oauth scopes requested.", new Object[] { this.userName, repositoryName }); } - return null; + return new RepoRights(null); // treat as a private repo } - public GHTeam loadTeam(String organization, String team) { + @Nullable + GHTeam loadTeam(@Nonnull String organization, @Nonnull String team) { try { GHOrganization org = loadOrganization(organization); if (org != null) { @@ -518,7 +549,8 @@ public GHTeam loadTeam(String organization, String team) { return null; } - public GithubOAuthUserDetails getUserDetails(String username) throws IOException { + @Nullable + GithubOAuthUserDetails getUserDetails(@Nonnull String username) throws IOException { GHUser user = loadUser(username); if (user != null) { return new GithubOAuthUserDetails(user.getLogin(), this); diff --git a/src/main/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACL.java b/src/main/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACL.java index 75fa20d9..591e1667 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACL.java +++ b/src/main/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACL.java @@ -39,6 +39,7 @@ of this software and associated documentation files (the "Software"), to deal import java.util.logging.Logger; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import hudson.model.AbstractItem; import hudson.model.AbstractProject; @@ -94,56 +95,48 @@ public boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission perm return true; } - if (this.item != null) { - if (useRepositoryPermissions) { - if(hasRepositoryPermission(authenticationToken, permission)) { - log.finest("Granting Authenticated User " + permission.getId() + - " permission on project " + item.getName() + - "to user " + candidateName); - return true; - } - } else { - if (authenticatedUserReadPermission) { - if (checkReadPermission(permission)) { - log.finest("Granting Authenticated User read permission " + - "on project " + item.getName() + - "to user " + candidateName); - return true; - } - } - } - } else if (authenticatedUserReadPermission) { - if (checkReadPermission(permission)) { - // if we support authenticated read and this is a read - // request we allow it - log.finest("Granting Authenticated User read permission to user " - + candidateName); - return true; - } - } + // Streamline checks! + // Are they trying to create something and we have that setting enabled? Return quickly! if (authenticatedUserCreateJobPermission && permission.equals(Item.CREATE)) { return true; } - for (String organizationName : this.organizationNameList) { - if (authenticationToken.hasOrganizationPermission( - candidateName, organizationName)) { + // Are they trying to read? + if (checkReadPermission(permission)) { + // if we support authenticated read return early + if (authenticatedUserReadPermission) { + log.finest("Granting Authenticated User read permission to user " + + candidateName); + return true; + } - String[] parts = permission.getId().split("\\."); + // allow them to read if in whitelisted orgs + if (isInWhitelistedOrgs(authenticationToken)) { // 1 API call per-user, per-hour + log.finest("Granting READ rights to user " + + candidateName + " as a member of whitelisted organization"); + return true; + } + // falls through to try to use repo permissions... + } + // allow them to BUILD if in whitelisted orgs + else if (testBuildPermission(permission) && isInWhitelistedOrgs(authenticationToken)) { // 1 API call per-user, per-hour + log.finest("Granting BUILD rights to user " + + candidateName + " as a member of whitelisted organization"); + return true; + } - String test = parts[parts.length - 1].toLowerCase(); + // regardless of what permissions they're seeking, use the repo permissions to determine if possible + if (useRepositoryPermissions && this.item != null) { + String repositoryName = getRepositoryName(); - if (checkReadPermission(permission) - || testBuildPermission(permission)) { - // check the permission + if (repositoryName == null) { + return false; + } - log.finest("Granting READ and BUILD rights to user " - + candidateName + " a member of " - + organizationName); - return true; - } - } + // best case 0 API calls (repo is public and that flag is cached, or user's repo listing is already cached with repo in it) + // worst case, 2+ API calls to gather user repos (1 call per 100 for batch load, 1 add'l call if public repo not in list) + return authenticationToken.hasRepositoryPermission(repositoryName, permission); } // no match. @@ -161,7 +154,7 @@ public boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission perm } if (authenticatedUserName.equals("anonymous")) { - if(checkJobStatusPermission(permission) && allowAnonymousJobStatusPermission) { + if (checkJobStatusPermission(permission) && allowAnonymousJobStatusPermission) { return true; } @@ -197,6 +190,11 @@ public boolean hasPermission(@Nonnull Authentication a, @Nonnull Permission perm } } + @Nonnull + private boolean isInWhitelistedOrgs(@Nonnull GithubAuthenticationToken authenticationToken) { + return authenticationToken.isMemberOfAnyOrganizationInList(this.organizationNameList); + } + private boolean currentUriPathEquals( String specificPath ) { Jenkins jenkins = Jenkins.getInstance(); if (jenkins == null) { @@ -224,61 +222,31 @@ private boolean currentUriPathEndsWithSegment( String segment ) { } } + @Nullable private String requestURI() { StaplerRequest currentRequest = Stapler.getCurrentRequest(); return (currentRequest == null) ? null : currentRequest.getOriginalRequestURI(); } - private boolean testBuildPermission(Permission permission) { - if (permission.getId().equals("hudson.model.Hudson.Build") - || permission.getId().equals("hudson.model.Item.Build")) { - return true; - } else { - return false; - } + private boolean testBuildPermission(@Nonnull Permission permission) { + String id = permission.getId(); + return id.equals("hudson.model.Hudson.Build") + || id.equals("hudson.model.Item.Build"); } - private boolean checkReadPermission(Permission permission) { - if (permission.getId().equals("hudson.model.Hudson.Read") - || permission.getId().equals("hudson.model.Item.Workspace") - || permission.getId().equals("hudson.model.Item.Discover") - || permission.getId().equals("hudson.model.Item.Read")) { - return true; - } else { - return false; - } + private boolean checkReadPermission(@Nonnull Permission permission) { + String id = permission.getId(); + return (id.equals("hudson.model.Hudson.Read") + || id.equals("hudson.model.Item.Workspace") + || id.equals("hudson.model.Item.Discover") + || id.equals("hudson.model.Item.Read")); } - private boolean checkJobStatusPermission(Permission permission) { + private boolean checkJobStatusPermission(@Nonnull Permission permission) { return permission.getId().equals("hudson.model.Item.ViewStatus"); } - public boolean hasRepositoryPermission(GithubAuthenticationToken authenticationToken, Permission permission) { - String repositoryName = getRepositoryName(); - - if (repositoryName == null) { - if (authenticatedUserCreateJobPermission) { - if (permission.equals(Item.DISCOVER) || - permission.equals(Item.READ) || - permission.equals(Item.CONFIGURE) || - permission.equals(Item.DELETE) || - permission.equals(Item.EXTENDED_READ) || - permission.equals(Item.CANCEL)) { - return true; - } else { - return false; - } - } else { - return false; - } - } else if (checkReadPermission(permission) && - authenticationToken.isPublicRepository(repositoryName)) { - return true; - } else { - return authenticationToken.hasRepositoryPermission(repositoryName, permission); - } - } - + @Nullable private String getRepositoryName() { String repositoryName = null; String repoUrl = null; diff --git a/src/test/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACLTest.java b/src/test/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACLTest.java index 7b7e9163..e81c6cb7 100644 --- a/src/test/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACLTest.java +++ b/src/test/java/org/jenkinsci/plugins/GithubRequireOrganizationMembershipACLTest.java @@ -52,6 +52,8 @@ of this software and associated documentation files (the "Software"), to deal import org.kohsuke.github.PagedIterable; import org.kohsuke.github.RateLimitHandler; import org.kohsuke.github.extras.OkHttpConnector; +import org.kohsuke.stapler.Stapler; +import org.kohsuke.stapler.StaplerRequest; import org.mockito.Mock; import org.mockito.Mockito; import org.powermock.api.mockito.PowerMockito; @@ -86,7 +88,7 @@ of this software and associated documentation files (the "Software"), to deal * @author alex */ @RunWith(PowerMockRunner.class) -@PrepareForTest({GitHub.class, GitHubBuilder.class, Jenkins.class, GithubSecurityRealm.class, WorkflowJob.class}) +@PrepareForTest({GitHub.class, GitHubBuilder.class, Jenkins.class, GithubSecurityRealm.class, WorkflowJob.class, Stapler.class}) public class GithubRequireOrganizationMembershipACLTest extends TestCase { @Mock @@ -97,12 +99,30 @@ public class GithubRequireOrganizationMembershipACLTest extends TestCase { @Mock private GithubSecurityRealm securityRealm; + private boolean allowAnonymousReadPermission; + private boolean allowAnonymousJobStatusPermission; + private boolean useRepositoryPermissions; + private boolean authenticatedUserReadPermission; + private boolean authenticatedUserCreateJobPermission; + private boolean allowAnonymousWebhookPermission; + private boolean allowAnonymousCCTrayPermission; + @Before public void setUp() throws Exception { + // default to: use repository permissions; don't allow anonymous read/view status; don't allow authenticated read/create + allowAnonymousReadPermission = false; + allowAnonymousJobStatusPermission = false; + useRepositoryPermissions = true; + authenticatedUserReadPermission = false; + authenticatedUserCreateJobPermission = false; + allowAnonymousWebhookPermission = false; + allowAnonymousCCTrayPermission = false; + //GithubSecurityRealm myRealm = PowerMockito.mock(GithubSecurityRealm.class); PowerMockito.mockStatic(Jenkins.class); PowerMockito.when(Jenkins.getInstance()).thenReturn(jenkins); PowerMockito.when(jenkins.getSecurityRealm()).thenReturn(securityRealm); + PowerMockito.when(jenkins.getRootUrl()).thenReturn("https://www.jenkins.org/"); PowerMockito.when(securityRealm.getOauthScopes()).thenReturn("read:org,repo"); PowerMockito.when(securityRealm.hasScope("read:org")).thenReturn(true); PowerMockito.when(securityRealm.hasScope("repo")).thenReturn(true); @@ -113,64 +133,33 @@ public void setUp() throws Exception { Messages._Item_READ_description(), Permission.READ, PermissionScope.ITEM); - private final Authentication ANONYMOUS_USER = new AnonymousAuthenticationToken("anonymous", + private final Authentication ANONYMOUS_USER = new AnonymousAuthenticationToken("anonymous", "anonymous", new GrantedAuthority[]{new GrantedAuthorityImpl("anonymous")}); - boolean allowAnonymousJobStatusPermission = false; - - private GithubRequireOrganizationMembershipACL aclForProject(Project project) { - boolean useRepositoryPermissions = true; - boolean authenticatedUserReadPermission = true; - boolean authenticatedUserCreateJobPermission = false; - - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL( + private GithubRequireOrganizationMembershipACL createACL() { + return new GithubRequireOrganizationMembershipACL( "admin", "myOrg", authenticatedUserReadPermission, useRepositoryPermissions, authenticatedUserCreateJobPermission, - true, - true, - true, + allowAnonymousWebhookPermission, + allowAnonymousCCTrayPermission, + allowAnonymousReadPermission, allowAnonymousJobStatusPermission); - return acl.cloneForProject(project); + } + + private GithubRequireOrganizationMembershipACL aclForProject(Project project) { + return createACL().cloneForProject(project); } private GithubRequireOrganizationMembershipACL aclForMultiBranchProject(MultiBranchProject multiBranchProject) { - boolean useRepositoryPermissions = true; - boolean authenticatedUserReadPermission = true; - boolean authenticatedUserCreateJobPermission = false; - - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL( - "admin", - "myOrg", - authenticatedUserReadPermission, - useRepositoryPermissions, - authenticatedUserCreateJobPermission, - true, - true, - true, - allowAnonymousJobStatusPermission); - return acl.cloneForProject(multiBranchProject); + return createACL().cloneForProject(multiBranchProject); } private GithubRequireOrganizationMembershipACL aclForWorkflowJob(WorkflowJob workflowJob) { - boolean useRepositoryPermissions = true; - boolean authenticatedUserReadPermission = true; - boolean authenticatedUserCreateJobPermission = false; - - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL( - "admin", - "myOrg", - authenticatedUserReadPermission, - useRepositoryPermissions, - authenticatedUserCreateJobPermission, - true, - true, - true, - allowAnonymousJobStatusPermission); - return acl.cloneForProject(workflowJob); + return createACL().cloneForProject(workflowJob); } private GHMyself mockGHMyselfAs(String username) throws IOException { @@ -187,40 +176,31 @@ private GHMyself mockGHMyselfAs(String username) throws IOException { GHMyself me = PowerMockito.mock(GHMyself.class); PowerMockito.when(gh.getMyself()).thenReturn((GHMyself) me); PowerMockito.when(me.getLogin()).thenReturn(username); + mockReposFor(me, Collections.emptyList()); return me; } - private void mockReposFor(GHPerson person, List repositoryNames) throws IOException { - List repositories = repositoryListOf(repositoryNames); + // TODO: Add ability to set list of orgs user belongs to to check whitelisting! + + private void mockReposFor(GHPerson person, List repositories) throws IOException { PagedIterable pagedRepositories = PowerMockito.mock(PagedIterable.class); PowerMockito.when(person.listRepositories(100)).thenReturn(pagedRepositories); PowerMockito.when(pagedRepositories.asList()).thenReturn(repositories); - }; - - private List repositoryListOf(List repositoryNames) throws IOException { - List repositoriesSet = new ArrayList(); - for (String repositoryName : repositoryNames) { - String[] parts = repositoryName.split("/"); - GHRepository repository = mockGHRepository(parts[0], parts[1]); - repositoriesSet.add(repository); - } - return repositoriesSet; } - private GHRepository mockGHRepository(String ownerName, String name) throws IOException { - GHRepository ghRepository = PowerMockito.mock(GHRepository.class); - GHUser ghUser = PowerMockito.mock(GHUser.class); - PowerMockito.when(ghUser.getLogin()).thenReturn(ownerName); - PowerMockito.when(ghRepository.getFullName()).thenReturn(ownerName + "/" + name); - PowerMockito.when(ghRepository.getOwner()).thenReturn(ghUser); - PowerMockito.when(ghRepository.getName()).thenReturn(name); - return ghRepository; + private GHRepository mockRepository(String repositoryName, boolean isPublic, boolean admin, boolean push, boolean pull) throws IOException { + GHRepository ghRepository = PowerMockito.mock(GHRepository.class); + PowerMockito.when(gh.getRepository(repositoryName)).thenReturn(ghRepository); + PowerMockito.when(ghRepository.isPrivate()).thenReturn(!isPublic); + PowerMockito.when(ghRepository.hasAdminAccess()).thenReturn(admin); + PowerMockito.when(ghRepository.hasPushAccess()).thenReturn(push); + PowerMockito.when(ghRepository.hasPullAccess()).thenReturn(pull); + PowerMockito.when(ghRepository.getFullName()).thenReturn(repositoryName); + return ghRepository; } - private GHOrganization mockGHOrganization(String organizationName, List repositories) throws IOException { - GHOrganization ghOrganization = PowerMockito.mock(GHOrganization.class); - mockReposFor(ghOrganization, repositories); - return ghOrganization; + private GHRepository mockPublicRepository(String repositoryName) throws IOException { + return mockRepository(repositoryName, true, false, false, false); } private Project mockProject(String url) { @@ -233,6 +213,7 @@ private Project mockProject(String url) { PowerMockito.when(userRemoteConfig.getUrl()).thenReturn(url); return project; } + private WorkflowJob mockWorkflowJob(String url) { WorkflowJob project = PowerMockito.mock(WorkflowJob.class); GitSCM gitSCM = PowerMockito.mock(GitSCM.class); @@ -258,10 +239,18 @@ private MultiBranchProject mockMultiBranchProject(String url) { return multiBranchProject; } + @Override + protected void tearDown() throws Exception { + gh = null; + super.tearDown(); + GithubAuthenticationToken.clearCaches(); + } + @Test - public void testCanReadAndBuildOneOfMyRepositories() throws IOException { + public void testCanReadAndBuildOneOfMyPrivateRepositories() throws IOException { GHMyself me = mockGHMyselfAs("Me"); - mockReposFor(me, Arrays.asList("me/a-repo", "some-org/a-public-repo")); + GHRepository repo = mockRepository("me/a-repo", false, true, true, true); // private; admin, push, and pull rights + mockReposFor(me, Arrays.asList(repo)); // hook to my listing String repoUrl = "https://github.com/me/a-repo.git"; Project mockProject = mockProject(repoUrl); MultiBranchProject mockMultiBranchProject = mockMultiBranchProject(repoUrl); @@ -271,62 +260,48 @@ public void testCanReadAndBuildOneOfMyRepositories() throws IOException { GithubRequireOrganizationMembershipACL projectAcl = aclForProject(mockProject); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertTrue(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(projectAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(projectAcl.hasPermission(authenticationToken, Item.BUILD)); - assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.BUILD)); - assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.BUILD)); } - @Override - protected void tearDown() throws Exception { - gh = null; - super.tearDown(); - GithubAuthenticationToken.clearCaches(); - } - @Test - public void testCanReadAndBuildOrgRepositoryICollaborateOn() throws IOException { + public void testCanReadAndBuildAPublicRepository() throws IOException { GHMyself me = mockGHMyselfAs("Me"); - mockReposFor(me, Arrays.asList("me/a-repo", "some-org/a-private-repo")); - String repoUrl = "https://github.com/some-org/a-private-repo.git"; + GHRepository repo = mockPublicRepository("node/node"); + String repoUrl = "https://github.com/node/node.git"; Project mockProject = mockProject(repoUrl); MultiBranchProject mockMultiBranchProject = mockMultiBranchProject(repoUrl); WorkflowJob mockWorkflowJob = mockWorkflowJob(repoUrl); GithubRequireOrganizationMembershipACL workflowJobAcl = aclForWorkflowJob(mockWorkflowJob); GithubRequireOrganizationMembershipACL multiBranchProjectAcl = aclForMultiBranchProject(mockMultiBranchProject); GithubRequireOrganizationMembershipACL projectAcl = aclForProject(mockProject); - GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertTrue(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(projectAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(projectAcl.hasPermission(authenticationToken, Item.BUILD)); - assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); - assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.READ)); - assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.BUILD)); - assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.BUILD)); + assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); + assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.BUILD)); } @Test - public void testCanReadAndBuildOtherOrgPrivateRepositoryICollaborateOn() throws IOException { + public void testCanReadAndBuildPrivateRepositoryIHavePullRightsOn() throws IOException { GHMyself me = mockGHMyselfAs("Me"); - mockReposFor(me, Arrays.asList("me/a-repo", "some-org/a-private-repo")); - GHRepository ghRepository = PowerMockito.mock(GHRepository.class); - PowerMockito.when(gh.getRepository("org-i-dont-belong-to/a-private-repo-i-collaborate-on")).thenReturn(ghRepository); - PowerMockito.when(ghRepository.isPrivate()).thenReturn(true); - PowerMockito.when(ghRepository.hasAdminAccess()).thenReturn(false); - PowerMockito.when(ghRepository.hasPushAccess()).thenReturn(false); - PowerMockito.when(ghRepository.hasPullAccess()).thenReturn(true); - - // The user isn't part of "org-i-dont-belong-to" - String repoUrl = "https://github.com/org-i-dont-belong-to/a-private-repo-i-collaborate-on.git"; + // private repo I have pull rights to + GHRepository repo = mockRepository("some-org/a-private-repo", false, false, false, true); + mockReposFor(me, Arrays.asList(repo)); + String repoUrl = "https://github.com/some-org/a-private-repo.git"; Project mockProject = mockProject(repoUrl); MultiBranchProject mockMultiBranchProject = mockMultiBranchProject(repoUrl); WorkflowJob mockWorkflowJob = mockWorkflowJob(repoUrl); @@ -336,21 +311,21 @@ public void testCanReadAndBuildOtherOrgPrivateRepositoryICollaborateOn() throws GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertTrue(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(projectAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(projectAcl.hasPermission(authenticationToken, Item.BUILD)); - assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); - assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.READ)); - assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.BUILD)); - assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(workflowJobAcl.hasPermission(authenticationToken, Item.BUILD)); + assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.READ)); + assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); + assertTrue(multiBranchProjectAcl.hasPermission(authenticationToken, Item.BUILD)); } @Test public void testCanNotReadOrBuildRepositoryIDoNotCollaborateOn() throws IOException { GHMyself me = mockGHMyselfAs("Me"); - mockReposFor(me, Arrays.asList("me/a-repo", "some-org/a-private-repo")); + String repoUrl = "https://github.com/some-org/another-private-repo.git"; Project mockProject = mockProject(repoUrl); MultiBranchProject mockMultiBranchProject = mockMultiBranchProject(repoUrl); @@ -361,14 +336,14 @@ public void testCanNotReadOrBuildRepositoryIDoNotCollaborateOn() throws IOExcept GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertFalse(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(projectAcl.hasPermission(authenticationToken, Item.READ)); + assertFalse(projectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(projectAcl.hasPermission(authenticationToken, Item.BUILD)); - assertFalse(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(multiBranchProjectAcl.hasPermission(authenticationToken, Item.READ)); + assertFalse(multiBranchProjectAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(multiBranchProjectAcl.hasPermission(authenticationToken, Item.BUILD)); - assertFalse(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(workflowJobAcl.hasPermission(authenticationToken, Item.READ)); + assertFalse(workflowJobAcl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(workflowJobAcl.hasPermission(authenticationToken, Item.BUILD)); } @@ -382,8 +357,8 @@ public void testNotGrantedBuildWhenNotUsingGitSCM() throws IOException { GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(acl.hasPermission(authenticationToken, Item.READ)); + assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); } @Test @@ -394,8 +369,8 @@ public void testNotGrantedBuildWhenRepositoryIsEmpty() throws IOException { GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(acl.hasPermission(authenticationToken, Item.READ)); + assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); } @Test @@ -412,59 +387,52 @@ public void testNotGrantedReadWhenRepositoryUrlIsEmpty() throws IOException { GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(acl.hasPermission(authenticationToken, Item.READ)); + assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); } @Test public void testGlobalReadAvailableDueToAuthenticatedUserReadPermission() throws IOException { - boolean useRepositoryPermissions = false; - boolean authenticatedUserReadPermission = true; + this.useRepositoryPermissions = false; + this.authenticatedUserReadPermission = true; + mockGHMyselfAs("Me"); - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL("admin", "myOrg", - authenticatedUserReadPermission, useRepositoryPermissions, true, true, true, true, false); - Project mockProject = mockProject("https://github.com/some-org/another-private-repo.git"); + GithubRequireOrganizationMembershipACL acl = createACL(); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); assertTrue(acl.hasPermission(authenticationToken, Hudson.READ)); - } @Test public void testWithoutUseRepositoryPermissionsSetCanReadDueToAuthenticatedUserReadPermission() throws IOException { - boolean useRepositoryPermissions = false; - boolean authenticatedUserReadPermission = true; - mockGHMyselfAs("Me"); - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL("admin", "myOrg", - authenticatedUserReadPermission, useRepositoryPermissions, true, true, true, true, false); + this.useRepositoryPermissions = false; + this.authenticatedUserReadPermission = true; + mockGHMyselfAs("Me"); + GithubRequireOrganizationMembershipACL acl = createACL(); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertTrue(acl.hasPermission(authenticationToken, Item.DISCOVER)); assertTrue(acl.hasPermission(authenticationToken, Item.READ)); } @Test - public void testWithoutUseRepositoryPermissionsSetCannotReadWithoutToAuthenticatedUserReadPermission() throws IOException { - boolean useRepositoryPermissions = false; - boolean authenticatedUserReadPermission = false; - mockGHMyselfAs("Me"); - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL("admin", "myOrg", - authenticatedUserReadPermission, useRepositoryPermissions, true, true, true, true, false); + public void testWithoutUseRepositoryPermissionsSetCannotReadWithoutAuthenticatedUserReadPermission() throws IOException { + this.useRepositoryPermissions = false; + this.authenticatedUserReadPermission = false; + mockGHMyselfAs("Me"); + GithubRequireOrganizationMembershipACL acl = createACL(); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(acl.hasPermission(authenticationToken, Item.READ)); } @Test public void testUsersCannotCreateWithoutConfigurationEnabledPermission() throws IOException { - boolean authenticatedUserCreateJobPermission = false; - mockGHMyselfAs("Me"); - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL("admin", "myOrg", - true, true, authenticatedUserCreateJobPermission, true, true, true, false); + this.authenticatedUserCreateJobPermission = false; + mockGHMyselfAs("Me"); + GithubRequireOrganizationMembershipACL acl = createACL(); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); assertFalse(acl.hasPermission(authenticationToken, Item.CREATE)); @@ -472,48 +440,49 @@ public void testUsersCannotCreateWithoutConfigurationEnabledPermission() throws @Test public void testUsersCanCreateWithConfigurationEnabledPermission() throws IOException { - boolean authenticatedUserCreateJobPermission = true; - mockGHMyselfAs("Me"); - GithubRequireOrganizationMembershipACL acl = new GithubRequireOrganizationMembershipACL("admin", "myOrg", - true, true, authenticatedUserCreateJobPermission, true, true, true, false); + this.authenticatedUserCreateJobPermission = true; + mockGHMyselfAs("Me"); + GithubRequireOrganizationMembershipACL acl = createACL(); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); assertTrue(acl.hasPermission(authenticationToken, Item.CREATE)); } @Test - public void testCanReadConfigureDeleteAProjectWithAuthenticatedUserReadPermission() throws IOException { + public void testCanReadAProjectWithAuthenticatedUserReadPermission() throws IOException { + this.authenticatedUserReadPermission = true; + String nullProjectName = null; Project mockProject = mockProject(nullProjectName); - boolean authenticatedUserCreateJobPermission = true; mockGHMyselfAs("Me"); - GithubRequireOrganizationMembershipACL globalAcl = new GithubRequireOrganizationMembershipACL("admin", "myOrg", - true, true, authenticatedUserCreateJobPermission, true, true, true, false); - GithubRequireOrganizationMembershipACL acl = globalAcl.cloneForProject(mockProject); + GithubRequireOrganizationMembershipACL acl = aclForProject(mockProject); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertTrue(acl.hasPermission(authenticationToken, Item.DISCOVER)); + // Gives the user rights to see the project assertTrue(acl.hasPermission(authenticationToken, Item.READ)); - assertTrue(acl.hasPermission(authenticationToken, Item.CONFIGURE)); - assertTrue(acl.hasPermission(authenticationToken, Item.DELETE)); - assertTrue(acl.hasPermission(authenticationToken, Item.EXTENDED_READ)); - assertTrue(acl.hasPermission(authenticationToken, Item.CANCEL)); + assertTrue(acl.hasPermission(authenticationToken, Item.DISCOVER)); + // but not to build, cancel, configure, view configuration, delete it + assertFalse(acl.hasPermission(authenticationToken, Item.BUILD)); + assertFalse(acl.hasPermission(authenticationToken, Item.CONFIGURE)); + assertFalse(acl.hasPermission(authenticationToken, Item.DELETE)); + assertFalse(acl.hasPermission(authenticationToken, Item.EXTENDED_READ)); + assertFalse(acl.hasPermission(authenticationToken, Item.CANCEL)); } @Test - public void testCannotReadConfigureDeleteAProjectWithoutToAuthenticatedUserReadPermission() throws IOException { + public void testCannotReadAProjectWithoutAuthenticatedUserReadPermission() throws IOException { + this.authenticatedUserReadPermission = false; + String nullProjectName = null; Project mockProject = mockProject(nullProjectName); - boolean authenticatedUserCreateJobPermission = false; mockGHMyselfAs("Me"); - GithubRequireOrganizationMembershipACL globalAcl = new GithubRequireOrganizationMembershipACL("admin", "myOrg", - true, true, authenticatedUserCreateJobPermission, true, true, true, false); - GithubRequireOrganizationMembershipACL acl = globalAcl.cloneForProject(mockProject); + GithubRequireOrganizationMembershipACL acl = aclForProject(mockProject); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(acl.hasPermission(authenticationToken, Item.READ)); + assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); + assertFalse(acl.hasPermission(authenticationToken, Item.BUILD)); assertFalse(acl.hasPermission(authenticationToken, Item.CONFIGURE)); assertFalse(acl.hasPermission(authenticationToken, Item.DELETE)); assertFalse(acl.hasPermission(authenticationToken, Item.EXTENDED_READ)); @@ -523,14 +492,15 @@ public void testCannotReadConfigureDeleteAProjectWithoutToAuthenticatedUserReadP @Test public void testCannotReadRepositoryWithInvalidRepoUrl() throws IOException { GHMyself me = mockGHMyselfAs("Me"); - mockReposFor(me, Arrays.asList("me/a-repo", "some-org/a-repo")); + // private repo I have pull rights to + GHRepository repo = mockRepository("some-org/a-repo", false, false, false, true); + mockReposFor(me, Arrays.asList(repo)); String invalidRepoUrl = "git@github.com//some-org/a-repo.git"; Project mockProject = mockProject(invalidRepoUrl); GithubRequireOrganizationMembershipACL acl = aclForProject(mockProject); GithubAuthenticationToken authenticationToken = new GithubAuthenticationToken("accessToken", "https://api.github.com"); - assertFalse(acl.hasPermission(authenticationToken, Item.DISCOVER)); assertFalse(acl.hasPermission(authenticationToken, Item.READ)); } @@ -554,4 +524,82 @@ public void testAnonymousCannotViewJobStatusWhenNotGranted() throws IOException assertFalse(acl.hasPermission(ANONYMOUS_USER, VIEW_JOBSTATUS_PERMISSION)); } + @Test + public void testAnonymousCanReachWebhookWhenGranted() throws IOException { + this.allowAnonymousWebhookPermission = true; + + StaplerRequest currentRequest = PowerMockito.mock(StaplerRequest.class); + PowerMockito.mockStatic(Stapler.class); + PowerMockito.when(Stapler.getCurrentRequest()).thenReturn(currentRequest); + PowerMockito.when(currentRequest.getOriginalRequestURI()).thenReturn("https://www.jenkins.org/github-webhook/"); + + GithubRequireOrganizationMembershipACL acl = createACL(); + + assertTrue(acl.hasPermission(ANONYMOUS_USER, Item.READ)); + } + + @Test + public void testAnonymousCannotReachWebhookIfNotGranted() throws IOException { + this.allowAnonymousWebhookPermission = false; + + StaplerRequest currentRequest = PowerMockito.mock(StaplerRequest.class); + PowerMockito.mockStatic(Stapler.class); + PowerMockito.when(Stapler.getCurrentRequest()).thenReturn(currentRequest); + PowerMockito.when(currentRequest.getOriginalRequestURI()).thenReturn("https://www.jenkins.org/github-webhook/"); + + GithubRequireOrganizationMembershipACL acl = createACL(); + + assertFalse(acl.hasPermission(ANONYMOUS_USER, Item.READ)); + } + + @Test + public void testAnonymousCanReadAndDiscoverWhenGranted() throws IOException { + this.allowAnonymousReadPermission = true; + + Project mockProject = mockProject("https://github.com/some-org/a-public-repo.git"); + GithubRequireOrganizationMembershipACL acl = aclForProject(mockProject); + + assertTrue(acl.hasPermission(ANONYMOUS_USER, Item.READ)); + assertTrue(acl.hasPermission(ANONYMOUS_USER, Item.DISCOVER)); + } + + @Test + public void testAnonymousCantReadAndDiscoverWhenNotGranted() throws IOException { + this.allowAnonymousReadPermission = false; + + Project mockProject = mockProject("https://github.com/some-org/a-public-repo.git"); + GithubRequireOrganizationMembershipACL acl = aclForProject(mockProject); + + assertFalse(acl.hasPermission(ANONYMOUS_USER, Item.READ)); + assertFalse(acl.hasPermission(ANONYMOUS_USER, Item.DISCOVER)); + } + + @Test + public void testAnonymousCanReachCCTrayWhenGranted() throws IOException { + this.allowAnonymousCCTrayPermission = true; + + StaplerRequest currentRequest = PowerMockito.mock(StaplerRequest.class); + PowerMockito.mockStatic(Stapler.class); + PowerMockito.when(Stapler.getCurrentRequest()).thenReturn(currentRequest); + PowerMockito.when(currentRequest.getOriginalRequestURI()).thenReturn("https://www.jenkins.org/cc.xml"); + + GithubRequireOrganizationMembershipACL acl = createACL(); + + assertTrue(acl.hasPermission(ANONYMOUS_USER, Item.READ)); + } + + @Test + public void testAnonymousCannotReachCCTrayIfNotGranted() throws IOException { + this.allowAnonymousCCTrayPermission = false; + + StaplerRequest currentRequest = PowerMockito.mock(StaplerRequest.class); + PowerMockito.mockStatic(Stapler.class); + PowerMockito.when(Stapler.getCurrentRequest()).thenReturn(currentRequest); + PowerMockito.when(currentRequest.getOriginalRequestURI()).thenReturn("https://www.jenkins.org/cc.xml"); + + GithubRequireOrganizationMembershipACL acl = createACL(); + + assertFalse(acl.hasPermission(ANONYMOUS_USER, Item.READ)); + } + }