Files
unkinben 20613afb26 Initial vault-plugin-secrets-gitea engine
Add a Vault/OpenBao secrets engine that mints ephemeral, scoped Gitea
access tokens on demand. The engine holds a single seeded Gitea site-admin
Basic-Auth credential and, per role, mints a fresh per-user token via the
admin API, bound to a Vault lease and deleted from Gitea on revocation.
Gitea requires Basic Auth for token management (token auth is rejected),
and reqSelfOrAdmin lets a site admin manage any user's tokens, which is the
mechanism this relies on. Gitea tokens never expire server-side, so the
Vault lease is the sole expiry mechanism.

- add backend wiring, config (+ rotate-root), roles, creds paths
- add the gitea client (Basic Auth create/delete token, admin password change)
- add scope validation against Gitea's access-token scope set
- add unit tests (fake Gitea API) and a Vault+OpenBao e2e harness
- add Makefile, nfpm RPM packaging, and Woodpecker build/test/release pipelines

Claude-Session: https://claude.ai/code/session_015ur3i7D2azsMAWTSVABApv
2026-07-27 00:54:59 +10:00

102 lines
2.8 KiB
Go

package gitea
import (
"context"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func testClient(t *testing.T, url string) *giteaClient {
t.Helper()
c, err := newClient(&giteaConfig{
GiteaURL: url,
AdminUsername: "bot-admin",
AdminPassword: "seed-password",
})
if err != nil {
t.Fatalf("newClient: %v", err)
}
return c
}
func TestClientCreateAndDeleteToken(t *testing.T) {
fake := newFakeGitea("bot-admin", "seed-password")
srv := fake.server(t)
defer srv.Close()
c := testClient(t, srv.URL)
ctx := context.Background()
value, id, err := c.CreateToken(ctx, "teabot", "vault-teabot-abcd1234", []string{"read:repository"})
if err != nil {
t.Fatalf("CreateToken: %v", err)
}
if value == "" || id == "" {
t.Fatalf("CreateToken returned empty value/id: %q %q", value, id)
}
if !fake.has("teabot", id) {
t.Fatal("token not stored in fake")
}
if err := c.DeleteToken(ctx, "teabot", id); err != nil {
t.Fatalf("DeleteToken: %v", err)
}
if fake.has("teabot", id) {
t.Fatal("token still present after delete")
}
// Deleting a non-existent token (404) is treated as success.
if err := c.DeleteToken(ctx, "teabot", "999999"); err != nil {
t.Fatalf("DeleteToken of missing token should succeed, got: %v", err)
}
}
func TestClientUnauthorized(t *testing.T) {
fake := newFakeGitea("bot-admin", "correct-password")
srv := fake.server(t)
defer srv.Close()
c := testClient(t, srv.URL) // uses "seed-password", which is wrong here
_, _, err := c.CreateToken(context.Background(), "teabot", "x", []string{"read:repository"})
if err == nil {
t.Fatal("expected unauthorized error with wrong password")
}
if !strings.Contains(err.Error(), "401") {
t.Errorf("expected 401 in error, got: %v", err)
}
}
func TestClientVerifyAdminNonAdmin(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/api/v1/user" {
writeJSON(w, http.StatusOK, map[string]interface{}{"login": "bot-admin", "is_admin": false})
return
}
w.WriteHeader(http.StatusNotFound)
}))
defer srv.Close()
c := testClient(t, srv.URL)
err := c.VerifyAdmin(context.Background())
if err == nil {
t.Fatal("expected error for non-admin user")
}
if !strings.Contains(err.Error(), "not a Gitea site admin") {
t.Errorf("unexpected error: %v", err)
}
}
func TestClientRequiresCredentials(t *testing.T) {
if _, err := newClient(&giteaConfig{GiteaURL: "https://git.example.com", AdminUsername: "u"}); err == nil {
t.Fatal("expected error when admin_password missing")
}
if _, err := newClient(&giteaConfig{AdminUsername: "u", AdminPassword: "p"}); err == nil {
t.Fatal("expected error when gitea_url missing")
}
if _, err := newClient(&giteaConfig{GiteaURL: "x", AdminUsername: "u", AdminPassword: "p", CACert: "not-a-pem"}); err == nil {
t.Fatal("expected error for invalid ca_cert")
}
}