GitHub organisations plugged as GroupBackend

Initial support for using GitHub organisations
plugged into Gerrit as GroupBackend and thus visibles from:
- User’s group membership (/#/settings/group-memberships)
- Project’s access control (/#/admin/projects/All-Projects,access)

By typing github/OrgName in the Project’s access screen you will see
listed all the organisations that your user belongs to and you can then
assign Gerrit project permissions to it.
The members of that organisation will automatically be granted 
permissions to the Gerrit project. 

Membership is resolved at runtime by making a GitHub API call using
the current user scope, with a cache TTL of 15’. This means
that only 4 GitHub calls/h per user can be actually consumed 
by group resolution. Changes on GitHub organisation membership
will be then detected by Gerrit after at least 15’.

Gerrit cache for GitHub organisations membership is called:
github-plugin-organisations.

Change-Id: I051cd44c978b7f25a5508a33c298fd18b904177e
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/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/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..8822bd9 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.GitHubOrganisationsCache;
+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(GitHubOrganisationsCache.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/GitHubGroupBackend.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java
new file mode 100644
index 0000000..af72030
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupBackend.java
@@ -0,0 +1,107 @@
+// 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 GitHubOrganisationsCache ghOrganisationCache;
+
+  @Inject
+  public GitHubGroupBackend(
+      UserScopedProvider<GitHubLogin> ghLogin,
+      GitHubGroupMembership.Factory ghMembershipProvider,
+      GitHubOrganisationsCache 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 orgNamePrefixLowercase = orgNamePrefix.toLowerCase();
+      Set<String> ghOrgs = ghOrganisationCache.getOrganisationsForCurrentUser();
+      log.debug("Full list of user's organisations: {}", ghOrgs);
+
+      Builder<GroupReference> orgGroups =
+          new ImmutableSet.Builder<GroupReference>();
+      for (String ghOrg : ghOrgs) {
+        if (ghOrg.toLowerCase().startsWith(orgNamePrefixLowercase)) {
+          orgGroups.add(GitHubOrganisationGroup.groupReference(ghOrg));
+        }
+      }
+      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..26a12bc
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubGroupMembership.java
@@ -0,0 +1,66 @@
+// 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
+  public GitHubGroupMembership(GitHubOrganisationsCache ghOrganisationCache,
+      @Assisted String username) {
+    this.groups =
+        new ImmutableSet.Builder<UUID>().addAll(
+            ghOrganisationCache.getOrganisationsGroupsForUsername(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/GitHubOrganisationGroup.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationGroup.java
new file mode 100644
index 0000000..ace3687
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationGroup.java
@@ -0,0 +1,82 @@
+// 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 implements Basic {
+  public static final String UUID_PREFIX = "github:";
+  public static final String NAME_PREFIX = "github/";
+
+  public interface Factory {
+    GitHubOrganisationGroup get(@Assisted("orgName") String orgName,
+        @Assisted("orgUrl") @Nullable String orgUrl);
+  }
+
+  private final String orgName;
+  private final UUID uuid;
+  private final String url;
+
+  @Inject
+  GitHubOrganisationGroup(@Assisted("orgName") String orgName,
+      @Assisted("orgUrl") @Nullable String orgUrl) {
+    this.orgName = orgName;
+    this.uuid = uuid(orgName);
+    this.url = orgUrl;
+  }
+
+  @Override
+  public String getEmailAddress() {
+    return "";
+  }
+
+  @Override
+  public UUID getGroupUUID() {
+    return uuid;
+  }
+
+  @Override
+  public String getName() {
+    return NAME_PREFIX + orgName;
+  }
+
+  @Override
+  public String getUrl() {
+    return url;
+  }
+
+  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/GitHubOrganisationsCache.java b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationsCache.java
new file mode 100644
index 0000000..56bbe31
--- /dev/null
+++ b/github-plugin/src/main/java/com/googlesource/gerrit/plugins/github/group/GitHubOrganisationsCache.java
@@ -0,0 +1,119 @@
+// 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.util.Collections;
+import java.util.Set;
+import java.util.concurrent.ExecutionException;
+
+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.ImmutableSet;
+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 GitHubOrganisationsCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(GitHubOrganisationsCache.class);
+  private static final String CACHE_NAME = "organisations";
+  protected static final long GROUPS_CACHE_TTL_MINS = 15;
+
+  public static class Loader extends CacheLoader<String, Set<String>> {
+    private static final Logger log = LoggerFactory.getLogger(Loader.class);
+    private final UserScopedProvider<GitHubLogin> ghLoginProvider;
+
+    @Inject
+    public Loader(UserScopedProvider<GitHubLogin> ghLoginProvider) {
+      this.ghLoginProvider = ghLoginProvider;
+    }
+
+    @Override
+    public Set<String> load(String username) throws Exception {
+      GitHubLogin ghLogin = ghLoginProvider.get(username);
+      if (ghLogin == null) {
+        log.warn("Cannot login to GitHub on behalf of '{}'", username);
+        return Collections.emptySet();
+      }
+
+      log.debug("Getting list of organisations for user '{}'", username);
+      Set<String> myOrganisationsLogins = ghLogin.getMyOrganisationsLogins();
+      log.debug("GitHub user '{}' belongs to: {}", username, myOrganisationsLogins);
+      return myOrganisationsLogins;
+    }
+  }
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(CACHE_NAME, String.class, new TypeLiteral<Set<String>>() {})
+            .expireAfterWrite(GROUPS_CACHE_TTL_MINS, MINUTES)
+            .loader(Loader.class);
+        bind(GitHubOrganisationsCache.class);
+      }
+    };
+  }
+
+  private LoadingCache<String, Set<String>> byUsername;
+  private final Provider<IdentifiedUser> userProvider;
+
+  @Inject
+  public GitHubOrganisationsCache(
+      @Named(CACHE_NAME) LoadingCache<String, Set<String>> byUsername,
+      Provider<IdentifiedUser> userProvider) {
+    this.byUsername = byUsername;
+    this.userProvider = userProvider;
+  }
+
+  public Set<String> getOrganisationsForUsername(String username)
+      throws ExecutionException {
+    return byUsername.get(username);
+  }
+  
+  public Set<String> getOrganisationsForCurrentUser()
+      throws ExecutionException {
+    return byUsername.get(userProvider.get().getUserName());
+  }
+
+  public Set<UUID> getOrganisationsGroupsForUsername(String username) {
+    try {
+      Set<String> ghOrgsLogins = getOrganisationsForUsername(username);
+      log.debug("GitHub user '{}' belongs to: {}", username, ghOrgsLogins);
+      ImmutableSet.Builder<UUID> groupSet = new ImmutableSet.Builder<UUID>();
+      for (String ghOrg : ghOrgsLogins) {
+        groupSet.add(GitHubOrganisationGroup.uuid(ghOrg));
+      }
+      return groupSet.build();
+    } catch (ExecutionException e) {
+      log.warn("Cannot get GitHub organisations for user '" + username + "'", e);
+    }
+    return Collections.emptySet();
+  }
+}