Files
vault-plugin-secrets-ghp/scopes.go
T
unkin-agent 64d9b89dcd
ci/woodpecker/pr/build Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/pre-commit Pipeline was successful
Scaffold ghp secrets engine modelled on vault-plugin-secrets-gitea
Mints ephemeral, scoped ghp access tokens via ghp's admin token API
(POST /api/tokens), bound to a Vault lease and revoked on lease
expiry (DELETE /api/tokens/{id}).

- config: base_url + write-only admin_token (ghpsvc_ service token),
  TLS settings; verifies the token is a ghp admin on write. No
  rotate-root: the service token is static and operator-managed.
- roles: token_type (agent/proxy), installation_id, app_record_id,
  repositories, scopes (permission:level), session_prefix, ttl/max_ttl.
- creds: mint a lease-bound token; ghp-side duration bounded by the
  lease ceiling as defence in depth.
- secret ghp_token: idempotent revoke + lease renew.
- Unit tests (config/role/creds/client/scopes/revocation), mock-ghp
  e2e on Vault + OpenBao, Woodpecker pre-commit/build/test/release,
  Makefile patch/minor/major, nfpm RPM packaging.
2026-08-15 19:13:44 +10:00

51 lines
1.7 KiB
Go

package ghp
import (
"fmt"
"strings"
)
// normalizeScopes validates ghp scope entries of the form "permission:level"
// (matching ghp's token.ParseScopeString), where level is read or write and
// permission is a GitHub App permission key. It trims, lower-cases the level,
// de-duplicates identical entries and rejects a permission requested at two
// different levels. Order is preserved (first occurrence wins). An empty input
// is valid and yields no scopes: ghp treats an open-scoped token as all-repos.
func normalizeScopes(scopes []string) ([]string, error) {
seen := make(map[string]string, len(scopes))
out := make([]string, 0, len(scopes))
for _, raw := range scopes {
s := strings.TrimSpace(raw)
if s == "" {
continue
}
kv := strings.SplitN(s, ":", 2)
if len(kv) != 2 {
return nil, fmt.Errorf("invalid scope %q; expected permission:level (e.g. contents:read)", raw)
}
perm := strings.TrimSpace(kv[0])
level := strings.ToLower(strings.TrimSpace(kv[1]))
if perm == "" {
return nil, fmt.Errorf("invalid scope %q; permission must not be empty", raw)
}
if level != "read" && level != "write" {
return nil, fmt.Errorf("invalid scope level %q in %q; must be read or write", kv[1], raw)
}
if prev, ok := seen[perm]; ok {
if prev != level {
return nil, fmt.Errorf("permission %q requested at conflicting levels %q and %q", perm, prev, level)
}
continue
}
seen[perm] = level
out = append(out, perm+":"+level)
}
return out, nil
}
// scopeString joins normalized scopes into the comma-separated form ghp's
// POST /api/tokens expects; an empty slice yields an empty (open-scoped) string.
func scopeString(scopes []string) string {
return strings.Join(scopes, ",")
}