com.google.gerrit.gpg.server.PostGpgKeys.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.gpg.server.PostGpgKeys.java

Source

// Copyright (C) 2015 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.gpg.server;

import static com.google.gerrit.gpg.PublicKeyStore.keyIdToString;
import static com.google.gerrit.gpg.PublicKeyStore.keyToString;
import static com.google.gerrit.server.account.externalids.ExternalId.SCHEME_GPGKEY;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.stream.Collectors.toList;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.BaseEncoding;
import com.google.gerrit.common.errors.EmailException;
import com.google.gerrit.extensions.common.GpgKeyInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestModifyView;
import com.google.gerrit.gpg.CheckResult;
import com.google.gerrit.gpg.Fingerprint;
import com.google.gerrit.gpg.GerritPublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyChecker;
import com.google.gerrit.gpg.PublicKeyStore;
import com.google.gerrit.gpg.server.PostGpgKeys.Input;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountResource;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.account.externalids.ExternalId;
import com.google.gerrit.server.account.externalids.ExternalIds;
import com.google.gerrit.server.account.externalids.ExternalIdsUpdate;
import com.google.gerrit.server.mail.send.AddKeySender;
import com.google.gerrit.server.query.account.InternalAccountQuery;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.bouncycastle.bcpg.ArmoredInputStream;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPPublicKey;
import org.bouncycastle.openpgp.PGPPublicKeyRing;
import org.bouncycastle.openpgp.bc.BcPGPObjectFactory;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Singleton
public class PostGpgKeys implements RestModifyView<AccountResource, Input> {
    public static class Input {
        public List<String> add;
        public List<String> delete;
    }

    private final Logger log = LoggerFactory.getLogger(getClass());
    private final Provider<PersonIdent> serverIdent;
    private final Provider<ReviewDb> db;
    private final Provider<CurrentUser> self;
    private final Provider<PublicKeyStore> storeProvider;
    private final GerritPublicKeyChecker.Factory checkerFactory;
    private final AddKeySender.Factory addKeyFactory;
    private final AccountCache accountCache;
    private final Provider<InternalAccountQuery> accountQueryProvider;
    private final ExternalIds externalIds;
    private final ExternalIdsUpdate.User externalIdsUpdateFactory;

    @Inject
    PostGpgKeys(@GerritPersonIdent Provider<PersonIdent> serverIdent, Provider<ReviewDb> db,
            Provider<CurrentUser> self, Provider<PublicKeyStore> storeProvider,
            GerritPublicKeyChecker.Factory checkerFactory, AddKeySender.Factory addKeyFactory,
            AccountCache accountCache, Provider<InternalAccountQuery> accountQueryProvider, ExternalIds externalIds,
            ExternalIdsUpdate.User externalIdsUpdateFactory) {
        this.serverIdent = serverIdent;
        this.db = db;
        this.self = self;
        this.storeProvider = storeProvider;
        this.checkerFactory = checkerFactory;
        this.addKeyFactory = addKeyFactory;
        this.accountCache = accountCache;
        this.accountQueryProvider = accountQueryProvider;
        this.externalIds = externalIds;
        this.externalIdsUpdateFactory = externalIdsUpdateFactory;
    }

    @Override
    public Map<String, GpgKeyInfo> apply(AccountResource rsrc, Input input)
            throws ResourceNotFoundException, BadRequestException, ResourceConflictException, PGPException,
            OrmException, IOException, ConfigInvalidException {
        GpgKeys.checkVisible(self, rsrc);

        Collection<ExternalId> existingExtIds = externalIds.byAccount(db.get(), rsrc.getUser().getAccountId(),
                SCHEME_GPGKEY);
        try (PublicKeyStore store = storeProvider.get()) {
            Set<Fingerprint> toRemove = readKeysToRemove(input, existingExtIds);
            List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, toRemove);
            List<ExternalId> newExtIds = new ArrayList<>(existingExtIds.size());

            for (PGPPublicKeyRing keyRing : newKeys) {
                PGPPublicKey key = keyRing.getPublicKey();
                ExternalId.Key extIdKey = toExtIdKey(key.getFingerprint());
                Account account = getAccountByExternalId(extIdKey);
                if (account != null) {
                    if (!account.getId().equals(rsrc.getUser().getAccountId())) {
                        throw new ResourceConflictException("GPG key already associated with another account");
                    }
                } else {
                    newExtIds.add(ExternalId.create(extIdKey, rsrc.getUser().getAccountId()));
                }
            }

            storeKeys(rsrc, newKeys, toRemove);

            List<ExternalId.Key> extIdKeysToRemove = toRemove.stream().map(fp -> toExtIdKey(fp.get()))
                    .collect(toList());
            externalIdsUpdateFactory.create().replace(db.get(), rsrc.getUser().getAccountId(), extIdKeysToRemove,
                    newExtIds);
            accountCache.evict(rsrc.getUser().getAccountId());
            return toJson(newKeys, toRemove, store, rsrc.getUser());
        }
    }

    private Set<Fingerprint> readKeysToRemove(Input input, Collection<ExternalId> existingExtIds) {
        if (input.delete == null || input.delete.isEmpty()) {
            return ImmutableSet.of();
        }
        Set<Fingerprint> fingerprints = Sets.newHashSetWithExpectedSize(input.delete.size());
        for (String id : input.delete) {
            try {
                fingerprints.add(new Fingerprint(GpgKeys.parseFingerprint(id, existingExtIds)));
            } catch (ResourceNotFoundException e) {
                // Skip removal.
            }
        }
        return fingerprints;
    }

    private List<PGPPublicKeyRing> readKeysToAdd(Input input, Set<Fingerprint> toRemove)
            throws BadRequestException, IOException {
        if (input.add == null || input.add.isEmpty()) {
            return ImmutableList.of();
        }
        List<PGPPublicKeyRing> keyRings = new ArrayList<>(input.add.size());
        for (String armored : input.add) {
            try (InputStream in = new ByteArrayInputStream(armored.getBytes(UTF_8));
                    ArmoredInputStream ain = new ArmoredInputStream(in)) {
                @SuppressWarnings("unchecked")
                List<Object> objs = Lists.newArrayList(new BcPGPObjectFactory(ain));
                if (objs.size() != 1 || !(objs.get(0) instanceof PGPPublicKeyRing)) {
                    throw new BadRequestException("Expected exactly one PUBLIC KEY BLOCK");
                }
                PGPPublicKeyRing keyRing = (PGPPublicKeyRing) objs.get(0);
                if (toRemove.contains(new Fingerprint(keyRing.getPublicKey().getFingerprint()))) {
                    throw new BadRequestException(
                            "Cannot both add and delete key: " + keyToString(keyRing.getPublicKey()));
                }
                keyRings.add(keyRing);
            }
        }
        return keyRings;
    }

    private void storeKeys(AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Set<Fingerprint> toRemove)
            throws BadRequestException, ResourceConflictException, PGPException, IOException {
        try (PublicKeyStore store = storeProvider.get()) {
            List<String> addedKeys = new ArrayList<>();
            for (PGPPublicKeyRing keyRing : keyRings) {
                PGPPublicKey key = keyRing.getPublicKey();
                // Don't check web of trust; admins can fill in certifications later.
                CheckResult result = checkerFactory.create(rsrc.getUser(), store).disableTrust().check(key);
                if (!result.isOk()) {
                    throw new BadRequestException(String.format("Problems with public key %s:\n%s",
                            keyToString(key), Joiner.on('\n').join(result.getProblems())));
                }
                addedKeys.add(PublicKeyStore.keyToString(key));
                store.add(keyRing);
            }
            for (Fingerprint fp : toRemove) {
                store.remove(fp.get());
            }
            CommitBuilder cb = new CommitBuilder();
            PersonIdent committer = serverIdent.get();
            cb.setAuthor(rsrc.getUser().newCommitterIdent(committer.getWhen(), committer.getTimeZone()));
            cb.setCommitter(committer);

            RefUpdate.Result saveResult = store.save(cb);
            switch (saveResult) {
            case NEW:
            case FAST_FORWARD:
            case FORCED:
                try {
                    addKeyFactory.create(rsrc.getUser(), addedKeys).send();
                } catch (EmailException e) {
                    log.error("Cannot send GPG key added message to "
                            + rsrc.getUser().getAccount().getPreferredEmail(), e);
                }
                break;
            case NO_CHANGE:
                break;
            case IO_FAILURE:
            case LOCK_FAILURE:
            case NOT_ATTEMPTED:
            case REJECTED:
            case REJECTED_CURRENT_BRANCH:
            case RENAMED:
            default:
                // TODO(dborowitz): Backoff and retry on LOCK_FAILURE.
                throw new ResourceConflictException("Failed to save public keys: " + saveResult);
            }
        }
    }

    private ExternalId.Key toExtIdKey(byte[] fp) {
        return ExternalId.Key.create(SCHEME_GPGKEY, BaseEncoding.base16().encode(fp));
    }

    private Account getAccountByExternalId(ExternalId.Key extIdKey) throws OrmException {
        List<AccountState> accountStates = accountQueryProvider.get().byExternalId(extIdKey);

        if (accountStates.isEmpty()) {
            return null;
        }

        if (accountStates.size() > 1) {
            StringBuilder msg = new StringBuilder();
            msg.append("GPG key ").append(extIdKey.get()).append(" associated with multiple accounts: ");
            Joiner.on(", ").appendTo(msg, Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION));
            log.error(msg.toString());
            throw new IllegalStateException(msg.toString());
        }

        return accountStates.get(0).getAccount();
    }

    private Map<String, GpgKeyInfo> toJson(Collection<PGPPublicKeyRing> keys, Set<Fingerprint> deleted,
            PublicKeyStore store, IdentifiedUser user) throws IOException {
        // Unlike when storing keys, include web-of-trust checks when producing
        // result JSON, so the user at least knows of any issues.
        PublicKeyChecker checker = checkerFactory.create(user, store);
        Map<String, GpgKeyInfo> infos = Maps.newHashMapWithExpectedSize(keys.size() + deleted.size());
        for (PGPPublicKeyRing keyRing : keys) {
            PGPPublicKey key = keyRing.getPublicKey();
            CheckResult result = checker.check(key);
            GpgKeyInfo info = GpgKeys.toJson(key, result);
            infos.put(info.id, info);
            info.id = null;
        }
        for (Fingerprint fp : deleted) {
            infos.put(keyIdToString(fp.getId()), new GpgKeyInfo());
        }
        return infos;
    }
}