rolify

package module
v0.0.0-...-4d2e717 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: 0 Imported by: 0

README

go-ruby-rolify/rolify

rolify — go-ruby-rolify

Docs License Go Coverage

A pure-Go (no cgo) reimplementation of the deterministic core of Ruby's rolify gem — role management with global, class-scoped, and instance-scoped roles. It reproduces the role model, the user-side API (add_role / has_role? / remove_role / roles / has_all_roles? / has_any_role?), the resource-side helpers (applied_roles / roles_to_administrate), the faithful scope-matching semantics, the :any wildcard, and the strict_rolify / role_cache options — without any Ruby runtime.

It is the role engine for go-embedded-ruby, but a standalone, reusable module.

What it is — and isn't. Everything rolify does above the database is deterministic and needs no interpreter, so it lives here as pure Go: creating roles, joining them to users, and — crucially — the scope-matching rules that decide whether a stored role satisfies a query. Persistence itself is a host seam: the Store interface (FindRole / CreateRole / AssignRole / RemoveRole / RolesFor / AllRoles) is the only piece that touches storage. The default in-repo MemoryStore is an allocation-only implementation the tests drive; a future rbgo binding wires the seam to ActiveRecord, mirroring the gem, whose only persistent concern is the roles table and the users↔roles join.

Features

