Merge "Prevent removing e-mail associated with OpenId accounts" into stable-3.2
diff --git a/Documentation/config-accounts.txt b/Documentation/config-accounts.txt
index b4a5cef..67159f4 100644
--- a/Documentation/config-accounts.txt
+++ b/Documentation/config-accounts.txt
@@ -359,6 +359,15 @@
 * hashed passwords of external IDs with scheme `username` cannot be
   decoded
 
+Users can edit some external IDs via the user settings page or the
+REST API. Note that email addresses cannot be deleted if they are
+associated with the user's login credentials external ID, for
+example the email address associated with an OpenId or OAUTH external
+ID. If users wish to remove these email addresses from Gerrit they must
+first update the external authentication record in that system,
+log in to Gerrit, then Gerrit will update the external ID record with
+the new email address.
+
 [[starred-changes]]
 == Starred Changes
 
diff --git a/java/com/google/gerrit/server/account/externalids/ExternalId.java b/java/com/google/gerrit/server/account/externalids/ExternalId.java
index 2d501ad..1d31da9 100644
--- a/java/com/google/gerrit/server/account/externalids/ExternalId.java
+++ b/java/com/google/gerrit/server/account/externalids/ExternalId.java
@@ -136,6 +136,15 @@
   /** Scheme for external auth used during authentication, e.g. OAuth Token */
   public static final String SCHEME_EXTERNAL = "external";
 
+  /** Scheme for http resources. OpenID in particular makes use of these external IDs. */
+  public static final String SCHEME_HTTP = "http";
+
+  /** Scheme for https resources. OpenID in particular makes use of these external IDs. */
+  public static final String SCHEME_HTTPS = "https";
+
+  /** Scheme for xri resources. OpenID in particular makes use of these external IDs. */
+  public static final String SCHEME_XRI = "xri";
+
   @AutoValue
   public abstract static class Key implements Serializable {
     private static final long serialVersionUID = 1L;
diff --git a/java/com/google/gerrit/server/auth/openid/OpenIdRealm.java b/java/com/google/gerrit/server/auth/openid/OpenIdRealm.java
new file mode 100644
index 0000000..4e1a1ce
--- /dev/null
+++ b/java/com/google/gerrit/server/auth/openid/OpenIdRealm.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2021 Open Infrastructure Foundation
+//
+// 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.google.gerrit.server.auth.openid;
+
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTP;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTPS;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_XRI;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.gerrit.server.account.DefaultRealm;
+import com.google.gerrit.server.account.EmailExpander;
+import com.google.gerrit.server.account.Emails;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import java.util.Collection;
+
+@Singleton
+public class OpenIdRealm extends DefaultRealm {
+  @Inject
+  @VisibleForTesting
+  public OpenIdRealm(EmailExpander emailExpander, Provider<Emails> emails, AuthConfig authConfig) {
+    super(emailExpander, emails, authConfig);
+  }
+
+  @Override
+  public boolean accountBelongsToRealm(Collection<ExternalId> externalIds) {
+    for (ExternalId id : externalIds) {
+      if (id.isScheme(SCHEME_HTTP) || id.isScheme(SCHEME_HTTPS) || id.isScheme(SCHEME_XRI)) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/java/com/google/gerrit/server/config/AuthModule.java b/java/com/google/gerrit/server/config/AuthModule.java
index 5b0f73d..56f2e2a 100644
--- a/java/com/google/gerrit/server/config/AuthModule.java
+++ b/java/com/google/gerrit/server/config/AuthModule.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.auth.InternalAuthBackend;
 import com.google.gerrit.server.auth.ldap.LdapModule;
 import com.google.gerrit.server.auth.oauth.OAuthRealm;
+import com.google.gerrit.server.auth.openid.OpenIdRealm;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 
@@ -50,10 +51,14 @@
       case CUSTOM_EXTENSION:
         break;
 
-      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-      case HTTP:
       case OPENID:
       case OPENID_SSO:
+        bind(Realm.class).to(OpenIdRealm.class);
+        DynamicSet.bind(binder(), AuthBackend.class).to(InternalAuthBackend.class);
+        break;
+
+      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
+      case HTTP:
       default:
         bind(Realm.class).to(DefaultRealm.class);
         DynamicSet.bind(binder(), AuthBackend.class).to(InternalAuthBackend.class);
diff --git a/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java b/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
index ba40d8c..042222f 100644
--- a/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
+++ b/javatests/com/google/gerrit/server/auth/ldap/LdapRealmTest.java
@@ -16,8 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTP;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTPS;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_XRI;
 import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_CACHE;
 import static com.google.gerrit.server.auth.ldap.LdapModule.GROUP_EXIST_CACHE;
 import static com.google.gerrit.server.auth.ldap.LdapModule.PARENT_GROUPS_CACHE;
@@ -90,5 +93,8 @@
     assertThat(accountBelongsToRealm(SCHEME_USERNAME, "xxgerritxx")).isFalse();
     assertThat(accountBelongsToRealm(SCHEME_MAILTO, "gerrit.foo@bar.com")).isFalse();
     assertThat(accountBelongsToRealm(SCHEME_MAILTO, "bar.gerrit@bar.com")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_HTTP, "example.org/test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_HTTPS, "example.org/test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_XRI, "example.org/test")).isFalse();
   }
 }
diff --git a/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java b/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
index dc62a61..8f7cc35 100644
--- a/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
+++ b/javatests/com/google/gerrit/server/auth/oauth/OAuthRealmTest.java
@@ -16,8 +16,11 @@
 
 import static com.google.common.truth.Truth.assertThat;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTP;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTPS;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
 import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_XRI;
 
 import com.google.gerrit.entities.Account;
 import com.google.gerrit.server.account.externalids.ExternalId;
@@ -65,5 +68,8 @@
     assertThat(accountBelongsToRealm(SCHEME_USERNAME, "xxexternalxx")).isFalse();
     assertThat(accountBelongsToRealm(SCHEME_MAILTO, "external.foo@bar.com")).isFalse();
     assertThat(accountBelongsToRealm(SCHEME_MAILTO, "bar.external@bar.com")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_HTTP, "example.org/test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_HTTPS, "example.org/test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_XRI, "example.org/test")).isFalse();
   }
 }
