Skip to content

Commit

Permalink
Merge pull request #107 from nitrictech/feature/secret-gcp
Browse files Browse the repository at this point in the history
Add gcp secret manager plugin.
  • Loading branch information
jyecusch authored Aug 4, 2021
2 parents 0920f3a + 605c444 commit 7ff20fa
Show file tree
Hide file tree
Showing 8 changed files with 571 additions and 12 deletions.
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ bin/
*.coverprofile

# Ignore proto generated interfaces
interfaces/
interfaces/

mocks/
5 changes: 2 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module github.com/nitric-dev/membrane
go 1.15

require (
cloud.google.com/go v0.75.0
cloud.google.com/go/firestore v1.5.0
cloud.google.com/go/pubsub v1.3.1
cloud.google.com/go/storage v1.10.0
Expand All @@ -15,14 +16,13 @@ require (
github.com/asdine/storm v2.1.2+incompatible
github.com/aws/aws-lambda-go v1.20.0
github.com/aws/aws-sdk-go v1.36.12
github.com/golang/mock v1.4.4
github.com/golang/protobuf v1.5.2
github.com/golang/snappy v0.0.3 // indirect
github.com/google/addlicense v0.0.0-20210727174409-874627749a46
github.com/google/uuid v1.1.2
github.com/googleapis/gax-go/v2 v2.0.5
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 // indirect
github.com/mitchellh/mapstructure v1.4.1
github.com/nanobox-io/golang-scribble v0.0.0-20190309225732-aa3e7c118975
github.com/onsi/ginkgo v1.16.4
github.com/onsi/gomega v1.10.1
github.com/uw-labs/lichen v0.1.4
Expand All @@ -38,5 +38,4 @@ require (
google.golang.org/grpc v1.35.0
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0
google.golang.org/protobuf v1.26.0
gopkg.in/yaml.v2 v2.4.0
)
6 changes: 0 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,6 @@ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpO
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639 h1:mV02weKRL81bEnm8A0HT1/CAelMQDBuQIfLw8n+d6xI=
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25 h1:EFT6MH3igZK/dIVqgGbTqWVvkZ7wJ5iGN03SVtvvdd8=
github.com/jcelliott/lumber v0.0.0-20160324203708-dd349441af25/go.mod h1:sWkGw/wsaHtRsT9zGQ/WyJCotGWG/Anow/9hsAcBWRw=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
Expand Down Expand Up @@ -235,8 +233,6 @@ github.com/mitchellh/mapstructure v1.4.1 h1:CpVNEelQCZBooIPDn+AR3NpivK/TIKU8bDxd
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/muesli/termenv v0.7.4 h1:/pBqvU5CpkY53tU0vVn+xgs2ZTX63aH5nY+SSps5Xa8=
github.com/muesli/termenv v0.7.4/go.mod h1:pZ7qY9l3F7e5xsAOS0zCew2tME+p7bWeBkotCEcIIcc=
github.com/nanobox-io/golang-scribble v0.0.0-20190309225732-aa3e7c118975 h1:zm/Rb2OsnLWCY88Njoqgo4X6yt/lx3oBNWhepX0AOMU=
github.com/nanobox-io/golang-scribble v0.0.0-20190309225732-aa3e7c118975/go.mod h1:4Mct/lWCFf1jzQTTAaWtOI7sXqmG+wBeiBfT4CxoaJk=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
Expand Down Expand Up @@ -512,8 +508,6 @@ golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
golang.org/x/tools v0.1.4 h1:cVngSRcfgyZCzys3KYOpCFa+4dqX/Oub9tAq00ttGVs=
golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand Down
10 changes: 8 additions & 2 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ test-integration: install-tools generate-proto
@go run github.com/onsi/ginkgo/ginkgo ./tests/...

# Run all tests
test: test-adapters test-membrane test-plugins
test: generate-mocks test-adapters test-membrane test-plugins
@echo Done.

license-check-dev: dev-static
Expand Down Expand Up @@ -194,4 +194,10 @@ do-docker: do-docker-static
# # Generate proto files locally before building docker images
# # TODO: Get alpine image generating its own sources
# membrane-docker: generate-proto membrane-docker-alpine membrane-docker-debian
# @echo Built Docker Images
# @echo Built Docker Images

# generate mock implementations
generate-mocks:
@echo Generating Mock Clients
@mkdir -p mocks/mock_secret_manager
@go run github.com/golang/mock/mockgen github.com/nitric-dev/membrane/pkg/plugins/secret/secret_manager SecretManagerClient > mocks/mock_secret_manager/mock.go
214 changes: 214 additions & 0 deletions pkg/plugins/secret/secret_manager/secret_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
// Copyright 2021 Nitric Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package secret_manager_secret_service

import (
"context"
"fmt"
"strings"

secretmanager "cloud.google.com/go/secretmanager/apiv1"
gax "github.com/googleapis/gax-go/v2"
"github.com/nitric-dev/membrane/pkg/plugins"
"github.com/nitric-dev/membrane/pkg/plugins/secret"
"golang.org/x/oauth2/google"
secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// SecretManagerClient - iface that exposes utilized subset of generated SecretManagerServiceClient
// Used with gomock to assert create client -> service interaction in unit tests
type SecretManagerClient interface {
AccessSecretVersion(context.Context, *secretmanagerpb.AccessSecretVersionRequest, ...gax.CallOption) (*secretmanagerpb.AccessSecretVersionResponse, error)
AddSecretVersion(context.Context, *secretmanagerpb.AddSecretVersionRequest, ...gax.CallOption) (*secretmanagerpb.SecretVersion, error)
CreateSecret(context.Context, *secretmanagerpb.CreateSecretRequest, ...gax.CallOption) (*secretmanagerpb.Secret, error)
GetSecret(context.Context, *secretmanagerpb.GetSecretRequest, ...gax.CallOption) (*secretmanagerpb.Secret, error)
UpdateSecret(context.Context, *secretmanagerpb.UpdateSecretRequest, ...gax.CallOption) (*secretmanagerpb.Secret, error)
}

type secretManagerSecretService struct {
secret.UnimplementedSecretPlugin
client SecretManagerClient
projectId string
}

func validateNewSecret(sec *secret.Secret, val []byte) error {
if sec == nil {
return plugins.NewInvalidArgError("provide non-nil secret")
}
if len(sec.Name) == 0 {
return plugins.NewInvalidArgError("provide non-blank secret name")
}
if len(val) == 0 {
return plugins.NewInvalidArgError("provide non-blank secret value")
}

return nil
}

func (s *secretManagerSecretService) getParentName() string {
return fmt.Sprintf("projects/%s", s.projectId)
}

func (s *secretManagerSecretService) buildSecretName(sec *secret.Secret) (string, error) {
if len(sec.Name) == 0 {
return "", plugins.NewInvalidArgError("provide non-blank name")
}

return fmt.Sprintf("%s/secrets/%s", s.getParentName(), sec.Name), nil
}

func (s *secretManagerSecretService) buildSecretVersionName(sv *secret.SecretVersion) (string, error) {
parent, err := s.buildSecretName(sv.Secret)

if err != nil {
return "", err
}

if len(sv.Version) == 0 {
return "", plugins.NewInvalidArgError("provide non-blank version")
}

return fmt.Sprintf("%s/versions/%s", parent, sv.Version), nil
}

// ensure a secret container exists for storing secret versions
func (s *secretManagerSecretService) ensureSecret(sec *secret.Secret) (*secretmanagerpb.Secret, error) {
secName, err := s.buildSecretName(sec)

if err != nil {
return nil, err
}

getReq := &secretmanagerpb.GetSecretRequest{
Name: secName,
}

result, err := s.client.GetSecret(context.TODO(), getReq)

if err != nil {
// check error status, if it was an RPC NOT_FOUND error then continue
if s, ok := status.FromError(err); ok && s.Code() != codes.NotFound {
return nil, err
} else if !ok {
// It wasn't an RPC error so return
return nil, err
}
}

if result == nil {
// Creates the secret container
secReq := &secretmanagerpb.CreateSecretRequest{
Parent: s.getParentName(),
SecretId: sec.Name,
Secret: &secretmanagerpb.Secret{
Replication: &secretmanagerpb.Replication{
Replication: &secretmanagerpb.Replication_Automatic_{
Automatic: &secretmanagerpb.Replication_Automatic{},
},
},
},
}

result, err = s.client.CreateSecret(context.TODO(), secReq)
if err != nil {
return nil, fmt.Errorf("failed to create new secret: %v", err)
}
}

return result, nil
}

// Put - Creates a new secret if one doesn't exist, or just adds a new secret version
func (s *secretManagerSecretService) Put(sec *secret.Secret, val []byte) (*secret.SecretPutResponse, error) {
if err := validateNewSecret(sec, val); err != nil {
return nil, err
}

// ensure the secret container exists...
parentSec, err := s.ensureSecret(sec)

if err != nil {
return nil, err
}

verResult, err := s.client.AddSecretVersion(context.TODO(), &secretmanagerpb.AddSecretVersionRequest{
Parent: parentSec.Name,
Payload: &secretmanagerpb.SecretPayload{
Data: val,
},
})

if err != nil {
return nil, fmt.Errorf("Failed to add secret version: %v", err)
}

versionStringParts := strings.Split(verResult.Name, "/")
version := versionStringParts[len(versionStringParts)-1]

return &secret.SecretPutResponse{
SecretVersion: &secret.SecretVersion{
Secret: &secret.Secret{
Name: sec.Name,
},
Version: version,
},
}, nil
}

// Get - Retrieves a secret given a name and a version
func (s *secretManagerSecretService) Access(sv *secret.SecretVersion) (*secret.SecretAccessResponse, error) {
fullName, err := s.buildSecretVersionName(sv)

if err != nil {
return nil, err
}

req := &secretmanagerpb.AccessSecretVersionRequest{
Name: fullName,
}

result, err := s.client.AccessSecretVersion(context.TODO(), req)
if err != nil {
return nil, fmt.Errorf("failed to access secret version: %v", err)
}

return &secret.SecretAccessResponse{
// Return the original secret version payload
SecretVersion: sv,
Value: result.Payload.GetData(),
}, nil
}

// New - Creates a new Nitric secret service with GCP Secret Manager provider
func New() (secret.SecretService, error) {
ctx := context.Background()

credentials, credentialsError := google.FindDefaultCredentials(ctx, secretmanager.DefaultAuthScopes()...)
if credentialsError != nil {
return nil, fmt.Errorf("GCP credentials error: %v", credentialsError)
}

client, clientError := secretmanager.NewClient(ctx)
if clientError != nil {
return nil, fmt.Errorf("secret manager client error: %v", clientError)
}

return &secretManagerSecretService{
client: client,
projectId: credentials.ProjectID,
}, nil
}
27 changes: 27 additions & 0 deletions pkg/plugins/secret/secret_manager/secret_manager_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2021 Nitric Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package secret_manager_secret_service

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestSecretManager(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Secret Manager Suite")
}
Loading

0 comments on commit 7ff20fa

Please sign in to comment.