Invite Azure Active Directory Guest Users via terraform
September 10, 2022โข957 words
Introduction
At work one of my systems that I built and maintain is a collection of PowerBI datasets and reports that our external clients consume to view their performance in our portfolio. To make this possible we invite the external users to our Azure Active Directory tenant and give them access to a PowerBI app we created for them. This way they can consume reports but not edit - essentially look but don't touch!
So far that process of inviting users is fairly manual and time-consuming: First we need to invite the users individually from the GUI of Azure Portal, then we have to send them each an email to explain the invite and include the link to the PowerBI app. Seems fine for one or two users but as soon as I have to invite 6 or 7 users at once I find myself pushing it off my to-do list day after day because it takes so long.
I hope to replace all of this with terraform! Alternatively there could absolutely be a fancy PowerShell Script that automates the guest invites, but the great thing about terraform is that it expresses a state rather than an action. This is what our infrastructure should look like and if it doesn't, then please make it that way, whatever the deployment path to that state may be. Also we already manage our Azure subscription fully via terraform and less spots where something is configured are generally preferable, in my opinion.
Creating AAD invited via terraform
Let's see what an aad invite looks like in terraform:
resource "azuread_invitation" "externalClientUser" {
user_email_address = "niceperson@client.de"
}
It is that simple! At this point I already save time, anytime I have to invite 5 or 6 users at once. But of course now we do not want to just copy those three lines for every single guest user. Instead we can use the for_each argument.
resource "azuread_invitation" "externalClientUsers" {
for_each = toset(["niceperson@client.de", "otherniceperson@client2.de"])
user_email_address = each.key
}
This is what makes terraform so incredibly powerful! We only have to define this one resource and terraform understands we actually need two invites.
Now with currently around 80 external users in our system, this set would get so long that it would probably violate some data structure laws, I don't know, I am not a lawyer. So we can put that into its own file with the .tfvars
ending.
tf-aad-mandanten.tfvars:
externalClientUsers = [
"niceperson@client.de",
"otherniceperson@client2.de"
]
This also needs a variable definition in our .tf file:
tf-aad-mandanten.tf:
variable "externalClientUsers" {
type = list(string)
}
resource "azuread_invitation" "externalClientUsers" {
for_each = toset(var.externalClientUsers)
user_email_address = each.key
}
Much cleaner! The .tfvars
file needs to be specified on apply or we can rename it to .auto.tfvars
and terraform will look for it, as the name suggests, automatically.
Refining the resource configuration
Now remember the additional steps I was facing? Why do I send our clients an additional email after the invite? Well I want to be sure they can make sense of the invite. However now I can just include the message that needs to be sent to every client when they receive the invite:
tf-aad-mandanten.tf:
variable "externalClientUsers" {
type = list(string)
}
resource "azuread_invitation" "externalClientUsers" {
for_each = toset(var.externalClientUsers)
user_email_address = each.key
message {
body = "Hey Client! Please accept this invite to access our PowerBI-Reports! Sincerely, restlessmodem"
}
}
This works great, the email now includes this message. However the big one line seems a bit messy to me, especially because I would like to actually have a much longer message here to provide some more context. I thought about inserting \n
line breaks, however it turns out terraform easily supports multiline strings.
message {
body = <<EOH
Hey Client!
Please accept this invite to access our PowerBI-Reports!
Sincerely, restlessmodem
EOH
}
I can also make sure I get a copy of every sent out invite to be confident everything went according to plan.
message {
additional_recipients = ["mail@christopherpfister.de"]
body = "<message>"
}
My final problem was that I still needed to inform the client users on where to find our app. The invite just sent them to the Office 365 homepage. Fortunately the redirect url is configurable.
resource "azuread_invitation" "externalClientUsers" {
for_each = toset(var.externalClientUsers)
user_email_address = each.key
redirect_url = "<link to our PowerBI app>"
message {
additional_recipients = ["mail@christopherpfister.de"]
body = "<message>"
}
}
Oh no! Something doesn't work!
Applying the changes fully expecting this to work, I was suddenly greeted with an error. At the same time Azure actually sent out the invite email as confirmed by a notification on my phone at the same time. The error message was:
Error: Failed to patch guest user after creating invitation
What went wrong? After discovering this issue on Github I realised it seems like not only the User.Invite.All
permission was required but also write access to all users!
As we are part of a medium-sized corporation, we are (understandably) not granted such a broad access for a single service account that should not need it.
What do we do now? The user has been invited successfully, however terraform still things it has to do that again. Why? Because it has marked the resource as tainted meaning it is a sick little resource that needs help, in this case help is recreation.
As a solution I suggest a messy fix: We can untaint resources manually, either though the terraform cli, or if we are feeling brave and know there should not be any tainted modules in our configuration
sed -i '' "/\"status\": \"tainted\"\,/d" terraform.tfstate
with one line we can untaint them all.