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

finegrained

package
v3.3.0 Latest Latest
Warning

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

Go to latest
Published: Oct 20, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package finegrained holds what a fine-grained personal access token needs for each catalog action, and the decision this server takes on it.

Three facts with three owners meet here (issue 952). Which requests an action makes is this repository's, derived from the handlers by cmd/gen_action_grants; what each REST route and GraphQL element demands of a fine-grained token is GitLab's, recorded from a booted instance in docs/development/gitlab-api-live.json; and how an action's requests combine is the handler author's, written beside the request as a //gitlab:request directive. The generator joins the three into one Table per recorded GitLab version, emitted as Go in internal/tools/actiongrants and compiled into the binary, and the types of that table are the ones declared here.

Two levels, because a wire request and a GraphQL position differ

An Operation is one wire request: a REST route or one GraphQL operation. GitLab judges every request on its own and carries no authority from one to the next, so one execution of an action needs every operation it makes, and a Requirement is reachable when every operation of some path passes. An Element is one object position a GraphQL document selects: GitLab checks a fine-grained token per object type it resolves, so whether a GraphQL read works is decided position by position inside one operation, and a position the grant fails either takes the answer with it (it lies on the answer spine, or a non-null chain carries its null there) or leaves one field empty (it is degraded).

The decision

An Authority is what one credential is worth, in one of two phases. Without a grant, phase A (Unevaluated), it withholds exactly the actions no fine-grained token can reach at the recorded GitLab version (a Requirement with a Denial) and allows the rest; an action the table has no row for is unknown authority and allowed, never locked out, since a stale table or a newer action is not evidence against the caller. With the grant read, phase B (Evaluate), it lists an action when the grant alone reaches it and lets it be called when the grant or GitLab's anonymous access to a public object can, so a call is refused here only where GitLab would refuse it whatever the object. Judge decides which of the two a Reading is worth, falling back to phase A with its reason when the instance is not a recorded release, the grant could not be read or names a permission the table does not know; Rejudge is the same decision for a re-read, which replaces the authority only on reads that answered. Authority.Decide and Authority.Lists are what the listing and the call read; Authority.WithheldText writes the one sentence every surface answers a withheld action with, naming the permission a phase B refusal needs, and Authority.DegradedNote, Authority.NullNote and Authority.EmptyNote the next steps a served answer is given where GitLab leaves part of it empty, answers null or removes items over GraphQL without saying so.

The classic scope the same requests decide

The requests that decide what a fine-grained token needs decide what a classic personal access token or an OAuth token needs too, so the table carries that as well: a ClassicScope per Operation, read from the request (a REST GET or HEAD and a GraphQL query need read_api, anything else api, a route GitLab grants otherwise declared where it is derived), and one per Requirement, the way of running the action that needs the least. The server narrows a token carrying read_api and not api to the actions ClassicScope.ReachableWith says read_api reaches (ADR-0026), and the tool reference prints the value per action. The scopes a catalog group demands besides, admin_mode, stay where they are decided, in internal/tools.MetaToolScopes.

A leaf beside the register

The package imports the standard library and internal/tenancy alone, so internal/gitlab can hold an Authority and the e2e harness can call the same decision the server takes. What one granted scope covers is a policy rule, so it lives in the register as tenancy.CoverableAt (row AUT-008) and the vocabulary it is written in with it; the types here alias them. Every slice of a Table holds constants only, which lets the Go compiler lay a generated table out statically: the binary runs no initialization code for it.

Index

Constants

View Source
const (
	AccessPersonalProjects    = tenancy.AccessPersonalProjects
	AccessSelectedMemberships = tenancy.AccessSelectedMemberships
	AccessAllMemberships      = tenancy.AccessAllMemberships
	AccessUser                = tenancy.AccessUser
	AccessInstance            = tenancy.AccessInstance
)

The access levels GitLab 19.4 defines.

View Source
const (
	// NamespaceNone is a scope with no namespace: all memberships, the user
	// and the instance.
	NamespaceNone = tenancy.NamespaceNone
	// NamespaceUser is the creating user's own namespace, which a personal
	// projects scope carries and the entity exposes as neither id.
	NamespaceUser = tenancy.NamespaceUser
	// NamespaceGroup is a group, which covers the group and every project
	// under it.
	NamespaceGroup = tenancy.NamespaceGroup
	// NamespaceProject is one project's namespace.
	NamespaceProject = tenancy.NamespaceProject
)

The namespace kinds a scope can carry.

View Source
const (
	BoundaryProject  = tenancy.BoundaryProject
	BoundaryGroup    = tenancy.BoundaryGroup
	BoundaryUser     = tenancy.BoundaryUser
	BoundaryInstance = tenancy.BoundaryInstance
)

The four boundary types, one bit each, so a requirement that any of several boundaries may satisfy is one value.

