access

package module
v0.1.1-0...-e473059 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 14, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

README

github.com/ubgo/auth/plugin/access

A permission vocabulary and the roles built from it, the Go shape of better-auth's access plugin (createAccessControl, ac.newRole, role.authorize). The organization plugin builds its roles from it, the accessadmin adapter gives the admin plugin fine-grained roles with it, and any app can use it on its own: package access has no storage, no transport and imports only the standard library.

Install

go get github.com/ubgo/auth/plugin/access

Its own module, because a permission model is a policy, not authentication: the admin and organization plugins ask an Authorizer and the apikey plugin a PermissionChecker, and this is one way to answer them. An app that decides permissions with Casbin, OpenFGA, OPA or rules of its own never compiles it. It brings no third-party dependency.

Use it

Example · plugin/access/example_test.go:12-28

func Example() {
	ac := access.MustNew(access.Statements{
		"project": {"create", "share", "delete"},
		"billing": {"read", "update"},
	})
	editor := ac.MustRole(access.Permissions{"project": {"create", "share"}})
	accountant := ac.MustRole(access.Permissions{"billing": {"read", "update"}})

	fmt.Println(editor.Allows(access.Request{"project": access.All("create", "share")}))
	fmt.Println(editor.Authorize(access.Request{"project": access.Any("delete", "share")}, access.And) == nil)
	err := accountant.Authorize(access.Request{"project": access.All("create")}, access.And)
	fmt.Println(errors.Is(err, access.ErrDenied), err)
	// Output:
	// true
	// true
	// true access: denied: You are not allowed to access resource: project
}

API

Name What it does
Statements Resources and the actions each allows: the whole vocabulary
New(Statements), MustNew Declares the vocabulary; refuses an empty one, a resource with no action, and an empty or repeated action
(*AccessControl).NewRole(Permissions), MustRole A role granting a subset of the statements; ErrUnknownResource or ErrUnknownAction for anything undeclared, so a typo fails at start-up
(*AccessControl).Check(Permissions) The same validation, for permissions stored at run time before they are saved
(*AccessControl).Statements() A copy of the vocabulary
Permissions Actions per resource; Request() turns it into a check requiring every action, Clone() copies it
Request, All(actions...), Any(actions...) A check: per resource, every action (AND) or one of them (OR)
Role.Authorize(Request, Connector) nil when allowed; And (default) needs every resource, Or one. A resource the role grants nothing on fails at once, and an empty request is denied
Role.Allows(Request) Authorize with And, as a boolean
Role.Permissions() A copy of what the role grants
ErrDenied Wrapped by every denial, with better-auth's message: You are not allowed to access resource: X, unauthorized to access resource "X", or Not authorized

The zero Role grants nothing. An AccessControl and a Role are immutable once built and safe for concurrent use.

With the admin plugin: accessadmin

Package accessadmin is the adapter that decides the admin plugin's permissions with access roles (better-auth's admin ac and roles): admin.WithAuthorizer(accessadmin.MustNew(ac, roles)).

Name What it does
DefaultAccessControl The access control of admin.DefaultStatements
DefaultRoles() admin.DefaultRoles() as access roles: admin (everything but impersonate-admins) and user (nothing)
New(ac, roles), MustNew An admin.Authorizer and admin.RoleCatalog; roles are added to DefaultRoles() or replace one by name. Refuses a nil access control (ErrNoAccessControl), a role naming an undeclared statement and a role name that is empty, padded or has a comma
(*Authorizer).Authorize Allowed when one of the request's roles passes the whole request under its connector; never fails
(*Authorizer).DefinesRole The role names set-role accepts

TestDefaultRolesMatchTheBuiltIn checks on 5000 random requests that the adapter's default roles answer exactly as the admin plugin's built-in ones.

With the apikey plugin: accessapikey

Package accessapikey checks key permissions with an access control, so the statements your roles use are the vocabulary of your API keys too: apikey.WithPermissions(accessapikey.New(ac)). Check is ac.Check and Grants is role.Allows over the key's permissions that the statements still declare (a removed resource or action is skipped, and asking for it is denied, as apikey.Vocabulary does); a nil access control refuses every permission. TestMatchesTheBuiltInVocabulary and TestShrunkVocabularyAgreesWithTheBuiltIn check on 5000 random cases each, the second after a resource and an action were removed, that it answers as apikey.Vocabulary over the same statements, and TestR36_KeyPermissionsWithAccess runs the whole key-permission flow through it on every engine.

Documentation

Overview

Package access is a permission vocabulary and the roles built from it: an app (or a plugin such as organization) declares which actions exist on which resources, defines roles as subsets of those statements, and asks a role whether it may do something.

It is the Go shape of better-auth's access plugin (createAccessControl, ac.newRole, role.authorize) and has no storage, no transport and no import of the auth core, so a role check costs a map lookup and the package can decide permissions for anything, not only organizations.

Why a role may only name declared statements

A typo in a role definition ("membr": ["delete"]) must fail when the role is built, at start-up, rather than silently granting nothing until someone notices a button that never works, or, worse, granting something under a resource name an attacker can also spell. NewRole refuses any resource or action the AccessControl did not declare.

Example

Example declares a vocabulary, builds two roles from it and checks them. ds:def id=example-gwg3tegh owner=@khanakia stability=stable

package main

import (
	"errors"
	"fmt"

	"github.com/ubgo/auth/plugin/access"
)

