Auth Rules
Access Control Rules with FPJSON
One big constraint of FPJSON is we can only do pure functional programming with point-free style, which means functions cannot have arguments. Functional programming is extremely powerful, but pure FP sometimes makes it overly complicated and impractical to build a simple logic.
WeaveDB elegantly extends the base FPJSON to makes it easier and more practical by injecting side-effect variables and imperative programming features such as if-else conditional statement.
allow() / deny()
The simplest form of access control rules is just allow everything.
["set", ["allow()"]]
or deny everything.
["set", ["deny()"]]
Pattern Matching
The first element is an accepted operation and the condition will be evaluated only if the query matches the operation.
add
|set
|update
|upsert
|del
: these matche query types
You can always use the basic operation types, but a better solution is define custom operations such as add:post
and del:post
.
["add:post", ["allow()"]]
The first part of a custome tag matches query types, and the second part is an arbitrary operation name.
- custome tag:
type:name
- types:
add
|set
|update
|upsert
|del
In this way, users will only be able to execute the preset custom queries, so you will be in better control.
await db.set("add:post", {title: "Test", body: "hello"}, "posts")
Preset Variables
You can access preset variables in access rule evaluations as explained here.
let vars = {
op, // set:post ( op = opcode:operand )
opcode, // set
operand, // post
db, // database ID
owner // database owner
signer, // message signer
signer23, // message signer in WDB23
ts, // timestamp
dir, // directory ID
doc, // document ID
query, // query = [ req, dir, doc ]
req, // data in request query ( before + req = after )
before, // data before updating
after, // data after updating
allow: false, // auth passes if allow is eventually true
}
For instance, if { title: "Title", body: "hellow" }
is already stored, and the query is updating { body: "bye" }
, the following is what will be assigned.
$before
:{ title: "Title", body: "hellow" }
$after
:{ title: "Title", body: "bye" }
mod()
mod()
will manipulate the uploading data before commiting permanently.
[ "add:post", [ [ "mod()", { id: "$doc", owner: "$signer", date: "$ts" } ], ["allow()"] ] ]
This will set id
to the auto-generated docID, owner
to the transaction signer, and date
to the transaction timestamp.
const tx = await db.set("add:post", {title: "Test", body: "hello"}, "posts")
const post = await db.get("posts", tx.docID)
// the post has the extra fields auto-assigned : { title, body, id, owner, date }
This is how you can control the values of updated fields and minimize the fields users will upload.
fields()
You can also constrain the user updated fields with fields()
, and it works great with mods()
.
In the previous example, you only want users to update title
and body
, not anything else.
Use ["fields()", ["title", "body"]]
for such an restriction.
[
"add:post",
[
["fields()", ["title", "body"]],
["mod()", { id: "$id", owner: "$signer", date: "$ts" }],
["allow()"],
],
]
*
will make the field mandatory. e.g. ["fields()", ["*title", "*body"]]
// these will be rejected
await db.set("add:post", {title: "Test", body: "hello", id: "abc"}, "posts")
await db.set("add:post", {title: "Test"}, "posts") // missing mandatory body
You can also individually whitelist and blacklist fields with requested_fields()
and disallowed_fields()
respectively.
=$
=$
will assign the result of the following block to a variable. You can use FPJSON logic in the second block.
// this will check if the signer is the post owner
["=$isOwner", ["equals", "$signer", "$before.owner"]]
allowif() / allowifall()
Assigned variables can be used in any later blocks. It's especially useful when combined with allowif()
.
[
"delete:post",
[
["=$isOwner", ["equals", "$signer", "$before.owner"]], // the second block is FPJSON
["allowif()", "$isOwner"], // allow if the second element is true
],
]
You can use multiple conditions with allowifall()
. The following also checks if the signer is the database owner.
[
"delete:post",
[
["=$isDataOwner", ["equals", "$signer", "$before.owner"]],
["=$isDBOwner", ["equals", "$signer", "$owner"]],
["allowifall()", ["$isOwner", "$isDBOwner"],
],
]
You can use allowifany()
, denyif()
, denyifall()
, denyifany()
, breakif()
in the same principle.
get()
get()
allows you to query other data during access evaluations.
The following checks if the signer exists in users
collection. It's equivalent to await data.get("users", "$signer")
.
[
"add:post",
[
["=$user", ["get()", ["users", "$signer"]]],
["=$existsUser", [["complement",["isNil"]], "$user"]],
["allowif()", "$existsUser"],
],
]
Shortcut Symbols
As you can see, functional programming can get a bit too verbose for simple logic like $existsUser
. So we have a bunch of shortcut symbols to make it more pleasant.
o$
:["complement",["isNil"]]
: true if data existsx$
:["isNil"]
: true if data isnull
orundefined
!$
:["not"]
: flip booleanl$
:["toLower"]
: lowercaseu$
:["toUpper"]
: uppercase$
:["tail"]
: remove the first element, useful for escaping in FPJSON
For instance, you can simplify the previous example as follows.
[
"add:post",
[
["=$user", ["get()", ["users", "$signer"]]],
["allowif()", "o$user"], // true if $user exists
],
]
if-else conditions
Sometimes you want to execute some blocks only if a certain condition is met.
if
executes the third block only if the second block evaluates true
.
["if", "o$user", ["=$existsUser", true]]
You can combine if
with elif
and else
.
["=$existsUser", ["if", "o$user", true, "else", false]]
User break
to exit the whole evaluation without allow()
and deny()
.
["if", "x$user", ["break"]]
In this case, the query validity depends on other matched conditions. For example, you could define conditions for add:post
, but also another condition for add
and the query matches both patterns.
Helper Functions
parse()
equivalent to JSON.parse()
.
["=$json", ["parse", "$req.json_str"]]
stringify()
equivalent to JSON.stringify()
.
["=$str", ["stringify", "$req.json"]]
wdb23()
["=$addr23", ["wdb23()", "$signer"]]
wdb160()
["=$hash", ["wdb160()", [["$req.from", "$req.to"]]]]
cid()
["=$cid", ["cid()", { str: "abc" }]]
Set Auth Rules
const auth = [
[
"add:note",
[
["fields()", ["*content"]],
["mod()", { id: "$doc", actor: "$signer", published: "$ts", likes: 0 }],
["allow()"],
],
],
]
await db.setAuth(auth, "notes")