Help us shape the future of Go! Take the 2026 Go Developer Survey

authz

package
v0.15.0 Latest Latest
Warning

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

Go to latest
Published: Oct 24, 2026 License: AGPL-3.0 Imports: 3 Imported by: 0

Documentation

Overview

Package authz evaluates the two authorization planes.

Control plane is role-scoped and governs managing an app. Data plane is binary and governs using one. They are never conflated: CheckData contains exactly one cross-plane implication, ownership (R-072), and this was reversed once during design.

Evaluation order is fixed and each step can only deny. See design 06 §2 and the package CLAUDE.md before changing anything here.

Index

Constants

View Source
const (
	RoleViewer   = "role_viewer"
	RoleOperator = "role_operator"
	RoleOwner    = "role_owner"

	// RoleAdministrator is install-scoped and holds every install verb and no
	// app verb — but the install.apps.* verbs among them are every app verb
	// on every app (R-081), so an administrator can look after any app
	// without a grant on it. They are still not its owner: R-031's owner of
	// record is unchanged, and using the app still needs a data grant (R-087).
	RoleAdministrator = "role_administrator"

	// RoleCreator is install-scoped and holds one verb, app.create. A creator
	// manages the apps they make because making one writes them its owner
	// (R-073) — not because this role says anything about apps — so they
	// manage those and nothing else in the installation.
	RoleCreator = "role_creator"

	// RoleAppViewer and RoleAppManager are the Viewer and the Owner on every
	// app, install-scoped: the counterparts of each of their app verbs. They
	// replace the install.apps.view and install.apps.manage bundles, so a
	// grant of either reads the same as one of those did (issue #81).
	// App manager does not approve deploys, as Owner does not (R-155).
	RoleAppViewer  = "role_app_viewer"
	RoleAppManager = "role_app_manager"

	// RoleAuditor sees every app, reads its logs and reads the audit log, and
	// changes nothing: the security group's role (issue #81). Not
	// install.view — the accounts, adapters and policy are not what it audits.
	RoleAuditor = "role_auditor"
)

Built-in role IDs, seeded by migration and protected by trigger (R-081).

Variables

Verbs is the catalog (design 06 §5, R-080).

There is no implication graph: holding app.delete does not imply app.view. Implication graphs are where authorization bugs live, because the graph is consulted in one place and forgotten in another. The console suggests sensible combinations instead, which is a UI concern and cannot go wrong silently.

Functions

func InstallScoped

func InstallScoped(v Verb) bool

InstallScoped reports whether a verb is held install-wide rather than on one app.

Checked at the boundary between CheckControl and CheckInstall so that a verb cannot be authorized in the wrong scope. Passing an install verb to CheckControl, or an app verb to CheckInstall, is a programming error and is refused rather than evaluated — an install verb evaluated against an app would look for a grant that cannot exist and deny, which is safe; an app verb evaluated install-wide would look for a grant that *can* exist and allow, which is not.

func IsVerb

func IsVerb(v Verb) bool

IsVerb reports whether v is in the catalog. Custom roles are arbitrary subsets of it (R-082), so this is what validates one.

Types

type Auditor

type Auditor interface {
	Denied(ctx context.Context, p Principal, appID string, verb Verb, reason errs.Code)

	// ThroughInstall records an app verb allowed by an install-wide grant
	// rather than one on the app (issue #81), so the audit log shows that
	// the access came from everywhere and not from here: which install verb,
	// through which grant. Not called for app.view and app.logs.read, which
	// the console asks on every screen and every few seconds (see
	// auditsReach).
	ThroughInstall(ctx context.Context, p Principal, appID string, verb, through Verb, grant Grant)
}

Auditor records authorization outcomes.

type Authorizer

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

Authorizer evaluates both planes.

func New

func New(store Store, policy Policy, auditor Auditor) *Authorizer

func (*Authorizer) Allows

func (a *Authorizer) Allows(ctx context.Context, p Principal, appID string, verb Verb) (bool, error)

Allows reports whether CheckControl would allow the verb, without auditing a denial — for deciding what to show, as AppVerbs does, one verb at a time.

func (*Authorizer) AllowsEach

func (a *Authorizer) AllowsEach(ctx context.Context, p Principal, appIDs []string, verbs ...Verb) (map[string]map[Verb]bool, error)