Faithful port of the rolify role engine:

  • Role modelRole{Name, ResourceType, ResourceID}: global (both empty), class-scoped (type set, id empty), or instance-scoped (both set).
  • User APIAddRole (idempotent), HasRole, HasCachedRole, RemoveRole, Roles, HasAllRoles, HasAnyRole.
  • ScopesGlobal(), ClassScope(type), InstanceScope(type, id), and the Any() wildcard (Ruby's has_role? :admin, :any).
  • Faithful matching — a global role matches any query; a class role matches any instance of that class and the class itself; an instance role matches exactly; :any matches a role of any scope.
  • Resource sideAppliedRoles(type, id) (instance + class + global roles on a resource) and RolesToAdministrate(type, id) (roles scoped to the resource or its class).
  • OptionsStrict(true) (strict_rolify: no scope inheritance for class/instance queries) and Cache(true) (role_cache: load a user's roles once, invalidate on mutation).
  • Persistence seamStore; the default MemoryStore opens no storage of its own, and a host binding wires it to ActiveRecord.

CGO-free, dependency-free (stdlib only), 100% test coverage, gofmt + go vet clean, and green across the six 64-bit Go targets (amd64, arm64, riscv64, loong64, ppc64le, s390x — big-endian) plus js/wasm and wasip1/wasm.

Install

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

Usage

package main

import (
	"fmt"

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

func main() {
	rf := rolify.New(rolify.NewMemoryStore())
	user := rf.User("42")

	user.AddRole("admin", rolify.Global())                    // add_role :admin
	user.AddRole("moderator", rolify.ClassScope("Forum"))     // add_role :moderator, Forum
	user.AddRole("owner", rolify.InstanceScope("Forum", "7")) // add_role :owner, @forum

	fmt.Println(user.HasRole("admin", rolify.Global()))                      // true
	fmt.Println(user.HasRole("moderator", rolify.InstanceScope("Forum", "1"))) // true (class role)
	fmt.Println(user.HasRole("owner", rolify.Any()))                        // true
	fmt.Println(user.HasRole("owner", rolify.InstanceScope("Forum", "8")))    // false

	user.RemoveRole("admin", rolify.Global())

	// Resource side.
	rf.AppliedRoles("Forum", "7")        // instance + class + global roles on @forum
	rf.RolesToAdministrate("Forum", "7") // roles scoped to @forum or the Forum class
}
Strict mode & caching
rf := rolify.New(rolify.NewMemoryStore(), rolify.Strict(true), rolify.Cache(true))
user := rf.User("42")
user.AddRole("admin", rolify.Global())

// strict_rolify: a global role no longer satisfies a narrower query.
user.HasRole("admin", rolify.Global())               // true
user.HasRole("admin", rolify.ClassScope("Forum"))    // false (strict)
Injecting a store (hosts)
type ActiveRecordStore struct{ /* db handle */ }
// ... implement rolify.Store: FindRole / CreateRole / AssignRole / RemoveRole /
//     RolesFor / AllRoles over your users↔roles tables ...

rf := rolify.New(&ActiveRecordStore{ /* ... */ })

Value model

gem this package
user.add_role :admin user.AddRole("admin", Global())
user.add_role :mod, Forum user.AddRole("mod", ClassScope("Forum"))
user.add_role :mod, @forum user.AddRole("mod", InstanceScope("Forum", id))
user.has_role? :admin user.HasRole("admin", Global())
user.has_role? :admin, :any user.HasRole("admin", Any())
user.has_cached_role? :admin user.HasCachedRole("admin", Global())
user.remove_role :admin user.RemoveRole("admin", Global())
user.roles user.Roles()
user.has_all_roles?(…) user.HasAllRoles(…)
user.has_any_role?(…) user.HasAnyRole(…)
resourcify + @forum.applied_roles rf.AppliedRoles("Forum", id)
@forum.roles_to_administrate rf.RolesToAdministrate("Forum", id)
strict_rolify / role_cache Strict(true) / Cache(true)
the roles table + users↔roles join Store (host seam; MemoryStore in tests)

Scope semantics

A role names a permission and carries an optional scope:

  • Global (ResourceType == "" && ResourceID == "") — matches any query.
  • Class (ResourceType set, ResourceID == "") — matches any instance of that class and the class itself.
  • Instance (ResourceType and ResourceID set) — matches exactly that instance.

A query names a role and a Scope (Global / ClassScope / InstanceScope / Any). Any() is the :any wildcard: it matches a role of any scope. Under Strict(true) a class/instance query is matched exactly — a broader (global or class) role no longer satisfies it.

Tests & coverage

The suite is deterministic and storage-local: the in-repo MemoryStore backs every test, so the cross-arch qemu lanes and the Windows lane all hold coverage at 100% — including global vs. class vs. instance matching, the :any wildcard, idempotent duplicate adds, removing a nonexistent/unheld role, has_all_roles? / has_any_role?, strict mode, and the role cache.

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

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, …)

License

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

Documentation

Overview

Package rolify is a pure-Go (CGO-free) reimplementation of the deterministic core of Ruby's rolify gem — role management with global, class-scoped, and instance-scoped roles. It reproduces the role model, the user-side API (add_role / has_role? / remove_role / roles / has_all_roles? / has_any_role?), the resource-side helpers (applied_roles / roles_to_administrate), the faithful scope-matching semantics, the :any wildcard, and the strict_rolify / role_cache options — without any Ruby runtime.

It is the role engine for [go-embedded-ruby](https://github.com/go-embedded-ruby/ruby), but a standalone, reusable module.

What it is — and isn't

Everything rolify does above the database is deterministic and needs no interpreter, so it lives here as pure Go: creating roles, joining them to users, and — crucially — the scope-matching rules that decide whether a stored role satisfies a query. Persistence itself is a host seam: the Store interface (FindRole / CreateRole / AssignRole / RemoveRole / RolesFor / AllRoles) is the only piece that touches storage. The default in-repo store is MemoryStore, an allocation-only implementation used by the tests; a future rbgo binding wires the seam to ActiveRecord, mirroring the gem, whose only persistent concern is the roles table and the users↔roles join.

Scope semantics

A Role carries a Name and an optional scope expressed as a ResourceType and ResourceID:

  • Global role: ResourceType == "" and ResourceID == "". Matches any query.
  • Class role: ResourceType set, ResourceID == "". Matches any instance of that class and the class itself.
  • Instance role: ResourceType and ResourceID set. Matches exactly.

A query names a role and a Scope: Global, ClassScope, InstanceScope, or the Any wildcard (Ruby's has_role? :admin, :any) which matches a role of any scope. The strict_rolify option (Strict) disables scope inheritance so a global or class role no longer satisfies a narrower class/instance query.

Flow

rf := rolify.New(rolify.NewMemoryStore())
user := rf.User("42")

user.AddRole("admin", rolify.Global())                 // add_role :admin
user.AddRole("moderator", rolify.ClassScope("Forum"))  // add_role :moderator, Forum
user.AddRole("owner", rolify.InstanceScope("Forum", "7"))

user.HasRole("admin", rolify.Global())                    // true
user.HasRole("moderator", rolify.InstanceScope("Forum", "1")) // true (class role)
user.HasRole("owner", rolify.Any())                       // true
user.RemoveRole("admin", rolify.Global())

Value model

A host (go-embedded-ruby / rbgo) maps its Ruby Role / user / resource objects to and from these shapes:

user.add_role :admin              -> User.AddRole("admin", Global())
user.add_role :mod, Forum         -> User.AddRole("mod", ClassScope("Forum"))
user.add_role :mod, @forum        -> User.AddRole("mod", InstanceScope("Forum", id))
user.has_role? :admin             -> User.HasRole("admin", Global())
user.has_role? :admin, :any       -> User.HasRole("admin", Any())
user.has_cached_role? :admin      -> User.HasCachedRole("admin", Global())
user.remove_role :admin           -> User.RemoveRole("admin", Global())
user.roles                        -> User.Roles()
user.has_all_roles?(...)          -> User.HasAllRoles(...)
user.has_any_role?(...)           -> User.HasAnyRole(...)
@forum.applied_roles              -> Rolify.AppliedRoles("Forum", id)
@forum.roles_to_administrate      -> Rolify.RolesToAdministrate("Forum", id)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type MemoryStore

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

MemoryStore is an allocation-only Store: a slice of roles plus a userID→role-ID join set. It is deterministic (insertion order is preserved on every read), so it behaves identically under qemu emulation and wasm.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty in-memory Store.

func (*MemoryStore) AllRoles

func (m *MemoryStore) AllRoles() []*Role

AllRoles implements Store.

func (*MemoryStore) AssignRole

func (m *MemoryStore) AssignRole(userID string, role *Role)

AssignRole implements Store.

func (*MemoryStore) CreateRole

func (m *MemoryStore) CreateRole(name, resourceType, resourceID string) *Role

CreateRole implements Store.

func (*MemoryStore) FindRole

func (m *MemoryStore) FindRole(name, resourceType, resourceID string) (*Role, bool)

FindRole implements Store.

func (*MemoryStore) RemoveRole

func (m *MemoryStore) RemoveRole(userID string, role *Role) bool

RemoveRole implements Store.

func (*MemoryStore) RolesFor

func (m *MemoryStore) RolesFor(userID string) []*Role

RolesFor implements Store.

type Option

type Option func(*Rolify)

Option configures a Rolify engine.

func Cache

func Cache(on bool) Option

Cache sets rolify's role_cache option. When on, a User loads its roles once and reuses that snapshot until a mutation (AddRole / RemoveRole) invalidates it.

func Strict

func Strict(on bool) Option

Strict sets rolify's strict_rolify option. In strict mode a class/instance query is matched exactly: a broader (global or class) role no longer satisfies a narrower query.

type Query

type Query struct {
	Name  string
	Scope Scope
}

Query pairs a role name with a scope for the multi-role predicates.

type Role

type Role struct {
	ID           int64
	Name         string
	ResourceType string
	ResourceID   string
}

Role is rolify's role value model: a Name plus an optional scope expressed as a ResourceType and ResourceID. The empty string denotes an absent scope component (rolify stores SQL NULL). ID is the store-assigned identity used for the users↔roles join; the value the gem cares about is the (Name, ResourceType, ResourceID) triple.

  • Global role: ResourceType == "" and ResourceID == "".
  • Class role: ResourceType set, ResourceID == "".
  • Instance role: ResourceType and ResourceID set.

type Rolify

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

Rolify is the role engine bound to a Store and a set of options.

func New

func New(store Store, opts ...Option) *Rolify

New returns a role engine over store, applying opts.

func (*Rolify) AppliedRoles

func (rf *Rolify) AppliedRoles(resourceType, resourceID string) []*Role

AppliedRoles returns the roles that apply to the resource instance identified by (resourceType, resourceID): instance-scoped roles on it, class-scoped roles on its class, and global roles (Ruby: @resource.applied_roles).

func (*Rolify) RolesToAdministrate

func (rf *Rolify) RolesToAdministrate(resourceType, resourceID string) []*Role

RolesToAdministrate returns the roles that administrate the resource instance identified by (resourceType, resourceID): the roles scoped to this resource or to its class, excluding global roles (Ruby: @resource.roles_to_administrate).

func (*Rolify) User

func (rf *Rolify) User(id string) *User

User returns a handle for the user identified by id. The handle carries the role cache (when the Cache option is set).

type Scope

type Scope struct {
	Type string
	ID   string
	// contains filtered or unexported fields
}

Scope names the target of a role query or assignment. Build one with Global, ClassScope, InstanceScope, or Any. The zero Scope is Global.

func Any

func Any() Scope

Any is the wildcard query (Ruby: has_role? :admin, :any); it matches a role of any scope. Using it for an assignment is treated as Global.

func ClassScope

func ClassScope(resourceType string) Scope

ClassScope scopes to a resource class (Ruby: add_role :mod, Forum).

func Global

func Global() Scope

Global is the unscoped query/assignment (Ruby: add_role :admin / has_role? :admin).

func InstanceScope

func InstanceScope(resourceType, resourceID string) Scope

InstanceScope scopes to a single resource instance (Ruby: add_role :mod, @forum).

type Store

type Store interface {
	// FindRole returns the role matching the exact (name, resourceType,
	// resourceID) triple, and whether it was found.
	FindRole(name, resourceType, resourceID string) (*Role, bool)
	// CreateRole persists a new role for the triple and returns it.
	CreateRole(name, resourceType, resourceID string) *Role
	// AssignRole joins userID to role. It is idempotent: joining an
	// already-joined role is a no-op.
	AssignRole(userID string, role *Role)
	// RemoveRole unjoins userID from role, reporting whether a join existed.
	RemoveRole(userID string, role *Role) bool
	// RolesFor returns every role joined to userID, in a deterministic order.
	RolesFor(userID string) []*Role
	// AllRoles returns every persisted role, in a deterministic order. It backs
	// the resource-side helpers (applied_roles / roles_to_administrate).
	AllRoles() []*Role
}

Store is the persistence seam. It models rolify's roles table and its users↔roles join over the Role value model. The in-repo MemoryStore backs the tests; a host binding wires it to ActiveRecord.

type User

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

User is a rolify role holder: the object the gem mixes add_role / has_role? / remove_role into.

func (*User) AddRole

func (u *User) AddRole(name string, s Scope) *Role

AddRole grants name in scope s (Ruby: add_role). It is idempotent: an existing role is reused and re-granting it changes nothing. It returns the role.

func (*User) HasAllRoles

func (u *User) HasAllRoles(queries ...Query) bool

HasAllRoles reports whether the user has every listed role (Ruby: has_all_roles?). With no queries it reports false.

func (*User) HasAnyRole

func (u *User) HasAnyRole(queries ...Query) bool

HasAnyRole reports whether the user has at least one listed role (Ruby: has_any_role?).

func (*User) HasCachedRole

func (u *User) HasCachedRole(name string, s Scope) bool

HasCachedRole reports whether the user has name in scope s using only the cached role snapshot (Ruby: has_cached_role?). The snapshot is loaded once on first use and never re-queries the store until a mutation invalidates it, so it answers without touching persistence even when the Cache option is off.

func (*User) HasRole

func (u *User) HasRole(name string, s Scope) bool

HasRole reports whether the user has name in scope s (Ruby: has_role?). Under strict_rolify a class/instance query is matched exactly; otherwise scope inheritance applies (a global role satisfies any query, a class role satisfies its instances).

func (*User) RemoveRole

func (u *User) RemoveRole(name string, s Scope) bool

RemoveRole revokes name in scope s (Ruby: remove_role). Removing a role the user does not have — or one that does not exist — is a no-op; the returned bool reports whether a grant was removed.

func (*User) Roles

func (u *User) Roles() []*Role

Roles returns every role granted to the user (Ruby: user.roles).

Jump to

Keyboard shortcuts

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