authz

package
v0.41.0 Latest Latest
Warning

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

Go to latest
Published: Jul 30, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package authz holds the ClusterProvider namespace-admission decision.

A ClusterProvider is cluster-scoped and holds a credential that can read a lot of a source cluster; its spec.allowedNamespaces is that provider's explicit, deny-by-default admission of the GitTarget NAMESPACES permitted to mirror through it.

The decision lives in its own package — outside both internal/controller and internal/watch — because more than one call site compiles rules or starts a data plane for a GitTarget: the GitTarget reconciler, the ClusterWatchRule reconciler, and the watch manager's startup bootstrap. A gate that only one of them runs is not a gate, so every such call site routes through GitTargetAdmitted and one policy read answers all of them identically.

Index

Constants

View Source
const (
	// ReasonClusterProviderNotFound is the denial reason when a GitTarget's referenced
	// ClusterProvider does not exist. This is a HARD GATE: a GitTarget may mirror a source cluster
	// ONLY through an existing ClusterProvider, "default" included. The operator never creates one,
	// so a target whose provider was never declared is denied rather than mirroring on an implicit
	// local identity.
	ReasonClusterProviderNotFound = "ClusterProviderNotFound"

	// ReasonNamespaceNotAuthorized is the denial reason when the GitTarget's namespace is not
	// admitted by its ClusterProvider's spec.allowedNamespaces — including the case where that
	// policy carries a selector the apiserver accepted but that does not convert to a selector.
	ReasonNamespaceNotAuthorized = "NamespaceNotAuthorized"
)
View Source
const (
	// ReasonLegacySourceNamespace is the True reason when EVERY item watches the rule's OWN
	// namespace and the GitTarget declares no allowedSourceNamespaces policy. No authorization was
	// needed.
	ReasonLegacySourceNamespace = "LegacySourceNamespace"

	// ReasonSourceNamespaceAllowed is the True reason when every item passed the policy and at
	// least one names a namespace other than the rule's own — including an own-namespace item that
	// a DECLARED policy explicitly admits.
	ReasonSourceNamespaceAllowed = "SourceNamespaceAllowed"

	// ReasonNoAdmittedSourceNamespaces is the True reason when every item was admitted but the
	// resolved scope is EMPTY — a "*" item against a policy that currently admits nothing. The rule
	// is not stalled (nothing is wrong with it) but it mirrors nothing, and a rule that mirrors
	// nothing while reporting Ready=True with no explanation is a silent no-op.
	ReasonNoAdmittedSourceNamespaces = "NoAdmittedSourceNamespaces"

	// ReasonSourceNamespaceNotAllowed is the TERMINAL False reason for a refusal: the delegation
	// flag is off, the GitTarget declares no policy for an override, or a declared policy
	// evaluated and does not admit the namespace. The policy was READ; this is a decision, not an
	// inability to decide, and it must never share a code path with the unevaluatable case.
	ReasonSourceNamespaceNotAllowed = "SourceNamespaceNotAllowed"

	// ReasonSourceNamespacePolicyUnavailable is the reason when a SELECTOR policy cannot be
	// evaluated at all — an invalid selector, or Namespace reads permanently Forbidden on the
	// source cluster. Its STATUS depends on whether a scope was ever established for the rule:
	// False/Stalled=True while establishing (nothing runs, and only an operator change will fix
	// it), Unknown/Stalled=False while maintaining an already-resolved scope (which is retained).
	ReasonSourceNamespacePolicyUnavailable = "SourceNamespacePolicyUnavailable"

	// ReasonCheckingSourceNamespacePolicy is the Unknown reason while the answer is still being
	// established: the source-cluster Namespace cache has not synced, or a retryable read error is
	// being retried. It is NOT a denial — encoding "cannot say yet" as "denied" is exactly how a
	// transient outage becomes a terminal Stalled=True and a stopped stream.
	ReasonCheckingSourceNamespacePolicy = "CheckingSourceNamespacePolicy"
)

Reasons for the WatchRule SourceNamespaceAuthorized condition. They are the rule-side names, so a reader never has to know which of the three gate inputs produced the verdict — the Message carries that.

Variables

This section is empty.

Functions

This section is empty.

Types

type Decision

type Decision struct {
	// Allowed reports whether the GitTarget's namespace may mirror through its ClusterProvider.
	Allowed bool
	// Reason is a CamelCase condition reason, set only when Allowed is false.
	Reason string
	// Message explains the denial to an operator, set only when Allowed is false.
	Message string
}

