2023-01-10 07:02:10 +00:00
|
|
|
// Copyright © 2022 Roberto Hidalgo <joao@un.rob.mx>
|
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package vault
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"fmt"
|
|
|
|
"net/http"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
"git.rob.mx/nidito/joao/internal/vault/middleware"
|
|
|
|
"git.rob.mx/nidito/joao/pkg/version"
|
|
|
|
"github.com/1Password/connect-sdk-go/connect"
|
|
|
|
"github.com/hashicorp/vault/sdk/framework"
|
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
|
|
ttlcache "github.com/jellydator/ttlcache/v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
userAgent = "joao/%s"
|
|
|
|
)
|
|
|
|
|
|
|
|
type backend struct {
|
|
|
|
*framework.Backend
|
|
|
|
configCache *ttlcache.Cache[string, string]
|
|
|
|
client *connect.Client
|
|
|
|
}
|
|
|
|
|
|
|
|
var ConnectClientFactory func(s logical.Storage) (connect.Client, error) = onePasswordConnectClient
|
|
|
|
|
|
|
|
// Factory returns a new backend as logical.Backend.
|
|
|
|
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
|
2023-11-26 02:13:32 +00:00
|
|
|
b := newBackend()
|
2023-01-10 07:02:10 +00:00
|
|
|
if err := b.Setup(ctx, conf); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return b, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
type clientCallback func(client connect.Client, r *logical.Request, fd *framework.FieldData) (*logical.Response, error)
|
|
|
|
|
|
|
|
func withClient(b *backend, callback clientCallback) framework.OperationFunc {
|
|
|
|
return func(ctx context.Context, r *logical.Request, fd *framework.FieldData) (*logical.Response, error) {
|
|
|
|
client, err := b.Client(r.Storage)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("plugin is not configured: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return callback(client, r, fd)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func itemPattern(name string) string {
|
|
|
|
return fmt.Sprintf("(?P<%s>\\w(([\\w-.:]+)?\\w)?)", name)
|
|
|
|
}
|
|
|
|
|
|
|
|
func optionalVaultPattern(suffix string) string {
|
|
|
|
return fmt.Sprintf("(?P<vault>([\\w:]+)%s)?", suffix)
|
|
|
|
}
|
|
|
|
|
2023-11-26 02:13:32 +00:00
|
|
|
func newBackend() *backend {
|
2023-01-10 07:02:10 +00:00
|
|
|
var b = &backend{
|
|
|
|
configCache: ttlcache.New(
|
|
|
|
ttlcache.WithTTL[string, string](5 * time.Minute),
|
|
|
|
),
|
|
|
|
}
|
|
|
|
|
|
|
|
b.Backend = &framework.Backend{
|
2023-01-14 21:56:32 +00:00
|
|
|
BackendType: logical.TypeLogical,
|
2023-01-10 07:02:10 +00:00
|
|
|
Help: "joao reads configuration entries from 1Password Connect",
|
|
|
|
PathsSpecial: &logical.Paths{
|
|
|
|
SealWrapStorage: []string{
|
|
|
|
middleware.ConfigPath,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Paths: framework.PathAppend(
|
|
|
|
[]*framework.Path{
|
|
|
|
{
|
|
|
|
Pattern: middleware.ConfigPath,
|
|
|
|
HelpSynopsis: "Configures the connection to a 1Password Connect Server",
|
|
|
|
HelpDescription: "Provide a `host` and `token`, with an optional default `vault` to query 1Password Connect at",
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
|
|
"host": {
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "The address for the 1Password Connect server",
|
|
|
|
},
|
|
|
|
"token": {
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "A 1Password Connect token",
|
|
|
|
},
|
|
|
|
"vault": {
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "An optional vault id or name to use for queries",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
|
|
Callback: middleware.ReadConfig,
|
|
|
|
},
|
|
|
|
logical.UpdateOperation: &framework.PathOperation{
|
|
|
|
Callback: func(ctx context.Context, r *logical.Request, fd *framework.FieldData) (*logical.Response, error) {
|
|
|
|
res, err := middleware.WriteConfig(ctx, r, fd)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
b.client = nil
|
|
|
|
if _, err := b.Client(r.Storage); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return res, nil
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Pattern: "trees/" + optionalVaultPattern(""),
|
|
|
|
HelpSynopsis: `List configuration trees`,
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
|
|
logical.ListOperation: &framework.PathOperation{
|
|
|
|
Callback: withClient(b, middleware.ListTrees),
|
|
|
|
Summary: "List available entries",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
|
|
"vault": {
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "Specifies the id of the vault to list from.",
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{
|
|
|
|
Pattern: "tree/" + optionalVaultPattern("/") + itemPattern("id"),
|
|
|
|
HelpSynopsis: `Returns a configuration tree`,
|
|
|
|
Operations: map[logical.Operation]framework.OperationHandler{
|
|
|
|
logical.ReadOperation: &framework.PathOperation{
|
|
|
|
Callback: withClient(b, middleware.ReadTree),
|
|
|
|
Summary: "Retrieve nested key values from specified item",
|
|
|
|
},
|
|
|
|
},
|
|
|
|
Fields: map[string]*framework.FieldSchema{
|
|
|
|
"id": {
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "The item name or id to read",
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
"vault": {
|
|
|
|
Type: framework.TypeString,
|
|
|
|
Description: "The vault name or id to read from",
|
|
|
|
Required: true,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
),
|
|
|
|
Secrets: []*framework.Secret{},
|
|
|
|
}
|
|
|
|
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
func (b *backend) Client(s logical.Storage) (connect.Client, error) {
|
|
|
|
if b.client != nil {
|
|
|
|
return *b.client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
client, err := ConnectClientFactory(s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
b.client = &client
|
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func onePasswordConnectClient(s logical.Storage) (connect.Client, error) {
|
|
|
|
config, err := middleware.ConfigFromStorage(context.Background(), s)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("error retrieving config for client: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if config == nil {
|
|
|
|
return nil, fmt.Errorf("no config set for backend, write host, token and vault to [mount]/1password")
|
|
|
|
}
|
|
|
|
|
|
|
|
http.DefaultClient.Timeout = 15 * time.Second
|
|
|
|
client := connect.NewClientWithUserAgent(config.Host, config.Token, fmt.Sprintf(userAgent, version.Version))
|
|
|
|
|
|
|
|
return client, nil
|
|
|
|
}
|