cancancan

package module
v0.0.0-...-f61cac2 Latest Latest
Warning

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

Go to latest
Published: Jul 17, 2026 License: BSD-3-Clause Imports: 1 Imported by: 0

README

go-ruby-cancancan/cancancan

cancancan — go-ruby-cancancan

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the rule ENGINE of Ruby's CanCanCan authorization gem (v3.6, MRI 4.0.5) — the Ability rule store and the can? / cannot? / authorize! matching semantics — without any Ruby runtime. It mirrors the gem's observable behaviour: reverse-precedence rule resolution (last matching rule wins, cannot overrides can), :manage / :all wildcards, action aliases (readindex,show, …), hash-attribute and block conditions, and CanCan::AccessDenied on denial.

It is the CanCanCan backend for go-embedded-ruby, but is a standalone, reusable module with no dependency on the Ruby runtime — a sibling of go-ruby-connection-pool and go-ruby-set.

MRI-faithful engine, not a Rails plug. This is the authorization engine — the rule store and matching semantics — not the Rails controller/view integration. The Go library owns the rules and the matching; a host (rbgo) supplies the Ruby condition-blocks and attribute reads through two function seams.

The two Ruby seams

The engine cannot know how to read a Ruby object's attribute or how to run a Ruby condition block, so the host injects those as functions on the Ability:

// reads subject.send(key) for hash-condition matching
AttrGet func(subject any, key string) any
// runs the ruleID'th rule's Ruby proc against an instance
BlockEval func(ruleID int, subject any) bool

Can / Cannot return the new rule's integer ID, so the host can register the corresponding Ruby block under it and dispatch through BlockEval. A subject instance may implement the optional Classified interface (CanCanClass() / CanCanAncestors()) so a rule on a class or superclass matches it — the gem's matches_subject_class?.

Install

go get github.com/go-ruby-cancancan/cancancan

Usage

package main

import (
	"fmt"

	"github.com/go-ruby-cancancan/cancancan"
)

type Article struct {
	Published bool
	AuthorID  int
}

func (Article) CanCanClass() cancancan.Class       { return "Article" }
func (Article) CanCanAncestors() []cancancan.Class { return []cancancan.Class{"Record"} }

func main() {
	a := cancancan.New()
	a.AttrGet = func(s any, k string) any {
		art := s.(*Article)
		if k == "published" {
			return art.Published
		}
		return art.AuthorID
	}

	//   can :read,    Article, published: true
	//   can :manage,  Article, author_id: 1      # manage ⇒ every action
	//   cannot :destroy, Article
	a.Can("read", cancancan.Class("Article"), map[string]any{"published": true})
	a.Can(cancancan.Manage, cancancan.Class("Article"), map[string]any{"author_id": 1})
	a.Cannot("destroy", cancancan.Class("Article"))

	pub := &Article{Published: true, AuthorID: 2}
	mine := &Article{Published: false, AuthorID: 1}

	fmt.Println(a.CanQ("read", pub))                       // true  (published)
	fmt.Println(a.CanQ("show", pub))                       // true  (read alias)
	fmt.Println(a.CanQ("update", mine))                    // true  (manage author_id:1)
	fmt.Println(a.CanQ("destroy", mine))                   // false (cannot overrides)
	fmt.Println(a.CanQ("read", cancancan.Class("Article"))) // true  (possible)

	if err := a.AuthorizeBang("destroy", mine); err != nil {
		fmt.Println(err) // *cancancan.AccessDenied
	}
}

API

func New() *Ability                                  // seeded with default aliases

// rule store (return the new rule's ID for block wiring)
func (a *Ability) Can(action, subject any, conditions ...any) int    // can
func (a *Ability) Cannot(action, subject any, conditions ...any) int // cannot
func (a *Ability) AliasAction(to Action, actions ...Action)          // alias_action

// queries
func (a *Ability) CanQ(action Action, subject any) bool     // can?
func (a *Ability) CannotQ(action Action, subject any) bool  // cannot?
func (a *Ability) AuthorizeBang(action Action, subject any) error // authorize!

// wildcards & markers
var  Manage manageT   // :manage — matches every action
var  All    allT      // :all    — matches every subject
type Block  struct{}  // marks a rule as carrying a Ruby condition block
type Class  string    // a subject class token

// seams (fields on Ability)
AttrGet   func(subject any, key string) any
BlockEval func(ruleID int, subject any) bool

// optional subject interface for class matching
type Classified interface {
	CanCanClass() Class
	CanCanAncestors() []Class
}

// modeled errors (all report true from IsCanCanError)
type Error                     struct{ Msg string }                 // CanCan::Error
type AccessDenied              struct{ Msg string; Action Action; Subject any } // CanCan::AccessDenied
type AuthorizationNotPerformed struct{ Msg string }                 // CanCan::AuthorizationNotPerformed
func IsCanCanError(err error) bool