Decision is the outcome of a ClusterProvider namespace-admission check. A denial always carries a Reason and an operator-legible Message; an admission carries neither.

func GitTargetAdmitted

func GitTargetAdmitted(
	ctx context.Context,
	reader client.Reader,
	target *configv1alpha3.GitTarget,
) (Decision, error)

GitTargetAdmitted reports whether target's namespace is admitted by the ClusterProvider that target references, reading both the provider and the target's Namespace through reader.

It is evaluated on every reconcile rather than only at admission, so a policy TIGHTENED after a GitTarget was created revokes it too. The two denial paths are deliberate and ordered: a missing provider is denied before any namespace policy is consulted, because an absent provider has no policy to consult and defaulting to "allow" would make an undeclared provider a bypass.

A non-NotFound read error is returned as err so the caller requeues instead of tearing down a running data plane on a transient apiserver failure. A NotFound Namespace is NOT an error: the policy is then evaluated against empty labels, so a `names` entry still admits a namespace that has not been created yet while a selector correctly does not match it.

type ResolvedSourceScope

type ResolvedSourceScope struct {
	// Items is index-aligned with spec.rules.
	Items []SourceNamespaceDecision
	// Verdict is the aggregate over Items, per the status contract's reason precedence.
	Verdict SourceScopeVerdict
	// Reason is the aggregate SourceNamespaceAuthorized reason.
	Reason string
	// Message explains the aggregate, naming the deciding item when one item decided it.
	Message string
}

ResolvedSourceScope is a WHOLE WatchRule's source-namespace verdict: one decision per spec.rules item, index-aligned, plus the aggregate the SourceNamespaceAuthorized condition publishes.

It is a pure function of (rule spec, target policy, source Namespace snapshot), recomputed on every compile and replaced atomically. Nothing per-item is persisted across a spec change, which is what lets rule items have no stable API identity: no state outlives the spec that produced it.

func ResolveWatchRuleSourceScope

func ResolveWatchRuleSourceScope(
	ctx context.Context,
	reader client.Reader,
	rule *configv1alpha3.WatchRule,
	target *configv1alpha3.GitTarget,
	resolver SourceNamespaceResolver,
) (ResolvedSourceScope, error)

ResolveWatchRuleSourceScope is the WatchRule source-namespace gate: which source-cluster namespaces may each of this rule's items watch, in its GitTarget's source cluster?

It is CROSS-OBJECT authorization — WatchRule → GitTarget → ClusterProvider — and the selector half needs remote state, so it is not expressible in CEL and is deliberately a reconciler check rather than a webhook (docs/spec/where-validation-lives.md). Like GitTargetAdmitted it runs on every reconcile, so a policy TIGHTENED after a rule was accepted revokes it.

The per-candidate ordering is the contract, unchanged from the single-namespace gate it generalizes:

  1. Own namespace + NO declared GitTarget policy → allowed, with no delegation flag and no policy. This is the legacy case and it must stay free: gating it would break every existing WatchRule on upgrade.
  2. A DIFFERENT namespace — including "*" — additionally requires the GitTarget's namespace to be admitted by its ClusterProvider, and that provider to set allowSourceNamespaceOverride.
  3. Whenever a policy is declared it is EXHAUSTIVE — evaluated even for an own-namespace item, with no self-namespace carve-out — and an override against a target with NO policy is denied by default.

A non-NotFound ClusterProvider read error is returned as err so the caller requeues instead of tearing down a running stream on a transient apiserver failure.

func (ResolvedSourceScope) Admitted

func (s ResolvedSourceScope) Admitted() bool

Admitted reports whether the whole rule may compile.

func (ResolvedSourceScope) Fingerprint

func (s ResolvedSourceScope) Fingerprint() string

Fingerprint renders the resolved scope as a stable string, per item, for the watched-type re-projection gate.

This is the SILENT hazard the design calls out: a wildcard's inputs — the GitTarget policy and the source cluster's Namespace labels — are not rule state, so a mapper that merely requeues the WatchRule is not enough. If the fingerprint hashed the rule spec instead of the RESOLVED set, reconciliation would run, the fingerprint would be unchanged, the table rebuild would be skipped, and every stream would carry on at its old width with no visible failure anywhere.

func (ResolvedSourceScope) NamespacesFor

func (s ResolvedSourceScope) NamespacesFor(index int) []string

NamespacesFor returns the resolved namespace set for one item index.

func (ResolvedSourceScope) Terminal