View Source
const (
	PublicProject = 0
	PublicGroup   = 1
)

PublicProject and PublicGroup index Table.PublicAnonymous.

View Source
const AllBoundaries = tenancy.AllBoundaries

AllBoundaries is every boundary type, which is what a requirement whose boundary GitLab computes at run time with no declared type may resolve to.

Variables

View Source
var ErrUnconfirmedWrite = errors.New("GitLab ran the write and answered without the object it returns")

ErrUnconfirmedWrite is what Authority.UnconfirmedWrite's error is, so a caller can tell a write GitLab probably committed from one it refused.

Functions

func Bucket

func Bucket(version string) string

Bucket returns the major.minor of a version an instance reports, which is what a recorded table is chosen by, or "" for anything that does not start with two numbers.

func DecodeGrant

func DecodeGrant(body []byte, maxScopes int) (Grant, FallbackReason)

DecodeGrant reads a token's grant out of the body GitLab answers GET /personal_access_tokens/:id with, holding it to at most maxScopes scopes.

It falls back rather than guesses. A body that does not decode, a token that says it is not fine-grained, an access level GitLab 19.4 does not define, a scope naming both a project and a group, and a selected memberships scope naming neither are the grant's shape being unknown; an absent or null scope list is a grant that cannot be read; more scopes than maxScopes is a grant too large. A present, empty list is a grant of nothing, and is returned as one.

Scopes on the same namespace are unioned: GitLab validates every input of one creation request before it builds any, so one request can carry the same namespace twice, and several scopes with no namespace at once (a user and an instance scope together is what the startup permissions need).

func Moved

func Moved(from, to *Authority) bool

Moved reports whether replacing from with to changes the verdict a log line about the authority states (Authority.LogArgs): its phase, the reason it stayed in phase A, or the major.minor it was judged at. A re-read that answered replaces the authority on every revalidation, so this is what keeps the line a re-read logs to the ones that moved a token: an instance upgraded to a release no table records or back to the recorded one, or GitLab.com moving onto or past the listing-only prerelease. A nil from is an authority nothing had judged yet, which any verdict moves.

func Selection

func Selection(path string) string

Selection writes a position of an answer as the GraphQL selection that reaches it from the field the action asks for, `project { vulnerabilities { nodes } }` for the dotted path a table element holds. It is how a reader meets the position in a query, and it is the one spelling of a position this package hands out: a dotted path reads as a canonical action ID wherever its first field shares a domain's name, as `vulnerability.project` does, to a model reading the detail and to the documentation's name check reading the fine-grained permissions page alike.

Types

type Assignable

type Assignable struct {
	Name string
	// Permissions are indices into [Table.Permissions], the raw permissions
	// it expands to.
	Permissions []uint16
	// Boundaries are the boundary types it can be granted at.
	Boundaries Boundary
	// Deprecated names are kept so a grant written under an older name still
	// reads.
	Deprecated bool
	// Grantable is set when a fine-grained token may hold it, which is every
	// assignable but those GitLab reserves for roles.
	Grantable bool
}

Assignable is one permission a user grants a token, as GitLab names it.

type Authority

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

Authority is what one fine-grained credential may do, for the table of one recorded GitLab version. It is immutable: a re-read builds a new one.

What it holds is sized by the table, never by the grant it was evaluated from, which it drops: four sets over the raw permissions and two over the actions, about a kilobyte, so a pool at its ceiling of entries holds about ten mebibytes of them. The parts of an answer a grant leaves empty are computed for the one action asked about rather than kept per action, which is what keeps it that small.

func Evaluate

func Evaluate(t *Table, g Grant) *Authority

Evaluate returns the phase B authority of a grant against a table: which raw permissions the grant can cover at each boundary type, which actions it is listed and which it may call.

What one scope covers is the register's rule, tenancy.CoverableAt (row AUT-008), applied for each boundary type; a scope's permissions are the raw permissions its assignable names expand to, a name the table does not define expanding to nothing. Then each action is judged: it is reachable when some path passes, a path when every operation in it passes, an operation when it opts out of the check or when every one of its groups and every group of every position on its answer spine passes, and a group when, at some boundary type it may be held at, every one of its permissions is covered. That is GitLab's own any boundary, all permissions, read existentially, since the target of a call is not known before it is made.

An action is listed when the grant alone reaches it. It is callable when it is listed, or when GitLab would serve the call on a public project or group: for some boundary among those two, every permission of the group is covered or public to anonymous callers there, permission by permission, as GitLab's anonymous bypass enables each on its own. While the table carries no evaluated public set, a REST group held at a project or a group passes the call guard whatever the grant, so this server never refuses on the public question with a set it knows nothing about and GitLab judges the call. A denied action is neither listed nor callable.

