PushCertificateStore.java

  1. /*
  2.  * Copyright (C) 2015, Google Inc. and others
  3.  *
  4.  * This program and the accompanying materials are made available under the
  5.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  6.  * https://www.eclipse.org/org/documents/edl-v10.php.
  7.  *
  8.  * SPDX-License-Identifier: BSD-3-Clause
  9.  */

  10. package org.eclipse.jgit.transport;

  11. import static java.nio.charset.StandardCharsets.UTF_8;
  12. import static org.eclipse.jgit.lib.Constants.OBJ_BLOB;
  13. import static org.eclipse.jgit.lib.Constants.OBJ_COMMIT;
  14. import static org.eclipse.jgit.lib.FileMode.TYPE_FILE;

  15. import java.io.BufferedReader;
  16. import java.io.IOException;
  17. import java.io.InputStream;
  18. import java.io.InputStreamReader;
  19. import java.io.Reader;
  20. import java.text.MessageFormat;
  21. import java.util.ArrayList;
  22. import java.util.Collection;
  23. import java.util.Collections;
  24. import java.util.HashMap;
  25. import java.util.Iterator;
  26. import java.util.List;
  27. import java.util.Map;
  28. import java.util.NoSuchElementException;

  29. import org.eclipse.jgit.dircache.DirCache;
  30. import org.eclipse.jgit.dircache.DirCacheEditor;
  31. import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
  32. import org.eclipse.jgit.dircache.DirCacheEntry;
  33. import org.eclipse.jgit.internal.JGitText;
  34. import org.eclipse.jgit.lib.BatchRefUpdate;
  35. import org.eclipse.jgit.lib.CommitBuilder;
  36. import org.eclipse.jgit.lib.Constants;
  37. import org.eclipse.jgit.lib.FileMode;
  38. import org.eclipse.jgit.lib.ObjectId;
  39. import org.eclipse.jgit.lib.ObjectInserter;
  40. import org.eclipse.jgit.lib.ObjectLoader;
  41. import org.eclipse.jgit.lib.ObjectReader;
  42. import org.eclipse.jgit.lib.PersonIdent;
  43. import org.eclipse.jgit.lib.Ref;
  44. import org.eclipse.jgit.lib.RefUpdate;
  45. import org.eclipse.jgit.lib.Repository;
  46. import org.eclipse.jgit.revwalk.RevCommit;
  47. import org.eclipse.jgit.revwalk.RevWalk;
  48. import org.eclipse.jgit.treewalk.TreeWalk;
  49. import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
  50. import org.eclipse.jgit.treewalk.filter.PathFilter;
  51. import org.eclipse.jgit.treewalk.filter.PathFilterGroup;
  52. import org.eclipse.jgit.treewalk.filter.TreeFilter;

  53. /**
  54.  * Storage for recorded push certificates.
  55.  * <p>
  56.  * Push certificates are stored in a special ref {@code refs/meta/push-certs}.
  57.  * The filenames in the tree are ref names followed by the special suffix
  58.  * <code>@{cert}</code>, and the contents are the latest push cert affecting
  59.  * that ref. The special suffix allows storing certificates for both refs/foo
  60.  * and refs/foo/bar in case those both existed at some point.
  61.  *
  62.  * @since 4.1
  63.  */
  64. public class PushCertificateStore implements AutoCloseable {
  65.     /** Ref name storing push certificates. */
  66.     static final String REF_NAME =
  67.             Constants.R_REFS + "meta/push-certs"; //$NON-NLS-1$

  68.     private static class PendingCert {
  69.         PushCertificate cert;
  70.         PersonIdent ident;
  71.         Collection<ReceiveCommand> matching;

  72.         PendingCert(PushCertificate cert, PersonIdent ident,
  73.                 Collection<ReceiveCommand> matching) {
  74.             this.cert = cert;
  75.             this.ident = ident;
  76.             this.matching = matching;
  77.         }
  78.     }

  79.     private final Repository db;
  80.     private final List<PendingCert> pending;
  81.     ObjectReader reader;
  82.     RevCommit commit;

  83.     /**
  84.      * Create a new store backed by the given repository.
  85.      *
  86.      * @param db
  87.      *            the repository.
  88.      */
  89.     public PushCertificateStore(Repository db) {
  90.         this.db = db;
  91.         pending = new ArrayList<>();
  92.     }

  93.     /**
  94.      * {@inheritDoc}
  95.      * <p>
  96.      * Close resources opened by this store.
  97.      * <p>
  98.      * If {@link #get(String)} was called, closes the cached object reader
  99.      * created by that method. Does not close the underlying repository.
  100.      */
  101.     @Override
  102.     public void close() {
  103.         if (reader != null) {
  104.             reader.close();
  105.             reader = null;
  106.             commit = null;
  107.         }
  108.     }

  109.     /**
  110.      * Get latest push certificate associated with a ref.
  111.      * <p>
  112.      * Lazily opens {@code refs/meta/push-certs} and reads from the repository as
  113.      * necessary. The state is cached between calls to {@code get}; to reread the,
  114.      * call {@link #close()} first.
  115.      *
  116.      * @param refName
  117.      *            the ref name to get the certificate for.
  118.      * @return last certificate affecting the ref, or null if no cert was recorded
  119.      *         for the last update to this ref.
  120.      * @throws java.io.IOException
  121.      *             if a problem occurred reading the repository.
  122.      */
  123.     public PushCertificate get(String refName) throws IOException {
  124.         if (reader == null) {
  125.             load();
  126.         }
  127.         try (TreeWalk tw = newTreeWalk(refName)) {
  128.             return read(tw);
  129.         }
  130.     }

  131.     /**
  132.      * Iterate over all push certificates affecting a ref.
  133.      * <p>
  134.      * Only includes push certificates actually stored in the tree; see class
  135.      * Javadoc for conditions where this might not include all push certs ever
  136.      * seen for this ref.
  137.      * <p>
  138.      * The returned iterable may be iterated multiple times, and push certs will
  139.      * be re-read from the current state of the store on each call to {@link
  140.      * Iterable#iterator()}. However, method calls on the returned iterator may
  141.      * fail if {@code save} or {@code close} is called on the enclosing store
  142.      * during iteration.
  143.      *
  144.      * @param refName
  145.      *            the ref name to get certificates for.
  146.      * @return iterable over certificates; must be fully iterated in order to
  147.      *         close resources.
  148.      */
  149.     public Iterable<PushCertificate> getAll(String refName) {
  150.         return () -> new Iterator<>() {
  151.             private final String path = pathName(refName);

  152.             private PushCertificate next;

  153.             private RevWalk rw;
  154.             {
  155.                 try {
  156.                     if (reader == null) {
  157.                         load();
  158.                     }
  159.                     if (commit != null) {
  160.                         rw = new RevWalk(reader);
  161.                         rw.setTreeFilter(AndTreeFilter.create(
  162.                                 PathFilterGroup.create(Collections
  163.                                         .singleton(PathFilter.create(path))),
  164.                                 TreeFilter.ANY_DIFF));
  165.                         rw.setRewriteParents(false);
  166.                         rw.markStart(rw.parseCommit(commit));
  167.                     } else {
  168.                         rw = null;
  169.                     }
  170.                 } catch (IOException e) {
  171.                     throw new RuntimeException(e);
  172.                 }
  173.             }

  174.             @Override
  175.             public boolean hasNext() {
  176.                 try {
  177.                     if (next == null) {
  178.                         if (rw == null) {
  179.                             return false;
  180.                         }
  181.                         try {
  182.                             RevCommit c = rw.next();
  183.                             if (c != null) {
  184.                                 try (TreeWalk tw = TreeWalk.forPath(
  185.                                         rw.getObjectReader(), path,
  186.                                         c.getTree())) {
  187.                                     next = read(tw);
  188.                                 }
  189.                             } else {
  190.                                 next = null;
  191.                             }
  192.                         } catch (IOException e) {
  193.                             throw new RuntimeException(e);
  194.                         }
  195.                     }
  196.                     return next != null;
  197.                 } finally {
  198.                     if (next == null && rw != null) {
  199.                         rw.close();
  200.                         rw = null;
  201.                     }
  202.                 }
  203.             }

  204.             @Override
  205.             public PushCertificate next() {
  206.                 if (!hasNext()) {
  207.                     throw new NoSuchElementException();
  208.                 }
  209.                 PushCertificate n = next;
  210.                 next = null;
  211.                 return n;
  212.             }

  213.             @Override
  214.             public void remove() {
  215.                 throw new UnsupportedOperationException();
  216.             }
  217.         };
  218.     }

  219.     void load() throws IOException {
  220.         close();
  221.         reader = db.newObjectReader();
  222.         Ref ref = db.getRefDatabase().exactRef(REF_NAME);
  223.         if (ref == null) {
  224.             // No ref, same as empty.
  225.             return;
  226.         }
  227.         try (RevWalk rw = new RevWalk(reader)) {
  228.             commit = rw.parseCommit(ref.getObjectId());
  229.         }
  230.     }

  231.     static PushCertificate read(TreeWalk tw) throws IOException {
  232.         if (tw == null || (tw.getRawMode(0) & TYPE_FILE) != TYPE_FILE) {
  233.             return null;
  234.         }
  235.         ObjectLoader loader =
  236.                 tw.getObjectReader().open(tw.getObjectId(0), OBJ_BLOB);
  237.         try (InputStream in = loader.openStream();
  238.                 Reader r = new BufferedReader(
  239.                         new InputStreamReader(in, UTF_8))) {
  240.             return PushCertificateParser.fromReader(r);
  241.         }
  242.     }

  243.     /**
  244.      * Put a certificate to be saved to the store.
  245.      * <p>
  246.      * Writes the contents of this certificate for each ref mentioned. It is up
  247.      * to the caller to ensure this certificate accurately represents the state
  248.      * of the ref.
  249.      * <p>
  250.      * Pending certificates added to this method are not returned by
  251.      * {@link #get(String)} and {@link #getAll(String)} until after calling
  252.      * {@link #save()}.
  253.      *
  254.      * @param cert
  255.      *            certificate to store.
  256.      * @param ident
  257.      *            identity for the commit that stores this certificate. Pending
  258.      *            certificates are sorted by identity timestamp during
  259.      *            {@link #save()}.
  260.      */
  261.     public void put(PushCertificate cert, PersonIdent ident) {
  262.         put(cert, ident, null);
  263.     }

  264.     /**
  265.      * Put a certificate to be saved to the store, matching a set of commands.
  266.      * <p>
  267.      * Like {@link #put(PushCertificate, PersonIdent)}, except a value is only
  268.      * stored for a push certificate if there is a corresponding command in the
  269.      * list that exactly matches the old/new values mentioned in the push
  270.      * certificate.
  271.      * <p>
  272.      * Pending certificates added to this method are not returned by
  273.      * {@link #get(String)} and {@link #getAll(String)} until after calling
  274.      * {@link #save()}.
  275.      *
  276.      * @param cert
  277.      *            certificate to store.
  278.      * @param ident
  279.      *            identity for the commit that stores this certificate. Pending
  280.      *            certificates are sorted by identity timestamp during
  281.      *            {@link #save()}.
  282.      * @param matching
  283.      *            only store certs for the refs listed in this list whose values
  284.      *            match the commands in the cert.
  285.      */
  286.     public void put(PushCertificate cert, PersonIdent ident,
  287.             Collection<ReceiveCommand> matching) {
  288.         pending.add(new PendingCert(cert, ident, matching));
  289.     }

  290.     /**
  291.      * Save pending certificates to the store.
  292.      * <p>
  293.      * One commit is created per certificate added with
  294.      * {@link #put(PushCertificate, PersonIdent)}, in order of identity
  295.      * timestamps, and a single ref update is performed.
  296.      * <p>
  297.      * The pending list is cleared if and only the ref update fails, which
  298.      * allows for easy retries in case of lock failure.
  299.      *
  300.      * @return the result of attempting to update the ref.
  301.      * @throws java.io.IOException
  302.      *             if there was an error reading from or writing to the
  303.      *             repository.
  304.      */
  305.     public RefUpdate.Result save() throws IOException {
  306.         ObjectId newId = write();
  307.         if (newId == null) {
  308.             return RefUpdate.Result.NO_CHANGE;
  309.         }
  310.         try (ObjectInserter inserter = db.newObjectInserter()) {
  311.             RefUpdate.Result result = updateRef(newId);
  312.             switch (result) {
  313.                 case FAST_FORWARD:
  314.                 case NEW:
  315.                 case NO_CHANGE:
  316.                     pending.clear();
  317.                     break;
  318.                 default:
  319.                     break;
  320.             }
  321.             return result;
  322.         } finally {
  323.             close();
  324.         }
  325.     }

  326.     /**
  327.      * Save pending certificates to the store in an existing batch ref update.
  328.      * <p>
  329.      * One commit is created per certificate added with
  330.      * {@link #put(PushCertificate, PersonIdent)}, in order of identity
  331.      * timestamps, all commits are flushed, and a single command is added to the
  332.      * batch.
  333.      * <p>
  334.      * The cached ref value and pending list are <em>not</em> cleared. If the
  335.      * ref update succeeds, the caller is responsible for calling
  336.      * {@link #close()} and/or {@link #clear()}.
  337.      *
  338.      * @param batch
  339.      *            update to save to.
  340.      * @return whether a command was added to the batch.
  341.      * @throws java.io.IOException
  342.      *             if there was an error reading from or writing to the
  343.      *             repository.
  344.      */
  345.     public boolean save(BatchRefUpdate batch) throws IOException {
  346.         ObjectId newId = write();
  347.         if (newId == null || newId.equals(commit)) {
  348.             return false;
  349.         }
  350.         batch.addCommand(new ReceiveCommand(
  351.                 commit != null ? commit : ObjectId.zeroId(), newId, REF_NAME));
  352.         return true;
  353.     }

  354.     /**
  355.      * Clear pending certificates added with {@link #put(PushCertificate,
  356.      * PersonIdent)}.
  357.      */
  358.     public void clear() {
  359.         pending.clear();
  360.     }

  361.     private ObjectId write() throws IOException {
  362.         if (pending.isEmpty()) {
  363.             return null;
  364.         }
  365.         if (reader == null) {
  366.             load();
  367.         }
  368.         sortPending(pending);

  369.         ObjectId curr = commit;
  370.         DirCache dc = newDirCache();
  371.         try (ObjectInserter inserter = db.newObjectInserter()) {
  372.             for (PendingCert pc : pending) {
  373.                 curr = saveCert(inserter, dc, pc, curr);
  374.             }
  375.             inserter.flush();
  376.             return curr;
  377.         }
  378.     }

  379.     private static void sortPending(List<PendingCert> pending) {
  380.         Collections.sort(pending, (PendingCert a, PendingCert b) -> Long.signum(
  381.                 a.ident.getWhen().getTime() - b.ident.getWhen().getTime()));
  382.     }

  383.     private DirCache newDirCache() throws IOException {
  384.         if (commit != null) {
  385.             return DirCache.read(reader, commit.getTree());
  386.         }
  387.         return DirCache.newInCore();
  388.     }

  389.     private ObjectId saveCert(ObjectInserter inserter, DirCache dc,
  390.             PendingCert pc, ObjectId curr) throws IOException {
  391.         Map<String, ReceiveCommand> byRef;
  392.         if (pc.matching != null) {
  393.             byRef = new HashMap<>();
  394.             for (ReceiveCommand cmd : pc.matching) {
  395.                 if (byRef.put(cmd.getRefName(), cmd) != null) {
  396.                     throw new IllegalStateException();
  397.                 }
  398.             }
  399.         } else {
  400.             byRef = null;
  401.         }

  402.         DirCacheEditor editor = dc.editor();
  403.         String certText = pc.cert.toText() + pc.cert.getSignature();
  404.         final ObjectId certId = inserter.insert(OBJ_BLOB, certText.getBytes(UTF_8));
  405.         boolean any = false;
  406.         for (ReceiveCommand cmd : pc.cert.getCommands()) {
  407.             if (byRef != null && !commandsEqual(cmd, byRef.get(cmd.getRefName()))) {
  408.                 continue;
  409.             }
  410.             any = true;
  411.             editor.add(new PathEdit(pathName(cmd.getRefName())) {
  412.                 @Override
  413.                 public void apply(DirCacheEntry ent) {
  414.                     ent.setFileMode(FileMode.REGULAR_FILE);
  415.                     ent.setObjectId(certId);
  416.                 }
  417.             });
  418.         }
  419.         if (!any) {
  420.             return curr;
  421.         }
  422.         editor.finish();
  423.         CommitBuilder cb = new CommitBuilder();
  424.         cb.setAuthor(pc.ident);
  425.         cb.setCommitter(pc.ident);
  426.         cb.setTreeId(dc.writeTree(inserter));
  427.         if (curr != null) {
  428.             cb.setParentId(curr);
  429.         } else {
  430.             cb.setParentIds(Collections.<ObjectId> emptyList());
  431.         }
  432.         cb.setMessage(buildMessage(pc.cert));
  433.         return inserter.insert(OBJ_COMMIT, cb.build());
  434.     }

  435.     private static boolean commandsEqual(ReceiveCommand c1, ReceiveCommand c2) {
  436.         if (c1 == null || c2 == null) {
  437.             return c1 == c2;
  438.         }
  439.         return c1.getRefName().equals(c2.getRefName())
  440.                 && c1.getOldId().equals(c2.getOldId())
  441.                 && c1.getNewId().equals(c2.getNewId());
  442.     }

  443.     private RefUpdate.Result updateRef(ObjectId newId) throws IOException {
  444.         RefUpdate ru = db.updateRef(REF_NAME);
  445.         ru.setExpectedOldObjectId(commit != null ? commit : ObjectId.zeroId());
  446.         ru.setNewObjectId(newId);
  447.         ru.setRefLogIdent(pending.get(pending.size() - 1).ident);
  448.         ru.setRefLogMessage(JGitText.get().storePushCertReflog, false);
  449.         try (RevWalk rw = new RevWalk(reader)) {
  450.             return ru.update(rw);
  451.         }
  452.     }

  453.     private TreeWalk newTreeWalk(String refName) throws IOException {
  454.         if (commit == null) {
  455.             return null;
  456.         }
  457.         return TreeWalk.forPath(reader, pathName(refName), commit.getTree());
  458.     }

  459.     static String pathName(String refName) {
  460.         return refName + "@{cert}"; //$NON-NLS-1$
  461.     }

  462.     private static String buildMessage(PushCertificate cert) {
  463.         StringBuilder sb = new StringBuilder();
  464.         if (cert.getCommands().size() == 1) {
  465.             sb.append(MessageFormat.format(
  466.                     JGitText.get().storePushCertOneRef,
  467.                     cert.getCommands().get(0).getRefName()));
  468.         } else {
  469.             sb.append(MessageFormat.format(
  470.                     JGitText.get().storePushCertMultipleRefs,
  471.                     Integer.valueOf(cert.getCommands().size())));
  472.         }
  473.         return sb.append('\n').toString();
  474.     }
  475. }