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>
This commit is contained in:
@@ -64,6 +64,7 @@ public interface AccountConstants extends Constants {
|
||||
|
||||
String contactFieldFullName();
|
||||
String contactFieldEmail();
|
||||
String contactPrivacyDetailsHtml();
|
||||
String contactFieldAddress();
|
||||
String contactFieldCountry();
|
||||
String contactFieldPhone();
|
||||
|
@@ -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
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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}.
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
|
||||
final CellFormatter fmt = info.getCellFormatter();
|
||||
fmt.addStyleName(0, 0, "topmost");
|
||||
fmt.addStyleName(0, 1, "topmost");
|
||||
fmt.addStyleName(5, 0, "bottomheader");
|
||||
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);
|
||||
|
||||
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);
|
||||
|
@@ -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>();
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
98
src/main/java/com/google/gerrit/pgm/EncryptContactInfo.java
Normal file
98
src/main/java/com/google/gerrit/pgm/EncryptContactInfo.java
Normal file
@@ -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();
|
||||
}
|
||||
}
|
@@ -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;
|
||||
|
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@@ -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');
|
||||
}
|
||||
}
|
@@ -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()];
|
||||
|
@@ -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();
|
||||
|
Reference in New Issue
Block a user