It is pure, keeps nothing of the grant, and allocates only the sets it returns and the transient index of assignable names.

func Judge

func Judge(t *Table, r Reading) *Authority

Judge returns the authority one fine-grained token's reading is worth against a recorded table: phase B, the grant evaluated, when every guard holds, and phase A with the reason when one does not (issue 952).

The guards, in the order a reason is chosen: the reading carries no reason (the grant was read and decoded within its bounds, and the version read left a version to judge at); the instance reported a version; the version's major.minor is the table's; and every assignable name the grant holds is one the table defines, deprecated ones included, so a rename or a new assignable falls back rather than being read as nothing granted. Any of them failing is phase A with the newest record's verdict, naming the version the verdict comes from, since evaluating a grant against a table recorded for another release reads permission names that may mean something else there.

One release outside the record is let in for the listing only: the prerelease of the release right after the table's ([nextPrerelease]), which is what GitLab.com reports while releases stop at the table's minor (19.5.0-pre against 19.4, 20.0.0-pre against 19.11). Such a session is listed what its grant reaches as the table declares it and may call everything phase A allows, so a requirement that changed in that one milestone is GitLab's own answer to the call rather than a refusal here with a stale name.

It is pure: it reads nothing but its arguments, and returns a new authority.

func Rejudge

func Rejudge(t *Table, current *Authority, r Reading) (*Authority, bool)

Rejudge returns what a fine-grained token's authority becomes after a re-read of its grant and of the instance version, and whether that replaces current.

