Skip to content

Commit e08a968

Browse files
Adding DataSource for KMS Autokey Keyhandle (#12553) (#8933)
[upstream:0669054c194d06807b6f086a3019755c50f04df2] Signed-off-by: Modular Magician <[email protected]>
1 parent 0e3f70d commit e08a968

File tree

7 files changed

+451
-10
lines changed

7 files changed

+451
-10
lines changed

.changelog/12553.txt

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
```release-note:new-datasource
2+
`google_kms_key_handle`
3+
```

google-beta/acctest/bootstrap_test_utils.go

+301-10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import (
1212
"testing"
1313
"time"
1414
// For beta tests only
15+
"github.com/hashicorp/terraform-provider-google-beta/google-beta/services/kms"
16+
tpgservicusage "github.com/hashicorp/terraform-provider-google-beta/google-beta/services/serviceusage"
17+
resourceManagerV3 "google.golang.org/api/cloudresourcemanager/v3"
1518
"net/http"
1619

1720
"github.com/hashicorp/terraform-provider-google-beta/google-beta/envvar"
@@ -35,6 +38,11 @@ import (
3538
)
3639

3740
var SharedKeyRing = "tftest-shared-keyring-1"
41+
42+
var DefaultKeyHandleName = "eed58b7b-20ad-4da8-ad85-ba78a0d5ab87"
43+
var DefaultKeyHandleResourceType = "compute.googleapis.com/Disk"
44+
var CloudKmsSrviceName = "cloudkms.googleapis.com"
45+
3846
var SharedCryptoKey = map[string]string{
3947
"ENCRYPT_DECRYPT": "tftest-shared-key-1",
4048
"ASYMMETRIC_SIGN": "tftest-shared-sign-key-1",
@@ -76,6 +84,237 @@ func BootstrapKMSKeyWithPurposeInLocation(t *testing.T, purpose, locationID stri
7684
return BootstrapKMSKeyWithPurposeInLocationAndName(t, purpose, locationID, SharedCryptoKey[purpose])
7785
}
7886

87+
type BootstrappedKMSAutokey struct {
88+
*cloudkms.KeyHandle
89+
}
90+
91+
func BootstrapKMSAutokeyKeyHandle(t *testing.T) BootstrappedKMSAutokey {
92+
return BootstrapKMSAutokeyKeyHandleWithLocation(t, "global")
93+
}
94+
95+
func BootstrapKMSAutokeyKeyHandleWithLocation(t *testing.T, locationID string) BootstrappedKMSAutokey {
96+
config := BootstrapConfig(t)
97+
if config == nil {
98+
return BootstrappedKMSAutokey{
99+
&cloudkms.KeyHandle{},
100+
}
101+
}
102+
103+
autokeyFolder, kmsProject, resourceProject := setupAutokeyTestResources(t, config)
104+
105+
// Enable autokey on autokey test folder
106+
kmsClient := config.NewKmsClient(config.UserAgent)
107+
autokeyConfigID := fmt.Sprintf("%s/autokeyConfig", autokeyFolder.Name)
108+
_, err := kmsClient.Folders.UpdateAutokeyConfig(autokeyConfigID, &cloudkms.AutokeyConfig{
109+
KeyProject: fmt.Sprintf("projects/%s", kmsProject.ProjectId),
110+
}).UpdateMask("keyProject").Do()
111+
if err != nil {
112+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot enable autokey on autokey test folder: %s", err)
113+
}
114+
115+
keyHandleParent := fmt.Sprintf("projects/%s/locations/%s", resourceProject.ProjectId, locationID)
116+
keyHandleName := fmt.Sprintf("%s/keyHandles/%s", keyHandleParent, DefaultKeyHandleName)
117+
118+
// Get or Create the hard coded keyHandle for testing
119+
keyHandle, err := kmsClient.Projects.Locations.KeyHandles.Get(keyHandleName).Do()
120+
121+
if err != nil {
122+
if transport_tpg.IsGoogleApiErrorWithCode(err, 404) {
123+
newKeyHandle := cloudkms.KeyHandle{
124+
ResourceTypeSelector: DefaultKeyHandleResourceType,
125+
}
126+
127+
keyHandleOp, err := kmsClient.Projects.Locations.KeyHandles.Create(keyHandleParent, &newKeyHandle).KeyHandleId(DefaultKeyHandleName).Do()
128+
if err != nil {
129+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot create new KeyHandle: %s", err)
130+
}
131+
132+
opAsMap, err := tpgresource.ConvertToMap(keyHandleOp)
133+
if err != nil {
134+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot get operation map: %s", err)
135+
}
136+
137+
var response map[string]interface{}
138+
err = kms.KMSOperationWaitTimeWithResponse(config, opAsMap, &response, resourceProject.ProjectId, "creating keyHandle", config.UserAgent, time.Duration(5)*time.Minute)
139+
if err != nil {
140+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot wait for create keyhandle operation: %s", err)
141+
}
142+
keyHandle = &cloudkms.KeyHandle{
143+
Name: response["name"].(string),
144+
KmsKey: response["kmsKey"].(string),
145+
ResourceTypeSelector: response["resourceTypeSelector"].(string),
146+
}
147+
} else {
148+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot call KeyHandle service: %s", err)
149+
}
150+
}
151+
152+
if keyHandle == nil {
153+
t.Fatalf("unable to bootstrap KMS keyHandle. KeyHandle is nil!")
154+
}
155+
156+
return BootstrappedKMSAutokey{
157+
keyHandle,
158+
}
159+
}
160+
161+
func setupAutokeyTestResources(t *testing.T, config *transport_tpg.Config) (*resourceManagerV3.Folder, *cloudresourcemanager.Project, *cloudresourcemanager.Project) {
162+
projectIDSuffix := strings.Replace(envvar.GetTestProjectFromEnv(), "ci-test-project-", "", 1)
163+
defaultAutokeyTestFolderName := fmt.Sprintf("autokeytest-%s-fd", projectIDSuffix)
164+
defaultAutokeyTestKmsProject := fmt.Sprintf("test-kms-%s-prj", projectIDSuffix)
165+
defaultAutokeyTestResourceProject := fmt.Sprintf("test-res-%s-prj", projectIDSuffix)
166+
167+
curUserEmail, err := transport_tpg.GetCurrentUserEmail(config, config.UserAgent)
168+
if err != nil {
169+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot get current usr: %s", err)
170+
}
171+
// create a folder to configure autokey config and resource folder
172+
autokeyFolder := BootstrapFolder(t, defaultAutokeyTestFolderName)
173+
parent := &cloudresourcemanager.ResourceId{
174+
Type: "folder",
175+
Id: strings.Split(autokeyFolder.Name, "/")[1],
176+
}
177+
// create and setup kms project for hosting keyring and keys for autokey
178+
kmsProject := BootstrapProjectWithParent(t, defaultAutokeyTestKmsProject, envvar.GetTestBillingAccountFromEnv(t), parent, []string{CloudKmsSrviceName})
179+
kmsProjectID := fmt.Sprintf("projects/%s", kmsProject.ProjectId)
180+
kmsSAEmail, err := GenerateCloudKmsServiceIdentity(config, fmt.Sprintf("%v", kmsProject.ProjectNumber))
181+
if err != nil {
182+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot create cloudkms service identity: %s", err)
183+
}
184+
err = addFolderBinding2(config.NewResourceManagerV3Client(config.UserAgent), autokeyFolder.Name, fmt.Sprintf("user:%s", curUserEmail), []string{"roles/cloudkms.admin"})
185+
if err != nil {
186+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot assign cloudkms.admin role to current user on autokey test folder: %s", err)
187+
}
188+
err = addProjectBinding(config.NewResourceManagerV3Client(config.UserAgent), kmsProjectID, fmt.Sprintf("user:%s", curUserEmail), []string{"roles/resourcemanager.projectIamAdmin", "roles/cloudkms.admin"})
189+
if err != nil {
190+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot assign cloudkms.admin and projectIamAdmin role to current user on kms project: %s", err)
191+
}
192+
err = addProjectBinding(config.NewResourceManagerV3Client(config.UserAgent), kmsProjectID, fmt.Sprintf("serviceAccount:%s", kmsSAEmail), []string{"roles/cloudkms.admin"})
193+
if err != nil {
194+
t.Errorf("unable to bootstrap KMS keyHandle. Cannot assign cloudkms.admin role to cloudkms service identity on kms project: %s", err)
195+
}
196+
197+
// create and setup resource folder to host keyhandle
198+
resourceProject := BootstrapProjectWithParent(t, defaultAutokeyTestResourceProject, envvar.GetTestBillingAccountFromEnv(t), parent, []string{})
199+
return autokeyFolder, kmsProject, resourceProject
200+
}
201+
202+
// GenerateCloudKmsServiceIdentity generates cloud kms service identity within a project
203+
func GenerateCloudKmsServiceIdentity(config *transport_tpg.Config, projectNum string) (string, error) {
204+
url := fmt.Sprintf("https://serviceusage.googleapis.com/v1beta1/projects/%s/services/%s:generateServiceIdentity", projectNum, CloudKmsSrviceName)
205+
206+
res, err := transport_tpg.SendRequest(transport_tpg.SendRequestOptions{
207+
Config: config,
208+
Method: "POST",
209+
Project: projectNum,
210+
RawURL: url,
211+
UserAgent: config.UserAgent,
212+
Timeout: time.Minute * 4,
213+
})
214+
if err != nil {
215+
return "", fmt.Errorf("error creating cloudkms service identity: %s", err)
216+
}
217+
218+
var opRes map[string]interface{}
219+
err = tpgservicusage.ServiceUsageOperationWaitTimeWithResponse(
220+
config, res, &opRes, projectNum, "Creating cloudkms service identity", config.UserAgent,
221+
time.Minute*4)
222+
if err != nil {
223+
return "", err
224+
}
225+
return fmt.Sprintf("service-%[email protected]", projectNum), nil
226+
}
227+
228+
func addProjectBinding(crmService *resourceManagerV3.Service, projectID string, member string, roles []string) error {
229+
return addBinding(crmService, "project", projectID, member, roles)
230+
}
231+
232+
func addFolderBinding2(crmService *resourceManagerV3.Service, folderID string, member string, roles []string) error {
233+
return addBinding(crmService, "folder", folderID, member, roles)
234+
}
235+
236+
// addBinding adds the member to the project's IAM policy
237+
func addBinding(crmService *resourceManagerV3.Service, resourceType string, resourceID string, member string, roles []string) error {
238+
239+
policy, err := getPolicy(crmService, resourceType, resourceID)
240+
if err != nil {
241+
return err
242+
}
243+
244+
// Find the policy binding for role. Only one binding can have the role.
245+
var binding *resourceManagerV3.Binding
246+
for _, role := range roles {
247+
for _, b := range policy.Bindings {
248+
if b.Role == role {
249+
binding = b
250+
break
251+
}
252+
}
253+
254+
if binding != nil {
255+
// If the binding exists, adds the member to the binding
256+
binding.Members = append(binding.Members, member)
257+
} else {
258+
// If the binding does not exist, adds a new binding to the policy
259+
binding = &resourceManagerV3.Binding{
260+
Role: role,
261+
Members: []string{member},
262+
}
263+
policy.Bindings = append(policy.Bindings, binding)
264+
}
265+
}
266+
setPolicy(crmService, resourceType, resourceID, policy)
267+
return nil
268+
}
269+
270+
// getPolicy gets the IAM policy on input resourceID
271+
// resourceType can be "project" or "folder"
272+
func getPolicy(crmService *resourceManagerV3.Service, resourceType string, resourceID string) (*resourceManagerV3.Policy, error) {
273+
274+
ctx := context.Background()
275+
276+
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
277+
defer cancel()
278+
request := new(resourceManagerV3.GetIamPolicyRequest)
279+
var policy *resourceManagerV3.Policy
280+
var err error
281+
if resourceType == "project" {
282+
policy, err = crmService.Projects.GetIamPolicy(resourceID, request).Do()
283+
} else if resourceType == "folder" {
284+
policy, err = crmService.Folders.GetIamPolicy(resourceID, request).Do()
285+
} else {
286+
return nil, fmt.Errorf("invalid resourceType, supported values: project or folder")
287+
}
288+
if err != nil {
289+
return nil, fmt.Errorf("error getting iam policy: %s", err)
290+
}
291+
return policy, nil
292+
}
293+
294+
// setPolicy sets the IAM policy on input resourceID
295+
// resourceType can be "project" or "folder"
296+
func setPolicy(crmService *resourceManagerV3.Service, resourceType string, resourceID string, policy *resourceManagerV3.Policy) error {
297+
298+
ctx := context.Background()
299+
300+
ctx, cancel := context.WithTimeout(ctx, time.Second*10)
301+
defer cancel()
302+
request := new(resourceManagerV3.SetIamPolicyRequest)
303+
request.Policy = policy
304+
var err error
305+
if resourceType == "project" {
306+
_, err = crmService.Projects.SetIamPolicy(resourceID, request).Do()
307+
} else if resourceType == "folder" {
308+
_, err = crmService.Folders.SetIamPolicy(resourceID, request).Do()
309+
} else {
310+
return fmt.Errorf("invalid resourceType, supported values: project or folder")
311+
}
312+
if err != nil {
313+
return fmt.Errorf("error setting iam policy: %s", err)
314+
}
315+
return nil
316+
}
317+
79318
func BootstrapKMSKeyWithPurposeInLocationAndName(t *testing.T, purpose, locationID, keyShortName string) BootstrappedKMS {
80319
config := BootstrapConfig(t)
81320
if config == nil {
@@ -630,37 +869,89 @@ func BootstrapServicePerimeterProjects(t *testing.T, desiredProjects int) []*clo
630869
return projects
631870
}
632871

872+
// BootstrapFolder creates or get a folder having a input folderDisplayName within a TestOrgEnv
873+
func BootstrapFolder(t *testing.T, folderDisplayName string) *resourceManagerV3.Folder {
874+
config := BootstrapConfig(t)
875+
if config == nil {
876+
return nil
877+
}
878+
879+
crmClient := config.NewResourceManagerV3Client(config.UserAgent)
880+
searchQuery := fmt.Sprintf("displayName=%s", folderDisplayName)
881+
folderSearchResp, err := crmClient.Folders.Search().Query(searchQuery).Do()
882+
if err != nil {
883+
t.Fatalf("error searching for folder with displayName: %s", folderDisplayName)
884+
}
885+
var folder *resourceManagerV3.Folder
886+
if len(folderSearchResp.Folders) == 0 {
887+
op, err := crmClient.Folders.Create(&resourceManagerV3.Folder{
888+
DisplayName: folderDisplayName,
889+
Parent: fmt.Sprintf("organizations/%s", envvar.GetTestOrgFromEnv(t)),
890+
}).Do()
891+
if err != nil {
892+
t.Fatalf("error bootstrapping test folder: %s", err)
893+
}
894+
895+
opAsMap, err := tpgresource.ConvertToMap(op)
896+
if err != nil {
897+
t.Fatalf("error converting folder operation map: %s", err)
898+
}
899+
var responseMap map[string]interface{}
900+
err = resourcemanager.ResourceManagerOperationWaitTimeWithResponse(config, opAsMap, &responseMap, "creating folder", config.UserAgent, 4*time.Minute)
901+
if err != nil {
902+
t.Fatalf("error waiting for create folder operation: %s", err)
903+
}
904+
folder, err = crmClient.Folders.Get(responseMap["name"].(string)).Do()
905+
if err != nil {
906+
t.Fatalf("error getting folder: %s", err)
907+
}
908+
} else {
909+
folder = folderSearchResp.Folders[0]
910+
}
911+
912+
if folder.State == "DELETE_REQUESTED" {
913+
_, err := crmClient.Folders.Undelete(folder.Name, &resourceManagerV3.UndeleteFolderRequest{}).Do()
914+
if err != nil {
915+
t.Fatalf("error undeleting folder: %s", err)
916+
}
917+
}
918+
return folder
919+
}
920+
633921
// BootstrapProject will create or get a project named
634922
// "<projectIDPrefix><projectIDSuffix>" that will persist across test runs,
635923
// where projectIDSuffix is based off of getTestProjectFromEnv(). The reason
636924
// for the naming is to isolate bootstrapped projects by test environment.
637925
// Given the existing projects being used by our team, the prefix provided to
638926
// this function can be no longer than 18 characters.
639927
func BootstrapProject(t *testing.T, projectIDPrefix, billingAccount string, services []string) *cloudresourcemanager.Project {
640-
config := BootstrapConfig(t)
641-
if config == nil {
642-
return nil
928+
org := envvar.GetTestOrgFromEnv(t)
929+
parent := &cloudresourcemanager.ResourceId{
930+
Type: "organization",
931+
Id: org,
643932
}
644-
645933
projectIDSuffix := strings.Replace(envvar.GetTestProjectFromEnv(), "ci-test-project-", "", 1)
646934
projectID := projectIDPrefix + projectIDSuffix
647935

936+
return BootstrapProjectWithParent(t, projectID, billingAccount, parent, services)
937+
}
938+
939+
func BootstrapProjectWithParent(t *testing.T, projectID string, billingAccount string, parent *cloudresourcemanager.ResourceId, services []string) *cloudresourcemanager.Project {
940+
config := BootstrapConfig(t)
941+
if config == nil {
942+
return nil
943+
}
648944
crmClient := config.NewResourceManagerClient(config.UserAgent)
649945

650946
project, err := crmClient.Projects.Get(projectID).Do()
651947
if err != nil {
652948
if !transport_tpg.IsGoogleApiErrorWithCode(err, 403) {
653949
t.Fatalf("Error getting bootstrapped project: %s", err)
654950
}
655-
org := envvar.GetTestOrgFromEnv(t)
656-
657951
op, err := crmClient.Projects.Create(&cloudresourcemanager.Project{
658952
ProjectId: projectID,
659953
Name: "Bootstrapped Test Project",
660-
Parent: &cloudresourcemanager.ResourceId{
661-
Type: "organization",
662-
Id: org,
663-
},
954+
Parent: parent,
664955
}).Do()
665956
if err != nil {
666957
t.Fatalf("Error creating bootstrapped test project: %s", err)

google-beta/provider/provider_mmv1_resources.go

+1
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,7 @@ var handwrittenDatasources = map[string]*schema.Resource{
282282
"google_kms_crypto_key_versions": kms.DataSourceGoogleKmsCryptoKeyVersions(),
283283
"google_kms_key_ring": kms.DataSourceGoogleKmsKeyRing(),
284284
"google_kms_key_rings": kms.DataSourceGoogleKmsKeyRings(),
285+
"google_kms_key_handle": kms.DataSourceGoogleKmsKeyHandle(),
285286
"google_kms_secret": kms.DataSourceGoogleKmsSecret(),
286287
"google_kms_secret_ciphertext": kms.DataSourceGoogleKmsSecretCiphertext(),
287288
"google_kms_secret_asymmetric": kms.DataSourceGoogleKmsSecretAsymmetric(),

0 commit comments

Comments
 (0)