Skip to content

Commit 2cebb7a

Browse files
committed
feat(secret): secret reference
1 parent 9111f92 commit 2cebb7a

File tree

23 files changed

+1361
-52
lines changed

23 files changed

+1361
-52
lines changed

config.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -180,3 +180,34 @@ tracing:
180180
# Example of grpc:
181181
# protocol: grpc
182182
# endpoint: localhost:4317
183+
184+
185+
#------------------------------------------------------------------------------
186+
# Secret (remote secret)
187+
# reference: {secret://<provider>/<name>[?<properties>][#/jsonpointer]}
188+
# Example: {secret://aws/path/to/secret#/password?ttl=1}
189+
#------------------------------------------------------------------------------
190+
secret:
191+
providers: # Specifies enabled providers.
192+
- '@default'
193+
- '@all'
194+
ttl: 10s
195+
aws: # AWS SecretManager provider
196+
region: us-west-1
197+
url: http://localhost:4566 # aws_url?
198+
199+
vault: # HashiCorp Vault provider
200+
address: http://localhost:8200
201+
mount_path: secret
202+
namespace:
203+
auth_method: approle
204+
authn:
205+
token:
206+
token:
207+
approle:
208+
role_id:
209+
secret_id:
210+
response_wrapping: false
211+
kubernetes:
212+
role:
213+
token_path:

config/config.go

Lines changed: 115 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
package config
22

33
import (
4+
"context"
45
"encoding/json"
56
"fmt"
67
"slices"
78

89
"github.com/creasty/defaults"
910
"github.com/webhookx-io/webhookx/pkg/envconfig"
11+
"github.com/webhookx-io/webhookx/pkg/secret"
12+
"github.com/webhookx-io/webhookx/pkg/secret/reference"
13+
"github.com/webhookx-io/webhookx/utils"
14+
"go.uber.org/zap"
1015
"gopkg.in/yaml.v3"
1116
)
1217

@@ -37,6 +42,7 @@ type Config struct {
3742
Tracing TracingConfig `yaml:"tracing" json:"tracing" envconfig:"TRACING"`
3843
Role Role `yaml:"role" json:"role" envconfig:"ROLE" default:"standalone"`
3944
AnonymousReports bool `yaml:"anonymous_reports" json:"anonymous_reports" envconfig:"ANONYMOUS_REPORTS" default:"true"`
45+
Secret SecretConfig `yaml:"secret" json:"secret" envconfig:"SECRET"`
4046
}
4147

