From 13abffb10b064f39be4f957bd9aaa4970d09c9f1 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Fri, 13 Aug 2021 17:47:11 +0100 Subject: [PATCH 01/11] Update Hamilton to v0.24.0 --- go.mod | 2 +- go.sum | 4 +- .../manicminer/hamilton/auth/azcli.go | 6 +- .../hamilton/msgraph/app_role_assignments.go | 19 +- .../hamilton/msgraph/applications.go | 112 ++++++--- .../manicminer/hamilton/msgraph/client.go | 8 +- .../msgraph/conditionalaccesspolicy.go | 28 ++- .../msgraph/directory_audit_reports.go | 16 +- .../hamilton/msgraph/directory_objects.go | 210 +++++++++++++++++ .../msgraph/directory_role_templates.go | 16 +- .../hamilton/msgraph/directory_roles.go | 71 +++--- .../manicminer/hamilton/msgraph/domains.go | 16 +- .../manicminer/hamilton/msgraph/groups.go | 163 ++++++++----- .../hamilton/msgraph/identity_providers.go | 37 ++- .../hamilton/msgraph/invitations.go | 11 +- .../manicminer/hamilton/msgraph/me.go | 21 +- .../manicminer/hamilton/msgraph/models.go | 216 +++++++++++------- .../hamilton/msgraph/namedlocations.go | 65 ++++-- .../manicminer/hamilton/msgraph/reports.go | 199 ++++++++++++++++ .../hamilton/msgraph/schema_extensions.go | 29 ++- .../hamilton/msgraph/serviceprincipals.go | 114 ++++++--- .../hamilton/msgraph/sign_in_reports.go | 16 +- .../manicminer/hamilton/msgraph/users.go | 64 ++++-- .../manicminer/hamilton/msgraph/valuetypes.go | 138 ++++++++++- .../manicminer/hamilton/odata/odata.go | 58 ++++- vendor/modules.txt | 2 +- 26 files changed, 1299 insertions(+), 342 deletions(-) create mode 100644 vendor/github.com/manicminer/hamilton/msgraph/directory_objects.go create mode 100644 vendor/github.com/manicminer/hamilton/msgraph/reports.go diff --git a/go.mod b/go.mod index 64ff432a3e..d7ebe19ce3 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect github.com/klauspost/compress v1.12.2 // indirect - github.com/manicminer/hamilton v0.23.1 + github.com/manicminer/hamilton v0.24.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 628d2da13d..9f72058715 100644 --- a/go.sum +++ b/go.sum @@ -285,8 +285,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.23.1 h1:7p5WviHLT0VKG7DKs/PggsUeapA1w6fd8B19ah62+LY= -github.com/manicminer/hamilton v0.23.1/go.mod h1:4bnCX1oYiQuNa9CQnmT+WMHRNAEF4mT7ygW9Q/D3o+Y= +github.com/manicminer/hamilton v0.24.0 h1:KUa8+NYP61esmb6QKGPy7t4zrxB0sujhWDRhiKpoScE= +github.com/manicminer/hamilton v0.24.0/go.mod h1:QryxpD/4+cdKuXNi0UjLDvgxYdP0LLmYz7dYU7DAX4U= 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/auth/azcli.go b/vendor/github.com/manicminer/hamilton/auth/azcli.go index 14bf2fa562..4799516a4a 100644 --- a/vendor/github.com/manicminer/hamilton/auth/azcli.go +++ b/vendor/github.com/manicminer/hamilton/auth/azcli.go @@ -31,7 +31,6 @@ type AzureCliAuthorizer struct { // Token returns an access token using the Azure CLI as an authentication mechanism. func (a AzureCliAuthorizer) Token() (*oauth2.Token, error) { - // We don't need to handle token caching and refreshing since az-cli does that for us var token struct { AccessToken string `json:"accessToken"` ExpiresOn string `json:"expiresOn"` @@ -86,11 +85,12 @@ func NewAzureCliConfig(api Api, tenantId string) (*AzureCliConfig, error) { // TokenSource provides a source for obtaining access tokens using AzureCliAuthorizer. func (c *AzureCliConfig) TokenSource(ctx context.Context) Authorizer { - return &AzureCliAuthorizer{ + // Cache access tokens internally to avoid unnecessary `az` invocations + return CachedAuthorizer(AzureCliAuthorizer{ TenantID: c.TenantID, ctx: ctx, conf: c, - } + }) } // checkAzVersion tries to determine the version of Azure CLI in the path and checks for a compatible version diff --git a/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go b/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go index f6491f46ca..8ac0d0a4fa 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/app_role_assignments.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -58,17 +58,20 @@ func (c *AppRoleAssignmentsClient) List(ctx context.Context, id string) (*[]AppR if err != nil { return nil, status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { AppRoleAssignments []AppRoleAssignment `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.AppRoleAssignments, status, nil } @@ -85,12 +88,14 @@ func (c *AppRoleAssignmentsClient) Remove(ctx context.Context, id, appRoleAssign if err != nil { return status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Delete(): %v", err) } + return status, nil } // Assign assigns an app role to a user, group or service principal depending on client resource type. func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, clientServicePrincipalId, resourceServicePrincipalId, appRoleId string) (*AppRoleAssignment, int, error) { var status int + data := struct { PrincipalId string `json:"principalId"` ResourceId string `json:"resourceId"` @@ -105,6 +110,7 @@ func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, clientServicePrin if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -117,14 +123,17 @@ func (c *AppRoleAssignmentsClient) Assign(ctx context.Context, clientServicePrin if err != nil { return nil, status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var appRoleAssignment AppRoleAssignment if err := json.Unmarshal(respBody, &appRoleAssignment); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &appRoleAssignment, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/applications.go b/vendor/github.com/manicminer/hamilton/msgraph/applications.go index 0c0a9cb91c..a7effa11ef 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/applications.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/applications.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -37,27 +37,32 @@ func (c *ApplicationsClient) List(ctx context.Context, query odata.Query) (*[]Ap if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Applications []Application `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Applications, status, nil } // Create creates a new Application. func (c *ApplicationsClient) Create(ctx context.Context, application Application) (*Application, int, error) { var status int + body, err := json.Marshal(application) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -69,15 +74,18 @@ func (c *ApplicationsClient) Create(ctx context.Context, application Application if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newApplication Application if err := json.Unmarshal(respBody, &newApplication); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newApplication, status, nil } @@ -95,15 +103,18 @@ func (c *ApplicationsClient) Get(ctx context.Context, id string, query odata.Que if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var application Application if err := json.Unmarshal(respBody, &application); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &application, status, nil } @@ -122,28 +133,34 @@ func (c *ApplicationsClient) GetDeleted(ctx context.Context, id string, query od if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var application Application if err := json.Unmarshal(respBody, &application); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &application, status, nil } // Update amends the manifest of an existing Application. func (c *ApplicationsClient) Update(ctx context.Context, application Application) (int, error) { var status int + if application.ID == nil { return status, errors.New("ApplicationsClient.Update(): cannot update application with nil ID") } + body, err := json.Marshal(application) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -156,6 +173,7 @@ func (c *ApplicationsClient) Update(ctx context.Context, application Application if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -172,6 +190,7 @@ func (c *ApplicationsClient) Delete(ctx context.Context, id string) (int, error) if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -189,6 +208,7 @@ func (c *ApplicationsClient) DeletePermanently(ctx context.Context, id string) ( if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -206,14 +226,16 @@ func (c *ApplicationsClient) ListDeleted(ctx context.Context, query odata.Query) if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { DeletedApps []Application `json:"value"` } if err = json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + return &data.DeletedApps, status, nil } @@ -231,21 +253,25 @@ func (c *ApplicationsClient) RestoreDeleted(ctx context.Context, id string) (*Ap if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var restoredApplication Application if err = json.Unmarshal(respBody, &restoredApplication); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &restoredApplication, status, nil } // AddPassword appends a new password credential to an Application. func (c *ApplicationsClient) AddPassword(ctx context.Context, applicationId string, passwordCredential PasswordCredential) (*PasswordCredential, int, error) { var status int + body, err := json.Marshal(struct { PwdCredential PasswordCredential `json:"passwordCredential"` }{ @@ -254,6 +280,7 @@ func (c *ApplicationsClient) AddPassword(ctx context.Context, applicationId stri if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -266,21 +293,25 @@ func (c *ApplicationsClient) AddPassword(ctx context.Context, applicationId stri if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newPasswordCredential PasswordCredential if err := json.Unmarshal(respBody, &newPasswordCredential); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newPasswordCredential, status, nil } // RemovePassword removes a password credential from an Application. func (c *ApplicationsClient) RemovePassword(ctx context.Context, applicationId string, keyId string) (int, error) { var status int + body, err := json.Marshal(struct { KeyId string `json:"keyId"` }{ @@ -289,6 +320,7 @@ func (c *ApplicationsClient) RemovePassword(ctx context.Context, applicationId s if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -301,6 +333,7 @@ func (c *ApplicationsClient) RemovePassword(ctx context.Context, applicationId s if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + return status, nil } @@ -319,11 +352,13 @@ func (c *ApplicationsClient) ListOwners(ctx context.Context, id string) (*[]stri if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Owners []struct { Type string `json:"@odata.type"` @@ -333,10 +368,12 @@ func (c *ApplicationsClient) ListOwners(ctx context.Context, id string) (*[]stri if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Owners)) for i, v := range data.Owners { ret[i] = v.Id } + return &ret, status, nil } @@ -356,11 +393,13 @@ func (c *ApplicationsClient) GetOwner(ctx context.Context, applicationId, ownerI if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -370,37 +409,36 @@ func (c *ApplicationsClient) GetOwner(ctx context.Context, applicationId, ownerI if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddOwners adds a new owner to an Application. -// First populate the Owners field of the Application using the AppendOwner method of the model, then call this method. +// AddOwners adds new owners to an Application. +// First populate the `owners` field, then call this method func (c *ApplicationsClient) AddOwners(ctx context.Context, application *Application) (int, error) { var status int + if application.ID == nil { return status, errors.New("cannot update application with nil ID") } if application.Owners == nil { return status, errors.New("cannot update application with nil Owners") } + for _, owner := range *application.Owners { // don't fail if an owner already exists checkOwnerAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Owner string `json:"@odata.id"` - }{ - Owner: owner, - } - body, err := json.Marshal(data) + body, err := json.Marshal(DirectoryObject{ODataId: owner.ODataId}) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -415,6 +453,7 @@ func (c *ApplicationsClient) AddOwners(ctx context.Context, application *Applica return status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -423,9 +462,11 @@ func (c *ApplicationsClient) AddOwners(ctx context.Context, application *Applica // ownerIds is a *[]string containing object IDs of owners to remove. func (c *ApplicationsClient) RemoveOwners(ctx context.Context, applicationId string, ownerIds *[]string) (int, error) { var status int + if ownerIds == nil { return status, errors.New("cannot remove, nil ownerIds") } + for _, ownerId := range *ownerIds { // check for ownership before attempting deletion if _, status, err := c.GetOwner(ctx, applicationId, ownerId); err != nil { @@ -437,7 +478,7 @@ func (c *ApplicationsClient) RemoveOwners(ctx context.Context, applicationId str // despite the above check, sometimes owners are just gone checkOwnerGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -457,6 +498,7 @@ func (c *ApplicationsClient) RemoveOwners(ctx context.Context, applicationId str return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -473,10 +515,11 @@ func (c *ApplicationsClient) ListExtensions(ctx context.Context, id string, quer if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.List(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var data struct { @@ -485,16 +528,19 @@ func (c *ApplicationsClient) ListExtensions(ctx context.Context, id string, quer if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.ApplicationExtension, status, nil } // Create creates a new ApplicationExtension. func (c *ApplicationsClient) CreateExtension(ctx context.Context, applicationExtension ApplicationExtension, id string) (*ApplicationExtension, int, error) { var status int + body, err := json.Marshal(applicationExtension) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -506,10 +552,11 @@ func (c *ApplicationsClient) CreateExtension(ctx context.Context, applicationExt if err != nil { return nil, status, fmt.Errorf("ApplicationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var newApplicationExtension ApplicationExtension @@ -533,5 +580,6 @@ func (c *ApplicationsClient) DeleteExtension(ctx context.Context, applicationId, if err != nil { return status, fmt.Errorf("ApplicationsClient.BaseClient.Delete(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/client.go b/vendor/github.com/manicminer/hamilton/msgraph/client.go index 8610bba1de..1d56f08dbc 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/client.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/client.go @@ -35,7 +35,7 @@ type ResponseMiddleware func(*http.Request, *http.Response) (*http.Response, err // RetryOn404ConsistencyFailureFunc can be used to retry a request when a 404 response is received func RetryOn404ConsistencyFailureFunc(resp *http.Response, _ *odata.OData) bool { - return resp.StatusCode == http.StatusNotFound + return resp != nil && resp.StatusCode == http.StatusNotFound } // ValidStatusFunc is a function that tests whether an HTTP response is considered valid for the particular request. @@ -85,7 +85,7 @@ type Client struct { // HttpClient is the underlying http.Client, which by default uses a retryable client HttpClient *http.Client - retryableClient *retryablehttp.Client + RetryableClient *retryablehttp.Client } // NewClient returns a new Client configured with the specified API version and tenant ID. @@ -99,7 +99,7 @@ func NewClient(apiVersion ApiVersion, tenantId string) Client { TenantId: tenantId, UserAgent: "Hamilton (Go-http-client/1.1)", HttpClient: r.StandardClient(), - retryableClient: r, + RetryableClient: r, } } @@ -152,7 +152,7 @@ func (c Client) performRequest(req *http.Request, input HttpRequestInput) (*http } } - c.retryableClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { + c.RetryableClient.CheckRetry = func(ctx context.Context, resp *http.Response, err error) (bool, error) { if resp != nil && !c.DisableRetries { if resp.StatusCode == http.StatusFailedDependency { return true, nil diff --git a/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go b/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go index 99f95d0c80..0c99d3a2a8 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/conditionalaccesspolicy.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -37,17 +37,20 @@ func (c *ConditionalAccessPolicyClient) List(ctx context.Context, query odata.Qu if err != nil { return nil, status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { ConditionalAccessPolicys []ConditionalAccessPolicy `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.ConditionalAccessPolicys, status, nil } @@ -58,6 +61,7 @@ func (c *ConditionalAccessPolicyClient) Create(ctx context.Context, conditionalA if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -69,15 +73,18 @@ func (c *ConditionalAccessPolicyClient) Create(ctx context.Context, conditionalA if err != nil { return nil, status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newConditionalAccessPolicy ConditionalAccessPolicy if err := json.Unmarshal(respBody, &newConditionalAccessPolicy); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newConditionalAccessPolicy, status, nil } @@ -95,21 +102,25 @@ func (c *ConditionalAccessPolicyClient) Get(ctx context.Context, id string, quer if err != nil { return nil, status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var conditionalAccessPolicy ConditionalAccessPolicy if err := json.Unmarshal(respBody, &conditionalAccessPolicy); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &conditionalAccessPolicy, status, nil } // Update amends an existing ConditionalAccessPolicy. func (c *ConditionalAccessPolicyClient) Update(ctx context.Context, conditionalAccessPolicy ConditionalAccessPolicy) (int, error) { var status int + if conditionalAccessPolicy.ID == nil { return status, errors.New("cannot update conditionalAccessPolicy with nil ID") } @@ -118,6 +129,7 @@ func (c *ConditionalAccessPolicyClient) Update(ctx context.Context, conditionalA if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -130,6 +142,7 @@ func (c *ConditionalAccessPolicyClient) Update(ctx context.Context, conditionalA if err != nil { return status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -146,5 +159,6 @@ func (c *ConditionalAccessPolicyClient) Delete(ctx context.Context, id string) ( if err != nil { return status, fmt.Errorf("ConditionalAccessPolicyClient.BaseClient.Delete(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go index 203b9a2fe1..38cb78b02b 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_audit_reports.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,17 +36,20 @@ func (c *DirectoryAuditReportsClient) List(ctx context.Context, query odata.Quer if err != nil { return nil, status, fmt.Errorf("DirectoryAuditReportsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { DirectoryAuditReports []DirectoryAudit `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.DirectoryAuditReports, status, nil } @@ -64,14 +67,17 @@ func (c *DirectoryAuditReportsClient) Get(ctx context.Context, id string, query if err != nil { return nil, status, fmt.Errorf("DirectoryAuditReportsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var directoryAuditReport DirectoryAudit if err := json.Unmarshal(respBody, &directoryAuditReport); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &directoryAuditReport, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_objects.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_objects.go new file mode 100644 index 0000000000..f9183fd7cf --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_objects.go @@ -0,0 +1,210 @@ +package msgraph + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/internal/utils" + "github.com/manicminer/hamilton/odata" +) + +// DirectoryObjectsClient performs operations on Directory Objects (the base type for other objects such as users and groups) +type DirectoryObjectsClient struct { + BaseClient Client +} + +// NewDirectoryObjectsClient returns a new DirectoryObjectsClient. +func NewDirectoryObjectsClient(tenantId string) *DirectoryObjectsClient { + return &DirectoryObjectsClient{ + BaseClient: NewClient(Version10, tenantId), + } +} + +// Get retrieves a DirectoryObject. +func (c *DirectoryObjectsClient) Get(ctx context.Context, id string, query odata.Query) (*DirectoryObject, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directoryObjects/%s", id), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjects.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 directoryObject DirectoryObject + if err := json.Unmarshal(respBody, &directoryObject); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &directoryObject, status, nil +} + +// GetByIds retrieves multiple DirectoryObjects from a list of IDs. +func (c *DirectoryObjectsClient) GetByIds(ctx context.Context, ids []string, types []odata.ShortType) (*[]DirectoryObject, int, error) { + var status int + + body, err := json.Marshal(struct { + IDs []string `json:"ids"` + Types []odata.Type `json:"types"` + }{ + IDs: ids, + Types: types, + }) + 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.StatusOK}, + Uri: Uri{ + Entity: "/directoryObjects/getByIds", + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjects.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 struct { + Objects []DirectoryObject `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.Objects, status, nil +} + +// Delete removes a DirectoryObject. +func (c *DirectoryObjectsClient) 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("/directoryObjects/%s", id), + HasTenantId: true, + }, + }) + if err != nil { + return status, fmt.Errorf("DirectoryObjects.BaseClient.Get(): %v", err) + } + + return status, nil +} + +// GetMemberGroups retrieves IDs of the groups and directory roles that a directory object is a member of. +// id is the object ID of the directory object. +func (c *DirectoryObjectsClient) GetMemberGroups(ctx context.Context, id string, securityEnabledOnly bool) (*[]DirectoryObject, int, error) { + var status int + + body, err := json.Marshal(struct { + SecurityEnabledOnly bool `json:"securityEnabledOnly"` + }{ + SecurityEnabledOnly: securityEnabledOnly, + }) + 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.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directoryObjects/%s/getMemberGroups", id), + HasTenantId: false, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjectsClient.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 struct { + IDs []string `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + result := make([]DirectoryObject, len(data.IDs)) + for i, id := range data.IDs { + result[i].ID = utils.StringPtr(id) + } + + return &result, status, nil +} + +// GetMemberObjects retrieves IDs of the groups and directory roles that a directory object is a member of. +// id is the object ID of the directory object. +func (c *DirectoryObjectsClient) GetMemberObjects(ctx context.Context, id string, securityEnabledOnly bool) (*[]DirectoryObject, int, error) { + var status int + + body, err := json.Marshal(struct { + SecurityEnabledOnly bool `json:"securityEnabledOnly"` + }{ + SecurityEnabledOnly: securityEnabledOnly, + }) + 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.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/directoryObjects/%s/getMemberObjects", id), + HasTenantId: false, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("DirectoryObjectsClient.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 struct { + IDs []string `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + result := make([]DirectoryObject, len(data.IDs)) + for i, id := range data.IDs { + result[i].ID = utils.StringPtr(id) + } + + return &result, status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go index cf9c7464a0..ab95032b4d 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_role_templates.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -32,17 +32,20 @@ func (c *DirectoryRoleTemplatesClient) List(ctx context.Context) (*[]DirectoryRo if err != nil { return nil, status, fmt.Errorf("DirectoryRoleTemplatesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { DirectoryRoleTemplates []DirectoryRoleTemplate `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.DirectoryRoleTemplates, status, nil } @@ -59,14 +62,17 @@ func (c *DirectoryRoleTemplatesClient) Get(ctx context.Context, id string) (*Dir if err != nil { return nil, status, fmt.Errorf("DirectoryRoleTemplatesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var dirRoleTemplate DirectoryRoleTemplate if err := json.Unmarshal(respBody, &dirRoleTemplate); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &dirRoleTemplate, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go b/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go index 1a9c9c9d98..5612da1d50 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/directory_roles.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -35,17 +35,20 @@ func (c *DirectoryRolesClient) List(ctx context.Context) (*[]DirectoryRole, int, if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { DirectoryRoles []DirectoryRole `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.DirectoryRoles, status, nil } @@ -61,15 +64,18 @@ func (c *DirectoryRolesClient) Get(ctx context.Context, id string) (*DirectoryRo if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var dirRole DirectoryRole if err := json.Unmarshal(respBody, &dirRole); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &dirRole, status, nil } @@ -87,11 +93,13 @@ func (c *DirectoryRolesClient) ListMembers(ctx context.Context, id string) (*[]s if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Members []struct { Type string `json:"@odata.type"` @@ -101,43 +109,45 @@ func (c *DirectoryRolesClient) ListMembers(ctx context.Context, id string) (*[]s 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 } -// AddMembers adds a new member to a Directory Role. -// First populate the Members field of the DirectoryRole using the AppendMember method of the model, then call this method. +// AddMembers adds new members to a Directory Role. +// First populate the `members` field, then call this method func (c *DirectoryRolesClient) AddMembers(ctx context.Context, directoryRole *DirectoryRole) (int, error) { var status int + if directoryRole.ID == nil { return status, errors.New("cannot update directory role with nil ID") } if directoryRole.Members == nil { return status, errors.New("cannot update directory role with nil Owners") } + for _, member := range *directoryRole.Members { // don't fail if a member already exists checkMemberAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest { - if o.Error != nil { - return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) - } + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { + return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Member string `json:"@odata.id"` + body, err := json.Marshal(struct { + Member odata.Id `json:"@odata.id"` }{ - Member: member, - } - body, err := json.Marshal(data) + Member: *member.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusNoContent}, @@ -151,6 +161,7 @@ func (c *DirectoryRolesClient) AddMembers(ctx context.Context, directoryRole *Di return status, fmt.Errorf("DirectoryRolesClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -159,9 +170,11 @@ func (c *DirectoryRolesClient) AddMembers(ctx context.Context, directoryRole *Di // memberIds is a *[]string containing object IDs of members to remove. func (c *DirectoryRolesClient) RemoveMembers(ctx context.Context, directoryRoleId string, memberIds *[]string) (int, error) { var status int + if memberIds == nil { return status, errors.New("cannot remove, nil memberIds") } + for _, memberId := range *memberIds { // check for membership before attempting deletion if _, status, err := c.GetMember(ctx, directoryRoleId, memberId); err != nil { @@ -170,6 +183,7 @@ func (c *DirectoryRolesClient) RemoveMembers(ctx context.Context, directoryRoleI } return status, err } + var err error _, status, _, err = c.BaseClient.Delete(ctx, DeleteHttpRequestInput{ ValidStatusCodes: []int{http.StatusNoContent}, @@ -182,6 +196,7 @@ func (c *DirectoryRolesClient) RemoveMembers(ctx context.Context, directoryRoleI return status, fmt.Errorf("DirectoryRolesClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -200,11 +215,13 @@ func (c *DirectoryRolesClient) GetMember(ctx context.Context, directoryRoleId, m if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -214,6 +231,7 @@ func (c *DirectoryRolesClient) GetMember(ctx context.Context, directoryRoleId, m if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } @@ -224,10 +242,8 @@ func (c *DirectoryRolesClient) Activate(ctx context.Context, roleTemplateID stri // don't fail if a role is already activated checkRoleAlreadyActivated := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest { - if o.Error != nil { - return o.Error.Match(odata.ErrorConflictingObjectPresentInDirectory) - } + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { + return o.Error.Match(odata.ErrorConflictingObjectPresentInDirectory) } return false } @@ -254,14 +270,17 @@ func (c *DirectoryRolesClient) Activate(ctx context.Context, roleTemplateID stri if err != nil { return nil, status, fmt.Errorf("DirectoryRolesClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newDirRole DirectoryRole if err := json.Unmarshal(respBody, &newDirRole); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newDirRole, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/domains.go b/vendor/github.com/manicminer/hamilton/msgraph/domains.go index 35caff51a6..7a14c78b8f 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/domains.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/domains.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -33,21 +33,19 @@ func (c *DomainsClient) List(ctx context.Context, query odata.Query) (*[]Domain, HasTenantId: true, }, }) - if err != nil { return nil, status, fmt.Errorf("DomainsClient.BaseClient.Get(): %v", err) } defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var data struct { Domains []Domain `json:"value"` } - if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } @@ -58,6 +56,7 @@ func (c *DomainsClient) List(ctx context.Context, query odata.Query) (*[]Domain, // Get retrieves a Domain. func (c *DomainsClient) Get(ctx context.Context, id string, query odata.Query) (*Domain, int, error) { var status int + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, ValidStatusCodes: []int{http.StatusOK}, @@ -70,14 +69,17 @@ func (c *DomainsClient) Get(ctx context.Context, id string, query odata.Query) ( if err != nil { return nil, status, fmt.Errorf("DomainsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var domain Domain if err := json.Unmarshal(respBody, &domain); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &domain, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/groups.go b/vendor/github.com/manicminer/hamilton/msgraph/groups.go index a2d1f6ebcd..816ee9f194 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/groups.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/groups.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,30 +36,40 @@ func (c *GroupsClient) List(ctx context.Context, query odata.Query) (*[]Group, i if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Groups []Group `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Groups, status, nil } // Create creates a new Group. func (c *GroupsClient) Create(ctx context.Context, group Group) (*Group, int, error) { var status int + body, err := json.Marshal(group) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + + ownersNotReplicated := func(resp *http.Response, o *odata.OData) bool { + return o != nil && o.Error != nil && o.Error.Match(odata.ErrorResourceDoesNotExist) + } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ - Body: body, - ValidStatusCodes: []int{http.StatusCreated}, + Body: body, + ConsistencyFailureFunc: ownersNotReplicated, + ValidStatusCodes: []int{http.StatusCreated}, Uri: Uri{ Entity: "/groups", HasTenantId: true, @@ -68,15 +78,18 @@ func (c *GroupsClient) Create(ctx context.Context, group Group) (*Group, int, er if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newGroup Group if err := json.Unmarshal(respBody, &newGroup); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newGroup, status, nil } @@ -94,15 +107,18 @@ func (c *GroupsClient) Get(ctx context.Context, id string, query odata.Query) (* if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var group Group if err := json.Unmarshal(respBody, &group); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &group, status, nil } @@ -136,16 +152,18 @@ func (c *GroupsClient) GetWithSchemaExtensions(ctx context.Context, id string, q if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } group.SchemaExtensions = schemaExtensions if err := json.Unmarshal(respBody, group); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return group, status, nil } @@ -163,25 +181,30 @@ func (c *GroupsClient) GetDeleted(ctx context.Context, id string, query odata.Qu if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var group Group if err := json.Unmarshal(respBody, &group); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &group, status, nil } // Update amends an existing Group. func (c *GroupsClient) Update(ctx context.Context, group Group) (int, error) { var status int + body, err := json.Marshal(group) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -194,6 +217,7 @@ func (c *GroupsClient) Update(ctx context.Context, group Group) (int, error) { if err != nil { return status, fmt.Errorf("GroupsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -210,6 +234,7 @@ func (c *GroupsClient) Delete(ctx context.Context, id string) (int, error) { if err != nil { return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -226,6 +251,7 @@ func (c *GroupsClient) DeletePermanently(ctx context.Context, id string) (int, e if err != nil { return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -243,14 +269,16 @@ func (c *GroupsClient) ListDeleted(ctx context.Context, query odata.Query) (*[]G if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { DeletedGroups []Group `json:"value"` } if err = json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + return &data.DeletedGroups, status, nil } @@ -267,15 +295,18 @@ func (c *GroupsClient) RestoreDeleted(ctx context.Context, id string) (*Group, i if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var restoredGroup Group if err = json.Unmarshal(respBody, &restoredGroup); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &restoredGroup, status, nil } @@ -294,11 +325,13 @@ func (c *GroupsClient) ListMembers(ctx context.Context, id string) (*[]string, i if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Members []struct { Type string `json:"@odata.type"` @@ -308,10 +341,12 @@ func (c *GroupsClient) ListMembers(ctx context.Context, id string) (*[]string, i 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 } @@ -331,11 +366,13 @@ func (c *GroupsClient) GetMember(ctx context.Context, groupId, memberId string) if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -345,58 +382,52 @@ func (c *GroupsClient) GetMember(ctx context.Context, groupId, memberId string) if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddMembers adds a new member to a Group. -// First populate the Members field of the Group using the AppendMember method of the model, then call this method. +// AddMembers adds new members to a Group. +// First populate the `members` field, then call this method func (c *GroupsClient) AddMembers(ctx context.Context, group *Group) (int, error) { var status int - // Patching group members support up to 20 members per request - var memberChunks [][]string + if group.Members == nil || len(*group.Members) == 0 { return status, fmt.Errorf("no members specified") } - members := *group.Members - max := len(members) - // Chunk into slices of 20 for batching - for i := 0; i < max; i += 20 { - end := i + 20 - if end > max { - end = max - } - memberChunks = append(memberChunks, members[i:end]) - } - for _, members := range memberChunks { - // don't fail if a member already exists + + for _, member := range *group.Members { + // don't fail if an member already exists checkMemberAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := Group{ - Members: &members, - } - body, err := json.Marshal(data) + body, err := json.Marshal(struct { + Member odata.Id `json:"@odata.id"` + }{ + Member: *member.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } - _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ + + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, ValidStatusCodes: []int{http.StatusNoContent}, ValidStatusFunc: checkMemberAlreadyExists, Uri: Uri{ - Entity: fmt.Sprintf("/groups/%s", *group.ID), + Entity: fmt.Sprintf("/groups/%s/members/$ref", *group.ID), HasTenantId: true, }, }) if err != nil { - return status, fmt.Errorf("GroupsClient.BaseClient.Patch(): %v", err) + return status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -405,9 +436,11 @@ func (c *GroupsClient) AddMembers(ctx context.Context, group *Group) (int, error // memberIds is a *[]string containing object IDs of members to remove. func (c *GroupsClient) RemoveMembers(ctx context.Context, id 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, id, memberId); err != nil { @@ -419,7 +452,7 @@ func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds * // despite the above check, sometimes members are just gone checkMemberGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -439,6 +472,7 @@ func (c *GroupsClient) RemoveMembers(ctx context.Context, id string, memberIds * return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -457,11 +491,13 @@ func (c *GroupsClient) ListOwners(ctx context.Context, id string) (*[]string, in if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Owners []struct { Type string `json:"@odata.type"` @@ -471,10 +507,12 @@ func (c *GroupsClient) ListOwners(ctx context.Context, id string) (*[]string, in if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Owners)) for i, v := range data.Owners { ret[i] = v.Id } + return &ret, status, nil } @@ -494,11 +532,13 @@ func (c *GroupsClient) GetOwner(ctx context.Context, groupId, ownerId string) (* if err != nil { return nil, status, fmt.Errorf("GroupsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -508,34 +548,37 @@ func (c *GroupsClient) GetOwner(ctx context.Context, groupId, ownerId string) (* if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddOwners adds a new owner to a Group. -// First populate the Owners field of the Group using the AppendOwner method of the model, then call this method. +// AddOwners adds new owners to a Group. +// First populate the `owners` field, then call this method func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) { var status int + if group.Owners == nil || len(*group.Owners) == 0 { return status, fmt.Errorf("no owners specified") } + for _, owner := range *group.Owners { // don't fail if an owner already exists checkOwnerAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Owner string `json:"@odata.id"` + body, err := json.Marshal(struct { + Owner odata.Id `json:"@odata.id"` }{ - Owner: owner, - } - body, err := json.Marshal(data) + Owner: *owner.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -550,6 +593,7 @@ func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) return status, fmt.Errorf("GroupsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -558,9 +602,11 @@ func (c *GroupsClient) AddOwners(ctx context.Context, group *Group) (int, error) // ownerIds is a *[]string containing object IDs of owners to remove. func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[]string) (int, error) { var status int + if ownerIds == nil || len(*ownerIds) == 0 { return status, fmt.Errorf("no owners specified") } + for _, ownerId := range *ownerIds { // check for ownership before attempting deletion if _, status, err := c.GetOwner(ctx, id, ownerId); err != nil { @@ -572,7 +618,7 @@ func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[] // despite the above check, sometimes owners are just gone checkOwnerGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -592,5 +638,6 @@ func (c *GroupsClient) RemoveOwners(ctx context.Context, id string, ownerIds *[] return status, fmt.Errorf("GroupsClient.BaseClient.Delete(): %v", err) } } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go b/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go index 79652e9174..734cc747a4 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/identity_providers.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -33,27 +33,32 @@ func (c *IdentityProvidersClient) List(ctx context.Context) (*[]IdentityProvider if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { IdentityProviders []IdentityProvider `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.IdentityProviders, status, nil } // Create creates a new IdentityProvider. func (c *IdentityProvidersClient) Create(ctx context.Context, provider IdentityProvider) (*IdentityProvider, int, error) { var status int + body, err := json.Marshal(provider) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -65,15 +70,18 @@ func (c *IdentityProvidersClient) Create(ctx context.Context, provider IdentityP if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newProvider IdentityProvider if err := json.Unmarshal(respBody, &newProvider); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newProvider, status, nil } @@ -90,28 +98,34 @@ func (c *IdentityProvidersClient) Get(ctx context.Context, id string) (*Identity if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var provider IdentityProvider if err := json.Unmarshal(respBody, &provider); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &provider, status, nil } // Update amends an existing IdentityProvider. func (c *IdentityProvidersClient) Update(ctx context.Context, provider IdentityProvider) (int, error) { var status int + if provider.ID == nil { return status, errors.New("IdentityProvidersClient.Update(): cannot update identity provider with nil ID") } + body, err := json.Marshal(provider) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -124,6 +138,7 @@ func (c *IdentityProvidersClient) Update(ctx context.Context, provider IdentityP if err != nil { return status, fmt.Errorf("IdentityProvidersClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -140,6 +155,7 @@ func (c *IdentityProvidersClient) Delete(ctx context.Context, id string) (int, e if err != nil { return status, fmt.Errorf("IdentityProvidersClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -155,16 +171,19 @@ func (c *IdentityProvidersClient) ListAvailableProviderTypes(ctx context.Context if err != nil { return nil, status, fmt.Errorf("IdentityProvidersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { IdentityProviderTypes []string `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.IdentityProviderTypes, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/invitations.go b/vendor/github.com/manicminer/hamilton/msgraph/invitations.go index 26b2ddd439..82859f00e4 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/invitations.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/invitations.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" ) @@ -23,10 +23,12 @@ func NewInvitationsClient(tenantId string) *InvitationsClient { // Create creates a new Invitation. func (c *InvitationsClient) Create(ctx context.Context, invitation Invitation) (*Invitation, int, error) { var status int + body, err := json.Marshal(invitation) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -38,14 +40,17 @@ func (c *InvitationsClient) Create(ctx context.Context, invitation Invitation) ( if err != nil { return nil, status, fmt.Errorf("InvitationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newInvitation Invitation if err := json.Unmarshal(respBody, &newInvitation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newInvitation, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/me.go b/vendor/github.com/manicminer/hamilton/msgraph/me.go index 7e3903238e..40b3970f8b 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/me.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/me.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -25,6 +25,7 @@ func NewMeClient(tenantId string) *MeClient { // Get retrieves information about the authenticated user. func (c *MeClient) Get(ctx context.Context, query odata.Query) (*Me, int, error) { var status int + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, Uri: Uri{ @@ -36,21 +37,25 @@ func (c *MeClient) Get(ctx context.Context, query odata.Query) (*Me, int, error) if err != nil { return nil, status, fmt.Errorf("MeClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var me Me if err := json.Unmarshal(respBody, &me); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &me, status, nil } // GetProfile retrieves the profile of the authenticated user. func (c *MeClient) GetProfile(ctx context.Context, query odata.Query) (*Me, int, error) { var status int + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ ValidStatusCodes: []int{http.StatusOK}, Uri: Uri{ @@ -62,15 +67,18 @@ func (c *MeClient) GetProfile(ctx context.Context, query odata.Query) (*Me, int, if err != nil { return nil, status, fmt.Errorf("MeClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var me Me if err := json.Unmarshal(respBody, &me); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &me, status, nil } @@ -78,10 +86,12 @@ func (c *MeClient) GetProfile(ctx context.Context, query odata.Query) (*Me, int, // TODO: Needs testing with an O365 user principal func (c *MeClient) Sendmail(ctx context.Context, message MailMessage) (int, error) { var status int + body, err := json.Marshal(message) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusOK, http.StatusAccepted}, @@ -93,5 +103,6 @@ func (c *MeClient) Sendmail(ctx context.Context, message MailMessage) (int, erro if err != nil { return status, fmt.Errorf("MeClient.BaseClient.Post(): %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 2f648daf0d..57902cd868 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/models.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/models.go @@ -7,6 +7,8 @@ import ( "strings" "time" + "github.com/manicminer/hamilton/odata" + "github.com/manicminer/hamilton/environments" "github.com/manicminer/hamilton/errors" ) @@ -36,7 +38,9 @@ type AppIdentity struct { // Application describes an Application object. type Application struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Owners *Owners `json:"owners@odata.bind,omitempty"` + AddIns *[]AddIn `json:"addIns,omitempty"` Api *ApplicationApi `json:"api,omitempty"` AppId *string `json:"appId,omitempty"` @@ -46,7 +50,7 @@ type Application struct { DeletedDateTime *time.Time `json:"deletedDateTime,omitempty"` DisabledByMicrosoftStatus interface{} `json:"disabledByMicrosoftStatus,omitempty"` DisplayName *string `json:"displayName,omitempty"` - GroupMembershipClaims *[]GroupMembershipClaim `json:"groupMembershipClaims,omitempty"` + GroupMembershipClaims *[]GroupMembershipClaim `json:"-"` // see Application.MarshalJSON / Application.UnmarshalJSON IdentifierUris *[]string `json:"identifierUris,omitempty"` Info *InformationalUrl `json:"info,omitempty"` IsAuthorizationServiceEnabled *bool `json:"isAuthorizationServiceEnabled,omitempty"` @@ -69,8 +73,6 @@ type Application struct { UniqueName *string `json:"uniqueName,omitempty"` VerifiedPublisher *VerifiedPublisher `json:"verifiedPublisher,omitempty"` Web *ApplicationWeb `json:"web,omitempty"` - - Owners *[]string `json:"owners@odata.bind,omitempty"` } func (a Application) MarshalJSON() ([]byte, error) { @@ -83,25 +85,30 @@ func (a Application) MarshalJSON() ([]byte, error) { theClaims := StringNullWhenEmpty(strings.Join(claims, ",")) val = &theClaims } + + // Local type needed to avoid recursive MarshalJSON calls type application Application - return json.Marshal(&struct { + app := struct { GroupMembershipClaims *StringNullWhenEmpty `json:"groupMembershipClaims,omitempty"` *application }{ GroupMembershipClaims: val, application: (*application)(&a), - }) + } + buf, err := json.Marshal(&app) + return buf, err } func (a *Application) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls type application Application - app := &struct { + app := struct { GroupMembershipClaims *string `json:"groupMembershipClaims"` *application }{ application: (*application)(a), } - if err := json.Unmarshal(data, app); err != nil { + if err := json.Unmarshal(data, &app); err != nil { return err } if app.GroupMembershipClaims != nil { @@ -114,17 +121,6 @@ func (a *Application) UnmarshalJSON(data []byte) error { return nil } -// AppendOwner appends a new owner object URI to the Owners slice. -func (a *Application) AppendOwner(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var owners []string - if a.Owners != nil { - owners = *a.Owners - } - owners = append(owners, val) - a.Owners = &owners -} - // AppendAppRole adds a new AppRole to an Application, checking to see if it already exists. func (a *Application) AppendAppRole(role AppRole) error { if role.ID == nil { @@ -337,11 +333,11 @@ type AuditActivityInitiator struct { } type BaseNamedLocation struct { - ODataType *string `json:"@odata.type,omitempty"` - ID *string `json:"id,omitempty"` - DisplayName *string `json:"displayName,omitempty"` - CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` - ModifiedDateTime *time.Time `json:"modifiedDateTime,omitempty"` + ODataType *odata.Type `json:"@odata.type,omitempty"` + ID *string `json:"id,omitempty"` + DisplayName *string `json:"displayName,omitempty"` + CreatedDateTime *time.Time `json:"createdDateTime,omitempty"` + ModifiedDateTime *time.Time `json:"modifiedDateTime,omitempty"` } type CloudAppSecurityControl struct { @@ -417,6 +413,30 @@ type CountryNamedLocation struct { IncludeUnknownCountriesAndRegions *bool `json:"includeUnknownCountriesAndRegions,omitempty"` } +type CredentialUserRegistrationCount struct { + ID *string `json:"id,omitempty"` + TotalUserCount *int64 `json:"totalUserCount,omitempty"` + UserRegistrationCounts *[]UserRegistrationCount `json:"userRegistrationCounts,omitempty"` +} + +type CredentialUsageSummary struct { + AuthMethod *UsageAuthMethod `json:"usageAuthMethod,omitempty"` + FailureActivityCount *int64 `json:"failureActivityCount,omitempty"` + Feature *FeatureType `json:"feature,omitempty"` + ID *string `json:"id,omitempty"` + SuccessfulActivityCount *int64 `json:"successfulActivityCount,omitempty"` +} +type CredentialUserRegistrationDetails struct { + AuthMethods *[]RegistrationAuthMethod `json:"authMethods,omitempty"` + ID *string `json:"id,omitempty"` + IsCapable *bool `json:"isCapable,omitempty"` + IsEnabled *bool `json:"isEnabled,omitempty"` + IsMfaRegistered *bool `json:"isMfaRegistered,omitempty"` + IsRegistered *bool `json:"isRegistered,omitempty"` + UserDisplayName *string `json:"userDisplayName,omitempty"` + UserPrincipalName *string `json:"UserPrincipalName,omitempty"` +} + type DeviceDetail struct { Browser *string `json:"browser,omitempty"` DeviceId *string `json:"deviceId,omitempty"` @@ -441,24 +461,36 @@ type DirectoryAudit struct { TargetResources *[]TargetResource `json:"targetResources,omitempty"` } +type DirectoryObject struct { + ODataId *odata.Id `json:"@odata.id,omitempty"` + ODataType *odata.Type `json:"@odata.type,omitempty"` + ID *string `json:"id,omitempty"` +} + +func (o *DirectoryObject) Uri(endpoint environments.ApiEndpoint, apiVersion ApiVersion) string { + if o.ID == nil { + return "" + } + return fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, *o.ID) +} + type DirectoryRole struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Members *Members `json:"-"` + Description *string `json:"description,omitempty"` DisplayName *string `json:"displayName,omitempty"` RoleTemplateId *string `json:"roleTemplateId,omitempty"` - - Members *[]string `json:"-"` } -// AppendMember appends a new member object URI to the Members slice. -func (d *DirectoryRole) AppendMember(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var members []string - if d.Members != nil { - members = *d.Members +func (r *DirectoryRole) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls + type directoryrole DirectoryRole + r2 := (*directoryrole)(r) + if err := json.Unmarshal(data, r2); err != nil { + return err } - members = append(members, val) - d.Members = &members + return nil } // DirectoryRoleTemplate describes a Directory Role Template. @@ -509,7 +541,11 @@ type GeoCoordinates struct { // Group describes a Group object. type Group struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Members *Members `json:"members@odata.bind,omitempty"` + Owners *Owners `json:"owners@odata.bind,omitempty"` + SchemaExtensions *[]SchemaExtensionData `json:"-"` + AllowExternalSenders *string `json:"allowExternalSenders,omitempty"` AssignedLabels *[]GroupAssignedLabel `json:"assignedLabels,omitempty"` AssignedLicenses *[]GroupAssignedLicense `json:"assignLicenses,omitempty"` @@ -550,17 +586,13 @@ type Group struct { UnseenCount *int `json:"unseenCount,omitempty"` Visibility *GroupVisibility `json:"visibility,omitempty"` IsAssignableToRole *bool `json:"isAssignableToRole,omitempty"` - - SchemaExtensions *[]SchemaExtensionData `json:"-"` - - Members *[]string `json:"members@odata.bind,omitempty"` - Owners *[]string `json:"owners@odata.bind,omitempty"` } func (g Group) MarshalJSON() ([]byte, error) { docs := make([][]byte, 0) + // Local type needed to avoid recursive MarshalJSON calls type group Group - d, err := json.Marshal(group(g)) + d, err := json.Marshal((*group)(&g)) if err != nil { return d, err } @@ -578,6 +610,7 @@ func (g Group) MarshalJSON() ([]byte, error) { } func (g *Group) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls type group Group g2 := (*group)(g) if err := json.Unmarshal(data, g2); err != nil { @@ -599,28 +632,6 @@ func (g *Group) UnmarshalJSON(data []byte) error { return nil } -// AppendMember appends a new member object URI to the Members slice. -func (g *Group) AppendMember(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var members []string - if g.Members != nil { - members = *g.Members - } - members = append(members, val) - g.Members = &members -} - -// AppendOwner appends a new owner object URI to the Owners slice. -func (g *Group) AppendOwner(endpoint environments.ApiEndpoint, apiVersion ApiVersion, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var owners []string - if g.Owners != nil { - owners = *g.Owners - } - owners = append(owners, val) - g.Owners = &owners -} - // HasTypes returns true if the group has all the specified GroupTypes func (g *Group) HasTypes(types []GroupType) bool { for _, t := range types { @@ -656,12 +667,12 @@ type GroupOnPremisesProvisioningError struct { } type IdentityProvider struct { - ODataType *string `json:"@odata.type,omitempty"` - ID *string `json:"id,omitempty"` - ClientId *string `json:"clientId,omitempty"` - ClientSecret *string `json:"clientSecret,omitempty"` - Type *string `json:"identityProviderType,omitempty"` - Name *string `json:"displayName,omitempty"` + ODataType *odata.Type `json:"@odata.type,omitempty"` + ID *string `json:"id,omitempty"` + ClientId *string `json:"clientId,omitempty"` + ClientSecret *string `json:"clientSecret,omitempty"` + Type *string `json:"identityProviderType,omitempty"` + Name *string `json:"displayName,omitempty"` } type ImplicitGrantSettings struct { @@ -899,7 +910,9 @@ func (se SchemaExtensionData) MarshalJSON() ([]byte, error) { // ServicePrincipal describes a Service Principal object. type ServicePrincipal struct { - ID *string `json:"id,omitempty"` + DirectoryObject + Owners *Owners `json:"-"` + AccountEnabled *bool `json:"accountEnabled,omitempty"` AddIns *[]AddIn `json:"addIns,omitempty"` AlternativeNames *[]string `json:"alternativeNames,omitempty"` @@ -933,19 +946,16 @@ type ServicePrincipal struct { Tags *[]string `json:"tags,omitempty"` TokenEncryptionKeyId *string `json:"tokenEncryptionKeyId,omitempty"` VerifiedPublisher *VerifiedPublisher `json:"verifiedPublisher,omitempty"` - - Owners *[]string `json:"owners@odata.bind,omitempty"` } -// AppendOwner appends a new owner object URI to the Owners slice. -func (a *ServicePrincipal) AppendOwner(endpoint string, apiVersion string, id string) { - val := fmt.Sprintf("%s/%s/directoryObjects/%s", endpoint, apiVersion, id) - var owners []string - if a.Owners != nil { - owners = *a.Owners +func (s *ServicePrincipal) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls + type serviceprincipal ServicePrincipal + s2 := (*serviceprincipal)(s) + if err := json.Unmarshal(data, s2); err != nil { + return err } - owners = append(owners, val) - a.Owners = &owners + return nil } type SignInActivity struct { @@ -1009,7 +1019,8 @@ type TargetResource struct { // User describes a User object. type User struct { - ID *string `json:"id,omitempty"` + DirectoryObject + AboutMe *string `json:"aboutMe,omitempty"` AccountEnabled *bool `json:"accountEnabled,omitempty"` AgeGroup *AgeGroup `json:"ageGroup,omitempty"` @@ -1036,6 +1047,7 @@ type User struct { JobTitle *StringNullWhenEmpty `json:"jobTitle,omitempty"` Mail *StringNullWhenEmpty `json:"mail,omitempty"` MailNickname *string `json:"mailNickname,omitempty"` + MemberOf *[]DirectoryObject `json:"memberOf,omitempty"` MobilePhone *StringNullWhenEmpty `json:"mobilePhone,omitempty"` MySite *string `json:"mySite,omitempty"` OfficeLocation *StringNullWhenEmpty `json:"officeLocation,omitempty"` @@ -1075,6 +1087,7 @@ type User struct { func (u User) MarshalJSON() ([]byte, error) { docs := make([][]byte, 0) + // Local type needed to avoid recursive MarshalJSON calls type user User d, err := json.Marshal(user(u)) if err != nil { @@ -1094,6 +1107,7 @@ func (u User) MarshalJSON() ([]byte, error) { } func (u *User) UnmarshalJSON(data []byte) error { + // Local type needed to avoid recursive UnmarshalJSON calls type user User u2 := (*user)(u) if err := json.Unmarshal(data, u2); err != nil { @@ -1128,6 +1142,44 @@ type UserPasswordProfile struct { Password *string `json:"password,omitempty"` } +type UserRegistrationCount struct { + RegistrationStatus *RegistrationStatus `json:"registrationStatus,omitempty"` + RegistrationCount *int64 `json:"registrationCount,omitempty"` +} + +type UserRegistrationFeatureCount struct { + Feature *AuthenticationMethodFeature `json:"feature,omitempty"` + UserCount *int64 `json:"userCount"` +} +type UserRegistrationFeatureSummary struct { + TotalUserCount *int64 `json:"totalUserCount,omitempty"` + UserRegistrationFeatureCounts *[]UserRegistrationFeatureCount `json:"userRegistrationFeatureCounts"` + UserRoles IncludedUserRoles `json:"userRoles,omitempty"` + UserTypes IncludedUserTypes `json:"userTypes,omitempty"` +} + +type UserRegistrationMethodCount struct { + AuthenticationMethod *string `json:"authenticationMethod,omitempty"` + UserCount *int64 `json:"userCount,omitempty"` +} + +type UserRegistrationMethodSummary struct { + TotalUserCount *int64 `json:"totalUserCount"` + UserRegistrationMethodsCount *[]UserRegistrationMethodCount `json:"userRegistrationMethodCounts,omitempty"` + UerRoles IncludedUserRoles `json:"userRoles,omitempty"` + UserTypes IncludedUserTypes `json:"userTypes,omitempty"` +} + +type UserCredentialUsageDetails struct { + AuthMethod *UsageAuthMethod `json:"authMethod,omitempty"` + EventDateTime *time.Time `json:"eventDateTime,omitempty"` + FailureReason *string `json:"failureReason,omitempty"` + Feature *FeatureType `json:"feature,omitempty"` + ID *string `json:"id,omitempty"` + IsSuccess *bool `json:"isSuccess,omitempty"` + UserDisplayName *string `json:"userDisplayName,omitempty"` + UserPrincipalName *string `json:"userPrincipalName,omitempty"` +} type VerifiedPublisher struct { AddedDateTime *time.Time `json:"addedDateTime,omitempty"` DisplayName *string `json:"displayName,omitempty"` diff --git a/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go b/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go index 96bfcf552a..ac5c5845a6 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/namedlocations.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/internal/utils" @@ -40,9 +40,9 @@ func (c *NamedLocationsClient) List(ctx context.Context, query odata.Query) (*[] } defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var data struct { @@ -71,13 +71,13 @@ func (c *NamedLocationsClient) List(ctx context.Context, query odata.Query) (*[] continue } switch *o.Type { - case "#microsoft.graph.countryNamedLocation": + case odata.TypeCountryNamedLocation: var loc CountryNamedLocation if err := json.Unmarshal(namedLocation, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } ret = append(ret, loc) - case "#microsoft.graph.ipNamedLocation": + case odata.TypeIpNamedLocation: var loc IPNamedLocation if err := json.Unmarshal(namedLocation, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) @@ -103,6 +103,7 @@ func (c *NamedLocationsClient) Delete(ctx context.Context, id string) (int, erro if err != nil { return status, fmt.Errorf("NamedLocationsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -110,11 +111,12 @@ func (c *NamedLocationsClient) Delete(ctx context.Context, id string) (int, erro func (c *NamedLocationsClient) CreateIP(ctx context.Context, ipNamedLocation IPNamedLocation) (*IPNamedLocation, int, error) { var status int - ipNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.ipNamedLocation") + ipNamedLocation.ODataType = utils.StringPtr(odata.TypeIpNamedLocation) body, err := json.Marshal(ipNamedLocation) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -126,15 +128,18 @@ func (c *NamedLocationsClient) CreateIP(ctx context.Context, ipNamedLocation IPN if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newIPNamedLocation IPNamedLocation if err := json.Unmarshal(respBody, &newIPNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newIPNamedLocation, status, nil } @@ -142,12 +147,12 @@ func (c *NamedLocationsClient) CreateIP(ctx context.Context, ipNamedLocation IPN func (c *NamedLocationsClient) CreateCountry(ctx context.Context, countryNamedLocation CountryNamedLocation) (*CountryNamedLocation, int, error) { var status int - countryNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.countryNamedLocation") - + countryNamedLocation.ODataType = utils.StringPtr(odata.TypeCountryNamedLocation) body, err := json.Marshal(countryNamedLocation) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -159,15 +164,18 @@ func (c *NamedLocationsClient) CreateCountry(ctx context.Context, countryNamedLo if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newCountryNamedLocation CountryNamedLocation if err := json.Unmarshal(respBody, &newCountryNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newCountryNamedLocation, status, nil } @@ -185,15 +193,18 @@ func (c *NamedLocationsClient) GetIP(ctx context.Context, id string, query odata if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var ipNamedLocation IPNamedLocation if err := json.Unmarshal(respBody, &ipNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &ipNamedLocation, status, nil } @@ -211,10 +222,11 @@ func (c *NamedLocationsClient) Get(ctx context.Context, id string, query odata.Q if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } var o odata.OData @@ -231,13 +243,13 @@ func (c *NamedLocationsClient) Get(ctx context.Context, id string, query odata.Q } switch *o.Type { - case "#microsoft.graph.countryNamedLocation": + case odata.TypeCountryNamedLocation: var loc CountryNamedLocation if err := json.Unmarshal(respBody, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } ret = loc - case "#microsoft.graph.ipNamedLocation": + case odata.TypeIpNamedLocation: var loc IPNamedLocation if err := json.Unmarshal(respBody, &loc); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) @@ -263,15 +275,18 @@ func (c *NamedLocationsClient) GetCountry(ctx context.Context, id string, query if err != nil { return nil, status, fmt.Errorf("NamedLocationsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var countryNamedLocation CountryNamedLocation if err := json.Unmarshal(respBody, &countryNamedLocation); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &countryNamedLocation, status, nil } @@ -279,12 +294,12 @@ func (c *NamedLocationsClient) GetCountry(ctx context.Context, id string, query func (c *NamedLocationsClient) UpdateIP(ctx context.Context, ipNamedLocation IPNamedLocation) (int, error) { var status int - ipNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.ipNamedLocation") - + ipNamedLocation.ODataType = utils.StringPtr(odata.TypeIpNamedLocation) body, err := json.Marshal(ipNamedLocation) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -297,6 +312,7 @@ func (c *NamedLocationsClient) UpdateIP(ctx context.Context, ipNamedLocation IPN if err != nil { return status, fmt.Errorf("NamedLocationsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -304,12 +320,12 @@ func (c *NamedLocationsClient) UpdateIP(ctx context.Context, ipNamedLocation IPN func (c *NamedLocationsClient) UpdateCountry(ctx context.Context, countryNamedLocation CountryNamedLocation) (int, error) { var status int - countryNamedLocation.ODataType = utils.StringPtr("#microsoft.graph.countryNamedLocation") - + countryNamedLocation.ODataType = utils.StringPtr(odata.TypeCountryNamedLocation) body, err := json.Marshal(countryNamedLocation) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -322,5 +338,6 @@ func (c *NamedLocationsClient) UpdateCountry(ctx context.Context, countryNamedLo if err != nil { return status, fmt.Errorf("NamedLocationsClient.BaseClient.Patch(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/reports.go b/vendor/github.com/manicminer/hamilton/msgraph/reports.go new file mode 100644 index 0000000000..61ca552457 --- /dev/null +++ b/vendor/github.com/manicminer/hamilton/msgraph/reports.go @@ -0,0 +1,199 @@ +package msgraph + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/manicminer/hamilton/odata" +) + +// ReportsClient Client performs operations on reports. +type ReportsClient struct { + BaseClient Client +} + +// NewReportsClient returns a new ReportsClient. +func NewReportsClient(tenantId string) *ReportsClient { + return &ReportsClient{ + BaseClient: NewClient(VersionBeta, tenantId), + } +} + +func (c *ReportsClient) GetCredentialUserRegistrationCount(ctx context.Context, query odata.Query) (*[]CredentialUserRegistrationCount, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/getCredentialUserRegistrationCount", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.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 { + CredentialUserRegistrationCount []CredentialUserRegistrationCount `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.CredentialUserRegistrationCount, status, nil +} + +func (c *ReportsClient) GetCredentialUserRegistrationDetails(ctx context.Context, query odata.Query) (*[]CredentialUserRegistrationDetails, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/credentialUserRegistrationDetails", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.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 { + CredentialUserRegistrationDetails []CredentialUserRegistrationDetails `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.CredentialUserRegistrationDetails, status, nil +} + +func (c *ReportsClient) GetUserCredentialUsageDetails(ctx context.Context, query odata.Query) (*[]UserCredentialUsageDetails, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/userCredentialUsageDetails", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.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 { + UserCredentialUsageDetails []UserCredentialUsageDetails `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.UserCredentialUsageDetails, status, nil +} + +func (c *ReportsClient) GetCredentialUsageSummary(ctx context.Context, period CredentialUsageSummaryPeriod, query odata.Query) (*[]CredentialUsageSummary, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: fmt.Sprintf("/reports/getCredentialUsageSummary(period='%s')", period), + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.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 { + CredentialUsageSummary []CredentialUsageSummary `json:"value"` + } + if err := json.Unmarshal(respBody, &data); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &data.CredentialUsageSummary, status, nil +} + +func (c *ReportsClient) GetAuthenticationMethodsUsersRegisteredByFeature(ctx context.Context, query odata.Query) (*UserRegistrationFeatureSummary, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/authenticationMethods/usersRegisteredByFeature", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.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 userRegistrationFeatureSummary UserRegistrationFeatureSummary + if err := json.Unmarshal(respBody, &userRegistrationFeatureSummary); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &userRegistrationFeatureSummary, status, nil +} + +func (c *ReportsClient) GetAuthenticationMethodsUsersRegisteredByMethod(ctx context.Context, query odata.Query) (*UserRegistrationMethodSummary, int, error) { + resp, status, _, err := c.BaseClient.Get(ctx, GetHttpRequestInput{ + DisablePaging: query.Top > 0, + ValidStatusCodes: []int{http.StatusOK}, + Uri: Uri{ + Entity: "/reports/authenticationMethods/usersRegisteredByMethod", + Params: query.Values(), + HasTenantId: true, + }, + }) + if err != nil { + return nil, status, fmt.Errorf("ReportsClient.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 userRegistrationMethodSummary UserRegistrationMethodSummary + if err := json.Unmarshal(respBody, &userRegistrationMethodSummary); err != nil { + return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) + } + + return &userRegistrationMethodSummary, status, nil +} diff --git a/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go b/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go index 4a1ea75db4..9cb378bcf1 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/schema_extensions.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,17 +36,20 @@ func (c *SchemaExtensionsClient) List(ctx context.Context, query odata.Query) (* if err != nil { return nil, status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { SchemaExtensions []SchemaExtension `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.SchemaExtensions, status, nil } @@ -64,25 +67,30 @@ func (c *SchemaExtensionsClient) Get(ctx context.Context, id string, query odata if err != nil { return nil, status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var schemaExtension SchemaExtension if err := json.Unmarshal(respBody, &schemaExtension); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &schemaExtension, status, nil } // Update amends an existing schema Extension. func (c *SchemaExtensionsClient) Update(ctx context.Context, schemaExtension SchemaExtension) (int, error) { var status int + body, err := json.Marshal(schemaExtension) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -95,16 +103,19 @@ func (c *SchemaExtensionsClient) Update(ctx context.Context, schemaExtension Sch if err != nil { return status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Patch(): %v", err) } + return status, nil } // Create creates a new Schema Extension func (c *SchemaExtensionsClient) Create(ctx context.Context, schemaExtension SchemaExtension) (*SchemaExtension, int, error) { var status int + body, err := json.Marshal(schemaExtension) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -116,15 +127,18 @@ func (c *SchemaExtensionsClient) Create(ctx context.Context, schemaExtension Sch if err != nil { return nil, status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newSchemaExtension SchemaExtension if err := json.Unmarshal(respBody, &newSchemaExtension); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newSchemaExtension, status, nil } @@ -141,5 +155,6 @@ func (c *SchemaExtensionsClient) Delete(ctx context.Context, id string) (int, er if err != nil { return status, fmt.Errorf("SchemaExtensionsClient.BaseClient.Delete(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go b/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go index 85efe6bf18..8aefea2568 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/serviceprincipals.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -37,30 +37,36 @@ func (c *ServicePrincipalsClient) List(ctx context.Context, query odata.Query) ( if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { ServicePrincipals []ServicePrincipal `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.ServicePrincipals, status, nil } // Create creates a new Service Principal. func (c *ServicePrincipalsClient) Create(ctx context.Context, servicePrincipal ServicePrincipal) (*ServicePrincipal, int, error) { var status int + body, err := json.Marshal(servicePrincipal) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + appNotReplicated := func(resp *http.Response, o *odata.OData) bool { - return o.Error != nil && o.Error.Match(odata.ErrorServicePrincipalInvalidAppId) + return o != nil && o.Error != nil && o.Error.Match(odata.ErrorServicePrincipalInvalidAppId) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: appNotReplicated, @@ -73,15 +79,18 @@ func (c *ServicePrincipalsClient) Create(ctx context.Context, servicePrincipal S if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newServicePrincipal ServicePrincipal if err := json.Unmarshal(respBody, &newServicePrincipal); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newServicePrincipal, status, nil } @@ -99,28 +108,34 @@ func (c *ServicePrincipalsClient) Get(ctx context.Context, id string, query odat if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var servicePrincipal ServicePrincipal if err := json.Unmarshal(respBody, &servicePrincipal); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &servicePrincipal, status, nil } // Update amends an existing Service Principal. func (c *ServicePrincipalsClient) Update(ctx context.Context, servicePrincipal ServicePrincipal) (int, error) { var status int + if servicePrincipal.ID == nil { return status, errors.New("cannot update service principal with nil ID") } + body, err := json.Marshal(servicePrincipal) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -133,6 +148,7 @@ func (c *ServicePrincipalsClient) Update(ctx context.Context, servicePrincipal S if err != nil { return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -149,6 +165,7 @@ func (c *ServicePrincipalsClient) Delete(ctx context.Context, id string) (int, e if err != nil { return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -167,11 +184,13 @@ func (c *ServicePrincipalsClient) ListOwners(ctx context.Context, id string) (*[ if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Owners []struct { Type string `json:"@odata.type"` @@ -181,10 +200,12 @@ func (c *ServicePrincipalsClient) ListOwners(ctx context.Context, id string) (*[ if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + ret := make([]string, len(data.Owners)) for i, v := range data.Owners { ret[i] = v.Id } + return &ret, status, nil } @@ -204,11 +225,13 @@ func (c *ServicePrincipalsClient) GetOwner(ctx context.Context, servicePrincipal if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Context string `json:"@odata.context"` Type string `json:"@odata.type"` @@ -218,37 +241,40 @@ func (c *ServicePrincipalsClient) GetOwner(ctx context.Context, servicePrincipal if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Id, status, nil } -// AddOwners adds a new owner to a Service Principal. -// First populate the Owners field of the ServicePrincipal using the AppendOwner method of the model, then call this method. +// AddOwners adds owners to a Service Principal. +// First populate the `owners` field, then call this method func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipal *ServicePrincipal) (int, error) { var status int + if servicePrincipal.ID == nil { return status, errors.New("cannot update service principal with nil ID") } if servicePrincipal.Owners == nil { return status, errors.New("cannot update service principal with nil Owners") } + for _, owner := range *servicePrincipal.Owners { // don't fail if an owner already exists checkOwnerAlreadyExists := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorAddedObjectReferencesAlreadyExist) } return false } - data := struct { - Owner string `json:"@odata.id"` + body, err := json.Marshal(struct { + Owner odata.Id `json:"@odata.id"` }{ - Owner: owner, - } - body, err := json.Marshal(data) + Owner: *owner.ODataId, + }) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -263,6 +289,7 @@ func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipa return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } } + return status, nil } @@ -271,9 +298,11 @@ func (c *ServicePrincipalsClient) AddOwners(ctx context.Context, servicePrincipa // ownerIds is a *[]string containing object IDs of owners to remove. func (c *ServicePrincipalsClient) RemoveOwners(ctx context.Context, servicePrincipalId string, ownerIds *[]string) (int, error) { var status int + if ownerIds == nil { return status, errors.New("cannot remove, nil ownerIds") } + for _, ownerId := range *ownerIds { // check for ownership before attempting deletion if _, status, err := c.GetOwner(ctx, servicePrincipalId, ownerId); err != nil { @@ -285,7 +314,7 @@ func (c *ServicePrincipalsClient) RemoveOwners(ctx context.Context, servicePrinc // despite the above check, sometimes owners are just gone checkOwnerGone := func(resp *http.Response, o *odata.OData) bool { - if resp.StatusCode == http.StatusBadRequest && o.Error != nil { + if resp.StatusCode == http.StatusBadRequest && o != nil && o.Error != nil { return o.Error.Match(odata.ErrorRemovedObjectReferencesDoNotExist) } return false @@ -304,6 +333,7 @@ func (c *ServicePrincipalsClient) RemoveOwners(ctx context.Context, servicePrinc return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Delete(): %v", err) } } + return status, nil } @@ -322,23 +352,27 @@ func (c *ServicePrincipalsClient) ListGroupMemberships(ctx context.Context, id s if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Groups []Group `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Groups, status, nil } // AddPassword appends a new password credential to a Service Principal. func (c *ServicePrincipalsClient) AddPassword(ctx context.Context, servicePrincipalId string, passwordCredential PasswordCredential) (*PasswordCredential, int, error) { var status int + body, err := json.Marshal(struct { PwdCredential PasswordCredential `json:"passwordCredential"` }{ @@ -347,6 +381,7 @@ func (c *ServicePrincipalsClient) AddPassword(ctx context.Context, servicePrinci if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -359,21 +394,25 @@ func (c *ServicePrincipalsClient) AddPassword(ctx context.Context, servicePrinci if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newPasswordCredential PasswordCredential if err := json.Unmarshal(respBody, &newPasswordCredential); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newPasswordCredential, status, nil } // RemovePassword removes a password credential from a Service Principal. func (c *ServicePrincipalsClient) RemovePassword(ctx context.Context, servicePrincipalId string, keyId string) (int, error) { var status int + body, err := json.Marshal(struct { KeyId string `json:"keyId"` }{ @@ -382,6 +421,7 @@ func (c *ServicePrincipalsClient) RemovePassword(ctx context.Context, servicePri if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -394,6 +434,7 @@ func (c *ServicePrincipalsClient) RemovePassword(ctx context.Context, servicePri if err != nil { return status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + return status, nil } @@ -412,8 +453,9 @@ func (c *ServicePrincipalsClient) ListOwnedObjects(ctx context.Context, id strin if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { OwnedObjects []struct { Type string `json:"@odata.type"` @@ -423,10 +465,12 @@ func (c *ServicePrincipalsClient) ListOwnedObjects(ctx context.Context, id strin if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + ret := make([]string, len(data.OwnedObjects)) for i, v := range data.OwnedObjects { ret[i] = v.Id } + return &ret, status, nil } @@ -444,17 +488,20 @@ func (c *ServicePrincipalsClient) ListAppRoleAssignments(ctx context.Context, re if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { AppRoleAssignments []AppRoleAssignment `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.AppRoleAssignments, status, nil } @@ -471,6 +518,7 @@ func (c *ServicePrincipalsClient) RemoveAppRoleAssignment(ctx context.Context, r if err != nil { return status, fmt.Errorf("AppRoleAssignmentsClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -482,6 +530,7 @@ func (c *ServicePrincipalsClient) RemoveAppRoleAssignment(ctx context.Context, r // appRoleId: The id of the appRole (defined on the resource service principal) to assign to a user, group, or service principal. func (c *ServicePrincipalsClient) AssignAppRoleForResource(ctx context.Context, principalId, resourceId, appRoleId string) (*AppRoleAssignment, int, error) { var status int + data := struct { PrincipalId string `json:"principalId"` ResourceId string `json:"resourceId"` @@ -491,11 +540,11 @@ func (c *ServicePrincipalsClient) AssignAppRoleForResource(ctx context.Context, ResourceId: resourceId, AppRoleId: appRoleId, } - body, err := json.Marshal(data) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -508,14 +557,17 @@ func (c *ServicePrincipalsClient) AssignAppRoleForResource(ctx context.Context, if err != nil { return nil, status, fmt.Errorf("ServicePrincipalsClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var appRoleAssignment AppRoleAssignment if err := json.Unmarshal(respBody, &appRoleAssignment); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &appRoleAssignment, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go b/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go index bd4f906012..88ffa314dc 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/sign_in_reports.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,17 +36,20 @@ func (c *SignInReportsClient) List(ctx context.Context, query odata.Query) (*[]S if err != nil { return nil, status, fmt.Errorf("SignInLogsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { SignInLogs []SignInReport `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.SignInLogs, status, nil } @@ -64,14 +67,17 @@ func (c *SignInReportsClient) Get(ctx context.Context, id string, query odata.Qu if err != nil { return nil, status, fmt.Errorf("SignInLogsClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var signInReport SignInReport if err := json.Unmarshal(respBody, &signInReport); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &signInReport, status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/users.go b/vendor/github.com/manicminer/hamilton/msgraph/users.go index 39680f618b..83b36c9d6a 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/users.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/users.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net/http" "github.com/manicminer/hamilton/odata" @@ -36,27 +36,32 @@ func (c *UsersClient) List(ctx context.Context, query odata.Query) (*[]User, int if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Users []User `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Users, status, nil } // Create creates a new User. func (c *UsersClient) Create(ctx context.Context, user User) (*User, int, error) { var status int + body, err := json.Marshal(user) if err != nil { return nil, status, fmt.Errorf("json.Marshal(): %v", err) } + resp, status, _, err := c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusCreated}, @@ -68,15 +73,18 @@ func (c *UsersClient) Create(ctx context.Context, user User) (*User, int, error) if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var newUser User if err := json.Unmarshal(respBody, &newUser); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &newUser, status, nil } @@ -94,15 +102,18 @@ func (c *UsersClient) Get(ctx context.Context, id string, query odata.Query) (*U if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var user User if err := json.Unmarshal(respBody, &user); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &user, status, nil } @@ -136,16 +147,18 @@ func (c *UsersClient) GetWithSchemaExtensions(ctx context.Context, id string, qu if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } user.SchemaExtensions = schemaExtensions if err := json.Unmarshal(respBody, user); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return user, status, nil } @@ -163,25 +176,30 @@ func (c *UsersClient) GetDeleted(ctx context.Context, id string, query odata.Que if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var user User if err := json.Unmarshal(respBody, &user); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &user, status, nil } // Update amends an existing User. func (c *UsersClient) Update(ctx context.Context, user User) (int, error) { var status int + body, err := json.Marshal(user) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Patch(ctx, PatchHttpRequestInput{ Body: body, ConsistencyFailureFunc: RetryOn404ConsistencyFailureFunc, @@ -194,6 +212,7 @@ func (c *UsersClient) Update(ctx context.Context, user User) (int, error) { if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Patch(): %v", err) } + return status, nil } @@ -210,6 +229,7 @@ func (c *UsersClient) Delete(ctx context.Context, id string) (int, error) { if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -226,6 +246,7 @@ func (c *UsersClient) DeletePermanently(ctx context.Context, id string) (int, er if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Delete(): %v", err) } + return status, nil } @@ -243,14 +264,16 @@ func (c *UsersClient) ListDeleted(ctx context.Context, query odata.Query) (*[]Us if err != nil { return nil, status, err } + defer resp.Body.Close() - respBody, _ := ioutil.ReadAll(resp.Body) + respBody, _ := io.ReadAll(resp.Body) var data struct { DeletedUsers []User `json:"value"` } if err = json.Unmarshal(respBody, &data); err != nil { return nil, status, err } + return &data.DeletedUsers, status, nil } @@ -267,15 +290,18 @@ func (c *UsersClient) RestoreDeleted(ctx context.Context, id string) (*User, int if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Post(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var restoredUser User if err = json.Unmarshal(respBody, &restoredUser); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &restoredUser, status, nil } @@ -294,17 +320,20 @@ func (c *UsersClient) ListGroupMemberships(ctx context.Context, id string, query if err != nil { return nil, status, fmt.Errorf("UsersClient.BaseClient.Get(): %v", err) } + defer resp.Body.Close() - respBody, err := ioutil.ReadAll(resp.Body) + respBody, err := io.ReadAll(resp.Body) if err != nil { - return nil, status, fmt.Errorf("ioutil.ReadAll(): %v", err) + return nil, status, fmt.Errorf("io.ReadAll(): %v", err) } + var data struct { Groups []Group `json:"value"` } if err := json.Unmarshal(respBody, &data); err != nil { return nil, status, fmt.Errorf("json.Unmarshal(): %v", err) } + return &data.Groups, status, nil } @@ -312,10 +341,12 @@ func (c *UsersClient) ListGroupMemberships(ctx context.Context, id string, query // TODO: Needs testing with an O365 user principal func (c *UsersClient) Sendmail(ctx context.Context, id string, message MailMessage) (int, error) { var status int + body, err := json.Marshal(message) if err != nil { return status, fmt.Errorf("json.Marshal(): %v", err) } + _, status, _, err = c.BaseClient.Post(ctx, PostHttpRequestInput{ Body: body, ValidStatusCodes: []int{http.StatusOK, http.StatusAccepted}, @@ -327,5 +358,6 @@ func (c *UsersClient) Sendmail(ctx context.Context, id string, message MailMessa if err != nil { return status, fmt.Errorf("UsersClient.BaseClient.Post(): %v", err) } + return status, nil } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go index f5acae9513..e3db24c85a 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go @@ -1,6 +1,11 @@ package msgraph -import "encoding/json" +import ( + "encoding/json" + goerrors "errors" + + "github.com/manicminer/hamilton/odata" +) // 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 @@ -51,6 +56,16 @@ const ( AppRoleAllowedMemberTypeUser AppRoleAllowedMemberType = "User" ) +type AuthenticationMethodFeature = string + +const ( + AuthenticationMethodFeatureSsprRegistered AuthenticationMethodFeature = "ssprRegistered" + AuthenticationMethodFeatureSsprEnabled AuthenticationMethodFeature = "ssprEnabled" + AuthenticationMethodFeatureSsprCapable AuthenticationMethodFeature = "ssprCapable" + AuthenticationMethodFeaturePasswordlessCapable AuthenticationMethodFeature = "passwordlessCapable" + AuthenticationMethodFeatureMfaCapable AuthenticationMethodFeature = "mfaCapable" +) + type BodyType = string const ( @@ -67,6 +82,14 @@ const ( ConsentProvidedForMinorNotRequired ConsentProvidedForMinor = "NotRequired" ) +type CredentialUsageSummaryPeriod = string + +const ( + CredentialUsageSummaryPeriod30 CredentialUsageSummaryPeriod = "D30" + CredentialUsageSummaryPeriod7 CredentialUsageSummaryPeriod = "D7" + CredentialUsageSummaryPeriod1 CredentialUsageSummaryPeriod = "D1" +) + type ExtensionSchemaTargetType = string const ( @@ -91,6 +114,14 @@ const ( ExtensionSchemaPropertyDataString ExtensionSchemaPropertyDataType = "String" ) +type FeatureType = string + +const ( + FeatureTypeRegistration FeatureType = "registration" + FeatureTypeReset FeatureType = "reset" + FeatureTypeUnknownFutureValue FeatureType = "unknownFutureValue" +) + type GroupType = string const ( @@ -157,6 +188,54 @@ const ( KeyCredentialUsageVerify KeyCredentialUsage = "Verify" ) +type Members []DirectoryObject + +func (o Members) MarshalJSON() ([]byte, error) { + members := make([]odata.Id, len(o)) + for i, v := range o { + if v.ODataId == nil { + return nil, goerrors.New("marshaling Members: encountered DirectoryObject with nil ODataId") + } + members[i] = *v.ODataId + } + return json.Marshal(members) +} + +func (o *Members) UnmarshalJSON(data []byte) error { + var members []odata.Id + if err := json.Unmarshal(data, &members); err != nil { + return err + } + for _, v := range members { + *o = append(*o, DirectoryObject{ODataId: &v}) + } + return nil +} + +type Owners []DirectoryObject + +func (o Owners) MarshalJSON() ([]byte, error) { + owners := make([]odata.Id, len(o)) + for i, v := range o { + if v.ODataId == nil { + return nil, goerrors.New("marshaling Owners: encountered DirectoryObject with nil ODataId") + } + owners[i] = *v.ODataId + } + return json.Marshal(owners) +} + +func (o *Owners) UnmarshalJSON(data []byte) error { + var owners []odata.Id + if err := json.Unmarshal(data, &owners); err != nil { + return err + } + for _, v := range owners { + *o = append(*o, DirectoryObject{ODataId: &v}) + } + return nil +} + type PermissionScopeType = string const ( @@ -174,6 +253,30 @@ const ( PreferredSingleSignOnModeSaml PreferredSingleSignOnMode = "saml" ) +type RegistrationAuthMethod = string + +const ( + RegistrationAuthMethodEmail RegistrationAuthMethod = "email" + RegistrationAuthMethodMobilePhone RegistrationAuthMethod = "mobilePhone" + RegistrationAuthMethodOfficePhone RegistrationAuthMethod = "officePhone" + RegistrationAuthMethodSecurityQuestion RegistrationAuthMethod = "securityQuestion" + RegistrationAuthMethodAppNotification RegistrationAuthMethod = "appNotification" + RegistrationAuthMethodAppCode RegistrationAuthMethod = "appCode" + RegistrationAuthMethodAlternateMobilePhone RegistrationAuthMethod = "alternateMobilePhone" + RegistrationAuthMethodFido RegistrationAuthMethod = "fido" + RegistrationAuthMethodAppPassword RegistrationAuthMethod = "appPassword" + RegistrationAuthMethodUnknownFutureValue RegistrationAuthMethod = "unknownFutureValue" +) + +type RegistrationStatus = string + +const ( + RegistrationStatusRegistered RegistrationStatus = "registered" + RegistrationStatusEnabled RegistrationStatus = "enabled" + RegistrationStatusCapable RegistrationStatus = "capable" + RegistrationStatusMfaRegistered RegistrationStatus = "mfaRegistered" +) + type ResourceAccessType = string const ( @@ -209,3 +312,36 @@ const ( SignInAudienceAzureADandPersonalMicrosoftAccount SignInAudience = "AzureADandPersonalMicrosoftAccount" SignInAudiencePersonalMicrosoftAccount SignInAudience = "PersonalMicrosoftAccount" ) + +type UsageAuthMethod = string + +const ( + UsageAuthMethodEmail UsageAuthMethod = "email" + UsageAuthMethodMobileSMS UsageAuthMethod = "mobileSMS" + UsageAuthMethodMobileCall UsageAuthMethod = "mobileCall" + UsageAuthMethodOfficePhone UsageAuthMethod = "officePhone" + UsageAuthMethodSecurityQuestion UsageAuthMethod = "securityQuestion" + UsageAuthMethodAppNotification UsageAuthMethod = "appNotification" + UsageAuthMethodAppCode UsageAuthMethod = "appCode" + UsageAuthMethodAlternativeMobileCall UsageAuthMethod = "alternateMobileCall" + UsageAuthMethodFido UsageAuthMethod = "fido" + UsageAuthMethodAppPassword UsageAuthMethod = "appPassword" + UsageAuthMethodUnknownFutureValue UsageAuthMethod = "unknownFutureValue" +) + +type IncludedUserRoles = string + +const ( + IncludedUserRolesAll IncludedUserRoles = "all" + IncludedUserRolesPrivilegedAdmin IncludedUserRoles = "privilegedAdmin" + IncludedUserRolesAdmin IncludedUserRoles = "admin" + IncludedUserRolesUser IncludedUserRoles = "user" +) + +type IncludedUserTypes = string + +const ( + IncludedUserTypesAll IncludedUserTypes = "all" + IncludedUserTypesMember IncludedUserTypes = "member" + IncludedUserTypesGuest IncludedUserTypes = "guest" +) diff --git a/vendor/github.com/manicminer/hamilton/odata/odata.go b/vendor/github.com/manicminer/hamilton/odata/odata.go index 1a7e09ba8e..e8dbbe29db 100644 --- a/vendor/github.com/manicminer/hamilton/odata/odata.go +++ b/vendor/github.com/manicminer/hamilton/odata/odata.go @@ -10,20 +10,72 @@ import ( const ( ErrorAddedObjectReferencesAlreadyExist = "One or more added object references already exist" ErrorConflictingObjectPresentInDirectory = "A conflicting object with one or more of the specified property values is present in the directory" + ErrorResourceDoesNotExist = "Resource '.+' does not exist or one of its queried reference-property objects are not present" ErrorRemovedObjectReferencesDoNotExist = "One or more removed object references do not exist" - ErrorServicePrincipalInvalidAppId = "The appId '.+' of the service principal does not reference a valid application object." + ErrorServicePrincipalInvalidAppId = "The appId '.+' of the service principal does not reference a valid application object" +) + +type Id string + +func (o *Id) UnmarshalJSON(data []byte) error { + var id string + if err := json.Unmarshal(data, &id); err != nil { + return err + } + *o = Id(regexp.MustCompile(`/v2/`).ReplaceAllString(id, `/v1.0/`)) + return nil +} + +type ShortType = string + +const ( + ShortTypeAdministrativeUnit ShortType = "administrativeUnit" + ShortTypeApplication ShortType = "application" + ShortTypeConditionalAccessPolicy ShortType = "conditionalAccessPolicy" + ShortTypeCountryNamedLocation ShortType = "countryNamedLocation" + ShortTypeDevice ShortType = "device" + ShortTypeDirectoryRole ShortType = "directoryRole" + ShortTypeDirectoryRoleTemplate ShortType = "directoryRoleTemplate" + ShortTypeDomain ShortType = "domain" + ShortTypeGroup ShortType = "group" + ShortTypeIpNamedLocation ShortType = "ipNamedLocation" + ShortTypeNamedLocation ShortType = "namedLocation" + ShortTypeOrganization ShortType = "organization" + ShortTypeServicePrincipal ShortType = "servicePrincipal" + ShortTypeSocialIdentityProvider ShortType = "socialIdentityProvider" + ShortTypeUser ShortType = "user" +) + +type Type = string + +const ( + TypeAdministrativeUnit Type = "#microsoft.graph.administrativeUnit" + TypeApplication Type = "#microsoft.graph.application" + TypeConditionalAccessPolicy Type = "#microsoft.graph.conditionalAccessPolicy" + TypeCountryNamedLocation Type = "#microsoft.graph.countryNamedLocation" + TypeDevice Type = "#microsoft.graph.device" + TypeDirectoryRole Type = "#microsoft.graph.directoryRole" + TypeDirectoryRoleTemplate Type = "#microsoft.graph.directoryRoleTemplate" + TypeDomain Type = "#microsoft.graph.domain" + TypeGroup Type = "#microsoft.graph.group" + TypeIpNamedLocation Type = "#microsoft.graph.ipNamedLocation" + TypeNamedLocation Type = "#microsoft.graph.namedLocation" + TypeOrganization Type = "#microsoft.graph.organization" + TypeServicePrincipal Type = "#microsoft.graph.servicePrincipal" + TypeSocialIdentityProvider Type = "#microsoft.graph.socialIdentityProvider" + TypeUser Type = "#microsoft.graph.user" ) // OData is used to unmarshall OData metadata from an API response. type OData struct { Context *string `json:"@odata.context"` MetadataEtag *string `json:"@odata.metadataEtag"` - Type *string `json:"@odata.type"` + Type *Type `json:"@odata.type"` Count *string `json:"@odata.count"` NextLink *string `json:"@odata.nextLink"` Delta *string `json:"@odata.delta"` DeltaLink *string `json:"@odata.deltaLink"` - Id *string `json:"@odata.id"` + Id *Id `json:"@odata.id"` Etag *string `json:"@odata.etag"` Error *Error `json:"-"` diff --git a/vendor/modules.txt b/vendor/modules.txt index fd0fc039a0..4b6792c9e9 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -192,7 +192,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.23.1 +# github.com/manicminer/hamilton v0.24.0 ## explicit github.com/manicminer/hamilton/auth github.com/manicminer/hamilton/environments From 10ae9ab822ef04d1dec07443f9694b31309c6d36 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Fri, 13 Aug 2021 17:47:27 +0100 Subject: [PATCH 02/11] Fixes for some object ownership issues with applications, groups and service principals Where no owners are specified in configuration, The calling principal is explicitly assigned as the owner at creation time, and then removed immediately afterwards. This provides stable behavior across resources where sometimes the API will assign the caller as the owner and sometimes not, and it's impossible to explicitly specify zero owners at creation time. Where owners are specified, 19 of them are assigned at creation time, in addition to the calling principal. This is unordered since the `owners` property is a TypeSet in all cases. The remaining owners (if any) are subsequently assigned immediately after creation. However for groups, the first 19 principals are preferentially selected to be users, followed by service principals. This aims to work around strict validation with Microsoft 365 groups where at least one owner must be a user at all times! Ensuring that at least one user is included in the set of owners in configuration is an exercise left to the practitioner. In all cases, if the calling principal is not explicitly included in the set of owners in the configuration, it will not remain an owner following an apply operation. This too is a decision left to the practitioner to explicitly include this principal as an owner if so required. Create and Update timeouts are also increased for groups to 20 mins, and applications & service princpals to 10 mins, to accommodate the time needed to post large numbers of members/owners. --- docs/resources/application.md | 5 +- docs/resources/group.md | 27 +- docs/resources/service_principal.md | 8 + internal/common/client_options.go | 2 + .../application_certificate_resource.go | 8 +- .../application_pre_authorized_resource.go | 12 +- .../applications/application_resource.go | 118 +++++++- .../applications/application_resource_test.go | 96 ++++++ .../services/applications/applications.go | 41 +-- .../services/applications/client/client.go | 13 +- internal/services/groups/client/client.go | 13 +- .../services/groups/group_member_resource.go | 13 +- internal/services/groups/group_resource.go | 166 ++++++++--- .../services/groups/group_resource_test.go | 278 ++++++++++-------- .../serviceprincipals/client/client.go | 11 +- .../service_principal_certificate_resource.go | 8 +- .../service_principal_resource.go | 126 +++++++- .../service_principal_resource_test.go | 207 +++++++++++++ internal/services/users/user_resource.go | 4 +- 19 files changed, 920 insertions(+), 236 deletions(-) diff --git a/docs/resources/application.md b/docs/resources/application.md index 2d980d045a..af0c28c879 100644 --- a/docs/resources/application.md +++ b/docs/resources/application.md @@ -135,7 +135,10 @@ The following arguments are supported: * `marketing_url` - (Optional) URL of the application's marketing page. * `oauth2_post_response_required` - (Optional) Specifies whether, as part of OAuth 2.0 token requests, Azure AD allows POST requests, as opposed to GET requests. Defaults to `false`, which specifies that only GET requests are allowed. * `optional_claims` - (Optional) An `optional_claims` block as documented below. -* `owners` - (Optional) A list of object IDs of principals that will be granted ownership of the application. It's recommended to specify the object ID of the authenticated principal running Terraform, to ensure sufficient permissions that the application can be subsequently updated. +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the application. Supported object types are users or service principals. By default, no owners are assigned. + +-> **Ownership of Applications** It's recommended to always specify one or more application owners, including the principal being used to execute Terraform, such as in the example above. + * `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing application is found with the same name. Defaults to `false`. * `privacy_statement_url` - (Optional) URL of the application's privacy statement. * `public_client` - (Optional) A `public_client` block as documented below, which configures non-web app or non-web API application settings, for example mobile or other public clients such as an installed application running on a desktop device. diff --git a/docs/resources/group.md b/docs/resources/group.md index 0f2c8ca7ef..1fb43ef2f7 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -11,8 +11,11 @@ Manages a group within Azure Active Directory. *Basic example* ```terraform +data "azuread_client_config" "current" {} + resource "azuread_group" "example" { display_name = "example" + owners = [data.azuread_client_config.current.object_id] security_enabled = true } ``` @@ -20,26 +23,44 @@ resource "azuread_group" "example" { *Microsoft 365 group* ```terraform +data "azuread_client_config" "current" {} + +resource "azuread_user" "group_owner" { + user_principal_name = "example-group-owner@hashicorp.com" + display_name = "Group Owner" + mail_nickname = "example-group-owner" + password = "SecretP@sswd99!" +} + resource "azuread_group" "example" { display_name = "example" mail_enabled = true mail_nickname = "ExampleGroup" security_enabled = true types = ["Unified"] + + owners = [ + data.azuread_client_config.current.object_id, + azuread_user.group_owner.object_id, + ] } ``` *Group with members* ```terraform +data "azuread_client_config" "current" {} + resource "azuread_user" "example" { display_name = "J Doe" + owners = [data.azuread_client_config.current.object_id] password = "notSecure123" user_principal_name = "jdoe@hashicorp.com" } resource "azuread_group" "example" { display_name = "MyGroup" + owners = [data.azuread_client_config.current.object_id] security_enabled = true members = [ @@ -60,11 +81,9 @@ The following arguments are supported: * `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. A group can be mail enabled _and_ security enabled. * `mail_nickname` - (Optional) The mail alias for the group, unique in the organisation. Required for mail-enabled groups. Changing this forces a new resource to be created. * `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals. -* `owners` - (Optional) A set of owners who own this group. Supported object types are Users or Service Principals. - -~> **Group Ownership and Permissions** Terraform always adds its own principal as a group owner to ensure that groups can continue to be managed. If using a user principal to execute Terraform, we recommend assigning the directory role `Groups Administrator` (or a role with the same effective permissions) to that user, in order to help prevent scenarios where groups may become unmanageable without administrative intervention. +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, no owners are assigned. --> **Ownership of Microsoft 365 Groups** Microsoft 365 groups are required to have at least one owner which _must be a user_ (i.e. not a service principal). If you are running Terraform with an Azure AD user principal, you do not need to specify any owners for a group, although we suggest always specifying at least one user as an owner in your configuration. +-> **Group Ownership** It's recommended to always specify one or more group owners, including the principal being used to execute Terraform, such as in the example above. Microsoft 365 groups are required to have at least one owner which _must be a user_ (i.e. not a service principal). * `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing group is found with the same name. Defaults to `false`. * `provisioning_options` - (Optional) A set of provisioning options for a Microsoft 365 group. The only supported value is `Team`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for details. Changing this forces a new resource to be created. diff --git a/docs/resources/service_principal.md b/docs/resources/service_principal.md index 850cdfb6f4..c7a7a35724 100644 --- a/docs/resources/service_principal.md +++ b/docs/resources/service_principal.md @@ -9,13 +9,17 @@ Manages a service principal associated with an application within Azure Active D ## Example Usage ```terraform +data "azuread_client_config" "current" {} + resource "azuread_application" "example" { display_name = "example" + owners = [data.azuread_client_config.current.object_id] } resource "azuread_service_principal" "example" { application_id = azuread_application.example.application_id app_role_assignment_required = false + owners = [data.azuread_client_config.current.object_id] tags = ["example", "tags", "here"] } @@ -33,6 +37,10 @@ The following arguments are supported: * `login_url` - (Optional) The URL where the service provider redirects the user to Azure AD to authenticate. Azure AD uses the URL to launch the application from Microsoft 365 or the Azure AD My Apps. When blank, Azure AD performs IdP-initiated sign-on for applications configured with SAML-based single sign-on. * `notes` - (Optional) A free text field to capture information about the service principal, typically used for operational purposes. * `notification_email_addresses` - (Optional) A set of email addresses where Azure AD sends a notification when the active certificate is near the expiration date. This is only for the certificates used to sign the SAML token issued for Azure AD Gallery applications. +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the service principal. Supported object types are users or service principals. By default, no owners are assigned. + +-> **Ownership of Service Principals** It's recommended to always specify one or more service principal owners, including the principal being used to execute Terraform, such as in the example above. + * `preferred_single_sign_on_mode` - (Optional) The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps. Supported values are `oidc`, `password`, `saml` or `notSupported`. Omit this property or specify a blank string to unset. * `tags` - (Optional) A set of tags to apply to the service principal. * `use_existing` - (Optional) When true, any existing service principal linked to the same application will be automatically imported. When false, an import error will be raised for any pre-existing service principal. diff --git a/internal/common/client_options.go b/internal/common/client_options.go index 17aec8a433..8e33e93061 100644 --- a/internal/common/client_options.go +++ b/internal/common/client_options.go @@ -39,6 +39,8 @@ func (o ClientOptions) ConfigureClient(c *msgraph.Client) { } *c.RequestMiddlewares = append(*c.RequestMiddlewares, o.requestLogger) *c.ResponseMiddlewares = append(*c.ResponseMiddlewares, o.responseLogger) + + c.RetryableClient.RetryMax = 20 } func (o ClientOptions) requestLogger(req *http.Request) (*http.Request, error) { diff --git a/internal/services/applications/application_certificate_resource.go b/internal/services/applications/application_certificate_resource.go index 67e518ac7c..afe368ec73 100644 --- a/internal/services/applications/application_certificate_resource.go +++ b/internal/services/applications/application_certificate_resource.go @@ -163,7 +163,9 @@ func applicationCertificateResourceCreate(ctx context.Context, d *schema.Resourc newCredentials = append(newCredentials, *credential) properties := msgraph.Application{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { @@ -257,7 +259,9 @@ func applicationCertificateResourceDelete(ctx context.Context, d *schema.Resourc } properties := msgraph.Application{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { diff --git a/internal/services/applications/application_pre_authorized_resource.go b/internal/services/applications/application_pre_authorized_resource.go index dfcd7bce3c..2a51ff7e1a 100644 --- a/internal/services/applications/application_pre_authorized_resource.go +++ b/internal/services/applications/application_pre_authorized_resource.go @@ -104,7 +104,9 @@ func applicationPreAuthorizedResourceCreate(ctx context.Context, d *schema.Resou }) properties := msgraph.Application{ - ID: app.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: app.ID, + }, Api: &msgraph.ApplicationApi{ PreAuthorizedApplications: &newPreAuthorizedApps, }, @@ -157,7 +159,9 @@ func applicationPreAuthorizedResourceUpdate(ctx context.Context, d *schema.Resou } properties := msgraph.Application{ - ID: app.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: app.ID, + }, Api: &msgraph.ApplicationApi{ PreAuthorizedApplications: &newPreAuthorizedApps, }, @@ -245,7 +249,9 @@ func applicationPreAuthorizedResourceDelete(ctx context.Context, d *schema.Resou } properties := msgraph.Application{ - ID: app.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: app.ID, + }, Api: &msgraph.ApplicationApi{ PreAuthorizedApplications: &newPreAuthorizedApps, }, diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index de71075bad..7a1cec022d 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -38,9 +38,9 @@ func applicationResource() *schema.Resource { CustomizeDiff: applicationResourceCustomizeDiff, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(5 * time.Minute), + Create: schema.DefaultTimeout(10 * time.Minute), Read: schema.DefaultTimeout(5 * time.Minute), - Update: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), Delete: schema.DefaultTimeout(5 * time.Minute), }, @@ -332,12 +332,13 @@ func applicationResource() *schema.Resource { }, "owners": { - Description: "A list of object IDs of principals that will be granted ownership of the application. It's recommended to specify the object ID of the authenticated principal running Terraform, to ensure sufficient permissions that the application can be subsequently updated", + Description: "A list of object IDs of principals that will be granted ownership of the application", Type: schema.TypeSet, Optional: true, + Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, - ValidateDiagFunc: validate.NoEmptyStrings, + ValidateDiagFunc: validate.UUID, }, }, @@ -806,6 +807,8 @@ func applicationDiffSuppress(k, old, new string, d *schema.ResourceData) bool { func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Applications.ApplicationsClient + directoryObjectsClient := meta.(*clients.Client).Applications.DirectoryObjectsClient + callerId := meta.(*clients.Client).Claims.ObjectId displayName := d.Get("display_name").(string) // Perform this check at apply time to catch any duplicate names created during the same apply @@ -846,6 +849,52 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta Web: expandApplicationWeb(d.Get("web").([]interface{})), } + // Chunk the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + ownerChunks := make([]msgraph.Owners, 2) + + // The calling principal should always be in the first block of owners + callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) + } + if callerObject == nil { + return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) + } + ownerChunks[0] = msgraph.Owners{*callerObject} + + // Track whether we need to remove the calling principal later on + removeCallerOwner := true + + // Retrieve and set the initial owners, which can be up to 20 in total when creating the application + if v, ok := d.GetOk("owners"); ok { + c := 0 + for _, id := range v.(*schema.Set).List() { + i := 0 + if c >= 19 { + i = 1 + } + if strings.EqualFold(id.(string), callerId) { + removeCallerOwner = false + continue + } + ownerObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("ownerObject was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) + } + ownerChunks[i] = append(ownerChunks[i], *ownerObject) + c++ + } + } + + // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration + properties.Owners = &ownerChunks[0] + app, _, err := client.Create(ctx, properties) if err != nil { return tf.ErrorDiagF(err, "Could not create application") @@ -857,9 +906,19 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(*app.ID) - owners := *tf.ExpandStringSlicePtr(d.Get("owners").(*schema.Set).List()) - if err := applicationSetOwners(ctx, client, app, owners); err != nil { - return tf.ErrorDiagPathF(err, "owners", "Could not set owners for application with object ID: %q", *app.ID) + if len(ownerChunks[1]) > 0 { + // Add any remaining owners after the application is created + app.Owners = &ownerChunks[1] + if _, err := client.AddOwners(ctx, app); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to application with object ID: %q", d.Id()) + } + } + + // If the calling principal was not included in configuration, remove it now + if removeCallerOwner { + if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { + return tf.ErrorDiagF(err, "Could not remove initial owner from group with object ID: %q", d.Id()) + } } return applicationResourceRead(ctx, d, meta) @@ -867,6 +926,7 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Applications.ApplicationsClient + directoryObjectsClient := meta.(*clients.Client).Applications.DirectoryObjectsClient applicationId := d.Id() displayName := d.Get("display_name").(string) @@ -890,7 +950,9 @@ func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta } properties := msgraph.Application{ - ID: utils.String(applicationId), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(applicationId), + }, Api: expandApplicationApi(d.Get("api").([]interface{})), AppRoles: expandApplicationAppRoles(d.Get("app_role").(*schema.Set).List()), DisplayName: utils.String(displayName), @@ -922,12 +984,44 @@ func applicationResourceUpdate(ctx context.Context, d *schema.ResourceData, meta } if _, err := client.Update(ctx, properties); err != nil { - return tf.ErrorDiagF(err, "Could not update application with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not update application with object ID: %q", d.Id()) } - owners := *tf.ExpandStringSlicePtr(d.Get("owners").(*schema.Set).List()) - if err := applicationSetOwners(ctx, client, &properties, owners); err != nil { - return tf.ErrorDiagPathF(err, "owners", "Could not set owners for application with object ID: %q", d.Id()) + if v, ok := d.GetOk("owners"); ok && d.HasChange("owners") { + owners, _, err := client.ListOwners(ctx, applicationId) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owners for application with object ID: %q", d.Id()) + } + + desiredOwners := *tf.ExpandStringSlicePtr(v.(*schema.Set).List()) + existingOwners := *owners + ownersForRemoval := utils.Difference(existingOwners, desiredOwners) + ownersToAdd := utils.Difference(desiredOwners, existingOwners) + + if len(ownersToAdd) > 0 { + newOwners := make(msgraph.Owners, 0) + for _, m := range ownersToAdd { + ownerObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", m) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", m) + } + newOwners = append(newOwners, *ownerObject) + } + + properties.Owners = &newOwners + if _, err := client.AddOwners(ctx, &properties); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to application with object ID: %q", d.Id()) + } + } + + if len(ownersForRemoval) > 0 { + if _, err = client.RemoveOwners(ctx, d.Id(), &ownersForRemoval); err != nil { + return tf.ErrorDiagF(err, "Could not remove owners from application with object ID: %q", d.Id()) + } + } } return applicationResourceRead(ctx, d, meta) diff --git a/internal/services/applications/application_resource_test.go b/internal/services/applications/application_resource_test.go index 19ebefb3b9..378851c33b 100644 --- a/internal/services/applications/application_resource_test.go +++ b/internal/services/applications/application_resource_test.go @@ -266,6 +266,22 @@ func TestAccApplication_owners(t *testing.T) { ), }, data.ImportStep(), + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + { + Config: r.singleOwner(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), { Config: r.threeOwners(data), Check: resource.ComposeTestCheckFunc( @@ -285,6 +301,38 @@ func TestAccApplication_owners(t *testing.T) { }) } +func TestAccApplication_createWithNoOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application", "test") + r := ApplicationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplication_manyOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_application", "test") + r := ApplicationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.manyOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("45"), + ), + }, + data.ImportStep(), + }) +} + func TestAccApplication_preventDuplicateNamesPass(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_application", "test") r := ApplicationResource{} @@ -888,6 +936,17 @@ resource "azuread_user" "testC" { `, data.RandomInteger, data.RandomPassword) } +func (ApplicationResource) noOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + owners = [] +} +`, data.RandomInteger) +} + func (r ApplicationResource) singleOwner(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -915,3 +974,40 @@ resource "azuread_application" "test" { } `, r.templateThreeUsers(data), data.RandomInteger) } + +func (r ApplicationResource) manyOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +data "azuread_client_config" "test" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_application" "owner" { + count = 27 + display_name = "acctestApplicationOwner${count.index}-%[1]d" +} + +resource "azuread_service_principal" "owner" { + count = 27 + application_id = azuread_application.owner[count.index].application_id +} + +resource "azuread_user" "owner" { + count = 17 + user_principal_name = "acctestApplicationOwner${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestApplicationOwner${count.index}-%[1]d" + password = "Qwer5678!@#" +} + +resource "azuread_application" "test" { + display_name = "acctest-APP-%[1]d" + + owners = flatten([ + data.azuread_client_config.test.object_id, + azuread_service_principal.owner.*.object_id, + azuread_user.owner.*.object_id, + ]) +} +`, data.RandomInteger) +} diff --git a/internal/services/applications/applications.go b/internal/services/applications/applications.go index 04d940be61..8ebfcf44fd 100644 --- a/internal/services/applications/applications.go +++ b/internal/services/applications/applications.go @@ -80,7 +80,9 @@ func applicationDisableAppRoles(ctx context.Context, client *msgraph.Application if disable { // Disable any changed or removed roles properties := msgraph.Application{ - ID: application.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: application.ID, + }, AppRoles: &existingRoles, } if _, err := client.Update(ctx, properties); err != nil { @@ -193,7 +195,9 @@ func applicationDisableOauth2PermissionScopes(ctx context.Context, client *msgra if disable { // Disable any changed or removed scopes properties := msgraph.Application{ - ID: application.ID, + DirectoryObject: msgraph.DirectoryObject{ + ID: application.ID, + }, Api: &msgraph.ApplicationApi{ OAuth2PermissionScopes: &existingScopes, }, @@ -302,39 +306,6 @@ func applicationFindByName(ctx context.Context, client *msgraph.ApplicationsClie return &result, nil } -func applicationSetOwners(ctx context.Context, client *msgraph.ApplicationsClient, application *msgraph.Application, desiredOwners []string) error { - if application.ID == nil { - return fmt.Errorf("Cannot use Application model with nil ID") - } - - owners, _, err := client.ListOwners(ctx, *application.ID) - if err != nil { - return fmt.Errorf("retrieving owners for Application with object ID %q: %+v", *application.ID, err) - } - - existingOwners := *owners - ownersForRemoval := utils.Difference(existingOwners, desiredOwners) - ownersToAdd := utils.Difference(desiredOwners, existingOwners) - - if ownersToAdd != nil { - for _, m := range ownersToAdd { - application.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, m) - } - - if _, err := client.AddOwners(ctx, application); err != nil { - return fmt.Errorf("adding owners to Application with object ID %q: %+v", *application.ID, err) - } - } - - if ownersForRemoval != nil { - if _, err = client.RemoveOwners(ctx, *application.ID, &ownersForRemoval); err != nil { - return fmt.Errorf("removing owner from Application with object ID %q: %+v", *application.ID, err) - } - } - - return nil -} - func applicationValidateRolesScopes(appRoles, oauth2Permissions []interface{}) error { var ids, values []string diff --git a/internal/services/applications/client/client.go b/internal/services/applications/client/client.go index e4a74b3d6c..7fa39b0477 100644 --- a/internal/services/applications/client/client.go +++ b/internal/services/applications/client/client.go @@ -7,14 +7,19 @@ import ( ) type Client struct { - ApplicationsClient *msgraph.ApplicationsClient + ApplicationsClient *msgraph.ApplicationsClient + DirectoryObjectsClient *msgraph.DirectoryObjectsClient } func NewClient(o *common.ClientOptions) *Client { - msClient := msgraph.NewApplicationsClient(o.TenantID) - o.ConfigureClient(&msClient.BaseClient) + applicationsClient := msgraph.NewApplicationsClient(o.TenantID) + o.ConfigureClient(&applicationsClient.BaseClient) + + directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID) + o.ConfigureClient(&directoryObjectsClient.BaseClient) return &Client{ - ApplicationsClient: msClient, + ApplicationsClient: applicationsClient, + DirectoryObjectsClient: directoryObjectsClient, } } diff --git a/internal/services/groups/client/client.go b/internal/services/groups/client/client.go index 3481c284e4..6311a0a72d 100644 --- a/internal/services/groups/client/client.go +++ b/internal/services/groups/client/client.go @@ -7,14 +7,19 @@ import ( ) type Client struct { - GroupsClient *msgraph.GroupsClient + DirectoryObjectsClient *msgraph.DirectoryObjectsClient + GroupsClient *msgraph.GroupsClient } func NewClient(o *common.ClientOptions) *Client { - msClient := msgraph.NewGroupsClient(o.TenantID) - o.ConfigureClient(&msClient.BaseClient) + directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID) + o.ConfigureClient(&directoryObjectsClient.BaseClient) + + groupsClient := msgraph.NewGroupsClient(o.TenantID) + o.ConfigureClient(&groupsClient.BaseClient) return &Client{ - GroupsClient: msClient, + DirectoryObjectsClient: directoryObjectsClient, + GroupsClient: groupsClient, } } diff --git a/internal/services/groups/group_member_resource.go b/internal/services/groups/group_member_resource.go index d6971a17d4..5435ad8a35 100644 --- a/internal/services/groups/group_member_resource.go +++ b/internal/services/groups/group_member_resource.go @@ -2,11 +2,14 @@ package groups import ( "context" + "errors" "log" "net/http" "strings" "time" + "github.com/manicminer/hamilton/msgraph" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/manicminer/hamilton/odata" @@ -57,6 +60,7 @@ func groupMemberResource() *schema.Resource { func groupMemberResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient groupId := d.Get("group_object_id").(string) memberId := d.Get("member_object_id").(string) @@ -85,7 +89,14 @@ func groupMemberResourceCreate(ctx context.Context, d *schema.ResourceData, meta } } - group.AppendMember(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, memberId) + ownerObject, _, err := directoryObjectsClient.Get(ctx, memberId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve principal object %q", memberId) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", memberId) + } + group.Owners = &msgraph.Owners{*ownerObject} if _, err := client.AddMembers(ctx, group); err != nil { return tf.ErrorDiagF(err, "Adding group member %q to group %q", memberId, groupId) diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index 491bf22d28..a4aded1ff5 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net/http" + "strings" "time" "github.com/hashicorp/go-uuid" @@ -33,9 +34,9 @@ func groupResource() *schema.Resource { CustomizeDiff: groupResourceCustomizeDiff, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(5 * time.Minute), + Create: schema.DefaultTimeout(20 * time.Minute), Read: schema.DefaultTimeout(5 * time.Minute), - Update: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(20 * time.Minute), Delete: schema.DefaultTimeout(5 * time.Minute), }, @@ -115,7 +116,6 @@ func groupResource() *schema.Resource { Description: "A set of owners who own this group. Supported object types are Users or Service Principals", Type: schema.TypeSet, Optional: true, - Computed: true, Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, @@ -253,17 +253,6 @@ func groupResource() *schema.Resource { func groupResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, meta interface{}) error { client := meta.(*clients.Client).Groups.GroupsClient - callerId := meta.(*clients.Client).Claims.ObjectId - - // Suppress the diff when the only change is to remove the calling principal as a group owner - // as we always want to retain such ownership in order to avoid orphaning the group - existingOwnersRaw, newOwnersRaw := diff.GetChange("owners") - existingOwners := tf.ExpandStringSlice(existingOwnersRaw.(*schema.Set).List()) - newOwners := tf.ExpandStringSlice(newOwnersRaw.(*schema.Set).List()) - ownersToRemove := utils.Difference(existingOwners, newOwners) - if len(ownersToRemove) == 1 && ownersToRemove[0] == callerId { - diff.Clear("owners") - } // Check for duplicate names oldDisplayName, newDisplayName := diff.GetChange("display_name") @@ -346,6 +335,7 @@ func groupResourceCustomizeDiff(ctx context.Context, diff *schema.ResourceDiff, func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient callerId := meta.(*clients.Client).Claims.ObjectId displayName := d.Get("display_name").(string) @@ -408,8 +398,59 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter properties.Visibility = utils.String(visibility) } - // The calling principal is the initial owner (retained after other owners are also set) - properties.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, callerId) + // Chunk the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + ownerChunks := make([]msgraph.Owners, 2) + + // The calling principal should always be in the first block of owners + callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) + } + if callerObject == nil { + return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) + } + ownerChunks[0] = msgraph.Owners{*callerObject} + + // Track whether we need to remove the calling principal later on + removeCallerOwner := true + + // Retrieve and set the initial owners, which can be up to 20 in total when creating the group. + // Prefer users first, then service principals, to try and avoid API validation errors for Microsoft 365 groups. + if v, ok := d.GetOk("owners"); ok { + c := 0 + for _, t := range []odata.Type{odata.TypeUser, odata.TypeServicePrincipal, odata.TypeGroup} { + for _, id := range v.(*schema.Set).List() { + i := 0 + if c >= 19 { + i = 1 + } + if strings.EqualFold(id.(string), callerId) { + removeCallerOwner = false + continue + } + ownerObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("ownerObject was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerObject.ODataType == nil { + return tf.ErrorDiagF(errors.New("ODataType was nil"), "Could not retrieve owner principal object %q", id) + } + if *ownerObject.ODataType == t { + if ownerObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) + } + ownerChunks[i] = append(ownerChunks[i], *ownerObject) + c++ + } + } + } + } + + // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration + properties.Owners = &ownerChunks[0] group, _, err := client.Create(ctx, properties) if err != nil { @@ -422,25 +463,42 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter d.SetId(*group.ID) - // Configure owners after the group is created, so they can be set one-by-one - if v, ok := d.GetOk("owners"); ok { - owners := v.(*schema.Set).List() - for _, o := range owners { - group.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, o.(string)) - } + // Add any remaining owners after the group is created + if len(ownerChunks[1]) > 0 { + group.Owners = &ownerChunks[1] if _, err := client.AddOwners(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Could not add owners to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add owners to group with object ID: %q", d.Id()) } } - // Configure members after the group is created, so they can be reliably batched + // If the calling principal was not included in configuration, remove it now + if removeCallerOwner { + if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { + return tf.ErrorDiagF(err, "Could not remove initial owner from group with object ID: %q", d.Id()) + } + } + + // Add members after the group is created + members := make(msgraph.Members, 0) if v, ok := d.GetOk("members"); ok { - members := v.(*schema.Set).List() - for _, o := range members { - group.AppendMember(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, o.(string)) + for _, id := range v.(*schema.Set).List() { + memberObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve member principal object %q", id) + } + if memberObject == nil { + return tf.ErrorDiagF(errors.New("memberObject was nil"), "Could not retrieve member principal object %q", id) + } + if memberObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve member principal object %q", id) + } + members = append(members, *memberObject) } + } + if len(members) > 0 { + group.Members = &members if _, err := client.AddMembers(ctx, group); err != nil { - return tf.ErrorDiagF(err, "Could not add members to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add members to group with object ID: %q", d.Id()) } } @@ -449,7 +507,7 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient - callerId := meta.(*clients.Client).Claims.ObjectId + directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient groupId := d.Id() displayName := d.Get("display_name").(string) @@ -476,7 +534,9 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter } group := msgraph.Group{ - ID: utils.String(groupId), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(groupId), + }, Description: utils.NullableString(d.Get("description").(string)), DisplayName: utils.String(displayName), MailEnabled: utils.Bool(d.Get("mail_enabled").(bool)), @@ -498,7 +558,7 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter if v, ok := d.GetOk("members"); ok && d.HasChange("members") { members, _, err := client.ListMembers(ctx, *group.ID) if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve members for group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not retrieve members for group with object ID: %q", d.Id()) } existingMembers := *members @@ -506,19 +566,28 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter membersForRemoval := utils.Difference(existingMembers, desiredMembers) membersToAdd := utils.Difference(desiredMembers, existingMembers) - if membersForRemoval != nil { + if len(membersForRemoval) > 0 { if _, err = client.RemoveMembers(ctx, d.Id(), &membersForRemoval); err != nil { - return tf.ErrorDiagF(err, "Could not remove members from group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not remove members from group with object ID: %q", d.Id()) } } - if membersToAdd != nil { + if len(membersToAdd) > 0 { + newMembers := make(msgraph.Members, 0) for _, m := range membersToAdd { - group.AppendMember(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, m) + memberObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve principal object %q", m) + } + if memberObject == nil { + return tf.ErrorDiagF(errors.New("returned memberObject was nil"), "Could not retrieve member principal object %q", m) + } + newMembers = append(newMembers, *memberObject) } + group.Members = &newMembers if _, err := client.AddMembers(ctx, &group); err != nil { - return tf.ErrorDiagF(err, "Could not add members to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add members to group with object ID: %q", d.Id()) } } } @@ -526,29 +595,36 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter if v, ok := d.GetOk("owners"); ok && d.HasChange("owners") { owners, _, err := client.ListOwners(ctx, *group.ID) if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve owners for group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not retrieve owners for group with object ID: %q", d.Id()) } - // The calling principal should always be an owner, regardless of the owners property - desiredOwners := utils.EnsureStringInSlice(*tf.ExpandStringSlicePtr(v.(*schema.Set).List()), callerId) - + desiredOwners := *tf.ExpandStringSlicePtr(v.(*schema.Set).List()) existingOwners := *owners ownersForRemoval := utils.Difference(existingOwners, desiredOwners) ownersToAdd := utils.Difference(desiredOwners, existingOwners) - if ownersToAdd != nil { + if len(ownersToAdd) > 0 { + newOwners := make(msgraph.Owners, 0) for _, m := range ownersToAdd { - group.AppendOwner(client.BaseClient.Endpoint, client.BaseClient.ApiVersion, m) + ownerObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", m) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", m) + } + newOwners = append(newOwners, *ownerObject) } + group.Owners = &newOwners if _, err := client.AddOwners(ctx, &group); err != nil { - return tf.ErrorDiagF(err, "Could not add owners to group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not add owners to group with object ID: %q", d.Id()) } } - if ownersForRemoval != nil { + if len(ownersForRemoval) > 0 { if _, err = client.RemoveOwners(ctx, d.Id(), &ownersForRemoval); err != nil { - return tf.ErrorDiagF(err, "Could not remove owners from group with ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not remove owners from group with object ID: %q", d.Id()) } } } diff --git a/internal/services/groups/group_resource_test.go b/internal/services/groups/group_resource_test.go index caaf630d5e..b4e55ce859 100644 --- a/internal/services/groups/group_resource_test.go +++ b/internal/services/groups/group_resource_test.go @@ -113,92 +113,89 @@ func TestAccGroup_owners(t *testing.T) { r := GroupResource{} data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + { + Config: r.withOneOwner(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.withNoOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), { Config: r.withThreeOwners(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("3"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_members(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withThreeMembers(data), + Config: r.withOneOwner(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_membersAndOwners(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withOwnersAndMembers(data), + Config: r.withServicePrincipalOwner(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_manyMembersAndOwners(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withManyOwnersAndMembers(data), + Config: r.withDiverseOwners(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("2"), ), }, data.ImportStep(), - }) -} - -func TestAccGroup_membersDiverse(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withDiverseMembers(data), + Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), ), }, data.ImportStep(), }) } - -func TestAccGroup_ownersDiverse(t *testing.T) { +func TestAccGroup_createWithNoOwners(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_group", "test") r := GroupResource{} data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withDiverseOwners(data), + Config: r.withNoOwners(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), ), }, data.ImportStep(), }) } -func TestAccGroup_membersUpdate(t *testing.T) { +func TestAccGroup_members(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_group", "test") r := GroupResource{} @@ -207,20 +204,23 @@ func TestAccGroup_membersUpdate(t *testing.T) { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("1"), ), }, data.ImportStep(), { - Config: r.withOneMember(data), + Config: r.withThreeMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("3"), ), }, data.ImportStep(), { - Config: r.withThreeMembers(data), + Config: r.withOneMember(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("1"), ), }, data.ImportStep(), @@ -228,49 +228,57 @@ func TestAccGroup_membersUpdate(t *testing.T) { Config: r.withServicePrincipalMember(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.withDiverseMembers(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("3"), ), }, data.ImportStep(), { - Config: r.noMembers(data), + Config: r.withNoMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("0"), ), }, data.ImportStep(), }) } -func TestAccGroup_ownersUpdate(t *testing.T) { +func TestAccGroup_membersAndOwners(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_group", "test") r := GroupResource{} data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.basic(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - { - Config: r.withThreeOwners(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - ), - }, - data.ImportStep(), - { - Config: r.withOneOwner(data), + Config: r.withOwnersAndMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("2"), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), + }) +} + +func TestAccGroup_manyMembersAndOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_group", "test") + r := GroupResource{} + + data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.withServicePrincipalOwner(data), + Config: r.withManyOwnersAndMembers(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("66"), + check.That(data.ResourceName).Key("owners.#").HasValue("45"), ), }, data.ImportStep(), @@ -507,30 +515,44 @@ resource "azuread_group" "test" { `, data.RandomInteger, visibility) } -func (GroupResource) noMembers(data acceptance.TestData) string { +func (GroupResource) withNoOwners(data acceptance.TestData) string { return fmt.Sprintf(` resource "azuread_group" "test" { display_name = "acctestGroup-%[1]d" security_enabled = true - members = [] + owners = [] } `, data.RandomInteger) } -func (r GroupResource) withDiverseMembers(data acceptance.TestData) string { +func (r GroupResource) withOneOwner(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true - members = [ - azuread_user.test.object_id, - azuread_group.member.object_id, - azuread_service_principal.test.object_id - ] + owners = [azuread_user.testA.object_id] } -`, r.templateDiverseDirectoryObjects(data), data.RandomInteger) +`, r.templateThreeUsers(data), data.RandomInteger) +} + +func (GroupResource) withServicePrincipalOwner(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + display_name = "acctestGroup-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id +} + +resource "azuread_group" "test" { + display_name = "acctestGroup-%[1]d" + security_enabled = true + owners = [azuread_service_principal.test.object_id] +} +`, data.RandomInteger) } func (r GroupResource) withDiverseOwners(data acceptance.TestData) string { @@ -541,38 +563,70 @@ resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true owners = [ + azuread_service_principal.test.object_id, azuread_user.test.object_id, - azuread_service_principal.test.object_id ] } `, r.templateDiverseDirectoryObjects(data), data.RandomInteger) } -func (r GroupResource) withOneMember(data acceptance.TestData) string { +func (r GroupResource) withThreeOwners(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true - members = [azuread_user.testA.object_id] + owners = [ + azuread_user.testA.object_id, + azuread_user.testB.object_id, + azuread_user.testC.object_id + ] } `, r.templateThreeUsers(data), data.RandomInteger) } -func (r GroupResource) withOneOwner(data acceptance.TestData) string { +func (GroupResource) withNoMembers(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_group" "test" { + display_name = "acctestGroup-%[1]d" + security_enabled = true + members = [] +} +`, data.RandomInteger) +} + +func (r GroupResource) withOneMember(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true - owners = [azuread_user.testA.object_id] + members = [azuread_user.testA.object_id] } `, r.templateThreeUsers(data), data.RandomInteger) } -func (r GroupResource) withThreeMembers(data acceptance.TestData) string { +func (GroupResource) withServicePrincipalMember(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + display_name = "acctestGroup-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id +} + +resource "azuread_group" "test" { + display_name = "acctestGroup-%[1]d" + security_enabled = true + members = [azuread_service_principal.test.object_id] +} +`, data.RandomInteger) +} + +func (r GroupResource) withDiverseMembers(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s @@ -580,22 +634,22 @@ resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true members = [ - azuread_user.testA.object_id, - azuread_user.testB.object_id, - azuread_user.testC.object_id + azuread_user.test.object_id, + azuread_group.member.object_id, + azuread_service_principal.test.object_id ] } -`, r.templateThreeUsers(data), data.RandomInteger) +`, r.templateDiverseDirectoryObjects(data), data.RandomInteger) } -func (r GroupResource) withThreeOwners(data acceptance.TestData) string { +func (r GroupResource) withThreeMembers(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s resource "azuread_group" "test" { display_name = "acctestGroup-%[2]d" security_enabled = true - owners = [ + members = [ azuread_user.testA.object_id, azuread_user.testB.object_id, azuread_user.testC.object_id @@ -622,59 +676,51 @@ resource "azuread_group" "test" { func (r GroupResource) withManyOwnersAndMembers(data acceptance.TestData) string { return fmt.Sprintf(` +data "azuread_client_config" "test" {} + data "azuread_domains" "test" { only_initial = true } -resource "azuread_user" "test" { - count = 25 - - user_principal_name = "acctestGroupParticipant${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" - display_name = "acctestGroupParticipant${count.index}-%[1]d" - password = "Qwer5678!@#" -} - -resource "azuread_group" "test" { - display_name = "acctestGroup-%[1]d" +resource "azuread_group" "member" { + count = 21 + display_name = "acctestGroupParticipant${count.index}-%[1]d" security_enabled = true - owners = azuread_user.test.*.object_id - members = azuread_user.test.*.object_id -} -`, data.RandomInteger) } -func (GroupResource) withServicePrincipalMember(data acceptance.TestData) string { - return fmt.Sprintf(` resource "azuread_application" "test" { - display_name = "acctestGroup-%[1]d" + count = 27 + display_name = "acctestGroupParticipant${count.index}-%[1]d" } resource "azuread_service_principal" "test" { - application_id = azuread_application.test.application_id -} - -resource "azuread_group" "test" { - display_name = "acctestGroup-%[1]d" - security_enabled = true - members = [azuread_service_principal.test.object_id] -} -`, data.RandomInteger) + count = 27 + application_id = azuread_application.test[count.index].application_id } -func (GroupResource) withServicePrincipalOwner(data acceptance.TestData) string { - return fmt.Sprintf(` -resource "azuread_application" "test" { - display_name = "acctestGroup-%[1]d" -} - -resource "azuread_service_principal" "test" { - application_id = azuread_application.test.application_id +resource "azuread_user" "test" { + count = 17 + user_principal_name = "acctestGroupParticipant${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestGroupParticipant${count.index}-%[1]d" + password = "Qwer5678!@#" } resource "azuread_group" "test" { display_name = "acctestGroup-%[1]d" security_enabled = true - owners = [azuread_service_principal.test.object_id] + + owners = flatten([ + data.azuread_client_config.test.object_id, + azuread_service_principal.test.*.object_id, + azuread_user.test.*.object_id, + ]) + + members = flatten([ + data.azuread_client_config.test.object_id, + azuread_group.member.*.object_id, + azuread_service_principal.test.*.object_id, + azuread_user.test.*.object_id, + ]) } `, data.RandomInteger) } diff --git a/internal/services/serviceprincipals/client/client.go b/internal/services/serviceprincipals/client/client.go index d3928811fd..ee8acac1cb 100644 --- a/internal/services/serviceprincipals/client/client.go +++ b/internal/services/serviceprincipals/client/client.go @@ -7,14 +7,19 @@ import ( ) type Client struct { + DirectoryObjectsClient *msgraph.DirectoryObjectsClient ServicePrincipalsClient *msgraph.ServicePrincipalsClient } func NewClient(o *common.ClientOptions) *Client { - msClient := msgraph.NewServicePrincipalsClient(o.TenantID) - o.ConfigureClient(&msClient.BaseClient) + directoryObjectsClient := msgraph.NewDirectoryObjectsClient(o.TenantID) + o.ConfigureClient(&directoryObjectsClient.BaseClient) + + servicePrincipalsClient := msgraph.NewServicePrincipalsClient(o.TenantID) + o.ConfigureClient(&servicePrincipalsClient.BaseClient) return &Client{ - ServicePrincipalsClient: msClient, + DirectoryObjectsClient: directoryObjectsClient, + ServicePrincipalsClient: servicePrincipalsClient, } } diff --git a/internal/services/serviceprincipals/service_principal_certificate_resource.go b/internal/services/serviceprincipals/service_principal_certificate_resource.go index ad8a35af2c..b9b82e05f5 100644 --- a/internal/services/serviceprincipals/service_principal_certificate_resource.go +++ b/internal/services/serviceprincipals/service_principal_certificate_resource.go @@ -163,7 +163,9 @@ func servicePrincipalCertificateResourceCreate(ctx context.Context, d *schema.Re newCredentials = append(newCredentials, *credential) properties := msgraph.ServicePrincipal{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { @@ -257,7 +259,9 @@ func servicePrincipalCertificateResourceDelete(ctx context.Context, d *schema.Re } properties := msgraph.ServicePrincipal{ - ID: &id.ObjectId, + DirectoryObject: msgraph.DirectoryObject{ + ID: &id.ObjectId, + }, KeyCredentials: &newCredentials, } if _, err := client.Update(ctx, properties); err != nil { diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index d29adadde1..a9b0abf605 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -33,9 +33,9 @@ func servicePrincipalResource() *schema.Resource { DeleteContext: servicePrincipalResourceDelete, Timeouts: &schema.ResourceTimeout{ - Create: schema.DefaultTimeout(5 * time.Minute), + Create: schema.DefaultTimeout(10 * time.Minute), Read: schema.DefaultTimeout(5 * time.Minute), - Update: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(10 * time.Minute), Delete: schema.DefaultTimeout(5 * time.Minute), }, @@ -109,6 +109,17 @@ func servicePrincipalResource() *schema.Resource { }, }, + "owners": { + Description: "A list of object IDs of principals that will be granted ownership of the service principal", + Type: schema.TypeSet, + Optional: true, + Set: schema.HashString, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateDiagFunc: validate.UUID, + }, + }, + "preferred_single_sign_on_mode": { Description: "The single sign-on mode configured for this application. Azure AD uses the preferred single sign-on mode to launch the application from Microsoft 365 or the Azure AD My Apps", Type: schema.TypeString, @@ -231,6 +242,8 @@ func servicePrincipalResource() *schema.Resource { func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + directoryObjectsClient := meta.(*clients.Client).ServicePrincipals.DirectoryObjectsClient + callerId := meta.(*clients.Client).Claims.ObjectId appId := d.Get("application_id").(string) result, _, err := client.List(ctx, odata.Query{Filter: fmt.Sprintf("appId eq '%s'", appId)}) @@ -272,6 +285,52 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), } + // Chunk the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + ownerChunks := make([]msgraph.Owners, 2) + + // The calling principal should always be in the first block of owners + callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) + } + if callerObject == nil { + return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) + } + ownerChunks[0] = msgraph.Owners{*callerObject} + + // Track whether we need to remove the calling principal later on + removeCallerOwner := true + + // Retrieve and set the initial owners, which can be up to 20 in total when creating the service principal + if v, ok := d.GetOk("owners"); ok { + c := 0 + for _, id := range v.(*schema.Set).List() { + i := 0 + if c >= 19 { + i = 1 + } + if strings.EqualFold(id.(string), callerId) { + removeCallerOwner = false + continue + } + ownerObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("ownerObject was nil"), "Could not retrieve owner principal object %q", id) + } + if ownerObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) + } + ownerChunks[i] = append(ownerChunks[i], *ownerObject) + c++ + } + } + + // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration + properties.Owners = &ownerChunks[0] + servicePrincipal, _, err = client.Create(ctx, properties) if err != nil { return tf.ErrorDiagF(err, "Could not create service principal") @@ -282,14 +341,32 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, } d.SetId(*servicePrincipal.ID) + // Add any remaining owners after the service principal is created + if len(ownerChunks[1]) > 0 { + servicePrincipal.Owners = &ownerChunks[1] + if _, err := client.AddOwners(ctx, servicePrincipal); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to service principal with object ID: %q", d.Id()) + } + } + + // If the calling principal was not included in configuration, remove it now + if removeCallerOwner { + if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { + return tf.ErrorDiagF(err, "Could not remove initial owner from group with object ID: %q", d.Id()) + } + } + return servicePrincipalResourceRead(ctx, d, meta) } func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + directoryObjectsClient := meta.(*clients.Client).ServicePrincipals.DirectoryObjectsClient properties := msgraph.ServicePrincipal{ - ID: utils.String(d.Id()), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(d.Id()), + }, AlternativeNames: tf.ExpandStringSlicePtr(d.Get("alternative_names").(*schema.Set).List()), AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)), AppRoleAssignmentRequired: utils.Bool(d.Get("app_role_assignment_required").(bool)), @@ -305,6 +382,43 @@ func servicePrincipalResourceUpdate(ctx context.Context, d *schema.ResourceData, return tf.ErrorDiagF(err, "Updating service principal with object ID: %q", d.Id()) } + if v, ok := d.GetOk("owners"); ok && d.HasChange("owners") { + owners, _, err := client.ListOwners(ctx, d.Id()) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owners for service principal with object ID: %q", d.Id()) + } + + desiredOwners := *tf.ExpandStringSlicePtr(v.(*schema.Set).List()) + existingOwners := *owners + ownersForRemoval := utils.Difference(existingOwners, desiredOwners) + ownersToAdd := utils.Difference(desiredOwners, existingOwners) + + if len(ownersToAdd) > 0 { + newOwners := make(msgraph.Owners, 0) + for _, m := range ownersToAdd { + ownerObject, _, err := directoryObjectsClient.Get(ctx, m, odata.Query{}) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", m) + } + if ownerObject == nil { + return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", m) + } + newOwners = append(newOwners, *ownerObject) + } + + properties.Owners = &newOwners + if _, err := client.AddOwners(ctx, &properties); err != nil { + return tf.ErrorDiagF(err, "Could not add owners to service principal with object ID: %q", d.Id()) + } + } + + if len(ownersForRemoval) > 0 { + if _, err = client.RemoveOwners(ctx, d.Id(), &ownersForRemoval); err != nil { + return tf.ErrorDiagF(err, "Could not remove owners from service principal with object ID: %q", d.Id()) + } + } + } + return servicePrincipalResourceRead(ctx, d, meta) } @@ -358,6 +472,12 @@ func servicePrincipalResourceRead(ctx context.Context, d *schema.ResourceData, m tf.Set(d, "tags", servicePrincipal.Tags) tf.Set(d, "type", servicePrincipal.ServicePrincipalType) + owners, _, err := client.ListOwners(ctx, *servicePrincipal.ID) + if err != nil { + return tf.ErrorDiagPathF(err, "owners", "Could not retrieve owners for service principal with object ID %q", d.Id()) + } + tf.Set(d, "owners", owners) + return nil } diff --git a/internal/services/serviceprincipals/service_principal_resource_test.go b/internal/services/serviceprincipals/service_principal_resource_test.go index 68fdd43097..1e39d1d8e2 100644 --- a/internal/services/serviceprincipals/service_principal_resource_test.go +++ b/internal/services/serviceprincipals/service_principal_resource_test.go @@ -102,6 +102,94 @@ func TestAccServicePrincipal_update(t *testing.T) { }) } +func TestAccServicePrincipal_owners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal", "test") + r := ServicePrincipalResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + { + Config: r.singleOwner(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + { + Config: r.singleOwner(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), + { + Config: r.threeOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("3"), + ), + }, + data.ImportStep(), + { + Config: r.basic(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccApplication_createWithNoOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal", "test") + r := ServicePrincipalResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.noOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("0"), + ), + }, + data.ImportStep(), + }) +} + +func TestAccServicePrincipal_manyOwners(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_service_principal", "test") + r := ServicePrincipalResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.manyOwners(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("owners.#").HasValue("45"), + ), + }, + data.ImportStep(), + }) +} + func TestAccServicePrincipal_useExisting(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_service_principal", "msgraph") r := ServicePrincipalResource{} @@ -230,6 +318,125 @@ resource "azuread_service_principal" "test" { `, data.RandomInteger, data.UUID(), data.UUID(), data.UUID(), data.UUID()) } +func (ServicePrincipalResource) templateThreeUsers(data acceptance.TestData) string { + return fmt.Sprintf(` +provider "azuread" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_user" "testA" { + user_principal_name = "acctestUser.%[1]d.A@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d-A" + password = "%[2]s" +} + +resource "azuread_user" "testB" { + user_principal_name = "acctestUser.%[1]d.B@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d-B" + mail_nickname = "acctestUser-%[1]d-B" + password = "%[2]s" +} + +resource "azuread_user" "testC" { + user_principal_name = "acctestUser.%[1]d.C@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestUser-%[1]d-C" + password = "%[2]s" +} +`, data.RandomInteger, data.RandomPassword) +} + +func (ServicePrincipalResource) noOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + owners = [] +} +`, data.RandomInteger) +} + +func (r ServicePrincipalResource) singleOwner(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[2]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + owners = [ + azuread_user.testA.object_id, + ] +} +`, r.templateThreeUsers(data), data.RandomInteger) +} + +func (r ServicePrincipalResource) threeOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[2]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + owners = [ + azuread_user.testA.object_id, + azuread_user.testB.object_id, + azuread_user.testC.object_id, + ] +} +`, r.templateThreeUsers(data), data.RandomInteger) +} + +func (r ServicePrincipalResource) manyOwners(data acceptance.TestData) string { + return fmt.Sprintf(` +data "azuread_client_config" "test" {} + +data "azuread_domains" "test" { + only_initial = true +} + +resource "azuread_application" "owner" { + count = 27 + display_name = "acctestServicePrincipalOwner${count.index}-%[1]d" +} + +resource "azuread_service_principal" "owner" { + count = 27 + application_id = azuread_application.owner[count.index].application_id +} + +resource "azuread_user" "owner" { + count = 17 + user_principal_name = "acctestServicePrincipalOwner${count.index}-%[1]d@${data.azuread_domains.test.domains.0.domain_name}" + display_name = "acctestServicePrincipalOwner${count.index}-%[1]d" + password = "Qwer5678!@#" +} + +resource "azuread_application" "test" { + display_name = "acctestServicePrincipal-%[1]d" +} + +resource "azuread_service_principal" "test" { + application_id = azuread_application.test.application_id + + owners = flatten([ + data.azuread_client_config.test.object_id, + azuread_service_principal.owner.*.object_id, + azuread_user.owner.*.object_id, + ]) +} +`, data.RandomInteger) +} + func (ServicePrincipalResource) useExisting(_ acceptance.TestData) string { return ` resource "azuread_service_principal" "msgraph" { diff --git a/internal/services/users/user_resource.go b/internal/services/users/user_resource.go index 82c7be73cb..e7ffe0dc2e 100644 --- a/internal/services/users/user_resource.go +++ b/internal/services/users/user_resource.go @@ -425,7 +425,9 @@ func userResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interf client := meta.(*clients.Client).Users.UsersClient properties := msgraph.User{ - ID: utils.String(d.Id()), + DirectoryObject: msgraph.DirectoryObject{ + ID: utils.String(d.Id()), + }, AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)), AgeGroup: utils.NullableString(d.Get("age_group").(string)), City: utils.NullableString(d.Get("city").(string)), From 0f291291ec1ed59d46a1d57842cf8c0a6cecd965 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Aug 2021 00:39:22 +0100 Subject: [PATCH 03/11] Update to Groups: groups cannot be created with no owners or have all of their owners removed under any circumstances --- docs/resources/group.md | 4 +- .../services/groups/group_member_resource.go | 14 ++- internal/services/groups/group_resource.go | 112 ++++++++++++------ .../services/groups/group_resource_test.go | 37 +----- 4 files changed, 85 insertions(+), 82 deletions(-) diff --git a/docs/resources/group.md b/docs/resources/group.md index 1fb43ef2f7..49b07876b4 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -81,9 +81,9 @@ The following arguments are supported: * `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. A group can be mail enabled _and_ security enabled. * `mail_nickname` - (Optional) The mail alias for the group, unique in the organisation. Required for mail-enabled groups. Changing this forces a new resource to be created. * `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals. -* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, no owners are assigned. +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, the principal being used to execute Terraform is assigned as the sole owner. Groups cannot be created with no owners. --> **Group Ownership** It's recommended to always specify one or more group owners, including the principal being used to execute Terraform, such as in the example above. Microsoft 365 groups are required to have at least one owner which _must be a user_ (i.e. not a service principal). +-> **Group Ownership** It's recommended to always specify one or more group owners, including the principal being used to execute Terraform, such as in the example above. Microsoft 365 groups are required to have at least one owner which _must be a user_ (i.e. not a service principal). * `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing group is found with the same name. Defaults to `false`. * `provisioning_options` - (Optional) A set of provisioning options for a Microsoft 365 group. The only supported value is `Team`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for details. Changing this forces a new resource to be created. diff --git a/internal/services/groups/group_member_resource.go b/internal/services/groups/group_member_resource.go index 5435ad8a35..7093c7b5c4 100644 --- a/internal/services/groups/group_member_resource.go +++ b/internal/services/groups/group_member_resource.go @@ -8,10 +8,9 @@ import ( "strings" "time" - "github.com/manicminer/hamilton/msgraph" - "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/manicminer/hamilton/msgraph" "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" @@ -89,14 +88,17 @@ func groupMemberResourceCreate(ctx context.Context, d *schema.ResourceData, meta } } - ownerObject, _, err := directoryObjectsClient.Get(ctx, memberId, odata.Query{}) + memberObject, _, err := directoryObjectsClient.Get(ctx, memberId, odata.Query{}) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve principal object %q", memberId) } - if ownerObject == nil { - return tf.ErrorDiagF(errors.New("returned ownerObject was nil"), "Could not retrieve owner principal object %q", memberId) + if memberObject == nil { + return tf.ErrorDiagF(errors.New("returned memberObject was nil"), "Could not retrieve member principal object %q", memberId) + } + if memberObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve member principal object %q", memberId) } - group.Owners = &msgraph.Owners{*ownerObject} + group.Members = &msgraph.Members{*memberObject} if _, err := client.AddMembers(ctx, group); err != nil { return tf.ErrorDiagF(err, "Adding group member %q to group %q", memberId, groupId) diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index a4aded1ff5..7b0783d78d 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -6,7 +6,6 @@ import ( "fmt" "log" "net/http" - "strings" "time" "github.com/hashicorp/go-uuid" @@ -116,6 +115,8 @@ func groupResource() *schema.Resource { Description: "A set of owners who own this group. Supported object types are Users or Service Principals", Type: schema.TypeSet, Optional: true, + Computed: true, + MinItems: 1, Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, @@ -337,6 +338,7 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter client := meta.(*clients.Client).Groups.GroupsClient directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient callerId := meta.(*clients.Client).Claims.ObjectId + displayName := d.Get("display_name").(string) // Perform this check at apply time to catch any duplicate names created during the same apply @@ -401,47 +403,66 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter // Chunk the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice ownerChunks := make([]msgraph.Owners, 2) - // The calling principal should always be in the first block of owners - callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) - if err != nil { - return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) - } - if callerObject == nil { - return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) + // getOwnerObject retrieves and validates a DirectoryObject for a given object ID + getOwnerObject := func(ctx context.Context, id string) (*msgraph.DirectoryObject, error) { + ownerObject, _, err := directoryObjectsClient.Get(ctx, id, odata.Query{}) + if err != nil { + return nil, err + } + if ownerObject == nil { + return nil, errors.New("ownerObject was nil") + } + if ownerObject.ID == nil { + return nil, errors.New("ownerObject ID was nil") + } + if ownerObject.ODataId == nil { + return nil, errors.New("ODataId was nil") + } + if ownerObject.ODataType == nil { + return nil, errors.New("ownerObject ODataType was nil") + } + return ownerObject, nil } - ownerChunks[0] = msgraph.Owners{*callerObject} - - // Track whether we need to remove the calling principal later on - removeCallerOwner := true // Retrieve and set the initial owners, which can be up to 20 in total when creating the group. - // Prefer users first, then service principals, to try and avoid API validation errors for Microsoft 365 groups. + // First look for the calling principal, then prefer users, followed by service principals, to try and avoid + // ownership-related API validation errors for Microsoft 365 groups. if v, ok := d.GetOk("owners"); ok { + owners := v.(*schema.Set).List() c := 0 - for _, t := range []odata.Type{odata.TypeUser, odata.TypeServicePrincipal, odata.TypeGroup} { - for _, id := range v.(*schema.Set).List() { + + // First look for the calling principal in the specified owners; it should always be included in the initial + // owners to avoid orphaning a group when the caller doesn't have the Groups.ReadWrite.All scope. + for _, id := range owners { + i := 0 + if c >= 20 { + i = 1 + } + ownerObject, err := getOwnerObject(ctx, id.(string)) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) + } + if *ownerObject.ID == callerId { + if ownerObject.ODataId == nil { + return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) + } + ownerChunks[i] = append(ownerChunks[i], *ownerObject) + c++ + } + } + + // Then look for users, and finally service principals + for _, t := range []odata.Type{odata.TypeUser, odata.TypeServicePrincipal} { + for _, id := range owners { i := 0 - if c >= 19 { + if c >= 20 { i = 1 } - if strings.EqualFold(id.(string), callerId) { - removeCallerOwner = false - continue - } - ownerObject, _, err := directoryObjectsClient.Get(ctx, id.(string), odata.Query{}) + ownerObject, err := getOwnerObject(ctx, id.(string)) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) } - if ownerObject == nil { - return tf.ErrorDiagF(errors.New("ownerObject was nil"), "Could not retrieve owner principal object %q", id) - } - if ownerObject.ODataType == nil { - return tf.ErrorDiagF(errors.New("ODataType was nil"), "Could not retrieve owner principal object %q", id) - } if *ownerObject.ODataType == t { - if ownerObject.ODataId == nil { - return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) - } ownerChunks[i] = append(ownerChunks[i], *ownerObject) c++ } @@ -449,7 +470,17 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter } } - // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration + if len(ownerChunks[0]) == 0 { + // The calling principal is the default owner if no others are specified. This is the default API behaviour, so + // we're being explicit about this in order to minimise confusion and avoid inconsistent API behaviours. + callerObject, err := getOwnerObject(ctx, callerId) + if err != nil { + return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) + } + ownerChunks[0] = msgraph.Owners{*callerObject} + } + + // Set the initial owners, which either be the calling principal, or up to 20 of the owners specified in configuration properties.Owners = &ownerChunks[0] group, _, err := client.Create(ctx, properties) @@ -471,13 +502,6 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter } } - // If the calling principal was not included in configuration, remove it now - if removeCallerOwner { - if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { - return tf.ErrorDiagF(err, "Could not remove initial owner from group with object ID: %q", d.Id()) - } - } - // Add members after the group is created members := make(msgraph.Members, 0) if v, ok := d.GetOk("members"); ok { @@ -508,6 +532,8 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient directoryObjectsClient := meta.(*clients.Client).Groups.DirectoryObjectsClient + callerId := meta.(*clients.Client).Claims.ObjectId + groupId := d.Id() displayName := d.Get("display_name").(string) @@ -598,7 +624,15 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter return tf.ErrorDiagF(err, "Could not retrieve owners for group with object ID: %q", d.Id()) } - desiredOwners := *tf.ExpandStringSlicePtr(v.(*schema.Set).List()) + // If all owners are removed, restore the calling principal as the sole owner, in order to meet API + // restrictions about removing all owners, and maintain consistency with the Create behaviour. + // In theory this path should never be reached, since the property is Computed and conditionally ForceNew for + // the case of changing from some owners to zero owners, but we handle it anyway. + desiredOwners := tf.ExpandStringSlice(v.(*schema.Set).List()) + if len(desiredOwners) == 0 { + desiredOwners = []string{callerId} + } + existingOwners := *owners ownersForRemoval := utils.Difference(existingOwners, desiredOwners) ownersToAdd := utils.Difference(desiredOwners, existingOwners) diff --git a/internal/services/groups/group_resource_test.go b/internal/services/groups/group_resource_test.go index b4e55ce859..203506388b 100644 --- a/internal/services/groups/group_resource_test.go +++ b/internal/services/groups/group_resource_test.go @@ -117,7 +117,7 @@ func TestAccGroup_owners(t *testing.T) { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("owners.#").HasValue("0"), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), ), }, data.ImportStep(), @@ -129,14 +129,6 @@ func TestAccGroup_owners(t *testing.T) { ), }, data.ImportStep(), - { - Config: r.withNoOwners(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("owners.#").HasValue("0"), - ), - }, - data.ImportStep(), { Config: r.withThreeOwners(data), Check: resource.ComposeTestCheckFunc( @@ -173,22 +165,7 @@ func TestAccGroup_owners(t *testing.T) { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("owners.#").HasValue("0"), - ), - }, - data.ImportStep(), - }) -} -func TestAccGroup_createWithNoOwners(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_group", "test") - r := GroupResource{} - - data.ResourceTest(t, r, []resource.TestStep{ - { - Config: r.withNoOwners(data), - Check: resource.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("owners.#").HasValue("0"), + check.That(data.ResourceName).Key("owners.#").HasValue("2"), ), }, data.ImportStep(), @@ -515,16 +492,6 @@ resource "azuread_group" "test" { `, data.RandomInteger, visibility) } -func (GroupResource) withNoOwners(data acceptance.TestData) string { - return fmt.Sprintf(` -resource "azuread_group" "test" { - display_name = "acctestGroup-%[1]d" - security_enabled = true - owners = [] -} -`, data.RandomInteger) -} - func (r GroupResource) withOneOwner(data acceptance.TestData) string { return fmt.Sprintf(` %[1]s From 35c2bb472abc8e4cd00a779991305fc72adf3a65 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Aug 2021 01:36:15 +0100 Subject: [PATCH 04/11] Document inability to remove the last user owner from a group, add a test case, some error text fixes --- docs/resources/group.md | 2 +- .../applications/application_resource.go | 2 +- .../services/groups/group_resource_test.go | 36 ++++++++++++++++--- .../service_principal_resource.go | 2 +- 4 files changed, 35 insertions(+), 7 deletions(-) diff --git a/docs/resources/group.md b/docs/resources/group.md index 49b07876b4..428e04f422 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -83,7 +83,7 @@ The following arguments are supported: * `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals. * `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, the principal being used to execute Terraform is assigned as the sole owner. Groups cannot be created with no owners. --> **Group Ownership** It's recommended to always specify one or more group owners, including the principal being used to execute Terraform, such as in the example above. Microsoft 365 groups are required to have at least one owner which _must be a user_ (i.e. not a service principal). +-> **Group Ownership** It's recommended to always specify one or more group owners, including the principal being used to execute Terraform, such as in the example above. When removing group owners, if a user principal has been assigned ownership, the last user cannot be removed as an owner. Microsoft 365 groups are required to always have at least one owner which _must be a user_ (i.e. not a service principal). * `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing group is found with the same name. Defaults to `false`. * `provisioning_options` - (Optional) A set of provisioning options for a Microsoft 365 group. The only supported value is `Team`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for details. Changing this forces a new resource to be created. diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index 7a1cec022d..aec43a685d 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -917,7 +917,7 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta // If the calling principal was not included in configuration, remove it now if removeCallerOwner { if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { - return tf.ErrorDiagF(err, "Could not remove initial owner from group with object ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not remove initial owner from application with object ID: %q", d.Id()) } } diff --git a/internal/services/groups/group_resource_test.go b/internal/services/groups/group_resource_test.go index 203506388b..46dcca94d6 100644 --- a/internal/services/groups/group_resource_test.go +++ b/internal/services/groups/group_resource_test.go @@ -181,7 +181,7 @@ func TestAccGroup_members(t *testing.T) { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("members.#").HasValue("1"), + check.That(data.ResourceName).Key("members.#").HasValue("0"), ), }, data.ImportStep(), @@ -259,6 +259,15 @@ func TestAccGroup_manyMembersAndOwners(t *testing.T) { ), }, data.ImportStep(), + { + Config: r.withOneOwnerAndNoMembers(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("members.#").HasValue("0"), + check.That(data.ResourceName).Key("owners.#").HasValue("1"), + ), + }, + data.ImportStep(), }) } @@ -641,7 +650,7 @@ resource "azuread_group" "test" { `, r.templateThreeUsers(data), data.RandomInteger) } -func (r GroupResource) withManyOwnersAndMembers(data acceptance.TestData) string { +func (GroupResource) manyObjectsTemplate(data acceptance.TestData) string { return fmt.Sprintf(` data "azuread_client_config" "test" {} @@ -671,9 +680,15 @@ resource "azuread_user" "test" { display_name = "acctestGroupParticipant${count.index}-%[1]d" password = "Qwer5678!@#" } +`, data.RandomInteger) +} + +func (r GroupResource) withManyOwnersAndMembers(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s resource "azuread_group" "test" { - display_name = "acctestGroup-%[1]d" + display_name = "acctestGroup-%[2]d" security_enabled = true owners = flatten([ @@ -689,7 +704,20 @@ resource "azuread_group" "test" { azuread_user.test.*.object_id, ]) } -`, data.RandomInteger) +`, r.manyObjectsTemplate(data), data.RandomInteger) +} + +func (r GroupResource) withOneOwnerAndNoMembers(data acceptance.TestData) string { + return fmt.Sprintf(` +%[1]s + +resource "azuread_group" "test" { + display_name = "acctestGroup-%[2]d" + security_enabled = true + owners = [azuread_user.test.0.object_id] + members = [] +} +`, r.manyObjectsTemplate(data), data.RandomInteger) } func (GroupResource) preventDuplicateNamesPass(data acceptance.TestData) string { diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index a9b0abf605..1d0056f3bd 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -352,7 +352,7 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, // If the calling principal was not included in configuration, remove it now if removeCallerOwner { if _, err = client.RemoveOwners(ctx, d.Id(), &[]string{callerId}); err != nil { - return tf.ErrorDiagF(err, "Could not remove initial owner from group with object ID: %q", d.Id()) + return tf.ErrorDiagF(err, "Could not remove initial owner from service principal with object ID: %q", d.Id()) } } From e2c7f7155390c0acdcafb9523755b2175c8aabc3 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Aug 2021 02:52:27 +0100 Subject: [PATCH 05/11] Clarify group owner removal [inability] --- docs/resources/group.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/resources/group.md b/docs/resources/group.md index 428e04f422..b75c660aba 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -81,7 +81,7 @@ The following arguments are supported: * `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. A group can be mail enabled _and_ security enabled. * `mail_nickname` - (Optional) The mail alias for the group, unique in the organisation. Required for mail-enabled groups. Changing this forces a new resource to be created. * `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals. -* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, the principal being used to execute Terraform is assigned as the sole owner. Groups cannot be created with no owners. +* `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, the principal being used to execute Terraform is assigned as the sole owner. Groups cannot be created with no owners or have all their owners removed. -> **Group Ownership** It's recommended to always specify one or more group owners, including the principal being used to execute Terraform, such as in the example above. When removing group owners, if a user principal has been assigned ownership, the last user cannot be removed as an owner. Microsoft 365 groups are required to always have at least one owner which _must be a user_ (i.e. not a service principal). From c3e9b3bd541a3238da12ac1d71100e92789194f4 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Aug 2021 22:25:05 +0100 Subject: [PATCH 06/11] Groups: maximum of 100 owners per group --- internal/services/groups/group_resource.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index 7b0783d78d..6580516db6 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -117,6 +117,7 @@ func groupResource() *schema.Resource { Optional: true, Computed: true, MinItems: 1, + MaxItems: 100, Set: schema.HashString, Elem: &schema.Schema{ Type: schema.TypeString, From ee8b97d765f4ee39728fc773398b33539e9013e7 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Aug 2021 22:27:11 +0100 Subject: [PATCH 07/11] Applications: maximum of 100 owners per application --- internal/services/applications/application_resource.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index aec43a685d..c60ed3124d 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -336,6 +336,7 @@ func applicationResource() *schema.Resource { Type: schema.TypeSet, Optional: true, Set: schema.HashString, + MaxItems: 100, Elem: &schema.Schema{ Type: schema.TypeString, ValidateDiagFunc: validate.UUID, From b9413197a3e0913156fdef4f819cc0be0e749f03 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Aug 2021 22:33:25 +0100 Subject: [PATCH 08/11] Groups: docs clarification on group types and supported features --- docs/resources/group.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/resources/group.md b/docs/resources/group.md index b75c660aba..78a3575ffe 100644 --- a/docs/resources/group.md +++ b/docs/resources/group.md @@ -78,7 +78,7 @@ The following arguments are supported: * `behaviors` - (Optional) A set of behaviors for a Microsoft 365 group. Possible values are `AllowOnlyMembersToPost`, `HideGroupInOutlook`, `SubscribeNewGroupMembers` and `WelcomeEmailDisabled`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for more details. Changing this forces a new resource to be created. * `description` - (Optional) The description for the group. * `display_name` - (Required) The display name for the group. -* `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. A group can be mail enabled _and_ security enabled. +* `mail_enabled` - (Optional) Whether the group is a mail enabled, with a shared group mailbox. At least one of `mail_enabled` or `security_enabled` must be specified. Only Microsoft 365 groups can be mail enabled (see the `types` property). * `mail_nickname` - (Optional) The mail alias for the group, unique in the organisation. Required for mail-enabled groups. Changing this forces a new resource to be created. * `members` - (Optional) A set of members who should be present in this group. Supported object types are Users, Groups or Service Principals. * `owners` - (Optional) A set of object IDs of principals that will be granted ownership of the group. Supported object types are users or service principals. By default, the principal being used to execute Terraform is assigned as the sole owner. Groups cannot be created with no owners or have all their owners removed. @@ -87,9 +87,12 @@ The following arguments are supported: * `prevent_duplicate_names` - (Optional) If `true`, will return an error if an existing group is found with the same name. Defaults to `false`. * `provisioning_options` - (Optional) A set of provisioning options for a Microsoft 365 group. The only supported value is `Team`. See [official documentation](https://docs.microsoft.com/en-us/graph/group-set-options) for details. Changing this forces a new resource to be created. -* `security_enabled` - (Optional) Whether the group is a security group for controlling access to in-app resources. At least one of `security_enabled` or `mail_enabled` must be specified. A group can be security enabled _and_ mail enabled. +* `security_enabled` - (Optional) Whether the group is a security group for controlling access to in-app resources. At least one of `security_enabled` or `mail_enabled` must be specified. A Microsoft 365 group can be security enabled _and_ mail enabled (see the `types` property). * `theme` - (Optional) The colour theme for a Microsoft 365 group. Possible values are `Blue`, `Green`, `Orange`, `Pink`, `Purple`, `Red` or `Teal`. By default, no theme is set. * `types` - (Optional) A set of group types to configure for the group. The only supported type is `Unified`, which specifies a Microsoft 365 group. Required when `mail_enabled` is true. Changing this forces a new resource to be created. + +-> **Supported Group Types** At present, only security groups and Microsoft 365 groups can be created or managed with this resource. Distribution groups and mail-enabled security groups are not supported. Microsoft 365 groups can be security-enabled. + * `visibility` - (Optional) The group join policy and group content visibility. Possible values are `Private`, `Public`, or `Hiddenmembership`. Only Microsoft 365 groups can have `Hiddenmembership` visibility and this value must be set when the group is created. By default, security groups will receive `Private` visibility and Microsoft 365 groups will receive `Public` visibility. -> **Group Name Uniqueness** Group names are not unique within Azure Active Directory. Use the `prevent_duplicate_names` argument to check for existing groups if you want to avoid name collisions. From 129052a8b423a9dd99110d854a30119e15650a2b Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Tue, 17 Aug 2021 22:42:03 +0100 Subject: [PATCH 09/11] Fix comment --- internal/services/groups/group_resource.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index 6580516db6..e316d2ab0d 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -627,8 +627,7 @@ func groupResourceUpdate(ctx context.Context, d *schema.ResourceData, meta inter // If all owners are removed, restore the calling principal as the sole owner, in order to meet API // restrictions about removing all owners, and maintain consistency with the Create behaviour. - // In theory this path should never be reached, since the property is Computed and conditionally ForceNew for - // the case of changing from some owners to zero owners, but we handle it anyway. + // In theory this path should never be reached, since the property is Computed and has MinItems: 1, but we handle it anyway. desiredOwners := tf.ExpandStringSlice(v.(*schema.Set).List()) if len(desiredOwners) == 0 { desiredOwners = []string{callerId} From a1e37785c409d32b7c4dbe2d11076a124bf7cdf2 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Fri, 20 Aug 2021 00:18:44 +0100 Subject: [PATCH 10/11] Address review: use two vars instead of a slice of slices for better clarity, improved naming for counter variable --- .../applications/application_resource.go | 29 +++++++------- internal/services/groups/group_resource.go | 40 +++++++++---------- .../service_principal_resource.go | 29 +++++++------- 3 files changed, 48 insertions(+), 50 deletions(-) diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index c60ed3124d..6d16d59891 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -850,10 +850,8 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta Web: expandApplicationWeb(d.Get("web").([]interface{})), } - // Chunk the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice - ownerChunks := make([]msgraph.Owners, 2) - - // The calling principal should always be in the first block of owners + // Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + // The calling principal should always be in the first slice of owners callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) @@ -861,19 +859,16 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta if callerObject == nil { return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) } - ownerChunks[0] = msgraph.Owners{*callerObject} + ownersFirst20 := msgraph.Owners{*callerObject} + var ownersExtra msgraph.Owners // Track whether we need to remove the calling principal later on removeCallerOwner := true // Retrieve and set the initial owners, which can be up to 20 in total when creating the application if v, ok := d.GetOk("owners"); ok { - c := 0 + ownerCount := 0 for _, id := range v.(*schema.Set).List() { - i := 0 - if c >= 19 { - i = 1 - } if strings.EqualFold(id.(string), callerId) { removeCallerOwner = false continue @@ -888,13 +883,17 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta if ownerObject.ODataId == nil { return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) } - ownerChunks[i] = append(ownerChunks[i], *ownerObject) - c++ + if ownerCount < 19 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ } } // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration - properties.Owners = &ownerChunks[0] + properties.Owners = &ownersFirst20 app, _, err := client.Create(ctx, properties) if err != nil { @@ -907,9 +906,9 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(*app.ID) - if len(ownerChunks[1]) > 0 { + if len(ownersExtra) > 0 { // Add any remaining owners after the application is created - app.Owners = &ownerChunks[1] + app.Owners = &ownersExtra if _, err := client.AddOwners(ctx, app); err != nil { return tf.ErrorDiagF(err, "Could not add owners to application with object ID: %q", d.Id()) } diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index e316d2ab0d..e1906dad0e 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -401,8 +401,8 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter properties.Visibility = utils.String(visibility) } - // Chunk the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice - ownerChunks := make([]msgraph.Owners, 2) + // Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + var ownersFirst20, ownersExtra msgraph.Owners // getOwnerObject retrieves and validates a DirectoryObject for a given object ID getOwnerObject := func(ctx context.Context, id string) (*msgraph.DirectoryObject, error) { @@ -430,15 +430,11 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter // ownership-related API validation errors for Microsoft 365 groups. if v, ok := d.GetOk("owners"); ok { owners := v.(*schema.Set).List() - c := 0 + ownerCount := 0 // First look for the calling principal in the specified owners; it should always be included in the initial // owners to avoid orphaning a group when the caller doesn't have the Groups.ReadWrite.All scope. for _, id := range owners { - i := 0 - if c >= 20 { - i = 1 - } ownerObject, err := getOwnerObject(ctx, id.(string)) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) @@ -447,42 +443,46 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter if ownerObject.ODataId == nil { return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) } - ownerChunks[i] = append(ownerChunks[i], *ownerObject) - c++ + if ownerCount < 20 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ } } // Then look for users, and finally service principals for _, t := range []odata.Type{odata.TypeUser, odata.TypeServicePrincipal} { for _, id := range owners { - i := 0 - if c >= 20 { - i = 1 - } ownerObject, err := getOwnerObject(ctx, id.(string)) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve owner principal object %q", id) } if *ownerObject.ODataType == t { - ownerChunks[i] = append(ownerChunks[i], *ownerObject) - c++ + if ownerCount < 20 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ } } } } - if len(ownerChunks[0]) == 0 { + if len(ownersFirst20) == 0 { // The calling principal is the default owner if no others are specified. This is the default API behaviour, so // we're being explicit about this in order to minimise confusion and avoid inconsistent API behaviours. callerObject, err := getOwnerObject(ctx, callerId) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) } - ownerChunks[0] = msgraph.Owners{*callerObject} + ownersFirst20 = msgraph.Owners{*callerObject} } // Set the initial owners, which either be the calling principal, or up to 20 of the owners specified in configuration - properties.Owners = &ownerChunks[0] + properties.Owners = &ownersFirst20 group, _, err := client.Create(ctx, properties) if err != nil { @@ -496,8 +496,8 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter d.SetId(*group.ID) // Add any remaining owners after the group is created - if len(ownerChunks[1]) > 0 { - group.Owners = &ownerChunks[1] + if len(ownersExtra) > 0 { + group.Owners = &ownersExtra if _, err := client.AddOwners(ctx, group); err != nil { return tf.ErrorDiagF(err, "Could not add owners to group with object ID: %q", d.Id()) } diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index 1d0056f3bd..a8f62fa0a3 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -285,10 +285,8 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, Tags: tf.ExpandStringSlicePtr(d.Get("tags").(*schema.Set).List()), } - // Chunk the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice - ownerChunks := make([]msgraph.Owners, 2) - - // The calling principal should always be in the first block of owners + // Sort the owners into two slices, the first containing up to 20 and the rest overflowing to the second slice + // The calling principal should always be in the first slice of owners callerObject, _, err := directoryObjectsClient.Get(ctx, callerId, odata.Query{}) if err != nil { return tf.ErrorDiagF(err, "Could not retrieve calling principal object %q", callerId) @@ -296,19 +294,16 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, if callerObject == nil { return tf.ErrorDiagF(errors.New("returned callerObject was nil"), "Could not retrieve calling principal object %q", callerId) } - ownerChunks[0] = msgraph.Owners{*callerObject} + ownersFirst20 := msgraph.Owners{*callerObject} + var ownersExtra msgraph.Owners // Track whether we need to remove the calling principal later on removeCallerOwner := true // Retrieve and set the initial owners, which can be up to 20 in total when creating the service principal if v, ok := d.GetOk("owners"); ok { - c := 0 + ownerCount := 0 for _, id := range v.(*schema.Set).List() { - i := 0 - if c >= 19 { - i = 1 - } if strings.EqualFold(id.(string), callerId) { removeCallerOwner = false continue @@ -323,13 +318,17 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, if ownerObject.ODataId == nil { return tf.ErrorDiagF(errors.New("ODataId was nil"), "Could not retrieve owner principal object %q", id) } - ownerChunks[i] = append(ownerChunks[i], *ownerObject) - c++ + if ownerCount < 19 { + ownersFirst20 = append(ownersFirst20, *ownerObject) + } else { + ownersExtra = append(ownersExtra, *ownerObject) + } + ownerCount++ } } // Set the initial owners, which should include the calling principal plus up to 19 of owners specified in configuration - properties.Owners = &ownerChunks[0] + properties.Owners = &ownersFirst20 servicePrincipal, _, err = client.Create(ctx, properties) if err != nil { @@ -342,8 +341,8 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, d.SetId(*servicePrincipal.ID) // Add any remaining owners after the service principal is created - if len(ownerChunks[1]) > 0 { - servicePrincipal.Owners = &ownerChunks[1] + if len(ownersExtra) > 0 { + servicePrincipal.Owners = &ownersExtra if _, err := client.AddOwners(ctx, servicePrincipal); err != nil { return tf.ErrorDiagF(err, "Could not add owners to service principal with object ID: %q", d.Id()) } From 50cf97594b993ef27b2776f62d82181405ce86b2 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Fri, 20 Aug 2021 00:50:56 +0100 Subject: [PATCH 11/11] Reduce aggressive retry limit as it slows down genuine 404s too much, disable retries for data sources for same reason --- internal/common/client_options.go | 3 ++- internal/services/applications/application_data_source.go | 1 + internal/services/domains/domains_data_source.go | 1 + internal/services/groups/group_data_source.go | 1 + internal/services/groups/groups_data_source.go | 1 + .../serviceprincipals/service_principal_data_source.go | 1 + internal/services/users/user_data_source.go | 1 + internal/services/users/users_data_source.go | 1 + 8 files changed, 9 insertions(+), 1 deletion(-) diff --git a/internal/common/client_options.go b/internal/common/client_options.go index 8e33e93061..5c32efa75b 100644 --- a/internal/common/client_options.go +++ b/internal/common/client_options.go @@ -40,7 +40,8 @@ func (o ClientOptions) ConfigureClient(c *msgraph.Client) { *c.RequestMiddlewares = append(*c.RequestMiddlewares, o.requestLogger) *c.ResponseMiddlewares = append(*c.ResponseMiddlewares, o.responseLogger) - c.RetryableClient.RetryMax = 20 + // Default retry limit, can be overridden from within a resource + c.RetryableClient.RetryMax = 8 } func (o ClientOptions) requestLogger(req *http.Request) (*http.Request, error) { diff --git a/internal/services/applications/application_data_source.go b/internal/services/applications/application_data_source.go index bd2fe02cba..4dae8ea964 100644 --- a/internal/services/applications/application_data_source.go +++ b/internal/services/applications/application_data_source.go @@ -433,6 +433,7 @@ func applicationDataSource() *schema.Resource { func applicationDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Applications.ApplicationsClient + client.BaseClient.DisableRetries = true var app *msgraph.Application diff --git a/internal/services/domains/domains_data_source.go b/internal/services/domains/domains_data_source.go index f824f619bd..95425c5e63 100644 --- a/internal/services/domains/domains_data_source.go +++ b/internal/services/domains/domains_data_source.go @@ -133,6 +133,7 @@ func domainsDataSource() *schema.Resource { func domainsDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Domains.DomainsClient + client.BaseClient.DisableRetries = true adminManaged := d.Get("admin_managed").(bool) onlyDefault := d.Get("only_default").(bool) diff --git a/internal/services/groups/group_data_source.go b/internal/services/groups/group_data_source.go index a57838ee22..4031ea446e 100644 --- a/internal/services/groups/group_data_source.go +++ b/internal/services/groups/group_data_source.go @@ -190,6 +190,7 @@ func groupDataSource() *schema.Resource { func groupDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + client.BaseClient.DisableRetries = true var group msgraph.Group var displayName string diff --git a/internal/services/groups/groups_data_source.go b/internal/services/groups/groups_data_source.go index ed150737e6..95a6c9d8f7 100644 --- a/internal/services/groups/groups_data_source.go +++ b/internal/services/groups/groups_data_source.go @@ -58,6 +58,7 @@ func groupsDataSource() *schema.Resource { func groupsDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + client.BaseClient.DisableRetries = true var groups []msgraph.Group var expectedCount int diff --git a/internal/services/serviceprincipals/service_principal_data_source.go b/internal/services/serviceprincipals/service_principal_data_source.go index 5410961bcc..c9a12c60cd 100644 --- a/internal/services/serviceprincipals/service_principal_data_source.go +++ b/internal/services/serviceprincipals/service_principal_data_source.go @@ -199,6 +199,7 @@ func servicePrincipalData() *schema.Resource { func servicePrincipalDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + client.BaseClient.DisableRetries = true var servicePrincipal *msgraph.ServicePrincipal diff --git a/internal/services/users/user_data_source.go b/internal/services/users/user_data_source.go index 091f85132f..ac4f8e86b5 100644 --- a/internal/services/users/user_data_source.go +++ b/internal/services/users/user_data_source.go @@ -286,6 +286,7 @@ func userDataSource() *schema.Resource { func userDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Users.UsersClient + client.BaseClient.DisableRetries = true var user msgraph.User diff --git a/internal/services/users/users_data_source.go b/internal/services/users/users_data_source.go index 8e93407d8c..9fa6d63c92 100644 --- a/internal/services/users/users_data_source.go +++ b/internal/services/users/users_data_source.go @@ -146,6 +146,7 @@ func usersData() *schema.Resource { func usersDataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Users.UsersClient + client.BaseClient.DisableRetries = true var users []msgraph.User var expectedCount int