Documentation
¶
Overview ¶
Package catalogderive builds a service's in-process authz.RPCMap from the per-RPC authorization annotations carried by the proto descriptors already linked into its binary.
Why the map is derived and not written ¶
Two authorization decisions are taken on every public RPC: the api-gateway checks the generated permission catalog, and the owning service re-checks its own map. Until now the second was written by hand, so the two could name different relations on different objects — and then the effective requirement was their intersection, which is recorded in no document anyone grants against. A whole service could also carry no map at all, in which case the catalog's promise about that service was unbacked and no comparison could see it.
Deriving removes the second declaration rather than adding a third checker: both artefacts now come from ONE source, the `corelib.authz.v1` method options, so "the service asks something else" stops being expressible.
Why the annotations and not the generated JSON ¶
The catalog JSON is generated FROM these annotations. Reading the JSON at service start would need a third embedded copy of it (the gateway and iam each carry one), and copies drift. The descriptors are already in the binary: a service links the generated stubs for the RPCs it serves, and those stubs carry the options verbatim. Deriving from them costs no new asset, and it makes the tree-wide comparison "catalog == annotations" a real cross-check of the generator rather than a tautology.
What fails at start rather than at request time ¶
The Go compiler cannot check an annotation: `from_request_field` is a string, and a typo in it names a field that does not exist. Derive resolves every field descriptor once, when the map is built, so such a row refuses to start the process instead of failing the Check of whoever calls that RPC first. The same posture covers a proto package that is not linked, a duplicate method key, and a method carrying no authorization annotation at all.
Index ¶
- Constants
- func Derive(protoPackages ...string) (authz.RPCMap, error)
- func MethodCount(protoPackage string) int
- func MustDerive(protoPackages ...string) authz.RPCMap
- func ObjectScopedTypes(m authz.RPCMap) map[string]struct{}
- func RangeAnnotated(protoPackages []string, ...)
- func ScopeObjectType(fullMethod string, entry authz.RPCEntry) (objectType string, ok bool)
- type Annotations
- type Entry
Constants ¶
const ( // ExemptPermission — the literal a method carries instead of a permission // string when it takes no per-RPC Check at all. ExemptPermission = "<exempt>" // ClusterObjectType / ClusterSingletonID — the deployment-wide singleton the // cluster-anchored rows resolve to. Exactly one cluster row exists // (iam domain, Cluster.id is pinned to this literal), so an annotation that // anchors on `cluster` names that object and nothing else. ClusterObjectType = "cluster" ClusterSingletonID = "cluster_root" // WildcardField — `from_request_field: "*"` says the scope is not carried by // the request at all: either the deployment-wide cluster singleton, or an // object the caller does not name (which the relation store then refuses as // unscoped — deliberately, see wildcardExtractor). WildcardField = "*" )
Variables ¶
This section is empty.
Functions ¶
func Derive ¶
Derive builds the RPCMap for the named proto packages — every method of every service declared in them, in the exact key form grpc-go passes to an interceptor ("/kacho.cloud.storage.v1.VolumeService/Get").
A service names the packages whose services it registers: its own domain plus `kacho.cloud.operation` for the LRO envelope. That list is the service's identity, not a second statement of its permissions — the permissions come from the annotations, one per method, with no place left for the two to disagree.
Every failure mode is fail-closed and named at the call site, because all of them produce the same runtime symptom otherwise: a method missing from the map is refused by the interceptor as unmapped, which reads to the caller as a permission problem and to the operator as nothing at all.
func MethodCount ¶
MethodCount reports how many RPCs the named proto package declares in this binary. It exists so a caller can tell "no findings" from "nothing was read" without reaching into the registry itself.
func MustDerive ¶
MustDerive is Derive for a composition root, which has no second course of action: every failure it can report is a property of the BINARY (a stub that is not linked, an annotation naming a field that does not exist, an RPC carrying no annotation at all), identical on every start and unaffected by any request. A service that continued past it would serve with a map missing the very methods the operator thinks are gated, and each of them would then be refused as unmapped — a refusal that names neither the method nor the omission.
So it refuses to start, loudly, naming the offending method. The message is the operator-facing refusal text and is meant to be read from a crash-looping pod.
func ObjectScopedTypes ¶
ObjectScopedTypes returns the per-resource FGA object types a map anchors on, excluding the hierarchy anchors (`project`, `account`, `cluster`).
The distinction is the one existence-hiding turns on: a deny on a per-resource object must be answered with the owner's NotFound, because the caller named that object and a PermissionDenied would confirm it exists. A deny on a hierarchy anchor confirms nothing of the sort — the caller named a collection, and its existence is not the secret.
Deriving the set rather than listing it removes the way it used to go stale: a new resource type reaches the map through its annotations, and a hand-written list is the one place nobody remembers to extend — which shows up as a resource whose denial silently stops hiding.
func RangeAnnotated ¶
func RangeAnnotated(protoPackages []string, fn func(fullMethod string, md protoreflect.MethodDescriptor, a Annotations))
RangeAnnotated walks every RPC of the named proto packages and calls fn with the gRPC full method and its annotations. Used by the gates that compare the annotations against the generated catalog.
func ScopeObjectType ¶
ScopeObjectType recovers the FGA object type an RPCEntry's extractor declares, by invoking it against a zero-valued instance of the method's request message resolved from the global proto registry. ok=false when the request type is not registered, when the entry carries no extractor, or when the extractor is request-dependent (it errors or panics on an empty request) — in which case there is no single declared type to compare.
Types ¶
type Annotations ¶
type Annotations struct {
Permission string
RequiredRelation string
ScopeObjectType string
ScopeFromRequestField string
ScopeObjectTypeFromRequest string
HideExistence bool
ScopeFiltered bool
}
Annotations is one method's authorization annotations, read off its descriptor. Exported so a gate can compare the annotations against the generated catalog without re-implementing the extension reads.
func AnnotationsOf ¶
func AnnotationsOf(md protoreflect.MethodDescriptor) Annotations
AnnotationsOf reads the authz options off a method descriptor.
func (Annotations) Exempt ¶
func (a Annotations) Exempt() bool
Exempt reports the lane where the edge runs no per-RPC Check at all.
type Entry ¶
type Entry struct {
FQN string `json:"fqn"`
Permission string `json:"permission"`
RequiredRelation string `json:"required_relation"`
ScopeExtractor struct {
ObjectType string `json:"object_type"`
FromRequestField string `json:"from_request_field"`
ObjectTypeFromRequestField string `json:"object_type_from_request_field"`
} `json:"scope_extractor"`
// ScopeFiltered — the catalog's own declaration that the OWNING SERVICE
// authorizes this call over the data it answers with, so the edge
// authenticates and runs no per-RPC Check. It is the catalog-side counterpart
// of authz.RPCEntry.ScopeFiltered, and Compare requires the two to agree.
ScopeFiltered bool `json:"scope_filtered"`
// HideExistence — the catalog's explicit mark that a deny on this method is
// answered with the owning service's NotFound. Mirrors
// authz.RPCEntry.HideExistence; see catalogHidesExistence for the derived
// (unmarked) majority.
HideExistence bool `json:"hide_existence"`
}
Entry is the subset of a catalog row this comparison needs.