Initial vault-plugin-secrets-netbox
Vault/OpenBao secrets engine that mints NetBox API tokens via /api/users/tokens/. A single seeded admin token (config) mints short-lived, per-user tokens (roles -> creds) whose NetBox expiry is aligned to the Vault lease; revoke deletes the token, renew extends its expiry. config/rotate reissues the seeded admin token. Handles NetBox 4.6 v2 tokens (Bearer nbt_<key>.<secret>) and legacy v1. Unit tests against an httptest NetBox mock; dual Vault/OpenBao RPMs via nfpm; tag-driven release to artifactapi. Claude-Session: https://claude.ai/code/session_01JUoARVdmhxKQHyyyp1pxeT
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
/dist/
|
||||
*.out
|
||||
*.test
|
||||
.env
|
||||
test/plugins/
|
||||
@@ -0,0 +1,15 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/dnephin/pre-commit-golang
|
||||
rev: v0.5.1
|
||||
hooks:
|
||||
- id: go-fmt
|
||||
- id: go-vet
|
||||
- id: go-mod-tidy
|
||||
@@ -0,0 +1,18 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: golang:1.25
|
||||
commands:
|
||||
- make build
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
@@ -0,0 +1,18 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: pre-commit
|
||||
image: git.unkin.net/unkin/almalinux9-gobuilder:20260606
|
||||
commands:
|
||||
- uvx pre-commit run --all-files
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
@@ -0,0 +1,47 @@
|
||||
when:
|
||||
- event: tag
|
||||
|
||||
steps:
|
||||
- name: build
|
||||
image: git.unkin.net/unkin/almalinux9-gobuilder:20260606
|
||||
commands:
|
||||
- make build VERSION=${CI_COMMIT_TAG}
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests: {memory: 512Mi, cpu: 1}
|
||||
limits: {memory: 2Gi, cpu: 2}
|
||||
|
||||
- name: package
|
||||
image: git.unkin.net/unkin/almalinux9-rpmbuilder:latest
|
||||
commands:
|
||||
- ./scripts/build-rpm.sh ${CI_COMMIT_TAG}
|
||||
depends_on: [build]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests: {memory: 512Mi, cpu: 1}
|
||||
limits: {memory: 2Gi, cpu: 2}
|
||||
|
||||
- name: upload
|
||||
image: git.unkin.net/unkin/almalinux9-base:20260606
|
||||
commands:
|
||||
- |
|
||||
HOST="https://artifactapi.k8s.syd1.au.unkin.net"
|
||||
REPO="rpm-internal"
|
||||
for rpm in dist/*.rpm; do
|
||||
FILE=$$(basename "$$rpm")
|
||||
code=$$(curl -s -o /dev/null -w '%{http_code}' "$$HOST/api/v2/remotes/$$REPO/files/Packages/$$FILE" || true)
|
||||
if [ "$$code" = "200" ]; then echo "$$FILE exists; skipping"; continue; fi
|
||||
echo "Uploading $$FILE (probe $$code)"
|
||||
curl -f -X PUT "$$HOST/api/v2/remotes/$$REPO/files/$$FILE" -H "Content-Type: application/x-rpm" --data-binary @"$$rpm"
|
||||
done
|
||||
depends_on: [package]
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests: {memory: 128Mi, cpu: 100m}
|
||||
limits: {memory: 512Mi, cpu: 500m}
|
||||
@@ -0,0 +1,33 @@
|
||||
when:
|
||||
- event: pull_request
|
||||
|
||||
steps:
|
||||
- name: lint
|
||||
image: golang:1.25
|
||||
commands:
|
||||
- make lint
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
|
||||
- name: test
|
||||
image: golang:1.25
|
||||
commands:
|
||||
- make test
|
||||
backend_options:
|
||||
kubernetes:
|
||||
serviceAccountName: default
|
||||
resources:
|
||||
requests:
|
||||
memory: 512Mi
|
||||
cpu: 1
|
||||
limits:
|
||||
memory: 2Gi
|
||||
cpu: 2
|
||||
@@ -0,0 +1,77 @@
|
||||
.PHONY: build install test lint fmt clean tidy rpm rpm-package patch minor major check-go e2e e2e-vault e2e-openbao e2e-up e2e-down
|
||||
|
||||
BINARY := vault-plugin-secrets-netbox
|
||||
PKG := ./cmd/vault-plugin-secrets-netbox
|
||||
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo "0.0.0-dev")
|
||||
OS ?= $(shell go env GOOS)
|
||||
ARCH ?= $(shell go env GOARCH)
|
||||
PLUGIN_DIR ?= ./dist
|
||||
|
||||
GO_VERSION_REQUIRED := 1.25
|
||||
GO_VERSION_ACTUAL := $(shell go version | sed 's/go version go\([0-9]*\.[0-9]*\).*/\1/')
|
||||
|
||||
check-go:
|
||||
@if [ "$$(printf '%s\n%s' "$(GO_VERSION_REQUIRED)" "$(GO_VERSION_ACTUAL)" | sort -V | head -1)" != "$(GO_VERSION_REQUIRED)" ]; then \
|
||||
echo "ERROR: Go >= $(GO_VERSION_REQUIRED) required, found $(GO_VERSION_ACTUAL)"; exit 1; \
|
||||
fi
|
||||
|
||||
build: check-go tidy
|
||||
CGO_ENABLED=0 GOOS=$(OS) GOARCH=$(ARCH) go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(PLUGIN_DIR)/$(BINARY) $(PKG)
|
||||
|
||||
install: build
|
||||
@echo "Built $(PLUGIN_DIR)/$(BINARY) (register it with: vault plugin register -sha256=<sha> secret $(BINARY))"
|
||||
|
||||
test: check-go
|
||||
go test -race -count=1 ./...
|
||||
|
||||
lint: check-go
|
||||
go vet ./...
|
||||
|
||||
fmt: check-go
|
||||
gofmt -w .
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
clean:
|
||||
rm -rf $(PLUGIN_DIR)
|
||||
|
||||
rpm: build rpm-package
|
||||
|
||||
rpm-package:
|
||||
./scripts/build-rpm.sh $(VERSION)
|
||||
|
||||
# End-to-end tests bring up a mock NetBox API plus Vault and OpenBao in Docker
|
||||
# and drive the full lifecycle against each with the same plugin binary.
|
||||
e2e:
|
||||
./scripts/e2e.sh
|
||||
|
||||
e2e-vault:
|
||||
ENGINES=vault ./scripts/e2e.sh
|
||||
|
||||
e2e-openbao:
|
||||
ENGINES=openbao ./scripts/e2e.sh
|
||||
|
||||
e2e-up:
|
||||
docker compose -f test/docker-compose.yml up -d --build
|
||||
|
||||
e2e-down:
|
||||
docker compose -f test/docker-compose.yml down -v
|
||||
|
||||
_LATEST := $(shell git tag --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$$' | head -1)
|
||||
_BASE := $(if $(_LATEST),$(_LATEST),v0.0.0)
|
||||
_MAJ := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f1)
|
||||
_MIN := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f2)
|
||||
_PAT := $(shell echo $(_BASE) | sed 's/^v//' | cut -d. -f3)
|
||||
|
||||
patch:
|
||||
@NEW=v$(_MAJ).$(_MIN).$(shell expr $(_PAT) + 1); \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
|
||||
minor:
|
||||
@NEW=v$(_MAJ).$(shell expr $(_MIN) + 1).0; \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
|
||||
major:
|
||||
@NEW=v$(shell expr $(_MAJ) + 1).0.0; \
|
||||
git tag $$NEW && echo "Tagged $$NEW" && git push origin $$NEW
|
||||
@@ -1,3 +1,77 @@
|
||||
# vault-plugin-secrets-netbox
|
||||
|
||||
HashiCorp Vault / OpenBao secrets engine for NetBox API tokens (seeded admin + dynamic per-user tokens with expiry-aligned leases)
|
||||
A Vault / OpenBao secrets engine that mints **NetBox API tokens** through
|
||||
NetBox's REST API (`/api/users/tokens/`).
|
||||
|
||||
## Why
|
||||
|
||||
Long-lived NetBox tokens handed to CI and to Puppet fact collectors are hard to
|
||||
rotate and easy to leak. This engine issues **short-lived, per-user tokens on
|
||||
demand**, each bound to a Vault lease: the token's NetBox `expires` is aligned to
|
||||
the lease, the lease renewal pushes `expires` forward, and revoking the lease
|
||||
deletes the token from NetBox.
|
||||
|
||||
A single seeded **admin token** authenticates the engine. NetBox lets an admin
|
||||
token create tokens for *other* users (with `add_token` + `grant_token`), so one
|
||||
credential is enough — a role just names a pre-existing NetBox service user.
|
||||
|
||||
## NetBox token model (4.6+)
|
||||
|
||||
NetBox 4.6 issues **v2 tokens** by default: only an HMAC digest is stored, and
|
||||
the plaintext is returned **once**, at creation. A v2 credential authenticates as
|
||||
`Authorization: Bearer nbt_<key>.<secret>`. Legacy **v1 tokens** (bare 40-char
|
||||
value) authenticate as `Authorization: Token <value>`.
|
||||
|
||||
- The engine returns both the raw `token` credential and a ready-to-use
|
||||
`authorization` header value on each mint.
|
||||
- v2 tokens require `API_TOKEN_PEPPERS` to be configured on the NetBox server. If
|
||||
yours is not, set `token_version=1` on `config` to mint v1 tokens (drop-in for
|
||||
older NetBox API clients that only send the `Token` scheme).
|
||||
|
||||
## Paths
|
||||
|
||||
| Path | Description |
|
||||
|------|-------------|
|
||||
| `config` | NetBox URL, TLS settings, seeded admin `token`, `token_version`, optional `admin_user_id`/`admin_token_id`. |
|
||||
| `config/rotate` | Reissue the seeded admin token (mint a replacement for the admin user, delete the old). |
|
||||
| `roles/<name>` | Mint policy: `netbox_user_id` (or `netbox_username`), `write_enabled` (default false), `ttl`, `max_ttl`, `description`. |
|
||||
| `creds/<role>` | Read to mint a short-lived, lease-bound token. |
|
||||
|
||||
## Usage
|
||||
|
||||
```sh
|
||||
vault secrets enable -path=netbox vault-plugin-secrets-netbox
|
||||
|
||||
vault write netbox/config \
|
||||
netbox_url=https://netbox.example.com \
|
||||
token="nbt_ab12cd34ef56.XXXXXXXX" \
|
||||
ca_cert=@netbox-ca.pem
|
||||
|
||||
# A read-only role for a NetBox service user (write_enabled defaults to false).
|
||||
vault write netbox/roles/puppet-facts netbox_username=svc-puppet-facts ttl=1h max_ttl=8h
|
||||
|
||||
# A write-enabled role.
|
||||
vault write netbox/roles/terraform-ipam netbox_username=svc-terraform-ipam \
|
||||
write_enabled=true ttl=30m max_ttl=4h
|
||||
|
||||
# Mint one.
|
||||
vault read netbox/creds/puppet-facts
|
||||
# -> token, authorization ("Bearer nbt_<key>.<secret>"), expires, ...
|
||||
```
|
||||
|
||||
Each role points at its **own** pre-existing NetBox service user (e.g.
|
||||
`svc-terraform-ipam`, `svc-puppet-facts`), so a minted token carries exactly that
|
||||
user's NetBox permissions. `write_enabled=false` additionally forbids
|
||||
create/update/delete regardless of the user's rights.
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
make build # build the plugin binary into ./dist
|
||||
make test # unit tests (httptest mock NetBox)
|
||||
make e2e # full lifecycle vs mock NetBox on Vault + OpenBao (Docker)
|
||||
make rpm # build Vault + OpenBao RPMs via nfpm
|
||||
```
|
||||
|
||||
Releases are tag-driven (`make patch|minor|major`): a Woodpecker pipeline builds
|
||||
the RPMs and uploads them to the internal artifactapi yum repo.
|
||||
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
// Package netbox implements a Vault / OpenBao secrets engine that mints NetBox
|
||||
// API tokens through NetBox's REST API (/api/users/tokens/).
|
||||
//
|
||||
// The engine is seeded with a single long-lived NetBox admin token (config).
|
||||
// Unlike some upstreams, NetBox lets an admin token create tokens for *other*
|
||||
// users, so one seeded credential is enough: a role names a pre-existing NetBox
|
||||
// service user and the token options (ttl/max_ttl, write_enabled), and each read
|
||||
// of creds/<role> mints a fresh, lease-bound token for that user whose NetBox
|
||||
// `expires` is aligned to the Vault lease. Revoking the lease deletes the token
|
||||
// from NetBox; renewing it PATCHes `expires` forward.
|
||||
//
|
||||
// The seeded admin token can be reissued via config/rotate (NetBox has no
|
||||
// in-place rotate; the engine mints a replacement for the admin user and deletes
|
||||
// the old one).
|
||||
package netbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
// errBackendNotConfigured is returned when a credential is requested before the
|
||||
// NetBox connection has been configured.
|
||||
var errBackendNotConfigured = errors.New("netbox backend not configured; write config first")
|
||||
|
||||
type netboxBackend struct {
|
||||
*framework.Backend
|
||||
|
||||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// Factory returns a configured NetBox secrets backend.
|
||||
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
||||
b := backend()
|
||||
if err := b.Setup(ctx, conf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func backend() *netboxBackend {
|
||||
b := &netboxBackend{}
|
||||
|
||||
b.Backend = &framework.Backend{
|
||||
Help: strings.TrimSpace(backendHelp),
|
||||
BackendType: logical.TypeLogical,
|
||||
PathsSpecial: &logical.Paths{
|
||||
SealWrapStorage: []string{configStoragePath},
|
||||
},
|
||||
Paths: framework.PathAppend(
|
||||
[]*framework.Path{
|
||||
pathConfig(b),
|
||||
pathConfigRotate(b),
|
||||
pathRole(b),
|
||||
pathRolesList(b),
|
||||
pathCredentials(b),
|
||||
},
|
||||
),
|
||||
Secrets: []*framework.Secret{
|
||||
b.netboxTokenSecret(),
|
||||
},
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// client builds a NetBox client from the stored config, authenticated with the
|
||||
// seeded admin token.
|
||||
func (b *netboxBackend) client(ctx context.Context, s logical.Storage) (*netboxClient, error) {
|
||||
config, err := getConfig(ctx, s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, errBackendNotConfigured
|
||||
}
|
||||
return newClient(config)
|
||||
}
|
||||
|
||||
const backendHelp = `
|
||||
The netbox secrets engine mints NetBox API tokens via /api/users/tokens/.
|
||||
|
||||
Seed a single NetBox admin token in config; roles name a pre-existing NetBox
|
||||
service user plus token options (ttl/max_ttl, write_enabled). Reading
|
||||
creds/<role> mints a short-lived token for that user with its NetBox expiry
|
||||
aligned to the Vault lease, deleted from NetBox on revoke. config/rotate
|
||||
reissues the seeded admin token.
|
||||
`
|
||||
+477
@@ -0,0 +1,477 @@
|
||||
package netbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
// fakeToken is an in-memory NetBox Token.
|
||||
type fakeToken struct {
|
||||
id int
|
||||
key string
|
||||
plaintext string
|
||||
version int
|
||||
writeEnabled bool
|
||||
expires string
|
||||
userID int
|
||||
}
|
||||
|
||||
func (t *fakeToken) credential() string {
|
||||
if t.version == 2 {
|
||||
return tokenPrefix + t.key + "." + t.plaintext
|
||||
}
|
||||
return t.plaintext
|
||||
}
|
||||
|
||||
// fakeNetbox is a minimal stand-in for the NetBox token API. Any credential it
|
||||
// has issued (or the seeded admin credential) is accepted as admin auth, so
|
||||
// rotation chains work exactly as in production.
|
||||
type fakeNetbox struct {
|
||||
mu sync.Mutex
|
||||
seq int
|
||||
tokens map[int]*fakeToken
|
||||
valid map[string]bool // credential -> accepted for auth
|
||||
users map[string]int // username -> id
|
||||
patchCount int
|
||||
}
|
||||
|
||||
func newFakeNetbox() *fakeNetbox {
|
||||
return &fakeNetbox{
|
||||
tokens: map[int]*fakeToken{},
|
||||
valid: map[string]bool{},
|
||||
users: map[string]int{},
|
||||
}
|
||||
}
|
||||
|
||||
// seedAdmin registers a v2 admin token and returns its credential string.
|
||||
func (f *fakeNetbox) seedAdmin(userID int) string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.seq++
|
||||
t := &fakeToken{id: f.seq, key: fmt.Sprintf("admkey%d", f.seq), plaintext: fmt.Sprintf("admsec%d", f.seq), version: 2, writeEnabled: true, userID: userID}
|
||||
f.tokens[t.id] = t
|
||||
cred := t.credential()
|
||||
f.valid[cred] = true
|
||||
return cred
|
||||
}
|
||||
|
||||
// seedAdminV1 registers a v1 admin token and returns its bare credential.
|
||||
func (f *fakeNetbox) seedAdminV1(userID int) string {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.seq++
|
||||
t := &fakeToken{id: f.seq, plaintext: fmt.Sprintf("v1adminplaintext%d", f.seq), version: 1, writeEnabled: true, userID: userID}
|
||||
f.tokens[t.id] = t
|
||||
cred := t.credential()
|
||||
f.valid[cred] = true
|
||||
return cred
|
||||
}
|
||||
|
||||
func (f *fakeNetbox) server(t *testing.T) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
cred := strings.TrimPrefix(strings.TrimPrefix(auth, "Bearer "), "Token ")
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
if !f.valid[cred] {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.URL.Path == usersPath && r.Method == http.MethodGet:
|
||||
id, ok := f.users[r.URL.Query().Get("username")]
|
||||
results := []map[string]interface{}{}
|
||||
if ok {
|
||||
results = append(results, map[string]interface{}{"id": id})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
|
||||
|
||||
case r.URL.Path == tokensPath && r.Method == http.MethodPost:
|
||||
var in struct {
|
||||
User int `json:"user"`
|
||||
WriteEnabled bool `json:"write_enabled"`
|
||||
Version int `json:"version"`
|
||||
Expires string `json:"expires"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&in)
|
||||
f.seq++
|
||||
tok := &fakeToken{
|
||||
id: f.seq,
|
||||
plaintext: fmt.Sprintf("secret%d", f.seq),
|
||||
version: in.Version,
|
||||
writeEnabled: in.WriteEnabled,
|
||||
expires: in.Expires,
|
||||
userID: in.User,
|
||||
}
|
||||
if tok.version == 0 {
|
||||
tok.version = 2
|
||||
}
|
||||
if tok.version == 2 {
|
||||
tok.key = fmt.Sprintf("key%d", f.seq)
|
||||
}
|
||||
f.tokens[tok.id] = tok
|
||||
f.valid[tok.credential()] = true
|
||||
writeJSON(w, http.StatusCreated, map[string]interface{}{
|
||||
"id": tok.id, "key": tok.key, "token": tok.plaintext,
|
||||
"version": tok.version, "write_enabled": tok.writeEnabled,
|
||||
"expires": tok.expires, "user": map[string]interface{}{"id": tok.userID},
|
||||
})
|
||||
|
||||
case r.URL.Path == tokensPath && r.Method == http.MethodGet:
|
||||
// Lookup by key.
|
||||
key := r.URL.Query().Get("key")
|
||||
results := []map[string]interface{}{}
|
||||
for _, tok := range f.tokens {
|
||||
if tok.key == key && key != "" {
|
||||
results = append(results, map[string]interface{}{
|
||||
"id": tok.id, "key": tok.key, "version": tok.version,
|
||||
"user": map[string]interface{}{"id": tok.userID},
|
||||
})
|
||||
}
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"results": results})
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, tokensPath):
|
||||
idStr := strings.Trim(strings.TrimPrefix(r.URL.Path, tokensPath), "/")
|
||||
id, err := strconv.Atoi(idStr)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
tok, ok := f.tokens[id]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPatch:
|
||||
var in struct {
|
||||
Expires string `json:"expires"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&in)
|
||||
tok.expires = in.Expires
|
||||
f.patchCount++
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"id": tok.id, "expires": tok.expires})
|
||||
case http.MethodDelete:
|
||||
delete(f.tokens, id)
|
||||
delete(f.valid, tok.credential())
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, code int, v interface{}) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func newTestBackend(t *testing.T) (*netboxBackend, logical.Storage) {
|
||||
t.Helper()
|
||||
config := logical.TestBackendConfig()
|
||||
config.StorageView = &logical.InmemStorage{}
|
||||
b, err := Factory(context.Background(), config)
|
||||
if err != nil {
|
||||
t.Fatalf("Factory: %v", err)
|
||||
}
|
||||
return b.(*netboxBackend), config.StorageView
|
||||
}
|
||||
|
||||
func req(t *testing.T, b *netboxBackend, s logical.Storage, op logical.Operation, path string, data map[string]interface{}) *logical.Response {
|
||||
t.Helper()
|
||||
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: op,
|
||||
Path: path,
|
||||
Data: data,
|
||||
Storage: s,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", op, path, err)
|
||||
}
|
||||
if resp != nil && resp.IsError() {
|
||||
t.Fatalf("%s %s: %v", op, path, resp.Error())
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func TestLifecycle(t *testing.T) {
|
||||
fake := newFakeNetbox()
|
||||
admin := fake.seedAdmin(100)
|
||||
fake.users["svc-puppet-facts"] = 42
|
||||
srv := fake.server(t)
|
||||
defer srv.Close()
|
||||
|
||||
b, s := newTestBackend(t)
|
||||
|
||||
req(t, b, s, logical.CreateOperation, "config", map[string]interface{}{
|
||||
"netbox_url": srv.URL,
|
||||
"token": admin,
|
||||
})
|
||||
|
||||
// Role via username resolution; write_enabled defaults to false (read-only).
|
||||
req(t, b, s, logical.CreateOperation, "roles/puppet-facts", map[string]interface{}{
|
||||
"netbox_username": "svc-puppet-facts",
|
||||
"ttl": "1h",
|
||||
"max_ttl": "8h",
|
||||
})
|
||||
|
||||
role := req(t, b, s, logical.ReadOperation, "roles/puppet-facts", nil)
|
||||
if id, _ := toInt(role.Data["netbox_user_id"]); id != 42 {
|
||||
t.Fatalf("username not resolved to id: got %v", role.Data["netbox_user_id"])
|
||||
}
|
||||
if role.Data["write_enabled"].(bool) {
|
||||
t.Fatal("write_enabled should default to false")
|
||||
}
|
||||
|
||||
creds := req(t, b, s, logical.ReadOperation, "creds/puppet-facts", nil)
|
||||
if creds.Secret == nil {
|
||||
t.Fatal("creds returned no secret")
|
||||
}
|
||||
if v := creds.Data["version"].(int); v != 2 {
|
||||
t.Fatalf("version = %d, want 2", v)
|
||||
}
|
||||
tok := creds.Data["token"].(string)
|
||||
if !strings.HasPrefix(tok, tokenPrefix) {
|
||||
t.Errorf("v2 credential = %q, want %s prefix", tok, tokenPrefix)
|
||||
}
|
||||
if auth := creds.Data["authorization"].(string); auth != "Bearer "+tok {
|
||||
t.Errorf("authorization = %q, want Bearer %s", auth, tok)
|
||||
}
|
||||
if creds.Data["write_enabled"].(bool) {
|
||||
t.Error("minted token should be read-only (write_enabled false)")
|
||||
}
|
||||
// Expiry aligned to the 1h lease.
|
||||
exp, err := time.Parse(time.RFC3339, creds.Data["expires"].(string))
|
||||
if err != nil {
|
||||
t.Fatalf("parsing expires: %v", err)
|
||||
}
|
||||
if d := time.Until(exp); d < 55*time.Minute || d > 65*time.Minute {
|
||||
t.Errorf("expires not aligned to 1h lease: %s away", d)
|
||||
}
|
||||
tokenID, _ := toInt(creds.Secret.InternalData["token_id"])
|
||||
fake.mu.Lock()
|
||||
if _, ok := fake.tokens[tokenID]; !ok {
|
||||
t.Error("minted token not present in netbox")
|
||||
}
|
||||
fake.mu.Unlock()
|
||||
|
||||
// Revoke deletes the token from NetBox.
|
||||
if _, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.RevokeOperation,
|
||||
Secret: creds.Secret,
|
||||
Storage: s,
|
||||
}); err != nil {
|
||||
t.Fatalf("revoke: %v", err)
|
||||
}
|
||||
fake.mu.Lock()
|
||||
if _, ok := fake.tokens[tokenID]; ok {
|
||||
t.Error("token still present after revoke")
|
||||
}
|
||||
fake.mu.Unlock()
|
||||
}
|
||||
|
||||
func TestWriteEnabledRole(t *testing.T) {
|
||||
fake := newFakeNetbox()
|
||||
admin := fake.seedAdmin(100)
|
||||
srv := fake.server(t)
|
||||
defer srv.Close()
|
||||
|
||||
b, s := newTestBackend(t)
|
||||
req(t, b, s, logical.CreateOperation, "config", map[string]interface{}{"netbox_url": srv.URL, "token": admin})
|
||||
req(t, b, s, logical.CreateOperation, "roles/ipam", map[string]interface{}{
|
||||
"netbox_user_id": 7,
|
||||
"write_enabled": true,
|
||||
"ttl": "30m",
|
||||
})
|
||||
creds := req(t, b, s, logical.ReadOperation, "creds/ipam", nil)
|
||||
if !creds.Data["write_enabled"].(bool) {
|
||||
t.Fatal("expected write_enabled token")
|
||||
}
|
||||
if got, _ := toInt(creds.Data["netbox_user_id"]); got != 7 {
|
||||
t.Fatalf("netbox_user_id = %v, want 7", creds.Data["netbox_user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenewExtendsExpiry(t *testing.T) {
|
||||
fake := newFakeNetbox()
|
||||
admin := fake.seedAdmin(100)
|
||||
srv := fake.server(t)
|
||||
defer srv.Close()
|
||||
|
||||
b, s := newTestBackend(t)
|
||||
req(t, b, s, logical.CreateOperation, "config", map[string]interface{}{"netbox_url": srv.URL, "token": admin})
|
||||
req(t, b, s, logical.CreateOperation, "roles/ci", map[string]interface{}{"netbox_user_id": 7, "ttl": "1h", "max_ttl": "8h"})
|
||||
creds := req(t, b, s, logical.ReadOperation, "creds/ci", nil)
|
||||
|
||||
before := fake.patchCount
|
||||
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.RenewOperation,
|
||||
Secret: creds.Secret,
|
||||
Storage: s,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("renew: %v", err)
|
||||
}
|
||||
if resp == nil || resp.Secret == nil {
|
||||
t.Fatal("renew returned no secret")
|
||||
}
|
||||
if fake.patchCount != before+1 {
|
||||
t.Errorf("renew did not PATCH netbox expires (patchCount %d -> %d)", before, fake.patchCount)
|
||||
}
|
||||
tokenID, _ := toInt(creds.Secret.InternalData["token_id"])
|
||||
fake.mu.Lock()
|
||||
exp, perr := time.Parse(time.RFC3339, fake.tokens[tokenID].expires)
|
||||
fake.mu.Unlock()
|
||||
if perr != nil {
|
||||
t.Fatalf("parsing renewed expires: %v", perr)
|
||||
}
|
||||
if time.Until(exp) <= 0 {
|
||||
t.Error("renewed expiry is not in the future")
|
||||
}
|
||||
}
|
||||
|
||||
func TestV1Tokens(t *testing.T) {
|
||||
fake := newFakeNetbox()
|
||||
admin := fake.seedAdminV1(100)
|
||||
srv := fake.server(t)
|
||||
defer srv.Close()
|
||||
|
||||
b, s := newTestBackend(t)
|
||||
req(t, b, s, logical.CreateOperation, "config", map[string]interface{}{
|
||||
"netbox_url": srv.URL,
|
||||
"token": admin,
|
||||
"token_version": 1,
|
||||
})
|
||||
req(t, b, s, logical.CreateOperation, "roles/legacy", map[string]interface{}{"netbox_user_id": 7, "ttl": "1h"})
|
||||
creds := req(t, b, s, logical.ReadOperation, "creds/legacy", nil)
|
||||
if v := creds.Data["version"].(int); v != 1 {
|
||||
t.Fatalf("version = %d, want 1", v)
|
||||
}
|
||||
tok := creds.Data["token"].(string)
|
||||
if strings.HasPrefix(tok, tokenPrefix) {
|
||||
t.Errorf("v1 credential = %q, should not carry the %s prefix", tok, tokenPrefix)
|
||||
}
|
||||
if auth := creds.Data["authorization"].(string); auth != "Token "+tok {
|
||||
t.Errorf("authorization = %q, want Token %s", auth, tok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigRotate(t *testing.T) {
|
||||
fake := newFakeNetbox()
|
||||
admin := fake.seedAdmin(100) // registered with key so lookup-by-key works
|
||||
srv := fake.server(t)
|
||||
defer srv.Close()
|
||||
|
||||
b, s := newTestBackend(t)
|
||||
req(t, b, s, logical.CreateOperation, "config", map[string]interface{}{"netbox_url": srv.URL, "token": admin})
|
||||
|
||||
fake.mu.Lock()
|
||||
oldCount := len(fake.tokens)
|
||||
fake.mu.Unlock()
|
||||
|
||||
rot := req(t, b, s, logical.UpdateOperation, "config/rotate", nil)
|
||||
if id, _ := toInt(rot.Data["admin_token_id"]); id == 0 {
|
||||
t.Fatal("rotate did not report a new admin_token_id")
|
||||
}
|
||||
|
||||
// The old admin credential must no longer be accepted; a new one replaces it.
|
||||
fake.mu.Lock()
|
||||
if fake.valid[admin] {
|
||||
t.Error("old admin credential still valid after rotation")
|
||||
}
|
||||
if len(fake.tokens) != oldCount {
|
||||
t.Errorf("token count changed after rotation: %d -> %d", oldCount, len(fake.tokens))
|
||||
}
|
||||
fake.mu.Unlock()
|
||||
|
||||
// The engine can still mint using the rotated admin token.
|
||||
req(t, b, s, logical.CreateOperation, "roles/after", map[string]interface{}{"netbox_user_id": 7, "ttl": "1h"})
|
||||
req(t, b, s, logical.ReadOperation, "creds/after", nil)
|
||||
}
|
||||
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
b, s := newTestBackend(t)
|
||||
|
||||
// Missing token.
|
||||
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.CreateOperation,
|
||||
Path: "config",
|
||||
Data: map[string]interface{}{"netbox_url": "https://netbox.example.com"},
|
||||
Storage: s,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if resp == nil || !resp.IsError() {
|
||||
t.Fatal("expected error when token is missing")
|
||||
}
|
||||
|
||||
// Bad token_version.
|
||||
resp, err = b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.CreateOperation,
|
||||
Path: "config",
|
||||
Data: map[string]interface{}{"netbox_url": "https://n", "token": "x", "token_version": 3},
|
||||
Storage: s,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if resp == nil || !resp.IsError() {
|
||||
t.Fatal("expected error for token_version=3")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRoleRequiresUser(t *testing.T) {
|
||||
fake := newFakeNetbox()
|
||||
admin := fake.seedAdmin(100)
|
||||
srv := fake.server(t)
|
||||
defer srv.Close()
|
||||
|
||||
b, s := newTestBackend(t)
|
||||
req(t, b, s, logical.CreateOperation, "config", map[string]interface{}{"netbox_url": srv.URL, "token": admin})
|
||||
resp, err := b.HandleRequest(context.Background(), &logical.Request{
|
||||
Operation: logical.CreateOperation,
|
||||
Path: "roles/x",
|
||||
Data: map[string]interface{}{"ttl": "1h"},
|
||||
Storage: s,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected err: %v", err)
|
||||
}
|
||||
if resp == nil || !resp.IsError() {
|
||||
t.Fatal("expected error for role without a netbox user")
|
||||
}
|
||||
}
|
||||
|
||||
// toInt coerces the numeric shapes returned across storage round-trips.
|
||||
func toInt(v interface{}) (int, bool) {
|
||||
switch n := v.(type) {
|
||||
case int:
|
||||
return n, true
|
||||
case int64:
|
||||
return int(n), true
|
||||
case float64:
|
||||
return int(n), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package netbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const defaultHTTPTimeout = 30 * time.Second
|
||||
|
||||
// tokenPrefix is prepended to a v2 token's identification key when forming the
|
||||
// Authorization header (NetBox's TOKEN_PREFIX). A credential string that starts
|
||||
// with it is a v2 token; otherwise it is a legacy v1 (bare 40-char) token.
|
||||
const tokenPrefix = "nbt_"
|
||||
|
||||
const (
|
||||
tokensPath = "/api/users/tokens/"
|
||||
usersPath = "/api/users/users/"
|
||||
)
|
||||
|
||||
// netboxClient talks to the NetBox REST API authenticated with the seeded admin
|
||||
// token. NetBox 4.6 issues v2 tokens by default (HMAC-digest; the plaintext is
|
||||
// returned only at creation), which authenticate as "Bearer nbt_<key>.<token>".
|
||||
// Legacy v1 tokens authenticate as "Token <plaintext>".
|
||||
type netboxClient struct {
|
||||
baseURL string
|
||||
authHeader string // full Authorization header value for the admin token
|
||||
version int // token version to request when minting (1 or 2)
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// mintedToken is the subset of a NetBox Token we consume.
|
||||
type mintedToken struct {
|
||||
ID int `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Token string `json:"token"` // plaintext, only present at creation
|
||||
Version int `json:"version"`
|
||||
WriteEnabled bool `json:"write_enabled"`
|
||||
Expires string `json:"expires"`
|
||||
User struct {
|
||||
ID int `json:"id"`
|
||||
} `json:"user"`
|
||||
}
|
||||
|
||||
// mintRequest describes a token to create.
|
||||
type mintRequest struct {
|
||||
UserID int
|
||||
WriteEnabled bool
|
||||
Description string
|
||||
Expires time.Time // zero means non-expiring
|
||||
}
|
||||
|
||||
func newClient(cfg *netboxConfig) (*netboxClient, error) {
|
||||
if cfg == nil {
|
||||
return nil, errors.New("netbox client configuration is nil")
|
||||
}
|
||||
if cfg.NetboxURL == "" {
|
||||
return nil, errors.New("netbox_url is required")
|
||||
}
|
||||
if cfg.Token == "" {
|
||||
return nil, errors.New("token (admin) is required")
|
||||
}
|
||||
|
||||
timeout := defaultHTTPTimeout
|
||||
if cfg.RequestTimeoutSeconds > 0 {
|
||||
timeout = time.Duration(cfg.RequestTimeoutSeconds) * time.Second
|
||||
}
|
||||
|
||||
tlsConfig := &tls.Config{InsecureSkipVerify: cfg.TLSSkipVerify} //nolint:gosec // opt-in via config
|
||||
if cfg.CACert != "" {
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM([]byte(cfg.CACert)) {
|
||||
return nil, errors.New("ca_cert is not a valid PEM certificate")
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
version := cfg.TokenVersion
|
||||
if version == 0 {
|
||||
version = 2
|
||||
}
|
||||
|
||||
return &netboxClient{
|
||||
baseURL: strings.TrimRight(cfg.NetboxURL, "/"),
|
||||
authHeader: authHeaderFor(cfg.Token),
|
||||
version: version,
|
||||
httpClient: &http.Client{
|
||||
Timeout: timeout,
|
||||
Transport: &http.Transport{TLSClientConfig: tlsConfig},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// authHeaderFor returns the full Authorization header value for a NetBox token
|
||||
// credential, inferring the scheme from the token's version (v2 credentials are
|
||||
// prefixed nbt_ and use Bearer; v1 use Token), exactly as NetBox does.
|
||||
func authHeaderFor(credential string) string {
|
||||
if strings.HasPrefix(credential, tokenPrefix) {
|
||||
return "Bearer " + credential
|
||||
}
|
||||
return "Token " + credential
|
||||
}
|
||||
|
||||
// credentialFor assembles the usable credential string a client presents for a
|
||||
// minted token: v2 tokens are "nbt_<key>.<plaintext>"; v1 tokens are the bare
|
||||
// plaintext.
|
||||
func credentialFor(t *mintedToken) string {
|
||||
if t.Version == 2 {
|
||||
return tokenPrefix + t.Key + "." + t.Token
|
||||
}
|
||||
return t.Token
|
||||
}
|
||||
|
||||
// MintToken creates a NetBox token for the given user and returns it (including
|
||||
// the one-time plaintext).
|
||||
func (c *netboxClient) MintToken(ctx context.Context, req mintRequest) (*mintedToken, error) {
|
||||
body := map[string]interface{}{
|
||||
"user": req.UserID,
|
||||
"write_enabled": req.WriteEnabled,
|
||||
"version": c.version,
|
||||
}
|
||||
if req.Description != "" {
|
||||
body["description"] = req.Description
|
||||
}
|
||||
if !req.Expires.IsZero() {
|
||||
body["expires"] = req.Expires.UTC().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
var out mintedToken
|
||||
if err := c.do(ctx, http.MethodPost, tokensPath, body, &out); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if out.Token == "" {
|
||||
return nil, errors.New("netbox returned an empty token value")
|
||||
}
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// ExtendToken updates a token's expiry (used on lease renewal). NetBox permits
|
||||
// updating expires on an existing token.
|
||||
func (c *netboxClient) ExtendToken(ctx context.Context, id int, expires time.Time) error {
|
||||
body := map[string]interface{}{
|
||||
"expires": expires.UTC().Format(time.RFC3339),
|
||||
}
|
||||
return c.do(ctx, http.MethodPatch, fmt.Sprintf("%s%d/", tokensPath, id), body, nil)
|
||||
}
|
||||
|
||||
// DeleteToken removes a token by id. A missing token is treated as success.
|
||||
func (c *netboxClient) DeleteToken(ctx context.Context, id int) error {
|
||||
if id == 0 {
|
||||
return nil
|
||||
}
|
||||
return c.do(ctx, http.MethodDelete, fmt.Sprintf("%s%d/", tokensPath, id), nil, nil)
|
||||
}
|
||||
|
||||
// LookupTokenByKey finds a v2 token by its identification key, returning its id
|
||||
// and owning user id. Used to auto-discover the seeded admin token's ids for
|
||||
// rotation.
|
||||
func (c *netboxClient) LookupTokenByKey(ctx context.Context, key string) (id, userID int, err error) {
|
||||
var out struct {
|
||||
Results []mintedToken `json:"results"`
|
||||
}
|
||||
q := tokensPath + "?" + url.Values{"key": {key}}.Encode()
|
||||
if err := c.do(ctx, http.MethodGet, q, nil, &out); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
if len(out.Results) == 0 {
|
||||
return 0, 0, errNotFound
|
||||
}
|
||||
return out.Results[0].ID, out.Results[0].User.ID, nil
|
||||
}
|
||||
|
||||
// ResolveUserID looks up a NetBox user's id by username.
|
||||
func (c *netboxClient) ResolveUserID(ctx context.Context, username string) (int, error) {
|
||||
var out struct {
|
||||
Results []struct {
|
||||
ID int `json:"id"`
|
||||
} `json:"results"`
|
||||
}
|
||||
q := usersPath + "?" + url.Values{"username": {username}}.Encode()
|
||||
if err := c.do(ctx, http.MethodGet, q, nil, &out); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if len(out.Results) == 0 {
|
||||
return 0, fmt.Errorf("no netbox user found with username %q", username)
|
||||
}
|
||||
return out.Results[0].ID, nil
|
||||
}
|
||||
|
||||
// errNotFound flags a 404 (or empty lookup) so callers can treat absence as
|
||||
// non-fatal.
|
||||
var errNotFound = errors.New("not found")
|
||||
|
||||
func (c *netboxClient) do(ctx context.Context, method, path string, payload, out interface{}) error {
|
||||
var body io.Reader
|
||||
if payload != nil {
|
||||
raw, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding request body: %w", err)
|
||||
}
|
||||
body = bytes.NewReader(raw)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("building request: %w", err)
|
||||
}
|
||||
req.Header.Set("Authorization", c.authHeader)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("calling netbox %s %s: %w", method, path, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
if method == http.MethodDelete {
|
||||
return nil
|
||||
}
|
||||
return errNotFound
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("netbox %s %s returned %d: %s", method, path, resp.StatusCode, strings.TrimSpace(string(respBody)))
|
||||
}
|
||||
|
||||
if out == nil {
|
||||
return nil
|
||||
}
|
||||
if err := json.Unmarshal(respBody, out); err != nil {
|
||||
return fmt.Errorf("decoding netbox response: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
hclog "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/sdk/plugin"
|
||||
|
||||
netbox "git.unkin.net/unkin/vault-plugin-secrets-netbox"
|
||||
)
|
||||
|
||||
func main() {
|
||||
apiClientMeta := &api.PluginAPIClientMeta{}
|
||||
flags := apiClientMeta.FlagSet()
|
||||
if err := flags.Parse(os.Args[1:]); err != nil {
|
||||
logger := hclog.New(&hclog.LoggerOptions{})
|
||||
logger.Error("failed to parse flags", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
tlsConfig := apiClientMeta.GetTLSConfig()
|
||||
tlsProviderFunc := api.VaultPluginTLSProvider(tlsConfig)
|
||||
|
||||
err := plugin.ServeMultiplex(&plugin.ServeOpts{
|
||||
BackendFactoryFunc: netbox.Factory,
|
||||
TLSProviderFunc: tlsProviderFunc,
|
||||
})
|
||||
if err != nil {
|
||||
logger := hclog.New(&hclog.LoggerOptions{})
|
||||
logger.Error("plugin shutting down", "error", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
module git.unkin.net/unkin/vault-plugin-secrets-netbox
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/hashicorp/go-hclog v1.6.3
|
||||
github.com/hashicorp/vault/api v1.15.0
|
||||
github.com/hashicorp/vault/sdk v0.14.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/Microsoft/go-winio v0.6.1 // indirect
|
||||
github.com/armon/go-metrics v0.4.1 // indirect
|
||||
github.com/armon/go-radix v1.0.0 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/docker v26.1.5+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.4 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
|
||||
github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 // indirect
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.8 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-plugin v1.6.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.0 // indirect
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
|
||||
github.com/hashicorp/go-sockaddr v1.0.6 // indirect
|
||||
github.com/hashicorp/go-uuid v1.0.3 // indirect
|
||||
github.com/hashicorp/go-version v1.6.0 // indirect
|
||||
github.com/hashicorp/golang-lru v0.5.4 // indirect
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
|
||||
github.com/hashicorp/yamux v0.1.1 // indirect
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/oklog/run v1.1.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/ryanuber/go-glob v1.0.0 // indirect
|
||||
github.com/sasha-s/go-deadlock v0.2.0 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 // indirect
|
||||
go.opentelemetry.io/otel v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.45.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 // indirect
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
golang.org/x/crypto v0.54.0 // indirect
|
||||
golang.org/x/mod v0.37.0 // indirect
|
||||
golang.org/x/net v0.57.0 // indirect
|
||||
golang.org/x/sync v0.22.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.40.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.47.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect
|
||||
google.golang.org/grpc v1.82.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -0,0 +1,340 @@
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
||||
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
|
||||
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
|
||||
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/go-metrics v0.4.1 h1:hR91U9KYmb6bLBYLQjyM+3j+rcd/UhE+G78SFnF8gJA=
|
||||
github.com/armon/go-metrics v0.4.1/go.mod h1:E6amYzXo6aW1tqzoZGT755KkbgrJsSdpwZ+3JqfkOG4=
|
||||
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA=
|
||||
github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag=
|
||||
github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I=
|
||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||
github.com/docker/docker v26.1.5+incompatible h1:NEAxTwEjxV6VbBMBoGG3zPqbiJosIApZjxlbrG9q3/g=
|
||||
github.com/docker/docker v26.1.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww=
|
||||
github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4=
|
||||
github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/frankban/quicktest v1.14.0 h1:+cqqvzZV87b4adx/5ayVOaYZ2CrvM4ejQvUdBzPPUss=
|
||||
github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 h1:moDMcTHmvE6Groj34emNPLs/qtYXRVcd6S7NHbHz3kA=
|
||||
github.com/go-jose/go-jose/v4 v4.1.4/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8=
|
||||
github.com/go-logr/logr v1.4.4/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg=
|
||||
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
|
||||
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
|
||||
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
|
||||
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
|
||||
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
|
||||
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
|
||||
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
|
||||
github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 h1:pSjQfW3vPtrOTcasTUKgCTQT7OGPPTTMVRrOfU6FJD8=
|
||||
github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0/go.mod h1:xvb32K2keAc+R8DSFG2IwDcydK9DBQE+fGA5fsw6hSk=
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.8 h1:9Q2lu1YbbmiAgvYZ7Pr31RdlVonUpX+mmDL7Z7qTA2U=
|
||||
github.com/hashicorp/go-kms-wrapping/v2 v2.0.8/go.mod h1:qTCjxGig/kjuj3hk1z8pOUrzbse/GxB1tGfbrq8tGJg=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/go-plugin v1.6.1 h1:P7MR2UP6gNKGPp+y7EZw2kOiq4IR9WiqLvp0XOsVdwI=
|
||||
github.com/hashicorp/go-plugin v1.6.1/go.mod h1:XPHFku2tFo3o3QKFgSYo+cghcUhw1NA1hZyMK0PWAw0=
|
||||
github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
|
||||
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
|
||||
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2 h1:p4AKXPPS24tO8Wc8i1gLvSKdmkiSY5xuju57czJ/IJQ=
|
||||
github.com/hashicorp/go-secure-stdlib/mlock v0.1.2/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8 h1:iBt4Ew4XEGLfh6/bPk4rSYmuZJGizr6/x/AEizP0CQc=
|
||||
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.8/go.mod h1:aiJI+PIApBRQG7FZTEBx5GiiX+HbOHilUdNxUZi4eV0=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.0 h1:7Yran48kl6X7jfUg3sfYDrFot1gD3LvzdC3oPu5l/qo=
|
||||
github.com/hashicorp/go-secure-stdlib/plugincontainer v0.4.0/go.mod h1:9WJFu7L3d+Z4ViZmwUf+6/73/Uy7YMY1NXrB9wdElYE=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
|
||||
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
|
||||
github.com/hashicorp/go-sockaddr v1.0.6 h1:RSG8rKU28VTUTvEKghe5gIhIQpv8evvNpnDEyqO4u9I=
|
||||
github.com/hashicorp/go-sockaddr v1.0.6/go.mod h1:uoUUmtwU7n9Dv3O4SNLeFvg0SxQ3lyjsj6+CCykpaxI=
|
||||
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
|
||||
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
|
||||
github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek=
|
||||
github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
|
||||
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM=
|
||||
github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
|
||||
github.com/hashicorp/vault/api v1.15.0 h1:O24FYQCWwhwKnF7CuSqP30S51rTV7vz1iACXE/pj5DA=
|
||||
github.com/hashicorp/vault/api v1.15.0/go.mod h1:+5YTO09JGn0u+b6ySD/LLVf8WkJCPLAL2Vkmrn2+CM8=
|
||||
github.com/hashicorp/vault/sdk v0.14.0 h1:8vagjlpLurkFTnKT9aFSGs4U1XnK2IFytnWSxgFrDo0=
|
||||
github.com/hashicorp/vault/sdk v0.14.0/go.mod h1:3hnGK5yjx3CW2hFyk+Dw1jDgKxdBvUvjyxMHhq0oUFc=
|
||||
github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE=
|
||||
github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c=
|
||||
github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531 h1:hgVxRoDDPtQE68PT4LFvNlPz2nBKd3OMlGKIQ69OmR4=
|
||||
github.com/joshlf/go-acl v0.0.0-20200411065538-eae00ae38531/go.mod h1:fqTUQpVYBvhCNIsMXGl2GE9q6z94DIP6NtFKXCSTVbg=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d h1:J8tJzRyiddAFF65YVgxli+TyWBi0f79Sld6rJP6CBcY=
|
||||
github.com/joshlf/testutil v0.0.0-20170608050642-b5d8aa79d93d/go.mod h1:b+Q3v8Yrg5o15d71PSUraUzYb+jWl6wQMSBXSGS/hv0=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw=
|
||||
github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
|
||||
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
|
||||
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
|
||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
|
||||
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA=
|
||||
github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
|
||||
github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
|
||||
github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 h1:q2e307iGHPdTGp0hoxKjt1H5pDo6utceo3dQVK3I5XQ=
|
||||
github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible h1:9UY3+iC23yxF0UfGaYrGplQ+79Rg+h/q9FV9ix19jjM=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
|
||||
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
|
||||
github.com/sasha-s/go-deadlock v0.2.0 h1:lMqc+fUb7RrFS3gQLtoQsJ7/6TV/pAIFvBsqX73DK8Y=
|
||||
github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0 h1:Xs2Ncz0gNihqu9iosIZ5SkBbWo5T8JhhLJFMQL1qmLI=
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.51.0/go.mod h1:vy+2G/6NvVMpwGX/NyLqcC41fxepnuKHk16E6IZUcJc=
|
||||
go.opentelemetry.io/otel v1.45.0 h1:pdrWmLHofpubmArBv1LgFSv1Z0Ie/ppdZzu+kUN5EeU=
|
||||
go.opentelemetry.io/otel v1.45.0/go.mod h1:XZxIqPapzEYnhNSScF5DIqXhm/rYi0FzCe2XddAwZfQ=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0 h1:QRefszxJmfPdjXUUm3j6iDzY03mTPXMjqErFqQ67vUg=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.45.0/go.mod h1:Tiz03lTBVBrm7eWZBOidzEaYaJa8tjwGUGv6d8mlTyk=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
|
||||
go.opentelemetry.io/otel/metric v1.45.0 h1:7Eg1uH7CJ5cXv9is6tnBe1FI6rj1nwUdbFypRm3br/M=
|
||||
go.opentelemetry.io/otel/metric v1.45.0/go.mod h1:HAPbm1nd3p1PmFH7v2dR+6BjXxw+Lq4a2+pndMAm08s=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0 h1:4VVSMgQ83dUgW2aoX5f6JgLvHwIvzcuLnF9lUdCSpCw=
|
||||
go.opentelemetry.io/otel/sdk v1.45.0/go.mod h1:Sr40LgXV7DsKMMJMKOhUWOgMWTfAaqvm2kF0g7ilwuA=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0 h1:oVFszMfyj1Am6s24Vtc7wBb8BKLcwepJjNEYILuiE3o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.45.0/go.mod h1:vUWUxDZvu1WVRj8JA8S0AdhsPrZoDpA2DdZauIh4mDA=
|
||||
go.opentelemetry.io/otel/trace v1.45.0 h1:l/mP6Uv7oNO7/TblbhpbgMidxhq1uO/rPsikOyVhxag=
|
||||
go.opentelemetry.io/otel/trace v1.45.0/go.mod h1:qoJJA2xNMnxRrdISU/kLtfUH2wNeQbiv+jhs/CxI8bc=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0 h1:5rrYs0Ykyj50sdU/JU0x8etU+LubXWb+gED6TbEdMIk=
|
||||
go.opentelemetry.io/proto/otlp v1.11.0/go.mod h1:SmVizdCOAm3XBtG1g1NnOdhW6jtddT72hLMhv8VwA8E=
|
||||
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw=
|
||||
golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
|
||||
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE=
|
||||
golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
|
||||
golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
|
||||
golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
|
||||
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a h1:97PfJ4tCxY5C7NzzgGqQEMZmXbISdvSArNNEOoUGKBg=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a/go.mod h1:1brfde68Npq6+WA75c1EHWPijZEG1kMus61ygPZfn4A=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a h1:qI/YMH1ep2qQtqcp00gMQyoU7mjvbhg88GJKCvfoLj0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
|
||||
google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
|
||||
google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY=
|
||||
gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
Executable
BIN
Binary file not shown.
@@ -0,0 +1,35 @@
|
||||
---
|
||||
# nfpm config for the vault-plugin-secrets-netbox RPM. Rendered through
|
||||
# envsubst (see scripts/build-rpm.sh) then fed to `nfpm pkg`. Built once per
|
||||
# target server (Vault, OpenBao); PACKAGE_NAME and PACKAGE_PLUGIN_DIR vary.
|
||||
|
||||
name: ${PACKAGE_NAME}
|
||||
version: ${PACKAGE_VERSION}
|
||||
release: ${PACKAGE_RELEASE}
|
||||
arch: ${PACKAGE_ARCH}
|
||||
platform: ${PACKAGE_PLATFORM}
|
||||
section: default
|
||||
priority: extra
|
||||
description: "${PACKAGE_DESCRIPTION}"
|
||||
|
||||
maintainer: ${PACKAGE_MAINTAINER}
|
||||
homepage: ${PACKAGE_HOMEPAGE}
|
||||
license: ${PACKAGE_LICENSE}
|
||||
|
||||
disable_globbing: false
|
||||
|
||||
replaces:
|
||||
- ${PACKAGE_NAME}
|
||||
provides:
|
||||
- ${PACKAGE_NAME}
|
||||
|
||||
contents:
|
||||
- src: dist/vault-plugin-secrets-netbox
|
||||
dst: ${PACKAGE_PLUGIN_DIR}/vault-plugin-secrets-netbox
|
||||
file_info:
|
||||
mode: 0755
|
||||
owner: root
|
||||
group: root
|
||||
|
||||
scripts:
|
||||
preinstall: ${PACKAGE_PREINSTALL}
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Ensure the plugin directory exists before the binary is laid down.
|
||||
# Rendered per flavour via envsubst (see scripts/build-rpm.sh).
|
||||
mkdir -p ${PACKAGE_PLUGIN_DIR}
|
||||
+302
@@ -0,0 +1,302 @@
|
||||
package netbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const configStoragePath = "config"
|
||||
|
||||
// netboxConfig is the connection to NetBox plus the seeded admin token the
|
||||
// engine authenticates with.
|
||||
type netboxConfig struct {
|
||||
NetboxURL string `json:"netbox_url"`
|
||||
// Token is the seeded admin credential (write-only from the API). For v2
|
||||
// tokens this is the full "nbt_<key>.<secret>" string; for v1 the bare
|
||||
// 40-char value.
|
||||
Token string `json:"token"`
|
||||
// AdminUserID / AdminTokenID identify the seeded token so config/rotate can
|
||||
// mint a replacement for the same user and delete the old token. Optional for
|
||||
// v2 (auto-discovered from the key); required for v1 rotation.
|
||||
AdminUserID int `json:"admin_user_id"`
|
||||
AdminTokenID int `json:"admin_token_id"`
|
||||
// TokenVersion is the NetBox token version to request when minting (default
|
||||
// 2). Set to 1 when the NetBox server has no API_TOKEN_PEPPERS configured.
|
||||
TokenVersion int `json:"token_version"`
|
||||
CACert string `json:"ca_cert"`
|
||||
TLSSkipVerify bool `json:"tls_skip_verify"`
|
||||
RequestTimeoutSeconds int `json:"request_timeout_seconds"`
|
||||
}
|
||||
|
||||
func pathConfig(b *netboxBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "netbox",
|
||||
OperationSuffix: "config",
|
||||
},
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"netbox_url": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Base URL of the NetBox server, e.g. https://netbox.example.com.",
|
||||
Required: true,
|
||||
},
|
||||
"token": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Seeded NetBox admin API token used to mint per-user tokens. Write-only. v2: the full nbt_<key>.<secret> string; v1: the bare 40-char value.",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
Name: "Admin Token",
|
||||
Sensitive: true,
|
||||
},
|
||||
},
|
||||
"admin_user_id": {
|
||||
Type: framework.TypeInt,
|
||||
Description: "NetBox user id of the seeded admin token, so config/rotate can reissue it. Auto-discovered for v2 tokens if omitted.",
|
||||
},
|
||||
"admin_token_id": {
|
||||
Type: framework.TypeInt,
|
||||
Description: "NetBox token id of the seeded admin token, so config/rotate can delete it after reissue. Auto-discovered for v2 tokens if omitted.",
|
||||
},
|
||||
"token_version": {
|
||||
Type: framework.TypeInt,
|
||||
Description: "NetBox token version to request when minting (default 2). Use 1 if the NetBox server has no API_TOKEN_PEPPERS configured.",
|
||||
Default: 2,
|
||||
},
|
||||
"ca_cert": {
|
||||
Type: framework.TypeString,
|
||||
Description: "PEM CA certificate that signed the NetBox server's TLS certificate.",
|
||||
},
|
||||
"tls_skip_verify": {
|
||||
Type: framework.TypeBool,
|
||||
Description: "Skip TLS verification of the NetBox server (not recommended).",
|
||||
Default: false,
|
||||
},
|
||||
"request_timeout_seconds": {
|
||||
Type: framework.TypeInt,
|
||||
Description: "HTTP timeout in seconds for calls to NetBox (default 30).",
|
||||
Default: 30,
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{Callback: b.pathConfigRead},
|
||||
logical.CreateOperation: &framework.PathOperation{Callback: b.pathConfigWrite},
|
||||
logical.UpdateOperation: &framework.PathOperation{Callback: b.pathConfigWrite},
|
||||
logical.DeleteOperation: &framework.PathOperation{Callback: b.pathConfigDelete},
|
||||
},
|
||||
ExistenceCheck: b.pathConfigExistenceCheck,
|
||||
HelpSynopsis: "Configure the connection to NetBox and the seeded admin token.",
|
||||
HelpDescription: "Configure the URL, TLS settings, and seeded admin token the backend uses to mint NetBox tokens. Roles are configured on roles/<name>.",
|
||||
}
|
||||
}
|
||||
|
||||
func pathConfigRotate(b *netboxBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "config/rotate$",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "netbox",
|
||||
OperationSuffix: "config-rotate",
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.UpdateOperation: &framework.PathOperation{Callback: b.pathConfigRotate},
|
||||
},
|
||||
HelpSynopsis: "Reissue the seeded NetBox admin token.",
|
||||
HelpDescription: "Mints a fresh admin token for the seeded user with the current token, stores it, and deletes the old token. NetBox has no in-place rotation.",
|
||||
}
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathConfigExistenceCheck(ctx context.Context, req *logical.Request, _ *framework.FieldData) (bool, error) {
|
||||
config, err := getConfig(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return config != nil, nil
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathConfigRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
config, err := getConfig(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, nil
|
||||
}
|
||||
// The admin token is never returned.
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"netbox_url": config.NetboxURL,
|
||||
"admin_user_id": config.AdminUserID,
|
||||
"admin_token_id": config.AdminTokenID,
|
||||
"token_version": config.TokenVersion,
|
||||
"tls_skip_verify": config.TLSSkipVerify,
|
||||
"request_timeout_seconds": config.RequestTimeoutSeconds,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathConfigWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
config, err := getConfig(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
if req.Operation == logical.UpdateOperation {
|
||||
return nil, errors.New("config not found during update operation")
|
||||
}
|
||||
config = &netboxConfig{}
|
||||
}
|
||||
|
||||
if v, ok := data.GetOk("netbox_url"); ok {
|
||||
config.NetboxURL = v.(string)
|
||||
}
|
||||
if v, ok := data.GetOk("token"); ok {
|
||||
config.Token = v.(string)
|
||||
}
|
||||
if v, ok := data.GetOk("admin_user_id"); ok {
|
||||
config.AdminUserID = v.(int)
|
||||
}
|
||||
if v, ok := data.GetOk("admin_token_id"); ok {
|
||||
config.AdminTokenID = v.(int)
|
||||
}
|
||||
if v, ok := data.GetOk("token_version"); ok {
|
||||
config.TokenVersion = v.(int)
|
||||
} else if req.Operation == logical.CreateOperation {
|
||||
config.TokenVersion = data.Get("token_version").(int)
|
||||
}
|
||||
if v, ok := data.GetOk("ca_cert"); ok {
|
||||
config.CACert = v.(string)
|
||||
}
|
||||
if v, ok := data.GetOk("tls_skip_verify"); ok {
|
||||
config.TLSSkipVerify = v.(bool)
|
||||
}
|
||||
if v, ok := data.GetOk("request_timeout_seconds"); ok {
|
||||
config.RequestTimeoutSeconds = v.(int)
|
||||
} else if req.Operation == logical.CreateOperation {
|
||||
config.RequestTimeoutSeconds = data.Get("request_timeout_seconds").(int)
|
||||
}
|
||||
|
||||
if config.NetboxURL == "" {
|
||||
return logical.ErrorResponse("netbox_url is required"), nil
|
||||
}
|
||||
if config.Token == "" {
|
||||
return logical.ErrorResponse("token (admin) is required"), nil
|
||||
}
|
||||
if config.TokenVersion != 1 && config.TokenVersion != 2 {
|
||||
return logical.ErrorResponse("token_version must be 1 or 2"), nil
|
||||
}
|
||||
|
||||
return nil, setJSON(ctx, req.Storage, configStoragePath, config)
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathConfigDelete(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
return nil, req.Storage.Delete(ctx, configStoragePath)
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathConfigRotate(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
config, err := getConfig(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if config == nil {
|
||||
return nil, errBackendNotConfigured
|
||||
}
|
||||
|
||||
client, err := newClient(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Resolve the ids of the current admin token so we can reissue for the same
|
||||
// user and delete the old one. Auto-discover from the v2 key if not stored.
|
||||
userID, oldTokenID := config.AdminUserID, config.AdminTokenID
|
||||
if (userID == 0 || oldTokenID == 0) && strings.HasPrefix(config.Token, tokenPrefix) {
|
||||
if key := adminTokenKey(config.Token); key != "" {
|
||||
if id, uid, lerr := client.LookupTokenByKey(ctx, key); lerr == nil {
|
||||
if oldTokenID == 0 {
|
||||
oldTokenID = id
|
||||
}
|
||||
if userID == 0 {
|
||||
userID = uid
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if userID == 0 {
|
||||
return logical.ErrorResponse("admin_user_id is unknown; set it on config to enable rotation"), nil
|
||||
}
|
||||
|
||||
minted, err := client.MintToken(ctx, mintRequest{
|
||||
UserID: userID,
|
||||
WriteEnabled: true,
|
||||
Description: "vault-managed netbox admin token",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minting replacement admin token: %w", err)
|
||||
}
|
||||
|
||||
config.Token = credentialFor(minted)
|
||||
config.AdminUserID = userID
|
||||
config.AdminTokenID = minted.ID
|
||||
if err := setJSON(ctx, req.Storage, configStoragePath, config); err != nil {
|
||||
return nil, fmt.Errorf("persisting rotated admin token: %w", err)
|
||||
}
|
||||
|
||||
// Delete the superseded token using the new credential.
|
||||
if oldTokenID != 0 && oldTokenID != minted.ID {
|
||||
if newClient, cerr := newClient(config); cerr == nil {
|
||||
if derr := newClient.DeleteToken(ctx, oldTokenID); derr != nil {
|
||||
b.Logger().Warn("netbox: could not delete superseded admin token", "token_id", oldTokenID, "error", derr)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"admin_user_id": config.AdminUserID,
|
||||
"admin_token_id": config.AdminTokenID,
|
||||
"rotated_at": time.Now().UTC().Format(time.RFC3339),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// adminTokenKey extracts the v2 identification key from an "nbt_<key>.<secret>"
|
||||
// credential, or "" if the shape is not recognised.
|
||||
func adminTokenKey(credential string) string {
|
||||
rest := strings.TrimPrefix(credential, tokenPrefix)
|
||||
if key, _, ok := strings.Cut(rest, "."); ok {
|
||||
return key
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func getConfig(ctx context.Context, s logical.Storage) (*netboxConfig, error) {
|
||||
entry, err := s.Get(ctx, configStoragePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
config := &netboxConfig{}
|
||||
if err := entry.DecodeJSON(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// setJSON stores a value as a JSON storage entry.
|
||||
func setJSON(ctx context.Context, s logical.Storage, key string, value interface{}) error {
|
||||
entry, err := logical.StorageEntryJSON(key, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.Put(ctx, entry)
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
package netbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
func pathCredentials(b *netboxBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "creds/" + framework.GenericNameRegex("name"),
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "netbox",
|
||||
OperationSuffix: "credentials",
|
||||
},
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeLowerCaseString,
|
||||
Description: "Name of the role to mint a token for.",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{Callback: b.pathCredentialsRead},
|
||||
logical.UpdateOperation: &framework.PathOperation{Callback: b.pathCredentialsRead},
|
||||
},
|
||||
HelpSynopsis: "Mint a NetBox token from a role.",
|
||||
HelpDescription: "Reading this path mints a new, lease-bound NetBox API token for the role's user with its NetBox expiry aligned to the lease; the token is deleted from NetBox when the lease is revoked.",
|
||||
}
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathCredentialsRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
roleName := data.Get("name").(string)
|
||||
role, err := b.getRole(ctx, req.Storage, roleName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil {
|
||||
return logical.ErrorResponse("role %q does not exist", roleName), nil
|
||||
}
|
||||
|
||||
client, err := b.client(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ttl, maxTTL := b.resolveTTLs(role.TTL, role.MaxTTL)
|
||||
|
||||
description := role.Description
|
||||
if description == "" {
|
||||
description = fmt.Sprintf("vault dynamic token (role %q)", roleName)
|
||||
}
|
||||
|
||||
minted, err := client.MintToken(ctx, mintRequest{
|
||||
UserID: role.NetboxUserID,
|
||||
WriteEnabled: role.WriteEnabled,
|
||||
Description: description,
|
||||
Expires: time.Now().Add(ttl),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("minting netbox token: %w", err)
|
||||
}
|
||||
|
||||
credential := credentialFor(minted)
|
||||
|
||||
internal := map[string]interface{}{
|
||||
"token_id": minted.ID,
|
||||
}
|
||||
external := map[string]interface{}{
|
||||
"token": credential,
|
||||
"token_scheme": schemeFor(minted.Version),
|
||||
"authorization": schemeFor(minted.Version) + " " + credential,
|
||||
"key": minted.Key,
|
||||
"version": minted.Version,
|
||||
"netbox_user_id": role.NetboxUserID,
|
||||
"write_enabled": role.WriteEnabled,
|
||||
"expires": minted.Expires,
|
||||
}
|
||||
|
||||
resp := b.Secret(netboxTokenType).Response(external, internal)
|
||||
resp.Secret.TTL = ttl
|
||||
resp.Secret.MaxTTL = maxTTL
|
||||
resp.Secret.Renewable = true
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// schemeFor returns the HTTP Authorization scheme keyword for a token version.
|
||||
func schemeFor(version int) string {
|
||||
if version == 2 {
|
||||
return "Bearer"
|
||||
}
|
||||
return "Token"
|
||||
}
|
||||
|
||||
// resolveTTLs clamps a role's TTL/MaxTTL against the mount and system limits.
|
||||
func (b *netboxBackend) resolveTTLs(roleTTL, roleMaxTTL time.Duration) (ttl, maxTTL time.Duration) {
|
||||
sysMaxTTL := b.System().MaxLeaseTTL()
|
||||
maxTTL = roleMaxTTL
|
||||
if maxTTL <= 0 || maxTTL > sysMaxTTL {
|
||||
maxTTL = sysMaxTTL
|
||||
}
|
||||
ttl = roleTTL
|
||||
if ttl <= 0 {
|
||||
ttl = b.System().DefaultLeaseTTL()
|
||||
}
|
||||
if ttl > maxTTL {
|
||||
ttl = maxTTL
|
||||
}
|
||||
return ttl, maxTTL
|
||||
}
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
package netbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const roleStoragePrefix = "role/"
|
||||
|
||||
// netboxRole mints NetBox tokens for a pre-existing NetBox service user. Each
|
||||
// read of creds/<name> produces a unique, lease-bound token for NetboxUserID.
|
||||
type netboxRole struct {
|
||||
// NetboxUserID is the id of the NetBox user tokens are minted for.
|
||||
NetboxUserID int `json:"netbox_user_id"`
|
||||
// NetboxUsername is informational (the resolved user's name), kept for
|
||||
// readability of role reads.
|
||||
NetboxUsername string `json:"netbox_username"`
|
||||
// WriteEnabled controls whether minted tokens permit write operations.
|
||||
// Defaults to false so roles are read-only unless explicitly opted in.
|
||||
WriteEnabled bool `json:"write_enabled"`
|
||||
// Description is applied to each minted token (helps auditing in NetBox).
|
||||
Description string `json:"description"`
|
||||
TTL time.Duration `json:"ttl"`
|
||||
MaxTTL time.Duration `json:"max_ttl"`
|
||||
}
|
||||
|
||||
func pathRole(b *netboxBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "roles/" + framework.GenericNameRegex("name"),
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "netbox",
|
||||
OperationSuffix: "role",
|
||||
},
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"name": {
|
||||
Type: framework.TypeLowerCaseString,
|
||||
Description: "Name of the role.",
|
||||
Required: true,
|
||||
},
|
||||
"netbox_user_id": {
|
||||
Type: framework.TypeInt,
|
||||
Description: "Id of the pre-existing NetBox service user that minted tokens belong to. Either this or netbox_username is required.",
|
||||
},
|
||||
"netbox_username": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Username of the NetBox service user, resolved to an id at write time. Alternative to netbox_user_id.",
|
||||
},
|
||||
"write_enabled": {
|
||||
Type: framework.TypeBool,
|
||||
Description: "Whether minted tokens permit create/update/delete (default false: read-only tokens).",
|
||||
Default: false,
|
||||
},
|
||||
"description": {
|
||||
Type: framework.TypeString,
|
||||
Description: "Description applied to each minted NetBox token.",
|
||||
},
|
||||
"ttl": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: "Default lease TTL for tokens minted from this role. The minted token's NetBox expiry is aligned to the lease.",
|
||||
},
|
||||
"max_ttl": {
|
||||
Type: framework.TypeDurationSecond,
|
||||
Description: "Maximum lease TTL for tokens minted from this role.",
|
||||
},
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ReadOperation: &framework.PathOperation{Callback: b.pathRoleRead},
|
||||
logical.CreateOperation: &framework.PathOperation{Callback: b.pathRoleWrite},
|
||||
logical.UpdateOperation: &framework.PathOperation{Callback: b.pathRoleWrite},
|
||||
logical.DeleteOperation: &framework.PathOperation{Callback: b.pathRoleDelete},
|
||||
},
|
||||
ExistenceCheck: b.pathRoleExistenceCheck,
|
||||
HelpSynopsis: "Manage roles that mint NetBox tokens for a service user.",
|
||||
HelpDescription: "Each read of creds/<name> mints a unique, lease-bound NetBox token for the role's NetBox user.",
|
||||
}
|
||||
}
|
||||
|
||||
func pathRolesList(b *netboxBackend) *framework.Path {
|
||||
return &framework.Path{
|
||||
Pattern: "roles/?$",
|
||||
DisplayAttrs: &framework.DisplayAttributes{
|
||||
OperationPrefix: "netbox",
|
||||
OperationSuffix: "roles",
|
||||
},
|
||||
Operations: map[logical.Operation]framework.OperationHandler{
|
||||
logical.ListOperation: &framework.PathOperation{Callback: b.pathRolesList},
|
||||
},
|
||||
HelpSynopsis: "List roles.",
|
||||
HelpDescription: "List the token-minting roles configured on this backend.",
|
||||
}
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathRoleExistenceCheck(ctx context.Context, req *logical.Request, data *framework.FieldData) (bool, error) {
|
||||
role, err := b.getRole(ctx, req.Storage, data.Get("name").(string))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return role != nil, nil
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathRoleRead(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
role, err := b.getRole(ctx, req.Storage, data.Get("name").(string))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return &logical.Response{
|
||||
Data: map[string]interface{}{
|
||||
"netbox_user_id": role.NetboxUserID,
|
||||
"netbox_username": role.NetboxUsername,
|
||||
"write_enabled": role.WriteEnabled,
|
||||
"description": role.Description,
|
||||
"ttl": int64(role.TTL.Seconds()),
|
||||
"max_ttl": int64(role.MaxTTL.Seconds()),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathRoleWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
name := data.Get("name").(string)
|
||||
role, err := b.getRole(ctx, req.Storage, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if role == nil {
|
||||
role = &netboxRole{}
|
||||
}
|
||||
|
||||
if v, ok := data.GetOk("netbox_user_id"); ok {
|
||||
role.NetboxUserID = v.(int)
|
||||
}
|
||||
if v, ok := data.GetOk("netbox_username"); ok {
|
||||
role.NetboxUsername = v.(string)
|
||||
}
|
||||
if v, ok := data.GetOk("write_enabled"); ok {
|
||||
role.WriteEnabled = v.(bool)
|
||||
}
|
||||
if v, ok := data.GetOk("description"); ok {
|
||||
role.Description = v.(string)
|
||||
}
|
||||
if v, ok := data.GetOk("ttl"); ok {
|
||||
role.TTL = time.Duration(v.(int)) * time.Second
|
||||
}
|
||||
if v, ok := data.GetOk("max_ttl"); ok {
|
||||
role.MaxTTL = time.Duration(v.(int)) * time.Second
|
||||
}
|
||||
|
||||
// Resolve a username to an id if no id was given directly.
|
||||
if role.NetboxUserID == 0 && role.NetboxUsername != "" {
|
||||
client, cerr := b.client(ctx, req.Storage)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
id, rerr := client.ResolveUserID(ctx, role.NetboxUsername)
|
||||
if rerr != nil {
|
||||
return logical.ErrorResponse("resolving netbox_username %q: %s", role.NetboxUsername, rerr), nil
|
||||
}
|
||||
role.NetboxUserID = id
|
||||
}
|
||||
|
||||
if role.NetboxUserID == 0 {
|
||||
return logical.ErrorResponse("netbox_user_id or netbox_username is required"), nil
|
||||
}
|
||||
if role.MaxTTL > 0 && role.TTL > role.MaxTTL {
|
||||
return logical.ErrorResponse("ttl must not exceed max_ttl"), nil
|
||||
}
|
||||
|
||||
return nil, setJSON(ctx, req.Storage, roleStoragePrefix+name, role)
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
return nil, req.Storage.Delete(ctx, roleStoragePrefix+data.Get("name").(string))
|
||||
}
|
||||
|
||||
func (b *netboxBackend) pathRolesList(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
entries, err := req.Storage.List(ctx, roleStoragePrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return logical.ListResponse(entries), nil
|
||||
}
|
||||
|
||||
func (b *netboxBackend) getRole(ctx context.Context, s logical.Storage, name string) (*netboxRole, error) {
|
||||
if name == "" {
|
||||
return nil, errors.New("missing role name")
|
||||
}
|
||||
entry, err := s.Get(ctx, roleStoragePrefix+name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entry == nil {
|
||||
return nil, nil
|
||||
}
|
||||
role := &netboxRole{}
|
||||
if err := entry.DecodeJSON(role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return role, nil
|
||||
}
|
||||
Executable
+44
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# Package the (already built) plugin binary into RPMs with nfpm. Builds one RPM
|
||||
# per target server: Vault (/opt/vault-plugins) and OpenBao (/opt/openbao-plugins).
|
||||
# Usage: scripts/build-rpm.sh [version] (version defaults to $CI_COMMIT_TAG)
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
cd "${ROOT_DIR}"
|
||||
|
||||
VERSION="${1:-${CI_COMMIT_TAG:-0.0.0-dev}}"
|
||||
VERSION="${VERSION#v}"
|
||||
BINARY="vault-plugin-secrets-netbox"
|
||||
DIST="dist"
|
||||
|
||||
if [ ! -f "${DIST}/${BINARY}" ]; then
|
||||
echo "ERROR: ${DIST}/${BINARY} not found; run 'make build' first" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
export PACKAGE_VERSION="${VERSION}"
|
||||
export PACKAGE_RELEASE="1"
|
||||
export PACKAGE_ARCH="amd64"
|
||||
export PACKAGE_PLATFORM="linux"
|
||||
export PACKAGE_DESCRIPTION="Vault/OpenBao secrets engine for NetBox API tokens (/api/users/tokens/)"
|
||||
export PACKAGE_MAINTAINER="Ben Vincent <ben@unkin.net>"
|
||||
export PACKAGE_HOMEPAGE="https://git.unkin.net/unkin/vault-plugin-secrets-netbox"
|
||||
export PACKAGE_LICENSE="MIT"
|
||||
|
||||
build_flavor() {
|
||||
export PACKAGE_NAME="$1"
|
||||
export PACKAGE_PLUGIN_DIR="$2"
|
||||
export PACKAGE_PREINSTALL="${DIST}/preinstall-${PACKAGE_NAME}.sh"
|
||||
envsubst '${PACKAGE_PLUGIN_DIR}' < packaging/scripts/preinstall.sh.tmpl > "${PACKAGE_PREINSTALL}"
|
||||
envsubst < packaging/nfpm.yaml > "${DIST}/nfpm-${PACKAGE_NAME}.yaml"
|
||||
nfpm pkg --config "${DIST}/nfpm-${PACKAGE_NAME}.yaml" --target "${DIST}" --packager rpm
|
||||
}
|
||||
|
||||
build_flavor "vault-plugin-secrets-netbox" "/opt/vault-plugins"
|
||||
build_flavor "openbao-plugin-secrets-netbox" "/opt/openbao-plugins"
|
||||
|
||||
echo "Built:"
|
||||
ls -1 "${DIST}"/*.rpm
|
||||
Executable
+102
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# End-to-end test for vault-plugin-secrets-netbox.
|
||||
#
|
||||
# Builds the plugin, brings up a mock NetBox token API plus both Vault and
|
||||
# OpenBao, then drives the identical lifecycle against each engine to prove the
|
||||
# same binary works on both:
|
||||
# configure -> rotate admin -> role -> creds -> renew -> revoke.
|
||||
#
|
||||
# Select engines with ENGINES (default "vault openbao"), e.g. ENGINES=openbao.
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
COMPOSE_FILE="${ROOT_DIR}/test/docker-compose.yml"
|
||||
COMPOSE="docker compose -f ${COMPOSE_FILE}"
|
||||
BINARY="vault-plugin-secrets-netbox"
|
||||
|
||||
SEED_TOKEN="nbt_admkey.admsecret"
|
||||
NETBOX_ADDR="http://127.0.0.1:8080" # mock netbox, from the host
|
||||
MOUNT="netbox"
|
||||
ENGINES="${ENGINES:-vault openbao}"
|
||||
|
||||
red() { printf '\033[31m%s\033[0m\n' "$*"; }
|
||||
green() { printf '\033[32m%s\033[0m\n' "$*"; }
|
||||
blue() { printf '\033[34m==> %s\033[0m\n' "$*"; }
|
||||
|
||||
cleanup() { blue "Tearing down containers"; ${COMPOSE} down -v >/dev/null 2>&1 || true; }
|
||||
trap cleanup EXIT
|
||||
fail() { red "FAIL: $*"; exit 1; }
|
||||
|
||||
wait_for() {
|
||||
local desc="$1"; shift
|
||||
local i=0
|
||||
until "$@" >/dev/null 2>&1; do
|
||||
i=$((i + 1))
|
||||
[ "$i" -ge "${WAIT_RETRIES:-90}" ] && fail "timed out waiting for ${desc}"
|
||||
sleep 2
|
||||
done
|
||||
green "ready: ${desc}"
|
||||
}
|
||||
|
||||
jq_field() { python3 -c "import sys,json;print(json.load(sys.stdin)$1)"; }
|
||||
|
||||
run_engine() {
|
||||
local engine="$1" container="$2" cli="$3"
|
||||
blue "[${engine}] exercising the plugin"
|
||||
ex() { ${COMPOSE} exec -T "${container}" "${cli}" "$@"; }
|
||||
|
||||
local sha; sha="$(sha256sum "${ROOT_DIR}/dist/${BINARY}" | awk '{print $1}')"
|
||||
ex plugin register -sha256="${sha}" secret "${BINARY}" >/dev/null || true
|
||||
ex secrets disable "${MOUNT}" >/dev/null 2>&1 || true
|
||||
ex secrets enable -path="${MOUNT}" "${BINARY}" >/dev/null
|
||||
green "[${engine}] plugin registered and mounted"
|
||||
|
||||
# The plugin runs inside the engine container, so it reaches netbox by name.
|
||||
ex write "${MOUNT}/config" netbox_url="http://netbox:8080" token="${SEED_TOKEN}" tls_skip_verify=true >/dev/null
|
||||
green "[${engine}] configured"
|
||||
|
||||
# Reissue the seeded admin token (auto-discovers ids from the v2 key).
|
||||
ex write -f "${MOUNT}/config/rotate" >/dev/null
|
||||
green "[${engine}] admin token rotated"
|
||||
|
||||
# --- role + dynamic creds (read-only by default) ---
|
||||
ex write "${MOUNT}/roles/ipam" netbox_username="svc-terraform-ipam" write_enabled=true ttl=1h max_ttl=24h >/dev/null
|
||||
local json lease tok auth
|
||||
json="$(ex read -format=json "${MOUNT}/creds/ipam")"
|
||||
lease="$(printf '%s' "${json}" | jq_field '["lease_id"]')"
|
||||
tok="$(printf '%s' "${json}" | jq_field '["data"]["token"]')"
|
||||
auth="$(printf '%s' "${json}" | jq_field '["data"]["authorization"]')"
|
||||
[ -n "${tok}" ] || fail "[${engine}] dynamic creds returned empty token value"
|
||||
case "${auth}" in Bearer\ nbt_*) : ;; *) fail "[${engine}] unexpected authorization: ${auth}" ;; esac
|
||||
green "[${engine}] dynamic token issued (lease ${lease})"
|
||||
|
||||
# renew extends the lease (and the NetBox expiry)
|
||||
ex lease renew "${lease}" >/dev/null
|
||||
green "[${engine}] lease renewed"
|
||||
|
||||
# revoke -> token deleted from netbox (re-reading its creds still works via admin)
|
||||
ex lease revoke "${lease}" >/dev/null
|
||||
green "[${engine}] revoked"
|
||||
|
||||
green "[${engine}] PASSED"
|
||||
}
|
||||
|
||||
blue "Building plugin for linux/amd64"
|
||||
OS=linux ARCH=amd64 PLUGIN_DIR="${ROOT_DIR}/dist" make -C "${ROOT_DIR}" build
|
||||
|
||||
blue "Starting Docker stack (netbox + vault + openbao)"
|
||||
${COMPOSE} up -d --build
|
||||
|
||||
wait_for "netbox" curl -fsS "${NETBOX_ADDR}/healthz"
|
||||
|
||||
for engine in ${ENGINES}; do
|
||||
case "${engine}" in
|
||||
vault) wait_for "vault" ${COMPOSE} exec -T vault vault status -address=http://127.0.0.1:8200; run_engine vault vault vault ;;
|
||||
openbao) wait_for "openbao" ${COMPOSE} exec -T openbao bao status -address=http://127.0.0.1:8200; run_engine openbao openbao bao ;;
|
||||
*) fail "unknown engine: ${engine}" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
green "ALL END-TO-END CHECKS PASSED (${ENGINES})"
|
||||
+100
@@ -0,0 +1,100 @@
|
||||
package netbox
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/sdk/framework"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
)
|
||||
|
||||
const netboxTokenType = "netbox_token"
|
||||
|
||||
// expiryBuffer keeps the NetBox token's expiry slightly ahead of the Vault lease
|
||||
// so it never lapses a moment before the lease it is bound to.
|
||||
const expiryBuffer = time.Minute
|
||||
|
||||
func (b *netboxBackend) netboxTokenSecret() *framework.Secret {
|
||||
return &framework.Secret{
|
||||
Type: netboxTokenType,
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"token": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The usable NetBox token credential (v2: nbt_<key>.<secret>; v1: bare value).",
|
||||
},
|
||||
"authorization": {
|
||||
Type: framework.TypeString,
|
||||
Description: "The full Authorization header value for the token.",
|
||||
},
|
||||
},
|
||||
Revoke: b.secretRevoke,
|
||||
Renew: b.secretRenew,
|
||||
}
|
||||
}
|
||||
|
||||
// secretRevoke deletes the minted NetBox token via the API, using the seeded
|
||||
// admin token.
|
||||
func (b *netboxBackend) secretRevoke(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
tokenID, err := internalInt(req.Secret.InternalData, "token_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := b.client(ctx, req.Storage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := client.DeleteToken(ctx, tokenID); err != nil {
|
||||
return nil, fmt.Errorf("revoking netbox token %d: %w", tokenID, err)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// secretRenew extends the Vault lease and pushes the NetBox token's expiry
|
||||
// forward to match, since NetBox permits updating a token's expires.
|
||||
func (b *netboxBackend) secretRenew(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
|
||||
tokenID, err := internalInt(req.Secret.InternalData, "token_id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.Secret.TTL > 0 {
|
||||
client, cerr := b.client(ctx, req.Storage)
|
||||
if cerr != nil {
|
||||
return nil, cerr
|
||||
}
|
||||
newExpiry := time.Now().Add(req.Secret.TTL + expiryBuffer)
|
||||
if err := client.ExtendToken(ctx, tokenID, newExpiry); err != nil {
|
||||
return nil, fmt.Errorf("extending netbox token %d expiry: %w", tokenID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return &logical.Response{Secret: req.Secret}, nil
|
||||
}
|
||||
|
||||
// internalInt reads an integer from a secret's internal data, tolerating the
|
||||
// float64/json.Number shapes JSON round-tripping produces.
|
||||
func internalInt(data map[string]interface{}, key string) (int, error) {
|
||||
raw, ok := data[key]
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("secret is missing internal %s data", key)
|
||||
}
|
||||
switch n := raw.(type) {
|
||||
case int:
|
||||
return n, nil
|
||||
case int64:
|
||||
return int(n), nil
|
||||
case float64:
|
||||
return int(n), nil
|
||||
case json.Number:
|
||||
i, err := n.Int64()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("secret internal %s data is not an integer: %w", key, err)
|
||||
}
|
||||
return int(i), nil
|
||||
default:
|
||||
return 0, fmt.Errorf("secret internal %s data has unexpected type %T", key, raw)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
# End-to-end test stack. A mock NetBox token API (in-memory, no database) plus
|
||||
# two secrets-engine hosts running the exact same plugin binary: HashiCorp Vault
|
||||
# and OpenBao. Bind mounts use ":z" so they work under SELinux.
|
||||
services:
|
||||
netbox:
|
||||
image: golang:1.25-alpine
|
||||
working_dir: /src
|
||||
environment:
|
||||
MOCKNETBOX_ADDR: ":8080"
|
||||
MOCKNETBOX_TOKEN: "nbt_admkey.admsecret"
|
||||
GOFLAGS: "-mod=mod"
|
||||
command: ["go", "run", "./test/mocknetbox"]
|
||||
volumes:
|
||||
- ..:/src:ro,z
|
||||
ports:
|
||||
- "8080:8080"
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:8080/healthz"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 40
|
||||
|
||||
vault:
|
||||
image: hashicorp/vault:1.18
|
||||
depends_on:
|
||||
netbox:
|
||||
condition: service_healthy
|
||||
cap_add: [IPC_LOCK]
|
||||
environment:
|
||||
VAULT_DEV_ROOT_TOKEN_ID: root
|
||||
VAULT_ADDR: http://127.0.0.1:8200
|
||||
VAULT_TOKEN: root
|
||||
command: ["server", "-dev", "-dev-listen-address=0.0.0.0:8200", "-config=/vault/vault.hcl"]
|
||||
volumes:
|
||||
- ../dist:/vault/plugins:ro,z
|
||||
- ./vault/vault.hcl:/vault/vault.hcl:ro,z
|
||||
ports: ["8200:8200"]
|
||||
healthcheck:
|
||||
test: ["CMD", "vault", "status", "-address=http://127.0.0.1:8200"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
|
||||
openbao:
|
||||
image: openbao/openbao:latest
|
||||
depends_on:
|
||||
netbox:
|
||||
condition: service_healthy
|
||||
cap_add: [IPC_LOCK]
|
||||
environment:
|
||||
BAO_DEV_ROOT_TOKEN_ID: root
|
||||
BAO_ADDR: http://127.0.0.1:8200
|
||||
BAO_TOKEN: root
|
||||
command: ["server", "-dev", "-dev-listen-address=0.0.0.0:8200", "-config=/openbao/bao.hcl"]
|
||||
volumes:
|
||||
- ../dist:/openbao/plugins:ro,z
|
||||
- ./openbao/bao.hcl:/openbao/bao.hcl:ro,z
|
||||
ports: ["8300:8200"]
|
||||
healthcheck:
|
||||
test: ["CMD", "bao", "status", "-address=http://127.0.0.1:8200"]
|
||||
interval: 3s
|
||||
timeout: 3s
|
||||
retries: 20
|
||||
@@ -0,0 +1,160 @@
|
||||
// Command mocknetbox is an in-memory stand-in for the NetBox token API used by
|
||||
// the e2e tests. It implements just enough of /api/users/tokens/ (create by user,
|
||||
// lookup by key, patch expires, delete) and /api/users/users/ (lookup by
|
||||
// username) that the plugin exercises — no NetBox or database required.
|
||||
//
|
||||
// Any credential it has issued (or the seed admin, MOCKNETBOX_TOKEN) is accepted
|
||||
// as auth, so admin-token rotation chains work exactly as the plugin relies on.
|
||||
// Not for production use.
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenPrefix = "nbt_"
|
||||
tokensPath = "/api/users/tokens/"
|
||||
usersPath = "/api/users/users/"
|
||||
)
|
||||
|
||||
type tok struct {
|
||||
id int
|
||||
key string
|
||||
plaintext string
|
||||
version int
|
||||
writeEnabled bool
|
||||
expires string
|
||||
userID int
|
||||
}
|
||||
|
||||
func (t *tok) credential() string {
|
||||
if t.version == 2 {
|
||||
return tokenPrefix + t.key + "." + t.plaintext
|
||||
}
|
||||
return t.plaintext
|
||||
}
|
||||
|
||||
type store struct {
|
||||
mu sync.Mutex
|
||||
seq int
|
||||
tokens map[int]*tok
|
||||
valid map[string]bool
|
||||
users map[string]int
|
||||
}
|
||||
|
||||
func (s *store) handle(w http.ResponseWriter, r *http.Request) {
|
||||
cred := strings.TrimPrefix(strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer "), "Token ")
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if !s.valid[cred] {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
switch {
|
||||
case r.URL.Path == usersPath && r.Method == http.MethodGet:
|
||||
results := []map[string]any{}
|
||||
if id, ok := s.users[r.URL.Query().Get("username")]; ok {
|
||||
results = append(results, map[string]any{"id": id})
|
||||
}
|
||||
writeJSON(w, 200, map[string]any{"results": results})
|
||||
|
||||
case r.URL.Path == tokensPath && r.Method == http.MethodPost:
|
||||
var in struct {
|
||||
User int `json:"user"`
|
||||
WriteEnabled bool `json:"write_enabled"`
|
||||
Version int `json:"version"`
|
||||
Expires string `json:"expires"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&in)
|
||||
s.seq++
|
||||
t := &tok{id: s.seq, plaintext: fmt.Sprintf("secret%d", s.seq), version: in.Version, writeEnabled: in.WriteEnabled, expires: in.Expires, userID: in.User}
|
||||
if t.version == 0 {
|
||||
t.version = 2
|
||||
}
|
||||
if t.version == 2 {
|
||||
t.key = fmt.Sprintf("key%d", s.seq)
|
||||
}
|
||||
s.tokens[t.id] = t
|
||||
s.valid[t.credential()] = true
|
||||
writeJSON(w, 201, map[string]any{"id": t.id, "key": t.key, "token": t.plaintext, "version": t.version, "write_enabled": t.writeEnabled, "expires": t.expires, "user": map[string]any{"id": t.userID}})
|
||||
|
||||
case r.URL.Path == tokensPath && r.Method == http.MethodGet:
|
||||
key := r.URL.Query().Get("key")
|
||||
results := []map[string]any{}
|
||||
for _, t := range s.tokens {
|
||||
if key != "" && t.key == key {
|
||||
results = append(results, map[string]any{"id": t.id, "key": t.key, "version": t.version, "user": map[string]any{"id": t.userID}})
|
||||
}
|
||||
}
|
||||
writeJSON(w, 200, map[string]any{"results": results})
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, tokensPath):
|
||||
id, err := strconv.Atoi(strings.Trim(strings.TrimPrefix(r.URL.Path, tokensPath), "/"))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
t, ok := s.tokens[id]
|
||||
if !ok {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case http.MethodPatch:
|
||||
var in struct {
|
||||
Expires string `json:"expires"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&in)
|
||||
t.expires = in.Expires
|
||||
writeJSON(w, 200, map[string]any{"id": t.id, "expires": t.expires})
|
||||
case http.MethodDelete:
|
||||
delete(s.tokens, id)
|
||||
delete(s.valid, t.credential())
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
default:
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
}
|
||||
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, code int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(code)
|
||||
_ = json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
|
||||
func main() {
|
||||
addr := os.Getenv("MOCKNETBOX_ADDR")
|
||||
if addr == "" {
|
||||
addr = ":8080"
|
||||
}
|
||||
seed := os.Getenv("MOCKNETBOX_TOKEN")
|
||||
if seed == "" {
|
||||
seed = "nbt_admkey.admsecret"
|
||||
}
|
||||
adminUser := 1
|
||||
s := &store{
|
||||
tokens: map[int]*tok{1: {id: 1, key: "admkey", plaintext: "admsecret", version: 2, writeEnabled: true, userID: adminUser}},
|
||||
valid: map[string]bool{seed: true},
|
||||
users: map[string]int{"svc-terraform-ipam": 42, "svc-puppet-facts": 43},
|
||||
}
|
||||
s.seq = 1
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc(usersPath, s.handle)
|
||||
mux.HandleFunc(tokensPath, s.handle)
|
||||
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("ok")) })
|
||||
log.Printf("mock netbox token API listening on %s", addr)
|
||||
log.Fatal(http.ListenAndServe(addr, mux)) //nolint:gosec // test-only mock
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
# OpenBao is plugin-protocol compatible with Vault, so the very same plugin
|
||||
# binary registers and runs here unchanged. Combined with `-dev` at runtime.
|
||||
plugin_directory = "/openbao/plugins"
|
||||
api_addr = "http://127.0.0.1:8200"
|
||||
@@ -0,0 +1,4 @@
|
||||
# Combined with `-dev` at runtime; supplies the plugin_directory the dev server
|
||||
# would otherwise leave unset, so the plugin binary in ../dist can be registered.
|
||||
plugin_directory = "/vault/plugins"
|
||||
api_addr = "http://127.0.0.1:8200"
|
||||
Reference in New Issue
Block a user