feat(vault-tui): implement KV client and service for managing secrets
- Added internal/vault/client.go for creating a Vault client with configuration settings. - Introduced internal/vault/errors.go to classify Vault API errors for better UI handling. - Created internal/vault/kv.go to manage KV secrets, including listing, reading, writing, and deleting operations. - Implemented internal/vault/mounts.go to list and describe secret engine mounts. - Developed internal/vault/service.go to provide a unified entry point for Vault operations. - Added internal/vault/kv_test.go for comprehensive testing of KV operations. - Introduced internal/ui/toast.go for transient notifications in the UI. - Added renovate.json for dependency management and updates.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
func init() { register(ldapMethod{}) }
|
||||
|
||||
type ldapMethod struct{}
|
||||
|
||||
func (ldapMethod) Name() string { return "ldap" }
|
||||
func (ldapMethod) DisplayName() string { return "LDAP" }
|
||||
func (ldapMethod) DefaultMount() string { return "ldap" }
|
||||
|
||||
func (ldapMethod) Fields() []Field {
|
||||
return []Field{
|
||||
{Name: "username", Label: "Username", Kind: FieldText, Required: true,
|
||||
EnvFallback: []string{"VAULT_USERNAME", "LOGNAME", "USER"}},
|
||||
{Name: "password", Label: "Password", Kind: FieldSecret, Required: true,
|
||||
EnvFallback: []string{"VAULT_LDAP_PASSWORD", "VAULT_PASSWORD"}},
|
||||
}
|
||||
}
|
||||
|
||||
func (ldapMethod) Login(ctx context.Context, c *api.Client, req Request) (*api.Secret, error) {
|
||||
mount := mountOf(req, "ldap")
|
||||
return loginWrite(ctx, c, mount, "login/"+url.PathEscape(req.Creds.Get("username")),
|
||||
map[string]interface{}{"password": req.Creds.Get("password")})
|
||||
}
|
||||
|
||||
func init() { register(oktaMethod{}) }
|
||||
|
||||
// oktaMethod mirrors ldapMethod's request shape but adds an optional
|
||||
// best-effort poll of auth/<mount>/verify/<nonce> for Okta Verify
|
||||
// number-matching, reported via Request.Events so the TUI can show "tap 42
|
||||
// in Okta Verify". This endpoint is not documented in Vault's public API
|
||||
// reference; treat any poll failure as non-fatal and fall back to waiting
|
||||
// for the original login response.
|
||||
type oktaMethod struct{}
|
||||
|
||||
func (oktaMethod) Name() string { return "okta" }
|
||||
func (oktaMethod) DisplayName() string { return "Okta" }
|
||||
func (oktaMethod) DefaultMount() string { return "okta" }
|
||||
|
||||
func (oktaMethod) Description() string {
|
||||
return "Okta username/password, with optional TOTP and Okta Verify push."
|
||||
}
|
||||
|
||||
func (oktaMethod) Fields() []Field {
|
||||
return []Field{
|
||||
{Name: "username", Label: "Username", Kind: FieldText, Required: true,
|
||||
EnvFallback: []string{"VAULT_USERNAME", "LOGNAME", "USER"}},
|
||||
{Name: "password", Label: "Password", Kind: FieldSecret, Required: true,
|
||||
EnvFallback: []string{"VAULT_PASSWORD"}},
|
||||
{Name: "totp", Label: "TOTP code (optional)", Kind: FieldText},
|
||||
}
|
||||
}
|
||||
|
||||
func (oktaMethod) Login(ctx context.Context, c *api.Client, req Request) (*api.Secret, error) {
|
||||
mount := mountOf(req, "okta")
|
||||
data := map[string]interface{}{"password": req.Creds.Get("password")}
|
||||
if totp := req.Creds.Get("totp"); totp != "" {
|
||||
data["totp"] = totp
|
||||
}
|
||||
nonce := randomNonce(12)
|
||||
data["nonce"] = nonce
|
||||
|
||||
if req.Events != nil {
|
||||
go pollOktaVerify(ctx, c, mount, nonce, req)
|
||||
}
|
||||
req.Emit(Event{Kind: EventStatus, Message: "contacting Okta…"})
|
||||
return loginWrite(ctx, c, mount, "login/"+url.PathEscape(req.Creds.Get("username")), data)
|
||||
}
|
||||
|
||||
func pollOktaVerify(ctx context.Context, c *api.Client, mount, nonce string, req Request) {
|
||||
t := time.NewTicker(1 * time.Second)
|
||||
defer t.Stop()
|
||||
p := fmt.Sprintf("auth/%s/verify/%s", mount, nonce)
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-t.C:
|
||||
sec, err := c.Logical().ReadWithContext(ctx, p)
|
||||
if err != nil || sec == nil || sec.Data == nil {
|
||||
continue // best-effort; the primary login request is the source of truth
|
||||
}
|
||||
if answer, ok := sec.Data["correct_answer"].(string); ok && answer != "" {
|
||||
req.Emit(Event{Kind: EventStatus, Message: fmt.Sprintf("in Okta Verify, tap the number %q", answer)})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user