Merge "Fix OAuth callback URL for non-root Gerrit canonical URL"
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
index 9210fc2..0d66026 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubLogin.java
@@ -18,8 +18,10 @@
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeSet;
 
@@ -44,7 +46,7 @@
 import com.googlesource.gerrit.plugins.github.oauth.OAuthProtocol.Scope;
 
 public class GitHubLogin {
-  private static final Logger LOG = LoggerFactory.getLogger(GitHubLogin.class);
+  private static final Logger log = LoggerFactory.getLogger(GitHubLogin.class);
   private static final List<Scope> DEFAULT_SCOPES = Arrays.asList(
       Scope.PUBLIC_REPO, Scope.USER_EMAIL);
   private static final int YEARS = 365;
@@ -65,9 +67,10 @@
   @Getter
   protected GitHub hub;
 
+  protected GHMyself myself;
+
   private transient OAuthProtocol oauth;
 
-  private GHMyself myself;
   private SortedSet<Scope> loginScopes;
   private final GitHubOAuthConfig config;
 
@@ -79,6 +82,14 @@
     }
   }
 
+  public Set<String> getMyOrganisationsLogins() throws IOException {
+    if (isLoggedIn()) {
+      return hub.getMyOrganizations().keySet();
+    } else {
+      return Collections.emptySet();
+    }
+  }
+
   @Inject
   public GitHubLogin(final OAuthProtocol oauth, final GitHubOAuthConfig config) {
     this.oauth = oauth;
@@ -91,7 +102,7 @@
       try {
         myself = hub.getMyself();
       } catch (Throwable e) {
-        LOG.error("Connection to GitHub broken: logging out", e);
+        log.error("Connection to GitHub broken: logging out", e);
         logout();
         loggedIn = false;
       }
@@ -111,13 +122,13 @@
       return true;
     }
 
-    LOG.debug("Login " + this);
+    log.debug("Login " + this);
 
     if (OAuthProtocol.isOAuthFinal(request)) {
-      LOG.debug("Login-FINAL " + this);
+      log.debug("Login-FINAL " + this);
       login(oauth.loginPhase2(request, response));
       if (isLoggedIn()) {
-        LOG.debug("Login-SUCCESS " + this);
+        log.debug("Login-SUCCESS " + this);
         response.sendRedirect(OAuthProtocol.getTargetUrl(request));
         return true;
       } else {
@@ -126,7 +137,7 @@
       }
     } else {
       this.loginScopes = getScopes(getScopesKey(request, response), scopes);
-      LOG.debug("Login-PHASE1 " + this);
+      log.debug("Login-PHASE1 " + this);
       oauth.loginPhase1(request, response, loginScopes);
       return false;
     }
