rbac

package module
v0.0.11 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: MIT Imports: 7 Imported by: 0

README

tinywasm/rbac

Role-based authorization runtime for TinyWasm applications. Authentication and sessions belong to tinywasm/auth. Both are siblings that depend only on tinywasm/user and never on each other.

BREAKING CHANGE: DeleteRole now returns rbac.ErrRoleNotFound when attempting to delete a non-existent role, instead of returning nil.

BREAKING CHANGE: GetRoleByCode now returns rbac.ErrRoleNotFound (not orm.ErrNotFound) when the code doesn't exist in the project, and rbac.ErrDuplicateRoleCode if more than one row matches (should not happen after the unique index, but is now reported instead of silently picking one). Any caller comparing err == orm.ErrNotFound after GetRoleByCode must switch to err == rbac.ErrRoleNotFound — the code still compiles either way, so this fails silently at runtime (a 500 where a 404 used to be), not at compile time.

flowchart TD
    U[user] --> R[rbac]
    U --> A[auth]
    R --> C[app]
    A --> C

Documentation

  • Architecture — Dependency rules and authorization mechanics

Usage

Every table — and every call — carries a projectID: one Service over one database serves every consuming project (misitio, mjosefa-cms, ...) without their roles/permissions colliding or leaking into each other.

Schema reconciliation (rbac.Migrate) is performed once at deploy time, not inside rbac.New:

import (
    "github.com/tinywasm/rbac"
    "github.com/tinywasm/model"
    "github.com/tinywasm/orm"
    "github.com/tinywasm/sqlite"
    "github.com/tinywasm/sqlt"
)

conn, _ := sqlite.Open("app.db")
_ = rbac.Migrate(conn, sqlt.NewCompiler())

db := orm.New(conn)
svc, _ := rbac.New(db)

const projectID = "misitio"

_ = svc.CreateRole(projectID, "role_admin", "admin", "Administrator", "")
_ = svc.CreatePermission(projectID, "service_catalog:crud", "catalog", "service_catalog", model.AllActions)
_ = svc.AssignPermission(projectID, "role_admin", "service_catalog:crud")
_ = svc.AssignRole(projectID, string(subjectID), "role_admin")

if svc.Can(projectID, string(subjectID), "service_catalog", model.Read) {
    // granted
}

Empty or unknown subject IDs are denied — rbac never persists a "user" row, so a subject with no assignments simply resolves to zero permissions, not an error. Malformed stored actions deny and surface an error (never a silent false, nil).

API — Quiero X → Uso Y

Objetivo Método
Asignar rol por código svc.AssignRoleByCode(projectID, userID, roleCode)
Revocar rol por código (invalida caché) svc.RevokeRoleByCode(projectID, userID, roleCode)
Listar usuarios de un rol svc.UsersInRole(projectID, roleCode)
Contar usuarios de un rol svc.RoleUserCount(projectID, roleCode)
Eliminar rol y asignaciones por código svc.DeleteRoleByCode(projectID, roleCode)
Detectar roles duplicados antes de migrar rbac.FindDuplicateRoleCodes(db)

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrDuplicateRoleCode = fmt.Err("rbac", "duplicate", "role", "code")

ErrDuplicateRoleCode reporta que la base tiene dos roles con el mismo code dentro de un proyecto — un estado que este paquete ya no permite crear pero que una base anterior a esta versión pudo haber acumulado. Se resuelve a mano: hay que decidir cuál de los dos roles sobrevive y reasignar sus usuarios. Migrate NO lo resuelve solo porque elegir cuál borrar es una decisión de política, no de esquema.

View Source
var ErrNotFound = fmt.Err("rbac", "not", "found")

ErrNotFound reports that a role or permission id has no matching row.

View Source
var ErrRoleNotFound = fmt.Err("rbac", "role", "not", "found")

ErrRoleNotFound reporta que el rol identificado por su code no existe en el proyecto especificado.