4248
func (cfg Config) String() string {
@@ -81,6 +87,9 @@ func (cfg Config) Validate() error {
8187
if !slices.Contains([]Role{RoleStandalone, RoleCP, RoleDPWorker, RoleDPProxy}, cfg.Role) {
8288
return fmt.Errorf("invalid role: '%s'", cfg.Role)
8389
}
90+
if err := cfg.Secret.Validate(); err != nil {
91+
return err
92+
}
8493

8594
return nil
8695
}
@@ -89,27 +98,126 @@ type Options struct {
8998
YAML []byte
9099
}
91100

101+
func resolveReferences(n *yaml.Node, manager *secret.Manager) error {
102+
switch n.Kind {
103+
case yaml.ScalarNode:
104+
if reference.IsReference(n.Value) {
105+
ref, err := reference.Parse(n.Value)
106+
if err != nil {
107+
return err
108+
}
109+
val, err := manager.ResolveReference(context.TODO(), ref)
110+
if err != nil {
111+
return err
112+
}
113+
n.Value = val
114+
}
115+
case yaml.MappingNode:
116+
for i := 0; i < len(n.Content); i += 2 {
117+
if err := resolveReferences(n.Content[i+1], manager); err != nil {
118+
return err
119+
}
120+
}
121+
case yaml.AliasNode:
122+
if n.Alias != nil {
123+
if err := resolveReferences(n.Alias, manager); err != nil {
124+
return err
125+
}
126+
}
127+
default:
128+
for _, c := range n.Content {
129+
if err := resolveReferences(c, manager); err != nil {
130+
return err
131+
}
132+
}
133+
}
134+
return nil
135+
}
136+
92137
func New(opts *Options) (*Config, error) {
93138
var cfg Config
94139
err := defaults.Set(&cfg)
95140
if err != nil {
96141
return nil, err
97142
}
98143

99-
if opts != nil {
100-
if len(opts.YAML) > 0 {
101-
if err := yaml.Unmarshal(opts.YAML, &cfg); err != nil {
102-
return nil, err
144+
if opts == nil {
145+
opts = &Options{}
146+
}
147+
148+
var doc *yaml.Node
149+
if len(opts.YAML) > 0 {
150+
doc = new(yaml.Node)
151+
if err := yaml.Unmarshal(opts.YAML, doc); err != nil {
152+
return nil, err
153+
}
154+
}
155+
156+
// todo: only if secret feature is allowed
157+
if doc != nil {
158+
// fixme
159+
if len(doc.Content) > 0 {
160+
if node := utils.FindYaml(doc.Content[0], "secret"); node != nil {
161+
if err := node.Decode(&cfg.Secret); err != nil {
162+
return nil, err
163+
}
103164
}
104165
}
105166
}
167+
if err := envconfig.Process("WEBHOOKX_SECRET", &cfg.Secret); err != nil {
168+
return nil, err
169+
}
106170

107-
err = envconfig.Process("WEBHOOKX", &cfg)
108-
if err != nil {
171+
var reader = envconfig.EnvironmentReader
172+
173+
if err := cfg.Secret.Validate(); err != nil {
109174
return nil, err
110175
}
111176

112-
return &cfg, nil
177+
if cfg.Secret.Enabled() {
178+
providers := make(map[string]map[string]interface{})
179+
for _, p := range cfg.Secret.Providers {
180+
name := string(p)
181+
providers[name] = cfg.Secret.GetProviderConfiguration(name)
182+
}
183+
manager := secret.NewManager(zap.S(), providers)
184+
reader = func(key string) (string, bool, error) {
185+
value, ok, _ := envconfig.EnvironmentReader.Read(key)
186+
if ok && reference.IsReference(value) {
187+
ref, err := reference.Parse(value)
188+
if err != nil {
189+
return "", false, err
190+
}
191+
resolved, err := manager.ResolveReference(context.TODO(), ref)
192+
if err != nil {
193+
return "", false, err
194+
}
195+
value = resolved
196+
}
197+
return value, ok, nil
198+
}
199+
200+
if doc != nil {
201+
if len(doc.Content) > 0 {
202+
if err := resolveReferences(doc.Content[0], manager); err != nil {
203+
return nil, err
204+
}
205+
}
206+
resolvedYaml := utils.Must(yaml.Marshal(doc))
207+
fmt.Println(string(resolvedYaml))
208+
doc = new(yaml.Node)
209+
_ = yaml.Unmarshal(resolvedYaml, doc)
210+
}
211+
}
212+
213+
if doc != nil {
214+
if err := doc.Decode(&cfg); err != nil {
215+
return nil, err
216+
}
217+
}
218+
219+
err = envconfig.ProcessWithReader("WEBHOOKX", &cfg, reader)
220+
return &cfg, err
113221
}
114222

115223
func (cfg *Config) OverrideByRole(role Role) {

config/config_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,47 @@ func TestWorkerProxyConfig(t *testing.T) {
481481
}
482482
}
483483

484+
func TestSecretConfig(t *testing.T) {
485+
tests := []struct {
486+
desc string
487+
cfg SecretConfig
488+
validateErr error
489+
}{
490+
{
491+
desc: "sanity",
492+
cfg: SecretConfig{
493+
Vault: VaultProviderConfig{
494+
AuthMethod: "token",
495+
},
496+
},
497+
validateErr: nil,
498+
},
499+
{
500+
desc: "invalid provider",
501+
cfg: SecretConfig{
502+
Providers: []Provider{"aws", "vault", "unknown"},
503+
Vault: VaultProviderConfig{
504+
AuthMethod: "token",
505+
},
506+
},
507+
validateErr: errors.New("invalid provider: unknown"),
508+
},
509+
{
510+
desc: "invalid vault.auth_method",
511+
cfg: SecretConfig{
512+
Vault: VaultProviderConfig{
513+
AuthMethod: "unknown",
514+
},
515+
},
516+
validateErr: errors.New("invalid auth_method: unknown"),
517+
},
518+
}
519+
for _, test := range tests {
520+
actual := test.cfg.Validate()
521+
assert.Equal(t, test.validateErr, actual, "expected %v got %v", test.validateErr, actual)
522+
}
523+
}
524+
484525
func TestConfig(t *testing.T) {
485526
cfg, err := New(nil)
486527
assert.Nil(t, err)

config/secret.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package config
2+
3+
import (
4+
"fmt"
5+
"slices"
6+
7+
"github.com/webhookx-io/webhookx/pkg/secret/provider/vault"
8+
"github.com/webhookx-io/webhookx/utils"
9+
)
10+
11+
type Provider string
12+
13+
const (
14+
ProviderAWS Provider = "aws"
15+
ProviderVault Provider = "vault"
16+
)
17+
18+
type SecretConfig struct {
19+
Providers []Provider `json:"providers" yaml:"providers" default:"[\"aws\", \"vault\"]"`
20+
Aws AwsProviderConfig `json:"aws" yaml:"aws"`
21+
Vault VaultProviderConfig `json:"vault" yaml:"vault"`
22+
}
23+
24+
func (cfg *SecretConfig) Validate() error {
25+
for _, name := range cfg.Providers {
26+
if !slices.Contains([]Provider{ProviderAWS, ProviderVault}, name) {
27+
return fmt.Errorf("invalid provider: %s", name)
28+
}
29+
}
30+
if err := cfg.Aws.Validate(); err != nil {
31+
return err
32+
}
33+
if err := cfg.Vault.Validate(); err != nil {
34+
return err
35+
}
36+
return nil
37+
}
38+
39+
func (cfg *SecretConfig) Enabled() bool {
40+
return len(cfg.Providers) > 0
41+
}
42+
43+
func (cfg *SecretConfig) GetProviderConfiguration(name string) map[string]interface{} {
44+
switch Provider(name) {
45+
case ProviderAWS:
46+
return utils.Must(utils.StructToMap(cfg.Aws))
47+
case ProviderVault:
48+
return utils.Must(utils.StructToMap(cfg.Vault))
49+
default:
50+
return nil
51+
}
52+
}
53+
54+
type AwsProviderConfig struct {
55+
Region string `json:"region" yaml:"region"`
56+
URL string `json:"url" yaml:"url"`
57+
}
58+
59+
func (cfg *AwsProviderConfig) Validate() error {
60+
return nil
61+
}
62+
63+
type VaultProviderConfig struct {
64+
Address string `json:"address" yaml:"address" default:"http://localhost:8200"`
65+
MountPath string `json:"mount_path" yaml:"mount_path" default:"secret" split_words:"true"`
66+
Namespace string `json:"namespace" yaml:"namespace"`
67+
AuthMethod string `json:"auth_method" yaml:"auth_method" default:"token" split_words:"true"`
68+
AuthN vault.AuthN `json:"authn" yaml:"authn"`
69+
}
70+
71+
func (cfg *VaultProviderConfig) Validate() error {
72+
if !slices.Contains([]string{"token", "approle", "kubernetes"}, cfg.AuthMethod) {
73+
return fmt.Errorf("invalid auth_method: %s", cfg.AuthMethod)
74+
}
75+
return nil
76+
}

0 commit comments

Comments
 (0)