func main() {
	ac := access.MustNew(access.Statements{
		"project": {"create", "share", "delete"},
		"billing": {"read", "update"},
	})
	editor := ac.MustRole(access.Permissions{"project": {"create", "share"}})
	accountant := ac.MustRole(access.Permissions{"billing": {"read", "update"}})

	fmt.Println(editor.Allows(access.Request{"project": access.All("create", "share")}))
	fmt.Println(editor.Authorize(access.Request{"project": access.Any("delete", "share")}, access.And) == nil)
	err := accountant.Authorize(access.Request{"project": access.All("create")}, access.And)
	fmt.Println(errors.Is(err, access.ErrDenied), err)
}
Output:
true
true
true access: denied: You are not allowed to access resource: project

Index

Examples

Constants

This section is empty.

Variables

View Source
var (
	// ErrUnknownResource is a role, or a check, naming a resource the
	// statements do not declare.
	ErrUnknownResource = errors.New("access: unknown resource")
	// ErrUnknownAction is a role naming an action its resource does not
	// declare.
	ErrUnknownAction = errors.New("access: unknown action")
	// ErrEmptyStatements is an AccessControl with no resource, which no role
	// could grant anything from.
	ErrEmptyStatements = errors.New("access: no statements")
	// ErrDenied is a check the role does not pass; the returned error wraps
	// it with better-auth's message, which names the resource.
	ErrDenied = errors.New("access: denied")
)

Errors.

Functions

This section is empty.

Types

type AccessControl

type AccessControl struct {
	// contains filtered or unexported fields
}

AccessControl holds the declared statements and builds roles from them. Build it with New; it is immutable afterwards and safe for concurrent use.

func MustNew

func MustNew(s Statements) *AccessControl

MustNew is New for statements known at compile time; it panics on an error, which then shows at start-up.

func New

func New(s Statements) (*AccessControl, error)

New declares the statements. Each resource needs at least one action and no action may repeat within a resource, so the vocabulary has one spelling for everything.

func (*AccessControl) Check

func (ac *AccessControl) Check(p Permissions) error

Check reports whether p names only declared resources and actions: the validation NewRole applies, exported for a role stored at run time (the organization plugin's dynamic roles) before it is saved.

func (*AccessControl) MustRole

func (ac *AccessControl) MustRole(p Permissions) Role

MustRole is NewRole for a role known at compile time; it panics on an error.

func (*AccessControl) NewRole

func (ac *AccessControl) NewRole(p Permissions) (Role, error)

NewRole builds a role granting p, which must be a subset of the statements (Check).

func (*AccessControl) Statements

func (ac *AccessControl) Statements() Statements

Statements returns a copy of the declared statements.

type Actions

type Actions struct {
	Actions   []string
	Connector Connector
}

Actions is one resource's part of a Request: the actions asked for, and whether all of them (And, the default) or any one (Or) must be granted.

func All

func All(actions ...string) Actions

All asks for every one of actions.

func Any

func Any(actions ...string) Actions

Any asks for at least one of actions.

type Connector

type Connector string

Connector says how several checks combine: every one must pass (And) or one is enough (Or).

const (
	// And requires every action (or every resource); the default.
	And Connector = "AND"
	// Or requires one action (or one resource).
	Or Connector = "OR"
)

Connectors.

type Permissions

type Permissions map[string][]string

Permissions are a set of actions per resource, the same shape as Statements: what a role grants, or what a request asks for. JSON-encoded it is better-auth's permission object, so it is what an HTTP client sends.

func (Permissions) Clone

func (p Permissions) Clone() Permissions

Clone returns a deep copy, so a caller can change it without touching a role's own map.

func (Permissions) Request

func (p Permissions) Request() Request

Request turns p into a Request requiring every listed action.

type Request

type Request map[string]Actions

Request is a permission check: per resource, the actions asked for. Build one from Permissions with Permissions.Request when every action of every resource is required.

type Role

type Role struct {
	// contains filtered or unexported fields
}

Role is a set of granted actions per resource. The zero value grants nothing.

func (Role) Allows

func (r Role) Allows(req Request) bool

Allows is Authorize with And, as a boolean, for a caller that needs no reason.

func (Role) Authorize

func (r Role) Authorize(req Request, connector Connector) error

Authorize checks req against the role; nil means allowed. connector joins the resources (And when empty): with And every resource must pass, with Or one is enough. A resource the role grants nothing on fails at once, whatever the connector, as in better-auth. An empty request is denied: asking for nothing proves nothing.

A denial wraps ErrDenied with better-auth's wording, so errors.Is works and the message matches what a better-auth client shows.

func (Role) Permissions

func (r Role) Permissions() Permissions

Permissions returns a copy of what the role grants.

type Statements

type Statements map[string][]string

Statements are the resources of an application and the actions each one allows: {"project": ["create", "share", "delete"]}. The declared set is the whole vocabulary: a role can name nothing outside it.

Directories

Path Synopsis
Package accessadmin decides plugin/admin's permissions with plugin/access roles: better-auth's admin `ac` and `roles` options.
Package accessadmin decides plugin/admin's permissions with plugin/access roles: better-auth's admin `ac` and `roles` options.
Package accessapikey checks plugin/apikey's key permissions with a plugin/access access control, so the statements an app already declared for its roles are the vocabulary of its API keys too:
Package accessapikey checks plugin/apikey's key permissions with a plugin/access access control, so the statements an app already declared for its roles are the vocabulary of its API keys too:

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL