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 Support for Static Account #107

Merged
merged 35 commits into from
Jul 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b16ba07
initial static account changes
emilymye Nov 28, 2019
0f6ebaa
Fix test build
lawliet89 Nov 11, 2020
4b3381c
Rename file for consistency
lawliet89 Nov 12, 2020
8d022dc
Move common test code
lawliet89 Nov 12, 2020
1b6ef72
Refactor cleanup
lawliet89 Nov 12, 2020
38782ec
Add basic tests
lawliet89 Nov 12, 2020
5505a80
Add binding tests
lawliet89 Nov 18, 2020
7a60d27
Move roleset secrets tests to new file
lawliet89 Nov 18, 2020
07daefa
Refactor roleset secrets test functions
lawliet89 Nov 18, 2020
e6fc849
Improve robustness of some secrets tests
lawliet89 Nov 18, 2020
39a9f94
Add static secrets tests
lawliet89 Nov 18, 2020
7f99cc1
Add rotate key tests
lawliet89 Nov 18, 2020
b0ea17e
Rename "email" to "service_account_email"
lawliet89 Nov 20, 2020
6189821
Add test for SA Keys
lawliet89 Nov 20, 2020
8b3072f
Reorder cleaning up of account to remove test warnings
lawliet89 Feb 17, 2021
d8a8d86
Address feedback
lawliet89 Jun 28, 2021
a60ef69
Rename `static_account` to `name`
lawliet89 Jun 28, 2021
bd71751
Remove existence check
lawliet89 Jun 28, 2021
d950194
Add nil check
lawliet89 Jun 29, 2021
02ea944
Add missing `StaticAccount` field to WAL entries
lawliet89 Jun 29, 2021
f50e480
Drop WAL Entry on missing Static Account
lawliet89 Jun 30, 2021
10c13c5
Fix incorrect method call
lawliet89 Jun 30, 2021
d22508b
Text consistency
lawliet89 Jun 30, 2021
d10dd14
Use `staticAccountLock`
lawliet89 Jun 30, 2021
c1aef82
Pass context to GCP IAM Calls
lawliet89 Jul 1, 2021
44650b1
Apply suggestions from code review
lawliet89 Jul 2, 2021
17c1921
Use function instead of global variable
lawliet89 Jul 5, 2021
51441cc
Add lock in `pathStaticAccountDelete`
lawliet89 Jul 5, 2021
f75a274
Reorder imports
lawliet89 Jul 5, 2021
e73c574
Remove "currently xxx" in errors
lawliet89 Jul 5, 2021
1d69048
Add godoc for `parseOkInputServiceAccountEmail`
lawliet89 Jul 5, 2021
a7e9ea7
Add more details to field description
lawliet89 Jul 5, 2021
533727e
Update Secret Path help wording
lawliet89 Jul 5, 2021
1bdd252
Add test step to list static accounts
lawliet89 Jul 5, 2021
8fba665
Change path prefix
lawliet89 Jul 7, 2021
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
16 changes: 13 additions & 3 deletions plugin/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ type backend struct {

resources iamutil.ResourceParser

rolesetLock sync.Mutex
rolesetLock sync.Mutex
staticAccountLock sync.Mutex
}

// Factory returns a new backend as logical.Backend.
Expand Down Expand Up @@ -69,12 +70,21 @@ func Backend() *backend {
[]*framework.Path{
pathConfig(b),
pathConfigRotateRoot(b),
// Roleset
pathRoleSet(b),
pathRoleSetList(b),
pathRoleSetRotateAccount(b),
pathRoleSetRotateKey(b),
pathSecretAccessToken(b),
pathSecretServiceAccountKey(b),
pathRoleSetSecretAccessToken(b),
pathRoleSetSecretServiceAccountKey(b),
deprecatedPathRoleSetSecretAccessToken(b),
deprecatedPathRoleSetSecretServiceAccountKey(b),
// Static Account
pathStaticAccount(b),
pathStaticAccountList(b),
pathStaticAccountRotateKey(b),
pathStaticAccountSecretAccessToken(b),
pathStaticAccountSecretServiceAccountKey(b),
},
),
Secrets: []*framework.Secret{
Expand Down
117 changes: 117 additions & 0 deletions plugin/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,19 @@ package gcpsecrets

import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"time"

"github.com/hashicorp/go-gcp-common/gcputil"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util"
"github.com/hashicorp/vault/sdk/logical"
"google.golang.org/api/cloudresourcemanager/v1"
"google.golang.org/api/iam/v1"
"google.golang.org/api/option"
)

