auditlistfilter

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Sep 10, 2026 License: AGPL-3.0 Imports: 1 Imported by: 0

Documentation

Overview

Package auditlistfilter states how kaname is laid out for the public-List gate. The analysis itself — and why it parses instead of grepping — lives in pkg/listfiltergate.

Why this exists

It did not, until 2026-08. Every other service with a public listing surface had an analyser of this class; iam, which has the widest listing surface in the repository — 30 methods across 21 packages, more than compute, nlb, registry and storage combined (3+6+5+7=21) — had none. Not a weak one, not a stale one: none. The counts are the gate's own census re-measured on the tree this profile landed on; the branch this came from said 31 across 22, true until the tenant conditional-access surface was retired out from under it. And nothing was red, because the set of services to analyse was written by hand, in a CI loop and in whoever remembered to create a directory, and iam was in neither list.

That is the shape of the defect the gates themselves are written against: a check is silent about what it never looked at. The remedy is not only this profile but pkg/listfiltergate/coverage_test.go, which derives the service list from the committed tree so an unanalysed service is a FINDING rather than a quiet gap.

This service's shape

iam colocates transport and use-cases per resource, like vpc and nlb: internal/apps/kaname/api/<res> holds a `Handler` whose listing methods delegate to per-RPC use-cases in the same package. So the PACKAGE tells one resource from another, and the analyser's walk reaches the use-case.

Two things differ from its neighbours and both shaped the declarations below.

First, the vocabulary. iam does NOT have FilterVisibleIDs or FilterVisiblePage — those names exist only in compute and storage. Its per-object question to the model is internal/authzfilter.VisibleSet (batched) or .Visible (single), reached through package-local helpers with names like visibleAccountIDs and visibleBindingIDsOnPage. A profile copied from a neighbour would have named calls that do not exist here and failed every resource, which is the kind of red that gets a gate disabled rather than read.

Second, the surface is genuinely heterogeneous — far more so than vpc's. iam lists its own authorization graph, its own operation histories, the members of a group, the keys of a service account, and the objects the authorization store itself can see. There is no single shape that fits, which is precisely why every method is declared and why all six shapes of the vocabulary appear below.

What is NOT proven here, stated so nobody reads more into a pass

access_binding.ListByRole narrows per row, but not by a batched VisibleSet: it asks grant-authority about each row's SCOPE. Since #2054 it asks through a per-request memo (pageAuthority.grantAuthorityVerdict) — the super-gate once per request, the scope once per DISTINCT scope — because page_size reaches 1000 and the un-memoised form spent two store questions per row. The memo changes no verdict; it removes the repeat. So grantAuthorityVerdict is accepted as a filter call alongside requireGrantAuthority, which the single-object verbs still use.

This caveat said "by evaluating requireGrantAuthority inside the loop" until 2026-09-06, and #2054 had made that false three weeks after it was written: the loop evaluates the memo, and the analyser walks calls on the RECEIVER and its fields but not on a local variable, so nothing along ListByRole reached any declared filter. The gate went red — correctly, on its own terms — while the narrowing was fully present and locked by four tests. A profile is a claim about the tree and expires with it.

What the pass still does NOT prove, said so nobody reads more into it: the analyser cannot tell a call inside a per-row loop from a single call before it. For this one method the pass means "the per-object question is asked", not "it is asked for every row". The stronger statement is a test's job — TestListByRole_StrangerSeesNothing and TestListByRole_FilteringKeepsExactlyTheAuthorisedRows in services/iam/internal/apps/kaname/api/access_binding — not this gate's, and pretending otherwise would be exactly the form-without-substance this class is about.

Likewise, the three EdgeGate methods delegate their check to the per-RPC authorization at the edge. The gate verifies that the delegation is real — the RPC carries a required_relation and a scope_extractor on the declared field, and where the scope is the cluster singleton, that the relation is not one a wildcard tuple satisfies. It does not verify that the edge is deployed, which is the boot guard's subject.

