Skip to content

Commit

Permalink
config: Mad sure config is generatable: Added unsafe option for Secrets.
Browse files Browse the repository at this point in the history
Signed-off-by: Bartek Plotka <[email protected]>
  • Loading branch information
bwplotka committed Mar 18, 2019
1 parent f40ecee commit d39aa46
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 76 deletions.
140 changes: 112 additions & 28 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,25 +41,76 @@ func init() {
secretTokenJSON = string(b)
}

// Secret is a string that must not be revealed on marshaling.
type Secret string
// Secret is a string that must not be revealed on marshaling unless unsafe flag is set.
type Secret struct {
string

unsafe bool
}

func NewSecret(str string) *Secret{
return &Secret{string: str, unsafe: false}
}

func NewUnsafeSecret(str string) *Secret {
return &Secret{string: str, unsafe: true}
}

func (s *Secret) String() string {
if s == nil {
return ""
}
return s.string
}

// MarshalYAML implements the yaml.Marshaler interface for Secret.
func (s Secret) MarshalYAML() (interface{}, error) {
if s != "" {
if s.unsafe {
return s.string, nil
}

if s.string != "" {
return secretToken, nil
}

return nil, nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for Secret.
func (s *Secret) UnmarshalYAML(unmarshal func(interface{}) error) error {
type plain Secret
return unmarshal((*plain)(s))
var str string
if err := unmarshal(&str); err != nil {
return err
}

if str == "" {
return errors.New("secret value cannot be empty")
}

s.string = str
return nil
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for Secret.
func (s *Secret) UnmarshalJSON(data []byte) error {
var str string
if err := json.Unmarshal(data, &str); err != nil {
return err
}

if str == "" {
return errors.New("secret value cannot be empty")
}

s.string = str
return nil
}

// MarshalJSON implements the json.Marshaler interface for Secret.
func (s Secret) MarshalJSON() ([]byte, error) {
if s.unsafe {
return json.Marshal(s.string)
}
return json.Marshal(secretToken)
}

Expand Down Expand Up @@ -118,12 +169,28 @@ func (u *URL) UnmarshalJSON(data []byte) error {
return nil
}

// SecretURL is a URL that must not be revealed on marshaling.
type SecretURL URL
// SecretURL is a URL that must not be revealed on marshaling unless unsafe flag is set.
type SecretURL struct {
URL

unsafe bool
}

func NewSecretURL(u URL) *SecretURL{
return &SecretURL{URL: u, unsafe: false}
}

func NewUnsafeSecretURL(u URL) *SecretURL {
return &SecretURL{URL: u, unsafe: true}
}

// MarshalYAML implements the yaml.Marshaler interface for SecretURL.
func (s SecretURL) MarshalYAML() (interface{}, error) {
if s.URL != nil {
if s.unsafe {
return s.URL.MarshalYAML()
}

if s.URL.URL != nil {
return secretToken, nil
}
return nil, nil
Expand All @@ -139,14 +206,23 @@ func (s *SecretURL) UnmarshalYAML(unmarshal func(interface{}) error) error {
// the Alertmanager API with amtool), `<secret>` needs to be treated
// specially, as it isn't a valid URL.
if str == secretToken {
s.URL = &url.URL{}
s.URL.URL = &url.URL{}
return nil
}
return unmarshal((*URL)(s))

var u URL
if err := unmarshal(&u); err != nil {
return err
}
s.URL = u
return nil
}

// MarshalJSON implements the json.Marshaler interface for SecretURL.
func (s SecretURL) MarshalJSON() ([]byte, error) {
if s.unsafe {
return s.URL.MarshalJSON()
}
return json.Marshal(secretToken)
}

Expand All @@ -156,10 +232,17 @@ func (s *SecretURL) UnmarshalJSON(data []byte) error {
// the Alertmanager API with amtool), `<secret>` needs to be treated
// specially, as it isn't a valid URL.
if string(data) == secretToken || string(data) == secretTokenJSON {
s.URL = &url.URL{}
s.URL.URL = &url.URL{}
return nil
}
return json.Unmarshal(data, (*URL)(s))

var u URL
if err := json.Unmarshal(data, &u); err != nil {
return err
}

s.URL = u
return nil
}

// Load parses the YAML input s into a Config.
Expand Down Expand Up @@ -282,10 +365,10 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if ec.AuthUsername == "" {
ec.AuthUsername = c.Global.SMTPAuthUsername
}
if ec.AuthPassword == "" {
if ec.AuthPassword == nil {
ec.AuthPassword = c.Global.SMTPAuthPassword
}
if ec.AuthSecret == "" {
if ec.AuthSecret == nil {
ec.AuthSecret = c.Global.SMTPAuthSecret
}
if ec.AuthIdentity == "" {
Expand Down Expand Up @@ -320,8 +403,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if !strings.HasSuffix(hc.APIURL.Path, "/") {
hc.APIURL.Path += "/"
}
if hc.AuthToken == "" {
if c.Global.HipchatAuthToken == "" {

if hc.AuthToken == nil {
if c.Global.HipchatAuthToken == nil {
return fmt.Errorf("no global Hipchat Auth Token set")
}
hc.AuthToken = c.Global.HipchatAuthToken
Expand Down Expand Up @@ -356,8 +440,8 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if !strings.HasSuffix(ogc.APIURL.Path, "/") {
ogc.APIURL.Path += "/"
}
if ogc.APIKey == "" {
if c.Global.OpsGenieAPIKey == "" {
if ogc.APIKey == nil {
if c.Global.OpsGenieAPIKey == nil {
return fmt.Errorf("no global OpsGenie API Key set")
}
ogc.APIKey = c.Global.OpsGenieAPIKey
Expand All @@ -375,8 +459,8 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
wcc.APIURL = c.Global.WeChatAPIURL
}

if wcc.APISecret == "" {
if c.Global.WeChatAPISecret == "" {
if wcc.APISecret == nil {
if c.Global.WeChatAPISecret == nil {
return fmt.Errorf("no global Wechat ApiSecret set")
}
wcc.APISecret = c.Global.WeChatAPISecret
Expand Down Expand Up @@ -406,8 +490,8 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if !strings.HasSuffix(voc.APIURL.Path, "/") {
voc.APIURL.Path += "/"
}
if voc.APIKey == "" {
if c.Global.VictorOpsAPIKey == "" {
if voc.APIKey == nil {
if c.Global.VictorOpsAPIKey == nil {
return fmt.Errorf("no global VictorOps API Key set")
}
voc.APIKey = c.Global.VictorOpsAPIKey
Expand Down Expand Up @@ -500,21 +584,21 @@ type GlobalConfig struct {
SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"`
SMTPSmarthost string `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"`
SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"`
SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"`
SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"`
SMTPAuthPassword *Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"`
SMTPAuthSecret *Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"`
SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"`
SMTPRequireTLS bool `yaml:"smtp_require_tls,omitempty" json:"smtp_require_tls,omitempty"`
SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"`
PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"`
HipchatAPIURL *URL `yaml:"hipchat_api_url,omitempty" json:"hipchat_api_url,omitempty"`
HipchatAuthToken Secret `yaml:"hipchat_auth_token,omitempty" json:"hipchat_auth_token,omitempty"`
HipchatAuthToken *Secret `yaml:"hipchat_auth_token,omitempty" json:"hipchat_auth_token,omitempty"`
OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"`
OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"`
OpsGenieAPIKey *Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"`
WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"`
WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"`
WeChatAPISecret *Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"`
WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"`
VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"`
VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"`
VictorOpsAPIKey *Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig.
Expand Down
107 changes: 101 additions & 6 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,11 @@ func TestJSONMarshal(t *testing.T) {
}
}

func TestJSONMarshalSecret(t *testing.T) {
func TestMarshalSecret(t *testing.T) {
test := struct {
S Secret
S *Secret
}{
S: Secret("test"),
S: NewSecret("test"),
}

c, err := json.Marshal(test)
Expand All @@ -358,14 +358,76 @@ func TestJSONMarshalSecret(t *testing.T) {
// u003c -> "<"
// u003e -> ">"
require.Equal(t, "{\"S\":\"\\u003csecret\\u003e\"}", string(c), "Secret not properly elided.")

c, err = yaml.Marshal(test)
if err != nil {
t.Fatal(err)
}

// u003c -> "<"
// u003e -> ">"
require.Equal(t, "s: \u003csecret\u003e\n", string(c), "Secret not properly elided.")
}

func TestMarshalUnsafeSecret(t *testing.T) {
test := struct {
S *Secret
}{
S: NewUnsafeSecret("test"),
}

c, err := json.Marshal(test)
if err != nil {
t.Fatal(err)
}

require.Equal(t, "{\"S\":\"test\"}", string(c), "Unsafe Secret not marshaled.")

c, err = yaml.Marshal(test)
if err != nil {
t.Fatal(err)
}

require.Equal(t, "s: test\n", string(c), "Unsafe Secret not marshaled.")
}

func TestUnmarshalSecret(t *testing.T) {
b := []byte(`"secret"`)
var u Secret

err := json.Unmarshal(b, &u)
if err != nil {
t.Fatal(err)
}
require.Equal(t, "secret", u.String(), "Secret not properly unmarshalled from JSON.")
require.Equal(t, false, u.unsafe, "Secret unmarhalled as unsafe from JSON.")

err = yaml.Unmarshal(b, &u)
if err != nil {
t.Fatal(err)
}

require.Equal(t, "secret", u.String(), "Secret not properly unmarshalled from YAML.")
require.Equal(t, false, u.unsafe, "Secret unmarhalled as unsafe from YAML.")
}

func TestUnmarshalEmptySecret(t *testing.T) {
b := []byte(`""`)
var u Secret

err := json.Unmarshal(b, &u)
require.Error(t, err, "expected error while unmarshalling empty secret from JSON.")

err = yaml.Unmarshal(b, &u)
require.Error(t, err, "expected error while unmarshalling empty secret from YAML.")
}

func TestMarshalSecretURL(t *testing.T) {
urlp, err := url.Parse("http://example.com/")
if err != nil {
t.Fatal(err)
}
u := &SecretURL{urlp}
u := NewSecretURL(URL{urlp})

c, err := json.Marshal(u)
if err != nil {
Expand Down Expand Up @@ -394,6 +456,39 @@ func TestMarshalSecretURL(t *testing.T) {
}
}

func TestMarshalUnsafeSecretURL(t *testing.T) {
urlp, err := url.Parse("http://example.com/")
if err != nil {
t.Fatal(err)
}
u := NewUnsafeSecretURL(URL{urlp})

c, err := json.Marshal(u)
if err != nil {
t.Fatal(err)
}

require.Equal(t, "\"http://example.com/\"", string(c), "SecretURL not properly marshaled in JSON.")
// Check that the marshaled data can be unmarshaled again.
out := &SecretURL{}
err = json.Unmarshal(c, out)
if err != nil {
t.Fatal(err)
}

c, err = yaml.Marshal(u)
if err != nil {
t.Fatal(err)
}
require.Equal(t, "http://example.com/\n", string(c), "SecretURL not properly marshaled in YAML.")
// Check that the marshaled data can be unmarshaled again.
out = &SecretURL{}
err = yaml.Unmarshal(c, &out)
if err != nil {
t.Fatal(err)
}
}

func TestUnmarshalSecretURL(t *testing.T) {
b := []byte(`"http://example.com/se cret"`)
var u SecretURL
Expand Down Expand Up @@ -552,9 +647,9 @@ func TestEmptyFieldsAndRegex(t *testing.T) {
ResolveTimeout: model.Duration(5 * time.Minute),
SMTPSmarthost: "localhost:25",
SMTPFrom: "[email protected]",
HipchatAuthToken: "mysecret",
HipchatAuthToken: NewSecret("mysecret"),
HipchatAPIURL: mustParseURL("https://hipchat.foobar.org/"),
SlackAPIURL: (*SecretURL)(mustParseURL("http://slack.example.com/")),
SlackAPIURL: NewSecretURL(*mustParseURL("http://slack.example.com/")),
SMTPRequireTLS: true,
PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"),
OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"),
Expand Down
Loading

0 comments on commit d39aa46

Please sign in to comment.