Skip to content

Commit

Permalink
Add support for Context Aware Access UserAccessBinding resource (#4368)
Browse files Browse the repository at this point in the history
* Added basic GcpUserAccessBinding skeleton

* Corrected GcpUserAccessBinding base url and parameters

* Updated GcpUserAccessBinding example to include cloud identity group

* Exclude GcpUserAccessBinding from inspec

* Run gcp user access binding tests as part of access policy tests

* Trim groups/ prefix from group id

This should make the value match what gcp_user_access_binding expects

* Corrected trimprefix usage

* Added trimprefix usage to example

* Added handling of metadata field by operation waiters

* Added explicit self_link of {{name}}

* Marked group_key as input: true because it is required and immutable

* Switched to camel case for parameter/property names

* Use self link for import link

Follows pattern of UptimeCheckConfig and others.

* Switched custom_import because self_link_as_name expects project in path
  • Loading branch information
melinath authored Jan 11, 2021
1 parent d8494ee commit 3956192
Show file tree
Hide file tree
Showing 17 changed files with 251 additions and 13 deletions.
40 changes: 40 additions & 0 deletions products/accesscontextmanager/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1267,3 +1267,43 @@ objects:
Format: projects/{project_number}
required: true
input: true
- !ruby/object:Api::Resource
name: 'GcpUserAccessBinding'
base_url: "organizations/{{organization_id}}/gcpUserAccessBindings"
self_link: "{{name}}"
update_verb: :PATCH
update_mask: true
description: |
Restricts access to Cloud Console and Google Cloud APIs for a set of users using Context-Aware Access.
references: !ruby/object:Api::Resource::ReferenceLinks
api: 'https://cloud.google.com/access-context-manager/docs/reference/rest/v1/organizations.gcpUserAccessBindings'
parameters:
# Parent is a path parameter that _cannot_ be read or sent in the request at all.
# This must be done at the provider level.
- !ruby/object:Api::Type::String
name: organizationId
input: true
required: true
url_param_only: true
description: |
Required. ID of the parent organization.
properties:
- !ruby/object:Api::Type::String
name: 'name'
output: true
description: |
Immutable. Assigned by the server during creation. The last segment has an arbitrary length and has only URI unreserved characters (as defined by RFC 3986 Section 2.3). Should not be specified by the client during creation. Example: "organizations/256/gcpUserAccessBindings/b3-BhcX_Ud5N"
- !ruby/object:Api::Type::String
name: 'groupKey'
required: true
input: true
description: |
Required. Immutable. Google Group id whose members are subject to this binding's restrictions. See "id" in the G Suite Directory API's Groups resource. If a group's email address/alias is changed, this resource will continue to point at the changed group. This field does not accept group email addresses or aliases. Example: "01d520gv4vjcrht"
- !ruby/object:Api::Type::Array
name: 'accessLevels'
item_type: Api::Type::String
required: true
min_size: 1
max_size: 1
description: |
Required. Access level that a user must have to be granted access. Only one access level is supported, not multiple. This repeated field must have exactly one element. Example: "accessPolicies/9522/accessLevels/device_trusted"
2 changes: 2 additions & 0 deletions products/accesscontextmanager/inspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,5 @@ overrides: !ruby/object:Overrides::ResourceOverrides
exclude: true
ServicePerimeterResource: !ruby/object:Overrides::Inspec::ResourceOverride
exclude: true
GcpUserAccessBinding: !ruby/object:Overrides::Inspec::ResourceOverride
exclude: true
22 changes: 22 additions & 0 deletions products/accesscontextmanager/terraform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,28 @@ overrides: !ruby/object:Overrides::ResourceOverrides
service_perimeter_name: "restrict_all"
custom_code: !ruby/object:Provider::Terraform::CustomCode
custom_import: templates/terraform/custom_import/access_context_manager_service_perimeter_resource.go.erb
GcpUserAccessBinding: !ruby/object:Overrides::Terraform::ResourceOverride
id_format: "{{name}}"
import_format: ["{{name}}"]
autogen_async: true
exclude_validator: true
examples:
- !ruby/object:Provider::Terraform::Examples
name: "access_context_manager_gcp_user_access_binding_basic"
# Has a handwritten test due to AccessPolicy-related tests needing to run synchronously
skip_test: true
primary_resource_id: "gcp_user_access_binding"
vars:
group_id: "my-identity-group"
access_level_id: "access_level_id_for_user_access_binding"
access_level_name: "chromeos_no_lock"
test_env_vars:
org_id: :ORG_ID
org_domain: :ORG_DOMAIN
cust_id: :CUST_ID
custom_code: !ruby/object:Provider::Terraform::CustomCode
custom_import: templates/terraform/custom_import/set_id_name_with_slashes.go.erb

# This is for copying files over
files: !ruby/object:Provider::Config::Files
# These files have templating (ERB) code that will be run.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
resource "google_cloud_identity_group" "group" {
display_name = "<%= ctx[:vars]['group_id'] %>"

parent = "customers/<%= ctx[:test_env_vars]['cust_id'] %>"

group_key {
id = "<%= ctx[:vars]['group_id'] %>@<%= ctx[:test_env_vars]['org_domain'] %>"
}

labels = {
"cloudidentity.googleapis.com/groups.discussion_forum" = ""
}
}

resource "google_access_context_manager_access_level" "<%= ctx[:vars]['access_level_id'] %>" {
parent = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}"
name = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}/accessLevels/<%= ctx[:vars]['access_level_name'] %>"
title = "<%= ctx[:vars]['access_level_name'] %>"
basic {
conditions {
device_policy {
require_screen_lock = true
os_constraints {
os_type = "DESKTOP_CHROME_OS"
}
}
regions = [
"US",
]
}
}
}

