Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
142 lines
4.3 KiB
Go
142 lines
4.3 KiB
Go
// Copyright © 2026 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package check
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/ory/keto/internal/relationtuple"
|
|
)
|
|
|
|
type (
|
|
// Result holds the outcome of a single permission check.
|
|
// Limitation is non-empty only when Membership is MembershipUnknown.
|
|
Result struct {
|
|
Membership Membership
|
|
Err error
|
|
Limitation LimitationKind
|
|
}
|
|
|
|
Membership int
|
|
|
|
// LimitationKind identifies which traversal constraint was hit. A non-empty
|
|
// value on a Result means the membership answer is indeterminate.
|
|
LimitationKind string
|
|
)
|
|
|
|
//go:generate stringer -type Membership
|
|
const (
|
|
MembershipUnknown Membership = iota
|
|
IsMember
|
|
NotMember
|
|
)
|
|
|
|
const (
|
|
LimitationMaxDepthExceeded LimitationKind = "max_depth_exceeded"
|
|
LimitationMaxWidthExceeded LimitationKind = "max_width_exceeded"
|
|
)
|
|
|
|
var (
|
|
ResultUnknown = Result{Membership: MembershipUnknown}
|
|
ResultIsMember = Result{Membership: IsMember}
|
|
ResultNotMember = Result{Membership: NotMember}
|
|
)
|
|
|
|
// CheckRequest carries all inputs for a single check evaluation step.
|
|
type CheckRequest struct {
|
|
Tuple *relationtuple.RelationTuple
|
|
RestDepth int
|
|
}
|
|
|
|
func (r CheckRequest) WithDepth(d int) CheckRequest {
|
|
r.RestDepth = d
|
|
return r
|
|
}
|
|
|
|
// PlannedStep pairs a Step with the CheckRequest it should run against.
|
|
type PlannedStep struct {
|
|
Step Step
|
|
Req CheckRequest
|
|
}
|
|
|
|
// StepRunner orchestrates execution of multiple steps with short-circuit semantics.
|
|
// Typical usage:
|
|
//
|
|
// runner := ex.NewUnionRunner(ctx)
|
|
// for _, ps := range steps {
|
|
// if runner.Done() { break }
|
|
// runner.Add(ps)
|
|
// }
|
|
// return runner.Result()
|
|
type StepRunner interface {
|
|
// Add enqueues a step for execution. Returns early if Done() is true.
|
|
// No-op if the runner has already reached a decisive result or been cancelled.
|
|
Add(PlannedStep)
|
|
|
|
// Done reports whether a decisive result was reached or context cancelled.
|
|
// Use to short-circuit the enqueue loop.
|
|
Done() bool
|
|
|
|
// Ctx returns the runner's context, usable for shared state like visited tracking.
|
|
// The context is cancelled when a decisive result is reached (to signal siblings).
|
|
Ctx() context.Context
|
|
|
|
// AddLimitation records that some branches were skipped due to given [LimitationKind] constraint.
|
|
AddLimitation(LimitationKind)
|
|
|
|
// Result waits for all in-flight steps to complete and returns the aggregated result.
|
|
// Call it after the Add loop finishes (or breaks early on Done()).
|
|
Result() Result
|
|
}
|
|
|
|
// Executor coordinates permission check execution with middleware support.
|
|
// It runs individual steps (Run), sequences steps with union/intersection logic
|
|
// (RunUnion, RunIntersection), and supports custom execution strategies via
|
|
// middleware. All Executor implementations are stateless; dependencies come
|
|
// from EngineDependencies.
|
|
//
|
|
// Typical usage:
|
|
//
|
|
// result := executor.Run(ctx, DirectStep{}, req)
|
|
// unionResult := executor.RunUnion(ctx, step1, step2)
|
|
// intersectionResult := executor.RunIntersection(ctx, step1, step2)
|
|
type Executor interface {
|
|
// Run executes a single step through the middleware chain.
|
|
Run(ctx context.Context, step Step, req CheckRequest) Result
|
|
|
|
// RunUnion runs steps with OR semantics: returns IsMember on first match,
|
|
// short-circuits siblings, falls through to NotMember if none match.
|
|
RunUnion(ctx context.Context, steps ...PlannedStep) Result
|
|
|
|
// RunIntersection runs steps with AND semantics: returns NotMember or Error
|
|
// on first failure, short-circuits siblings.
|
|
RunIntersection(ctx context.Context, steps ...PlannedStep) Result
|
|
|
|
// NewUnionRunner creates a StepRunner for streaming union execution.
|
|
// It is useful when Steps are created dynamically and the full list is not known upfront.
|
|
NewUnionRunner(ctx context.Context) StepRunner
|
|
|
|
Deps() EngineDependencies
|
|
}
|
|
|
|
// Middleware intercepts a step execution within the pipeline. next continues
|
|
// down the remaining middlewares and ultimately calls step.Execute.
|
|
type Middleware func(
|
|
ctx context.Context,
|
|
step Step,
|
|
req CheckRequest,
|
|
next func(context.Context, Step, CheckRequest) Result,
|
|
) Result
|
|
|
|
// Checker performs a full permission check for a single relation tuple.
|
|
// Implemented by Executor in the step package.
|
|
type (
|
|
CheckerProvider interface {
|
|
Checker() Checker
|
|
}
|
|
Checker interface {
|
|
CheckRelationTuple(ctx context.Context, r *relationtuple.RelationTuple, restDepth int) Result
|
|
}
|
|
)
|