githubid

package
v0.8.74 Latest Latest
Warning

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

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package githubid manages the daemon's set of GitHub identities: named (login, host, token) triples that islands clone and push as. The daemon is the credential owner, so islands work from any client device — including ones with no gh of their own. A client that does have gh can seed the store with `dejima auth push --github`. Each island selects one identity (or the default) at create time; the daemon materializes just that identity into the island's gh config (see HostsYAML), never the whole set.

Index

Constants

View Source
const DefaultHost = "github.com"

DefaultHost is the GitHub host assumed when an identity doesn't name one.

Variables

This section is empty.

Functions

func ConfigYAML

func ConfigYAML() string

ConfigYAML renders the gh config.yml that accompanies HostsYAML. Its only job is to carry the schema version marker: with it present, gh treats the materialized config as already-migrated and never tries to write to the read-only GH_CONFIG_DIR mount. Without it, gh runs a migration on first use and fails on the read-only dir (see HostsYAML).

func GitAuthor

func GitAuthor(id Identity) (name, email string)

GitAuthor derives the git commit author (name, email) for an island that acts as this identity. Without it, commits inherit the host's gitconfig user.* — so a push authenticated as "work" gets authored with whatever email the daemon host happens to have, which GitHub then misattributes (it keys attribution off the email). We use GitHub's privacy-preserving noreply email so commits attribute to the right account without exposing a real address:

  • canonical form (preferred): "<id>+<login>@users.noreply.<host>"
  • fallback when the numeric id is unknown (identity stored before id capture): "<login>@users.noreply.<host>" — still account-linked, just the older form.

func GitConfig

func GitConfig(id Identity) string

GitConfig renders a minimal gitconfig carrying just the identity's commit author. The daemon mounts this at /opt/host/gitconfig for identity-scoped islands (in place of the host's own gitconfig), and the entrypoint applies user.name/user.email from it — so authorship matches the push credential.

func HostsYAML

func HostsYAML(id Identity) string

HostsYAML renders the gh hosts.yml for a single identity — what gh reads from GH_CONFIG_DIR to authenticate git over HTTPS inside an island. Only ever one identity per file.

