The go-spocp library provides three main packages:
spocp- Main engine and APIpkg/sexp- S-expression parser and typespkg/starform- Star form implementationspkg/compare- Comparison algorithm
The main SPOCP authorization engine.
For most use cases, simply use spocp.New():
- Automatically optimizes based on ruleset characteristics
- No configuration needed
- Optimal performance for both small and large rulesets
- Returns an
*AdaptiveEngine
Use NewEngine() or NewEngineWithIndexing() for:
- Benchmarking: Measuring specific indexing strategies
- Testing: Verifying indexing behavior
- Known workloads: When profiling shows a specific strategy is always optimal
type AdaptiveEngine struct {
// contains filtered or unexported fields
}Recommended for most users. Automatically decides whether to use tag-based indexing based on ruleset characteristics. Provides optimal performance without manual configuration.
func New() *AdaptiveEngineRecommended constructor. Creates a new adaptive SPOCP engine that automatically optimizes query strategy. This is an alias for NewAdaptiveEngine().
Example:
engine := spocp.New() // Recommended!
// Add rules - indexing automatically adapts
engine.AddRule("(4:read4:file)")
// Check if indexing was enabled
stats := engine.Stats()
fmt.Printf("Indexing: %v\n", stats.IndexingEnabled)func NewAdaptiveEngine() *AdaptiveEngineCreates a new adaptive SPOCP engine. This is the same as New() - use whichever name you prefer.
Example:
engine := spocp.NewAdaptiveEngine() // Same as spocp.New()func (ae *AdaptiveEngine) Stats() AdaptiveStatsReturns statistics about the adaptive behavior including rule counts, tag diversity, and whether indexing is enabled.
Example:
stats := engine.Stats()
fmt.Printf("Rules: %d, Tags: %d, Indexing: %v\n",
stats.TotalRules, stats.UniqueTags, stats.IndexingEnabled)func (ae *AdaptiveEngine) ForceIndexing(enabled bool)Manually override the adaptive indexing decision. Useful for testing or debugging.
Example:
engine.ForceIndexing(true) // Force enable indexing
engine.ForceIndexing(false) // Force disable indexingSee ADAPTIVE_ENGINE.md for detailed documentation.
type Engine struct {
// contains filtered or unexported fields
}The base SPOCP policy engine. Use AdaptiveEngine instead unless you need explicit control over indexing for benchmarking or testing.
func NewEngine() *EngineCreates a new empty SPOCP engine.
Example:
engine := spocp.NewEngine()func (e *Engine) AddRule(rule string) errorAdds a policy rule to the engine using canonical S-expression format.
Parameters:
rule- Canonical form S-expression (e.g.,"(5:admin)")
Returns:
error- Parse error if the rule is invalid
Example:
err := engine.AddRule("(4:http(4:page10:index.html))")func (e *Engine) AddRuleElement(rule sexp.Element)Adds a pre-parsed rule element to the engine.
Parameters:
rule- A parsed S-expression element
Example:
rule := sexp.NewList("http", sexp.NewAtom("GET"))
engine.AddRuleElement(rule)func (e *Engine) Query(query string) (bool, error)Checks if a query (in canonical form) is authorized by any rule.
Parameters:
query- Canonical form S-expression
Returns:
bool- True if authorizederror- Parse error if the query is invalid
Example:
authorized, err := engine.Query("(4:http3:GET)")func (e *Engine) QueryElement(query sexp.Element) boolChecks if a pre-parsed query element is authorized.
Parameters:
query- A parsed S-expression element
Returns:
bool- True if query <= some rule in the engine
Example:
query := sexp.NewList("http", sexp.NewAtom("GET"))
if engine.QueryElement(query) {
fmt.Println("Authorized")
}func (e *Engine) FindMatchingRules(query string) ([]sexp.Element, error)Returns all rules that authorize the given query.
Parameters:
query- Canonical form S-expression
Returns:
[]sexp.Element- All matching ruleserror- Parse error if the query is invalid
func (e *Engine) RuleCount() intReturns the number of rules in the engine.
func (e *Engine) Clear()Removes all rules from the engine.
S-expression parsing and representation.
type Element interface {
String() string
IsAtom() bool
IsList() bool
IsStarForm() bool
}Base interface for all S-expression elements.
type Atom struct {
Value string
}Represents an octet string (atom) in an S-expression.
func NewAtom(value string) *AtomCreates a new atom with the given value.
Example:
atom := sexp.NewAtom("hello")
fmt.Println(atom.String()) // "5:hello"type List struct {
Tag string
Elements []Element
}Represents an S-expression list with a tag and elements.
func NewList(tag string, elements ...Element) *ListCreates a new list with the given tag and elements.
Example:
list := sexp.NewList("http",
sexp.NewAtom("GET"),
sexp.NewList("page", sexp.NewAtom("index.html")),
)type Parser struct {
// contains filtered or unexported fields
}Parser for canonical S-expressions.
func NewParser(input string) *ParserCreates a new parser for the given canonical form input.
func (p *Parser) Parse() (Element, error)Parses the input and returns an Element.
Example:
parser := sexp.NewParser("(5:hello5:world)")
elem, err := parser.Parse()func AdvancedForm(elem Element) stringConverts a canonical S-expression to human-readable advanced form.
Example:
elem := sexp.NewList("http", sexp.NewAtom("GET"))
fmt.Println(sexp.AdvancedForm(elem)) // "(http GET)"Star form implementations for pattern matching.
type StarForm interface {
sexp.Element
Match(value sexp.Element) bool
Type() string
}Base interface for all star forms.
type Wildcard struct{}Matches any single element. Represents (*).
Example:
wildcard := &starform.Wildcard{}
// Matches anythingtype Set struct {
Elements []sexp.Element
}Matches any element in the set. Represents (* set ...).
Example:
set := &starform.Set{
Elements: []sexp.Element{
sexp.NewAtom("read"),
sexp.NewAtom("write"),
sexp.NewAtom("execute"),
},
}type Range struct {
RangeType RangeType
LowerBound *RangeBound
UpperBound *RangeBound
}Matches values within a range. Represents (* range ...).
Example:
// Match numbers 10-20
numRange := &starform.Range{
RangeType: starform.RangeNumeric,
LowerBound: &starform.RangeBound{
Op: starform.OpGE,
Value: "10",
},
UpperBound: &starform.RangeBound{
Op: starform.OpLE,
Value: "20",
},
}const (
RangeAlpha RangeType = "alpha"
RangeNumeric RangeType = "numeric"
RangeDate RangeType = "date"
RangeTime RangeType = "time"
RangeIPv4 RangeType = "ipv4"
RangeIPv6 RangeType = "ipv6"
)const (
OpLT RangeOp = "lt" // less than
OpLE RangeOp = "le" // less than or equal
OpGT RangeOp = "gt" // greater than
OpGE RangeOp = "ge" // greater than or equal
)type Prefix struct {
Value string
}Matches strings with the given prefix. Represents (* prefix ...).
Example:
prefix := &starform.Prefix{Value: "/etc/"}
// Matches "/etc/passwd", "/etc/hosts", etc.type Suffix struct {
Value string
}Matches strings with the given suffix. Represents (* suffix ...).
Example:
suffix := &starform.Suffix{Value: ".pdf"}
// Matches "document.pdf", "report.pdf", etc.Comparison algorithm for S-expressions.
func LessPermissive(s, t sexp.Element) boolReturns true if S is less permissive than T (S <= T).
This implements the partial order relation defined in the SPOCP specification where S <= T means "rule S grants fewer permissions than rule T".
Parameters:
s- The subject S-expressiont- The target S-expression
Returns:
bool- True if s <= t
Example:
s := sexp.NewList("fruit", sexp.NewAtom("apple"), sexp.NewAtom("red"))
t := sexp.NewList("fruit", sexp.NewAtom("apple"))
if compare.LessPermissive(s, t) {
fmt.Println("s is less permissive than t")
}Comparison Rules:
T = (*)→ always true (wildcard matches anything)- Both atoms and equal → true
- S is atom, T is star form matching S → true
- Both ranges and T contains S → true
- Both prefixes and T's prefix contains S → true
- Both suffixes and T's suffix contains S → true
- Both lists,
len(T) <= len(S)andS[i] <= T[i]for all i → true - S is set and all elements
<= T→ true - T is set and S
<=some element → true
func Normalize(elem sexp.Element) sexp.ElementNormalizes an S-expression by joining ranges and atoms in sets.
// Allow any user to GET a specific page
rule := sexp.NewList("http",
sexp.NewList("page", sexp.NewAtom("index.html")),
sexp.NewList("action", sexp.NewAtom("GET")),
sexp.NewList("user"),
)
engine.AddRuleElement(rule)
// Query: Can alice GET index.html?
query := sexp.NewList("http",
sexp.NewList("page", sexp.NewAtom("index.html")),
sexp.NewList("action", sexp.NewAtom("GET")),
sexp.NewList("user", sexp.NewAtom("alice")),
)
authorized := engine.QueryElement(query) // true// Allow access to files under /etc/
rule := sexp.NewList("file", &starform.Prefix{Value: "/etc/"})
engine.AddRuleElement(rule)
query := sexp.NewList("file", sexp.NewAtom("/etc/passwd"))
authorized := engine.QueryElement(query) // true// Work hours: 08:00 - 17:00
rule := sexp.NewList("access", &starform.Range{
RangeType: starform.RangeTime,
LowerBound: &starform.RangeBound{Op: starform.OpGE, Value: "08:00:00"},
UpperBound: &starform.RangeBound{Op: starform.OpLE, Value: "17:00:00"},
})
engine.AddRuleElement(rule)// Admin can do anything
adminRule := sexp.NewList("permission",
sexp.NewList("role", sexp.NewAtom("admin")),
sexp.NewList("action", &starform.Wildcard{}),
)
// User can read or write
userRule := sexp.NewList("permission",
sexp.NewList("role", sexp.NewAtom("user")),
sexp.NewList("action", &starform.Set{
Elements: []sexp.Element{
sexp.NewAtom("read"),
sexp.NewAtom("write"),
},
}),
)All parsing operations return errors that should be checked:
parser := sexp.NewParser(input)
elem, err := parser.Parse()
if err != nil {
log.Fatalf("Parse error: %v", err)
}
authorized, err := engine.Query(queryStr)
if err != nil {
log.Fatalf("Query error: %v", err)
}- Rule Order: The engine checks rules in the order they were added
- Early Exit: Query evaluation stops at the first matching rule
- Normalization: Not yet fully implemented for set optimization
- Caching: No built-in caching (consider adding if needed)
The current implementation is not thread-safe. If you need concurrent access, wrap the engine with appropriate synchronization:
type SafeEngine struct {
mu sync.RWMutex
engine *spocp.Engine
}
func (s *SafeEngine) Query(query string) (bool, error) {
s.mu.RLock()
defer s.mu.RUnlock()
return s.engine.Query(query)
}