-
Notifications
You must be signed in to change notification settings - Fork 4.3k
/
Copy pathheaders.go
264 lines (214 loc) · 7.99 KB
/
headers.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package audit
import (
"context"
"fmt"
"strings"
"sync"
"github.com/hashicorp/vault/sdk/logical"
)
// N.B.: While we could use textproto to get the canonical mime header, HTTP/2
// requires all headers to be converted to lower case, so we just do that.
const (
// auditedHeadersEntry is the key used in storage to store and retrieve the header config
auditedHeadersEntry = "audited-headers"
// AuditedHeadersSubPath is the path used to create a sub view within storage.
AuditedHeadersSubPath = "audited-headers-config/"
)
type durableStorer interface {
Get(ctx context.Context, key string) (*logical.StorageEntry, error)
Put(ctx context.Context, entry *logical.StorageEntry) error
}
// HeaderFormatter is an interface defining the methods of the
// vault.HeadersConfig structure needed in this package.
type HeaderFormatter interface {
// ApplyConfig returns a map of header values that consists of the
// intersection of the provided set of header values with a configured
// set of headers and will hash headers that have been configured as such.
ApplyConfig(context.Context, map[string][]string, Salter) (map[string][]string, error)
}
// AuditedHeadersKey returns the key at which audit header configuration is stored.
func AuditedHeadersKey() string {
return AuditedHeadersSubPath + auditedHeadersEntry
}
type HeaderSettings struct {
// HMAC is used to indicate whether the value of the header should be HMAC'd.
HMAC bool `json:"hmac"`
}
// HeadersConfig is used by the Audit Broker to write only approved
// headers to the audit logs. It uses a BarrierView to persist the settings.
type HeadersConfig struct {
// headerSettings stores the current headers that should be audited, and their settings.
headerSettings map[string]*HeaderSettings
// view is the barrier view which should be used to access underlying audit header config data.
view durableStorer
sync.RWMutex
}
// NewHeadersConfig should be used to create HeadersConfig.
func NewHeadersConfig(view durableStorer) (*HeadersConfig, error) {
if view == nil {
return nil, fmt.Errorf("barrier view cannot be nil")
}
// This should be the only place where the HeadersConfig struct is initialized.
// Store the view so that we can reload headers when we 'Invalidate'.
return &HeadersConfig{
view: view,
headerSettings: make(map[string]*HeaderSettings),
}, nil
}
// Header attempts to retrieve a copy of the settings associated with the specified header.
// The second boolean return parameter indicates whether the header existed in configuration,
// it should be checked as when 'false' the returned settings will have the default values.
func (a *HeadersConfig) Header(name string) (HeaderSettings, bool) {
a.RLock()
defer a.RUnlock()
var s HeaderSettings
v, ok := a.headerSettings[strings.ToLower(name)]
if ok {
s.HMAC = v.HMAC
}
return s, ok
}
// Headers returns all existing headers along with a copy of their current settings.
func (a *HeadersConfig) Headers() map[string]HeaderSettings {
a.RLock()
defer a.RUnlock()
// We know how many entries the map should have.
headers := make(map[string]HeaderSettings, len(a.headerSettings))
// Clone the headers
for name, setting := range a.headerSettings {
headers[name] = HeaderSettings{HMAC: setting.HMAC}
}
return headers
}
// Add adds or overwrites a header in the config and updates the barrier view
// NOTE: Add will acquire a write lock in order to update the underlying headers.
func (a *HeadersConfig) Add(ctx context.Context, header string, hmac bool) error {
if header == "" {
return fmt.Errorf("header value cannot be empty")
}
// Grab a write lock
a.Lock()
defer a.Unlock()
if a.headerSettings == nil {
a.headerSettings = make(map[string]*HeaderSettings, 1)
}
a.headerSettings[strings.ToLower(header)] = &HeaderSettings{hmac}
entry, err := logical.StorageEntryJSON(auditedHeadersEntry, a.headerSettings)
if err != nil {
return fmt.Errorf("failed to persist audited headers config: %w", err)
}
if err := a.view.Put(ctx, entry); err != nil {
return fmt.Errorf("failed to persist audited headers config: %w", err)
}
return nil
}
// Remove deletes a header out of the header config and updates the barrier view
// NOTE: Remove will acquire a write lock in order to update the underlying headers.
func (a *HeadersConfig) Remove(ctx context.Context, header string) error {
if header == "" {
return fmt.Errorf("header value cannot be empty")
}
// Grab a write lock
a.Lock()
defer a.Unlock()
// Nothing to delete
if len(a.headerSettings) == 0 {
return nil
}
delete(a.headerSettings, strings.ToLower(header))
entry, err := logical.StorageEntryJSON(auditedHeadersEntry, a.headerSettings)
if err != nil {
return fmt.Errorf("failed to persist audited headers config: %w", err)
}
if err := a.view.Put(ctx, entry); err != nil {
return fmt.Errorf("failed to persist audited headers config: %w", err)
}
return nil
}
// DefaultHeaders can be used to retrieve the set of default headers that will be
// added to HeadersConfig in order to allow them to appear in audit logs in a raw
// format. If the Vault Operator adds their own setting for any of the defaults,
// their setting will be honored.
func (a *HeadersConfig) DefaultHeaders() map[string]*HeaderSettings {
// Support deprecated 'x-' prefix (https://datatracker.ietf.org/doc/html/rfc6648)
const correlationID = "correlation-id"
xCorrelationID := fmt.Sprintf("x-%s", correlationID)
return map[string]*HeaderSettings{
correlationID: {},
xCorrelationID: {},
}
}
// Invalidate attempts to refresh the allowed audit headers and their settings.
// NOTE: Invalidate will acquire a write lock in order to update the underlying headers.
func (a *HeadersConfig) Invalidate(ctx context.Context) error {
a.Lock()
defer a.Unlock()
// Get the actual headers entries, e.g. sys/audited-headers-config/audited-headers
out, err := a.view.Get(ctx, auditedHeadersEntry)
if err != nil {
return fmt.Errorf("failed to read config: %w", err)
}
// If we cannot update the stored 'new' headers, we will clear the existing
// ones as part of invalidation.
headers := make(map[string]*HeaderSettings)
if out != nil {
err = out.DecodeJSON(&headers)
if err != nil {
return fmt.Errorf("failed to parse config: %w", err)
}
}
// Ensure that we are able to case-sensitively access the headers;
// necessary for the upgrade case
lowerHeaders := make(map[string]*HeaderSettings, len(headers))
for k, v := range headers {
lowerHeaders[strings.ToLower(k)] = v
}
// Ensure that we have default headers configured to appear in the audit log.
// Add them if they're missing.
for header, setting := range a.DefaultHeaders() {
if _, ok := lowerHeaders[header]; !ok {
lowerHeaders[header] = setting
}
}
a.headerSettings = lowerHeaders
return nil
}
// ApplyConfig returns a map of approved headers and their values, either HMAC'd or plaintext.
// If the supplied headers are empty or nil, an empty set of headers will be returned.
func (a *HeadersConfig) ApplyConfig(ctx context.Context, headers map[string][]string, salter Salter) (result map[string][]string, retErr error) {
// Return early if we don't have headers.
if len(headers) < 1 {
return map[string][]string{}, nil
}
// Grab a read lock
a.RLock()
defer a.RUnlock()
// Make a copy of the incoming headers with everything lower so we can
// case-insensitively compare
lowerHeaders := make(map[string][]string, len(headers))
for k, v := range headers {
lowerHeaders[strings.ToLower(k)] = v
}
result = make(map[string][]string, len(a.headerSettings))
for key, settings := range a.headerSettings {
if val, ok := lowerHeaders[key]; ok {
// copy the header values so we don't overwrite them
hVals := make([]string, len(val))
copy(hVals, val)
// Optionally hmac the values
if settings.HMAC {
for i, el := range hVals {
hVal, err := HashString(ctx, salter, el)
if err != nil {
return nil, err
}
hVals[i] = hVal
}
}
result[key] = hVals
}
}
return result, nil
}