1package file
2
3import (
4	"bytes"
5	"context"
6	"fmt"
7	"io"
8	"os"
9	"path/filepath"
10	"strconv"
11	"strings"
12	"sync"
13	"sync/atomic"
14
15	"github.com/hashicorp/errwrap"
16	"github.com/hashicorp/vault/audit"
17	"github.com/hashicorp/vault/sdk/helper/salt"
18	"github.com/hashicorp/vault/sdk/logical"
19)
20
21func Factory(ctx context.Context, conf *audit.BackendConfig) (audit.Backend, error) {
22	if conf.SaltConfig == nil {
23		return nil, fmt.Errorf("nil salt config")
24	}
25	if conf.SaltView == nil {
26		return nil, fmt.Errorf("nil salt view")
27	}
28
29	path, ok := conf.Config["file_path"]
30	if !ok {
31		path, ok = conf.Config["path"]
32		if !ok {
33			return nil, fmt.Errorf("file_path is required")
34		}
35	}
36
37	// normalize path if configured for stdout
38	if strings.EqualFold(path, "stdout") {
39		path = "stdout"
40	}
41	if strings.EqualFold(path, "discard") {
42		path = "discard"
43	}
44
45	format, ok := conf.Config["format"]
46	if !ok {
47		format = "json"
48	}
49	switch format {
50	case "json", "jsonx":
51	default:
52		return nil, fmt.Errorf("unknown format type %q", format)
53	}
54
55	// Check if hashing of accessor is disabled
56	hmacAccessor := true
57	if hmacAccessorRaw, ok := conf.Config["hmac_accessor"]; ok {
58		value, err := strconv.ParseBool(hmacAccessorRaw)
59		if err != nil {
60			return nil, err
61		}
62		hmacAccessor = value
63	}
64
65	// Check if raw logging is enabled
66	logRaw := false
67	if raw, ok := conf.Config["log_raw"]; ok {
68		b, err := strconv.ParseBool(raw)
69		if err != nil {
70			return nil, err
71		}
72		logRaw = b
73	}
74
75	// Check if mode is provided
76	mode := os.FileMode(0600)
77	if modeRaw, ok := conf.Config["mode"]; ok {
78		m, err := strconv.ParseUint(modeRaw, 8, 32)
79		if err != nil {
80			return nil, err
81		}
82		if m != 0 {
83			mode = os.FileMode(m)
84		}
85	}
86
87	b := &Backend{
88		path:       path,
89		mode:       mode,
90		saltConfig: conf.SaltConfig,
91		saltView:   conf.SaltView,
92		salt:       new(atomic.Value),
93		formatConfig: audit.FormatterConfig{
94			Raw:          logRaw,
95			HMACAccessor: hmacAccessor,
96		},
97	}
98
99	// Ensure we are working with the right type by explicitly storing a nil of
100	// the right type
101	b.salt.Store((*salt.Salt)(nil))
102
103	switch format {
104	case "json":
105		b.formatter.AuditFormatWriter = &audit.JSONFormatWriter{
106			Prefix:   conf.Config["prefix"],
107			SaltFunc: b.Salt,
108		}
109	case "jsonx":
110		b.formatter.AuditFormatWriter = &audit.JSONxFormatWriter{
111			Prefix:   conf.Config["prefix"],
112			SaltFunc: b.Salt,
113		}
114	}
115
116	switch path {
117	case "stdout", "discard":
118		// no need to test opening file if outputting to stdout or discarding
119	default:
120		// Ensure that the file can be successfully opened for writing;
121		// otherwise it will be too late to catch later without problems
122		// (ref: https://github.com/hashicorp/vault/issues/550)
123		if err := b.open(); err != nil {
124			return nil, errwrap.Wrapf(fmt.Sprintf("sanity check failed; unable to open %q for writing: {{err}}", path), err)
125		}
126	}
127
128	return b, nil
129}
130
131// Backend is the audit backend for the file-based audit store.
132//
133// NOTE: This audit backend is currently very simple: it appends to a file.
134// It doesn't do anything more at the moment to assist with rotation
135// or reset the write cursor, this should be done in the future.
136type Backend struct {
137	path string
138
139	formatter    audit.AuditFormatter
140	formatConfig audit.FormatterConfig
141
142	fileLock sync.RWMutex
143	f        *os.File
144	mode     os.FileMode
145
146	saltMutex  sync.RWMutex
147	salt       *atomic.Value
148	saltConfig *salt.Config
149	saltView   logical.Storage
150}
151
152var _ audit.Backend = (*Backend)(nil)
153
154func (b *Backend) Salt(ctx context.Context) (*salt.Salt, error) {
155	s := b.salt.Load().(*salt.Salt)
156	if s != nil {
157		return s, nil
158	}
159
160	b.saltMutex.Lock()
161	defer b.saltMutex.Unlock()
162
163	s = b.salt.Load().(*salt.Salt)
164	if s != nil {
165		return s, nil
166	}
167
168	newSalt, err := salt.NewSalt(ctx, b.saltView, b.saltConfig)
169	if err != nil {
170		b.salt.Store((*salt.Salt)(nil))
171		return nil, err
172	}
173
174	b.salt.Store(newSalt)
175	return newSalt, nil
176}
177
178func (b *Backend) GetHash(ctx context.Context, data string) (string, error) {
179	salt, err := b.Salt(ctx)
180	if err != nil {
181		return "", err
182	}
183
184	return audit.HashString(salt, data), nil
185}
186
187func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
188	var writer io.Writer
189	switch b.path {
190	case "stdout":
191		writer = os.Stdout
192	case "discard":
193		return nil
194	}
195
196	buf := bytes.NewBuffer(make([]byte, 0, 2000))
197	err := b.formatter.FormatRequest(ctx, buf, b.formatConfig, in)
198	if err != nil {
199		return err
200	}
201
202	return b.log(ctx, buf, writer)
203}
204
205func (b *Backend) log(ctx context.Context, buf *bytes.Buffer, writer io.Writer) error {
206	reader := bytes.NewReader(buf.Bytes())
207
208	b.fileLock.Lock()
209
210	if writer == nil {
211		if err := b.open(); err != nil {
212			b.fileLock.Unlock()
213			return err
214		}
215		writer = b.f
216	}
217
218	if _, err := reader.WriteTo(writer); err == nil {
219		b.fileLock.Unlock()
220		return nil
221	} else if b.path == "stdout" {
222		b.fileLock.Unlock()
223		return err
224	}
225
226	// If writing to stdout there's no real reason to think anything would have
227	// changed so return above. Otherwise, opportunistically try to re-open the
228	// FD, once per call.
229	b.f.Close()
230	b.f = nil
231
232	if err := b.open(); err != nil {
233		b.fileLock.Unlock()
234		return err
235	}
236
237	reader.Seek(0, io.SeekStart)
238	_, err := reader.WriteTo(writer)
239	b.fileLock.Unlock()
240	return err
241}
242
243func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
244	var writer io.Writer
245	switch b.path {
246	case "stdout":
247		writer = os.Stdout
248	case "discard":
249		return nil
250	}
251
252	buf := bytes.NewBuffer(make([]byte, 0, 6000))
253	err := b.formatter.FormatResponse(ctx, buf, b.formatConfig, in)
254	if err != nil {
255		return err
256	}
257
258	return b.log(ctx, buf, writer)
259}
260
261// The file lock must be held before calling this
262func (b *Backend) open() error {
263	if b.f != nil {
264		return nil
265	}
266	if err := os.MkdirAll(filepath.Dir(b.path), b.mode); err != nil {
267		return err
268	}
269
270	var err error
271	b.f, err = os.OpenFile(b.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, b.mode)
272	if err != nil {
273		return err
274	}
275
276	// Change the file mode in case the log file already existed. We special
277	// case /dev/null since we can't chmod it and bypass if the mode is zero
278	switch b.path {
279	case "/dev/null":
280	default:
281		if b.mode != 0 {
282			err = os.Chmod(b.path, b.mode)
283			if err != nil {
284				return err
285			}
286		}
287	}
288
289	return nil
290}
291
292func (b *Backend) Reload(_ context.Context) error {
293	switch b.path {
294	case "stdout", "discard":
295		return nil
296	}
297
298	b.fileLock.Lock()
299	defer b.fileLock.Unlock()
300
301	if b.f == nil {
302		return b.open()
303	}
304
305	err := b.f.Close()
306	// Set to nil here so that even if we error out, on the next access open()
307	// will be tried
308	b.f = nil
309	if err != nil {
310		return err
311	}
312
313	return b.open()
314}
315
316func (b *Backend) Invalidate(_ context.Context) {
317	b.saltMutex.Lock()
318	defer b.saltMutex.Unlock()
319	b.salt.Store((*salt.Salt)(nil))
320}
321