@@ -26,12 +26,15 @@ of this software and associated documentation files (the "Software"), to deal
26
26
*/
27
27
package org .jenkinsci .plugins ;
28
28
29
+ import com .google .common .base .Optional ;
29
30
import com .google .common .cache .Cache ;
30
31
import com .google .common .cache .CacheBuilder ;
31
32
import com .squareup .okhttp .OkHttpClient ;
32
33
import com .squareup .okhttp .OkUrlFactory ;
33
34
35
+ import hudson .security .Permission ;
34
36
import hudson .security .SecurityRealm ;
37
+ import hudson .model .Item ;
35
38
import jenkins .model .Jenkins ;
36
39
import org .acegisecurity .GrantedAuthority ;
37
40
import org .acegisecurity .GrantedAuthorityImpl ;
@@ -92,23 +95,31 @@ public class GithubAuthenticationToken extends AbstractAuthenticationToken {
92
95
private static final Cache <String , Set <String >> userOrganizationCache =
93
96
CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
94
97
95
- private static final Cache <String , Set <String >> repositoryCollaboratorsCache =
96
- CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
97
-
98
98
private static final Cache <String , Set <String >> repositoriesByUserCache =
99
99
CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
100
100
101
- private static final Cache <String , Boolean > publicRepositoryCache =
101
+ private static final Cache <String , GithubUser > usersByIdCache =
102
102
CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
103
103
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 =
105
116
CacheBuilder .newBuilder ().expireAfterWrite (1 , CACHE_EXPIRY ).build ();
106
117
107
118
private final List <GrantedAuthority > authorities = new ArrayList <GrantedAuthority >();
108
119
109
120
private static final GithubUser UNKNOWN_USER = new GithubUser (null );
110
121
111
- /** Wrapper for cache **/
122
+ /** Wrappers for cache **/
112
123
static class GithubUser {
113
124
public final GHUser user ;
114
125
@@ -117,6 +128,45 @@ public GithubUser(GHUser user) {
117
128
}
118
129
}
119
130
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
+
120
170
public GithubAuthenticationToken (final String accessToken , final String githubServer ) throws IOException {
121
171
super (new GrantedAuthority [] {});
122
172
@@ -170,7 +220,6 @@ public GithubAuthenticationToken(final String accessToken, final String githubSe
170
220
*/
171
221
public static void clearCaches () {
172
222
userOrganizationCache .invalidateAll ();
173
- repositoryCollaboratorsCache .invalidateAll ();
174
223
repositoriesByUserCache .invalidateAll ();
175
224
usersByIdCache .invalidateAll ();
176
225
}
@@ -193,14 +242,14 @@ public String getGithubServer() {
193
242
194
243
public GitHub getGitHub () throws IOException {
195
244
if (this .gh == null ) {
196
-
245
+
197
246
String host ;
198
247
try {
199
248
host = new URL (this .githubServer ).getHost ();
200
249
} catch (MalformedURLException e ) {
201
250
throw new IOException ("Invalid GitHub API URL: " + this .githubServer , e );
202
251
}
203
-
252
+
204
253
OkHttpClient client = new OkHttpClient ().setProxy (getProxy (host ));
205
254
206
255
this .gh = GitHubBuilder .fromEnvironment ()
@@ -212,7 +261,7 @@ public GitHub getGitHub() throws IOException {
212
261
}
213
262
return gh ;
214
263
}
215
-
264
+
216
265
/**
217
266
* Uses proxy if configured on pluginManager/advanced page
218
267
*
@@ -289,8 +338,32 @@ public Set<String> call() throws Exception {
289
338
}
290
339
}
291
340
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 ;
294
367
}
295
368
296
369
public Set <String > myRepositories () {
@@ -328,23 +401,9 @@ public Set<String> listToNames(Collection<GHRepository> respositories) throws IO
328
401
return names ;
329
402
}
330
403
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 ();
348
407
}
349
408
350
409
private static final Logger LOGGER = Logger
@@ -377,17 +436,26 @@ public GHOrganization loadOrganization(String organization) {
377
436
return null ;
378
437
}
379
438
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 ;
391
459
}
392
460
393
461
public GHTeam loadTeam (String organization , String team ) {
0 commit comments