View Source
var PermissionModel = model.Definition{
	Name: "permission",
	Fields: model.Fields{
		{Name: "project_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
		{Name: "id", Type: model.Text(), DB: &model.FieldDB{PK: true}},
		{Name: "name", Type: model.Text()},
		{Name: "resource", Type: model.Text()},
		{Name: "action", Type: model.Text()},
	},
}
View Source
var Permission_ = struct {
	ProjectId string
	Id        string
	Name      string
	Resource  string
	Action    string
}{
	ProjectId: "project_id",
	Id:        "id",
	Name:      "name",
	Resource:  "resource",
	Action:    "action",
}
View Source
var RoleModel = model.Definition{
	Name: "role",
	Fields: model.Fields{
		{Name: "project_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
		{Name: "id", Type: model.Text(), DB: &model.FieldDB{PK: true}},
		{Name: "code", Type: model.Text(), NotNull: true},
		{Name: "name", Type: model.Text()},
		{Name: "description", Type: model.Text()},

		{Name: "session_ttl", Type: model.Int()},
	},
}
View Source
var RolePermissionModel = model.Definition{
	Name: "role_permission",
	Fields: model.Fields{
		{Name: "project_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
		{Name: "role_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
		{Name: "permission_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
	},
}
View Source
var RolePermission_ = struct {
	ProjectId    string
	RoleId       string
	PermissionId string
}{
	ProjectId:    "project_id",
	RoleId:       "role_id",
	PermissionId: "permission_id",
}
View Source
var Role_ = struct {
	ProjectId   string
	Id          string
	Code        string
	Name        string
	Description string
	SessionTtl  string
}{
	ProjectId:   "project_id",
	Id:          "id",
	Code:        "code",
	Name:        "name",
	Description: "description",
	SessionTtl:  "session_ttl",
}
View Source
var UserRoleModel = model.Definition{
	Name: "user_role",
	Fields: model.Fields{
		{Name: "project_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
		{Name: "user_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
		{Name: "role_id", Type: model.Text(), DB: &model.FieldDB{PK: true}, NotNull: true},
	},
}
View Source
var UserRole_ = struct {
	ProjectId string
	UserId    string
	RoleId    string
}{
	ProjectId: "project_id",
	UserId:    "user_id",
	RoleId:    "role_id",
}

Functions

func Migrate added in v0.0.7

func Migrate(conn ddl.Execer, ddlCompiler ddl.Compiler) error

Migrate reconciles the database schema this package owns: Role, Permission, UserRole and RolePermission, in dependency order.

It is deliberately NOT called by New. Schema reconciliation is deploy-time work — running it per process start costs a network round trip per model, which in a Cloudflare Worker is paid again on every isolate cold start (measured at 8.5–10.4 s across ~14 models in veltylabs/iam). Call this once from a migration binary, then let New assume the schema exists.

conn is a ddl.Execer, not an *orm.DB, so a deploy-time transport that can only execute DDL satisfies it — goflare.NewD1Migrator returns exactly that. An *orm.DB's RawConn() also satisfies it, for local/test callers:

// deploy time, against D1's HTTP API:
conn, _ := goflare.NewD1Migrator(accountID, databaseID, apiToken)
err := rbac.Migrate(conn, sqlt.NewCompiler())

// local dev / tests, against an in-memory or sqlite DB:
err := rbac.Migrate(db.RawConn(), db.RawConn().(ddl.Compiler))

Types

type Permission added in v0.0.2

type Permission struct {
	ProjectId string
	Id        string
	Name      string
	Resource  string
	Action    string
}

func ReadOnePermission added in v0.0.5

func ReadOnePermission(qb *orm.QB, model *Permission) (*Permission, error)

func (*Permission) DecodeFields added in v0.0.5

func (m *Permission) DecodeFields(r model.FieldReader)

func (*Permission) EncodeFields added in v0.0.5

func (m *Permission) EncodeFields(w model.FieldWriter)

func (*Permission) IsNil added in v0.0.5

func (m *Permission) IsNil() bool

func (*Permission) ModelName added in v0.0.5

func (m *Permission) ModelName() string

func (*Permission) Pointers added in v0.0.5

func (m *Permission) Pointers() []any

func (*Permission) Schema added in v0.0.5

func (m *Permission) Schema() []model.Field

func (*Permission) Validate added in v0.0.5

func (m *Permission) Validate(action byte) error

type PermissionList added in v0.0.5

type PermissionList []*Permission

func ReadAllPermission added in v0.0.5

func ReadAllPermission(qb *orm.QB) (PermissionList, error)

func (*PermissionList) Append added in v0.0.5

func (s *PermissionList) Append() model.Fielder

func (*PermissionList) At added in v0.0.5

func (s *PermissionList) At(i int) model.Fielder

func (*PermissionList) DecodeFields added in v0.0.5

func (s *PermissionList) DecodeFields(_ model.FieldReader)

func (*PermissionList) EncodeFields added in v0.0.5

func (s *PermissionList) EncodeFields(_ model.FieldWriter)

func (*PermissionList) IsNil added in v0.0.5

func (s *PermissionList) IsNil() bool

func (*PermissionList) Len added in v0.0.5

func (s *PermissionList) Len() int

func (*PermissionList) Pointers added in v0.0.5

func (s *PermissionList) Pointers() []any

func (*PermissionList) Schema added in v0.0.5

func (s *PermissionList) Schema() []model.Field

type RBACObject added in v0.0.5

type RBACObject interface {
	HandlerName() string
	AllowedRoles(action model.Action) []model.RoleCode
}

type Role added in v0.0.2

type Role struct {
	ProjectId   string
	Id          string
	Code        string
	Name        string
	Description string
	SessionTtl  int64
}

func ReadOneRole added in v0.0.5

func ReadOneRole(qb *orm.QB, model *Role) (*Role, error)

func (*Role) DecodeFields added in v0.0.5

func (m *Role) DecodeFields(r model.FieldReader)

func (*Role) EncodeFields added in v0.0.5

func (m *Role) EncodeFields(w model.FieldWriter)

func (*Role) IsNil added in v0.0.5

func (m *Role) IsNil() bool

func (*Role) ModelName added in v0.0.5

func (m *Role) ModelName() string

func (*Role) Pointers added in v0.0.5

func (m *Role) Pointers() []any

func (*Role) Schema added in v0.0.5

func (m *Role) Schema() []model.Field

func (*Role) Validate added in v0.0.5

func (m *Role) Validate(action byte) error

type RoleCodeRef added in v0.0.11

type RoleCodeRef struct {
	ProjectID string
	Code      string
}

RoleCodeRef nombra un rol por su par natural, el que el consumidor usa.

func FindDuplicateRoleCodes added in v0.0.11

func FindDuplicateRoleCodes(db *orm.DB) ([]RoleCodeRef, error)

FindDuplicateRoleCodes devuelve los pares (project_id, code) que aparecen más de una vez. Vacío = la base está lista para el índice único.

type RoleList added in v0.0.5

type RoleList []*Role

func ReadAllRole added in v0.0.5

func ReadAllRole(qb *orm.QB) (RoleList, error)

func (*RoleList) Append added in v0.0.5

func (s *RoleList) Append() model.Fielder

func (*RoleList) At added in v0.0.5

func (s *RoleList) At(i int) model.Fielder

func (*RoleList) DecodeFields added in v0.0.5

func (s *RoleList) DecodeFields(_ model.FieldReader)

func (*RoleList) EncodeFields added in v0.0.5

func (s *RoleList) EncodeFields(_ model.FieldWriter)

func (*RoleList) IsNil added in v0.0.5

func (s *RoleList) IsNil() bool

func (*RoleList) Len added in v0.0.5

func (s *RoleList) Len() int

func (*RoleList) Pointers added in v0.0.5

func (s *RoleList) Pointers() []any

func (*RoleList) Schema added in v0.0.5

func (s *RoleList) Schema() []model.Field

type RolePermission added in v0.0.5

type RolePermission struct {
	ProjectId    string
	RoleId       string
	PermissionId string
}

func ReadOneRolePermission added in v0.0.5

func ReadOneRolePermission(qb *orm.QB, model *RolePermission) (*RolePermission, error)

func (*RolePermission) DecodeFields added in v0.0.5

func (m *RolePermission) DecodeFields(r model.FieldReader)

func (*RolePermission) EncodeFields added in v0.0.5

func (m *RolePermission) EncodeFields(w model.FieldWriter)

func (*RolePermission) IsNil added in v0.0.5

func (m *RolePermission) IsNil() bool

func (*RolePermission) ModelName added in v0.0.5

func (m *RolePermission) ModelName() string

func (*RolePermission) Pointers added in v0.0.5

func (m *RolePermission) Pointers() []any

func (*RolePermission) Schema added in v0.0.5

func (m *RolePermission) Schema() []model.Field

func (*RolePermission) Validate added in v0.0.5

func (m *RolePermission) Validate(action byte) error

type RolePermissionList added in v0.0.5

type RolePermissionList []*RolePermission

func ReadAllRolePermission added in v0.0.5

func ReadAllRolePermission(qb *orm.QB) (RolePermissionList, error)

func (*RolePermissionList) Append added in v0.0.5

func (s *RolePermissionList) Append() model.Fielder

func (*RolePermissionList) At added in v0.0.5

func (*RolePermissionList) DecodeFields added in v0.0.5

func (s *RolePermissionList) DecodeFields(_ model.FieldReader)

func (*RolePermissionList) EncodeFields added in v0.0.5

func (s *RolePermissionList) EncodeFields(_ model.FieldWriter)

func (*RolePermissionList) IsNil added in v0.0.5

func (s *RolePermissionList) IsNil() bool

func (*RolePermissionList) Len added in v0.0.5

func (s *RolePermissionList) Len() int

func (*RolePermissionList) Pointers added in v0.0.5

func (s *RolePermissionList) Pointers() []any

func (*RolePermissionList) Schema added in v0.0.5

func (s *RolePermissionList) Schema() []model.Field

type Service added in v0.0.5

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

Service owns role, permission, and subject-assignment persistence, scoped by project — every table carries project_id, so one Service over one database serves every consuming project (see ARCHITECTURE.md).

func New

func New(db *orm.DB) (*Service, error)

New creates an authorization service over the injected database.

func (*Service) AssignPermission added in v0.0.5

func (m *Service) AssignPermission(projectID, roleID, permissionID string) error

func (*Service) AssignRole added in v0.0.5

func (m *Service) AssignRole(projectID, userID, roleID string) error

func (*Service) AssignRoleByCode added in v0.0.11

func (m *Service) AssignRoleByCode(projectID, userID string, code model.RoleCode) error

AssignRoleByCode concede el rol identificado por su code. Idempotente. ErrRoleNotFound si el code no existe en el proyecto — a diferencia de CreateRole, NO lo crea: conceder un rol y definirlo son decisiones distintas y mezclarlas hace que un typo en el code cree un rol vacío.

func (*Service) Can added in v0.0.5

func (s *Service) Can(projectID, subjectID string, resource model.Resource, action model.Action) bool

Can reports whether subjectID has a grant for resource/action within projectID.

func (*Service) CanSubject added in v0.0.5

func (s *Service) CanSubject(projectID string, id user.SubjectID, resource model.Resource, action model.Action) bool

CanSubject is the typed alias for Can that accepts the stable user.SubjectID.

func (*Service) CreatePermission added in v0.0.5

func (m *Service) CreatePermission(projectID, id, name string, resource model.Resource, action model.Action) error

func (*Service) CreateRole added in v0.0.5

func (m *Service) CreateRole(projectID, id string, code model.RoleCode, name, description string) error

func (*Service) DeletePermission added in v0.0.5

func (m *Service) DeletePermission(projectID, id string) error

func (*Service) DeleteRole added in v0.0.5

func (m *Service) DeleteRole(projectID, id string) error

func (*Service) DeleteRoleByCode added in v0.0.11

func (m *Service) DeleteRoleByCode(projectID string, code model.RoleCode) error

DeleteRoleByCode borra el rol y todas sus asignaciones. ErrRoleNotFound si el code no existe en el proyecto.

func (*Service) GetPermission added in v0.0.5

func (m *Service) GetPermission(projectID, id string) (*Permission, error)

func (*Service) GetRole added in v0.0.5

func (m *Service) GetRole(projectID, id string) (*Role, error)

func (*Service) GetRoleByCode added in v0.0.5

func (m *Service) GetRoleByCode(projectID string, code model.RoleCode) (*Role, error)

func (*Service) GetUserRoles added in v0.0.5

func (m *Service) GetUserRoles(projectID, userID string) ([]Role, error)

func (*Service) HasPermission added in v0.0.5

func (m *Service) HasPermission(projectID, subjectID string, resource model.Resource, action model.Action) (bool, error)

func (*Service) Register added in v0.0.5

func (m *Service) Register(projectID string, handlers ...RBACObject) error

Register builds permissions from handlers' declared resource/action grants and assigns them to the roles those handlers name — policy stays with the caller (see README: "Policy belongs to the consumer"); rbac only persists what Register is told.

func (*Service) RevokeRole added in v0.0.5

func (m *Service) RevokeRole(projectID, userID, roleID string) error

func (*Service) RevokeRoleByCode added in v0.0.11

func (m *Service) RevokeRoleByCode(projectID, userID string, code model.RoleCode) error

RevokeRoleByCode quita el rol identificado por su code al usuario dentro del proyecto. Es el par de AssignRoleByCode y el camino que debe usar un consumidor que habla en codes — nunca borrar la fila UserRole a mano: el borrado directo NO invalida el caché de permisos y deja concediendo accesos ya revocados.

Idempotente: revocar un rol que el usuario no tiene no es un error. ErrRoleNotFound si el code no existe en el proyecto.

func (*Service) RoleUserCount added in v0.0.11

func (m *Service) RoleUserCount(projectID string, code model.RoleCode) (int64, error)

RoleUserCount devuelve cuántos usuarios tienen el rol, sin traerlos.

func (*Service) SetRoleSessionTTL added in v0.0.5

func (m *Service) SetRoleSessionTTL(projectID, id string, ttl int64) error

SetRoleSessionTTL sets the role's SessionTtl (seconds; 0 reverts to "use the caller's default"). See RoleModel's session_ttl comment for the most-restrictive-wins policy a caller applies across a user's roles.

func (*Service) UsersInRole added in v0.0.11

func (m *Service) UsersInRole(projectID string, code model.RoleCode) ([]string, error)

UsersInRole devuelve los ids de usuario que tienen el rol. Sólo ids: este paquete no conoce la tabla de usuarios (ver ARCHITECTURE.md), así que resolver perfiles es del consumidor.

type UserRole added in v0.0.5

type UserRole struct {
	ProjectId string
	UserId    string
	RoleId    string
}

func ReadOneUserRole added in v0.0.5

func ReadOneUserRole(qb *orm.QB, model *UserRole) (*UserRole, error)

func (*UserRole) DecodeFields added in v0.0.5

func (m *UserRole) DecodeFields(r model.FieldReader)

func (*UserRole) EncodeFields added in v0.0.5

func (m *UserRole) EncodeFields(w model.FieldWriter)

func (*UserRole) IsNil added in v0.0.5

func (m *UserRole) IsNil() bool

func (*UserRole) ModelName added in v0.0.5

func (m *UserRole) ModelName() string

func (*UserRole) Pointers added in v0.0.5

func (m *UserRole) Pointers() []any

func (*UserRole) Schema added in v0.0.5

func (m *UserRole) Schema() []model.Field

func (*UserRole) Validate added in v0.0.5

func (m *UserRole) Validate(action byte) error

type UserRoleList added in v0.0.5

type UserRoleList []*UserRole

func ReadAllUserRole added in v0.0.5

func ReadAllUserRole(qb *orm.QB) (UserRoleList, error)

func (*UserRoleList) Append added in v0.0.5

func (s *UserRoleList) Append() model.Fielder

func (*UserRoleList) At added in v0.0.5

func (s *UserRoleList) At(i int) model.Fielder

func (*UserRoleList) DecodeFields added in v0.0.5

func (s *UserRoleList) DecodeFields(_ model.FieldReader)

func (*UserRoleList) EncodeFields added in v0.0.5

func (s *UserRoleList) EncodeFields(_ model.FieldWriter)

func (*UserRoleList) IsNil added in v0.0.5

func (s *UserRoleList) IsNil() bool

func (*UserRoleList) Len added in v0.0.5

func (s *UserRoleList) Len() int

func (*UserRoleList) Pointers added in v0.0.5

func (s *UserRoleList) Pointers() []any

func (*UserRoleList) Schema added in v0.0.5

func (s *UserRoleList) Schema() []model.Field

Jump to

Keyboard shortcuts

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