From 008ccd82f97975d083b469de7e7b64e203901412 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Mon, 4 Dec 2023 08:16:48 -0600 Subject: [PATCH] Feat: add new OIDC credentials creation and assigment (AWS data source) (#759) * Feat: add new OIDC credentials creation and assigment (AWS data source) * fix integration test by adding oidc to policy * revert integration test --- client/api_client.go | 1 + client/api_client_mock.go | 15 +++ client/api_key.go | 14 ++ client/api_key_test.go | 32 +++++ env0/data_aws_oidc_credentials.go | 66 ++++++++++ env0/data_aws_oidc_credentials_test.go | 124 ++++++++++++++++++ env0/provider.go | 1 + .../env0_aws_oidc_credentials/data-source.tf | 16 +++ 8 files changed, 269 insertions(+) create mode 100644 env0/data_aws_oidc_credentials.go create mode 100644 env0/data_aws_oidc_credentials_test.go create mode 100644 examples/data-sources/env0_aws_oidc_credentials/data-source.tf diff --git a/client/api_client.go b/client/api_client.go index 78445ff3..e466c5b9 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -22,6 +22,7 @@ type ApiClientInterface interface { OrganizationId() (string, error) OrganizationPolicyUpdate(OrganizationPolicyUpdatePayload) (*Organization, error) OrganizationUserUpdateRole(userId string, roleId string) error + OidcSub() (string, error) Policy(projectId string) (Policy, error) PolicyUpdate(payload PolicyUpdatePayload) (Policy, error) Projects() ([]Project, error) diff --git a/client/api_client_mock.go b/client/api_client_mock.go index ba717cca..4f288c8c 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -1040,6 +1040,21 @@ func (mr *MockApiClientInterfaceMockRecorder) Notifications() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Notifications", reflect.TypeOf((*MockApiClientInterface)(nil).Notifications)) } +// OidcSub mocks base method. +func (m *MockApiClientInterface) OidcSub() (string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "OidcSub") + ret0, _ := ret[0].(string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// OidcSub indicates an expected call of OidcSub. +func (mr *MockApiClientInterfaceMockRecorder) OidcSub() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OidcSub", reflect.TypeOf((*MockApiClientInterface)(nil).OidcSub)) +} + // Organization mocks base method. func (m *MockApiClientInterface) Organization() (Organization, error) { m.ctrl.T.Helper() diff --git a/client/api_key.go b/client/api_key.go index 750b5602..45163043 100644 --- a/client/api_key.go +++ b/client/api_key.go @@ -63,3 +63,17 @@ func (client *ApiClient) ApiKeys() ([]ApiKey, error) { return result, err } + +func (client *ApiClient) OidcSub() (string, error) { + organizationId, err := client.OrganizationId() + if err != nil { + return "", err + } + + var result string + if err := client.http.Get("/api-keys/oidc-sub", map[string]string{"organizationId": organizationId}, &result); err != nil { + return "", err + } + + return result, nil +} diff --git a/client/api_key_test.go b/client/api_key_test.go index 5f930ea7..a3648160 100644 --- a/client/api_key_test.go +++ b/client/api_key_test.go @@ -94,4 +94,36 @@ var _ = Describe("ApiKey Client", func() { httpCall.Times(1) }) }) + + Describe("Get Oidc Sub", func() { + var returnedOidcSub string + var err error + mockedOidcSub := "oidc sub 1234" + + BeforeEach(func() { + mockOrganizationIdCall(organizationId) + httpCall = mockHttpClient.EXPECT(). + Get("/api-keys/oidc-sub", map[string]string{"organizationId": organizationId}, gomock.Any()). + Do(func(path string, request interface{}, response *string) { + *response = mockedOidcSub + }) + returnedOidcSub, err = apiClient.OidcSub() + }) + + It("Should get organization id", func() { + organizationIdCall.Times(1) + }) + + It("Should send GET request", func() { + httpCall.Times(1) + }) + + It("Should return Oidc sub", func() { + Expect(returnedOidcSub).To(Equal(mockedOidcSub)) + }) + + It("Should not return error", func() { + Expect(err).To(BeNil()) + }) + }) }) diff --git a/env0/data_aws_oidc_credentials.go b/env0/data_aws_oidc_credentials.go new file mode 100644 index 00000000..5eab8ab1 --- /dev/null +++ b/env0/data_aws_oidc_credentials.go @@ -0,0 +1,66 @@ +package env0 + +import ( + "context" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataAwsOidcCredentials() *schema.Resource { + return &schema.Resource{ + ReadContext: dataAwsOidcCredentialRead, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "the name of the aws oidc credentials", + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + "id": { + Type: schema.TypeString, + Description: "the id of the aws oidc credentials", + Optional: true, + ExactlyOneOf: []string{"name", "id"}, + }, + "oidc_sub": { + Type: schema.TypeString, + Computed: true, + Description: "the jwt oidc sub", + }, + }, + } +} + +func dataAwsOidcCredentialRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + var credentials client.Credentials + var err error + + id, ok := d.GetOk("id") + if ok { + credentials, err = getCredentialsById(id.(string), credentialsTypeToPrefixList[AWS_OIDC_TYPE], meta) + } else { + credentials, err = getCredentialsByName(d.Get("name").(string), credentialsTypeToPrefixList[AWS_OIDC_TYPE], meta) + } + + if err != nil { + return DataGetFailure("aws oidc credentials", id, err) + } + + if err := writeResourceData(&credentials, d); err != nil { + return diag.Errorf("schema resource data serialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + oidcSub, err := apiClient.OidcSub() + if err != nil { + return diag.Errorf("failed to get oidc sub: %v", err) + } + + d.Set("oidc_sub", oidcSub) + + return nil +} diff --git a/env0/data_aws_oidc_credentials_test.go b/env0/data_aws_oidc_credentials_test.go new file mode 100644 index 00000000..03222e0e --- /dev/null +++ b/env0/data_aws_oidc_credentials_test.go @@ -0,0 +1,124 @@ +package env0 + +import ( + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/env0/terraform-provider-env0/client/http" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAwsOidcCredentialDataSource(t *testing.T) { + credentials := client.Credentials{ + Id: "id0", + Name: "name0", + Type: string(client.AwsOidcCredentialsType), + } + + credentialsOther1 := client.Credentials{ + Id: "id1", + Name: "name1", + Type: string(client.AwsOidcCredentialsType), + } + + credentialsOther2 := client.Credentials{ + Id: "id2", + Name: "name2", + Type: string(client.AwsAssumedRoleCredentialsType), + } + + oidcSub := "oidc sub 123345 !!!" + + byName := map[string]interface{}{"name": credentials.Name} + byId := map[string]interface{}{"id": credentials.Id} + + resourceType := "env0_aws_oidc_credentials" + resourceName := "test_aws_oidc_credentials" + accessor := dataSourceAccessor(resourceType, resourceName) + + getValidTestCase := func(input map[string]interface{}) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", credentials.Id), + resource.TestCheckResourceAttr(accessor, "name", credentials.Name), + resource.TestCheckResourceAttr(accessor, "oidc_sub", oidcSub), + ), + }, + }, + } + } + + getErrorTestCase := func(input map[string]interface{}, expectedError string) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, input), + ExpectError: regexp.MustCompile(expectedError), + }, + }, + } + } + + mockGetCredentials := func(returnValue client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentials(credentials.Id).AnyTimes().Return(returnValue, nil) + mock.EXPECT().OidcSub().AnyTimes().Return(oidcSub, nil) + } + } + + mockListCredentials := func(returnValue []client.Credentials) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentialsList().AnyTimes().Return(returnValue, nil) + mock.EXPECT().OidcSub().AnyTimes().Return(oidcSub, nil) + } + } + + t.Run("by id", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(byId), + mockGetCredentials(credentials), + ) + }) + + t.Run("by name", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(byName), + mockListCredentials([]client.Credentials{credentials, credentialsOther1, credentialsOther2}), + ) + }) + + t.Run("throw error when no name or id is supplied", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(map[string]interface{}{}, "one of `id,name` must be specified"), + func(mock *client.MockApiClientInterface) {}, + ) + }) + + t.Run("throw error when by name and more than one is returned", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(byName, "found multiple credentials"), + mockListCredentials([]client.Credentials{credentials, credentialsOther1, credentialsOther2, credentials}), + ) + }) + + t.Run("Throw error when by name and not found", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(byName, "not found"), + mockListCredentials([]client.Credentials{credentialsOther1, credentialsOther2}), + ) + }) + + t.Run("Throw error when by id and not found", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase(byId, fmt.Sprintf("id %s not found", credentials.Id)), + func(mock *client.MockApiClientInterface) { + mock.EXPECT().CloudCredentials(credentials.Id).AnyTimes().Return(client.Credentials{}, http.NewMockFailedResponseError(404)) + }, + ) + }) +} diff --git a/env0/provider.go b/env0/provider.go index 1076a4dd..aecd0028 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -69,6 +69,7 @@ func Provider(version string) plugin.ProviderFunc { "env0_azure_cost_credentials": dataCredentials(AZURE_COST_TYPE), "env0_google_cost_credentials": dataCredentials(GCP_COST_TYPE), "env0_aws_credentials": dataCredentials(AWS_TYPE), + "env0_aws_oidc_credentials": dataAwsOidcCredentials(), "env0_gcp_credentials": dataCredentials(GCP_TYPE), "env0_azure_credentials": dataCredentials(AZURE_TYPE), "env0_team": dataTeam(), diff --git a/examples/data-sources/env0_aws_oidc_credentials/data-source.tf b/examples/data-sources/env0_aws_oidc_credentials/data-source.tf new file mode 100644 index 00000000..9359c096 --- /dev/null +++ b/examples/data-sources/env0_aws_oidc_credentials/data-source.tf @@ -0,0 +1,16 @@ +resource "env0_aws_oidc_credentials" "example" { + name = "name" + role_arn = "role_arn" +} + +data "env0_aws_oidc_credentials" "by_id" { + id = env0_aws_oidc_credentials.example.id +} + +data "env0_aws_oidc_credentials" "by_name" { + name = env0_aws_oidc_credentials.example.name +} + +output "oidc_sub" { + value = data.env0_aws_oidc_credentials.by_name.oidc_sub +}