8ced48901f
Anonymous GitHub is capped at 60 requests/hour and cannot read private repositories, so a machine credential usable by a free (non-enterprise) account is needed to lift the request budget and reach private release assets. - Add internal/githubauth: a process-wide credential delivered via env/secret, applied by default to every outbound GitHub request. - Support two modes: a Personal Access Token sent as `Authorization: Bearer`, and a GitHub App that mints a short-lived RS256 JWT (stdlib crypto, no new dependency), exchanges it for a ~1h installation token, caches it, and single-flights a refresh a few minutes before expiry. - Inject the credential at the two GitHub call paths: the rpm github provider (releases scan + ranged asset-header GETs) and the generic byte proxy (private release-asset downloads for github.com hosts). - Honor precedence: a remote's own username/password overrides the server credential; no credential configured stays anonymous. - Fail closed at startup on partial App configuration; never persist the credential to the DB, return it from an API, or log it. - Read GITHUB_TOKEN / GITHUB_APP_ID / GITHUB_APP_INSTALLATION_ID / GITHUB_APP_PRIVATE_KEY[_PATH] via the existing getenv convention. - Document PAT vs App setup, the free-account fine-grained PAT scopes (Contents:read + Metadata:read), precedence, and the rate-limit implication.
115 lines
3.5 KiB
Go
115 lines
3.5 KiB
Go
package generic_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/githubauth"
|
|
"git.unkin.net/unkin/artifactapi/internal/provider"
|
|
"git.unkin.net/unkin/artifactapi/internal/provider/generic"
|
|
"git.unkin.net/unkin/artifactapi/pkg/models"
|
|
)
|
|
|
|
type staticCred string
|
|
|
|
func (s staticCred) Token(context.Context) (string, error) { return string(s), nil }
|
|
|
|
func TestProvider_AuthHeaders_GitHubServerCredential(t *testing.T) {
|
|
githubauth.SetServer(staticCred("ghs_server"))
|
|
t.Cleanup(func() { githubauth.SetServer(nil) })
|
|
|
|
p := &generic.Provider{}
|
|
h, err := p.AuthHeaders(context.Background(), models.Remote{BaseURL: "https://github.com"})
|
|
if err != nil {
|
|
t.Fatalf("auth headers: %v", err)
|
|
}
|
|
if h.Get("Authorization") != "Bearer ghs_server" {
|
|
t.Fatalf("Authorization = %q, want Bearer ghs_server", h.Get("Authorization"))
|
|
}
|
|
}
|
|
|
|
func TestProvider_AuthHeaders_NonGitHubHostNoServerCredential(t *testing.T) {
|
|
githubauth.SetServer(staticCred("ghs_server"))
|
|
t.Cleanup(func() { githubauth.SetServer(nil) })
|
|
|
|
p := &generic.Provider{}
|
|
h, _ := p.AuthHeaders(context.Background(), models.Remote{BaseURL: "https://example.com/downloads"})
|
|
if h.Get("Authorization") != "" {
|
|
t.Fatalf("server credential must not be sent to non-github host, got %q", h.Get("Authorization"))
|
|
}
|
|
}
|
|
|
|
func TestProvider_AuthHeaders_PerRemoteOverridesServerCredential(t *testing.T) {
|
|
githubauth.SetServer(staticCred("ghs_server"))
|
|
t.Cleanup(func() { githubauth.SetServer(nil) })
|
|
|
|
p := &generic.Provider{}
|
|
h, _ := p.AuthHeaders(context.Background(), models.Remote{
|
|
BaseURL: "https://github.com",
|
|
Username: "user",
|
|
Password: "pass",
|
|
})
|
|
if got := h.Get("Authorization"); got != "Basic dXNlcjpwYXNz" {
|
|
t.Fatalf("per-remote Basic auth must win, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestProvider_Type(t *testing.T) {
|
|
p := &generic.Provider{}
|
|
if p.Type() != models.PackageGeneric {
|
|
t.Errorf("expected generic, got %q", p.Type())
|
|
}
|
|
}
|
|
|
|
func TestProvider_Classify_AllImmutable(t *testing.T) {
|
|
p := &generic.Provider{}
|
|
paths := []string{"file.tar.gz", "path/to/binary", "index.html", "data.json"}
|
|
for _, path := range paths {
|
|
if p.Classify(path) != provider.Immutable {
|
|
t.Errorf("generic should classify %q as immutable", path)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProvider_ContentType(t *testing.T) {
|
|
p := &generic.Provider{}
|
|
tests := []struct{ path, want string }{
|
|
{"file.tar.gz", "application/gzip"},
|
|
{"file.tgz", "application/gzip"},
|
|
{"file.zip", "application/zip"},
|
|
{"file.rpm", "application/x-rpm"},
|
|
{"file.json", "application/json"},
|
|
{"file.unknown", "application/octet-stream"},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := p.ContentType(tt.path); got != tt.want {
|
|
t.Errorf("ContentType(%q) = %q, want %q", tt.path, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProvider_UpstreamURL(t *testing.T) {
|
|
p := &generic.Provider{}
|
|
got := p.UpstreamURL(models.Remote{BaseURL: "https://example.com/repo"}, "path/to/file.tar.gz")
|
|
want := "https://example.com/repo/path/to/file.tar.gz"
|
|
if got != want {
|
|
t.Errorf("got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestProvider_AuthHeaders_BasicAuth(t *testing.T) {
|
|
p := &generic.Provider{}
|
|
h, _ := p.AuthHeaders(context.Background(), models.Remote{Username: "user", Password: "pass"})
|
|
if h.Get("Authorization") != "Basic dXNlcjpwYXNz" {
|
|
t.Errorf("unexpected auth header: %q", h.Get("Authorization"))
|
|
}
|
|
}
|
|
|
|
func TestProvider_AuthHeaders_NoAuth(t *testing.T) {
|
|
p := &generic.Provider{}
|
|
h, _ := p.AuthHeaders(context.Background(), models.Remote{})
|
|
if h.Get("Authorization") != "" {
|
|
t.Error("expected no auth header")
|
|
}
|
|
}
|