Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c4692d24d7 | ||
|
|
89ac628428 | ||
|
|
437fc99875 | ||
|
|
80ce68602a | ||
|
|
d37e53f846 | ||
|
|
5677905e16 | ||
|
|
92a5d6424a | ||
|
|
4e5cd298d6 | ||
|
|
b5332e277b | ||
|
|
317e51b73c | ||
|
|
5c04b8a21e | ||
|
|
bcb928e4f9 | ||
|
|
877d285eef | ||
|
|
e17cbdb140 | ||
|
|
295eb7a018 | ||
|
|
c04cb367ab | ||
|
|
5b6541491e |
No files matched your search
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2024, 2025
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
contact_links:
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
## PCI review checklist
|
||||
|
||||
<!-- heimdall_github_prtemplate:grc-pci_dss-2024-01-05 -->
|
||||
|
||||
- [ ] I have documented a clear reason for, and description of, the change I am making.
|
||||
|
||||
- [ ] If applicable, I've documented a plan to revert these changes if they require more than reverting the pull request.
|
||||
|
||||
- [ ] If applicable, I've documented the impact of any changes to security controls.
|
||||
|
||||
Examples of changes to security controls include using new access control methods, adding or removing logging pipelines, etc.
|
||||
@@ -1,4 +1,4 @@
|
||||
Copyright (c) 2021 HashiCorp, Inc.
|
||||
Copyright IBM Corp. 2024, 2025
|
||||
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/hashicorp/boundary-plugin-gcp
|
||||
|
||||
go 1.21
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
cloud.google.com/go/iam v1.2.0
|
||||
@@ -9,7 +9,7 @@ require (
|
||||
github.com/googleapis/gax-go/v2 v2.13.0
|
||||
github.com/hashicorp/boundary/sdk v0.0.47
|
||||
github.com/hashicorp/terraform-json v0.23.0
|
||||
golang.org/x/oauth2 v0.22.0
|
||||
golang.org/x/oauth2 v0.27.0
|
||||
google.golang.org/api v0.196.0
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ require (
|
||||
go.opentelemetry.io/otel v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.29.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.29.0 // indirect
|
||||
golang.org/x/sync v0.8.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/time v0.6.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
@@ -55,10 +55,10 @@ require (
|
||||
github.com/mitchellh/reflectwalk v1.0.2 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.9.0
|
||||
golang.org/x/crypto v0.26.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.24.0 // indirect
|
||||
golang.org/x/text v0.17.0 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.66.0
|
||||
google.golang.org/protobuf v1.34.2
|
||||
|
||||
@@ -215,8 +215,8 @@ go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
@@ -234,27 +234,27 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.22.0 h1:BzDx2FehcG7jJwgWLELCdmLuxk2i+x9UDpSiss2u0ZA=
|
||||
golang.org/x/oauth2 v0.22.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
|
||||
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
|
||||
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
|
||||
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -119,15 +119,17 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
Zone: "us-central-1a",
|
||||
},
|
||||
expected: &Config{
|
||||
Zone: "us-central-1a",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
Zone: "us-central-1a",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "nil attributes",
|
||||
attrs: nil,
|
||||
expected: &Config{
|
||||
Scopes: []string{defaultGCPScope},
|
||||
Scopes: []string{defaultGCPScope},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -154,12 +156,13 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
ClientEmail: "test@test.com",
|
||||
},
|
||||
expected: &Config{
|
||||
ProjectId: "test-project",
|
||||
Zone: "us-central-1a",
|
||||
ClientEmail: "test@test.com",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
ProjectId: "test-project",
|
||||
Zone: "us-central-1a",
|
||||
ClientEmail: "test@test.com",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -220,12 +223,13 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
Zone: "us-central-1a",
|
||||
},
|
||||
expected: &Config{
|
||||
ProjectId: "test-project",
|
||||
Zone: "us-central-1a",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test@test.com",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
ProjectId: "test-project",
|
||||
Zone: "us-central-1a",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test@test.com",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -241,13 +245,14 @@ func TestGetCredentialsConfig(t *testing.T) {
|
||||
TargetServiceAccountId: "test-target-service-account",
|
||||
},
|
||||
expected: &Config{
|
||||
ProjectId: "test-project",
|
||||
Zone: "us-central-1a",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test@test.com",
|
||||
TargetServiceAccountId: "test-target-service-account",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
ProjectId: "test-project",
|
||||
Zone: "us-central-1a",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test@test.com",
|
||||
TargetServiceAccountId: "test-target-service-account",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -41,17 +41,21 @@ const (
|
||||
// and then controlling the service account's access by granting it IAM roles.
|
||||
// https://cloud.google.com/compute/docs/access/service-accounts#scopes_best_practice
|
||||
defaultGCPScope = "https://www.googleapis.com/auth/cloud-platform"
|
||||
// defaultValidateServiceAccountKeyTimeout is the timeout for validating
|
||||
// a service account key.
|
||||
defaultValidateServiceAccountKeyTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
// Config is the configuration for the GCP credential.
|
||||
type Config struct {
|
||||
ProjectId string
|
||||
PrivateKey string
|
||||
PrivateKeyId string
|
||||
Zone string
|
||||
ClientEmail string
|
||||
TargetServiceAccountId string
|
||||
Scopes []string
|
||||
ProjectId string
|
||||
PrivateKey string
|
||||
PrivateKeyId string
|
||||
Zone string
|
||||
ClientEmail string
|
||||
TargetServiceAccountId string
|
||||
Scopes []string
|
||||
validateServiceAccountKeyTimeout time.Duration
|
||||
}
|
||||
|
||||
// credentials represents a simplified version of the GCP credentials file format.
|
||||
@@ -72,13 +76,14 @@ func NewConfig(opt ...Option) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
c := &Config{
|
||||
ProjectId: opts.WithProjectId,
|
||||
PrivateKey: opts.WithPrivateKey,
|
||||
PrivateKeyId: opts.WithPrivateKeyId,
|
||||
ClientEmail: opts.WithClientEmail,
|
||||
TargetServiceAccountId: opts.WithTargetServiceAccountId,
|
||||
Zone: opts.WithZone,
|
||||
Scopes: opts.WithScopes,
|
||||
ProjectId: opts.WithProjectId,
|
||||
PrivateKey: opts.WithPrivateKey,
|
||||
PrivateKeyId: opts.WithPrivateKeyId,
|
||||
ClientEmail: opts.WithClientEmail,
|
||||
TargetServiceAccountId: opts.WithTargetServiceAccountId,
|
||||
Zone: opts.WithZone,
|
||||
Scopes: opts.WithScopes,
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
@@ -97,12 +102,14 @@ func (c *Config) toCredentials() *credentials {
|
||||
// clone returns a copy of the configuration.
|
||||
func (c *Config) clone() *Config {
|
||||
return &Config{
|
||||
ProjectId: c.ProjectId,
|
||||
PrivateKey: c.PrivateKey,
|
||||
PrivateKeyId: c.PrivateKeyId,
|
||||
ClientEmail: c.ClientEmail,
|
||||
TargetServiceAccountId: c.TargetServiceAccountId,
|
||||
Scopes: c.Scopes,
|
||||
ProjectId: c.ProjectId,
|
||||
Zone: c.Zone,
|
||||
PrivateKey: c.PrivateKey,
|
||||
PrivateKeyId: c.PrivateKeyId,
|
||||
ClientEmail: c.ClientEmail,
|
||||
TargetServiceAccountId: c.TargetServiceAccountId,
|
||||
Scopes: c.Scopes,
|
||||
validateServiceAccountKeyTimeout: c.validateServiceAccountKeyTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -403,13 +403,14 @@ func TestNewConfig(t *testing.T) {
|
||||
WithScopes([]string{"scope1", "scope2"}),
|
||||
},
|
||||
expected: &Config{
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test-email@example.com",
|
||||
TargetServiceAccountId: "target-service-account@example.com",
|
||||
Zone: "test-zone",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test-email@example.com",
|
||||
TargetServiceAccountId: "target-service-account@example.com",
|
||||
Zone: "test-zone",
|
||||
Scopes: []string{"scope1", "scope2"},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
@@ -417,7 +418,8 @@ func TestNewConfig(t *testing.T) {
|
||||
name: "No Options",
|
||||
options: []Option{},
|
||||
expected: &Config{
|
||||
Scopes: []string{defaultGCPScope},
|
||||
Scopes: []string{defaultGCPScope},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
@@ -442,13 +444,14 @@ func TestNewConfig(t *testing.T) {
|
||||
WithZone("test-zone"),
|
||||
},
|
||||
expected: &Config{
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test-email@example.com",
|
||||
TargetServiceAccountId: "target-service-account@example.com",
|
||||
Zone: "test-zone",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
PrivateKeyId: "test-private-key-id",
|
||||
ClientEmail: "test-email@example.com",
|
||||
TargetServiceAccountId: "target-service-account@example.com",
|
||||
Zone: "test-zone",
|
||||
Scopes: []string{defaultGCPScope},
|
||||
validateServiceAccountKeyTimeout: defaultValidateServiceAccountKeyTimeout,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -6,8 +6,10 @@ package credential
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
admin "cloud.google.com/go/iam/admin/apiv1"
|
||||
"cloud.google.com/go/iam/admin/apiv1/adminpb"
|
||||
@@ -19,8 +21,16 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
ComputeInstancesListPermission = "compute.instances.list"
|
||||
// ComputeInstancesListPermission is the IAM permission required
|
||||
// to list compute instances.
|
||||
ComputeInstancesListPermission = "compute.instances.list"
|
||||
// IAMServiceAccountKeysCreatePermission is the IAM permission
|
||||
// required to create a service account key.
|
||||
// This permission is required to rotate service account keys.
|
||||
IAMServiceAccountKeysCreatePermission = "iam.serviceAccountKeys.create"
|
||||
// IAMServiceAccountKeysDeletePermission is the IAM permission
|
||||
// required to delete a service account key.
|
||||
// This permission is required to rotate service account keys.
|
||||
IAMServiceAccountKeysDeletePermission = "iam.serviceAccountKeys.delete"
|
||||
)
|
||||
|
||||
@@ -40,14 +50,25 @@ type ServiceAccountPrivateKey struct {
|
||||
ClientX509CertURL string `json:"client_x509_cert_url"`
|
||||
}
|
||||
|
||||
// ValidateCredsCallback is called to validate the credentials after rotating the service account key.
|
||||
// The callback should return an error if the credentials are invalid.
|
||||
type ValidateCredsCallback func(*Config, ...option.ClientOption) error
|
||||
|
||||
// RotateServiceAccountKey takes the private key from this credentials config
|
||||
// and first creates a new private key and private key id, then deletes the
|
||||
// old private key.
|
||||
//
|
||||
// If deletion of the old private key is successful, the new private key and
|
||||
// private key id are written into the credentials config and nil is returned.
|
||||
// If validation fails or deleting the old private key fails, the function
|
||||
// attempts to delete the newly created key as a rollback operation.
|
||||
// On any error, the old credentials are not overwritten.
|
||||
func (c *Config) RotateServiceAccountKey(ctx context.Context, permissions []string, opts ...option.ClientOption) error {
|
||||
func (c *Config) RotateServiceAccountKey(
|
||||
ctx context.Context,
|
||||
permissions []string,
|
||||
validateCredsCallback ValidateCredsCallback,
|
||||
opts ...option.ClientOption,
|
||||
) error {
|
||||
if c.PrivateKey == "" {
|
||||
return status.Error(codes.InvalidArgument, "cannot rotate credentials when private key is not set")
|
||||
}
|
||||
@@ -69,12 +90,12 @@ func (c *Config) RotateServiceAccountKey(ctx context.Context, permissions []stri
|
||||
clientOptions := []option.ClientOption{option.WithTokenSource(creds.TokenSource)}
|
||||
clientOptions = append(clientOptions, opts...)
|
||||
|
||||
iamClient, err := admin.NewIamClient(ctx, clientOptions...)
|
||||
currentIAMClient, err := admin.NewIamClient(ctx, clientOptions...)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "error creating IAM client: %v", err)
|
||||
}
|
||||
|
||||
createServiceAccountKeyRes, err := iamClient.CreateServiceAccountKey(ctx, &adminpb.CreateServiceAccountKeyRequest{
|
||||
createServiceAccountKeyRes, err := currentIAMClient.CreateServiceAccountKey(ctx, &adminpb.CreateServiceAccountKeyRequest{
|
||||
Name: fmt.Sprintf("projects/%s/serviceAccounts/%s", c.ProjectId, c.ClientEmail),
|
||||
PrivateKeyType: adminpb.ServiceAccountPrivateKeyType_TYPE_GOOGLE_CREDENTIALS_FILE,
|
||||
KeyAlgorithm: adminpb.ServiceAccountKeyAlgorithm_KEY_ALG_RSA_2048,
|
||||
@@ -104,22 +125,40 @@ func (c *Config) RotateServiceAccountKey(ctx context.Context, permissions []stri
|
||||
clientOptions = []option.ClientOption{option.WithTokenSource(newCreds.TokenSource)}
|
||||
clientOptions = append(clientOptions, opts...)
|
||||
|
||||
// Validate that the new credentials have the necessary permissions.
|
||||
_, err = newConfig.ValidateIamPermissions(ctx, permissions, clientOptions...)
|
||||
// Validate the new service account key
|
||||
err = newConfig.ValidateServiceAccountKey(ctx, permissions, validateCredsCallback, clientOptions...)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.PermissionDenied, "error testing IAM permissions with rotated service account key: %v", err)
|
||||
// Roll back the new key if validation fails.
|
||||
rollbackErr := currentIAMClient.DeleteServiceAccountKey(ctx, &adminpb.DeleteServiceAccountKeyRequest{
|
||||
Name: fmt.Sprintf("projects/%s/serviceAccounts/%s/keys/%s", newConfig.ProjectId, newConfig.ClientEmail, newConfig.PrivateKeyId),
|
||||
})
|
||||
if rollbackErr != nil {
|
||||
return status.Errorf(codes.Internal, "error validating rotated service account key: %v; error rolling back newly created service account key: %v; service account may have multiple active keys", err, rollbackErr)
|
||||
}
|
||||
|
||||
return status.Errorf(codes.PermissionDenied, "error validating rotated service account key: %v; successfully rolled back newly created service account key", err)
|
||||
}
|
||||
|
||||
iamClient, err = admin.NewIamClient(ctx, clientOptions...)
|
||||
newIAMClient, err := admin.NewIamClient(ctx, clientOptions...)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "error creating IAM client with rotated service account key: %v", err)
|
||||
}
|
||||
|
||||
err = iamClient.DeleteServiceAccountKey(ctx, &adminpb.DeleteServiceAccountKeyRequest{
|
||||
err = newIAMClient.DeleteServiceAccountKey(ctx, &adminpb.DeleteServiceAccountKeyRequest{
|
||||
Name: fmt.Sprintf("projects/%s/serviceAccounts/%s/keys/%s", c.ProjectId, c.ClientEmail, c.PrivateKeyId),
|
||||
})
|
||||
// If deletion of the old key fails, attempt to delete the new key to avoid leaving a dangling key, then return an error.
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "error deleting service account key: %v", err)
|
||||
// delete the new key using old client since the new client may not have permissions that the old client has.
|
||||
rollbackErr := currentIAMClient.DeleteServiceAccountKey(ctx, &adminpb.DeleteServiceAccountKeyRequest{
|
||||
Name: fmt.Sprintf("projects/%s/serviceAccounts/%s/keys/%s", newConfig.ProjectId, newConfig.ClientEmail, newConfig.PrivateKeyId),
|
||||
})
|
||||
// If rollback also fails, return an error indicating both the failure to delete the old key and the failure to roll back the new key.
|
||||
if rollbackErr != nil {
|
||||
return status.Errorf(codes.Internal, "error deleting service account key: %v; error rolling back newly created service account key: %v; service account may have multiple active keys", err, rollbackErr)
|
||||
}
|
||||
|
||||
return status.Errorf(codes.Internal, "error deleting service account key: %v; successfully rolled back newly created service account key", err)
|
||||
}
|
||||
|
||||
c.PrivateKey = newConfig.PrivateKey
|
||||
@@ -128,6 +167,44 @@ func (c *Config) RotateServiceAccountKey(ctx context.Context, permissions []stri
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateServiceAccountKey validates the service account key by checking the IAM permissions
|
||||
// and calling the validation callback.
|
||||
// The function will retry validation until the timeout is reached.
|
||||
func (c *Config) ValidateServiceAccountKey(
|
||||
ctx context.Context,
|
||||
permissions []string,
|
||||
validateCredsCallback ValidateCredsCallback,
|
||||
opts ...option.ClientOption) error {
|
||||
var validatePermissionsErr error
|
||||
var validationCallbackErr error
|
||||
|
||||
ticker := time.NewTicker(200 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, c.validateServiceAccountKeyTimeout)
|
||||
defer cancel()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
return fmt.Errorf("failed to validate service account key before timeout: %w", errors.Join(validatePermissionsErr, validationCallbackErr))
|
||||
}
|
||||
return ctx.Err()
|
||||
case <-ticker.C:
|
||||
_, validatePermissionsErr = c.ValidateIamPermissions(ctx, permissions, opts...)
|
||||
|
||||
if validateCredsCallback != nil {
|
||||
validationCallbackErr = validateCredsCallback(c, opts...)
|
||||
}
|
||||
|
||||
if validatePermissionsErr == nil && validationCallbackErr == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Config) DeletePrivateKey(ctx context.Context, opts ...option.ClientOption) error {
|
||||
if c.PrivateKey == "" {
|
||||
return status.Error(codes.InvalidArgument, "cannot delete credentials when private key is not set")
|
||||
@@ -172,18 +249,7 @@ func (c *Config) ValidateIamPermissions(ctx context.Context, permissions []strin
|
||||
return nil, status.Error(codes.InvalidArgument, "permissions are required")
|
||||
}
|
||||
|
||||
creds, err := c.GenerateCredentials(ctx)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Unauthenticated, "error generating credentials: %v", err)
|
||||
}
|
||||
if creds.TokenSource == nil {
|
||||
return nil, status.Error(codes.Unauthenticated, "error generating credentials: token source is nil")
|
||||
}
|
||||
|
||||
clientOptions := []option.ClientOption{option.WithTokenSource(creds.TokenSource)}
|
||||
clientOptions = append(clientOptions, opts...)
|
||||
|
||||
rmClient, err := resourcemanager.NewProjectsClient(ctx, clientOptions...)
|
||||
rmClient, err := resourcemanager.NewProjectsClient(ctx, opts...)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "failed to create Resource Manager client: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"cloud.google.com/go/iam/admin/apiv1/adminpb"
|
||||
"cloud.google.com/go/iam/apiv1/iampb"
|
||||
@@ -30,6 +31,7 @@ func TestRotateServiceAccountKey(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
testValidateTimeout := 1 * time.Second
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -70,32 +72,95 @@ func TestRotateServiceAccountKey(t *testing.T) {
|
||||
},
|
||||
setup: func() {
|
||||
testIAMAdminServer.testCreateServiceAccountKeyError = errors.New("create key error")
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyErrors = nil
|
||||
testResourceServer.testIamPermissionsError = nil
|
||||
},
|
||||
expectedError: errors.New("create key error"),
|
||||
},
|
||||
{
|
||||
name: "DeleteServiceAccountKey returns error",
|
||||
name: "ValidateServiceAccountKey fails and rollback succeeds",
|
||||
config: &Config{
|
||||
PrivateKey: "some-private-key",
|
||||
PrivateKeyId: "some-private-key-id",
|
||||
ClientEmail: "client@example.com",
|
||||
},
|
||||
setup: func() {
|
||||
testIAMAdminServer.testCreateServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyError = errors.New("delete key error")
|
||||
},
|
||||
expectedError: errors.New("delete key error"),
|
||||
},
|
||||
{
|
||||
name: "Successful rotation",
|
||||
config: &Config{
|
||||
PrivateKey: "some-private-key",
|
||||
PrivateKeyId: "some-private-key-id",
|
||||
ClientEmail: "client@example.com",
|
||||
PrivateKey: "some-private-key",
|
||||
PrivateKeyId: "some-private-key-id",
|
||||
ClientEmail: "client@example.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
setup: func() {
|
||||
testIAMAdminServer.testCreateServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyErrors = []error{nil}
|
||||
testResourceServer.testIamPermissionsError = errors.New("iam permissions error")
|
||||
},
|
||||
expectedError: errors.New("successfully rolled back newly created service account key"),
|
||||
},
|
||||
{
|
||||
name: "ValidateServiceAccountKey fails and rollback fails",
|
||||
config: &Config{
|
||||
PrivateKey: "some-private-key",
|
||||
PrivateKeyId: "some-private-key-id",
|
||||
ClientEmail: "client@example.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
setup: func() {
|
||||
testIAMAdminServer.testCreateServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyErrors = []error{errors.New("rollback key error")}
|
||||
testResourceServer.testIamPermissionsError = errors.New("iam permissions error")
|
||||
},
|
||||
expectedError: errors.New("error rolling back newly created service account key"),
|
||||
},
|
||||
{
|
||||
name: "DeleteServiceAccountKey old key fails and rollback fails",
|
||||
config: &Config{
|
||||
PrivateKey: "some-private-key",
|
||||
PrivateKeyId: "some-private-key-id",
|
||||
ClientEmail: "client@example.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
setup: func() {
|
||||
testIAMAdminServer.testCreateServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyErrors = []error{
|
||||
errors.New("delete key error"),
|
||||
errors.New("rollback key error"),
|
||||
}
|
||||
testResourceServer.testIamPermissionsError = nil
|
||||
},
|
||||
expectedError: errors.New("error rolling back newly created service account key"),
|
||||
},
|
||||
{
|
||||
name: "DeleteServiceAccountKey old key fails and rollback succeeds",
|
||||
config: &Config{
|
||||
PrivateKey: "some-private-key",
|
||||
PrivateKeyId: "some-private-key-id",
|
||||
ClientEmail: "client@example.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
setup: func() {
|
||||
testIAMAdminServer.testCreateServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyErrors = []error{
|
||||
errors.New("delete key error"),
|
||||
nil,
|
||||
}
|
||||
testResourceServer.testIamPermissionsError = nil
|
||||
},
|
||||
expectedError: errors.New("successfully rolled back newly created service account key"),
|
||||
},
|
||||
{
|
||||
name: "Successful rotation",
|
||||
config: &Config{
|
||||
PrivateKey: "some-private-key",
|
||||
PrivateKeyId: "some-private-key-id",
|
||||
ClientEmail: "client@example.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
setup: func() {
|
||||
testIAMAdminServer.testCreateServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyError = nil
|
||||
testIAMAdminServer.testDeleteServiceAccountKeyErrors = nil
|
||||
testResourceServer.testIamPermissionsError = nil
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
@@ -128,8 +193,14 @@ func TestRotateServiceAccountKey(t *testing.T) {
|
||||
IAMServiceAccountKeysCreatePermission,
|
||||
IAMServiceAccountKeysDeletePermission,
|
||||
}
|
||||
validateCredsCallbackCalled := false
|
||||
|
||||
err = tt.config.RotateServiceAccountKey(ctx, permissions, testOptions...)
|
||||
validateCredsCallBack := func(c *Config, opts ...googleOption.ClientOption) error {
|
||||
validateCredsCallbackCalled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
err = tt.config.RotateServiceAccountKey(ctx, permissions, validateCredsCallBack, testOptions...)
|
||||
if tt.expectedError != nil {
|
||||
require.ErrorContains(t, err, tt.expectedError.Error())
|
||||
require.Equal(t, initialClientEmail, tt.config.ClientEmail)
|
||||
@@ -143,6 +214,7 @@ func TestRotateServiceAccountKey(t *testing.T) {
|
||||
require.NotEmpty(t, tt.config.PrivateKeyId)
|
||||
require.NotEqual(t, initialPrivateKey, tt.config.PrivateKey)
|
||||
require.NotEqual(t, initialPrivateKeyId, tt.config.PrivateKeyId)
|
||||
require.True(t, validateCredsCallbackCalled)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -183,16 +255,6 @@ func TestValidateIamPermissions(t *testing.T) {
|
||||
wantErr: true,
|
||||
expectedErr: "permissions are required",
|
||||
},
|
||||
{
|
||||
name: "Failed to Generate Credentials",
|
||||
config: &Config{
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
},
|
||||
permissions: []string{"permission1", "permission2"},
|
||||
wantErr: true,
|
||||
expectedErr: "failed to generate credentials",
|
||||
},
|
||||
{
|
||||
name: "Failed to Test IAM Permissions",
|
||||
config: &Config{
|
||||
@@ -264,6 +326,104 @@ func TestValidateIamPermissions(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServiceAccountKey(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
testResourceServer := &testResourceServer{}
|
||||
testValidateTimeout := 1 * time.Second
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
config *Config
|
||||
permissions []string
|
||||
validateCredsCallback ValidateCredsCallback
|
||||
testIamPermissionsError error
|
||||
expectedError error
|
||||
}{
|
||||
{
|
||||
name: "Successful validation",
|
||||
config: &Config{
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
ClientEmail: "test@test.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
permissions: []string{
|
||||
ComputeInstancesListPermission,
|
||||
IAMServiceAccountKeysCreatePermission,
|
||||
IAMServiceAccountKeysDeletePermission,
|
||||
},
|
||||
validateCredsCallback: func(c *Config, opts ...googleOption.ClientOption) error {
|
||||
return nil
|
||||
},
|
||||
expectedError: nil,
|
||||
},
|
||||
{
|
||||
name: "Validation callback returns error",
|
||||
config: &Config{
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
ClientEmail: "test@test.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
permissions: []string{
|
||||
ComputeInstancesListPermission,
|
||||
IAMServiceAccountKeysCreatePermission,
|
||||
IAMServiceAccountKeysDeletePermission,
|
||||
},
|
||||
validateCredsCallback: func(c *Config, opts ...googleOption.ClientOption) error {
|
||||
return errors.New("validation callback error")
|
||||
},
|
||||
expectedError: errors.New("validation callback error"),
|
||||
},
|
||||
{
|
||||
name: "IAM permissions validation returns error",
|
||||
config: &Config{
|
||||
ProjectId: "test-project-id",
|
||||
PrivateKey: "test-private-key",
|
||||
ClientEmail: "test@test.com",
|
||||
validateServiceAccountKeyTimeout: testValidateTimeout,
|
||||
},
|
||||
permissions: []string{
|
||||
ComputeInstancesListPermission,
|
||||
IAMServiceAccountKeysCreatePermission,
|
||||
IAMServiceAccountKeysDeletePermission,
|
||||
},
|
||||
testIamPermissionsError: errors.New("IAM permissions error"),
|
||||
expectedError: errors.New("failed to validate IAM permissions"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
testResourceServer.testIamPermissionsResponse = &iampb.TestIamPermissionsResponse{
|
||||
Permissions: tt.permissions,
|
||||
}
|
||||
testResourceServer.testIamPermissionsError = tt.testIamPermissionsError
|
||||
|
||||
gsrv := NewGRPCServer()
|
||||
resourcemanagerpb.RegisterProjectsServer(gsrv.Server, testResourceServer)
|
||||
addr, err := gsrv.Start()
|
||||
require.NoError(t, err)
|
||||
|
||||
testOptions := []googleOption.ClientOption{
|
||||
googleOption.WithEndpoint(addr),
|
||||
googleOption.WithoutAuthentication(),
|
||||
googleOption.WithGRPCDialOption(grpc.WithTransportCredentials(insecure.NewCredentials())),
|
||||
googleOption.WithTokenSource(nil),
|
||||
}
|
||||
|
||||
err = tt.config.ValidateServiceAccountKey(ctx, tt.permissions, tt.validateCredsCallback, testOptions...)
|
||||
if tt.expectedError != nil {
|
||||
require.ErrorContains(t, err, tt.expectedError.Error())
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeletePrivateKey(t *testing.T) {
|
||||
testIAMAdminServer := &testIAMAdminServer{}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -38,7 +38,11 @@ func NewPersistedState(opt ...Option) (*PersistedState, error) {
|
||||
}
|
||||
|
||||
// RotateCreds rotates the credentials for the GCP catalog and updates the last rotated time.
|
||||
func (s *PersistedState) RotateCreds(ctx context.Context, permissions []string, opts ...option.ClientOption) error {
|
||||
func (s *PersistedState) RotateCreds(
|
||||
ctx context.Context,
|
||||
permissions []string,
|
||||
validateCredsCallback ValidateCredsCallback,
|
||||
opts ...option.ClientOption) error {
|
||||
if s.CredentialsConfig == nil {
|
||||
return status.Error(codes.InvalidArgument, "missing credentials config")
|
||||
}
|
||||
@@ -46,7 +50,7 @@ func (s *PersistedState) RotateCreds(ctx context.Context, permissions []string,
|
||||
return status.Error(codes.InvalidArgument, "cannot rotate non-rotatable credentials")
|
||||
}
|
||||
|
||||
err := s.CredentialsConfig.RotateServiceAccountKey(ctx, permissions, opts...)
|
||||
err := s.CredentialsConfig.RotateServiceAccountKey(ctx, permissions, validateCredsCallback, opts...)
|
||||
if err != nil {
|
||||
return status.Errorf(codes.Internal, "failed to rotate credentials: %v", err)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package credential
|
||||
@@ -18,8 +18,9 @@ import (
|
||||
|
||||
type testIAMAdminServer struct {
|
||||
adminpb.UnimplementedIAMServer
|
||||
testCreateServiceAccountKeyError error
|
||||
testDeleteServiceAccountKeyError error
|
||||
testCreateServiceAccountKeyError error
|
||||
testDeleteServiceAccountKeyError error
|
||||
testDeleteServiceAccountKeyErrors []error
|
||||
}
|
||||
|
||||
func NewTestIAMAdminServer(createServiceAccountKeyError error, deleteServiceAccountKeyError error) *testIAMAdminServer {
|
||||
@@ -46,6 +47,13 @@ func (f *testIAMAdminServer) CreateServiceAccountKey(ctx context.Context, req *a
|
||||
}
|
||||
|
||||
func (f *testIAMAdminServer) DeleteServiceAccountKey(ctx context.Context, req *adminpb.DeleteServiceAccountKeyRequest) (*emptypb.Empty, error) {
|
||||
// Use queued delete results so tests can separately control deleting the old key and deleting the new key during rollback.
|
||||
if len(f.testDeleteServiceAccountKeyErrors) > 0 {
|
||||
err := f.testDeleteServiceAccountKeyErrors[0]
|
||||
f.testDeleteServiceAccountKeyErrors = f.testDeleteServiceAccountKeyErrors[1:]
|
||||
return &emptypb.Empty{}, err
|
||||
}
|
||||
|
||||
return &emptypb.Empty{}, f.testDeleteServiceAccountKeyError
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package errors
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package errors
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package values
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package plugin
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -108,7 +108,7 @@ func buildListInstancesRequest(attributes *SetAttributes, catalog *CatalogAttrib
|
||||
return nil, fmt.Errorf("error building filters: %w", err)
|
||||
}
|
||||
|
||||
if len(filters) > 1 {
|
||||
if len(filters) > 0 {
|
||||
filters := strings.Join(filters, " AND ")
|
||||
request.Filter = &filters
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -183,6 +183,23 @@ func TestBuildListInstancesRequest(t *testing.T) {
|
||||
},
|
||||
expectedErrContains: "error building filters",
|
||||
},
|
||||
{
|
||||
name: "valid request with no filters",
|
||||
attributes: &SetAttributes{
|
||||
Filters: []string{},
|
||||
},
|
||||
catalog: &CatalogAttributes{
|
||||
CredentialAttributes: &cred.CredentialAttributes{
|
||||
ProjectId: "test-12345",
|
||||
Zone: "us-central-1a",
|
||||
},
|
||||
},
|
||||
expected: &computepb.ListInstancesRequest{
|
||||
Project: "test-12345",
|
||||
Zone: "us-central-1a",
|
||||
Filter: stringPtr("status = running"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -87,8 +87,22 @@ func (p *HostPlugin) OnCreateCatalog(ctx context.Context, req *pb.OnCreateCatalo
|
||||
credential.IAMServiceAccountKeysDeletePermission,
|
||||
}
|
||||
|
||||
if !credState.CredentialsConfig.IsRotatable() && !catalogAttributes.DisableCredentialRotation {
|
||||
return nil, status.Error(codes.InvalidArgument, "cannot rotate credentials for non-rotatable credentials")
|
||||
}
|
||||
|
||||
if credState.CredentialsConfig.IsRotatable() && !catalogAttributes.DisableCredentialRotation {
|
||||
if err := credState.RotateCreds(ctx, permissions, p.testGCPClientOpts...); err != nil {
|
||||
state, err := newGCPCatalogPersistedState(
|
||||
append([]gcpCatalogPersistedStateOption{
|
||||
withCredentials(credState),
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err)
|
||||
}
|
||||
validateCredsCallback := state.ValidateCredsCallbackFn(ctx, p.testGCPClientOpts...)
|
||||
|
||||
if err := credState.RotateCreds(ctx, permissions, validateCredsCallback, p.testGCPClientOpts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -215,7 +229,17 @@ func (p *HostPlugin) OnUpdateCatalog(ctx context.Context, req *pb.OnUpdateCatalo
|
||||
// If we're enabling rotation now but didn't before, or have
|
||||
// freshly replaced credentials, we can rotate here.
|
||||
if !newCatalogAttributes.DisableCredentialRotation && credState.CredsLastRotatedTime.IsZero() {
|
||||
if err := credState.RotateCreds(ctx, permissions, p.testGCPClientOpts...); err != nil {
|
||||
state, err := newGCPCatalogPersistedState(
|
||||
append([]gcpCatalogPersistedStateOption{
|
||||
withCredentials(credState),
|
||||
}, p.testCatalogStateOpts...)...,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, status.Errorf(codes.Internal, "error setting up persisted state: %s", err)
|
||||
}
|
||||
validateCredsCallback := state.ValidateCredsCallbackFn(ctx, p.testGCPClientOpts...)
|
||||
|
||||
if err := credState.RotateCreds(ctx, permissions, validateCredsCallback, p.testGCPClientOpts...); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
@@ -593,11 +617,6 @@ func validateSet(s *hostsets.HostSet) error {
|
||||
}
|
||||
|
||||
badFields := make(map[string]string)
|
||||
_, filterSet := attrMap[ConstListInstancesFilter]
|
||||
|
||||
if filterSet && len(attrs.Filters) == 0 {
|
||||
badFields[fmt.Sprintf("attributes.%s", ConstListInstancesFilter)] = "must not be empty"
|
||||
}
|
||||
|
||||
for f := range attrMap {
|
||||
if _, ok := allowedSetFields[f]; !ok {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -878,6 +878,53 @@ func TestCreateCatalog(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with application default credentials and credential rotation disabled",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
cred.ConstProjectId: structpb.NewStringValue("test-project"),
|
||||
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
|
||||
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(true),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
catalogOpts: []gcpCatalogPersistedStateOption{
|
||||
withTestInstancesAPIFunc(newTestMockInstances(ctx,
|
||||
nil,
|
||||
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
|
||||
testMockInstancesWithListInstancesError(nil),
|
||||
)),
|
||||
},
|
||||
expectedRsp: &pb.OnCreateCatalogResponse{
|
||||
Persisted: &pb.HostCatalogPersisted{
|
||||
Secrets: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create with application default credentials and credential rotation enabled",
|
||||
req: &pb.OnCreateCatalogRequest{
|
||||
Catalog: &hostcatalogs.HostCatalog{
|
||||
Attrs: &hostcatalogs.HostCatalog_Attributes{
|
||||
Attributes: &structpb.Struct{
|
||||
Fields: map[string]*structpb.Value{
|
||||
cred.ConstProjectId: structpb.NewStringValue("test-project"),
|
||||
cred.ConstZone: structpb.NewStringValue("us-central1-a"),
|
||||
cred.ConstDisableCredentialRotation: structpb.NewBoolValue(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErr: "cannot rotate credentials for non-rotatable credentials",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/googleapis/gax-go/v2"
|
||||
"github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||
pb "github.com/hashicorp/boundary/sdk/pbs/plugin"
|
||||
"google.golang.org/api/iterator"
|
||||
"google.golang.org/api/option"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
@@ -99,3 +100,30 @@ func (s *gcpCatalogPersistedState) InstancesClient(
|
||||
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// validateCredsCallbackFn returns a ValidateCredsCallback function that
|
||||
// validates the credentials by listing instances in the project and zone.
|
||||
func (s *gcpCatalogPersistedState) ValidateCredsCallbackFn(
|
||||
ctx context.Context,
|
||||
opts ...option.ClientOption,
|
||||
) credential.ValidateCredsCallback {
|
||||
return func(config *credential.Config, opts ...option.ClientOption) error {
|
||||
s.CredentialsConfig = config
|
||||
instancesClient, err := s.InstancesClient(ctx, opts...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating instances client: %w", err)
|
||||
}
|
||||
|
||||
it := instancesClient.List(ctx, &computepb.ListInstancesRequest{
|
||||
Project: s.CredentialsConfig.ProjectId,
|
||||
Zone: s.CredentialsConfig.Zone,
|
||||
})
|
||||
|
||||
_, err = it.Next()
|
||||
if err != nil && err != iterator.Done {
|
||||
return fmt.Errorf("error listing instances: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,15 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
computepb "cloud.google.com/go/compute/apiv1/computepb"
|
||||
"github.com/hashicorp/boundary-plugin-gcp/internal/credential"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -87,3 +89,44 @@ func TestInstancesClient(t *testing.T) {
|
||||
assert.Contains(t, err.Error(), "error creating instances client")
|
||||
})
|
||||
}
|
||||
func TestValidateCredsCallbackFn(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cred := &credential.PersistedState{
|
||||
CredentialsConfig: &credential.Config{
|
||||
ProjectId: "test-project-id",
|
||||
Zone: "test-zone",
|
||||
},
|
||||
}
|
||||
state := &gcpCatalogPersistedState{PersistedState: cred}
|
||||
|
||||
t.Run("List instances error", func(t *testing.T) {
|
||||
state.testInstancesAPIFunc = newTestMockInstances(ctx,
|
||||
nil,
|
||||
testMockInstancesWithListInstancesOutput(nil),
|
||||
testMockInstancesWithListInstancesError(errors.New("error listing instances")),
|
||||
)
|
||||
|
||||
callback := state.ValidateCredsCallbackFn(ctx)
|
||||
err := callback(&credential.Config{
|
||||
ProjectId: "test-project-id",
|
||||
Zone: "test-zone",
|
||||
})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "error listing instances")
|
||||
})
|
||||
|
||||
t.Run("Success", func(t *testing.T) {
|
||||
state.testInstancesAPIFunc = newTestMockInstances(ctx,
|
||||
nil,
|
||||
testMockInstancesWithListInstancesOutput(&computepb.InstanceList{}),
|
||||
testMockInstancesWithListInstancesError(nil),
|
||||
)
|
||||
|
||||
callback := state.ValidateCredsCallbackFn(ctx)
|
||||
err := callback(&credential.Config{
|
||||
ProjectId: "test-project-id",
|
||||
Zone: "test-zone",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package host
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2024, 2025
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
resource "random_id" "service_account_client_email" {
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2024, 2025
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
output "client_email" {
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2024, 2025
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
terraform {
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
# Copyright (c) HashiCorp, Inc.
|
||||
# Copyright IBM Corp. 2024, 2025
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
data "google_client_openid_userinfo" "current" {}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// Copyright IBM Corp. 2024, 2025
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
package testing
|
||||
|
||||
Reference in new issue
Block a user