diff --git a/.gitignore b/.gitignore index 54fa331..7dd6311 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ /target/* .clj-kondo/babashka .clj-kondo/metosin +.dir-locals.el diff --git a/deps.edn b/deps.edn index ca3b134..27fb7a1 100644 --- a/deps.edn +++ b/deps.edn @@ -11,7 +11,7 @@ metosin/spec-tools {:mvn/version "0.10.5"} ring-cors/ring-cors {:mvn/version "0.1.13"} com.fluree/db {:git/url "https://github.com/fluree/db.git" - :git/sha "269c71e1174be615265abb8e25c52da549f13175"}} + :git/sha "f394d7867dee564b66943c17f3227d79787eb49c"}} :aliases {:dev diff --git a/src/fluree/http_api/components/http.clj b/src/fluree/http_api/components/http.clj index f92cb76..c89ddde 100644 --- a/src/fluree/http_api/components/http.clj +++ b/src/fluree/http_api/components/http.clj @@ -32,6 +32,7 @@ (s/def ::ledger ::non-empty-string) (s/def ::txn (s/or :single-map map? :collection-of-maps (s/coll-of map?))) (s/def ::defaultContext any?) +(s/def ::opts map?) (def server #::ds{:start (fn [{{:keys [handler options]} ::ds/config}] @@ -202,7 +203,8 @@ :handler ledger/create}}] ["/transact" {:post {:summary "Endpoint for submitting transactions" - :parameters {:body (s/keys :req-un [::ledger ::txn])} + :parameters {:body (s/keys :req-un [::ledger ::txn] + :opt-un [::opts])} :responses {200 {:body (s/keys :opt-un [::address ::id] :req-un [::alias ::t])} 400 {:body string?} diff --git a/src/fluree/http_api/handlers/ledger.clj b/src/fluree/http_api/handlers/ledger.clj index ffb99ed..d7ac57a 100644 --- a/src/fluree/http_api/handlers/ledger.clj +++ b/src/fluree/http_api/handlers/ledger.clj @@ -12,6 +12,12 @@ (assoc m* (keyword k) v)) {} m)) +(defn transform-policy-opts + [opts] + (or (not-empty (select-keys opts [:role :did])) + (let [{:strs [role did] :as policy-opts} opts] + (keywordize-keys policy-opts)))) + (defn deref! "Derefs promise p and throws if the result is an exception, returns it otherwise." [p] @@ -41,20 +47,23 @@ :body {:error (ex-message t)}}})))))) (defn txn-body->opts - [{:keys [defaultContext txn] :as _body}] + [{:keys [defaultContext txn opts] :as _body}] (let [first-txn (if (map? txn) txn - (first txn))] - (cond-> {} + (first txn)) + policy-opts (transform-policy-opts opts)] + (cond-> policy-opts (-> first-txn keys first keyword?) (assoc :context-type :keyword) (-> first-txn keys first string?) (assoc :context-type :string) defaultContext (assoc :defaultContext defaultContext)))) (defn query-body->opts [{:keys [query] :as _body}] - (cond-> {} - (-> query keys first keyword?) (assoc :context-type :keyword) - (-> query keys first string?) (assoc :context-type :string))) + (let [policy-opts (transform-policy-opts (or (get query "opts") + (get query :opts)))] + (cond-> policy-opts + (-> query keys first keyword?) (assoc :context-type :keyword) + (-> query keys first string?) (assoc :context-type :string)))) (defn ledger-summary [db] diff --git a/test/fluree/http_api/system_test.clj b/test/fluree/http_api/system_test.clj index b921003..0b6ab1a 100644 --- a/test/fluree/http_api/system_test.clj +++ b/test/fluree/http_api/system_test.clj @@ -260,3 +260,213 @@ :rdf/type [:schema/Test]}] :f/retract []}}}] (-> query-res :body edn/read-string)))))) + + +(deftest ^:integration ^:json policy-opts-test + (testing "policy-enforcing opts are correctly handled" + (let [ledger-name (create-rand-ledger "policy-opts-test") + json-headers {"Content-Type" "application/json" + "Accept" "application/json"} + alice-did "did:fluree:Tf6i5oh2ssYNRpxxUM2zea1Yo7x4uRqyTeU" + txn-req {:body + (json/write-value-as-string + {:ledger ledger-name + :txn [{"id" "ex:alice" + "type" "ex:User" + "ex:secret" "alice's secret"} + {"id" "ex:bob" + "type" "ex:User" + "ex:secret" "bob's secret"} + {"id" "ex:UserPolicy" + "type" ["f:Policy"] + "f:targetClass" {"id" "ex:User"} + "f:allow" + [{"id" "ex:globalViewAllow" + "f:targetRole" {"id" "ex:userRole"} + "f:action" [{"id" "f:view"}]}] + "f:property" + [{"f:path" {"id" "ex:secret"} + "f:allow" + [{"id" "ex:secretsRule" + "f:targetRole" {"id" "ex:userRole"} + "f:action" [{"id" "f:view"} {"id" "f:modify"}] + "f:equals" {"@list" [{"id" "f:$identity"}{"id" "ex:User"}]}}]}]} + {"id" alice-did + "ex:User" {"id" "ex:alice"} + "f:role" {"id" "ex:userRole"}}]}) + :headers json-headers} + txn-res (post :transact txn-req) + _ (assert (= 200 (:status txn-res))) + secret-query {"select" {"?s" ["*"]} + "where" [["?s" "rdf:type" "ex:User"]]} + + query-req {:body + (json/write-value-as-string + {:ledger ledger-name + :query (assoc secret-query + :opts {"role" "ex:userRole" + "did" alice-did})}) + :headers json-headers} + query-res (post :query query-req)] + (is (= 200 (:status query-res)) + (str "policy-enforced query response was: " (pr-str query-res))) + (is (= [{"id" "ex:bob", "rdf:type" ["ex:User"]} + {"id" "ex:alice", + "rdf:type" ["ex:User"], + "ex:secret" "alice's secret"}] + (-> query-res :body json/read-value)) + "query policy opts should prevent seeing bob's secret") + (let [txn-req {:body + (json/write-value-as-string + {:ledger ledger-name + :txn [{"id" "ex:alice" + "ex:secret" "alice's NEW secret"}] + :opts {"role" "ex:userRole" + "did" alice-did}}) + :headers json-headers} + txn-res (post :transact txn-req) + _ (assert (= 200 (:status txn-res))) + query-req {:body + (json/write-value-as-string + {:ledger ledger-name + :query secret-query}) + :headers json-headers} + query-res (post :query query-req) + _ (assert (= 200 (:status query-res)))] + (is (= [{"id" "ex:bob", + "rdf:type" ["ex:User"], + "ex:secret" "bob's secret"} + {"id" "ex:alice", + "rdf:type" ["ex:User"], + "ex:secret" "alice's NEW secret"}] + (-> query-res :body json/read-value)) + "alice's secret should be modified") + (let [txn-req {:body + (json/write-value-as-string + {:ledger ledger-name + :txn [{"id" "ex:bob" + "ex:secret" "bob's new secret"}] + :opts {"role" "ex:userRole" + "did" alice-did}}) + :headers json-headers} + txn-res (post :transact txn-req)] + (is (not= 200 (:status txn-res)) + (str "transaction policy opts should have prevented modification, instead response was:" (pr-str txn-res))) + (let [query-req {:body + (json/write-value-as-string + {:ledger ledger-name + :query {:history "ex:bob" + :t {:from 1} + :opts {"role" "ex:userRole" + "did" alice-did}}}) + :headers json-headers} + query-res (post :history query-req)] + (is (= 200 (:status query-res)) + (str "History query response was: " (pr-str query-res))) + (is (= [{"id" "ex:bob", "rdf:type" ["ex:User"]}] + (-> query-res :body json/read-value first (get "f:assert"))) + "policy opts should have prevented seeing bob's secret"))))))) + + +(deftest ^:integration ^:edn policy-opts-test + (testing "policy-enforcing opts are correctly handled" + (let [ledger-name (create-rand-ledger "policy-opts-test") + edn-headers {"Content-Type" "application/edn" + "Accept" "application/edn"} + alice-did "did:fluree:Tf6i5oh2ssYNRpxxUM2zea1Yo7x4uRqyTeU" + txn-req {:body + (pr-str + {:ledger ledger-name + :txn [{:id :ex/alice, + :type :ex/User, + :ex/secret "alice's secret"} + {:id :ex/bob, + :type :ex/User, + :ex/secret "bob's secret"} + {:id :ex/UserPolicy, + :type [:f/Policy], + :f/targetClass :ex/User + :f/allow [{:id :ex/globalViewAllow + :f/targetRole :ex/userRole + :f/action [:f/view]}] + :f/property [{:f/path :ex/secret + :f/allow [{:id :ex/secretsRule + :f/targetRole :ex/userRole + :f/action [:f/view :f/modify] + :f/equals {:list [:f/$identity :ex/User]}}]}]} + {:id alice-did + :ex/User :ex/alice + :f/role :ex/userRole}]}) + :headers edn-headers} + txn-res (post :transact txn-req) + _ (assert (= 200 (:status txn-res))) + secret-query '{:select {?s [:*]} + :where [[?s :rdf/type :ex/User]]} + + query-req {:body + (pr-str + {:ledger ledger-name + :query (assoc secret-query + :opts {:role :ex/userRole + :did alice-did})}) + :headers edn-headers} + query-res (post :query query-req)] + (is (= 200 (:status query-res)) + (str "policy-enforced query response was: " (pr-str query-res))) + (is (= [{:id :ex/bob + :rdf/type [:ex/User]} + {:id :ex/alice + :rdf/type [:ex/User] + :ex/secret "alice's secret"}] + (-> query-res :body edn/read-string)) + "query policy opts should prevent seeing bob's secret") + (let [txn-req {:body + (pr-str + {:ledger ledger-name + :txn [{:id :ex/alice + :ex/secret "alice's NEW secret"}] + :opts {:role :ex/userRole + :did alice-did}}) + :headers edn-headers} + txn-res (post :transact txn-req) + _ (assert (= 200 (:status txn-res))) + query-req {:body + (pr-str + {:ledger ledger-name + :query secret-query}) + :headers edn-headers} + query-res (post :query query-req) + _ (assert (= 200 (:status query-res)))] + (is (= [{:id :ex/bob + :rdf/type [:ex/User] + :ex/secret "bob's secret"} + {:id :ex/alice + :rdf/type [:ex/User] + :ex/secret "alice's NEW secret"}] + (-> query-res :body edn/read-string)) + "alice's secret should be modified") + (let [txn-req {:body + (pr-str + {:ledger ledger-name + :txn [{:id :ex/bob + :ex/secret "bob's NEW secret"}] + :opts {:role :ex/userRole + :did alice-did}}) + :headers edn-headers} + txn-res (post :transact txn-req)] + (is (not= 200 (:status txn-res)) + (str "transaction policy opts should have prevented modification, instead response was:" (pr-str txn-res))) + (let [query-req {:body + (pr-str + {:ledger ledger-name + :query {:history :ex/bob + :t {:from 1} + :opts {:role :ex/userRole + :did alice-did}}}) + :headers edn-headers} + query-res (post :history query-req)] + (is (= 200 (:status query-res)) + (str "History query response was: " (pr-str query-res))) + (is (= [{:id :ex/bob :rdf/type [:ex/User]}] + (-> query-res :body edn/read-string first (get :f/assert))) + "policy opts should have prevented seeing bob's secret")))))))