githubapp

package
v1.67.0 Latest Latest
Warning

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

Go to latest
Published: Oct 7, 2026 License: MIT Imports: 23 Imported by: 0

Documentation

Overview

Package githubapp is the part of GitHub's App API that connecting an organisation needs — creating an App from a manifest, finding where it is installed, and uninstalling it — and the part linking a person's GitHub account needs: authorizing the link App as themselves, and reading back who they are and which addresses GitHub verified for them.

An App is created by an organisation's owner in two clicks — Create, then Install — and nothing about it is typed or pasted. This is how the GitHub controller gets a credential, and it is the same verb as connecting a directory: the operator grants access once, in a browser, and the credential is kept by this service rather than delivered by a deployment.

Ported from github-roster 0.x (`pkg/githubapp`) onto the JWT library access-roster already uses. A conversion's webhook secret is dropped on the floor, because nothing here receives a webhook; its client secret is kept only for the link App, which is the one App people authorize.

Index

Constants

View Source
const Retries = 3

Retries is how many times a rate-limited call is retried.

Variables

View Source
var (
	// WebBase is where a browser is sent to create and install an App.
	WebBase = "https://github.com"
	// APIBase is where the API calls go.
	APIBase = "https://api.github.com"
)

The hosts every call goes to. Variables rather than constants for one reason: a test points them at a fake. Production never moves them, and nothing reads them from configuration.

View Source
var (
	// Sleep passes one rate-limit wait. It returns early, with the
	// context's error, when the context ends.
	Sleep = sleepContext
	// Now is the clock a reset time is measured against.
	Now = time.Now
)

Variables rather than constants for one reason: a test replaces them so no wait is spent in real time. Production never moves them.

View Source
var ErrAuthorizationRefused = errors.New("github: GitHub refused the authorization")

ErrAuthorizationRefused is GitHub refusing a code or a refresh token. It is an answer, not an outage: the grant is spent, expired or revoked.

View Source
var ErrInstallationGone = errors.New("github: the installation no longer exists")

ErrInstallationGone is an installation GitHub no longer has: somebody uninstalled the App on GitHub.

View Source
var ErrNotInstalled = errors.New("github: the App is not installed on the organisation")

ErrNotInstalled is an App with no installation on the organisation yet: the owner created it and has not clicked Install.

View Source
var ErrTokenRefused = errors.New("github: GitHub refused the token")

ErrTokenRefused is GitHub refusing a person's token outright (401). Whether that is a revoked authorization or something else is a second question — CheckUserToken answers it.

Functions

func AppToken

func AppToken(appID int64, privateKey string, now time.Time) (string, error)

AppToken is the short-lived JWT an App authenticates App-level calls with, signed by its private key.

func AuthorizeURL

func AuthorizeURL(clientID, redirect, state string) string

AuthorizeURL is where a person is sent to authorize the link App.

func CheckUserToken

func CheckUserToken(ctx context.Context, client *http.Client, clientID, secret, token string) (bool, error)

CheckUserToken asks GitHub, as the App, whether a person's token is still valid. False with no error is GitHub's own answer that it is not: the authorization was revoked, or the account is gone.

func CreateURL

func CreateURL(org, state string) string

CreateURL is where the owner is sent to create the App, by a form POST carrying the manifest. The state comes back on the redirect.

func DeleteInstallation

func DeleteInstallation(ctx context.Context, client *http.Client, appToken string, installation int64) error

DeleteInstallation uninstalls the App from wherever that installation is. It is Disconnect's revoke: the App registration stays on GitHub — the API cannot delete one — but nothing can act through it any more.

func FindInstallation

func FindInstallation(ctx context.Context, client *http.Client, appToken, org string) (int64, error)

FindInstallation returns where the App is installed on an organisation.

The setup redirect carries an installation id, and it is not what is kept: it arrives in a query a browser controls. This asks GitHub, as the App, which of its installations belongs to that organisation.

func FormatID

func FormatID(id int64) string

FormatID is a GitHub id as the string it is kept under.

func InstallURL

func InstallURL(slug, state string) string

InstallURL is where the owner is sent to install an App they created. The state comes back on the setup redirect.

func InstallationToken

func InstallationToken(ctx context.Context, client *http.Client, appToken string, installation int64) (string, time.Time, error)

InstallationToken is what the App acts in one installation with: an hour-long token GitHub mints on request, signed for by the App JWT.

func LinkApp

func LinkApp(owner string) catalogue.App

LinkApp is the App a person authorizes to link their GitHub account to their work identity, as a catalogue entry.