resource "google_access_context_manager_access_policy" "access-policy" {
parent = "organizations/<%= ctx[:test_env_vars]['org_id'] %>"
title = "my policy"
}



resource "google_access_context_manager_gcp_user_access_binding" "<%= ctx[:primary_resource_id] %>" {
organization_id = "<%= ctx[:test_env_vars]['org_id'] %>"
group_key = trimprefix(google_cloud_identity_group.group.id, "groups/")
access_levels = [
google_access_context_manager_access_level.<%= ctx[:vars]['access_level_id'] %>.name,
]
}
7 changes: 5 additions & 2 deletions templates/terraform/operation.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ func (w *<%= product_name -%>OperationWaiter) QueryOp() (interface{}, error) {

func create<%= product_name %>Waiter(config *Config, op map[string]interface{}, <% if has_project -%> project, <% end -%> activity, userAgent string) (*<%=product_name%>OperationWaiter, error) {
if val, ok := op["name"]; !ok || val == "" {
// This was a synchronous call - there is no operation to wait for.
return nil, nil
// An operation could also be indicated with a "metadata" field.
if _, ok := op["metadata"]; !ok {
// This was a synchronous call - there is no operation to wait for.
return nil, nil
}
}
w := &<%= product_name -%>OperationWaiter{
Config: config,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// Since access approval settings are heirarchical, and only one can exist per folder/project/org,
// and all refer to the same organization, they need to be ran serially
// and all refer to the same organization, they need to be run serially
// See AccessApprovalOrganizationSettings for the test runner.
func testAccAccessApprovalFolderSettings(t *testing.T) {
context := map[string]interface{}{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// Since access approval settings are heirarchical, and only one can exist per folder/project/org,
// and all refer to the same organization, they need to be ran serially
// and all refer to the same organization, they need to be run serially
func TestAccAccessApprovalSettings(t *testing.T) {
testCases := map[string]func(t *testing.T){
"folder": testAccAccessApprovalFolderSettings,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// Since access approval settings are heirarchical, and only one can exist per folder/project/org,
// and all refer to the same organization, they need to be ran serially.
// and all refer to the same organization, they need to be run serially.
// See AccessApprovalOrganizationSettings for the test runner.
func testAccAccessApprovalProjectSettings(t *testing.T) {
context := map[string]interface{}{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be ran serially. See AccessPolicy for the test runner.
// can exist, they need to be run serially. See AccessPolicy for the test runner.

func testAccAccessContextManagerAccessLevelCondition_basicTest(t *testing.T) {
org := getTestOrgFromEnv(t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be ran serially. See AccessPolicy for the test runner.
// can exist, they need to be run serially. See AccessPolicy for the test runner.

func testAccAccessContextManagerAccessLevel_basicTest(t *testing.T) {
org := getTestOrgFromEnv(t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be ran serially. See AccessPolicy for the test runner.
// can exist, they need to be run serially. See AccessPolicy for the test runner.

func testAccAccessContextManagerAccessLevels_basicTest(t *testing.T) {
org := getTestOrgFromEnv(t)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func testSweepAccessContextManagerPolicies(region string) error {
}

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be ran serially
// can exist, they need to be run serially
func TestAccAccessContextManager(t *testing.T) {
testCases := map[string]func(t *testing.T){
"access_policy": testAccAccessContextManagerAccessPolicy_basicTest,
Expand All @@ -88,6 +88,7 @@ func TestAccAccessContextManager(t *testing.T) {
"access_levels": testAccAccessContextManagerAccessLevels_basicTest,
"access_level_condition": testAccAccessContextManagerAccessLevelCondition_basicTest,
"service_perimeters": testAccAccessContextManagerServicePerimeters_basicTest,
"gcp_user_access_binding": testAccAccessContextManagerGcpUserAccessBinding_basicTest,
}

for name, tc := range testCases {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package google

import (
"fmt"
"strings"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be run serially. See AccessPolicy for the test runner.

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

context := map[string]interface{}{
"org_id": getTestOrgFromEnv(t),
"org_domain": getTestOrgDomainFromEnv(t),
"cust_id": getTestCustIdFromEnv(t),
"random_suffix": randString(t, 10),
}

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
ExternalProviders: map[string]resource.ExternalProvider{
"random": {},
},
CheckDestroy: testAccCheckAccessContextManagerGcpUserAccessBindingDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccAccessContextManagerGcpUserAccessBinding_accessContextManagerGcpUserAccessBindingBasicExample(context),
},
{
ResourceName: "google_access_context_manager_gcp_user_access_binding.gcp_user_access_binding",
ImportState: true,
ImportStateVerify: true,
ImportStateVerifyIgnore: []string{"organization_id"},
},
},
})
}

func testAccAccessContextManagerGcpUserAccessBinding_accessContextManagerGcpUserAccessBindingBasicExample(context map[string]interface{}) string {
return Nprintf(`
resource "google_cloud_identity_group" "group" {
display_name = "tf-test-my-identity-group%{random_suffix}"
parent = "customers/%{cust_id}"
group_key {
id = "tf-test-my-identity-group%{random_suffix}@%{org_domain}"
}
labels = {
"cloudidentity.googleapis.com/groups.discussion_forum" = ""
}
}
resource "google_access_context_manager_access_level" "tf_test_access_level_id_for_user_access_binding%{random_suffix}" {
parent = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}"
name = "accessPolicies/${google_access_context_manager_access_policy.access-policy.name}/accessLevels/tf_test_chromeos_no_lock%{random_suffix}"
title = "tf_test_chromeos_no_lock%{random_suffix}"
basic {
conditions {
device_policy {
require_screen_lock = true
os_constraints {
os_type = "DESKTOP_CHROME_OS"
}
}
regions = [
"US",
]
}
}
}
resource "google_access_context_manager_access_policy" "access-policy" {
parent = "organizations/%{org_id}"
title = "my policy"
}
resource "google_access_context_manager_gcp_user_access_binding" "gcp_user_access_binding" {
organization_id = "%{org_id}"
group_key = trimprefix(google_cloud_identity_group.group.id, "groups/")
access_levels = [
google_access_context_manager_access_level.tf_test_access_level_id_for_user_access_binding%{random_suffix}.name,
]
}
`, context)
}

func testAccCheckAccessContextManagerGcpUserAccessBindingDestroyProducer(t *testing.T) func(s *terraform.State) error {
return func(s *terraform.State) error {
for name, rs := range s.RootModule().Resources {
if rs.Type != "google_access_context_manager_gcp_user_access_binding" {
continue
}
if strings.HasPrefix(name, "data.") {
continue
}

config := googleProviderConfig(t)

url, err := replaceVarsForTest(config, rs, "{{AccessContextManagerBasePath}}organizations/{{organization_id}}/gcpUserAccessBindings/{{name}}")
if err != nil {
return err
}

_, err = sendRequest(config, "GET", "", url, config.userAgent, nil)
if err == nil {
return fmt.Errorf("AccessContextManagerGcpUserAccessBinding still exists at %s", url)
}
}

return nil
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be ran serially. See AccessPolicy for the test runner.
// can exist, they need to be run serially. See AccessPolicy for the test runner.

func testAccAccessContextManagerServicePerimeterResource_basicTest(t *testing.T) {
// Multiple fine-grained resources
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be ran serially. See AccessPolicy for the test runner.
// can exist, they need to be run serially. See AccessPolicy for the test runner.
func testAccAccessContextManagerServicePerimeter_basicTest(t *testing.T) {
org := getTestOrgFromEnv(t)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (
)

// Since each test here is acting on the same organization and only one AccessPolicy
// can exist, they need to be ran serially. See AccessPolicy for the test runner.
// can exist, they need to be run serially. See AccessPolicy for the test runner.
func testAccAccessContextManagerServicePerimeters_basicTest(t *testing.T) {
org := getTestOrgFromEnv(t)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

// The service account TF uses needs the permission granted in the configs
// but it will get deleted by parallel tests, so they need to be ran serially.
// but it will get deleted by parallel tests, so they need to be run serially.
func TestAccBigqueryDataTransferConfig(t *testing.T) {
testCases := map[string]func(t *testing.T){
"basic": testAccBigqueryDataTransferConfig_scheduledQuery_basic,
Expand Down

0 comments on commit 3956192

Please sign in to comment.