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: nothing here receives a webhook, and an App that has one delivered elsewhere gets a secret this service generated itself and sets with PatchHookConfig. A conversion's client secret is kept only for the link App, which is the one App people authorize.
Index ¶
- Constants
- Variables
- func AppToken(appID int64, privateKey string, now time.Time) (string, error)
- func AuthorizeURL(clientID, redirect, state string) string
- func CheckUserToken(ctx context.Context, client *http.Client, clientID, secret, token string) (bool, error)
- func CreateURL(org, state string) string
- func DeleteInstallation(ctx context.Context, client *http.Client, appToken string, installation int64) error
- func FindInstallation(ctx context.Context, client *http.Client, appToken, org string) (int64, error)
- func FormatID(id int64) string
- func InstallURL(slug, state string) string
- func InstallationToken(ctx context.Context, client *http.Client, appToken string, installation int64) (string, time.Time, error)
- func LinkApp(owner string) catalogue.App
- func LinkAppNamed(owner, configured string) catalogue.App
- func OrganisationApp(org string) catalogue.App
- func OrganisationAppNamed(org, configured string) catalogue.App
- func Ping(ctx context.Context, client *http.Client, target, secret string) error
- func PublicEmail(ctx context.Context, client *http.Client, token, login string) (string, error)
- func Redeliver(ctx context.Context, client *http.Client, appToken string, delivery int64) error
- func RunnerApp(org, tier string) catalogue.App
- func Sign(secret string, body []byte) string
- func UserID(ctx context.Context, client *http.Client, token, login string) (int64, error)
- func VerifiedEmails(ctx context.Context, client *http.Client, token string) ([]string, error)
- type AppInfo
- type Delivery
- type FailedInvitation
- type HookAttributes
- type HookConfig
- type HookUpdate
- type InstallationInfo
- type Invitation
- type Manifest
- func ManifestFor(app catalogue.App, homepage, redirect, setup string, callbacks []string) Manifest
- func NewLinkManifest(owner, homepage, redirect, callback string) Manifest
- func NewLinkManifestNamed(owner, name, homepage, redirect, callback string) Manifest
- func NewManifest(org, homepage, redirect, setup string) Manifest
- func NewManifestNamed(org, name, homepage, redirect, setup string) Manifest
- func NewRunnerManifest(org, tier, homepage, redirect, setup string) Manifest
- type Member
- type MintedToken
- type Narrowing
- type Org
- func (o Org) FailedInvitations(ctx context.Context, token string) ([]FailedInvitation, error)
- func (o Org) Invitations(ctx context.Context, token string) ([]Invitation, error)
- func (o Org) Invite(ctx context.Context, token, email string, teams []int64) error
- func (o Org) InviteUser(ctx context.Context, token string, id int64, teams []int64) error
- func (o Org) IsMember(ctx context.Context, token, login string) (bool, error)
- func (o Org) Members(ctx context.Context, token string) ([]Member, error)
- func (o Org) OutsideCollaborators(ctx context.Context, token string) ([]string, error)
- func (o Org) Plan(ctx context.Context, token string) (Plan, bool, error)
- func (o Org) RemoveFromOrg(ctx context.Context, token, login string) error
- func (o Org) RemoveFromTeam(ctx context.Context, token, slug, login string) error
- func (o Org) SetTeamRole(ctx context.Context, token, slug, login string, maintainer bool) error
- func (o Org) TeamMembers(ctx context.Context, token, slug string) ([]TeamMember, error)
- func (o Org) Teams(ctx context.Context, token string) ([]Team, error)
- type Plan
- type Registration
- type StatusError
- type Team
- type TeamMember
- type User
- type UserTokens
Constants ¶
const ContentTypeJSON = "json"
ContentTypeJSON is the only content type this service configures.
const Retries = 3
Retries is how many times a rate-limited call is retried.
Variables ¶
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.
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.
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.
var ErrInstallationGone = errors.New("github: the installation no longer exists")
ErrInstallationGone is an installation GitHub no longer has: somebody uninstalled the App on GitHub.
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.
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 ¶
AppToken is the short-lived JWT an App authenticates App-level calls with, signed by its private key.
func AuthorizeURL ¶
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 ¶
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 InstallURL ¶
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 ¶
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 LinkAppNamed ¶ added in v1.75.0
LinkAppNamed is LinkApp under the name an operator configured: used as given (the configuration holds it to GitHub's limit), and empty is the default, <owner>-access-roster-link cut to the limit.
func OrganisationApp ¶
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 OrganisationAppNamed ¶ added in v1.75.0
OrganisationAppNamed is OrganisationApp under the name an operator configured: used as given (the configuration holds it to GitHub's limit), and empty is the default, <org>-access-roster cut to the limit.
func Ping ¶ added in v1.74.0
Ping POSTs a signed `ping` to a webhook target, as GitHub does when a webhook is created, and returns nil only for a 2xx answer. It is how a rotation learns that a consumer already holds the new secret before GitHub is told to sign with it: a consumer that does not answers 401 or 403, and the error says what it answered.
func PublicEmail ¶
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 Redeliver ¶ added in v1.74.0
Redeliver asks GitHub to send a delivery again (`POST /app/hook/deliveries/{id}/attempts`, an App JWT). GitHub answers 202 and delivers asynchronously, to the URL and with the secret the App holds now.
func RunnerApp ¶
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 VerifiedEmails ¶
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.
type Delivery ¶ added in v1.74.0
type Delivery struct {
ID int64 `json:"id"`
GUID string `json:"guid"`
DeliveredAt time.Time `json:"delivered_at"`
Redelivery bool `json:"redelivery"`
Status string `json:"status"`
StatusCode int `json:"status_code"`
Event string `json:"event"`
Action string `json:"action"`
}
Delivery is one attempt GitHub made, or is making, to deliver an event.
type FailedInvitation ¶
FailedInvitation is an invitation that expired or was cancelled.
type HookAttributes ¶
HookAttributes is an App's webhook. It is active only for a catalogue App that declares where deliveries go; any other App polls, and GitHub still wants a URL beside an inactive webhook.
type HookConfig ¶ added in v1.74.0
type HookConfig struct {
URL string `json:"url"`
ContentType string `json:"content_type"`
// InsecureSSL is "0" (verify) or "1" (do not), a string on the wire.
InsecureSSL string `json:"insecure_ssl"`
// Secret is "********" when one is set, empty when none is.
Secret string `json:"secret,omitempty"`
}
HookConfig is an App's webhook as GitHub holds it. The secret is never returned: GitHub masks it, so it cannot be compared, only replaced.
func GetHookConfig ¶ added in v1.74.0
GetHookConfig reads the webhook configuration of the App the token is for (an App JWT).
func PatchHookConfig ¶ added in v1.74.0
func PatchHookConfig(ctx context.Context, client *http.Client, appToken string, update HookUpdate) (HookConfig, error)
PatchHookConfig sets the App's webhook URL, secret and content type (`PATCH /app/hook/config`, an App JWT). It does not switch the webhook on: that is decided when the App is created.
type HookUpdate ¶ added in v1.74.0
HookUpdate is what PatchHookConfig sets. Every field is sent: a rotation changes the secret and, for a Kargo receiver, the URL in the one call, so the two never disagree on GitHub's side.
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 ¶
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. They are
// delivered only if the webhook is active.
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 ¶
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 inactive unless the entry declares one: nothing here receives a delivery, so no endpoint of ours has to be reachable from GitHub. A declared webhook is active from the start, because GitHub has no API to switch an existing App's webhook on; its secret (and, for a Kargo receiver, its final URL) is set right after the conversion, by PatchHookConfig.
func NewLinkManifest ¶
NewLinkManifest is LinkApp's manifest: no setup callback, because it is installed nowhere, and the one callback a person's authorization returns to.
func NewLinkManifestNamed ¶ added in v1.75.0
NewLinkManifestNamed is NewLinkManifest with the configured name of the App (LinkAppNamed).
func NewManifest ¶
NewManifest is OrganisationApp's manifest: redirect and setup are the two callbacks, after Create and after Install.
func NewManifestNamed ¶ added in v1.75.0
NewManifestNamed is NewManifest with the configured name of the App (OrganisationAppNamed).
func NewRunnerManifest ¶
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 ¶
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 ¶
FailedInvitations reads the organisation's failed invitations.
func (Org) Invitations ¶
Invitations reads the organisation's pending invitations.
func (Org) Invite ¶
Invite invites an address into the organisation, straight into teams, so that accepting is the only step left.
func (Org) InviteUser ¶
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) OutsideCollaborators ¶
OutsideCollaborators reads who has access to the organisation's repositories without being a member of it.
func (Org) Plan ¶
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 ¶
RemoveFromOrg removes a member from the organisation, and so from every team in it.
func (Org) RemoveFromTeam ¶
RemoveFromTeam removes a member from one team.
func (Org) SetTeamRole ¶
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 ¶
TeamMembers reads one team's members and which of them maintain it.
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.
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 TeamMember ¶
TeamMember is one member of a team, in one of GitHub's two roles.
type User ¶
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.
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.
Source Files
¶
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 githubtokens is how another module asks the GitHub module for an installation token (docs/decisions/0071): a Minter port, the in-process implementation that holds the App's key and talks to GitHub, and the modcall pair (Register, Client) that carries the same call as the `github` module's `mint_installation` method.
|
Package githubtokens is how another module asks the GitHub module for an installation token (docs/decisions/0071): a Minter port, the in-process implementation that holds the App's key and talks to GitHub, and the modcall pair (Register, Client) that carries the same call as the `github` module's `mint_installation` method. |
|
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. |