diff --git a/javatests/com/google/gerrit/server/auth/openid/OpenIdRealmTest.java b/javatests/com/google/gerrit/server/auth/openid/OpenIdRealmTest.java
new file mode 100644
index 0000000..f5373ad
--- /dev/null
+++ b/javatests/com/google/gerrit/server/auth/openid/OpenIdRealmTest.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2021 Open Infrastructure Foundation
+//
+// 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.google.gerrit.server.auth.openid;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_EXTERNAL;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTP;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_HTTPS;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_XRI;
+
+import com.google.gerrit.entities.Account;
+import com.google.gerrit.server.account.externalids.ExternalId;
+import com.google.gerrit.testing.InMemoryModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import java.util.Arrays;
+import org.junit.Before;
+import org.junit.Test;
+
+public final class OpenIdRealmTest {
+  @Inject private OpenIdRealm openidRealm = null;
+
+  @Before
+  public void setUpInjector() throws Exception {
+    Injector injector = Guice.createInjector(new InMemoryModule());
+    injector.injectMembers(this);
+  }
+
+  private ExternalId id(String scheme, String id) {
+    return ExternalId.create(scheme, id, Account.id(1000));
+  }
+
+  private boolean accountBelongsToRealm(ExternalId... ids) {
+    return openidRealm.accountBelongsToRealm(Arrays.asList(ids));
+  }
+
+  private boolean accountBelongsToRealm(String scheme, String id) {
+    return accountBelongsToRealm(id(scheme, id));
+  }
+
+  @Test
+  public void accountBelongsToRealm() throws Exception {
+    assertThat(accountBelongsToRealm(SCHEME_HTTP, "example.org/test")).isTrue();
+    assertThat(accountBelongsToRealm(SCHEME_HTTPS, "example.org/test")).isTrue();
+    assertThat(accountBelongsToRealm(SCHEME_XRI, "example.org/test")).isTrue();
+    assertThat(
+            accountBelongsToRealm(id(SCHEME_USERNAME, "test"), id(SCHEME_HTTP, "example.org/test")))
+        .isTrue();
+    assertThat(
+            accountBelongsToRealm(
+                id(SCHEME_USERNAME, "test"), id(SCHEME_HTTPS, "example.org/test")))
+        .isTrue();
+    assertThat(
+            accountBelongsToRealm(id(SCHEME_USERNAME, "test"), id(SCHEME_XRI, "example.org/test")))
+        .isTrue();
+    assertThat(
+            accountBelongsToRealm(id(SCHEME_HTTP, "example.org/test"), id(SCHEME_USERNAME, "test")))
+        .isTrue();
+    assertThat(
+            accountBelongsToRealm(
+                id(SCHEME_HTTPS, "example.org/test"), id(SCHEME_USERNAME, "test")))
+        .isTrue();
+    assertThat(
+            accountBelongsToRealm(id(SCHEME_XRI, "test"), id(SCHEME_USERNAME, "example.org/test")))
+        .isTrue();
+
+    assertThat(accountBelongsToRealm(SCHEME_EXTERNAL, "test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_GERRIT, "test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "test")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "foo@bar.com")).isFalse();
+
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "gerrit")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_USERNAME, "xxgerritxx")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "gerrit.foo@bar.com")).isFalse();
+    assertThat(accountBelongsToRealm(SCHEME_MAILTO, "bar.gerrit@bar.com")).isFalse();
+  }
+}