erquhart
erquhart•15mo ago

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
erquhart
erquhartOP•15mo ago
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.
// Convex function
export const getAuthor = query({
args: { authorId: v.id('author') },
handler: async (ctx, { authorId }) => {
const authzCtx = await db.author.authz(ctx, authorId)
return db.author.get(authzCtx, authorId)
},
})

// db.author.get
export const get = async (
ctx: AuthzAuthorCtx<QueryCtx>,
id: Id<'author'>
) => {
assertAuthz(ctx, id)
return ctx.db.get(ctx, id)
}
// Convex function
export const getAuthor = query({
args: { authorId: v.id('author') },
handler: async (ctx, { authorId }) => {
const authzCtx = await db.author.authz(ctx, authorId)
return db.author.get(authzCtx, authorId)
},
})

// db.author.get
export const get = async (
ctx: AuthzAuthorCtx<QueryCtx>,
id: Id<'author'>
) => {
assertAuthz(ctx, id)
return ctx.db.get(ctx, id)
}
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.
ian
ian•15mo ago
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
magicseth
magicseth•15mo ago
@erquhart I'd love to talk to you about this as I'm about to do something similar with a more complicated auth structure
erquhart
erquhartOP•15mo ago
For sure, feel free to dm

Did you find this page helpful?