fga

package
v0.267.0 Latest Latest
Warning

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

Go to latest
Published: Aug 3, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package fga holds OpenFGA domain helpers shared by the CLI commands and the TUI: parsing tuple shorthand and turning an authorization model into a graph.

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrReadFilterNeedsObject = errors.New("the filter needs an object — a whole type (document:) or one object (document:roadmap)")
	ErrReadFilterBareType    = errors.New("a bare object type isn't enough — add an object id (document:roadmap) or a user")
)

The two halves of the cross-field rule, exported so each surface can phrase them for its own audience — a form names its fields, a command names flags.

Functions

func ConditionRows

func ConditionRows(c *openfga.RelationshipCondition) [][2]string

ConditionRows renders a tuple's condition as label/value pairs for detail views: a "Condition" row with the name, and a "Condition Context" row with the context as compact JSON when present. Returns nil for an unconditioned tuple.

func DecodeStrictJSON added in v0.267.0

func DecodeStrictJSON(data []byte, v any) error

DecodeStrictJSON JSON-decodes data into v, rejecting unknown fields anywhere in the value so a mistyped field name surfaces as a parse error instead of being silently dropped. It also rejects trailing data after the value, matching json.Unmarshal's stricter behavior (json.Decoder.Decode alone only reads one value and ignores what follows).

Every document the CLI accepts from a user should go through this: dropping an unrecognised field quietly turns a typo into a semantic change, since the field the author meant to set keeps its zero value.

func ExpandTree

func ExpandTree(root *ResNode, rootRef string, expand Expander, tupleset func(object, relation string) []string, maxDepth, maxNodes int) bool

ExpandTree recursively expands computed-userset and tuple-to-userset leaves in place, attaching each reference's own resolution subtree so nested branches appear instead of dead-end leaves — e.g. a `viewer` node that resolves through `owner` gains `owner`'s subtree (and, in turn, its users) as a child.

OpenFGA's Expand API resolves only one level, so this issues a fresh expand per referenced relation via the `expand` callback; `tupleset` lists the objects a tuple-to-userset points at. rootRef ("object#relation" of root) seeds cycle detection. maxDepth bounds recursion depth and maxNodes bounds the total number of expansions (i.e. extra API calls), so a deep or cyclic model can't fan out unbounded. Call this before MarkGranted. ExpandTree returns whether it was truncated: an expandable arm (a computed or tuple-to-userset leaf) was left unexpanded because the depth or node budget ran out. A cycle stop does NOT count as truncation (it is correct). A caller computing coverage from the tree should treat a truncated tree as partial — arms below the cut were never evaluated and so can't be credited.

func FormatContextJSON

func FormatContextJSON(ctx map[string]any) string

FormatContextJSON renders a context map as compact JSON, or "" when empty.

func FormatContextualTuple

func FormatContextualTuple(k openfga.TupleKey) string

FormatContextualTuple renders a contextual tuple in userset shorthand, with a " [conditionName]" suffix when the tuple carries a condition.

func FormatTuple

func FormatTuple(k openfga.TupleKey) string

func FormatUserset

func FormatUserset(object, relation, user string) string

FormatUserset renders a tuple's parts in userset shorthand: "object#relation@user". Taking parts rather than a typed key lets it serve both openfga.TupleKey and openfga.CheckRequestTupleKey (which lack a common interface).

func MarkGranted

func MarkGranted(root *ResNode, user string, r GrantResolver) bool

MarkGranted annotates each node with whether it grants `user`. Direct-user leaves match the user string exactly, computed usersets resolve via a Check, and tuple-to-userset leaves read the tupleset then Check the computed relation on each related object. It returns the root's grant status.

func ParseJSONObject

func ParseJSONObject(label, s string) (map[string]any, error)

ParseJSONObject parses s as a JSON object into a map, returning nil for empty input. label names the field in the error message (e.g. "--context" or "context") so both the CLI and TUI can share one implementation.

func ParseTuple

func ParseTuple(user, relation, object string) (openfga.TupleKey, error)

ParseTuple parses the canonical "user relation object" triple from three separate arguments and returns a TupleKey. Each part is validated lightly: user and object should look like "type:id" (user may also carry "#relation" or be a wildcard "type:*").

func RenderResolution

func RenderResolution(root *ResNode, user, object, relation string) string

RenderResolution draws the resolution rooted at object#relation as the playground-style node-link diagram. user is the queried user (its box is highlighted); nodes and connectors on the granting branch are tinted.

func SplitObject

func SplitObject(object string) (typ, id string)

SplitObject splits "type:id" into its type and id components.

func SplitTTU added in v0.266.0

func SplitTTU(label string) (target, via string, ok bool)

SplitTTU parses a tuple-to-userset edge label of the form "target from via".

func Triple

