schema

package
v0.3.0 Latest Latest
Warning

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

Go to latest
Published: Jan 8, 2026 License: MIT Imports: 15 Imported by: 0

Documentation

Overview

Package schema provides OpenFGA schema types and transformation logic for melange.

This package contains the core data structures and algorithms for converting OpenFGA authorization models into database-friendly representations. It sits between the tooling package (which parses .fga files) and the runtime checker (which executes permission checks).

Package Responsibilities

The schema package handles three critical transformations:

  1. Schema representation (TypeDefinition, RelationDefinition) - parsed FGA models
  2. SQL model generation (ToAuthzModels) - flattening rules for SQL generation
  3. Precomputation (ComputeRelationClosure, ToUsersetRules) - optimizing runtime checks

Key Types

TypeDefinition represents a parsed object type from an FGA schema. Each type has relations that define permissions and roles. For example:

type repository
  relations
    define owner: [user]
    define can_read: owner or [user]

AuthzModel represents a flattened authorization rule used during SQL generation. The ToAuthzModels function converts TypeDefinitions into rule rows, performing transitive closure of implied-by relationships to support efficient checks.

ClosureRow represents precomputed transitive relationships. This eliminates recursive SQL during permission checks by answering "what relations satisfy this relation?" with a simple lookup.

Migration Workflow

The Migrator orchestrates loading schemas into PostgreSQL:

  1. Parse schema via tooling package (returns []TypeDefinition)
  2. MigrateWithTypes - validates, transforms, and loads generated SQL

Typical usage:

import "github.com/pthm/melange/tooling"
types, _ := tooling.ParseSchema("schemas/schema.fga")
migrator := schema.NewMigrator(db, "schemas")
err := migrator.MigrateWithTypes(ctx, types)

Code Generation

GenerateGo produces type-safe Go constants from schema types. This enables compile-time checking of permission checks:

types, _ := tooling.ParseSchema("schema.fga")
schema.GenerateGo(file, types, schema.DefaultGenerateConfig())

Generated code includes ObjectType constants, Relation constants, and constructor functions for creating melange.Object values.

Validation

DetectCycles validates schemas before migration. It checks for:

  • Implied-by cycles within a type (admin -> owner -> admin)
  • Cross-type parent relation cycles
  • Allows hierarchical recursion (folder -> parent folder)

Invalid schemas are rejected with ErrCyclicSchema before reaching the database.

Relationship to Other Packages

The schema package is dependency-free (stdlib only) and imported by both:

  • tooling package (adds OpenFGA parser, provides convenience functions)
  • root melange package (uses Execer interface but no other types)

This layering keeps the core runtime (melange package) lightweight while supporting rich schema manipulation in tooling contexts.

Index

Constants

View Source
const (
	// RuleGroupModeIntersection indicates all rules in the group must be satisfied (AND).
	// Used for "viewer: writer and editor" patterns.
	RuleGroupModeIntersection = "intersection"

	// RuleGroupModeExcludeIntersection indicates an exclusion where all rules must be
	// satisfied for the exclusion to apply. Used for "but not (editor and owner)" patterns.
	RuleGroupModeExcludeIntersection = "exclude_intersection"
)

RuleGroupMode constants define how rules within a group are combined.

View Source
const CodegenVersion = "1"

CodegenVersion is incremented when SQL generation templates or logic change. This ensures migrations re-run even if schema checksum matches. Bump this when:

  • SQL templates in schema/templates/ change
  • Codegen logic in schema/codegen.go or schema/codegen_list.go changes
  • New function patterns are added

Variables

View Source
var ErrCyclicSchema = errors.New("melange/schema: cyclic schema")

ErrCyclicSchema is returned when the schema contains a cycle in the relation graph.

Functions

func CollectFunctionNames added in v0.3.0

func CollectFunctionNames(analyses []RelationAnalysis) []string

CollectFunctionNames returns all function names that will be generated for the given analyses. This is used for migration tracking and orphan detection.

The returned list includes:

  • Specialized check functions: check_{type}_{relation}
  • No-wildcard check variants: check_{type}_{relation}_no_wildcard
  • Specialized list functions: list_{type}_{relation}_objects, list_{type}_{relation}_subjects
  • Dispatcher functions (always included)

func ComputeSchemaChecksum added in v0.3.0

func ComputeSchemaChecksum(content string) string

ComputeSchemaChecksum returns a SHA256 hash of the schema content. Used to detect schema changes for skip-if-unchanged optimization.

func DetectCycles

func DetectCycles(types []TypeDefinition) error

DetectCycles checks for cycles in the relation graph. It validates both implied-by cycles (within a single type) and parent relation cycles (across types). Returns an error describing the cycle if one is found.

This function is called by both GenerateGo and MigrateWithTypes to catch invalid schemas before they cause runtime issues.

Example cyclic schemas that would be detected:

// Implied-by cycle:
type resource
  relations
    define admin: [user] or owner
    define owner: [user] or admin  // CYCLE: admin ↔ owner

// Parent relation cycle:
type organization
  relations
    define repo: [repository]
    define can_read: can_read from repo
type repository
  relations
    define org: [organization]
    define can_read: can_read from org  // CYCLE: crosses types

func GenerateGo

func GenerateGo(w io.Writer, types []TypeDefinition, cfg *GenerateConfig) error

GenerateGo writes Go code for the types and relations from a parsed schema. Unlike some FGA implementations, this generates constants for ALL relations, not just those with "can_" prefix (unless filtered via config).

The generated code includes:

  • ObjectType constants (TypeUser, TypeRepository, etc.)
  • Relation constants (RelCanRead, RelOwner, etc.)
  • Constructor functions (User(id), Repository(id), etc.)
  • Wildcard constructors (AnyUser(), AnyRepository(), etc.)

The constructors return melange.Object values, enabling type-safe permission checks:

checker.Check(ctx, authz.User(123), authz.RelCanRead, authz.Repository(456))

Wildcard constructors support public access patterns:

// Grant all users permission
INSERT INTO tuples (subject_type, subject_id, relation, object_type, object_id)
VALUES ('user', '*', 'can_read', 'repository', '456')

checker.Check(ctx, authz.AnyUser(), authz.RelCanRead, authz.Repository(456))

func IsCyclicSchemaErr

func IsCyclicSchemaErr(err error) bool

IsCyclicSchemaErr returns true if err is or wraps ErrCyclicSchema.

func RelationSubjects

func RelationSubjects(types []TypeDefinition, objectType string, relation string) []string

RelationSubjects returns the subject types that can have a specific relation on objects of the given type. This is useful for understanding who can be granted a particular permission.

Example:

types, _ := tooling.ParseSchema("schema.fga")
subjects := schema.RelationSubjects(types, "repository", "owner")
// Returns: ["user"] - only users can be repository owners

readers := schema.RelationSubjects(types, "repository", "can_read")
// Returns: ["user", "organization"] - users and orgs can read repositories

func SubjectTypes

func SubjectTypes(types []TypeDefinition) []string

SubjectTypes returns all types that can be subjects in authorization checks. A type is a subject type if it appears in any relation's SubjectTypeRefs list. This is useful for understanding which types can be the "who" in permission checks.

Example:

types, _ := tooling.ParseSchema("schema.fga")
subjects := schema.SubjectTypes(types)
// Returns: ["user", "organization", "team"]

Types

type AnchorPathStep added in v0.3.0