Index

Constants

This section is empty.

Variables

View Source
var Profile = listfiltergate.Profile{
	Service:    "iam",
	AnchorRoot: "internal/apps/kaname/api",

	PerPackage:     true,
	ReceiverSuffix: "Handler",

	ExtraReceivers: []string{"PublicHandler"},

	Filters: []string{"VisibleSet", "Visible", "requireGrantAuthority", "grantAuthorityVerdict"},

	Banned: []string{"ListAllowedIDs", "ListObjects"},

	EnumerationSources: []listfiltergate.EnumerationSource{
		{Dir: "internal/clients", Type: "RelationQueries"},
		{Dir: "internal/repo/kaname/pg/relverdict", Type: "Asker"},
	},

	SubjectScopers: []string{"ListForCaller", "listOp.Execute", "identityOfAuthenticatedCaller"},

	ProtoFiles: []string{
		"kaname/cloud/iam/v1/internal_cluster_service.proto",
		"kaname/cloud/iam/v1/membership_service.proto",
		"kaname/cloud/iam/v1/sa_key_service.proto",
		"kaname/cloud/iam/v1/user_token_service.proto",
	},
	FGAModel: "kaname/cloud/iam/v1/fga_model.fga",

	Listings: map[string]listfiltergate.Listing{

		"access_binding.List":        rowFilter,
		"access_binding.ListByScope": rowFilter,

		"access_binding.ListByAccount": rowFilter,
		"access_binding.ListByRole":    rowFilter,
		"account.List":                 rowFilter,
		"group.List":                   rowFilter,
		"project.List":                 rowFilter,
		"role.List":                    rowFilter,
		"service_account.List":         rowFilter,
		"user.List":                    rowFilter,

		"access_binding.ListOperations":  subjectScoped,
		"account.ListOperations":         subjectScoped,
		"group.ListOperations":           subjectScoped,
		"project.ListOperations":         subjectScoped,
		"role.ListOperations":            subjectScoped,
		"service_account.ListOperations": subjectScoped,
		"user.ListOperations":            subjectScoped,

		"identityquota.List": subjectScoped,

		"access_binding.ListSubjectPrivileges": rowFilter,

		"access_binding.ListBySubject": rowFilter,

		"access_binding.ListAssignableRoles": subjectGate("requireGrantAuthority"),

		"account.ListAllOperations":             subjectGate("requireAccountViewAuthority"),
		"internal_operations.ListIamOperations": subjectGate("requireClusterSystemAdmin"),
		"group.ListMembers":                     subjectGate("AllowsVerb"),
		"session_revocations.ListByUser":        subjectGate("authorizeListByUser"),

		"authorize.ListSubjects": {Shape: listfiltergate.StoreQuery, Gate: "authorizeCaller"},

		"cluster.ListAdmins": edgeGate("internal_cluster_service.proto", "*"),

		"sa_keys.List":     edgeGate("sa_key_service.proto", "service_account_id"),
		"user_tokens.List": edgeGate("user_token_service.proto", "user_id"),

		"membership.List": edgeGate("membership_service.proto", "account_id"),

		"permission_catalog.ListPermissionCatalog": {
			Shape: listfiltergate.ClusterScoped,
			Reason: "the permission catalog is the platform's own static list of RPCs and the " +
				"relations they require — global reference data with no per-object grants to " +
				"narrow to, and the caller must be authenticated to reach it. The exclusion " +
				"expires with its method: retire the RPC and this entry becomes a finding.",
		},

		"limit.List": {
			Shape: listfiltergate.ClusterScoped,
			Reason: "a resource-count ceiling is a CLUSTER-level administrative record: the row " +
				"carries a scope (DEFAULT/ACCOUNT/PROJECT) but no owner to grant against, so " +
				"there is no per-object grant to narrow the page to — RowFilter here would state " +
				"a check whose subject does not exist. What bounds the caller instead is the " +
				"surface: the RPC lives ONLY on InternalLimitService, is registered ONLY on the " +
				"cluster-internal listener (ban #6), and its catalog entry demands `system_admin` " +
				"on `cluster` — a relation defined `[user, service_account]` with NO `user:*` " +
				"member, so unlike `viewer` it is not satisfiable by a wildcard tuple and does " +
				"narrow. The exclusion expires with its subject twice over: retire the RPC and " +
				"this entry becomes a finding, and give the ceiling a per-object owner and the " +
				"reason above stops being true — at which point this must become RowFilter.",
		},
		"limit.ListChangedSince": {
			Shape: listfiltergate.ClusterScoped,
			Reason: "the incremental read owner services poll to refresh their ceiling cache. Its " +
				"caller is a MACHINE, not a tenant: the catalog entry demands `quota_reader` on " +
				"`cluster`, defined `[service_account, group#member] or system_admin` — no " +
				"`user:*` member, so it is not satisfiable by a wildcard tuple. The grant is held " +
				"by a GROUP rather than by enumerated subjects, so revoking one owner service is " +
				"one membership row (data-integrity.md B18). Narrowing this page per object would " +
				"be wrong, not merely absent: an owner service polls ceilings for every scope it " +
				"enforces, and a page filtered to what the MACHINE can see would silently drop " +
				"tenants whose limits it must apply. The exclusion expires with the RPC.",
		},
		"module.List": {
			Shape: listfiltergate.ClusterScoped,
			Reason: "the module catalog is the platform's own registry of modules, their " +
				"resources and verbs — the rows the rights model itself is keyed on. It carries " +
				"no project_id, account_id or owner column, so there is no per-object grant to " +
				"narrow the page to: RowFilter here would state a check whose subject does not " +
				"exist. What bounds the caller is the surface instead: the RPC lives ONLY on " +
				"InternalModuleService, is registered ONLY on the cluster-internal listener " +
				"(ban #6, pinned by register_module_internal_only_test.go), and its catalog entry " +
				"demands `system_admin` on `cluster` — a relation defined `[user, service_account]` " +
				"with NO `user:*` member, so unlike `viewer` it is not satisfiable by a wildcard " +
				"tuple. The same relation is re-checked in the handler as its first statement " +
				"(authz_order_test.go pins that order), because the internal listener carries no " +
				"authorization interceptor of its own. Add an owner column to the catalog rows and " +
				"the reason above stops being true — at which point this must become RowFilter. " +
				"The exclusion expires with its method: retire the RPC and this entry becomes a finding.",
		},
		"interactive_client.List": {
			Shape: listfiltergate.ClusterScoped,
			Reason: "an interactive-login client is a CLUSTER-level OAuth2 client registration: " +
				"the table carries no project_id, account_id or owner column, so there is no " +
				"per-object grant to narrow the page to — RowFilter here would state a check " +
				"whose subject does not exist. What bounds the caller instead is the surface " +
				"itself: the RPC lives ONLY on InternalInteractiveClientService, is registered " +
				"ONLY on the cluster-internal listener, and its catalog entry demands " +
				"`system_admin` on `cluster` — a relation defined `[user, service_account]` with " +
				"NO `user:*` member, so unlike `viewer` it is not satisfiable by a wildcard tuple " +
				"and does narrow. The exclusion expires with its subject twice over: retire the " +
				"RPC and this entry becomes a finding, and give the resource an owner column and " +
				"the reason above stops being true — at which point this must become RowFilter.",
		},
	},
}

Profile describes kaname to the analyser.

Functions

This section is empty.

Types

This section is empty.

Directories

Path Synopsis
cmd
audit-list-filter command
Command audit-list-filter is the CI entry point of kaname's public-List gate.
Command audit-list-filter is the CI entry point of kaname's public-List gate.

Jump to

Keyboard shortcuts

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