Declarative, type safe authz pattern
I've been working on an authz framework pattern that's declarative and type enforced, and I'm about to start rolling it out across my project. Thought I'd drop some info about it in a thread. Thoughts/feedback welcome! 🧵
4 Replies
Goals
- Make it obvious - authz functions that I can see and follow in my editor to understand and troubleshoot.
- Make it type safe - fully enforce proper authz with Typescript. Convex gives amazing underpinnings for this with typed ids and tables, enabling static authz assurances during coding. Great ergonomics so far.
- Make it flexible - give freedom to favor ubiquitous authz everywhere or choose to authz entire complex operations ahead of time to favor performance.
Implementation
- Establish a database layer solely responsible for direct db interactions
- The database layer handles everything a relational db handles, eg., cascading, getting relations, computed fields, timestamps, enforcing uniqueness, etc. through exported methods for each table
- Additionally for each table, authz methods are exported
- The authz methods accept a context and return a context with a new or augmented
authz
object - this object essentially carries ids that that are now authorized to be accessed
- The exported database read/write methods require a context with a properly typed authz object for the specific method being called
- This means I need to get proper authz at the call site before reading/writing, and it's enforced by typescript
- Finally, an assertAuthz
method is used in the db layer for each table, which simply statically checks to ensure the provided context includes proper authz for the provided id. It runs synchronously to allow checks against preauthorization with minimal performance impact.
Three authz methods are provided for each table: authz
, authzDeferred
, and authzBypass
.
- authzBypass
is specifically typed so that it's only acceptable to methods that allow it - necessary for certain operations that need authz without actually having a user, like webhooks.
- authzDeferred
accepts the userId as an argument, deferring to the caller to ensure the id is rightly acquired. This is primarily for things like scheduled functions, where auth info needs to be passed in. A function is also provided specifically for composing an authz context from an authz object passed into a convex function (typically a scheduled function).
- authz
is fully authoritative, as it authorizes for the currently authenticated user. The authz
functions generally get the current user and then call the table's authzDeferred
for consistency.
I won't be able to do much writing or anything about this approach at the moment, but I wanted to at least share an overview in case anyone is also figuring out authz for their own project and is trying to determine how to handle it in a declarative, flexible, type safe manner.Sweet, thanks for sharing! Would love to have it as a reference - maybe in #show-and-tell or a stack post, if / when you have more to share
@erquhart I'd love to talk to you about this as I'm about to do something similar with a more complicated auth structure
For sure, feel free to dm