type AnchorPathStep struct {
	// Type is either "ttu" or "userset"
	Type string

	// For TTU patterns (e.g., "viewer from parent"):
	// LinkingRelation is "parent" (the relation that links to the parent object)
	// TargetType is the first parent object type found with an anchor (e.g., "folder")
	// TargetRelation is the relation to check on the parent (e.g., "viewer")
	// AllTargetTypes contains ALL object types that the linking relation can point to
	// when each type has the target relation with direct grants. This is used for
	// generating UNION queries when parent can be multiple types (e.g., [document, folder]).
	// RecursiveTypes contains object types where the target relation is recursive
	// (same type as the object type being checked). These require check_permission_internal
	// instead of list function composition to handle the recursion correctly.
	LinkingRelation string
	TargetType      string
	TargetRelation  string
	AllTargetTypes  []string
	RecursiveTypes  []string

	// For userset patterns (e.g., [group#member]):
	// SubjectType is "group"
	// SubjectRelation is "member"
	SubjectType     string
	SubjectRelation string
}

AnchorPathStep represents one step in the path from a relation to its anchor. Steps can be either TTU (tuple-to-userset) or userset patterns.

type AuthzModel

type AuthzModel struct {
	ID               int64
	ObjectType       string  // Object type this rule applies to
	Relation         string  // Relation this rule defines
	SubjectType      *string // Allowed subject type (for direct rules)
	ImpliedBy        *string // Implying relation (for role hierarchy)
	ParentRelation   *string // Parent relation to check (for inheritance)
	ExcludedRelation *string // Relation to exclude (for "but not" rules)
	SubjectWildcard  *bool   // Whether wildcard subjects are allowed for SubjectType
	// Excluded parent relation for tuple-to-userset exclusions.
	ExcludedParentRelation *string // Parent relation to exclude (for "but not rel from parent")
	ExcludedParentType     *string // Linking relation for the excluded parent relation
	// New fields for userset references and intersection support
	SubjectRelation       *string // For userset refs [type#relation]: the relation part
	RuleGroupID           *int64  // Groups rules that form an intersection
	RuleGroupMode         *string // RuleGroupModeIntersection or RuleGroupModeExcludeIntersection
	CheckRelation         *string // For intersection: which relation to check
	CheckExcludedRelation *string // For intersection: exclusion on check_relation (e.g., "editor but not owner")
	CheckParentRelation   *string // For intersection: parent relation to check (tuple-to-userset)
	CheckParentType       *string // For intersection: linking relation on current object
}

AuthzModel represents an entry in the flattened authorization model. Each row defines one authorization rule that generated SQL evaluates.

The model stores normalized rules for efficient query execution.

