Another Microsoft Teams Governance Approach – Using Azure AD Identity Governance

Since Microsoft published the Request a Teams App Solution on github ( https://github.com/OfficeDev/microsoft-teams-apps-requestateam ), I stopped all my „ambitions“ to create my own deployment tool for Microsoft Teams, because it´s designed very well and especially the PowerApp-Part is better than I could ever build it. So I concentrated on other Governance topics than the creation of teams.

I´m sure everybody who is reading this article is member/owner of a bunch of teams in various tenants. As a consultant most of the teams I’m a member of, are only relevant for me or a project group for a certain period of time. But because I’m lazy ( and team Owners don’t care about it ), I stay a member in most of the teams I ever was invited to.

The approach I will explain in this blog is about how to handle Team-memberships over a long time. I describe how to get it done, that only persons who have a valid reason or interest on Informations and data shared in a Team, keep the membership. Furthermore, I will show how to use approvals in this process, for example to allow a project owner of a customer to be involved in the approval process for a team in the Tenant of the consultant company.

Also, how can we carry out to treat internal Users and external Guests in different ways. How can we build multi staged approval for Teams participations.

My goal was to build a completely automated solution for keeping the Teams environment nice and clean on the one hand, and on the other hand keep the benefits of the user-interaction and autonomy without long running ITSM Request etc.

I know there are a few third party apps available to get similar Teams Management & Governance tasks managed, but I’m a friend of solving topics with onboard tools. Microsoft has a big zoo of tools and features bundled in Azure & M365 to get this and other tasks done, the key is the centralized API to combine all that great stuff to build your own customized but manageable solution. The main-Features we’re using in this approach are:

  • Power Apps: Frontend for the User to create new Teams and create new partner relations
  • SharePoint: The „database“ (yes, I know) for the solution
  • Power Automate: Flow’s will create and handle approvals
  • Logic Apps: The big brother of PowerAutomate is used to combine all the different components and use the ideal tool for every task in the whole process.
  • Azure Automation: I like to use Powershell Runbooks to speak with the Graph API. Especially when you must use delegated App Permissions it’s much easier to handle. The runbooks are responsible to create the Entitlement-Management
  • Graph: The API that makes this all possible
  • Azure AD: Especially the Identity Governance Features and Azure App Registrations are basics for this approach.

As you can see there are a lot of components involved, and I needed a while to learn which technology I can use for what, but in the end they work together very good.

Because we’re using AAD entitlement management ( https://docs.microsoft.com/en-us/azure/active-directory/governance/entitlement-management-overview ), we have to own AAD P2 Licenses for every user which is consuming it. This fact makes this approach, not a everyone’s solution. Furthermore you have to own a Azure Subscription with some credit to allow the runbooks and Logic Apps to make their job.

Let´s start

This article is very extensive and covers complex scenarios and technologies. You might want to have a short look on the part „What have we done?„. It’s good to get an impression of what the configurations, scripts, accounts etc. we will build are good for. And how the user experience will be like. With that conclusion you can better estimate if a solution like this is feasible for you and your organization.

Install the Request a Team App

If you not already have a solution to govern the creation of teams you should check out the Request-a-Team App which you can find on github.

I will explain all the upcoming steps with regard to the Request-a-team app. But if you’re not using it already, and you do not plan to use either, you can adapt the considerations and grab the scripts I’ve shared here and implement it in your own way.

I will not describe the installation of the App here, because the deployment guide ( https://github.com/OfficeDev/microsoft-teams-apps-requestateam/wiki/Deployment-Guide ) describes how to get it done, and the community is reacting fast, if you run in problems implementing it.

After installing the github solution you can edit the Logic App to enrich the deployment process. I often adjust the notifications methods, apply sensitivity labels or remove Wiki’s from the deployed Team

enriched teams deployment flow

You also might want to edit the imported PowerApp and SharePoint lists to get familiarize yourself with the different components and to customize it for your needs.

In my demo environment I implemented my favorite template centric interpretation of the app where I only offer predefined Templates. We mainly differentiate between persistent and temporary teams. Some of the templates allow guests (Sensitivity Labels were used to get this done), some require a end date to be named to start actions when a project is finished.

AzureAD Module Installation

To be able to add Access Packages etc. via Powershell Runbooks in Azure Automation, we have to add the AzureAD Module to the Automation Account which will be used for for the EntitlementManagement handling. We will use the Automation Account which was created with the „Request-a-Team“ app. Navigate to the automation account „teamsautomate-auto“ in the azure portal. Here go to „Modules gallery“ and search for „AzureAD“. Click on the Module, import it and wait for the success notification.

Build up a Partner Relationship

In our scenario we want to control with which partner organizations we were able to collaborate in Teams. Furthermore we want to be able to approve new team memberships. We will use Connected Organizations to get this done. This Identity Governance Feature allows us to build up a relation between two or more Organizations. So, it´s about guest user handling… Ideally this connected organization is a M365 Tenant. You can define internal and external sponsors for a partner organization, which we will use for membership approvals.

I definied in the Teams Creation Process that you can only create a team which allows guest invitations, if you choose a exiting partner organization (a „Connected Organization“). Only guests from this partner organization are allowed to access the team. The security background for this limitation may be company secrets or the protection of intellectual property. We as partners may have signed a NDA and want to protect us that no unauthorized persons can access the data which we share in this team. We want to protect ourselves against intentional or unintentional non-compliance acting.

We now want to enable partner managers in our company to create new organization relationships to make them available in the Team creation process for a regular user.

So, let’s start nice and easy with adding a few SharePoint Columns:

I’ve created a new SPO List „PartnerOrganizations“ in the same Site that was created with the Request a Team App. Only Partner Managers have access to this SPO List. The List Contains the following columns:

FullName (Single line of Text)
SMTPDomain (Single line of Text)
ShortName (Single line of Text)
Status (Single line of Text)
IsM365Tenant (Yes/No)
InternalSponsors (Single line of Text)
ExternalSponsors (Single line of Text)
TenantID (Single line of Text)
Relation (Single line of Text)
ConnectedOrgID (Single line of Text)
PendingInvitations (Single line of Text)
RecheckPendingInvitations (Yes/No)

You can now decide if the partner managers should use the default list form to add new entrys or whatever. In this example I integrated the form as a seperate Sceen in the Request a Team Power App.

The app ingests the essential data (ShortName, SMTPDomain, InternalSponsors (here static) and ExternalSponsor(s)) into the List. A Power Automate Flow can follow to set the Status Value to „Approved“.
We do now have all Information that we need to build a relation to the connected org.

If you’re not familiar with PowerApps Coding, just insert the essential Values for a new connected organization in the SPO List. For testing just insert these values in the format you can see in the PartnerOrganizations List Screenshot.

Now we want to use a combination of Logic Apps and Azure Automation Powershell Runbooks to create a connected Org out of this list entry.

The Identity Governance stuff is pretty new, thats why we have to use the Beta of the Graph API to automatically create a new connected Org. The Graph Methods we want to use only support delegated Permissions To allow an automated processing we have to create two things in the Azure AD:

  • a „functional“ Account: Create this account with a complex password, exclude it from MFA Requirements and assign a password policy that the password will not expire. Assign the Admin Role „User Admin“ to this Account
  • a App Registration: Create a App Registration and assign the API Permissions (all delegated Microsoft Graph Permissions)
    – EntitlementManagement.ReadWrite.All
    – Group.Read.All
    – User.Invite.All
    – User.Read.All
    and grant admin consent.
    You have to adjust the App Registration Default Client type. Turn On the Switch for „Treat application as a public client“ in the Authentication Settings
    You can follow the screenshots to get it done:

We have to use this account and the app registration in a Azure Automation Runbook, but don’t want to store the credentials etc. in the Runbook itself.
We can use the Azure Automation Variables to store the data in a protected way.
So, lets navigate to the Azure Automation Account (If you have deployed the Request a Team app with the default values you find it if you´re searching in the azure portal for „teamsautomate-auto“).

We navigate to Variables, and add a variable named „DelegatedAppClientID“. Insert the AppID of the App we’ve created before as the value of the automation variable

Now let’s store the user credentials in the „Credentials“ Section of the automation Account

The next step is to create a new Runbook in this Automation Account:

Insert the script I published in my gist here:
https://gist.github.com/J-a-k-o-b/5bd788d12fc6ce1087774136a8497588

If you’ve used the same Names for the Automation Variable and Credentials, you don’t have to edit the Values in Line 32+33.
Don’t forget to save and publish the Runbook after you pasted it in here.

I will explain what the script does later in this section.

Now we have a SPO List which contains the Data for the connected organization and a Azure Automation Runbook which will create this relation. Now we will build a Logic App to start the Runbook when a new list entry was created. The LA will hand over parameters to the Runbook.

Navigate to the Logic Apps Portal. Here you can also use the Subscription and Ressource Group you’re using for the Request a Team App. Create a new Logic App named „LA-CreateConnectedOrganization“ and store it in the Location you prefer.

The flow we compose in this Logic App is pretty simple.
We´re checking the SPO list PartnerOrganization by the value „Approved“ in the Status Column. If a new/modified entry fits to this trigger condition (@equals(triggerBody()?[‚Status‘],’Approved‘)), the flow will start the Azure Automation Runbook. The Logic App will update the SPO list entry when it was the Runbook is finished. You can follow the screenshots to rebuild the flow.

The schema you have to use in the „Parse JSON“ Action is the following:

{
    "properties": {
        "ConnectedOrgID": {
            "type": [
                "string",
                "null"
            ]
        },
        "GuestUsersInvited": {
            "items": {
                "type": [
                    "string",
                    "null"
                ]
            },
            "type": "array"
        },
        "IsM365Tenant": {
            "type": "boolean"
        },
        "Status": {
            "type": [
                "string",
                "null"
            ]
        },
        "TenantID": {
            "type": [
                "string",
                "null"
            ]
        }
    },
    "type": "object"
}

Cool! Now you have finished the first part. What is the result so far?:
1. Partner Managers are able to request a new partner organization
2. The request is filed to the SPO List PartnerOrganizations
3. The LogicApp LA-CreateConnectedOrg checks the SPO List in 1 hour intervalls for new approved entrys
4. The LogicApp starts an Azure Automation Runbook
5. The Azure Automation Runbook creates a Connected Organization with internal sponsors and returns the values to the Logic App
6. The Logic App updates the entries in the SPO List PartnerOrganizations and updates the Status to OrgCreated
7. The new created connected Org can be used in the Request A Team App for new Teams in which the connected Org shall collaborate.

If you not already have reviewed the Powershell Script, here is what it does in detail:
It´s requesting an access token of the functional account which has permissions to create a connected org.
Next it checks if the requested connected Org Domain (Value SMTPDomain in the SPO List) is a M365 Tenant. If not, it does nothing 🙂 – I will care about this in a V2 of this script and update it here.
If the partner Organization is a M365 Tenant it will create a connected organization with the given domain. For this connected organization two Internal Sponsors (Value InternalSponsors from the SPO List) were added to the connected organization.
The external Sponsors (Value ExternalSponsors from the SPO List) must have a valid guest account in our tenant first. So the script is checking if the Guest Account exists.
If not, it will generate a guest invitation and send it out to the External Sponsors. It will return this in the output and the SPO List Entry Value PendingInvitations will contain this addresses.
If the guest accounts for the external Sponsors already exist, the Script will add them as External Sponsors to the connected organization

Puh, that was a lot of stuff… And what’s the benefit so far? To be honest, not so much. But we´ve build the basics for the upcoming steps.
These will result in a controlled way to allow guest participation in a single Team. But you need a bit more patience and preparation.

Finish Partner Relation

As you´ve recognized before we only added external Sponsors to the Connected Organization that were already completely provisioned in our tenant. The reason for that is that we can’t add Sponsors to a connected Org when the Guest Invitation isn’t redeemed.
But because we want to include the external sponsors in the approval process we have to get this finished somehow. One Option would be to redesign the logic App and make it to a long running flow which periodically checks the guest Accounts for there invitation acceptance.
I´ve decided to solve it in another way. Partner Managers should be able to force a check if the guest invitation process is finished. So added a new button in the PowerApp Connected Org Screen to recheck pending invitations

The PowerApp Button will update the SPO List Item in the List PartnerOrganizations, and set the boolean Value „RecheckPendingInvitations“ to true.

You also might want to simulate this PowerApp Integration by just editing the SPOList Item manually.“

To get the check done we again have to build a Logic App and an Azure Automation Runbook

So, navigate to the automation Account „teamsautomate-auto“ again and create a new Runbook „RB-CheckGuestUserStatus“

Insert the Powershell Script I shared in my gist into the Runbook: https://gist.github.com/J-a-k-o-b/4651b301ce628d5c050dc6086e5ba7da
Same here. If you´re using the same Names in the Automation Variables and Credentials, you don´t have to adjust the Script. Don’t forget to save and publish the runbook.

The Logic App is very similar to the one we used to create the connected Organization. We´re checking in intervals if there is a modified list entry where the ReCheckPendingInvitations Value is true (@equals(triggerBody()?[‚RecheckPendingInvitations‘],true)).
We add a compose action to get the PendingInvitations Value into the right format to be interpreted as an array in the automation runbook. Then I start the runbook with parameters and update the list with the results of the runbook.

We have archived the following in this section:
1. Partner Managers are able to request a check of the external sponsor status
2. The Trigger is set in the SPO List PartnerOrganizations
3. The LogicApp LA-CreateConnectedOrg checks the SPO List in 1 hour intervalls for entry´s it has to process
4. The LogicApp starts an Azure Automation Runbook
5. The Azure Automation Runbook checks the Status of the guest user, and adds the user if the invitation was redeemed to the Connected Organization
6. The Logic App updates the entries in the SPO List PartnerOrganizations: PendindInvitations and sets the ReckeckPendingInvitations to false

The script which is feeded with the Connected Org Id and the Pending Guest Accounts checks if the guest user accounts were in the Status „Accepted“. If the Guest User Accounts are in this status it will add the Guest Users to the Connected Org as External Sponsors. If not it will return the Accounts which still waiting for an acceptance.

So, now we are able to create connected organizations. Internal and External Sponsors were assigned. The customer/partner relation is ready to be used in the teams provisioning process.

Now it´s getting serious – Teams Requests & Access Packages

In this section we will extend the Team creation process to set up a Access Package for every new Team. The Access Policy with its ressources and policies, bound to a Team, will conclude this solution.

You have to start by adding the Customer/Partner Choice to the Team Information context in the Request a Team App.

To implement that you have to add a new Column to the SPO List „Teams Requests“ before. This column will contain the Connected Org ID of the chosen Partner/Customer:
PrimaryConnectedOrgID (Single line of Text)
Furthermore please add a Column:
GuestSettings (Single line of Text)
We´re using this second column to allow a separate handling of non-M365 Tenant Partners.
The last one is:
AccessPackageURI (Single Line of Text)
This column will contain the link to the access Package which will be build for a Team and allows easy access to this information in different use cases.

Now we have to edit the Power App: I´ve added dropdown field „ddCustomerShortName“ to the App which only shows the partner organizations which have the Status-Value „OrgCreated“. From the chosen Org I store the OrganizationID in a hidden Text Field „txtConnectedOrgID“.
Additionally I´ve modified the existing Patch Statement on the Next Button „btnNext_TeamInformation“ to store the ConnectedOrgID in the new column we created before.
You can again use the screenshots to rebuild it. (You can follow my instructions here, or ask someone how to do it right like an expert 😉)

I’ve also added the Customer ShortName to the Teams Name as prefix. So you easily enrich your Naming Conventions with standardized Customer Names by the way.

Additionally fill the Column GuestSettings with the value „GuestInvitationsAllowed“ or „GuestInvitationsAllowed“. You can hardcode it in the patch statement, or e.g. set it depending the chosen Template.

When you finished that, all new Teams Requests, which were created to collaborate with a Customer/Partner from now on will contain Values in the Columns GuestSettings and PrimaryConnectedOrgID (if the partner, has a M365 Tenant).

Now it is time now to create another Runbook. Navigate to the automation account and create a PowerShell Runbook „RG-CreateAccessPackage“

Insert the Powershell Script I shared in my gist into the Runbook:
https://gist.github.com/J-a-k-o-b/96d3fb206b60dc103f5a3fb27af3a181
Same here. If you´re using the same Names in the Automation Variables and Credentials, you don´t have to adjust the Script. Don’t forget to save and publish the runbook.

Let’s proceed in Logic Apps. This time we have to adjust the existing LA „ProcessTeamRequest“ which comes with the Request-a-Team App.

Start with initializing a new integer Variable for the GuestInvitations-Switch (int and no boolean because I didn’t get it done to hand over a boolean value from LogicApps to Azure Automation):

Then add a switch condition to set the variable to „1“ if the SPO Item Value is „GuestInvitationsAllowed“:

Now, add a Scope for the Entitlement Management handling somewhen after the „Create Team“ Scope, and start the Azure automation Runbook from here. Fill the parameters with TeamID (Output from the „Parse created team JSON“ Action), the Values for Alias and the connected organization from the SharePoint Trigger, and the GuestInvitationEnabled defined in the var we created before.
The last step in the Scope is to Parse the output of the runbook to make use of it in the next step.

Use this Schema to Parse the output:

{
    "properties": {
        "AccessPackageID": {
            "type": [
                "string",
                "null"
            ]
        },
        "AccessPackageURI": {
            "type": [
                "string",
                "null"
            ]
        }
    },
    "type": "object"
}

Update the SPO List with the Access Package URL:

The last step is to share the Access Package Link with the Team Owner. We can use the Flow Bot chat to do that.

What we developed in this section:
1. Users can request a Team with a Partner/Customer Context as used from the Request-a-Team PowerApp
2. This updates the SPO List Item for the Teams Request with the connectedOrgID
3. When the Teams Request was approved the Logic App creates the Team as usual.
4. When the Team was created the action we defined starts a Azure Automation Runbook with Parameters.
5. The Runbook creates a catalog, an access package and one or more access policies for internal and external Team Members.
6. The Logic App Upates the SPO List
7. The Team Owners will get the Access Package URL to share it.

The Runbook we started does the following: It´s generating an access token. As the delegated user it starts with creating an access Package Catalog. The Teams we create in the same logic app will be assigned to the Catalog. Now it generates the Access Package itself, which connects the Access Package Catalog with Policies. Furthermore we generate at least one policy and assign it to the Access Package. The first policy will handle internal memberships in the team. If the team will be used to work with Guests, a second policy will be generated which handles the Guest Membership in the created Team.

Now, go fetch a coffee and prepare yourself for the next 20 steps… Naa, just kidding, grab a drink and celebrate that you finished it! While getting tipsy you can review what we gained.

What have we done?

Now that we have created so many runbooks, Logic Apps etc., how will this optimize our Teams Governance?
With every teams you generate now over the „Request-a-Team“ App an Access Package is generated. This Access Packages allow to govern the memberships.
The Policies we configured aim at two different Team Memberships:

Internal Users: Every internal user is able to request the Team Membership. The membership is granted without any approval. You can easily adjust the Azure Automation Script to change this setting. E.g. define that an approval of the Team Owner is needed, or that only specific Group Members should be able to access without approval.

Guests: Every User from the M365 Tenant of the Connected Organization can request the Team Membership. The external Sponsor has to approve the membership, the Team Owners are configured as Backup Approver. You can easily adjust this approval e.g. to involve an internal Sponsor or the Team owner to be the primary approver and make a two stage approval out of this.
If the Connected Organization is not an M365 Tenant, the Access Policy for Guests is not bound to an connected Organization. Every existing Guest User can request the Team Membership. The Team Owners have to approve it.

For both cases (Internal Users / Guest) an expiration of the group membership is configured. After 365 days (change it in the RB-CreateAccessPackage to any value) every member has to validate that he still wants / must have access to the Team. He will be able to extend it. You can also easily adjust the script to involve a new approval in the extension.

Users from your and the connected organization can easily access a team without an active invitation, with a click on an URL or visiting the MyAccess Portal (https://myAccess.microsoft.com) where all Access Packages which they are eligible for, and which are not hidden, are listed. The Membership Requests are handled according to the related access package policy.

The Team Owner can also actively share the Access Link to collect his members.

So, team owners and other people don´t have to validate any longer that only relevant people are involved in the team, and the user itself gets an automated way of keeping his Teams-Environments cleaner.

If you want you can also combine the expiry with an access review and let the users themselves or e.g. the external Sponsor/Team Owners check in intervals which Guest and Members are still valid.

Extend the solution with defining your own access policies and assign them related to the team type.

Look and Feel for users

This Gif shows the Process Look and Feel for the participating users:

What’s happening here:

  1. The Partner Manager of UCC Requests a new Partner (Contoso) to collaborate with, he´s naming a responsible Person AdeleV @ Contoso to give him the role as external Sponsor (Approver)
  2. With finishing that the AdeleV gets an invitation to create a guest Account @UCC.
  3. When AdeleV has accepted the invitation the Partner Manager forces to check the pending invitation again. The Pending Invitation will be resolved and the Partner Relation is established
  4. Now somebody at UCC requests a new Project Teams to collaborate with Contoso
  5. When the Logic App processed the Request the Teams Owner gets a Link for the created Access Package
  6. The Team Owner can share the link internal and with the related Partner. The link is the same for both. The Team Owner hands over the link to the External Sponsor.
  7. The External Sponsor shares the link with his coworkers @ Contoso. Project Participiants request the teams membership by clicking on the link and providing a justification
  8. The External Sponsor is getting an info that he has to approve the requests
  9. The Members of Contoso can now participate in the UCC Team

365 days after the users (internal & guests) requested access to the team, the expiration Process starts and the members were asked if they still need access to the team. If they don’t extend the access, they will be kicked out of the team.

Additional Considerations

I like this governance possibility very much. It’s pretty neat how the Teams Service can benefit from Identity Governance. I´m looking forward to implement similar business workflows at other customers. The next expansion stages on my personal List are:

  • Allow the automated addition of partner relations to an existing one to allow multi tenant Team Collaboration & Governance Scenarios, included in the Partner Management Surface of the Request-a-Team App.
  • Build more templates for Access Package Policies for various use cases and let the Partner Manager / Team Owner Role participate in decisions like the Access Expiration time or approval Stages
  • Optimize the non-M365 Tenant Partner Relation Policys. How does it work with OneTime Passwords etc.
  • Migrate from SPO to a relational database
  • Allow additional domains per connected organization
  • to be continued… so much possibilities, so little time

All the Logic Apps are designed to run by a schedule. So it can take a while till a partner manager can make use of the new relation. It’s possible to change this by using HTTP Requests the PowerApp and Webhooks as the trigger of the Logic Apps. But I haven’t fully understood right now what’s included in the PowerApp License which is contained in the regular M365 License. From Power Automate i know that the HTTP Request is a premium feature and that would make the solution overall very expensive. Thats why I did not implemented it on this way. Maybe one of you knows how it works and can give me feedback.

Thank you

…for reading this article till the end! I tried to keep it short and thought about splitting it up, but I think that having all the related steps together, described in-depth makes more sense here.

Please leave your comment here or contact me via twitter if you have questions about the topics I described here, or for general feedback.

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.