diff --git a/pom.xml b/pom.xml index ab9a76a1..2446e494 100644 --- a/pom.xml +++ b/pom.xml @@ -78,7 +78,7 @@ org.jenkins-ci.plugins github-api - 1.58 + 1.67 diff --git a/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java b/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java index 673d59d1..3535d542 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java +++ b/src/main/java/org/jenkinsci/plugins/GithubAuthenticationToken.java @@ -27,7 +27,6 @@ of this software and associated documentation files (the "Software"), to deal package org.jenkinsci.plugins; import java.io.IOException; -import java.io.FileNotFoundException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -43,6 +42,8 @@ of this software and associated documentation files (the "Software"), to deal import com.google.common.cache.CacheBuilder; import hudson.security.SecurityRealm; import java.util.Collection; + +import org.jenkinsci.plugins.GithubOAuthUserDetails; import org.acegisecurity.GrantedAuthority; import org.acegisecurity.GrantedAuthorityImpl; import org.acegisecurity.providers.AbstractAuthenticationToken; @@ -56,14 +57,14 @@ of this software and associated documentation files (the "Software"), to deal /** * @author mocleiri - * + * * to hold the authentication token from the github oauth process. - * + * */ public class GithubAuthenticationToken extends AbstractAuthenticationToken { /** - * + * */ private static final long serialVersionUID = 1L; private final String accessToken; @@ -71,9 +72,9 @@ public class GithubAuthenticationToken extends AbstractAuthenticationToken { private final String userName; private final GitHub gh; private final GHMyself me; - + /** - * Cache for faster organization based security + * Cache for faster organization based security */ private static final Cache> userOrganizationCache = CacheBuilder.newBuilder().expireAfterWrite(1,TimeUnit.HOURS).build(); @@ -103,9 +104,17 @@ public GithubAuthenticationToken(String accessToken, String githubServer) throws this.userName = this.me.getLogin(); authorities.add(SecurityRealm.AUTHENTICATED_AUTHORITY); - for (String name : gh.getMyOrganizations().keySet()) - authorities.add(new GrantedAuthorityImpl(name)); - } + Map> myTeams = gh.getMyTeams(); + for (String orgLogin : myTeams.keySet()) { + LOGGER.log(Level.FINE, "Fetch teams for user " + userName + " in organization " + orgLogin); + authorities.add(new GrantedAuthorityImpl(orgLogin)); + for (GHTeam team : myTeams.get(orgLogin)) { + authorities.add(new GrantedAuthorityImpl(orgLogin + GithubOAuthGroupDetails.ORG_TEAM_SEPARATOR + + team.getName())); + } + } + + } /** * Necessary for testing @@ -146,12 +155,12 @@ public String getPrincipal() { /** * 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 * @param organization * @return @@ -240,55 +249,90 @@ public Boolean call() throws Exception { } } - private static final Logger LOGGER = Logger - .getLogger(GithubAuthenticationToken.class.getName()); + private static final Logger LOGGER = Logger + .getLogger(GithubAuthenticationToken.class.getName()); - public GHUser loadUser(String username) throws IOException { - if (gh != null && isAuthenticated()) - return gh.getUser(username); - else - return null; - } + public GHUser loadUser(String username) { + try { + if (gh != null && isAuthenticated()) + return gh.getUser(username); + } catch (IOException e) { + LOGGER.log(Level.FINEST, e.getMessage(), e); + } + return null; + } - public GHOrganization loadOrganization(String organization) - throws IOException { + public GHOrganization loadOrganization(String organization) { + try { + if (gh != null && isAuthenticated()) + return gh.getOrganization(organization); + } catch (IOException e) { + LOGGER.log(Level.FINEST, e.getMessage(), e); + } + return null; + } - if (gh != null && isAuthenticated()) - return gh.getOrganization(organization); - else - return null; + public GHRepository loadRepository(String repositoryName) { + try { + if (gh != null && isAuthenticated()) { + return gh.getRepository(repositoryName); + } + } catch (IOException e) { + LOGGER.log(Level.WARNING, + "Looks like a bad GitHub URL OR the Jenkins user does not have access to the repository{0}", + repositoryName); + } + return null; + } - } + public GHTeam loadTeam(String organization, String team) { + try { + GHOrganization org = loadOrganization(organization); + if (org != null) { + return org.getTeamByName(team); + } + } catch (IOException e) { + LOGGER.log(Level.FINEST, e.getMessage(), e); + } + return null; + } - public GHRepository loadRepository(String repositoryName) { + /** + * @since 0.21 + */ + public GithubOAuthUserDetails getUserDetails(String username) { + GHUser user = loadUser(username); + if (user != null) { + List groups = new ArrayList(); try { - if (gh != null && isAuthenticated()) { - return gh.getRepository(repositoryName); - } else { - return null; + for (GHOrganization ghOrganization : user.getOrganizations()) { + String orgLogin = ghOrganization.getLogin(); + LOGGER.log(Level.FINE, "Fetch teams for user " + username + " in organization " + orgLogin); + groups.add(new GrantedAuthorityImpl(orgLogin)); + try { + if (!me.isMemberOf(ghOrganization)) { + continue; + } + Map teams = ghOrganization.getTeams(); + for (String team : teams.keySet()) { + if (teams.get(team).hasMember(user)) { + groups.add(new GrantedAuthorityImpl(orgLogin + GithubOAuthGroupDetails.ORG_TEAM_SEPARATOR + + team)); + } + } + } catch (IOException ignore) { + LOGGER.log(Level.FINEST, "not enough rights to list teams from " + orgLogin, ignore); + continue; + } catch (Error ignore) { + LOGGER.log(Level.FINEST, "not enough rights to list teams from " + orgLogin, ignore); + continue; + } } - } catch(FileNotFoundException e) { - LOGGER.log(Level.WARNING, "Looks like a bad github URL OR the Jenkins user does not have access to the repository{0}", repositoryName); - return null; } catch(IOException e) { - LOGGER.log(Level.WARNING, "Looks like a bad github URL OR the Jenkins user does not have access to the repository{0}", repositoryName); - return null; + LOGGER.log(Level.FINE, e.getMessage(), e); } - } - - public GHTeam loadTeam(String organization, String team) throws IOException { - if (gh != null && isAuthenticated()) { - - GHOrganization org = gh.getOrganization(organization); - - if (org != null) { - Map teamMap = org.getTeams(); - - return teamMap.get(team); - } else - return null; - - } else - return null; - } + return new GithubOAuthUserDetails(user, groups.toArray(new GrantedAuthority[groups.size()])); + } + return null; + } } diff --git a/src/main/java/org/jenkinsci/plugins/GithubOAuthGroupDetails.java b/src/main/java/org/jenkinsci/plugins/GithubOAuthGroupDetails.java index 490759a5..144b5517 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubOAuthGroupDetails.java +++ b/src/main/java/org/jenkinsci/plugins/GithubOAuthGroupDetails.java @@ -4,6 +4,7 @@ package org.jenkinsci.plugins; import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHTeam; import hudson.security.GroupDetails; @@ -13,24 +14,42 @@ */ public class GithubOAuthGroupDetails extends GroupDetails { - private final GHOrganization org; - - public GithubOAuthGroupDetails(GHOrganization org) { - super(); - this.org = org; - } - - /* (non-Javadoc) - * @see hudson.security.GroupDetails#getName() - */ - @Override - public String getName() { - if (org != null) - return org.getLogin(); - else - return null; - } - - + private final GHOrganization org; + private final GHTeam team; + static final String ORG_TEAM_SEPARATOR = "*"; + + /** + * Group based on organization name + * @param org + */ + public GithubOAuthGroupDetails(GHOrganization org) { + super(); + this.org = org; + this.team = null; + } + + /** + * Group based on team name + * @param ghTeam + */ + public GithubOAuthGroupDetails(GHTeam team) { + super(); + this.org = team.getOrganization(); + this.team = team; + } + + /* (non-Javadoc) + * @see hudson.security.GroupDetails#getName() + */ + @Override + public String getName() { + if (team != null) + return org.getLogin() + ORG_TEAM_SEPARATOR + team.getName(); + if (org != null) + return org.getLogin(); + return null; + } + + } diff --git a/src/main/java/org/jenkinsci/plugins/GithubOAuthUserDetails.java b/src/main/java/org/jenkinsci/plugins/GithubOAuthUserDetails.java index 57fccc94..47287470 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubOAuthUserDetails.java +++ b/src/main/java/org/jenkinsci/plugins/GithubOAuthUserDetails.java @@ -1,9 +1,10 @@ /** - * + * */ package org.jenkinsci.plugins; import org.acegisecurity.GrantedAuthority; +import org.acegisecurity.userdetails.User; import org.acegisecurity.userdetails.UserDetails; import org.kohsuke.github.GHUser; @@ -11,71 +12,12 @@ * @author Mike * */ -public class GithubOAuthUserDetails implements UserDetails { - - private final GHUser user; - - /** - * - */ - public GithubOAuthUserDetails(GHUser user) { - this.user = user; - } - - /* (non-Javadoc) - * @see org.acegisecurity.userdetails.UserDetails#getAuthorities() - */ - @Override - public GrantedAuthority[] getAuthorities() { - return new GrantedAuthority [] {}; - } - - /* (non-Javadoc) - * @see org.acegisecurity.userdetails.UserDetails#getPassword() - */ - @Override - public String getPassword() { - return null; - } - - /* (non-Javadoc) - * @see org.acegisecurity.userdetails.UserDetails#getUsername() - */ - @Override - public String getUsername() { - return user.getLogin(); - } - - /* (non-Javadoc) - * @see org.acegisecurity.userdetails.UserDetails#isAccountNonExpired() - */ - @Override - public boolean isAccountNonExpired() { - return true; - } - - /* (non-Javadoc) - * @see org.acegisecurity.userdetails.UserDetails#isAccountNonLocked() - */ - @Override - public boolean isAccountNonLocked() { - return true; - } +public class GithubOAuthUserDetails extends User implements UserDetails { - /* (non-Javadoc) - * @see org.acegisecurity.userdetails.UserDetails#isCredentialsNonExpired() - */ - @Override - public boolean isCredentialsNonExpired() { - return true; - } + private static final long serialVersionUID = 1L; - /* (non-Javadoc) - * @see org.acegisecurity.userdetails.UserDetails#isEnabled() - */ - @Override - public boolean isEnabled() { - return true; - } + public GithubOAuthUserDetails(GHUser user, GrantedAuthority[] authorities) { + super(user.getLogin(), "", true, true, true, true, authorities); + } } diff --git a/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java b/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java index 42480f58..144d7c7c 100644 --- a/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java +++ b/src/main/java/org/jenkinsci/plugins/GithubSecurityRealm.java @@ -60,6 +60,7 @@ of this software and associated documentation files (the "Software"), to deal import org.apache.http.util.EntityUtils; import org.jfree.util.Log; import org.kohsuke.github.GHOrganization; +import org.kohsuke.github.GHTeam; import org.kohsuke.github.GHUser; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.Header; @@ -89,7 +90,7 @@ of this software and associated documentation files (the "Software"), to deal * This is based on the MySQLSecurityRealm from the mysql-auth-plugin written by * Alex Ackerman. */ -public class GithubSecurityRealm extends SecurityRealm { +public class GithubSecurityRealm extends SecurityRealm implements UserDetailsService { private static final String DEFAULT_WEB_URI = "https://github.com"; private static final String DEFAULT_API_URI = "https://api.github.com"; private static final String DEFAULT_ENTERPRISE_API_SUFFIX = "/api/v3"; @@ -448,7 +449,7 @@ public HttpResponse doFinishLogin(StaplerRequest request) u.addProperty(new Mailer.UserProperty(self.getEmail())); } - fireAuthenticated(new GithubOAuthUserDetails(self)); + fireAuthenticated(new GithubOAuthUserDetails(self, auth.getAuthorities())); } else { Log.info("Github did not return an access token."); @@ -600,27 +601,18 @@ public UserDetails loadUserByUsername(String username) } try { - - GroupDetails group = null; - try { - group = loadGroupByGroupname(username); - } catch (DataRetrievalFailureException e) { - LOGGER.config("No group found with name: " + username); - } catch (UsernameNotFoundException e) { - LOGGER.config("No group found with name: " + username); - } - - if (group != null) { - throw new UsernameNotFoundException ("user("+username+") is also an organization"); + GithubOAuthUserDetails userDetails = authToken.getUserDetails(username); + if (userDetails == null) + throw new UsernameNotFoundException("Unknown user: " + username); + + // Check the username is not an homonym of an organization + GHOrganization ghOrg = authToken.loadOrganization(username); + if (ghOrg != null) { + throw new UsernameNotFoundException("user(" + username + ") is also an organization"); } - user = authToken.loadUser(username); - - if (user != null) - return new GithubOAuthUserDetails(user); - else - throw new UsernameNotFoundException("No known user: " + username); - } catch (IOException e) { + return userDetails; + } catch (Error e) { throw new DataRetrievalFailureException("loadUserByUsername (username=" + username +")", e); } } @@ -642,14 +634,26 @@ public GroupDetails loadGroupByGroupname(String groupName) throw new UsernameNotFoundException("No known group: " + groupName); try { - GHOrganization org = authToken.loadOrganization(groupName); - - if (org != null) - return new GithubOAuthGroupDetails(org); - else - throw new UsernameNotFoundException("No known group: " + groupName); - } catch (IOException e) { - throw new DataRetrievalFailureException("loadGroupByGroupname (groupname=" + groupName +")", e); + int idx = groupName.indexOf(GithubOAuthGroupDetails.ORG_TEAM_SEPARATOR); + if (idx > -1 && groupName.length() > idx + 1) { // groupName = "GHOrganization*GHTeam" + String orgName = groupName.substring(0, idx); + String teamName = groupName.substring(idx + 1); + LOGGER.config(String.format("Lookup for team %s in organization %s", teamName, orgName)); + GHTeam ghTeam = authToken.loadTeam(orgName, teamName); + if (ghTeam == null) { + throw new UsernameNotFoundException("Unknown GitHub team: " + teamName + " in organization " + + orgName); + } + return new GithubOAuthGroupDetails(ghTeam); + } else { // groupName = "GHOrganization" + GHOrganization ghOrg = authToken.loadOrganization(groupName); + if (ghOrg == null) { + throw new UsernameNotFoundException("Unknown GitHub organization: " + groupName); + } + return new GithubOAuthGroupDetails(ghOrg); + } + } catch (Error e) { + throw new DataRetrievalFailureException("loadGroupByGroupname (groupname=" + groupName + ")", e); } }