Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor POC into actual package #1

Merged
merged 9 commits into from
May 5, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}