From c2342e74b3ca8e698dc3badc149a406b659edb64 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Wed, 24 Nov 2021 00:43:22 +0000 Subject: [PATCH 1/3] Update Hamilton to v0.36.0 --- go.mod | 2 +- go.sum | 4 +- .../hamilton/msgraph/administrative_units.go | 441 ++++++++++++++++++ .../delegated_permission_grants_client.go | 179 +++++++ .../manicminer/hamilton/msgraph/models.go | 127 ++++- .../manicminer/hamilton/msgraph/valuetypes.go | 100 ++++ vendor/modules.txt | 2 +- 7 files changed, 834 insertions(+), 21 deletions(-) create mode 100644 vendor/github.com/manicminer/hamilton/msgraph/administrative_units.go create mode 100644 vendor/github.com/manicminer/hamilton/msgraph/delegated_permission_grants_client.go diff --git a/go.mod b/go.mod index efd5f05ffa..0ac997ba0a 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.8.0 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect github.com/klauspost/compress v1.12.2 // indirect - github.com/manicminer/hamilton v0.35.0 + github.com/manicminer/hamilton v0.36.0 github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect diff --git a/go.sum b/go.sum index c399dbc081..a732a4ce14 100644 --- a/go.sum +++ b/go.sum @@ -299,8 +299,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/manicminer/hamilton v0.35.0 h1:K72BHXLhlO+H/evI5iburLDfVe19bDaXO+hEIQEVkdg= -github.com/manicminer/hamilton v0.35.0/go.mod h1:IOYn2Dc9SUiZ7Ryw6c8Ay795vPPMnrCZe3MktS447dc= +github.com/manicminer/hamilton v0.36.0 h1:HBH1yJB2nA0d4ZebF9R8LSZMwkyujNUQr4mnIthUKE4= +github.com/manicminer/hamilton v0.36.0/go.mod h1:IOYn2Dc9SUiZ7Ryw6c8Ay795vPPMnrCZe3MktS447dc= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/vendor/github.com/manicminer/hamilton/msgraph/administrative_units.go b/vendor/github.com/manicminer/hamilton/msgraph/administrative_units.go new file mode 100644 index 0000000000..04b5b3008e --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/administrative_units.go @@ -0,0 +1,441 @@ +package msgraph + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/odata" +) + +// AdministrativeUnitsClient performs operations on Administrative Units +type AdministrativeUnitsClient struct { + BaseClient Client +} + +// NewAdministrativeUnitsClient returns a new AdministrativeUnitsClient. +func NewAdministrativeUnitsClient(tenantId string) *AdministrativeUnitsClient { + return &AdministrativeUnitsClient{ + BaseClient: NewClient(VersionBeta, tenantId), + } +} + +// List returns a list of AdministrativeUnits, optionally queried using OData. +func (c *AdministrativeUnitsClient) List(ctx context.Context, query odata.Query) (*[]AdministrativeUnit, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/administrativeUnits", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + AdministrativeUnits []AdministrativeUnit `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.AdministrativeUnits, status, nil +} + +// Create creates a new AdministrativeUnit. +func (c *AdministrativeUnitsClient) Create(ctx context.Context, administrativeUnit AdministrativeUnit) (*AdministrativeUnit, int, error) { + var status int + + body, err := json.Marshal(administrativeUnit) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + OData: odata.Query{ + Metadata: odata.MetadataFull, + }, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: "/administrativeUnits", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var newAdministrativeUnit AdministrativeUnit + if err := json.Unmarshal(respBody, &newAdministrativeUnit); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &newAdministrativeUnit, status, nil +} + +// Get retrieves an AdministrativeUnit +func (c *AdministrativeUnitsClient) Get(ctx context.Context, id string, query odata.Query) (*AdministrativeUnit, int, error) { + query.Metadata = odata.MetadataFull + + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var administrativeUnit AdministrativeUnit + if err := json.Unmarshal(respBody, &administrativeUnit); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &administrativeUnit, status, nil +} + +// Update amends an existing AdministrativeUnit. +func (c *AdministrativeUnitsClient) Update(ctx context.Context, administrativeUnit AdministrativeUnit) (int, error) { + var status int + + body, err := json.Marshal(administrativeUnit) + if err != nil { + return status, fmt.Errorf("json.Marshal(): %v", err) + } + + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s", *administrativeUnit.ID), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Patch(): %v", err) + } + + return status, nil +} + +// Delete removes a AdministrativeUnit. +func (c *AdministrativeUnitsClient) Delete(ctx context.Context, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AdministrativeUnits.BaseClient.Get(): %v", err) + } + + return status, nil +} + +// ListMembers retrieves the members of the specified AdministrativeUnit. +func (c *AdministrativeUnitsClient) ListMembers(ctx context.Context, administrativeUnitId string) (*[]string, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + OData: odata.Query{ + Select: []string{"id"}, + }, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/members", administrativeUnitId), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + Members []struct { + Type string `json:"@odata.type"` + Id string `json:"id"` + } `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + ret := make([]string, len(data.Members)) + for i, v := range data.Members { + ret[i] = v.Id + } + + return &ret, status, nil +} + +// GetMember retrieves a single member of the specified AdministrativeUnit. +func (c *AdministrativeUnitsClient) GetMember(ctx context.Context, administrativeUnitId, memberId string) (*string, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + OData: odata.Query{ + Select: []string{"id", "url"}, + }, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/members/%s/$ref", administrativeUnitId, memberId), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + Context string `json:"@odata.context"` + Type string `json:"@odata.type"` + Id string `json:"id"` + Url string `json:"url"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.Id, status, nil +} + +// AddMembers adds new members to a AdministrativeUnit. +func (c *AdministrativeUnitsClient) AddMembers(ctx context.Context, administrativeUnitId string, members *Members) (int, error) { + var status int + + if members == nil || len(*members) == 0 { + return status, fmt.Errorf("no members specified") + } + + for _, member := range *members { + // don't fail if a member already exists + checkMemberAlreadyExists := func(resp *http.Response, o *odata.OData) bool { + if resp != nil && resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { + return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) + } + return false + } + + body, err := json.Marshal(DirectoryObject{ODataId: member.ODataId}) + if err != nil { + return status, fmt.Errorf("json.Marshal(): %v", err) + } + + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + ValidStatusFunc: checkMemberAlreadyExists, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/members/$ref", administrativeUnitId), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Post(): %v", err) + } + } + + return status, nil +} + +// RemoveMembers removes members from a AdministrativeUnit. +func (c *AdministrativeUnitsClient) RemoveMembers(ctx context.Context, administrativeUnitId string, memberIds *[]string) (int, error) { + var status int + + if memberIds == nil || len(*memberIds) == 0 { + return status, fmt.Errorf("no members specified") + } + + for _, memberId := range *memberIds { + // check for membership before attempting deletion + if _, status, err := c.GetMember(ctx, administrativeUnitId, memberId); err != nil { + if status == http.StatusNotFound { + continue + } + return status, err + } + + // despite the above check, sometimes members are just gone + checkMemberGone := func(resp *http.Response, o *odata.OData) bool { + if resp != nil && resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { + return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) + } + return false + } + + var err error + _, status, _, err = c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + ValidStatusFunc: checkMemberGone, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/members/%s/$ref", administrativeUnitId, memberId), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Delete(): %v", err) + } + } + + return status, nil +} + +// ListScopedRoleMembers retrieves the members of the specified AdministrativeUnit. +func (c *AdministrativeUnitsClient) ListScopedRoleMembers(ctx context.Context, administrativeUnitId string, query odata.Query) (*[]ScopedRoleMembership, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/scopedRoleMembers", administrativeUnitId), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + ScopedRoleMembers []ScopedRoleMembership `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.ScopedRoleMembers, status, nil +} + +// GetScopedRoleMember retrieves a single member of the specified AdministrativeUnit. +func (c *AdministrativeUnitsClient) GetScopedRoleMember(ctx context.Context, administrativeUnitId, scopedRoleMembershipId string, query odata.Query) (*ScopedRoleMembership, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/scopedRoleMembers/%s", administrativeUnitId, scopedRoleMembershipId), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data ScopedRoleMembership + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data, status, nil +} + +// AddScopedRoleMember adds a new scoped role membership for a AdministrativeUnit. +func (c *AdministrativeUnitsClient) AddScopedRoleMember(ctx context.Context, administrativeUnitId string, scopedRoleMembership ScopedRoleMembership) (*ScopedRoleMembership, int, error) { + var status int + + body, err := json.Marshal(scopedRoleMembership) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/scopedRoleMembers", administrativeUnitId), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data ScopedRoleMembership + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data, status, nil +} + +// RemoveScopedRoleMembers removes members from a AdministrativeUnit. +func (c *AdministrativeUnitsClient) RemoveScopedRoleMembers(ctx context.Context, administrativeUnitId, scopedRoleMembershipId string) (int, error) { + var status int + + var err error + _, status, _, err = c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/administrativeUnits/%s/scopedRoleMembers/%s", administrativeUnitId, scopedRoleMembershipId), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("AdministrativeUnitsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/delegated_permission_grants_client.go b/vendor/github.com/manicminer/hamilton/msgraph/delegated_permission_grants_client.go new file mode 100644 index 0000000000..48b1c9c4c6 --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/delegated_permission_grants_client.go @@ -0,0 +1,179 @@ +package msgraph + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/odata" +) + +// DelegatedPermissionGrantsClient performs operations on DelegatedPermissionGrants. +type DelegatedPermissionGrantsClient struct { + BaseClient Client +} + +// NewDelegatedPermissionGrantsClient returns a new DelegatedPermissionGrantsClient +func NewDelegatedPermissionGrantsClient(tenantId string) *DelegatedPermissionGrantsClient { + return &DelegatedPermissionGrantsClient{ + BaseClient: NewClient(Version10, tenantId), + } +} + +// List returns a list of delegated permission grants +func (c *DelegatedPermissionGrantsClient) List(ctx context.Context, query odata.Query) (*[]DelegatedPermissionGrant, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/oauth2PermissionGrants", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DelegatedPermissionGrantsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data struct { + DelegatedPermissionGrants []DelegatedPermissionGrant `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.DelegatedPermissionGrants, status, nil +} + +// Create creates a new delegated permission grant +func (c *DelegatedPermissionGrantsClient) Create(ctx context.Context, delegatedPermissionGrant DelegatedPermissionGrant) (*DelegatedPermissionGrant, int, error) { + var status int + + if delegatedPermissionGrant.ClientId == nil { + return nil, status, errors.New("DelegatedPermissionGrantsClient.Create(): ClientId was nil for delegatedPermissionGrant") + } + + body, err := json.Marshal(delegatedPermissionGrant) + if err != nil { + return nil, status, fmt.Errorf("json.Marshal(): %v", err) + } + + consistencyFunc := func(resp *http.Response, o *odata.OData) bool { + if resp != nil && o != nil && o.Error != nil { + if resp.StatusCode == http.StatusNotFound { + return true + } else if resp.StatusCode == http.StatusBadRequest { + return o.Error.Match(odata.ErrorResourceDoesNotExist) + } + } + return false + } + + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: consistencyFunc, + ValidStatusCodes: []int{http.StatusCreated}, + Uri: Uri{ + Entity: "/oauth2PermissionGrants", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DelegatedPermissionGrantsClient.BaseClient.Post(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var newDelegatedPermissionGrant DelegatedPermissionGrant + if err := json.Unmarshal(respBody, &newDelegatedPermissionGrant); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &newDelegatedPermissionGrant, status, nil +} + +// Get returns a delegated permission grant +func (c *DelegatedPermissionGrantsClient) Get(ctx context.Context, id string, query odata.Query) (*DelegatedPermissionGrant, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + OData: query, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/oauth2PermissionGrants/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DelegatedPermissionGrantsClient.BaseClient.Get(): %v", err) + } + + defer resp.Body.Close() + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) + } + + var data DelegatedPermissionGrant + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data, status, nil +} + +// Update amends an existing delegated permission grant +func (c *DelegatedPermissionGrantsClient) Update(ctx context.Context, delegatedPermissionGrant DelegatedPermissionGrant) (int, error) { + var status int + + if delegatedPermissionGrant.Id == nil { + return status, errors.New("cannot update delegated permission grant with nil ID") + } + + body, err := json.Marshal(delegatedPermissionGrant) + if err != nil { + return status, fmt.Errorf("json.Marshal(): %v", err) + } + + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ + Body: body, + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/oauth2PermissionGrants/%s", *delegatedPermissionGrant.Id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("DelegatedPermissionGrantsClient.BaseClient.Patch(): %v", err) + } + + return status, nil +} + +// Delete removes a delegated permission grant +func (c *DelegatedPermissionGrantsClient) Delete(ctx context.Context, id string) (int, error) { + _, status, _, err := c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusNoContent}, + Uri: Uri{ + Entity: fmt.Sprintf("/oauth2PermissionGrants/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("DelegatedPermissionGrantsClient.BaseClient.Delete(): %v", err) + } + + return status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/models.go b/vendor/github.com/manicminer/hamilton/msgraph/models.go index 3f5eae8da5..710403a071 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/models.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/models.go @@ -157,6 +157,13 @@ type AddInKeyValue struct { Value *string `json:"value,omitempty"` } +type AdministrativeUnit struct { + Description *StringNullWhenEmpty `json:"description,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + ID *string `json:"id,omitempty"` + Visibility *AdministrativeUnitVisibility `json:"visibility,omitempty"` +} + type ApiPreAuthorizedApplication struct { AppId *string `json:"appId,omitempty"` PermissionIds *[]string `json:"permissionIds,omitempty"` @@ -518,8 +525,8 @@ type BaseNamedLocation struct { } type CloudAppSecurityControl struct { - IsEnabled *bool `json:"isEnabled,omitempty"` - CloudAppSecurityType *string `json:"cloudAppSecurityType,omitempty"` + IsEnabled *bool `json:"isEnabled,omitempty"` + CloudAppSecurityType *ConditionalAccessCloudAppSecuritySessionControlType `json:"cloudAppSecurityType,omitempty"` } type ConditionalAccessApplications struct { @@ -529,20 +536,38 @@ type ConditionalAccessApplications struct { } type ConditionalAccessConditionSet struct { - Applications *ConditionalAccessApplications `json:"applications,omitempty"` - Users *ConditionalAccessUsers `json:"users,omitempty"` - ClientAppTypes *[]string `json:"clientAppTypes,omitempty"` - Locations *ConditionalAccessLocations `json:"locations,omitempty"` - Platforms *ConditionalAccessPlatforms `json:"platforms,omitempty"` - SignInRiskLevels *[]string `json:"signInRiskLevels,omitempty"` - UserRiskLevels *[]string `json:"userRiskLevels,omitempty"` + Applications *ConditionalAccessApplications `json:"applications,omitempty"` + ClientAppTypes *[]ConditionalAccessClientAppType `json:"clientAppTypes,omitempty"` + Devices *ConditionalAccessDevices `json:"devices,omitempty"` + DeviceStates *ConditionalAccessDeviceStates `json:"deviceStates,omitempty"` + Locations *ConditionalAccessLocations `json:"locations,omitempty"` + Platforms *ConditionalAccessPlatforms `json:"platforms,omitempty"` + SignInRiskLevels *[]ConditionalAccessRiskLevel `json:"signInRiskLevels,omitempty"` + UserRiskLevels *[]ConditionalAccessRiskLevel `json:"userRiskLevels,omitempty"` + Users *ConditionalAccessUsers `json:"users,omitempty"` +} + +type ConditionalAccessDevices struct { + IncludeDevices *[]string `json:"includeDevices,omitempty"` + ExcludeDevices *[]string `json:"excludeDevices,omitempty"` + DeviceFilter *ConditionalAccessFilter `json:"deviceFilter,omitempty"` +} + +type ConditionalAccessDeviceStates struct { + IncludeStates *ConditionalAccessDeviceStatesInclude `json:"includeStates,omitempty"` + ExcludeStates *ConditionalAccessDeviceStatesExclude `json:"excludeStates,omitempty"` +} + +type ConditionalAccessFilter struct { + Mode *ConditionalAccessFilterMode `json:"mode,omitempty"` + Rule *string `json:"rule,omitempty"` } type ConditionalAccessGrantControls struct { - Operator *string `json:"operator,omitempty"` - BuiltInControls *[]string `json:"builtInControls,omitempty"` - CustomAuthenticationFactors *[]string `json:"customAuthenticationFactors,omitempty"` - TermsOfUse *[]string `json:"termsOfUse,omitempty"` + Operator *string `json:"operator,omitempty"` + BuiltInControls *[]ConditionalAccessGrantControl `json:"builtInControls,omitempty"` + CustomAuthenticationFactors *[]string `json:"customAuthenticationFactors,omitempty"` + TermsOfUse *[]string `json:"termsOfUse,omitempty"` } type ConditionalAccessLocations struct { @@ -551,8 +576,8 @@ type ConditionalAccessLocations struct { } type ConditionalAccessPlatforms struct { - IncludePlatforms *[]string `json:"includePlatforms,omitempty"` - ExcludePlatforms *[]string `json:"excludePlatforms,omitempty"` + IncludePlatforms *[]ConditionalAccessDevicePlatform `json:"includePlatforms,omitempty"` + ExcludePlatforms *[]ConditionalAccessDevicePlatform `json:"excludePlatforms,omitempty"` } // ConditionalAccessPolicy describes an Conditional Access Policy object. @@ -618,6 +643,61 @@ type CredentialUserRegistrationDetails struct { UserPrincipalName *string `json:"UserPrincipalName,omitempty"` } +type DelegatedPermissionGrant struct { + Id *string `json:"id,omitempty"` + ClientId *string `json:"clientId,omitempty"` + ConsentType *DelegatedPermissionGrantConsentType `json:"consentType,omitempty"` + PrincipalId *string `json:"principalId,omitempty"` + ResourceId *string `json:"resourceId,omitempty"` + Scopes *[]string `json:"-"` +} + +func (d DelegatedPermissionGrant) MarshalJSON() ([]byte, error) { + var val *StringNullWhenEmpty + if d.Scopes != nil { + scopes := make([]string, 0) + for _, s := range *d.Scopes { + scopes = append(scopes, string(s)) + } + theScopes := StringNullWhenEmpty(strings.Join(scopes, " ")) + val = &theScopes + } + + // Local type needed to avoid recursive MarshalJSON calls + type delegatedPermissionGrant DelegatedPermissionGrant + grant := struct { + Scopes *StringNullWhenEmpty `json:"scope,omitempty"` + *delegatedPermissionGrant + }{ + Scopes: val, + delegatedPermissionGrant: (*delegatedPermissionGrant)(&d), + } + buf, err := json.Marshal(&grant) + return buf, err +} + +func (d *DelegatedPermissionGrant) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls + type delegatedPermissionGrant DelegatedPermissionGrant + grant := struct { + Scopes *string `json:"scope"` + *delegatedPermissionGrant + }{ + delegatedPermissionGrant: (*delegatedPermissionGrant)(d), + } + if err := json.Unmarshal(data, &grant); err != nil { + return err + } + if grant.Scopes != nil { + var scopes []string + for _, s := range strings.Split(*grant.Scopes, " ") { + scopes = append(scopes, strings.TrimSpace(s)) + } + d.Scopes = &scopes + } + return nil +} + type DeviceDetail struct { Browser *string `json:"browser,omitempty"` DeviceId *string `json:"deviceId,omitempty"` @@ -862,6 +942,12 @@ type GroupOnPremisesProvisioningError struct { Value *string `json:"value,omitempty"` } +type Identity struct { + DisplayName *string `json:"displayName,omitempty"` + Id *string `json:"id,omitempty"` + TenantId *string `json:"tenantId,omitempty"` +} + type IdentityProvider struct { ODataType *odata.Type `json:"@odata.type,omitempty"` ID *string `json:"id,omitempty"` @@ -1071,8 +1157,8 @@ type PermissionScope struct { } type PersistentBrowserSessionControl struct { - IsEnabled *bool `json:"isEnabled,omitempty"` - Mode *string `json:"mode,omitempty"` + IsEnabled *bool `json:"isEnabled,omitempty"` + Mode *PersistentBrowserSessionMode `json:"mode,omitempty"` } type PhoneAuthenticationMethod struct { @@ -1129,6 +1215,13 @@ func (se SchemaExtensionData) MarshalJSON() ([]byte, error) { return json.Marshal(in) } +type ScopedRoleMembership struct { + AdministrativeUnitId *string `json:"administrativeUnitId,omitempty"` + Id *string `json:"id,omitempty"` + RoleId *string `json:"roleId,omitempty"` + RoleMemberInfo *Identity `json:"roleMemberInfo"` +} + // ServicePrincipal describes a Service Principal object. type ServicePrincipal struct { DirectoryObject diff --git a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go index eb33cb5b76..cee03b8ac2 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go @@ -7,6 +7,9 @@ import ( "github.com/manicminer/hamilton/odata" ) +// NullableString returns a pointer to a StringNullWhenEmpty for use in model structs +func NullableString(s StringNullWhenEmpty) *StringNullWhenEmpty { return &s } + // StringNullWhenEmpty is a string type that marshals its JSON representation as null when set to its zero value. // Can be used with a pointer reference with the `omitempty` tag to omit a field when the pointer is nil, but send a // JSON null value when the string is empty. @@ -60,6 +63,13 @@ const ( AccessPackageResourceTypeSharePointOnlineSite AccessPackageResourceType = "SharePoint Online Site" ) +type AdministrativeUnitVisibility = string + +const ( + AdministrativeUnitVisibilityHiddenMembership AdministrativeUnitVisibility = "HiddenMembership" + AdministrativeUnitVisibilityPublic AdministrativeUnitVisibility = "Public" +) + type AgeGroup = StringNullWhenEmpty const ( @@ -191,6 +201,71 @@ const ( CredentialUsageSummaryPeriod1 CredentialUsageSummaryPeriod = "D1" ) +type ConditionalAccessClientAppType = string + +const ( + ConditionalAccessClientAppTypeAll ConditionalAccessClientAppType = "all" + ConditionalAccessClientAppTypeBrowser ConditionalAccessClientAppType = "browser" + ConditionalAccessClientAppTypeEasSupported ConditionalAccessClientAppType = "easSupported" + ConditionalAccessClientAppTypeExchangeActiveSync ConditionalAccessClientAppType = "exchangeActiveSync" + ConditionalAccessClientAppTypeMobileAppsAndDesktopClients ConditionalAccessClientAppType = "mobileAppsAndDesktopClients" + ConditionalAccessClientAppTypeOther ConditionalAccessClientAppType = "other" +) + +type ConditionalAccessCloudAppSecuritySessionControlType = string + +const ( + ConditionalAccessCloudAppSecuritySessionControlTypeBlockDownloads ConditionalAccessCloudAppSecuritySessionControlType = "blockDownloads" + ConditionalAccessCloudAppSecuritySessionControlTypeMcasConfigured ConditionalAccessCloudAppSecuritySessionControlType = "mcasConfigured" + ConditionalAccessCloudAppSecuritySessionControlTypeMonitorOnly ConditionalAccessCloudAppSecuritySessionControlType = "monitorOnly" + ConditionalAccessCloudAppSecuritySessionControlTypeUnknownFutureValue ConditionalAccessCloudAppSecuritySessionControlType = "unknownFutureValue" +) + +type ConditionalAccessDevicePlatform = string + +const ( + ConditionalAccessDevicePlatformAll ConditionalAccessDevicePlatform = "All" + ConditionalAccessDevicePlatformAndroid ConditionalAccessDevicePlatform = "android" + ConditionalAccessDevicePlatformIos ConditionalAccessDevicePlatform = "iOS" + ConditionalAccessDevicePlatformMacOs ConditionalAccessDevicePlatform = "macOS" + ConditionalAccessDevicePlatformUnknownFutureValue ConditionalAccessDevicePlatform = "unknownFutureValue" + ConditionalAccessDevicePlatformWindows ConditionalAccessDevicePlatform = "windows" + ConditionalAccessDevicePlatformWindowsPhone ConditionalAccessDevicePlatform = "windowsPhone" +) + +type ConditionalAccessDeviceStatesInclude = string + +const ( + ConditionalAccessDeviceStatesIncludeAll ConditionalAccessDeviceStatesInclude = "All" +) + +type ConditionalAccessDeviceStatesExclude = string + +const ( + ConditionalAccessDeviceStatesExcludeCompliant ConditionalAccessDeviceStatesExclude = "Compliant" + ConditionalAccessDeviceStatesExcludeDomainJoined ConditionalAccessDeviceStatesExclude = "DomainJoined" +) + +type ConditionalAccessFilterMode = string + +const ( + ConditionalAccessFilterModeExclude ConditionalAccessFilterMode = "exclude" + ConditionalAccessFilterModeInclude ConditionalAccessFilterMode = "include" +) + +type ConditionalAccessGrantControl = string + +const ( + ConditionalAccessGrantControlApprovedApplication ConditionalAccessGrantControl = "approvedApplication" + ConditionalAccessGrantControlBlock ConditionalAccessGrantControl = "block" + ConditionalAccessGrantControlCompliantApplication ConditionalAccessGrantControl = "compliantApplication" + ConditionalAccessGrantControlCompliantDevice ConditionalAccessGrantControl = "compliantDevice" + ConditionalAccessGrantControlDomainJoinedDevice ConditionalAccessGrantControl = "domainJoinedDevice" + ConditionalAccessGrantControlMfa ConditionalAccessGrantControl = "mfa" + ConditionalAccessGrantControlPasswordChange ConditionalAccessGrantControl = "passwordChange" + ConditionalAccessGrantControlUnknownFutureValue ConditionalAccessGrantControl = "unknownFutureValue" +) + type ConditionalAccessPolicyState = string const ( @@ -199,6 +274,24 @@ const ( ConditionalAccessPolicyStateEnabledForReportingButNotEnforced ConditionalAccessPolicyState = "enabledForReportingButNotEnforced" ) +type ConditionalAccessRiskLevel = string + +const ( + ConditionalAccessRiskLevelHidden ConditionalAccessRiskLevel = "hidden" + ConditionalAccessRiskLevelHigh ConditionalAccessRiskLevel = "high" + ConditionalAccessRiskLevelLow ConditionalAccessRiskLevel = "low" + ConditionalAccessRiskLevelMedium ConditionalAccessRiskLevel = "medium" + ConditionalAccessRiskLevelNone ConditionalAccessRiskLevel = "none" + ConditionalAccessRiskLevelUnknownFutureValue ConditionalAccessRiskLevel = "unknownFutureValue" +) + +type DelegatedPermissionGrantConsentType = string + +const ( + DelegatedPermissionGrantConsentTypeAllPrincipals DelegatedPermissionGrantConsentType = "AllPrincipals" + DelegatedPermissionGrantConsentTypePrincipal DelegatedPermissionGrantConsentType = "Principal" +) + type ExtensionSchemaTargetType = string const ( @@ -369,6 +462,13 @@ const ( PermissionScopeTypeUser PermissionScopeType = "User" ) +type PersistentBrowserSessionMode = string + +const ( + PersistentBrowserSessionModeAlways PersistentBrowserSessionMode = "always" + PersistentBrowserSessionModeNever PersistentBrowserSessionMode = "never" +) + type PreferredSingleSignOnMode = StringNullWhenEmpty const ( diff --git a/vendor/modules.txt b/vendor/modules.txt index 111a191d91..ab0f084245 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -203,7 +203,7 @@ github.com/klauspost/compress/fse github.com/klauspost/compress/huff0 github.com/klauspost/compress/zstd github.com/klauspost/compress/zstd/internal/xxhash -# github.com/manicminer/hamilton v0.35.0 +# github.com/manicminer/hamilton v0.36.0 ## explicit github.com/manicminer/hamilton/auth github.com/manicminer/hamilton/environments From 8cb30b26b37c406a6131d1bb89a1f1a0aaa7b54a Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Wed, 24 Nov 2021 00:43:41 +0000 Subject: [PATCH 2/3] New resource: azuread_service_principal_delegated_permission_grant --- ...ce_principal_delegated_permission_grant.md | 128 +++++++++++++ .../app_role_assignment_resource.go | 2 +- .../serviceprincipals/client/client.go | 13 +- .../serviceprincipals/registration.go | 7 +- ...pal_delegated_permission_grant_resource.go | 170 ++++++++++++++++++ ...elegated_permission_grant_resource_test.go | 130 ++++++++++++++ 6 files changed, 442 insertions(+), 8 deletions(-) create mode 100644 docs/resources/service_principal_delegated_permission_grant.md create mode 100644 internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go create mode 100644 internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource_test.go diff --git a/docs/resources/service_principal_delegated_permission_grant.md b/docs/resources/service_principal_delegated_permission_grant.md new file mode 100644 index 0000000000..e612a620b6 --- /dev/null +++ b/docs/resources/service_principal_delegated_permission_grant.md @@ -0,0 +1,128 @@ +--- +subcategory: "Delegated Permission Grants" +--- + +# Resource: azuread_service_principal_delegated_permission_grant + +Manages a delegated permission grant for a service principal, on behalf of a single user, or all users. + +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires the following application role: `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one the following directory role: `Global Administrator` + +## Example Usage + +*Delegated permission grant for all users* + +```terraform +data "azuread_application_published_app_ids" "well_known" {} + +resource "azuread_service_principal" "msgraph" { + application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + use_existing = true +} + +resource "azuread_application" "example" { + display_name = "example" + + required_resource_access { + resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"] + type = "Scope" + } + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.Read"] + type = "Scope" + } + } +} + +resource "azuread_service_principal" "example" { + application_id = azuread_application.example.application_id +} + +resource "azuread_service_principal_delegated_permission_grant" "example" { + service_principal_object_id = azuread_service_principal.example.object_id + resource_service_principal_object_id = azuread_service_principal.msgraph.object_id + claim_values = ["openid", "User.Read.All"] +} +``` + +*Delegated permission grant for a single user* + +```terraform +data "azuread_application_published_app_ids" "well_known" {} + +resource "azuread_service_principal" "msgraph" { + application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + use_existing = true +} + +resource "azuread_application" "example" { + display_name = "example" + + required_resource_access { + resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"] + type = "Scope" + } + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.Read"] + type = "Scope" + } + } +} + +resource "azuread_service_principal" "example" { + application_id = azuread_application.example.application_id +} + +resource "azuread_user" "example" { + display_name = "J. Doe" + user_principal_name = "jdoe@hashicorp.com" + mail_nickname = "jdoe" + password = "SecretP@sswd99!" +} + +resource "azuread_service_principal_delegated_permission_grant" "example" { + service_principal_object_id = azuread_service_principal.example.object_id + resource_service_principal_object_id = azuread_service_principal.msgraph.object_id + claim_values = ["openid", "User.Read.All"] + user_object_id = azuread_user.example.object_id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `claim_values` - (Required) - A set of claim values for delegated permission scopes which should be included in access tokens for the resource. +* `resource_service_principal_object_id` - (Required) The object ID of the service principal representing the resource to be accessed. Changing this forces a new resource to be created. +* `service_principal_object_id` - (Required) The object ID of the service principal for which this delegated permission grant should be created. Changing this forces a new resource to be created. +* `user_object_id` - (Optional) - The object ID of the user on behalf of whom the service principal is authorized to access the resource. When omitted, the delegated permission grant will be consented for all users. Changing this forces a new resource to be created. + +-> **Granting Admin Consent** To grant admin consent for the service principal to impersonate all users, just omit the `user_object_id` property. + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `id` - The ID of the delegated permission grant. + +## Import + +Delegated permission grants can be imported using their ID, e.g. + +```shell +terraform import azuread_service_principal_delegated_permission_grant.example aaBBcDDeFG6h5JKLMN2PQrrssTTUUvWWxxxxxyyyzzz +``` diff --git a/internal/services/approleassignments/app_role_assignment_resource.go b/internal/services/approleassignments/app_role_assignment_resource.go index cf59cd6c84..0caa5fa088 100644 --- a/internal/services/approleassignments/app_role_assignment_resource.go +++ b/internal/services/approleassignments/app_role_assignment_resource.go @@ -94,7 +94,7 @@ func appRoleAssignmentResourceCreate(ctx context.Context, d *schema.ResourceData if status == http.StatusNotFound { return tf.ErrorDiagPathF(err, "principal_object_id", "Service principal not found for resource (Object ID: %q)", resourceId) } - return tf.ErrorDiagF(err, "Could not retrieve service principal for resource (Object ID: %q)", principalId) + return tf.ErrorDiagF(err, "Could not retrieve service principal for resource (Object ID: %q)", resourceId) } properties := msgraph.AppRoleAssignment{ AppRoleId: utils.String(appRoleId), diff --git a/internal/services/serviceprincipals/client/client.go b/internal/services/serviceprincipals/client/client.go index ee8acac1cb..f423eb5236 100644 --- a/internal/services/serviceprincipals/client/client.go +++ b/internal/services/serviceprincipals/client/client.go @@ -7,11 +7,15 @@ import ( ) type Client struct { - DirectoryObjectsClient *msgraph.DirectoryObjectsClient - ServicePrincipalsClient *msgraph.ServicePrincipalsClient + DelegatedPermissionGrantsClient *msgraph.DelegatedPermissionGrantsClient + DirectoryObjectsClient *msgraph.DirectoryObjectsClient + ServicePrincipalsClient *msgraph.ServicePrincipalsClient } func NewClient(o *common.ClientOptions) *Client { + delegatedPermissionGrantsClient := msgraph.NewDelegatedPermissionGrantsClient(o.TenantID) + o.ConfigureClient(&delegatedPermissionGrantsClient.BaseClient) + directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID) o.ConfigureClient(&directoryObjectsClient.BaseClient) @@ -19,7 +23,8 @@ func NewClient(o *common.ClientOptions) *Client { o.ConfigureClient(&servicePrincipalsClient.BaseClient) return &Client{ - DirectoryObjectsClient: directoryObjectsClient, - ServicePrincipalsClient: servicePrincipalsClient, + DelegatedPermissionGrantsClient: delegatedPermissionGrantsClient, + DirectoryObjectsClient: directoryObjectsClient, + ServicePrincipalsClient: servicePrincipalsClient, } } diff --git a/internal/services/serviceprincipals/registration.go b/internal/services/serviceprincipals/registration.go index cefe241430..9d46e2cbb6 100644 --- a/internal/services/serviceprincipals/registration.go +++ b/internal/services/serviceprincipals/registration.go @@ -30,8 +30,9 @@ func (r Registration) SupportedDataSources() map[string]*schema.Resource { // SupportedResources returns the supported Resources supported by this Service func (r Registration) SupportedResources() map[string]*schema.Resource { return map[string]*schema.Resource{ - "azuread_service_principal": servicePrincipalResource(), - "azuread_service_principal_certificate": servicePrincipalCertificateResource(), - "azuread_service_principal_password": servicePrincipalPasswordResource(), + "azuread_service_principal": servicePrincipalResource(), + "azuread_service_principal_certificate": servicePrincipalCertificateResource(), + "azuread_service_principal_delegated_permission_grant": servicePrincipalDelegatedPermissionGrantResource(), + "azuread_service_principal_password": servicePrincipalPasswordResource(), } } diff --git a/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go b/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go new file mode 100644 index 0000000000..a8b596a7e1 --- /dev/null +++ b/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go @@ -0,0 +1,170 @@ +package serviceprincipals + +import ( + "context" + "errors" + "log" + "net/http" + "time" + + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/manicminer/hamilton/msgraph" + + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/manicminer/hamilton/odata" + + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" +) + +func servicePrincipalDelegatedPermissionGrantResource() *schema.Resource { + return &schema.Resource{ + CreateContext: servicePrincipalDelegatedPermissionGrantResourceCreate, + UpdateContext: servicePrincipalDelegatedPermissionGrantResourceUpdate, + ReadContext: servicePrincipalDelegatedPermissionGrantResourceRead, + DeleteContext: servicePrincipalDelegatedPermissionGrantResourceDelete, + + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, + + Schema: map[string]*schema.Schema{ + "claim_values": { + Description: "A set of claim values for delegated permission scopes which should be included in access tokens for the resource", + Type: schema.TypeSet, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + }, + + "resource_service_principal_object_id": { + Description: "The object ID of the service principal representing the resource to be accessed", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "service_principal_object_id": { + Description: "The object ID of the service principal for which this delegated permission grant should be created", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + + "user_object_id": { + Description: "The object ID of the user on behalf of whom the service principal is authorized to access the resource", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + ValidateDiagFunc: validate.UUID, + }, + }, + } +} + +func servicePrincipalDelegatedPermissionGrantResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient + servicePrincipalsClient := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + + servicePrincipalId := d.Get("service_principal_object_id").(string) + resourceId := d.Get("resource_service_principal_object_id").(string) + + if _, status, err := servicePrincipalsClient.Get(ctx, servicePrincipalId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(err, "principal_object_id", "Service principal with object ID %q was not found)", servicePrincipalId) + } + return tf.ErrorDiagF(err, "Could not retrieve service principal with object ID %q", servicePrincipalId) + } + + if _, status, err := servicePrincipalsClient.Get(ctx, resourceId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagPathF(err, "principal_object_id", "Service principal not found for resource (Object ID: %q)", resourceId) + } + return tf.ErrorDiagF(err, "Could not retrieve service principal for resource (Object ID: %q)", resourceId) + } + + properties := msgraph.DelegatedPermissionGrant{ + ClientId: utils.String(servicePrincipalId), + ResourceId: utils.String(resourceId), + Scopes: tf.ExpandStringSlicePtr(d.Get("claim_values").(*schema.Set).List()), + } + + if v, ok := d.GetOk("user_object_id"); ok && v.(string) != "" { + properties.PrincipalId = utils.String(v.(string)) + properties.ConsentType = utils.String(msgraph.DelegatedPermissionGrantConsentTypePrincipal) + } else { + properties.ConsentType = utils.String(msgraph.DelegatedPermissionGrantConsentTypeAllPrincipals) + } + + delegatedPermissionGrant, _, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Could not create delegated permission grant") + } + + if delegatedPermissionGrant.Id == nil || *delegatedPermissionGrant.Id == "" { + return tf.ErrorDiagF(errors.New("ID returned for delegated permission grant is nil"), "Bad API response") + } + + d.SetId(*delegatedPermissionGrant.Id) + + return servicePrincipalDelegatedPermissionGrantResourceRead(ctx, d, meta) +} + +func servicePrincipalDelegatedPermissionGrantResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient + + properties := msgraph.DelegatedPermissionGrant{ + Id: utils.String(d.Id()), + Scopes: tf.ExpandStringSlicePtr(d.Get("claim_values").(*schema.Set).List()), + } + + if _, err := client.Update(ctx, properties); err != nil { + return tf.ErrorDiagF(err, "Could not update delegated permission grant") + } + + return servicePrincipalDelegatedPermissionGrantResourceRead(ctx, d, meta) +} + +func servicePrincipalDelegatedPermissionGrantResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient + + delegatedPermissionGrant, status, err := client.Get(ctx, d.Id(), odata.Query{}) + if err != nil { + if status == http.StatusNoContent { + log.Printf("[DEBUG] Delegated Permission Grant with ID %q was not found - removing from state", d.Id()) + d.SetId("") + return nil + } + return tf.ErrorDiagPathF(err, "id", "Retrieving Delegated Permission Grant with ID %q", d.Id()) + } + + tf.Set(d, "claim_values", delegatedPermissionGrant.Scopes) + tf.Set(d, "resource_service_principal_object_id", delegatedPermissionGrant.ResourceId) + tf.Set(d, "service_principal_object_id", delegatedPermissionGrant.ClientId) + tf.Set(d, "user_object_id", delegatedPermissionGrant.PrincipalId) + + return nil +} + +func servicePrincipalDelegatedPermissionGrantResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).ServicePrincipals.DelegatedPermissionGrantsClient + + id := d.Id() + + if status, err := client.Delete(ctx, id); err != nil { + return tf.ErrorDiagPathF(err, "id", "Deleting delegated permission grant with ID %q, got status %d", id, status) + } + + return nil +} diff --git a/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource_test.go b/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource_test.go new file mode 100644 index 0000000000..57118cdc97 --- /dev/null +++ b/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource_test.go @@ -0,0 +1,130 @@ +package serviceprincipals_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/manicminer/hamilton/odata" + + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type ServicePrincipalDelegatedPermissionGrantResource struct{} + +func TestAccServicePrincipalDelegatedPermissionGrant_allUsers(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal_delegated_permission_grant", "test") + r := ServicePrincipalDelegatedPermissionGrantResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.allUsers(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func TestAccServicePrincipalDelegatedPermissionGrant_singleUser(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal_delegated_permission_grant", "test") + r := ServicePrincipalDelegatedPermissionGrantResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.singleUser(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + +func (r ServicePrincipalDelegatedPermissionGrantResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { + client := clients.ServicePrincipals.DelegatedPermissionGrantsClient + client.BaseClient.DisableRetries = true + + if _, status, err := client.Get(ctx, state.ID, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return nil, fmt.Errorf("Delegated Permission Grant with ID %q does not exist", state.ID) + } + return nil, fmt.Errorf("failed to retrieve Delegated Permission Grant with ID %q: %+v", state.ID, err) + } + + return utils.Bool(true), nil +} + +func (r ServicePrincipalDelegatedPermissionGrantResource) template(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +data "azuread_application_published_app_ids" "well_known" {} + +resource "azuread_service_principal" "msgraph" { + application_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + use_existing = true +} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + + required_resource_access { + resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["openid"] + type = "Scope" + } + + resource_access { + id = azuread_service_principal.msgraph.oauth2_permission_scope_ids["User.Read"] + type = "Scope" + } + } +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id +} +`, data.RandomInteger) +} + +func (r ServicePrincipalDelegatedPermissionGrantResource) allUsers(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_service_principal_delegated_permission_grant" "test" { + service_principal_object_id = azuread_service_principal.test.object_id + resource_service_principal_object_id = azuread_service_principal.msgraph.object_id + claim_values = ["openid", "User.Read.All"] +} +`, r.template(data)) +} + +func (r ServicePrincipalDelegatedPermissionGrantResource) singleUser(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_user" "test" { + user_principal_name = "acctestUser'%[2]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[2]d" + password = "%[3]s" +} + +resource "azuread_service_principal_delegated_permission_grant" "test" { + service_principal_object_id = azuread_service_principal.test.object_id + resource_service_principal_object_id = azuread_service_principal.msgraph.object_id + claim_values = ["openid", "User.Read.All"] +} +`, r.template(data), data.RandomInteger, data.RandomPassword) +} From 786d21fb02d14d259733ad93f410567467fc6877 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Wed, 24 Nov 2021 00:50:21 +0000 Subject: [PATCH 3/3] goimports --- ...ce_principal_delegated_permission_grant_resource.go | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go b/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go index a8b596a7e1..9b29d2554d 100644 --- a/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go +++ b/internal/services/serviceprincipals/service_principal_delegated_permission_grant_resource.go @@ -7,16 +7,14 @@ import ( "net/http" "time" - "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/manicminer/hamilton/msgraph" - - "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" )