Skip to content

Commit

Permalink
initial server security example...no code yet
Browse files Browse the repository at this point in the history
  • Loading branch information
awkay committed Apr 20, 2016
1 parent 92bca80 commit 55a5ed5
Show file tree
Hide file tree
Showing 16 changed files with 281 additions and 0 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ You should now be able to see the app at: http://localhost:8080.

## Server-side

- [Server-side security for UI-generated queries](recipes/server-query-security)
- Create and integrate a custom server component
- Add a Server Route that generates an image.
- Interface with SQL on the server
Expand Down
55 changes: 55 additions & 0 deletions recipes/server-query-security/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Query Security

IN PROGRESS...NO CODE YET, BUT DESCRIPTION IS GOOD

This recipe walks you through a suggestion on handling query security. It
uses Spectre to walk the query data structure.

## Running the code

You want to run both a figwheel build and a server. See the top-level
README for instructions. Be sure you access the app through the server
port or the remote load will not work.

## UI Query Security

If you examine any UI query it will have a tree form. That is the nature of Datomic pull syntax
and UI's. For any such query, you can imagine it as a graph walk:

Take this query:

```
[:a {:join1 [:b {:join2 [:c :d]}]}]
```

```
QUERY PART IMPLIED DATABASE graph
[:a {:join1 { :a 6 :join1 [:tableX id1] }
\
\
\|
[:b {:join2 :tableX { id1 { :id id1 :join2 [:tableY id2]
/
/
|/
[:c :d]}]}] :tableY { id2 { :id id2 :c 4 :d 5 }}
```

One idea that works pretty well for us is based on this realization: There is a starting point of this walk...and
it *must* be specified (or implied at least) by the incoming query. A tradition logic check needs to be run on
this object to see if it is OK for the user to *start* reading the database there.

Two things remain from there:

1. Verify the user is allowed to read the specific *data* at the node of the graph (e.g. :a, :c, and :d)
2. Verify the user is allowed to *walk* across a given *reference* at that node of the graph.

However, since both of those cases are essentially the same in practice (can the user read the given property), the
algorithm simplifies to:

- Verify the user is allowed to read the "top" object. If not, disallow the query.
- Create a whitelist of keywords that are allowed to be read by the query in question. This can be a one-time
declarative configuration, or something dynamic based on user rights.
- Walk the query, and gather up all keywords
- Find the set difference of the whitelist and the query keywords.
- If the difference if not empty, refuse to run the query
30 changes: 30 additions & 0 deletions recipes/server-query-security/dev/client/cljs/user.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
(ns cljs.user
(:require
[app.core :refer [app]]
[untangled.client.core :as core]
[cljs.pprint :refer [pprint]]
[devtools.core :as devtools]
[untangled.client.logging :as log]
[app.ui :as ui]))

(enable-console-print!)

; Use Chrome...these enable proper formatting of cljs data structures!
(devtools/enable-feature! :sanity-hints)
(devtools/install!)

; Mount the app and remember it.
(reset! app (core/mount @app ui/Root "app"))

; use this from REPL to view bits of the application db
(defn log-app-state
"Helper for logging the app-state, pass in top-level keywords from the app-state and it will print only those
keys and their values."
[& keywords]
(pprint (let [app-state @(:reconciler @app)]
(if (= 0 (count keywords))
app-state
(select-keys app-state keywords)))))

; Om/dev logging level
;(log/set-level :none)
55 changes: 55 additions & 0 deletions recipes/server-query-security/dev/server/user.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
(ns user
(:require
[clojure.pprint :refer (pprint)]
[clojure.stacktrace :refer (print-stack-trace)]
[clojure.tools.namespace.repl :refer [disable-reload! refresh clear set-refresh-dirs]]
[com.stuartsierra.component :as component]
[figwheel-sidecar.repl-api :as ra]
[app.system :as sys]
[taoensso.timbre :as timbre]))

;;FIGWHEEL

(def figwheel-config
{:figwheel-options {:css-dirs ["resources/public/css"]}
:build-ids ["dev"]
:all-builds (figwheel-sidecar.repl/get-project-cljs-builds)})

(defn start-figwheel
"Start Figwheel on the given builds, or defaults to build-ids in `figwheel-config`."
([]
(let [props (System/getProperties)
all-builds (->> figwheel-config :all-builds (mapv :id))]
(start-figwheel (keys (select-keys props all-builds)))))
([build-ids]
(let [default-build-ids (:build-ids figwheel-config)
build-ids (if (empty? build-ids) default-build-ids build-ids)]
(println "STARTING FIGWHEEL ON BUILDS: " build-ids)
(ra/start-figwheel! (assoc figwheel-config :build-ids build-ids))
(ra/cljs-repl))))

(set-refresh-dirs "src/server")

(defonce system (atom nil))

(defn init
"Create a web server from configurations. Use `start` to start it."
[]
(reset! system (sys/make-system)))

(defn start "Start (an already initialized) web server." [] (swap! system component/start))

(defn stop "Stop the running web server." []
(when @system
(swap! system component/stop)
(reset! system nil)))

(defn go "Load the overall web server system and start it." []
(init)
(start))