func Triple(args []string, userFlag, relationFlag, objectFlag string) (user, relation, object string, err error)

Triple resolves a user/relation/object triple from positional args and the --user/--relation/--object flags. Flags set the fields they name; the remaining positionals then fill the still-unset fields left to right. This means `--user user:anne viewer document:roadmap` reads the two positionals as relation and object (rather than shifting them by index). Extra positionals that can't fill an unset field are an error, so a flag and a positional never silently fight over the same field. It errors if any part is missing.

func ValidateObjectRef

func ValidateObjectRef(object string) error

ValidateObjectRef checks that object is a concrete "type:id" (no wildcard, no userset), matching ParseTuple's object rules.

func ValidateReadObject added in v0.267.0

func ValidateReadObject(s string) error

ValidateReadObject checks a /read filter's object on its own: a whole type ("document:") or one object ("document:roadmap"). The server's combination rule spans fields, so it lives in ReadFilter.Validate; this is what a form can check as the user types, and what a flag can check in isolation.

func ValidateReadRelation added in v0.267.0

func ValidateReadRelation(s string) error

ValidateReadRelation checks a /read filter's relation on its own. A relation is a bare name — the server's own pattern is ^[^:#@\s]{1,50}$ — so anything shaped like a reference is a swapped field rather than a relation.

func ValidateReadUser added in v0.267.0

func ValidateReadUser(s string) error

ValidateReadUser checks a /read filter's user on its own. Unlike a tuple being written, it is optional, and /read matches usersets and wildcards, so this only asks for a type — the exact formats are the server's business.

func ValidateUserRef

func ValidateUserRef(user string) error

ValidateUserRef checks that user is a "type:id", a wildcard "type:*", or a userset "type:id#relation" — the same shape ParseTuple accepts for the user position — so a swapped argument yields a friendly hint, not a raw 400.

Types

type DiagramEdge

type DiagramEdge struct {
	From string `json:"from"`
	To   string `json:"to"`
	Kind string `json:"kind"`
	Via  string `json:"via"`
}

DiagramEdge is a directed dependency between two object types: type From has a relation that can be satisfied by users (or usersets) of type To. Kind is "direct" or "ttu" (tuple-to-userset / inherited). Via names the relation the dependency flows through (the relation on From for direct edges, the tupleset relation for ttu edges).

type Expander

type Expander func(object, relation string) *ResNode

Expander resolves an object#relation to its (single-level) Expand subtree, or nil when it can't be expanded (API error, or no such resolution).

type GrantResolver

type GrantResolver struct {
	Check    func(user, relation, object string) bool
	Tupleset func(object, relation string) []string
	// CheckDirectLeaves, when true, makes a direct-user leaf defer to Check so
	// tuple conditions are honored; this requires Check to forward the request
	// context (Context/ContextualTuples) so an ABAC-conditioned direct tuple is
	// evaluated against the same context the engine used. When false (the
	// default) direct membership from the Expand tree is trusted as-is — the
	// cheap, context-free behavior a caller with no context (e.g. the
	// playground) wants, avoiding a remote Check per direct leaf.
	CheckDirectLeaves bool
}

GrantResolver supplies the live lookups MarkGranted needs. Check reports whether `user` has `relation` on `object`. Tupleset returns the objects related to `object` via `relation` — the "user" side of matching tuples — and may be nil to skip tuple-to-userset resolution.

type Graph

type Graph struct {
	SchemaVersion string     `json:"schema_version"`
	Types         []TypeNode `json:"types"`
	// Edges are the inter-type dependencies used to draw the node-link diagram.
	Edges []DiagramEdge `json:"edges"`
	// contains filtered or unexported fields
}

Graph is the parsed, render-ready view of an authorization model.

func ParseModel

func ParseModel(m *openfga.AuthorizationModel) Graph

ParseModel converts an authorization model into a Graph by interpreting the relation rewrite rules and the directly-related-user-types metadata. Slices are initialized (never nil) so `--json` output serializes empty collections as [] rather than null.

func (Graph) RelationsForObject added in v0.267.0

func (g Graph) RelationsForObject(object string) ([]string, error)

RelationsForObject returns every relation defined on the type of object (the part before the ":"), in the graph's sorted order. It backs the list-relations query, which tests a user against all of them. It errors when object carries no type, the type is absent from the model, or the type has no relations — none of which yield anything to test.

func (Graph) Render

func (g Graph) Render() string

Render draws the graph as a colored tree with a legend. width is advisory.

func (Graph) RenderDiagram

func (g Graph) RenderDiagram() string

RenderDiagram draws the authorization model as a node-link diagram: one rounded card per object type (a colored header plus its relations) laid out left→right by dependency depth, with edges routed orthogonally around the cards (never through them) and colored by resolution kind. The result is a wide, multi-line string meant to be shown inside a scrollable/pannable viewport.

func (Graph) RenderWeightedDiagram added in v0.265.0

func (g Graph) RenderWeightedDiagram() string

RenderWeightedDiagram draws the fully-expanded weighted graph (relation, operator, direct-grouping and terminal-type nodes with per-terminal-type weights), in the style of openfga/model-visualizer. It is built lazily so the text and JSON model-graph outputs, which never use it, do not pay for it.

func (Graph) Summary

func (g Graph) Summary() string

Summary returns a one-line summary like "4 types, 11 relations".

type ReadFilter added in v0.267.0

type ReadFilter struct {
	User     string
	Relation string
	Object   string
}

ReadFilter is the tuple_key a /read request narrows on. The zero value means no filter: read the whole store.

It lives here so the CLI's `tuples read` and the playground's Tuples pane share one shape and one rule — the server's cross-field requirement is easy to trip and its 400 says nothing useful, so both surfaces catch it locally.

func NewReadFilter added in v0.267.0

func NewReadFilter(user, relation, object string) ReadFilter

NewReadFilter builds a filter from raw input, trimming each field. Both surfaces need this: the server's own patterns reject whitespace, so a padded value would pass the local check and then fail the round trip it exists to prevent — and a whitespace-only field must read as unset, not as a filter.

func (ReadFilter) Active added in v0.267.0

func (f ReadFilter) Active() bool

Active reports whether the filter narrows anything.

func (ReadFilter) TupleKey added in v0.267.0

func (f ReadFilter) TupleKey() *openfga.ReadRequestTupleKey

TupleKey renders the filter for a ReadRequest, or nil when nothing is set — /read treats an all-empty tuple_key differently from an absent one.

func (ReadFilter) Validate added in v0.267.0

func (f ReadFilter) Validate() error

Validate mirrors the server-side /read tuple_key rule (openfga v1.18.1, ReadQuery.Execute): the object must carry a type, and the object id and user cannot both be empty. The zero filter is valid — it means "no filter".

The object is split on its first colon exactly as the server splits it (tuple.SplitObject), so a colon-less object yields no type and is caught here rather than round-tripping into a 400. This is the cross-field half of the rule, which no single field validator can express; the per-field halves are ValidateReadUser, ValidateReadRelation and ValidateReadObject.

type Relation

type Relation struct {
	Name  string         `json:"name"`
	Edges []RelationEdge `json:"edges"`
	// Weight is the worst-case resolution cost (>=1); -1 when Recursive.
	Weight    int  `json:"weight"`
	Recursive bool `json:"recursive"`
}

Relation is a single relation on a type with its resolution edges.

type RelationEdge

type RelationEdge struct {
	// Kind is "direct", "computed", or "ttu" (tuple-to-userset).
	Kind string `json:"kind"`
	// Label is a human-readable description of the edge target.
	Label string `json:"label"`
}

RelationEdge describes one resolution path for a relation.

type ResNode

type ResNode struct {
	Name     string     // the "object#relation" this node resolves
	Op       ResOp      // how Children combine (ResLeaf → no children)
	Children []*ResNode // operands for union / intersection / exclusion

	// Leaf payloads — at most one is populated when Op == ResLeaf:
	Users    []string // direct users/usersets, e.g. ["user:anne", "team:eng#member"]
	Computed string   // a computed userset, e.g. "document:roadmap#owner"
	TTUFrom  string   // tuple-to-userset: the tupleset relation, e.g. "document:x#parent"
	TTUTo    []string // the computed usersets reached through that tupleset

	Granted bool // set by MarkGranted: this node reaches the queried user
}

ResNode is one node of a Check resolution. Each node resolves a single object#relation, either as a leaf (direct users, a computed userset, or a tuple-to-userset) or as a boolean combination of child nodes.

func GrantedPath

func GrantedPath(n *ResNode) *ResNode

GrantedPath returns a pruned copy of the tree keeping only the branch(es) that reach the user — the ACL resolution path. It returns nil when nothing grants (e.g. a denied check). Call MarkGranted first.

func ParseResolution

func ParseResolution(tree map[string]any) (*ResNode, bool)

ParseResolution builds a ResNode tree from an Expand response's untyped tree (openfga.ExpandResponse.Tree). It returns false when the tree has no root.

type ResOp

type ResOp int

ResOp identifies how a resolution node combines its children.

const (
	ResLeaf         ResOp = iota // a direct-users / computed / tuple-to-userset leaf
	ResUnion                     // any child grants
	ResIntersection              // all children grant
	ResExclusion                 // base grants and subtract does not (difference)
)

type TypeNode

type TypeNode struct {
	Name      string     `json:"name"`
	Relations []Relation `json:"relations"`
}

TypeNode is one object type in the model with its relations.

Jump to

Keyboard shortcuts

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