Skip to content
This repository has been archived by the owner on May 13, 2024. It is now read-only.

Commit

Permalink
Merge pull request #6 from darrenmcc/save-secrets
Browse files Browse the repository at this point in the history
Save secrets
  • Loading branch information
jprobinson authored Nov 8, 2018
2 parents aa1af9c + 815c22a commit 3e6a99a
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 34 deletions.
97 changes: 76 additions & 21 deletions gcpvault.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import (
"context"
"encoding/json"
"fmt"
"net/http"
"time"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"

"github.com/hashicorp/vault/api"
"github.com/pkg/errors"

"golang.org/x/oauth2"
"golang.org/x/oauth2/google"
iam "google.golang.org/api/iam/v1"
)

Expand Down Expand Up @@ -56,7 +55,11 @@ type Config struct {
}

// GetSecrets will use GCP Auth to access any secrets under the given SecretPath in
// Vault. Under the hood, this uses a JWT signed with the default Google application
// Vault.
//
// This is comparable to the `vault read` command.
//
// Under the hood, this uses a JWT signed with the default Google application
// credentials to login to Vault via
// https://godoc.org/github.com/hashicorp/vault/api#Logical.Write and to read secrets via
// https://godoc.org/github.com/hashicorp/vault/api#Logical.Read. For more details about
Expand All @@ -77,39 +80,91 @@ func GetSecrets(ctx context.Context, cfg Config) (map[string]interface{}, error)
return getLocalSecrets(ctx, cfg)
}

// create signed JWT with our service account
jwt, err := newJWT(ctx, cfg)
vClient, err := login(ctx, cfg)
if err != nil {
return nil, errors.Wrap(err, "unable to create JWT")
return nil, errors.Wrap(err, "unable to login to vault")
}

// init vault client
vcfg := api.DefaultConfig()
vcfg.MaxRetries = cfg.MaxRetries
vcfg.Address = cfg.VaultAddress
vcfg.HttpClient = getHTTPClient(ctx)
vClient, err := api.NewClient(vcfg)
// fetch secrets
secrets, err := vClient.Logical().Read(cfg.SecretPath)
if err != nil {
return nil, errors.Wrap(err, "unable to get secrets")
}
return secrets.Data, nil
}

// PutSecrets writes secrets to Vault at the configured path.
// This is comparable to the `vault write` command.
func PutSecrets(ctx context.Context, cfg Config, secrets map[string]interface{}) error {
vClient, err := login(ctx, cfg)
if err != nil {
return errors.Wrap(err, "unable to login to vault")
}
_, err = vClient.Logical().Write(cfg.SecretPath, secrets)
return errors.Wrap(err, "unable to make vault request")
}

// GetVersionedSecrets reads versioned secrets from Vault.
// This is comparable to the `vault kv get` command.
func GetVersionedSecrets(ctx context.Context, cfg Config) (map[string]interface{}, error) {
secs, err := GetSecrets(ctx, cfg)
if err != nil {
return nil, err
}
// versioned secrets are contained under a 'data' key
return secs["data"].(map[string]interface{}), nil
}

// PutVersionedSecrets writes versioned secrets to Vault at the configured path.
// This is comparable to the `vault kv put` command.
func PutVersionedSecrets(ctx context.Context, cfg Config, secrets map[string]interface{}) error {
vClient, err := login(ctx, cfg)
if err != nil {
return errors.Wrap(err, "unable to login to vault")
}

req := vClient.NewRequest(http.MethodPost, "v1/"+cfg.SecretPath)
req.BodyBytes, err = json.Marshal(map[string]map[string]interface{}{
"data": secrets,
})
if err != nil {
return errors.Wrap(err, "unable to marshal request body")
}
_, err = vClient.RawRequestWithContext(ctx, req)
return errors.Wrap(err, "unable to make vault request")
}

