Skip to content

Commit

Permalink
secrets: add 'NewTestSecrets'
Browse files Browse the repository at this point in the history
This allows you to create a new secrets.Store using a MockFilewatcher
with the secrets you pass into it rather than having to create a
temporary directory and writing the secrets JSON yourself in tests.

This change also exports some additional parts of the secrets
package:

 * Encoding is now exported.  This is a part of the GenericSecret
   struct which is used by NewTestSecrets but was not previously
   exported.
 * Export the strings that define the three secret types.

Both of these are a part of the Baseplate spec so exporting them
should be reasonable and they are unlikely to change.
  • Loading branch information
pacejackson committed May 19, 2020
1 parent 7924513 commit c2704da
Show file tree
Hide file tree
Showing 7 changed files with 539 additions and 52 deletions.
4 changes: 4 additions & 0 deletions secrets/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ go_library(
srcs = [
"config.go",
"doc.go",
"encoding.go",
"errors.go",
"secrets.go",
"store.go",
"testing.go",
],
importpath = "github.com/reddit/baseplate.go/secrets",
visibility = ["//visibility:public"],
Expand All @@ -22,10 +24,12 @@ go_test(
name = "go_default_test",
size = "small",
srcs = [
"encoding_test.go",
"secrets_test.go",
"store_bench_test.go",
"store_internal_test.go",
"store_test.go",
"testing_test.go",
],
embed = [":go_default_library"],
# Mark it as flaky as sometimes fsnotify took too long to notify the code
Expand Down
74 changes: 74 additions & 0 deletions secrets/encoding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package secrets

import (
"encoding/base64"
"encoding/json"
)

// Encoding represents the Encoding used to encode a secret.
type Encoding int

const (
// IdentityEncoding indicates no encoding beyond JSON itself.
IdentityEncoding Encoding = iota
// Base64Encoding indicates that the secret is base64 encoded.
Base64Encoding
)

const (
identityEncodingJSON = `"identity"`
identityEncodingStr = "identity"

base64EncodingJSON = `"base64"`
base64EncodingStr = "base64"
)

// MarshalJSON returns a JSON string representation of the encoding.
func (e Encoding) MarshalJSON() ([]byte, error) {
switch e {
case IdentityEncoding:
return []byte(identityEncodingJSON), nil
case Base64Encoding:
return []byte(base64EncodingJSON), nil
default:
return nil, ErrInvalidEncoding
}
}

// UnmarshalJSON unmarshals the given JSON data into an encoding.
func (e *Encoding) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch s {
case identityEncodingStr, "":
*e = IdentityEncoding
case base64EncodingStr:
*e = Base64Encoding
default:
return ErrInvalidEncoding
}
return nil
}

func (e Encoding) decodeValue(value string) (Secret, error) {
if value == "" {
return nil, nil
}
switch e {
case IdentityEncoding:
return Secret(value), nil
default:
data, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return nil, err
}
return Secret(data), nil
}
}

var (
_ json.Marshaler = Encoding(0)
_ json.Unmarshaler = (*Encoding)(nil)
)
98 changes: 98 additions & 0 deletions secrets/encoding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package secrets

import (
"errors"
"strings"
"testing"
)

