summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorShawn O. Pearce <sop@google.com>2009-02-23 10:27:49 -0800
committerShawn O. Pearce <sop@google.com>2009-02-23 11:32:23 -0800
commitf3de4b1b7c19fe2d88fdd28e53fe937e52745ead (patch)
treeb22fc821a2ebba469afa0100dac16f1b5a0ba0ce
parentb71dca0adb909f6dafc76a56366a35c60c0542ff (diff)
Move all contact information out of database to encrypted store
A security review strongly suggested moving the personal contact details for an account out of the database and into an encrypted data store that is stored separately from the rest of Gerrit's metadata. The rationale being that the contact information is really quite personal, and just doesn't need to be accessed, except in the most extreme circumstances, like if a court has issued a valid subpoena to the Gerrit administrators to turn over contact information for a specific account. Any captured contact information is now encrypted using GnuPG, and fired off via SSL protected HTTP POST to another system. That other system could be "gerrit-contactstore", running on Google App Engine, or it could be a very simple CGI which stores the encrypted data to files on disk. With this change, Gerrit only has the user's contact information transiently in memory while it is encrypting the message for long-term storage. Only the GnuPG public key needs to be available, so Gerrit reads an ASCII armored key, e.g. "gpg --export -a KEY >pub", simplifying the installation of Gerrit. Signed-off-by: Shawn O. Pearce <sop@google.com>
-rw-r--r--Documentation/config-contact.txt172
-rw-r--r--Documentation/config-gerrit.txt10
-rw-r--r--Documentation/index.txt1
-rw-r--r--pom.xml7
-rw-r--r--src/main/java/com/google/gerrit/client/account/AccountConstants.java1
-rw-r--r--src/main/java/com/google/gerrit/client/account/AccountConstants.properties8
-rw-r--r--src/main/java/com/google/gerrit/client/account/AccountMessages.java3
-rw-r--r--src/main/java/com/google/gerrit/client/account/AccountMessages.properties1
-rw-r--r--src/main/java/com/google/gerrit/client/account/AccountSecurity.java3
-rw-r--r--src/main/java/com/google/gerrit/client/account/ContactPanel.java104
-rw-r--r--src/main/java/com/google/gerrit/client/data/GerritConfig.java9
-rw-r--r--src/main/java/com/google/gerrit/client/reviewdb/Account.java16
-rw-r--r--src/main/java/com/google/gerrit/client/reviewdb/ContactInformation.java18
-rw-r--r--src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java2
-rw-r--r--src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java8
-rw-r--r--src/main/java/com/google/gerrit/client/rpc/ContactInformationStoreException.java28
-rw-r--r--src/main/java/com/google/gerrit/pgm/EncryptContactInfo.java98
-rw-r--r--src/main/java/com/google/gerrit/public/Gerrit.css12
-rw-r--r--src/main/java/com/google/gerrit/server/AccountSecurityImpl.java23
-rw-r--r--src/main/java/com/google/gerrit/server/EncryptedContactStore.java323
-rw-r--r--src/main/java/com/google/gerrit/server/GerritServer.java9
-rw-r--r--src/main/java/com/google/gerrit/server/ssh/Receive.java4
-rw-r--r--src/main/webapp/WEB-INF/sql/upgrade004_005_part1.sql31
-rw-r--r--src/main/webapp/WEB-INF/sql/upgrade004_005_part2.sql7
24 files changed, 848 insertions, 50 deletions
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
new file mode 100644
index 0000000000..ee15e422f0
--- /dev/null
+++ b/Documentation/config-contact.txt
@@ -0,0 +1,172 @@
+Gerrit2 - Contact Information
+=============================
+
+To help ensure contributor privacy, but still support gathering of
+contributor agreements as necessary, Gerrit encrypts all offline
+contact information gathered from users. This data is shipped to
+another server, typically at a different location, to make it more
+difficult for an attacker to obtain.
+
+This feature is optional. If the crypto APIs aren't installed
+and the `contact_store_url` column in `system_config` is left
+NULL, Gerrit will not collect contact information from users.
+
+
+Setup
+-----
+
+Ensure Bouncy Castle Crypto API is available in the web application's
+CLASSPATH (e.g. in `'JETTY_HOME'/lib/plus` for Jetty). Gerrit needs
+both `bcprov-jdk\*-*.jar` and `bcpg-jdk\*-*.jar` to be provided
+for the contact encryption to work.
+
+* link:http://www.bouncycastle.org/latest_releases.html[Bouncy Castle Crypto API]
+
+Ensure a proper JCE policy file is installed. By default most
+JRE installations forbid the use of a strong key, resulting in
+SecurityException messages when trying to encrypt the contact data.
+You need to obtain a strong JCE policy file and install it by hand.
+Look for the 'Unlimited Strength Jurisdiction Policy' download.
+
+* link:http://java.sun.com/javase/downloads/index.jsp[Java SE Downloads]
+
+Create a public/private key pair for contact data handling.
+Generate the keys on a protected system, where the resulting
+private key is unlikely to fall into the wrong hands.
+
+====
+ gpg --gen-key
+====
+
+Select to use a `DSA and Elgamal` key type, as the public key will
+be used for data encryption.
+
+The information chosen for name, email and comment fields can be
+anything reasonable which would identify the contact store of this
+Gerrit instance. It is probably a good idea to not use a real
+person's name here, but instead some sort of organizational role.
+The actual values chosen don't matter later, and are only to help
+document the purpose of the key.
+
+Chose a fairly long expiration period, such as 20 years. For most
+Gerrit instances, contact data will be written once, and rarely,
+if ever, read back.
+
+Export the public key for Gerrit to use during encryption. The
+public key must be stored in a file called `contact_information.pub`
+and reside inside of the `site_config` directory. Armoring it
+during export makes it easier to transport between systems, as
+you can easily copy-and-paste the text. Gerrit can read both the
+armored and unarmored formats.
+
+====
+ gpg --export --armor KEYEMAIL >$site_config/contact_information.pub
+====
+
+Consider storing the private key with some sort of key escrow
+service within your organization. Without the private key it
+is impossible to recover contact records.
+
+Install a contact store implementation somewhere to receive
+the contact records. To be really paranoid, Gerrit always
+ships the data to another HTTP server, preferrably over HTTPS.
+Existing open-source server implementations can be found in the
+gerrit-contactstore project.
+
+* link:http://android.git.kernel.org/?p=tools/gerrit-contactstore.git[gerrit-contactstore]
+
+Configure the `system_config` table with the contact store's URL
+(in field `contact_store_url`), and if needed, APPSEC value (in
+field `contact_store_appsec`).
+
+
+Contact Store Protocol
+----------------------
+
+To implement a new contact store, the following details are useful.
+
+Gerrit connects to the contact store by sending a standard
+HTTP POST request to the store URL (the exact URL that is in
+`contact_store_url` in the `system_config` table), with the
+following form parameters in the body:
+
+* APPSEC
++
+A shared secret "password" that should be known only to Gerrit
+and the contact store. The contact store should test this value to
+deter spamming of the contact store by outside parties. Gerrit reads
+this from `system_config` in the `contact_store_appsec` field.
+
+* account_id
++
+Unique account_id value from the Gerrit database for the account
+the contact information belongs to. Base 10 integer.
+
+* email
++
+Preferred email address of the account. May facilitate lookups in
+the contact store at a future date. May be omitted or the empty
+string if the user hasn't chosen a preferred email.
+
+* filed
++
+Seconds since the UNIX epoch of when the contact information
+was filed. May be omitted or the empty string if the application
+doesn't think the supplied contact information is valid enough.
+
+* data
++
+Encrypted account data as an armored ASCII blob. This is usually
+several KB of text data as a single string, with embedded newlines
+to break the lines at about 70-75 characters. Data can be decoded
+using GnuPG with the correct private key.
+
+Upon successful store, the contact store application should respond
+with HTTP status code `200` and a body consisting only of `OK`
+(or `OK\n`). Any other response code or body is considered to be
+a failure by Gerrit.
+
+Using `https://` for the store URL is *highly* encouraged, as it
+prevents man-in-the-middle attacks from reading the shared secret
+APPSEC token, or messing with the data field.
+
+
+
+[[upgrade_203]]
+Upgrading From Gerrit 2.0.3 (or Earlier)
+----------------------------------------
+
+To upgrade from schema 4 (Gerrit 2.0.3 and earlier) to schema 5
+(Gerrit 2.0.4 and later), run the following sequence.
+
+Replace `your-appid` with the Google App Engine application id of
+your contact store application, and `APPSEC` with the value compiled
+into the application when it was uploaded.
+
+The use of Google App Engine isn't required. It is relatively easy
+to create another web service to receive the application data,
+and point the `contact_store_url` at that server. See above for
+more details about the form data posted.
+
+====
+ git clone git://android.git.kernel.org/tools/gerrit-contactstore.git
+ cd gerrit-contactstore/google_appengine
+ make update APPID=your-appid
+
+ gpg --export --armor KEYEMAIL >$site_config/contact_information.pub
+
+ java -jar gerrit.war --cat sql/upgrade004_005_part1.sql | psql reviewdb
+ psql -c "UPDATE system_config SET contact_store_url = 'https://your-appid.appspot.com/store'" reviewdb
+ psql -c "UPDATE system_config SET contact_store_appsec = 'APPSEC'" reviewdb
+ java -DGerritServer=GerritServer.properties -cp bcprov-jdk15-141.jar:bcpg-jdk15-141.jar:gerrit.war ExecutableWarMain EncryptContactInfo
+====
+
+It may be a good idea to delay part2.sql until after the existing
+contact information has been archived and has been verified to be
+readable. The part2 script simply drops the old contact columns
+from the `accounts` table.
+
+====
+ java -jar gerrit.war --cat sql/upgrade004_005_part2.sql | psql reviewdb
+====
+
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index a17b53192a..ac3e9b4162 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -121,6 +121,16 @@ more agreements.
By default this is `N` (no agreements are used).
+contact_store_url, contact_store_appsec
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+URL and shared secret of the web based contact store Gerrit will
+send any offline contact information to when it collects the data
+from users as part of a contributor agreement.
+
+* link:config-contact.html[Contact Information]
+
+
sshd_port
~~~~~~~~~
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 101cdef536..b1b41a6445 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -28,6 +28,7 @@ Configuration
-------------
* link:config-gerrit.html[system_config Settings]
+* link:config-contact.html[User Contact Information]
* link:config-replication.html[Git Replication/Mirroring]
* link:config-gitweb.html[Gitweb Integration]
* link:config-headerfooter.html[Site Header/Footer]
diff --git a/pom.xml b/pom.xml
index cfae566f71..2b20c1654f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -475,6 +475,13 @@ limitations under the License.
</dependency>
<dependency>
+ <groupId>bouncycastle</groupId>
+ <artifactId>bcpg-jdk15</artifactId>
+ <version>140</version>
+ <scope>provided</scope>
+ </dependency>
+
+ <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.4.3</version>
diff --git a/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index 49a5318b85..3b8415449f 100644
--- a/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -64,6 +64,7 @@ public interface AccountConstants extends Constants {
String contactFieldFullName();
String contactFieldEmail();
+ String contactPrivacyDetailsHtml();
String contactFieldAddress();
String contactFieldCountry();
String contactFieldPhone();
diff --git a/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 663e326f31..a94ed6a4ae 100644
--- a/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -45,6 +45,14 @@ watchedProjectColumnAllComments = All Comments
contactFieldFullName = Full Name
contactFieldEmail = Preferred Email
+contactPrivacyDetailsHtml = \
+ <b>The following offline contact information is stored encrypted.</b><br />\
+ <br />\
+ Contact information will only be made available to administrators if it is \
+ necessary to reach you through non-email based communication. Received data \
+ is stored encrypted with a strong public/private key pair algorithm, and \
+ this site does not have the private key. Once saved, you will be unable to \
+ retrieve previously stored contact details.
contactFieldAddress = Mailing Address
contactFieldCountry = Country
contactFieldPhone = Phone Number
diff --git a/src/main/java/com/google/gerrit/client/account/AccountMessages.java b/src/main/java/com/google/gerrit/client/account/AccountMessages.java
index 6049a892e0..5ded58b604 100644
--- a/src/main/java/com/google/gerrit/client/account/AccountMessages.java
+++ b/src/main/java/com/google/gerrit/client/account/AccountMessages.java
@@ -16,7 +16,10 @@ package com.google.gerrit.client.account;
import com.google.gwt.i18n.client.Messages;
+import java.util.Date;
+
public interface AccountMessages extends Messages {
String lines(short cnt);
String enterIAGREE(String iagree);
+ String contactOnFile(Date lastDate);
}
diff --git a/src/main/java/com/google/gerrit/client/account/AccountMessages.properties b/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
index 57790568e3..4632cc840f 100644
--- a/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
+++ b/src/main/java/com/google/gerrit/client/account/AccountMessages.properties
@@ -1,3 +1,4 @@
lines = {0} lines
enterIAGREE = (enter {0} in the box to the left)
+contactOnFile = Contact information last updated on {0,date,medium} at {0,time,short}.
diff --git a/src/main/java/com/google/gerrit/client/account/AccountSecurity.java b/src/main/java/com/google/gerrit/client/account/AccountSecurity.java
index a853b05230..af6ab67aab 100644
--- a/src/main/java/com/google/gerrit/client/account/AccountSecurity.java
+++ b/src/main/java/com/google/gerrit/client/account/AccountSecurity.java
@@ -14,6 +14,7 @@
package com.google.gerrit.client.account;
+import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountExternalId;
import com.google.gerrit.client.reviewdb.AccountSshKey;
import com.google.gerrit.client.reviewdb.ContactInformation;
@@ -42,7 +43,7 @@ public interface AccountSecurity extends RemoteJsonService {
@SignInRequired
void updateContact(String fullName, String emailAddr,
- ContactInformation info, AsyncCallback<VoidResult> callback);
+ ContactInformation info, AsyncCallback<Account> callback);
@SignInRequired
void enterAgreement(ContributorAgreement.Id id,
diff --git a/src/main/java/com/google/gerrit/client/account/ContactPanel.java b/src/main/java/com/google/gerrit/client/account/ContactPanel.java
index 6ee2a44b55..e77b89ee47 100644
--- a/src/main/java/com/google/gerrit/client/account/ContactPanel.java
+++ b/src/main/java/com/google/gerrit/client/account/ContactPanel.java
@@ -18,6 +18,7 @@ import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.reviewdb.Account;
import com.google.gerrit.client.reviewdb.AccountExternalId;
import com.google.gerrit.client.reviewdb.ContactInformation;
+import com.google.gerrit.client.rpc.Common;
import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.ui.AutoCenterDialogBox;
import com.google.gerrit.client.ui.TextSaveButtonListener;
@@ -33,16 +34,18 @@ import com.google.gwt.user.client.ui.FormSubmitCompleteEvent;
import com.google.gwt.user.client.ui.FormSubmitEvent;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.ListBox;
import com.google.gwt.user.client.ui.TextArea;
import com.google.gwt.user.client.ui.TextBox;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwtjsonrpc.client.VoidResult;
+import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@@ -50,7 +53,6 @@ import java.util.Set;
class ContactPanel extends Composite {
private final AccountSettings parentScreen;
private int labelIdx, fieldIdx;
- private Grid info;
private String currentEmail;
private boolean haveAccount;
@@ -59,6 +61,7 @@ class ContactPanel extends Composite {
private TextBox nameTxt;
private ListBox emailPick;
private Button registerNewEmail;
+ private Label hasContact;
private TextArea addressTxt;
private TextBox countryTxt;
private TextBox phoneTxt;
@@ -102,10 +105,27 @@ class ContactPanel extends Composite {
faxTxt.setMaxLength(30);
final FlowPanel body = new FlowPanel();
- info = new Grid(6, 2);
- info.setStyleName("gerrit-InfoBlock");
- info.addStyleName("gerrit-AccountInfoBlock");
- body.add(info);
+ final Grid infoPlainText = new Grid(2, 2);
+ infoPlainText.setStyleName("gerrit-InfoBlock");
+ infoPlainText.addStyleName("gerrit-AccountInfoBlock");
+
+ final Grid infoSecure = new Grid(4, 2);
+ infoSecure.setStyleName("gerrit-InfoBlock");
+ infoSecure.addStyleName("gerrit-AccountInfoBlock");
+
+ final HTML privhtml = new HTML(Util.C.contactPrivacyDetailsHtml());
+ privhtml.setStyleName("gerrit-AccountContactPrivacyDetails");
+
+ hasContact = new Label();
+ hasContact.setStyleName("gerrit-AccountContactOnFile");
+ hasContact.setVisible(false);
+
+ body.add(infoPlainText);
+ if (Common.getGerritConfig().isUseContactInfo()) {
+ body.add(privhtml);
+ body.add(hasContact);
+ body.add(infoSecure);
+ }
registerNewEmail = new Button(Util.C.buttonOpenRegisterNewEmail());
registerNewEmail.setEnabled(false);
@@ -118,17 +138,21 @@ class ContactPanel extends Composite {
emailLine.add(emailPick);
emailLine.add(registerNewEmail);
- row(0, Util.C.contactFieldFullName(), nameTxt);
- row(1, Util.C.contactFieldEmail(), emailLine);
- row(2, Util.C.contactFieldAddress(), addressTxt);
- row(3, Util.C.contactFieldCountry(), countryTxt);
- row(4, Util.C.contactFieldPhone(), phoneTxt);
- row(5, Util.C.contactFieldFax(), faxTxt);
+ row(infoPlainText, 0, Util.C.contactFieldFullName(), nameTxt);
+ row(infoPlainText, 1, Util.C.contactFieldEmail(), emailLine);
+
+ row(infoSecure, 0, Util.C.contactFieldAddress(), addressTxt);
+ row(infoSecure, 1, Util.C.contactFieldCountry(), countryTxt);
+ row(infoSecure, 2, Util.C.contactFieldPhone(), phoneTxt);
+ row(infoSecure, 3, Util.C.contactFieldFax(), faxTxt);
- final CellFormatter fmt = info.getCellFormatter();
- fmt.addStyleName(0, 0, "topmost");
- fmt.addStyleName(0, 1, "topmost");
- fmt.addStyleName(5, 0, "bottomheader");
+ infoPlainText.getCellFormatter().addStyleName(0, 0, "topmost");
+ infoPlainText.getCellFormatter().addStyleName(0, 1, "topmost");
+ infoPlainText.getCellFormatter().addStyleName(1, 0, "bottomheader");
+
+ infoSecure.getCellFormatter().addStyleName(0, 0, "topmost");
+ infoSecure.getCellFormatter().addStyleName(0, 1, "topmost");
+ infoSecure.getCellFormatter().addStyleName(3, 0, "bottomheader");
save = new Button(Util.C.buttonSaveContact());
save.setEnabled(false);
@@ -244,27 +268,34 @@ class ContactPanel extends Composite {
}
}
- private void row(final int row, final String name, final Widget field) {
+ private void row(final Grid info, final int row, final String name,
+ final Widget field) {
info.setText(row, labelIdx, name);
info.setWidget(row, fieldIdx, field);
info.getCellFormatter().addStyleName(row, 0, "header");
}
private void display(final Account userAccount) {
- ContactInformation info = userAccount.getContactInformation();
- if (info == null) {
- info = new ContactInformation();
- }
-
currentEmail = userAccount.getPreferredEmail();
nameTxt.setText(userAccount.getFullName());
- addressTxt.setText(info.getAddress());
- countryTxt.setText(info.getCountry());
- phoneTxt.setText(info.getPhoneNumber());
- faxTxt.setText(info.getFaxNumber());
+ displayHasContact(userAccount);
+ addressTxt.setText("");
+ countryTxt.setText("");
+ phoneTxt.setText("");
+ faxTxt.setText("");
save.setEnabled(false);
}
+ private void displayHasContact(final Account userAccount) {
+ if (userAccount.isContactFiled()) {
+ final Timestamp dt = userAccount.getContactFiledOn();
+ hasContact.setText(Util.M.contactOnFile(new Date(dt.getTime())));
+ hasContact.setVisible(true);
+ } else {
+ hasContact.setVisible(false);
+ }
+ }
+
private void doRegisterNewEmail() {
final AutoCenterDialogBox box = new AutoCenterDialogBox(true, true);
final VerticalPanel body = new VerticalPanel();
@@ -332,22 +363,27 @@ class ContactPanel extends Composite {
newEmail = currentEmail;
}
- final ContactInformation info = new ContactInformation();
- info.setAddress(addressTxt.getText());
- info.setCountry(countryTxt.getText());
- info.setPhoneNumber(phoneTxt.getText());
- info.setFaxNumber(faxTxt.getText());
+ final ContactInformation info;
+ if (Common.getGerritConfig().isUseContactInfo()) {
+ info = new ContactInformation();
+ info.setAddress(addressTxt.getText());
+ info.setCountry(countryTxt.getText());
+ info.setPhoneNumber(phoneTxt.getText());
+ info.setFaxNumber(faxTxt.getText());
+ } else {
+ info = null;
+ }
save.setEnabled(false);
registerNewEmail.setEnabled(false);
Util.ACCOUNT_SEC.updateContact(newName, newEmail, info,
- new GerritCallback<VoidResult>() {
- public void onSuccess(final VoidResult result) {
+ new GerritCallback<Account>() {
+ public void onSuccess(final Account result) {
registerNewEmail.setEnabled(false);
final Account me = Gerrit.getUserAccount();
me.setFullName(newName);
me.setPreferredEmail(newEmail);
- me.setContactInformation(info);
+ displayHasContact(result);
Gerrit.refreshMenuBar();
if (parentScreen != null) {
parentScreen.display(me);
diff --git a/src/main/java/com/google/gerrit/client/data/GerritConfig.java b/src/main/java/com/google/gerrit/client/data/GerritConfig.java
index fb45a482eb..107c1009cd 100644
--- a/src/main/java/com/google/gerrit/client/data/GerritConfig.java
+++ b/src/main/java/com/google/gerrit/client/data/GerritConfig.java
@@ -29,6 +29,7 @@ public class GerritConfig {
protected List<ApprovalType> actionTypes;
protected int sshdPort;
protected boolean useContributorAgreements;
+ protected boolean useContactInfo;
protected SystemConfig.LoginType loginType;
protected boolean useRepoDownload;
protected String gitDaemonUrl;
@@ -109,6 +110,14 @@ public class GerritConfig {
useContributorAgreements = r;
}
+ public boolean isUseContactInfo() {
+ return useContactInfo;
+ }
+
+ public void setUseContactInfo(final boolean r) {
+ useContactInfo = r;
+ }
+
public ApprovalType getApprovalType(final ApprovalCategory.Id id) {
if (byCategoryId == null) {
byCategoryId = new HashMap<ApprovalCategory.Id, ApprovalType>();
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/Account.java b/src/main/java/com/google/gerrit/client/reviewdb/Account.java
index 7c4c89a140..2100ebba04 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/Account.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/Account.java
@@ -122,9 +122,9 @@ public final class Account {
@Column
protected boolean showSiteHeader;
- /** Non-Internet based contact details for the account's owner. */
+ /** When did the user last give us contact information? Null if never. */
@Column(notNull = false)
- protected ContactInformation contact;
+ protected Timestamp contactFiledOn;
protected Account() {
}
@@ -199,11 +199,15 @@ public final class Account {
showSiteHeader = b;
}
- public ContactInformation getContactInformation() {
- return contact;
+ public boolean isContactFiled() {
+ return contactFiledOn != null;
}
- public void setContactInformation(final ContactInformation i) {
- contact = i;
+ public Timestamp getContactFiledOn() {
+ return contactFiledOn;
+ }
+
+ public void setContactFiled() {
+ contactFiledOn = new Timestamp(System.currentTimeMillis());
}
}
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/ContactInformation.java b/src/main/java/com/google/gerrit/client/reviewdb/ContactInformation.java
index 1b44fe05c9..ac79edebf8 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/ContactInformation.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/ContactInformation.java
@@ -64,4 +64,22 @@ public final class ContactInformation {
public void setFaxNumber(final String f) {
faxNbr = f;
}
+
+ public static boolean hasData(final ContactInformation contactInformation) {
+ if (contactInformation == null) {
+ return false;
+ }
+ return hasData(contactInformation.address)
+ || hasData(contactInformation.country)
+ || hasData(contactInformation.phoneNbr)
+ || hasData(contactInformation.faxNbr);
+ }
+
+ public static boolean hasAddress(final ContactInformation contactInformation) {
+ return contactInformation != null && hasData(contactInformation.address);
+ }
+
+ private static boolean hasData(final String s) {
+ return s != null && s.trim().length() > 0;
+ }
}
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java b/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java
index 44e4e3efda..6e33eb3d97 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/ReviewDb.java
@@ -21,7 +21,7 @@ import com.google.gwtorm.client.Sequence;
/** The review service database schema. */
public interface ReviewDb extends Schema {
- public static final int VERSION = 4;
+ public static final int VERSION = 5;
@Relation
SchemaVersionAccess schemaVersion();
diff --git a/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java b/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java
index 49aeeb5ab8..087fa78d52 100644
--- a/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java
+++ b/src/main/java/com/google/gerrit/client/reviewdb/SystemConfig.java
@@ -160,6 +160,14 @@ public final class SystemConfig {
@Column
public AccountGroup.Id registeredGroupId;
+ /** Optional URL of the contact information store. */
+ @Column(notNull = false)
+ public transient String contactStoreURL;
+
+ /** APPSEC token to get into {@link #contactStoreURL}. */
+ @Column(notNull = false)
+ public transient String contactStoreAPPSEC;
+
public LoginType getLoginType() {
return loginType != null ? LoginType.valueOf(loginType) : null;
}
diff --git a/src/main/java/com/google/gerrit/client/rpc/ContactInformationStoreException.java b/src/main/java/com/google/gerrit/client/rpc/ContactInformationStoreException.java
new file mode 100644
index 0000000000..98e4582913
--- /dev/null
+++ b/src/main/java/com/google/gerrit/client/rpc/ContactInformationStoreException.java
@@ -0,0 +1,28 @@
+// Copyright 2009 Google Inc.
+//
+// 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.client.rpc;
+
+/** Error indicating the server cannot store contact information. */
+public class ContactInformationStoreException extends Exception {
+ public static final String MESSAGE = "Cannot store contact information";
+
+ public ContactInformationStoreException() {
+ super(MESSAGE);
+ }
+
+ public ContactInformationStoreException(final Throwable why) {
+ super(MESSAGE, why);
+ }
+}
diff --git a/src/main/java/com/google/gerrit/pgm/EncryptContactInfo.java b/src/main/java/com/google/gerrit/pgm/EncryptContactInfo.java
new file mode 100644
index 0000000000..ec1cb32fee
--- /dev/null
+++ b/src/main/java/com/google/gerrit/pgm/EncryptContactInfo.java
@@ -0,0 +1,98 @@
+// Copyright 2008 Google Inc.
+//
+// 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.pgm;
+
+import com.google.gerrit.client.reviewdb.Account;
+import com.google.gerrit.client.reviewdb.ContactInformation;
+import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gerrit.client.rpc.Common;
+import com.google.gerrit.client.rpc.ContactInformationStoreException;
+import com.google.gerrit.git.WorkQueue;
+import com.google.gerrit.server.EncryptedContactStore;
+import com.google.gerrit.server.GerritServer;
+import com.google.gwtjsonrpc.server.XsrfException;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.jdbc.JdbcSchema;
+
+import org.spearce.jgit.lib.ProgressMonitor;
+import org.spearce.jgit.lib.TextProgressMonitor;
+
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+
+/** Export old contact columns to the encrypted contact store. */
+public class EncryptContactInfo {
+ public static void main(final String[] argv) throws OrmException,
+ XsrfException, ContactInformationStoreException, SQLException {
+ try {
+ mainImpl(argv);
+ } finally {
+ WorkQueue.terminate();
+ }
+ }
+
+ private static void mainImpl(final String[] argv) throws OrmException,
+ XsrfException, ContactInformationStoreException, SQLException {
+ final ProgressMonitor pm = new TextProgressMonitor();
+ GerritServer.getInstance();
+ final ReviewDb db = Common.getSchemaFactory().open();
+ try {
+ pm.start(1);
+ pm.beginTask("Enumerate accounts", ProgressMonitor.UNKNOWN);
+ final Connection sql = ((JdbcSchema) db).getConnection();
+ final Statement stmt = sql.createStatement();
+ final ResultSet rs =
+ stmt.executeQuery("SELECT" + " account_id" + ",contact_address"
+ + ",contact_country" + ",contact_phone_nbr" + ",contact_fax_nbr"
+ + " FROM accounts WHERE contact_filed_on IS NOT NULL"
+ + " ORDER BY account_id");
+ final ArrayList<ToDo> todo = new ArrayList<ToDo>();
+ while (rs.next()) {
+ final ToDo d = new ToDo();
+ d.id = new Account.Id(rs.getInt(1));
+ d.info.setAddress(rs.getString(2));
+ d.info.setCountry(rs.getString(3));
+ d.info.setPhoneNumber(rs.getString(4));
+ d.info.setFaxNumber(rs.getString(5));
+ todo.add(d);
+ pm.update(1);
+ }
+ rs.close();
+ stmt.close();
+ pm.endTask();
+
+ pm.start(1);
+ pm.beginTask("Store contact", todo.size());
+ for (final ToDo d : todo) {
+ final Account them = db.accounts().get(d.id);
+ if (them.isContactFiled() && ContactInformation.hasData(d.info)) {
+ EncryptedContactStore.store(them, d.info);
+ }
+ pm.update(1);
+ }
+ pm.endTask();
+ } finally {
+ db.close();
+ }
+ }
+
+ static class ToDo {
+ Account.Id id;
+ final ContactInformation info = new ContactInformation();
+ }
+}
diff --git a/src/main/java/com/google/gerrit/public/Gerrit.css b/src/main/java/com/google/gerrit/public/Gerrit.css
index 30d05c5f74..f1bdfdf965 100644
--- a/src/main/java/com/google/gerrit/public/Gerrit.css
+++ b/src/main/java/com/google/gerrit/public/Gerrit.css
@@ -686,6 +686,18 @@
.gerrit-AccountInfoBlock {
margin-bottom: 10px;
}
+.gerrit-AccountContactPrivacyDetails {
+ margin-left: 10px;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ width: 40em;
+}
+.gerrit-AccountContactOnFile {
+ margin-left: 10px;
+ margin-top: 5px;
+ margin-bottom: 5px;
+ font-weight: bold;
+}
.gerrit-AddSshKeyPanel {
margin-top: 10px;
diff --git a/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java b/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java
index 912a7fad9a..0e0c08b689 100644
--- a/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java
+++ b/src/main/java/com/google/gerrit/server/AccountSecurityImpl.java
@@ -24,6 +24,7 @@ import com.google.gerrit.client.reviewdb.ContributorAgreement;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.rpc.BaseServiceImplementation;
import com.google.gerrit.client.rpc.Common;
+import com.google.gerrit.client.rpc.ContactInformationStoreException;
import com.google.gerrit.client.rpc.InvalidSshKeyException;
import com.google.gerrit.client.rpc.NoSuchEntityException;
import com.google.gerrit.server.ssh.SshUtil;
@@ -140,16 +141,28 @@ class AccountSecurityImpl extends BaseServiceImplementation implements
}
public void updateContact(final String fullName, final String emailAddr,
- final ContactInformation info, final AsyncCallback<VoidResult> callback) {
- run(callback, new Action<VoidResult>() {
- public VoidResult run(ReviewDb db) throws OrmException {
+ final ContactInformation info, final AsyncCallback<Account> callback) {
+ run(callback, new Action<Account>() {
+ public Account run(ReviewDb db) throws OrmException, Failure {
final Account me = db.accounts().get(Common.getAccountId());
me.setFullName(fullName);
me.setPreferredEmail(emailAddr);
- me.setContactInformation(info);
+ if (Common.getGerritConfig().isUseContactInfo()) {
+ if (ContactInformation.hasAddress(info)
+ || (me.isContactFiled() && ContactInformation.hasData(info))) {
+ me.setContactFiled();
+ }
+ if (ContactInformation.hasData(info)) {
+ try {
+ EncryptedContactStore.store(me, info);
+ } catch (ContactInformationStoreException e) {
+ throw new Failure(e);
+ }
+ }
+ }
db.accounts().update(Collections.singleton(me));
Common.getAccountCache().invalidate(me.getId());
- return VoidResult.INSTANCE;
+ return me;
}
});
}
diff --git a/src/main/java/com/google/gerrit/server/EncryptedContactStore.java b/src/main/java/com/google/gerrit/server/EncryptedContactStore.java
new file mode 100644
index 0000000000..fb1aac2dec
--- /dev/null
+++ b/src/main/java/com/google/gerrit/server/EncryptedContactStore.java
@@ -0,0 +1,323 @@
+// Copyright 2009 Google Inc.
+//
+// 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;
+
+import com.google.gerrit.client.reviewdb.Account;
+import com.google.gerrit.client.reviewdb.AccountExternalId;
+import com.google.gerrit.client.reviewdb.ContactInformation;
+import com.google.gerrit.client.reviewdb.ReviewDb;
+import com.google.gerrit.client.rpc.Common;
+import com.google.gerrit.client.rpc.ContactInformationStoreException;
+import com.google.gwtjsonrpc.server.XsrfException;
+import com.google.gwtorm.client.OrmException;
+
+import org.apache.sshd.common.util.SecurityUtils;
+import org.bouncycastle.bcpg.ArmoredOutputStream;
+import org.bouncycastle.openpgp.PGPCompressedData;
+import org.bouncycastle.openpgp.PGPCompressedDataGenerator;
+import org.bouncycastle.openpgp.PGPEncryptedData;
+import org.bouncycastle.openpgp.PGPEncryptedDataGenerator;
+import org.bouncycastle.openpgp.PGPException;
+import org.bouncycastle.openpgp.PGPLiteralData;
+import org.bouncycastle.openpgp.PGPLiteralDataGenerator;
+import org.bouncycastle.openpgp.PGPPublicKey;
+import org.bouncycastle.openpgp.PGPPublicKeyRing;
+import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
+import org.bouncycastle.openpgp.PGPUtil;
+import org.mortbay.util.UrlEncoded;
+import org.spearce.jgit.util.NB;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SecureRandom;
+import java.sql.Timestamp;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Iterator;
+import java.util.TimeZone;
+
+/** Encrypts {@link ContactInformation} instances and saves them. */
+public class EncryptedContactStore {
+ private static final TimeZone UTC = TimeZone.getTimeZone("UTC");
+ private static boolean inited;
+ private static EncryptedContactStore self;
+
+ private static synchronized EncryptedContactStore getInstance()
+ throws ContactInformationStoreException {
+ if (!inited) {
+ inited = true;
+ self = new EncryptedContactStore();
+ }
+ if (self == null) {
+ throw new ContactInformationStoreException();
+ }
+ return self;
+ }
+
+ public static void store(final Account account, final ContactInformation info)
+ throws ContactInformationStoreException {
+ getInstance().storeImpl(account, info);
+ }
+
+ private PGPPublicKey dest;
+ private SecureRandom prng;
+ private URL storeUrl;
+ private String storeAPPSEC;
+
+ private EncryptedContactStore() throws ContactInformationStoreException {
+ final GerritServer gs;
+ try {
+ gs = GerritServer.getInstance();
+ } catch (OrmException e) {
+ throw new ContactInformationStoreException(e);
+ } catch (XsrfException e) {
+ throw new ContactInformationStoreException(e);
+ }
+
+ if (gs.getContactStoreURL() == null) {
+ throw new ContactInformationStoreException(new IllegalStateException(
+ "No contactStoreURL configured"));
+ }
+ try {
+ storeUrl = new URL(gs.getContactStoreURL());
+ } catch (MalformedURLException e) {
+ throw new ContactInformationStoreException(e);
+ }
+ storeAPPSEC = gs.getContactStoreAPPSEC();
+
+ if (!SecurityUtils.isBouncyCastleRegistered()) {
+ throw new ContactInformationStoreException(new NoSuchProviderException(
+ "BC (aka BouncyCastle)"));
+ }
+
+ try {
+ prng = SecureRandom.getInstance("SHA1PRNG");
+ } catch (NoSuchAlgorithmException e) {
+ throw new ContactInformationStoreException(e);
+ }
+
+ dest = selectKey(readPubRing(gs));
+ }
+
+ private PGPPublicKeyRingCollection readPubRing(final GerritServer gs)
+ throws ContactInformationStoreException {
+ final File pub = new File(gs.getSitePath(), "contact_information.pub");
+ try {
+ InputStream in = new FileInputStream(pub);
+ try {
+ in = PGPUtil.getDecoderStream(in);
+ return new PGPPublicKeyRingCollection(in);
+ } finally {
+ in.close();
+ }
+ } catch (IOException e) {
+ throw new ContactInformationStoreException(e);
+ } catch (PGPException e) {
+ throw new ContactInformationStoreException(e);
+ }
+ }
+
+ private PGPPublicKey selectKey(final PGPPublicKeyRingCollection rings) {
+ for (final Iterator<?> ri = rings.getKeyRings(); ri.hasNext();) {
+ final PGPPublicKeyRing currRing = (PGPPublicKeyRing) ri.next();
+ for (final Iterator<?> ki = currRing.getPublicKeys(); ki.hasNext();) {
+ final PGPPublicKey k = (PGPPublicKey) ki.next();
+ if (k.isEncryptionKey()) {
+ return k;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void storeImpl(final Account account, final ContactInformation info)
+ throws ContactInformationStoreException {
+ try {
+ final byte[] plainText = format(account, info).getBytes("UTF-8");
+ final byte[] encText = encrypt(dest, compress(account, plainText));
+ final String encStr = new String(encText, "UTF-8");
+
+ final Timestamp filedOn = account.getContactFiledOn();
+ final UrlEncoded u = new UrlEncoded();
+ if (storeAPPSEC != null) {
+ u.add("APPSEC", storeAPPSEC);
+ }
+ if (account.getPreferredEmail() != null) {
+ u.add("email", account.getPreferredEmail());
+ }
+ if (filedOn != null) {
+ u.add("filed", String.valueOf(filedOn.getTime() / 1000L));
+ }
+ u.add("account_id", String.valueOf(account.getId().get()));
+ u.add("data", encStr);
+ final byte[] body = u.encode().getBytes("UTF-8");
+
+ final HttpURLConnection c = (HttpURLConnection) storeUrl.openConnection();
+ c.setRequestMethod("POST");
+ c.setRequestProperty("Content-Type",
+ "application/x-www-form-urlencoded; charset=UTF-8");
+ c.setDoOutput(true);
+ c.setFixedLengthStreamingMode(body.length);
+ final OutputStream out = c.getOutputStream();
+ out.write(body);
+ out.close();
+
+ if (c.getResponseCode() == 200) {
+ final byte[] dst = new byte[2];
+ final InputStream in = c.getInputStream();
+ try {
+ NB.readFully(in, dst, 0, 2);
+ } finally {
+ in.close();
+ }
+ if (dst[0] != 'O' || dst[1] != 'K') {
+ throw new IOException("Store failed: " + c.getResponseCode());
+ }
+ } else {
+ throw new IOException("Store failed: " + c.getResponseCode());
+ }
+
+ } catch (IOException e) {
+ throw new ContactInformationStoreException(e);
+ } catch (PGPException e) {
+ throw new ContactInformationStoreException(e);
+ } catch (NoSuchProviderException e) {
+ throw new ContactInformationStoreException(e);
+ }
+ }
+
+ private byte[] encrypt(final PGPPublicKey dst, final byte[] zText)
+ throws NoSuchProviderException, PGPException, IOException {
+ final PGPEncryptedDataGenerator cpk =
+ new PGPEncryptedDataGenerator(PGPEncryptedData.CAST5, true, prng, "BC");
+ cpk.addMethod(dst);
+
+ final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ final ArmoredOutputStream aout = new ArmoredOutputStream(buf);
+ final OutputStream cout = cpk.open(aout, zText.length);
+ cout.write(zText);
+ cout.close();
+ aout.close();
+
+ return buf.toByteArray();
+ }
+
+ private static byte[] compress(final Account account, final byte[] plainText)
+ throws IOException {
+ final ByteArrayOutputStream buf = new ByteArrayOutputStream();
+ final PGPCompressedDataGenerator comdg;
+ final String name = "account-" + account.getId();
+ final int len = plainText.length;
+ Date date = account.getContactFiledOn();
+ if (date == null) {
+ date = PGPLiteralData.NOW;
+ }
+
+ comdg = new PGPCompressedDataGenerator(PGPCompressedData.ZIP);
+ final OutputStream out =
+ new PGPLiteralDataGenerator().open(comdg.open(buf),
+ PGPLiteralData.BINARY, name, len, date);
+ out.write(plainText);
+ out.close();
+ comdg.close();
+ return buf.toByteArray();
+ }
+
+ private static String format(final Account account,
+ final ContactInformation info) throws ContactInformationStoreException {
+ Timestamp on = account.getContactFiledOn();
+ if (on == null) {
+ on = new Timestamp(System.currentTimeMillis());
+ }
+
+ final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
+ df.setTimeZone(UTC);
+
+ final StringBuilder b = new StringBuilder();
+ field(b, "Account-Id", account.getId().toString());
+ field(b, "Date", df.format(on) + " " + UTC.getID());
+ field(b, "Full-Name", account.getFullName());
+ field(b, "Preferred-Email", account.getPreferredEmail());
+
+ try {
+ final ReviewDb db = Common.getSchemaFactory().open();
+ try {
+ for (final AccountExternalId e : db.accountExternalIds().byAccount(
+ account.getId())) {
+ final StringBuilder oistr = new StringBuilder();
+ if (e.getEmailAddress() != null && e.getEmailAddress().length() > 0) {
+ if (oistr.length() > 0) {
+ oistr.append(' ');
+ }
+ oistr.append(e.getEmailAddress());
+ }
+ if (e.getExternalId() != null && e.getExternalId().length() > 0
+ && !e.getExternalId().startsWith("mailto:")) {
+ if (oistr.length() > 0) {
+ oistr.append(' ');
+ }
+ oistr.append('<');
+ oistr.append(e.getExternalId());
+ oistr.append('>');
+ }
+ field(b, "Identity", oistr.toString());
+ }
+ } finally {
+ db.close();
+ }
+ } catch (OrmException e) {
+ throw new ContactInformationStoreException(e);
+ }
+
+ field(b, "Address", info.getAddress());
+ field(b, "Country", info.getCountry());
+ field(b, "Phone-Number", info.getPhoneNumber());
+ field(b, "Fax-Number", info.getFaxNumber());
+ return b.toString();
+ }
+
+ private static void field(final StringBuilder b, final String name,
+ String value) {
+ if (value == null) {
+ return;
+ }
+ value = value.trim();
+ if (value.length() == 0) {
+ return;
+ }
+
+ b.append(name);
+ b.append(':');
+ if (value.indexOf('\n') == -1) {
+ b.append(' ');
+ b.append(value);
+ } else {
+ value = value.replaceAll("\r\n", "\n");
+ value = value.replaceAll("\n", "\n\t");
+ b.append("\n\t");
+ b.append(value);
+ }
+ b.append('\n');
+ }
+}
diff --git a/src/main/java/com/google/gerrit/server/GerritServer.java b/src/main/java/com/google/gerrit/server/GerritServer.java
index 320da2dc76..bbbe78978f 100644
--- a/src/main/java/com/google/gerrit/server/GerritServer.java
+++ b/src/main/java/com/google/gerrit/server/GerritServer.java
@@ -453,6 +453,7 @@ public class GerritServer {
r.setUseContributorAgreements(sConfig.useContributorAgreements);
r.setGitDaemonUrl(sConfig.gitDaemonUrl);
r.setUseRepoDownload(sConfig.useRepoDownload);
+ r.setUseContactInfo(sConfig.contactStoreURL != null);
r.setLoginType(sConfig.getLoginType());
if (sConfig.gitwebUrl != null) {
r.setGitwebLink(new GitwebLink(sConfig.gitwebUrl));
@@ -527,6 +528,14 @@ public class GerritServer {
return sConfig.emailFormat;
}
+ public String getContactStoreURL() {
+ return sConfig.contactStoreURL;
+ }
+
+ public String getContactStoreAPPSEC() {
+ return sConfig.contactStoreAPPSEC;
+ }
+
/** A binary string key to encrypt cookies related to account data. */
public String getAccountCookieKey() {
byte[] r = new byte[sConfig.accountPrivateKey.length()];
diff --git a/src/main/java/com/google/gerrit/server/ssh/Receive.java b/src/main/java/com/google/gerrit/server/ssh/Receive.java
index b8f1bcec73..91f9107a4c 100644
--- a/src/main/java/com/google/gerrit/server/ssh/Receive.java
+++ b/src/main/java/com/google/gerrit/server/ssh/Receive.java
@@ -30,7 +30,6 @@ import com.google.gerrit.client.reviewdb.Branch;
import com.google.gerrit.client.reviewdb.Change;
import com.google.gerrit.client.reviewdb.ChangeApproval;
import com.google.gerrit.client.reviewdb.ChangeMessage;
-import com.google.gerrit.client.reviewdb.ContactInformation;
import com.google.gerrit.client.reviewdb.ContributorAgreement;
import com.google.gerrit.client.reviewdb.PatchSet;
import com.google.gerrit.client.reviewdb.PatchSetInfo;
@@ -223,11 +222,10 @@ class Receive extends AbstractGitCommand {
}
if (bestCla != null && bestCla.isRequireContactInformation()) {
- final ContactInformation info = userAccount.getContactInformation();
boolean fail = false;
fail |= missing(userAccount.getFullName());
fail |= missing(userAccount.getPreferredEmail());
- fail |= info == null || missing(info.getAddress());
+ fail |= !userAccount.isContactFiled();
if (fail) {
final StringBuilder msg = new StringBuilder();
diff --git a/src/main/webapp/WEB-INF/sql/upgrade004_005_part1.sql b/src/main/webapp/WEB-INF/sql/upgrade004_005_part1.sql
new file mode 100644
index 0000000000..f3b2ad4ddd
--- /dev/null
+++ b/src/main/webapp/WEB-INF/sql/upgrade004_005_part1.sql
@@ -0,0 +1,31 @@
+-- Upgrade: schema_version 4 to 5
+--
+
+ALTER TABLE accounts ADD contact_filed_on TIMESTAMP WITH TIME ZONE;
+UPDATE accounts
+ SET contact_filed_on = (SELECT MAX(accepted_on)
+ FROM account_agreements g
+ WHERE g.account_id = accounts.account_id
+ AND g.status <> 'R')
+ WHERE full_name IS NOT NULL
+ AND full_name <> ''
+ AND preferred_email IS NOT null
+ AND preferred_email <> ''
+ AND contact_address IS NOT NULL
+ AND contact_address <> ''
+ AND EXISTS (SELECT 1 FROM account_agreements g
+ WHERE g.account_id = accounts.account_id
+ AND g.status <> 'R');
+UPDATE accounts SET contact_filed_on = registered_on
+ WHERE full_name IS NOT NULL
+ AND full_name <> ''
+ AND preferred_email IS NOT null
+ AND preferred_email <> ''
+ AND contact_address IS NOT NULL
+ AND contact_address <> ''
+ AND contact_filed_on IS NULL;
+
+ALTER TABLE system_config ADD contact_store_url VARCHAR(255);
+ALTER TABLE system_config ADD contact_store_appsec VARCHAR(255);
+
+UPDATE schema_version SET version_nbr = 5;
diff --git a/src/main/webapp/WEB-INF/sql/upgrade004_005_part2.sql b/src/main/webapp/WEB-INF/sql/upgrade004_005_part2.sql
new file mode 100644
index 0000000000..868e535e11
--- /dev/null
+++ b/src/main/webapp/WEB-INF/sql/upgrade004_005_part2.sql
@@ -0,0 +1,7 @@
+-- Upgrade: schema_version 4 to 5 (part 2)
+--
+
+ALTER TABLE accounts DROP COLUMN contact_address;
+ALTER TABLE accounts DROP COLUMN contact_country;
+ALTER TABLE accounts DROP COLUMN contact_phone_nbr;
+ALTER TABLE accounts DROP COLUMN contact_fax_nbr;