Installation and Use
This tutorial/demo that shows how to build web-apps with STORM. Like STORM, the demo focuses only on the web-server back-end. You can use your favorite front-end framework to write the client. For examples, look at voltron and disco, two complete applications we built with STORM and vue.js.
Here’s a video demonstrating disco.
Prerequisites
STORM has two dependencies: the Haskell build tool stack
and the SMT solver z3
.
stack v2.5.1 Install using these instructions
z3 v4.8.8 Install by placing a copy of the precompiled
z3
binary in your$PATH
. You can ignore the shared libraries and bindings for Java and Python.
Additionally this demo will use curl
to test our app as we go.
Install
To start, you need to install storm-codegen
git clone git@github.com:storm-framework/storm-codegen.git
$ cd storm-codegen
$ stack install $
Next, clone the starter template app
git clone git@github.com:storm-framework/demo.git storm-app
$ cd storm-app
$ git checkout v0 $
This tutorial is split into multiple pieces or versions which can be found in the the cloned repo as branches v0
, v1
, v2
, v3
, v4
, and v5
. You can skip ahead and look at the progression via
git diff v0..v1 $
Architecture
A STORM app has the following essential components.
File | Description |
---|---|
Models.storm |
Model-policy file |
Routes.hs |
Route-controller mapping |
Types.hs |
Global type definitions |
Config.hs |
Configuration options (e.g. env variables) |
Controllers/ |
Controller implementations |
Build
You can build the web-app by typing
make build $
This will take a long while to download all the dependencies, including building the LiquidHaskell verifier. Grab some coffee, like some tweets, brush your teeth, or go for a jog. Fortunately, you only need to do this once; subsequent make
should be quick.
Version 0: Kick the Tires
Lets kick the tires to make sure stuff is working.
Step 1. Fire up the server
make $
You should see the following at your terminal
Starting server at: host = "127.0.0.1", port = 3000
Step 2. Test
In a separate terminal test your server with curl
curl http://localhost:3000/api/ping
$ "pong"
Version 1: Routes and Controllers
(You can “cheat” by switching to the branch v1
in the repo.)
Next, lets add our first route and its controller.
Step 1: Add a Route
Edit src/Routes.hs
so that the definition of routes
looks like
routes :: [Route]
=
routes "/api/signin" signIn
[ post "/api/signout" signOut
, post "/api/ping" pong
, get "/api/list/:uid" list -- add this line
, get ]
Step 2: Implement a Controller
Edit src/Controllers/List.hs
to add the following definition that simply responds with a string repeating the UserId
and the current time.
{-@ list :: UserId -> TaggedT<{\_ -> False}, {\_ -> True}> _ _ _ @-}
list :: UserId -> Controller ()
= do
list uid <- getTime
time let msg = "OK: hello " <> tShow uid <> " at " <> time :: T.Text
respondJSON status200 msg
getTime :: Controller T.Text
= tShow <$> liftTIO currentTime getTime
The text inside {-@ ... @-}
is a refinement type signature that tells the verifier that list
is a top-level controller. For more details see [the paper].
Step 3: Test
Run scripts/test_1.sh
to test your new route and controller!
./scripts/test_1.sh
$
list ... returns current time UTC
"OK: hello ... {unSqlBackendKey = 1} at 2021-07-13 22:53:25.51654 UTC"
list ... returns current time UTC
"OK: hello ... {unSqlBackendKey = 2} at 2021-07-13 22:53:25.5285 UTC"
list ... returns current time UTC
"OK: hello ... {unSqlBackendKey = 3} at 2021-07-13 22:53:25.537898 UTC"
Version 2: Adding Authentication
Lets look at the model-policy file src/Models.storm
The User
table defines the set of users of the app.
User
emailAddress Text
password ByteString
firstName Text
lastName Text
UniqueEmailAddress emailAddress
Step 1: Require Authentication
Lets modify the list
controller (in src/Controllers/List.hs
) to use requireAuthUser
which succeeds only when the request is from a logged in user.
{-@ list :: UserId -> TaggedT<{\_ -> False}, {\_ -> True}> _ _ _ @-}
list :: UserId -> Controller ()
= do
list uid
requireAuthUser<- getTime
time <- selectFirstOr notFoundJSON (userId' ==. uid)
user <- extractUserNG user
userNG let msg = "OK: " <> tShow userNG <> " at " <> time
respondJSON status200 msg
Additionally, notice that
We use the query
selectFirstOr
with the query(userId' ==. uid)
to find the first record in theUser
table whose primary key equalsuid
– i.e. to find the record foruid
if it exists or else, respond withNot Found
.We use
extractUserNG
, which internally, usesproject
to pull out the values of individual field of theuser
record to build the pure valueuserNG
sent back to the client.
Step 2: Test
Run make
to build and run the server in one terminal.
In another test your updated controller with scripts/test_2.sh
.
add users
... INFO: addUser: CreateUser {crUserEmail = "alice@st.orm" ...}
... INFO: addUser: CreateUser {crUserEmail = "bob@st.orm" ...}
... INFO: addUser: CreateUser {crUserEmail = "carlos@st.orm" ...}
list ... returns nothing
sign-in first!
{"firstName":"Alice","lastName":"Waters"}
list ... now succeeds
"OK: UserNG {userFirstName = \"Alice\", userLastName = \"Waters\"} ..."
list ... now succeeds
"OK: UserNG {userFirstName = \"Robert\", userLastName = \"Barron\"} ..."
list ... returns {} as no such user
{}
Note that we don’t get any response until we’re signed in.
Version 3: Adding Items
Next, lets start populating our users’ lists with some items!
Step 1: Add a Database Table
First, lets make an Item
table by editing src/Models.storm
to include
Item
owner UserId
description Text
level String
Similarly, we want a JSON-compatible pure representation of Item
so edit src/Types.hs
to
data ItemData = ItemData
itemDescription :: Text
{ itemLevel :: String
,
}deriving (Show, Generic)
mkItemData :: Text -> String -> ItemData
= ItemData (strip descr) (sStrip levl)
mkItemData descr levl
instance FromJSON ItemData where
= genericParseJSON (stripPrefix "item")
parseJSON
instance ToJSON ItemData where
= genericToEncoding (stripPrefix "item") toEncoding
Step 2: Implement the API to Add Items
Next, lets make a new API endpoint to insert items for a user
Edit src/Routes.hs
to include
routes :: [Route]
=
routes "/api/signin" signIn
[ post "/api/signout" signOut
, post "/api/ping" pong
, get "/api/list/:uid" list
, get "/api/add" add
, get ]
Edit src/Controllers/List.hs
to include the controller
{-@ add :: TaggedT<{\_ -> True}, {\_ -> True}> _ _ _ @-}
add :: Controller ()
= do
add <- requireAuthUser
owner <- project userId' owner
ownerId <- project userEmailAddress' owner
ownerEm <- decodeBody
items <- mapT (\ItemData {..} ->
items'
insert (mkItem ownerId itemDescription itemLevel)
) itemslet msg = "OK: add " <> tShow (length items') <> " items " <> ownerEm
respondJSON status200 msg
Note how the controller
- Finds the
ownerId
andownerEmail
usingrequireAuthUser
andproject
- Extracts the JSON payload via
decodeBody
- Uses
mkItem
to create theItem
records and - Uses
insert
to add the records to the DB.
Step 3: Modify list
Return Items
Next, lets update list
to return the items of the given UserId
(instead of just responding with their names…). Edit the src/Controllers/List.hs
so that
= do
list userId <- selectList (itemOwner' ==. userId)
items <- mapT (\i ->
itemDs ItemData `fmap` project itemDescription' i
<*> project itemLevel' i
) items respondJSON status200 itemDs
Now, notice that first selectList
is invoked with a query asking for all the Item
s whose owner
is userId
. Next, we build a response object by project
ing the description
and level
values of each record in items
to build an array of JSON objects that is sent back to the client.
Step 4: Test
The file scripts/alice_json
has a few items for alice
:
more scripts/items_alice.json
$ [ {"description":"katla","level":"public"},
{"description":"counterpart","level":"public"},
"description":"marley and me","level":"private"},
{"description":"daniel tiger","level":"follower"},
{{"description":"lupin","level":"follower"}
]
Lets test the app by adding and querying the server for those items
./scripts/test_3.sh
$
********* add users
...
******** sign-in alice!
{"firstName":"Alice","lastName":"Waters"}
******** add items for alice
"OK: add 5 items for alice@st.orm"
******* logout
******** sign-in alice!
{"firstName":"Alice","lastName":"Waters"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"},
"description":"marley and me","level":"private"},
{"description":"daniel tiger","level":"follower"},
{{"description":"lupin","level":"follower"}]
******** logout
******** sign-in bob!
{"firstName":"Robert","lastName":"Barron"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"},
"description":"marley and me","level":"private"},
{"description":"daniel tiger","level":"follower"},
{{"description":"lupin","level":"follower"}]
******** logout
Version 4: Restrict to Public Items
Oops, we hit a bit of a snag – bob
(or anyone for that matter!) can see all of alice
’s items – even the ones marked "private"
!
Step 1: Specify a Security Policy
Lets specify that only items marked "public"
should be visible to users other than the owner, who should, of course, be able to see all their own items.
Edit src/Models.storm
to include two predicates
IsOwner item viewer
which is true whenviewer
ownsitem
,IsPublic item
which is true when theitem
is marked"public"
.
policy IsOwner = \item viewer ->
itemOwner item == entityKey viewer
policy IsPublic = \item ->
itemLevel item == "public"
policy OwnPublic = \item viewer ->
IsOwner item viewer || IsPublic item
and further add a read
clause to restrict description
the owner-unless-marked-public
Item
Item
owner UserId
description Text
level String
read [ description ] @OwnPublic
Now when you run make
, compilation fails with a type error!
**** LIQUID: UNSAFE **************************************************
../storm-demo/src/Controllers/List.hs:43:40: error:
Liquid Type Mismatch
...
|
43 | ItemData `fmap` project itemDescription' i
| ^^^^^^^^^^^^^^^^
STORM is unimpressed as selectList
yields all of userId
’s items but the viewer can be someone else who is not allowed (per OwnPublic
) to view those description
s!
Step 2. Fix the Controller
Lets fix the controller by checking if the viewer is userId
and otherwise, by conjoining the query with a clause that the Item
’s level be "public"
. Edit src/Controllers/List.hs
so that list
looks like
{-@ list :: UserId -> TaggedT<{\_ -> False}, {\_ -> True}> _ _ _ @-}
list :: UserId -> Controller ()
= do
list userId <- project userId' =<< requireAuthUser
viewId let pub = if userId == viewId
then trueF
else itemLevel' ==. "public"
<- selectList (itemOwner' ==. userId &&: pub)
items <- mapT (\i -> ItemData `fmap` project itemDescription' i
itemDs <*> project itemLevel' i
) items respondJSON status200 itemDs
Step 3. Test
STORM gladly verifies that the fix controller is policy compliant! Lets test our server to make sure that only public items are shown to users other than alice
scripts/test_3.sh
$ ********* add users
...
******** sign-in alice!
{"firstName":"Alice","lastName":"Waters"}
******** add items for alice
"OK: add 5 items for alice@st.orm"
******* logout
******** sign-in alice!
{"firstName":"Alice","lastName":"Waters"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"},
"description":"marley and me","level":"private"},
{"description":"daniel tiger","level":"follower"},
{{"description":"lupin","level":"follower"}]
******** logout
******** sign-in bob!
{"firstName":"Robert","lastName":"Barron"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"}]
******** logout
Note that bob
only gets to see the subset of public
items.
Version 5 : Restrict to Followers
As a last step, lets add a feature where a user can share some of their items with followers.
Step 1. Add Followers to the Model
Add the below definitions to represent the followers relationship in src/Models.storm
.
predicate follows :: UserId -> UserId -> Bool
Follower
subscriber UserId
publisher UserId
status String
assert {status == "accepted" => follows subscriber publisher}
The Follower
table records the actual relationship. The predicate follows
reflects the relationship at the logical “policy” level, using the assert
clause which says that each tuple in the Follower
table bears witness to the fact that its subscriber field follows its publisher field, when the status
field has the value "accepted"
. (This lets one user initiate the relationship, which is only cemented when the other user accepts.)
Step 2. Modify Policy to Allow Followers Access
Next, lets update the policies for Item
to allow the owner’s followers to read the description
. Edit src/Models.storm
to define
policy IsFollower = \item viewer ->
itemLevel item == "follower" &&
follows (entityKey viewer) (itemOwner item)
policy OwnFollowPub = \item viewer ->
IsOwner item viewer ||
IsFollower item viewer ||
IsPublic item
and then edit the read
clause with the new OwnFollowPub
policy
Item
owner UserId
description Text
level String
read [ description ] @OwnFollowPub
Step 3. Update the Controller
The old controller should still compile – but its not what we want as it still only shows the public
items to other users.
script/test_5.sh
$
********* add users
...
******** bob follows alice
******** sign-in bob!
{"firstName":"Robert","lastName":"Barron"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"}]
******** logout
Of course, we have to change how the controller behaves as well, by making it issue queries that also return "follower"
level items. Edit src/Controllers/List.hs
to include a function checkFollower vId uId
that queries the database to determine whether vId
is a follower of uId
{-@ checkFollower ::
vId:_ -> uId:_ ->
TaggedT<{\_ -> True}, {\_ -> False}> _ _ {b:_|b => follows vId uId}
@-}
checkFollower :: UserId -> UserId -> Controller Bool
= do
checkFollower vId uId <- selectList (followerSubscriber' ==. vId &&:
flws ==. uId)
followerPublisher' case flws of
-> return False
[] -> return True _
The text inside {-@ ... @-}
says that checkFollower
is a read-only controller, that accesses data visible to all users (True
) and sends data to no users (False
). Furthermore, the output is a Bool
which if True
implies the follows
relationship holds between the input UserId
s. For more details on the signatures, see the paper.
Next, update list
in src/Controllers/List.hs
to use a separate conjunct if the viewer
is indeed a follower of userId
{-@ list :: _ -> TaggedT<{\_ -> False}, {\_ -> True}> _ _ _ @-}
list :: UserId -> Controller ()
= do
list userId <- project userId' =<< requireAuthUser
viewerId <- checkFollower viewerId userId
follower let self = viewerId == userId
let chk
| self = trueF
| follower = itemLevel' <-. ["public", "follower"]
| otherwise = itemLevel' ==. "public"
<- selectList (itemOwner' ==. userId &&: chk)
items <- mapT (\i ->
itemDatas ItemData `fmap` project itemDescription' i
<*> project itemLevel' i
)items respondJSON status200 itemDatas
Oops, there is a type error, can you figure it out?
Hint: Is the query that checks the follower status correct?
Step 4. Test
Now, after fixing the type error, when you run the tests, we’ll see all the right items for users that are indeed followers!
********* add users
...
******** sign-in alice!
...
******** add items for alice
...
******* logout
******** sign-in alice!
{"firstName":"Alice","lastName":"Waters"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"},
"description":"marley and me","level":"private"},
{"description":"daniel tiger","level":"follower"},
{{"description":"lupin","level":"follower"}]
******** logout
******** sign-in bob!
{"firstName":"Robert","lastName":"Barron"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"}]
******** logout
******** bob follows alice
******** sign-in bob!
{"firstName":"Robert","lastName":"Barron"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"},
"description":"daniel tiger","level":"follower"},
{{"description":"lupin","level":"follower"}]
******** logout
******** sign-in carlos!
{"firstName":"Carlos","lastName":"Cortado"}
******** list items for alice ...
{"description":"katla","level":"public"},
[{"description":"counterpart","level":"public"}]
******** logout
Now alice
sees all her items, after adding bob
as a follower, he sees the public and shared items, but carlos
only sees the public ones.
While the above should get you started, there are lots of important technical details that we’ve skipped over. Take a look at the OSDI 21 paper for more details or email the team if you want to contribute to or use STORM!