It is a different App from an organisation's, on purpose. A person's token for an App carries that App's permissions, so a token for the organisation's App could manage members; this App asks for one thing — reading the person's own email addresses — and is installed nowhere, so a token for it reads one person's address list and nothing else.

PUBLIC, because a private App can only be authorized by members of the organisation that owns it: a new hire who is not in the organisation yet, and a partner company's people who never will be, could not link. Anybody may install a public App, and installing this one grants nothing — it has no permission on any organisation or repository.

func OrganisationApp

func OrganisationApp(org string) catalogue.App

OrganisationApp is the App the GitHub controller acts through in one organisation, as a catalogue entry.

Private, because a public App can be installed by anybody on anything, and one App per organisation is what a private App forces anyway: it installs only on the account that owns it. One permission, `members: write`, which is enough to invite, add to and remove from teams, change a team role, and read who is a member — and nothing about any repository. No webhook: the controller asks, so nothing needs to be delivered to it and no endpoint of ours has to be reachable from GitHub.

func PublicEmail

func PublicEmail(ctx context.Context, client *http.Client, token, login string) (string, error)

PublicEmail is the address an account shows on its profile, lowercased, or empty. GitHub lets an account publish only an address it has verified, so a published address is one GitHub vouches for.

func RunnerApp

func RunnerApp(org, tier string) catalogue.App

RunnerApp is the App one tier's self-hosted runners register with in one organisation, as a catalogue entry.

Private and installed only on its owner, like an organisation's App. One permission, `organization_self_hosted_runners: write`: what a runner scale set needs to register runners at organisation level, and nothing about members or any repository. One App per organisation per tier, so the key a tier's runner plane holds opens nothing in another tier. No webhook: a scale set asks.

The name carries the tier; a long organisation login is cut, never the tier, so two tiers' Apps never share a name.

func UserID

func UserID(ctx context.Context, client *http.Client, token, login string) (int64, error)

UserID resolves a login to its account id.

func VerifiedEmails

func VerifiedEmails(ctx context.Context, client *http.Client, token string) ([]string, error)

VerifiedEmails are the addresses GitHub has verified for the token's account, lowercased. Unverified ones are left out: anybody can add an address to their account, and only verifying it proves they receive mail there.

Types

type AppInfo

type AppInfo struct {
	ID          int64
	Slug        string
	Owner       string
	HTMLURL     string
	Permissions map[string]string
	Events      []string
}

AppInfo is what GitHub says an App is, asked as the App.

func GetApp

func GetApp(ctx context.Context, client *http.Client, appToken string) (AppInfo, error)

GetApp reads the App the token belongs to: its permissions and events as GitHub holds them now, which an owner may have edited since it was created.

type FailedInvitation

type FailedInvitation struct {
	Login    string
	Email    string
	FailedAt time.Time
}

FailedInvitation is an invitation that expired or was cancelled.

type HookAttributes

type HookAttributes struct {
	URL    string `json:"url"`
	Active bool   `json:"active"`
}

HookAttributes is an App's webhook. The controller polls, so it is never active; GitHub still wants a URL beside it.

type InstallationInfo

type InstallationInfo struct {
	ID                  int64
	Account             string
	Permissions         map[string]string
	Events              []string
	RepositorySelection string
	Suspended           bool
}

InstallationInfo is one installation as GitHub holds it: the permissions its owner accepted, which lag the App's until they approve a change.

func GetInstallation

func GetInstallation(ctx context.Context, client *http.Client, appToken string, installation int64) (InstallationInfo, error)

GetInstallation reads one of the App's installations.

type Invitation

type Invitation struct {
	ID    int64
	Email string
	Login string
}

Invitation is a pending invitation to the organisation.

type Manifest

type Manifest struct {
	Name               string            `json:"name"`
	URL                string            `json:"url"`
	HookAttributes     HookAttributes    `json:"hook_attributes"`
	RedirectURL        string            `json:"redirect_url"`
	SetupURL           string            `json:"setup_url"`
	Public             bool              `json:"public"`
	DefaultPermissions map[string]string `json:"default_permissions"`
	// CallbackURLs are where GitHub may send somebody back after they
	// authorize the App as themselves. Only the link App has one.
	CallbackURLs []string `json:"callback_urls,omitempty"`
	// Description is shown on the App's page. Only a catalogue App has
	// one.
	Description string `json:"description,omitempty"`
	// DefaultEvents are the webhook events the App subscribes to. The
	// webhook stays inactive whatever they are.
	DefaultEvents []string `json:"default_events,omitempty"`
}

