diff --git a/docs/resources/invitation.md b/docs/resources/invitation.md index 503e140d53..4ed48ea13d 100644 --- a/docs/resources/invitation.md +++ b/docs/resources/invitation.md @@ -6,7 +6,13 @@ subcategory: "Invitations" Manages an invitation of a guest user within Azure Active Directory. --> **NOTE:** If you're authenticating using a Service Principal then it must have permissions to `User.ReadWrite.All` within the `Microsoft Graph` API. +## API Permissions + +The following API permissions are required in order to use this resource. + +When authenticated with a service principal, this resource requires one of the following application roles: `User.Invite.All`, `User.ReadWrite.All` or `Directory.ReadWrite.All` + +When authenticated with a user principal, this resource requires one of the following directory roles: `Guest Inviter`, `User Administrator` or `Global Administrator` ## Example Usage @@ -19,7 +25,20 @@ resource "azuread_invitation" "example" { } ``` -*Invitation with custom email* +*Invitation with standard message* + +```terraform +resource "azuread_invitation" "example" { + user_email_address = "jdoe@hashicorp.com" + redirect_url = "https://portal.azure.com" + + user_message_info { + language = "en-US" + } +} +``` + +*Invitation with custom message and a CC recipient* ```terraform resource "azuread_invitation" "example" { @@ -27,12 +46,9 @@ resource "azuread_invitation" "example" { user_email_address = "bbobson@hashicorp.com" redirect_url = "https://portal.azure.com" - send_invitation_message = true - user_message_info { - cc_recipients = ["aaliceberg@hashicorp.com"] - customised_message_body = "Hello there! You are invited to join my Azure tenant !" - message_language = "en-US" + cc_recipients = ["aaliceberg@hashicorp.com"] + customized_body = "Hello there! You are invited to join my Azure tenant!" } } ``` @@ -41,25 +57,28 @@ resource "azuread_invitation" "example" { The following arguments are supported: -* `redirect_url` - (Required) URL the user should be redirected to once the invitation is redeemed. -* `send_invitation_message` - (Optional) If `true`, an email will be sent to the user being invited. Must be set to `true` if a `user_message_info` block is specified. Defaults to `false`. -* `user_display_name` - (Optional) Display name of the user being invited. -* `user_email_address` - (Required) Email address of the user being invited. -* `user_message_info` - (Optional) A `user_message_info` block as documented below, which configures the message being sent to the invited user. `send_invitation_message` must be set to `true` if this block is specified. +* `redirect_url` - (Required) The URL that the user should be redirected to once the invitation is redeemed. +* `user_display_name` - (Optional) The display name of the user being invited. +* `user_email_address` - (Required) The email address of the user being invited. +* `user_message` - (Optional) A `user_message` block as documented below, which configures the message being sent to the invited user. If this block is omitted, no message will be sent. +* `user_type` - (Optional) The user type of the user being invited. Must be one of `Guest` or `Member`. Only Global Administrators can invite users as members. Defaults to `Guest`. --- -`user_message_info` block supports the following: +`user_message` block supports the following: -* `cc_recipients` - (Optional) Additional recipients the invitation message should be sent to. Currently only 1 additional recipient is supported by Azure. -* `customised_message_body` - (Optional) Customised message body you want to send if you don't want the default message. -* `message_language` - (Optional) Language the message will be sent in. The value specified must be in ISO 639 format. Defaults to `en-US`. +* `cc_recipients` - (Optional) Email addresses of additional recipients the invitation message should be sent to. Only 1 additional recipient is currently supported by Azure. +* `customized_body` - (Optional) Customized message body you want to send if you don't want to send the default message. Cannot be specified with `language`. +* `language` - (Optional) The language you want to send the default message in. The value specified must be in ISO 639 format. Defaults to `en-US`. Cannot be specified with `customized_body`. ## Attributes Reference In addition to all arguments above, the following attributes are exported: -* `id` - ID of the invitation. -* `redeem_url` - URL the user can use to redeem the invitation. -* `user_id` - Object ID of the invited user. \ No newline at end of file +* `redeem_url` - The URL the user can use to redeem their invitation. +* `user_id` - Object ID of the invited user. + +## Import + +This resource does not support importing. diff --git a/go.mod b/go.mod index ba428ab450..0b2a506520 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0 github.com/hashicorp/yamux v0.0.0-20210316155119-a95892c5f864 // indirect github.com/klauspost/compress v1.12.2 // indirect - github.com/manicminer/hamilton v0.26.0 + github.com/manicminer/hamilton v0.27.0 github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/mapstructure v1.4.1 // indirect diff --git a/go.sum b/go.sum index bebef8c4e8..8becb8e4a8 100644 --- a/go.sum +++ b/go.sum @@ -285,8 +285,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= -github.com/manicminer/hamilton v0.26.0 h1:AJ8RrSAG8xkTBKC+hOeUijgVFXiXaqPBDs7oRP3O14o= -github.com/manicminer/hamilton v0.26.0/go.mod h1:QryxpD/4+cdKuXNi0UjLDvgxYdP0LLmYz7dYU7DAX4U= +github.com/manicminer/hamilton v0.27.0 h1:IRyrikO0lh9IAzI3XD2FjnoR7l24GGdhHt+2MXty7GI= +github.com/manicminer/hamilton v0.27.0/go.mod h1:QryxpD/4+cdKuXNi0UjLDvgxYdP0LLmYz7dYU7DAX4U= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= diff --git a/internal/services/invitations/client/client.go b/internal/services/invitations/client/client.go index 513c79696f..78ab9a1417 100644 --- a/internal/services/invitations/client/client.go +++ b/internal/services/invitations/client/client.go @@ -7,14 +7,19 @@ import ( ) type Client struct { - MsClient *msgraph.InvitationsClient + InvitationsClient *msgraph.InvitationsClient + UsersClient *msgraph.UsersClient } func NewClient(o *common.ClientOptions) *Client { - msClient := msgraph.NewInvitationsClient(o.TenantID) - o.ConfigureClient(&msClient.BaseClient) + invitationsClient := msgraph.NewInvitationsClient(o.TenantID) + o.ConfigureClient(&invitationsClient.BaseClient) + + usersClient := msgraph.NewUsersClient(o.TenantID) + o.ConfigureClient(&usersClient.BaseClient) return &Client{ - MsClient: msClient, + InvitationsClient: invitationsClient, + UsersClient: usersClient, } } diff --git a/internal/services/invitations/invitation_resource.go b/internal/services/invitations/invitation_resource.go index a02c0c6b56..6639d21d73 100644 --- a/internal/services/invitations/invitation_resource.go +++ b/internal/services/invitations/invitation_resource.go @@ -6,11 +6,14 @@ import ( "fmt" "log" "net/http" + "time" - "github.com/hashicorp/go-uuid" "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/tf" @@ -24,106 +27,118 @@ func invitationResource() *schema.Resource { ReadContext: invitationResourceRead, DeleteContext: invitationResourceDelete, - Importer: tf.ValidateResourceIDPriorToImport(func(id string) error { - if _, err := uuid.ParseUUID(id); err != nil { - return fmt.Errorf("specified ID (%q) is not valid: %s", id, err) - } - return nil - }), + Timeouts: &schema.ResourceTimeout{ + Create: schema.DefaultTimeout(5 * time.Minute), + Read: schema.DefaultTimeout(5 * time.Minute), + Update: schema.DefaultTimeout(5 * time.Minute), + Delete: schema.DefaultTimeout(5 * time.Minute), + }, Schema: map[string]*schema.Schema{ - "id": { - Type: schema.TypeString, - Computed: true, - }, - "user_email_address": { + "redirect_url": { + Description: "The URL that the user should be redirected to once the invitation is redeemed", Type: schema.TypeString, Required: true, ForceNew: true, - ValidateDiagFunc: validate.StringIsEmailAddress, + ValidateDiagFunc: validate.IsHTTPOrHTTPSURL, }, - "redirect_url": { + + "user_email_address": { + Description: "The email address of the user being invited", Type: schema.TypeString, Required: true, ForceNew: true, - ValidateDiagFunc: validate.IsHTTPOrHTTPSURL, - }, - "user_id": { - Type: schema.TypeString, - Computed: true, + ValidateDiagFunc: validate.StringIsEmailAddress, }, + "user_display_name": { + Description: "The display name of the user being invited", Type: schema.TypeString, Optional: true, ForceNew: true, ValidateDiagFunc: validate.NoEmptyStrings, }, - "user_message_info": { - Type: schema.TypeList, - Optional: true, - ForceNew: true, - MaxItems: 1, - RequiredWith: []string{"send_invitation_message"}, + + "user_message": { + Description: "Customize the message sent to the invited user", + Type: schema.TypeList, + Optional: true, + ForceNew: true, + MaxItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "cc_recipients": { - Type: schema.TypeList, - Optional: true, + Description: "Email addresses of additional recipients the invitation message should be sent to", + Type: schema.TypeList, + Optional: true, + MaxItems: 1, Elem: &schema.Schema{ - Type: schema.TypeString, + Type: schema.TypeString, + ValidateDiagFunc: validate.StringIsEmailAddress, }, }, - "customised_message_body": { + + "customized_body": { + Description: "Customized message body you want to send if you don't want to send the default message", Type: schema.TypeString, Optional: true, + ConflictsWith: []string{"user_message.0.language"}, ValidateDiagFunc: validate.NoEmptyStrings, }, - "message_language": { + + "language": { + Description: "The language you want to send the default message in", Type: schema.TypeString, Optional: true, - ValidateDiagFunc: validate.NoEmptyStrings, + ConflictsWith: []string{"user_message.0.customized_body"}, + ValidateDiagFunc: validate.ISO639Language, }, }, }, }, - "send_invitation_message": { - Type: schema.TypeBool, - Optional: true, - ForceNew: true, + + "user_type": { + Description: "The user type of the user being invited", + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "Guest", + ValidateFunc: validation.StringInSlice([]string{ + msgraph.InvitedUserTypeGuest, + msgraph.InvitedUserTypeMember, + }, false), }, + "redeem_url": { - Type: schema.TypeString, - Computed: true, + Description: "The URL the user can use to redeem their invitation", + Type: schema.TypeString, + Computed: true, + }, + + "user_id": { + Description: "Object ID of the invited user", + Type: schema.TypeString, + Computed: true, }, }, } } func invitationResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*clients.Client).Invitations.MsClient - - invitedUserEmailAddress := d.Get("user_email_address").(string) - inviteRedirectUrl := d.Get("redirect_url").(string) + client := meta.(*clients.Client).Invitations.InvitationsClient properties := msgraph.Invitation{ - InvitedUserEmailAddress: utils.String(invitedUserEmailAddress), - InviteRedirectURL: utils.String(inviteRedirectUrl), + InvitedUserEmailAddress: utils.String(d.Get("user_email_address").(string)), + InviteRedirectURL: utils.String(d.Get("redirect_url").(string)), + InvitedUserType: utils.String(d.Get("user_type").(string)), } if v, ok := d.GetOk("user_display_name"); ok { properties.InvitedUserDisplayName = utils.String(v.(string)) } - if v, ok := d.GetOk("send_invitation_message"); ok { - properties.SendInvitationMessage = utils.Bool(v.(bool)) - } - - if v, ok := d.GetOk("user_message_info"); ok { - // Since ValidateFunc and ValidateDiagFunc are not yet supported on lists or sets we must check for send_invitation_message value here - if properties.SendInvitationMessage == nil || !*properties.SendInvitationMessage { - return tf.ErrorDiagF(errors.New("Wrong value"), "When `user_message_info` is specified, `send_invitation_message` must be set to `true`") - } - + if v, ok := d.GetOk("user_message"); ok { + properties.SendInvitationMessage = utils.Bool(true) properties.InvitedUserMessageInfo = expandInvitedUserMessageInfo(v.([]interface{})) } @@ -147,26 +162,22 @@ func invitationResourceCreate(ctx context.Context, d *schema.ResourceData, meta } d.Set("redeem_url", invitation.InviteRedeemURL) - if err != nil { - return tf.ErrorDiagF(err, "Waiting for User with object ID: %q", *invitation.InvitedUser.ID) - } - return invitationResourceRead(ctx, d, meta) } func invitationResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*clients.Client).Users.UsersClient + client := meta.(*clients.Client).Invitations.UsersClient userID := d.Get("user_id").(string) - user, status, err := client.Get(ctx, userID) + user, status, err := client.Get(ctx, userID, odata.Query{}) if err != nil { if status == http.StatusNotFound { - log.Printf("[DEBUG] User with Object ID %q was not found - removing from state!", userID) + log.Printf("[DEBUG] Invited user with Object ID %q was not found - removing from state!", userID) d.Set("user_id", "") return nil } - return tf.ErrorDiagF(err, "Retrieving user with object ID: %q", userID) + return tf.ErrorDiagF(err, "Retrieving invited user with object ID: %q", userID) } tf.Set(d, "user_id", user.ID) @@ -176,29 +187,60 @@ func invitationResourceRead(ctx context.Context, d *schema.ResourceData, meta in } func invitationResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { - client := meta.(*clients.Client).Users.UsersClient + client := meta.(*clients.Client).Invitations.UsersClient userID := d.Get("user_id").(string) - _, status, err := client.Get(ctx, userID) + _, 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", userID) + return tf.ErrorDiagPathF(fmt.Errorf("User was not found"), "id", "Retrieving invited user with object ID %q", userID) } - return tf.ErrorDiagPathF(err, "id", "Retrieving user with object ID %q", userID) + return tf.ErrorDiagPathF(err, "id", "Retrieving invited user with object ID %q", userID) } status, err = client.Delete(ctx, userID) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Deleting user with object ID %q, got status %d", userID, status) + return tf.ErrorDiagPathF(err, "id", "Deleting invited user with object ID %q, got status %d with error: %+v", userID, status, err) + } + + // 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 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 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 { + if len(in) == 0 || in[0] == nil { return nil } @@ -206,8 +248,8 @@ func expandInvitedUserMessageInfo(in []interface{}) *msgraph.InvitedUserMessageI config := in[0].(map[string]interface{}) ccRecipients := config["cc_recipients"].([]interface{}) - messageBody := config["customised_message_body"].(string) - messageLanguage := config["message_language"].(string) + messageBody := config["customized_body"].(string) + messageLanguage := config["language"].(string) result.CCRecipients = expandRecipients(ccRecipients) result.CustomizedMessageBody = &messageBody diff --git a/internal/services/invitations/invitation_resource_test.go b/internal/services/invitations/invitation_resource_test.go index 952fb40cd7..3abaa22343 100644 --- a/internal/services/invitations/invitation_resource_test.go +++ b/internal/services/invitations/invitation_resource_test.go @@ -5,10 +5,10 @@ import ( "fmt" "net/http" "testing" - "time" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/manicminer/hamilton/odata" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" @@ -27,48 +27,114 @@ func TestAccInvitation_basic(t *testing.T) { Config: r.basic(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("id").Exists(), + check.That(data.ResourceName).Key("redeem_url").Exists(), + check.That(data.ResourceName).Key("redirect_url").HasValue("https://portal.azure.com"), + check.That(data.ResourceName).Key("user_email_address").HasValue(fmt.Sprintf("acctest-user-%s@test.com", data.RandomString)), check.That(data.ResourceName).Key("user_id").Exists(), + check.That(data.ResourceName).Key("user_type").HasValue("Guest"), + ), + }, + }) +} + +func TestAccInvitation_member(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_invitation", "test") + r := InvitationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.member(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("redeem_url").Exists(), - check.That(data.ResourceName).Key("user_email_address").HasValue(fmt.Sprintf("test-user-%s@test.com", data.RandomString)), check.That(data.ResourceName).Key("redirect_url").HasValue("https://portal.azure.com"), + check.That(data.ResourceName).Key("user_email_address").HasValue(fmt.Sprintf("acctest-user-%s@test.com", data.RandomString)), + check.That(data.ResourceName).Key("user_id").Exists(), + check.That(data.ResourceName).Key("user_type").HasValue("Member"), ), }, }) } -func TestAccInvitation_complete(t *testing.T) { +func TestAccInvitation_message(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_invitation", "test") r := InvitationResource{} data.ResourceTest(t, r, []resource.TestStep{ { - Config: r.complete(data), + Config: r.withMessage(data), Check: resource.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("id").Exists(), + check.That(data.ResourceName).Key("redeem_url").Exists(), + check.That(data.ResourceName).Key("redirect_url").HasValue("https://portal.azure.com"), + check.That(data.ResourceName).Key("user_display_name").HasValue("Test user"), + check.That(data.ResourceName).Key("user_email_address").HasValue(fmt.Sprintf("acctest-user-%s@test.com", data.RandomString)), check.That(data.ResourceName).Key("user_id").Exists(), + check.That(data.ResourceName).Key("user_message.#").HasValue("1"), + check.That(data.ResourceName).Key("user_type").HasValue("Guest"), + ), + }, + }) +} + +func TestAccInvitation_messageWithCustomizedBody(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_invitation", "test") + r := InvitationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.withMessageHavingCustomizedBody(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), + check.That(data.ResourceName).Key("redeem_url").Exists(), + check.That(data.ResourceName).Key("redirect_url").HasValue("https://portal.azure.com"), + check.That(data.ResourceName).Key("user_display_name").HasValue("Test user"), + check.That(data.ResourceName).Key("user_email_address").HasValue(fmt.Sprintf("acctest-user-%s@test.com", data.RandomString)), + check.That(data.ResourceName).Key("user_id").Exists(), + check.That(data.ResourceName).Key("user_message.#").HasValue("1"), + check.That(data.ResourceName).Key("user_message.0.cc_recipients.#").HasValue("1"), + check.That(data.ResourceName).Key("user_message.0.cc_recipients.0").HasValue(fmt.Sprintf("acctest-another-%s@test.com", data.RandomString)), + check.That(data.ResourceName).Key("user_message.0.customized_body").HasValue("Hello there! You are invited to join my Azure tenant."), + check.That(data.ResourceName).Key("user_type").HasValue("Guest"), + ), + }, + }) +} + +func TestAccInvitation_messageWithLanguage(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_invitation", "test") + r := InvitationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.withMessageHavingLanguage(data), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("redeem_url").Exists(), - check.That(data.ResourceName).Key("user_email_address").HasValue(fmt.Sprintf("test-user-%s@test.com", data.RandomString)), check.That(data.ResourceName).Key("redirect_url").HasValue("https://portal.azure.com"), check.That(data.ResourceName).Key("user_display_name").HasValue("Test user"), - check.That(data.ResourceName).Key("send_invitation_message").HasValue("true"), + check.That(data.ResourceName).Key("user_email_address").HasValue(fmt.Sprintf("acctest-user-%s@test.com", data.RandomString)), + check.That(data.ResourceName).Key("user_id").Exists(), + check.That(data.ResourceName).Key("user_message.#").HasValue("1"), + check.That(data.ResourceName).Key("user_message.0.language").HasValue("fr-CA"), + check.That(data.ResourceName).Key("user_type").HasValue("Guest"), ), }, }) } func (r InvitationResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { - time.Sleep(time.Second * 10) + client := clients.Invitations.UsersClient + client.BaseClient.DisableRetries = true userID := state.Attributes["user_id"] - user, status, err := clients.Users.UsersClient.Get(ctx, userID) + user, status, err := client.Get(ctx, userID, odata.Query{}) if err != nil { if status == http.StatusNotFound { - return nil, fmt.Errorf("User with object ID %q does not exist", userID) + return nil, fmt.Errorf("Invited user with object ID %q does not exist", userID) } - return nil, fmt.Errorf("failed to retrieve User with object ID %q: %+v", userID, err) + return nil, fmt.Errorf("failed to retrieve invited user with object ID %q: %+v", userID, err) } return utils.Bool(user.ID != nil && *user.ID == userID), nil @@ -77,27 +143,59 @@ func (r InvitationResource) Exists(ctx context.Context, clients *clients.Client, func (InvitationResource) basic(data acceptance.TestData) string { return fmt.Sprintf(` resource "azuread_invitation" "test" { - user_email_address = "test-user-%s@test.com" redirect_url = "https://portal.azure.com" + user_email_address = "acctest-user-%[1]s@test.com" } `, data.RandomString) } -func (InvitationResource) complete(data acceptance.TestData) string { +func (InvitationResource) member(data acceptance.TestData) string { return fmt.Sprintf(` resource "azuread_invitation" "test" { - user_email_address = "test-user-%s@test.com" redirect_url = "https://portal.azure.com" + user_email_address = "acctest-user-%[1]s@test.com" + user_type = "Member" +} +`, data.RandomString) +} - user_display_name = "Test user" +func (InvitationResource) withMessage(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_invitation" "test" { + redirect_url = "https://portal.azure.com" + user_email_address = "acctest-user-%[1]s@test.com" + user_display_name = "Test user" - send_invitation_message = true + user_message {} +} +`, data.RandomString) +} - user_message_info { - cc_recipients = ["test-user-%s@test.com"] - customised_message_body = "Hello there! You are invited to join my Azure tenant." - message_language = "en-US" +func (InvitationResource) withMessageHavingCustomizedBody(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_invitation" "test" { + redirect_url = "https://portal.azure.com" + user_email_address = "acctest-user-%[1]s@test.com" + user_display_name = "Test user" + + user_message { + cc_recipients = ["acctest-another-%[1]s@test.com"] + customized_body = "Hello there! You are invited to join my Azure tenant." } } -`, data.RandomString, data.RandomString) +`, data.RandomString) +} + +func (InvitationResource) withMessageHavingLanguage(data acceptance.TestData) string { + return fmt.Sprintf(` +resource "azuread_invitation" "test" { + redirect_url = "https://portal.azure.com" + user_email_address = "acctest-user-%[1]s@test.com" + user_display_name = "Test user" + + user_message { + language = "fr-CA" + } +} +`, data.RandomString) } diff --git a/vendor/github.com/manicminer/hamilton/msgraph/models.go b/vendor/github.com/manicminer/hamilton/msgraph/models.go index e3086041e8..72377cea19 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/models.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/models.go @@ -389,7 +389,7 @@ type ConditionalAccessPolicy struct { ID *string `json:"id,omitempty"` ModifiedDateTime *time.Time `json:"modifiedDateTime,omitempty"` SessionControls *ConditionalAccessSessionControls `json:"sessionControls,omitempty"` - State *string `json:"state,omitempty"` + State *ConditionalAccessPolicyState `json:"state,omitempty"` } type ConditionalAccessSessionControls struct { @@ -707,14 +707,14 @@ type InformationalUrl struct { // Invitation describes a Invitation object. type Invitation struct { - ID *string `json:"id,omitempty"` - InvitedUserDisplayName *string `json:"invitedUserDisplayName,omitempty"` - InvitedUserEmailAddress *string `json:"invitedUserEmailAddress,omitempty"` - SendInvitationMessage *bool `json:"sendInvitationMessage,omitempty"` - InviteRedirectURL *string `json:"inviteRedirectUrl,omitempty"` - InviteRedeemURL *string `json:"inviteRedeemUrl,omitempty"` - Status *string `json:"status,omitempty"` - InvitedUserType *string `json:"invitedUserType,omitempty"` + ID *string `json:"id,omitempty"` + InvitedUserDisplayName *string `json:"invitedUserDisplayName,omitempty"` + InvitedUserEmailAddress *string `json:"invitedUserEmailAddress,omitempty"` + SendInvitationMessage *bool `json:"sendInvitationMessage,omitempty"` + InviteRedirectURL *string `json:"inviteRedirectUrl,omitempty"` + InviteRedeemURL *string `json:"inviteRedeemUrl,omitempty"` + Status *string `json:"status,omitempty"` + InvitedUserType *InvitedUserType `json:"invitedUserType,omitempty"` InvitedUserMessageInfo *InvitedUserMessageInfo `json:"invitedUserMessageInfo,omitempty"` InvitedUser *User `json:"invitedUser,omitempty"` diff --git a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go index ac2c0e6859..5e9b9ea92f 100644 --- a/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go +++ b/vendor/github.com/manicminer/hamilton/msgraph/valuetypes.go @@ -113,6 +113,14 @@ const ( CredentialUsageSummaryPeriod1 CredentialUsageSummaryPeriod = "D1" ) +type ConditionalAccessPolicyState = string + +const ( + ConditionalAccessPolicyStateEnabled ConditionalAccessPolicyState = "enabled" + ConditionalAccessPolicyStateDisabled ConditionalAccessPolicyState = "disabled" + ConditionalAccessPolicyStateEnabledForReportingButNotEnforced ConditionalAccessPolicyState = "enabledForReportingButNotEnforced" +) + type ExtensionSchemaTargetType = string const ( @@ -197,6 +205,13 @@ const ( GroupVisibilityPublic GroupVisibility = "Public" ) +type InvitedUserType = string + +const ( + InvitedUserTypeGuest InvitedUserType = "Guest" + InvitedUserTypeMember InvitedUserType = "Member" +) + type KeyCredentialType = string const ( diff --git a/vendor/modules.txt b/vendor/modules.txt index b4015d6804..9ee575eb59 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -192,7 +192,7 @@ github.com/klauspost/compress/fse github.com/klauspost/compress/huff0 github.com/klauspost/compress/zstd github.com/klauspost/compress/zstd/internal/xxhash -# github.com/manicminer/hamilton v0.26.0 +# github.com/manicminer/hamilton v0.27.0 ## explicit github.com/manicminer/hamilton/auth github.com/manicminer/hamilton/environments