diff --git a/.teamcity/components/project.kt b/.teamcity/components/project.kt index 592e45124b..59467ed2c2 100644 --- a/.teamcity/components/project.kt +++ b/.teamcity/components/project.kt @@ -7,6 +7,7 @@ var services = mapOf( "applications" to "Applications", "domains" to "Domains", "groups" to "Groups", + "invitations" to "Invitations", "serviceprincipals" to "Service Principals", "users" to "Users" ) diff --git a/docs/resources/invitation.md b/docs/resources/invitation.md new file mode 100644 index 0000000000..0ee21cb054 --- /dev/null +++ b/docs/resources/invitation.md @@ -0,0 +1,84 @@ +--- +subcategory: "Invitations" +--- + +# Resource: azuread_invitation + +Manages an invitation of a guest user within Azure Active Directory. + +## 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 + +*Basic example* + +```terraform +resource "azuread_invitation" "example" { + user_email_address = "jdoe@hashicorp.com" + redirect_url = "https://portal.azure.com" +} +``` + +*Invitation with standard message* + +```terraform +resource "azuread_invitation" "example" { + user_email_address = "jdoe@hashicorp.com" + redirect_url = "https://portal.azure.com" + + message { + language = "en-US" + } +} +``` + +*Invitation with custom message body and an additional recipient* + +```terraform +resource "azuread_invitation" "example" { + user_display_name = "Bob Bobson" + user_email_address = "bbobson@hashicorp.com" + redirect_url = "https://portal.azure.com" + + message { + additional_recipients = ["aaliceberg@hashicorp.com"] + body = "Hello there! You are invited to join my Azure tenant!" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +* `message` - (Optional) A `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. +* `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_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`. + +--- + +`message` block supports the following: + +* `additional_recipients` - (Optional) Email addresses of additional recipients the invitation message should be sent to. Only 1 additional recipient is currently supported by Azure. +* `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 `body`. + + +## Attributes Reference + +In addition to all arguments above, the following attributes are exported: + +* `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/clients/client.go b/internal/clients/client.go index b02f6a3e81..3e88a38a6d 100644 --- a/internal/clients/client.go +++ b/internal/clients/client.go @@ -11,6 +11,7 @@ import ( applications "github.com/hashicorp/terraform-provider-azuread/internal/services/applications/client" domains "github.com/hashicorp/terraform-provider-azuread/internal/services/domains/client" groups "github.com/hashicorp/terraform-provider-azuread/internal/services/groups/client" + invitations "github.com/hashicorp/terraform-provider-azuread/internal/services/invitations/client" serviceprincipals "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals/client" users "github.com/hashicorp/terraform-provider-azuread/internal/services/users/client" ) @@ -29,6 +30,7 @@ type Client struct { Applications *applications.Client Domains *domains.Client Groups *groups.Client + Invitations *invitations.Client ServicePrincipals *serviceprincipals.Client Users *users.Client } @@ -39,6 +41,7 @@ func (client *Client) build(ctx context.Context, o *common.ClientOptions) error client.Applications = applications.NewClient(o) client.Domains = domains.NewClient(o) client.Groups = groups.NewClient(o) + client.Invitations = invitations.NewClient(o) client.ServicePrincipals = serviceprincipals.NewClient(o) client.Users = users.NewClient(o) diff --git a/internal/provider/services.go b/internal/provider/services.go index 7772bdeae2..22434fb23b 100644 --- a/internal/provider/services.go +++ b/internal/provider/services.go @@ -4,6 +4,7 @@ import ( "github.com/hashicorp/terraform-provider-azuread/internal/services/applications" "github.com/hashicorp/terraform-provider-azuread/internal/services/domains" "github.com/hashicorp/terraform-provider-azuread/internal/services/groups" + "github.com/hashicorp/terraform-provider-azuread/internal/services/invitations" "github.com/hashicorp/terraform-provider-azuread/internal/services/serviceprincipals" "github.com/hashicorp/terraform-provider-azuread/internal/services/users" ) @@ -13,6 +14,7 @@ func SupportedServices() []ServiceRegistration { applications.Registration{}, domains.Registration{}, groups.Registration{}, + invitations.Registration{}, serviceprincipals.Registration{}, users.Registration{}, } diff --git a/internal/services/invitations/client/client.go b/internal/services/invitations/client/client.go new file mode 100644 index 0000000000..78ab9a1417 --- /dev/null +++ b/internal/services/invitations/client/client.go @@ -0,0 +1,25 @@ +package client + +import ( + "github.com/manicminer/hamilton/msgraph" + + "github.com/hashicorp/terraform-provider-azuread/internal/common" +) + +type Client struct { + InvitationsClient *msgraph.InvitationsClient + UsersClient *msgraph.UsersClient +} + +func NewClient(o *common.ClientOptions) *Client { + invitationsClient := msgraph.NewInvitationsClient(o.TenantID) + o.ConfigureClient(&invitationsClient.BaseClient) + + usersClient := msgraph.NewUsersClient(o.TenantID) + o.ConfigureClient(&usersClient.BaseClient) + + return &Client{ + InvitationsClient: invitationsClient, + UsersClient: usersClient, + } +} diff --git a/internal/services/invitations/invitation_resource.go b/internal/services/invitations/invitation_resource.go new file mode 100644 index 0000000000..1e4a0184ea --- /dev/null +++ b/internal/services/invitations/invitation_resource.go @@ -0,0 +1,276 @@ +package invitations + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "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/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" + "github.com/hashicorp/terraform-provider-azuread/internal/validate" +) + +func invitationResource() *schema.Resource { + return &schema.Resource{ + CreateContext: invitationResourceCreate, + ReadContext: invitationResourceRead, + DeleteContext: invitationResourceDelete, + + 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{ + "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.IsHTTPOrHTTPSURL, + }, + + "user_email_address": { + Description: "The email address of the user being invited", + Type: schema.TypeString, + Required: true, + ForceNew: 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, + }, + + "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{ + "additional_recipients": { + 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, + ValidateDiagFunc: validate.StringIsEmailAddress, + }, + }, + + "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{"message.0.language"}, + ValidateDiagFunc: validate.NoEmptyStrings, + }, + + "language": { + Description: "The language you want to send the default message in", + Type: schema.TypeString, + Optional: true, + ConflictsWith: []string{"message.0.body"}, + ValidateDiagFunc: validate.ISO639Language, + }, + }, + }, + }, + + "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": { + 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.InvitationsClient + + properties := msgraph.Invitation{ + 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("message"); ok { + properties.SendInvitationMessage = utils.Bool(true) + properties.InvitedUserMessageInfo = expandInvitedUserMessageInfo(v.([]interface{})) + } + + invitation, _, err := client.Create(ctx, properties) + if err != nil { + return tf.ErrorDiagF(err, "Could not create invitation") + } + + if invitation.ID == nil || *invitation.ID == "" { + return tf.ErrorDiagF(errors.New("Bad API response"), "Object ID returned for invitation is nil/empty") + } + d.SetId(*invitation.ID) + + if invitation.InvitedUser == nil || invitation.InvitedUser.ID == nil || *invitation.InvitedUser.ID == "" { + return tf.ErrorDiagF(errors.New("Bad API response"), "Invited user object ID returned for invitation is nil/empty") + } + d.Set("user_id", invitation.InvitedUser.ID) + + if invitation.InviteRedeemURL == nil || *invitation.InviteRedeemURL == "" { + return tf.ErrorDiagF(errors.New("Bad API response"), "Redeem URL returned for invitation is nil/empty") + } + d.Set("redeem_url", invitation.InviteRedeemURL) + + return invitationResourceRead(ctx, d, meta) +} + +func invitationResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).Invitations.UsersClient + + userID := d.Get("user_id").(string) + + user, status, err := client.Get(ctx, userID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + 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 invited user with object ID: %q", userID) + } + + tf.Set(d, "user_id", user.ID) + tf.Set(d, "user_email_address", user.Mail) + + return nil +} + +func invitationResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client).Invitations.UsersClient + + userID := d.Get("user_id").(string) + + _, 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 invited 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 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 || 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 new file mode 100644 index 0000000000..26ba1ffaec --- /dev/null +++ b/internal/services/invitations/invitation_resource_test.go @@ -0,0 +1,201 @@ +package invitations_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/manicminer/hamilton/odata" + + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" + "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" + "github.com/hashicorp/terraform-provider-azuread/internal/clients" + "github.com/hashicorp/terraform-provider-azuread/internal/utils" +) + +type InvitationResource struct{} + +func TestAccInvitation_basic(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_invitation", "test") + r := InvitationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(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_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("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_message(t *testing.T) { + data := acceptance.BuildTestData(t, "azuread_invitation", "test") + r := InvitationResource{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.withMessage(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("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("message.#").HasValue("1"), + check.That(data.ResourceName).Key("message.0.additional_recipients.#").HasValue("1"), + check.That(data.ResourceName).Key("message.0.additional_recipients.0").HasValue(fmt.Sprintf("acctest-another-%s@test.com", data.RandomString)), + check.That(data.ResourceName).Key("message.0.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("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("message.#").HasValue("1"), + check.That(data.ResourceName).Key("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) { + client := clients.Invitations.UsersClient + client.BaseClient.DisableRetries = true + + userID := state.Attributes["user_id"] + + user, status, err := client.Get(ctx, userID, odata.Query{}) + if err != nil { + if status == http.StatusNotFound { + return nil, fmt.Errorf("Invited user with object ID %q does not exist", userID) + } + 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 +} + +func (InvitationResource) basic(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" +} +`, data.RandomString) +} + +func (InvitationResource) member(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_type = "Member" +} +`, data.RandomString) +} + +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" + + message {} +} +`, data.RandomString) +} + +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" + + message { + additional_recipients = ["acctest-another-%[1]s@test.com"] + body = "Hello there! You are invited to join my Azure tenant." + } +} +`, 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" + + message { + language = "fr-CA" + } +} +`, data.RandomString) +} diff --git a/internal/services/invitations/registration.go b/internal/services/invitations/registration.go new file mode 100644 index 0000000000..a2b9b01b2c --- /dev/null +++ b/internal/services/invitations/registration.go @@ -0,0 +1,31 @@ +package invitations + +import ( + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +type Registration struct{} + +// Name is the name of this Service +func (r Registration) Name() string { + return "Invitations" +} + +// WebsiteCategories returns a list of categories which can be used for the sidebar +func (r Registration) WebsiteCategories() []string { + return []string{ + "Invitations", + } +} + +// SupportedDataSources returns the supported Data Sources supported by this Service +func (r Registration) SupportedDataSources() map[string]*schema.Resource { + return map[string]*schema.Resource{} +} + +// SupportedResources returns the supported Resources supported by this Service +func (r Registration) SupportedResources() map[string]*schema.Resource { + return map[string]*schema.Resource{ + "azuread_invitation": invitationResource(), + } +} 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