Manifest is what an App is created from. See https://docs.github.com/en/apps/sharing-github-apps/registering-a-github-app-from-a-manifest

func ManifestFor

func ManifestFor(app catalogue.App, homepage, redirect, setup string, callbacks []string) Manifest

ManifestFor is the one manifest builder: every App this service creates, its own and a catalogue's, is an entry of the catalogue's shape turned into what GitHub's create page takes.

homepage is where the App's page links; redirect and setup are the two callbacks, after Create and after Install (setup empty for an App that is installed nowhere); callbacks are where a person authorizing the App as themselves may be sent back to.

The webhook is never active: nothing here receives one, so no endpoint of ours has to be reachable from GitHub, whatever events are declared.

func NewLinkManifest

func NewLinkManifest(owner, homepage, redirect, callback string) Manifest

NewLinkManifest is LinkApp's manifest: no setup callback, because it is installed nowhere, and the one callback a person's authorization returns to.

func NewManifest

func NewManifest(org, homepage, redirect, setup string) Manifest

NewManifest is OrganisationApp's manifest: redirect and setup are the two callbacks, after Create and after Install.

func NewRunnerManifest

func NewRunnerManifest(org, tier, homepage, redirect, setup string) Manifest

NewRunnerManifest is RunnerApp's manifest.

type Member

type Member struct {
	// ID is the account's id, which a link is kept by.
	ID    int64
	Login string
	// Owner is the organisation's admin role. The controller never changes
	// it and never removes an owner from the organisation.
	Owner bool
	// Emails are the member's addresses in the organisation's VERIFIED
	// domains, which GitHub discloses only to an organisation on its
	// Enterprise Cloud plan. Everywhere else this is empty, and a member is
	// matched to a person by the link they made themselves.
	Emails []string
}

Member is one member of the organisation.

type MintedToken

type MintedToken struct {
	Token     string
	ExpiresAt time.Time
	// Repositories are the repositories the token is narrowed to, by name.
	// Empty is every repository the installation can reach.
	Repositories []string
	// Permissions are what the token carries.
	Permissions map[string]string
}

MintedToken is an installation token and what GitHub says it carries, which is what was granted rather than what was asked for.

func InstallationTokenFor

func InstallationTokenFor(
	ctx context.Context, client *http.Client, appToken string, installation int64, narrowing Narrowing,
) (MintedToken, error)

InstallationTokenFor mints an installation token narrowed to some repositories and permissions. GitHub refuses a narrowing wider than the installation with a 422, returned as a StatusError.

type Narrowing

type Narrowing struct {
	// Repositories are names within the installation's account, without
	// the owner.
	Repositories []string `json:"repositories,omitempty"`
	// Permissions are GitHub permission names to read, write or admin.
	Permissions map[string]string `json:"permissions,omitempty"`
}

Narrowing is what an installation token is cut down to: some of the installation's repositories, some of its permissions. Empty fields narrow nothing, and GitHub then grants everything the installation has.

type Org

type Org struct {
	HTTP  *http.Client
	Login string
}

Org is one organisation, as the App installed in it sees and changes it. Every call takes the installation token rather than holding one, so the caller decides when a token is minted and for how long it is kept.

func (Org) FailedInvitations

func (o Org) FailedInvitations(ctx context.Context, token string) ([]FailedInvitation, error)

FailedInvitations reads the organisation's failed invitations.

func (Org) Invitations

func (o Org) Invitations(ctx context.Context, token string) ([]Invitation, error)

Invitations reads the organisation's pending invitations.

func (Org) Invite

func (o Org) Invite(ctx context.Context, token, email string, teams []int64) error

Invite invites an address into the organisation, straight into teams, so that accepting is the only step left.

func (Org) InviteUser

func (o Org) InviteUser(ctx context.Context, token string, id int64, teams []int64) error

InviteUser invites a GitHub account by its id, straight into teams. It is how a person who linked their account is invited: the account is known, so nothing about the invitation has to be matched back afterwards.

func (Org) IsMember

func (o Org) IsMember(ctx context.Context, token, login string) (bool, error)

IsMember reports whether an account is a member of the organisation.

func (Org) Members

func (o Org) Members(ctx context.Context, token string) ([]Member, error)

Members reads every member of the organisation.

func (Org) OutsideCollaborators

func (o Org) OutsideCollaborators(ctx context.Context, token string) ([]string, error)

OutsideCollaborators reads who has access to the organisation's repositories without being a member of it.

func (Org) Plan

func (o Org) Plan(ctx context.Context, token string) (Plan, bool, error)