const (
Expand All @@ -31,3 +39,112 @@ func getTestBackend(tb testing.TB) (*backend, logical.Storage) {
}
return b.(*backend), config.StorageView
}

// Set up/Teardown
type testData struct {
B logical.Backend
S logical.Storage
Project string
HttpClient *http.Client
IamAdmin *iam.Service
}

func setupTest(t *testing.T, ttl, maxTTL string) *testData {
proj := util.GetTestProject(t)
credsJson, creds := util.GetTestCredentials(t)
httpC, err := gcputil.GetHttpClient(creds, iam.CloudPlatformScope)
if err != nil {
t.Fatal(err)
}

iamAdmin, err := iam.NewService(context.Background(), option.WithHTTPClient(httpC))
if err != nil {
t.Fatal(err)
}

b, reqStorage := getTestBackend(t)

testConfigUpdate(t, b, reqStorage, map[string]interface{}{
"credentials": credsJson,
"ttl": ttl,
"max_ttl": maxTTL,
})

return &testData{
B: b,
S: reqStorage,
Project: proj,
HttpClient: httpC,
IamAdmin: iamAdmin,
}
}

func cleanup(t *testing.T, td *testData, saDisplayName string, roles util.StringSet) {
resp, err := td.IamAdmin.Projects.ServiceAccounts.List(fmt.Sprintf("projects/%s", td.Project)).Do()
if err != nil {
t.Logf("[WARNING] Could not clean up test service accounts %s or projects/%s IAM policy bindings (did test fail?)", saDisplayName, td.Project)
return
}

memberStrs := make(util.StringSet)
for _, sa := range resp.Accounts {
if sa.DisplayName == saDisplayName {
memberStrs.Add("serviceAccount:" + sa.Email)
if _, err := td.IamAdmin.Projects.ServiceAccounts.Delete(sa.Name).Do(); err != nil {
if isGoogleAccountNotFoundErr(err) {
// Eventual consistency. We can ignore.
continue
}
t.Logf("[WARNING] found test service account %s that should have been deleted, did test fail? Auto-delete failed - manually clean up service account: %v", sa.Name, err)
}
t.Logf("[WARNING] found test service account %s that should have been deleted, did test fail? Manually deleted", sa.Name)
}
}

crm, err := cloudresourcemanager.New(td.HttpClient)
if err != nil {
t.Logf("[WARNING] Unable to ensure test project bindings deleted: %v", err)
return
}

p, err := crm.Projects.GetIamPolicy(td.Project, &cloudresourcemanager.GetIamPolicyRequest{}).Do()
if err != nil {
t.Logf("[WARNING] Unable to ensure test project bindings deleted, could not get policy: %v", err)
return
}

var changesMade bool
found := make(util.StringSet)
for idx, b := range p.Bindings {
if roles.Includes(b.Role) {
members := make([]string, 0, len(b.Members))
for _, m := range b.Members {
if memberStrs.Includes(m) {
changesMade = true
found.Add(b.Role)
} else {
members = append(members, m)
}
}
p.Bindings[idx].Members = members
}
}

if !changesMade {
return
}

t.Logf("[WARNING] had to clean up some roles (%s) for test service account %s - should have been deleted (did test fail?)",
strings.Join(found.ToSlice(), ","), saDisplayName)
if _, err := crm.Projects.SetIamPolicy(td.Project, &cloudresourcemanager.SetIamPolicyRequest{Policy: p}).Do(); err != nil {
t.Logf("[WARNING] Auto-delete failed - manually remove bindings on project %s: %v", td.Project, err)
}
}

