providerpolicy

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Jul 9, 2026 License: MIT Imports: 12 Imported by: 0

Documentation

Overview

Package providerpolicy syncs a cloud-hosted provider access-control policy down to the managed endpoint and evaluates classified provider activity against it locally. It is the provider-neutral core shared by the per-provider packages (githubpolicy, hubspotpolicy): those own their classifier and a Config; everything else — rule matching, precedence, caching, fetching, fail-closed schema validation — lives here and MUST stay identical across providers.

Precedence in the local engine is: general deterministic guardrails > this synced policy > probabilistic signals.

Index

Constants

View Source
const (
	ReasonCodeAllow = "ALLOWED_POLICY_CHECK"
	ReasonCodeDeny  = "DENY_POLICY_CHECK"
)

Reason codes shared with the cloud evaluator.

View Source
const (
	ModeObserve = "observe"
	ModeEnforce = "enforce"
)

Enforcement directive for the local engine.

  • ModeObserve — evaluate every action and record a dry-run decision, but never block. Anything other than an explicit "enforce" is treated as observe.
  • ModeEnforce — block denied actions. Reserved for a later, careful step; not emitted during the observer-mode pilot.
View Source
const (
	LayerOrg   = "org"
	LayerUser  = "user"
	LayerAgent = "agent"
	// LayerEndpoint binds a rule to a managed endpoint's installation
	// ("ins_…") id. Unlike user/agent, this subject is always resolvable on
	// the managed endpoint, so it is how device-scoped policy is enforced
	// locally.
	LayerEndpoint = "endpoint"
	// LayerGroup binds a rule to a SCIM directory group's uuid. It matches
	// when the id is in the snapshot's EndpointDirectory.GroupIDs — the cloud
	// resolves this endpoint's user email against the org's SCIM directory at
	// snapshot-generation time. Only schema versions with GroupLayer support
	// carry group-layer rules.
	LayerGroup = "group"
)

Policy layers. Each rule binds a subject in exactly one layer.

View Source
const (
	EffectAllow = "allow"
	EffectDeny  = "deny"
)

Rule effects.

View Source
const DefaultRefreshInterval = 60 * time.Second

DefaultRefreshInterval is how often the managed daemon re-fetches the snapshot. Hash-unchanged responses are cheap: they only bump freshness.

View Source
const MaxSnapshotBodyBytes = 4 << 20

MaxSnapshotBodyBytes caps how large a snapshot response the client accepts.

Variables

View Source
var ErrNotConfigured = errors.New("policy snapshot not configured")

ErrNotConfigured reports that the cloud has no snapshot surface for this provider/org (HTTP 404) — expected when the org has not activated the provider. Callers should treat it as "no policy" rather than a fetch failure worth alarming on.

Functions

func DefaultCachePathForDB

func DefaultCachePathForDB(dbPath string, config Config) string

DefaultCachePathForDB stores the snapshot next to the guard database, the same convention managedstream uses for its cursor state.

func IntervalFromEnv

func IntervalFromEnv(config Config) time.Duration

IntervalFromEnv returns the refresh interval, honoring the provider's env override when set to a positive duration.

Types

type Cache

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

Cache holds the per-installation snapshot in memory and mirrors it to disk so the policy survives daemon restarts and cloud outages.

func NewCache

func NewCache(path string, config Config) *Cache

func (*Cache) Apply

func (c *Cache) Apply(snapshot Snapshot, fetchedAt time.Time) error

Apply installs a freshly fetched snapshot. When the identity is unchanged the rules are not re-applied or re-persisted; only freshness is updated.

func (*Cache) CurrentSnapshot

func (c *Cache) CurrentSnapshot() (Snapshot, Status, bool)

func (*Cache) LoadPersisted

func (c *Cache) LoadPersisted() error

LoadPersisted primes the cache from disk so evaluation works before the first fetch completes. The persisted snapshot starts out stale until the cloud confirms it.

func (*Cache) MarkFetchFailed

func (c *Cache) MarkFetchFailed(err error)

MarkFetchFailed records that the latest refresh failed; the cached snapshot keeps serving evaluations.

type Config

type Config struct {
	// ProviderKey names the provider in error messages ("github", "hubspot").
	ProviderKey string
	// SnapshotEndpoint is the cloud path serving the policy snapshot. Tenancy
	// is resolved from the install token; missing/unknown/revoked tokens get
	// 401.
	SnapshotEndpoint string
	// RequestSchema is the schema version to request via `?schema=`; empty
	// omits the parameter.
	RequestSchema string
	// Schemas lists the accepted wire-format versions, most preferred first.
	Schemas []SchemaSupport
	// CacheFileName is the snapshot's on-disk name next to the guard DB.
	CacheFileName string
	// CacheTempPattern is the os.CreateTemp pattern for atomic persistence.
	CacheTempPattern string
	// RefreshEnvVar optionally overrides the refresh interval.
	RefreshEnvVar string
}

