Documentation
¶
Overview ¶
Package forge is the code host behind a board: the identity provider the server signs people in with, the authority on who may read or write a repository, the directory of names and avatars, and the credential git pushes with. GitHub was the first; GitLab is the second. Everything the server does with a forge goes through the Forge interface, and the server never assembles a forge URL of its own.
Index ¶
- Constants
- Variables
- func CanPushGit(ctx context.Context, client *http.Client, f Forge, token, repoURL string) (bool, error)
- func HostOf(repoURL string) string
- func IsAppClientID(clientID string) bool
- func IsUserToServerToken(token string) bool
- type AppGitAuth
- type AppNotInstalledError
- type CLI
- type Credential
- type Forge
- type GitHubApp
- type Kind
- type Person
- type User
Constants ¶
const UnansweredTTL = 30 * time.Second
UnansweredTTL is how long a source remembers that the forge could not say who a token belongs to — a 403 for a token with no user behind it, a 5xx, a timeout, a DNS failure. Without it the question is asked again on every request, under the lock the source holds while it asks, so one unreachable forge serialises every caller behind a full timeout each.
It matches the ceiling those sources put on the call, so a window holds at most one attempt that runs to the end; and half a minute is short enough that a forge coming back — a VPN reconnecting, a rate limit lifting — names the person again without a restart. A refusal is NOT this: a 401 is an answer, and it is remembered until the token changes.
Variables ¶
var ( // ErrBadToken is the forge refusing the token it was given (401). ErrBadToken = errors.New("forge: the token was rejected") // ErrNotFound is a login the forge does not know. ErrNotFound = errors.New("forge: no such person") // ErrRateLimited is the forge throttling us. It shares the 403 of "you // may not see this" and must never be read as one: a moment of // throttling would otherwise be remembered as a visitor's lack of // access to their own board. ErrRateLimited = errors.New("forge: rate limited") )
Errors a forge reports.
Functions ¶
func CanPushGit ¶ added in v0.27.0
func CanPushGit(ctx context.Context, client *http.Client, f Forge, token, repoURL string) (bool, error)
CanPushGit asks the git transport itself whether a token may push: the receive-pack advertisement (GET /info/refs?service=git-receive-pack) answers 200 exactly when it may, 403 when it may not, 401 when the token is nobody there. The REST permissions block is a different animal — a GitHub App's user token can push a repository the REST probe says nothing useful about, and the transport is the authority the push will face anyway.
func HostOf ¶
HostOf is the host of a repository URL — https://host/…, git@host:… — or "" for a bare owner/repo.
func IsAppClientID ¶ added in v0.27.0
IsAppClientID reports whether a client id belongs to a GitHub App rather than an OAuth App. A GitHub App has no scopes — its permissions come from the installation, and GitHub ignores a scope parameter — so asking for one would leave "scope=repo" in a URL that grants nothing of the sort. GitHub App client ids carry an Iv prefix (Iv1.<hex> historically, Iv23li… now); anything else is taken for an OAuth App, whose sign-in breaks without its scope — so the doubt falls the safe way.
func IsUserToServerToken ¶ added in v0.27.0
IsUserToServerToken reports whether a token was minted for a person BY a GitHub App: such a token reaches only the repositories the app is installed on, so a refusal about someone's own repository means "not installed there", not "no access". GitHub says which kind a token is in its prefix — ghu_ for user-to-server, gho_ for an OAuth App's, ghp_ for a classic personal token, ghs_ for an installation's own.
Types ¶
type AppGitAuth ¶ added in v0.27.0
type AppGitAuth struct {
// contains filtered or unexported fields
}
AppGitAuth stamps a fresh installation token onto each git HTTP request.
func (*AppGitAuth) Name ¶ added in v0.27.0
func (g *AppGitAuth) Name() string
func (*AppGitAuth) SetAuth ¶ added in v0.27.0
func (g *AppGitAuth) SetAuth(r *http.Request)
SetAuth cannot return an error; when the mint fails the request goes out bare and the operation fails with the forge's own refusal, to be retried by the sync — the mint error itself surfaces on the next Token caller.
func (*AppGitAuth) String ¶ added in v0.27.0
func (g *AppGitAuth) String() string
type AppNotInstalledError ¶ added in v0.27.0
type AppNotInstalledError struct {
App string // the app id
Slug string // owner/repo
InstallURL string // where the app is installed
}
AppNotInstalledError is a repository the app has no installation for — the one startup trouble a page with a button can fix, so callers tell it apart (errors.As) and serve that page instead of refusing to run.
func (*AppNotInstalledError) Error ¶ added in v0.27.0
func (e *AppNotInstalledError) Error() string
type CLI ¶
type CLI interface {
// Token is this source's credential. A source with nothing to give
// may answer either way — an error, or the empty string — and a
// caller standing for SEVERAL sources owes the difference: it must
// return an error when none of them answered, never a silent empty
// string, because the server decides "have we got a credential at
// all" on that error alone.
Token(ctx context.Context) (string, error)
// Login is who the forge says this source's token belongs to, which
// need not be whoever ran a tool on the machine: a stored bot token
// belongs to the bot. It must describe the token Token would hand
// back right now, or the push is made with one account's credential
// and the work signed with another's name.
Login(ctx context.Context) (string, error)
}
CLI is a source of the person's credential and identity on a single-user server — the forge's own tool (gh, glab), the environment, or the OS keychain. Its token is what the server reads the forge and pushes with, and its login is who that token belongs to; the two come from ONE source, or the board pushes as one account and signs the work with another's name.
type Credential ¶ added in v0.33.0
type Credential interface {
CLI
TokenAndLogin(ctx context.Context) (token, login string, err error)
}
Credential is a CLI that can answer both questions at once. A caller that needs the token AND the person it belongs to must not ask twice: a CLI standing for several sources re-decides between the calls when one of them empties, and the caller is then holding one source's token beside another's name — which is the split the pairing exists to prevent. A CLI that is a single source has nothing to add and need not implement it; ask separately when the assertion fails.
type Forge ¶
type Forge interface {
// Kind names the forge; Label is its display name for UI copy.
Kind() Kind
Label() string
// Host is the instance this forge is: github.com, or the host of a
// GitLab's base URL. It is what a credential belongs to, so a caller
// keeping one token per forge keys it by this rather than parsing a
// repository URL of its own.
Host() string
// AuthorizeURL and TokenURL are the OAuth endpoints the sign-in flow
// uses; DefaultScopes the scope string a board needs when the operator
// sets none.
AuthorizeURL() string
TokenURL() string
DefaultScopes() string
// ExchangeForm and RefreshForm are the token endpoint's request bodies
// in the forge's dialect (GitLab names the grant type and wants the
// redirect URI on a refresh too).
ExchangeForm(clientID, secret, code, redirectURI string) url.Values
RefreshForm(clientID, secret, refresh, redirectURI string) url.Values
// User is who a token belongs to. A rejected token is ErrBadToken.
User(ctx context.Context, client *http.Client, token string) (User, error)
// RepoRef is a repository URL as the forge's API names the repository —
// and the check that the URL names one at all.
RepoRef(repoURL string) (string, error)
// Access asks, as the visitor, what they may do with the repository:
// read it, write it. A repository they cannot see is (false, false, nil);
// a rejected token is ErrBadToken.
Access(ctx context.Context, client *http.Client, token, repoURL string) (read, write bool, err error)
// Readers is the subset of logins that may read the repository, asked
// with the server's credential, keyed by login — with whatever the forge
// said about each on the way (a member list carries names and avatars).
Readers(ctx context.Context, client *http.Client, serverToken, repoURL string, logins []string) (map[string]Person, error)
// Lookup finds a person by login. It may or may not call the forge.
Lookup(ctx context.Context, client *http.Client, token, login string) (Person, error)
// GitAuth is the HTTPS credential git operations use with a token.
GitAuth(token string) *githttp.BasicAuth
}
Forge is one code host.
func Detect ¶
Detect names the forge for a repository: the explicit kind when given, else by the URL's host — github.com is GitHub, a host that says gitlab is that GitLab instance — else GitLab when a base URL for one is configured, else GitHub, the forge this code base grew up on.
func NewGitHubAt ¶
NewGitHubAt is GitHub with its REST base elsewhere — tests.
type GitHubApp ¶ added in v0.27.0
type GitHubApp struct {
// contains filtered or unexported fields
}
GitHubApp is the server credential minted instead of kept: the server signs a short-lived JWT with the app's private key, asks which installation a repository belongs to, and trades the JWT for an installation token — scoped to the repositories the app is installed on, expiring within the hour, renewed here before it runs out. One key covers every organisation the app is installed in; nothing is issued by hand and nothing quietly expires in a .env file.
func NewGitHubApp ¶ added in v0.27.0
NewGitHubApp is a GitHub App credential for github.com. id is the app id (the JWT issuer), pemKey the private key GitHub generated for the app. The key is validated here — a broken PEM stops the server at startup, not the first push an hour in.
func NewGitHubAppAt ¶ added in v0.27.0
NewGitHubAppAt is NewGitHubApp with the REST base elsewhere — tests.
func (*GitHubApp) GitAuthFor ¶ added in v0.27.0
func (a *GitHubApp) GitAuthFor(repoURL string) *AppGitAuth
GitAuthFor is the git transport credential for one repository: the token is asked for on every request, so one renewed between two pushes is picked up without re-wiring the remote.
func (*GitHubApp) InstallURL ¶ added in v0.27.0
InstallURL is where the app is installed on an account: the app's own page plus /installations/new, which lets the person pick the account and the repositories. Asked from the forge once; a lookup that fails answers the app's settings path, which always exists.