From 5f5f60d45443b46db5833bdf563105a1611929c7 Mon Sep 17 00:00:00 2001 From: Dominik Giger Date: Fri, 26 Jul 2024 17:08:57 +0200 Subject: [PATCH 1/2] Implement organizations API. Adds the APIs for managing the organization: - Get/List/Update organizations - List/Create/Delete organization invitations - List/Delete organization members - Add/Remove organization member role assignments (API docs: https://www.elastic.co/guide/en/cloud/current/Organizations.html) --- .changelog/1.21.0/477.yml | 4 + pkg/api/organizationapi/get.go | 61 +++++++ pkg/api/organizationapi/get_test.go | 103 ++++++++++++ pkg/api/organizationapi/invitation_create.go | 78 +++++++++ .../organizationapi/invitation_create_test.go | 150 ++++++++++++++++++ pkg/api/organizationapi/invitation_delete.go | 70 ++++++++ .../organizationapi/invitation_delete_test.go | 80 ++++++++++ pkg/api/organizationapi/invitation_list.go | 61 +++++++ .../organizationapi/invitation_list_test.go | 146 +++++++++++++++++ pkg/api/organizationapi/list.go | 54 +++++++ pkg/api/organizationapi/list_test.go | 108 +++++++++++++ pkg/api/organizationapi/member_delete.go | 69 ++++++++ pkg/api/organizationapi/member_delete_test.go | 80 ++++++++++ pkg/api/organizationapi/member_list.go | 61 +++++++ pkg/api/organizationapi/member_list_test.go | 122 ++++++++++++++ .../member_role_assignments_add.go | 63 ++++++++ .../member_role_assignments_add_test.go | 100 ++++++++++++ .../member_role_assignments_remove.go | 63 ++++++++ .../member_role_assignments_remove_test.go | 100 ++++++++++++ pkg/api/organizationapi/update.go | 73 +++++++++ pkg/api/organizationapi/update_test.go | 128 +++++++++++++++ 21 files changed, 1774 insertions(+) create mode 100644 .changelog/1.21.0/477.yml create mode 100644 pkg/api/organizationapi/get.go create mode 100644 pkg/api/organizationapi/get_test.go create mode 100644 pkg/api/organizationapi/invitation_create.go create mode 100644 pkg/api/organizationapi/invitation_create_test.go create mode 100644 pkg/api/organizationapi/invitation_delete.go create mode 100644 pkg/api/organizationapi/invitation_delete_test.go create mode 100644 pkg/api/organizationapi/invitation_list.go create mode 100644 pkg/api/organizationapi/invitation_list_test.go create mode 100644 pkg/api/organizationapi/list.go create mode 100644 pkg/api/organizationapi/list_test.go create mode 100644 pkg/api/organizationapi/member_delete.go create mode 100644 pkg/api/organizationapi/member_delete_test.go create mode 100644 pkg/api/organizationapi/member_list.go create mode 100644 pkg/api/organizationapi/member_list_test.go create mode 100644 pkg/api/organizationapi/member_role_assignments_add.go create mode 100644 pkg/api/organizationapi/member_role_assignments_add_test.go create mode 100644 pkg/api/organizationapi/member_role_assignments_remove.go create mode 100644 pkg/api/organizationapi/member_role_assignments_remove_test.go create mode 100644 pkg/api/organizationapi/update.go create mode 100644 pkg/api/organizationapi/update_test.go diff --git a/.changelog/1.21.0/477.yml b/.changelog/1.21.0/477.yml new file mode 100644 index 00000000..b5040c81 --- /dev/null +++ b/.changelog/1.21.0/477.yml @@ -0,0 +1,4 @@ +category: enhancement +title: Add the ESS-only organizations API. +description: | + Adds support for the organizations API (https://www.elastic.co/guide/en/cloud/current/Organizations.html). This API is currently unavailable in self-hosted ECE and can only be used against the Elastic cloud service. diff --git a/pkg/api/organizationapi/get.go b/pkg/api/organizationapi/get.go new file mode 100644 index 00000000..f99df435 --- /dev/null +++ b/pkg/api/organizationapi/get.go @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type GetParams struct { + *api.API + + OrganizationID string +} + +func (params GetParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.OrganizationID == "" { + merr = merr.Append(errors.New("OrganizationID is not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func Get(params GetParams) (*models.Organization, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.Organizations.GetOrganization( + organizations.NewGetOrganizationParams(). + WithOrganizationID(params.OrganizationID), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload, nil +} diff --git a/pkg/api/organizationapi/get_test.go b/pkg/api/organizationapi/get_test.go new file mode 100644 index 00000000..0e27f117 --- /dev/null +++ b/pkg/api/organizationapi/get_test.go @@ -0,0 +1,103 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestGetOrganization(t *testing.T) { + tests := []struct { + name string + params GetParams + want *models.Organization + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 2 errors occurred:\n\t* OrganizationID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: GetParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "GET", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations/testorg", + }, + mock.NewStringBody(` +{ + "billing_contacts" : [ + "contact-a", "contact-b" + ], + "default_disk_usage_alerts_enabled" : true, + "id" : "testorg", + "name" : "testorganization", + "notifications_allowed_email_domains" : [ + "mail@test" + ], + "operational_contacts" : [ + "op@test" + ] +}`)), + ), + OrganizationID: "testorg", + }, + want: &models.Organization{ + ID: ec.String("testorg"), + Name: ec.String("testorganization"), + BillingContacts: []string{"contact-a", "contact-b"}, + DefaultDiskUsageAlertsEnabled: ec.Bool(true), + NotificationsAllowedEmailDomains: []string{"mail@test"}, + OperationalContacts: []string{"op@test"}, + }, + }, + { + name: "handles failure response", + params: GetParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "organization.not_found", + Message: "organization not found", + }), + ), + OrganizationID: "testorg", + }, + err: "api error: 1 error occurred:\n\t* organization.not_found: organization not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := Get(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if !assert.Equal(t, test.want, got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/invitation_create.go b/pkg/api/organizationapi/invitation_create.go new file mode 100644 index 00000000..73d7a24e --- /dev/null +++ b/pkg/api/organizationapi/invitation_create.go @@ -0,0 +1,78 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type CreateInvitationParams struct { + *api.API + + OrganizationID string + + // The email addresses to invite to the organization + Emails []string + + // The expiration time for the invitation, for example 24h, 7d. Defaults to 72h. + ExpiresIn string + + // Roles to assign to the newly invited user + RoleAssignments *models.RoleAssignments +} + +func (params CreateInvitationParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.OrganizationID == "" { + merr = merr.Append(errors.New("OrganizationID is not specified and is required for this operation")) + } + if len(params.Emails) == 0 { + merr = merr.Append(errors.New("Emails must specify at least one email address")) + } + return merr.ErrorOrNil() +} + +func CreateInvitation(params CreateInvitationParams) (*models.OrganizationInvitations, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.Organizations.CreateOrganizationInvitations( + organizations.NewCreateOrganizationInvitationsParams(). + WithOrganizationID(params.OrganizationID). + WithBody(&models.OrganizationInvitationRequest{ + Emails: params.Emails, + ExpiresIn: params.ExpiresIn, + RoleAssignments: params.RoleAssignments, + }), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload, nil +} diff --git a/pkg/api/organizationapi/invitation_create_test.go b/pkg/api/organizationapi/invitation_create_test.go new file mode 100644 index 00000000..0ca101a3 --- /dev/null +++ b/pkg/api/organizationapi/invitation_create_test.go @@ -0,0 +1,150 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/go-openapi/strfmt" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestCreateInvitation(t *testing.T) { + dateTime := strfmt.DateTime(time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC)) + tests := []struct { + name string + params CreateInvitationParams + want *models.OrganizationInvitations + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 3 errors occurred:\n\t* Emails must specify at least one email address\n\t* OrganizationID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: CreateInvitationParams{ + API: api.NewMock( + mock.New201ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultWriteMockHeaders, + Method: "POST", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations/testorg/invitations", + Body: mock.NewStringBody("{\"emails\":[\"test@mail\"],\"expires_in\":\"3d\"}\n"), + }, + mock.NewStringBody(` +{ + "invitations" : [ + { + "accepted_at" : "2019-01-01T00:00:00Z", + "created_at" : "2019-01-01T00:00:00Z", + "email" : "test@mail", + "expired" : false, + "expires_at" : "2019-01-01T00:00:00Z", + "organization" : { + "billing_contacts" : [ + "billing@mail" + ], + "default_disk_usage_alerts_enabled" : true, + "id" : "testorg", + "name" : "testorganization", + "notifications_allowed_email_domains" : [ + "allowed@mail" + ], + "operational_contacts" : [ + "op@mail" + ] + }, + "role_assignments": { + "organization": [ + { + "organization_id": "testorg", + "role_id": "billing-admin" + } + ] + }, + "token" : "token" + } + ] +}`)), + ), + OrganizationID: "testorg", + Emails: []string{"test@mail"}, + ExpiresIn: "3d", + }, + want: &models.OrganizationInvitations{ + Invitations: []*models.OrganizationInvitation{ + { + Email: ec.String("test@mail"), + CreatedAt: &dateTime, + AcceptedAt: dateTime, + ExpiresAt: &dateTime, + Expired: ec.Bool(false), + Organization: &models.Organization{ + ID: ec.String("testorg"), + Name: ec.String("testorganization"), + BillingContacts: []string{"billing@mail"}, + DefaultDiskUsageAlertsEnabled: ec.Bool(true), + NotificationsAllowedEmailDomains: []string{"allowed@mail"}, + OperationalContacts: []string{"op@mail"}, + }, + RoleAssignments: &models.RoleAssignments{ + Organization: []*models.OrganizationRoleAssignment{ + { + OrganizationID: ec.String("testorg"), + RoleID: ec.String("billing-admin"), + }, + }, + }, + Token: ec.String("token"), + }, + }, + }, + }, + { + name: "handles failure response", + params: CreateInvitationParams{ + API: api.NewMock( + mock.NewErrorResponse(401, mock.APIError{ + Code: "user.not_found", + Message: "user not found", + }), + ), + OrganizationID: "testorg", + Emails: []string{"test@mail"}, + }, + err: "api error: 1 error occurred:\n\t* user.not_found: user not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := CreateInvitation(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if !assert.Equal(t, test.want, got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/invitation_delete.go b/pkg/api/organizationapi/invitation_delete.go new file mode 100644 index 00000000..00638315 --- /dev/null +++ b/pkg/api/organizationapi/invitation_delete.go @@ -0,0 +1,70 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" + "strings" +) + +type DeleteInvitationParams struct { + *api.API + + OrganizationID string + + InvitationTokens []string +} + +func (params DeleteInvitationParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.OrganizationID == "" { + merr = merr.Append(errors.New("OrganizationID is not specified and is required for this operation")) + } + if len(params.InvitationTokens) == 0 { + merr = merr.Append(errors.New("InvitationTokens are not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func DeleteInvitation(params DeleteInvitationParams) (models.EmptyResponse, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + invitationTokens := strings.Join(params.InvitationTokens, ",") + + response, err := params.V1API.Organizations.DeleteOrganizationInvitations( + organizations.NewDeleteOrganizationInvitationsParams(). + WithOrganizationID(params.OrganizationID). + WithInvitationTokens(invitationTokens), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload, nil +} diff --git a/pkg/api/organizationapi/invitation_delete_test.go b/pkg/api/organizationapi/invitation_delete_test.go new file mode 100644 index 00000000..a9502962 --- /dev/null +++ b/pkg/api/organizationapi/invitation_delete_test.go @@ -0,0 +1,80 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDeleteInvitation(t *testing.T) { + tests := []struct { + name string + params DeleteInvitationParams + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 3 errors occurred:\n\t* InvitationTokens are not specified and is required for this operation\n\t* OrganizationID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: DeleteInvitationParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultWriteMockHeaders, + Method: "DELETE", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations/testorg/invitations/invtoken", + }, + mock.NewStringBody("{}")), + ), + OrganizationID: "testorg", + InvitationTokens: []string{"invtoken"}, + }, + }, + { + name: "handles failure response", + params: DeleteInvitationParams{ + API: api.NewMock( + mock.NewErrorResponse(400, mock.APIError{ + Code: "root.invalid_data", + Message: "No valid invitation token was supplied", + }), + ), + OrganizationID: "testorg", + InvitationTokens: []string{"invtooken"}, + }, + err: "api error: 1 error occurred:\n\t* root.invalid_data: No valid invitation token was supplied\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := DeleteInvitation(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if err == nil && !assert.NotNil(t, got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/invitation_list.go b/pkg/api/organizationapi/invitation_list.go new file mode 100644 index 00000000..c9c1d886 --- /dev/null +++ b/pkg/api/organizationapi/invitation_list.go @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type ListInvitationsParams struct { + *api.API + + OrganizationID string +} + +func (params ListInvitationsParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.OrganizationID == "" { + merr = merr.Append(errors.New("OrganizationID is not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func ListInvitations(params ListInvitationsParams) (*models.OrganizationInvitations, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.Organizations.ListOrganizationInvitations( + organizations.NewListOrganizationInvitationsParams(). + WithOrganizationID(params.OrganizationID), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload, nil +} diff --git a/pkg/api/organizationapi/invitation_list_test.go b/pkg/api/organizationapi/invitation_list_test.go new file mode 100644 index 00000000..eded9588 --- /dev/null +++ b/pkg/api/organizationapi/invitation_list_test.go @@ -0,0 +1,146 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/go-openapi/strfmt" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestListInvitations(t *testing.T) { + dateTime := strfmt.DateTime(time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC)) + tests := []struct { + name string + params ListInvitationsParams + want *models.OrganizationInvitations + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 2 errors occurred:\n\t* OrganizationID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: ListInvitationsParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "GET", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations/testorg/invitations", + }, + mock.NewStringBody(` +{ + "invitations" : [ + { + "accepted_at" : "2019-01-01T00:00:00Z", + "created_at" : "2019-01-01T00:00:00Z", + "email" : "test@mail", + "expired" : false, + "expires_at" : "2019-01-01T00:00:00Z", + "organization" : { + "billing_contacts" : [ + "billing@mail" + ], + "default_disk_usage_alerts_enabled" : true, + "id" : "testorg", + "name" : "testorganization", + "notifications_allowed_email_domains" : [ + "allowed@mail" + ], + "operational_contacts" : [ + "op@mail" + ] + }, + "role_assignments": { + "organization": [ + { + "organization_id": "testorg", + "role_id": "billing-admin" + } + ] + }, + "token" : "token" + } + ] +}`)), + ), + OrganizationID: "testorg", + }, + want: &models.OrganizationInvitations{ + Invitations: []*models.OrganizationInvitation{ + { + Email: ec.String("test@mail"), + CreatedAt: &dateTime, + AcceptedAt: dateTime, + ExpiresAt: &dateTime, + Expired: ec.Bool(false), + Organization: &models.Organization{ + ID: ec.String("testorg"), + Name: ec.String("testorganization"), + BillingContacts: []string{"billing@mail"}, + DefaultDiskUsageAlertsEnabled: ec.Bool(true), + NotificationsAllowedEmailDomains: []string{"allowed@mail"}, + OperationalContacts: []string{"op@mail"}, + }, + RoleAssignments: &models.RoleAssignments{ + Organization: []*models.OrganizationRoleAssignment{ + { + OrganizationID: ec.String("testorg"), + RoleID: ec.String("billing-admin"), + }, + }, + }, + Token: ec.String("token"), + }, + }, + }, + }, + { + name: "handles failure response", + params: ListInvitationsParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "organization.not_found", + Message: "organization not found", + }), + ), + OrganizationID: "testorg", + }, + err: "api error: 1 error occurred:\n\t* organization.not_found: organization not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := ListInvitations(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if !assert.Equal(t, test.want, got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/list.go b/pkg/api/organizationapi/list.go new file mode 100644 index 00000000..3e2be596 --- /dev/null +++ b/pkg/api/organizationapi/list.go @@ -0,0 +1,54 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type ListParams struct { + *api.API +} + +func (params ListParams) Validate() error { + var err = multierror.NewPrefixed("invalid user params") + if params.API == nil { + err = err.Append(apierror.ErrMissingAPI) + } + return err.ErrorOrNil() +} + +func List(params ListParams) ([]*models.Organization, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.Organizations.ListOrganizations( + organizations.NewListOrganizationsParams(), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload.Organizations, nil +} diff --git a/pkg/api/organizationapi/list_test.go b/pkg/api/organizationapi/list_test.go new file mode 100644 index 00000000..932087da --- /dev/null +++ b/pkg/api/organizationapi/list_test.go @@ -0,0 +1,108 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestListOrganization(t *testing.T) { + tests := []struct { + name string + params ListParams + want []*models.Organization + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 1 error occurred:\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: ListParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "GET", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations", + }, + mock.NewStringBody(` +{ + "next_page" : "string", + "organizations" : [ + { + "billing_contacts" : [ + "contact-a", "contact-b" + ], + "default_disk_usage_alerts_enabled" : true, + "id" : "testorg", + "name" : "testorganization", + "notifications_allowed_email_domains" : [ + "mail@test" + ], + "operational_contacts" : [ + "op@test" + ] + } + ] +}`)), + ), + }, + want: []*models.Organization{ + { + ID: ec.String("testorg"), + Name: ec.String("testorganization"), + BillingContacts: []string{"contact-a", "contact-b"}, + DefaultDiskUsageAlertsEnabled: ec.Bool(true), + NotificationsAllowedEmailDomains: []string{"mail@test"}, + OperationalContacts: []string{"op@test"}, + }, + }, + }, + { + name: "handles failure response", + params: ListParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "organization.not_found", + Message: "organization not found", + }), + ), + }, + err: "api error: 1 error occurred:\n\t* organization.not_found: organization not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := List(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if !assert.Equal(t, test.want, got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/member_delete.go b/pkg/api/organizationapi/member_delete.go new file mode 100644 index 00000000..f6b8d941 --- /dev/null +++ b/pkg/api/organizationapi/member_delete.go @@ -0,0 +1,69 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" + "strings" +) + +type DeleteMemberParams struct { + *api.API + + OrganizationID string + + UserIDs []string +} + +func (params DeleteMemberParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.OrganizationID == "" { + merr = merr.Append(errors.New("OrganizationID is not specified and is required for this operation")) + } + if len(params.UserIDs) == 0 { + merr = merr.Append(errors.New("UserIDs is not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func DeleteMember(params DeleteMemberParams) (models.EmptyResponse, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + userIds := strings.Join(params.UserIDs, ",") + response, err := params.V1API.Organizations.DeleteOrganizationMemberships( + organizations.NewDeleteOrganizationMembershipsParams(). + WithOrganizationID(params.OrganizationID). + WithUserIds(userIds), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload, nil +} diff --git a/pkg/api/organizationapi/member_delete_test.go b/pkg/api/organizationapi/member_delete_test.go new file mode 100644 index 00000000..b9a66dae --- /dev/null +++ b/pkg/api/organizationapi/member_delete_test.go @@ -0,0 +1,80 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestDeleteMember(t *testing.T) { + tests := []struct { + name string + params DeleteMemberParams + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 3 errors occurred:\n\t* OrganizationID is not specified and is required for this operation\n\t* UserIDs is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: DeleteMemberParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultWriteMockHeaders, + Method: "DELETE", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations/testorg/members/orgmember1,orgmember2", + }, + mock.NewStringBody("{}")), + ), + OrganizationID: "testorg", + UserIDs: []string{"orgmember1", "orgmember2"}, + }, + }, + { + name: "handles failure response", + params: DeleteMemberParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "organization.membership_not_found", + Message: "Organization membership not found", + }), + ), + OrganizationID: "testorg", + UserIDs: []string{"orgmember"}, + }, + err: "api error: 1 error occurred:\n\t* organization.membership_not_found: Organization membership not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := DeleteMember(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if err == nil && !assert.NotNil(t, got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/member_list.go b/pkg/api/organizationapi/member_list.go new file mode 100644 index 00000000..b9df5e08 --- /dev/null +++ b/pkg/api/organizationapi/member_list.go @@ -0,0 +1,61 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type ListMembersParams struct { + *api.API + + OrganizationID string +} + +func (params ListMembersParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.OrganizationID == "" { + merr = merr.Append(errors.New("OrganizationID is not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func ListMembers(params ListMembersParams) (*models.OrganizationMemberships, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.Organizations.ListOrganizationMembers( + organizations.NewListOrganizationMembersParams(). + WithOrganizationID(params.OrganizationID), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload, nil +} diff --git a/pkg/api/organizationapi/member_list_test.go b/pkg/api/organizationapi/member_list_test.go new file mode 100644 index 00000000..6e857b15 --- /dev/null +++ b/pkg/api/organizationapi/member_list_test.go @@ -0,0 +1,122 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/go-openapi/strfmt" + "github.com/stretchr/testify/assert" + "testing" + "time" +) + +func TestListMembers(t *testing.T) { + dateTime := strfmt.DateTime(time.Date(2019, time.January, 1, 0, 0, 0, 0, time.UTC)) + tests := []struct { + name string + params ListMembersParams + want *models.OrganizationMemberships + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 2 errors occurred:\n\t* OrganizationID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: ListMembersParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultReadMockHeaders, + Method: "GET", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations/testorg/members", + }, + mock.NewStringBody(` +{ + "members" : [ + { + "email" : "user@mail", + "member_since" : "2019-01-01T00:00:00Z", + "name" : "user", + "organization_id" : "testorg", + "user_id" : "userid", + "role_assignments": { + "organization": [ + { + "organization_id": "testorg", + "role_id": "billing-admin" + } + ] + } + } + ] +}`)), + ), + OrganizationID: "testorg", + }, + want: &models.OrganizationMemberships{ + Members: []*models.OrganizationMembership{ + { + Email: "user@mail", + MemberSince: &dateTime, + Name: "user", + OrganizationID: ec.String("testorg"), + UserID: ec.String("userid"), + RoleAssignments: &models.RoleAssignments{ + Organization: []*models.OrganizationRoleAssignment{ + { + OrganizationID: ec.String("testorg"), + RoleID: ec.String("billing-admin"), + }, + }, + }, + }, + }, + }, + }, + { + name: "handles failure response", + params: ListMembersParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "organization.not_found", + Message: "organization not found", + }), + ), + OrganizationID: "testorg", + }, + err: "api error: 1 error occurred:\n\t* organization.not_found: organization not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := ListMembers(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if !assert.Equal(t, test.want, got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/member_role_assignments_add.go b/pkg/api/organizationapi/member_role_assignments_add.go new file mode 100644 index 00000000..5d5780a2 --- /dev/null +++ b/pkg/api/organizationapi/member_role_assignments_add.go @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/user_role_assignments" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type AddRoleAssignmentsParams struct { + *api.API + + UserID string + RoleAssignments models.RoleAssignments +} + +func (params AddRoleAssignmentsParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.UserID == "" { + merr = merr.Append(errors.New("UserID is not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func AddRoleAssignments(params AddRoleAssignmentsParams) (*models.EmptyResponse, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.UserRoleAssignments.AddRoleAssignments( + user_role_assignments.NewAddRoleAssignmentsParams(). + WithUserID(params.UserID). + WithBody(¶ms.RoleAssignments), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return &response.Payload, nil +} diff --git a/pkg/api/organizationapi/member_role_assignments_add_test.go b/pkg/api/organizationapi/member_role_assignments_add_test.go new file mode 100644 index 00000000..83fadecf --- /dev/null +++ b/pkg/api/organizationapi/member_role_assignments_add_test.go @@ -0,0 +1,100 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestAddRoleAssignments(t *testing.T) { + tests := []struct { + name string + params AddRoleAssignmentsParams + want models.EmptyResponse + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 2 errors occurred:\n\t* UserID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: AddRoleAssignmentsParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultWriteMockHeaders, + Method: "POST", + Host: api.DefaultMockHost, + Path: "/api/v1/users/testuser/role_assignments", + Body: mock.NewStringBody(`{"deployment":null,"organization":[{"organization_id":"testorg","role_id":"billing-admin"}],"platform":null}` + "\n"), + }, + mock.NewStringBody("{}"), + ), + ), + UserID: "testuser", + RoleAssignments: models.RoleAssignments{ + Organization: []*models.OrganizationRoleAssignment{ + { + OrganizationID: ec.String("testorg"), + RoleID: ec.String("billing-admin"), + }, + }, + }, + }, + want: map[string]interface{}{}, + }, + { + name: "handles failure response", + params: AddRoleAssignmentsParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "user.not_found", + Message: "user not found", + }), + ), + UserID: "testuser", + RoleAssignments: models.RoleAssignments{ + Organization: []*models.OrganizationRoleAssignment{ + { + OrganizationID: ec.String("testorg"), + RoleID: ec.String("billing-admin"), + }, + }, + }, + }, + err: "api error: 1 error occurred:\n\t* user.not_found: user not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := AddRoleAssignments(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if got != nil && !assert.Equal(t, test.want, *got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/member_role_assignments_remove.go b/pkg/api/organizationapi/member_role_assignments_remove.go new file mode 100644 index 00000000..bff83689 --- /dev/null +++ b/pkg/api/organizationapi/member_role_assignments_remove.go @@ -0,0 +1,63 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/user_role_assignments" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type RemoveRoleAssignmentsParams struct { + *api.API + + UserID string + RoleAssignments models.RoleAssignments +} + +func (params RemoveRoleAssignmentsParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.UserID == "" { + merr = merr.Append(errors.New("UserID is not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func RemoveRoleAssignments(params RemoveRoleAssignmentsParams) (*models.EmptyResponse, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.UserRoleAssignments.RemoveRoleAssignments( + user_role_assignments.NewRemoveRoleAssignmentsParams(). + WithUserID(params.UserID). + WithBody(¶ms.RoleAssignments), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return &response.Payload, nil +} diff --git a/pkg/api/organizationapi/member_role_assignments_remove_test.go b/pkg/api/organizationapi/member_role_assignments_remove_test.go new file mode 100644 index 00000000..a643e88d --- /dev/null +++ b/pkg/api/organizationapi/member_role_assignments_remove_test.go @@ -0,0 +1,100 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRemoveRoleAssignments(t *testing.T) { + tests := []struct { + name string + params RemoveRoleAssignmentsParams + want models.EmptyResponse + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 2 errors occurred:\n\t* UserID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: RemoveRoleAssignmentsParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultWriteMockHeaders, + Method: "DELETE", + Host: api.DefaultMockHost, + Path: "/api/v1/users/testuser/role_assignments", + Body: mock.NewStringBody(`{"deployment":null,"organization":[{"organization_id":"testorg","role_id":"billing-admin"}],"platform":null}` + "\n"), + }, + mock.NewStringBody("{}"), + ), + ), + UserID: "testuser", + RoleAssignments: models.RoleAssignments{ + Organization: []*models.OrganizationRoleAssignment{ + { + OrganizationID: ec.String("testorg"), + RoleID: ec.String("billing-admin"), + }, + }, + }, + }, + want: map[string]interface{}{}, + }, + { + name: "handles failure response", + params: RemoveRoleAssignmentsParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "user.not_found", + Message: "user not found", + }), + ), + UserID: "testuser", + RoleAssignments: models.RoleAssignments{ + Organization: []*models.OrganizationRoleAssignment{ + { + OrganizationID: ec.String("testorg"), + RoleID: ec.String("billing-admin"), + }, + }, + }, + }, + err: "api error: 1 error occurred:\n\t* user.not_found: user not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := RemoveRoleAssignments(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if got != nil && !assert.Equal(t, test.want, *got) { + t.Error(err) + } + }) + } +} diff --git a/pkg/api/organizationapi/update.go b/pkg/api/organizationapi/update.go new file mode 100644 index 00000000..3e451638 --- /dev/null +++ b/pkg/api/organizationapi/update.go @@ -0,0 +1,73 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "errors" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/apierror" + "github.com/elastic/cloud-sdk-go/pkg/client/organizations" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/multierror" +) + +type UpdateParams struct { + *api.API + + OrganizationID string + Name string + DefaultDiskUsageAlertsEnabled *bool + BillingContacts []string + AllowedEmailDomains []string + OperationalContacts []string +} + +func (params UpdateParams) Validate() error { + var merr = multierror.NewPrefixed("invalid user params") + if params.API == nil { + merr = merr.Append(apierror.ErrMissingAPI) + } + if params.OrganizationID == "" { + merr = merr.Append(errors.New("OrganizationID is not specified and is required for this operation")) + } + return merr.ErrorOrNil() +} + +func Update(params UpdateParams) (*models.Organization, error) { + if err := params.Validate(); err != nil { + return nil, err + } + + response, err := params.V1API.Organizations.UpdateOrganization( + organizations.NewUpdateOrganizationParams(). + WithOrganizationID(params.OrganizationID). + WithBody(&models.OrganizationRequest{ + Name: params.Name, + DefaultDiskUsageAlertsEnabled: params.DefaultDiskUsageAlertsEnabled, + BillingContacts: params.BillingContacts, + NotificationsAllowedEmailDomains: params.AllowedEmailDomains, + OperationalContacts: params.OperationalContacts, + }), + params.AuthWriter, + ) + if err != nil { + return nil, apierror.Wrap(err) + } + + return response.Payload, nil +} diff --git a/pkg/api/organizationapi/update_test.go b/pkg/api/organizationapi/update_test.go new file mode 100644 index 00000000..486e2777 --- /dev/null +++ b/pkg/api/organizationapi/update_test.go @@ -0,0 +1,128 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you 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. + +package organizationapi + +import ( + "bytes" + "encoding/json" + "github.com/elastic/cloud-sdk-go/pkg/api" + "github.com/elastic/cloud-sdk-go/pkg/api/mock" + "github.com/elastic/cloud-sdk-go/pkg/models" + "github.com/elastic/cloud-sdk-go/pkg/util/ec" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestUpdateOrganization(t *testing.T) { + responseBody := ` +{ + "billing_contacts" : [ + "contact-a", "contact-b" + ], + "default_disk_usage_alerts_enabled" : true, + "id" : "testorg", + "name" : "testorganization", + "notifications_allowed_email_domains" : [ + "mail@test" + ], + "operational_contacts" : [ + "op@test" + ] +}` + requestBody := ` +{ + "billing_contacts" : [ + "contact-a", "contact-b" + ], + "default_disk_usage_alerts_enabled" : true, + "name" : "testorganization", + "notifications_allowed_email_domains" : [ + "mail@test" + ], + "operational_contacts" : [ + "op@test" + ] +}` + requestBodyCompact := &bytes.Buffer{} + json.Compact(requestBodyCompact, []byte(requestBody)) + tests := []struct { + name string + params UpdateParams + want *models.Organization + err string + }{ + { + name: "fails due to parameter validation", + err: "invalid user params: 2 errors occurred:\n\t* OrganizationID is not specified and is required for this operation\n\t* api reference is required for the operation\n\n", + }, + { + name: "handles successful response", + params: UpdateParams{ + API: api.NewMock( + mock.New200ResponseAssertion( + &mock.RequestAssertion{ + Header: api.DefaultWriteMockHeaders, + Method: "PUT", + Host: api.DefaultMockHost, + Path: "/api/v1/organizations/testorg", + Body: mock.NewStringBody(requestBodyCompact.String() + "\n"), + }, + mock.NewStringBody(responseBody)), + ), + OrganizationID: "testorg", + Name: "testorganization", + DefaultDiskUsageAlertsEnabled: ec.Bool(true), + BillingContacts: []string{"contact-a", "contact-b"}, + AllowedEmailDomains: []string{"mail@test"}, + OperationalContacts: []string{"op@test"}, + }, + want: &models.Organization{ + ID: ec.String("testorg"), + Name: ec.String("testorganization"), + BillingContacts: []string{"contact-a", "contact-b"}, + DefaultDiskUsageAlertsEnabled: ec.Bool(true), + NotificationsAllowedEmailDomains: []string{"mail@test"}, + OperationalContacts: []string{"op@test"}, + }, + }, + { + name: "handles failure response", + params: UpdateParams{ + API: api.NewMock( + mock.NewErrorResponse(404, mock.APIError{ + Code: "organization.not_found", + Message: "organization not found", + }), + ), + OrganizationID: "testorg", + }, + err: "api error: 1 error occurred:\n\t* organization.not_found: organization not found\n\n", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + got, err := Update(test.params) + if err != nil && !assert.EqualError(t, err, test.err) { + t.Error(err) + } + if !assert.Equal(t, test.want, got) { + t.Error(err) + } + }) + } +} From 4ceec3a84f6e85b13fed1a48339376cfb18b412c Mon Sep 17 00:00:00 2001 From: Dominik Giger Date: Tue, 10 Sep 2024 10:47:48 +0200 Subject: [PATCH 2/2] Refactor test checks. Clearly check either the error case or the successful result case. --- pkg/api/organizationapi/get_test.go | 11 ++++++----- pkg/api/organizationapi/invitation_create_test.go | 11 ++++++----- pkg/api/organizationapi/invitation_delete_test.go | 11 ++++++----- pkg/api/organizationapi/invitation_list_test.go | 11 ++++++----- pkg/api/organizationapi/list_test.go | 11 ++++++----- pkg/api/organizationapi/member_delete_test.go | 11 ++++++----- pkg/api/organizationapi/member_list_test.go | 11 ++++++----- .../member_role_assignments_add_test.go | 11 ++++++----- .../member_role_assignments_remove_test.go | 11 ++++++----- pkg/api/organizationapi/update_test.go | 11 ++++++----- 10 files changed, 60 insertions(+), 50 deletions(-) diff --git a/pkg/api/organizationapi/get_test.go b/pkg/api/organizationapi/get_test.go index 0e27f117..6c917258 100644 --- a/pkg/api/organizationapi/get_test.go +++ b/pkg/api/organizationapi/get_test.go @@ -92,11 +92,12 @@ func TestGetOrganization(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := Get(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if !assert.Equal(t, test.want, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, got) } }) } diff --git a/pkg/api/organizationapi/invitation_create_test.go b/pkg/api/organizationapi/invitation_create_test.go index 0ca101a3..ec58923e 100644 --- a/pkg/api/organizationapi/invitation_create_test.go +++ b/pkg/api/organizationapi/invitation_create_test.go @@ -139,11 +139,12 @@ func TestCreateInvitation(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := CreateInvitation(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if !assert.Equal(t, test.want, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, got) } }) } diff --git a/pkg/api/organizationapi/invitation_delete_test.go b/pkg/api/organizationapi/invitation_delete_test.go index a9502962..f8122ce9 100644 --- a/pkg/api/organizationapi/invitation_delete_test.go +++ b/pkg/api/organizationapi/invitation_delete_test.go @@ -69,11 +69,12 @@ func TestDeleteInvitation(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := DeleteInvitation(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if err == nil && !assert.NotNil(t, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) } }) } diff --git a/pkg/api/organizationapi/invitation_list_test.go b/pkg/api/organizationapi/invitation_list_test.go index eded9588..2990fa76 100644 --- a/pkg/api/organizationapi/invitation_list_test.go +++ b/pkg/api/organizationapi/invitation_list_test.go @@ -135,11 +135,12 @@ func TestListInvitations(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := ListInvitations(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if !assert.Equal(t, test.want, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, got) } }) } diff --git a/pkg/api/organizationapi/list_test.go b/pkg/api/organizationapi/list_test.go index 932087da..b5048371 100644 --- a/pkg/api/organizationapi/list_test.go +++ b/pkg/api/organizationapi/list_test.go @@ -97,11 +97,12 @@ func TestListOrganization(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := List(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if !assert.Equal(t, test.want, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, got) } }) } diff --git a/pkg/api/organizationapi/member_delete_test.go b/pkg/api/organizationapi/member_delete_test.go index b9a66dae..0c5ccb46 100644 --- a/pkg/api/organizationapi/member_delete_test.go +++ b/pkg/api/organizationapi/member_delete_test.go @@ -69,11 +69,12 @@ func TestDeleteMember(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := DeleteMember(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if err == nil && !assert.NotNil(t, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.NotNil(t, got) } }) } diff --git a/pkg/api/organizationapi/member_list_test.go b/pkg/api/organizationapi/member_list_test.go index 6e857b15..79653252 100644 --- a/pkg/api/organizationapi/member_list_test.go +++ b/pkg/api/organizationapi/member_list_test.go @@ -111,11 +111,12 @@ func TestListMembers(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := ListMembers(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if !assert.Equal(t, test.want, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, got) } }) } diff --git a/pkg/api/organizationapi/member_role_assignments_add_test.go b/pkg/api/organizationapi/member_role_assignments_add_test.go index 83fadecf..4e88df5f 100644 --- a/pkg/api/organizationapi/member_role_assignments_add_test.go +++ b/pkg/api/organizationapi/member_role_assignments_add_test.go @@ -89,11 +89,12 @@ func TestAddRoleAssignments(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := AddRoleAssignments(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if got != nil && !assert.Equal(t, test.want, *got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, *got) } }) } diff --git a/pkg/api/organizationapi/member_role_assignments_remove_test.go b/pkg/api/organizationapi/member_role_assignments_remove_test.go index a643e88d..92029de1 100644 --- a/pkg/api/organizationapi/member_role_assignments_remove_test.go +++ b/pkg/api/organizationapi/member_role_assignments_remove_test.go @@ -89,11 +89,12 @@ func TestRemoveRoleAssignments(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := RemoveRoleAssignments(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if got != nil && !assert.Equal(t, test.want, *got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, *got) } }) } diff --git a/pkg/api/organizationapi/update_test.go b/pkg/api/organizationapi/update_test.go index 486e2777..dd74559c 100644 --- a/pkg/api/organizationapi/update_test.go +++ b/pkg/api/organizationapi/update_test.go @@ -117,11 +117,12 @@ func TestUpdateOrganization(t *testing.T) { for _, test := range tests { t.Run(test.name, func(t *testing.T) { got, err := Update(test.params) - if err != nil && !assert.EqualError(t, err, test.err) { - t.Error(err) - } - if !assert.Equal(t, test.want, got) { - t.Error(err) + if test.err != "" { + assert.EqualError(t, err, test.err) + assert.Nil(t, got) + } else { + assert.NoError(t, err) + assert.Equal(t, test.want, got) } }) }