Config is the per-provider parameterization of the sync machinery.

type EndpointDirectory

type EndpointDirectory struct {
	InstallationID  string   `json:"installationId"`
	DirectoryUserID *string  `json:"directoryUserId"`
	GroupIDs        []string `json:"groupIds"`
}

EndpointDirectory is the endpoint's directory identity, resolved by the cloud at snapshot-generation time from the endpoint-reported user email and the org's SCIM directory. Membership is deliberately not baked into policy epochs: a SCIM change shows up here on the next snapshot refresh.

DirectoryUserID is nil when the email was missing, unmatched, or ambiguous (ambiguity fails closed — never a union of candidate groups); GroupIDs is then empty and group-layer rules can never match.

type Evaluation

type Evaluation struct {
	Request      Request
	Result       string // "allow" | "deny"
	ReasonCode   string
	Reason       string
	MatchedRules []MatchedRule
	// DecidingRuleID is the rule that determined the outcome: the first
	// matching deny, or the org-layer allow that anchored an allow. Empty
	// when a layer vetoed by silence.
	DecidingRuleID string
	Mode           string
	Epoch          int
	Hash           string
	// Stale is true when the snapshot used was not confirmed by the cloud on
	// the most recent refresh attempt.
	Stale bool
	// SubjectsResolved is false when no Kontext user/application identity was
	// available, so user/agent-layer rules could not match their subject.
	SubjectsResolved bool
	// GroupsResolved is true when this endpoint's directory identity was
	// resolved to a user (even one with zero group memberships). False means
	// group-layer rules could not have matched for identity reasons: v2
	// snapshot, no installation id sent, directory for another endpoint, or
	// a missing/unmatched/ambiguous email.
	GroupsResolved bool
	// SchemaVersion is the wire format of the snapshot that produced this
	// evaluation (v2 or v3 during the negotiation window).
	SchemaVersion string
}

Evaluation is the would-be decision for one request under one snapshot.

func Evaluate

func Evaluate(snapshot Snapshot, status Status, request Request) (Evaluation, bool)

Evaluate mirrors the cloud evaluator exactly. It is most-specific-wins:

  1. A rule matches when its layer subject matches and each non-null dimension equals the request value exactly (null = wildcard, no globs).
  2. Among the matching rules the most specific one decides, by this order: (a) more pinned dimensions (action/resource/branch) beats fewer; then (b) layer rank: org < group < user/agent/endpoint — a group rule beats an org rule and is beaten by a user/agent/endpoint rule; then (c) on an exact tie of (a) and (b) — including two groups this endpoint belongs to — deny beats allow. A broad org deny is therefore overridden by a more specific user/agent allow, which is in turn overridden by an even more specific deny.
  3. If no rule matches the request, it is denied (default deny).

The boolean result is false when the snapshot carries no rules at all (no active policy authored yet) — there is nothing to dry-run.

type MatchedRule

type MatchedRule struct {
	ID      string `json:"id"`
	Layer   string `json:"layer"`
	Effect  string `json:"effect"`
	Decided bool   `json:"decided,omitempty"`
}

MatchedRule records one rule that matched the request, and whether it was the rule that decided the outcome.

type Request

type Request struct {
	Action        string
	Resource      string
	BranchOrRef   string
	UserID        string
	ApplicationID string
	EndpointID    string
}

Request is one classified GitHub action to evaluate. UserID and ApplicationID are the Kontext user and application subjects; the managed endpoint's trusted identity is the service account + installation, so both are typically empty (unresolved) and user/agent-layer rules then never match their subject. EndpointID is the installation ("ins_…") identity of this managed endpoint — it is always known locally, so endpoint-layer rules are the way to scope policy to a specific device on the managed path.

type Rule

type Rule struct {
	ID    string `json:"id"`
	Layer string `json:"layer"`
	// SubjectID is the org id, kontext user id, application id, endpoint
	// installation id, or directory group uuid depending on Layer.
	SubjectID string `json:"subjectId"`
	// ResourceID is the provider resource anchor, or nil for any resource.
	ResourceID *string `json:"resourceId"`
	// ActionName is a canonical action name (e.g. "github.pr.write",
	// "hubspot.object.write"), or nil for any action.
	ActionName *string `json:"actionName"`
	// BranchOrRef is a branch or ref constraint, or nil for any branch.
	BranchOrRef *string `json:"branchOrRef"`
	Effect      string  `json:"effect"`
	// Specificity is a diagnostic hint the cloud computes for display/sorting.
	// The evaluator does not read it: it derives precedence from the rule's
	// own dimensions and layer (see Evaluate — most-specific-wins).
	Specificity int `json:"specificity"`
}

