1package rabbitmq
2
3import (
4	"context"
5	"fmt"
6	"strings"
7	"sync"
8
9	cleanhttp "github.com/hashicorp/go-cleanhttp"
10	"github.com/hashicorp/vault/sdk/framework"
11	"github.com/hashicorp/vault/sdk/logical"
12	rabbithole "github.com/michaelklishin/rabbit-hole"
13)
14
15// Factory creates and configures the backend
16func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
17	b := Backend()
18	if err := b.Setup(ctx, conf); err != nil {
19		return nil, err
20	}
21	return b, nil
22}
23
24// Creates a new backend with all the paths and secrets belonging to it
25func Backend() *backend {
26	var b backend
27	b.Backend = &framework.Backend{
28		Help: strings.TrimSpace(backendHelp),
29
30		PathsSpecial: &logical.Paths{
31			SealWrapStorage: []string{
32				"config/connection",
33			},
34		},
35
36		Paths: []*framework.Path{
37			pathConfigConnection(&b),
38			pathConfigLease(&b),
39			pathListRoles(&b),
40			pathCreds(&b),
41			pathRoles(&b),
42		},
43
44		Secrets: []*framework.Secret{
45			secretCreds(&b),
46		},
47
48		Clean:       b.resetClient,
49		Invalidate:  b.invalidate,
50		BackendType: logical.TypeLogical,
51	}
52
53	return &b
54}
55
56type backend struct {
57	*framework.Backend
58
59	client *rabbithole.Client
60	lock   sync.RWMutex
61}
62
63// DB returns the database connection.
64func (b *backend) Client(ctx context.Context, s logical.Storage) (*rabbithole.Client, error) {
65	b.lock.RLock()
66
67	// If we already have a client, return it
68	if b.client != nil {
69		b.lock.RUnlock()
70		return b.client, nil
71	}
72
73	b.lock.RUnlock()
74
75	// Otherwise, attempt to make connection
76	entry, err := s.Get(ctx, "config/connection")
77	if err != nil {
78		return nil, err
79	}
80	if entry == nil {
81		return nil, fmt.Errorf("configure the client connection with config/connection first")
82	}
83
84	var connConfig connectionConfig
85	if err := entry.DecodeJSON(&connConfig); err != nil {
86		return nil, err
87	}
88
89	b.lock.Lock()
90	defer b.lock.Unlock()
91
92	// If the client was created during the lock switch, return it
93	if b.client != nil {
94		return b.client, nil
95	}
96
97	b.client, err = rabbithole.NewClient(connConfig.URI, connConfig.Username, connConfig.Password)
98	if err != nil {
99		return nil, err
100	}
101	// Use a default pooled transport so there would be no leaked file descriptors
102	b.client.SetTransport(cleanhttp.DefaultPooledTransport())
103
104	return b.client, nil
105}
106
107// resetClient forces a connection next time Client() is called.
108func (b *backend) resetClient(_ context.Context) {
109	b.lock.Lock()
110	defer b.lock.Unlock()
111
112	b.client = nil
113}
114
115func (b *backend) invalidate(ctx context.Context, key string) {
116	switch key {
117	case "config/connection":
118		b.resetClient(ctx)
119	}
120}
121
122// Lease returns the lease information
123func (b *backend) Lease(ctx context.Context, s logical.Storage) (*configLease, error) {
124	entry, err := s.Get(ctx, "config/lease")
125	if err != nil {
126		return nil, err
127	}
128	if entry == nil {
129		return nil, nil
130	}
131
132	var result configLease
133	if err := entry.DecodeJSON(&result); err != nil {
134		return nil, err
135	}
136
137	return &result, nil
138}
139
140const backendHelp = `
141The RabbitMQ backend dynamically generates RabbitMQ users.
142
143After mounting this backend, configure it using the endpoints within
144the "config/" path.
145`
146