Authorization made most simple

Photo by Liam Tucker on Unsplash

Authorization made most simple

One of the things I often struggle with in my go applications is authorization. Lots of the tools out there are complex or made for use-cases much larger than my own. In a recent project I came up with a very simple approach.

TLDR; Checkout a generic form here: https://github.com/ryanfaerman/can

I'll show you what I came up with first and then, a slightly more generic version.

Let's define a couple interfaces and a model:


// Canner allows a resource to define if an account can perform 
// a given action.
type Canner interface {
    Can(account *Account, action string) error
}

// Verber is used to allow a resource to define the actions 
// it supports.
type Verber interface {
    Verbs() []string
}

type Account {
    ID        int
    Name      string
    Anonymous bool
    // ... use your actual model
}

var AnonymousAccount = Account{ID: -1, Name: "Anon", Anonymous: true}

And while we're at it, a basic container type:

// Policy is a map of actions to functions that
// return an error if the action cannot be performed for the 
// given account.
type Policy map[string]func(*Account) error

// Verbs implements the Verber interface.
func (p Policy) Verbs() []string {
    var verbs []string
    for verb := range p {
        verbs = append(verbs, verb)
    }
    return verbs
}

// Can implements the Canner interface.
func (p Policy) Can(account *Account, action string) error {
    if fn, ok := p[action]; ok {
        return fn(account)
    }
    return nil
}

// An example default policy.
var DefaultPolicy = Policy{
    // Anonymous users aren't allowed to view anything
    "view": func(account *Account) error {
        if account.Anonymous {
            return errors.New("must not be anonymous")
        }
    },

    // Editing is disabled for everyone
    "edit": func(account *Account) error {
        return errors.New("editing is forbidden")
    },
}

And, let's also create an example subject that we want to act on:

type BlogPost struct {
    Title string
    Body  string
    Owner *Account
}

func (b *BlogPost) Verbs() []string {
    return []string{"view", "edit", "promote", "delete"}
}

func (b *BlogPost) Can(account *Account, action string) error {
    switch action {
        case "view":
            // anyone can view
            return nil
        case "edit"
            if b.Owner.ID != account.ID {
                return errors.New("cannot edit someone elses blogpost")
            }
            return nil
        // case ...
    }
    return nil
}

Now with those few things, we can build out the actual function that makes this idea work:

func Can(account *Account, action string, resources ...any) error {
    if account == nil {
        account = AnonymousAccount
    }
    if len(resources) == 0 {
        resources = append(resources, DefaultPolicy)
    }

    for _, resource := range resources {
        if r, ok := resource.(Verber); ok {
            if !slices.Contains(r.Verbs(), action) {
                return ErrInvalidAction
            }
        }

        if r, ok := resource.(Canner); ok {
            if err := r.Can(account, action); err != nil {
                return errors.Join(ErrNotAuthorized, err)
            }
        }
    }

    return nil
}

// Some sugar!
func Cannot(account *Account, action string, resources ...any) error {
    !Can(account, action, resources...)
}

Now, scattered in our codebase we can do things like:

// ..
b := LoadBlogPost()
if Cannot(someAccount, "edit", b) {
    //... do something
}

Granted, this is partially contrived. In my application I'm using an actual model, not just a random struct, but most of it is pretty similar to what I have.

Two more fun things before we wrap this up.

If you're using templ, like I am, I made a couple helpers.

func CurrentAccount(ctx context.Context) *models.Account {
    // Get the current user from the account
    // Or return an AnonymousUser
}

func UserCan(ctx context.Context, action string, resources ...any) bool {
    if err := services.Authorization.Can(CurrentAccount(ctx), action, resources...); err != nil {
        return false
    }
    return true
}

templ Can(action string, resources ...any) {
    if UserCan(ctx, action, resources...) {
        { children... }
    }
}

templ Cannot(action string, resources ...any) {
    if !UserCan(ctx, action, resources...) {
        { children... }
    }
}

// in some templ file:

@Can("edit", theBlogPost) {
    <h1>You can Edit this!</h1>
}
@Cannot("edit", theBlogPost) {
    <h1>Sorry bud! You cannot do that</h1>
}

I've split up the UserCan from Can and Cannot because doing assignments and error checking is a bit of a pain in templ.

If you want to see this in a less of a contrived fashion, you can view it in action in my application: https://github.com/ryanfaerman/netctl/blob/master/internal/services/authorization.go

I've also made a simple package with a generic form here: https://github.com/ryanfaerman/can