Skip to content

Commit

Permalink
Refactor POC into actual package (#1)
Browse files Browse the repository at this point in the history
* Add internal envparser

* Update documentation

* Add GCP Secret Manager provider

* Add initial client api

* Add comment token as a const

* Flip if condition to be more readable

* Remove cmd folder

* Update property name

* Move serum.go into its own folder
  • Loading branch information
hebime committed May 5, 2020
1 parent 072cac4 commit af1b2fe
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 129 deletions.
129 changes: 0 additions & 129 deletions cmd/serum/main.go

This file was deleted.

85 changes: 85 additions & 0 deletions internal/envparser/envparser.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package envparser

import (
"bufio"
"fmt"
"os"
"regexp"
"strings"
)

const (
commentToken = "#"
kvSeparator = "="
secretRegex = `^${(?P<secretval>.+)}$`
)

var secretRe *regexp.Regexp

func init() {
secretRe = regexp.MustCompile(secretRegex)
}

//EnvVars contains the plain text key value mappings as well as the encrypted secret key value mappings
//parsed from an env file
type EnvVars struct {
Plain map[string]string
Secrets map[string]string
}

//ParseFile parses a .env file at path and returns the key value
//mappings for plain text variables and secret variables
func ParseFile(path string) (*EnvVars, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("error opening file %s: %s", path, err)
}
defer f.Close()

envVars := &EnvVars{
Plain: make(map[string]string),
Secrets: make(map[string]string),
}

scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if err := parseLine(envVars, line); err != nil {
return nil, fmt.Errorf("error parsing line: %s: %s", line, err)
}
}
if err := scanner.Err(); err != nil {
return nil, fmt.Errorf("error parsing file: %s", err)
}

return envVars, nil
}

func parseLine(envVars *EnvVars, l string) error {
//ignore commented line
//TODO: remove inline comments
if strings.HasPrefix(l, commentToken) {
return nil
}

//split line into two pieces (k,v) based on key value seperator
splits := strings.SplitN(l, kvSeparator, 2)
if len(splits) != 2 {
return fmt.Errorf("invalid format %s", l)
}

//key is first index, value is second
k := strings.TrimSpace(splits[0])
v := strings.TrimSpace(splits[1])

//check if value is encrypted secret
if secretRe.MatchString(v) {
//fill in secret value - replace template value with capture group "secretval"
envVars.Secrets[k] = secretRe.ReplaceAllString(v, "$secretval")
return nil
}

//not a secret, fill in plain text value
envVars.Plain[k] = v
return nil
}
44 changes: 44 additions & 0 deletions secretprovider/gsmanager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package secretprovider

import (
"context"
"fmt"

secretmanager "cloud.google.com/go/secretmanager/apiv1"
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
)

//GSManager is a secret provider that communicates with Google Cloud Platform's Secret Manager
//to decrypt secrets. Internally it uses the Google Cloud SDK.
type GSManager struct {
smClient *secretmanager.Client
}

//NewGSManager return's an initialized GSManager using a new secret manager client.
func NewGSManager() (*GSManager, error) {
c, err := secretmanager.NewClient(context.Background())
if err != nil {
return nil, fmt.Errorf("gsmanager: failed to initialize client: %w", err)
}

return &GSManager{smClient: c}, nil
}

//Decrypt will access the secret on GCP Secret Manager and return the plain text string.
func (g *GSManager) Decrypt(secret string) (string, error) {
req := &secretmanagerpb.AccessSecretVersionRequest{
Name: secret,
}

result, err := g.smClient.AccessSecretVersion(context.Background(), req)
if err != nil {
return "", fmt.Errorf("gsmanager: failed to access secret version: %w", err)
}

return result.Payload.String(), nil
}

//Close closes the connection to the secret manager API.
func (g *GSManager) Close() error {
return g.smClient.Close()
}
9 changes: 9 additions & 0 deletions secretprovider/secretprovider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package secretprovider

//SecretProvider is an interface that wraps the decrypt and close methods.
//Close should be called when the secret provier is no longer needed.
//It may be a no-op in cases where there's no underlying connection to be closed.
type SecretProvider interface {
Decrypt(secret string) (string, error)
Close() error
}
56 changes: 56 additions & 0 deletions serum/serum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package serum

import (
"fmt"
"os"
"wingocard/serum/internal/envparser"
"wingocard/serum/secretprovider"
)

//Injector injects environment variables into the current running process. Key/value pairs can
//be read in from a .env file using the load method.
type Injector struct {
SecretProvider secretprovider.SecretProvider
envVars *envparser.EnvVars
}

//Inject will inject the loaded environment variables into the current running process' environment.
//Any secret values found will attempt to be decrypted using the provided secret provider.
//The presence of secrets with a nil SecretProvider will return an error.
func (in *Injector) Inject() error {
if len(in.envVars.Secrets) > 0 && in.SecretProvider == nil {
return fmt.Errorf("serum: error injecting env vars: secrets were loaded but the SecretProvider is nil")
}

//inject secrets
for k, v := range in.envVars.Secrets {
decrypted, err := in.SecretProvider.Decrypt(v)
if err != nil {
return fmt.Errorf("serum: error decrypting secret %s: %s", v, err)
}

if err := os.Setenv(k, decrypted); err != nil {
return fmt.Errorf("serum: error setting env var %s: %s", k, err)
}
}

//inject plain text vars
for k, v := range in.envVars.Plain {
if err := os.Setenv(k, v); err != nil {
return fmt.Errorf("serum: error setting env var %s: %s", k, err)
}
}
return nil
}

//Load will parse a .env file for key/value pairs and prepair them to be injected using the
//Inject method.
func (in *Injector) Load(path string) error {
envVars, err := envparser.ParseFile(path)
if err != nil {
return fmt.Errorf("serum: error loading env vars: %s", err)
}

in.envVars = envVars
return nil
}

0 comments on commit af1b2fe

Please sign in to comment.