Semantics. CanQ scans rules from last-defined to first-defined and returns the base_behavior of the first whose action, subject, and conditions all match — so a later rule overrides an earlier one and a cannot overrides a can. :manage matches every action; :all every subject. Action aliases expand a declared action to the actions it grants (read grants index/show, etc.). Hash conditions match an attribute by equality, by membership when the value is a []any, or recursively when the value is a nested map[string]any (an association). A conditional rule cannot be evaluated against a class rather than an instance, so a class-level query is treated as possible — mirroring CanCanCan's "you can act on some such subject".

Tests & coverage

The suite pairs deterministic, ruby-free tests (which alone hold coverage at 100%, so the qemu cross-arch and Windows lanes pass the gate) with a differential CanCanCan oracle: a shared Ability is defined here and in the real gem, and every can?(action, subject) over a matrix of actions × subjects is compared. CanCanCan is not part of Ruby core, so the oracle skips itself where the gem is not installed (the CI ruby lanes) and validates locally where it is.

COVERPKG=$(go list ./... | paste -sd, -)
go test -race -coverpkg="$COVERPKG" -coverprofile=cover.out ./...
go tool cover -func=cover.out | tail -1   # 100.0%

CGO-free, dependency-free, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x) and three OSes (Linux, macOS, Windows).

License

BSD-3-Clause — see LICENSE. Copyright the go-ruby-cancancan/cancancan authors.

WebAssembly

Being pure Go (CGO=0), this library also compiles to WebAssembly — both GOOS=js GOARCH=wasm (browser / Node.js) and GOOS=wasip1 GOARCH=wasm (WASI). CI builds both targets on every push, alongside the six 64-bit native/qemu arches.

GOOS=js     GOARCH=wasm go build ./...   # browser / Node
GOOS=wasip1 GOARCH=wasm go build ./...   # WASI (wasmtime, wasmer, wasmedge, …)

Documentation

Overview

Package cancancan is a pure-Go (no cgo) reimplementation of the rule ENGINE of Ruby's CanCanCan authorization gem (v3.6, MRI 4.0.5) — the Ability rule store and the can?/cannot?/authorize! matching semantics — without any Ruby runtime. It is the CanCanCan backend for go-embedded-ruby but is a standalone, reusable module with no dependency on the Ruby runtime.

What it models

