From f4950167ee88c81b75529220198ecef34663d147 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:01:55 +0000 Subject: [PATCH 01/17] Switch to hashicorp/go-uuid in acceptance package --- go.mod | 1 - go.sum | 1 - internal/acceptance/data.go | 11 +- vendor/github.com/google/uuid/.travis.yml | 9 - vendor/github.com/google/uuid/CONTRIBUTING.md | 10 - vendor/github.com/google/uuid/CONTRIBUTORS | 9 - vendor/github.com/google/uuid/LICENSE | 27 -- vendor/github.com/google/uuid/README.md | 19 -- vendor/github.com/google/uuid/dce.go | 80 ------ vendor/github.com/google/uuid/doc.go | 12 - vendor/github.com/google/uuid/go.mod | 1 - vendor/github.com/google/uuid/hash.go | 53 ---- vendor/github.com/google/uuid/marshal.go | 38 --- vendor/github.com/google/uuid/node.go | 90 ------- vendor/github.com/google/uuid/node_js.go | 12 - vendor/github.com/google/uuid/node_net.go | 33 --- vendor/github.com/google/uuid/sql.go | 59 ----- vendor/github.com/google/uuid/time.go | 123 --------- vendor/github.com/google/uuid/util.go | 43 --- vendor/github.com/google/uuid/uuid.go | 245 ------------------ vendor/github.com/google/uuid/version1.go | 44 ---- vendor/github.com/google/uuid/version4.go | 43 --- vendor/modules.txt | 3 - 23 files changed, 8 insertions(+), 958 deletions(-) delete mode 100644 vendor/github.com/google/uuid/.travis.yml delete mode 100644 vendor/github.com/google/uuid/CONTRIBUTING.md delete mode 100644 vendor/github.com/google/uuid/CONTRIBUTORS delete mode 100644 vendor/github.com/google/uuid/LICENSE delete mode 100644 vendor/github.com/google/uuid/README.md delete mode 100644 vendor/github.com/google/uuid/dce.go delete mode 100644 vendor/github.com/google/uuid/doc.go delete mode 100644 vendor/github.com/google/uuid/go.mod delete mode 100644 vendor/github.com/google/uuid/hash.go delete mode 100644 vendor/github.com/google/uuid/marshal.go delete mode 100644 vendor/github.com/google/uuid/node.go delete mode 100644 vendor/github.com/google/uuid/node_js.go delete mode 100644 vendor/github.com/google/uuid/node_net.go delete mode 100644 vendor/github.com/google/uuid/sql.go delete mode 100644 vendor/github.com/google/uuid/time.go delete mode 100644 vendor/github.com/google/uuid/util.go delete mode 100644 vendor/github.com/google/uuid/uuid.go delete mode 100644 vendor/github.com/google/uuid/version1.go delete mode 100644 vendor/github.com/google/uuid/version4.go diff --git a/go.mod b/go.mod index f6c75d48d..ceff9b56f 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/aws/aws-sdk-go v1.38.43 // indirect github.com/fatih/color v1.11.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/uuid v1.1.2 github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-hclog v0.16.1 // indirect diff --git a/go.sum b/go.sum index fe47b9ce5..8d4c9202f 100644 --- a/go.sum +++ b/go.sum @@ -187,7 +187,6 @@ github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210506205249-923b5ab0fc1a/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM= diff --git a/internal/acceptance/data.go b/internal/acceptance/data.go index 3dd418bb4..bdd432ca1 100644 --- a/internal/acceptance/data.go +++ b/internal/acceptance/data.go @@ -6,7 +6,7 @@ import ( "strconv" "testing" - "github.com/google/uuid" + "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" "github.com/hashicorp/terraform-provider-azuread/internal/tf" @@ -39,7 +39,11 @@ type TestData struct { } func (t *TestData) UUID() string { - return uuid.New().String() + uuid, err := uuid.GenerateUUID() + if err != nil { + panic(err) + } + return uuid } // BuildTestData generates some test data for the given resource @@ -49,7 +53,6 @@ func BuildTestData(t *testing.T, resourceType string, resourceLabel string) Test testData := TestData{ RandomInteger: tf.AccRandTimeInt(), RandomString: acctest.RandString(5), - RandomID: uuid.New().String(), RandomPassword: fmt.Sprintf("%s%s", "p@$$Wd", acctest.RandString(6)), ResourceName: fmt.Sprintf("%s.%s", resourceType, resourceLabel), @@ -57,6 +60,8 @@ func BuildTestData(t *testing.T, resourceType string, resourceLabel string) Test resourceLabel: resourceLabel, } + testData.RandomID = testData.UUID() + return testData } diff --git a/vendor/github.com/google/uuid/.travis.yml b/vendor/github.com/google/uuid/.travis.yml deleted file mode 100644 index d8156a60b..000000000 --- a/vendor/github.com/google/uuid/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: go - -go: - - 1.4.3 - - 1.5.3 - - tip - -script: - - go test -v ./... diff --git a/vendor/github.com/google/uuid/CONTRIBUTING.md b/vendor/github.com/google/uuid/CONTRIBUTING.md deleted file mode 100644 index 04fdf09f1..000000000 --- a/vendor/github.com/google/uuid/CONTRIBUTING.md +++ /dev/null @@ -1,10 +0,0 @@ -# How to contribute - -We definitely welcome patches and contribution to this project! - -### Legal requirements - -In order to protect both you and ourselves, you will need to sign the -[Contributor License Agreement](https://cla.developers.google.com/clas). - -You may have already signed it for other Google projects. diff --git a/vendor/github.com/google/uuid/CONTRIBUTORS b/vendor/github.com/google/uuid/CONTRIBUTORS deleted file mode 100644 index b4bb97f6b..000000000 --- a/vendor/github.com/google/uuid/CONTRIBUTORS +++ /dev/null @@ -1,9 +0,0 @@ -Paul Borman -bmatsuo -shawnps -theory -jboverfelt -dsymonds -cd1 -wallclockbuilder -dansouza diff --git a/vendor/github.com/google/uuid/LICENSE b/vendor/github.com/google/uuid/LICENSE deleted file mode 100644 index 5dc68268d..000000000 --- a/vendor/github.com/google/uuid/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2009,2014 Google Inc. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/google/uuid/README.md b/vendor/github.com/google/uuid/README.md deleted file mode 100644 index f765a46f9..000000000 --- a/vendor/github.com/google/uuid/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) -The uuid package generates and inspects UUIDs based on -[RFC 4122](http://tools.ietf.org/html/rfc4122) -and DCE 1.1: Authentication and Security Services. - -This package is based on the github.com/pborman/uuid package (previously named -code.google.com/p/go-uuid). It differs from these earlier packages in that -a UUID is a 16 byte array rather than a byte slice. One loss due to this -change is the ability to represent an invalid UUID (vs a NIL UUID). - -###### Install -`go get github.com/google/uuid` - -###### Documentation -[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid) - -Full `go doc` style documentation for the package can be viewed online without -installing this package by using the GoDoc site here: -http://pkg.go.dev/github.com/google/uuid diff --git a/vendor/github.com/google/uuid/dce.go b/vendor/github.com/google/uuid/dce.go deleted file mode 100644 index fa820b9d3..000000000 --- a/vendor/github.com/google/uuid/dce.go +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "encoding/binary" - "fmt" - "os" -) - -// A Domain represents a Version 2 domain -type Domain byte - -// Domain constants for DCE Security (Version 2) UUIDs. -const ( - Person = Domain(0) - Group = Domain(1) - Org = Domain(2) -) - -// NewDCESecurity returns a DCE Security (Version 2) UUID. -// -// The domain should be one of Person, Group or Org. -// On a POSIX system the id should be the users UID for the Person -// domain and the users GID for the Group. The meaning of id for -// the domain Org or on non-POSIX systems is site defined. -// -// For a given domain/id pair the same token may be returned for up to -// 7 minutes and 10 seconds. -func NewDCESecurity(domain Domain, id uint32) (UUID, error) { - uuid, err := NewUUID() - if err == nil { - uuid[6] = (uuid[6] & 0x0f) | 0x20 // Version 2 - uuid[9] = byte(domain) - binary.BigEndian.PutUint32(uuid[0:], id) - } - return uuid, err -} - -// NewDCEPerson returns a DCE Security (Version 2) UUID in the person -// domain with the id returned by os.Getuid. -// -// NewDCESecurity(Person, uint32(os.Getuid())) -func NewDCEPerson() (UUID, error) { - return NewDCESecurity(Person, uint32(os.Getuid())) -} - -// NewDCEGroup returns a DCE Security (Version 2) UUID in the group -// domain with the id returned by os.Getgid. -// -// NewDCESecurity(Group, uint32(os.Getgid())) -func NewDCEGroup() (UUID, error) { - return NewDCESecurity(Group, uint32(os.Getgid())) -} - -// Domain returns the domain for a Version 2 UUID. Domains are only defined -// for Version 2 UUIDs. -func (uuid UUID) Domain() Domain { - return Domain(uuid[9]) -} - -// ID returns the id for a Version 2 UUID. IDs are only defined for Version 2 -// UUIDs. -func (uuid UUID) ID() uint32 { - return binary.BigEndian.Uint32(uuid[0:4]) -} - -func (d Domain) String() string { - switch d { - case Person: - return "Person" - case Group: - return "Group" - case Org: - return "Org" - } - return fmt.Sprintf("Domain%d", int(d)) -} diff --git a/vendor/github.com/google/uuid/doc.go b/vendor/github.com/google/uuid/doc.go deleted file mode 100644 index 5b8a4b9af..000000000 --- a/vendor/github.com/google/uuid/doc.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package uuid generates and inspects UUIDs. -// -// UUIDs are based on RFC 4122 and DCE 1.1: Authentication and Security -// Services. -// -// A UUID is a 16 byte (128 bit) array. UUIDs may be used as keys to -// maps or compared directly. -package uuid diff --git a/vendor/github.com/google/uuid/go.mod b/vendor/github.com/google/uuid/go.mod deleted file mode 100644 index fc84cd79d..000000000 --- a/vendor/github.com/google/uuid/go.mod +++ /dev/null @@ -1 +0,0 @@ -module github.com/google/uuid diff --git a/vendor/github.com/google/uuid/hash.go b/vendor/github.com/google/uuid/hash.go deleted file mode 100644 index b17461631..000000000 --- a/vendor/github.com/google/uuid/hash.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "crypto/md5" - "crypto/sha1" - "hash" -) - -// Well known namespace IDs and UUIDs -var ( - NameSpaceDNS = Must(Parse("6ba7b810-9dad-11d1-80b4-00c04fd430c8")) - NameSpaceURL = Must(Parse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")) - NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) - NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) - Nil UUID // empty UUID, all zeros -) - -// NewHash returns a new UUID derived from the hash of space concatenated with -// data generated by h. The hash should be at least 16 byte in length. The -// first 16 bytes of the hash are used to form the UUID. The version of the -// UUID will be the lower 4 bits of version. NewHash is used to implement -// NewMD5 and NewSHA1. -func NewHash(h hash.Hash, space UUID, data []byte, version int) UUID { - h.Reset() - h.Write(space[:]) - h.Write(data) - s := h.Sum(nil) - var uuid UUID - copy(uuid[:], s) - uuid[6] = (uuid[6] & 0x0f) | uint8((version&0xf)<<4) - uuid[8] = (uuid[8] & 0x3f) | 0x80 // RFC 4122 variant - return uuid -} - -// NewMD5 returns a new MD5 (Version 3) UUID based on the -// supplied name space and data. It is the same as calling: -// -// NewHash(md5.New(), space, data, 3) -func NewMD5(space UUID, data []byte) UUID { - return NewHash(md5.New(), space, data, 3) -} - -// NewSHA1 returns a new SHA1 (Version 5) UUID based on the -// supplied name space and data. It is the same as calling: -// -// NewHash(sha1.New(), space, data, 5) -func NewSHA1(space UUID, data []byte) UUID { - return NewHash(sha1.New(), space, data, 5) -} diff --git a/vendor/github.com/google/uuid/marshal.go b/vendor/github.com/google/uuid/marshal.go deleted file mode 100644 index 14bd34072..000000000 --- a/vendor/github.com/google/uuid/marshal.go +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import "fmt" - -// MarshalText implements encoding.TextMarshaler. -func (uuid UUID) MarshalText() ([]byte, error) { - var js [36]byte - encodeHex(js[:], uuid) - return js[:], nil -} - -// UnmarshalText implements encoding.TextUnmarshaler. -func (uuid *UUID) UnmarshalText(data []byte) error { - id, err := ParseBytes(data) - if err != nil { - return err - } - *uuid = id - return nil -} - -// MarshalBinary implements encoding.BinaryMarshaler. -func (uuid UUID) MarshalBinary() ([]byte, error) { - return uuid[:], nil -} - -// UnmarshalBinary implements encoding.BinaryUnmarshaler. -func (uuid *UUID) UnmarshalBinary(data []byte) error { - if len(data) != 16 { - return fmt.Errorf("invalid UUID (got %d bytes)", len(data)) - } - copy(uuid[:], data) - return nil -} diff --git a/vendor/github.com/google/uuid/node.go b/vendor/github.com/google/uuid/node.go deleted file mode 100644 index d651a2b06..000000000 --- a/vendor/github.com/google/uuid/node.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "sync" -) - -var ( - nodeMu sync.Mutex - ifname string // name of interface being used - nodeID [6]byte // hardware for version 1 UUIDs - zeroID [6]byte // nodeID with only 0's -) - -// NodeInterface returns the name of the interface from which the NodeID was -// derived. The interface "user" is returned if the NodeID was set by -// SetNodeID. -func NodeInterface() string { - defer nodeMu.Unlock() - nodeMu.Lock() - return ifname -} - -// SetNodeInterface selects the hardware address to be used for Version 1 UUIDs. -// If name is "" then the first usable interface found will be used or a random -// Node ID will be generated. If a named interface cannot be found then false -// is returned. -// -// SetNodeInterface never fails when name is "". -func SetNodeInterface(name string) bool { - defer nodeMu.Unlock() - nodeMu.Lock() - return setNodeInterface(name) -} - -func setNodeInterface(name string) bool { - iname, addr := getHardwareInterface(name) // null implementation for js - if iname != "" && addr != nil { - ifname = iname - copy(nodeID[:], addr) - return true - } - - // We found no interfaces with a valid hardware address. If name - // does not specify a specific interface generate a random Node ID - // (section 4.1.6) - if name == "" { - ifname = "random" - randomBits(nodeID[:]) - return true - } - return false -} - -// NodeID returns a slice of a copy of the current Node ID, setting the Node ID -// if not already set. -func NodeID() []byte { - defer nodeMu.Unlock() - nodeMu.Lock() - if nodeID == zeroID { - setNodeInterface("") - } - nid := nodeID - return nid[:] -} - -// SetNodeID sets the Node ID to be used for Version 1 UUIDs. The first 6 bytes -// of id are used. If id is less than 6 bytes then false is returned and the -// Node ID is not set. -func SetNodeID(id []byte) bool { - if len(id) < 6 { - return false - } - defer nodeMu.Unlock() - nodeMu.Lock() - copy(nodeID[:], id) - ifname = "user" - return true -} - -// NodeID returns the 6 byte node id encoded in uuid. It returns nil if uuid is -// not valid. The NodeID is only well defined for version 1 and 2 UUIDs. -func (uuid UUID) NodeID() []byte { - var node [6]byte - copy(node[:], uuid[10:]) - return node[:] -} diff --git a/vendor/github.com/google/uuid/node_js.go b/vendor/github.com/google/uuid/node_js.go deleted file mode 100644 index 24b78edc9..000000000 --- a/vendor/github.com/google/uuid/node_js.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2017 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build js - -package uuid - -// getHardwareInterface returns nil values for the JS version of the code. -// This remvoves the "net" dependency, because it is not used in the browser. -// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. -func getHardwareInterface(name string) (string, []byte) { return "", nil } diff --git a/vendor/github.com/google/uuid/node_net.go b/vendor/github.com/google/uuid/node_net.go deleted file mode 100644 index 0cbbcddbd..000000000 --- a/vendor/github.com/google/uuid/node_net.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2017 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// +build !js - -package uuid - -import "net" - -var interfaces []net.Interface // cached list of interfaces - -// getHardwareInterface returns the name and hardware address of interface name. -// If name is "" then the name and hardware address of one of the system's -// interfaces is returned. If no interfaces are found (name does not exist or -// there are no interfaces) then "", nil is returned. -// -// Only addresses of at least 6 bytes are returned. -func getHardwareInterface(name string) (string, []byte) { - if interfaces == nil { - var err error - interfaces, err = net.Interfaces() - if err != nil { - return "", nil - } - } - for _, ifs := range interfaces { - if len(ifs.HardwareAddr) >= 6 && (name == "" || name == ifs.Name) { - return ifs.Name, ifs.HardwareAddr - } - } - return "", nil -} diff --git a/vendor/github.com/google/uuid/sql.go b/vendor/github.com/google/uuid/sql.go deleted file mode 100644 index f326b54db..000000000 --- a/vendor/github.com/google/uuid/sql.go +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "database/sql/driver" - "fmt" -) - -// Scan implements sql.Scanner so UUIDs can be read from databases transparently -// Currently, database types that map to string and []byte are supported. Please -// consult database-specific driver documentation for matching types. -func (uuid *UUID) Scan(src interface{}) error { - switch src := src.(type) { - case nil: - return nil - - case string: - // if an empty UUID comes from a table, we return a null UUID - if src == "" { - return nil - } - - // see Parse for required string format - u, err := Parse(src) - if err != nil { - return fmt.Errorf("Scan: %v", err) - } - - *uuid = u - - case []byte: - // if an empty UUID comes from a table, we return a null UUID - if len(src) == 0 { - return nil - } - - // assumes a simple slice of bytes if 16 bytes - // otherwise attempts to parse - if len(src) != 16 { - return uuid.Scan(string(src)) - } - copy((*uuid)[:], src) - - default: - return fmt.Errorf("Scan: unable to scan type %T into UUID", src) - } - - return nil -} - -// Value implements sql.Valuer so that UUIDs can be written to databases -// transparently. Currently, UUIDs map to strings. Please consult -// database-specific driver documentation for matching types. -func (uuid UUID) Value() (driver.Value, error) { - return uuid.String(), nil -} diff --git a/vendor/github.com/google/uuid/time.go b/vendor/github.com/google/uuid/time.go deleted file mode 100644 index e6ef06cdc..000000000 --- a/vendor/github.com/google/uuid/time.go +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "encoding/binary" - "sync" - "time" -) - -// A Time represents a time as the number of 100's of nanoseconds since 15 Oct -// 1582. -type Time int64 - -const ( - lillian = 2299160 // Julian day of 15 Oct 1582 - unix = 2440587 // Julian day of 1 Jan 1970 - epoch = unix - lillian // Days between epochs - g1582 = epoch * 86400 // seconds between epochs - g1582ns100 = g1582 * 10000000 // 100s of a nanoseconds between epochs -) - -var ( - timeMu sync.Mutex - lasttime uint64 // last time we returned - clockSeq uint16 // clock sequence for this run - - timeNow = time.Now // for testing -) - -// UnixTime converts t the number of seconds and nanoseconds using the Unix -// epoch of 1 Jan 1970. -func (t Time) UnixTime() (sec, nsec int64) { - sec = int64(t - g1582ns100) - nsec = (sec % 10000000) * 100 - sec /= 10000000 - return sec, nsec -} - -// GetTime returns the current Time (100s of nanoseconds since 15 Oct 1582) and -// clock sequence as well as adjusting the clock sequence as needed. An error -// is returned if the current time cannot be determined. -func GetTime() (Time, uint16, error) { - defer timeMu.Unlock() - timeMu.Lock() - return getTime() -} - -func getTime() (Time, uint16, error) { - t := timeNow() - - // If we don't have a clock sequence already, set one. - if clockSeq == 0 { - setClockSequence(-1) - } - now := uint64(t.UnixNano()/100) + g1582ns100 - - // If time has gone backwards with this clock sequence then we - // increment the clock sequence - if now <= lasttime { - clockSeq = ((clockSeq + 1) & 0x3fff) | 0x8000 - } - lasttime = now - return Time(now), clockSeq, nil -} - -// ClockSequence returns the current clock sequence, generating one if not -// already set. The clock sequence is only used for Version 1 UUIDs. -// -// The uuid package does not use global static storage for the clock sequence or -// the last time a UUID was generated. Unless SetClockSequence is used, a new -// random clock sequence is generated the first time a clock sequence is -// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) -func ClockSequence() int { - defer timeMu.Unlock() - timeMu.Lock() - return clockSequence() -} - -func clockSequence() int { - if clockSeq == 0 { - setClockSequence(-1) - } - return int(clockSeq & 0x3fff) -} - -// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to -// -1 causes a new sequence to be generated. -func SetClockSequence(seq int) { - defer timeMu.Unlock() - timeMu.Lock() - setClockSequence(seq) -} - -func setClockSequence(seq int) { - if seq == -1 { - var b [2]byte - randomBits(b[:]) // clock sequence - seq = int(b[0])<<8 | int(b[1]) - } - oldSeq := clockSeq - clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant - if oldSeq != clockSeq { - lasttime = 0 - } -} - -// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in -// uuid. The time is only defined for version 1 and 2 UUIDs. -func (uuid UUID) Time() Time { - time := int64(binary.BigEndian.Uint32(uuid[0:4])) - time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 - time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 - return Time(time) -} - -// ClockSequence returns the clock sequence encoded in uuid. -// The clock sequence is only well defined for version 1 and 2 UUIDs. -func (uuid UUID) ClockSequence() int { - return int(binary.BigEndian.Uint16(uuid[8:10])) & 0x3fff -} diff --git a/vendor/github.com/google/uuid/util.go b/vendor/github.com/google/uuid/util.go deleted file mode 100644 index 5ea6c7378..000000000 --- a/vendor/github.com/google/uuid/util.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "io" -) - -// randomBits completely fills slice b with random data. -func randomBits(b []byte) { - if _, err := io.ReadFull(rander, b); err != nil { - panic(err.Error()) // rand should never fail - } -} - -// xvalues returns the value of a byte as a hexadecimal digit or 255. -var xvalues = [256]byte{ - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 255, 255, 255, 255, 255, 255, - 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 10, 11, 12, 13, 14, 15, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, - 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, -} - -// xtob converts hex characters x1 and x2 into a byte. -func xtob(x1, x2 byte) (byte, bool) { - b1 := xvalues[x1] - b2 := xvalues[x2] - return (b1 << 4) | b2, b1 != 255 && b2 != 255 -} diff --git a/vendor/github.com/google/uuid/uuid.go b/vendor/github.com/google/uuid/uuid.go deleted file mode 100644 index 524404cc5..000000000 --- a/vendor/github.com/google/uuid/uuid.go +++ /dev/null @@ -1,245 +0,0 @@ -// Copyright 2018 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "bytes" - "crypto/rand" - "encoding/hex" - "errors" - "fmt" - "io" - "strings" -) - -// A UUID is a 128 bit (16 byte) Universal Unique IDentifier as defined in RFC -// 4122. -type UUID [16]byte - -// A Version represents a UUID's version. -type Version byte - -// A Variant represents a UUID's variant. -type Variant byte - -// Constants returned by Variant. -const ( - Invalid = Variant(iota) // Invalid UUID - RFC4122 // The variant specified in RFC4122 - Reserved // Reserved, NCS backward compatibility. - Microsoft // Reserved, Microsoft Corporation backward compatibility. - Future // Reserved for future definition. -) - -var rander = rand.Reader // random function - -// Parse decodes s into a UUID or returns an error. Both the standard UUID -// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and -// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the -// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex -// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. -func Parse(s string) (UUID, error) { - var uuid UUID - switch len(s) { - // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - case 36: - - // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - case 36 + 9: - if strings.ToLower(s[:9]) != "urn:uuid:" { - return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) - } - s = s[9:] - - // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} - case 36 + 2: - s = s[1:] - - // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - case 32: - var ok bool - for i := range uuid { - uuid[i], ok = xtob(s[i*2], s[i*2+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - } - return uuid, nil - default: - return uuid, fmt.Errorf("invalid UUID length: %d", len(s)) - } - // s is now at least 36 bytes long - // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' { - return uuid, errors.New("invalid UUID format") - } - for i, x := range [16]int{ - 0, 2, 4, 6, - 9, 11, - 14, 16, - 19, 21, - 24, 26, 28, 30, 32, 34} { - v, ok := xtob(s[x], s[x+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - uuid[i] = v - } - return uuid, nil -} - -// ParseBytes is like Parse, except it parses a byte slice instead of a string. -func ParseBytes(b []byte) (UUID, error) { - var uuid UUID - switch len(b) { - case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { - return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) - } - b = b[9:] - case 36 + 2: // {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} - b = b[1:] - case 32: // xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - var ok bool - for i := 0; i < 32; i += 2 { - uuid[i/2], ok = xtob(b[i], b[i+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - } - return uuid, nil - default: - return uuid, fmt.Errorf("invalid UUID length: %d", len(b)) - } - // s is now at least 36 bytes long - // it must be of the form xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - if b[8] != '-' || b[13] != '-' || b[18] != '-' || b[23] != '-' { - return uuid, errors.New("invalid UUID format") - } - for i, x := range [16]int{ - 0, 2, 4, 6, - 9, 11, - 14, 16, - 19, 21, - 24, 26, 28, 30, 32, 34} { - v, ok := xtob(b[x], b[x+1]) - if !ok { - return uuid, errors.New("invalid UUID format") - } - uuid[i] = v - } - return uuid, nil -} - -// MustParse is like Parse but panics if the string cannot be parsed. -// It simplifies safe initialization of global variables holding compiled UUIDs. -func MustParse(s string) UUID { - uuid, err := Parse(s) - if err != nil { - panic(`uuid: Parse(` + s + `): ` + err.Error()) - } - return uuid -} - -// FromBytes creates a new UUID from a byte slice. Returns an error if the slice -// does not have a length of 16. The bytes are copied from the slice. -func FromBytes(b []byte) (uuid UUID, err error) { - err = uuid.UnmarshalBinary(b) - return uuid, err -} - -// Must returns uuid if err is nil and panics otherwise. -func Must(uuid UUID, err error) UUID { - if err != nil { - panic(err) - } - return uuid -} - -// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// , or "" if uuid is invalid. -func (uuid UUID) String() string { - var buf [36]byte - encodeHex(buf[:], uuid) - return string(buf[:]) -} - -// URN returns the RFC 2141 URN form of uuid, -// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, or "" if uuid is invalid. -func (uuid UUID) URN() string { - var buf [36 + 9]byte - copy(buf[:], "urn:uuid:") - encodeHex(buf[9:], uuid) - return string(buf[:]) -} - -func encodeHex(dst []byte, uuid UUID) { - hex.Encode(dst, uuid[:4]) - dst[8] = '-' - hex.Encode(dst[9:13], uuid[4:6]) - dst[13] = '-' - hex.Encode(dst[14:18], uuid[6:8]) - dst[18] = '-' - hex.Encode(dst[19:23], uuid[8:10]) - dst[23] = '-' - hex.Encode(dst[24:], uuid[10:]) -} - -// Variant returns the variant encoded in uuid. -func (uuid UUID) Variant() Variant { - switch { - case (uuid[8] & 0xc0) == 0x80: - return RFC4122 - case (uuid[8] & 0xe0) == 0xc0: - return Microsoft - case (uuid[8] & 0xe0) == 0xe0: - return Future - default: - return Reserved - } -} - -// Version returns the version of uuid. -func (uuid UUID) Version() Version { - return Version(uuid[6] >> 4) -} - -func (v Version) String() string { - if v > 15 { - return fmt.Sprintf("BAD_VERSION_%d", v) - } - return fmt.Sprintf("VERSION_%d", v) -} - -func (v Variant) String() string { - switch v { - case RFC4122: - return "RFC4122" - case Reserved: - return "Reserved" - case Microsoft: - return "Microsoft" - case Future: - return "Future" - case Invalid: - return "Invalid" - } - return fmt.Sprintf("BadVariant%d", int(v)) -} - -// SetRand sets the random number generator to r, which implements io.Reader. -// If r.Read returns an error when the package requests random data then -// a panic will be issued. -// -// Calling SetRand with nil sets the random number generator to the default -// generator. -func SetRand(r io.Reader) { - if r == nil { - rander = rand.Reader - return - } - rander = r -} diff --git a/vendor/github.com/google/uuid/version1.go b/vendor/github.com/google/uuid/version1.go deleted file mode 100644 index 463109629..000000000 --- a/vendor/github.com/google/uuid/version1.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import ( - "encoding/binary" -) - -// NewUUID returns a Version 1 UUID based on the current NodeID and clock -// sequence, and the current time. If the NodeID has not been set by SetNodeID -// or SetNodeInterface then it will be set automatically. If the NodeID cannot -// be set NewUUID returns nil. If clock sequence has not been set by -// SetClockSequence then it will be set automatically. If GetTime fails to -// return the current NewUUID returns nil and an error. -// -// In most cases, New should be used. -func NewUUID() (UUID, error) { - var uuid UUID - now, seq, err := GetTime() - if err != nil { - return uuid, err - } - - timeLow := uint32(now & 0xffffffff) - timeMid := uint16((now >> 32) & 0xffff) - timeHi := uint16((now >> 48) & 0x0fff) - timeHi |= 0x1000 // Version 1 - - binary.BigEndian.PutUint32(uuid[0:], timeLow) - binary.BigEndian.PutUint16(uuid[4:], timeMid) - binary.BigEndian.PutUint16(uuid[6:], timeHi) - binary.BigEndian.PutUint16(uuid[8:], seq) - - nodeMu.Lock() - if nodeID == zeroID { - setNodeInterface("") - } - copy(uuid[10:], nodeID[:]) - nodeMu.Unlock() - - return uuid, nil -} diff --git a/vendor/github.com/google/uuid/version4.go b/vendor/github.com/google/uuid/version4.go deleted file mode 100644 index c110465db..000000000 --- a/vendor/github.com/google/uuid/version4.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2016 Google Inc. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package uuid - -import "io" - -// New creates a new random UUID or panics. New is equivalent to -// the expression -// -// uuid.Must(uuid.NewRandom()) -func New() UUID { - return Must(NewRandom()) -} - -// NewRandom returns a Random (Version 4) UUID. -// -// The strength of the UUIDs is based on the strength of the crypto/rand -// package. -// -// A note about uniqueness derived from the UUID Wikipedia entry: -// -// Randomly generated UUIDs have 122 random bits. One's annual risk of being -// hit by a meteorite is estimated to be one chance in 17 billion, that -// means the probability is about 0.00000000006 (6 × 10−11), -// equivalent to the odds of creating a few tens of trillions of UUIDs in a -// year and having one duplicate. -func NewRandom() (UUID, error) { - return NewRandomFromReader(rander) -} - -// NewRandomFromReader returns a UUID based on bytes read from a given io.Reader. -func NewRandomFromReader(r io.Reader) (UUID, error) { - var uuid UUID - _, err := io.ReadFull(r, uuid[:]) - if err != nil { - return Nil, err - } - uuid[6] = (uuid[6] & 0x0f) | 0x40 // Version 4 - uuid[8] = (uuid[8] & 0x3f) | 0x80 // Variant is 10 - return uuid, nil -} diff --git a/vendor/modules.txt b/vendor/modules.txt index 6c3188606..0c25459bd 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -91,9 +91,6 @@ github.com/golang/protobuf/ptypes/empty github.com/golang/protobuf/ptypes/timestamp # github.com/golang/snappy v0.0.3 github.com/golang/snappy -# github.com/google/uuid v1.1.2 -## explicit -github.com/google/uuid # github.com/googleapis/gax-go/v2 v2.0.5 github.com/googleapis/gax-go/v2 # github.com/hashicorp/errwrap v1.1.0 From b61f197b0ddf03c8c675fb3f1afca7995e2ed00d Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:02:25 +0000 Subject: [PATCH 02/17] Increase retry limit by one --- internal/common/client_options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/common/client_options.go b/internal/common/client_options.go index 7bcfd64e7..77019638f 100644 --- a/internal/common/client_options.go +++ b/internal/common/client_options.go @@ -45,7 +45,7 @@ func (o ClientOptions) ConfigureClient(c *msgraph.Client) { *c.ResponseMiddlewares = append(*c.ResponseMiddlewares, o.responseLogger) // Default retry limit, can be overridden from within a resource - c.RetryableClient.RetryMax = 8 + c.RetryableClient.RetryMax = 9 } func (o ClientOptions) requestLogger(req *http.Request) (*http.Request, error) { From 349687d31de31368823aac5f571d01bd27f60c8d Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:03:08 +0000 Subject: [PATCH 03/17] Add WaitForDeletion helper func --- internal/helpers/consistency.go | 43 +++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 internal/helpers/consistency.go diff --git a/internal/helpers/consistency.go b/internal/helpers/consistency.go new file mode 100644 index 000000000..38f2fe9d9 --- /dev/null +++ b/internal/helpers/consistency.go @@ -0,0 +1,43 @@ +package helpers + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +type existsFunc func(ctx context.Context) (*bool, error) + +func WaitForDeletion(ctx context.Context, f existsFunc) error { + deadline, ok := ctx.Deadline() + if !ok { + return errors.New("context has no deadline") + } + + timeout := time.Until(deadline) + _, err := (&resource.StateChangeConf{ + Pending: []string{"Waiting"}, + Target: []string{"Deleted"}, + Timeout: timeout, + MinTimeout: 5 * time.Second, + ContinuousTargetOccurence: 5, + Refresh: func() (interface{}, string, error) { + exists, err := f(ctx) + if err != nil { + return nil, "Error", fmt.Errorf("retrieving resource: %+v", err) + } + if exists == nil { + return nil, "Error", fmt.Errorf("retrieving resource: exists was nil") + } + if *exists { + return "stub", "Waiting", nil + } + return "stub", "Deleted", nil + }, + }).WaitForStateContext(ctx) + + return err +} From 8c7f2ffa4096bcd186dd3945f39b25324c2e8bac Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:04:05 +0000 Subject: [PATCH 04/17] Use WaitForDeletion helper in conditionalaccess package, move expand/flatten funcs into own source file --- .../conditional_access_policy_resource.go | 344 +-------------- .../conditionalaccess/conditionalaccess.go | 410 ++++++++++++++++++ .../named_location_resource.go | 150 +------ 3 files changed, 445 insertions(+), 459 deletions(-) create mode 100644 internal/services/conditionalaccess/conditionalaccess.go diff --git a/internal/services/conditionalaccess/conditional_access_policy_resource.go b/internal/services/conditionalaccess/conditional_access_policy_resource.go index 20256d941..503c72fc7 100644 --- a/internal/services/conditionalaccess/conditional_access_policy_resource.go +++ b/internal/services/conditionalaccess/conditional_access_policy_resource.go @@ -17,6 +17,7 @@ import ( "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" @@ -515,348 +516,35 @@ func conditionalAccessPolicyResourceRead(ctx context.Context, d *schema.Resource func conditionalAccessPolicyResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ConditionalAccess.PoliciesClient + policyId := d.Id() - _, status, err := client.Get(ctx, d.Id(), odata.Query{}) + _, status, err := client.Get(ctx, policyId, odata.Query{}) if err != nil { if status == http.StatusNotFound { - log.Printf("[DEBUG] Conditional Access Policy with ID %q already deleted", d.Id()) + log.Printf("[DEBUG] Conditional Access Policy with ID %q already deleted", policyId) return nil } - return tf.ErrorDiagPathF(err, "id", "Retrieving conditional access policy with ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Retrieving conditional access policy with ID %q", policyId) } - status, err = client.Delete(ctx, d.Id()) + status, err = client.Delete(ctx, policyId) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Deleting conditional access policy with ID %q, got status %d", d.Id(), status) + return tf.ErrorDiagPathF(err, "id", "Deleting conditional access policy with ID %q, got status %d", policyId, status) } - log.Printf("[DEBUG] Waiting for conditional access policy %q to disappear", d.Id()) - timeout, _ := ctx.Deadline() - stateConf := &resource.StateChangeConf{ - Pending: []string{"Pending"}, - Target: []string{"Deleted"}, - Timeout: time.Until(timeout), - MinTimeout: 5 * time.Second, - ContinuousTargetOccurence: 5, - Refresh: func() (interface{}, string, error) { - client.BaseClient.DisableRetries = true - _, status, err := client.Get(ctx, d.Id(), odata.Query{}) + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, policyId, odata.Query{}); err != nil { if status == http.StatusNotFound { - return "stub", "Deleted", nil + return utils.Bool(false), nil } - if err != nil { - return nil, "Error", err - } - - return "stub", "Pending", nil - }, - } - if _, err = stateConf.WaitForStateContext(ctx); err != nil { - return tf.ErrorDiagF(err, "waiting for deletion of conditional access policy with ID %q", d.Id()) - } - - return nil -} - -func flattenConditionalAccessConditionSet(in *msgraph.ConditionalAccessConditionSet) []interface{} { - if in == nil { - return []interface{}{} - } - - return []interface{}{ - map[string]interface{}{ - "applications": flattenConditionalAccessApplications(in.Applications), - "users": flattenConditionalAccessUsers(in.Users), - "client_app_types": tf.FlattenStringSlicePtr(in.ClientAppTypes), - "locations": flattenConditionalAccessLocations(in.Locations), - "platforms": flattenConditionalAccessPlatforms(in.Platforms), - "sign_in_risk_levels": tf.FlattenStringSlicePtr(in.SignInRiskLevels), - "user_risk_levels": tf.FlattenStringSlicePtr(in.UserRiskLevels), - }, - } -} - -func flattenConditionalAccessApplications(in *msgraph.ConditionalAccessApplications) []interface{} { - if in == nil { - return []interface{}{} - } - - return []interface{}{ - map[string]interface{}{ - "included_applications": tf.FlattenStringSlicePtr(in.IncludeApplications), - "excluded_applications": tf.FlattenStringSlicePtr(in.ExcludeApplications), - "included_user_actions": tf.FlattenStringSlicePtr(in.IncludeUserActions), - }, - } -} - -func flattenConditionalAccessUsers(in *msgraph.ConditionalAccessUsers) []interface{} { - if in == nil { - return []interface{}{} - } - - return []interface{}{ - map[string]interface{}{ - "included_users": tf.FlattenStringSlicePtr(in.IncludeUsers), - "excluded_users": tf.FlattenStringSlicePtr(in.ExcludeUsers), - "included_groups": tf.FlattenStringSlicePtr(in.IncludeGroups), - "excluded_groups": tf.FlattenStringSlicePtr(in.ExcludeGroups), - "included_roles": tf.FlattenStringSlicePtr(in.IncludeRoles), - "excluded_roles": tf.FlattenStringSlicePtr(in.ExcludeRoles), - }, - } -} - -func flattenConditionalAccessLocations(in *msgraph.ConditionalAccessLocations) []interface{} { - if in == nil { - return []interface{}{} - } - - return []interface{}{ - map[string]interface{}{ - "included_locations": tf.FlattenStringSlicePtr(in.IncludeLocations), - "excluded_locations": tf.FlattenStringSlicePtr(in.ExcludeLocations), - }, - } -} - -func flattenConditionalAccessPlatforms(in *msgraph.ConditionalAccessPlatforms) []interface{} { - if in == nil { - return []interface{}{} - } - - return []interface{}{ - map[string]interface{}{ - "included_platforms": tf.FlattenStringSlicePtr(in.IncludePlatforms), - "excluded_platforms": tf.FlattenStringSlicePtr(in.ExcludePlatforms), - }, - } -} - -func flattenConditionalAccessGrantControls(in *msgraph.ConditionalAccessGrantControls) []interface{} { - if in == nil { - return []interface{}{} - } - - return []interface{}{ - map[string]interface{}{ - "operator": in.Operator, - "built_in_controls": tf.FlattenStringSlicePtr(in.BuiltInControls), - "custom_authentication_factors": tf.FlattenStringSlicePtr(in.CustomAuthenticationFactors), - "terms_of_use": tf.FlattenStringSlicePtr(in.TermsOfUse), - }, - } -} - -func flattenConditionalAccessSessionControls(in *msgraph.ConditionalAccessSessionControls) []interface{} { - if in == nil { - return []interface{}{} - } - - applicationEnforceRestrictions := false - if in.ApplicationEnforcedRestrictions != nil { - applicationEnforceRestrictions = *in.ApplicationEnforcedRestrictions.IsEnabled - } - - cloudAppSecurity := "" - if in.CloudAppSecurity != nil && in.CloudAppSecurity.CloudAppSecurityType != nil { - cloudAppSecurity = *in.CloudAppSecurity.CloudAppSecurityType - } - - signInFrequency := 0 - signInFrequencyPeriod := "" - if in.SignInFrequency != nil && in.SignInFrequency.Value != nil && in.SignInFrequency.Type != nil { - signInFrequency = int(*in.SignInFrequency.Value) - signInFrequencyPeriod = *in.SignInFrequency.Type - } - - return []interface{}{ - map[string]interface{}{ - "application_enforced_restrictions_enabled": applicationEnforceRestrictions, - "cloud_app_security_policy": cloudAppSecurity, - "sign_in_frequency": signInFrequency, - "sign_in_frequency_period": signInFrequencyPeriod, - }, - } -} - -func expandConditionalAccessConditionSet(in []interface{}) *msgraph.ConditionalAccessConditionSet { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.ConditionalAccessConditionSet{} - config := in[0].(map[string]interface{}) - - applications := config["applications"].([]interface{}) - users := config["users"].([]interface{}) - clientAppTypes := config["client_app_types"].([]interface{}) - locations := config["locations"].([]interface{}) - platforms := config["platforms"].([]interface{}) - signInRiskLevels := config["sign_in_risk_levels"].([]interface{}) - userRiskLevels := config["user_risk_levels"].([]interface{}) - - result.Applications = expandConditionalAccessApplications(applications) - result.Users = expandConditionalAccessUsers(users) - result.ClientAppTypes = tf.ExpandStringSlicePtr(clientAppTypes) - result.Locations = expandConditionalAccessLocations(locations) - result.Platforms = expandConditionalAccessPlatforms(platforms) - result.SignInRiskLevels = tf.ExpandStringSlicePtr(signInRiskLevels) - result.UserRiskLevels = tf.ExpandStringSlicePtr(userRiskLevels) - - return &result -} - -func expandConditionalAccessApplications(in []interface{}) *msgraph.ConditionalAccessApplications { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.ConditionalAccessApplications{} - config := in[0].(map[string]interface{}) - - includeApplications := config["included_applications"].([]interface{}) - excludeApplications := config["excluded_applications"].([]interface{}) - includeUserActions := config["included_user_actions"].([]interface{}) - - result.IncludeApplications = tf.ExpandStringSlicePtr(includeApplications) - result.ExcludeApplications = tf.ExpandStringSlicePtr(excludeApplications) - result.IncludeUserActions = tf.ExpandStringSlicePtr(includeUserActions) - - return &result -} - -func expandConditionalAccessUsers(in []interface{}) *msgraph.ConditionalAccessUsers { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.ConditionalAccessUsers{} - config := in[0].(map[string]interface{}) - - includeUsers := config["included_users"].([]interface{}) - excludeUsers := config["excluded_users"].([]interface{}) - includeGroups := config["included_groups"].([]interface{}) - excludeGroups := config["excluded_groups"].([]interface{}) - includeRoles := config["included_roles"].([]interface{}) - excludeRoles := config["excluded_roles"].([]interface{}) - - result.IncludeUsers = tf.ExpandStringSlicePtr(includeUsers) - result.ExcludeUsers = tf.ExpandStringSlicePtr(excludeUsers) - result.IncludeGroups = tf.ExpandStringSlicePtr(includeGroups) - result.ExcludeGroups = tf.ExpandStringSlicePtr(excludeGroups) - result.IncludeRoles = tf.ExpandStringSlicePtr(includeRoles) - result.ExcludeRoles = tf.ExpandStringSlicePtr(excludeRoles) - - return &result -} - -func expandConditionalAccessPlatforms(in []interface{}) *msgraph.ConditionalAccessPlatforms { - result := msgraph.ConditionalAccessPlatforms{} - if len(in) == 0 || in[0] == nil { - return &result - } - - config := in[0].(map[string]interface{}) - - includePlatforms := config["included_platforms"].([]interface{}) - excludePlatforms := config["excluded_platforms"].([]interface{}) - - result.IncludePlatforms = tf.ExpandStringSlicePtr(includePlatforms) - result.ExcludePlatforms = tf.ExpandStringSlicePtr(excludePlatforms) - - return &result -} - -func expandConditionalAccessLocations(in []interface{}) *msgraph.ConditionalAccessLocations { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.ConditionalAccessLocations{} - config := in[0].(map[string]interface{}) - - includeLocations := config["included_locations"].([]interface{}) - excludeLocations := config["excluded_locations"].([]interface{}) - - result.IncludeLocations = tf.ExpandStringSlicePtr(includeLocations) - result.ExcludeLocations = tf.ExpandStringSlicePtr(excludeLocations) - - return &result -} - -func expandConditionalAccessGrantControls(in []interface{}) *msgraph.ConditionalAccessGrantControls { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.ConditionalAccessGrantControls{} - config := in[0].(map[string]interface{}) - - operator := config["operator"].(string) - builtInControls := config["built_in_controls"].([]interface{}) - customAuthenticationFactors := config["custom_authentication_factors"].([]interface{}) - termsOfUse := config["terms_of_use"].([]interface{}) - - result.Operator = &operator - result.BuiltInControls = tf.ExpandStringSlicePtr(builtInControls) - result.CustomAuthenticationFactors = tf.ExpandStringSlicePtr(customAuthenticationFactors) - result.TermsOfUse = tf.ExpandStringSlicePtr(termsOfUse) - - return &result -} - -func expandConditionalAccessSessionControls(in []interface{}, create bool) *msgraph.ConditionalAccessSessionControls { - result := msgraph.ConditionalAccessSessionControls{} - - if create && (len(in) == 0 || in[0] == nil) { - return &result - } - - // API doesn't accept boolean false values here in POST requests, it should be omitted instead - // When updating, omitting a setting doesn't change it, so we default to false - if !create { - result.ApplicationEnforcedRestrictions = &msgraph.ApplicationEnforcedRestrictionsSessionControl{ - IsEnabled: utils.Bool(false), - } - result.CloudAppSecurity = &msgraph.CloudAppSecurityControl{ - IsEnabled: utils.Bool(false), + return nil, err } - result.SignInFrequency = &msgraph.SignInFrequencySessionControl{ - IsEnabled: utils.Bool(false), - } - } - - if len(in) == 0 || in[0] == nil { - return &result - } - - config := in[0].(map[string]interface{}) - - result.ApplicationEnforcedRestrictions = &msgraph.ApplicationEnforcedRestrictionsSessionControl{ - IsEnabled: utils.Bool(config["application_enforced_restrictions_enabled"].(bool)), + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of conditional access policy with ID %q", policyId) } - if cloudAppSecurity := config["cloud_app_security_policy"].(string); cloudAppSecurity != "" { - result.CloudAppSecurity = &msgraph.CloudAppSecurityControl{ - IsEnabled: utils.Bool(true), - CloudAppSecurityType: utils.String(cloudAppSecurity), - } - } - - if signInFrequency := config["sign_in_frequency"].(int); signInFrequency > 0 { - result.SignInFrequency = &msgraph.SignInFrequencySessionControl{ - IsEnabled: utils.Bool(true), - Type: utils.String(config["sign_in_frequency_period"].(string)), - Value: utils.Int32(int32(signInFrequency)), - } - } - - // API doesn't accept all disabled settings on POST, instead should be an empty object - if create && !*result.ApplicationEnforcedRestrictions.IsEnabled && !*result.CloudAppSecurity.IsEnabled && !*result.SignInFrequency.IsEnabled { - return &msgraph.ConditionalAccessSessionControls{} - } - - return &result + return nil } diff --git a/internal/services/conditionalaccess/conditionalaccess.go b/internal/services/conditionalaccess/conditionalaccess.go new file mode 100644 index 000000000..f93ea1026 --- /dev/null +++ b/internal/services/conditionalaccess/conditionalaccess.go @@ -0,0 +1,410 @@ +package conditionalaccess + +import ( + "github.com/manicminer/hamilton/msgraph" + + "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +func flattenConditionalAccessConditionSet(in *msgraph.ConditionalAccessConditionSet) []interface{} { + if in == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "applications": flattenConditionalAccessApplications(in.Applications), + "users": flattenConditionalAccessUsers(in.Users), + "client_app_types": tf.FlattenStringSlicePtr(in.ClientAppTypes), + "locations": flattenConditionalAccessLocations(in.Locations), + "platforms": flattenConditionalAccessPlatforms(in.Platforms), + "sign_in_risk_levels": tf.FlattenStringSlicePtr(in.SignInRiskLevels), + "user_risk_levels": tf.FlattenStringSlicePtr(in.UserRiskLevels), + }, + } +} + +func flattenConditionalAccessApplications(in *msgraph.ConditionalAccessApplications) []interface{} { + if in == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "included_applications": tf.FlattenStringSlicePtr(in.IncludeApplications), + "excluded_applications": tf.FlattenStringSlicePtr(in.ExcludeApplications), + "included_user_actions": tf.FlattenStringSlicePtr(in.IncludeUserActions), + }, + } +} + +func flattenConditionalAccessUsers(in *msgraph.ConditionalAccessUsers) []interface{} { + if in == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "included_users": tf.FlattenStringSlicePtr(in.IncludeUsers), + "excluded_users": tf.FlattenStringSlicePtr(in.ExcludeUsers), + "included_groups": tf.FlattenStringSlicePtr(in.IncludeGroups), + "excluded_groups": tf.FlattenStringSlicePtr(in.ExcludeGroups), + "included_roles": tf.FlattenStringSlicePtr(in.IncludeRoles), + "excluded_roles": tf.FlattenStringSlicePtr(in.ExcludeRoles), + }, + } +} + +func flattenConditionalAccessLocations(in *msgraph.ConditionalAccessLocations) []interface{} { + if in == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "included_locations": tf.FlattenStringSlicePtr(in.IncludeLocations), + "excluded_locations": tf.FlattenStringSlicePtr(in.ExcludeLocations), + }, + } +} + +func flattenConditionalAccessPlatforms(in *msgraph.ConditionalAccessPlatforms) []interface{} { + if in == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "included_platforms": tf.FlattenStringSlicePtr(in.IncludePlatforms), + "excluded_platforms": tf.FlattenStringSlicePtr(in.ExcludePlatforms), + }, + } +} + +func flattenConditionalAccessGrantControls(in *msgraph.ConditionalAccessGrantControls) []interface{} { + if in == nil { + return []interface{}{} + } + + return []interface{}{ + map[string]interface{}{ + "operator": in.Operator, + "built_in_controls": tf.FlattenStringSlicePtr(in.BuiltInControls), + "custom_authentication_factors": tf.FlattenStringSlicePtr(in.CustomAuthenticationFactors), + "terms_of_use": tf.FlattenStringSlicePtr(in.TermsOfUse), + }, + } +} + +func flattenConditionalAccessSessionControls(in *msgraph.ConditionalAccessSessionControls) []interface{} { + if in == nil { + return []interface{}{} + } + + applicationEnforceRestrictions := false + if in.ApplicationEnforcedRestrictions != nil { + applicationEnforceRestrictions = *in.ApplicationEnforcedRestrictions.IsEnabled + } + + cloudAppSecurity := "" + if in.CloudAppSecurity != nil && in.CloudAppSecurity.CloudAppSecurityType != nil { + cloudAppSecurity = *in.CloudAppSecurity.CloudAppSecurityType + } + + signInFrequency := 0 + signInFrequencyPeriod := "" + if in.SignInFrequency != nil && in.SignInFrequency.Value != nil && in.SignInFrequency.Type != nil { + signInFrequency = int(*in.SignInFrequency.Value) + signInFrequencyPeriod = *in.SignInFrequency.Type + } + + return []interface{}{ + map[string]interface{}{ + "application_enforced_restrictions_enabled": applicationEnforceRestrictions, + "cloud_app_security_policy": cloudAppSecurity, + "sign_in_frequency": signInFrequency, + "sign_in_frequency_period": signInFrequencyPeriod, + }, + } +} + +func flattenCountryNamedLocation(in *msgraph.CountryNamedLocation) []interface{} { + if in == nil { + return []interface{}{} + } + + includeUnknown := false + if in.IncludeUnknownCountriesAndRegions != nil { + includeUnknown = *in.IncludeUnknownCountriesAndRegions + } + + return []interface{}{ + map[string]interface{}{ + "countries_and_regions": tf.FlattenStringSlicePtr(in.CountriesAndRegions), + "include_unknown_countries_and_regions": includeUnknown, + }, + } +} + +func flattenIPNamedLocation(in *msgraph.IPNamedLocation) []interface{} { + if in == nil { + return []interface{}{} + } + + trusted := false + if in.IsTrusted != nil { + trusted = *in.IsTrusted + } + + return []interface{}{ + map[string]interface{}{ + "ip_ranges": flattenIPNamedLocationIPRange(in.IPRanges), + "trusted": trusted, + }, + } +} + +func flattenIPNamedLocationIPRange(in *[]msgraph.IPNamedLocationIPRange) []interface{} { + if in == nil || len(*in) == 0 { + return []interface{}{} + } + + result := make([]string, 0) + for _, cidr := range *in { + if cidr.CIDRAddress != nil { + result = append(result, *cidr.CIDRAddress) + } + } + + return tf.FlattenStringSlice(result) +} + +func expandConditionalAccessConditionSet(in []interface{}) *msgraph.ConditionalAccessConditionSet { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.ConditionalAccessConditionSet{} + config := in[0].(map[string]interface{}) + + applications := config["applications"].([]interface{}) + users := config["users"].([]interface{}) + clientAppTypes := config["client_app_types"].([]interface{}) + locations := config["locations"].([]interface{}) + platforms := config["platforms"].([]interface{}) + signInRiskLevels := config["sign_in_risk_levels"].([]interface{}) + userRiskLevels := config["user_risk_levels"].([]interface{}) + + result.Applications = expandConditionalAccessApplications(applications) + result.Users = expandConditionalAccessUsers(users) + result.ClientAppTypes = tf.ExpandStringSlicePtr(clientAppTypes) + result.Locations = expandConditionalAccessLocations(locations) + result.Platforms = expandConditionalAccessPlatforms(platforms) + result.SignInRiskLevels = tf.ExpandStringSlicePtr(signInRiskLevels) + result.UserRiskLevels = tf.ExpandStringSlicePtr(userRiskLevels) + + return &result +} + +func expandConditionalAccessApplications(in []interface{}) *msgraph.ConditionalAccessApplications { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.ConditionalAccessApplications{} + config := in[0].(map[string]interface{}) + + includeApplications := config["included_applications"].([]interface{}) + excludeApplications := config["excluded_applications"].([]interface{}) + includeUserActions := config["included_user_actions"].([]interface{}) + + result.IncludeApplications = tf.ExpandStringSlicePtr(includeApplications) + result.ExcludeApplications = tf.ExpandStringSlicePtr(excludeApplications) + result.IncludeUserActions = tf.ExpandStringSlicePtr(includeUserActions) + + return &result +} + +func expandConditionalAccessUsers(in []interface{}) *msgraph.ConditionalAccessUsers { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.ConditionalAccessUsers{} + config := in[0].(map[string]interface{}) + + includeUsers := config["included_users"].([]interface{}) + excludeUsers := config["excluded_users"].([]interface{}) + includeGroups := config["included_groups"].([]interface{}) + excludeGroups := config["excluded_groups"].([]interface{}) + includeRoles := config["included_roles"].([]interface{}) + excludeRoles := config["excluded_roles"].([]interface{}) + + result.IncludeUsers = tf.ExpandStringSlicePtr(includeUsers) + result.ExcludeUsers = tf.ExpandStringSlicePtr(excludeUsers) + result.IncludeGroups = tf.ExpandStringSlicePtr(includeGroups) + result.ExcludeGroups = tf.ExpandStringSlicePtr(excludeGroups) + result.IncludeRoles = tf.ExpandStringSlicePtr(includeRoles) + result.ExcludeRoles = tf.ExpandStringSlicePtr(excludeRoles) + + return &result +} + +func expandConditionalAccessPlatforms(in []interface{}) *msgraph.ConditionalAccessPlatforms { + result := msgraph.ConditionalAccessPlatforms{} + if len(in) == 0 || in[0] == nil { + return &result + } + + config := in[0].(map[string]interface{}) + + includePlatforms := config["included_platforms"].([]interface{}) + excludePlatforms := config["excluded_platforms"].([]interface{}) + + result.IncludePlatforms = tf.ExpandStringSlicePtr(includePlatforms) + result.ExcludePlatforms = tf.ExpandStringSlicePtr(excludePlatforms) + + return &result +} + +func expandConditionalAccessLocations(in []interface{}) *msgraph.ConditionalAccessLocations { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.ConditionalAccessLocations{} + config := in[0].(map[string]interface{}) + + includeLocations := config["included_locations"].([]interface{}) + excludeLocations := config["excluded_locations"].([]interface{}) + + result.IncludeLocations = tf.ExpandStringSlicePtr(includeLocations) + result.ExcludeLocations = tf.ExpandStringSlicePtr(excludeLocations) + + return &result +} + +func expandConditionalAccessGrantControls(in []interface{}) *msgraph.ConditionalAccessGrantControls { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.ConditionalAccessGrantControls{} + config := in[0].(map[string]interface{}) + + operator := config["operator"].(string) + builtInControls := config["built_in_controls"].([]interface{}) + customAuthenticationFactors := config["custom_authentication_factors"].([]interface{}) + termsOfUse := config["terms_of_use"].([]interface{}) + + result.Operator = &operator + result.BuiltInControls = tf.ExpandStringSlicePtr(builtInControls) + result.CustomAuthenticationFactors = tf.ExpandStringSlicePtr(customAuthenticationFactors) + result.TermsOfUse = tf.ExpandStringSlicePtr(termsOfUse) + + return &result +} + +func expandConditionalAccessSessionControls(in []interface{}, create bool) *msgraph.ConditionalAccessSessionControls { + result := msgraph.ConditionalAccessSessionControls{} + + if create && (len(in) == 0 || in[0] == nil) { + return &result + } + + // API doesn't accept boolean false values here in POST requests, it should be omitted instead + // When updating, omitting a setting doesn't change it, so we default to false + if !create { + result.ApplicationEnforcedRestrictions = &msgraph.ApplicationEnforcedRestrictionsSessionControl{ + IsEnabled: utils.Bool(false), + } + result.CloudAppSecurity = &msgraph.CloudAppSecurityControl{ + IsEnabled: utils.Bool(false), + } + result.SignInFrequency = &msgraph.SignInFrequencySessionControl{ + IsEnabled: utils.Bool(false), + } + } + + if len(in) == 0 || in[0] == nil { + return &result + } + + config := in[0].(map[string]interface{}) + + result.ApplicationEnforcedRestrictions = &msgraph.ApplicationEnforcedRestrictionsSessionControl{ + IsEnabled: utils.Bool(config["application_enforced_restrictions_enabled"].(bool)), + } + + if cloudAppSecurity := config["cloud_app_security_policy"].(string); cloudAppSecurity != "" { + result.CloudAppSecurity = &msgraph.CloudAppSecurityControl{ + IsEnabled: utils.Bool(true), + CloudAppSecurityType: utils.String(cloudAppSecurity), + } + } + + if signInFrequency := config["sign_in_frequency"].(int); signInFrequency > 0 { + result.SignInFrequency = &msgraph.SignInFrequencySessionControl{ + IsEnabled: utils.Bool(true), + Type: utils.String(config["sign_in_frequency_period"].(string)), + Value: utils.Int32(int32(signInFrequency)), + } + } + + // API doesn't accept all disabled settings on POST, instead should be an empty object + if create && !*result.ApplicationEnforcedRestrictions.IsEnabled && !*result.CloudAppSecurity.IsEnabled && !*result.SignInFrequency.IsEnabled { + return &msgraph.ConditionalAccessSessionControls{} + } + + return &result +} + +func expandCountryNamedLocation(in []interface{}) *msgraph.CountryNamedLocation { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.CountryNamedLocation{} + config := in[0].(map[string]interface{}) + + countriesAndRegions := config["countries_and_regions"].([]interface{}) + includeUnknown := config["include_unknown_countries_and_regions"] + + result.CountriesAndRegions = tf.ExpandStringSlicePtr(countriesAndRegions) + result.IncludeUnknownCountriesAndRegions = utils.Bool(includeUnknown.(bool)) + + return &result +} + +func expandIPNamedLocation(in []interface{}) *msgraph.IPNamedLocation { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.IPNamedLocation{} + config := in[0].(map[string]interface{}) + + ipRanges := config["ip_ranges"].([]interface{}) + trusted := config["trusted"] + + result.IPRanges = expandIPNamedLocationIPRange(ipRanges) + result.IsTrusted = utils.Bool(trusted.(bool)) + + return &result +} + +func expandIPNamedLocationIPRange(in []interface{}) *[]msgraph.IPNamedLocationIPRange { + if len(in) == 0 { + return nil + } + + result := make([]msgraph.IPNamedLocationIPRange, 0) + for _, cidr := range in { + result = append(result, msgraph.IPNamedLocationIPRange{ + CIDRAddress: utils.String(cidr.(string)), + }) + } + + return &result +} diff --git a/internal/services/conditionalaccess/named_location_resource.go b/internal/services/conditionalaccess/named_location_resource.go index 1369bd9d9..b9f75ec87 100644 --- a/internal/services/conditionalaccess/named_location_resource.go +++ b/internal/services/conditionalaccess/named_location_resource.go @@ -17,6 +17,7 @@ import ( "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" @@ -273,160 +274,47 @@ func namedLocationResourceRead(ctx context.Context, d *schema.ResourceData, meta func namedLocationResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ConditionalAccess.NamedLocationsClient + namedLocationId := d.Id() if _, ok := d.GetOk("ip"); ok { - _, status, err := client.GetIP(ctx, d.Id(), odata.Query{}) - if err != nil { + if _, status, err := client.GetIP(ctx, namedLocationId, odata.Query{}); err != nil { if status == http.StatusNotFound { - log.Printf("[DEBUG] Named Location with ID %q already deleted", d.Id()) + log.Printf("[DEBUG] Named Location with ID %q already deleted", namedLocationId) return nil } - return tf.ErrorDiagPathF(err, "id", "Retrieving named location with ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Retrieving named location with ID %q", namedLocationId) } } if _, ok := d.GetOk("country"); ok { - _, status, err := client.GetCountry(ctx, d.Id(), odata.Query{}) - if err != nil { + if _, status, err := client.GetCountry(ctx, namedLocationId, odata.Query{}); err != nil { if status == http.StatusNotFound { - log.Printf("[DEBUG] Named Location with ID %q already deleted", d.Id()) + log.Printf("[DEBUG] Named Location with ID %q already deleted", namedLocationId) return nil } - return tf.ErrorDiagPathF(err, "id", "Retrieving named location with ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Retrieving named location with ID %q", namedLocationId) } } - status, err := client.Delete(ctx, d.Id()) + status, err := client.Delete(ctx, namedLocationId) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Deleting named location with ID %q, got status %d", d.Id(), status) + return tf.ErrorDiagPathF(err, "id", "Deleting named location with ID %q, got status %d", namedLocationId, status) } - log.Printf("[DEBUG] Waiting for named location %q to disappear", d.Id()) - timeout, _ := ctx.Deadline() - stateConf := &resource.StateChangeConf{ - Pending: []string{"Pending"}, - Target: []string{"Deleted"}, - Timeout: time.Until(timeout), - MinTimeout: 5 * time.Second, - ContinuousTargetOccurence: 5, - Refresh: func() (interface{}, string, error) { - client.BaseClient.DisableRetries = true - _, status, err := client.Get(ctx, d.Id(), odata.Query{}) + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, namedLocationId, odata.Query{}); err != nil { if status == http.StatusNotFound { - return "stub", "Deleted", nil + return utils.Bool(false), nil } - if err != nil { - return nil, "Error", err - } - - return "stub", "Pending", nil - }, - } - if _, err = stateConf.WaitForStateContext(ctx); err != nil { - return tf.ErrorDiagF(err, "waiting for deletion of named location with ID %q", d.Id()) - } - - return nil -} - -func expandIPNamedLocation(in []interface{}) *msgraph.IPNamedLocation { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.IPNamedLocation{} - config := in[0].(map[string]interface{}) - - ipRanges := config["ip_ranges"].([]interface{}) - trusted := config["trusted"] - - result.IPRanges = expandIPNamedLocationIPRange(ipRanges) - result.IsTrusted = utils.Bool(trusted.(bool)) - - return &result -} - -func expandIPNamedLocationIPRange(in []interface{}) *[]msgraph.IPNamedLocationIPRange { - if len(in) == 0 { - return nil - } - - result := make([]msgraph.IPNamedLocationIPRange, 0) - for _, cidr := range in { - result = append(result, msgraph.IPNamedLocationIPRange{ - CIDRAddress: utils.String(cidr.(string)), - }) - } - - return &result -} - -func expandCountryNamedLocation(in []interface{}) *msgraph.CountryNamedLocation { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.CountryNamedLocation{} - config := in[0].(map[string]interface{}) - - countriesAndRegions := config["countries_and_regions"].([]interface{}) - includeUnknown := config["include_unknown_countries_and_regions"] - - result.CountriesAndRegions = tf.ExpandStringSlicePtr(countriesAndRegions) - result.IncludeUnknownCountriesAndRegions = utils.Bool(includeUnknown.(bool)) - - return &result -} - -func flattenIPNamedLocation(in *msgraph.IPNamedLocation) []interface{} { - if in == nil { - return []interface{}{} - } - - trusted := false - if in.IsTrusted != nil { - trusted = *in.IsTrusted - } - - return []interface{}{ - map[string]interface{}{ - "ip_ranges": flattenIPNamedLocationIPRange(in.IPRanges), - "trusted": trusted, - }, - } -} - -func flattenIPNamedLocationIPRange(in *[]msgraph.IPNamedLocationIPRange) []interface{} { - if in == nil || len(*in) == 0 { - return []interface{}{} - } - - result := make([]string, 0) - for _, cidr := range *in { - if cidr.CIDRAddress != nil { - result = append(result, *cidr.CIDRAddress) + return nil, err } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "waiting for deletion of named location with ID %q", namedLocationId) } - return tf.FlattenStringSlice(result) -} - -func flattenCountryNamedLocation(in *msgraph.CountryNamedLocation) []interface{} { - if in == nil { - return []interface{}{} - } - - includeUnknown := false - if in.IncludeUnknownCountriesAndRegions != nil { - includeUnknown = *in.IncludeUnknownCountriesAndRegions - } - - return []interface{}{ - map[string]interface{}{ - "countries_and_regions": tf.FlattenStringSlicePtr(in.CountriesAndRegions), - "include_unknown_countries_and_regions": includeUnknown, - }, - } + return nil } From a004a5b3dcf050e45b6db718c6cdfcab49a3da51 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:04:41 +0000 Subject: [PATCH 05/17] azuread_user: check for consistency on deletion --- internal/services/users/user_resource.go | 26 +++++++++++++++++++----- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/internal/services/users/user_resource.go b/internal/services/users/user_resource.go index 4088c94fe..c3835a1ce 100644 --- a/internal/services/users/user_resource.go +++ b/internal/services/users/user_resource.go @@ -17,6 +17,7 @@ import ( "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" @@ -676,19 +677,34 @@ func userResourceRead(ctx context.Context, d *schema.ResourceData, meta interfac func userResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Users.UsersClient + userId := d.Id() - _, status, err := client.Get(ctx, d.Id(), odata.Query{}) + _, status, err := client.Get(ctx, userId, odata.Query{}) if err != nil { if status == http.StatusNotFound { - return tf.ErrorDiagPathF(fmt.Errorf("User was not found"), "id", "Retrieving user with object ID %q", d.Id()) + return tf.ErrorDiagPathF(fmt.Errorf("User was not found"), "id", "Retrieving user with object ID %q", userId) } - return tf.ErrorDiagPathF(err, "id", "Retrieving user with object ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Retrieving user with object ID %q", userId) } - status, err = client.Delete(ctx, d.Id()) + status, err = client.Delete(ctx, userId) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Deleting user with object ID %q, got status %d", d.Id(), status) + return tf.ErrorDiagPathF(err, "id", "Deleting user with object ID %q, got status %d", userId, status) + } + + // Wait for user object to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, userId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of user with object ID %q", userId) } return nil From 034f4605b64c0f1e62cb98a529afcbdfbe72c871 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:05:45 +0000 Subject: [PATCH 06/17] azuread_invitation: check for consistency on creation by attmpting to patch the guest user, use helper func for consistency check on deletion, move flatten/expand funcs to own source file --- .../invitations/invitation_resource.go | 101 +++++++----------- .../invitations/invitation_resource_test.go | 36 +++++++ internal/services/invitations/invitations.go | 39 +++++++ 3 files changed, 113 insertions(+), 63 deletions(-) create mode 100644 internal/services/invitations/invitations.go diff --git a/internal/services/invitations/invitation_resource.go b/internal/services/invitations/invitation_resource.go index 1c7f906ec..2081cfca9 100644 --- a/internal/services/invitations/invitation_resource.go +++ b/internal/services/invitations/invitation_resource.go @@ -9,13 +9,13 @@ import ( "time" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" "github.com/manicminer/hamilton/msgraph" "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" @@ -126,6 +126,7 @@ func invitationResource() *schema.Resource { func invitationResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Invitations.InvitationsClient + usersClient := meta.(*clients.Client).Invitations.UsersClient properties := msgraph.Invitation{ InvitedUserEmailAddress: utils.String(d.Get("user_email_address").(string)), @@ -162,6 +163,33 @@ func invitationResourceCreate(ctx context.Context, d *schema.ResourceData, meta } d.Set("redeem_url", invitation.InviteRedeemURL) + // Attempt to patch the newly created guest user, which will tell us whether it exists yet + // The SDK handles retries for us here in the event of 404, 429 or 5xx, then returns after giving up + status, err := usersClient.Update(ctx, msgraph.User{ + DirectoryObject: msgraph.DirectoryObject{ + ID: invitation.InvitedUser.ID, + }, + CompanyName: utils.NullableString("TERRAFORM_UPDATE"), + }) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagF(err, "Timed out whilst waiting for new guest user to be replicated in Azure AD") + } + return tf.ErrorDiagF(err, "Failed to patch guest user after creating invitation") + } + status, err = usersClient.Update(ctx, msgraph.User{ + DirectoryObject: msgraph.DirectoryObject{ + ID: invitation.InvitedUser.ID, + }, + CompanyName: utils.NullableString(""), + }) + if err != nil { + if status == http.StatusNotFound { + return tf.ErrorDiagF(err, "Timed out whilst waiting for new guest user to be replicated in Azure AD") + } + return tf.ErrorDiagF(err, "Failed to patch guest user after creating invitation") + } + return invitationResourceRead(ctx, d, meta) } @@ -206,71 +234,18 @@ func invitationResourceDelete(ctx context.Context, d *schema.ResourceData, meta } // Wait for user object to be deleted, this seems much slower for invited users - deadline, ok := ctx.Deadline() - if !ok { - return tf.ErrorDiagF(errors.New("context has no deadline"), "Waiting for deletion of invited user %q", userID) - } - timeout := time.Until(deadline) - _, err = (&resource.StateChangeConf{ - Pending: []string{"Waiting"}, - Target: []string{"Deleted"}, - Timeout: timeout, - MinTimeout: 5 * time.Second, - ContinuousTargetOccurence: 5, - Refresh: func() (interface{}, string, error) { - client.BaseClient.DisableRetries = true - user, status, err := client.Get(ctx, userID, odata.Query{}) - if err != nil { - if status == http.StatusNotFound { - return "stub", "Deleted", nil - } - return nil, "Error", fmt.Errorf("retrieving Invited User with object ID %q: %+v", userID, err) + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, userID, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil } - if user == nil { - return nil, "Error", fmt.Errorf("retrieving Invited User with object ID %q: user was nil", userID) - } - return *user, "Waiting", nil - }, - }).WaitForStateContext(ctx) - if err != nil { + return nil, err + } + return utils.Bool(true), nil + }); err != nil { return tf.ErrorDiagF(err, "Waiting for deletion of invited user with object ID %q", userID) } return nil } - -func expandInvitedUserMessageInfo(in []interface{}) *msgraph.InvitedUserMessageInfo { - if len(in) == 0 || in[0] == nil { - return nil - } - - result := msgraph.InvitedUserMessageInfo{} - config := in[0].(map[string]interface{}) - - additionalRecipients := config["additional_recipients"].([]interface{}) - messageBody := config["body"].(string) - messageLanguage := config["language"].(string) - - result.CCRecipients = expandRecipients(additionalRecipients) - result.CustomizedMessageBody = &messageBody - result.MessageLanguage = &messageLanguage - - return &result -} - -func expandRecipients(in []interface{}) *[]msgraph.Recipient { - recipients := make([]msgraph.Recipient, 0, len(in)) - for _, recipientRaw := range in { - recipient := recipientRaw.(string) - - newRecipient := msgraph.Recipient{ - EmailAddress: &msgraph.EmailAddress{ - Address: &recipient, - }, - } - - recipients = append(recipients, newRecipient) - } - - return &recipients -} diff --git a/internal/services/invitations/invitation_resource_test.go b/internal/services/invitations/invitation_resource_test.go index 26ba1ffae..abe63ab7d 100644 --- a/internal/services/invitations/invitation_resource_test.go +++ b/internal/services/invitations/invitation_resource_test.go @@ -123,6 +123,21 @@ func TestAccInvitation_messageWithLanguage(t *testing.T) { }) } +func TestAccInvitation_withGroupMembership(t *testing.T) { + count := 10 + data := acceptance.BuildTestData(t, "azuread_invitation", fmt.Sprintf("test.%d", count-1)) + r := InvitationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.withGroupMembership(data, count), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + ), + }, + }) +} + func (r InvitationResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { client := clients.Invitations.UsersClient client.BaseClient.DisableRetries = true @@ -199,3 +214,24 @@ resource "azuread_invitation" "test" { } `, data.RandomString) } + +func (InvitationResource) withGroupMembership(data acceptance.TestData, count int) string { + return fmt.Sprintf(` +resource "azuread_group" "test" { + display_name = "acctestGroup-%[1]d" + security_enabled = true +} + +resource "azuread_invitation" "test" { + count = %[3]d + redirect_url = "https://portal.azure.com" + user_email_address = "acctest-user-%[2]s-groupMember-${count.index}@test.com" +} + +resource "azuread_group_member" "test" { + count = %[3]d + group_object_id = azuread_group.test.object_id + member_object_id = azuread_invitation.test[count.index].user_id +} +`, data.RandomInteger, data.RandomString, count) +} diff --git a/internal/services/invitations/invitations.go b/internal/services/invitations/invitations.go new file mode 100644 index 000000000..f2621bfa8 --- /dev/null +++ b/internal/services/invitations/invitations.go @@ -0,0 +1,39 @@ +package invitations + +import "github.com/manicminer/hamilton/msgraph" + +func expandInvitedUserMessageInfo(in []interface{}) *msgraph.InvitedUserMessageInfo { + if len(in) == 0 || in[0] == nil { + return nil + } + + result := msgraph.InvitedUserMessageInfo{} + config := in[0].(map[string]interface{}) + + additionalRecipients := config["additional_recipients"].([]interface{}) + messageBody := config["body"].(string) + messageLanguage := config["language"].(string) + + result.CCRecipients = expandRecipients(additionalRecipients) + result.CustomizedMessageBody = &messageBody + result.MessageLanguage = &messageLanguage + + return &result +} + +func expandRecipients(in []interface{}) *[]msgraph.Recipient { + recipients := make([]msgraph.Recipient, 0, len(in)) + for _, recipientRaw := range in { + recipient := recipientRaw.(string) + + newRecipient := msgraph.Recipient{ + EmailAddress: &msgraph.EmailAddress{ + Address: &recipient, + }, + } + + recipients = append(recipients, newRecipient) + } + + return &recipients +} From f65be8a9829582fc7c10e7cae007bbb6bcfcf4b8 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:07:06 +0000 Subject: [PATCH 07/17] azuread_group_member: check for consistency on deletion --- internal/services/groups/group_member_resource.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/services/groups/group_member_resource.go b/internal/services/groups/group_member_resource.go index 04a1d9eae..fcd4446db 100644 --- a/internal/services/groups/group_member_resource.go +++ b/internal/services/groups/group_member_resource.go @@ -15,6 +15,7 @@ import ( "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/services/groups/parse" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/utils" @@ -164,5 +165,19 @@ func groupMemberResourceDelete(ctx context.Context, d *schema.ResourceData, meta return tf.ErrorDiagF(err, "Removing member %q from group with object ID: %q", id.MemberId, id.GroupId) } + // Wait for membership link to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.GetMember(ctx, id.GroupId, id.MemberId); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for removal of member %q from group with object ID %q", id.MemberId, id.GroupId) + } + return nil } From 520693dba24913ac94b1da7796904f76c1b46d2e Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:07:46 +0000 Subject: [PATCH 08/17] azuread_group: check for consistency on creation by attempting to patch the new group, perform consistency check on deletion --- internal/services/groups/group_resource.go | 46 +++++++++++++++++----- 1 file changed, 37 insertions(+), 9 deletions(-) diff --git a/internal/services/groups/group_resource.go b/internal/services/groups/group_resource.go index ac6e1bc5a..d5eb2a447 100644 --- a/internal/services/groups/group_resource.go +++ b/internal/services/groups/group_resource.go @@ -17,6 +17,7 @@ import ( "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" @@ -383,9 +384,16 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter provisioningOptions = append(provisioningOptions, v.(string)) } + // Set a temporary display name as we'll attempt to patch the group with the correct name after creating it + uuid, err := uuid.GenerateUUID() + if err != nil { + return tf.ErrorDiagF(err, "Failed to generate a UUID") + } + tempDisplayName := fmt.Sprintf("TERRAFORM_UPDATE_%s", uuid) + properties := msgraph.Group{ Description: utils.NullableString(d.Get("description").(string)), - DisplayName: utils.String(displayName), + DisplayName: utils.String(tempDisplayName), GroupTypes: groupTypes, IsAssignableToRole: utils.Bool(d.Get("assignable_to_role").(bool)), MailEnabled: utils.Bool(mailEnabled), @@ -498,14 +506,19 @@ func groupResourceCreate(ctx context.Context, d *schema.ResourceData, meta inter d.SetId(*group.ID) - // Wait until the group is updatable (the SDK handles retries for us) - _, err = client.Update(ctx, msgraph.Group{ + // Attempt to patch the newly created group with the correct name, which will tell us whether it exists yet + // The SDK handles retries for us here in the event of 404, 429 or 5xx, then returns after giving up + status, err := client.Update(ctx, msgraph.Group{ DirectoryObject: msgraph.DirectoryObject{ ID: group.ID, }, + DisplayName: utils.String(displayName), }) if err != nil { - return tf.ErrorDiagF(err, "Timed out whilst waiting for new group to be replicated in Azure AD") + if status == http.StatusNotFound { + return tf.ErrorDiagF(err, "Timed out whilst waiting for new group to be replicated in Azure AD") + } + return tf.ErrorDiagF(err, "Failed to patch group after creating") } // Add any remaining owners after the group is created @@ -754,17 +767,32 @@ func groupResourceRead(ctx context.Context, d *schema.ResourceData, meta interfa func groupResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Groups.GroupsClient + groupId := d.Id() - _, status, err := client.Get(ctx, d.Id(), odata.Query{}) + _, status, err := client.Get(ctx, groupId, odata.Query{}) if err != nil { if status == http.StatusNotFound { - return tf.ErrorDiagPathF(fmt.Errorf("Group was not found"), "id", "Retrieving group with object ID %q", d.Id()) + return tf.ErrorDiagPathF(fmt.Errorf("Group was not found"), "id", "Retrieving group with object ID %q", groupId) } - return tf.ErrorDiagPathF(err, "id", "Retrieving group with object ID: %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Retrieving group with object ID: %q", groupId) } - if _, err := client.Delete(ctx, d.Id()); err != nil { - return tf.ErrorDiagF(err, "Deleting group with object ID: %q", d.Id()) + if _, err := client.Delete(ctx, groupId); err != nil { + return tf.ErrorDiagF(err, "Deleting group with object ID: %q", groupId) + } + + // Wait for group object to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, groupId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of group with object ID %q", groupId) } return nil From ce26230bb885d04b3cc270c2fde457b96a3e8949 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:08:15 +0000 Subject: [PATCH 09/17] azuread_directory_role_member: check for consistency on deletion --- .../directory_role_member_resource.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/internal/services/directoryroles/directory_role_member_resource.go b/internal/services/directoryroles/directory_role_member_resource.go index b4204cc08..2d6adbea1 100644 --- a/internal/services/directoryroles/directory_role_member_resource.go +++ b/internal/services/directoryroles/directory_role_member_resource.go @@ -15,6 +15,7 @@ import ( "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/services/directoryroles/parse" "github.com/hashicorp/terraform-provider-azuread/internal/tf" "github.com/hashicorp/terraform-provider-azuread/internal/utils" @@ -174,5 +175,19 @@ func directoryRoleMemberResourceDelete(ctx context.Context, d *schema.ResourceDa return tf.ErrorDiagF(err, "Removing member %q from directory role with object ID: %q", id.MemberId, id.DirectoryRoleId) } + // Wait for membership link to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.GetMember(ctx, id.DirectoryRoleId, id.MemberId); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for removal of member %q from directory role with object ID %q", id.MemberId, id.DirectoryRoleId) + } + return nil } From 292f3c0506469f7d70ecc9d92b458fb35edde014 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:08:49 +0000 Subject: [PATCH 10/17] Helper funcs for retrieving a KeyCredential/PasswordCredential by its uuid --- internal/helpers/credentials.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/internal/helpers/credentials.go b/internal/helpers/credentials.go index 5b39178c4..ed859b4b3 100644 --- a/internal/helpers/credentials.go +++ b/internal/helpers/credentials.go @@ -28,6 +28,30 @@ func (e CredentialError) Error() string { return e.str } +func GetKeyCredential(keyCredentials *[]msgraph.KeyCredential, id string) (credential *msgraph.KeyCredential) { + if keyCredentials != nil { + for _, cred := range *keyCredentials { + if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id) { + credential = &cred + break + } + } + } + return +} + +func GetPasswordCredential(passwordCredentials *[]msgraph.PasswordCredential, id string) (credential *msgraph.PasswordCredential) { + if passwordCredentials != nil { + for _, cred := range *passwordCredentials { + if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id) { + credential = &cred + break + } + } + } + return +} + func KeyCredentialForResource(d *schema.ResourceData) (*msgraph.KeyCredential, error) { keyType := d.Get("type").(string) value := d.Get("value").(string) From f1af2440e3dd65ad767b6df2ec28116aabee7ccd Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:09:40 +0000 Subject: [PATCH 11/17] azuread_application_certificate: check for consistency on deletion --- .../application_certificate_resource.go | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/internal/services/applications/application_certificate_resource.go b/internal/services/applications/application_certificate_resource.go index a84d73483..ec4e0eeb7 100644 --- a/internal/services/applications/application_certificate_resource.go +++ b/internal/services/applications/application_certificate_resource.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/parse" "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" ) @@ -228,16 +229,7 @@ func applicationCertificateResourceRead(ctx context.Context, d *schema.ResourceD return tf.ErrorDiagPathF(err, "application_object_id", "Retrieving Application with object ID %q", id.ObjectId) } - var credential *msgraph.KeyCredential - if app.KeyCredentials != nil { - for _, cred := range *app.KeyCredentials { - if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id.KeyId) { - credential = &cred - break - } - } - } - + credential := helpers.GetKeyCredential(app.KeyCredentials, id.KeyId) if credential == nil { log.Printf("[DEBUG] Certificate credential %q (ID %q) was not found - removing from state!", id.KeyId, id.ObjectId) d.SetId("") @@ -301,5 +293,24 @@ func applicationCertificateResourceDelete(ctx context.Context, d *schema.Resourc return tf.ErrorDiagF(err, "Removing certificate credential %q from application with object ID %q", id.KeyId, id.ObjectId) } + // Wait for application certificate to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + + app, _, err := client.Get(ctx, id.ObjectId, odata.Query{}) + if err != nil { + return nil, err + } + + credential := helpers.GetKeyCredential(app.KeyCredentials, id.KeyId) + if credential == nil { + return utils.Bool(false), nil + } + + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of certificate credential %q from application with object ID %q", id.KeyId, id.ObjectId) + } + return nil } From ec6fd1479da3e91ced86035b1103308a1964c2d4 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:10:17 +0000 Subject: [PATCH 12/17] azuread_application_password: check for consistency on deletion --- .../application_password_resource.go | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/internal/services/applications/application_password_resource.go b/internal/services/applications/application_password_resource.go index bb766cb51..3013cf134 100644 --- a/internal/services/applications/application_password_resource.go +++ b/internal/services/applications/application_password_resource.go @@ -13,7 +13,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" - "github.com/manicminer/hamilton/msgraph" "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/clients" @@ -21,6 +20,7 @@ import ( "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/migrations" "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/parse" "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" ) @@ -219,16 +219,7 @@ func applicationPasswordResourceRead(ctx context.Context, d *schema.ResourceData return tf.ErrorDiagPathF(err, "application_object_id", "Retrieving application with object ID %q", id.ObjectId) } - var credential *msgraph.PasswordCredential - if app.PasswordCredentials != nil { - for _, cred := range *app.PasswordCredentials { - if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id.KeyId) { - credential = &cred - break - } - } - } - + credential := helpers.GetPasswordCredential(app.PasswordCredentials, id.KeyId) if credential == nil { log.Printf("[DEBUG] Password credential %q (ID %q) was not found - removing from state!", id.KeyId, id.ObjectId) d.SetId("") @@ -279,5 +270,24 @@ func applicationPasswordResourceDelete(ctx context.Context, d *schema.ResourceDa return tf.ErrorDiagF(err, "Removing password credential %q from application with object ID %q", id.KeyId, id.ObjectId) } + // Wait for application password to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + + app, _, err := client.Get(ctx, id.ObjectId, odata.Query{}) + if err != nil { + return nil, err + } + + credential := helpers.GetPasswordCredential(app.PasswordCredentials, id.KeyId) + if credential == nil { + return utils.Bool(false), nil + } + + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of password credential %q from application with object ID %q", id.KeyId, id.ObjectId) + } + return nil } From 30aafee0943fc3de1febd3cad22c6b9f6f5ff254 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:18:26 +0000 Subject: [PATCH 13/17] azuread_application: check for consistency on creation by attempting to patch the new application, perform consistency check on deletion --- .../applications/application_resource.go | 45 +++++++++++++++---- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/internal/services/applications/application_resource.go b/internal/services/applications/application_resource.go index 061562c47..843446f35 100644 --- a/internal/services/applications/application_resource.go +++ b/internal/services/applications/application_resource.go @@ -940,11 +940,18 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta return applicationResourceUpdate(ctx, d, meta) } + // Set a temporary display name as we'll attempt to patch the application with the correct name after creating it + uuid, err := uuid.GenerateUUID() + if err != nil { + return tf.ErrorDiagF(err, "Failed to generate a UUID") + } + tempDisplayName := fmt.Sprintf("TERRAFORM_UPDATE_%s", uuid) + // Create a new application properties := msgraph.Application{ Api: expandApplicationApi(d.Get("api").([]interface{})), AppRoles: expandApplicationAppRoles(d.Get("app_role").(*schema.Set).List()), - DisplayName: utils.String(displayName), + DisplayName: utils.String(tempDisplayName), GroupMembershipClaims: expandApplicationGroupMembershipClaims(d.Get("group_membership_claims").(*schema.Set).List()), IdentifierUris: tf.ExpandStringSlicePtr(d.Get("identifier_uris").(*schema.Set).List()), Info: &msgraph.InformationalUrl{ @@ -1032,14 +1039,19 @@ func applicationResourceCreate(ctx context.Context, d *schema.ResourceData, meta d.SetId(*app.ID) - // Wait until the application is updatable (the SDK handles retries for us) - _, err = client.Update(ctx, msgraph.Application{ + // Attempt to patch the newly created group with the correct name, which will tell us whether it exists yet + // The SDK handles retries for us here in the event of 404, 429 or 5xx, then returns after giving up + status, err := client.Update(ctx, msgraph.Application{ DirectoryObject: msgraph.DirectoryObject{ ID: app.ID, }, + DisplayName: utils.String(displayName), }) if err != nil { - return tf.ErrorDiagF(err, "Timed out whilst waiting for new application to be replicated in Azure AD") + if status == http.StatusNotFound { + return tf.ErrorDiagF(err, "Timed out whilst waiting for new application to be replicated in Azure AD") + } + return tf.ErrorDiagF(err, "Failed to patch application after creating") } if len(ownersExtra) > 0 { @@ -1276,19 +1288,34 @@ func applicationResourceRead(ctx context.Context, d *schema.ResourceData, meta i func applicationResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).Applications.ApplicationsClient + appId := d.Id() - _, status, err := client.Get(ctx, d.Id(), odata.Query{}) + _, status, err := client.Get(ctx, appId, odata.Query{}) if err != nil { if status == http.StatusNotFound { - return tf.ErrorDiagPathF(fmt.Errorf("Application was not found"), "id", "Retrieving Application with object ID %q", d.Id()) + return tf.ErrorDiagPathF(fmt.Errorf("Application was not found"), "id", "Retrieving Application with object ID %q", appId) } - return tf.ErrorDiagPathF(err, "id", "Retrieving application with object ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Retrieving application with object ID %q", appId) } - status, err = client.Delete(ctx, d.Id()) + status, err = client.Delete(ctx, appId) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Deleting application with object ID %q, got status %d", d.Id(), status) + return tf.ErrorDiagPathF(err, "id", "Deleting application with object ID %q, got status %d", appId, status) + } + + // Wait for application object to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, appId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of application with object ID %q", appId) } return nil From d038d2e8171782a81611bfe54691699854d6d4f7 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:18:45 +0000 Subject: [PATCH 14/17] azuread_service_principal_certificate: check for consistency on deletion --- .../service_principal_certificate_resource.go | 33 ++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/internal/services/serviceprincipals/service_principal_certificate_resource.go b/internal/services/serviceprincipals/service_principal_certificate_resource.go index 8ee8cee77..9c3d6690f 100644 --- a/internal/services/serviceprincipals/service_principal_certificate_resource.go +++ b/internal/services/serviceprincipals/service_principal_certificate_resource.go @@ -20,6 +20,7 @@ import ( "github.com/hashicorp/terraform-provider-azuread/internal/helpers" "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/parse" "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" ) @@ -218,7 +219,7 @@ func servicePrincipalCertificateResourceRead(ctx context.Context, d *schema.Reso return tf.ErrorDiagPathF(err, "id", "Parsing certificate credential with ID %q", d.Id()) } - app, status, err := client.Get(ctx, id.ObjectId, odata.Query{}) + servicePrincipal, status, err := client.Get(ctx, id.ObjectId, odata.Query{}) if err != nil { if status == http.StatusNotFound { log.Printf("[DEBUG] Service Principal with ID %q for %s credential %q was not found - removing from state!", id.ObjectId, id.KeyType, id.KeyId) @@ -228,16 +229,7 @@ func servicePrincipalCertificateResourceRead(ctx context.Context, d *schema.Reso return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", id.ObjectId) } - var credential *msgraph.KeyCredential - if app.KeyCredentials != nil { - for _, cred := range *app.KeyCredentials { - if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id.KeyId) { - credential = &cred - break - } - } - } - + credential := helpers.GetKeyCredential(servicePrincipal.KeyCredentials, id.KeyId) if credential == nil { log.Printf("[DEBUG] Certificate credential %q (ID %q) was not found - removing from state!", id.KeyId, id.ObjectId) d.SetId("") @@ -301,5 +293,24 @@ func servicePrincipalCertificateResourceDelete(ctx context.Context, d *schema.Re return tf.ErrorDiagF(err, "Removing certificate credential %q from service principal with object ID %q", id.KeyId, id.ObjectId) } + // Wait for service principal certificate to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + + servicePrincipal, _, err := client.Get(ctx, id.ObjectId, odata.Query{}) + if err != nil { + return nil, err + } + + credential := helpers.GetKeyCredential(servicePrincipal.KeyCredentials, id.KeyId) + if credential == nil { + return utils.Bool(false), nil + } + + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of certificate credential %q from service principal with object ID %q", id.KeyId, id.ObjectId) + } + return nil } From 226facc66d9e5aca22bc1610653dcdc44e329dda Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:19:01 +0000 Subject: [PATCH 15/17] azuread_service_principal_password: check for consistency on deletion --- .../service_principal_password_resource.go | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/internal/services/serviceprincipals/service_principal_password_resource.go b/internal/services/serviceprincipals/service_principal_password_resource.go index fafb1ccab..52a368cfd 100644 --- a/internal/services/serviceprincipals/service_principal_password_resource.go +++ b/internal/services/serviceprincipals/service_principal_password_resource.go @@ -12,7 +12,6 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "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" @@ -20,6 +19,7 @@ import ( "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/migrations" "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/parse" "github.com/hashicorp/terraform-provider-azuread/internal/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" "github.com/hashicorp/terraform-provider-azuread/internal/validate" ) @@ -190,7 +190,7 @@ func servicePrincipalPasswordResourceRead(ctx context.Context, d *schema.Resourc return tf.ErrorDiagPathF(err, "id", "Parsing password credential with ID %q", d.Id()) } - app, status, err := client.Get(ctx, id.ObjectId, odata.Query{}) + servicePrincipal, status, err := client.Get(ctx, id.ObjectId, odata.Query{}) if err != nil { if status == http.StatusNotFound { log.Printf("[DEBUG] Service Principal with ID %q for %s credential %q was not found - removing from state!", id.ObjectId, id.KeyType, id.KeyId) @@ -200,16 +200,7 @@ func servicePrincipalPasswordResourceRead(ctx context.Context, d *schema.Resourc return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", id.ObjectId) } - var credential *msgraph.PasswordCredential - if app.PasswordCredentials != nil { - for _, cred := range *app.PasswordCredentials { - if cred.KeyId != nil && strings.EqualFold(*cred.KeyId, id.KeyId) { - credential = &cred - break - } - } - } - + credential := helpers.GetPasswordCredential(servicePrincipal.PasswordCredentials, id.KeyId) if credential == nil { log.Printf("[DEBUG] Password credential %q (ID %q) was not found - removing from state!", id.KeyId, id.ObjectId) d.SetId("") @@ -259,5 +250,24 @@ func servicePrincipalPasswordResourceDelete(ctx context.Context, d *schema.Resou return tf.ErrorDiagF(err, "Removing password credential %q from service principal with object ID %q", id.KeyId, id.ObjectId) } + // Wait for service principal password to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + + servicePrincipal, _, err := client.Get(ctx, id.ObjectId, odata.Query{}) + if err != nil { + return nil, err + } + + credential := helpers.GetPasswordCredential(servicePrincipal.PasswordCredentials, id.KeyId) + if credential == nil { + return utils.Bool(false), nil + } + + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of password credential %q from service principal with object ID %q", id.KeyId, id.ObjectId) + } + return nil } From a9abfd824a0e814c4190aeacb2a032da126c959c Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Thu, 11 Nov 2021 18:22:25 +0000 Subject: [PATCH 16/17] azuread_service_principal: check for consistency on creation by attempting to patch the new service principal, perform consistency check on deletion --- .../service_principal_resource.go | 49 +++++++++++++++---- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/internal/services/serviceprincipals/service_principal_resource.go b/internal/services/serviceprincipals/service_principal_resource.go index e68e2c793..f4abc2679 100644 --- a/internal/services/serviceprincipals/service_principal_resource.go +++ b/internal/services/serviceprincipals/service_principal_resource.go @@ -391,12 +391,19 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, tags = tf.ExpandStringSlice(d.Get("tags").(*schema.Set).List()) } + // Set a temporary description as we'll attempt to patch the service principal with the correct description after creating it + uuid, err := uuid.GenerateUUID() + if err != nil { + return tf.ErrorDiagF(err, "Failed to generate a UUID") + } + tempDescription := fmt.Sprintf("TERRAFORM_UPDATE_%s", uuid) + properties := msgraph.ServicePrincipal{ AccountEnabled: utils.Bool(d.Get("account_enabled").(bool)), AlternativeNames: tf.ExpandStringSlicePtr(d.Get("alternative_names").(*schema.Set).List()), AppId: utils.String(d.Get("application_id").(string)), AppRoleAssignmentRequired: utils.Bool(d.Get("app_role_assignment_required").(bool)), - Description: utils.NullableString(d.Get("description").(string)), + Description: utils.NullableString(tempDescription), LoginUrl: utils.NullableString(d.Get("login_url").(string)), Notes: utils.NullableString(d.Get("notes").(string)), NotificationEmailAddresses: tf.ExpandStringSlicePtr(d.Get("notification_email_addresses").(*schema.Set).List()), @@ -471,14 +478,19 @@ func servicePrincipalResourceCreate(ctx context.Context, d *schema.ResourceData, } d.SetId(*servicePrincipal.ID) - // Wait until the service principal is updatable (the SDK handles retries for us) - _, err = client.Update(ctx, msgraph.ServicePrincipal{ + // Attempt to patch the newly created service principal with the correct description, which will tell us whether it exists yet + // The SDK handles retries for us here in the event of 404, 429 or 5xx, then returns after giving up + status, err := client.Update(ctx, msgraph.ServicePrincipal{ DirectoryObject: msgraph.DirectoryObject{ ID: servicePrincipal.ID, }, + Description: utils.NullableString(d.Get("description").(string)), }) if err != nil { - return tf.ErrorDiagF(err, "Timed out whilst waiting for new service principal to be replicated in Azure AD") + if status == http.StatusNotFound { + return tf.ErrorDiagF(err, "Timed out whilst waiting for new service principal to be replicated in Azure AD") + } + return tf.ErrorDiagF(err, "Failed to patch service principal after creating") } // Add any remaining owners after the service principal is created @@ -642,20 +654,37 @@ func servicePrincipalResourceRead(ctx context.Context, d *schema.ResourceData, m func servicePrincipalResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient + servicePrincipalId := d.Id() - _, status, err := client.Get(ctx, d.Id(), odata.Query{}) + _, status, err := client.Get(ctx, servicePrincipalId, odata.Query{}) if err != nil { if status == http.StatusNotFound { - return tf.ErrorDiagPathF(fmt.Errorf("Service Principal was not found"), "id", "Retrieving service principal with object ID %q", d.Id()) + return tf.ErrorDiagPathF(fmt.Errorf("Service Principal was not found"), "id", "Retrieving service principal with object ID %q", servicePrincipalId) } - return tf.ErrorDiagPathF(err, "id", "Retrieving service principal with object ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Retrieving service principal with object ID %q", servicePrincipalId) } useExisting := d.Get("use_existing").(bool) - status, err = client.Delete(ctx, d.Id()) - if err != nil && !useExisting { - return tf.ErrorDiagPathF(err, "id", "Deleting service principal with object ID %q, got status %d", d.Id(), status) + status, err = client.Delete(ctx, servicePrincipalId) + if !useExisting { + if err != nil && !useExisting { + return tf.ErrorDiagPathF(err, "id", "Deleting service principal with object ID %q, got status %d", servicePrincipalId, status) + } + + // Wait for service principal object to be deleted + if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + client.BaseClient.DisableRetries = true + if _, status, err := client.Get(ctx, servicePrincipalId, odata.Query{}); err != nil { + if status == http.StatusNotFound { + return utils.Bool(false), nil + } + return nil, err + } + return utils.Bool(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for deletion of group with object ID %q", servicePrincipalId) + } } return nil From d0cbaf686ea83b6a0e3ff5334b46905791fb3121 Mon Sep 17 00:00:00 2001 From: Tom Bamford Date: Fri, 12 Nov 2021 00:24:23 +0000 Subject: [PATCH 17/17] Fix azuread_service_principal test - https identifier_uris must use a verified domain --- .../serviceprincipals/service_principal_resource_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/services/serviceprincipals/service_principal_resource_test.go b/internal/services/serviceprincipals/service_principal_resource_test.go index f310d919f..c99f0921a 100644 --- a/internal/services/serviceprincipals/service_principal_resource_test.go +++ b/internal/services/serviceprincipals/service_principal_resource_test.go @@ -344,13 +344,15 @@ func (ServicePrincipalResource) templateComplete(data acceptance.TestData) strin return fmt.Sprintf(` provider "azuread" {} +data "azuread_domains" "test" {} + resource "azuread_application" "test" { display_name = "acctestServicePrincipal-%[1]d" sign_in_audience = "AzureADMyOrg" identifier_uris = [ "api://acctestServicePrincipal-%[1]d", - "https://acctestServicePrincipal-%[1]d.net", + "https://${data.azuread_domains.test.domains[0].domain_name}/acctestServicePrincipal-%[1]d", ] api {