From 57fc4782e0e6d729b840825dc4f62d75e4f3bd9f Mon Sep 17 00:00:00 2001 From: Toby Crawley Date: Tue, 16 Feb 2016 20:33:19 +0000 Subject: [PATCH] Make deployment atomic Deployments aren't currently atomic, so if a deploy is interrupted, it can leave the repo in an inconsistent state. This adds the basics of atomic deploys, with the following additional changes: * an update to pomegranate (0.0.13 -> 0.3.0) - I'm using aether to deploy from the tmp upload dir to the repo, and need the :artifact-map functionality that doesn't exist in 0.0.13 * replacing the auth helper macros with functions - this is to make it easier to debug, and easier to determine where values come from (instead of doing magic binding, like we were doing with 'account) The atomic deploy functionality uses sessions (since aether honors session cookies) to store uploads in a tmp dir that is scoped to the deploy. Once we see a non-snapshot maven-metadata.xml, we finalize the deployment by verifying the contents and then deploying them to the actual repo. This process ignores checksum files, since the redeploy recreates them. The finalize process also validates the deploy, which includes the existing validations (that were once per-artifact), plus adds some new ones that operate over the full set of artifacts. The validations are (existing validations marked with ^): * verify a pom was uploaded * verify the pom parses * verify the group, name, and version of a valid format ^ * verify the gav values in the pom match the gav in the url * verify this isn't a redeploy of a non-SNAPSHOT version ^ * verify that a jar was uploaded if the pom packaging is jar * verify that the provided checksums match the artifacts * verify that if any signature is uploaded, then every artifact has a signature Now that we are validating after all the artifacts are pushed, we no longer fail-fast on redeploys or invalid gav's, but I don't think that should cause issues. The change to require valid poms is debatable, given that it will prevent deploys of projects that are affected by #233. --- project.clj | 2 +- src/clojars/auth.clj | 20 +- src/clojars/file_utils.clj | 29 +-- src/clojars/maven.clj | 1 + src/clojars/routes/artifact.clj | 29 ++- src/clojars/routes/group.clj | 34 +-- src/clojars/routes/repo.clj | 248 ++++++++++++++++------ src/clojars/routes/user.clj | 6 +- src/clojars/web.clj | 55 ++--- test/clojars/test/integration/uploads.clj | 117 ++++++++-- test/clojars/test/test_helper.clj | 17 +- 11 files changed, 378 insertions(+), 180 deletions(-) diff --git a/project.clj b/project.clj index e23826ed..03a443e3 100644 --- a/project.clj +++ b/project.clj @@ -6,7 +6,7 @@ [org.apache.maven/maven-model "3.0.4" :exclusions [org.codehaus.plexus/plexus-utils]] - [com.cemerick/pomegranate "0.0.13" + [com.cemerick/pomegranate "0.3.0" :exclusions [org.apache.httpcomponents/httpcore commons-logging]] diff --git a/src/clojars/auth.clj b/src/clojars/auth.clj index f38f3b52..8e80ba66 100644 --- a/src/clojars/auth.clj +++ b/src/clojars/auth.clj @@ -2,21 +2,19 @@ (:require [cemerick.friend :as friend] [clojars.db :refer [group-membernames]])) -(defmacro with-account [body] - `(friend/authenticated (try-account ~body))) +(defn try-account [f] + (f (:username (friend/current-authentication)))) -(defmacro try-account [body] - `(let [~'account (:username (friend/current-authentication))] - ~body)) +(defn with-account [f] + (friend/authenticated (try-account f))) (defn authorized? [db account group] (if account (let [names (group-membernames db group)] (or (some #{account} names) (empty? names))))) -(defmacro require-authorization [db group & body] - `(if (authorized? ~db ~'account ~group) - (do ~@body) - (friend/throw-unauthorized friend/*identity* - {:cemerick.friend/exprs (quote [~@body]) - :cemerick.friend/required-roles ~group}))) +(defn require-authorization [db account group f] + (if (authorized? db account group) + (f) + (friend/throw-unauthorized friend/*identity* + {:cemerick.friend/required-roles group}))) diff --git a/src/clojars/file_utils.clj b/src/clojars/file_utils.clj index 9f5fa89c..7056914a 100644 --- a/src/clojars/file_utils.clj +++ b/src/clojars/file_utils.clj @@ -7,7 +7,7 @@ [file type] (let [file' (io/file file)] (io/file (.getParentFile file') - (format "%s.%s" (.getName file') (name type))))) + (format "%s.%s" (.getName file') (name type))))) (defn- create-sum [f file type] (let [file' (io/file file)] @@ -35,17 +35,22 @@ (defn valid-sum? "Checks to see if a sum of type `type` exists and is valid for `file`" - [file type] - (let [sig-file (sum-file file type)] - (and (.exists sig-file) - (= ((sum-generators type) (io/file file)) - (slurp sig-file))))) + ([file type] + (valid-sum? file type true)) + ([file type fail-if-missing?] + (let [sig-file (sum-file file type)] + (if (.exists sig-file) + (= ((sum-generators type) (io/file file)) + (slurp sig-file)) + (not fail-if-missing?))))) (defn valid-sums? "Checks to see if both md5 and sha1 sums exist and are valid for `file`" - [file] - (reduce (fn [valid? sig-type] - (and valid? - (valid-sum? file sig-type))) - true - [:md5 :sha1])) \ No newline at end of file + ([file] + (valid-sums? file true)) + ([file fail-if-missing?] + (reduce (fn [valid? sig-type] + (and valid? + (valid-sum? file sig-type fail-if-missing?))) + true + [:md5 :sha1]))) diff --git a/src/clojars/maven.clj b/src/clojars/maven.clj index 2d4c27fd..c1632002 100644 --- a/src/clojars/maven.clj +++ b/src/clojars/maven.clj @@ -48,6 +48,7 @@ :licenses (mapv license-to-seq (.getLicenses model)) :scm (scm-to-map (.getScm model)) :authors (mapv #(.getName %) (.getContributors model)) + :packaging (keyword (.getPackaging model)) :dependencies (mapv (fn [d] {:group_name (.getGroupId d) :jar_name (.getArtifactId d) diff --git a/src/clojars/routes/artifact.clj b/src/clojars/routes/artifact.clj index 7d9cf8e6..10f15dd9 100644 --- a/src/clojars/routes/artifact.clj +++ b/src/clojars/routes/artifact.clj @@ -10,31 +10,26 @@ (defn show [db reporter stats group-id artifact-id] (if-let [artifact (db/find-jar db group-id artifact-id)] (auth/try-account - (view/show-jar db - reporter - stats - account - artifact - (db/recent-versions db group-id artifact-id 5) - (db/count-versions db group-id artifact-id))))) + #(view/show-jar db + reporter + stats + % + artifact + (db/recent-versions db group-id artifact-id 5) + (db/count-versions db group-id artifact-id))))) (defn list-versions [db group-id artifact-id] (if-let [artifact (db/find-jar db group-id artifact-id)] (auth/try-account - (view/show-versions account - artifact - (db/recent-versions db group-id artifact-id))))) + #(view/show-versions % artifact + (db/recent-versions db group-id artifact-id))))) (defn show-version [db reporter stats group-id artifact-id version] (if-let [artifact (db/find-jar db group-id artifact-id version)] (auth/try-account - (view/show-jar db - reporter - stats - account - artifact - (db/recent-versions db group-id artifact-id 5) - (db/count-versions db group-id artifact-id))))) + #(view/show-jar db reporter stats % artifact + (db/recent-versions db group-id artifact-id 5) + (db/count-versions db group-id artifact-id))))) (defn response-based-on-format "render appropriate response based on the file type suffix provided: diff --git a/src/clojars/routes/group.clj b/src/clojars/routes/group.clj index 7d41b83d..98b41d2f 100644 --- a/src/clojars/routes/group.clj +++ b/src/clojars/routes/group.clj @@ -10,22 +10,24 @@ (GET ["/groups/:groupname", :groupname #"[^/]+"] [groupname] (if-let [membernames (seq (db/group-membernames db groupname))] (auth/try-account - (view/show-group db account groupname membernames)))) + #(view/show-group db % groupname membernames)))) (POST ["/groups/:groupname", :groupname #"[^/]+"] [groupname username] (if-let [membernames (seq (db/group-membernames db groupname))] (auth/try-account - (auth/require-authorization - db - groupname - (cond - (some #{username} membernames) - (view/show-group db account groupname membernames - "They're already a member!") - (db/find-user db username) - (do (db/add-member db groupname username account) - (view/show-group db account groupname - (conj membernames username))) - :else - (view/show-group db account groupname membernames - (str "No such user: " - username))))))))) + (fn [account] + (auth/require-authorization + db + account + groupname + #(cond + (some #{username} membernames) + (view/show-group db account groupname membernames + "They're already a member!") + (db/find-user db username) + (do (db/add-member db groupname username account) + (view/show-group db account groupname + (conj membernames username))) + :else + (view/show-group db account groupname membernames + (str "No such user: " + username)))))))))) diff --git a/src/clojars/routes/repo.clj b/src/clojars/routes/repo.clj index c4524ece..9337d099 100644 --- a/src/clojars/routes/repo.clj +++ b/src/clojars/routes/repo.clj @@ -4,18 +4,21 @@ [config :refer [config]] [db :as db] [errors :refer [report-error]] + [file-utils :as fu] [maven :as maven] [search :as search]] [clojure.java.io :as io] [clojure.string :as string] + [cemerick.pomegranate.aether :as aether] [compojure [core :as compojure :refer [PUT defroutes]] [route :refer [not-found]]] [ring.util [codec :as codec] - [response :as response]] - [clojars.maven :as mvn]) - (:import java.io.StringReader)) + [response :as response]]) + (:import java.io.StringReader + java.util.UUID + org.apache.commons.io.FileUtils)) (defn versions [group-id artifact-id] (->> (.listFiles (io/file (config :repo) group-id artifact-id)) @@ -46,59 +49,131 @@ (.delete sent-file) (throw e)))) -(defn- pom? [filename] - (.endsWith filename ".pom")) - -(defn- get-pom-info [contents info] - (-> contents - StringReader. - maven/pom-to-map - (merge info))) - -(defn- body-and-add-pom [db body filename info account] - (if (pom? filename) - (let [contents (slurp body)] - (db/add-jar db account (get-pom-info contents info)) - contents) - body)) - -(defmacro put-req [db groupname & body] - `(with-account - (require-authorization - ~db - ~groupname - ~@body - ;; should we only do 201 if the file didn't already exist? - {:status 201 :headers {} :body nil}))) +(defn- pom? [file] + (let [filename (if (string? file) file (.getName file))] + (.endsWith filename ".pom"))) + +(defn find-upload-dir [{:keys [upload-dir]}] + (let [dir (io/file upload-dir)] + (if (and dir (.exists dir)) + dir + (let [dir' (io/file (FileUtils/getTempDirectory) + (str "upload-" (UUID/randomUUID)))] + (FileUtils/forceMkdir dir') + dir')))) + +(defn upload-request [db groupname session f] + (with-account + (fn [account] + (let [upload-dir (find-upload-dir session)] + (require-authorization db account groupname (partial f account upload-dir)) + ;; should we only do 201 if the file didn't already exist? + {:status 201 + :headers {} + :session (assoc session :upload-dir (.getAbsolutePath upload-dir)) + :body nil})))) + +(defn find-pom [dir] + (->> dir + file-seq + (filter pom?) + first)) + +;; borrowed from +;; https://github.com/technomancy/leiningen/tree/2.5.3/src/leiningen/deploy.clj#L137 +;; and modified +(defn- extension [f] + (let [name (.getName f)] + (if-let [[_ signed-extension] (re-find #"\.([a-z]+\.asc)$" name)] + signed-extension + (last (.split name "\\."))))) + +(defn- match-file-name [re f] + (re-find re (.getName f))) + +(defn find-artifacts [dir] + (into [] + (comp + (filter (memfn isFile)) + (remove (partial match-file-name #".sha1$")) + (remove (partial match-file-name #".md5$")) + (remove (partial match-file-name #"^maven-metadata\.xml")) + (remove (partial match-file-name #"^metadata\.edn$"))) + (file-seq dir))) + +(defn- throw-invalid + ([message] + (throw-invalid message nil)) + ([message meta] + (throw-invalid message meta nil)) + ([message meta cause] + (throw + (ex-info message (merge {:report? false} meta) cause)))) (defn- validate-regex [x re message] (when-not (re-matches re x) - (throw (ex-info message {:value x - :regex re})))) + (throw-invalid message {:value x + :regex re}))) + +(defn- validate-pom-entry [pom-data key value] + (when-not (= (key pom-data) value) + (throw-invalid + (format "the %s in the pom (%s) does not match the %s you are deploying to (%s)" + (name key) (key pom-data) (name key) value) + {:pom pom-data}))) + +(defn- validate-pom [pom group name version] + (validate-pom-entry pom :group group) + (validate-pom-entry pom :name name) + (validate-pom-entry pom :version version)) (defn snapshot-version? [version] (.endsWith version "-SNAPSHOT")) -(defn assert-non-redeploy [group-id artifact-id version filename] +(defn assert-non-redeploy [group-id artifact-id version] (when (and (not (snapshot-version? version)) (.exists (io/file (config :repo) (string/replace group-id "." "/") - artifact-id version filename))) - (throw (ex-info "redeploying non-snapshots is not allowed (see http://git.io/vO2Tg)" - {:report? false})))) + artifact-id version))) + (throw-invalid "redeploying non-snapshots is not allowed (see http://git.io/vO2Tg)"))) -(defn validate-deploy [group-id artifact-id version filename] - (try +(defn assert-jar-uploaded [artifacts pom] + (when (and (= :jar (:packaging pom)) + (not (some (partial match-file-name #"\.jar$") artifacts))) + (throw-invalid "no jar file was uploaded"))) + +(defn validate-checksums [artifacts] + (doseq [f artifacts] + ;; verify that at least one type checksum file exists + (when (not (or (.exists (fu/sum-file f :md5)) + (.exists (fu/sum-file f :sha1)))) + (throw-invalid (str "no checksum provided for " (.getName f) {:file f}))) + ;; verify provided checksums are valid + (when (not (fu/valid-sums? f false)) + (throw-invalid (str "invalid checksum for " (.getName f) {:file f}))))) + +(defn assert-signatures [artifacts] + ;; if any signatures exist, require them for every artifact + (let [asc-matcher (partial match-file-name #"\.asc$")] + (when (some asc-matcher artifacts) + (doseq [f artifacts + :when (not (asc-matcher f)) + :when (not (.exists (io/file (str (.getAbsolutePath f) ".asc"))))] + (throw-invalid (format "%s has no signature" (.getName f)) {:file f}))))) + +(defn validate-gav [group name version] ;; We're on purpose *at least* as restrictive as the recommendations on ;; https://maven.apache.org/guides/mini/guide-naming-conventions.html ;; If you want loosen these please include in your proposal the ;; ramifications on usability, security and compatiblity with filesystems, ;; OSes, URLs and tools. - (validate-regex artifact-id #"^[a-z0-9_.-]+$" + (validate-regex name #"^[a-z0-9_.-]+$" (str "project names must consist solely of lowercase " "letters, numbers, hyphens and underscores (see http://git.io/vO2Uy)")) - (validate-regex group-id #"^[a-z0-9_.-]+$" + + (validate-regex group #"^[a-z0-9_.-]+$" (str "group names must consist solely of lowercase " "letters, numbers, hyphens and underscores (see http://git.io/vO2Uy)")) + ;; Maven's pretty accepting of version numbers, but so far in 2.5 years ;; bar one broken non-ascii exception only these characters have been used. ;; Even if we manage to support obscure characters some filesystems do not @@ -106,42 +181,74 @@ ;; compatible for everyone let's lock it down. (validate-regex version #"^[a-zA-Z0-9_.+-]+$" (str "version strings must consist solely of letters, " - "numbers, dots, pluses, hyphens and underscores (see http://git.io/vO2TO)")) - (assert-non-redeploy group-id artifact-id version filename) + "numbers, dots, pluses, hyphens and underscores (see http://git.io/vO2TO)"))) + +(defn validate-deploy [dir pom {:keys [group name version]}] + (try + (validate-gav group name version) + (validate-pom pom group name version) + (assert-non-redeploy group name version) + + (let [artifacts (find-artifacts dir)] + (assert-jar-uploaded artifacts pom) + (validate-checksums artifacts) + (assert-signatures artifacts)) + (catch Exception e (throw (ex-info (.getMessage e) (merge {:status 403 :status-message (str "Forbidden - " (.getMessage e)) - :group-id group-id - :artifact-id artifact-id - :version version - :file filename} - (ex-data e))))))) + :group group + :name name + :version version} + (ex-data e)) + (.getCause e)))))) + +(defn finalize-deploy [db search account repo dir] + (try + (if-let [pom-file (find-pom dir)] + (let [pom (try + (maven/pom-to-map pom-file) + (catch Exception e + (throw-invalid (str "invalid pom file: " (.getMessage e)) + {:file pom-file} + e))) + {:keys [group name version] :as posted-metadata} (read-string (slurp (io/file dir "metadata.edn")))] + (validate-deploy dir pom posted-metadata) + (db/check-and-add-group db account group) + (aether/deploy + :coordinates [(symbol group name) version] + :artifact-map (reduce #(assoc %1 + [:extension (extension %2)] %2) + {} (find-artifacts dir)) + :repository {"local" {:url (-> repo io/file .toURI .toURL)}}) + (db/add-jar db account pom) + (search/index! search (assoc pom + :at (.lastModified pom-file)))) + (throw-invalid "no pom file was uploaded")) + (finally + (FileUtils/deleteQuietly dir)))) -(defn- handle-versioned-upload [db search body group artifact version filename] +(defn- handle-versioned-upload [db body session group artifact version filename] (let [groupname (string/replace group "/" ".")] - (put-req + (upload-request db groupname - (let [file (io/file (config :repo) group artifact version filename) - info {:group groupname - :name artifact - :version version}] - (validate-deploy groupname artifact version filename) - (db/check-and-add-group db account groupname) - - (try-save-to-file file (body-and-add-pom db body filename info account)) - (when (pom? filename) - (search/index! search (assoc (mvn/pom-to-map file) - :at (.lastModified file)))))))) + session + (fn [_ upload-dir] + (spit (io/file upload-dir "metadata.edn") + (pr-str {:group groupname + :name artifact + :version version})) + (try-save-to-file (io/file upload-dir group artifact version filename) body))))) ;; web handlers (defn routes [db search] (compojure/routes (PUT ["/:group/:artifact/:file" :group #".+" :artifact #"[^/]+" :file #"maven-metadata\.xml[^/]*"] - {body :body {:keys [group artifact file]} :params} + {body :body session :session {:keys [group artifact file]} :params} (if (snapshot-version? artifact) ;; SNAPSHOT metadata will hit this route, but should be ;; treated as a versioned file upload. @@ -150,19 +257,24 @@ group-parts (string/split group #"/") group (string/join "/" (butlast group-parts)) artifact (last group-parts)] - (handle-versioned-upload db search body group artifact version file)) - (let [groupname (string/replace group "/" ".")] - (put-req - db - groupname - (let [file (io/file (config :repo) group artifact file)] - (db/check-and-add-group db account groupname) - (try-save-to-file file body)))))) + (handle-versioned-upload db body session group artifact version file)) + (when (re-find #"maven-metadata\.xml$" file) + ;; ignore metadata sums, since we'll recreate those when + ;; the deploy is finalizied + (let [groupname (string/replace group "/" ".")] + (upload-request + db + groupname + session + (fn [account upload-dir] + (let [file (io/file upload-dir group artifact file)] + (try-save-to-file file body) + (finalize-deploy db search account (config :repo) upload-dir)))))))) (PUT ["/:group/:artifact/:version/:filename" :group #"[^\.]+" :artifact #"[^/]+" :version #"[^/]+" :filename #"[^/]+(\.pom|\.jar|\.sha1|\.md5|\.asc)$"] - {body :body {:keys [group artifact version filename]} :params} - (handle-versioned-upload db search body group artifact version filename)) + {body :body session :session {:keys [group artifact version filename]} :params} + (handle-versioned-upload db body session group artifact version filename)) (PUT "*" _ {:status 400 :headers {}}) (not-found "Page not found"))) diff --git a/src/clojars/routes/user.clj b/src/clojars/routes/user.clj index dc86ddc9..269717f1 100644 --- a/src/clojars/routes/user.clj +++ b/src/clojars/routes/user.clj @@ -7,16 +7,16 @@ (defn show [db username] (if-let [user (db/find-user db username)] (auth/try-account - (view/show-user db account user)))) + #(view/show-user db % user)))) (defn routes [db mailer] (compojure/routes (GET "/profile" {:keys [flash]} (auth/with-account - (view/profile-form account (db/find-user db account) flash))) + #(view/profile-form % (db/find-user db %) flash))) (POST "/profile" {:keys [params]} (auth/with-account - (view/update-profile db account params))) + #(view/update-profile db % params))) (GET "/register" {:keys [params]} (view/register-form params)) diff --git a/src/clojars/web.clj b/src/clojars/web.clj index bf617b5f..e4c24d27 100644 --- a/src/clojars/web.clj +++ b/src/clojars/web.clj @@ -42,23 +42,23 @@ (defn main-routes [db reporter stats search-obj mailer] (routes (GET "/" _ - (try-account - (if account - (dashboard db account) - (index-page db stats account)))) + (try-account + #(if % + (dashboard db %) + (index-page db stats %)))) (GET "/search" {:keys [params]} (try-account - (let [validated-params (if (:page params) - (assoc params :page (Integer. (:page params))) - params)] - (search search-obj account validated-params)))) + #(let [validated-params (if (:page params) + (assoc params :page (Integer. (:page params))) + params)] + (search search-obj % validated-params)))) (GET "/projects" {:keys [params]} (try-account - (browse db account params))) + #(browse db % params))) (GET "/security" [] (try-account - (html-doc "Security" {:account account} - (raw (slurp (io/resource "security.html")))))) + #(html-doc "Security" {:account %} + (raw (slurp (io/resource "security.html")))))) session/routes (group/routes db) (artifact/routes db reporter stats) @@ -70,11 +70,11 @@ (PUT "*" _ {:status 405 :headers {} :body "Did you mean to use /repo?"}) (ANY "*" _ (try-account - (not-found - (html-doc "Page not found" {:account account} - [:div.small-section - [:h1 "Page not found"] - [:p "Thundering typhoons! I think we lost it. Sorry!"]])))))) + #(not-found + (html-doc "Page not found" {:account %} + [:div.small-section + [:h1 "Page not found"] + [:p "Thundering typhoons! I think we lost it. Sorry!"]])))))) (defn bad-attempt [attempts user] (let [failures (or (attempts user) 0)] @@ -101,17 +101,18 @@ (defn clojars-app [db reporter stats search mailer] (routes - (context "/repo" _ - (-> (repo/routes db search) - (friend/authenticate - {:credential-fn (credential-fn db) - :workflows [(workflows/http-basic :realm "clojars")] - :allow-anon? false - :unauthenticated-handler - (partial workflows/http-basic-deny "clojars")}) - (repo/wrap-exceptions reporter) - (repo/wrap-file (:repo config)) - (repo/wrap-reject-double-dot))) + (-> (context "/repo" _ + (-> (repo/routes db search) + (friend/authenticate + {:credential-fn (credential-fn db) + :workflows [(workflows/http-basic :realm "clojars")] + :allow-anon? false + :unauthenticated-handler + (partial workflows/http-basic-deny "clojars")}) + (repo/wrap-exceptions reporter) + (repo/wrap-file (:repo config)) + (repo/wrap-reject-double-dot))) + (wrap-secure-session)) (-> (main-routes db reporter stats search mailer) (friend/authenticate {:credential-fn (credential-fn db) diff --git a/test/clojars/test/integration/uploads.clj b/test/clojars/test/integration/uploads.clj index e4fea0b3..05493236 100644 --- a/test/clojars/test/integration/uploads.clj +++ b/test/clojars/test/integration/uploads.clj @@ -23,9 +23,10 @@ (help/delete-file-recursively help/local-repo) (help/delete-file-recursively help/local-repo2) (aether/deploy - :coordinates '[org.clojars.dantheman/test "1.0.0"] + :coordinates '[org.clojars.dantheman/test "0.0.1"] :jar-file (io/file (io/resource "test.jar")) - :pom-file (io/file (io/resource "test-0.0.1/test.pom")) + :pom-file (help/rewrite-pom (io/file (io/resource "test-0.0.1/test.pom")) + {:groupId "org.clojars.dantheman"}) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") :username "dantheman" :password "password"}} @@ -36,10 +37,10 @@ "clojars" "dantheman" "test" - "1.0.0"))))) - (is (= '{[org.clojars.dantheman/test "1.0.0"] nil} + "0.0.1"))))) + (is (= '{[org.clojars.dantheman/test "0.0.1"] nil} (aether/resolve-dependencies - :coordinates '[[org.clojars.dantheman/test "1.0.0"]] + :coordinates '[[org.clojars.dantheman/test "0.0.1"]] :repositories {"test" {:url (str "http://localhost:" help/test-port "/repo")}} :local-repo help/local-repo2))) @@ -59,10 +60,8 @@ (visit "/") (fill-in [:#search] "test") (press [:#search-button]) - ;; the pom is used as is, even if the data is wrong - ;; https://github.com/clojars/clojars-web/issues/358 (within [:div.result] - (has (text? "fake/test 0.0.1"))))) + (has (text? "org.clojars.dantheman/test 0.0.1"))))) (deftest user-can-deploy-to-new-group (-> (session (help/app-from-system)) @@ -116,7 +115,7 @@ (-> (session (help/app-from-system)) (register-as "dantheman" "test@example.org" "password")) (aether/deploy - :coordinates '[org.clojars.dantheman/test "0.0.1"] + :coordinates '[fake/test "0.0.1"] :jar-file (io/file (io/resource "test.jar")) :pom-file (io/file (io/resource "test-0.0.1/test.pom")) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") @@ -127,7 +126,7 @@ org.sonatype.aether.deployment.DeploymentException #"Forbidden - redeploying non-snapshots" (aether/deploy - :coordinates '[org.clojars.dantheman/test "0.0.1"] + :coordinates '[fake/test "0.0.1"] :jar-file (io/file (io/resource "test.jar")) :pom-file (io/file (io/resource "test-0.0.1/test.pom")) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") @@ -139,7 +138,7 @@ (-> (session (help/app-from-system)) (register-as "dantheman" "test@example.org" "password")) (aether/deploy - :coordinates '[org.clojars.dantheman/test "0.0.3-SNAPSHOT"] + :coordinates '[fake/test "0.0.3-SNAPSHOT"] :jar-file (io/file (io/resource "test.jar")) :pom-file (io/file (io/resource "test-0.0.3-SNAPSHOT/test.pom")) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") @@ -147,7 +146,7 @@ :password "password"}} :local-repo help/local-repo) (aether/deploy - :coordinates '[org.clojars.dantheman/test "0.0.3-SNAPSHOT"] + :coordinates '[fake/test "0.0.3-SNAPSHOT"] :jar-file (io/file (io/resource "test.jar")) :pom-file (io/file (io/resource "test-0.0.3-SNAPSHOT/test.pom")) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") @@ -161,12 +160,47 @@ (aether/deploy :coordinates '[org.clojars.dantheman/test.thing "0.0.3-SNAPSHOT"] :jar-file (io/file (io/resource "test.jar")) - :pom-file (io/file (io/resource "test-0.0.3-SNAPSHOT/test.pom")) + :pom-file (help/rewrite-pom (io/file (io/resource "test-0.0.3-SNAPSHOT/test.pom")) + {:groupId "org.clojars.dantheman" + :artifactId "test.thing"}) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") :username "dantheman" :password "password"}} :local-repo help/local-repo)) +(deftest user-can-deploy-with-signatures + (-> (session (help/app-from-system)) + (register-as "dantheman" "test@example.org" "password")) + (let [pom (io/file (io/resource "test-0.0.1/test.pom"))] + (aether/deploy + :coordinates '[fake/test "0.0.1"] + :artifact-map {[:extension "jar"] (io/file (io/resource "test.jar")) + [:extension "pom"] pom + ;; any content will do since we don't validate signatures + [:extension "jar.asc"] pom + [:extension "pom.asc"] pom} + :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") + :username "dantheman" + :password "password"}} + :local-repo help/local-repo))) + +(deftest missing-signature-fails-the-deploy + (-> (session (help/app-from-system)) + (register-as "dantheman" "test@example.org" "password")) + (let [pom (io/file (io/resource "test-0.0.1/test.pom"))] + (is (thrown-with-msg? org.sonatype.aether.deployment.DeploymentException + #"test-0.0.1.pom has no signature" + (aether/deploy + :coordinates '[fake/test "0.0.1"] + :artifact-map {[:extension "jar"] (io/file (io/resource "test.jar")) + [:extension "pom"] pom + ;; any content will do since we don't validate signatures + [:extension "jar.asc"] pom} + :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") + :username "dantheman" + :password "password"}} + :local-repo help/local-repo))))) + (deftest anonymous-cannot-deploy (is (thrown-with-msg? org.sonatype.aether.deployment.DeploymentException #"Unauthorized" @@ -188,13 +222,33 @@ :password "password"}} :local-repo help/local-repo)))) -(deftest deploy-requires-lowercase-group +(deftest deploy-requires-path-to-match-pom (-> (session (help/app-from-system)) - (register-as "dantheman" "test@example.org" "password")) + (register-as "dantheman" "test@example.org" "password")) (is (thrown-with-msg? org.sonatype.aether.deployment.DeploymentException - #"Forbidden - group names must consist solely of lowercase" + #"Forbidden - the group in the pom \(fake\) does not match the group you are deploying to \(flake\)" + (aether/deploy + :coordinates '[flake/test "0.0.1"] + :jar-file (io/file (io/resource "test.jar")) + :pom-file (io/file (io/resource "test-0.0.1/test.pom")) + :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") + :username "dantheman" + :password "password"}} + :local-repo help/local-repo))) + (is (thrown-with-msg? org.sonatype.aether.deployment.DeploymentException + #"Forbidden - the name in the pom \(test\) does not match the name you are deploying to \(toast\)" (aether/deploy - :coordinates '[faKE/test "1.0.0"] + :coordinates '[fake/toast "0.0.1"] + :jar-file (io/file (io/resource "test.jar")) + :pom-file (io/file (io/resource "test-0.0.1/test.pom")) + :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") + :username "dantheman" + :password "password"}} + :local-repo help/local-repo))) + (is (thrown-with-msg? org.sonatype.aether.deployment.DeploymentException + #"Forbidden - the version in the pom \(0.0.1\) does not match the version you are deploying to \(1.0.0\)" + (aether/deploy + :coordinates '[fake/test "1.0.0"] :jar-file (io/file (io/resource "test.jar")) :pom-file (io/file (io/resource "test-0.0.1/test.pom")) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") @@ -202,19 +256,35 @@ :password "password"}} :local-repo help/local-repo)))) -(deftest deploy-requires-lowercase-project +(deftest deploy-requires-lowercase-group (-> (session (help/app-from-system)) (register-as "dantheman" "test@example.org" "password")) (is (thrown-with-msg? org.sonatype.aether.deployment.DeploymentException - #"Forbidden - project names must consist solely of lowercase" + #"Forbidden - group names must consist solely of lowercase" (aether/deploy - :coordinates '[fake/teST "1.0.0"] + :coordinates '[faKE/test "0.0.1"] :jar-file (io/file (io/resource "test.jar")) - :pom-file (io/file (io/resource "test-0.0.1/test.pom")) + :pom-file (help/rewrite-pom (io/file (io/resource "test-0.0.1/test.pom")) + {:groupId "faKE"}) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") :username "dantheman" :password "password"}} - :local-repo help/local-repo)))) + :local-repo help/local-repo))) + + (deftest deploy-requires-lowercase-project + (-> (session (help/app-from-system)) + (register-as "dantheman" "test@example.org" "password")) + (is (thrown-with-msg? org.sonatype.aether.deployment.DeploymentException + #"Forbidden - project names must consist solely of lowercase" + (aether/deploy + :coordinates '[fake/teST "0.0.1"] + :jar-file (io/file (io/resource "test.jar")) + :pom-file (help/rewrite-pom (io/file (io/resource "test-0.0.1/test.pom")) + {:artifactId "teST"}) + :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") + :username "dantheman" + :password "password"}} + :local-repo help/local-repo))))) (deftest deploy-requires-ascii-version (-> (session (help/app-from-system)) @@ -224,7 +294,8 @@ (aether/deploy :coordinates '[fake/test "1.α.0"] :jar-file (io/file (io/resource "test.jar")) - :pom-file (io/file (io/resource "test-0.0.1/test.pom")) + :pom-file (help/rewrite-pom (io/file (io/resource "test-0.0.1/test.pom")) + {:version "1.α.0"}) :repository {"test" {:url (str "http://localhost:" help/test-port "/repo") :username "dantheman" :password "password"}} diff --git a/test/clojars/test/test_helper.clj b/test/clojars/test/test_helper.clj index 369cdb9d..e4e34d53 100644 --- a/test/clojars/test/test_helper.clj +++ b/test/clojars/test/test_helper.clj @@ -12,7 +12,7 @@ [clojure.java [io :as io] [jdbc :as jdbc]] - [clojure.string :as string] + [clojure.string :as str] [clucy.core :as clucy] [com.stuartsierra.component :as component]) (:import java.io.File)) @@ -107,9 +107,22 @@ (component/stop system)))))))) (defn get-content-type [resp] - (some-> resp :headers (get "content-type") (string/split #";") first)) + (some-> resp :headers (get "content-type") (str/split #";") first)) (defn assert-cors-header [resp] (some-> resp :headers (get "access-control-allow-origin") (= "*"))) + +(defn rewrite-pom [file m] + (let [new-pom (doto (File/createTempFile (.getName file) ".pom") + .deleteOnExit)] + (-> file + slurp + (as-> % (reduce (fn [accum [element new-value]] + (str/replace accum (re-pattern (format "<(%s)>.*?<" (name element))) + (format "<$1>%s<" new-value))) + % + m)) + (->> (spit new-pom))) + new-pom))