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

74 lines
2.1 KiB
Go

package gitea
import (
"fmt"
"sort"
"strings"
)
// validScopes is the authoritative set of Gitea access-token scopes, matching
// go-gitea/gitea models/auth/access_token_scope.go. "all" grants every
// permission; "public-only" restricts a token to public resources. Every
// category has read: and write: forms (write implies read).
var validScopes = map[string]struct{}{
"all": {},
"public-only": {},
"read:activitypub": {},
"write:activitypub": {},
"read:admin": {},
"write:admin": {},
"read:misc": {},
"write:misc": {},
"read:notification": {},
"write:notification": {},
"read:organization": {},
"write:organization": {},
"read:package": {},
"write:package": {},
"read:issue": {},
"write:issue": {},
"read:repository": {},
"write:repository": {},
"read:user": {},
"write:user": {},
}
// knownScopes returns the sorted list of valid scopes, for error messages.
func knownScopes() []string {
out := make([]string, 0, len(validScopes))
for s := range validScopes {
out = append(out, s)
}
sort.Strings(out)
return out
}
// normalizeScopes trims, lower-cases and de-duplicates the requested scopes,
// rejecting any that Gitea would not recognise. Order is preserved (first
// occurrence wins) so the stored role reads back predictably.
func normalizeScopes(scopes []string) ([]string, error) {
if len(scopes) == 0 {
return nil, fmt.Errorf("at least one scope is required; valid scopes: %s", strings.Join(knownScopes(), ", "))
}
seen := make(map[string]struct{}, len(scopes))
out := make([]string, 0, len(scopes))
for _, raw := range scopes {
s := strings.ToLower(strings.TrimSpace(raw))
if s == "" {
continue
}
if _, ok := validScopes[s]; !ok {
return nil, fmt.Errorf("invalid scope %q; valid scopes: %s", raw, strings.Join(knownScopes(), ", "))
}
if _, dup := seen[s]; dup {
continue
}
seen[s] = struct{}{}
out = append(out, s)
}
if len(out) == 0 {
return nil, fmt.Errorf("at least one scope is required; valid scopes: %s", strings.Join(knownScopes(), ", "))
}
return out, nil
}