@@ -26,12 +26,15 @@ of this software and associated documentation files (the "Software"), to deal
2626 */
2727package org .jenkinsci .plugins ;
2828
29+ import com .google .common .base .Optional ;
2930import com .google .common .cache .Cache ;
3031import com .google .common .cache .CacheBuilder ;
3132import com .squareup .okhttp .OkHttpClient ;
3233import com .squareup .okhttp .OkUrlFactory ;
3334
35+ import hudson .security .Permission ;
3436import hudson .security .SecurityRealm ;
37+ import hudson .model .Item ;
3538import jenkins .model .Jenkins ;
3639import org .acegisecurity .GrantedAuthority ;
3740import org .acegisecurity .GrantedAuthorityImpl ;
@@ -92,23 +95,31 @@ public class GithubAuthenticationToken extends AbstractAuthenticationToken {
9295 private static final Cache <String , Set <String >> userOrganizationCache =
9396 CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
9497
95- private static final Cache <String , Set <String >> repositoryCollaboratorsCache =
96- CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
97-
9898 private static final Cache <String , Set <String >> repositoriesByUserCache =
9999 CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
100100
101- private static final Cache <String , Boolean > publicRepositoryCache =
101+ private static final Cache <String , GithubUser > usersByIdCache =
102102 CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
103103
104- private static final Cache <String , GithubUser > usersByIdCache =
104+ /**
105+ * This cache is for repositories and is explicitly _not_ static because we
106+ * want to store repo information per-user (and this class should be per-user).
107+ * We potentially could hold a separe static cache for public repo info
108+ * that applies to all users, but it wouldn't be able to contain user-specific
109+ * details like exact permissions (read/write/admin).
110+ *
111+ * This representation of the repo holds details on whether the repo is
112+ * public/private, as well as whether the current user has pull/push/admin
113+ * access.
114+ */
115+ private final Cache <String , RepoRights > repositoryCache =
105116 CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
106117
107118 private final List <GrantedAuthority > authorities = new ArrayList <GrantedAuthority >();
108119
109120 private static final GithubUser UNKNOWN_USER = new GithubUser (null );
110121
111- /** Wrapper for cache **/
122+ /** Wrappers for cache **/
112123 static class GithubUser {
113124 public final GHUser user ;
114125
@@ -117,6 +128,45 @@ public GithubUser(GHUser user) {
117128 }
118129 }
119130
131+ static class RepoRights {
132+ public final boolean hasAdminAccess ;
133+ public final boolean hasPullAccess ;
134+ public final boolean hasPushAccess ;
135+ public final boolean isPrivate ;
136+
137+ public RepoRights (GHRepository repo ) {
138+ if (repo != null ) {
139+ this .hasAdminAccess = repo .hasAdminAccess ();
140+ this .hasPullAccess = repo .hasPullAccess ();
141+ this .hasPushAccess = repo .hasPushAccess ();
142+ this .isPrivate = repo .isPrivate ();
143+ } else {
144+ // assume null repo means we had no rights to view it
145+ // so must be private
146+ this .hasAdminAccess = false ;
147+ this .hasPullAccess = false ;
148+ this .hasPushAccess = false ;
149+ this .isPrivate = true ;
150+ }
151+ }
152+
153+ public boolean hasAdminAccess () {
154+ return this .hasAdminAccess ;
155+ }
156+
157+ public boolean hasPullAccess () {
158+ return this .hasPullAccess ;
159+ }
160+
161+ public boolean hasPushAccess () {
162+ return this .hasPushAccess ;
163+ }
164+
165+ public boolean isPrivate () {
166+ return this .isPrivate ;
167+ }
168+ }
169+
120170 public GithubAuthenticationToken (final String accessToken , final String githubServer ) throws IOException {
121171 super (new GrantedAuthority [] {});
122172
@@ -170,7 +220,6 @@ public GithubAuthenticationToken(final String accessToken, final String githubSe
170220 */
171221 public static void clearCaches () {
172222 userOrganizationCache .invalidateAll ();
173- repositoryCollaboratorsCache .invalidateAll ();
174223 repositoriesByUserCache .invalidateAll ();
175224 usersByIdCache .invalidateAll ();
176225 }
@@ -193,14 +242,14 @@ public String getGithubServer() {
193242
194243 public GitHub getGitHub () throws IOException {
195244 if (this .gh == null ) {
196-
245+
197246 String host ;
198247 try {
199248 host = new URL (this .githubServer ).getHost ();
200249 } catch (MalformedURLException e ) {
201250 throw new IOException ("Invalid GitHub API URL: " + this .githubServer , e );
202251 }
203-
252+
204253 OkHttpClient client = new OkHttpClient ().setProxy (getProxy (host ));
205254
206255 this .gh = GitHubBuilder .fromEnvironment ()
@@ -212,7 +261,7 @@ public GitHub getGitHub() throws IOException {
212261 }
213262 return gh ;
214263 }
215-
264+
216265 /**
217266 * Uses proxy if configured on pluginManager/advanced page
218267 *
@@ -289,8 +338,32 @@ public Set<String> call() throws Exception {
289338 }
290339 }
291340
292- public boolean hasRepositoryPermission (final String repositoryName ) {
293- return myRepositories ().contains (repositoryName );
341+ public boolean hasRepositoryPermission (String repositoryName , Permission permission ) {
342+ LOGGER .log (Level .FINEST , "Checking for permission: " + permission + " on repo: " + repositoryName + " for user: " + this .userName );
343+ boolean isRepoOfMine = myRepositories ().contains (repositoryName );
344+ if (isRepoOfMine ) {
345+ return true ;
346+ }
347+ // This is not my repository, nor is it a repository of an organization I belong to.
348+ // Check what rights I have on the github repo.
349+ RepoRights repository = loadRepository (repositoryName );
350+ if (repository == null ) {
351+ return false ;
352+ }
353+ // let admins do anything
354+ if (repository .hasAdminAccess ()) {
355+ return true ;
356+ }
357+ // WRITE or READ can Read/Build/View Workspace
358+ if (permission .equals (Item .READ ) || permission .equals (Item .BUILD ) || permission .equals (Item .WORKSPACE )) {
359+ return repository .hasPullAccess () || repository .hasPushAccess ();
360+ }
361+ // WRITE can cancel builds or view config
362+ if (permission .equals (Item .CANCEL ) || permission .equals (Item .EXTENDED_READ )) {
363+ return repository .hasPushAccess ();
364+ }
365+ // Need ADMIN rights to do rest: configure, create, delete, discover, wipeout
366+ return false ;
294367 }
295368
296369 public Set <String > myRepositories () {
@@ -328,23 +401,9 @@ public Set<String> listToNames(Collection<GHRepository> respositories) throws IO
328401 return names ;
329402 }
330403
331- public boolean isPublicRepository (final String repositoryName ) {
332- try {
333- return publicRepositoryCache .get (repositoryName ,
334- new Callable <Boolean >() {
335- @ Override
336- public Boolean call () throws Exception {
337- GHRepository repository = loadRepository (repositoryName );
338- // We don't have access so it must not be public (it could be non-existant)
339- return repository != null && !repository .isPrivate ();
340- }
341- }
342- );
343- } catch (ExecutionException e ) {
344- LOGGER .log (Level .SEVERE , "an exception was thrown" , e );
345- throw new RuntimeException ("authorization failed for user = "
346- + getName (), e );
347- }
404+ public boolean isPublicRepository (String repositoryName ) {
405+ RepoRights repository = loadRepository (repositoryName );
406+ return repository != null && !repository .isPrivate ();
348407 }
349408
350409 private static final Logger LOGGER = Logger
@@ -377,17 +436,26 @@ public GHOrganization loadOrganization(String organization) {
377436 return null ;
378437 }
379438
380- public GHRepository loadRepository (String repositoryName ) {
381- try {
382- if (gh != null && isAuthenticated ()) {
383- return getGitHub ().getRepository (repositoryName );
384- }
385- } catch (IOException e ) {
386- LOGGER .log (Level .WARNING ,
387- "Looks like a bad GitHub URL OR the Jenkins user does not have access to the repository{0}" ,
388- repositoryName );
389- }
390- return null ;
439+ public RepoRights loadRepository (final String repositoryName ) {
440+ try {
441+ if (gh != null && isAuthenticated () && (myRealm .hasScope ("repo" ) || myRealm .hasScope ("public_repo" ))) {
442+ return repositoryCache .get (repositoryName ,
443+ new Callable <RepoRights >() {
444+ @ Override
445+ public RepoRights call () throws Exception {
446+ GHRepository repo = getGitHub ().getRepository (repositoryName );
447+ return new RepoRights (repo );
448+ }
449+ }
450+ );
451+ }
452+ } catch (Exception e ) {
453+ LOGGER .log (Level .SEVERE , "an exception was thrown" , e );
454+ LOGGER .log (Level .WARNING ,
455+ "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." ,
456+ new Object [] { this .userName , repositoryName });
457+ }
458+ return null ;
391459 }
392460
393461 public GHTeam loadTeam (String organization , String team ) {
0 commit comments