The authority is replaced only by one computed from reads that answered: a grant decoded within its bounds with a validated version, or a validated version whose major.minor differs from the one current was judged at (the instance was upgraded, which moves the token to that release's verdict, phase A when no table records it). Anything else keeps current: a version that was not answered or not readable, and a grant that was not answered, was too large, could not be decoded or was refused. Falling back to phase A on a transient failure would make what a session is listed change with upstream availability rather than with its authorization (INV-009), and the grant cannot have changed in the meantime, since nothing edits one after the token is created.

func Unevaluated

func Unevaluated(table *Table, reason FallbackReason, reported string) *Authority

Unevaluated returns a phase A authority: one that withholds what no fine-grained token can reach at the table's version and allows the rest, carrying why the grant was not evaluated and the version the instance reported, for the words.

func (*Authority) Decide

func (a *Authority) Decide(id string) Decision

Decide answers for one canonical action ID.

An ID the table has no row for is allowed and reported unknown: a stale table on a deferred layer, or an action added since, is unknown authority, and unknown authority is served (INV-008, ADR-0018), since a wrong "no" hides a capability where a wrong "yes" surfaces as GitLab's own refusal. It is logged once per process at debug. A row with a Denial is withheld from listing and from calls. In phase A every other row is allowed, with the positions every fine-grained token gets empty.

In phase B a row is listed and callable as Evaluate judged it. One the grant does not reach carries CauseNotGranted and the groups it fails on its closest path, for the words; one the call guard passes carries the positions served empty for this grant, its own undeclared ones and every declared one off a spine the grant does not cover, computed here for the one action asked about.

func (*Authority) DegradedNote

func (a *Authority) DegradedNote(decision Decision) string

DegradedNote is the next step a served answer gets when GitLab leaves parts of it empty for this credential, or "" when it leaves none. Each part is written as the GraphQL selection that reaches it, with the type GitLab checks there and why it comes back empty: a type that declares no fine-grained permission is empty for every fine-grained token, and a declared one is empty unless the grant holds what the type needs.

func (*Authority) EmptyNote

func (a *Authority) EmptyNote(id string) string

EmptyNote is the next step an empty list gets when the action's answer is a GraphQL list or connection, or "" when it is not. GitLab leaves out of such a list the items a fine-grained token is not granted, with no error, so an empty answer may be the token rather than an absence.

func (*Authority) Fallback

func (a *Authority) Fallback() FallbackReason

Fallback returns why the authority stayed in phase A, or FallbackNone.

func (*Authority) HeldElsewhereNote added in v3.3.0

func (a *Authority) HeldElsewhereNote(boundary string, words []string) string

HeldElsewhereNote is the sentence added to GitLab's missing-permissions refusal of a call when this session's grant does hold, at the boundary type the refusal names, every permission it quotes, or "" when it does not.

GitLab writes the same sentence whether the grant lacks a permission or holds it for other projects or groups, and whether the token's user is a member of the project or group the call targets, so the description of the refusal names every cause. A session whose grant holds the permission can rule the first one out, and this says so: what the call is missing is the project or group in the grant, or its user's membership there.

boundary is the refusal's boundary word and words its quoted permissions, both GitLab's text. A word is matched against the words the table displays each raw permission by, and the grant is asked whether it covers at that boundary type any raw permission displayed that way, which is the most a grant's covered sets can say since they keep no namespace. The words the sentence repeats are therefore always the table's own. A phase A authority covers nothing, so it never writes the sentence.

func (*Authority) ListingOnly

func (a *Authority) ListingOnly() bool

ListingOnly reports whether the grant decides the listing only, which is the one release past the table's Judge lets in.

func (*Authority) Lists

func (a *Authority) Lists(id string) bool

Lists reports whether a listing shows the action, which is Authority.Decide's Listed without the rest of the decision: the groups a refusal names and the positions a served answer leaves empty are worded for the one action a caller runs, and a listing asks about every action the surface registers, so it asks this instead and computes neither.

func (*Authority) LogArgs

func (a *Authority) LogArgs() []any

LogArgs are the key and value pairs a log line about this authority may carry: its phase, why it stayed in phase A, the major.minor the instance reported and the one the table was recorded at, and for a grant naming permissions the table does not define, how many and the first few, each cut. Nothing of the grant, the token or its id is among them.

func (*Authority) NullNote

func (a *Authority) NullNote(id string) string

NullNote is the next step a not-found answer gets when the action reads GitLab over GraphQL, or "" when it does not. GitLab answers null, with no error, for an object a fine-grained token is not granted or that sits in a project or group outside its grant, so a reader told "not found" has to be told that it may be the token rather than the object.

func (*Authority) Phase

func (a *Authority) Phase() Phase

Phase returns how much the authority knows about the grant.

func (*Authority) Reported

func (a *Authority) Reported() string

Reported returns the validated version the instance reported, or "".

func (*Authority) Table

func (a *Authority) Table() *Table

Table returns the table the authority decides by.

func (*Authority) UnconfirmedWrite added in v3.3.0

func (a *Authority) UnconfirmedWrite(operation, object string, classic error) error

UnconfirmedWrite is the error a GraphQL write returns when GitLab answered without the object the write returns.

A session with no authority, which is every credential that is not a fine-grained token, gets classic, the handler's own error, unchanged. A fine-grained session gets the one sentence that says the write was probably committed and why, naming operation as the handler's errors do and object in a reader's words. It does not wrap classic: for a client-go write that is the not-found sentinel every 404 matches, and the write was no 404.

func (*Authority) UnknownPermissions

func (a *Authority) UnknownPermissions() (count int, sample []string)

UnknownPermissions returns how many assignable names the grant held that the table does not define, and at most three of them, each cut to 64 bytes: what the log line saying why the grant was not evaluated may name. Both are zero for any other authority.

func (*Authority) WithheldText

func (a *Authority) WithheldText(id string, decision Decision) string

WithheldText is the one sentence every surface answers a withheld action with, or "" for a decision that lists the action. It names the action by its canonical ID and never by a tool name, names permissions in the words the token creation page offers, and says what GitLab does to the answer the way the row's effect records it, so a read GitLab answers with null is not described as a refusal.

A call is refused only when the decision is not callable, which is when the call middleware and the dispatchers ask for these words. An action left out of the listing whose call still passes, one GitLab may serve on a public project or group or one listed past the recorded release, is worded too, for the detail of gitlab://tools/{id}, which says so.

type Boundary

type Boundary = tenancy.Boundary

Boundary is a set of GitLab's boundary types (app/models/authz/boundary.rb): the kinds of object a fine-grained permission is granted and checked at. It is the register's type, since the one rule that says what a grant covers lives there (tenancy.CoverableAt, register row AUT-008), and this package writes its tables in the same vocabulary rather than in a copy of it.

func ParseBoundary

func ParseBoundary(name string) (Boundary, bool)

ParseBoundary reads one boundary type as GitLab spells it, and reports false for anything else.

type Cause

type Cause string

Cause is why an action is withheld from a fine-grained token.

const (
	// CauseMutationUndeclared is a GraphQL mutation that declares nothing:
	// refused, nothing runs.
	CauseMutationUndeclared Cause = "graphql-mutation-undeclared"
	// CauseTypeUndeclared is an enforced GraphQL object type with no
	// directive on the answer spine.
	CauseTypeUndeclared Cause = "graphql-type-undeclared"
	// CausePayloadUndeclared is a declared mutation whose payload object is
	// undeclared, or reaches an undeclared non-null field: the write commits
	// and the answer is null.
	CausePayloadUndeclared Cause = "graphql-payload-undeclared"
	// CauseBoundaryUnresolvable is a type declared at a boundary the object
	// the action reaches never resolves to.
	CauseBoundaryUnresolvable Cause = "graphql-boundary-unresolvable"
	// CauseRESTTodo is a REST route whose declaration GitLab deferred.
	CauseRESTTodo Cause = "rest-todo"
	// CauseRESTUndeclared is a REST route that declares nothing.
	CauseRESTUndeclared Cause = "rest-undeclared"
	// CauseNotGranted is an action the token's grant does not reach, the one
	// cause a grant decides.
	CauseNotGranted Cause = "not-granted"
)

The causes, each one a way GitLab refuses a fine-grained token at the recorded version whatever its grant, and the one cause a grant decides.

func (Cause) GraphQL

func (c Cause) GraphQL() bool

GraphQL reports whether the cause is one only GraphQL answers with, which is the set a REST route serving the same object could remove (issue 1054).

type ClassicScope

type ClassicScope uint8

ClassicScope is what a classic personal access token, or an OAuth token, needs for a request or an action: which of GitLab's two API scopes lets it through. The values are ordered by strength, so a token carrying one scope reaches everything at or below it (ClassicScope.ReachableWith).

GitLab accepts api for every route and read_api for a GET or a HEAD only (lib/api/api.rb:60-61 at v19.4.1-ee), except where a route grants read_api for every method; over GraphQL it accepts read_api for a query and refuses it a mutation. Two values are below read_api because no scope of the token decides them: a request GitLab authenticates by another credential the caller passes, and an action that sends GitLab nothing.

const (
	// ClassicUnknown is the zero value: no row says what the action needs.
	ClassicUnknown ClassicScope = iota
	// ClassicNoRequest is an action that sends GitLab nothing.
	ClassicNoRequest
	// ClassicOtherCredential is a request GitLab authenticates by another
	// credential passed as a parameter (a runner's or a trigger's token),
	// never reading the token this server holds.
	ClassicOtherCredential
	// ClassicReadAPI is read_api, which api covers too.
	ClassicReadAPI
	// ClassicAPI is api.
	ClassicAPI
)

The classic scopes an action or a request can need, weakest first.

func ParseClassicScope

func ParseClassicScope(name string) (ClassicScope, bool)

ParseClassicScope reads a scope as ClassicScope.String spells it, and reports false for anything else, the spelling of ClassicUnknown included.

func (ClassicScope) ReachableWith

func (s ClassicScope) ReachableWith(token ClassicScope) bool

ReachableWith reports whether a token whose strongest API scope is token reaches what needs s: s is known and no stronger than the token's.

func (ClassicScope) String

func (s ClassicScope) String() string

String spells the scope as the committed records write it: a scope by its GitLab name, the two values no scope decides by what they are.

type Decision

type Decision struct {
	// Listed is whether tools/list, find and the manifest show the action.
	Listed bool
	// Callable is whether the call guard passes it.
	Callable bool
	// Cause is why not, when not.
	Cause Cause
	// Missing are indices into [Table.Groups]: in phase B, the groups the
	// grant fails, for the words.
	Missing []uint32
	// Degraded are indices into [Table.Elements]: the positions served empty
	// for this credential.
	Degraded []uint32
	// Known is false when the table has no row for the ID.
	Known bool
}

Decision is what Authority.Decide answers for one action ID.

type Denial

type Denial struct {
	Cause Cause `json:"cause"`
	// Element is the type, mutation or route that decides it.
	Element string `json:"element"`
	Effect  Effect `json:"effect"`
}

Denial says why no fine-grained token reaches an action.

type Description

type Description struct {
	// GitLabVersion is the release the requirement was recorded at.
	GitLabVersion string `json:"gitlab_version"`
	// AnyOf are the ways of running the action, any one of which is enough:
	// the action sends a different request depending on its input.
	AnyOf []Way `json:"any_of,omitempty"`
	// DeniedWays are the ways of running the action no fine-grained token
	// passes, beside the ways in AnyOf that it can: an input that selects one
	// of these meets the denial, every other input runs.
	DeniedWays []Denial `json:"denied_ways,omitempty"`
	// OptionalNeeds is what the requests the action sends on some calls only
	// need beyond what every way in AnyOf already needs: a grant without it
	// runs every call that does not send them, and a call that does may
	// answer with less or be refused.
	OptionalNeeds []Need `json:"optional_needs,omitempty"`
	// OptionalDenied are the denials of the requests the action sends on
	// some calls only that no fine-grained token passes at this release.
	OptionalDenied []Denial `json:"optional_denied,omitempty"`
	// Denied says why no fine-grained token runs the action at this release.
	Denied *Denial `json:"denied,omitempty"`
	// AlwaysEmpty are parts of the answer GitLab leaves empty for every
	// fine-grained token, while the rest is served, each written as a
	// [Selection].
	AlwaysEmpty []string `json:"always_empty,omitempty"`
	// EmptyWithout are parts of the answer GitLab leaves empty unless the
	// grant also holds what each names.
	EmptyWithout []Position `json:"empty_without,omitempty"`
}

Description is one action's requirement in GitLab's words: what the detail of an action serves a model and what the documentation site's fine-grained permissions page prints (https://docs.jmrp.io/gitlab-mcp-server/reference/fine-grained-permissions/), built here once so the two never word a requirement differently.

type Effect

type Effect string

Effect is what GitLab does to the answer when a fine-grained token is denied a position or an operation.

const (
	// EffectNull is a nullable object answered null, with no error.
	EffectNull Effect = "null"
	// EffectNullOrEmpty is an object a boundary does not resolve for: null at
	// a single position, removed from a list.
	EffectNullOrEmpty Effect = "null-or-empty"
	// EffectRemoved is a list or connection whose denied items GitLab
	// removes, with no error.
	EffectRemoved Effect = "removed-items"
	// EffectListNull is a list of non-null items that one denied item nulls
	// whole, with an error.
	EffectListNull Effect = "list-null"
	// EffectRefused is a request refused before anything runs.
	EffectRefused Effect = "refused"
	// EffectCommittedThenNull is a write that commits and answers null.
	EffectCommittedThenNull Effect = "committed-then-null"
)

The effects, each derived from GitLab's rules for the position's signature and its type's authorization.

type Element

type Element struct {
	// Path is where the position sits in the answer, root field first
	// ("vulnerability.issueLinks.nodes").
	Path string
	// Type is the object type, or the union or interface name.
	Type string
	// Members are an abstract position's possible types at the recorded
	// version.
	Members []string
	// Groups are indices into [Table.Groups]; for an abstract position, every
	// member's groups.
	Groups []uint32
	// Undeclared is set for an enforced type with no directive (for an
	// abstract position, when any member is one): no fine-grained token
	// passes it.
	Undeclared bool
	// Effect is what a denial here does to the answer.
	Effect Effect
}

Element is one GraphQL object position a document selects. At an abstract position (a union or an interface) it stands for the member types GitLab may resolve there, and is judged as the worst of them.

type FallbackReason

type FallbackReason string

FallbackReason is why an Authority stayed in phase A.

const (
	// FallbackNone is phase A with nothing to say: no grant was asked for.
	FallbackNone FallbackReason = ""
	// FallbackGrantUnreadable is a token that cannot read its own grant.
	FallbackGrantUnreadable FallbackReason = "grant-unreadable"
	// FallbackGrantUnanswered is a grant read the instance did not answer: a
	// transport failure, a timeout, a status that is no verdict on the read.
	// It is the one reason a later read can lift without anything about the
	// token changing, which the next revalidation does.
	FallbackGrantUnanswered FallbackReason = "grant-unanswered"
	// FallbackGrantTooLarge is a grant larger than this server reads.
	FallbackGrantTooLarge FallbackReason = "grant-too-large"
	// FallbackGrantShape is a grant holding something this server cannot
	// read without guessing.
	FallbackGrantShape FallbackReason = "grant-shape-unknown"
	// FallbackVersionUnreadable is an instance that answered the request for
	// its version and named none this server can read: it refused the
	// token's grant Metadata: Read, or it reported a string that does not
	// validate. Neither changes while the token and the instance stay what
	// they are, so the grant is not asked for.
	FallbackVersionUnreadable FallbackReason = "version-unreadable"
	// FallbackVersionUnanswered is a version read the instance did not
	// answer: a transport failure, a timeout, a status that is no verdict on
	// the read. Like an unanswered grant read, it says nothing about the
	// token, and a later read lifts it; the grant is not asked for, since no
	// grant is evaluated without a version.
	FallbackVersionUnanswered FallbackReason = "version-unanswered"
	// FallbackVersionOutside is an instance whose version no recorded table
	// describes.
	FallbackVersionOutside FallbackReason = "version-outside-record"
	// FallbackUnknownPermission is a grant naming a permission the recorded
	// version does not define.
	FallbackUnknownPermission FallbackReason = "unknown-permission"
)

The reasons, each with the words a withheld answer appends.

type Grant

type Grant struct {
	Scopes []Scope
}

Grant is what a fine-grained token was granted when it was created; nothing edits it afterwards.

type GrantAccess

type GrantAccess = tenancy.GrantAccess

GrantAccess is the access level of one granular scope, which says what namespaces it reaches (app/models/authz/granular_scope.rb). It is the register's type, the input tenancy.CoverableAt reads.

type Group

type Group struct {
	// Perms are indices into [Table.Permissions].
	Perms []uint16
	Any   Boundary
}

Group is one requirement GitLab checks on its own: every raw permission in Perms, held at one boundary type among Any. A REST route has one for its primary declaration and one per additional scope; a GraphQL element has one per requirement group of its directives, with the permissions of that group's first directive only, as GitLab reads them (lib/gitlab/graphql/authz/granular_scope_authorization.rb).

type NamespaceKind

type NamespaceKind = tenancy.NamespaceKind

NamespaceKind is the kind of namespace a scope is attached to, read from what the entity exposes: a project id only for a project namespace, a group id only for a group (lib/api/entities/personal_access_token_granular_scope.rb). It is the register's type, the input tenancy.CoverableAt reads.

type Need

type Need struct {
	Permissions []string `json:"permissions"`
	At          []string `json:"at"`
}

Need is one requirement GitLab checks on its own: every permission, held at one of the boundaries.

type Operation

type Operation struct {
	// Name is the route as the live record spells it ("POST
	// /projects/:id/merge_requests/:merge_request_iid/approve"), or a GraphQL
	// operation's kind and root field with the document it comes from.
	Name string
	// Classic is the classic scope GitLab requires for the request.
	Classic ClassicScope
	// Groups are indices into [Table.Groups]: a REST route's primary group and
	// additional scopes, or a GraphQL mutation's own groups.
	Groups []uint32
	// Skip is set when the route or mutation opts out of the check for any
	// reason: the grant does not decide it, GitLab does.
	Skip bool
	// Spine are indices into [Table.Elements]: the declared positions on the
	// answer spine, and every declared position whose denial a non-null chain
	// carries onto it. Every group of each must pass.
	Spine []uint32
	// OffSpine are the declared positions whose denial stays off the spine,
	// judged per grant for the degraded note and never for the listing.
	OffSpine []uint32
}

Operation is one wire request: a REST route or a GraphQL operation.

type Phase

type Phase uint8

Phase is how much an Authority knows about the grant it stands for.

const (
	// PhaseUnknown is phase A: the grant was not evaluated, so only what no
	// fine-grained token can reach is withheld.
	PhaseUnknown Phase = iota
	// PhaseGranted is phase B: the grant was read and evaluated.
	PhaseGranted
)

func (Phase) String

func (p Phase) String() string

String names the phase for a log line: "A" or "B".

type Position

type Position struct {
	Selection string `json:"selection"`
	Needs     []Need `json:"needs"`
}

Position is a part of an answer, written as a Selection, and what a grant needs for it to be served.

type Reading

type Reading struct {
	// Grant is the token's grant, as [DecodeGrant] read it.
	Grant Grant
	// Fallback is why the grant cannot be evaluated, or [FallbackNone] when it
	// can: a reason the grant read gave, or, when the version read left no
	// version to judge at, the reason it gave instead
	// ([FallbackVersionUnanswered], [FallbackVersionUnreadable]), since a
	// reader does not ask for a grant no answer could let it evaluate.
	Fallback FallbackReason
	// Version is the version the instance reported, validated by its reader,
	// and "" when it was not read or was not one.
	Version string
}

Reading is what was read about a fine-grained token for its authority: the grant it was created with, or why the grant cannot be evaluated, and the version the instance reported.

type Requirement

type Requirement struct {
	// ID is the canonical action ID.
	ID string
	// Classic is the classic scope the action needs: of every way it can run,
	// a fine-grained denied one included, the one needing the least, where a
	// way needs the most any of its mandatory requests needs. The scopes its
	// catalog group demands besides (admin_mode) are not held here.
	Classic ClassicScope
	// Paths are sets of indices into [Table.Operations], the mandatory
	// operations of each input-selected way the action can run.
	Paths [][]uint32
	// Denied is set when no fine-grained token reaches the action at the
	// recorded version.
	Denied *Denial
	// DeniedWays are the denials of the ways no fine-grained token runs,
	// each once, when another way is reachable: the action runs, except with
	// an input that selects one of these. They are kept out of Paths, which
	// is what a grant is judged against, since no grant passes them.
	DeniedWays []Denial
	// Optional are indices into [Table.Operations]: the requests the action
	// sends on some calls only and no way needs, such as a lookup its input
	// or GitLab's answer to an earlier request calls for (issue 1242). They
	// are kept out of Paths, since a grant without them still runs the
	// action, and are worded beside them so a reader knows what such a call
	// needs as well.
	Optional []uint32
	// OptionalDenied are the denials of the optional requests no
	// fine-grained token passes at the recorded version, each once.
	OptionalDenied []Denial
	// Degraded are indices into [Table.Elements]: undeclared positions off
	// every spine, served and always empty.
	Degraded []uint32
	// GraphQL is set when some mandatory operation of any way, a denied one
	// included, is a GraphQL one, which is what a null answer is worth a hint
	// for: an input that selects a denied GraphQL way is answered null too.
	GraphQL bool
	// Collection is set when the answer spine of any way, a denied one
	// included, passes through a list or a connection, which is what an
	// empty answer is worth a hint for.
	Collection bool
}

Requirement is one action's. It is reachable when some path passes, and a path passes when every operation in it passes.

type Scope

type Scope struct {
	Access    GrantAccess
	Namespace NamespaceKind
	// NamespaceID is the project or group id the entity exposes, 0 for the
	// kinds it exposes none for.
	NamespaceID int64
	// Permissions are assignable names, as a user grants them.
	Permissions []string
}

Scope is one granular scope of a grant.

type Table

type Table struct {
	// Version is the release the record was taken from ("19.4.1-ee") and
	// Bucket its major.minor ("19.4"), which is what an instance's version is
	// matched against.
	Version string
	Bucket  string
	// Permissions are the raw permission names, sorted.
	Permissions []string
	// Displays are the words GitLab's token creation page offers an
	// assignable permission by ("Merge Request: Approve"), each once and
	// sorted, the first of them empty.
	Displays []string
	// Display is, per raw permission, the index in Displays of the words of
	// the first assignable a token can be granted that expands to it; 0, the
	// empty words, when none can. Many raw permissions share one assignable,
	// so the words are held once and indexed rather than repeated.
	Display     []uint16
	Assignables []Assignable
	// PublicAnonymous are bit sets over Permissions, indexed by
	// [PublicProject] and [PublicGroup]: the anonymous policy GitLab evaluated
	// on a public project and group.
	PublicAnonymous [2][]uint64
	// PublicKnown is false when the record carries no evaluated set, in which
	// case nothing here refuses a call on the public question.
	PublicKnown bool
	Groups      []Group
	Operations  []Operation
	Elements    []Element
	// Actions are sorted by ID.
	Actions []Requirement
}

Table is the generated join, for one recorded GitLab version.

func (*Table) Assignable

func (t *Table) Assignable(name string) *Assignable

Assignable returns the assignable permission of one name, deprecated ones included, or nil for a name the recorded version does not define.

func (*Table) Describe

func (t *Table) Describe(row *Requirement) *Description

Describe words one row. A nil row describes nothing and yields nil.

func (*Table) DisplayVersion

func (t *Table) DisplayVersion() string

DisplayVersion is the version as a person reads it, without the edition suffix the record carries ("19.4.1").

func (*Table) Knows

func (t *Table) Knows(names []string) bool

Knows reports whether every assignable name in names is one the recorded version defines. A grant naming anything else was written for another release, which is the vocabulary guard: a rename or a new assignable falls back to phase A rather than being read as nothing granted.

func (*Table) Requirement

func (t *Table) Requirement(id string) *Requirement

Requirement returns the row for one canonical action ID, or nil when the table has none.

func (*Table) RouteTemplate

func (t *Table) RouteTemplate(method, escapedPath string) (string, bool)

RouteTemplate names the route a GitLab request was sent to, from its method and its escaped path, as the table's own operations spell it: the path template of the REST operation it matches, under "/api/v4", or the GraphQL endpoint's own path. It reports false for a request to anything else, which is a route no action of this table calls.

What it answers is always one of a closed set compiled into the binary, a template with every identifier a placeholder, and never a value the request carried. That is what lets a client span carry it as url.template without carrying a project path, a file name or a search query, which is the reason the span records no URL at all (internal/mcpotel's round tripper), and what keeps the attribute low in cardinality whatever a caller names.

A request matches a template segment by segment: a literal segment matches itself, ":name" any one segment, "*name" one segment or more, and Grape's optional groups ("(-/)", "(ref/:ref/)") both with and without what they hold. The template with the most literal segments wins, so /projects/1/issues/statistics names the statistics route rather than an issue. A HEAD request with no HEAD route of its own is matched against the GET routes, since Grape answers a HEAD from the GET it mounts beside it and checks that GET's authorization, which a fine-grained token on a booted GitLab 19.4.1 shows (a HEAD on a raw file is refused naming Repository: Read exactly when the GET is). Repeated slashes count as one, as the server in front of GitLab's router merges them: client-go builds its Sidekiq metrics paths as /api/v4//sidekiq/..., which GitLab serves as the /sidekiq routes, so an empty segment never names, nor fails to name, a route.

An instance installed under a path of its own (a relative URL root, https://host/gitlab) has every request carry that path in front of the API root, and the route is named from the API root on: what precedes the first "/api/" segment is the instance's, and is cut. It stays out of the template because it is not a route of the table, and because in the deployment mode where a caller names the instance it is the caller's to choose, which would make the attribute as unbounded as the URL it replaces. A path a project or file name writes "/api/" into comes after that segment, since such a name travels percent-encoded inside one segment, and a root that itself holds an "/api/" segment names no route, which is the method-only name a route the table does not hold gets.

type Way

type Way struct {
	Needs []Need `json:"needs,omitempty"`
	// NotJudged is set when a request on this way opts out of the grant
	// check, so GitLab judges it by the token's other properties.
	NotJudged bool `json:"not_judged_by_grant,omitempty"`
}

Way is one way of running an action: every need, each at one of its boundaries.

Jump to

Keyboard shortcuts

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