AllowsEach is Allows for several apps and verbs at once: for each app, the verbs CheckControl would allow on it, without auditing a denial. For a list that shows what the caller may do on each of its rows (issue #72).

Each answer is the one Allows gives, by the same rules in the same order: the principal, the policy document, the install grants and their roles are read once for the call, as AppVerbs reads them, and the control grants for every app in one query when the store is a BatchStore. None of it outlives the call (R-274).

func (*Authorizer) AllowsInstall

func (a *Authorizer) AllowsInstall(ctx context.Context, p Principal, verb Verb) (bool, error)

AllowsInstall reports whether CheckInstall would allow the verb, without auditing a denial. For a decision with two ways to be allowed — deploy approval is install.deploys.approve or app.deploy.approve (R-155) — where failing the first is not a denial at all if the second succeeds, and an audit log full of denials nobody suffered would bury the ones that matter.

func (*Authorizer) AppVerbs

func (a *Authorizer) AppVerbs(ctx context.Context, p Principal, appID string) ([]Verb, error)

AppVerbs returns the app verbs the principal may use on an app, by asking the same question CheckControl asks for each — so what the console shows as editable is what the API will allow, and cannot drift from it. Denials are not audited: this is somebody looking at a screen, not trying anything.

The question is asked once per app verb, with the same rules as control, but the principal's status, the policy document, the grants and their roles are read once for the whole call rather than once per verb (lookups). None of it outlives the call (R-274).

func (*Authorizer) AppVerbsHeld added in v0.14.0

func (a *Authorizer) AppVerbsHeld(ctx context.Context, p Principal, appID string) (map[Verb]bool, error)

AppVerbsHeld returns the app verbs p's grants carry on one app: its grants on the app, directly and through groups, and the install verbs that stand for an app verb on every app (R-080, R-081). Like InstallVerbsHeld it reads grants only; host policy and account status decide use, not holding.

func (*Authorizer) CheckActive added in v0.14.0

func (a *Authorizer) CheckActive(ctx context.Context, p Principal) error

CheckActive runs steps 1–4 alone, for an act that needs no verb because it is on the caller's own account: reading it, or changing its profile. A suspended account, a deleted one, a delegated token whose owner is either, or a session still setting up two-factor is refused here exactly as it would be by any verb (R-048, R-059). The denial is audited.

func (*Authorizer) CheckConfers added in v0.8.0

func (a *Authorizer) CheckConfers(ctx context.Context, p Principal, verbs []Verb, act string) error

CheckConfers refuses p handing on, or taking away, install verbs p does not hold itself (R-080a). act completes "Pando refused to …", for example "give the group Ops the role Administrator".

func (*Authorizer) CheckConfersOnApp added in v0.14.0

func (a *Authorizer) CheckConfersOnApp(ctx context.Context, p Principal, appID, roleID, act string) error

CheckConfersOnApp refuses p granting a role on an app, or giving a grant on it a new role, when the role carries an app verb p does not hold on that app itself (R-080a, R-155). Without it app.grants.manage would be every app verb: an Owner could give themselves app.deploy.approve through a custom role and approve their own deploys, which is the two-person rule an installation keeps by withholding that verb. act may use %s for the role's name, for example "grant the role %s".

func (*Authorizer) CheckConfersRole added in v0.8.0

func (a *Authorizer) CheckConfersRole(ctx context.Context, p Principal, roleID, act string) error

CheckConfersRole is CheckConfers for the install verbs of one role. An app-scoped role carries none, so there is nothing to check for it here: granting one on an app is app.grants.manage's question. act may use %s for the role's name, for example "grant the role %s".

func (*Authorizer) CheckConfersVerbsOnApp added in v0.15.0

func (a *Authorizer) CheckConfersVerbsOnApp(ctx context.Context, p Principal, appID string, verbs []Verb, act string) error

CheckConfersVerbsOnApp is CheckConfersOnApp for a list of verbs rather than a role: what editing a custom app role adds to everyone holding it on that app (R-080a, R-155). Install verbs in the list are ignored.

func (*Authorizer) CheckControl

func (a *Authorizer) CheckControl(ctx context.Context, p Principal, appID string, verb Verb) error

CheckControl authorizes a control-plane action: managing an app.

The order is fixed and each step can only deny; none can restore access denied by an earlier step (design 06 §2).

func (*Authorizer) CheckData

func (a *Authorizer) CheckData(ctx context.Context, p Principal, appID string) error

CheckData authorizes data-plane access: using an app through the proxy.

This function contains exactly ONE cross-plane implication — ownership (R-072). No other control-plane role appears in it, and being a Pando admin does not appear in it at all (R-087: an admin has root and can reach a container outside Pando, but the supported path requires a grant).

This was reversed once during design. If a change adds a control-plane check here, reject it — that is R-029, and the test asserting an operator on someone else's app is denied *use* exists to catch exactly that change.

func (*Authorizer) CheckHoldsEveryInstallVerb added in v0.8.0

func (a *Authorizer) CheckHoldsEveryInstallVerb(ctx context.Context, p Principal, act, because string) error

CheckHoldsEveryInstallVerb refuses p unless p holds every install verb there is (R-080a), for an act that reaches all of them: configuring an identity provider, which decides who signs in as which account, or restoring the installation, which replaces every grant. act completes "Pando refused to …"; because completes "…, because …".

func (*Authorizer) CheckInstall

func (a *Authorizer) CheckInstall(ctx context.Context, p Principal, verb Verb) error

CheckInstall authorizes an installation-wide action (O-17, R-265).

A separate function from CheckControl, taking different arguments, for the same reason CheckData is separate: the scopes are different questions and a single function with an optional appID is one missed argument away from authorizing an app verb install-wide. That mistake has already been made once in this package's history, in the other direction (design 06 §2).

Host policy still runs first (R-272) and still denies the holder: an install that has disabled a verb has disabled it for administrators too.

func (*Authorizer) CheckMayChangeAccount added in v0.8.0

func (a *Authorizer) CheckMayChangeAccount(ctx context.Context, p Principal, userID, act string) error

CheckMayChangeAccount refuses p changing an account that holds install verbs p does not (R-080a): a password reset, a linked identity or a changed email is a way into that account, and a suspension or deletion is a way to take its permissions from it. act completes "Pando refused to …". Your own account is always yours to change.

func (*Authorizer) InstallVerbsHeld added in v0.8.0

func (a *Authorizer) InstallVerbsHeld(ctx context.Context, p Principal) (map[Verb]bool, error)

InstallVerbsHeld returns the install-scoped verbs p's grants carry, directly and through groups. It reads grants only: host policy and account status decide whether a verb may be used now, not whether it is held, and holding is what passing it on is about.

func (*Authorizer) PreviewControl

func (a *Authorizer) PreviewControl(ctx context.Context, p Principal, appID string, verb Verb) error

PreviewControl is CheckControl's answer without auditing a denial: the same refusal, with the same reason, for a dry run that asks what would happen and changes nothing. A denial nobody suffered is not an event (design 06 §6).

type BatchStore

type BatchStore interface {
	// ControlGrantsForApps returns ControlGrantsFor's answer for each app,
	// with an entry, possibly empty, for every app asked about.
	ControlGrantsForApps(ctx context.Context, appIDs []string, p Principal) (map[string][]Grant, error)
}

BatchStore is a Store that can read the principal's control grants on several apps in one call. AllowsEach uses it when the store offers it; a store without it is asked app by app, with the same answers.

type DataFacts

type DataFacts struct {
	Owner     bool // the caller is the app's owner of record (R-072)
	Grant     bool // a data-plane grant reaches the caller: direct, group or account token
	Anonymous bool // the app is shared with everyone (R-075)
	Passcode  bool // and that sharing asks for a passcode (R-075a)
	Unlocked  bool // the unlock this request brought is live
}

DataFacts is everything CheckData decides on, about one app and one caller (issue #93). Facts only: the decision, and its order, stay in CheckData.

type DataFactsReader

type DataFactsReader interface {
	DataFacts(ctx context.Context, appID string, p Principal, passcodeToken string) (DataFacts, error)
}

DataFactsReader is a Store that reads DataFacts in one go — the state store's one query in place of four. A Store without it is asked fact by fact, stopping as soon as the answer is known.

type DenialAuditPolicy

type DenialAuditPolicy interface {
	AuditsAnonymousDenials(ctx context.Context) bool
}

DenialAuditPolicy is a Policy that says whether anonymous denials on the data plane are audited. A policy that does not implement it audits them.

type Grant

type Grant struct {
	ID            string
	AppID         string
	Plane         string
	PrincipalKind string
	PrincipalID   string
	RoleID        string

	// Role is the grant's role, when the store read it with the grant. Nil
	// means it did not, and the authorizer asks Store.Role for it.
	Role *Role
}

Grant binds a principal to an app on one plane.

type Policy

type Policy interface {
	// Allows reports whether host policy permits the verb at all. Policy is a
	// floor, not an override (R-272): a policy disabling exec install-wide
	// denies the owner too.
	//
	// The principal is passed because O-12's resolution needs it. "Agents may
	// not exec here" is expressed as a policy scoped to token principals rather
	// than as an MCP-layer exclusion list, because an agent holding a token can
	// call the REST API directly — an MCP-specific block is a speed bump, not a
	// boundary. Enforcement has to live where every surface passes through it,
	// and this is that place.
	Allows(ctx context.Context, p Principal, verb Verb, appID string) error
}

Policy is host policy, evaluated before grants.

type Principal

type Principal struct {
	Kind PrincipalKind
	ID   string

	// UserID is the user authorization runs against. For a user it equals ID.
	// For a delegated token it is the owner (R-058, R-059) — authorization then
	// proceeds exactly as if the owner made the request, and no grant is ever
	// written for the token itself.
	//
	// For an account token it is empty: an account token is its own principal
	// and is looked up in grants under its own ID (R-060).
	UserID string

	TokenID string

	// Groups are resolved live per request (R-079), never denormalized.
	Groups []string

	// Email and DisplayName travel into the assertion's claims. They are
	// display detail, never used for authorization — an email is not an
	// identity here, users.id is (R-054).
	Email       string
	DisplayName string

	AdapterID string

	// Status of the underlying user, checked at step 2 of the evaluation order.
	Status string

	// TwoFactorSetupPending marks a session of a local account that signed
	// in with its password and must set up two-factor authentication, which
	// host policy requires, before it is anything more (R-405). Authentication
	// is not finished, so the API answers it only on the routes that set the
	// second factor up and the proxy takes it for anonymous; step 2 refuses it
	// as well, so a route that forgot to is refused anyway.
	TwoFactorSetupPending bool

	// PasswordChangePending marks a session of an account whose password
	// was set by somebody else and must be changed at its first sign-in
	// (R-046). The API answers it only on the routes that change the
	// password, so a temporary password cannot be turned into a token or
	// used for anything else first.
	PasswordChangePending bool

	// Passcodes are the unlock tokens the request carried, by app ID (R-075a):
	// proof that whoever is visiting entered a passcode-protected app's
	// passcode. Unverified as carried — CheckData asks the store whether each
	// is live, on every request, as it asks about everything else.
	Passcodes map[string]string
}

Principal is the resolved identity of a request (design 06 §1).

func Anonymous

func Anonymous() Principal

Anonymous is the principal for an unauthenticated request. It is a real principal with a real check path, not an absence — R-075's anonymous grant is a grant like any other, and R-056's assertion carries sub: "anonymous".

func System

func System() Principal

System is the principal for the reconciler and background jobs. It bypasses grant checks but still writes audit events attributed to "system": anything a background job does must be as visible as anything a person does.

type PrincipalKind

type PrincipalKind string

PrincipalKind identifies what is making a request.

const (
	KindUser      PrincipalKind = "user"
	KindToken     PrincipalKind = "token"
	KindAnonymous PrincipalKind = "anonymous"
	KindSystem    PrincipalKind = "system"
)

type Role

type Role struct {
	ID      string `json:"id"`
	Name    string `json:"name"`
	Builtin bool   `json:"builtin"`
	Verbs   []Verb `json:"verbs"`
}

Role is a named set of verbs.

Tagged because this type is serialized directly by the API, and every other type on the wire is lower_snake_case. An untagged struct would put Go field names into the API surface — where they would then be a compatibility promise, and renaming a field would be a breaking change to a client.

func (Role) Has

func (r Role) Has(v Verb) bool

Has reports whether the role grants v.

type Snapshotter

type Snapshotter interface {
	Snapshot(ctx context.Context) (Policy, error)
}

Snapshotter is a Policy that can answer several questions from one read of its document. AppVerbs asks policy about every app verb for one screen, and reading the document once per verb was most of what that cost.

The snapshot lives for one call and is never kept: policy applies to a running install the moment it changes (R-274), so a snapshot held across requests would be the cache R-274 rules out.

type Store

type Store interface {
	// UserStatus returns the status of a user: active, suspended, or deleted.
	UserStatus(ctx context.Context, userID string) (string, error)

	// ControlGrantsFor returns control-plane grants matching the principal —
	// direct, by group, or by account token.
	ControlGrantsFor(ctx context.Context, appID string, p Principal) ([]Grant, error)

	// InstallGrantsFor returns the principal's installation-wide grants: the
	// ones with no app (O-17). A separate query from ControlGrantsFor rather
	// than a nullable argument, so an app-scoped lookup can never return an
	// install grant by passing the wrong value.
	InstallGrantsFor(ctx context.Context, p Principal) ([]Grant, error)

	// IsOwner reports whether userID owns the app (R-031).
	IsOwner(ctx context.Context, appID, userID string) (bool, error)

	// HasDataGrant reports whether the principal holds a data-plane grant,
	// directly or through a group.
	HasDataGrant(ctx context.Context, appID string, p Principal) (bool, error)

	// AnonymousAccess reports whether the app is shared with everyone (R-075),
	// and whether that sharing asks for a passcode (R-075a).
	AnonymousAccess(ctx context.Context, appID string) (granted, passcode bool, err error)

	// PasscodeUnlocked reports whether token is a live unlock of this app's
	// passcode: entered, not expired, and made under the grant that stands now.
	PasscodeUnlocked(ctx context.Context, appID, token string) (bool, error)

	// Role returns a role by ID.
	Role(ctx context.Context, roleID string) (Role, error)
}

Store is the authorizer's view of persisted state.

Narrow on purpose: the authorizer must not be able to reach anything else, and a small interface makes the evaluation order testable without a database.

type Verb

type Verb string

Verb is a control-plane permission.

Data-plane use is not a verb. Reaching an app through the proxy is binary (R-070) and is checked by CheckData, which is a different function taking different arguments for exactly that reason.

const (
	// Install-scoped verbs. Held through a grant with no app (O-17), which is
	// what makes "an administrator" a principal with a grant rather than a
	// separate mechanism beside grants and roles.
	//
	// Namespaced `install.*` so the scope is visible at every call site: a verb
	// that reads `install.` cannot be mistaken for one that needs an app, which
	// is the mistake that would reintroduce the hole these close.
	InstallView           Verb = "install.view"
	InstallUsersManage    Verb = "install.users.manage"
	InstallPolicyManage   Verb = "install.policy.manage"
	InstallAdaptersManage Verb = "install.adapters.manage"
	InstallAuditRead      Verb = "install.audit.read"

	// InstallBackupManage covers taking, verifying and restoring backups.
	// Its own verb rather than part of install.policy.manage: a restore
	// replaces the entire install, and folding that into the verb that edits a
	// source allowlist would hand it to everyone who could edit one.
	InstallBackupManage Verb = "install.backup.manage"

	// The install.apps.* verbs are each app verb's install-wide counterpart:
	// install.apps.deploy is app.deploy on every app, including apps made
	// later (issue #81). They are how an install grant bears on an app — the
	// one sanctioned way, read in exactly one place, CheckControl, through the
	// table in everyApp. One verb per app verb rather than bundles, so a
	// custom role can carry "read every app's logs" without "deploy every
	// app", and so a group gets a permission once instead of a grant on each
	// app. The old bundles, install.apps.view as Viewer and
	// install.apps.manage as Owner on every app, are built-in roles now:
	// App viewer and App manager (R-081).
	//
	// Control plane only. None appears in CheckData: using an app still needs
	// a data grant or ownership (R-072, R-087).
	InstallAppsView              Verb = "install.apps.view"
	InstallAppsLogsRead          Verb = "install.apps.logs.read"
	InstallAppsDeploy            Verb = "install.apps.deploy"
	InstallAppsRestart           Verb = "install.apps.restart"
	InstallAppsSpecEdit          Verb = "install.apps.spec.edit"
	InstallAppsSecretsWrite      Verb = "install.apps.secrets.write"
	InstallAppsSecretsRead       Verb = "install.apps.secrets.read"
	InstallAppsExec              Verb = "install.apps.exec"
	InstallAppsGrantsManage      Verb = "install.apps.grants.manage"
	InstallAppsRoutingOverride   Verb = "install.apps.routing.override"
	InstallAppsResourcesOverride Verb = "install.apps.resources.override"
	InstallAppsEgressTighten     Verb = "install.apps.egress.tighten"
	InstallAppsEgressLoosen      Verb = "install.apps.egress.loosen"
	InstallAppsDelete            Verb = "install.apps.delete"

	// InstallTokensManage covers service tokens (R-060): listing, creating and
	// revoking them. Its own verb rather than install.users.manage's, because a
	// service token is a principal an automation acts as — issuing one is a
	// different trust from managing people, and an installation may want
	// somebody who runs its CI to hold one without the other.
	InstallTokensManage Verb = "install.tokens.manage"

	// InstallDeploysApprove approves or rejects any deploy that needs
	// approval, on any app, including the holder's own (R-155). It is
	// app.deploy.approve's install-wide counterpart, older than the
	// install.apps.* verbs and named for what it does rather than renamed to
	// match them. Not in App manager: approval is a trust an installation
	// hands out on purpose, and managing apps does not imply it.
	InstallDeploysApprove Verb = "install.deploys.approve"

	// InstallUpgrade upgrades Pando itself in place (R-356): it replaces the
	// server everything else runs behind, so it is its own trust rather than
	// part of managing policy or adapters.
	InstallUpgrade Verb = "install.upgrade"

	// InstallEventsManage subscribes to events install-wide and manages
	// anybody's subscriptions (R-368). An install-wide subscription hears about
	// every app and about sign-ins, which is why seeing one app is not enough.
	InstallEventsManage Verb = "install.events.manage"

	// InstallAuditExport sends the audit log off the installation: creating,
	// changing or removing an audit sink (R-385). Not part of
	// install.audit.read — an auditor reads the log where it is, and pointing
	// it somewhere new is a different thing to trust someone with.
	InstallAuditExport Verb = "install.audit.export"

	// AppCreate is install-scoped despite its name: there is no app yet when it
	// is checked. Sequence A step 1 has always called it install-level.
	AppCreate Verb = "app.create"

	AppView             Verb = "app.view"
	AppLogsRead         Verb = "app.logs.read"
	AppDeploy           Verb = "app.deploy"
	AppRestart          Verb = "app.restart"
	AppSpecEdit         Verb = "app.spec.edit"
	AppSecretsWrite     Verb = "app.secrets.write"
	AppSecretsRead      Verb = "app.secrets.read"
	AppExec             Verb = "app.exec"
	AppGrantsManage     Verb = "app.grants.manage"
	AppRoutingOverride  Verb = "app.routing.override"
	AppResourceOverride Verb = "app.resources.override"

	// AppEgressTighten changes an app's egress within the installation's
	// rules (R-182, R-184): its own list, additions to a denylist, removals
	// from an allowlist, private addresses blocked. Never beyond policy
	// (R-272), so Operator holds it as well as Owner.
	AppEgressTighten Verb = "app.egress.tighten"

	// AppEgressLoosen loosens the installation's egress rules for an app,
	// where host policy permits loosening by verb (R-183, R-184). It was
	// app.egress.override until issue #79; the migration renamed it in every
	// role that held it. Holding it covers every egress change, tightening
	// included.
	AppEgressLoosen Verb = "app.egress.loosen"

	// AppDeployApprove approves or rejects this app's deploys that need
	// approval (R-155). In no built-in role: an installation grants it
	// deliberately, through a custom role.
	AppDeployApprove Verb = "app.deploy.approve"

	AppDelete Verb = "app.delete"
)

func AppVerbs

func AppVerbs() []Verb

AppVerbs is every app-scoped verb, in catalog order.

func EveryApp

func EveryApp(install Verb) (Verb, bool)

EveryApp returns the app verb an install verb stands for on every app, and whether it stands for one. For listing and explaining access; deciding it is CheckControl's.

func InstallCounterpart

func InstallCounterpart(app Verb) (Verb, bool)

InstallCounterpart returns the install verb that stands for an app verb on every app, and whether there is one.

Jump to

Keyboard shortcuts

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