diff --git a/products/cloudidentity/api.yaml b/products/cloudidentity/api.yaml new file mode 100644 index 000000000000..a665dd471532 --- /dev/null +++ b/products/cloudidentity/api.yaml @@ -0,0 +1,321 @@ +# Copyright 2020 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- !ruby/object:Api::Product +name: CloudIdentity +display_name: Cloud Identity +versions: + - !ruby/object:Api::Product::Version + name: beta + base_url: https://cloudidentity.googleapis.com/v1beta1/ +scopes: + - https://www.googleapis.com/auth/cloud-identity +apis_required: + - !ruby/object:Api::Product::ApiReference + name: Cloud Identity API + url: https://console.cloud.google.com/apis/api/cloudidentity.googleapis.com/overview +objects: + - !ruby/object:Api::Resource + name: 'Group' + base_url: groups + update_url: '{{name}}' + self_link: '{{name}}' + update_verb: :PATCH + update_mask: true + description: | + A Cloud Identity resource representing a Group. + properties: + - !ruby/object:Api::Type::String + name: 'name' + output: true + description: | + Resource name of the Group in the format: groups/{group_id}, where group_id + is the unique ID assigned to the Group. + - !ruby/object:Api::Type::NestedObject + name: 'groupKey' + required: true + input: true + description: | + EntityKey of the Group. + properties: + - !ruby/object:Api::Type::String + name: 'id' + required: true + input: true + description: | + The ID of the entity. + + For Google-managed entities, the id must be the email address of an existing + group or user. + + For external-identity-mapped entities, the id must be a string conforming + to the Identity Source's requirements. + + Must be unique within a namespace. + - !ruby/object:Api::Type::String + name: 'namespace' + input: true + description: | + The namespace in which the entity exists. + + If not specified, the EntityKey represents a Google-managed entity + such as a Google user or a Google Group. + + If specified, the EntityKey represents an external-identity-mapped group. + The namespace must correspond to an identity source created in Admin Console + and must be in the form of `identitysources/{identity_source_id}`. + - !ruby/object:Api::Type::String + name: 'parent' + required: true + input: true + description: | + The resource name of the entity under which this Group resides in the + Cloud Identity resource hierarchy. + + Must be of the form identitysources/{identity_source_id} for external-identity-mapped + groups or customers/{customer_id} for Google Groups. + - !ruby/object:Api::Type::String + name: 'displayName' + description: | + The display name of the Group. + - !ruby/object:Api::Type::String + name: 'description' + description: | + An extended description to help users determine the purpose of a Group. + Must not be longer than 4,096 characters. + - !ruby/object:Api::Type::String + name: 'createTime' + output: true + description: | + The time when the Group was created. + - !ruby/object:Api::Type::String + name: 'updateTime' + output: true + description: | + The time when the Group was last updated. + - !ruby/object:Api::Type::KeyValuePairs + name: 'labels' + required: true + input: true + description: | + The labels that apply to the Group. + + Must not contain more than one entry. Must contain the entry + 'cloudidentity.googleapis.com/groups.discussion_forum': '' if the Group is a Google Group or + 'system/groups/external': '' if the Group is an external-identity-mapped group. + # TODO (mbang): The full API doesn't seem to be implemented yet + # - !ruby/object:Api::Type::Array + # name: 'additionalGroupKeys' + # input: true + # description: | + # Additional entity key aliases for a Group. + # item_type: !ruby/object:Api::Type::NestedObject + # properties: + # - !ruby/object:Api::Type::String + # name: 'id' + # required: true + # description: | + # The ID of the entity. + + # For Google-managed entities, the id must be the email address of an existing + # group or user. + + # For external-identity-mapped entities, the id must be a string conforming + # to the Identity Source's requirements. + + # Must be unique within a namespace. + # - !ruby/object:Api::Type::String + # name: 'namespace' + # description: | + # The namespace in which the entity exists. + + # If not specified, the EntityKey represents a Google-managed entity + # such as a Google user or a Google Group. + + # If specified, the EntityKey represents an external-identity-mapped group. + # The namespace must correspond to an identity source created in Admin Console + # and must be in the form of `identitysources/{identity_source_id}. + # - !ruby/object:Api::Type::NestedObject + # name: 'dynamicGroupMetadata' + # input: true + # description: | + # Dynamic group metadata like queries and status. + # properties: + # - !ruby/object:Api::Type::Array + # name: 'queries' + # required: true + # description: | + # Memberships will be the union of all queries. Only one entry with USER resource is currently supported. + # item_type: !ruby/object:Api::Type::NestedObject + # properties: + # - !ruby/object:Api::Type::Enum + # name: 'resourceType' + # description: | + # Resources supported for dynamic groups. + # default_value: :USER + # values: + # - :USER + # - !ruby/object:Api::Type::String + # name: 'query' + # description: | + # Query that determines the memberships of the dynamic group. + + # Examples: All users with at least one organizations.department of engineering. + + # user.organizations.exists(org, org.department=='engineering') + + # All users with at least one location that has area of foo and building_id of bar. + + # user.locations.exists(loc, loc.area=='foo' && loc.building_id=='bar') + # - !ruby/object:Api::Type::NestedObject + # name: 'DynamicGroupStatus' + # output: true + # description: | + # Status of the dynamic group. + # properties: + # - !ruby/object:Api::Type::String + # name: 'status' + # description: | + # Status of the dynamic group. + # - !ruby/object:Api::Type::String + # name: 'statusTime' + # description: | + # The latest time at which the dynamic group is guaranteed to be in the given status. + # For example, if status is: UP_TO_DATE - The latest time at which this dynamic group + # was confirmed to be up to date. UPDATING_MEMBERSHIPS - The time at which dynamic group was created. + + # A timestamp in RFC3339 UTC "Zulu" format, accurate to nanoseconds. Example: "2014-10-02T15:01:23.045123456Z". + - !ruby/object:Api::Resource + name: 'GroupMembership' + base_url: '{{group}}/memberships' + self_link: '{{name}}' + description: | + A Membership defines a relationship between a Group and an entity belonging to that Group, referred to as a "member". + parameters: + - !ruby/object:Api::Type::ResourceRef + name: 'group' + resource: 'Group' + imports: 'name' + description: | + The name of the Group to create this membership in. + required: true + input: true + url_param_only: true + properties: + - !ruby/object:Api::Type::String + name: 'name' + output: true + description: | + The resource name of the Membership, of the form groups/{group_id}/memberships/{membership_id}. + - !ruby/object:Api::Type::NestedObject + name: 'memberKey' + input: true + description: | + EntityKey of the member. + exactly_one_of: + - member_key + - preferred_member_key + properties: + - !ruby/object:Api::Type::String + name: 'id' + required: true + input: true + description: | + The ID of the entity. + + For Google-managed entities, the id must be the email address of an existing + group or user. + + For external-identity-mapped entities, the id must be a string conforming + to the Identity Source's requirements. + + Must be unique within a namespace. + - !ruby/object:Api::Type::String + name: 'namespace' + input: true + description: | + The namespace in which the entity exists. + + If not specified, the EntityKey represents a Google-managed entity + such as a Google user or a Google Group. + + If specified, the EntityKey represents an external-identity-mapped group. + The namespace must correspond to an identity source created in Admin Console + and must be in the form of `identitysources/{identity_source_id}`. + - !ruby/object:Api::Type::NestedObject + name: 'preferredMemberKey' + input: true + description: | + EntityKey of the member. + exactly_one_of: + - member_key + - preferred_member_key + properties: + - !ruby/object:Api::Type::String + name: 'id' + required: true + input: true + description: | + The ID of the entity. + + For Google-managed entities, the id must be the email address of an existing + group or user. + + For external-identity-mapped entities, the id must be a string conforming + to the Identity Source's requirements. + + Must be unique within a namespace. + - !ruby/object:Api::Type::String + name: 'namespace' + input: true + description: | + The namespace in which the entity exists. + + If not specified, the EntityKey represents a Google-managed entity + such as a Google user or a Google Group. + + If specified, the EntityKey represents an external-identity-mapped group. + The namespace must correspond to an identity source created in Admin Console + and must be in the form of `identitysources/{identity_source_id}`. + - !ruby/object:Api::Type::String + name: 'createTime' + output: true + description: | + The time when the Membership was created. + - !ruby/object:Api::Type::String + name: 'updateTime' + output: true + description: | + The time when the Membership was last updated. + - !ruby/object:Api::Type::Array + name: 'roles' + required: true + description: | + The MembershipRoles that apply to the Membership. + Must not contain duplicate MembershipRoles with the same name. + item_type: !ruby/object:Api::Type::NestedObject + properties: + - !ruby/object:Api::Type::Enum + name: 'name' + required: true + description: | + The name of the MembershipRole. Must be one of OWNER, MANAGER, MEMBER. + values: + - :OWNER + - :MANAGER + - :MEMBER + - !ruby/object:Api::Type::String + name: 'type' + output: true + description: | + The type of the membership. diff --git a/products/cloudidentity/terraform.yaml b/products/cloudidentity/terraform.yaml new file mode 100644 index 000000000000..d6583f133597 --- /dev/null +++ b/products/cloudidentity/terraform.yaml @@ -0,0 +1,75 @@ +# Copyright 2020 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- !ruby/object:Provider::Terraform::Config +overrides: !ruby/object:Overrides::ResourceOverrides + Group: !ruby/object:Overrides::Terraform::ResourceOverride + import_format: ["{{name}}"] + examples: + - !ruby/object:Provider::Terraform::Examples + name: "cloud_identity_groups_basic" + primary_resource_id: "cloud_identity_group_basic" + min_version: beta + vars: + id_group: "my-identity-group" + test_env_vars: + org_domain: :ORG_DOMAIN + cust_id: :CUST_ID + ### The full API doesn't seem to be implemented yet + # - !ruby/object:Provider::Terraform::Examples + # name: "cloud_identity_groups_full" + # primary_resource_id: "cloud_identity_group_full" + # min_version: beta + # vars: + # id_group: "my-identity-group" + # test_env_vars: + # org_domain: :ORG_DOMAIN + # cust_id: :CUST_ID + custom_code: !ruby/object:Provider::Terraform::CustomCode + post_create: templates/terraform/post_create/set_computed_name.erb + GroupMembership: !ruby/object:Overrides::Terraform::ResourceOverride + import_format: ["{{name}}"] + examples: + - !ruby/object:Provider::Terraform::Examples + name: "cloud_identity_group_membership" + primary_resource_id: "cloud_identity_group_membership_basic" + min_version: beta + vars: + id_group: "my-identity-group" + test_env_vars: + org_domain: :ORG_DOMAIN + cust_id: :CUST_ID + - !ruby/object:Provider::Terraform::Examples + name: "cloud_identity_group_membership_user" + primary_resource_id: "cloud_identity_group_membership_basic" + min_version: beta + vars: + id_group: "my-identity-group" + test_env_vars: + org_domain: :ORG_DOMAIN + cust_id: :CUST_ID + identity_user: :IDENTITY_USER + properties: + memberKey: !ruby/object:Overrides::Terraform::PropertyOverride + default_from_api: true + preferredMemberKey: !ruby/object:Overrides::Terraform::PropertyOverride + default_from_api: true + custom_code: !ruby/object:Provider::Terraform::CustomCode + post_create: templates/terraform/post_create/set_computed_name.erb + +# This is for copying files over +files: !ruby/object:Provider::Config::Files + # These files have templating (ERB) code that will be run. + # This is usually to add licensing info, autogeneration notices, etc. + compile: +<%= lines(indent(compile('provider/terraform/product~compile.yaml'), 4)) -%> diff --git a/provider/terraform/examples.rb b/provider/terraform/examples.rb index e77a94d8d112..be8c6c379740 100644 --- a/provider/terraform/examples.rb +++ b/provider/terraform/examples.rb @@ -59,6 +59,8 @@ class Examples < Api::Object # - :ORG_TARGET # - :BILLING_ACCT # - :SERVICE_ACCT + # - :CUST_ID + # - :IDENTITY_USER # This list corresponds to the `get*FromEnv` methods in provider_test.go. attr_reader :test_env_vars @@ -138,7 +140,9 @@ def config_documentation(pwd) ORG_DOMAIN: 'example.com', ORG_TARGET: '123456789', BILLING_ACCT: '000000-0000000-0000000-000000', - SERVICE_ACCT: 'emailAddress:my@service-account.com' + SERVICE_ACCT: 'emailAddress:my@service-account.com', + CUST_ID: 'A01b123xz', + IDENTITY_USER: 'cloud_identity_user' } @vars ||= {} @test_env_vars ||= {} diff --git a/templates/terraform/env_var_context.go.erb b/templates/terraform/env_var_context.go.erb index d0809f7520be..3d9ac7b88a0d 100644 --- a/templates/terraform/env_var_context.go.erb +++ b/templates/terraform/env_var_context.go.erb @@ -17,5 +17,9 @@ "<%= var_name -%>": getTestProjectFromEnv(), <% elsif var_type == :FIRESTORE_PROJECT_NAME -%> "<%= var_name -%>": getTestFirestoreProjectFromEnv(t), + <% elsif var_type == :CUST_ID -%> + "<%= var_name -%>": getTestCustIdFromEnv(t), + <% elsif var_type == :IDENTITY_USER -%> + "<%= var_name -%>": getTestIdentityUserFromEnv(t), <% end -%> <% end -%> diff --git a/templates/terraform/examples/cloud_identity_group_membership.tf.erb b/templates/terraform/examples/cloud_identity_group_membership.tf.erb new file mode 100644 index 000000000000..d1edd447f956 --- /dev/null +++ b/templates/terraform/examples/cloud_identity_group_membership.tf.erb @@ -0,0 +1,42 @@ +resource "google_cloud_identity_group" "group" { + provider = google-beta + display_name = "<%= ctx[:vars]['id_group'] %>" + + parent = "customers/<%= ctx[:test_env_vars]['cust_id'] %>" + + group_key { + id = "<%= ctx[:vars]['id_group'] %>@<%= ctx[:test_env_vars]['org_domain'] %>" + } + + labels = { + "cloudidentity.googleapis.com/groups.discussion_forum" = "" + } +} + +resource "google_cloud_identity_group" "child-group" { + provider = google-beta + display_name = "<%= ctx[:vars]['id_group'] %>-child" + + parent = "customers/<%= ctx[:test_env_vars]['cust_id'] %>" + + group_key { + id = "<%= ctx[:vars]['id_group'] %>-child@<%= ctx[:test_env_vars]['org_domain'] %>" + } + + labels = { + "cloudidentity.googleapis.com/groups.discussion_forum" = "" + } +} + +resource "google_cloud_identity_group_membership" "<%= ctx[:primary_resource_id] %>" { + provider = google-beta + group = google_cloud_identity_group.group.id + + member_key { + id = google_cloud_identity_group.child-group.group_key[0].id + } + + roles { + name = "MEMBER" + } +} diff --git a/templates/terraform/examples/cloud_identity_group_membership_user.tf.erb b/templates/terraform/examples/cloud_identity_group_membership_user.tf.erb new file mode 100644 index 000000000000..d3a708ec7ea3 --- /dev/null +++ b/templates/terraform/examples/cloud_identity_group_membership_user.tf.erb @@ -0,0 +1,31 @@ +resource "google_cloud_identity_group" "group" { + provider = google-beta + display_name = "<%= ctx[:vars]['id_group'] %>" + + parent = "customers/<%= ctx[:test_env_vars]['cust_id'] %>" + + group_key { + id = "<%= ctx[:vars]['id_group'] %>@<%= ctx[:test_env_vars]['org_domain'] %>" + } + + labels = { + "cloudidentity.googleapis.com/groups.discussion_forum" = "" + } +} + +resource "google_cloud_identity_group_membership" "cloud_identity_group_membership_basic" { + provider = google-beta + group = google_cloud_identity_group.group.id + + member_key { + id = "<%= ctx[:test_env_vars]['identity_user'] %>@<%= ctx[:test_env_vars]['org_domain'] %>" + } + + roles { + name = "MEMBER" + } + + roles { + name = "MANAGER" + } +} \ No newline at end of file diff --git a/templates/terraform/examples/cloud_identity_groups_basic.tf.erb b/templates/terraform/examples/cloud_identity_groups_basic.tf.erb new file mode 100644 index 000000000000..6b5ea7405134 --- /dev/null +++ b/templates/terraform/examples/cloud_identity_groups_basic.tf.erb @@ -0,0 +1,14 @@ +resource "google_cloud_identity_group" "<%= ctx[:primary_resource_id] %>" { + provider = google-beta + display_name = "<%= ctx[:vars]['id_group'] %>" + + parent = "customers/<%= ctx[:test_env_vars]['cust_id'] %>" + + group_key { + id = "<%= ctx[:vars]['id_group'] %>@<%= ctx[:test_env_vars]['org_domain'] %>" + } + + labels = { + "cloudidentity.googleapis.com/groups.discussion_forum" = "" + } +} diff --git a/templates/terraform/examples/cloud_identity_groups_full.tf.erb b/templates/terraform/examples/cloud_identity_groups_full.tf.erb new file mode 100644 index 000000000000..2e01f47b5f97 --- /dev/null +++ b/templates/terraform/examples/cloud_identity_groups_full.tf.erb @@ -0,0 +1,26 @@ +resource "google_cloud_identity_group" "<%= ctx[:primary_resource_id] %>" { + provider = google-beta + display_name = "<%= ctx[:vars]['id_group'] %>" + description = "my new cloud identity group" + + parent = "customers/<%= ctx[:test_env_vars]['cust_id'] %>" + + group_key { + id = "<%= ctx[:vars]['id_group'] %>@<%= ctx[:test_env_vars]['org_domain'] %>" + } + + additional_group_keys { + id = "<%= ctx[:vars]['id_group'] %>-two@<%= ctx[:test_env_vars]['org_domain'] %>" + } + + labels = { + "cloudidentity.googleapis.com/groups.discussion_forum" = "" + } + + dynamic_group_metadata { + queries { + resource_type = "USER" + query = "organizations.department.exists(org, org.department=='engineering'" + } + } +} diff --git a/templates/terraform/post_create/set_computed_name.erb b/templates/terraform/post_create/set_computed_name.erb index 54638e146a87..4a3342ee8d8d 100644 --- a/templates/terraform/post_create/set_computed_name.erb +++ b/templates/terraform/post_create/set_computed_name.erb @@ -1,7 +1,15 @@ // `name` is autogenerated from the api so needs to be set post-create name, ok := res["name"] if !ok { - return fmt.Errorf("Create response didn't contain critical fields. Create may not have succeeded.") + respBody, ok := res["response"] + if !ok { + return fmt.Errorf("Create response didn't contain critical fields. Create may not have succeeded.") + } + + name, ok = respBody.(map[string]interface{})["name"] + if !ok { + return fmt.Errorf("Create response didn't contain critical fields. Create may not have succeeded.") + } } d.Set("name", name.(string)) d.SetId(name.(string)) diff --git a/third_party/terraform/tests/resource_cloud_identity_group_test.go.erb b/third_party/terraform/tests/resource_cloud_identity_group_test.go.erb new file mode 100644 index 000000000000..61622fa3f6b0 --- /dev/null +++ b/third_party/terraform/tests/resource_cloud_identity_group_test.go.erb @@ -0,0 +1,54 @@ +<% autogen_exception -%> +package google +<% unless version == 'ga' -%> + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccCloudIdentityGroup_update(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "org_domain": getTestOrgDomainFromEnv(t), + "cust_id": getTestCustIdFromEnv(t), + "random_suffix": randString(t, 10), + } + + vcrTest(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProvidersOiCS, + CheckDestroy: testAccCheckCloudIdentityGroupDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccCloudIdentityGroup_cloudIdentityGroupsBasicExample(context), + }, + { + Config: testAccCloudIdentityGroup_update(context), + }, + }, + }) +} + +func testAccCloudIdentityGroup_update(context map[string]interface{}) string { + return Nprintf(` +resource "google_cloud_identity_group" "cloud_identity_group_basic" { + provider = google-beta + display_name = "tf-test-my-identity-group%{random_suffix}-update" + description = "my-description" + + parent = "customers/%{cust_id}" + + group_key { + id = "tf-test-my-identity-group%{random_suffix}@%{org_domain}" + } + + labels = { + "cloudidentity.googleapis.com/groups.discussion_forum" = "" + } +} +`, context) +} +<% end -%> diff --git a/third_party/terraform/utils/config.go.erb b/third_party/terraform/utils/config.go.erb index 3932fb05ee9b..ba20475f780b 100644 --- a/third_party/terraform/utils/config.go.erb +++ b/third_party/terraform/utils/config.go.erb @@ -190,6 +190,7 @@ var <%= product[:definitions].name -%>DefaultBasePath = "<%= product[:definition var defaultClientScopes = []string{ "https://www.googleapis.com/auth/compute", "https://www.googleapis.com/auth/cloud-platform", + "https://www.googleapis.com/auth/cloud-identity", "https://www.googleapis.com/auth/ndev.clouddns.readwrite", "https://www.googleapis.com/auth/devstorage.full_control", "https://www.googleapis.com/auth/userinfo.email", diff --git a/third_party/terraform/utils/provider_test.go.erb b/third_party/terraform/utils/provider_test.go.erb index a9206ff9b528..d135be884108 100644 --- a/third_party/terraform/utils/provider_test.go.erb +++ b/third_party/terraform/utils/provider_test.go.erb @@ -63,6 +63,16 @@ var orgEnvVars = []string{ "GOOGLE_ORG", } +<% unless version == 'ga' -%> +var custIdEnvVars = []string{ + "GOOGLE_CUST_ID", +} + +var identityUserEnvVars = []string{ + "GOOGLE_IDENTITY_USER", +} +<% end -%> + var orgEnvDomainVars = []string{ "GOOGLE_ORG_DOMAIN", } @@ -845,6 +855,16 @@ func getTestZoneFromEnv() string { return multiEnvSearch(zoneEnvVars) } +<% unless version == 'ga' -%> +func getTestCustIdFromEnv(t *testing.T) string { + return multiEnvSearch(custIdEnvVars) +} + +func getTestIdentityUserFromEnv(t *testing.T) string { + return multiEnvSearch(identityUserEnvVars) +} +<% end -%> + // Firestore can't be enabled at the same time as Datastore, so we need a new // project to manage it until we can enable Firestore programmatically. func getTestFirestoreProjectFromEnv(t *testing.T) string {