It MUST emit the modern multi-account schema (the per-user `users:` map), not just the legacy top-level form. gh migrates a legacy-only hosts.yml to this schema on first use, which means writing back to GH_CONFIG_DIR — but the daemon mounts that dir read-only, so the migration fails and `gh auth setup-git` errors out, leaving the island with no git credential helper (the clone then can't authenticate). Materializing the already-migrated form means gh has nothing to write. See also ConfigYAML, which supplies the version marker gh checks before deciding to migrate.

func ScopeNote added in v0.8.74

func ScopeNote(scopes string) (note string, canWrite bool)

ScopeNote explains, in one line, what a token's scopes mean for the work an island does — or says plainly that it cannot tell.

Three states, and collapsing any two of them is how this went wrong:

""            a FINE-GRAINED token. GitHub sends no X-OAuth-Scopes header for
              these; permissions are per-repository and not visible from
              /user at all. "Unknown" is the honest answer, NOT "no scopes".
has "repo"    a classic token that can clone, push, and open pull requests.
otherwise     a classic token that authenticates and cannot write. This is
              the state that produced "Resource not accessible by personal
              access token" inside an island, hours after `github connect`
              reported success — because authenticating and being ABLE TO DO
              THE WORK are different questions and only the first was asked.

func ValidateName added in v0.8.17

func ValidateName(name string) error

ValidateName rejects an unusable identity name before it's stored.

func VerifyToken

func VerifyToken(ctx context.Context, host, token string) (login string, id int64, scopes string, err error)

VerifyToken confirms a token authenticates against host and returns the login, numeric user id, and SCOPES it carries. Called before storing an identity so a bad or expired token fails fast at `auth push` time instead of silently at clone/push time inside an island. The id feeds the canonical noreply commit email (see GitAuthor).

scopes comes from the X-OAuth-Scopes header GitHub returns on this very call — it was always in the response and always discarded. Without it "your token authenticates" was the strongest thing any surface could say, so a token that could clone and push but NOT open a pull request looked identical to a working one until an agent hit the wall: "Resource not accessible by personal access token", naming nothing that could be acted on.

Types

type Identity

type Identity struct {
	Name  string `json:"name"`         // dejima-local handle: "work", "personal", … (unique per Owner)
	Login string `json:"login"`        // GitHub username
	ID    int64  `json:"id,omitempty"` // GitHub numeric user id (for the canonical noreply commit email); 0 if unknown
	Host  string `json:"host"`         // "github.com" or an enterprise host
	Token string `json:"token"`        // OAuth/PAT token — secret, never returned to clients
	// Owner is the tenant that owns this identity ("" = a legacy/host identity).
	// Server-authoritative: set from the authenticated caller, never client-forged.
	// An identity is only ever materialized into islands of the same Owner (plus
	// host-Shared identities into any island) — the containment invariant.
	Owner string `json:"owner,omitempty"`
	// Shared marks a HOST identity as deliberately usable by every tenant's islands
	// (a team-wide org credential). Only the host owner may set it. Ignored on a
	// non-host identity.
	Shared bool `json:"shared,omitempty"`
	// UpdatedAt is when this identity's TOKEN was last written. It exists because
	// two identities for the same GitHub login are indistinguishable in a listing
	// — same name shape, same login, same host — and the only thing that separates
	// a live one from a month-dead one is when it was last refreshed. An operator
	// spent an incident looking at exactly that pair. Zero for identities that
	// predate this field (migrated legacy entries); render it as unknown, never as
	// the epoch, and never as "just now".
	UpdatedAt time.Time `json:"updated_at,omitzero"`
	// Scopes is the X-OAuth-Scopes GitHub returned when this token was verified.
	// Empty for a fine-grained token, which sends no such header — see ScopeNote.
	Scopes string `json:"scopes,omitempty"`
}

Identity is one GitHub login the daemon can act as. Owner scopes it to a tenant so a team member can self-serve credentials for their OWN islands without the host owner having to hold a token for the member's private repos.

type Meta

type Meta struct {
	Name    string `json:"name"`
	Login   string `json:"login"`
	Host    string `json:"host"`
	Default bool   `json:"default"`
	Owner   string `json:"owner,omitempty"`
	Shared  bool   `json:"shared,omitempty"`
	// UpdatedAt mirrors Identity.UpdatedAt — safe to publish (it is not the
	// token, only when the token was last written) and it is the field that
	// tells two same-login identities apart.
	UpdatedAt time.Time `json:"updated_at,omitzero"`
	// Scopes mirrors Identity.Scopes. Safe to publish: it describes what the token
	// may do, never the token.
	Scopes string `json:"scopes,omitempty"`
}

Meta is an identity without its token: the safe view to hand back to clients.

type Repo

type Repo struct {
	NameWithOwner string `json:"name_with_owner"`
	URL           string `json:"url"` // https clone URL
	Description   string `json:"description"`
	Private       bool   `json:"private"`
}

Repo is one repository visible to an identity, in the shape the island creator consumes: a full owner/name and the https clone URL.

type RepoList

type RepoList struct {
	Repos  []Repo `json:"repos"`
	Capped bool   `json:"capped"`
}

RepoList is a page of repositories plus whether the identity can see more than the page returned (the GitHub API advertises a next page via a Link header). Capped lets the UI say "showing the first N" honestly.

func ListRepos

func ListRepos(ctx context.Context, id Identity, limit int) (RepoList, error)

ListRepos returns repositories the identity can access (owner, collaborator, or org member), most-recently-pushed first, capped at limit (default/max 100, a single API page). The daemon owns this call so any client device — even one without gh — can browse before an island exists.

type Store

type Store struct {
	Default    string              `json:"default,omitempty"`    // host owner's default identity name
	Defaults   map[string]string   `json:"defaults,omitempty"`   // tenant owner → default identity name
	Idents     []Identity          `json:"idents,omitempty"`     // owner-scoped identities
	Identities map[string]Identity `json:"identities,omitempty"` // LEGACY map (bare-name keyed); migrated → Idents on load
}

Store is the per-daemon identity set. Identities are owner-scoped (a flat list, unique by (Owner, Name)). Default is the HOST owner's default name; Defaults holds each non-host tenant's default. The legacy Identities map is read on load and migrated into Idents (as host/"" identities), then dropped on save.

func Load

func Load() (*Store, error)

Load reads the store under the lock — a consistent snapshot for read-only use. Returns an empty (non-nil) store if none exists yet.

func Update

func Update(fn func(*Store) error) (*Store, error)

Update runs fn against the store under a process-wide lock and persists the result atomically. Use it for every read-modify-write — Put/Remove/SetDefault — so concurrent writers can't clobber each other (lost updates).

func (*Store) DefaultFor added in v0.8.17

func (s *Store) DefaultFor(owner string) string

DefaultFor returns owner's default identity name ("" if none). The host owner's default lives in Default (legacy-compatible); tenants' in Defaults.

func (*Store) DeleteOwned added in v0.8.17

func (s *Store) DeleteOwned(owner, name string) bool

DeleteOwned removes the (owner, name) identity, repointing that owner's default to a remaining identity of theirs (or clearing it).

func (*Store) Find added in v0.8.17

func (s *Store) Find(owner, name string) (Identity, bool)

Find returns the (owner, name) identity (with token) — for handlers that need to check existence/ownership before a write.

func (*Store) List

func (s *Store) List() []Meta

List returns host (+ host-shared) identity metadata.

func (*Store) ListForOwner added in v0.8.17

func (s *Store) ListForOwner(owner string, ownsAll bool) []Meta

ListForOwner returns the identity metadata (no tokens) visible to owner ("" = host): their own identities, plus host-Shared ones. ownsAll (the host owner) sees everything.

func (*Store) Put

func (s *Store) Put(id Identity)

Put adds/updates a host identity (its Owner is used as-is; the zero value "" is the host tenant).

func (*Store) PutOwned added in v0.8.17

func (s *Store) PutOwned(id Identity)

PutOwned adds or updates the identity keyed by (Owner, Name). id.Owner must be set by the caller (server-authoritative). The first identity added for an owner becomes that owner's default.

func (*Store) Remove

func (s *Store) Remove(name string) bool

Remove deletes a host identity.

func (*Store) Resolve

func (s *Store) Resolve(name string) (Identity, bool)

Resolve resolves a host identity by name (or the host default when empty).

func (*Store) ResolveForIsland added in v0.8.17

func (s *Store) ResolveForIsland(islandOwner, name string) (Identity, bool)

ResolveForIsland picks the identity to materialize into an island owned by islandOwner ("" = a host island), requesting name (or the owner's default when empty). THE CONTAINMENT CHOKEPOINT: a host island may use any host identity; a tenant island may use only its OWN identities or a host identity marked Shared. An operator's token can never reach another tenant's island.

func (*Store) Save

func (s *Store) Save() error

Save persists the store atomically at 0600 (it holds tokens).

func (*Store) SetDefault

func (s *Store) SetDefault(name string) error

SetDefault sets the host default identity.

func (*Store) SetDefaultFor added in v0.8.17

func (s *Store) SetDefaultFor(owner, name string) error

SetDefaultFor marks (owner, name) as owner's default. Errors if owner has no identity by that name.

Jump to

Keyboard shortcuts

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