Authorization etc.
As stated in the previous post… from 4 months ago… I want to be able to ship stuff in a single executable, with a single file as a database - I dream of keeping it simple.
This post aims to show a way of doing authorization that feels a bit like firebase/firestore where you write rules that need matching for a certain type. But I also want it in versioned code instead of a terraform setup or in a ui. I’m not claiming those examples are bad, I just prefer keeping my code in the code.
The example
Given the simple note object below, with some metadata fields embedded, we want to limit who can read what notes.
type MetaFields struct {
CreatedBy string `json:"createdBy"`
Created time.Time `json:"created"`
LastModified time.Time `json:"lastModified"`
SharedWith []string `json:"sharedWith"`
}
type Note struct {
MetaFields
Body string `json:"body"`
}
For this given type we will probably want a few simple rules for the CRUD operations and live updates. A few reasonable rules would be:
- Owners can do whatever with their documents
- People in the “shared with”-list can read, and maybe update?
Bam, business logic spec done.
Now we want some way of enforcing these rules, and one way would be the following type definition:
type AccessFunc func(echo.Context, []byte) bool
… given a byte array and a context for the request, return true or false. Keeping it []byte
lets
us store the data wherever, however we want: embedded DB (as in the previous post), a text file on disk,
a nosql-documentdb - we don’t have to care.
With this type defined its trivial to define a few operators that lets us combine AccessFunc
s into
more complex rules. We probably want All
, Any
, Or
, And
to cover most cases right away, let’s
assume we have them.
Getting the data
The byte array in the authFunc will be a json - we’ve decided that. With that in mind we could probably unmarshal the json into its struct, and check the fields’ values against some access rule. But there’s another way: gjson. gjson lets us read json documents by field path and just get the field we’re after. This is probably not a useful optimization, or an optimization at all, but it lets us keep the access rules very short and sweet.
Here’s a rule for enforcing the isOwner
rule, which checks the createdBy
field in the object in question.
We’ll leave out the authentication and fetching of userId, right now we’re user 124
.
var isOwner = auth.AccessFunc(func(c echo.Context, doc []byte) bool {
return gjson.GetBytes(doc, "createdBy").String() == "124"
})
That’s even shorter than a rule in firebase! Here’s a isSharedWith
rule:
var isSharedWith = auth.AccessFunc(func(c echo.Context, doc []byte) bool {
shares := gjson.GetBytes(doc, "sharedWith").Array()
for _, v := range shares {
if v.String() == "124" {
return true
}
}
return false
})
I assume you’ll figure out how a open
rule could look.
With some handler boilerplate out of the way we can now tie our rules to our CRUD handlers like this:
handlers.AddCrudEndpointsForType(e, ds, changes, "note", handlers.CRUDLAccessCheckers{
GetCheck: auth.Any(isOwner, isSharedWith),
PostCheck: open,
PutCheck: auth.Any(isOwner, isSharedWith),
DeleteCheck: isOwner,
LiveCheck: auth.Any(isOwner, isSharedWith),
})
Giving us a very nice way of checking what CRUD-operations require what rules, changing them, updating them etc.
Check out the example where this “service” can be found in its entirety. That example and the code behind it totals around ~1100 lines of code. The business logic (documents.go) with types, access rules, and endpoints total 117 LOC.
Whats the point?
Isn’t this just a unrlealistic way too simple example? Few lines of code to get CRUD up and running - isn’t that what a framework like Spring Boot or ASP.NET does?
Well, yes and yes. But I also want to make a stand for simplicity. This project has 4 direct dependencies. We have about 1100 LOC powering this simple but extendable web service. We have a trivial way to add CRUD endpoints. We have a pubsub ready to use, in this example providing live updates of documents. There’s an in-memory K/V store for caching as well as a database ready to rock.
If we add authentication and built out our model a bit, maybe manually manage a few indexes in the in-mem K/V store, this could probably be deployed and serve a good amount of users as it is right now.
The point is: it could probably be this simple, or even simpler.