@@ -142,6 +153,7 @@
   }
 
   public GitHub login(AccessToken authToken) throws IOException {
+    log.debug("Logging in using access token {}", authToken.access_token);
     this.token = authToken;
     this.hub = GitHub.connectUsingOAuth(authToken.access_token);
     this.myself = hub.getMyself();
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
index ab04304..14cce5c 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/GitHubOAuthConfig.java
@@ -55,6 +55,7 @@
   public final int fileUpdateMaxRetryCount;
   public final int fileUpdateMaxRetryIntervalMsec;
   public final Config gerritConfig;
+  public final String oauthHttpHeader;
 
   @Inject
   public GitHubOAuthConfig(@GerritServerConfig Config config)
@@ -62,6 +63,7 @@
     this.gerritConfig = config;
 
     httpHeader = config.getString("auth", null, "httpHeader");
+    oauthHttpHeader = config.getString("auth", null, "httpExternalIdHeader");
     gitHubUrl = dropTrailingSlash(
         Objects.firstNonNull(config.getString(CONF_SECTION, null, "url"),
             GITHUB_URL));
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/IdentifiedUserGitHubLoginProvider.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/IdentifiedUserGitHubLoginProvider.java
new file mode 100644
index 0000000..bffb7a1
--- /dev/null
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/IdentifiedUserGitHubLoginProvider.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.oauth;
+
+import java.io.IOException;
+import java.util.Collection;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.googlesource.gerrit.plugins.github.oauth.OAuthProtocol.AccessToken;
+
+@Singleton
+public class IdentifiedUserGitHubLoginProvider implements
+    UserScopedProvider<GitHubLogin> {
+  private static final Logger log = LoggerFactory.getLogger(IdentifiedUserGitHubLoginProvider.class);
+  private static final String EXTERNAL_ID_PREFIX = "external:"
+      + OAuthWebFilter.GITHUB_EXT_ID;
+
+  private final Provider<IdentifiedUser> userProvider;
+  private OAuthProtocol oauth;
+  private GitHubOAuthConfig config;
+  private AccountCache accountCache;
+
+  @Inject
+  public IdentifiedUserGitHubLoginProvider(
+      final Provider<IdentifiedUser> identifiedUserProvider,
+      final OAuthProtocol oauth, final GitHubOAuthConfig config,
+      final AccountCache accountCache) {
+    this.userProvider = identifiedUserProvider;
+    this.oauth = oauth;
+    this.config = config;
+    this.accountCache = accountCache;
+  }
+
+  @Override
+  public GitHubLogin get() {
+    IdentifiedUser currentUser = userProvider.get();
+    return get(currentUser.getUserName());
+  }
+
+  @Override
+  @Nullable
+  public GitHubLogin get(String username) {
+    try {
+      AccessToken accessToken = newAccessTokenFromUser(username);
+      if (accessToken != null) {
+        GitHubLogin login = new GitHubLogin(oauth, config);
+        login.login(accessToken);
+        return login;
+      } else {
+        return null;
+      }
+    } catch (IOException e) {
+      log.error("Cannot login to GitHub as '" + username
+          + "'", e);
+      return null;
+    }
+  }
+
+  private AccessToken newAccessTokenFromUser(String username) {
+    AccountState account = accountCache.getByUsername(username);
+    Collection<AccountExternalId> externalIds = account.getExternalIds();
+    for (AccountExternalId accountExternalId : externalIds) {
+      String key = accountExternalId.getKey().get();
+      if (key.startsWith(EXTERNAL_ID_PREFIX)) {
+        return new AccessToken(key.substring(EXTERNAL_ID_PREFIX.length()));
+      }
+    }
+
+    return null;
+  }
+}
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java
index 4eca364..f546753 100644
--- a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/OAuthWebFilter.java
@@ -47,6 +47,7 @@
   private static final org.slf4j.Logger log = LoggerFactory
       .getLogger(OAuthWebFilter.class);
   public static final String GERRIT_COOKIE_NAME = "GerritAccount";
+  public static final String GITHUB_EXT_ID = "github_oauth:";
 
   private final GitHubOAuthConfig config;
   private final Random retryRandom = new Random(System.currentTimeMillis());
@@ -89,7 +90,9 @@
         if (ghLogin != null && ghLogin.isLoggedIn()) {
           httpRequest =
               new AuthenticatedHttpRequest(httpRequest, config.httpHeader,
-                  ghLogin.getMyself().getLogin());
+                  ghLogin.getMyself().getLogin(),
+                  config.oauthHttpHeader,
+                  GITHUB_EXT_ID + ghLogin.getToken().access_token);
         }
 
         if (OAuthProtocol.isOAuthFinalForOthers(httpRequest)) {
diff --git a/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/UserScopedProvider.java b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/UserScopedProvider.java
new file mode 100644
index 0000000..93f474d
--- /dev/null
+++ b/github-oauth/src/main/java/com/googlesource/gerrit/plugins/github/oauth/UserScopedProvider.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.oauth;
+
+import com.google.gerrit.common.Nullable;
+import com.google.inject.Provider;
+
+public interface UserScopedProvider<T> extends Provider<T> {
+  @Nullable T get(String username);
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java
index d4c0b27..21926c4 100644
--- a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/GuiceModule.java
@@ -16,11 +16,32 @@
 
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.webui.TopMenu;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.inject.AbstractModule;
+import com.google.inject.TypeLiteral;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.googlesource.gerrit.plugins.github.group.GitHubGroupBackend;
+import com.googlesource.gerrit.plugins.github.group.GitHubGroupMembership;
+import com.googlesource.gerrit.plugins.github.group.GitHubGroupsCache;
+import com.googlesource.gerrit.plugins.github.group.GitHubOrganisationGroup;
+import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
+import com.googlesource.gerrit.plugins.github.oauth.IdentifiedUserGitHubLoginProvider;
+import com.googlesource.gerrit.plugins.github.oauth.UserScopedProvider;
 
 public class GuiceModule extends AbstractModule {
   @Override
   protected void configure() {
+    bind(new TypeLiteral<UserScopedProvider<GitHubLogin>>() {}).to(
+        IdentifiedUserGitHubLoginProvider.class);
+
+    install(GitHubGroupsCache.module());
+
     DynamicSet.bind(binder(), TopMenu.class).to(GitHubTopMenu.class);
+    DynamicSet.bind(binder(), GroupBackend.class).to(GitHubGroupBackend.class);
+
+    install(new FactoryModuleBuilder()
+        .build(GitHubOrganisationGroup.Factory.class));
+    install(new FactoryModuleBuilder()
+        .build(GitHubGroupMembership.Factory.class));
   }
 }
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroup.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroup.java
new file mode 100644
index 0000000..4f5947c
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroup.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.group;
+
+import lombok.Getter;
+
+import com.google.gerrit.common.data.GroupDescription.Basic;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+
+public abstract class GitHubGroup implements Basic {
+  public static final String UUID_PREFIX = "github:";
+  public static final String NAME_PREFIX = "github/";
+
+  @Getter
+  protected final UUID groupUUID;
+
+  @Getter
+  protected final String url;
+
+  GitHubGroup(UUID groupUUID, String url) {
+    this.groupUUID = groupUUID;
+    this.url = url;
+  }
+
+  @Override
+  public String getEmailAddress() {
+    return "";
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java
new file mode 100644
index 0000000..b6fd5cf
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.group;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.googlesource.gerrit.plugins.github.group.GitHubOrganisationGroup.NAME_PREFIX;
+import static com.googlesource.gerrit.plugins.github.group.GitHubOrganisationGroup.UUID_PREFIX;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSet.Builder;
+import com.google.gerrit.common.data.GroupDescription.Basic;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.inject.Inject;
+import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
+import com.googlesource.gerrit.plugins.github.oauth.UserScopedProvider;
+
+public class GitHubGroupBackend implements GroupBackend {
+  private static final Logger log = LoggerFactory
+      .getLogger(GitHubGroupBackend.class);
+  private final GitHubGroupMembership.Factory ghMembershipProvider;
+  private final GitHubGroupsCache ghOrganisationCache;
+
+  @Inject
+  GitHubGroupBackend(
+      UserScopedProvider<GitHubLogin> ghLogin,
+      GitHubGroupMembership.Factory ghMembershipProvider,
+      GitHubGroupsCache ghOrganisationCache) {
+    this.ghMembershipProvider = ghMembershipProvider;
+    this.ghOrganisationCache = ghOrganisationCache;
+  }
+
+  @Override
+  public boolean handles(UUID uuid) {
+    return uuid.get().startsWith(UUID_PREFIX);
+  }
+
+  @Override
+  public Basic get(UUID uuid) {
+    checkArgument(handles(uuid), "{} is not a valid GitHub Group UUID",
+        uuid.get());
+    return GitHubOrganisationGroup.fromUUID(uuid);
+  }
+
+  @Override
+  public Collection<GroupReference> suggest(String name, ProjectControl project) {
+    if (!name.startsWith(NAME_PREFIX)) {
+      return Collections.emptyList();
+    }
+    String orgNamePrefix = name.substring(NAME_PREFIX.length());
+    return listByPrefix(orgNamePrefix);
+  }
+
+  public Set<GroupReference> listByPrefix(String orgNamePrefix) {
+    try {
+      log.debug("Listing user's organisations starting with '{}'",
+          orgNamePrefix);
+
+      String[] namePrefixParts = orgNamePrefix.toLowerCase().split("/");
+      String orgNamePrefixLowercase =
+          namePrefixParts.length > 0 ? namePrefixParts[0] : "";
+      String teamNameLowercase =
+          namePrefixParts.length > 1 ? namePrefixParts[1] : "";
+
+      Set<String> ghOrgs = ghOrganisationCache.getOrganizationsForCurrentUser();
+      log.debug("Full list of user's organisations: {}", ghOrgs);
+
+      Builder<GroupReference> orgGroups =
+          new ImmutableSet.Builder<GroupReference>();
+      for (String organizationName : ghOrgs) {
+        if (organizationName.toLowerCase().startsWith(orgNamePrefixLowercase)) {
+          GroupReference teamGroupRef =
+              GitHubOrganisationGroup.groupReference(organizationName);
+
+          if ((orgNamePrefixLowercase.length() > 0 && orgNamePrefix
+              .endsWith("/")) || teamNameLowercase.length() > 0) {
+            for (String teamName : ghOrganisationCache.getTeamsForCurrentUser(organizationName)) {
+              if (teamName.toLowerCase().startsWith(teamNameLowercase)) {
+                orgGroups.add(GitHubTeamGroup.groupReference(teamGroupRef,
+                    teamName));
+              }
+            }
+          } else {
+            orgGroups.add(teamGroupRef);
+          }
+        }
+      }
+      return orgGroups.build();
+    } catch (ExecutionException e) {
+      log.warn("Cannot get GitHub organisations matching '" + orgNamePrefix
+          + "'", e);
+    }
+
+    return Collections.emptySet();
+  }
+
+  @Override
+  public GroupMembership membershipsOf(IdentifiedUser user) {
+    return ghMembershipProvider.get(user.getUserName());
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java
new file mode 100644
index 0000000..b93c138
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.group;
+
+import java.util.Set;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class GitHubGroupMembership implements GroupMembership {
+  private final Set<UUID> groups;
+
+  public interface Factory {
+    GitHubGroupMembership get(@Assisted String username);
+  }
+
+  @Inject
+  GitHubGroupMembership(GitHubGroupsCache ghOrganisationCache,
+      @Assisted String username) {
+    this.groups =
+        new ImmutableSet.Builder<UUID>().addAll(
+            ghOrganisationCache.getGroupsForUser(username)).build();
+  }
+
+  @Override
+  public boolean contains(UUID groupId) {
+    return groups.contains(groupId);
+  }
+
+  @Override
+  public boolean containsAnyOf(Iterable<UUID> groupIds) {
+    return !intersection(groupIds).isEmpty();
+  }
+
+  @Override
+  public Set<UUID> intersection(Iterable<UUID> groupIds) {
+    ImmutableSet.Builder<UUID> groups = new ImmutableSet.Builder<>();
+    for (UUID uuid : groupIds) {
+      if (contains(uuid)) {
+        groups.add(uuid);
+      }
+    }
+    return groups.build();
+  }
+
+  @Override
+  public Set<UUID> getKnownGroups() {
+    return groups;
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupsCache.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupsCache.java
new file mode 100644
index 0000000..d02d4ff
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupsCache.java
@@ -0,0 +1,176 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.group;
+
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+import org.kohsuke.github.GHTeam;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.googlesource.gerrit.plugins.github.oauth.GitHubLogin;
+import com.googlesource.gerrit.plugins.github.oauth.UserScopedProvider;
+
+@Singleton
+public class GitHubGroupsCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(GitHubGroupsCache.class);
+  private static final String ORGS_CACHE_NAME = "groups";
+  protected static final long GROUPS_CACHE_TTL_MINS = 60;
+  public static final String EVERYONE_TEAM_NAME = "Everyone";
+
+  public static class OrganisationLoader extends
+      CacheLoader<String, Multimap<String, String>> {
+    private static final Logger log = LoggerFactory
+        .getLogger(OrganisationLoader.class);
+    private final UserScopedProvider<GitHubLogin> ghLoginProvider;
+
+    @Inject
+    public OrganisationLoader(UserScopedProvider<GitHubLogin> ghLoginProvider) {
+      this.ghLoginProvider = ghLoginProvider;
+    }
+
+    @Override
+    public Multimap<String, String> load(String username) throws Exception {
+      Multimap<String, String> orgsTeams = HashMultimap.create();
+      GitHubLogin ghLogin = ghLoginProvider.get(username);
+      if (ghLogin == null) {
+        log.warn("Cannot login to GitHub on behalf of '{}'", username);
+        return orgsTeams;
+      }
+
+      try {
+        loadOrganisationsAndTeams(username, orgsTeams, ghLogin);
+      } catch (FileNotFoundException teamsNotFound) {
+        log.warn(
+            "Cannot access teams for user '{}': falling back to list of public organisations",
+            username);
+        loadOrganisations(username, orgsTeams, ghLogin);
+      }
+
+      log.debug("GitHub user '{}' belongs to: {}", username, orgsTeams);
+      return orgsTeams;
+    }
+
+    private void loadOrganisationsAndTeams(String username, Multimap<String, String> orgsTeams,
+        GitHubLogin ghLogin) throws IOException {
+      log.debug("Getting list of organisations/teams for user '{}'", username);
+      Map<String, Set<GHTeam>> myOrganisationsLogins =
+          ghLogin.getHub().getMyTeams();
+      for (Entry<String, Set<GHTeam>> teamsOrg : myOrganisationsLogins
+          .entrySet()) {
+        orgsTeams.put(teamsOrg.getKey(), EVERYONE_TEAM_NAME);
+        for (GHTeam team : teamsOrg.getValue()) {
+          orgsTeams.put(teamsOrg.getKey(), team.getName());
+        }
+      }
+    }
+
+    private void loadOrganisations(String username,
+        Multimap<String, String> orgsTeams, GitHubLogin ghLogin) throws IOException {
+      log.debug("Getting list of public organisations for user '{}'", username);
+      Set<String> organisations = ghLogin.getMyOrganisationsLogins();
+      for (String org : organisations) {
+        orgsTeams.put(org, EVERYONE_TEAM_NAME);
+      }
+    }
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(ORGS_CACHE_NAME, String.class,
+            new TypeLiteral<Multimap<String, String>>() {}).expireAfterWrite(
+            GROUPS_CACHE_TTL_MINS, MINUTES).loader(OrganisationLoader.class);
+        bind(GitHubGroupsCache.class);
+      }
+    };
+  }
+
+  private final LoadingCache<String, Multimap<String, String>> orgTeamsByUsername;
+  private final Provider<IdentifiedUser> userProvider;
+
+  @Inject
+  GitHubGroupsCache(
+      @Named(ORGS_CACHE_NAME) LoadingCache<String, Multimap<String, String>> byUsername,
+      Provider<IdentifiedUser> userProvider) {
+    this.orgTeamsByUsername = byUsername;
+    this.userProvider = userProvider;
+  }
+
+  Set<String> getOrganizationsForUser(String username) {
+    try {
+      return orgTeamsByUsername.get(username).keySet();
+    } catch (ExecutionException e) {
+      log.warn("Cannot get GitHub organisations for user '" + username + "'", e);
+      return Collections.emptySet();
+    }
+  }
+
+  Set<String> getOrganizationsForCurrentUser() throws ExecutionException {
+    return orgTeamsByUsername.get(userProvider.get().getUserName()).keySet();
+  }
+
+  Set<String> getTeamsForUser(String organizationName, String username) {
+    try {
+      return new ImmutableSet.Builder<String>().addAll(
+          orgTeamsByUsername.get(username).get(organizationName)).build();
+    } catch (ExecutionException e) {
+      log.warn("Cannot get Teams membership for organisation '"
+          + organizationName + "' and user '" + username + "'", e);
+      return Collections.emptySet();
+    }
+  }
+
+  Set<String> getTeamsForCurrentUser(String organizationName) {
+    return getTeamsForUser(organizationName, userProvider.get().getUserName());
+  }
+
+  public Set<UUID> getGroupsForUser(String username) {
+    ImmutableSet.Builder<UUID> groupsBuilder = new ImmutableSet.Builder<>();
+    for (String org : getOrganizationsForUser(username)) {
+      groupsBuilder.add(GitHubOrganisationGroup.uuid(org));
+
+      for (String team : getTeamsForUser(org, username)) {
+        groupsBuilder.add(GitHubTeamGroup.uuid(
+            GitHubOrganisationGroup.uuid(org), team));
+      }
+    }
+    return groupsBuilder.build();
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationGroup.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationGroup.java
new file mode 100644
index 0000000..7dca217
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationGroup.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.group;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupDescription.Basic;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class GitHubOrganisationGroup extends GitHubGroup implements Basic {
+  public interface Factory {
+    GitHubOrganisationGroup get(@Assisted("orgName") String orgName,
+        @Assisted("orgUrl") @Nullable String orgUrl);
+  }
+
+  private final String orgName;
+
+  @Inject
+  GitHubOrganisationGroup(@Assisted("orgName") String orgName,
+      @Assisted("orgUrl") @Nullable String orgUrl) {
+    super(uuid(orgName), orgUrl);
+    this.orgName = orgName;
+  }
+
+  @Override
+  public String getName() {
+    return NAME_PREFIX + orgName;
+  }
+
+  public static GitHubOrganisationGroup fromUUID(UUID uuid) {
+    checkArgument(uuid.get().startsWith(UUID_PREFIX), "Invalid GitHub UUID '"
+        + uuid + "'");
+    return new GitHubOrganisationGroup(uuid.get().substring(
+        UUID_PREFIX.length()), null);
+  }
+
+  public static UUID uuid(String orgName) {
+    return new AccountGroup.UUID(UUID_PREFIX + orgName);
+  }
+
+  public static GroupReference groupReference(String orgName) {
+    return new GroupReference(uuid(orgName), NAME_PREFIX + orgName);
+  }
+}
diff --git a/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubTeamGroup.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubTeamGroup.java
new file mode 100644
index 0000000..cd53736
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubTeamGroup.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2014 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.googlesource.gerrit.plugins.github.group;
+
+import com.google.gerrit.common.Nullable;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup.UUID;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class GitHubTeamGroup extends GitHubGroup {
+  public interface Factory {
+    GitHubTeamGroup get(@Assisted GitHubOrganisationGroup orgGroup,
+        @Assisted String teamName, @Nullable String teamUrl);
+  }
+
+  private final GitHubOrganisationGroup orgGroup;
+  private final String teamName;
+
+  @Inject
+  GitHubTeamGroup(@Assisted GitHubOrganisationGroup orgGroup,
+      @Assisted String teamName, @Nullable String teamUrl) {
+    super(uuid(orgGroup.getGroupUUID(), teamName), teamUrl);
+    this.orgGroup = orgGroup;
+    this.teamName = teamName;
+  }
+
+  @Override
+  public String getName() {
+    return orgGroup.getName() + "/" + teamName;
+  }
+
+  public static UUID uuid(UUID orgUUID, String teamName) {
+    return new AccountGroup.UUID(orgUUID.get() + "/" + teamName);
+  }
+
+  public static GroupReference groupReference(GroupReference orgReference,
+      String teamName) {
+    return new GroupReference(uuid(orgReference.getUUID(), teamName),
+        orgReference.getName() + "/" + teamName);
+  }
+}