20613afb26
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
74 lines
2.1 KiB
Go
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
|
|
}
|