Rule is a single matchable rule. nil on ResourceID / ActionName / BranchOrRef means "matches any" for that dimension; non-nil dimensions are exact string matches. What ResourceID and BranchOrRef mean is up to the provider (GitHub: repository slug + branch; HubSpot: CRM object type, with BranchOrRef unused and always nil).

type SchemaSupport

type SchemaSupport struct {
	Version string
	// GroupLayer is true when rules in this version may carry layer=group.
	// A group rule in a version without it is server misbehavior.
	GroupLayer bool
	// Directory is true when a server answering this version must echo the
	// endpointDirectory block whenever the request identified the endpoint.
	// Its absence would silently strip group-layer carve-out denies while
	// keeping broader allows.
	Directory bool
}

SchemaSupport declares one accepted snapshot wire-format version and which contract features it carries. Any version outside the config's list is rejected (fail closed) rather than misread under the wrong semantics.

type Snapshot

type Snapshot struct {
	SchemaVersion  string `json:"schemaVersion"`
	OrganizationID string `json:"organizationId"`
	// ProviderID is the provider row id, or nil when the org's policy is
	// key-only.
	ProviderID  *string `json:"providerId"`
	ProviderKey string  `json:"providerKey"`
	Mode        string  `json:"mode"`
	// Epoch is the active policy epoch; 0 when the org has no active policy.
	Epoch int `json:"epoch"`
	// Hash is a stable content hash over the epoch, rules, and (when the
	// contract carries one) the endpoint directory — a directory membership
	// change changes the hash. Independent of GeneratedAt, so an unchanged
	// policy keeps an unchanged hash.
	Hash  string `json:"hash"`
	Rules []Rule `json:"rules"`
	// PayloadCaptureMode is the org's tool-payload capture directive
	// ("omitted" | "summary" | "full"). Absent on pre-capture servers and on
	// providers that do not carry it (the github snapshot is authoritative).
	// Deliberately EXCLUDED from Hash (server-side decision), so a mode
	// flip arrives on a snapshot whose hash is unchanged — consumers must
	// not gate mode application on hash inequality (see Cache.Apply).
	// Normalize via payloadcapture.NormalizeMode; unknown values fall back
	// to "summary" (never capture on an unrecognized directive).
	PayloadCaptureMode string `json:"payloadCaptureMode,omitempty"`
	// EndpointDirectory is this endpoint's resolved directory identity (nil
	// on versions without directory support or when the request carried no
	// installation id).
	EndpointDirectory *EndpointDirectory `json:"endpointDirectory,omitempty"`
	GeneratedAt       string             `json:"generatedAt"`
}

Snapshot is the response body of a provider's snapshot endpoint. The server resolves the organization from the install token; there is no organization parameter.

func FetchSnapshot

func FetchSnapshot(ctx context.Context, client *http.Client, cloudURL, installToken, installationID string, config Config) (Snapshot, error)

FetchSnapshot retrieves the provider's policy snapshot from the cloud using the same per-customer install token as the authorization-ledger ingest.

installationID identifies this endpoint ("ins_…") so the cloud can resolve its directory identity for group-layer rules; empty is allowed and simply yields no group matches. The request asks for config.RequestSchema and accepts any version in config.Schemas; anything else is rejected (fail closed) rather than misread under the wrong semantics.

func (Snapshot) Enforce

func (s Snapshot) Enforce() bool

Enforce reports whether the snapshot explicitly directs enforcement. Anything other than the literal "enforce" is observe.

type SnapshotProvider

type SnapshotProvider interface {
	CurrentSnapshot() (Snapshot, Status, bool)
}

SnapshotProvider is what the guard runtime consumes: the current snapshot, its freshness, and whether a snapshot exists at all.

type Status

type Status struct {
	// FetchedAt is when the cached snapshot was last confirmed by the cloud
	// (fetch succeeded, whether or not the hash changed).
	FetchedAt time.Time `json:"fetched_at"`
	// Stale is true when the most recent fetch attempt failed and the cache
	// is serving the last known snapshot.
	Stale bool `json:"stale"`
	// LastError holds the most recent fetch failure, if any.
	LastError string `json:"last_error,omitempty"`
}

Status describes how trustworthy the cached snapshot currently is. A stale snapshot keeps being evaluated — stale-but-deterministic beats unavailable — and the staleness is recorded on every decision.

Jump to

Keyboard shortcuts

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