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 console is the GitHub module's half of the console: the handlers for connecting an organisation, the Apps view, the catalogue and runner Apps, the link App and people's links, moved out of the shared console so that the process which serves the console holds no App private key, no webhook secret and no connection record (docs/decisions/0071, v1.76 D-v176-1 b).
|
Package console is the GitHub module's half of the console: the handlers for connecting an organisation, the Apps view, the catalogue and runner Apps, the link App and people's links, moved out of the shared console so that the process which serves the console holds no App private key, no webhook secret and no connection record (docs/decisions/0071, v1.76 D-v176-1 b). |
|
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. |