diff --git a/build.clj b/build.clj index 6e2ea92..64678b0 100644 --- a/build.clj +++ b/build.clj @@ -1,6 +1,5 @@ (ns build (:require - [babashka.fs :as fs] [clojure.tools.build.api :as b] [deps-deploy.deps-deploy :as dd])) diff --git a/demo/resources/demoservices.edn b/demo/resources/demoservices.edn index 4c1d4cd..67b6189 100644 --- a/demo/resources/demoservices.edn +++ b/demo/resources/demoservices.edn @@ -7,14 +7,28 @@ :timbre {:start (modular.log/timbre-config! (:timbre/clj (deref (clip/ref :config))))} - + :webly {:start (webly.app.app/start-webly - (deref (clip/ref :config)) + (deref (clip/ref :config)) (:profile #ref [:modular])) - :stop (webly.app.app/stop-webly this)} - - :clj-service {:start (goldly.service/start-clj-services - (clip/ref :exts))} + :stop (webly.app.app/stop-webly this)} + + :permission {:start (modular.permission.core/start-permissions + {:demo {:roles #{:logistic} + :password "a231498f6c1f441aa98482ea0b224ffa" ; "1234" + :email ["john@doe.com"]} + :boss {:roles #{:logistic :supervisor :accounting} + :password "a231498f6c1f441aa98482ea0b224ffa" ; "1234" + :email ["boss@doe.com"]} + :florian {:roles #{:logistic} + :password "a231498f6c1f441aa98482ea0b224ffa" ; 1234 + :email ["hoertlehner@gmail.com"]} + :john {:roles #{:logistic} + :password "a231498f6c1f441aa98482ea0b224ffa" ; "1234" + :email ["john@doe.com"]}})} + + + :clj-service {:start (clj-service.core/start-clj-services (clip/ref :permission) (clip/ref :exts))} ; }} diff --git a/demo/resources/ext/demo.edn b/demo/resources/ext/demo.edn index 9ee0ff8..524e5db 100644 --- a/demo/resources/ext/demo.edn +++ b/demo/resources/ext/demo.edn @@ -4,12 +4,11 @@ :cljs-namespace [demo.page] :cljs-ns-bindings {'demo.page {'service-page demo.page/service-page}} ;runtime + :cljs-routes {"" demo.page/service-page} :clj-services {:name "demo" :permission #{} :symbols [demo.service/add demo.service/quote demo.service/quote-slow - demo.service/ex]} - - :cljs-routes {"" demo.page/service-page}} + demo.service/ex]}} diff --git a/demo/src/demo/page.cljs b/demo/src/demo/page.cljs index 920c2ef..75023c5 100644 --- a/demo/src/demo/page.cljs +++ b/demo/src/demo/page.cljs @@ -2,7 +2,7 @@ (:require [reagent.core :as r] [promesa.core :as p] - [goldly.service.core :refer [clj clj-atom run-a run-a-map]])) + [goldly.service.core :refer [clj]])) (def state (r/atom {})) diff --git a/demo/src/demo/service.clj b/demo/src/demo/service.clj index c0bf746..01c6b08 100644 --- a/demo/src/demo/service.clj +++ b/demo/src/demo/service.clj @@ -1,8 +1,5 @@ -(ns demo.service - (:require - [taoensso.timbre :as timbre :refer [debug]])) +(ns demo.service) -(debug "namespace demo.service is getting loaded...") (defn add [a b] (+ a b)) diff --git a/deps.edn b/deps.edn index 83d1cec..f14b9b1 100644 --- a/deps.edn +++ b/deps.edn @@ -2,14 +2,11 @@ "resources"] :deps {org.clojure/clojure {:mvn/version "1.11.1"} - org.clojure/core.async {:mvn/version "1.6.673"} - org.clojure/data.json {:mvn/version "2.4.0"} - com.rpl/specter {:mvn/version "1.1.4"} - org.pinkgorilla/websocket {:mvn/version "0.0.7"} - org.pinkgorilla/permission {:mvn/version "0.0.15"} - org.pinkgorilla/ui-dialog-keybindings {:mvn/version "0.1.6"} + ;org.clojure/data.json {:mvn/version "2.4.0"} + org.pinkgorilla/timbre {:mvn/version "0.0.6"} org.pinkgorilla/extension {:mvn/version "0.0.12"} - } + org.pinkgorilla/permission {:mvn/version "0.2.18"} + org.pinkgorilla/websocket {:mvn/version "0.0.11"}} :aliases {; github ci :build {:deps {io.github.clojure/tools.build {:mvn/version "0.9.6"} diff --git a/resources/ext/clj-service.edn b/resources/ext/clj-service.edn index 4ce0a4e..3954fba 100644 --- a/resources/ext/clj-service.edn +++ b/resources/ext/clj-service.edn @@ -4,15 +4,12 @@ :cljs-namespace [goldly.service.core] :cljs-ns-bindings {'goldly.service.core {'clj goldly.service.core/clj 'clj-atom goldly.service.core/clj-atom - 'run goldly.service.core/run - 'run-a goldly.service.core/run-a - 'run-a-map goldly.service.core/run-a-map - 'run-cb goldly.service.core/run-cb - 'wait-chan-result goldly.service.core/wait-chan-result}} + 'run-cb goldly.service.core/run-cb ; depreciate ?? + }} ; run ; clj-service allows requests via http post :api-routes {"clj-service" {:post goldly.service.handler/service-handler-wrapped}} - :clj-services {:name "clj-service-dissovery" + :clj-services {:name "clj-service-discovery" :permission #{} :symbols [goldly.service/services-list]} ; diff --git a/src/clj_service/core.clj b/src/clj_service/core.clj new file mode 100644 index 0000000..dafdeb8 --- /dev/null +++ b/src/clj_service/core.clj @@ -0,0 +1,88 @@ +(ns clj-service.core + (:require + [clojure.set :refer [rename-keys]] + [taoensso.timbre :as timbre :refer [info error]] + [extension :refer [write-target-webly get-extensions]] + [modular.permission.service :refer [add-permissioned-service]] + [modular.clj-service.websocket :refer [create-websocket-responder]])) + +;; EXPOSE FUNCTION + +(defn- resolve-symbol [s] + (try + (requiring-resolve s) + (catch Exception ex + (error "Exception in exposing service " s " - symbol cannot be required.") + (throw ex)))) + +(defn expose-function + "exposes one function + services args: this - created via clj-service.core + permission-service - created via modular.permission.core/start-permissions + function args: service - fully qualified symbol + permission - a set following modular.permission role based access + fixed-args - fixed args to be passed to the function executor as the beginning arguments" + [this permission-service {:keys [function permission fixed-args] + :or {fixed-args [] + permission nil}}] + (assert this "you need to pass the clj-service state") + (assert permission-service "you need to pass the modular.permission.core state") + (assert (symbol? function)) + (let [service-fn (resolve-symbol function)] + (add-permissioned-service permission-service function permission) + (swap! this assoc function {:service-fn service-fn + :permission permission + :fixed-args fixed-args}))) + +(defn expose-functions + "exposes multiple functions with the same permission and fixed-args." + [this permission-service + {:keys [function-symbols permission fixed-args name] + :or {permission nil + fixed-args [] + name "services"}}] + (assert (vector? function-symbols)) + (info "exposing [" name "] permission: " permission " functions: " function-symbols) + (doall + (map (fn [function] + (expose-function this permission-service {:function function + :permission permission + :fixed-args fixed-args})) function-symbols))) + +; services list + +(defn services-list [this] + (keys @this)) + +; start service + +(defn- exts->services [exts] + (->> (get-extensions exts {:clj-services nil}) + (map :clj-services) + (remove nil?))) + +(defn start-clj-services + "starts the clj-service service. + exposes stateless services that are discovered via the extension system. + non stateless services need to be exposed via expose-service" + [permission-service exts] + (info "starting clj-services ..") + (let [this (atom {}) + services (exts->services exts)] + (write-target-webly :clj-services services) + ; expose services list (which is stateful) + (expose-function this permission-service + {:service-fn 'clj-service.core/services-list + :permission nil + :fixed-args [this]}) + ; expose stateless services discovered via extension-manager + (doall + (map (fn [clj-service] + (expose-functions + this permission-service + (rename-keys clj-service {:symbols :function-symbols}))) + services)) + ; create websocket message handler + (create-websocket-responder this permission-service) + ; return the service state + this)) \ No newline at end of file diff --git a/src/clj_service/executor.clj b/src/clj_service/executor.clj new file mode 100644 index 0000000..f706af0 --- /dev/null +++ b/src/clj_service/executor.clj @@ -0,0 +1,35 @@ +(ns clj-service.executor + (:require + [de.otto.nom.core :as nom] + [modular.permission.core :refer [user-authorized?]])) + +;; USER + +(defonce ^:dynamic + ^{:doc "The user-id for which a clj-service gets executed"} + *user* nil) + +(defn execute [this permission-service user-id fun-symbol & args] + (if-let [fun (get this fun-symbol)] + (let [{:keys [function _permission fixed-args]} fun + full-args (concat fixed-args args)] + (if (user-authorized? permission-service fun-symbol user-id) + (try {:result (apply function full-args)} + (catch clojure.lang.ExceptionInfo e + (nom/fail ::exception {:fun fun-symbol + :error (pr-str e) + :message (str "exception when executing function " fun-symbol)})) + (catch AssertionError e + (nom/fail ::assert-error {:fun fun-symbol + :error (pr-str e) + :message (str "assert error when executing function " fun-symbol)})) + (catch Exception e + (nom/fail ::exception {:fun fun-symbol + :error (pr-str e) + :message (str "exception when executing function " fun-symbol)}))) + (nom/fail ::no-permission {:fun fun-symbol + :user user-id + :message (str "user-id " user-id " is not authorized for: " fun-symbol)}))) + (nom/fail ::function-not-found {:fun fun-symbol + :message "No service defined for this symbol."}))) + diff --git a/src/goldly/service/handler.clj b/src/clj_service/handler.clj similarity index 62% rename from src/goldly/service/handler.clj rename to src/clj_service/handler.clj index a3f92ef..7fbace3 100644 --- a/src/goldly/service/handler.clj +++ b/src/clj_service/handler.clj @@ -1,9 +1,10 @@ -(ns goldly.service.handler +(ns clj-service.handler (:require - [taoensso.timbre :refer [trace debug info error]] + [taoensso.timbre :refer [info]] [ring.util.response :as res] + [de.otto.nom.core :as nom] [modular.webserver.middleware.api :refer [wrap-api-handler]] - [goldly.service.core :refer [run-service]])) + [goldly.service.executor :refer [run-service *user*]])) (defn service-handler [req] @@ -11,8 +12,9 @@ (let [body-params (:body-params req) args (select-keys body-params [:fun :args]) _ (info "service: " args) - response (run-service args)] - (if (:error response) + response (binding [*user* nil] + (run-service args))] + (if (nom/anomaly? response) (res/bad-request response) (res/response response)))) diff --git a/src/clj_service/websocket.clj b/src/clj_service/websocket.clj new file mode 100644 index 0000000..e86fde6 --- /dev/null +++ b/src/clj_service/websocket.clj @@ -0,0 +1,22 @@ +(ns clj-service.websocket + (:require + [clojure.string] + [de.otto.nom.core :as nom] + [modular.ws.core :refer [send-response]] + [modular.ws.msg-handler :refer [-event-msg-handler]] + [modular.permission.session :refer [get-user]] + [clj-service.executor :refer [execute *user*]])) + +(defn create-websocket-responder [this permission-service] + (defmethod -event-msg-handler :clj/service + [{:keys [event _id _?data uid] :as req}] + (let [[_ params] event ; _ is :clj/service + {:keys [fun args]} params + user (get-user permission-service uid)] + (future + (let [r (binding [*user* user] + (execute this permission-service user fun args))] + (if (nom/anomaly? r) + (send-response req :clj/service {:error "Execution exception" + :uid uid}) + (send-response req :clj/service {:result r}))))))) diff --git a/src/goldly/service.clj b/src/goldly/service.clj deleted file mode 100644 index 47eaee0..0000000 --- a/src/goldly/service.clj +++ /dev/null @@ -1,20 +0,0 @@ -(ns goldly.service - (:require - [taoensso.timbre :as timbre :refer [info]] - [extension :refer [write-target-webly get-extensions]] - [goldly.service.expose :as expose])) - -(defn- expose-extension-clj-services [clj-services] - (doall - (map expose/start-services clj-services))) - -(defn- exts->services [exts] - (->> (get-extensions exts {:clj-services nil}) - (map :clj-services) - (remove nil?))) - -(defn start-clj-services [exts] - (info "starting clj-services ..") - (let [config (exts->services exts)] - (write-target-webly :clj-services config) - (expose-extension-clj-services config))) \ No newline at end of file diff --git a/src/goldly/service/core.clj b/src/goldly/service/core.clj deleted file mode 100644 index eca03a0..0000000 --- a/src/goldly/service/core.clj +++ /dev/null @@ -1,65 +0,0 @@ -(ns goldly.service.core - (:require - [clojure.string] - [taoensso.timbre :as log :refer [debug info infof warn error errorf]] - [modular.ws.core :refer [send! send-all! send-response]] - [modular.ws.msg-handler :refer [-event-msg-handler send-reject-response]] - [modular.permission.service :refer [add-permissioned-services]] - [modular.permission.app :refer [service-authorized?]])) - -;; services registry - -(def services-atom (atom {})) ; {:cookie/get db/get-cookie} - -(defn add [m] - (swap! services-atom merge m)) - -(defn services-list [] - (keys @services-atom)) - -(defn get-fn [kw] - (cond - (keyword? kw) (kw @services-atom) - (symbol? kw) (resolve kw))) - -; (get-fn :ff) -; (get-fn 'get-collections) - -(defn run [kw & args] - (if-let [fun (get-fn kw)] - (try {:result (if args - (apply fun args) - (fun))} - (catch clojure.lang.ExceptionInfo e - {:error (str "Exception: " (pr-str e))}) - (catch Exception e - {:error (str "Exception: " - "data:" (pr-str (ex-data e)) ;(pr-str e) - "msg:" (pr-str (ex-message e)))})) - {:error (str "service not found: " kw)})) - -(defn create-clj-run-response [{:keys [fun args] :as params}] - (let [result (if args - (apply run fun args) - (run fun))] - (merge params result))) - -(defn run-service [req {:keys [fun args] :as params}] - (try - (infof "%s %s" fun (into [] args)) - (let [response (create-clj-run-response params)] - (if (:error response) - (errorf "service fn: %s error: %s" (:fun response) (:error response)) - (debug "sending service response: " response)) - (send-response req :goldly/service response)) - (catch Exception e - (error "exception in run-service fun:" fun " ex: " e)))) - -(defmethod -event-msg-handler :goldly/service - [{:keys [event id ?data uid] :as req}] - (let [[_ params] event ; _ is :goldly/service - {:keys [fun args]} params] - (if (service-authorized? fun uid) - (future - (run-service req params)) - (send-response req :goldly/service {:error "Not Permissioned"})))) diff --git a/src/goldly/service/core.cljs b/src/goldly/service/core.cljs index 669e0de..5144422 100644 --- a/src/goldly/service/core.cljs +++ b/src/goldly/service/core.cljs @@ -1,12 +1,9 @@ (ns goldly.service.core (:require - [taoensso.timbre :refer-macros [trace debug debugf info infof warnf error errorf]] - [cljs.core.async :refer [>! chan close! put!] :refer-macros [go]] + [taoensso.timbre :refer-macros [infof warnf]] [promesa.core :as p] [reagent.core :as r] - [modular.ws.core :refer [send!]] - [frontend.notification :as n] - [goldly.service.result :refer [update-atom-where]])) + [modular.ws.core :refer [send!]])) (defn print-result [[event-type data]] (warnf "service result rcvd: type: %s data: %s" event-type data)) @@ -19,17 +16,23 @@ :as params}] (let [p-clean (dissoc params :cb :a :where)] (infof "running service :%s args: %s" fun args) - (send! [:goldly/service p-clean] cb timeout) + (send! [:clj/service p-clean] cb timeout) nil)) (defn clj + "executes clj function, returns a promise. + first parameter is the fully qualified function symbol. + second parameter is optionally a a map + {:timeout milliseconds} + all other parameter will be sent to the clj function." ([fun] (clj {} fun)) ([fun & args] (let [[opts fun args] (if (map? fun) [fun (first args) (rest args)] [{} fun args]) - {:keys [timeout] :or {timeout 120000}} opts + {:keys [timeout] + :or {timeout 120000}} opts r (p/deferred) on-result (fn [msg] (if (= msg :chsk/timeout) @@ -42,67 +45,16 @@ (run-cb {:fun fun :args (into [] args) :timeout timeout :cb on-result}) r))) -(defn clj-atom [& args] +(defn clj-atom + "executes clj function. same syntax as clj function. + returns an atom that contains a map + {:status (:pending :done :error) + :data + :error}" + [& args] (let [a (r/atom {:status :pending}) rp (apply clj args)] (-> rp (p/then (fn [data] (swap! a assoc :status :done :data data))) (p/catch (fn [error] (swap! a assoc :status :error :error error)))) - a)) - -; run with core-async channel - -(defn run [params] - (let [ch (chan) - cb (fn [event] - (infof "service/run cb: %s" event) + - (if (= event :chsk/timeout) - (put! ch {:error :timeout}) - (let [[_ data] event] - (put! ch data))))] - (run-cb (assoc params :cb cb)) - ch)) - -(defn wait-chan-result [ch fn-success fn-err] - (go - (let [{:keys [error result] :as r} (> (map (fn [s] [s permission]) symbols) - (into {}))] - ; {'time/now-date #{} - ; 'time/local nil} - - (add-permissioned-services data))) - -(comment - (set-permission ['crb.report/profit-loss 'crb.report/overdues] #{:management}) -; - ) - -(defn start-services [{:keys [symbols permission name] - :as args - :or {name "services"}}] - (info "exposing [" name "] permission: " permission " symbols: " symbols) - (let [services (->> (map resolve-symbol symbols) - (into {}))] - (goldly.service.core/add services) - (set-permission symbols permission) - args ; to stop this services. - )) - -(defn stop-services [{:keys [symbols permission]}] - (warn "stop-services: " symbols " permission: " permission " - not implemented")) - -; todo: 1. permissions 2. stop service - diff --git a/src/goldly/service/result.cljc b/src/goldly/service/result.cljc deleted file mode 100644 index ec1b079..0000000 --- a/src/goldly/service/result.cljc +++ /dev/null @@ -1,26 +0,0 @@ -(ns goldly.service.result - (:require - [clojure.walk :as walk] - [com.rpl.specter :refer [transform setval END]] - #?(:clj [taoensso.timbre :refer [debug debugf info infof warn error errorf]] - :cljs [taoensso.timbre :refer-macros [debug debugf info infof warn error errorf]]))) - -(defn specter-resolve - [specter-vector] - (walk/prewalk - (fn [x] (if (keyword? x) - (case x - :END END - x) - x)) - specter-vector)) - -(defn update-atom-where [a where result] - (debugf "updating atom where: %s with result: %s" where result) - (try - (let [where-resolved (specter-resolve where)] - (debug "specter resolved: " where-resolved) - (reset! a (setval where-resolved result @a))) - (catch #?(:cljs :default - :clj Exception) e - (errorf "update-atom-where: %s ex: %s" e)))) \ No newline at end of file diff --git a/test/service_update_test.clj b/test/service_update_test.clj deleted file mode 100644 index cea2e81..0000000 --- a/test/service_update_test.clj +++ /dev/null @@ -1,16 +0,0 @@ -(ns service-update-test - (:require - [clojure.test :refer [deftest is testing]] - [goldly.service.result :as sr])) - -(def a1 (atom {:a {:b 7}})) -(def a2 (atom {:a {:b 7 :c 2}})) - -(deftest services-test - (testing "service-update-result test" - (sr/update-atom-where a1 [:a :b] 13) - (sr/update-atom-where a2 [:a :b] 13) - (is (= @a1 {:a {:b 13}})) - (is (= @a2 {:a {:b 13 :c 2}})) - ; - )) \ No newline at end of file