func TestEncoding(t *testing.T) {
t.Parallel()

cases := []struct {
name string
enc Encoding
marshalled string
err error
}{
{
name: "invalid",
enc: -1,
marshalled: `"invalid"`,
err: ErrInvalidEncoding,
},
{
name: "identity",
enc: IdentityEncoding,
marshalled: identityEncodingJSON,
},
{
name: "base64",
enc: Base64Encoding,
marshalled: base64EncodingJSON,
},
}

for _, _c := range cases {
c := _c
t.Run(
"c.name",
func(t *testing.T) {
t.Run(
"MarshalJSON",
func(t *testing.T) {
b, err := c.enc.MarshalJSON()
if !errors.Is(err, c.err) {
t.Fatalf("error mismatch, expected %#v, got %#v", c.err, err)
}
if err != nil {
return
}

marshalled := string(b)
if strings.Compare(marshalled, c.marshalled) != 0 {
t.Fatalf("value mismatch, expected %q, got %q", c.marshalled, marshalled)
}
},
)

t.Run(
"UnmarshalJSON",
func(t *testing.T) {
var e Encoding
err := (&e).UnmarshalJSON([]byte(c.marshalled))
if !errors.Is(err, c.err) {
t.Fatalf("error mismatch, expected %#v, got %#v", c.err, err)
}
if err != nil {
return
}

if e != c.enc {
t.Fatalf("encoding does not match, expected %v, got %v", c.enc, e)
}
},
)
},
)
}

t.Run(
"UnmarshalJSON/fallback",
func(t *testing.T) {
var e Encoding
err := (&e).UnmarshalJSON([]byte(`""`))
if err != nil {
t.Fatal(err)
}

if e != IdentityEncoding {
t.Fatalf(
"encoding does not match, expected %v, got %v",
IdentityEncoding,
e,
)
}
},
)
}
67 changes: 16 additions & 51 deletions secrets/secrets.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package secrets

import (
"encoding/base64"
"encoding/json"
"fmt"
"io"
Expand All @@ -10,9 +9,15 @@ import (
)

const (
simpleSecret = "simple"
versionedSecret = "versioned"
credentialSecret = "credential"
// SimpleType secrets are basic string secrets.
SimpleType = "simple"

// VersionedType secrets are secrets that can be rotated gracefully.
VersionedType = "versioned"

// CredentialType secrets are username/password pairs as a single secret
// in vault.
CredentialType = "credential"
)

// A Secret is the base type of secrets.
Expand Down Expand Up @@ -180,21 +185,21 @@ type Document struct {
func (s *Document) Validate() error {
var batch batcherror.BatchError
for key, value := range s.Secrets {
if value.Type == simpleSecret && notOnlySimpleSecret(value) {
if value.Type == SimpleType && notOnlySimpleSecret(value) {
batch.Add(TooManyFieldsError{
SecretType: simpleSecret,
SecretType: SimpleType,
Key: key,
})
}
if value.Type == versionedSecret && notOnlyVersionedSecret(value) {
if value.Type == VersionedType && notOnlyVersionedSecret(value) {
batch.Add(TooManyFieldsError{
SecretType: versionedSecret,
SecretType: VersionedType,
Key: key,
})
}
if value.Type == credentialSecret && notOnlyCredentialSecret(value) {
if value.Type == CredentialType && notOnlyCredentialSecret(value) {
batch.Add(TooManyFieldsError{
SecretType: credentialSecret,
SecretType: CredentialType,
Key: key,
})
}
Expand All @@ -219,7 +224,7 @@ func notOnlyCredentialSecret(secret GenericSecret) bool {
type GenericSecret struct {
Type string `json:"type"`
Value string `json:"value"`
Encoding encoding `json:"encoding"`
Encoding Encoding `json:"encoding"`

Current string `json:"current"`
Previous string `json:"previous"`
Expand Down Expand Up @@ -283,43 +288,3 @@ func NewSecrets(r io.Reader) (*Secrets, error) {
}
return secrets, nil
}

// encoding represents the encoding used to encode the secrets.
type encoding int

const (
identityEncoding encoding = iota
base64Encoding
)

func (e *encoding) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
switch s {
case "identity", "":
*e = identityEncoding
case "base64":
*e = base64Encoding
default:
return ErrInvalidEncoding
}
return nil
}

func (e encoding) decodeValue(value string) (Secret, error) {
if value == "" {
return nil, nil
}
switch e {
case identityEncoding:
return Secret(value), nil
default:
data, err := base64.StdEncoding.DecodeString(value)
if err != nil {
return nil, err
}
return Secret(data), nil
}
}
2 changes: 1 addition & 1 deletion secrets/secrets_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ func TestNewSecrets(t *testing.T) {
}
`,
expectedError: TooManyFieldsError{
SecretType: simpleSecret,
SecretType: SimpleType,
Key: "secret/myservice/some-api-key",
},
},
Expand Down
Loading

0 comments on commit c2704da

Please sign in to comment.