Documentation
¶
Index ¶
- Constants
- Variables
- func HostTrusted(rawURL, trustedBaseURL string, opts ...TrustOption) bool
- func Register(sourceType string, factory ProviderFactory) error
- func Registered(sourceType string) bool
- func RegisteredTypes() []string
- func ResolveToken(cfg TokenConfig, fallbackEnv string) string
- func ResolveTokenContext(ctx context.Context, cfg TokenConfig, fallbackEnv string) string
- func Unregister(sourceType string) bool
- type AuthConfig
- type Authenticator
- type ChecksumProvider
- type Config
- type DeviceCode
- type KeyManager
- type Prompter
- type Provider
- type ProviderFactory
- type Release
- type ReleaseAsset
- type ReleaseSourceConfig
- type SignatureProvider
- type TokenConfig
- type TrustOption
Examples ¶
Constants ¶
const ( SourceTypeGitHub = "github" SourceTypeGitLab = "gitlab" SourceTypeBitbucket = "bitbucket" SourceTypeGitea = "gitea" SourceTypeCodeberg = "codeberg" SourceTypeDirect = "direct" )
SourceType constants name the first-party release providers, as a convenience for callers configuring one.
They are NOT a closed set, and declaring one here does not register it: every provider except direct ships as its own module and registers itself when blank-imported. The registry accepts any string, so a provider this module has never heard of works exactly as well — which is the point. Downstream consumers may declare their own constants.
const ( // DefaultMaxChecksumsSize bounds a checksums manifest. A GoReleaser // manifest for a typical multi-OS release is ~1 KiB, so 1 MiB is roughly // 1000x headroom while still capping a hostile stream. DefaultMaxChecksumsSize int64 = 1 << 20 // DefaultMaxSignatureSize bounds a detached signature. An OpenPGP // signature over a manifest is a few hundred bytes; 1 MiB is generous. DefaultMaxSignatureSize int64 = 1 << 20 )
Size bounds for the optional capability interfaces.
These are DEFAULTS, not mandates. The caller passes the bound it wants to ChecksumProvider.DownloadChecksumManifest and SignatureProvider.DownloadSignature, and the provider enforces whatever it receives — so a tool shipping unusually large artefacts can raise the ceiling without every provider needing a configuration knob of its own.
The split is deliberate: the caller owns the *policy*, the provider owns the *enforcement*. Making these constants here and mandatory in the providers would move policy to the wrong side of the boundary.
Variables ¶
var ( // ErrProviderNotFound is returned by Lookup when no factory has been // registered for the requested source type. ErrProviderNotFound = errors.New("no release provider registered for source type") // ErrNotSupported is returned by provider methods that are not applicable // for the underlying platform (e.g. ListReleases on Bitbucket). ErrNotSupported = errors.New("operation not supported by this release provider") // ErrReleaseNotFound is returned when a requested release — by tag, or the // latest — does not exist. // // Not yet honoured uniformly: the forgetest double returns it, but the // first-party providers still surface their platform's own not-found error. // Treat it as the sentinel to implement in a NEW provider, and do not rely // on errors.Is(err, ErrReleaseNotFound) across the first-party set until // they are aligned. ErrReleaseNotFound = errors.New("release not found") )
var ErrAlreadyRegistered = errors.New("a provider is already registered for this source type")
ErrAlreadyRegistered is returned by Register when a factory is already registered for the requested source type.
Functions ¶
func HostTrusted ¶
func HostTrusted(rawURL, trustedBaseURL string, opts ...TrustOption) bool
HostTrusted reports whether rawURL points at the same host as trustedBaseURL, and is safe to attach a credential to.
Why this exists ¶
Release metadata is author-controlled. An asset's download URL comes back from the forge API as data, and on most platforms a release author can set it to any URL at all. A provider that attaches its API token to whatever URL the metadata names will happily send that token to an attacker's server: the victim only has to install a tool whose release was published, or edited, by someone hostile.
So a provider must pin the credential to the host it authenticated against. This is that check, and it fails closed — an unparseable URL, a missing host, or any mismatch returns false, because the cost of a false negative is an unauthenticated request that may 404, while the cost of a false positive is a leaked credential.
What counts as trusted ¶
Both the host (including port) and the scheme must match:
- Host comparison is case-insensitive, since DNS names are. Ports are part of the comparison: a different port is a different service.
- The scheme must match too, which is stricter than comparing hosts alone. Without it, a base of https://git.example.com would trust http://git.example.com — and the credential would go out in plaintext to a host an attacker on the network path can impersonate. A downgrade is exactly what an attacker who can rewrite release metadata would choose.
Usage is the same shape in every provider:
if p.token != "" && forge.HostTrusted(downloadURL, p.baseURL) {
req.Header.Set("Authorization", "token "+p.token)
}
Strictness is the default, deliberately. Where a deployment genuinely needs more — assets on a separate storage domain, or a lab install with no TLS — WithAdditionalHosts and WithInsecureSchemeDowngrade widen it explicitly, at the call site, where the decision is visible and greppable.
Note what this does NOT do: it does not stop the download. An asset hosted elsewhere is still fetched, just without the credential attached — which is the correct behaviour for a public asset on a CDN.
func Register ¶
func Register(sourceType string, factory ProviderFactory) error
Register associates a source type string with a ProviderFactory. Safe to call concurrently.
It returns ErrAlreadyRegistered rather than overwriting. Silently overwriting would let a blank-imported provider module displace another with no diagnostic, with initialisation order deciding the winner — so the failure would surface later, as the wrong provider being used, far from its cause.
Returning an error rather than panicking keeps the decision with the caller. A provider module registering from init() has nothing sensible to do with a failure and should panic at its own call site, where the panic names the module at fault:
func init() {
if err := forge.Register("mycorp-git", newProvider); err != nil {
panic("myforge: " + err.Error())
}
}
Code building a registry programmatically — a plugin host, a test, a tool choosing providers at runtime — can instead handle the error. That case is the reason this does not panic on your behalf.
To register conditionally, consult Registered. To replace a provider deliberately, call Unregister first.
func Registered ¶
Registered reports whether a factory is registered for sourceType. Use it to register conditionally without tripping Register's duplicate panic.
func RegisteredTypes ¶
func RegisteredTypes() []string
RegisteredTypes returns a sorted slice of all currently registered source type strings. Used for generating user-facing error messages.
func ResolveToken ¶
func ResolveToken(cfg TokenConfig, fallbackEnv string) string
ResolveToken resolves an authentication token from a config subtree.
Resolution order:
- cfg.auth.env — NAME of an environment variable to read
- cfg.auth.keychain — "<service>/<account>" reference in the OS keychain; active only when the process has imported pkg/credentials/keychain (or otherwise registered a backend). Silently skipped when no backend is registered or when the keychain is unreachable at the time of the call.
- cfg.auth.value — literal token value stored in config. Viper's AutomaticEnv + tool prefix also surfaces <TOOL>_AUTH_VALUE style env vars via this step.
- fallbackEnv — well-known unprefixed environment variable (pass "" to skip).
Returns an empty string when no token is found; callers decide whether that is an error condition (e.g. private repositories require a token, public repositories can proceed without one).
The callers-supplied `cfg` is typically `props.Config.Sub("<vcs>")` — GTB's env-aware Sub ensures prefix-aware env binding fires even inside sub-containers (see pkg/config).
ResolveToken uses context.Background when calling the keychain backend. Callers who have a request context (HTTP handler, cobra command, chat provider) SHOULD use ResolveTokenContext instead so a slow remote backend (Vault, SSM) cannot stall the caller beyond its own deadline.
func ResolveTokenContext ¶
func ResolveTokenContext(ctx context.Context, cfg TokenConfig, fallbackEnv string) string
ResolveTokenContext is the context-aware form of ResolveToken. The context is propagated to the credentials backend so remote stores honour deadlines and cancellation. Recommended for any call path that already has a context in scope.
func Unregister ¶
Unregister removes the factory registered for sourceType, reporting whether one was present. It exists so that deliberately replacing a built-in provider stays possible now that Register refuses to overwrite silently — the intent has to be stated rather than assumed.
Like Register, this mutates process-wide state: call it during start-up, not from a test that runs under t.Parallel().
Types ¶
type AuthConfig ¶
type AuthConfig struct {
Env string `mapstructure:"env" json:"env" yaml:"env"`
Value string `mapstructure:"value" json:"value" yaml:"value"`
Keychain string `mapstructure:"keychain" json:"keychain" yaml:"keychain"`
}
AuthConfig is the typed auth shape shared by VCS providers. It still satisfies TokenConfig so existing token-resolution semantics are reused.
func (AuthConfig) GetString ¶
func (cfg AuthConfig) GetString(key string) string
type Authenticator ¶ added in v0.2.0
type Authenticator interface {
// Login runs the provider's interactive authentication flow, surfacing any
// user-facing step through p, and returns a usable API token on success. It
// blocks until the user completes the flow, ctx is cancelled, or the flow's own
// deadline (e.g. the device-code expiry) elapses.
//
// Returns [ErrNotSupported] when interactive login is unavailable in the
// provider's current configuration; the caller then falls back to manual entry.
Login(ctx context.Context, p Prompter) (token string, err error)
}
Authenticator is an OPTIONAL capability implemented by providers that support an interactive login flow (e.g. an OAuth device flow) yielding an API token.
Providers that only accept a pre-issued token or app password do not implement it — or implement it and return ErrNotSupported when their current configuration cannot perform an interactive login. The caller treats both cases identically: fall back to manual token/app-password entry.
Example ¶
ExampleAuthenticator shows the caller-side capability-detection pattern: use the interactive login when the provider offers it, otherwise fall back to a manual token.
package main
import (
"context"
"fmt"
"time"
"gitlab.com/phpboyscout/go/forge"
)
// capturePrompter records the DeviceCode it was shown, standing in for a CLI.
type capturePrompter struct {
dc forge.DeviceCode
shown bool
failWith error
}
func (c *capturePrompter) ShowDeviceCode(_ context.Context, dc forge.DeviceCode) error {
if c.failWith != nil {
return c.failWith
}
c.dc = dc
c.shown = true
return nil
}
// fakeProvider optionally implements Authenticator + KeyManager, to exercise the
// capability-detection contract a caller uses.
type fakeProvider struct {
token string
loginErr error
uploadErr error
uploaded []byte
}
func (f *fakeProvider) Login(ctx context.Context, p forge.Prompter) (string, error) {
if err := p.ShowDeviceCode(ctx, forge.DeviceCode{
UserCode: "WDJB-MJHT", VerificationURI: "https://example.test/device", ExpiresIn: 15 * time.Minute,
}); err != nil {
return "", err
}
return f.token, f.loginErr
}
func (f *fakeProvider) UploadKey(_ context.Context, _ string, publicKey []byte) error {
f.uploaded = publicKey
return f.uploadErr
}
func main() {
var provider any = &fakeProvider{token: "gho_example"}
if auth, ok := provider.(forge.Authenticator); ok {
tok, err := auth.Login(context.Background(), &capturePrompter{})
if err == nil {
fmt.Println("logged in, token:", tok)
}
} else {
fmt.Println("provider has no interactive login; prompt for a token")
}
}
Output: logged in, token: gho_example
type ChecksumProvider ¶
type ChecksumProvider interface {
// DownloadChecksumManifest returns the raw bytes of the checksums
// manifest for the given release.
//
// The implementation MUST enforce maxBytes and return an error when the
// response exceeds it, so a hostile server cannot stream indefinitely.
// Enforcement belongs here, at the trust boundary, not in the caller: a
// provider reaching a URL of its own composition is the only code that
// sees the response before it is buffered. [DefaultMaxChecksumsSize] is
// a reasonable bound, but the CALLER chooses the value and passes it —
// tools shipping unusually large artefacts legitimately raise it.
//
// Returns [ErrNotSupported] when the provider is configured in a
// way that disables checksum retrieval (e.g. the Direct
// provider's `checksum_url_template` param is empty). The caller
// treats [ErrNotSupported] exactly like "provider does not
// implement this interface" — fall back to asset-by-name lookup
// if one is available, otherwise respect the require_checksum
// policy.
DownloadChecksumManifest(ctx context.Context, rel Release, maxBytes int64) ([]byte, error)
}
ChecksumProvider is an OPTIONAL interface implemented by release providers that can fetch a checksums manifest by means other than a standard release-asset download. The canonical case is the Direct provider's `checksum_url_template` param, which composes a URL from a template that may point outside the release-asset listing entirely.
The update flow does a type assertion at runtime — providers that do not implement this interface fall back to the default behaviour of locating `checksums.txt` by filename within the release's asset list. This keeps third-party Provider implementations source-compatible: they gain the feature by opting in, not by implementing a new required method.
type DeviceCode ¶ added in v0.2.0
type DeviceCode struct {
// UserCode is the short code the user types at VerificationURI.
UserCode string
// VerificationURI is where the user enters UserCode to authorize the request.
VerificationURI string
// VerificationURIComplete, when non-empty, is a URL that pre-fills the code
// (some providers supply it so the user need not type UserCode).
VerificationURIComplete string
// ExpiresIn is how long UserCode remains valid.
ExpiresIn time.Duration
}
DeviceCode carries the user-facing details of a device-authorization prompt — the "open this URL and enter this code" step of an OAuth device flow.
type KeyManager ¶ added in v0.2.0
type KeyManager interface {
// UploadKey registers publicKey — an OpenSSH-format public key ("ssh-ed25519
// AAAA… comment") — under the given human-readable name on the authenticated
// account. The provider resolves its own credentials (as for the release
// [Provider]); name is a label the user will see in the account's key list.
UploadKey(ctx context.Context, name string, publicKey []byte) error
}
KeyManager is an OPTIONAL capability implemented by providers that can register an SSH public key on the authenticated account through their API.
Returns ErrNotSupported when the provider exposes no key API or it is disabled; the caller then skips automated upload and instructs the user to add the key manually.
type Prompter ¶ added in v0.2.0
type Prompter interface {
// ShowDeviceCode presents a device-authorization prompt to the user — typically
// "open dc.VerificationURI and enter dc.UserCode". It returns once the prompt has
// been shown; the [Authenticator] then polls the provider for completion. A
// non-nil error aborts the login.
ShowDeviceCode(ctx context.Context, dc DeviceCode) error
}
Prompter lets a provider surface an interactive authentication prompt without depending on any UI toolkit — the caller (typically a CLI) renders it. This is how forge offers a device flow while staying framework-free.
type Provider ¶
type Provider interface {
GetLatestRelease(ctx context.Context, owner, repo string) (Release, error)
GetReleaseByTag(ctx context.Context, owner, repo, tag string) (Release, error)
ListReleases(ctx context.Context, owner, repo string, limit int) ([]Release, error)
// DownloadReleaseAsset opens a stream of the asset's bytes.
//
// The second return value is a REDIRECT URL, and it is a security
// boundary rather than a convenience. Some APIs answer an asset request
// with a redirect to storage the client is expected to follow itself
// (this is go-github's behaviour, which surfaces the location instead of
// following it). A provider that receives such a redirect and does NOT
// follow it returns the location here, with a nil or empty reader.
//
// Callers are expected to REFUSE a non-empty redirect rather than follow
// it: following one would fetch bytes from a host the caller never
// vetted, defeating the point of pinning the API host. Return "" whenever
// the body is served directly, which is the common case — three of the
// four first-party providers always do.
DownloadReleaseAsset(ctx context.Context, owner, repo string, asset ReleaseAsset) (io.ReadCloser, string, error)
}
Provider defines the operations a release backend must support.
Implement every method. Where a platform has no equivalent concept, return an error wrapping ErrNotSupported rather than a nil result — see ChecksumProvider for how callers treat that sentinel.
type ProviderFactory ¶
type ProviderFactory func(source ReleaseSourceConfig, cfg Config) (Provider, error)
ProviderFactory is a function that constructs a forge.Provider from a ReleaseSourceConfig and an optional configuration reader.
func Lookup ¶
func Lookup(sourceType string) (ProviderFactory, error)
Lookup returns the ProviderFactory registered for the given source type. Returns ErrProviderNotFound if no factory has been registered for that type.
type Release ¶
type Release interface {
GetName() string
GetTagName() string
GetBody() string
GetDraft() bool
GetAssets() []ReleaseAsset
}
Release defines the common abstraction for a software forge.
type ReleaseAsset ¶
ReleaseAsset defines the common abstraction for a release asset.
type ReleaseSourceConfig ¶
type ReleaseSourceConfig struct {
Type string
Host string
Owner string
Repo string
Private bool
// Params holds provider-specific configuration key/value pairs.
// Keys use snake_case. Valid keys are documented per provider.
Params map[string]string
}
ReleaseSourceConfig carries the information a ProviderFactory needs to construct its client. It is populated from props.ReleaseSource, and exists as a separate type to avoid a circular import between pkg/vcs/release and pkg/props.
type SignatureProvider ¶
type SignatureProvider interface {
// DownloadSignature returns the raw bytes of the detached
// signature over the checksums manifest for the given release.
//
// As with [ChecksumProvider.DownloadChecksumManifest], the
// implementation MUST enforce maxBytes. The two are deliberately
// symmetric: an implementer should never have to remember which of the
// two capped for them. [DefaultMaxSignatureSize] is a reasonable bound,
// chosen by the caller.
//
// Returns [ErrNotSupported] when the provider is configured in a
// way that disables signature retrieval (e.g. the Direct
// provider's `signature_url_template` param is empty, or no
// signature file was uploaded to Bitbucket). The caller treats
// [ErrNotSupported] exactly like "provider does not implement this
// interface" — fall back to asset-by-name lookup if one is
// available, otherwise respect the require_signature policy.
DownloadSignature(ctx context.Context, rel Release, maxBytes int64) ([]byte, error)
}
SignatureProvider is an OPTIONAL interface implemented by release providers that can fetch a detached signature over the checksums manifest by means other than a standard release-asset download. It mirrors ChecksumProvider exactly: the Direct provider composes a URL from its `signature_url_template` param, and Bitbucket locates an uploaded `checksums.txt.sig` by exact filename in the downloads list.
The update flow type-asserts at runtime — providers that do not implement this interface fall back to locating the signature asset by filename within the release's asset list (the GitHub/GitLab path). This keeps third-party Provider implementations source- compatible: they gain signature support by opting in, not by implementing a new required method.
type TokenConfig ¶
TokenConfig is the minimal string lookup surface needed for VCS token resolution.
type TrustOption ¶
type TrustOption func(*trustPolicy)
TrustOption relaxes HostTrusted. Every option widens what is trusted with a credential attached, so each one is a deliberate, greppable decision rather than a default.
func WithAdditionalHosts ¶
func WithAdditionalHosts(hosts ...string) TrustOption
WithAdditionalHosts trusts hosts besides the API host with the credential.
The case this exists for is real: some deployments serve release assets from a separate storage domain that still requires the API credential — a self-hosted instance fronting an object store, or an enterprise install with a dedicated assets host. Without this they would have to choose between a failing download and no pinning at all.
Each host is matched exactly, case-insensitively, including port. It is never a suffix match: a suffix test is how host pinning is usually defeated, since "git.example.com" would then trust "git.example.com.attacker.net".
List only hosts you control. Every entry is somewhere your credential may be sent, and the point of pinning is that this list is short and deliberate.
func WithInsecureSchemeDowngrade ¶
func WithInsecureSchemeDowngrade() TrustOption
WithInsecureSchemeDowngrade permits attaching the credential when the target uses a different scheme from the base — in practice, plaintext HTTP against an HTTPS API.
It is named to be uncomfortable because it should be. A downgrade puts the credential on the wire in cleartext, and it is exactly what an attacker able to rewrite release metadata would choose. There is one defensible use: an air-gapped or lab deployment that genuinely has no TLS, where the operator accepts that the credential is not confidential in transit.
If you are reaching for this against an internet-facing forge, the answer is TLS, not this option.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package direct provides a forge.Provider implementation for tools distributed via arbitrary HTTP servers.
|
Package direct provides a forge.Provider implementation for tools distributed via arbitrary HTTP servers. |
|
Package releasetest provides an in-memory forge.Provider test double and asset builders for exercising the self-update pipeline without network or disk access.
|
Package releasetest provides an in-memory forge.Provider test double and asset builders for exercising the self-update pipeline without network or disk access. |