blob: 0542cd23c50e4d8500cc328c255c6d735e378a31 [file] [log] [blame]
// Copyright (C) 2008 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.gerrit.server;
import com.google.gerrit.client.data.AccountCache;
import com.google.gerrit.client.data.ApprovalType;
import com.google.gerrit.client.data.GerritConfig;
import com.google.gerrit.client.data.GitwebLink;
import com.google.gerrit.client.data.GroupCache;
import com.google.gerrit.client.data.ProjectCache;
import com.google.gerrit.client.reviewdb.AccountGroup;
import com.google.gerrit.client.reviewdb.ApprovalCategory;
import com.google.gerrit.client.reviewdb.ApprovalCategoryValue;
import com.google.gerrit.client.reviewdb.Branch;
import com.google.gerrit.client.reviewdb.Change;
import com.google.gerrit.client.reviewdb.Project;
import com.google.gerrit.client.reviewdb.ProjectRight;
import com.google.gerrit.client.reviewdb.ReviewDb;
import com.google.gerrit.client.reviewdb.SchemaVersion;
import com.google.gerrit.client.reviewdb.SystemConfig;
import com.google.gerrit.client.reviewdb.TrustedExternalId;
import com.google.gerrit.client.rpc.Common;
import com.google.gerrit.client.workflow.NoOpFunction;
import com.google.gerrit.client.workflow.SubmitFunction;
import com.google.gerrit.git.MergeQueue;
import com.google.gerrit.git.RepositoryCache;
import com.google.gerrit.git.WorkQueue;
import com.google.gwtjsonrpc.server.SignedToken;
import com.google.gwtjsonrpc.server.XsrfException;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.client.Transaction;
import com.google.gwtorm.jdbc.Database;
import com.google.gwtorm.jdbc.SimpleDataSource;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spearce.jgit.lib.PersonIdent;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.TimeUnit;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.sql.DataSource;
/** Global server-side state for Gerrit. */
public class GerritServer {
private static final Logger log = LoggerFactory.getLogger(GerritServer.class);
private static DataSource datasource;
private static GerritServer impl;
static void closeDataSource() {
if (datasource != null) {
try {
try {
Class.forName("com.mchange.v2.c3p0.DataSources").getMethod("destroy",
DataSource.class).invoke(null, datasource);
} catch (Throwable bad) {
// Oh well, its not a c3p0 pooled connection. Too bad its
// not standardized how "good applications cleanup".
}
} finally {
datasource = null;
}
}
}
/**
* Obtain the singleton server instance for this web application.
*
* @return the server instance. Never null.
* @throws OrmException the database could not be configured. There is
* something wrong with the schema configuration in {@link ReviewDb}
* that must be addressed by a developer.
* @throws XsrfException the XSRF support could not be correctly configured to
* protect the application against cross-site request forgery. The JVM
* is most likely lacking critical security algorithms.
*/
public static synchronized GerritServer getInstance() throws OrmException,
XsrfException {
if (impl == null) {
try {
impl = new GerritServer();
impl.reloadMergeQueue();
} catch (OrmException e) {
closeDataSource();
log.error("GerritServer ORM is unavailable", e);
throw e;
} catch (XsrfException e) {
closeDataSource();
log.error("GerritServer XSRF support failed to initailize", e);
throw e;
}
}
return impl;
}
private final Database<ReviewDb> db;
private SystemConfig sConfig;
private final PersonIdent gerritPersonIdentTemplate;
private final SignedToken xsrf;
private final SignedToken account;
private final SignedToken emailReg;
private final RepositoryCache repositories;
private final javax.mail.Session outgoingMail;
private GerritServer() throws OrmException, XsrfException {
db = createDatabase();
loadSystemConfig();
if (sConfig == null) {
throw new OrmException("No " + SystemConfig.class.getName() + " found");
}
xsrf = new SignedToken(sConfig.maxSessionAge, sConfig.xsrfPrivateKey);
final int accountCookieAge;
switch (sConfig.getLoginType()) {
case HTTP:
accountCookieAge = -1; // expire when the browser closes
break;
case OPENID:
default:
accountCookieAge = sConfig.maxSessionAge;
break;
}
account = new SignedToken(accountCookieAge, sConfig.accountPrivateKey);
emailReg = new SignedToken(5 * 24 * 60 * 60, sConfig.accountPrivateKey);
if (sConfig.gitBasePath != null) {
repositories = new RepositoryCache(new File(sConfig.gitBasePath));
} else {
repositories = null;
}
String email = sConfig.gerritGitEmail;
if (email == null || email.length() == 0) {
try {
email = "gerrit@" + InetAddress.getLocalHost().getCanonicalHostName();
} catch (UnknownHostException e) {
email = "gerrit@localhost";
}
}
gerritPersonIdentTemplate = new PersonIdent(sConfig.gerritGitName, email);
outgoingMail = createOutgoingMail();
Common.setSchemaFactory(db);
Common.setProjectCache(new ProjectCache());
Common.setAccountCache(new AccountCache());
Common.setGroupCache(new GroupCache(sConfig));
}
private Database<ReviewDb> createDatabase() throws OrmException {
final String dsName = "java:comp/env/jdbc/ReviewDb";
try {
datasource = (DataSource) new InitialContext().lookup(dsName);
} catch (NamingException namingErr) {
final Properties p = readGerritDataSource();
if (p == null) {
throw new OrmException("Initialization error:\n" + " * No DataSource "
+ dsName + "\n" + " * No -DGerritServer=GerritServer.properties"
+ " on Java command line", namingErr);
}
try {
datasource = new SimpleDataSource(p);
} catch (SQLException se) {
throw new OrmException("Database unavailable", se);
}
}
return new Database<ReviewDb>(datasource, ReviewDb.class);
}
private Properties readGerritDataSource() throws OrmException {
final Properties srvprop = new Properties();
String name = System.getProperty("GerritServer");
if (name == null) {
name = "GerritServer.properties";
}
try {
final InputStream in = new FileInputStream(name);
try {
srvprop.load(in);
} finally {
in.close();
}
} catch (IOException e) {
throw new OrmException("Cannot read " + name, e);
}
final Properties dbprop = new Properties();
for (final Map.Entry<Object, Object> e : srvprop.entrySet()) {
final String key = (String) e.getKey();
if (key.startsWith("database.")) {
dbprop.put(key.substring("database.".length()), e.getValue());
}
}
return dbprop;
}
private void initSystemConfig(final ReviewDb c) throws OrmException {
final AccountGroup admin =
new AccountGroup(new AccountGroup.NameKey("Administrators"),
new AccountGroup.Id(c.nextAccountGroupId()));
admin.setDescription("Gerrit Site Administrators");
c.accountGroups().insert(Collections.singleton(admin));
final AccountGroup anonymous =
new AccountGroup(new AccountGroup.NameKey("Anonymous Users"),
new AccountGroup.Id(c.nextAccountGroupId()));
anonymous.setDescription("Any user, signed-in or not");
anonymous.setOwnerGroupId(admin.getId());
c.accountGroups().insert(Collections.singleton(anonymous));
final AccountGroup registered =
new AccountGroup(new AccountGroup.NameKey("Registered Users"),
new AccountGroup.Id(c.nextAccountGroupId()));
registered.setDescription("Any signed-in user");
registered.setOwnerGroupId(admin.getId());
c.accountGroups().insert(Collections.singleton(registered));
final SystemConfig s = SystemConfig.create();
s.maxSessionAge = 12 * 60 * 60 /* seconds */;
s.xsrfPrivateKey = SignedToken.generateRandomKey();
s.accountPrivateKey = SignedToken.generateRandomKey();
s.sshdPort = 29418;
s.adminGroupId = admin.getId();
s.anonymousGroupId = anonymous.getId();
s.registeredGroupId = registered.getId();
s.gerritGitName = "Gerrit Code Review";
s.setLoginType(SystemConfig.LoginType.OPENID);
c.systemConfig().insert(Collections.singleton(s));
// By default with OpenID trust any http:// or https:// provider
//
initTrustedExternalId(c, "http://");
initTrustedExternalId(c, "https://");
initTrustedExternalId(c, "https://www.google.com/accounts/o8/id?id=");
}
private void initTrustedExternalId(final ReviewDb c, final String re)
throws OrmException {
c.trustedExternalIds().insert(
Collections.singleton(new TrustedExternalId(new TrustedExternalId.Key(
re))));
}
private void initWildCardProject(final ReviewDb c) throws OrmException {
final Project proj;
proj =
new Project(new Project.NameKey("-- All Projects --"),
ProjectRight.WILD_PROJECT);
proj.setDescription("Rights inherited by all other projects");
proj.setOwnerGroupId(sConfig.adminGroupId);
proj.setUseContributorAgreements(false);
c.projects().insert(Collections.singleton(proj));
}
private void initVerifiedCategory(final ReviewDb c) throws OrmException {
final Transaction txn = c.beginTransaction();
final ApprovalCategory cat;
final ArrayList<ApprovalCategoryValue> vals;
cat = new ApprovalCategory(new ApprovalCategory.Id("VRIF"), "Verified");
cat.setPosition((short) 0);
vals = new ArrayList<ApprovalCategoryValue>();
vals.add(value(cat, 1, "Verified"));
vals.add(value(cat, 0, "No score"));
vals.add(value(cat, -1, "Fails"));
c.approvalCategories().insert(Collections.singleton(cat), txn);
c.approvalCategoryValues().insert(vals, txn);
txn.commit();
}
private void initCodeReviewCategory(final ReviewDb c) throws OrmException {
final Transaction txn = c.beginTransaction();
final ApprovalCategory cat;
final ArrayList<ApprovalCategoryValue> vals;
cat = new ApprovalCategory(new ApprovalCategory.Id("CRVW"), "Code Review");
cat.setPosition((short) 1);
vals = new ArrayList<ApprovalCategoryValue>();
vals.add(value(cat, 2, "Looks good to me, approved"));
vals.add(value(cat, 1, "Looks good to me, but someone else must approve"));
vals.add(value(cat, 0, "No score"));
vals.add(value(cat, -1, "I would prefer that you didn't submit this"));
vals.add(value(cat, -2, "Do not submit"));
c.approvalCategories().insert(Collections.singleton(cat), txn);
c.approvalCategoryValues().insert(vals, txn);
txn.commit();
final ProjectRight approve =
new ProjectRight(new ProjectRight.Key(ProjectRight.WILD_PROJECT, cat
.getId(), sConfig.registeredGroupId));
approve.setMaxValue((short) 1);
approve.setMinValue((short) -1);
c.projectRights().insert(Collections.singleton(approve));
}
private void initReadCategory(final ReviewDb c) throws OrmException {
final Transaction txn = c.beginTransaction();
final ApprovalCategory cat;
final ArrayList<ApprovalCategoryValue> vals;
cat = new ApprovalCategory(ApprovalCategory.READ, "Read Access");
cat.setPosition((short) -1);
cat.setFunctionName(NoOpFunction.NAME);
vals = new ArrayList<ApprovalCategoryValue>();
vals.add(value(cat, 1, "Read access"));
vals.add(value(cat, -1, "No access"));
c.approvalCategories().insert(Collections.singleton(cat), txn);
c.approvalCategoryValues().insert(vals, txn);
txn.commit();
{
final ProjectRight read =
new ProjectRight(new ProjectRight.Key(ProjectRight.WILD_PROJECT, cat
.getId(), sConfig.anonymousGroupId));
read.setMaxValue((short) 1);
read.setMinValue((short) 0);
c.projectRights().insert(Collections.singleton(read));
}
{
final ProjectRight read =
new ProjectRight(new ProjectRight.Key(ProjectRight.WILD_PROJECT, cat
.getId(), sConfig.adminGroupId));
read.setMaxValue((short) 1);
read.setMinValue((short) 0);
c.projectRights().insert(Collections.singleton(read));
}
}
private void initSubmitCategory(final ReviewDb c) throws OrmException {
final Transaction txn = c.beginTransaction();
final ApprovalCategory cat;
final ArrayList<ApprovalCategoryValue> vals;
cat = new ApprovalCategory(ApprovalCategory.SUBMIT, "Submit");
cat.setPosition((short) -1);
cat.setFunctionName(SubmitFunction.NAME);
vals = new ArrayList<ApprovalCategoryValue>();
vals.add(value(cat, 1, "Submit"));
c.approvalCategories().insert(Collections.singleton(cat), txn);
c.approvalCategoryValues().insert(vals, txn);
txn.commit();
}
private void initPushTagCategory(final ReviewDb c) throws OrmException {
final Transaction txn = c.beginTransaction();
final ApprovalCategory cat;
final ArrayList<ApprovalCategoryValue> vals;
cat = new ApprovalCategory(ApprovalCategory.PUSH_TAG, "Push Annotated Tag");
cat.setPosition((short) -1);
cat.setFunctionName(NoOpFunction.NAME);
vals = new ArrayList<ApprovalCategoryValue>();
vals.add(value(cat, 1, "Create Tag"));
c.approvalCategories().insert(Collections.singleton(cat), txn);
c.approvalCategoryValues().insert(vals, txn);
txn.commit();
}
private void initPushUpdateBranchCategory(final ReviewDb c)
throws OrmException {
final Transaction txn = c.beginTransaction();
final ApprovalCategory cat;
final ArrayList<ApprovalCategoryValue> vals;
cat = new ApprovalCategory(ApprovalCategory.PUSH_HEAD, "Push Branch");
cat.setPosition((short) -1);
cat.setFunctionName(NoOpFunction.NAME);
vals = new ArrayList<ApprovalCategoryValue>();
vals.add(value(cat, ApprovalCategory.PUSH_HEAD_UPDATE, "Update Branch"));
vals.add(value(cat, ApprovalCategory.PUSH_HEAD_CREATE, "Create Branch"));
vals.add(value(cat, ApprovalCategory.PUSH_HEAD_REPLACE,
"Force Push Branch; Delete Branch"));
c.approvalCategories().insert(Collections.singleton(cat), txn);
c.approvalCategoryValues().insert(vals, txn);
txn.commit();
}
private static ApprovalCategoryValue value(final ApprovalCategory cat,
final int value, final String name) {
return new ApprovalCategoryValue(new ApprovalCategoryValue.Id(cat.getId(),
(short) value), name);
}
private void loadSystemConfig() throws OrmException {
final ReviewDb c = db.open();
try {
SchemaVersion sVer;
try {
sVer = c.schemaVersion().get(new SchemaVersion.Key());
} catch (OrmException e) {
// Assume the schema doesn't exist.
//
sVer = null;
}
if (sVer == null) {
// Assume the schema is empty and populate it.
//
c.createSchema();
sVer = SchemaVersion.create();
sVer.versionNbr = ReviewDb.VERSION;
c.schemaVersion().insert(Collections.singleton(sVer));
initSystemConfig(c);
sConfig = c.systemConfig().get(new SystemConfig.Key());
initWildCardProject(c);
initReadCategory(c);
initVerifiedCategory(c);
initCodeReviewCategory(c);
initSubmitCategory(c);
initPushTagCategory(c);
initPushUpdateBranchCategory(c);
}
if (sVer.versionNbr == 2) {
initPushTagCategory(c);
initPushUpdateBranchCategory(c);
sVer.versionNbr = 3;
c.schemaVersion().update(Collections.singleton(sVer));
}
if (sVer.versionNbr == ReviewDb.VERSION) {
sConfig = c.systemConfig().get(new SystemConfig.Key());
} else {
throw new OrmException("Unsupported schema version " + sVer.versionNbr);
}
loadGerritConfig(c);
} finally {
c.close();
}
}
private void loadGerritConfig(final ReviewDb db) throws OrmException {
final GerritConfig r = new GerritConfig();
r.setCanonicalUrl(getCanonicalURL());
r.setSshdPort(sConfig.sshdPort);
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));
}
for (final ApprovalCategory c : db.approvalCategories().all()) {
r.add(new ApprovalType(c, db.approvalCategoryValues().byCategory(
c.getId()).toList()));
}
Common.setGerritConfig(r);
}
private javax.mail.Session createOutgoingMail() {
final String dsName = "java:comp/env/mail/Outgoing";
try {
return (javax.mail.Session) new InitialContext().lookup(dsName);
} catch (NamingException namingErr) {
return null;
}
}
private void reloadMergeQueue() {
WorkQueue.schedule(new Runnable() {
public void run() {
final HashSet<Branch.NameKey> pending = new HashSet<Branch.NameKey>();
try {
final ReviewDb c = db.open();
try {
for (final Change change : c.changes().allSubmitted()) {
pending.add(change.getDest());
}
} finally {
c.close();
}
} catch (OrmException e) {
log.error("Cannot reload MergeQueue", e);
}
for (final Branch.NameKey branch : pending) {
MergeQueue.schedule(branch);
}
}
}, 0, TimeUnit.SECONDS);
}
/** Time (in seconds) that user sessions stay "signed in". */
public int getSessionAge() {
return sConfig.maxSessionAge;
}
/** Get the signature support used to protect against XSRF attacks. */
public SignedToken getXsrfToken() {
return xsrf;
}
/** Get the signature support used to protect user identity cookies. */
public SignedToken getAccountToken() {
return account;
}
/** Get the signature used for email registration/validation links. */
public SignedToken getEmailRegistrationToken() {
return emailReg;
}
public String getLoginHttpHeader() {
return sConfig.loginHttpHeader;
}
public String getEmailFormat() {
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()];
for (int k = r.length - 1; k >= 0; k--) {
r[k] = (byte) sConfig.accountPrivateKey.charAt(k);
}
r = Base64.decodeBase64(r);
final StringBuilder b = new StringBuilder();
for (int i = 0; i < r.length; i++) {
b.append((char) r[i]);
}
return b.toString();
}
/** Local filesystem location of header/footer/CSS configuration files. */
public File getSitePath() {
return sConfig.sitePath != null ? new File(sConfig.sitePath) : null;
}
/** Optional canonical URL for this application. */
public String getCanonicalURL() {
String u = sConfig.canonicalUrl;
if (u != null && !u.endsWith("/")) {
u += "/";
}
return u;
}
/** Get the repositories maintained by this server. */
public RepositoryCache getRepositoryCache() {
return repositories;
}
/** The mail session used to send messages; null if not configured. */
public javax.mail.Session getOutgoingMail() {
return outgoingMail;
}
/** Get a new identity representing this Gerrit server in Git. */
public PersonIdent newGerritPersonIdent() {
return new PersonIdent(gerritPersonIdentTemplate);
}
public boolean isAllowGoogleAccountUpgrade() {
return sConfig.allowGoogleAccountUpgrade;
}
}