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

Add secret manager for storing and loading SA keys #39

Merged
merged 2 commits into from
Sep 3, 2024
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
143 changes: 143 additions & 0 deletions internal/adminx/secrets.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package adminx

import (
"context"
"log"

"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/googleapis/gax-go"
)

// SecretManagerClient is an interface describing operations on the Google Cloud Secret Manager API.
type SecretManagerClient interface {
GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error)
GetSecretVersion(ctx context.Context, req *secretmanagerpb.GetSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
}

// SecretManager manages operations on secrets.
type SecretManager struct {
Namer *Namer
smc SecretManagerClient
sam *ServiceAccountsManager
version string
}

// NewSecretManager creates a new secret manager instance.
func NewSecretManager(smc SecretManagerClient, n *Namer, sam *ServiceAccountsManager) *SecretManager {
return &SecretManager{
Namer: n,
smc: smc,
sam: sam,
version: "latest",
}
}

// CreateSecret creates a new secret for the given org using the naming
// convention of the instance Namer.
func (s *SecretManager) CreateSecret(ctx context.Context, org string) error {
// Create a SecretManager secret for this organization.
// Versions are created separately.
getReq := &secretmanagerpb.GetSecretRequest{
Name: s.Namer.GetSecretName(org),
}
secret, err := s.smc.GetSecret(ctx, getReq)
switch {
case errIsNotFound(err):
// Create the request to create the secret.
log.Printf("Creating secret: %q", s.Namer.GetSecretID(org))
crReq := &secretmanagerpb.CreateSecretRequest{
Parent: s.Namer.GetProjectsName(),
SecretId: s.Namer.GetSecretID(org),
Secret: &secretmanagerpb.Secret{
Replication: &secretmanagerpb.Replication{
Replication: &secretmanagerpb.Replication_Automatic_{
Automatic: &secretmanagerpb.Replication_Automatic{},
},
},
},
}
secret, err = s.smc.CreateSecret(ctx, crReq)
if err != nil {
log.Printf("Creating secret failed for %q: %v", s.Namer.GetSecretName(org), err)
return err
}
case err != nil:
log.Printf("Get secret failed for %q: %v", s.Namer.GetSecretName(org), err)
return err
}
log.Println("Found secret:", secret.Name)
return nil
}

// LoadOrCreateKey is a single method to either create and store a key or
// read an existing key from SecretManager.
func (s *SecretManager) LoadOrCreateKey(ctx context.Context, org string) (string, error) {
key, err := s.LoadKey(ctx, org)
switch {
case errIsNotFound(err):
k, err := s.sam.CreateKey(ctx, org)
if err != nil {
log.Printf("CreateKey failed for %q: %v", s.Namer.GetServiceAccountName(org), err)
return "", err
}
// Store the new key in secret manager.
// NOTE: key is already base64 encoded.
err = s.StoreKey(ctx, org, k.PrivateKeyData)
if err != nil {
log.Printf("StoreKey failed for %q: %v", s.Namer.GetServiceAccountName(org), err)
return "", err
}
key = k.PrivateKeyData
case err != nil:
log.Printf("Loadkey failed for %q: %v", s.Namer.GetServiceAccountName(org), err)
return "", err
}
return key, nil
}

// StoreKey saves the given key in the org's secret.
func (s *SecretManager) StoreKey(ctx context.Context, org string, key string) error {
// Declare the payload to store.
payload := []byte(key)
req := &secretmanagerpb.GetSecretVersionRequest{
Name: s.Namer.GetSecretName(org) + "/versions/" + s.version,
}
// NOTE: once a secret is created it will not be overwritten. It must be deleted first.
version, err := s.smc.GetSecretVersion(ctx, req)
switch {
case errIsNotFound(err):
// Add secret.
addReq := &secretmanagerpb.AddSecretVersionRequest{
Parent: s.Namer.GetSecretName(org),
Payload: &secretmanagerpb.SecretPayload{
Data: payload,
},
}
version, err = s.smc.AddSecretVersion(ctx, addReq)
if err != nil {
return err
}
log.Println("Added version:", version.Name)
case err != nil:
return err
}
log.Println("Stored:", version.Name)
return nil
}

// LoadKey loads a key from the org's secret. LoadKey returns error if the key is not found.
func (s *SecretManager) LoadKey(ctx context.Context, org string) (string, error) {
// Build the request.
req := &secretmanagerpb.AccessSecretVersionRequest{
Name: s.Namer.GetSecretName(org) + "/versions/" + s.version,
}
// Call the API.
result, err := s.smc.AccessSecretVersion(ctx, req)
if err != nil {
return "", err
}
return string(result.Payload.Data), nil
}
244 changes: 244 additions & 0 deletions internal/adminx/secrets_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package adminx

import (
"context"
"fmt"
"testing"

"cloud.google.com/go/secretmanager/apiv1/secretmanagerpb"
"github.com/googleapis/gax-go"
"google.golang.org/api/iam/v1"
)

type fakeSMC struct {
getSec *secretmanagerpb.Secret
getSecErr error
createSec *secretmanagerpb.Secret
createSecErr error
getSecVer *secretmanagerpb.SecretVersion
getSecVerErr error
addSecVer *secretmanagerpb.SecretVersion
addSecVerErr error
accessSecVer *secretmanagerpb.AccessSecretVersionResponse
accessSecVerErr error
}