(defn reset
"Stop the web server, refresh all namespace source code from disk, then restart the web server."
[]
(stop)
(refresh :after 'user/go))
46 changes: 46 additions & 0 deletions recipes/server-query-security/project.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
(defproject untangled/demo "1.0.0"
:description "Untangled Cookbook Recipe"
:url ""
:license {:name "MIT"
:url "https://opensource.org/licenses/MIT"}

:dependencies [[com.datomic/datomic-free "0.9.5206" :exclusions [joda-time]]
[com.taoensso/timbre "4.3.1"]
[commons-codec "1.10"]
[org.clojure/clojure "1.7.0"]
[org.clojure/clojurescript "1.7.228"]
[org.omcljs/om "1.0.0-alpha31"]
[binaryage/devtools "0.5.2" :exclusions [environ]]
[figwheel-sidecar "0.5.0-3" :exclusions [ring/ring-core joda-time org.clojure/tools.reader]]
[com.cemerick/piggieback "0.2.1"]
[org.clojure/tools.nrepl "0.2.12"]
[juxt/dirwatch "0.2.3"]
[navis/untangled-client "0.4.6" :exclusions [cljsjs/react org.omcljs/om]]
[navis/untangled-server "0.4.5"]
[navis/untangled-datomic "0.4.4" :exclusions [com.datomic/datomic-free org.clojure/tools.cli]]]

:plugins [[lein-cljsbuild "1.1.2"] [lein-environ "1.0.0"]]

:source-paths ["dev/server" "src/server"]
:jvm-opts ["-server" "-Xmx1024m" "-Xms512m" "-XX:-OmitStackTraceInFastThrow"]
:clean-targets ^{:protect false} ["resources/public/js" "target"]

:cljsbuild {:builds
[{:id "dev"
:source-paths ["src/client" "dev/client"]
:figwheel true
:compiler {:main cljs.user
:asset-path "js/compiled/dev"
:output-to "resources/public/js/compiled/app.js"
:output-dir "resources/public/js/compiled/dev"
:optimizations :none
:parallel-build false
:verbose false
:recompile-dependents true
:source-map-timestamp true}}]}

:figwheel {:css-dirs ["resources/public/css"]}

:repl-options {:init-ns user
:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{:port 8080}
1 change: 1 addition & 0 deletions recipes/server-query-security/resources/config/recipe.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Empty file.
Empty file.
14 changes: 14 additions & 0 deletions recipes/server-query-security/resources/public/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Untangled Recipe</title>
<link rel="stylesheet" href="css/base.css">
<link rel="stylesheet" href="css/index.css">
</head>
<body>
<div id="app"></div>
<script src="js/compiled/app.js"></script>
</body>
</html>
3 changes: 3 additions & 0 deletions recipes/server-query-security/script/figwheel.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(require '[user :refer [start-figwheel]])

(start-figwheel)
17 changes: 17 additions & 0 deletions recipes/server-query-security/src/client/app/core.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
(ns app.core
(:require
app.mutations
[untangled.client.core :as uc]
[untangled.i18n :refer-macros [tr trf]]
[untangled.client.data-fetch :as df]
[om.next :as om]))

(def initial-state {:ui/react-key "abc"})

(defonce app (atom (uc/new-untangled-client
:initial-state initial-state
:started-callback
(fn [{:keys [reconciler]}]
; TODO
))))

3 changes: 3 additions & 0 deletions recipes/server-query-security/src/client/app/mutations.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
(ns app.mutations
(:require [untangled.client.mutations :as m]))

20 changes: 20 additions & 0 deletions recipes/server-query-security/src/client/app/ui.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
(ns app.ui
(:require [om.dom :as dom]
[om.next :as om :refer-macros [defui]]
[untangled.i18n :refer-macros [tr trf]]
yahoo.intl-messageformat-with-locales
[untangled.client.data-fetch :as df]))

(defui ^:once Child
Object
(render [this] (dom/p nil "TODO")))

(def ui-child (om/factory Child))

(defui ^:once Root
static om/IQuery
(query [this] [:ui/react-key])
Object
(render [this]
(let [{:keys [ui/react-key] :or {ui/react-key "ROOT"} :as props} (om/props this)]
(dom/div #js {:key react-key} (ui-child)))))
13 changes: 13 additions & 0 deletions recipes/server-query-security/src/server/app/api.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
(ns app.api
(:require [om.next.server :as om]
[om.next.impl.parser :as op]
[taoensso.timbre :as timbre]))

(defmulti apimutate om/dispatch)
(defmulti api-read om/dispatch)

(defmethod apimutate :default [e k p]
(timbre/error "Unrecognized mutation " k))

(defmethod api-read :default [{:keys [ast query] :as env} dispatch-key params]
(timbre/error "Unrecognized query " (op/ast->expr ast)))
22 changes: 22 additions & 0 deletions recipes/server-query-security/src/server/app/system.clj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
(ns app.system
(:require
[untangled.server.core :as core]
[app.api :as api]
[om.next.server :as om]
[taoensso.timbre :as timbre]
[om.next.impl.parser :as op]))

(defn logging-mutate [env k params]
(timbre/info "Mutation Request: " k)
(api/apimutate env k params))

(defn logging-query [{:keys [ast] :as env} k params]
(timbre/info "Query: " (op/ast->expr ast))
(api/api-read env k params))

(defn make-system []
(core/make-untangled-server
:config-path "config/recipe.edn"
:parser (om/parser {:read logging-query :mutate logging-mutate})
:parser-injections #{}
:components {}))

0 comments on commit 55a5ed5

Please sign in to comment.