func (s ResolvedSourceScope) Terminal() bool

Terminal reports whether the aggregate is a refusal rather than a retryable "cannot say yet".

type SourceNamespaceDecision

type SourceNamespaceDecision struct {
	// Index is the item's position in spec.rules.
	Index int
	// Requested is what the item asked for, verbatim: "" (omitted), a name, or "*".
	Requested string
	// Namespaces is the RESOLVED, concrete namespace set for this item. It is meaningful only when
	// the verdict is admitted, and it is deliberately allowed to be empty for a wildcard whose
	// policy currently admits nothing.
	Namespaces []string
	// Verdict is admitted / denied / cannot-say-yet / permanently-unevaluatable.
	Verdict SourceScopeVerdict
	// Reason is the SourceNamespaceAuthorized condition reason this item would produce.
	Reason string
	// Message explains the verdict to an operator.
	Message string
}

SourceNamespaceDecision is one rule ITEM's source-namespace verdict, plus the concrete namespace set it resolved to.

func (SourceNamespaceDecision) Admitted

func (d SourceNamespaceDecision) Admitted() bool

Admitted reports whether this item may contribute selections.

func (SourceNamespaceDecision) Terminal

func (d SourceNamespaceDecision) Terminal() bool

Terminal reports whether the verdict is a REFUSAL the controller should publish as Stalled=True while establishing a grant — as opposed to a retryable "cannot say yet". A permanently unevaluatable policy is terminal here only because this gate ESTABLISHES grants; a caller maintaining an already-resolved scope must retain it instead (see the establishing/maintaining contract in the PR 4 design), which is why that decision is the caller's and not encoded here.

type SourceNamespaceResolver

type SourceNamespaceResolver interface {
	// ResolveSourceNamespace answers whether ONE candidate namespace is admitted.
	ResolveSourceNamespace(
		ctx context.Context,
		target *configv1alpha3.GitTarget,
		namespace string,
	) SourceScopeResult

	// EnumerateSourceNamespaces expands a target's SELECTOR half into the concrete set of source
	// namespaces it currently admits. It answers the "*" case, which has no single candidate to
	// test.
	//
	// The returned slice is meaningful only when the result is SourceScopeAdmitted; an empty slice
	// with that verdict is a real answer ("the selector currently admits nothing"), which is
	// exactly why the verdict must not be inferred from the length. Unknown and Unavailable mean
	// the set could not be computed and MUST NOT be read as the empty set — an empty resolved scope
	// is the input to a resync sweep.
	EnumerateSourceNamespaces(
		ctx context.Context,
		target *configv1alpha3.GitTarget,
	) ([]string, SourceScopeResult)
}

SourceNamespaceResolver evaluates a GitTarget's allowedSourceNamespaces against namespaces in that target's SOURCE cluster. It is an interface here — and implemented by the watch manager — because the labels a selector needs live in the source cluster, whose connection and cache the watch manager already owns. A reconciler that dialled the source cluster itself on every pass would duplicate both.

Implementations MUST answer an exact-NAME policy without consulting the label cache, so a source cluster whose Namespace access is denied still supports name-based policies. That degradation path is deliberate, and it is the half most likely to regress unnoticed.

type SourceScopeResult

type SourceScopeResult struct {
	Verdict SourceScopeVerdict
	Message string
}

SourceScopeResult is a policy evaluation's outcome plus an operator-legible explanation.

type SourceScopeVerdict

type SourceScopeVerdict int

SourceScopeVerdict is the THREE-valued answer a source-namespace policy evaluation produces. Three-valued is the whole point: a two-valued interface forces "cannot say" to be encoded as "denied", which turns a transient source-cluster outage into a terminal failure and a stopped stream — and makes the Unknown row of the status contract unimplementable.

const (
	// SourceScopeUnknown means the policy could not be evaluated YET and the cause is retryable
	// (cache still syncing, source cluster momentarily unreachable). Retry; do not deny.
	SourceScopeUnknown SourceScopeVerdict = iota
	// SourceScopeAdmitted means the policy was evaluated and admits the namespace.
	SourceScopeAdmitted
	// SourceScopeDenied means the policy was evaluated and does NOT admit the namespace.
	SourceScopeDenied
	// SourceScopeUnavailable means the policy can never be evaluated as written without an
	// operator change — an invalid selector, or Namespace reads Forbidden for a selector policy.
	SourceScopeUnavailable
)

Jump to

Keyboard shortcuts

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