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<PushCertificate>() {
  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.                 hasNext();
  207.                 PushCertificate n = next;
  208.                 if (n == null) {
  209.                     throw new NoSuchElementException();
  210.                 }
  211.                 next = null;
  212.                 return n;
  213.             }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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