-
Notifications
You must be signed in to change notification settings - Fork 292
/
Copy pathconfig.go
666 lines (587 loc) · 19.6 KB
/
config.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
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
package config
import (
"bytes"
"crypto/rand"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"reflect"
"strconv"
"strings"
"time"
"github.com/hashicorp/boundary/internal/observability/event"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-secure-stdlib/base62"
"github.com/hashicorp/go-secure-stdlib/configutil"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/go-secure-stdlib/strutil"
"github.com/hashicorp/hcl"
"github.com/hashicorp/hcl/hcl/ast"
"github.com/mitchellh/mapstructure"
)
const (
desktopCorsOrigin = "serve://boundary"
devConfig = `
disable_mlock = true
telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
`
devControllerExtraConfig = `
controller {
name = "dev-controller"
description = "A default controller created in dev mode"
}
kms "aead" {
purpose = "root"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_root"
}
kms "aead" {
purpose = "worker-auth"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_worker-auth"
}
kms "aead" {
purpose = "recovery"
aead_type = "aes-gcm"
key = "%s"
key_id = "global_recovery"
}
listener "tcp" {
purpose = "api"
tls_disable = true
cors_enabled = true
cors_allowed_origins = ["*"]
}
listener "tcp" {
purpose = "cluster"
}
`
devWorkerExtraConfig = `
listener "tcp" {
purpose = "proxy"
}
worker {
name = "dev-worker"
description = "A default worker created in dev mode"
controllers = ["127.0.0.1"]
tags {
type = ["dev", "local"]
}
}
`
)
// Config is the configuration for the boundary controller
type Config struct {
*configutil.SharedConfig `hcl:"-"`
Worker *Worker `hcl:"worker"`
Controller *Controller `hcl:"controller"`
// Dev-related options
DevController bool `hcl:"-"`
DevUiPassthroughDir string `hcl:"-"`
DevControllerKey string `hcl:"-"`
DevWorkerAuthKey string `hcl:"-"`
DevRecoveryKey string `hcl:"-"`
// Eventing configuration for the controller
Eventing *event.EventerConfig `hcl:"events"`
// Plugin-related options
Plugins Plugins `hcl:"plugins"`
}
type Controller struct {
Name string `hcl:"name"`
Description string `hcl:"description"`
Database *Database `hcl:"database"`
PublicClusterAddr string `hcl:"public_cluster_addr"`
// AuthTokenTimeToLive is the total valid lifetime of a token denoted by time.Duration
AuthTokenTimeToLive interface{} `hcl:"auth_token_time_to_live"`
AuthTokenTimeToLiveDuration time.Duration
// AuthTokenTimeToStale is the total time a token can go unused before becoming invalid
// denoted by time.Duration
AuthTokenTimeToStale interface{} `hcl:"auth_token_time_to_stale"`
AuthTokenTimeToStaleDuration time.Duration
// StatusGracePeriod represents the period of time (as a duration) that the
// controller will wait before marking connections from a disconnected worker
// as invalid.
//
// TODO: This field is currently internal.
StatusGracePeriodDuration time.Duration `hcl:"-"`
// SchedulerRunJobInterval is the time interval between waking up the
// scheduler to run pending jobs.
//
// TODO: This field is currently internal.
SchedulerRunJobInterval time.Duration `hcl:"-"`
}
func (c *Controller) InitNameIfEmpty() (string, error) {
if c == nil {
return "", fmt.Errorf("controller config is empty")
}
if err := initNameIfEmpty(&c.Name); err != nil {
return "", fmt.Errorf("error auto-generating controller name: %w", err)
}
return c.Name, nil
}
type Worker struct {
Name string `hcl:"name"`
Description string `hcl:"description"`
PublicAddr string `hcl:"public_addr"`
// We use a raw interface here so that we can take in a string
// value pointing to an env var or file. We then resolve that
// and get the actual controller addresses.
Controllers []string `hcl:"-"`
ControllersRaw interface{} `hcl:"controllers"`
// We use a raw interface for parsing so that people can use JSON-like
// syntax that maps directly to the filter input or possibly more familiar
// key=value syntax, as well as accepting a string denoting an env or file
// pointer. This is trued up in the Parse function below.
Tags map[string][]string `hcl:"-"`
TagsRaw interface{} `hcl:"tags"`
// StatusGracePeriod represents the period of time (as a duration) that the
// worker will wait before disconnecting connections if it cannot make a
// status report to a controller.
//
// TODO: This field is currently internal.
StatusGracePeriodDuration time.Duration `hcl:"-"`
}
func (w *Worker) InitNameIfEmpty() (string, error) {
if w == nil {
return "", fmt.Errorf("worker config is empty")
}
if err := initNameIfEmpty(&w.Name); err != nil {
return "", fmt.Errorf("error auto-generating worker name: %w", err)
}
return w.Name, nil
}
func initNameIfEmpty(name *string) error {
if *name == "" {
var err error
if *name, err = base62.Random(10); err != nil {
return err
}
}
return nil
}
type Database struct {
Url string `hcl:"url"`
MigrationUrl string `hcl:"migration_url"`
MaxOpenConnections int `hcl:"-"`
MaxOpenConnectionsRaw interface{} `hcl:"max_open_connections"`
}
type Plugins struct {
ExecutionDir string `hcl:"execution_dir"`
}
// DevWorker is a Config that is used for dev mode of Boundary
// workers
func DevWorker() (*Config, error) {
parsed, err := Parse(devConfig + devWorkerExtraConfig)
if err != nil {
return nil, fmt.Errorf("error parsing dev config: %w", err)
}
return parsed, nil
}
func DevKeyGeneration() (string, string, string) {
var numBytes int64 = 96
randBuf := new(bytes.Buffer)
n, err := randBuf.ReadFrom(&io.LimitedReader{
R: rand.Reader,
N: numBytes,
})
if err != nil {
panic(err)
}
if n != numBytes {
panic(fmt.Errorf("expected to read 64 bytes, read %d", n))
}
controllerKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[0:32])
workerAuthKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[32:64])
recoveryKey := base64.StdEncoding.EncodeToString(randBuf.Bytes()[64:numBytes])
return controllerKey, workerAuthKey, recoveryKey
}
// DevController is a Config that is used for dev mode of Boundary
// controllers
func DevController() (*Config, error) {
controllerKey, workerAuthKey, recoveryKey := DevKeyGeneration()
hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig, controllerKey, workerAuthKey, recoveryKey)
parsed, err := Parse(hclStr)
if err != nil {
return nil, fmt.Errorf("error parsing dev config: %w", err)
}
parsed.DevController = true
parsed.DevControllerKey = controllerKey
parsed.DevWorkerAuthKey = workerAuthKey
parsed.DevRecoveryKey = recoveryKey
return parsed, nil
}
func DevCombined() (*Config, error) {
controllerKey, workerAuthKey, recoveryKey := DevKeyGeneration()
hclStr := fmt.Sprintf(devConfig+devControllerExtraConfig+devWorkerExtraConfig, controllerKey, workerAuthKey, recoveryKey)
parsed, err := Parse(hclStr)
if err != nil {
return nil, fmt.Errorf("error parsing dev config: %w", err)
}
parsed.DevController = true
parsed.DevControllerKey = controllerKey
parsed.DevWorkerAuthKey = workerAuthKey
parsed.DevRecoveryKey = recoveryKey
return parsed, nil
}
func New() *Config {
return &Config{
SharedConfig: new(configutil.SharedConfig),
}
}
// LoadFile loads the configuration from the given file.
func LoadFile(path string, wrapper wrapping.Wrapper) (*Config, error) {
d, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
raw := string(d)
if wrapper != nil {
raw, err = configutil.EncryptDecrypt(raw, true, true, wrapper)
if err != nil {
return nil, err
}
}
return Parse(raw)
}
func Parse(d string) (*Config, error) {
obj, err := hcl.Parse(d)
if err != nil {
return nil, err
}
result := New()
if err := hcl.DecodeObject(result, obj); err != nil {
return nil, err
}
// Perform controller configuration overrides for auth token settings
if result.Controller != nil {
result.Controller.Name, err = parseutil.ParsePath(result.Controller.Name)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return nil, fmt.Errorf("Error parsing controller name: %w", err)
}
if result.Controller.Name != strings.ToLower(result.Controller.Name) {
return nil, errors.New("Controller name must be all lower-case")
}
if !strutil.Printable(result.Controller.Name) {
return nil, errors.New("Controller name contains non-printable characters")
}
result.Controller.Description, err = parseutil.ParsePath(result.Controller.Description)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return nil, fmt.Errorf("Error parsing controller description: %w", err)
}
if !strutil.Printable(result.Controller.Description) {
return nil, errors.New("Controller description contains non-printable characters")
}
if result.Controller.AuthTokenTimeToLive != "" {
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToLive)
if err != nil {
return result, err
}
result.Controller.AuthTokenTimeToLiveDuration = t
}
if result.Controller.AuthTokenTimeToStale != "" {
t, err := parseutil.ParseDurationSecond(result.Controller.AuthTokenTimeToStale)
if err != nil {
return result, err
}
result.Controller.AuthTokenTimeToStaleDuration = t
}
if result.Controller.Database != nil {
if result.Controller.Database.MaxOpenConnectionsRaw != nil {
switch t := result.Controller.Database.MaxOpenConnectionsRaw.(type) {
case string:
maxOpenConnectionsString, err := parseutil.ParsePath(t)
if err != nil {
return nil, fmt.Errorf("Error parsing database max open connections: %w", err)
}
result.Controller.Database.MaxOpenConnections, err = strconv.Atoi(maxOpenConnectionsString)
if err != nil {
return nil, fmt.Errorf("Database max open connections value is not an int: %w", err)
}
case int:
result.Controller.Database.MaxOpenConnections = t
default:
return nil, fmt.Errorf("Database max open connections: unsupported type %q",
reflect.TypeOf(t).String())
}
}
}
}
// Parse worker tags
if result.Worker != nil {
result.Worker.Name, err = parseutil.ParsePath(result.Worker.Name)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return nil, fmt.Errorf("Error parsing worker name: %w", err)
}
if result.Worker.Name != strings.ToLower(result.Worker.Name) {
return nil, errors.New("Worker name must be all lower-case")
}
if !strutil.Printable(result.Worker.Name) {
return nil, errors.New("Worker name contains non-printable characters")
}
if result.Worker.TagsRaw != nil {
switch t := result.Worker.TagsRaw.(type) {
// We allow `tags` to be a simple string containing a URL with schema.
// See: https://github.com/hashicorp/go-secure-stdlib/blob/main/parseutil/parsepath.go
case string:
rawTags, err := parseutil.ParsePath(t)
if err != nil {
return nil, fmt.Errorf("Error parsing worker tags: %w", err)
}
var temp []map[string]interface{}
err = hcl.Decode(&temp, rawTags)
if err != nil {
return nil, fmt.Errorf("Error decoding raw worker tags: %w", err)
}
if err := mapstructure.WeakDecode(temp, &result.Worker.Tags); err != nil {
return nil, fmt.Errorf("Error decoding the worker's tags: %w", err)
}
// HCL allows multiple labeled blocks with the same name, turning it
// into a slice of maps, hence the slice here. This format is the
// one that ends up matching the JSON that we use in the expression.
case []map[string]interface{}:
for _, m := range t {
for k, v := range m {
// We allow the user to pass in only the keys in HCL, and
// then set the values to point to a URL with schema.
valStr, ok := v.(string)
if !ok {
continue
}
parsed, err := parseutil.ParsePath(valStr)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return nil, fmt.Errorf("Error parsing worker tag values: %w", err)
}
if valStr == parsed {
// Nothing was found, ignore.
// WeakDecode will still parse it though as we
// don't know if this could be a valid tag.
continue
}
var tags []string
err = json.Unmarshal([]byte(parsed), &tags)
if err != nil {
return nil, fmt.Errorf("Error unmarshalling env var/file contents: %w", err)
}
m[k] = tags
}
}
if err := mapstructure.WeakDecode(t, &result.Worker.Tags); err != nil {
return nil, fmt.Errorf("Error decoding the worker's %q section: %w", "tags", err)
}
// However for those that are used to other systems, we also accept
// key=value pairs
case []interface{}:
var strs []string
if err := mapstructure.WeakDecode(t, &strs); err != nil {
return nil, fmt.Errorf("Error decoding the worker's %q section: %w", "tags", err)
}
result.Worker.Tags = make(map[string][]string, len(strs))
// Aggregate the values by key. We care about the first equal
// sign only, to allow equals to be in values if needed. This
// also means we don't support equal signs in keys.
for _, str := range strs {
splitStr := strings.SplitN(str, "=", 2)
switch len(splitStr) {
case 1:
return nil, fmt.Errorf("Error decoding tag %q from string: must be in key = value format", str)
case 2:
key := splitStr[0]
v := result.Worker.Tags[key]
if len(v) == 0 {
v = make([]string, 0, 1)
}
result.Worker.Tags[key] = append(v, splitStr[1])
}
}
}
}
for k, v := range result.Worker.Tags {
if k != strings.ToLower(k) {
return nil, fmt.Errorf("Tag key %q is not all lower-case letters", k)
}
if !strutil.Printable(k) {
return nil, fmt.Errorf("Tag key %q contains non-printable characters", k)
}
for _, val := range v {
if val != strings.ToLower(val) {
return nil, fmt.Errorf("Tag value %q for tag key %q is not all lower-case letters", val, k)
}
if !strutil.Printable(k) {
return nil, fmt.Errorf("Tag value %q for tag key %q contains non-printable characters", v, k)
}
}
}
result.Worker.Controllers, err = parseWorkerControllers(result)
if err != nil {
return nil, fmt.Errorf("Failed to parse worker controllers: %w", err)
}
}
sharedConfig, err := configutil.ParseConfig(d)
if err != nil {
return nil, err
}
result.SharedConfig = sharedConfig
for _, listener := range result.SharedConfig.Listeners {
if strutil.StrListContains(listener.Purpose, "api") &&
(listener.CorsDisableDefaultAllowedOriginValues == nil || !*listener.CorsDisableDefaultAllowedOriginValues) {
switch listener.CorsEnabled {
case nil:
// If CORS wasn't specified, enable default value of *, which allows
// both the admin UI (without the user having to explicitly set an
// origin) and the desktop origin
listener.CorsEnabled = new(bool)
*listener.CorsEnabled = true
listener.CorsAllowedOrigins = []string{"*"}
default:
// If not the wildcard and they haven't disabled us auto-adding
// origin values, add the desktop client origin
if *listener.CorsEnabled &&
!strutil.StrListContains(listener.CorsAllowedOrigins, "*") {
listener.CorsAllowedOrigins = strutil.AppendIfMissing(listener.CorsAllowedOrigins, desktopCorsOrigin)
}
}
}
}
list, ok := obj.Node.(*ast.ObjectList)
if !ok {
return nil, fmt.Errorf("error parsing: file doesn't contain a root object")
}
eventList := list.Filter("events")
switch len(eventList.Items) {
case 0:
result.Eventing = event.DefaultEventerConfig()
case 1:
if result.Eventing, err = parseEventing(eventList.Items[0]); err != nil {
return nil, fmt.Errorf(`error parsing "events": %w`, err)
}
default:
return nil, fmt.Errorf(`too many "events" nodes (max 1, got %d)`, len(eventList.Items))
}
if result.Plugins.ExecutionDir != "" {
result.Plugins.ExecutionDir, err = parseutil.ParsePath(result.Plugins.ExecutionDir)
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return nil, fmt.Errorf("Error parsing plugins execution dir: %w", err)
}
}
return result, nil
}
func parseWorkerControllers(c *Config) ([]string, error) {
if c == nil || c.Worker == nil {
return nil, fmt.Errorf("config or worker field is nil")
}
if c.Worker.ControllersRaw == nil {
return nil, nil
}
switch t := c.Worker.ControllersRaw.(type) {
case []interface{}: // An array was configured directly in Boundary's HCL Config file.
var controllers []string
err := mapstructure.WeakDecode(c.Worker.ControllersRaw, &controllers)
if err != nil {
return nil, fmt.Errorf("failed to decode worker controllers block into config field: %w", err)
}
return controllers, nil
case string:
controllersStr, err := parseutil.ParsePath(t)
if err != nil {
return nil, fmt.Errorf("bad env var or file pointer: %w", err)
}
var addrs []string
err = json.Unmarshal([]byte(controllersStr), &addrs)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal env/file contents: %w", err)
}
return addrs, nil
default:
typ := reflect.TypeOf(t)
return nil, fmt.Errorf("unexpected type %q", typ.String())
}
}
func parseEventing(eventObj *ast.ObjectItem) (*event.EventerConfig, error) {
// Decode the outside struct
var result event.EventerConfig
if err := hcl.DecodeObject(&result, eventObj.Val); err != nil {
return nil, fmt.Errorf(`error decoding "events" node: %w`, err)
}
// Now, find the sinks
eventObjType, ok := eventObj.Val.(*ast.ObjectType)
if !ok {
return nil, fmt.Errorf(`error interpreting "events" node as an object type`)
}
list := eventObjType.List
sinkList := list.Filter("sink")
// Go through each sink and decode
for i, item := range sinkList.Items {
var s event.SinkConfig
if err := hcl.DecodeObject(&s, item.Val); err != nil {
return nil, fmt.Errorf("error decoding eventer sink entry %d", i)
}
// Fix up type and validate
switch {
case s.Type != "":
case len(item.Keys) == 1:
s.Type = event.SinkType(item.Keys[0].Token.Value().(string))
default:
switch {
case s.StderrConfig != nil:
// If we haven't found the type any other way, they _must_
// specify this block even though there are no config parameters
s.Type = event.StderrSink
case s.FileConfig != nil:
s.Type = event.FileSink
default:
return nil, fmt.Errorf("sink type could not be determined")
}
}
s.Type = event.SinkType(strings.ToLower(string(s.Type)))
if s.Type == event.StderrSink && s.StderrConfig == nil {
// StderrConfig is optional as it has no values, but ensure it's
// always populated if it's the type
s.StderrConfig = new(event.StderrSinkTypeConfig)
}
// parse the duration string specified in a file config into a time.Duration
if s.FileConfig != nil && s.FileConfig.RotateDurationHCL != "" {
var err error
s.FileConfig.RotateDuration, err = parseutil.ParseDurationSecond(s.FileConfig.RotateDurationHCL)
if err != nil {
return nil, fmt.Errorf("can't parse rotation duration %s", s.FileConfig.RotateDurationHCL)
}
}
if err := s.Validate(); err != nil {
return nil, err
}
// Append to result
result.Sinks = append(result.Sinks, &s)
}
if len(result.Sinks) == 0 {
result.Sinks = []*event.SinkConfig{event.DefaultSink()}
}
return &result, nil
}
// Sanitized returns a copy of the config with all values that are considered
// sensitive stripped. It also strips all `*Raw` values that are mainly
// used for parsing.
//
// Specifically, the fields that this method strips are:
// - KMS.Config
// - Telemetry.CirconusAPIToken
func (c *Config) Sanitized() map[string]interface{} {
// Create shared config if it doesn't exist (e.g. in tests) so that map
// keys are actually populated
if c.SharedConfig == nil {
c.SharedConfig = new(configutil.SharedConfig)
}
sharedResult := c.SharedConfig.Sanitized()
result := map[string]interface{}{}
for k, v := range sharedResult {
result[k] = v
}
return result
}