func (f *fakeSMC) GetSecret(ctx context.Context, req *secretmanagerpb.GetSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
return f.getSec, f.getSecErr
}
func (f *fakeSMC) CreateSecret(ctx context.Context, req *secretmanagerpb.CreateSecretRequest, opts ...gax.CallOption) (*secretmanagerpb.Secret, error) {
return f.createSec, f.createSecErr
}
func (f *fakeSMC) GetSecretVersion(ctx context.Context, req *secretmanagerpb.GetSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) {
return f.getSecVer, f.getSecVerErr
}
func (f *fakeSMC) AddSecretVersion(ctx context.Context, req *secretmanagerpb.AddSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.SecretVersion, error) {
return f.addSecVer, f.addSecVerErr
}
func (f *fakeSMC) AccessSecretVersion(ctx context.Context, req *secretmanagerpb.AccessSecretVersionRequest, opts ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error) {
return f.accessSecVer, f.accessSecVerErr
}

func TestSecretManager_CreateSecret(t *testing.T) {
tests := []struct {
name string
namer *Namer
smc SecretManagerClient
org string
wantErr bool
}{
{
name: "success-found",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
getSec: &secretmanagerpb.Secret{
Name: "projects/mlab-foo/secrets/fake-secret",
},
},
},
{
name: "success-not-found",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
getSecErr: createNotFoundErr(),
createSec: &secretmanagerpb.Secret{
Name: "projects/mlab-foo/secrets/fake-secret",
},
},
},
{
name: "error-not-found-fails-to-create-secret",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
getSecErr: createNotFoundErr(),
createSecErr: fmt.Errorf("failed to create new secret"),
},
wantErr: true,
},
{
name: "error-not-found-fails-to-create-secret",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
getSecErr: fmt.Errorf("other get error"),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
s := NewSecretManager(tt.smc, tt.namer, nil)
if err := s.CreateSecret(context.Background(), tt.org); (err != nil) != tt.wantErr {
t.Errorf("SecretManager.CreateSecret() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

func TestSecretManager_LoadOrCreateKey(t *testing.T) {
tests := []struct {
name string
namer *Namer
smc SecretManagerClient
iams IAMService
org string
want string
wantErr bool
}{
{
name: "success-load-key",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
accessSecVer: &secretmanagerpb.AccessSecretVersionResponse{
Name: "projects/mlab-foo/secrets/fake-secret/versions/lastest",
Payload: &secretmanagerpb.SecretPayload{
Data: []byte("fake data"),
},
},
},
org: "testorg",
want: "fake data",
},
{
name: "success-create-and-store-key",
namer: NewNamer("mlab-foo"),
iams: &fakeIAMService{
getAcct: &iam.ServiceAccount{
Name: "projects/mlab-foo/secrets/fake-secret",
},
key: &iam.ServiceAccountKey{
PrivateKeyData: "fake data",
},
},
smc: &fakeSMC{
accessSecVerErr: createNotFoundErr(),
getSecVerErr: createNotFoundErr(),
addSecVer: &secretmanagerpb.SecretVersion{
Name: "projects/mlab-foo/secrets/fake-secret/versions/lastest",
},
},
org: "testorg",
want: "fake data",
},
{
name: "error-create-key",
namer: NewNamer("mlab-foo"),
iams: &fakeIAMService{
getAcct: &iam.ServiceAccount{
Name: "projects/mlab-foo/secrets/fake-secret",
},
keyErr: fmt.Errorf("fake error creating key"),
},
smc: &fakeSMC{
accessSecVerErr: createNotFoundErr(),
getSecErr: createNotFoundErr(),
addSecVer: &secretmanagerpb.SecretVersion{
Name: "projects/mlab-foo/secrets/fake-secret/versions/lastest",
},
},
org: "testorg",
wantErr: true,
},
{
name: "error-store-key",
namer: NewNamer("mlab-foo"),
iams: &fakeIAMService{
getAcct: &iam.ServiceAccount{
Name: "projects/mlab-foo/secrets/fake-secret",
},
key: &iam.ServiceAccountKey{
PrivateKeyData: "fake data",
},
},
smc: &fakeSMC{
accessSecVerErr: createNotFoundErr(),
getSecVerErr: fmt.Errorf("a different fatal error"),
},
org: "testorg",
wantErr: true,
},
{
name: "error-load-key",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
accessSecVerErr: fmt.Errorf("fake error accessing key"),
},
org: "testorg",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sam := NewServiceAccountsManager(tt.iams, tt.namer)
s := NewSecretManager(tt.smc, tt.namer, sam)
got, err := s.LoadOrCreateKey(context.Background(), tt.org)
if (err != nil) != tt.wantErr {
t.Errorf("SecretManager.LoadOrCreateKey() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("SecretManager.LoadOrCreateKey() = %v, want %v", got, tt.want)
}
})
}
}

func TestSecretManager_StoreKey(t *testing.T) {
tests := []struct {
name string
namer *Namer
smc SecretManagerClient
org string
key string
wantErr bool
}{
{
name: "success",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
getSecVerErr: createNotFoundErr(),
addSecVer: &secretmanagerpb.SecretVersion{
Name: "fake key name",
},
},
},
{
name: "error-add-secret-version-fails",
namer: NewNamer("mlab-foo"),
smc: &fakeSMC{
getSecVerErr: createNotFoundErr(),
addSecVerErr: fmt.Errorf("failed"),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
sam := NewServiceAccountsManager(nil, tt.namer)
s := NewSecretManager(tt.smc, tt.namer, sam)
if err := s.StoreKey(context.Background(), tt.org, tt.key); (err != nil) != tt.wantErr {
t.Errorf("SecretManager.StoreKey() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}