Plan reads the organisation's seats. The second result is false when GitHub did not say — the App lacks organisation administration (read) — which is not the same as zero seats and must never be read as it.

func (Org) RemoveFromOrg

func (o Org) RemoveFromOrg(ctx context.Context, token, login string) error

RemoveFromOrg removes a member from the organisation, and so from every team in it.

func (Org) RemoveFromTeam

func (o Org) RemoveFromTeam(ctx context.Context, token, slug, login string) error

RemoveFromTeam removes a member from one team.

func (Org) SetTeamRole

func (o Org) SetTeamRole(ctx context.Context, token, slug, login string, maintainer bool) error

SetTeamRole adds a member to a team in a role, or changes the role they have. GitHub's one call does both.

func (Org) TeamMembers

func (o Org) TeamMembers(ctx context.Context, token, slug string) ([]TeamMember, error)

TeamMembers reads one team's members and which of them maintain it.

func (Org) Teams

func (o Org) Teams(ctx context.Context, token string) ([]Team, error)

Teams reads the organisation's teams.

type Plan

type Plan struct {
	// Seats is how many seats are paid for.
	Seats int
	// Filled is how many are taken: members, and outside collaborators on
	// private repositories.
	Filled int
}

Plan is what the organisation pays for, in seats.

type Registration

type Registration struct {
	ID    int64
	Slug  string
	Owner string
	// PEM is the App's private key. GitHub hands it over exactly once,
	// here; losing it means reconnecting.
	PEM     string
	HTMLURL string
	// ClientID and ClientSecret are what a person's authorization of the
	// App is redeemed and refreshed with. An organisation's App ignores
	// them.
	ClientID     string
	ClientSecret string
}

Registration is what creating an App returns, minus what nothing here keeps.

func Convert

func Convert(ctx context.Context, client *http.Client, code string) (Registration, error)

Convert exchanges the code GitHub returns after Create for the App's credentials. The code is the only authority the request carries and it is single-use, so a replay finds nothing.

type StatusError

type StatusError struct {
	Code int
	// contains filtered or unexported fields
}

StatusError is GitHub answering with a status other than the one a call expected, in GitHub's own words.

func (*StatusError) Error

func (e *StatusError) Error() string

type Team

type Team struct {
	ID   int64
	Slug string
}

Team is one team, by the slug the policy names it with.

type TeamMember

type TeamMember struct {
	Login      string
	Maintainer bool
}

TeamMember is one member of a team, in one of GitHub's two roles.

type User

type User struct {
	ID    int64
	Login string
}

User is a GitHub account. The id is what a link is kept by: a login can be renamed, and a renamed login can be taken by somebody else.

func CurrentUser

func CurrentUser(ctx context.Context, client *http.Client, token string) (User, error)

CurrentUser is the account a person's token belongs to.

type UserTokens

type UserTokens struct {
	AccessToken   string
	AccessExpires time.Time
	// RefreshToken is empty for an App whose tokens do not expire, and
	// then AccessExpires is zero too.
	RefreshToken   string
	RefreshExpires time.Time
}

UserTokens are what a person's authorization of the link App leaves: a token to read with, and one to renew it by.

GitHub rotates the refresh token on every use — the old refresh token and the old access token stop working the moment a new pair is issued — so a pair that was issued and not kept is a link that can never be checked again.

func ExchangeCode

func ExchangeCode(ctx context.Context, client *http.Client, clientID, secret, code, redirect string, now time.Time) (UserTokens, error)

ExchangeCode redeems the code a person's authorization returned.

func RefreshUserTokens

func RefreshUserTokens(ctx context.Context, client *http.Client, clientID, secret, refresh string, now time.Time) (UserTokens, error)

RefreshUserTokens trades a refresh token for a new pair. The pair it was called with is dead once this returns successfully.

Directories

Path Synopsis
Package catalogue is GitHub Apps declared as data: what each App is called, which organisation owns it, what it may do, and — for a later exchange that mints installation tokens — which groups may ask for how much of it.
Package catalogue is GitHub Apps declared as data: what each App is called, which organisation owns it, what it may do, and — for a later exchange that mints installation tokens — which groups may ask for how much of it.
Package githubfake is one GitHub organisation, in memory, answering the calls the controller makes and changing as they change it.
Package githubfake is one GitHub organisation, in memory, answering the calls the controller makes and changing as they change it.
Package mints keeps the last installation tokens asked of each GitHub App, so that an App's page can show them.
Package mints keeps the last installation tokens asked of each GitHub App, so that an App's page can show them.

Jump to

Keyboard shortcuts

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