func cleanupRoleset(t *testing.T, td *testData, rsName string, roles util.StringSet) {
cleanup(t, td, fmt.Sprintf(serviceAccountDisplayNameTmpl, rsName), roles)
}

func cleanupStatic(t *testing.T, td *testData, saName string, roles util.StringSet) {
cleanup(t, td, fmt.Sprintf(staticAccountDisplayNameTmpl, saName), roles)
}
108 changes: 108 additions & 0 deletions plugin/field_data_utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package gcpsecrets

import (
"fmt"

"github.com/hashicorp/errwrap"
"github.com/hashicorp/vault-plugin-secrets-gcp/plugin/util"
"github.com/hashicorp/vault/sdk/framework"
)

type inputParams struct {
name string
secretType string

hasBindings bool
rawBindings string
bindings ResourceBindings

project string
serviceAccountEmail string

scopes []string
}

func (input *inputParams) parseOkInputSecretType(d *framework.FieldData) (warnings []string, err error) {
lawliet89 marked this conversation as resolved.
Show resolved Hide resolved
secretType := d.Get("secret_type").(string)
if secretType == "" && input.secretType == "" {
return nil, fmt.Errorf("secret_type is required")
}
if input.secretType != "" && secretType != "" && input.secretType != secretType {
return nil, fmt.Errorf("cannot update secret_type")
}

switch secretType {
case SecretTypeKey, SecretTypeAccessToken:
input.secretType = secretType
return nil, nil
default:
return nil, fmt.Errorf("invalid secret_type %q", secretType)
}
}

// parseOkInputServiceAccountEmail checks that when creating a static acocunt, a service account
// email is provided. A service account email can be provide while updating the static account
// but it must be the same as the one in the static account and cannot be updated.
func (input *inputParams) parseOkInputServiceAccountEmail(d *framework.FieldData) (warnings []string, err error) {
email := d.Get("service_account_email").(string)
if email == "" && input.serviceAccountEmail == "" {
return nil, fmt.Errorf("email is required")
}
if input.serviceAccountEmail != "" && email != "" && input.serviceAccountEmail != email {
calvn marked this conversation as resolved.
Show resolved Hide resolved
return nil, fmt.Errorf("cannot update email")
}

input.serviceAccountEmail = email
return nil, nil
}

func (input *inputParams) parseOkInputTokenScopes(d *framework.FieldData) (warnings []string, err error) {
// Parse secretType if not yet parsed
if input.secretType == "" {
warnings, err = input.parseOkInputSecretType(d)
if err != nil {
return nil, err
}
}

v, ok := d.GetOk("token_scopes")
if ok {
scopes, castOk := v.([]string)
if !castOk {
return nil, fmt.Errorf("scopes unexpected type %T, expected []string", v)
}
input.scopes = scopes
}

if input.secretType == SecretTypeAccessToken && len(input.scopes) == 0 {
return nil, fmt.Errorf("non-empty token_scopes must be provided for generating access_token secrets")
}

if input.secretType != SecretTypeAccessToken && ok && len(input.scopes) > 0 {
warnings = append(warnings, "ignoring non-empty token_scopes, secret type not access_token")
}
return
}

func (input *inputParams) parseOkInputBindings(d *framework.FieldData) (warnings []string, err error) {
bRaw, ok := d.GetOk("bindings")
if !ok {
input.hasBindings = false
return nil, nil
}

rawBindings, castok := bRaw.(string)
if !castok {
return nil, fmt.Errorf("bindings are not a string")
}

bindings, err := util.ParseBindings(bRaw.(string))
if err != nil {
return nil, errwrap.Wrapf("unable to parse bindings: {{err}}", err)
lawliet89 marked this conversation as resolved.
Show resolved Hide resolved
}

input.hasBindings = true
input.rawBindings = rawBindings
input.bindings = bindings
return nil, nil
}
Loading