Rule types:

  • Direct: SubjectType is set, others NULL (user can have relation)
  • Implied: ImpliedBy is set (having one relation grants another)
  • Parent: ParentRelation and SubjectType set (inherit from parent object)
  • Exclusive: ExcludedRelation set (permission denied if exclusion holds)
  • Userset: SubjectType and SubjectRelation set (e.g., [group#member])
  • Intersection: RuleGroupID and RuleGroupMode set (AND semantics)

func ToAuthzModels

func ToAuthzModels(types []TypeDefinition) []AuthzModel

ToAuthzModels converts parsed type definitions to database models. This is the critical transformation that enables permission checking.

The conversion performs transitive closure of implied_by relationships to support role hierarchies. For example, if owner → admin and admin → member, the closure ensures owner also implies member without explicit declaration.

Each AuthzModel row represents one authorization rule:

  • Direct subject types: "repository.can_read allows user"
  • Implied relations: "repository.can_read implied by can_write"
  • Parent inheritance: "change.can_read from repository.can_read"
  • Exclusions: "change.can_read but not is_author"
  • Intersection groups: "viewer: writer and editor" (all must be satisfied)

The check_permission function queries these rows to evaluate permissions recursively, following the graph of implications and parent relationships.

type CheckFunctionData added in v0.3.0

type CheckFunctionData struct {
	ObjectType   string // The authorization object type (e.g., "document", "folder")
	Relation     string // The relation name (e.g., "viewer", "editor")
	FunctionName string // The generated function name (e.g., "check_document_viewer")
	// InternalCheckFunctionName is the dispatcher internal function name to call
	// for recursive or complex checks.
	InternalCheckFunctionName string
	FeaturesString            string // Human-readable list of enabled features for SQL comments
	ClosureValues             string // Inline SQL VALUES for closure lookups, eliminates JOIN
	UsersetValues             string // Inline SQL VALUES for userset patterns, eliminates JOIN

	// Feature flags
	HasDirect       bool
	HasImplied      bool
	HasWildcard     bool
	HasUserset      bool
	HasRecursive    bool
	HasExclusion    bool
	HasIntersection bool

	// HasStandaloneAccess is true if the relation has access paths outside of intersections.
	// When false and HasIntersection is true, the only access is through intersection groups.
	// For example, "viewer: [user] and writer" has NO standalone access - the [user] is
	// inside the intersection. But "viewer: [user] or (writer and editor)" HAS standalone
	// access via the [user] path.
	HasStandaloneAccess bool

	// Pre-rendered SQL fragments
	DirectCheck    string // Pre-rendered SQL EXISTS for direct tuple lookup
	UsersetCheck   string // Pre-rendered SQL EXISTS for userset membership checks
	ExclusionCheck string // Pre-rendered SQL EXISTS for exclusion (denial) checks
	AccessChecks   string // All access paths OR'd together for the final permission decision

	// For recursive (TTU) patterns
	ParentRelations []ParentRelationData // TTU patterns: parent object relations to check recursively

	// For implied relations that need function calls
	ImpliedFunctionCalls []ImpliedFunctionCall // Complex closure relations requiring function calls

	// For intersection patterns - each group is AND'd, groups are OR'd
	IntersectionGroups []IntersectionGroupData // AND groups where all parts must be satisfied
}

CheckFunctionData contains data for rendering check function templates.

type ClosureRow

type ClosureRow struct {
	ObjectType         string
	Relation           string
	SatisfyingRelation string
	ViaPath            []string // For debugging: path from relation to satisfying_relation
}

ClosureRow represents a precomputed relation closure row. The closure table is a critical optimization that precomputes transitive implied-by relationships at schema load time, eliminating the need for recursive function calls during permission checks.

Each row indicates that having satisfying_relation grants the relation on objects of object_type. For example, in a role hierarchy where owner -> admin -> member:

  • {object_type: "repo", relation: "member", satisfying_relation: "owner"}
  • {object_type: "repo", relation: "member", satisfying_relation: "admin"}
  • {object_type: "repo", relation: "member", satisfying_relation: "member"}

This allows check_permission to evaluate "does user have member?" with a simple JOIN rather than recursive traversal: just check if they have ANY of the satisfying relations.

func ComputeRelationClosure

func ComputeRelationClosure(types []TypeDefinition) []ClosureRow

ComputeRelationClosure computes the transitive closure for all relations. For each relation, it finds all relations that can satisfy it (directly or transitively).

This is a build-time optimization. Without closure, check_permission would need recursive SQL functions to walk implied-by chains. With closure, a single JOIN against the inlined closure resolves the entire hierarchy.

Example: For schema owner -> admin -> member:

  • member is satisfied by: member, admin, owner
  • admin is satisfied by: admin, owner
  • owner is satisfied by: owner

The closure table enables O(1) lookups instead of O(depth) recursion, which is critical for deeply nested role hierarchies.

type ComplexExclusionCheckData added in v0.3.0

type ComplexExclusionCheckData struct {
	ObjectType                string
	ExcludedRelation          string
	InternalCheckFunctionName string
}

ComplexExclusionCheckData contains data for rendering complex exclusion checks. These use check_permission_internal instead of direct tuple lookup.

type ComplexUsersetCheckData added in v0.3.0

type ComplexUsersetCheckData struct {
	ObjectType      string
	Relation        string
	SubjectType     string
	SubjectRelation string

	InternalCheckFunctionName string
}

ComplexUsersetCheckData contains data for rendering complex userset check template. Used when the userset closure contains relations with complex features.

type DirectCheckData added in v0.3.0

type DirectCheckData struct {
	ObjectType        string
	RelationList      string
	SubjectTypeFilter string // e.g., "'user', 'employee'" - allowed subject types
	SubjectIDCheck    string
}

DirectCheckData contains data for rendering direct check template.

type DispatcherCase added in v0.3.0

type DispatcherCase struct {
	ObjectType        string
	Relation          string
	CheckFunctionName string
}

DispatcherCase represents a single CASE WHEN branch in the dispatcher.

type DispatcherData added in v0.3.0

type DispatcherData struct {
	FunctionName            string
	HasSpecializedFunctions bool
	Cases                   []DispatcherCase
}

DispatcherData contains data for rendering dispatcher template.

type ExclusionCheckData added in v0.3.0

type ExclusionCheckData struct {
	ObjectType       string
	ExcludedRelation string
	SubjectIDCheck   string
}

ExclusionCheckData contains data for rendering exclusion check template.

type Execer

type Execer interface {
	ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
	QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
	QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
}

Execer is the minimal interface needed for schema migration operations. Implemented by *sql.DB, *sql.Tx, and *sql.Conn.

type GenerateConfig

type GenerateConfig struct {
	// Package name for generated code. Default: "authz"
	Package string

	// RelationPrefixFilter is a prefix filter for relation names.
	// Only relations with this prefix will have constants generated.
	// If empty, all relations will be generated.
	//
	// Example: "can_" generates only permission relations, omitting roles.
	RelationPrefixFilter string

	// IDType specifies the type to use for object IDs.
	// Default: "string"
	// The type must implement fmt.Stringer or be safely convertible to string.
	//
	// Common values: "string", "int64", "uuid.UUID"
	// The generated code uses fmt.Sprint(id) for conversion.
	IDType string
}

GenerateConfig configures code generation. The generator produces Go code with type-safe constants for object types and relations defined in the FGA schema.

func DefaultGenerateConfig

func DefaultGenerateConfig() *GenerateConfig

DefaultGenerateConfig returns sensible defaults. Package: "authz", no relation filter (all relations), string IDs.

type GeneratedSQL added in v0.3.0

type GeneratedSQL struct {
	// Functions contains CREATE OR REPLACE FUNCTION statements
	// for each specialized check function.
	Functions []string
	// NoWildcardFunctions contains CREATE OR REPLACE FUNCTION statements
	// for no-wildcard variants of each specialized check function.
	NoWildcardFunctions []string

	// Dispatcher contains the check_permission dispatcher function
	// that routes to specialized functions.
	Dispatcher string

	// DispatcherNoWildcard contains the check_permission_no_wildcard dispatcher.
	DispatcherNoWildcard string
}

GeneratedSQL contains all SQL generated for a schema. This is applied atomically during migration.

func GenerateSQL added in v0.3.0

func GenerateSQL(analyses []RelationAnalysis, inline InlineSQLData) (GeneratedSQL, error)

GenerateSQL generates specialized SQL functions for all relations. The generated SQL includes:

  • Per-relation check functions (check_{type}_{relation})
  • A dispatcher that routes check_permission to specialized functions

type ImpliedFunctionCall added in v0.3.0

type ImpliedFunctionCall struct {
	FunctionName string // Function to call for this implied relation (e.g., "check_document_editor")
}

ImpliedFunctionCall represents a function call to a complex implied relation. Used when an implied relation has exclusions and can't use simple tuple lookup.

type IndirectAnchorInfo added in v0.3.0

type IndirectAnchorInfo struct {
	// Path describes the traversal from this relation to the anchor.
	// For "document.viewer: viewer from folder" where folder.viewer: [user],
	// Path would contain one TTU step pointing to folder.viewer.
	// For multi-hop chains, Path contains multiple steps.
	Path []AnchorPathStep

	// AnchorType and AnchorRelation identify the relation with direct grants.
	// This is the final destination of the path - a relation that has HasDirect.
	AnchorType     string
	AnchorRelation string
}

IndirectAnchorInfo describes how to reach a relation with direct grants when the relation itself has no direct/implied access paths. This enables list generation for "pure" patterns like pure TTU or pure userset by tracing through to find an anchor relation that has [user] or similar direct grants.

For example, for "document.viewer: viewer from folder" where folder.viewer: [user], the IndirectAnchor would point to folder.viewer with a TTU path step.

type InlineSQLData added in v0.3.0

type InlineSQLData struct {
	// ClosureValues contains tuples of (object_type, relation, satisfying_relation).
	ClosureValues string
	// UsersetValues contains tuples of (object_type, relation, subject_type, subject_relation).
	UsersetValues string
}

InlineSQLData contains SQL VALUES payloads that replace database-backed model tables. Rationale: Model data is inlined into SQL VALUES clauses rather than querying database tables. This eliminates the need for persistent melange_model tables and ensures generated functions are self-contained. When the schema changes, migration regenerates all functions with updated inline data. This approach trades function size for runtime simplicity and removes a JOIN from every check.

func BuildInlineSQLData added in v0.3.0

func BuildInlineSQLData(closureRows []ClosureRow, analyses []RelationAnalysis) InlineSQLData

BuildInlineSQLData exposes inline SQL generation for tools and tests.

type IntersectionExclusionCheckData added in v0.3.0

type IntersectionExclusionCheckData struct {
	ObjectType string
	Parts      []string // Relations that must ALL be satisfied for exclusion to apply
}

IntersectionExclusionCheckData contains data for rendering intersection exclusion checks. These check "but not (A and B)" patterns by ANDing together check_permission_internal calls.

type IntersectionGroup

type IntersectionGroup struct {
	Relations       []string              // Relations that must all be satisfied (AND)
	ParentRelations []ParentRelationCheck // Parent inheritance checks (tuple-to-userset)
	Exclusions      map[string][]string   // Per-relation exclusions: relation -> list of excluded relations
}

IntersectionGroup represents a group of relations that must ALL be satisfied. For "viewer: writer and editor", the group would be ["writer", "editor"]. For "viewer: writer and (editor but not owner)", the group would be ["writer", "editor"] with Exclusions["editor"] = ["owner"].

type IntersectionGroupData added in v0.3.0

type IntersectionGroupData struct {
	Parts []IntersectionPartData // Individual checks within this AND group
}

IntersectionGroupData contains data for a single intersection group. All parts within a group must be satisfied (AND semantics).

type IntersectionGroupInfo added in v0.3.0

type IntersectionGroupInfo struct {
	Parts []IntersectionPart
}

IntersectionGroupInfo represents a group of parts that must ALL be satisfied (AND). Multiple groups are OR'd together.

type IntersectionPart added in v0.3.0

type IntersectionPart struct {
	IsThis           bool                // [user] - direct assignment check on the same relation
	HasWildcard      bool                // For IsThis parts: whether direct assignment allows wildcards
	Relation         string              // Relation to check
	ExcludedRelation string              // For nested exclusions like "editor but not owner"
	ParentRelation   *ParentRelationInfo // For tuple-to-userset in intersection
}

IntersectionPart represents one part of an intersection check. For "writer and (editor but not owner)", we'd have:

  • {Relation: "writer"}
  • {Relation: "editor", ExcludedRelation: "owner"}

type IntersectionPartData added in v0.3.0

type IntersectionPartData struct {
	// FunctionName is the check function to call (e.g., "check_document_writer")
	FunctionName string

	// IsThis is true if this part is a self-reference ([user] pattern)
	// When true, we check for a direct tuple on the relation being defined
	IsThis bool

	// ThisHasWildcard is true if this "This" part allows wildcard tuples.
	// This is only relevant when IsThis is true. It reflects whether the relation's
	// own direct subject types allow wildcards, NOT whether the relation's overall
	// HasWildcard flag is set (which may include wildcards from closure relations).
	ThisHasWildcard bool

	// HasExclusion is true if this part has a nested exclusion (e.g., "editor but not owner")
	HasExclusion bool

	// ExcludedRelation is the relation to exclude (for nested exclusions)
	ExcludedRelation string

	// IsTTU is true if this part is a tuple-to-userset pattern
	IsTTU bool

	// TTULinkingRelation is the linking relation for TTU patterns (e.g., "parent")
	TTULinkingRelation string

	// TTURelation is the relation to check on the parent for TTU patterns
	TTURelation string
}

IntersectionPartData contains data for a single part of an intersection.

type ListAnchorPathStepData added in v0.3.0

type ListAnchorPathStepData struct {
	Type string // "ttu" or "userset"

	// For TTU steps (e.g., "viewer from parent"):
	LinkingRelation string   // "parent"
	TargetType      string   // "folder" (first type with direct anchor)
	TargetRelation  string   // "viewer"
	AllTargetTypes  []string // All types with direct anchor (e.g., ["document", "folder"])
	RecursiveTypes  []string // Types needing check_permission_internal (same-type recursive TTU)

	// For userset steps (e.g., [group#member]):
	SubjectType             string // "group"
	SubjectRelation         string // "member"
	SatisfyingRelationsList string // SQL-formatted satisfying relations
	HasWildcard             bool   // Whether membership allows wildcards
}

ListAnchorPathStepData contains data for rendering one step in an indirect anchor path.

type ListDispatcherCase added in v0.3.0

type ListDispatcherCase struct {
	ObjectType   string
	Relation     string
	FunctionName string
}

ListDispatcherCase represents a single routing case in the list dispatcher.

type ListDispatcherData added in v0.3.0

type ListDispatcherData struct {
	// HasSpecializedFunctions is true if any specialized list functions were generated.
	HasSpecializedFunctions bool

	// Cases contains the routing cases for specialized functions.
	Cases []ListDispatcherCase
}

ListDispatcherData contains data for rendering list dispatcher templates.

type ListGeneratedSQL added in v0.3.0

type ListGeneratedSQL struct {
	// ListObjectsFunctions contains CREATE OR REPLACE FUNCTION statements
	// for each specialized list_objects function (list_{type}_{relation}_objects).
	ListObjectsFunctions []string

	// ListSubjectsFunctions contains CREATE OR REPLACE FUNCTION statements
	// for each specialized list_subjects function (list_{type}_{relation}_subjects).
	ListSubjectsFunctions []string

	// ListObjectsDispatcher contains the list_accessible_objects dispatcher function
	// that routes to specialized functions or falls back to generic.
	ListObjectsDispatcher string

	// ListSubjectsDispatcher contains the list_accessible_subjects dispatcher function
	// that routes to specialized functions or falls back to generic.
	ListSubjectsDispatcher string
}

ListGeneratedSQL contains all SQL generated for list functions. This is separate from check function generation to keep concerns isolated. Applied atomically during migration alongside check functions.

func GenerateListSQL added in v0.3.0

func GenerateListSQL(analyses []RelationAnalysis, inline InlineSQLData) (ListGeneratedSQL, error)

GenerateListSQL generates specialized SQL functions for list operations. The generated SQL includes:

  • Per-relation list_objects functions (list_{type}_{relation}_objects)
  • Per-relation list_subjects functions (list_{type}_{relation}_subjects)
  • Dispatchers that route to specialized functions or fall back to generic

During the migration phase, relations that cannot be generated will use the generic list functions as fallback. As more patterns are supported, the CanGenerateList criteria will be relaxed.

type ListIndirectAnchorData added in v0.3.0

type ListIndirectAnchorData struct {
	// Path steps from this relation to the anchor
	Path []ListAnchorPathStepData

	// First step's target function (used for composition)
	// For multi-hop chains, we compose with the first step's target, not the anchor.
	// e.g., for job.can_read -> permission.assignee -> role.assignee, we call
	// list_permission_assignee_objects (first step's target), not list_role_assignee_objects (anchor).
	FirstStepTargetFunctionName string // e.g., "list_permission_assignee_objects"

	// Anchor relation info (end of the chain)
	AnchorType              string // Type of anchor relation (e.g., "folder")
	AnchorRelation          string // Anchor relation name (e.g., "viewer")
	AnchorFunctionName      string // Name of anchor's list function (e.g., "list_folder_viewer_objects")
	AnchorSubjectTypes      string // SQL-formatted allowed subject types from anchor
	AnchorHasWildcard       bool   // Whether anchor supports wildcards
	SatisfyingRelationsList string // SQL-formatted list of relations that satisfy the anchor
}

ListIndirectAnchorData contains data for rendering composed access patterns in list templates. This is used when a relation has no direct/implied access but can reach subjects through TTU or userset patterns to an anchor relation that has direct grants.

type ListObjectsFunctionData added in v0.3.0

type ListObjectsFunctionData struct {
	ObjectType     string
	Relation       string
	FunctionName   string
	FeaturesString string
	ClosureValues  string

	// MaxUsersetDepth is the maximum userset chain depth reachable from this relation.
	// Used by depth-exceeded template to report the actual depth in error messages.
	MaxUsersetDepth int

	// ExceedsDepthLimit is true if MaxUsersetDepth >= 25.
	// Routes to depth-exceeded template which immediately raises M2002.
	ExceedsDepthLimit bool

	// RelationList is a SQL-formatted list of simple closure relations to check.
	// e.g., "'viewer', 'editor', 'owner'" - only relations that can use tuple lookup
	RelationList string

	// ComplexClosureRelations are closure relations that need check_permission_internal.
	// These have exclusions or other complex features that can't be resolved via tuple lookup.
	ComplexClosureRelations []string

	// IntersectionClosureRelations are closure relations that have intersection patterns
	// and are list-generatable. These need to be composed with their list function.
	IntersectionClosureRelations []string

	// SubjectIDCheck is the SQL fragment for checking subject_id with wildcard support.
	// e.g., "(t.subject_id = p_subject_id OR t.subject_id = '*')"
	SubjectIDCheck string

	// AllowedSubjectTypes is a SQL-formatted list of allowed subject types.
	// e.g., "'user', 'employee'" - used to enforce model type restrictions.
	AllowedSubjectTypes string

	// Exclusion-related fields (Phase 3)
	HasExclusion bool // true if this relation has exclusion patterns

	// SimpleExcludedRelations are excluded relations that can use direct tuple lookup.
	// These are relations without userset, TTU, exclusion, intersection, or implied closure.
	SimpleExcludedRelations []string

	// ComplexExcludedRelations are excluded relations that need check_permission_internal.
	// These have userset, TTU, intersection, exclusion, or implied closure.
	ComplexExcludedRelations []string

	// ExcludedParentRelations are TTU exclusions like "but not viewer from parent".
	ExcludedParentRelations []ParentRelationInfo

	// ExcludedIntersectionGroups are intersection exclusions like "but not (editor and owner)".
	ExcludedIntersectionGroups []IntersectionGroupInfo

	// Userset-related fields (Phase 4)
	HasUserset      bool                     // true if this relation has userset patterns
	UsersetPatterns []ListUsersetPatternData // [group#member] patterns for UNION expansion

	// TTU/Recursive-related fields (Phase 5)
	ParentRelations []ListParentRelationData // TTU patterns like "viewer from parent"

	// SelfReferentialLinkingRelations is a SQL-formatted list of linking relations
	// from self-referential TTU patterns. Used for depth checking in recursive CTE.
	// e.g., "'parent', 'folder'" when there are TTU patterns viewer from parent, viewer from folder
	SelfReferentialLinkingRelations string

	// Intersection-related fields (Phase 6)
	HasIntersection     bool                    // true if this relation has intersection patterns
	IntersectionGroups  []IntersectionGroupInfo // Intersection groups for list functions
	HasStandaloneAccess bool                    // true if there are access paths outside intersections

	// Phase 8: Indirect anchor for composed access patterns
	HasIndirectAnchor bool                    // true if access is via indirect anchor
	IndirectAnchor    *ListIndirectAnchorData // Anchor info for composed templates

	// Phase 9B: Self-referential userset patterns
	HasSelfReferentialUserset bool // true if any userset pattern references same type/relation
}

ListObjectsFunctionData contains data for rendering list_objects function templates.

type ListParentRelationData added in v0.3.0

type ListParentRelationData struct {
	Relation            string // Relation to check on parent (e.g., "viewer")
	LinkingRelation     string // Relation that links to parent (e.g., "parent")
	AllowedLinkingTypes string // SQL-formatted list of parent types (e.g., "'folder', 'org'")
	ParentType          string // First allowed linking type (for self-referential check)
	IsSelfReferential   bool   // True if any parent type equals the object type

	// CrossTypeLinkingTypes is a SQL-formatted list of linking types that are NOT self-referential.
	// When a parent relation allows both self-referential and cross-type links (e.g., [folder, document]
	// for document.parent), this contains only the cross-type entries (e.g., "'folder'").
	// Used to generate check_permission_internal calls for cross-type parents even when
	// IsSelfReferential is true for the same linking relation.
	CrossTypeLinkingTypes string
	HasCrossTypeLinks     bool // True if CrossTypeLinkingTypes is non-empty
}

ListParentRelationData contains data for rendering TTU pattern expansion in list templates. For a pattern like "viewer from parent", this represents the parent traversal.

type ListSubjectsFunctionData added in v0.3.0

type ListSubjectsFunctionData struct {
	ObjectType     string
	Relation       string
	FunctionName   string
	FeaturesString string
	ClosureValues  string

	// MaxUsersetDepth is the maximum userset chain depth reachable from this relation.
	// Used by depth-exceeded template to report the actual depth in error messages.
	MaxUsersetDepth int

	// ExceedsDepthLimit is true if MaxUsersetDepth >= 25.
	// Routes to depth-exceeded template which immediately raises M2002.
	ExceedsDepthLimit bool

	// RelationList is a SQL-formatted list of simple closure relations to check.
	RelationList string

	// AllSatisfyingRelations is a SQL-formatted list of ALL relations that satisfy this relation.
	// Includes both simple and complex closure relations. Used by userset filter case.
	// e.g., "'can_view', 'viewer'" when viewer implies can_view
	AllSatisfyingRelations string

	// ComplexClosureRelations are closure relations that need check_permission_internal.
	ComplexClosureRelations []string

	// IntersectionClosureRelations are closure relations that have intersection patterns
	// and are list-generatable. These need to be composed with their list function.
	IntersectionClosureRelations []string

	// AllowedSubjectTypes is a SQL-formatted list of allowed subject types.
	// e.g., "'user', 'employee'" - used to enforce model type restrictions.
	AllowedSubjectTypes string

	// HasWildcard is true if the model allows wildcard subjects.
	// When false, wildcard tuples (subject_id = '*') should be excluded from results.
	HasWildcard bool

	// Exclusion-related fields (Phase 3)
	HasExclusion bool // true if this relation has exclusion patterns

	// SimpleExcludedRelations are excluded relations that can use direct tuple lookup.
	SimpleExcludedRelations []string

	// ComplexExcludedRelations are excluded relations that need check_permission_internal.
	ComplexExcludedRelations []string

	// ExcludedParentRelations are TTU exclusions like "but not viewer from parent".
	ExcludedParentRelations []ParentRelationInfo

	// ExcludedIntersectionGroups are intersection exclusions like "but not (editor and owner)".
	ExcludedIntersectionGroups []IntersectionGroupInfo

	// Userset-related fields (Phase 4)
	HasUserset      bool                     // true if this relation has userset patterns
	UsersetPatterns []ListUsersetPatternData // [group#member] patterns for expansion

	// TTU/Recursive-related fields (Phase 5)
	ParentRelations []ListParentRelationData // TTU patterns like "viewer from parent"

	// Intersection-related fields (Phase 6)
	HasIntersection    bool                    // true if this relation has intersection patterns
	IntersectionGroups []IntersectionGroupInfo // Intersection groups for list functions

	// Phase 8: Indirect anchor for composed access patterns
	HasIndirectAnchor bool                    // true if access is via indirect anchor
	IndirectAnchor    *ListIndirectAnchorData // Anchor info for composed templates

	// Phase 9B: Self-referential userset patterns
	HasSelfReferentialUserset bool // true if any userset pattern references same type/relation
}

ListSubjectsFunctionData contains data for rendering list_subjects function templates.

type ListUsersetPatternData added in v0.3.0

type ListUsersetPatternData struct {
	SubjectType     string // e.g., "group"
	SubjectRelation string // e.g., "member"

	// SatisfyingRelationsList is a SQL-formatted list of relations that satisfy SubjectRelation.
	// e.g., "'member', 'admin'" when admin implies member.
	SatisfyingRelationsList string

	// SourceRelationList is a SQL-formatted list of relations to search for userset grant tuples.
	// For direct userset patterns, this is the same as the parent's RelationList.
	// For closure userset patterns (inherited from implied relations), this is the source relation.
	// e.g., "'viewer'" for a pattern inherited from viewer: [group#member]
	SourceRelationList string

	// SourceRelation is the relation where this userset pattern is defined (unquoted).
	// Used for closure patterns to verify permission via check_permission_internal.
	SourceRelation string

	// IsClosurePattern is true if this pattern is inherited from an implied relation.
	// When true, candidates need to be verified via check_permission_internal on the
	// source relation to apply any exclusions or complex features.
	IsClosurePattern bool

	// HasWildcard is true if any satisfying relation allows wildcards.
	// When true, membership check includes subject_id = '*'.
	HasWildcard bool

	// IsComplex is true if this pattern requires check_permission_internal for membership.
	// This happens when any relation in the closure has TTU, exclusion, or intersection.
	IsComplex bool

	// IsSelfReferential is true if SubjectType == ObjectType and SubjectRelation == Relation.
	// Self-referential usersets (e.g., group.member: [group#member]) require recursive CTEs.
	// Non-self-referential usersets use JOIN-based expansion.
	IsSelfReferential bool
}

ListUsersetPatternData contains data for rendering userset pattern expansion in list templates. For a pattern like [group#member], this generates a UNION block that: - Finds grant tuples where subject is group#member - JOINs with membership tuples to find subjects who are members

type MigrateOptions added in v0.3.0

type MigrateOptions struct {
	// DryRun outputs SQL to the provided writer without applying changes to the database.
	// If nil, migration proceeds normally. Use for previewing migrations or generating migration scripts.
	DryRun io.Writer

	// Force re-runs migration even if schema/codegen unchanged. Use when manually fixing corrupted state or testing.
	Force bool

	// SchemaContent is the raw schema text used for checksum calculation to detect schema changes.
	// If empty, skip-if-unchanged optimization is disabled.
	SchemaContent string
}

MigrateOptions controls migration behavior.

type MigrationRecord added in v0.3.0

type MigrationRecord struct {
	SchemaChecksum string
	CodegenVersion string
	FunctionNames  []string
}

MigrationRecord represents a row in the melange_migrations table.

type Migrator

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

Migrator handles loading authorization schemas into PostgreSQL. The migrator is idempotent - safe to run on every application startup.

The migration process:

  1. Creates/replaces check_permission and list_accessible_* functions
  2. Loads generated SQL entrypoints into the database

Usage with Tooling Module

For most use cases, use the tooling module's convenience functions which handle parsing and migration in one step:

import "github.com/pthm/melange/tooling"
err := tooling.Migrate(ctx, db, "schemas")

Use the core Migrator directly when you have pre-parsed TypeDefinitions or need fine-grained control (DDL-only, status checks, etc.):

types, _ := tooling.ParseSchema("schemas/schema.fga")
migrator := schema.NewMigrator(db, "schemas")
err := migrator.MigrateWithTypes(ctx, types)

This separation keeps the core melange package free of OpenFGA dependencies.

func NewMigrator

func NewMigrator(db Execer, schemasDir string) *Migrator

NewMigrator creates a new schema migrator. The schemasDir should contain a schema.fga file in OpenFGA DSL format. The Execer is typically *sql.DB but can be *sql.Tx for testing.

func (*Migrator) ApplyDDL

func (m *Migrator) ApplyDDL(ctx context.Context) error

ApplyDDL applies any base schema required by Melange. With fully generated SQL entrypoints, no base DDL is required.

func (*Migrator) GetLastMigration added in v0.3.0

func (m *Migrator) GetLastMigration(ctx context.Context) (*MigrationRecord, error)

GetLastMigration returns the most recent migration record, or nil if none exists. This can be used to check if migration is needed before calling MigrateWithTypesAndOptions.

func (*Migrator) GetStatus

func (m *Migrator) GetStatus(ctx context.Context) (*Status, error)

GetStatus returns the current migration status. Useful for health checks or migration diagnostics.

func (*Migrator) HasSchema

func (m *Migrator) HasSchema() bool

HasSchema returns true if the schema file exists. Use this to conditionally run migration or skip if not configured.

func (*Migrator) MigrateWithTypes

func (m *Migrator) MigrateWithTypes(ctx context.Context, types []TypeDefinition) error

MigrateWithTypes performs database migration using pre-parsed type definitions. This is the core migration method used by the tooling package's Migrate function.

The method:

  1. Validates the schema (checks for cycles)
  2. Computes derived data (closure)
  3. Analyzes relations and generates specialized SQL functions
  4. Applies everything atomically in a transaction: - Generated specialized functions and dispatcher

This is idempotent - safe to run multiple times with the same types.

Uses a transaction if the db supports it (*sql.DB). This ensures the schema is updated atomically or not at all.

func (*Migrator) MigrateWithTypesAndOptions added in v0.3.0

func (m *Migrator) MigrateWithTypesAndOptions(ctx context.Context, types []TypeDefinition, opts MigrateOptions) error

MigrateWithTypesAndOptions performs database migration with options. This is the full-featured migration method that supports dry-run, skip-if-unchanged, and orphan cleanup.

See MigrateWithTypes for basic usage without options.

func (*Migrator) SchemaPath

func (m *Migrator) SchemaPath() string

SchemaPath returns the path to the schema.fga file. Conventionally named schema.fga by OpenFGA tooling.

type ParentRelationCheck

type ParentRelationCheck struct {
	Relation        string // Relation to check on the parent object (e.g., "viewer")
	LinkingRelation string // Relation that links to the parent object (e.g., "parent")
}

ParentRelationCheck represents a tuple-to-userset (TTU) check. For "viewer from parent" on a folder type, this captures the TTU pattern.

Example: "viewer from parent" where parent: [folder]

  • Relation: "viewer" (the relation to check on the parent object)
  • LinkingRelation: "parent" (the relation that links to the parent object)

The actual parent type(s) are determined at runtime by looking up what types the linking relation can point to.

type ParentRelationData added in v0.3.0

type ParentRelationData struct {
	LinkingRelation     string // Relation linking to parent object (e.g., "parent" in "viewer from parent")
	ParentRelation      string // Relation to verify on parent (e.g., "viewer" in "viewer from parent")
	AllowedLinkingTypes string // SQL-formatted list of allowed parent types (e.g., "'folder', 'org'")
}

ParentRelationData contains data for rendering recursive access checks.

type ParentRelationInfo added in v0.3.0

type ParentRelationInfo struct {
	Relation            string   // Relation to check on parent (e.g., "viewer")
	LinkingRelation     string   // Relation that links to parent (e.g., "parent")
	AllowedLinkingTypes []string // Types allowed for linking relation (e.g., ["folder", "org"])
}

ParentRelationInfo represents a "X from Y" pattern (tuple-to-userset). For "viewer from parent" on a folder type, this would have:

  • Relation: "viewer" (the relation to check on the parent)
  • LinkingRelation: "parent" (the relation that links to the parent)
  • AllowedLinkingTypes: ["folder"] (types the linking relation can point to)

The actual parent type is determined at runtime from the linking relation's subject types. AllowedLinkingTypes captures these for code generation.

type RelationAnalysis added in v0.3.0

type RelationAnalysis struct {
	ObjectType string           // The object type (e.g., "document")
	Relation   string           // The relation name (e.g., "viewer")
	Features   RelationFeatures // Feature flags determining what SQL to generate

	// CanGenerate is true if this relation can use generated SQL for check.
	// This is computed by checking both the relation's own features AND
	// ensuring all relations in the satisfying closure are simply resolvable.
	// Set by ComputeCanGenerate after all relations are analyzed.
	CanGenerate bool

	// CannotGenerateReason explains why CanGenerate is false.
	// Empty when CanGenerate is true.
	CannotGenerateReason string

	// CanGenerateListValue is true if this relation can use generated SQL for list functions.
	// List functions have stricter requirements than check functions because they use
	// set operations (UNION, EXCEPT) rather than boolean composition.
	// Set by ComputeCanGenerate after all relations are analyzed.
	CanGenerateListValue bool

	// CannotGenerateListReason explains why CanGenerateListValue is false.
	// Empty when CanGenerateListValue is true.
	CannotGenerateListReason string

	// For Direct/Implied patterns - from closure table
	SatisfyingRelations []string // Relations that satisfy this one (e.g., ["viewer", "editor", "owner"])

	// For Exclusion patterns
	ExcludedRelations []string // Relations to exclude (for simple "but not X" patterns)

	// SimpleExcludedRelations are excluded relations that can be checked with
	// a direct tuple lookup (no userset, TTU, exclusion, or intersection).
	SimpleExcludedRelations []string

	// ComplexExcludedRelations are excluded relations that need function calls
	// (have userset, TTU, exclusion, intersection, or implied closure).
	// The generated code will call check_permission_internal for these.
	ComplexExcludedRelations []string

	// ExcludedParentRelations captures "but not X from Y" patterns (TTU exclusions).
	// These are resolved by looking up the linking relation Y and calling
	// check_permission_internal for relation X on each linked object.
	ExcludedParentRelations []ParentRelationInfo

	// ExcludedIntersectionGroups captures "but not (A and B)" patterns.
	// These are resolved by ANDing together check_permission_internal calls
	// for each relation in the group.
	ExcludedIntersectionGroups []IntersectionGroupInfo

	// For Userset patterns
	UsersetPatterns []UsersetPattern // [group#member] patterns

	// For Recursive patterns (tuple-to-userset)
	ParentRelations []ParentRelationInfo

	// For Intersection patterns
	IntersectionGroups []IntersectionGroupInfo

	// HasComplexUsersetPatterns is true if any userset pattern is complex
	// (requires check_permission_internal call). When true, the generated
	// function needs PL/pgSQL with cycle detection.
	HasComplexUsersetPatterns bool

	// Direct subject types (for generating direct tuple checks)
	DirectSubjectTypes []string // e.g., ["user", "org"]

	// AllowedSubjectTypes is the union of all subject types from satisfying relations.
	// This is used to enforce type restrictions in generated SQL.
	// Computed by ComputeCanGenerate.
	AllowedSubjectTypes []string

	// SimpleClosureRelations contains relations in the closure that can use tuple lookup.
	// These are relations without exclusions, usersets, recursion, or intersections.
	// Computed by ComputeCanGenerate.
	SimpleClosureRelations []string

	// ComplexClosureRelations contains relations in the closure that need function calls.
	// These are relations with exclusions that are themselves generatable.
	// Computed by ComputeCanGenerate.
	ComplexClosureRelations []string

	// IntersectionClosureRelations contains relations in the closure that have intersection
	// patterns and are list-generatable. These need to be handled by composing with their
	// list function rather than tuple lookup (since intersection relations have no tuples).
	// Computed by computeCanGenerateList.
	IntersectionClosureRelations []string

	// ClosureUsersetPatterns contains userset patterns from closure relations.
	// For example, if `can_view: viewer` and `viewer: [group#member]`, then
	// can_view's ClosureUsersetPatterns includes group#member.
	// This is used by list functions to expand usersets from implied relations.
	// Computed by ComputeCanGenerate.
	ClosureUsersetPatterns []UsersetPattern

	// ClosureParentRelations contains TTU patterns from closure relations.
	// For example, if `can_view: viewer` and `viewer: viewer from parent`, then
	// can_view's ClosureParentRelations includes the parent relation info.
	// This is used by list functions to traverse TTU paths from implied relations.
	// Computed by ComputeCanGenerate.
	ClosureParentRelations []ParentRelationInfo

	// ClosureExcludedRelations contains excluded relations from closure relations.
	// For example, if `can_read: reader` and `reader: [user] but not restricted`,
	// then can_read's ClosureExcludedRelations includes "restricted".
	// This ensures exclusions are applied when accessing through implied relations.
	// Computed by ComputeCanGenerate.
	ClosureExcludedRelations []string

	// IndirectAnchor describes how to reach a relation with direct grants when this
	// relation has no direct/implied access paths. For pure TTU or pure userset patterns,
	// this traces through the pattern to find an anchor relation with [user] or similar.
	// Nil if the relation has direct/implied access or if no anchor can be found.
	// Computed by ComputeCanGenerate via findIndirectAnchor.
	IndirectAnchor *IndirectAnchorInfo

	// MaxUsersetDepth is the maximum userset chain depth reachable from this relation.
	// -1 means infinite (self-referential userset cycle), 0 means no userset patterns.
	// Values >= 25 indicate the relation will always exceed the depth limit.
	// Computed by ComputeCanGenerate via computeMaxUsersetDepth.
	MaxUsersetDepth int

	// ExceedsDepthLimit is true if MaxUsersetDepth >= 25.
	// These relations generate functions that immediately raise M2002.
	ExceedsDepthLimit bool

	// SelfReferentialUsersets lists userset patterns that reference the same type and relation.
	// e.g., for group.member: [user, group#member], this would contain
	// UsersetPattern{SubjectType: "group", SubjectRelation: "member"}
	// These patterns require recursive CTEs to expand nested group membership.
	// Computed by ComputeCanGenerate.
	SelfReferentialUsersets []UsersetPattern

	// HasSelfReferentialUserset is true if len(SelfReferentialUsersets) > 0.
	// When true, the list templates use recursive CTEs to expand the userset chain.
	HasSelfReferentialUserset bool
}

RelationAnalysis contains all data needed to generate SQL for a relation. This struct is populated by AnalyzeRelations and consumed by the SQL generator.

func AnalyzeRelations added in v0.3.0

func AnalyzeRelations(types []TypeDefinition, closure []ClosureRow) []RelationAnalysis

AnalyzeRelations classifies all relations and gathers data needed for SQL generation. It uses the precomputed closure to determine satisfying relations for each relation.

func ComputeCanGenerate added in v0.3.0

func ComputeCanGenerate(analyses []RelationAnalysis) []RelationAnalysis

ComputeCanGenerate walks the dependency graph and sets CanGenerate on each analysis. A relation can be generated if: 1. Its own features allow generation (CanGenerate() on features returns true) 2. ALL relations in its satisfying closure are either:

  • Simply resolvable (can use tuple lookup), OR
  • Complex but generatable (have exclusions but can generate their own function)

3. If the relation has exclusions, excluded relations are classified as:

  • Simple: can use direct tuple lookup (simply resolvable AND no implied closure)
  • Complex: use check_permission_internal call (has TTU, userset, intersection, etc.)

For relations in the closure that need function calls (have exclusions but are generatable), the generated SQL will call their specialized check function rather than using tuple lookup.

This function also: - Propagates HasWildcard: if ANY relation in the closure supports wildcards - Collects AllowedSubjectTypes: union of all subject types from satisfying relations - Partitions closure relations into SimpleClosureRelations and ComplexClosureRelations - Partitions excluded relations into SimpleExcludedRelations and ComplexExcludedRelations

func (*RelationAnalysis) CanGenerateList added in v0.3.0

func (a *RelationAnalysis) CanGenerateList() bool

CanGenerateList returns true if we can generate specialized list SQL for this relation. This returns the CanGenerateListValue field which is computed by ComputeCanGenerate.

List functions have different constraints than check functions because they use set operations (UNION, EXCEPT, INTERSECT) rather than boolean composition.

This is progressively relaxed as more list codegen patterns are implemented: - Phase 2: Direct/Implied patterns (simple tuple lookup with closure) - Phase 3+: Will add Exclusion support - Phase 4+: Will add Userset support - Phase 5+: Will add Recursive/TTU support

When CanGenerateList returns false, the list dispatcher falls through to the generic list functions (list_accessible_objects, list_accessible_subjects).

type RelationDefinition

type RelationDefinition struct {
	Name              string   // Relation name: "owner", "can_read", etc.
	ImpliedBy         []string // Relations that imply this one: ["owner", "admin"]
	ParentRelations   []ParentRelationCheck
	ExcludedRelations []string // For nested exclusions: "(a but not b) but not c" -> ["b", "c"]
	// ExcludedParentRelations captures tuple-to-userset exclusions like "but not viewer from parent".
	ExcludedParentRelations []ParentRelationCheck
	// ExcludedIntersectionGroups captures exclusions that require ALL relations in a group.
	// For "viewer: writer but not (editor and owner)", this is [[editor, owner]].
	ExcludedIntersectionGroups []IntersectionGroup
	// SubjectTypeRefs provides detailed subject type info including userset relations.
	// For [user, group#member], this would contain:
	//   - {Type: "user", Relation: ""}
	//   - {Type: "group", Relation: "member"}
	SubjectTypeRefs []SubjectTypeRef
	// IntersectionGroups contains groups of relations that must ALL be satisfied.
	// Each group is an AND (intersection), multiple groups are OR'd together.
	// For "viewer: writer and editor", IntersectionGroups = [["writer", "editor"]]
	// For "viewer: (a and b) or (c and d)", IntersectionGroups = [["a","b"], ["c","d"]]
	IntersectionGroups []IntersectionGroup
}

RelationDefinition represents a parsed relation. Relations describe who can have what relationship with an object.

A relation can be:

  • Direct: explicitly granted via tuples (SubjectTypeRefs)
  • Implied: granted by having another relation (ImpliedBy)
  • Inherited: derived from a parent object (ParentRelations)
  • Exclusive: granted except for excluded subjects (ExcludedRelations)
  • Userset: granted via group membership (SubjectTypeRefs with Relation set)
  • Intersection: granted if ALL relations in a group are satisfied

type RelationFeatures added in v0.3.0

type RelationFeatures struct {
	HasDirect       bool // [user] - direct tuple lookup, the relation accepts specific subject types
	HasImplied      bool // viewer: editor - this relation is satisfied by another relation via closure
	HasWildcard     bool // [user:*] - allows wildcard grants that match any subject_id
	HasUserset      bool // [group#member] - grants via membership in another object's relation
	HasRecursive    bool // viewer from parent - grants inherited through parent/child relationships (TTU)
	HasExclusion    bool // but not blocked - denies access based on negative conditions
	HasIntersection bool // writer and editor - requires AND of all parts
}

RelationFeatures tracks which features a relation uses. Multiple features can be present and will be composed in generated SQL. For example, a relation with HasDirect, HasUserset, and HasRecursive will generate SQL that ORs together all three access paths.

func (RelationFeatures) CanGenerate added in v0.3.0

func (f RelationFeatures) CanGenerate() bool

CanGenerate returns true if we can generate specialized SQL for this feature set. This checks the features themselves - for full generation eligibility, also check that all dependency relations are generatable via RelationAnalysis.CanGenerate.

func (RelationFeatures) IsClosureCompatible added in v0.3.0

func (f RelationFeatures) IsClosureCompatible() bool

IsClosureCompatible returns true if this relation can participate in a closure-based tuple lookup (relation IN ('a', 'b', 'c')) WITHOUT additional permission logic.

This returns false for relations with exclusions because when a relation A implies relation B (e.g., can_view: viewer), and B has an exclusion (e.g., viewer: [user] but not blocked), checking A requires applying B's exclusion. A simple closure lookup won't do this.

Note: A relation can still generate code for ITSELF even if it has an exclusion - its generated function handles the exclusion. But it can't be part of another relation's closure lookup.

func (RelationFeatures) IsSimplyResolvable added in v0.3.0

func (f RelationFeatures) IsSimplyResolvable() bool

IsSimplyResolvable returns true if this relation can be fully resolved with a simple tuple lookup (no userset JOINs, recursion, exclusions, etc.). This is used to check if excluded relations can be handled with a simple EXISTS check, since exclusions on excluded relations would require full permission resolution.

func (RelationFeatures) NeedsCycleDetection added in v0.3.0

func (f RelationFeatures) NeedsCycleDetection() bool

NeedsCycleDetection returns true if the generated function needs cycle detection.

func (RelationFeatures) NeedsPLpgSQL added in v0.3.0

func (f RelationFeatures) NeedsPLpgSQL() bool

NeedsPLpgSQL returns true if the generated function needs PL/pgSQL (vs pure SQL). Required for cycle detection (variables, IF statements).

func (RelationFeatures) String added in v0.3.0

func (f RelationFeatures) String() string

String returns a human-readable description of the features.

type Status

type Status struct {
	// SchemaExists indicates if the schema.fga file exists on disk.
	SchemaExists bool

	// TuplesExists indicates if the melange_tuples relation exists (view, table, or materialized view).
	// This must be created by the user to map their domain tables.
	TuplesExists bool
}

Status represents the current migration state. Use GetStatus to check if the authorization system is properly configured.

type SubjectTypeRef

type SubjectTypeRef struct {
	Type     string // Subject type: "user", "group", etc.
	Relation string // For userset refs: the relation (e.g., "member" in [group#member])
	Wildcard bool   // True if this is a wildcard reference (user:*)
}

SubjectTypeRef represents a subject type reference in a relation definition. For userset references like [group#member], Type is "group" and Relation is "member". For direct references like [user], Type is "user" and Relation is empty.

type TTUExclusionCheckData added in v0.3.0

type TTUExclusionCheckData struct {
	ObjectType                string
	ExcludedRelation          string // The relation to check on the parent (e.g., "viewer")
	LinkingRelation           string // The linking relation (e.g., "parent")
	AllowedLinkingTypes       string // SQL-formatted list of allowed parent types (e.g., "'folder', 'org'")
	InternalCheckFunctionName string
}

TTUExclusionCheckData contains data for rendering TTU exclusion checks. These check "but not X from Y" patterns by looking up the linking relation and calling check_permission_internal for each linked object.

type TypeDefinition

type TypeDefinition struct {
	Name      string
	Relations []RelationDefinition
}

TypeDefinition represents a parsed type from an .fga file. Each type definition describes an object type (user, repository, etc.) and the relations that can exist on objects of that type.

type UsersetCheckData added in v0.3.0

type UsersetCheckData struct {
	ObjectType      string
	Relation        string
	SubjectType     string
	SubjectRelation string

	// SatisfyingRelationsList is a SQL-formatted list of relations that satisfy SubjectRelation.
	// For example: "'member_c4', 'member_c3', 'member_c2', 'member_c1', 'member'"
	SatisfyingRelationsList string

	// HasWildcard is true if the subject relation supports wildcards.
	// When true, the membership check should also match subject_id = '*'.
	HasWildcard bool

	InternalCheckFunctionName string
}

UsersetCheckData contains data for rendering userset check template.

type UsersetPattern added in v0.3.0

type UsersetPattern struct {
	SubjectType     string // e.g., "group"
	SubjectRelation string // e.g., "member"

	// SatisfyingRelations contains all relations in the closure of SubjectRelation.
	// For example, if SubjectRelation="member_c4" and member_c4 is implied by member,
	// this would contain ["member_c4", "member_c3", "member_c2", "member_c1", "member"].
	// This is populated by ComputeCanGenerate from the closure data.
	SatisfyingRelations []string

	// SourceRelation is the relation where this userset pattern is defined.
	// For direct patterns, this is the same as the relation being analyzed.
	// For closure patterns (inherited from implied relations), this is the source relation.
	// e.g., for can_view: viewer where viewer: [group#member], SourceRelation="viewer"
	// This is used by list functions to search for grant tuples in the correct relation.
	SourceRelation string

	// HasWildcard is true if any relation in the closure supports wildcards.
	// When true, the userset check should match membership tuples with subject_id = '*'.
	// This is populated by ComputeCanGenerate from the subject relation's features.
	HasWildcard bool

	// IsComplex is true if any relation in the closure is not closure-compatible
	// (has userset, TTU, exclusion, or intersection). When true, the userset check
	// must call check_permission_internal to verify membership instead of using
	// a simple tuple JOIN.
	// This is populated by ComputeCanGenerate.
	IsComplex bool
}

UsersetPattern represents a [type#relation] pattern in a relation definition. For example, [group#member] would have SubjectType="group" and SubjectRelation="member".

type UsersetRule

type UsersetRule struct {
	ObjectType                string
	Relation                  string
	TupleRelation             string
	SubjectType               string
	SubjectRelation           string
	SubjectRelationSatisfying string
}

UsersetRule represents a precomputed userset rule with relation closure applied. Userset rules handle permissions granted via group membership, expressed in OpenFGA as [group#member]. Unlike direct subject types where the tuple directly grants permission, usersets require checking if the subject has the specified relation on the group object.

Each row means: a tuple with tuple_relation on object_type can satisfy relation when the tuple subject is subject_type#subject_relation.

The rules are precomputed by expanding userset references through the relation closure data. This allows SQL to resolve userset permissions efficiently without nested subqueries for each implied relation.

Example: For "viewer: [group#member]" where admin->member, the rules include:

  • {relation: "viewer", tuple_relation: "viewer", subject_type: "group", subject_relation: "member", subject_relation_satisfying: "member"}
  • {relation: "viewer", tuple_relation: "viewer", subject_type: "group", subject_relation: "member", subject_relation_satisfying: "admin"}

This enables check_permission to match tuples where the subject has either member or admin on the group, without recursive relation resolution at query time.

func ToUsersetRules

func ToUsersetRules(types []TypeDefinition, closureRows []ClosureRow) []UsersetRule

ToUsersetRules expands userset references using the relation closure. This precomputes which tuple relations can satisfy a target relation for userset rules.

The expansion combines two sources of transitivity:

  1. Object relation closure: viewer might be satisfied by editor (implied-by)
  2. Subject relation closure: member might be satisfied by admin (implied-by)

By precomputing the cross-product of these closures, SQL can match userset permissions with a simple JOIN instead of recursive CTEs for each check.

This is analogous to ComputeRelationClosure but handles the subject-side relation traversal required for userset references.

Jump to

Keyboard shortcuts

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