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
- Variables
- func DefaultCachePathForDB(dbPath string, config Config) string
- func IntervalFromEnv(config Config) time.Duration
- type Cache
- type Config
- type EndpointDirectory
- type Evaluation
- type MatchedRule
- type Request
- type Rule
- type SchemaSupport
- type Snapshot
- type SnapshotProvider
- type Status
Constants ¶
const ( ReasonCodeAllow = "ALLOWED_POLICY_CHECK" ReasonCodeDeny = "DENY_POLICY_CHECK" )
Reason codes shared with the cloud evaluator.
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.
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.
const ( EffectAllow = "allow" EffectDeny = "deny" )
Rule effects.
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.
const MaxSnapshotBodyBytes = 4 << 20
MaxSnapshotBodyBytes caps how large a snapshot response the client accepts.
Variables ¶
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 ¶
DefaultCachePathForDB stores the snapshot next to the guard database, the same convention managedstream uses for its cursor state.
func IntervalFromEnv ¶
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 (*Cache) Apply ¶
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) LoadPersisted ¶
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 ¶
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:
- A rule matches when its layer subject matches and each non-null dimension equals the request value exactly (null = wildcard, no globs).
- 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.
- 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.
type SnapshotProvider ¶
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.