func login(ctx context.Context, cfg Config) (*api.Client, error) {
vClient, err := newClient(ctx, cfg)
if err != nil {
return nil, errors.Wrap(err, "unable to init vault client")
}

// create signed JWT with our service account
jwt, err := newJWT(ctx, cfg)
if err != nil {
return nil, errors.Wrap(err, "unable to create JWT")
}

// 'login' to vault using GCP auth
resp, err := vClient.Logical().Write(cfg.AuthPath+"/login", map[string]interface{}{
"role": cfg.Role, "jwt": jwt,
})
if err != nil {
return nil, errors.Wrap(err, "unable to login to vault")
return nil, errors.Wrap(err, "unable to make login request")
}

vClient.SetToken(resp.Auth.ClientToken)

// fetch secrets
secrets, err := vClient.Logical().Read(cfg.SecretPath)
if err != nil {
return nil, errors.Wrap(err, "unable to get secrets")
}
return vClient, nil
}

return secrets.Data, nil
func newClient(ctx context.Context, cfg Config) (*api.Client, error) {
vcfg := api.DefaultConfig()
vcfg.MaxRetries = cfg.MaxRetries
vcfg.Address = cfg.VaultAddress
vcfg.HttpClient = getHTTPClient(ctx)
return api.NewClient(vcfg)
}

func getLocalSecrets(ctx context.Context, cfg Config) (map[string]interface{}, error) {
Expand Down
6 changes: 3 additions & 3 deletions gcpvault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,8 +107,8 @@ func TestGetSecrets(t *testing.T) {
},
},
{
name: "GCP standard login, no meta email, fail",

name: "GCP standard login, no meta email, fail",
givenCreds: &google.Credentials{},
givenCfg: Config{
Role: "my-gcp-role",
SecretPath: "my-secret-path",
Expand Down Expand Up @@ -156,6 +156,7 @@ func TestGetSecrets(t *testing.T) {
name: "GCP standard login, meta fail",

givenEmail: "[email protected]",
givenCreds: &google.Credentials{},
givenCfg: Config{
Role: "my-gcp-role",
SecretPath: "my-secret-path",
Expand Down Expand Up @@ -292,7 +293,6 @@ func TestGetSecrets(t *testing.T) {
}
})
}

}

type testTokenSource struct{}
Expand Down
40 changes: 30 additions & 10 deletions gcpvaulttest/gcpvault.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,48 @@ import (
"io"
"net/http"
"net/http/httptest"

iam "google.golang.org/api/iam/v1"
"strings"
"sync"

"github.com/hashicorp/vault/api"
iam "google.golang.org/api/iam/v1"
)

// NewVaultServer is a stub Vault server for testing. It can be initialized
// with secrets if they're expected to be read-only by the service. Any writes
// will override any existing secrets.
func NewVaultServer(secrets map[string]interface{}) *httptest.Server {
var mu sync.Mutex

if secrets == nil {
secrets = map[string]interface{}{}
}

return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
if strings.Contains(r.URL.Path, "/login") {
json.NewEncoder(w).Encode(api.Secret{
Data: secrets,
Auth: &api.SecretAuth{ClientToken: "vault-test-token"},
})
case http.MethodPut:
json.NewEncoder(w).Encode(api.Secret{
Auth: &api.SecretAuth{
ClientToken: "vault-test-token",
},
return
}

mu.Lock()
defer mu.Unlock()

switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(map[string]interface{}{
"data": secrets,
})
case http.MethodPost, http.MethodPut:
var incoming map[string]interface{}
json.NewDecoder(r.Body).Decode(&incoming)
secrets = incoming
}
}))
}

// NewIAMServer creates a test IAM server.
func NewIAMServer() *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(iam.SignJwtResponse{
Expand All @@ -36,6 +55,7 @@ func NewIAMServer() *httptest.Server {
}))
}

// NewMetadataServer creates a test metadata server that returns the given email.
func NewMetadataServer(serviceAcctEmail string) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
io.WriteString(w, serviceAcctEmail)
Expand Down

0 comments on commit 3e6a99a

Please sign in to comment.