Skip to content

Commit

Permalink
VAULT-6727 role resolution for GCP (#135)
Browse files Browse the repository at this point in the history
* VAULT-6727 role resolution for GCP

* VAULT-6727 changelog

* VAULT-6727 Change errors to responses, add test

* Update CHANGELOG.md

Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>

Co-authored-by: Tom Proctor <tomhjp@users.noreply.github.com>
  • Loading branch information
VioletHynes and tomhjp authored Jul 21, 2022
1 parent 54acedf commit eee8ee8
Show file tree
Hide file tree
Showing 6 changed files with 135 additions and 22 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ BUG FIXES:
IMPROVEMENTS:

* Updates dependencies: `google.golang.org/api@v0.83.0`, `github.com/hashicorp/go-gcp-common@v0.8.0` [[GH-130](https://github.com/hashicorp/vault-plugin-auth-gcp/pull/130)]
* Enables GCP roles to be compatible with Vault's role based quotas [[GH-135](https://github.com/hashicorp/vault-plugin-auth-gcp/pull/135)].

## v0.13.0

Expand Down
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ require (
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-gcp-common v0.8.0
github.com/hashicorp/go-hclog v1.0.0
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2
github.com/hashicorp/go-uuid v1.0.2
github.com/hashicorp/vault/api v1.3.0
github.com/hashicorp/vault/sdk v0.3.0
github.com/hashicorp/vault/sdk v0.5.3
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/oauth2 v0.0.0-20220524215830-622c5d57e401
Expand Down
13 changes: 8 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -243,11 +243,12 @@ github.com/hashicorp/go-secure-stdlib/base62 v0.1.1/go.mod h1:EdWO6czbmthiwZ3/PU
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 h1:cCRo8gK7oq6A2L6LICkUZ+/a5rLiRXFMf1Qd4xSwxTc=
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1/go.mod h1:zq93CJChV6L9QTfGKtfBxKqD7BqqXx5O04A/ns2p5+I=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.1/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2 h1:Tz6v3Jb2DRnDCfifRSjYKG0m8dLdNq6bcDkB41en7nw=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6 h1:om4Al8Oy7kCm/B86rLCLah4Dt5Aa0Fr5rYBG60OzwHQ=
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.6/go.mod h1:QmrqtbKuxxSWTN3ETMPuB+VtEiBJ/A9XhoYGv8E1uD8=
github.com/hashicorp/go-secure-stdlib/password v0.1.1/go.mod h1:9hH302QllNwu1o2TGYtSk8I8kTAN0ca1EHpwhm5Mmzo=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1 h1:nd0HIW15E6FG1MsnArYaHfuw9C2zgzM8LxkG5Ty/788=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.1/go.mod h1:gKOamz3EwoIoJq7mlMIRBpVTAUn8qPCrEclOKKWhD3U=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs=
github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc=
github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A=
Expand All @@ -264,8 +265,9 @@ github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/vault/api v1.3.0 h1:uDy39PLSvy6gtKyjOCRPizy2QdFiIYSWBR2pxCEzYL8=
github.com/hashicorp/vault/api v1.3.0/go.mod h1:EabNQLI0VWbWoGlA+oBLC8PXmR9D60aUVgQGvangFWQ=
github.com/hashicorp/vault/sdk v0.3.0 h1:kR3dpxNkhh/wr6ycaJYqp6AFT/i2xaftbfnwZduTKEY=
github.com/hashicorp/vault/sdk v0.3.0/go.mod h1:aZ3fNuL5VNydQk8GcLJ2TV8YCRVvyaakYkhZRoVuhj0=
github.com/hashicorp/vault/sdk v0.5.3 h1:PWY8sq/9pRrK9vUIy75qCH2Jd8oeENAgkaa/qbhzFrs=
github.com/hashicorp/vault/sdk v0.5.3/go.mod h1:DoGraE9kKGNcVgPmTuX357Fm6WAx1Okvde8Vp3dPDoU=
github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ=
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM=
Expand Down Expand Up @@ -310,8 +312,9 @@ github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdI
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-wordwrap v1.0.0/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.4.2 h1:6h7AQ0yhTcIsmFmnAwQls75jp2Gzs4iB8W7pjMO+rqo=
github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/mitchellh/reflectwalk v1.0.0 h1:9D+8oIskB4VJBN5SFlmc27fSlIBZaov1Wpk/IfikLNY=
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
Expand Down
6 changes: 6 additions & 0 deletions plugin/mocks_test.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

54 changes: 40 additions & 14 deletions plugin/path_login.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,35 @@ GCE identity metadata token ('iam', 'gce' roles).`,
logical.AliasLookaheadOperation: &framework.PathOperation{
Callback: b.pathLogin,
},
logical.ResolveRoleOperation: &framework.PathOperation{
Callback: b.pathResolveRole,
},
},

HelpSynopsis: pathLoginHelpSyn,
HelpDescription: pathLoginHelpDesc,
}
}
func (b *GcpAuthBackend) pathResolveRole(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
if err := validateFields(req, data); err != nil {
return nil, logical.CodedError(http.StatusUnprocessableEntity, err.Error())
}

roleName := data.Get("role").(string)
if roleName == "" {
return logical.ErrorResponse("role is required"), nil
}

role, err := b.role(ctx, req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
return logical.ErrorResponse("role %q not found", roleName), nil
}

return logical.ResolveRoleResponse(roleName)
}

func (b *GcpAuthBackend) pathLogin(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
// Validate we didn't get extraneous fields
Expand Down Expand Up @@ -84,7 +107,7 @@ func (b *GcpAuthBackend) pathLogin(ctx context.Context, req *logical.Request, da
case gceRoleType:
return b.pathGceLogin(ctx, req, loginInfo)
default:
return logical.ErrorResponse("login against role type '%s' is unsupported", roleType), nil
return logical.ErrorResponse("login against role type %q is unsupported", roleType), nil
}
}

Expand All @@ -98,9 +121,9 @@ func (b *GcpAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Reques
if err != nil {
return nil, err
} else if role == nil {
return logical.ErrorResponse("role '%s' no longer exists", roleName), nil
return logical.ErrorResponse("role %q no longer exists", roleName), nil
} else if !policyutil.EquivalentPolicies(role.TokenPolicies, req.Auth.TokenPolicies) {
return logical.ErrorResponse("policies on role '%s' have changed, cannot renew", roleName), nil
return logical.ErrorResponse("policies on role %q have changed, cannot renew", roleName), nil
}

switch role.RoleType {
Expand All @@ -113,7 +136,7 @@ func (b *GcpAuthBackend) pathLoginRenew(ctx context.Context, req *logical.Reques
return logical.ErrorResponse(err.Error()), nil
}
default:
return nil, fmt.Errorf("unexpected role type '%s' for login renewal", role.RoleType)
return nil, fmt.Errorf("unexpected role type %q for login renewal", role.RoleType)
}

resp := &logical.Response{Auth: req.Auth}
Expand Down Expand Up @@ -150,19 +173,22 @@ func (b *GcpAuthBackend) parseAndValidateJwt(ctx context.Context, s logical.Stor
return nil, errors.New("unable to retrieve GCP configuration")
}

loginInfo.RoleName = data.Get("role").(string)
if loginInfo.RoleName == "" {
roleName := data.Get("role").(string)
if roleName == "" {
return nil, errors.New("role is required")
}

loginInfo.Role, err = b.role(ctx, s, loginInfo.RoleName)
role, err := b.role(ctx, s, roleName)
if err != nil {
return nil, err
}
if loginInfo.Role == nil {
return nil, fmt.Errorf("role '%s' not found", loginInfo.RoleName)
if role == nil {
return nil, fmt.Errorf("role %q not found", roleName)
}

loginInfo.RoleName = roleName
loginInfo.Role = role

// Process JWT string.
signedJwt, ok := data.GetOk("jwt")
if !ok {
Expand Down Expand Up @@ -278,7 +304,7 @@ func validateBaseJWTClaims(c *jwt.Claims, roleName string) error {
expectedAudSuffix := fmt.Sprintf(expectedJwtAudTemplate, roleName)
for _, aud := range c.Audience {
if !strings.HasSuffix(aud, expectedAudSuffix) {
return fmt.Errorf("at least one of the JWT claim 'aud' must end in '%s'", expectedAudSuffix)
return fmt.Errorf("at least one of the JWT claim 'aud' must end in %q", expectedAudSuffix)
}
}

Expand All @@ -296,7 +322,7 @@ func (b *GcpAuthBackend) pathIamLogin(ctx context.Context, req *logical.Request,
role := loginInfo.Role
if !role.AllowGCEInference && loginInfo.GceMetadata != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Got GCE token but IAM role '%s' does not allow GCE inference", loginInfo.RoleName)), nil
"Got GCE token but IAM role %q does not allow GCE inference", loginInfo.RoleName)), nil
}

// TODO(emilymye): move to general JWT validation once custom expiry is supported for other JWT types.
Expand Down Expand Up @@ -495,7 +521,7 @@ func (b *GcpAuthBackend) pathGceLogin(ctx context.Context, req *logical.Request,
EmailOrId: loginInfo.EmailOrId,
})
if err != nil {
return logical.ErrorResponse("Could not find service account '%s' used for GCE metadata token: %s", loginInfo.EmailOrId, err), nil
return logical.ErrorResponse("Could not find service account %q used for GCE metadata token: %s", loginInfo.EmailOrId, err), nil
}

auth := &logical.Auth{
Expand Down Expand Up @@ -641,7 +667,7 @@ func getInstanceMetadataFromAuth(authMetadata map[string]string) (*gcputil.GCEId
}
meta.ProjectNumber, err = strconv.ParseInt(projectNumber, 10, 64)
if err != nil {
return nil, fmt.Errorf("expected 'project_number' value '%s' to be a int64", projectNumber)
return nil, fmt.Errorf("expected 'project_number' value %q to be a int64", projectNumber)
}

createdAt, ok := authMetadata["instance_creation_timestamp"]
Expand All @@ -650,7 +676,7 @@ func getInstanceMetadataFromAuth(authMetadata map[string]string) (*gcputil.GCEId
}
meta.CreatedAt, err = strconv.ParseInt(createdAt, 10, 64)
if err != nil {
return nil, fmt.Errorf("expected 'instance_creation_timestamp' value '%s' to be int64", createdAt)
return nil, fmt.Errorf("expected 'instance_creation_timestamp' value %q to be int64", createdAt)
}

return meta, nil
Expand Down
77 changes: 77 additions & 0 deletions plugin/path_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,83 @@ import (
"gopkg.in/square/go-jose.v2/jwt"
)

func TestRoleResolution(t *testing.T) {
t.Parallel()

backend, storage := testBackend(t)
ctx := context.Background()

role := &gcpRole{
RoleID: "testRoleID",
RoleType: "iam",
MaxJwtExp: 30 * time.Minute,
}

roleName := "role-name"
entry, err := logical.StorageEntryJSON("role/"+roleName, role)
if err != nil {
t.Fatal(err)
}
if err := storage.Put(ctx, entry); err != nil {
t.Fatal(err)
}

loginReq := &logical.Request{
Operation: logical.ResolveRoleOperation,
Path: "login",
Storage: storage,
Data: map[string]interface{}{
"role": roleName,
},
Connection: &logical.Connection{
RemoteAddr: "127.0.0.1",
},
}

resp, err := backend.HandleRequest(context.Background(), loginReq)
if err != nil || (resp != nil && resp.IsError()) {
t.Fatalf("err:%v resp:%#v", err, resp)
}

if resp.Data["role"] != roleName {
t.Fatalf("Role was not as expected. Expected %s, received %s", roleName, resp.Data["role"])
}
}

func TestRoleResolution_RoleDoesNotExist(t *testing.T) {
t.Parallel()

backend, storage := testBackend(t)

roleName := "role-name"

loginReq := &logical.Request{
Operation: logical.ResolveRoleOperation,
Path: "login",
Storage: storage,
Data: map[string]interface{}{
"role": roleName,
},
Connection: &logical.Connection{
RemoteAddr: "127.0.0.1",
},
}

resp, err := backend.HandleRequest(context.Background(), loginReq)
if resp == nil && !resp.IsError() {
t.Fatalf("Response was not an error: err:%v resp:%#v", err, resp)
}

errString, ok := resp.Data["error"].(string)
if !ok {
t.Fatal("Error not part of response.")
}

if !strings.Contains(errString, "role \"role-name\" not found") {
t.Fatalf("Error was not due to invalid role name. Error: %s", errString)
}
}

func TestLogin_IAM(t *testing.T) {
t.Parallel()

Expand Down

0 comments on commit eee8ee8

Please sign in to comment.