An Ability collects can/cannot rules over (action, subject) pairs. A query (CanQ) resolves by scanning the rules in REVERSE definition order and taking the first whose action, subject, and conditions all match — so a later rule overrides an earlier one and a cannot overrides a can, exactly as CanCanCan's Ability#relevant_rules + detect do.

  • Actions are symbols. Manage is the wildcard (:manage) matching every action. Action aliases expand a declared action to the actions it grants: the CanCanCan defaults are read→{index,show}, create→{new}, update→{edit}, and AliasAction adds more (the gem's alias_action).
  • Subjects are class tokens (Class), the wildcard All (:all), or specific instances. A Class rule matches that class and its instances; an instance rule matches by value equality.
  • Conditions are a hash (attribute matcher) or a block/lambda. Neither can be evaluated when the subject is a class rather than an instance, so — as in CanCanCan — a class query against a conditional rule is treated as possible (it means "you can act on SOME such subject").

The Ruby seams

The engine owns the rule store and the matching semantics; the two things it cannot know — how to read a Ruby object's attribute and how to run a Ruby condition block — are injected as function seams on the Ability:

  • AttrGet(subject, key) reads an attribute for hash-condition matching. It stands in for `subject.send(key)`. The go-embedded-ruby binding dispatches the Ruby reader here.
  • BlockEval(ruleID, subject) evaluates a rule's Ruby condition block against an instance. Can/Cannot return the rule's ID so the host can register the corresponding Ruby proc under it. It stands in for `block.call(subject)`.

Index

Constants

View Source
const DefaultAccessDeniedMessage = "You are not authorized to access this page."

DefaultAccessDeniedMessage is the message CanCan::AccessDenied uses when the caller supplies none, matching the gem's default.

Variables

View Source
var All = allT{}

All is the wildcard subject (:all): a rule declared with it matches every subject, mirroring CanCanCan's :all.

View Source
var Manage = manageT{}

Manage is the wildcard action (:manage): a rule declared with it matches every action, mirroring CanCanCan's :manage.

Functions

func IsCanCanError

func IsCanCanError(err error) bool

IsCanCanError reports whether err is one of the modeled CanCan error types, mirroring the gem's `rescue CanCan::Error` (which catches AccessDenied and AuthorizationNotPerformed too, since both subclass CanCan::Error).

Types

type Ability

type Ability struct {

	// AttrGet reads subject's attribute key for hash-condition matching. It
	// stands in for `subject.send(key)`; required only when hash-conditioned
	// rules are evaluated against instances.
	AttrGet func(subject any, key string) any
	// BlockEval evaluates the rule ruleID's Ruby condition block against
	// subject. It stands in for `block.call(subject)`; required only when
	// block-conditioned rules are evaluated against instances.
	BlockEval func(ruleID int, subject any) bool
	// contains filtered or unexported fields
}

Ability is a store of can/cannot rules plus the matching engine, mirroring a class that `include CanCan::Ability`. Construct it with New; set AttrGet and BlockEval before evaluating hash- or block-conditioned rules.

func New

func New() *Ability

New returns an empty Ability seeded with CanCanCan's default action aliases (read→index,show; create→new; update→edit).

func (*Ability) AliasAction

func (a *Ability) AliasAction(to Action, actions ...Action)

AliasAction declares that granting `to` also grants `actions`, mirroring the gem's `alias_action *actions, to: to`. It appends to any existing aliases for `to` (the defaults included).

func (*Ability) AuthorizeBang

func (a *Ability) AuthorizeBang(action Action, subject any) error

AuthorizeBang returns nil when the ability permits action on subject, or an *AccessDenied otherwise, mirroring `authorize!(action, subject)` (which raises CanCan::AccessDenied on denial). The returned error carries the action and subject.

func (*Ability) Can

func (a *Ability) Can(action, subject any, conditions ...any) int

Can adds a permission rule. action is an Action/string, Manage, or a slice of these; subject is a Class, All, an instance, or a slice of these; conditions are an optional hash (map[string]any) and/or Block. It returns the new rule's ID (for wiring a Ruby block via BlockEval). It mirrors `can action, subject, conditions, &block`.

func (*Ability) CanQ

func (a *Ability) CanQ(action Action, subject any) bool

CanQ reports whether the ability permits action on subject, mirroring `can?(action, subject)`. It scans rules from last-defined to first-defined and returns the base_behavior of the first whose action, subject, and conditions all match; if none matches it returns false.

func (*Ability) Cannot

func (a *Ability) Cannot(action, subject any, conditions ...any) int

Cannot adds a revocation rule with the same argument shape as Can, mirroring `cannot action, subject, conditions, &block`. A matching Cannot overrides an earlier matching Can.

func (*Ability) CannotQ

func (a *Ability) CannotQ(action Action, subject any) bool

CannotQ is the negation of CanQ, mirroring `cannot?(action, subject)`.

type AccessDenied

type AccessDenied struct {
	Msg     string
	Action  Action
	Subject any
}

AccessDenied mirrors CanCan::AccessDenied (a CanCan::Error subclass). It is returned by AuthorizeBang when the ability denies the (Action, Subject) pair. It carries the denied action and subject so a host can render a tailored message, exactly as the gem exposes #action and #subject.

func NewAccessDenied

func NewAccessDenied(message string, action Action, subject any) *AccessDenied

NewAccessDenied builds an AccessDenied for the denied (action, subject). An empty message is replaced by DefaultAccessDeniedMessage, mirroring CanCan::AccessDenied.new(message, action, subject).

func (*AccessDenied) Error

func (e *AccessDenied) Error() string

type Action

type Action string

Action is an action name — a Ruby symbol such as :read or :update.

type AuthorizationNotPerformed

type AuthorizationNotPerformed struct{ Msg string }

AuthorizationNotPerformed mirrors CanCan::AuthorizationNotPerformed (a CanCan::Error subclass). The gem raises it from check_authorization when a controller action finishes without ever calling authorize!; it is provided here so a host controller layer can model the same guarantee.

func (*AuthorizationNotPerformed) Error

func (e *AuthorizationNotPerformed) Error() string

type Block

type Block struct{}

Block marks a rule as carrying a Ruby condition block/lambda. Pass it as a condition to Can/Cannot; at match time against an instance the engine calls Ability.BlockEval(ruleID, subject). It stands in for the gem's `&block`.

type Class

type Class string

Class is a subject class token — a Ruby class identified by name. A rule declared with a Class matches that class itself and any of its instances (see Classified).

type Classified

type Classified interface {
	CanCanClass() Class
	CanCanAncestors() []Class
}

Classified is the optional interface a subject instance may implement so the engine can match it against a Class rule. The go-embedded-ruby binding wraps a Ruby object to report its class and ancestor chain here, letting a rule on a superclass match an instance of a subclass — the gem's matches_subject_class?.

type Error

type Error struct{ Msg string }

Error is the base error type, mirroring CanCan::Error (a StandardError subclass in the gem). AccessDenied and AuthorizationNotPerformed are its modeled subclasses; IsCanCanError reports membership in this family.

func (*Error) Error

func (e *Error) Error() string

Jump to

Keyboard shortcuts

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