diff options
Diffstat (limited to 'java/com/google/gerrit/gpg/server/PostGpgKeys.java')
-rw-r--r-- | java/com/google/gerrit/gpg/server/PostGpgKeys.java | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/java/com/google/gerrit/gpg/server/PostGpgKeys.java b/java/com/google/gerrit/gpg/server/PostGpgKeys.java new file mode 100644 index 0000000000..7d08fca524 --- /dev/null +++ b/java/com/google/gerrit/gpg/server/PostGpgKeys.java @@ -0,0 +1,293 @@ +// 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 com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; +import com.google.common.flogger.FluentLogger; +import com.google.common.io.BaseEncoding; +import com.google.gerrit.common.errors.EmailException; +import com.google.gerrit.extensions.api.accounts.GpgKeysInput; +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.reviewdb.client.Account; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.GerritPersonIdent; +import com.google.gerrit.server.IdentifiedUser; +import com.google.gerrit.server.UserInitiated; +import com.google.gerrit.server.account.AccountResource; +import com.google.gerrit.server.account.AccountState; +import com.google.gerrit.server.account.AccountsUpdate; +import com.google.gerrit.server.account.externalids.ExternalId; +import com.google.gerrit.server.account.externalids.ExternalIds; +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 org.bouncycastle.bcpg.ArmoredInputStream; +import org.bouncycastle.openpgp.PGPException; +import org.bouncycastle.openpgp.PGPPublicKey; +import org.bouncycastle.openpgp.PGPPublicKeyRing; +import org.bouncycastle.openpgp.PGPRuntimeOperationException; +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; + +@Singleton +public class PostGpgKeys implements RestModifyView<AccountResource, GpgKeysInput> { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private final Provider<PersonIdent> serverIdent; + private final Provider<CurrentUser> self; + private final Provider<PublicKeyStore> storeProvider; + private final GerritPublicKeyChecker.Factory checkerFactory; + private final AddKeySender.Factory addKeyFactory; + private final Provider<InternalAccountQuery> accountQueryProvider; + private final ExternalIds externalIds; + private final Provider<AccountsUpdate> accountsUpdateProvider; + + @Inject + PostGpgKeys( + @GerritPersonIdent Provider<PersonIdent> serverIdent, + Provider<CurrentUser> self, + Provider<PublicKeyStore> storeProvider, + GerritPublicKeyChecker.Factory checkerFactory, + AddKeySender.Factory addKeyFactory, + Provider<InternalAccountQuery> accountQueryProvider, + ExternalIds externalIds, + @UserInitiated Provider<AccountsUpdate> accountsUpdateProvider) { + this.serverIdent = serverIdent; + this.self = self; + this.storeProvider = storeProvider; + this.checkerFactory = checkerFactory; + this.addKeyFactory = addKeyFactory; + this.accountQueryProvider = accountQueryProvider; + this.externalIds = externalIds; + this.accountsUpdateProvider = accountsUpdateProvider; + } + + @Override + public Map<String, GpgKeyInfo> apply(AccountResource rsrc, GpgKeysInput input) + throws ResourceNotFoundException, BadRequestException, ResourceConflictException, + PGPException, OrmException, IOException, ConfigInvalidException { + GpgKeys.checkVisible(self, rsrc); + + Collection<ExternalId> existingExtIds = + externalIds.byAccount(rsrc.getUser().getAccountId(), SCHEME_GPGKEY); + try (PublicKeyStore store = storeProvider.get()) { + Map<ExternalId, Fingerprint> toRemove = readKeysToRemove(input, existingExtIds); + Collection<Fingerprint> fingerprintsToRemove = toRemove.values(); + List<PGPPublicKeyRing> newKeys = readKeysToAdd(input, fingerprintsToRemove); + 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, fingerprintsToRemove); + + accountsUpdateProvider + .get() + .update( + "Update GPG Keys via API", + rsrc.getUser().getAccountId(), + u -> u.replaceExternalIds(toRemove.keySet(), newExtIds)); + return toJson(newKeys, fingerprintsToRemove, store, rsrc.getUser()); + } + } + + private Map<ExternalId, Fingerprint> readKeysToRemove( + GpgKeysInput input, Collection<ExternalId> existingExtIds) { + if (input.delete == null || input.delete.isEmpty()) { + return ImmutableMap.of(); + } + Map<ExternalId, Fingerprint> fingerprints = + Maps.newHashMapWithExpectedSize(input.delete.size()); + for (String id : input.delete) { + try { + ExternalId gpgKeyExtId = GpgKeys.findGpgKey(id, existingExtIds); + fingerprints.put(gpgKeyExtId, new Fingerprint(GpgKeys.parseFingerprint(gpgKeyExtId))); + } catch (ResourceNotFoundException e) { + // Skip removal. + } + } + return fingerprints; + } + + private List<PGPPublicKeyRing> readKeysToAdd(GpgKeysInput input, Collection<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); + } catch (PGPRuntimeOperationException e) { + throw new BadRequestException("Failed to parse GPG keys", e); + } + } + return keyRings; + } + + private void storeKeys( + AccountResource rsrc, List<PGPPublicKeyRing> keyRings, Collection<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) { + logger.atSevere().withCause(e).log( + "Cannot send GPG key added message to %s", + rsrc.getUser().getAccount().getPreferredEmail()); + } + break; + case NO_CHANGE: + break; + case IO_FAILURE: + case LOCK_FAILURE: + case NOT_ATTEMPTED: + case REJECTED: + case REJECTED_CURRENT_BRANCH: + case RENAMED: + case REJECTED_MISSING_OBJECT: + case REJECTED_OTHER_REASON: + 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: ") + .append(Lists.transform(accountStates, AccountState.ACCOUNT_ID_FUNCTION)); + throw new IllegalStateException(msg.toString()); + } + + return accountStates.get(0).getAccount(); + } + + private Map<String, GpgKeyInfo> toJson( + Collection<PGPPublicKeyRing> keys, + Collection<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; + } +} |