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:
- Schema representation (TypeDefinition, RelationDefinition) - parsed FGA models
- SQL model generation (ToAuthzModels) - flattening rules for SQL generation
- 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:
- Parse schema via tooling package (returns []TypeDefinition)
- 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
- Variables
- func CollectFunctionNames(analyses []RelationAnalysis) []string
- func ComputeSchemaChecksum(content string) string
- func DetectCycles(types []TypeDefinition) error
- func GenerateGo(w io.Writer, types []TypeDefinition, cfg *GenerateConfig) error
- func IsCyclicSchemaErr(err error) bool
- func RelationSubjects(types []TypeDefinition, objectType string, relation string) []string
- func SubjectTypes(types []TypeDefinition) []string
- type AnchorPathStep
- type AuthzModel
- type CheckFunctionData
- type ClosureRow
- type ComplexExclusionCheckData
- type ComplexUsersetCheckData
- type DirectCheckData
- type DispatcherCase
- type DispatcherData
- type ExclusionCheckData
- type Execer
- type GenerateConfig
- type GeneratedSQL
- type ImpliedFunctionCall
- type IndirectAnchorInfo
- type InlineSQLData
- type IntersectionExclusionCheckData
- type IntersectionGroup
- type IntersectionGroupData
- type IntersectionGroupInfo
- type IntersectionPart
- type IntersectionPartData
- type ListAnchorPathStepData
- type ListDispatcherCase
- type ListDispatcherData
- type ListGeneratedSQL
- type ListIndirectAnchorData
- type ListObjectsFunctionData
- type ListParentRelationData
- type ListSubjectsFunctionData
- type ListUsersetPatternData
- type MigrateOptions
- type MigrationRecord
- type Migrator
- func (m *Migrator) ApplyDDL(ctx context.Context) error
- func (m *Migrator) GetLastMigration(ctx context.Context) (*MigrationRecord, error)
- func (m *Migrator) GetStatus(ctx context.Context) (*Status, error)
- func (m *Migrator) HasSchema() bool
- func (m *Migrator) MigrateWithTypes(ctx context.Context, types []TypeDefinition) error
- func (m *Migrator) MigrateWithTypesAndOptions(ctx context.Context, types []TypeDefinition, opts MigrateOptions) error
- func (m *Migrator) SchemaPath() string
- type ParentRelationCheck
- type ParentRelationData
- type ParentRelationInfo
- type RelationAnalysis
- type RelationDefinition
- type RelationFeatures
- type Status
- type SubjectTypeRef
- type TTUExclusionCheckData
- type TypeDefinition
- type UsersetCheckData
- type UsersetPattern
- type UsersetRule
Constants ¶
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.
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 ¶
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
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 ¶
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
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
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
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
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:
- Creates/replaces check_permission and list_accessible_* functions
- 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 ¶
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 ¶
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 ¶
GetStatus returns the current migration status. Useful for health checks or migration diagnostics.
func (*Migrator) HasSchema ¶
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:
- Validates the schema (checks for cycles)
- Computes derived data (closure)
- Analyzes relations and generates specialized SQL functions
- 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 ¶
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:
- Object relation closure: viewer might be satisfied by editor (implied-by)
- 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.