From 769d6a1058febcc8dca88ad6c5c47058705954c1 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 14:25:13 +0200 Subject: [PATCH 01/20] Add new service principal entitlement resource --- .../resource_service_principal_entitlement.go | 385 +++++++++++ ...urce_service_principal_entitlement_test.go | 632 ++++++++++++++++++ azuredevops/provider.go | 1 + 3 files changed, 1018 insertions(+) create mode 100644 azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go create mode 100644 azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go new file mode 100644 index 000000000..0e102980f --- /dev/null +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -0,0 +1,385 @@ +package memberentitlementmanagement + +import ( + "fmt" + "regexp" + "strings" + + "github.com/ahmetb/go-linq" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/accounts" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/identity" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/licensing" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/memberentitlementmanagement" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/suppress" +) + +var ( + spConfigurationKeys = []string{ + "origin_id", + "origin", + "principal_name", + } +) + +// ResourceServicePrincipalEntitlement schema and implementation for service principal entitlement resource +func ResourceServicePrincipalEntitlement() *schema.Resource { + return &schema.Resource{ + Create: resourceServicePrincipalEntitlementCreate, + Read: resourceServicePrincipalEntitlementRead, + Delete: resourceServicePrincipalEntitlementDelete, + Update: resourceServicePrincipalEntitlementUpdate, + Importer: &schema.ResourceImporter{ + State: importServicePrincipalEntitlement, + }, + Schema: map[string]*schema.Schema{ + "principal_name": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"origin_id", "origin"}, + AtLeastOneOf: spConfigurationKeys, + DiffSuppressFunc: suppress.CaseDifference, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + "origin_id": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"principal_name"}, + AtLeastOneOf: spConfigurationKeys, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + "origin": { + Type: schema.TypeString, + Computed: true, + Optional: true, + ForceNew: true, + ConflictsWith: []string{"principal_name"}, + AtLeastOneOf: spConfigurationKeys, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + "account_license_type": { + Type: schema.TypeString, + Optional: true, + Default: licensing.AccountLicenseTypeValues.Express, + ValidateFunc: validation.StringInSlice([]string{ + string(licensing.AccountLicenseTypeValues.Advanced), + string(licensing.AccountLicenseTypeValues.EarlyAdopter), + string(licensing.AccountLicenseTypeValues.Express), + "basic", + string(licensing.AccountLicenseTypeValues.None), + string(licensing.AccountLicenseTypeValues.Professional), + string(licensing.AccountLicenseTypeValues.Stakeholder), + }, true), + DiffSuppressFunc: func(_, old, new string, _ *schema.ResourceData) bool { + equalEntitlements := []string{ + string(licensing.AccountLicenseTypeValues.EarlyAdopter), + string(licensing.AccountLicenseTypeValues.Express), + "basic", + } + stringInSlice := func(v string, valid []string) bool { + for _, str := range valid { + if strings.EqualFold(v, str) { + return true + } + } + return false + } + return strings.EqualFold(old, new) || + (stringInSlice(old, equalEntitlements) && stringInSlice(new, equalEntitlements)) + }, + }, + "licensing_source": { + Type: schema.TypeString, + Optional: true, + Default: string(licensing.LicensingSourceValues.Account), + ValidateFunc: validation.StringInSlice([]string{ + string(licensing.LicensingSourceValues.None), + string(licensing.LicensingSourceValues.Account), + string(licensing.LicensingSourceValues.Msdn), + string(licensing.LicensingSourceValues.Profile), + string(licensing.LicensingSourceValues.Auto), + string(licensing.LicensingSourceValues.Trial), + }, true), + DiffSuppressFunc: suppress.CaseDifference, + }, + "descriptor": { + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceServicePrincipalEntitlementCreate(d *schema.ResourceData, m interface{}) error { + clients := m.(*client.AggregatedClient) + servicePrincipalEntitlement, err := expandServicePrincipalEntitlement(d) + if err != nil { + return fmt.Errorf("Creating service principal entitlement: %v", err) + } + + addedServicePrincipalEntitlement, err := addServicePrincipalEntitlement(clients, servicePrincipalEntitlement) + if err != nil { + return fmt.Errorf("Creating service principal entitlement: %v", err) + } + + flattenServicePrincipalEntitlement(d, addedServicePrincipalEntitlement) + return resourceServicePrincipalEntitlementRead(d, m) +} + +func resourceServicePrincipalEntitlementRead(d *schema.ResourceData, m interface{}) error { + clients := m.(*client.AggregatedClient) + servicePrincipalEntitlementID := d.Id() + id, err := uuid.Parse(servicePrincipalEntitlementID) + if err != nil { + return fmt.Errorf("Error parsing ServicePrincipalEntitlementID: %s. %v", servicePrincipalEntitlementID, err) + } + + servicePrincipalEntitlement, err := readServicePrincipalEntitlement(clients, &id) + + if err != nil { + if utils.ResponseWasNotFound(err) || isServicePrincipalDeleted(servicePrincipalEntitlement) { + d.SetId("") + return nil + } + return fmt.Errorf("Error reading service principal entitlement: %v", err) + } + + flattenServicePrincipalEntitlement(d, servicePrincipalEntitlement) + return nil +} + +func expandServicePrincipalEntitlement(d *schema.ResourceData) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { + origin := d.Get("origin").(string) + originID := d.Get("origin_id").(string) + principalName := d.Get("principal_name").(string) + + if len(originID) > 0 && len(principalName) > 0 { + return nil, fmt.Errorf("Both origin_id and principal_name set. You can not use both: origin_id: %s principal_name %s", originID, principalName) + } + + if len(originID) == 0 && len(principalName) == 0 { + return nil, fmt.Errorf("Neither origin_id and principal_name set. Use origin_id or principal_name") + } + + if len(originID) > 0 && len(origin) == 0 { + return nil, fmt.Errorf("Origin_id requires an origin to be set") + } + + accountLicenseType, err := converter.AccountLicenseType(d.Get("account_license_type").(string)) + if err != nil { + return nil, err + } + licensingSource, err := converter.AccountLicensingSource(d.Get("licensing_source").(string)) + if err != nil { + return nil, err + } + + return &memberentitlementmanagement.ServicePrincipalEntitlement{ + + AccessLevel: &licensing.AccessLevel{ + AccountLicenseType: accountLicenseType, + LicensingSource: licensingSource, + }, + + // TODO check if it works in both case for GitHub and AzureDevOps + ServicePrincipal: &graph.GraphServicePrincipal{ + Origin: &origin, + OriginId: &originID, + PrincipalName: &principalName, + SubjectKind: converter.String("servicePrincipal"), + }, + }, nil +} + +func flattenServicePrincipalEntitlement(d *schema.ResourceData, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) { + d.SetId(servicePrincipalEntitlement.Id.String()) + d.Set("descriptor", *servicePrincipalEntitlement.ServicePrincipal.Descriptor) + d.Set("origin", *servicePrincipalEntitlement.ServicePrincipal.Origin) + if servicePrincipalEntitlement.ServicePrincipal.OriginId != nil { + d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) + } + d.Set("principal_name", *servicePrincipalEntitlement.ServicePrincipal.PrincipalName) + d.Set("account_license_type", string(*servicePrincipalEntitlement.AccessLevel.AccountLicenseType)) + d.Set("licensing_source", *servicePrincipalEntitlement.AccessLevel.LicensingSource) +} + +func addServicePrincipalEntitlement(clients *client.AggregatedClient, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { + servicePrincipalEntitlementsPostResponse, err := clients.MemberEntitleManagementClient.AddServicePrincipalEntitlement(clients.Ctx, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ + ServicePrincipalEntitlement: servicePrincipalEntitlement, + }) + + if err != nil { + return nil, err + } + + if !*servicePrincipalEntitlementsPostResponse.IsSuccess { + opResults := []memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{} + if servicePrincipalEntitlementsPostResponse.OperationResult != nil { + opResults = append(opResults, *servicePrincipalEntitlementsPostResponse.OperationResult) + } + return nil, fmt.Errorf("Adding service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(&opResults)) + } + + return servicePrincipalEntitlementsPostResponse.ServicePrincipalEntitlement, nil +} + +func readServicePrincipalEntitlement(clients *client.AggregatedClient, id *uuid.UUID) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { + return clients.MemberEntitleManagementClient.GetServicePrincipalEntitlement(clients.Ctx, memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: id, + }) +} + +func resourceServicePrincipalEntitlementDelete(d *schema.ResourceData, m interface{}) error { + if d.Id() == "" { + return nil + } + + servicePrincipalEntitlementID := d.Id() + id, err := uuid.Parse(servicePrincipalEntitlementID) + if err != nil { + return fmt.Errorf("Error parsing ServicePrincipalEntitlement ID. ServicePrincipalEntitlementID: %s. %v", servicePrincipalEntitlementID, err) + } + + clients := m.(*client.AggregatedClient) + + err = clients.MemberEntitleManagementClient.DeleteServicePrincipalEntitlement(m.(*client.AggregatedClient).Ctx, memberentitlementmanagement.DeleteServicePrincipalEntitlementArgs{ + ServicePrincipalId: &id, + }) + + if err != nil { + return fmt.Errorf("Deleting service principal entitlement: %v", err) + } + + return nil +} + +func resourceServicePrincipalEntitlementUpdate(d *schema.ResourceData, m interface{}) error { + servicePrincipalEntitlementID := d.Id() + id, err := uuid.Parse(servicePrincipalEntitlementID) + if err != nil { + return fmt.Errorf("Parsing ServicePrincipalEntitlement ID. ServicePrincipalEntitlementID: %s. %v", servicePrincipalEntitlementID, err) + } + + accountLicenseType, err := converter.AccountLicenseType(d.Get("account_license_type").(string)) + if err != nil { + return err + } + licensingSource, ok := d.GetOk("licensing_source") + if !ok { + return fmt.Errorf("Reading account licensing source for ServicePrincipalEntitlementID: %s", servicePrincipalEntitlementID) + } + + clients := m.(*client.AggregatedClient) + + patchResponse, err := clients.MemberEntitleManagementClient.UpdateServicePrincipalEntitlement(clients.Ctx, + memberentitlementmanagement.UpdateServicePrincipalEntitlementArgs{ + ServicePrincipalId: &id, + Document: &[]webapi.JsonPatchOperation{ + { + Op: &webapi.OperationValues.Replace, + From: nil, + Path: converter.String("/accessLevel"), + Value: struct { + AccountLicenseType string `json:"accountLicenseType"` + LicensingSource string `json:"licensingSource"` + }{ + string(*accountLicenseType), + licensingSource.(string), + }, + }, + }, + }) + + if err != nil { + return fmt.Errorf("Updating service principal entitlement: %v", err) + } + + if !*patchResponse.IsSuccess { + return fmt.Errorf("Updating service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(patchResponse.OperationResults)) + } + return resourceServicePrincipalEntitlementRead(d, m) +} + +var spEmailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") + +func importServicePrincipalEntitlement(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { + _, err := uuid.Parse(d.Id()) + if err != nil { + upn := d.Id() + if !spEmailRegexp.MatchString(upn) { + return nil, fmt.Errorf("Only UUID and UPN values can used for import [%s]", upn) + } + + clients := m.(*client.AggregatedClient) + result, err := clients.IdentityClient.ReadIdentities(clients.Ctx, identity.ReadIdentitiesArgs{ + SearchFilter: converter.String("General"), + FilterValue: &upn, + }) + if err != nil { + return nil, err + } + + if result == nil || len(*result) <= 0 { + return nil, fmt.Errorf("No entitlement found for [%s]", upn) + } + if len(*result) > 1 { + return nil, fmt.Errorf("More than one entitlement found for [%s]", upn) + } + + d.SetId((*result)[0].Id.String()) + } + return []*schema.ResourceData{d}, nil +} + +func getServicePrincipalEntitlementAPIErrorMessage(operationResults *[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult) string { + errMsg := "Unknown API error" + if operationResults != nil && len(*operationResults) > 0 { + errMsg = linq.From(*operationResults). + Where(func(elem interface{}) bool { + ueo := elem.(memberentitlementmanagement.ServicePrincipalEntitlementOperationResult) + return !*ueo.IsSuccess + }). + SelectMany(func(elem interface{}) linq.Query { + ueo := elem.(memberentitlementmanagement.ServicePrincipalEntitlementOperationResult) + if ueo.Errors == nil { + key := interface{}("0000") + value := interface{}("Unknown API error") + return linq.From([]azuredevops.KeyValuePair{ + { + Key: &key, + Value: &value, + }, + }) + } + return linq.From(*ueo.Errors) + }). + SelectT(func(err azuredevops.KeyValuePair) string { + return fmt.Sprintf("(%v) %s", *err.Key, *err.Value) + }). + AggregateT(func(agg string, elem string) string { + return agg + "\n" + elem + }).(string) + } + return errMsg +} + +func isServicePrincipalDeleted(servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) bool { + if servicePrincipalEntitlement == nil { + return true + } + + return *servicePrincipalEntitlement.AccessLevel.Status == accounts.AccountUserStatusValues.Deleted || + *servicePrincipalEntitlement.AccessLevel.Status == accounts.AccountUserStatusValues.None +} diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go new file mode 100644 index 000000000..bb4b9d1dc --- /dev/null +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go @@ -0,0 +1,632 @@ +//go:build (all || resource_user_entitlement) && !exclude_resource_user_entitlement +// +build all resource_user_entitlement +// +build !exclude_resource_user_entitlement + +package memberentitlementmanagement + +import ( + "context" + "fmt" + "testing" + + "github.com/golang/mock/gomock" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/identity" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/licensing" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/memberentitlementmanagement" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" + "github.com/microsoft/terraform-provider-azuredevops/azdosdkmocks" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/client" + "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/converter" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// if origin_id is provided, it will be used. if principal_name is also supplied, an error will be reported. +func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_DoNotAllowToSetOridinIdAndPrincipalName(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: nil, + Ctx: context.Background(), + } + + originID := "e97b0e7f-0a61-41ad-860c-748ec5fcb20b" + principalName := "foobar@microsoft.com" + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.Set("origin_id", originID) + resourceData.Set("principal_name", principalName) + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + assert.NotNil(t, err, "err should not be nil") + require.Regexp(t, "Both origin_id and principal_name set. You can not use both", err.Error()) +} + +// if origin_id is "" and principal_name is supplied, the principal_name will be used. +func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithPrincipalName(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + accountLicenseType := licensing.AccountLicenseTypeValues.Express + origin := "" + originID := "" + principalName := "foobar@microsoft.com" + descriptor := "baz" + id := uuid.New() + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, descriptor) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.Set("principal_name", principalName) + + expectedIsSuccess := true + memberEntitlementClient. + EXPECT(). + AddServicePrincipalEntitlement(gomock.Any(), MatchAddServicePrincipalEntitlementArgs(t, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ + ServicePrincipalEntitlement: mockServicePrincipalEntitlement, + })). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: mockServicePrincipalEntitlement, + }, nil). + Times(1) + + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: mockServicePrincipalEntitlement.Id, + }). + Return(mockServicePrincipalEntitlement, nil) + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + assert.Nil(t, err, "err should not be nil") +} + +// if origin_id is "" and principal_name is "", an error will be reported. +func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_Need_OriginID_Or_PrincipalName(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: nil, + Ctx: context.Background(), + } + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + // originID and principalName is not set. + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + assert.NotNil(t, err, "err should not be nil") + require.Regexp(t, "Use origin_id or principal_name", err.Error()) +} + +// if the REST-API return the failure, it should fail. + +func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + principalName := "foobar@microsoft.com" + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + // resourceData.Set("origin_id", originID) + resourceData.Set("account_license_type", "express") + resourceData.Set("principal_name", principalName) + + // No error but it has a error on the response. + memberEntitlementClient. + EXPECT(). + AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(nil, fmt.Errorf("error foo")). + Times(1) + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + assert.NotNil(t, err, "err should not be nil") +} + +// if the REST-API return the success, but fails on response +func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithEarlyAdopter(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + principalName := "foobar@microsoft.com" + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + // resourceData.Set("origin_id", originID) + resourceData.Set("account_license_type", "earlyAdopter") + resourceData.Set("principal_name", principalName) + + var expectedKey interface{} = 5000 + var expectedValue interface{} = "A user cannot be assigned an Account-EarlyAdopter license." + expectedErrors := []azuredevops.KeyValuePair{ + { + Key: &expectedKey, + Value: &expectedValue, + }, + } + expectedIsSuccess := false + + // No error but it has a error on the response. + memberEntitlementClient. + EXPECT(). + AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Errors: &expectedErrors, + }, + }, nil). + Times(1) + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + require.Contains(t, err.Error(), "A user cannot be assigned an Account-EarlyAdopter license.") +} + +// TestServicePrincipalEntitlement_Update_TestChangeEntitlement verfies that an entitlement can be changed +func TestServicePrincipalEntitlement_Update_TestChangeEntitlement(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + accountLicenseType := licensing.AccountLicenseTypeValues.Stakeholder + origin := "" + originID := "" + principalName := "foobar@microsoft.com" + descriptor := "baz" + id := uuid.New() + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, descriptor) + expectedIsSuccess := true + + memberEntitlementClient. + EXPECT(). + UpdateServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.UpdateServicePrincipalEntitlementArgs{ + ServicePrincipalId: &id, + Document: &[]webapi.JsonPatchOperation{ + { + Op: &webapi.OperationValues.Replace, + From: nil, + Path: converter.String("/accessLevel"), + Value: struct { + AccountLicenseType string `json:"accountLicenseType"` + LicensingSource string `json:"licensingSource"` + }{ + string(licensing.AccountLicenseTypeValues.Stakeholder), + string(licensing.LicensingSourceValues.Account), + }, + }, + }, + }). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: mockServicePrincipalEntitlement, + }, nil). + Times(1) + + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: mockServicePrincipalEntitlement.Id, + }). + Return(mockServicePrincipalEntitlement, nil). + Times(1) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.SetId(id.String()) + resourceData.Set("principal_name", principalName) + resourceData.Set("account_license_type", string(licensing.AccountLicenseTypeValues.Stakeholder)) + resourceData.Set("licensing_source", string(licensing.LicensingSourceValues.Account)) + + err := resourceServicePrincipalEntitlementUpdate(resourceData, clients) + assert.Nil(t, err) +} + +// TestServicePrincipalEntitlement_CreateUpdate_TestBasicEntitlement verifies that the (virtual) Basic entitlement can be set +func TestServicePrincipalEntitlement_CreateUpdate_TestBasicEntitlement(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + accountLicenseType := licensing.AccountLicenseTypeValues.Express + origin := "" + originID := "" + principalName := "foobar@microsoft.com" + descriptor := "baz" + id := uuid.New() + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, descriptor) + expectedIsSuccess := true + + memberEntitlementClient. + EXPECT(). + AddServicePrincipalEntitlement(gomock.Any(), MatchAddServicePrincipalEntitlementArgs(t, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ + ServicePrincipalEntitlement: mockServicePrincipalEntitlement, + })). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: mockServicePrincipalEntitlement, + }, nil). + Times(1) + + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: mockServicePrincipalEntitlement.Id, + }). + Return(mockServicePrincipalEntitlement, nil) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.Set("principal_name", principalName) + resourceData.Set("account_license_type", "basic") + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + assert.Nil(t, err, "err should be nil") +} + +// TestServicePrincipalEntitlement_Import_TestUPN tests if import is successful using an UPN +func TestServicePrincipalEntitlement_Import_TestUPN(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + identityClient := azdosdkmocks.NewMockIdentityClient(ctrl) + clients := &client.AggregatedClient{ + IdentityClient: identityClient, + Ctx: context.Background(), + } + + principalName := "foobar@microsoft.com" + id := uuid.New() + + identityClient. + EXPECT(). + ReadIdentities(gomock.Any(), gomock.Any()). + Return(&[]identity.Identity{ + { + Id: &id, + }, + }, nil). + Times(1) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.SetId(principalName) + + d, err := importServicePrincipalEntitlement(resourceData, clients) + assert.Nil(t, err) + assert.NotNil(t, d) + assert.Len(t, d, 1) + assert.Equal(t, id.String(), d[0].Id()) +} + +// TestServicePrincipalEntitlement_Import_TestID tests if import is successful using an UUID +func TestServicePrincipalEntitlement_Import_TestID(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + id := uuid.New().String() + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.SetId(id) + + d, err := importServicePrincipalEntitlement(resourceData, clients) + assert.Nil(t, err) + assert.NotNil(t, d) + assert.Len(t, d, 1) + assert.Equal(t, id, d[0].Id()) +} + +// TestServicePrincipalEntitlement_Import_TestInvalidValue tests if only a valid UPN and UUID can be used to import a resource +func TestServicePrincipalEntitlement_Import_TestInvalidValue(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + id := "InvalidValue-a73c5191-e20d" + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.SetId(id) + + d, err := importServicePrincipalEntitlement(resourceData, clients) + assert.Nil(t, d) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), "Only UUID and UPN values can used for import") +} + +func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + id, _ := uuid.NewUUID() + expectedIsSuccess := false + k1 := interface{}("9999") + v1 := interface{}("Error1") + k2 := interface{}("9998") + v2 := interface{}("Error2") + + memberEntitlementClient. + EXPECT(). + AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: &[]azuredevops.KeyValuePair{ + { + Key: &k1, + Value: &v1, + }, + { + Key: &k2, + Value: &v2, + }, + }, + }, + }, nil). + Times(1) + + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(nil, nil). + Times(0) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.Set("principal_name", "foobar@microsoft.com") + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + assert.NotNil(t, err, "err should not be nil") + assert.Contains(t, err.Error(), "(9999) Error1") + assert.Contains(t, err.Error(), "(9998) Error2") +} + +func TestServicePrincipalEntitlement_Create_TestEmptyErrors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + id, _ := uuid.NewUUID() + expectedIsSuccess := false + + memberEntitlementClient. + EXPECT(). + AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: nil, + }, + }, nil). + Times(1) + + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(nil, nil). + Times(0) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.Set("principal_name", "foobar@microsoft.com") + + err := resourceServicePrincipalEntitlementCreate(resourceData, clients) + assert.NotNil(t, err, "err should not be nil") + assert.Contains(t, err.Error(), "Unknown API error") +} + +func TestServicePrincipalEntitlement_Update_TestErrorFormatting(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + id, _ := uuid.NewUUID() + expectedIsSuccess := false + k1 := interface{}("9999") + v1 := interface{}("Error1") + k2 := interface{}("9998") + v2 := interface{}("Error2") + + memberEntitlementClient. + EXPECT(). + UpdateServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResults: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + { + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: &[]azuredevops.KeyValuePair{ + { + Key: &k1, + Value: &v1, + }, + { + Key: &k2, + Value: &v2, + }, + }, + }, + }, + }, nil). + Times(1) + + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(nil, nil). + Times(0) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.SetId(id.String()) + resourceData.Set("principal_name", "foobar@microsoft.com") + + err := resourceServicePrincipalEntitlementUpdate(resourceData, clients) + assert.NotNil(t, err, "err should not be nil") + assert.Contains(t, err.Error(), "(9999) Error1") + assert.Contains(t, err.Error(), "(9998) Error2") +} + +func TestServicePrincipalEntitlement_Update_TestEmptyErrors(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) + clients := &client.AggregatedClient{ + MemberEntitleManagementClient: memberEntitlementClient, + Ctx: context.Background(), + } + + id, _ := uuid.NewUUID() + expectedIsSuccess := false + + memberEntitlementClient. + EXPECT(). + UpdateServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResults: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + { + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: nil, + }, + }, + }, nil). + Times(1) + + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), gomock.Any()). + Return(nil, nil). + Times(0) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.SetId(id.String()) + resourceData.Set("principal_name", "foobar@microsoft.com") + + err := resourceServicePrincipalEntitlementUpdate(resourceData, clients) + assert.NotNil(t, err, "err should not be nil") + assert.Contains(t, err.Error(), "Unknown API error") +} + +func getMockServicePrincipalEntitlement(id *uuid.UUID, accountLicenseType licensing.AccountLicenseType, origin string, originID string, principalName string, descriptor string) *memberentitlementmanagement.ServicePrincipalEntitlement { + subjectKind := "servicePrincipal" + licensingSource := licensing.LicensingSourceValues.Account + + return &memberentitlementmanagement.ServicePrincipalEntitlement{ + AccessLevel: &licensing.AccessLevel{ + AccountLicenseType: &accountLicenseType, + LicensingSource: &licensingSource, + }, + Id: id, + ServicePrincipal: &graph.GraphServicePrincipal{ + Origin: &origin, + OriginId: &originID, + PrincipalName: &principalName, + SubjectKind: &subjectKind, + Descriptor: &descriptor, + }, + } +} + +type matchAddServicePrincipalEntitlementArgs struct { + t *testing.T + x memberentitlementmanagement.AddServicePrincipalEntitlementArgs +} + +func MatchAddServicePrincipalEntitlementArgs(t *testing.T, x memberentitlementmanagement.AddServicePrincipalEntitlementArgs) gomock.Matcher { + return &matchAddServicePrincipalEntitlementArgs{t, x} +} + +func (m *matchAddServicePrincipalEntitlementArgs) Matches(x interface{}) bool { + args := x.(memberentitlementmanagement.AddServicePrincipalEntitlementArgs) + m.t.Logf("MatchAddServicePrincipalEntitlementArgs:\nVALUE: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], principal_name: [%s]\n REF: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], principal_name: [%s]\n", + *args.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, + *args.ServicePrincipalEntitlement.AccessLevel.LicensingSource, + *args.ServicePrincipalEntitlement.ServicePrincipal.Origin, + *args.ServicePrincipalEntitlement.ServicePrincipal.OriginId, + *args.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName, + *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, + *m.x.ServicePrincipalEntitlement.AccessLevel.LicensingSource, + *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin, + *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId, + *m.x.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName) + + return *args.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType == *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType && + *args.ServicePrincipalEntitlement.ServicePrincipal.Origin == *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin && + *args.ServicePrincipalEntitlement.ServicePrincipal.OriginId == *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId && + *args.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName == *m.x.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName +} + +func (m *matchAddServicePrincipalEntitlementArgs) String() string { + return fmt.Sprintf("account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], principal_name: [%s]", + *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, + *m.x.ServicePrincipalEntitlement.AccessLevel.LicensingSource, + *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin, + *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId, + *m.x.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName) +} diff --git a/azuredevops/provider.go b/azuredevops/provider.go index f6b928243..9b221467c 100644 --- a/azuredevops/provider.go +++ b/azuredevops/provider.go @@ -94,6 +94,7 @@ func Provider() *schema.Provider { "azuredevops_git_repository_file": git.ResourceGitRepositoryFile(), "azuredevops_user_entitlement": memberentitlementmanagement.ResourceUserEntitlement(), "azuredevops_group_entitlement": memberentitlementmanagement.ResourceGroupEntitlement(), + "azuredevops_service_principal_entitlement": memberentitlementmanagement.ResourceServicePrincipalEntitlement(), "azuredevops_group_membership": graph.ResourceGroupMembership(), "azuredevops_agent_pool": taskagent.ResourceAgentPool(), "azuredevops_elastic_pool": taskagent.ResourceAgentPoolVMSS(), From 73166e7555c1be62455fd4371ce1d13ad4f73fd3 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 14:40:59 +0200 Subject: [PATCH 02/20] Add sp entitlement documentation page --- ...ervice_principal_entitlement.html.markdown | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 website/docs/r/service_principal_entitlement.html.markdown diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown new file mode 100644 index 000000000..74629b31f --- /dev/null +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -0,0 +1,48 @@ +--- +layout: "azuredevops" +page_title: "AzureDevops: azuredevops_service_principal_entitlement" +description: |- + Manages a service principal entitlement within Azure DevOps organization. +--- + +# azuredevops_service_principal_entitlement + +Manages a service principal entitlement within Azure DevOps. + +## Example Usage + +```hcl +resource "azuredevops_service_principal_entitlement" "example" { + principal_name = "foo@contoso.com" +} +``` + +## Argument Reference + +- `principal_name` - (Optional) The principal name is the PrincipalName of a graph member from the source provider. Usually, e-mail address. +- `origin_id` - (Optional) The unique identifier from the system of origin. Typically a sid, object id or Guid. e.g. Used for member of other tenant on Azure Active Directory. +- `origin` - (Optional) The type of source provider for the origin identifier. +- `account_license_type` - (Optional) Type of Account License. Valid values: `advanced`, `earlyAdopter`, `express`, `none`, `professional`, or `stakeholder`. Defaults to `express`. In addition the value `basic` is allowed which is an alias for `express` and reflects the name of the `express` license used in the Azure DevOps web interface. +- `licensing_source` - (Optional) The source of the licensing (e.g. Account. MSDN etc.) Valid values: `account` (Default), `auto`, `msdn`, `none`, `profile`, `trial` + +> **NOTE:** A service principal can only be referenced by it's `principal_name` or by the combination of `origin_id` and `origin`. + +## Attributes Reference + +The following attributes are exported: + +- `id` - The id of the entitlement. +- `descriptor` - The descriptor is the primary way to reference the graph subject while the system is running. This field will uniquely identify the service principal graph subject. + +## Relevant Links + +- [Azure DevOps Service REST API 7.0 - User Entitlements - Add](https://learn.microsoft.com/en-us/rest/api/azure/devops/memberentitlementmanagement/service-principal-entitlements/add?view=azure-devops-rest-7.1) +- [Programmatic mapping of access levels](https://docs.microsoft.com/en-us/azure/devops/organizations/security/access-levels?view=azure-devops#programmatic-mapping-of-access-levels) + +## Import + +The resources allows the import via the UUID of a service principal entitlement or by using the principal name of a service principal owning an entitlement. + +## PAT Permissions Required + +- **Member Entitlement Management**: Read & Write From a83280272cbc5849da8dbe45af9e236116427c49 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 15:19:57 +0200 Subject: [PATCH 03/20] Add entry in TestProvider --- .../resource_service_principal_entitlement_test.go | 6 +++--- azuredevops/provider_test.go | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go index bb4b9d1dc..7bdd6a171 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go @@ -1,6 +1,6 @@ -//go:build (all || resource_user_entitlement) && !exclude_resource_user_entitlement -// +build all resource_user_entitlement -// +build !exclude_resource_user_entitlement +//go:build (all || resource_service_principal_entitlement) && !exclude_resource_service_principal_entitlement +// +build all resource_service_principal_entitlement +// +build !exclude_resource_service_principal_entitlement package memberentitlementmanagement diff --git a/azuredevops/provider_test.go b/azuredevops/provider_test.go index c6fd9957e..21dd06942 100644 --- a/azuredevops/provider_test.go +++ b/azuredevops/provider_test.go @@ -96,6 +96,7 @@ func TestProvider_HasChildResources(t *testing.T) { "azuredevops_git_repository_file", "azuredevops_user_entitlement", "azuredevops_group_entitlement", + "azuredevops_service_principal_entitlement", "azuredevops_group_membership", "azuredevops_group", "azuredevops_agent_pool", From 1c253829ba145da50812429be045ab69589d79e6 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 16:14:10 +0200 Subject: [PATCH 04/20] Using group as template --- .../resource_service_principal_entitlement.go | 254 ++++++++---------- 1 file changed, 112 insertions(+), 142 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 0e102980f..431062102 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -2,7 +2,7 @@ package memberentitlementmanagement import ( "fmt" - "regexp" + "log" "strings" "github.com/ahmetb/go-linq" @@ -10,9 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/microsoft/azure-devops-go-api/azuredevops/v7" - "github.com/microsoft/azure-devops-go-api/azuredevops/v7/accounts" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph" - "github.com/microsoft/azure-devops-go-api/azuredevops/v7/identity" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/licensing" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/memberentitlementmanagement" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" @@ -24,49 +22,51 @@ import ( var ( spConfigurationKeys = []string{ + "display_name", "origin_id", - "origin", - "principal_name", } ) -// ResourceServicePrincipalEntitlement schema and implementation for service principal entitlement resource func ResourceServicePrincipalEntitlement() *schema.Resource { return &schema.Resource{ Create: resourceServicePrincipalEntitlementCreate, Read: resourceServicePrincipalEntitlementRead, - Delete: resourceServicePrincipalEntitlementDelete, Update: resourceServicePrincipalEntitlementUpdate, + Delete: resourceServicePrincipalEntitlementDelete, Importer: &schema.ResourceImporter{ State: importServicePrincipalEntitlement, }, Schema: map[string]*schema.Schema{ "principal_name": { - Type: schema.TypeString, - Computed: true, - Optional: true, - ForceNew: true, - ConflictsWith: []string{"origin_id", "origin"}, - AtLeastOneOf: spConfigurationKeys, - DiffSuppressFunc: suppress.CaseDifference, - ValidateFunc: validation.StringIsNotWhiteSpace, + Type: schema.TypeString, + Computed: true, }, - "origin_id": { + "display_name": { Type: schema.TypeString, + Optional: true, + ForceNew: true, Computed: true, + ConflictsWith: []string{"origin_id", "origin"}, + ExactlyOneOf: spConfigurationKeys, + ValidateFunc: validation.StringIsNotWhiteSpace, + }, + "origin_id": { + Type: schema.TypeString, Optional: true, + Computed: true, ForceNew: true, - ConflictsWith: []string{"principal_name"}, - AtLeastOneOf: spConfigurationKeys, + ConflictsWith: []string{"display_name"}, + RequiredWith: []string{"origin"}, + ExactlyOneOf: spConfigurationKeys, ValidateFunc: validation.StringIsNotWhiteSpace, }, "origin": { Type: schema.TypeString, - Computed: true, Optional: true, + Computed: true, ForceNew: true, - ConflictsWith: []string{"principal_name"}, - AtLeastOneOf: spConfigurationKeys, + ConflictsWith: []string{"display_name"}, + RequiredWith: []string{"origin_id"}, ValidateFunc: validation.StringIsNotWhiteSpace, }, "account_license_type": { @@ -77,7 +77,6 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { string(licensing.AccountLicenseTypeValues.Advanced), string(licensing.AccountLicenseTypeValues.EarlyAdopter), string(licensing.AccountLicenseTypeValues.Express), - "basic", string(licensing.AccountLicenseTypeValues.None), string(licensing.AccountLicenseTypeValues.Professional), string(licensing.AccountLicenseTypeValues.Stakeholder), @@ -134,7 +133,7 @@ func resourceServicePrincipalEntitlementCreate(d *schema.ResourceData, m interfa return fmt.Errorf("Creating service principal entitlement: %v", err) } - flattenServicePrincipalEntitlement(d, addedServicePrincipalEntitlement) + d.SetId(addedServicePrincipalEntitlement.Id.String()) return resourceServicePrincipalEntitlementRead(d, m) } @@ -145,100 +144,26 @@ func resourceServicePrincipalEntitlementRead(d *schema.ResourceData, m interface if err != nil { return fmt.Errorf("Error parsing ServicePrincipalEntitlementID: %s. %v", servicePrincipalEntitlementID, err) } - - servicePrincipalEntitlement, err := readServicePrincipalEntitlement(clients, &id) + servicePrincipalEntitlement, err := clients.MemberEntitleManagementClient.GetServicePrincipalEntitlement(clients.Ctx, memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: &id, + }) if err != nil { - if utils.ResponseWasNotFound(err) || isServicePrincipalDeleted(servicePrincipalEntitlement) { + if utils.ResponseWasNotFound(err) { d.SetId("") return nil } - return fmt.Errorf("Error reading service principal entitlement: %v", err) - } - - flattenServicePrincipalEntitlement(d, servicePrincipalEntitlement) - return nil -} - -func expandServicePrincipalEntitlement(d *schema.ResourceData) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { - origin := d.Get("origin").(string) - originID := d.Get("origin_id").(string) - principalName := d.Get("principal_name").(string) - - if len(originID) > 0 && len(principalName) > 0 { - return nil, fmt.Errorf("Both origin_id and principal_name set. You can not use both: origin_id: %s principal_name %s", originID, principalName) - } - - if len(originID) == 0 && len(principalName) == 0 { - return nil, fmt.Errorf("Neither origin_id and principal_name set. Use origin_id or principal_name") - } - - if len(originID) > 0 && len(origin) == 0 { - return nil, fmt.Errorf("Origin_id requires an origin to be set") - } - - accountLicenseType, err := converter.AccountLicenseType(d.Get("account_license_type").(string)) - if err != nil { - return nil, err - } - licensingSource, err := converter.AccountLicensingSource(d.Get("licensing_source").(string)) - if err != nil { - return nil, err + return fmt.Errorf(" reading service principal entitlement: %v", err) } - return &memberentitlementmanagement.ServicePrincipalEntitlement{ - - AccessLevel: &licensing.AccessLevel{ - AccountLicenseType: accountLicenseType, - LicensingSource: licensingSource, - }, - - // TODO check if it works in both case for GitHub and AzureDevOps - ServicePrincipal: &graph.GraphServicePrincipal{ - Origin: &origin, - OriginId: &originID, - PrincipalName: &principalName, - SubjectKind: converter.String("servicePrincipal"), - }, - }, nil -} - -func flattenServicePrincipalEntitlement(d *schema.ResourceData, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) { - d.SetId(servicePrincipalEntitlement.Id.String()) - d.Set("descriptor", *servicePrincipalEntitlement.ServicePrincipal.Descriptor) - d.Set("origin", *servicePrincipalEntitlement.ServicePrincipal.Origin) - if servicePrincipalEntitlement.ServicePrincipal.OriginId != nil { - d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) - } - d.Set("principal_name", *servicePrincipalEntitlement.ServicePrincipal.PrincipalName) - d.Set("account_license_type", string(*servicePrincipalEntitlement.AccessLevel.AccountLicenseType)) - d.Set("licensing_source", *servicePrincipalEntitlement.AccessLevel.LicensingSource) -} - -func addServicePrincipalEntitlement(clients *client.AggregatedClient, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { - servicePrincipalEntitlementsPostResponse, err := clients.MemberEntitleManagementClient.AddServicePrincipalEntitlement(clients.Ctx, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ - ServicePrincipalEntitlement: servicePrincipalEntitlement, - }) - - if err != nil { - return nil, err - } - - if !*servicePrincipalEntitlementsPostResponse.IsSuccess { - opResults := []memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{} - if servicePrincipalEntitlementsPostResponse.OperationResult != nil { - opResults = append(opResults, *servicePrincipalEntitlementsPostResponse.OperationResult) - } - return nil, fmt.Errorf("Adding service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(&opResults)) + if servicePrincipalEntitlement == nil || servicePrincipalEntitlement.Id == nil { + log.Println(" Service principal entitlement has been deleted") + d.SetId("") + return nil } - return servicePrincipalEntitlementsPostResponse.ServicePrincipalEntitlement, nil -} - -func readServicePrincipalEntitlement(clients *client.AggregatedClient, id *uuid.UUID) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { - return clients.MemberEntitleManagementClient.GetServicePrincipalEntitlement(clients.Ctx, memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ - ServicePrincipalId: id, - }) + flattenServicePrincipalEntitlement(d, servicePrincipalEntitlement) + return nil } func resourceServicePrincipalEntitlementDelete(d *schema.ResourceData, m interface{}) error { @@ -306,43 +231,97 @@ func resourceServicePrincipalEntitlementUpdate(d *schema.ResourceData, m interfa return fmt.Errorf("Updating service principal entitlement: %v", err) } - if !*patchResponse.IsSuccess { - return fmt.Errorf("Updating service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(patchResponse.OperationResults)) + result := *patchResponse.OperationResults + + if !*result[0].IsSuccess { + return fmt.Errorf("Updating service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(&result)) } return resourceServicePrincipalEntitlementRead(d, m) } -var spEmailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$") - func importServicePrincipalEntitlement(d *schema.ResourceData, m interface{}) ([]*schema.ResourceData, error) { - _, err := uuid.Parse(d.Id()) + upn := d.Id() + id, err := uuid.Parse(upn) + if err != nil { - upn := d.Id() - if !spEmailRegexp.MatchString(upn) { - return nil, fmt.Errorf("Only UUID and UPN values can used for import [%s]", upn) - } + return nil, fmt.Errorf("Only UUID values can used for import [%s]", upn) + } - clients := m.(*client.AggregatedClient) - result, err := clients.IdentityClient.ReadIdentities(clients.Ctx, identity.ReadIdentitiesArgs{ - SearchFilter: converter.String("General"), - FilterValue: &upn, - }) - if err != nil { - return nil, err - } + clients := m.(*client.AggregatedClient) + result, err := clients.MemberEntitleManagementClient.GetServicePrincipalEntitlement(clients.Ctx, memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: &id, + }) + if err != nil { + return nil, fmt.Errorf("Error getting the service principal entitlement with supplied id %s: %s", upn, err) + } - if result == nil || len(*result) <= 0 { - return nil, fmt.Errorf("No entitlement found for [%s]", upn) - } - if len(*result) > 1 { - return nil, fmt.Errorf("More than one entitlement found for [%s]", upn) - } + d.SetId((*result).Id.String()) - d.SetId((*result)[0].Id.String()) - } return []*schema.ResourceData{d}, nil } +func flattenServicePrincipalEntitlement(d *schema.ResourceData, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) { + d.SetId(servicePrincipalEntitlement.Id.String()) + d.Set("descriptor", *servicePrincipalEntitlement.ServicePrincipal.Descriptor) + d.Set("origin", *servicePrincipalEntitlement.ServicePrincipal.Origin) + d.Set("principal_name", *servicePrincipalEntitlement.ServicePrincipal.PrincipalName) + if servicePrincipalEntitlement.ServicePrincipal.OriginId != nil { + d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) + } + d.Set("display_name", *servicePrincipalEntitlement.ServicePrincipal.DisplayName) + d.Set("account_license_type", string(*servicePrincipalEntitlement.AccessLevel.AccountLicenseType)) + d.Set("licensing_source", *servicePrincipalEntitlement.AccessLevel.LicensingSource) +} + +func expandServicePrincipalEntitlement(d *schema.ResourceData) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { + origin := d.Get("origin").(string) + originID := d.Get("origin_id").(string) + displayName := d.Get("display_name").(string) + + accountLicenseType, err := converter.AccountLicenseType(d.Get("account_license_type").(string)) + if err != nil { + return nil, err + } + licensingSource, err := converter.AccountLicensingSource(d.Get("licensing_source").(string)) + if err != nil { + return nil, err + } + + return &memberentitlementmanagement.ServicePrincipalEntitlement{ + AccessLevel: &licensing.AccessLevel{ + AccountLicenseType: accountLicenseType, + LicensingSource: licensingSource, + }, + + ServicePrincipal: &graph.GraphServicePrincipal{ + Origin: &origin, + OriginId: &originID, + DisplayName: &displayName, + SubjectKind: converter.String("servicePrincipal"), + }, + }, nil +} + +func addServicePrincipalEntitlement(clients *client.AggregatedClient, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { + servicePrincipalEntitlementsPostResponse, err := clients.MemberEntitleManagementClient.AddServicePrincipalEntitlement(clients.Ctx, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ + ServicePrincipalEntitlement: servicePrincipalEntitlement, + }) + + if err != nil { + return nil, err + } + + if !*servicePrincipalEntitlementsPostResponse.IsSuccess { + opResults := []memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{} + if servicePrincipalEntitlementsPostResponse.OperationResult != nil { + opResults = append(opResults, *servicePrincipalEntitlementsPostResponse.OperationResult) + } + return nil, fmt.Errorf("Adding service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(&opResults)) + } + + return servicePrincipalEntitlementsPostResponse.ServicePrincipalEntitlement, nil +} + func getServicePrincipalEntitlementAPIErrorMessage(operationResults *[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult) string { errMsg := "Unknown API error" if operationResults != nil && len(*operationResults) > 0 { @@ -374,12 +353,3 @@ func getServicePrincipalEntitlementAPIErrorMessage(operationResults *[]memberent } return errMsg } - -func isServicePrincipalDeleted(servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) bool { - if servicePrincipalEntitlement == nil { - return true - } - - return *servicePrincipalEntitlement.AccessLevel.Status == accounts.AccountUserStatusValues.Deleted || - *servicePrincipalEntitlement.AccessLevel.Status == accounts.AccountUserStatusValues.None -} From 8f878c9991154f530c6aefb50510ba9e6986b411 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 16:40:19 +0200 Subject: [PATCH 05/20] Remove name as API throws if used --- .../resource_service_principal_entitlement.go | 50 +-- ...urce_service_principal_entitlement_test.go | 334 ++++++------------ 2 files changed, 127 insertions(+), 257 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 431062102..97812f752 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -22,7 +22,7 @@ import ( var ( spConfigurationKeys = []string{ - "display_name", + "origin", "origin_id", } ) @@ -37,37 +37,21 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { State: importServicePrincipalEntitlement, }, Schema: map[string]*schema.Schema{ - "principal_name": { - Type: schema.TypeString, - Computed: true, - }, - "display_name": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Computed: true, - ConflictsWith: []string{"origin_id", "origin"}, - ExactlyOneOf: spConfigurationKeys, - ValidateFunc: validation.StringIsNotWhiteSpace, - }, "origin_id": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"display_name"}, - RequiredWith: []string{"origin"}, - ExactlyOneOf: spConfigurationKeys, - ValidateFunc: validation.StringIsNotWhiteSpace, + Type: schema.TypeString, + Optional: false, + Computed: false, + ForceNew: true, + ExactlyOneOf: spConfigurationKeys, + ValidateFunc: validation.StringIsNotWhiteSpace, }, "origin": { - Type: schema.TypeString, - Optional: true, - Computed: true, - ForceNew: true, - ConflictsWith: []string{"display_name"}, - RequiredWith: []string{"origin_id"}, - ValidateFunc: validation.StringIsNotWhiteSpace, + Type: schema.TypeString, + Optional: false, + Computed: false, + ForceNew: true, + ExactlyOneOf: spConfigurationKeys, + ValidateFunc: validation.StringIsNotWhiteSpace, }, "account_license_type": { Type: schema.TypeString, @@ -264,11 +248,7 @@ func flattenServicePrincipalEntitlement(d *schema.ResourceData, servicePrincipal d.SetId(servicePrincipalEntitlement.Id.String()) d.Set("descriptor", *servicePrincipalEntitlement.ServicePrincipal.Descriptor) d.Set("origin", *servicePrincipalEntitlement.ServicePrincipal.Origin) - d.Set("principal_name", *servicePrincipalEntitlement.ServicePrincipal.PrincipalName) - if servicePrincipalEntitlement.ServicePrincipal.OriginId != nil { - d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) - } - d.Set("display_name", *servicePrincipalEntitlement.ServicePrincipal.DisplayName) + d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) d.Set("account_license_type", string(*servicePrincipalEntitlement.AccessLevel.AccountLicenseType)) d.Set("licensing_source", *servicePrincipalEntitlement.AccessLevel.LicensingSource) } @@ -276,7 +256,6 @@ func flattenServicePrincipalEntitlement(d *schema.ResourceData, servicePrincipal func expandServicePrincipalEntitlement(d *schema.ResourceData) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { origin := d.Get("origin").(string) originID := d.Get("origin_id").(string) - displayName := d.Get("display_name").(string) accountLicenseType, err := converter.AccountLicenseType(d.Get("account_license_type").(string)) if err != nil { @@ -296,7 +275,6 @@ func expandServicePrincipalEntitlement(d *schema.ResourceData) (*memberentitleme ServicePrincipal: &graph.GraphServicePrincipal{ Origin: &origin, OriginId: &originID, - DisplayName: &displayName, SubjectKind: converter.String("servicePrincipal"), }, }, nil diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go index 7bdd6a171..6a69ce5a9 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go @@ -14,7 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/microsoft/azure-devops-go-api/azuredevops/v7" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph" - "github.com/microsoft/azure-devops-go-api/azuredevops/v7/identity" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/licensing" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/memberentitlementmanagement" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" @@ -25,93 +24,7 @@ import ( "github.com/stretchr/testify/require" ) -// if origin_id is provided, it will be used. if principal_name is also supplied, an error will be reported. -func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_DoNotAllowToSetOridinIdAndPrincipalName(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - clients := &client.AggregatedClient{ - MemberEntitleManagementClient: nil, - Ctx: context.Background(), - } - - originID := "e97b0e7f-0a61-41ad-860c-748ec5fcb20b" - principalName := "foobar@microsoft.com" - - resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("origin_id", originID) - resourceData.Set("principal_name", principalName) - - err := resourceServicePrincipalEntitlementCreate(resourceData, clients) - assert.NotNil(t, err, "err should not be nil") - require.Regexp(t, "Both origin_id and principal_name set. You can not use both", err.Error()) -} - -// if origin_id is "" and principal_name is supplied, the principal_name will be used. -func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithPrincipalName(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - memberEntitlementClient := azdosdkmocks.NewMockMemberentitlementmanagementClient(ctrl) - clients := &client.AggregatedClient{ - MemberEntitleManagementClient: memberEntitlementClient, - Ctx: context.Background(), - } - - accountLicenseType := licensing.AccountLicenseTypeValues.Express - origin := "" - originID := "" - principalName := "foobar@microsoft.com" - descriptor := "baz" - id := uuid.New() - mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, descriptor) - - resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("principal_name", principalName) - - expectedIsSuccess := true - memberEntitlementClient. - EXPECT(). - AddServicePrincipalEntitlement(gomock.Any(), MatchAddServicePrincipalEntitlementArgs(t, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ - ServicePrincipalEntitlement: mockServicePrincipalEntitlement, - })). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalEntitlement: mockServicePrincipalEntitlement, - }, nil). - Times(1) - - memberEntitlementClient. - EXPECT(). - GetServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ - ServicePrincipalId: mockServicePrincipalEntitlement.Id, - }). - Return(mockServicePrincipalEntitlement, nil) - - err := resourceServicePrincipalEntitlementCreate(resourceData, clients) - assert.Nil(t, err, "err should not be nil") -} - -// if origin_id is "" and principal_name is "", an error will be reported. -func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_Need_OriginID_Or_PrincipalName(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - clients := &client.AggregatedClient{ - MemberEntitleManagementClient: nil, - Ctx: context.Background(), - } - - resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - // originID and principalName is not set. - - err := resourceServicePrincipalEntitlementCreate(resourceData, clients) - assert.NotNil(t, err, "err should not be nil") - require.Regexp(t, "Use origin_id or principal_name", err.Error()) -} - // if the REST-API return the failure, it should fail. - func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() @@ -122,12 +35,13 @@ func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithError Ctx: context.Background(), } - principalName := "foobar@microsoft.com" + originId := "837934ba-f473-45d8-ab55-83e081171cad" + origin := "aad" resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - // resourceData.Set("origin_id", originID) resourceData.Set("account_license_type", "express") - resourceData.Set("principal_name", principalName) + resourceData.Set("origin_id", originId) + resourceData.Set("origin", origin) // No error but it has a error on the response. memberEntitlementClient. @@ -151,15 +65,16 @@ func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithEarly Ctx: context.Background(), } - principalName := "foobar@microsoft.com" + originId := "837934ba-f473-45d8-ab55-83e081171cad" + origin := "aad" resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - // resourceData.Set("origin_id", originID) resourceData.Set("account_license_type", "earlyAdopter") - resourceData.Set("principal_name", principalName) + resourceData.Set("origin_id", originId) + resourceData.Set("origin", origin) var expectedKey interface{} = 5000 - var expectedValue interface{} = "A user cannot be assigned an Account-EarlyAdopter license." + var expectedValue interface{} = "A service principal cannot be assigned an Account-EarlyAdopter license." expectedErrors := []azuredevops.KeyValuePair{ { Key: &expectedKey, @@ -167,22 +82,22 @@ func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithEarly }, } expectedIsSuccess := false + operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Errors: &expectedErrors, + } - // No error but it has a error on the response. + // No error but it has an error on the response. memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ - IsSuccess: &expectedIsSuccess, - OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - Errors: &expectedErrors, - }, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ + Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, }, nil). Times(1) err := resourceServicePrincipalEntitlementCreate(resourceData, clients) - require.Contains(t, err.Error(), "A user cannot be assigned an Account-EarlyAdopter license.") + require.Contains(t, err.Error(), "A service principal cannot be assigned an Account-EarlyAdopter license.") } // TestServicePrincipalEntitlement_Update_TestChangeEntitlement verfies that an entitlement can be changed @@ -199,11 +114,16 @@ func TestServicePrincipalEntitlement_Update_TestChangeEntitlement(t *testing.T) accountLicenseType := licensing.AccountLicenseTypeValues.Stakeholder origin := "" originID := "" - principalName := "foobar@microsoft.com" + principalName := "[contoso]\\PrincipalName" + displayName := "displayName" descriptor := "baz" id := uuid.New() - mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, descriptor) + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, displayName, descriptor) expectedIsSuccess := true + operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Result: mockServicePrincipalEntitlement, + } memberEntitlementClient. EXPECT(). @@ -224,9 +144,8 @@ func TestServicePrincipalEntitlement_Update_TestChangeEntitlement(t *testing.T) }, }, }). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalEntitlement: mockServicePrincipalEntitlement, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ + Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, }, nil). Times(1) @@ -240,7 +159,7 @@ func TestServicePrincipalEntitlement_Update_TestChangeEntitlement(t *testing.T) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) resourceData.SetId(id.String()) - resourceData.Set("principal_name", principalName) + resourceData.Set("displayName", displayName) resourceData.Set("account_license_type", string(licensing.AccountLicenseTypeValues.Stakeholder)) resourceData.Set("licensing_source", string(licensing.LicensingSourceValues.Account)) @@ -262,20 +181,24 @@ func TestServicePrincipalEntitlement_CreateUpdate_TestBasicEntitlement(t *testin accountLicenseType := licensing.AccountLicenseTypeValues.Express origin := "" originID := "" - principalName := "foobar@microsoft.com" + principalName := "[contoso]\\PrinicipalName" + displayName := "displayName" descriptor := "baz" id := uuid.New() - mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, descriptor) + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, displayName, descriptor) expectedIsSuccess := true + operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Result: mockServicePrincipalEntitlement, + } memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), MatchAddServicePrincipalEntitlementArgs(t, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ ServicePrincipalEntitlement: mockServicePrincipalEntitlement, })). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalEntitlement: mockServicePrincipalEntitlement, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ + Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, }, nil). Times(1) @@ -287,47 +210,13 @@ func TestServicePrincipalEntitlement_CreateUpdate_TestBasicEntitlement(t *testin Return(mockServicePrincipalEntitlement, nil) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("principal_name", principalName) + resourceData.Set("display_name", displayName) resourceData.Set("account_license_type", "basic") err := resourceServicePrincipalEntitlementCreate(resourceData, clients) assert.Nil(t, err, "err should be nil") } -// TestServicePrincipalEntitlement_Import_TestUPN tests if import is successful using an UPN -func TestServicePrincipalEntitlement_Import_TestUPN(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - identityClient := azdosdkmocks.NewMockIdentityClient(ctrl) - clients := &client.AggregatedClient{ - IdentityClient: identityClient, - Ctx: context.Background(), - } - - principalName := "foobar@microsoft.com" - id := uuid.New() - - identityClient. - EXPECT(). - ReadIdentities(gomock.Any(), gomock.Any()). - Return(&[]identity.Identity{ - { - Id: &id, - }, - }, nil). - Times(1) - - resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.SetId(principalName) - - d, err := importServicePrincipalEntitlement(resourceData, clients) - assert.Nil(t, err) - assert.NotNil(t, d) - assert.Len(t, d, 1) - assert.Equal(t, id.String(), d[0].Id()) -} - // TestServicePrincipalEntitlement_Import_TestID tests if import is successful using an UUID func TestServicePrincipalEntitlement_Import_TestID(t *testing.T) { ctrl := gomock.NewController(t) @@ -339,15 +228,23 @@ func TestServicePrincipalEntitlement_Import_TestID(t *testing.T) { Ctx: context.Background(), } - id := uuid.New().String() + id := uuid.New() resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.SetId(id) + resourceData.SetId(id.String()) + + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, "", "", "", "", "", "") + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: mockServicePrincipalEntitlement.Id, + }). + Return(mockServicePrincipalEntitlement, nil) d, err := importServicePrincipalEntitlement(resourceData, clients) assert.Nil(t, err) assert.NotNil(t, d) assert.Len(t, d, 1) - assert.Equal(t, id, d[0].Id()) + assert.Equal(t, id.String(), d[0].Id()) } // TestServicePrincipalEntitlement_Import_TestInvalidValue tests if only a valid UPN and UUID can be used to import a resource @@ -368,7 +265,7 @@ func TestServicePrincipalEntitlement_Import_TestInvalidValue(t *testing.T) { d, err := importServicePrincipalEntitlement(resourceData, clients) assert.Nil(t, d) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Only UUID and UPN values can used for import") + assert.Contains(t, err.Error(), "Only UUID values can used for import") } func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { @@ -388,27 +285,27 @@ func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { k2 := interface{}("9998") v2 := interface{}("Error2") + operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalId: &id, + Errors: &[]azuredevops.KeyValuePair{ + { + Key: &k1, + Value: &v1, + }, + { + Key: &k2, + Value: &v2, + }, + }, + Result: nil, + } + memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalEntitlement: nil, - OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - Result: nil, - ServicePrincipalId: &id, - Errors: &[]azuredevops.KeyValuePair{ - { - Key: &k1, - Value: &v1, - }, - { - Key: &k2, - Value: &v2, - }, - }, - }, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ + Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, }, nil). Times(1) @@ -419,7 +316,7 @@ func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { Times(0) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("principal_name", "foobar@microsoft.com") + resourceData.Set("principal_name", "[contoso]\\Test") err := resourceServicePrincipalEntitlementCreate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") @@ -439,19 +336,18 @@ func TestServicePrincipalEntitlement_Create_TestEmptyErrors(t *testing.T) { id, _ := uuid.NewUUID() expectedIsSuccess := false + operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalId: &id, + Errors: nil, + Result: nil, + } memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalEntitlement: nil, - OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - Result: nil, - ServicePrincipalId: &id, - Errors: nil, - }, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ + Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, }, nil). Times(1) @@ -462,7 +358,7 @@ func TestServicePrincipalEntitlement_Create_TestEmptyErrors(t *testing.T) { Times(0) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("principal_name", "foobar@microsoft.com") + resourceData.Set("principal_name", "[contoso]\\PrincipalName") err := resourceServicePrincipalEntitlementCreate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") @@ -486,29 +382,27 @@ func TestServicePrincipalEntitlement_Update_TestErrorFormatting(t *testing.T) { k2 := interface{}("9998") v2 := interface{}("Error2") + operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalId: &id, + Errors: &[]azuredevops.KeyValuePair{ + { + Key: &k1, + Value: &v1, + }, + { + Key: &k2, + Value: &v2, + }, + }, + Result: nil, + } + memberEntitlementClient. EXPECT(). UpdateServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalEntitlement: nil, - OperationResults: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - { - IsSuccess: &expectedIsSuccess, - Result: nil, - ServicePrincipalId: &id, - Errors: &[]azuredevops.KeyValuePair{ - { - Key: &k1, - Value: &v1, - }, - { - Key: &k2, - Value: &v2, - }, - }, - }, - }, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ + Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, }, nil). Times(1) @@ -520,7 +414,7 @@ func TestServicePrincipalEntitlement_Update_TestErrorFormatting(t *testing.T) { resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) resourceData.SetId(id.String()) - resourceData.Set("principal_name", "foobar@microsoft.com") + resourceData.Set("principal_name", "[contoso]\\PrincipalName") err := resourceServicePrincipalEntitlementUpdate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") @@ -540,21 +434,18 @@ func TestServicePrincipalEntitlement_Update_TestEmptyErrors(t *testing.T) { id, _ := uuid.NewUUID() expectedIsSuccess := false + operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Errors: nil, + Result: nil, + ServicePrincipalId: &id, + } memberEntitlementClient. EXPECT(). UpdateServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalEntitlement: nil, - OperationResults: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - { - IsSuccess: &expectedIsSuccess, - Result: nil, - ServicePrincipalId: &id, - Errors: nil, - }, - }, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ + Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, }, nil). Times(1) @@ -566,14 +457,14 @@ func TestServicePrincipalEntitlement_Update_TestEmptyErrors(t *testing.T) { resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) resourceData.SetId(id.String()) - resourceData.Set("principal_name", "foobar@microsoft.com") + resourceData.Set("principal_name", "[contoso]\\PrincipalName") err := resourceServicePrincipalEntitlementUpdate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") assert.Contains(t, err.Error(), "Unknown API error") } -func getMockServicePrincipalEntitlement(id *uuid.UUID, accountLicenseType licensing.AccountLicenseType, origin string, originID string, principalName string, descriptor string) *memberentitlementmanagement.ServicePrincipalEntitlement { +func getMockServicePrincipalEntitlement(id *uuid.UUID, accountLicenseType licensing.AccountLicenseType, origin string, originID string, principalName string, displayName string, descriptor string) *memberentitlementmanagement.ServicePrincipalEntitlement { subjectKind := "servicePrincipal" licensingSource := licensing.LicensingSourceValues.Account @@ -587,6 +478,7 @@ func getMockServicePrincipalEntitlement(id *uuid.UUID, accountLicenseType licens Origin: &origin, OriginId: &originID, PrincipalName: &principalName, + DisplayName: &displayName, SubjectKind: &subjectKind, Descriptor: &descriptor, }, @@ -604,29 +496,29 @@ func MatchAddServicePrincipalEntitlementArgs(t *testing.T, x memberentitlementma func (m *matchAddServicePrincipalEntitlementArgs) Matches(x interface{}) bool { args := x.(memberentitlementmanagement.AddServicePrincipalEntitlementArgs) - m.t.Logf("MatchAddServicePrincipalEntitlementArgs:\nVALUE: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], principal_name: [%s]\n REF: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], principal_name: [%s]\n", + m.t.Logf("MatchAddServicePrincipalEntitlementArgs:\nVALUE: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], display_name: [%s]\n REF: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], display_name: [%s], principal_name: [%s]\n", *args.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, *args.ServicePrincipalEntitlement.AccessLevel.LicensingSource, *args.ServicePrincipalEntitlement.ServicePrincipal.Origin, *args.ServicePrincipalEntitlement.ServicePrincipal.OriginId, - *args.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName, + *args.ServicePrincipalEntitlement.ServicePrincipal.DisplayName, *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, *m.x.ServicePrincipalEntitlement.AccessLevel.LicensingSource, *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin, *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId, + *m.x.ServicePrincipalEntitlement.ServicePrincipal.DisplayName, *m.x.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName) return *args.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType == *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType && *args.ServicePrincipalEntitlement.ServicePrincipal.Origin == *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin && - *args.ServicePrincipalEntitlement.ServicePrincipal.OriginId == *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId && - *args.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName == *m.x.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName + *args.ServicePrincipalEntitlement.ServicePrincipal.OriginId == *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId } func (m *matchAddServicePrincipalEntitlementArgs) String() string { - return fmt.Sprintf("account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], principal_name: [%s]", + return fmt.Sprintf("account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], display_name: [%s]", *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, *m.x.ServicePrincipalEntitlement.AccessLevel.LicensingSource, *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin, *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId, - *m.x.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName) + *m.x.ServicePrincipalEntitlement.ServicePrincipal.DisplayName) } From 29f937db4f280cecb13d41e069d45883d5bc6654 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 16:53:42 +0200 Subject: [PATCH 06/20] Set origin to aad as default --- .../resource_service_principal_entitlement.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 97812f752..c94ae7b7d 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -39,7 +39,7 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { Schema: map[string]*schema.Schema{ "origin_id": { Type: schema.TypeString, - Optional: false, + Required: true, Computed: false, ForceNew: true, ExactlyOneOf: spConfigurationKeys, @@ -47,9 +47,10 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { }, "origin": { Type: schema.TypeString, - Optional: false, - Computed: false, + Optional: true, + Computed: true, ForceNew: true, + Default: string("aad"), ExactlyOneOf: spConfigurationKeys, ValidateFunc: validation.StringIsNotWhiteSpace, }, From 009704565118c8458ef40976a85d1337f0932171 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 17:13:02 +0200 Subject: [PATCH 07/20] Update tests Remove unused list of keys --- .../resource_service_principal_entitlement.go | 10 - ...urce_service_principal_entitlement_test.go | 257 +++++++++--------- ...ervice_principal_entitlement.html.markdown | 9 +- 3 files changed, 138 insertions(+), 138 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index c94ae7b7d..481b792d8 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -20,13 +20,6 @@ import ( "github.com/microsoft/terraform-provider-azuredevops/azuredevops/internal/utils/suppress" ) -var ( - spConfigurationKeys = []string{ - "origin", - "origin_id", - } -) - func ResourceServicePrincipalEntitlement() *schema.Resource { return &schema.Resource{ Create: resourceServicePrincipalEntitlementCreate, @@ -40,9 +33,7 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { "origin_id": { Type: schema.TypeString, Required: true, - Computed: false, ForceNew: true, - ExactlyOneOf: spConfigurationKeys, ValidateFunc: validation.StringIsNotWhiteSpace, }, "origin": { @@ -51,7 +42,6 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { Computed: true, ForceNew: true, Default: string("aad"), - ExactlyOneOf: spConfigurationKeys, ValidateFunc: validation.StringIsNotWhiteSpace, }, "account_license_type": { diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go index 6a69ce5a9..9bbf70a30 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go @@ -14,6 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/microsoft/azure-devops-go-api/azuredevops/v7" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph" + "github.com/microsoft/azure-devops-go-api/azuredevops/v7/identity" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/licensing" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/memberentitlementmanagement" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" @@ -35,13 +36,11 @@ func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithError Ctx: context.Background(), } - originId := "837934ba-f473-45d8-ab55-83e081171cad" - origin := "aad" + originID := "b83bb2a9-ebac-4afc-ba71-89902540d16c" resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.Set("origin_id", originID) resourceData.Set("account_license_type", "express") - resourceData.Set("origin_id", originId) - resourceData.Set("origin", origin) // No error but it has a error on the response. memberEntitlementClient. @@ -65,13 +64,11 @@ func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithEarly Ctx: context.Background(), } - originId := "837934ba-f473-45d8-ab55-83e081171cad" - origin := "aad" + originID := "b83bb2a9-ebac-4afc-ba71-89902540d16c" resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.Set("origin_id", originID) resourceData.Set("account_license_type", "earlyAdopter") - resourceData.Set("origin_id", originId) - resourceData.Set("origin", origin) var expectedKey interface{} = 5000 var expectedValue interface{} = "A service principal cannot be assigned an Account-EarlyAdopter license." @@ -82,17 +79,17 @@ func TestServicePrincipalEntitlement_CreateServicePrincipalEntitlement_WithEarly }, } expectedIsSuccess := false - operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - Errors: &expectedErrors, - } - // No error but it has an error on the response. + // No error but it has a error on the response. memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ - Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Errors: &expectedErrors, + }, }, nil). Times(1) @@ -112,18 +109,12 @@ func TestServicePrincipalEntitlement_Update_TestChangeEntitlement(t *testing.T) } accountLicenseType := licensing.AccountLicenseTypeValues.Stakeholder - origin := "" - originID := "" - principalName := "[contoso]\\PrincipalName" - displayName := "displayName" + origin := "aad" + originID := "b83bb2a9-ebac-4afc-ba71-89902540d16c" descriptor := "baz" id := uuid.New() - mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, displayName, descriptor) + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, descriptor) expectedIsSuccess := true - operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - Result: mockServicePrincipalEntitlement, - } memberEntitlementClient. EXPECT(). @@ -144,8 +135,9 @@ func TestServicePrincipalEntitlement_Update_TestChangeEntitlement(t *testing.T) }, }, }). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ - Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: mockServicePrincipalEntitlement, }, nil). Times(1) @@ -159,7 +151,7 @@ func TestServicePrincipalEntitlement_Update_TestChangeEntitlement(t *testing.T) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) resourceData.SetId(id.String()) - resourceData.Set("displayName", displayName) + resourceData.Set("origin_id", originID) resourceData.Set("account_license_type", string(licensing.AccountLicenseTypeValues.Stakeholder)) resourceData.Set("licensing_source", string(licensing.LicensingSourceValues.Account)) @@ -179,26 +171,21 @@ func TestServicePrincipalEntitlement_CreateUpdate_TestBasicEntitlement(t *testin } accountLicenseType := licensing.AccountLicenseTypeValues.Express - origin := "" - originID := "" - principalName := "[contoso]\\PrinicipalName" - displayName := "displayName" + origin := "aad" + originID := "b83bb2a9-ebac-4afc-ba71-89902540d16c" descriptor := "baz" id := uuid.New() - mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, principalName, displayName, descriptor) + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, accountLicenseType, origin, originID, descriptor) expectedIsSuccess := true - operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - Result: mockServicePrincipalEntitlement, - } memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), MatchAddServicePrincipalEntitlementArgs(t, memberentitlementmanagement.AddServicePrincipalEntitlementArgs{ ServicePrincipalEntitlement: mockServicePrincipalEntitlement, })). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ - Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: mockServicePrincipalEntitlement, }, nil). Times(1) @@ -210,13 +197,47 @@ func TestServicePrincipalEntitlement_CreateUpdate_TestBasicEntitlement(t *testin Return(mockServicePrincipalEntitlement, nil) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("display_name", displayName) + resourceData.Set("origin_id", originID) resourceData.Set("account_license_type", "basic") err := resourceServicePrincipalEntitlementCreate(resourceData, clients) assert.Nil(t, err, "err should be nil") } +// TestServicePrincipalEntitlement_Import_TestUPN tests if import is successful using an UPN +func TestServicePrincipalEntitlement_Import_TestUPN(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + identityClient := azdosdkmocks.NewMockIdentityClient(ctrl) + clients := &client.AggregatedClient{ + IdentityClient: identityClient, + Ctx: context.Background(), + } + + originID := "b83bb2a9-ebac-4afc-ba71-89902540d16c" + id := uuid.New() + + identityClient. + EXPECT(). + ReadIdentities(gomock.Any(), gomock.Any()). + Return(&[]identity.Identity{ + { + Id: &id, + }, + }, nil). + Times(1) + + resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) + resourceData.SetId(originID) + + d, err := importServicePrincipalEntitlement(resourceData, clients) + assert.Nil(t, err) + assert.NotNil(t, d) + assert.Len(t, d, 1) + assert.Equal(t, id.String(), d[0].Id()) +} + // TestServicePrincipalEntitlement_Import_TestID tests if import is successful using an UUID func TestServicePrincipalEntitlement_Import_TestID(t *testing.T) { ctrl := gomock.NewController(t) @@ -228,23 +249,15 @@ func TestServicePrincipalEntitlement_Import_TestID(t *testing.T) { Ctx: context.Background(), } - id := uuid.New() + id := uuid.New().String() resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.SetId(id.String()) - - mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, "", "", "", "", "", "") - memberEntitlementClient. - EXPECT(). - GetServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ - ServicePrincipalId: mockServicePrincipalEntitlement.Id, - }). - Return(mockServicePrincipalEntitlement, nil) + resourceData.SetId(id) d, err := importServicePrincipalEntitlement(resourceData, clients) assert.Nil(t, err) assert.NotNil(t, d) assert.Len(t, d, 1) - assert.Equal(t, id.String(), d[0].Id()) + assert.Equal(t, id, d[0].Id()) } // TestServicePrincipalEntitlement_Import_TestInvalidValue tests if only a valid UPN and UUID can be used to import a resource @@ -265,7 +278,7 @@ func TestServicePrincipalEntitlement_Import_TestInvalidValue(t *testing.T) { d, err := importServicePrincipalEntitlement(resourceData, clients) assert.Nil(t, d) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Only UUID values can used for import") + assert.Contains(t, err.Error(), "Only UUID and UPN values can used for import") } func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { @@ -285,27 +298,27 @@ func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { k2 := interface{}("9998") v2 := interface{}("Error2") - operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalId: &id, - Errors: &[]azuredevops.KeyValuePair{ - { - Key: &k1, - Value: &v1, - }, - { - Key: &k2, - Value: &v2, - }, - }, - Result: nil, - } - memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ - Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: &[]azuredevops.KeyValuePair{ + { + Key: &k1, + Value: &v1, + }, + { + Key: &k2, + Value: &v2, + }, + }, + }, }, nil). Times(1) @@ -316,7 +329,7 @@ func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { Times(0) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("principal_name", "[contoso]\\Test") + resourceData.Set("origin_id", "b83bb2a9-ebac-4afc-ba71-89902540d16c") err := resourceServicePrincipalEntitlementCreate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") @@ -336,18 +349,19 @@ func TestServicePrincipalEntitlement_Create_TestEmptyErrors(t *testing.T) { id, _ := uuid.NewUUID() expectedIsSuccess := false - operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalId: &id, - Errors: nil, - Result: nil, - } memberEntitlementClient. EXPECT(). AddServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ - Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPostResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResult: &memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: nil, + }, }, nil). Times(1) @@ -358,7 +372,7 @@ func TestServicePrincipalEntitlement_Create_TestEmptyErrors(t *testing.T) { Times(0) resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.Set("principal_name", "[contoso]\\PrincipalName") + resourceData.Set("origin_id", "b83bb2a9-ebac-4afc-ba71-89902540d16c") err := resourceServicePrincipalEntitlementCreate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") @@ -382,27 +396,29 @@ func TestServicePrincipalEntitlement_Update_TestErrorFormatting(t *testing.T) { k2 := interface{}("9998") v2 := interface{}("Error2") - operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - ServicePrincipalId: &id, - Errors: &[]azuredevops.KeyValuePair{ - { - Key: &k1, - Value: &v1, - }, - { - Key: &k2, - Value: &v2, - }, - }, - Result: nil, - } - memberEntitlementClient. EXPECT(). UpdateServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ - Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResults: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + { + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: &[]azuredevops.KeyValuePair{ + { + Key: &k1, + Value: &v1, + }, + { + Key: &k2, + Value: &v2, + }, + }, + }, + }, }, nil). Times(1) @@ -414,7 +430,7 @@ func TestServicePrincipalEntitlement_Update_TestErrorFormatting(t *testing.T) { resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) resourceData.SetId(id.String()) - resourceData.Set("principal_name", "[contoso]\\PrincipalName") + resourceData.Set("origin_id", "b83bb2a9-ebac-4afc-ba71-89902540d16c") err := resourceServicePrincipalEntitlementUpdate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") @@ -434,18 +450,21 @@ func TestServicePrincipalEntitlement_Update_TestEmptyErrors(t *testing.T) { id, _ := uuid.NewUUID() expectedIsSuccess := false - operationResult := memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ - IsSuccess: &expectedIsSuccess, - Errors: nil, - Result: nil, - ServicePrincipalId: &id, - } memberEntitlementClient. EXPECT(). UpdateServicePrincipalEntitlement(gomock.Any(), gomock.Any()). - Return(&memberentitlementmanagement.ServicePrincipalEntitlementOperationReference{ - Results: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{operationResult}, + Return(&memberentitlementmanagement.ServicePrincipalEntitlementsPatchResponse{ + IsSuccess: &expectedIsSuccess, + ServicePrincipalEntitlement: nil, + OperationResults: &[]memberentitlementmanagement.ServicePrincipalEntitlementOperationResult{ + { + IsSuccess: &expectedIsSuccess, + Result: nil, + ServicePrincipalId: &id, + Errors: nil, + }, + }, }, nil). Times(1) @@ -457,14 +476,14 @@ func TestServicePrincipalEntitlement_Update_TestEmptyErrors(t *testing.T) { resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) resourceData.SetId(id.String()) - resourceData.Set("principal_name", "[contoso]\\PrincipalName") + resourceData.Set("origin_id", "b83bb2a9-ebac-4afc-ba71-89902540d16c") err := resourceServicePrincipalEntitlementUpdate(resourceData, clients) assert.NotNil(t, err, "err should not be nil") assert.Contains(t, err.Error(), "Unknown API error") } -func getMockServicePrincipalEntitlement(id *uuid.UUID, accountLicenseType licensing.AccountLicenseType, origin string, originID string, principalName string, displayName string, descriptor string) *memberentitlementmanagement.ServicePrincipalEntitlement { +func getMockServicePrincipalEntitlement(id *uuid.UUID, accountLicenseType licensing.AccountLicenseType, origin string, originID string, descriptor string) *memberentitlementmanagement.ServicePrincipalEntitlement { subjectKind := "servicePrincipal" licensingSource := licensing.LicensingSourceValues.Account @@ -475,12 +494,10 @@ func getMockServicePrincipalEntitlement(id *uuid.UUID, accountLicenseType licens }, Id: id, ServicePrincipal: &graph.GraphServicePrincipal{ - Origin: &origin, - OriginId: &originID, - PrincipalName: &principalName, - DisplayName: &displayName, - SubjectKind: &subjectKind, - Descriptor: &descriptor, + Origin: &origin, + OriginId: &originID, + SubjectKind: &subjectKind, + Descriptor: &descriptor, }, } } @@ -496,18 +513,15 @@ func MatchAddServicePrincipalEntitlementArgs(t *testing.T, x memberentitlementma func (m *matchAddServicePrincipalEntitlementArgs) Matches(x interface{}) bool { args := x.(memberentitlementmanagement.AddServicePrincipalEntitlementArgs) - m.t.Logf("MatchAddServicePrincipalEntitlementArgs:\nVALUE: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], display_name: [%s]\n REF: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], display_name: [%s], principal_name: [%s]\n", + m.t.Logf("MatchAddServicePrincipalEntitlementArgs:\nVALUE: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s]\n REF: account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s]\n", *args.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, *args.ServicePrincipalEntitlement.AccessLevel.LicensingSource, *args.ServicePrincipalEntitlement.ServicePrincipal.Origin, *args.ServicePrincipalEntitlement.ServicePrincipal.OriginId, - *args.ServicePrincipalEntitlement.ServicePrincipal.DisplayName, *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, *m.x.ServicePrincipalEntitlement.AccessLevel.LicensingSource, *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin, - *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId, - *m.x.ServicePrincipalEntitlement.ServicePrincipal.DisplayName, - *m.x.ServicePrincipalEntitlement.ServicePrincipal.PrincipalName) + *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId) return *args.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType == *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType && *args.ServicePrincipalEntitlement.ServicePrincipal.Origin == *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin && @@ -515,10 +529,9 @@ func (m *matchAddServicePrincipalEntitlementArgs) Matches(x interface{}) bool { } func (m *matchAddServicePrincipalEntitlementArgs) String() string { - return fmt.Sprintf("account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s], display_name: [%s]", + return fmt.Sprintf("account_license_type: [%s], licensing_source: [%s], origin: [%s], origin_id: [%s]", *m.x.ServicePrincipalEntitlement.AccessLevel.AccountLicenseType, *m.x.ServicePrincipalEntitlement.AccessLevel.LicensingSource, *m.x.ServicePrincipalEntitlement.ServicePrincipal.Origin, - *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId, - *m.x.ServicePrincipalEntitlement.ServicePrincipal.DisplayName) + *m.x.ServicePrincipalEntitlement.ServicePrincipal.OriginId) } diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown index 74629b31f..7920c63f8 100644 --- a/website/docs/r/service_principal_entitlement.html.markdown +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -13,20 +13,17 @@ Manages a service principal entitlement within Azure DevOps. ```hcl resource "azuredevops_service_principal_entitlement" "example" { - principal_name = "foo@contoso.com" + origin_id = "90c9f86a-8ffb-4ff4-bae5-729100074a0c" } ``` ## Argument Reference -- `principal_name` - (Optional) The principal name is the PrincipalName of a graph member from the source provider. Usually, e-mail address. -- `origin_id` - (Optional) The unique identifier from the system of origin. Typically a sid, object id or Guid. e.g. Used for member of other tenant on Azure Active Directory. -- `origin` - (Optional) The type of source provider for the origin identifier. +- `origin_id` - (Required) The object ID of the enterprise application. +- `origin` - (Optional) The type of source provider for the origin identifier. Defaults to `aad`. - `account_license_type` - (Optional) Type of Account License. Valid values: `advanced`, `earlyAdopter`, `express`, `none`, `professional`, or `stakeholder`. Defaults to `express`. In addition the value `basic` is allowed which is an alias for `express` and reflects the name of the `express` license used in the Azure DevOps web interface. - `licensing_source` - (Optional) The source of the licensing (e.g. Account. MSDN etc.) Valid values: `account` (Default), `auto`, `msdn`, `none`, `profile`, `trial` -> **NOTE:** A service principal can only be referenced by it's `principal_name` or by the combination of `origin_id` and `origin`. - ## Attributes Reference The following attributes are exported: From 94fc5d79b39057bbb703a0a9cc547725458b98d3 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 17:24:19 +0200 Subject: [PATCH 08/20] Fix nil pointer --- .../resource_service_principal_entitlement.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 481b792d8..d4f9e57d3 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -206,10 +206,8 @@ func resourceServicePrincipalEntitlementUpdate(d *schema.ResourceData, m interfa return fmt.Errorf("Updating service principal entitlement: %v", err) } - result := *patchResponse.OperationResults - - if !*result[0].IsSuccess { - return fmt.Errorf("Updating service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(&result)) + if !*patchResponse.IsSuccess { + return fmt.Errorf("Updating service principal entitlement: %s", getServicePrincipalEntitlementAPIErrorMessage(patchResponse.OperationResults)) } return resourceServicePrincipalEntitlementRead(d, m) } From 4f3f26f910ec3a89e13ea23492dedf1b18e079b6 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 17:30:31 +0200 Subject: [PATCH 09/20] Update import tests --- ...urce_service_principal_entitlement_test.go | 51 +++++-------------- 1 file changed, 12 insertions(+), 39 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go index 9bbf70a30..bcc87d39e 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement_test.go @@ -14,7 +14,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/microsoft/azure-devops-go-api/azuredevops/v7" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/graph" - "github.com/microsoft/azure-devops-go-api/azuredevops/v7/identity" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/licensing" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/memberentitlementmanagement" "github.com/microsoft/azure-devops-go-api/azuredevops/v7/webapi" @@ -204,40 +203,6 @@ func TestServicePrincipalEntitlement_CreateUpdate_TestBasicEntitlement(t *testin assert.Nil(t, err, "err should be nil") } -// TestServicePrincipalEntitlement_Import_TestUPN tests if import is successful using an UPN -func TestServicePrincipalEntitlement_Import_TestUPN(t *testing.T) { - ctrl := gomock.NewController(t) - defer ctrl.Finish() - - identityClient := azdosdkmocks.NewMockIdentityClient(ctrl) - clients := &client.AggregatedClient{ - IdentityClient: identityClient, - Ctx: context.Background(), - } - - originID := "b83bb2a9-ebac-4afc-ba71-89902540d16c" - id := uuid.New() - - identityClient. - EXPECT(). - ReadIdentities(gomock.Any(), gomock.Any()). - Return(&[]identity.Identity{ - { - Id: &id, - }, - }, nil). - Times(1) - - resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.SetId(originID) - - d, err := importServicePrincipalEntitlement(resourceData, clients) - assert.Nil(t, err) - assert.NotNil(t, d) - assert.Len(t, d, 1) - assert.Equal(t, id.String(), d[0].Id()) -} - // TestServicePrincipalEntitlement_Import_TestID tests if import is successful using an UUID func TestServicePrincipalEntitlement_Import_TestID(t *testing.T) { ctrl := gomock.NewController(t) @@ -249,15 +214,23 @@ func TestServicePrincipalEntitlement_Import_TestID(t *testing.T) { Ctx: context.Background(), } - id := uuid.New().String() + id := uuid.New() resourceData := schema.TestResourceDataRaw(t, ResourceServicePrincipalEntitlement().Schema, nil) - resourceData.SetId(id) + resourceData.SetId(id.String()) + + mockServicePrincipalEntitlement := getMockServicePrincipalEntitlement(&id, "", "", "", "") + memberEntitlementClient. + EXPECT(). + GetServicePrincipalEntitlement(gomock.Any(), memberentitlementmanagement.GetServicePrincipalEntitlementArgs{ + ServicePrincipalId: mockServicePrincipalEntitlement.Id, + }). + Return(mockServicePrincipalEntitlement, nil) d, err := importServicePrincipalEntitlement(resourceData, clients) assert.Nil(t, err) assert.NotNil(t, d) assert.Len(t, d, 1) - assert.Equal(t, id, d[0].Id()) + assert.Equal(t, id.String(), d[0].Id()) } // TestServicePrincipalEntitlement_Import_TestInvalidValue tests if only a valid UPN and UUID can be used to import a resource @@ -278,7 +251,7 @@ func TestServicePrincipalEntitlement_Import_TestInvalidValue(t *testing.T) { d, err := importServicePrincipalEntitlement(resourceData, clients) assert.Nil(t, d) assert.NotNil(t, err) - assert.Contains(t, err.Error(), "Only UUID and UPN values can used for import") + assert.Contains(t, err.Error(), "Only UUID values can used for import") } func TestServicePrincipalEntitlement_Create_TestErrorFormatting(t *testing.T) { From 1e1dba7c0743d1af6eceb4c44346419055e21d00 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 17:35:52 +0200 Subject: [PATCH 10/20] Remove nil value --- .../resource_service_principal_entitlement.go | 1 - 1 file changed, 1 deletion(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index d4f9e57d3..786ffc41c 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -39,7 +39,6 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { "origin": { Type: schema.TypeString, Optional: true, - Computed: true, ForceNew: true, Default: string("aad"), ValidateFunc: validation.StringIsNotWhiteSpace, From f04abe401a2f650beffc3a97951f3b9b624c1d74 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Wed, 17 Apr 2024 17:45:52 +0200 Subject: [PATCH 11/20] Include basic as well --- .../resource_service_principal_entitlement.go | 1 + 1 file changed, 1 insertion(+) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 786ffc41c..5d4959929 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -51,6 +51,7 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { string(licensing.AccountLicenseTypeValues.Advanced), string(licensing.AccountLicenseTypeValues.EarlyAdopter), string(licensing.AccountLicenseTypeValues.Express), + "basic", string(licensing.AccountLicenseTypeValues.None), string(licensing.AccountLicenseTypeValues.Professional), string(licensing.AccountLicenseTypeValues.Stakeholder), From 89326f643f810fdc3a5dfe9d139278df4e3e2b67 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:06:06 +0200 Subject: [PATCH 12/20] Update website/docs/r/service_principal_entitlement.html.markdown Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- website/docs/r/service_principal_entitlement.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown index 7920c63f8..964110b29 100644 --- a/website/docs/r/service_principal_entitlement.html.markdown +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -13,7 +13,7 @@ Manages a service principal entitlement within Azure DevOps. ```hcl resource "azuredevops_service_principal_entitlement" "example" { - origin_id = "90c9f86a-8ffb-4ff4-bae5-729100074a0c" + origin_id = "00000000-0000-0000-0000-000000000001" } ``` From 5142b166f3b541df9361ae7a5aa9eaf999d3889c Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:07:53 +0200 Subject: [PATCH 13/20] Update website/docs/r/service_principal_entitlement.html.markdown Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- website/docs/r/service_principal_entitlement.html.markdown | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown index 964110b29..841f7d8be 100644 --- a/website/docs/r/service_principal_entitlement.html.markdown +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -21,7 +21,12 @@ resource "azuredevops_service_principal_entitlement" "example" { - `origin_id` - (Required) The object ID of the enterprise application. - `origin` - (Optional) The type of source provider for the origin identifier. Defaults to `aad`. -- `account_license_type` - (Optional) Type of Account License. Valid values: `advanced`, `earlyAdopter`, `express`, `none`, `professional`, or `stakeholder`. Defaults to `express`. In addition the value `basic` is allowed which is an alias for `express` and reflects the name of the `express` license used in the Azure DevOps web interface. +- `account_license_type` - (Optional) Type of Account License. Valid values: `advanced`, `earlyAdopter`, `express`, `none`, `professional`, or `stakeholder`. Defaults to `express`. + + ~> **Note** + The value `basic` is allowed which is an alias for `express` and reflects the name of the `express` license used in the Azure DevOps web interface. + + - `licensing_source` - (Optional) The source of the licensing (e.g. Account. MSDN etc.) Valid values: `account` (Default), `auto`, `msdn`, `none`, `profile`, `trial` ## Attributes Reference From ec9bfe90a53a2670ba8544ac2e25239612a9a9de Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:08:04 +0200 Subject: [PATCH 14/20] Update website/docs/r/service_principal_entitlement.html.markdown Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- website/docs/r/service_principal_entitlement.html.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown index 841f7d8be..9655c7ff4 100644 --- a/website/docs/r/service_principal_entitlement.html.markdown +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -45,6 +45,8 @@ The following attributes are exported: The resources allows the import via the UUID of a service principal entitlement or by using the principal name of a service principal owning an entitlement. +```sh +terraform import azuredevops_service_principal_entitlement.example 00000000-0000-0000-0000-000000000000 ## PAT Permissions Required - **Member Entitlement Management**: Read & Write From 772e91d28bfcedb911605518385b6a5c89a03306 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:08:18 +0200 Subject: [PATCH 15/20] Update website/docs/r/service_principal_entitlement.html.markdown Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- website/docs/r/service_principal_entitlement.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown index 9655c7ff4..d77d5d859 100644 --- a/website/docs/r/service_principal_entitlement.html.markdown +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -27,7 +27,7 @@ resource "azuredevops_service_principal_entitlement" "example" { The value `basic` is allowed which is an alias for `express` and reflects the name of the `express` license used in the Azure DevOps web interface. -- `licensing_source` - (Optional) The source of the licensing (e.g. Account. MSDN etc.) Valid values: `account` (Default), `auto`, `msdn`, `none`, `profile`, `trial` +- `licensing_source` - (Optional) The source of the licensing (e.g. Account. MSDN etc.) Valid values: `account`, `auto`, `msdn`, `none`, `profile`, `trial`. Defaults to `account` ## Attributes Reference From ac8f1f2e1cfe7c8b6d785fd72a24afa8777d3bc1 Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:08:35 +0200 Subject: [PATCH 16/20] Update azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- .../resource_service_principal_entitlement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 5d4959929..78616a656 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -40,7 +40,7 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { Type: schema.TypeString, Optional: true, ForceNew: true, - Default: string("aad"), + Default: "aad", ValidateFunc: validation.StringIsNotWhiteSpace, }, "account_license_type": { From 2c58d06ea0c27aca14dd3d53f0d4aebe05b047ac Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:08:51 +0200 Subject: [PATCH 17/20] Update azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- .../resource_service_principal_entitlement.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 78616a656..94278d950 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -234,7 +234,7 @@ func importServicePrincipalEntitlement(d *schema.ResourceData, m interface{}) ([ } func flattenServicePrincipalEntitlement(d *schema.ResourceData, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) { - d.SetId(servicePrincipalEntitlement.Id.String()) + d.Set("descriptor", *servicePrincipalEntitlement.ServicePrincipal.Descriptor) d.Set("origin", *servicePrincipalEntitlement.ServicePrincipal.Origin) d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) From 83bd8b92d0f3abf672618b4aa3b5f56711d964cd Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:09:07 +0200 Subject: [PATCH 18/20] Update azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- .../resource_service_principal_entitlement.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 94278d950..35fc38488 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -62,16 +62,8 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { string(licensing.AccountLicenseTypeValues.Express), "basic", } - stringInSlice := func(v string, valid []string) bool { - for _, str := range valid { - if strings.EqualFold(v, str) { - return true - } - } - return false - } return strings.EqualFold(old, new) || - (stringInSlice(old, equalEntitlements) && stringInSlice(new, equalEntitlements)) + (slices.Contains(equalEntitlements, old) && slices.Contains(equalEntitlements, new)) }, }, "licensing_source": { From de14ea5d93b6d5fea8873746b3b04b7f3c40a35f Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Fri, 5 Jul 2024 14:18:00 +0200 Subject: [PATCH 19/20] Add additional options --- website/docs/r/service_principal_entitlement.html.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown index d77d5d859..d905ad692 100644 --- a/website/docs/r/service_principal_entitlement.html.markdown +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -20,7 +20,7 @@ resource "azuredevops_service_principal_entitlement" "example" { ## Argument Reference - `origin_id` - (Required) The object ID of the enterprise application. -- `origin` - (Optional) The type of source provider for the origin identifier. Defaults to `aad`. +- `origin` - (Optional) The type of source provider for the origin identifier. Defaults to `aad`. Possible value ad, aad, msa. - `account_license_type` - (Optional) Type of Account License. Valid values: `advanced`, `earlyAdopter`, `express`, `none`, `professional`, or `stakeholder`. Defaults to `express`. ~> **Note** From 85ba43097c444fcc61d5d59af30dee67242e56fe Mon Sep 17 00:00:00 2001 From: Jan-Hendrik Peters Date: Tue, 16 Jul 2024 10:38:58 +0200 Subject: [PATCH 20/20] Apply suggestions from code review Co-authored-by: xuzhang3 <57888764+xuzhang3@users.noreply.github.com> --- .../resource_service_principal_entitlement.go | 42 ++++++++++++++----- ...ervice_principal_entitlement.html.markdown | 2 +- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go index 35fc38488..f89aac698 100644 --- a/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go +++ b/azuredevops/internal/service/memberentitlementmanagement/resource_service_principal_entitlement.go @@ -3,6 +3,7 @@ package memberentitlementmanagement import ( "fmt" "log" + "slices" "strings" "github.com/ahmetb/go-linq" @@ -37,11 +38,14 @@ func ResourceServicePrincipalEntitlement() *schema.Resource { ValidateFunc: validation.StringIsNotWhiteSpace, }, "origin": { - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Default: "aad", - ValidateFunc: validation.StringIsNotWhiteSpace, + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "AAD", + ValidateFunc: validation.StringInSlice([]string{ + "AAD", "AD", "MSA", + }, true), + DiffSuppressFunc: suppress.CaseDifference, }, "account_license_type": { Type: schema.TypeString, @@ -227,11 +231,29 @@ func importServicePrincipalEntitlement(d *schema.ResourceData, m interface{}) ([ func flattenServicePrincipalEntitlement(d *schema.ResourceData, servicePrincipalEntitlement *memberentitlementmanagement.ServicePrincipalEntitlement) { - d.Set("descriptor", *servicePrincipalEntitlement.ServicePrincipal.Descriptor) - d.Set("origin", *servicePrincipalEntitlement.ServicePrincipal.Origin) - d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) - d.Set("account_license_type", string(*servicePrincipalEntitlement.AccessLevel.AccountLicenseType)) - d.Set("licensing_source", *servicePrincipalEntitlement.AccessLevel.LicensingSource) + if servicePrincipalEntitlement.ServicePrincipal != nil { + if servicePrincipalEntitlement.ServicePrincipal.Descriptor != nil { + d.Set("descriptor", *servicePrincipalEntitlement.ServicePrincipal.Descriptor) + } + + if servicePrincipalEntitlement.ServicePrincipal.Origin != nil { + d.Set("origin", *servicePrincipalEntitlement.ServicePrincipal.Origin) + } + + if servicePrincipalEntitlement.ServicePrincipal.OriginId != nil { + d.Set("origin_id", *servicePrincipalEntitlement.ServicePrincipal.OriginId) + } + + } + + if servicePrincipalEntitlement.AccessLevel != nil { + if servicePrincipalEntitlement.AccessLevel.AccountLicenseType != nil { + d.Set("account_license_type", string(*servicePrincipalEntitlement.AccessLevel.AccountLicenseType)) + } + if servicePrincipalEntitlement.AccessLevel.LicensingSource != nil { + d.Set("licensing_source", *servicePrincipalEntitlement.AccessLevel.LicensingSource) + } + } } func expandServicePrincipalEntitlement(d *schema.ResourceData) (*memberentitlementmanagement.ServicePrincipalEntitlement, error) { diff --git a/website/docs/r/service_principal_entitlement.html.markdown b/website/docs/r/service_principal_entitlement.html.markdown index d905ad692..64965381c 100644 --- a/website/docs/r/service_principal_entitlement.html.markdown +++ b/website/docs/r/service_principal_entitlement.html.markdown @@ -20,7 +20,7 @@ resource "azuredevops_service_principal_entitlement" "example" { ## Argument Reference - `origin_id` - (Required) The object ID of the enterprise application. -- `origin` - (Optional) The type of source provider for the origin identifier. Defaults to `aad`. Possible value ad, aad, msa. +- `origin` - (Optional) The type of source provider for the origin identifier. Defaults to `AAD`. Possible value `AAD`, `AD`, `MSA`. - `account_license_type` - (Optional) Type of Account License. Valid values: `advanced`, `earlyAdopter`, `express`, `none`, `professional`, or `stakeholder`. Defaults to `express`. ~> **Note**