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);
+ }
+}