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)); + } + }