Vue normale

Il y a de nouveaux articles disponibles, cliquez pour rafraîchir la page.
À partir d’avant-hierREgarding 365 - Medium

Attempting to explain Teams shared channels to end users

Shared channels in Microsoft Teams are not exactly new, but at the same time relatively recent. Consistently amongst organisations and IT Pros the resounding commentary I’ve heard is that they are too confusing and have been disabled.

The problem is that shared channels are a mix of experiences borrowed from private channels and guest access, with extra layers of control wrapped around them.

My biggest frustration with the approach of disabling shared channels, is that I wished people did the same for private channels and guest access in the first place. Not because I think features should be disabled, but more so because I believe any feature before being made available to end-users should come with (in no particular order of importance):

  • Governance to ensure the organisation remains compliant.
  • Support to ensure that users don’t get lost and can get help if they need it.
  • Training to ensure that they actually understand the why/which/what/when/who/how of the particular feature.

Unfortunately, this isn’t reality, and both organisations and users chastise each other or Microsoft for not doing things properly. Sometimes it’s not their actual faults, sometimes it actually is.

But anyway, back to shared channels.

More than meets the eye

Before shared channels was released, I shared a quiz asking what people knew about the features and restrictions. The features I asked about were merely to do with basic functionality of shared channels — not even factoring in users from external tenants.

In total there were 11 questions with a total of 20 points available. Out of 260 responses, the average score was only 10.6 — with many people responding to me on social media that the quiz was too hard. And most of the respondents were IT professionals — not even end-users, so imagine how much more challenging it can be for them.

(You can view the results here.)

Two different experiences

With shared channels in Microsoft Teams, there are effectively two different experiences that eventuate for users.

The first, is shared channels for internal use. This, is relatively innocuous and serves as another choice of whether to use an existing Team, a new Team, a private channel, a group chat, etc.

The second, is external access — where you can share channels with a user in another organisation, a Team in another organisation, and vice versa. This is where things become not just a little bit more challenging, but A LOT more challenging — if you want to do things somewhat properly that is.

Two different stories

Here’s the first challenge with shared channels. By default in Microsoft Teams, shared channels are enabled for all users, including the ability for internal users to invite external users as well as for them to join shared channels in other organisations.

The problem is that the exact opposite is configured as a default in Azure Active Directory under cross-tenant access settings.

What this results in, is a user attempting to perform a function that the application says is allowed, only to find out upon performing the action that it’s not allowed — and unfortunately with relatively vague reasons in the error message.

What a user sees

What a user thinks

Going down the rabbit hole

The problem is that this is just the start of our experience.

The relationship

The reasons why a user may not be able to invite an external user to a channel include:

  • Both organisations set to block by default (Azure AD)
  • Host organisation set to block by default (Azure AD)
  • Host organisation set to block inbound (Azure AD)
  • External organisation set to block by default (Azure AD)
  • External organisation set to block outbound (Azure AD)
  • Inviter not allowed to invite external users (Teams)
  • Inviter not allowed to invite external users (Azure AD)
  • External user not allowed to join external channels (Teams)
  • External user not allowed to join external channels (Azure AD)

The problem is that it could be any permutation of these, and then there’s the added layer of being able to control the various aspects at a more granular level — by security group or individual user.

For example, you could have a policy in Teams that allows members of a particular security group to invite external users but have not set up the cross-tenant relationship in Azure AD.

Or you could have done all your bits correctly, but the other organisation hasn’t done theirs correctly.

The challenge is that before end-users can collaborate in a shared channel, the IT teams need to collaborate on setting up their relationship between organisations first.

The level of trust

And in order for external users to be able to actually upload files into a shared channel (separate from conversations), you need to enable trust settings between organisations.

Here’s the thing with trust settings… what if the external organisation has weak MFA or compliance policies?

What if they *shudder* allow SMS verification codes?

What if they allow jailbroken devices to be marked as compliant?

Getting personal

And if you want to get granular about controlling who from another organisation is allowed to come into your tenant shared channels, you can restrict this to specific users and groups:

But while that seems straightforward, wait until you get to the next screen:

When I think of an end-user trying to obtain this information from another end-user, and even having to use the term “object ID”, this is what I imagine:

Some months ago I was creating guidance for an organisation who wanted to start using shared channels, and when trying to come up with some documentation that could be provided to end users, and this is how I felt trying to explain it all:

In the end, I built a Power App to simplify the experience.

Teams Shared Channel Navigator

Yes, you read that right — it was easier for me to build an app than it was to explain the various outcomes and scenarios. Because working with shared channels, when done properly, is nothing short of a butterfly effect.

Not only did I build an app, but I also built associated workflows, AND found and worked out how to work with an undocumented/unsupported Microsoft API.

What it does

Instead of trying to explain the myriad of potential reasons why working with a shared channel may not be possible, this app simply shows the end user what they individually can or can’t do, and what is possible for the organisation.

How it works

When initially loading the app, it runs a workflow which calls an undocumented (and unsupported) Microsoft Teams API which lets us see what channel policies exist, and are assigned to the user.

(Initially I wanted to use an Azure Runbook with a PowerShell script to do it the supported way, but unfortunately the cmdlets require do not work with a combination of managed identities / service principals / application permissions — so there was no way to get the data in a secured manner.)

After a few seconds when the policies have been retrieved, the “Please wait…” text switches over to a clickable “Continue” button.

On the next screen, the application shows what the user can do based on the Microsoft Teams policies retrieved in the prior step. This is a crucial step, because not every user may have the ability to join shared channels in other organisations, or invite external people into internally hosted shared channels.

If both of those options are unavailable to the user, the text at the bottom is not visible and the “Search for an organisation” button does not appear.

If the user is able to perform either function and presses the “Search for an organisation” button, they are taken to the next screen which gives them to the option to see if a cross-tenant relationship has been established.

IMPORTANT: We can only check for our side of the process. If our side is set up correctly but the other side is not, we do not have the ability to check for that.

The user can enter either an email address of someone they wish to invite, or just the domain name of the organisation.

This calls a Microsoft Graph API endpoint which both checks to see if the partner organisation relationship has been established on our side, as well as the organisation name (as configured in their Azure Active Directory / Microsoft 365 tenant).

Based on the result it may return a negative and explain that a relationship has not been configured.

Or, if a relationship has been configured, it will identify the nature of the relationship is (ie. inbound or outbound).

If the relationship with the partner supports outbound access (i.e. the ability for users in our tenant to access channels in their tenant), the app then displays whether the user performing the search has the ability to access shared channels in the partner tenant.

This is an important step as the outbound relationship may be set to “All Users”, or may be restricted to specific individual users or security groups.

(In the below example I’ve decided to be a bit silly and show an animated GIF of Oprah Winfrey, however if the user does not have permission, it would return a “Computer says no” animated GIF from Little Britain.)

Summary

While shared channels do make it considerably easier for organisations to collaborate with each other, it’s the initial step of establishing that collaboration which can be somewhat challenging.

This solution allows users to perform some self-service discovery before contacting service desk teams to simply say “it doesn’t work” and have to begin the troubleshooting process from there.

This can reduce both the amount of frustration and time spent by all parties to get to the desired outcome of establishing collaboration between organisations and people.

And if you’re after the code, check my GitHub repository (https://github.com/loryanstrant/) as I’ll be explaining the back-end components and uploading the solution soon.

Originally published at Loryan Strant, Microsoft 365 MVP.


Attempting to explain Teams shared channels to end users was originally published in REgarding 365 on Medium, where people are continuing the conversation by highlighting and responding to this story.

Reporting on Teams shared channels & users

One of the core principles I live my life by is to be informed, to know something fully before I make a decision.

Maybe it’s because I’m Autistic, maybe it’s just common sense, maybe it’s how I was raised, whatever — let’s not get philosophical here.

Virtually every organisation I’ve spoken to has said they’ve disabled shared channels in Microsoft Teams until they know more about it, and some have actually engaged me to help them with a framework for using it.

There are a number of challenges with shared channels, and that comes from the fact it’s trying to provide more granularity than guest access currently does, while at the same time trying to be flexible and easy to use — and we know you generally can’t have all of these together.

For those that have chosen to allow access to external channels and users, how do they know where their people are — and who is inside their tenant?

While there is some reporting in the Teams Admin Center (TAC), we have to dig into each Team, then each shared channel, and only then we can we find the external users and where they came from.

Thankfully we have a few endpoints in Microsoft Graph we can use to build some of our own reporting, using Power Automate to call the Graph endpoints and Power BI to visualise the data.

You can store the data in SharePoint Lists, Dataverse tables, or whatever you like.

There are a few aspects to capture in order to get a full picture:

  • Organisations that have been configured in for cross-tenant access in Azure AD
  • Shared channels attached to Teams
  • Internal users accessing shared channels in other organisations
  • Users from other organisations accessing shared channels in your tenant
  • Identifying what internal shared channels the external users are in

DISCLAIMER: Some of these calls are using beta endpoints and are technically not supported — so use at your own peril/pleasure.

Pre-requisites

Before we can create any workflows that call Graph, you will need an Azure AD App Registration with the following permissions:

  • Channel.ReadBasic.All
  • ChannelMember.Read.All
  • CrossTenantInformation.ReadBasic.All
  • CrossTenantUserProfileSharing.Read.All
  • Directory.Read.All
  • Policy.Read.All

You’ll also need to already have (or build) a listing of:

  • Users — ID & display name
  • Teams — ID & display name

Key workflows

In all of my workflows, I use a variable called “GraphPath” and sometimes “GraphPathSuffix” to help make the design of them more scalable and repeatable, so where applicable I will include those variables.

List organisations configured for cross-tenant access

Here we will use two key requests:

First, let’s list all the partners we have defined in Azure AD:

As this only returns the tenant ID, we need to run a second call against each of them to capture their organisation name and primary domain:

The data we’ll record looks like this:

And in Power BI our output looks like this:

Listing shared channels in your tenant

Ideally you already have a list of Teams in your tenant stored somewhere, if you don’t — you’ll need it for this next step.

Here we will use one key request:

The two parameters we’re using for our call are:

We’re then going to use both of these together with the Group ID of the Teams to see what shared channels exist:

For each channel we find, we’ll store these values:

Using a relationship between the “tenantid” value in this table and the same value in the table from the previous step we can start to build this out:

Where the values are blank in the “Remote Tenant” column, this is because the tenant is not external (i.e. the channel is hosted in our tenant).

List internal users accessing shared channels in other organisations

Here we will use two key requests:

Let’s set our Graph query elements:

The first step is to simply find who in our tenant is accessing external tenants, with the only result being their ID:

For each of those users, we now need to extrapolate and find the related tenants:

Then for each tenant returned, we only need to store the user ID and external tenant ID:

Again, using a relationship between the “tenantid” value in this table and the same value in the table from the previous step we can start to build this out:

List users from other organisations accessing shared channels in your tenant

Here we will use two key requests:

To see who from other organisations is inside of our tenant, our query is this:

And our action is simple:

Then for each result we find, we need to store the external user’s ID, display name, and home tenant ID:

Again, using a relationship between the “homeTenantId” value in this table and the same value in the table from the previous step we can start to build this out:

Connecting the dots

List which external users are in which shared channels

As we already have a list of shared channels stored in a table somewhere (in my case, it’s in Dataverse), we can now run a query against them.

Using this, we can now go beyond just having a listing of knowing which external users in are in shared channels, but knowing which users are in which channels themselves.

In my query to list those channels, I’ve used the existing “TenantID” variable I declared in my workflow to use with the Azure AD App Registration, so this will filter only channels that are in our tenant:

For each channel we want to list the members:

We again use our tenant ID to filter out any members who are internal, and I’ve also put a condition in place to only proceed if the resulting list has anything in it by using length(body(‘Filter_array_-_only_include_external_users’)):

For any users that do exist, we now record them in our table:

Over in Power BI we’ll establish a relationship between the Channel ID of the shared channels table and the Channel ID from this table (along with some filtering to remove channels where the “User IDs” column is empty), as well as the User ID from this table and the previous listing to get the following result:

Now, if you’re wondering why the list of external users is smaller in this list than in the previous list, it’s because there are effectively two components of the external users that exist:

  • Their inbound user profile
  • Their membership in a channel

Through the course of creating this solution, what I’ve discovered is that if you invite an external user to a shared channel, but they don’t accept the invitation — they will not show up as members of the channel.

It’s for this reason we need to maintain both lists of data (which could be addressed through better Power BI reporting skills than what I possess) — to understand where external users are in your tenant, and where they aren’t. This is the same as what we must do for Azure AD B2B guest users who may exist as an object in your directory, but not actually be a member of a M365 Group or Team (for any number of reasons, such as the Team no longer existing, they were removed by an owner, etc.).

Summary

As I started this blog post saying, it’s important to know what we are working with before we make decisions, as well as to provide troubleshooting and support in scenarios where things aren’t working the way we expect.

While a lot of what I’ve shown is possible in PowerShell, my preference is to do this using Power Automate, HTTP calls to Microsoft Graph, store the data in Dataverse tables, and report them with Power BI as it provides me with a solution that can continually operate without human intervention, as well as storing and displaying the data in a way that stakeholders and decision makers can access easily.

You may have reached this point and be wondering where the code for all this is, as I often publish the components on GitHub. As some of this was worked out during the course of writing this blog post, the workflows are not exactly in the most effective structure they could be.

Once I’ve cleaned them up, I’ll publish them on GitHub and update this blog post. For now, I’ve hopefully provided enough information for you to start building out your own reports.

Originally published at Loryan Strant, Microsoft 365 MVP.


Reporting on Teams shared channels & users was originally published in REgarding 365 on Medium, where people are continuing the conversation by highlighting and responding to this story.

Creating a SharePoint page using Microsoft Graph API and Power Automate

Thanks to the workflow that notifies me of updates to the Microsoft Graph API, I saw a new addition to the list: the sitePage resource type.

This is exciting for me, as I currently have some workflows that distribute SharePoint pages to various sites both within our own tenant, as well as client tenants. Currently these are triggered by a page being published in a central location, with specific information used as trigger conditions.

What’s annoying about this scenario is that I need to create connectors in Power Automate to the client tenant using an account in their environment. It also means I need a workflow per client (to keep it clean).

Now with the addition of the sitePage resource type in Microsoft Graph, I can make this work programmatically across any number of clients — all from a single workflow.

WARNING: This is a beta feature at present, so don’t use it for production systems unless you’re find to accept the risks.

Requirements

The requirements of this are fairly simple. We need:

  • An app registration in Azure AD that has the “Sites.ReadWrite.All” application permission added
  • A repository where the details are stored, including:
  • Client name
  • Tenant ID
  • App/Client ID
  • Secret
  • SharePoint site ID

Now, we could use a different way to authenticate, and we could also use an action to perform a search in the tenant to find the relevant site by name or URL, but if we’ve got that — then it’s not exactly difficult to get the SharePoint site ID and store it in our repository.

For the purposes of this, I’m going to store it in a SharePoint list:

Workflow

At a high-level, my workflow is quite simple:

In my specific scenario, all the workflow is doing is publishing a page with an embedded video, as part of a program of regular content I create for clients. So all I need to provide is a page title and the URL suffix from the embed code.

The next step of the workflow takes my page title, and turns it into a file name:

The code used here is:

concat(replace(triggerBody()['text'],' ','-'),'.aspx')

From here, we’re now ready to retrieve all the sites we want to apply this to:

Within our Apply to Each, we have three steps:

  1. Create the page
  2. Parse the JSON of the page creation
  3. Publish the page, using the ID from step 2

(If you’re comfortable with extracting the page ID value directly from the results of step 1, then you don’t need the Parse JSON action.)

In the page creation action, I’m creating a very simple page that only has a single embed web part on it, and I’m passing variables from both the trigger as well as the Get Items action.

The Parse JSON is relatively straightforward:

And for the final step we hit publish on the page:

And that’s it! We have a simple page published in each tenant listed, with the same content.

If you want something more glamorous, refer to the sitePage resource type page to get a breakdown of the structure of the body of the content.

Appendix

Here’s the full details of the body of the page creation and Parse JSON actions.

Create page

{
"name": "@{outputs('Compose_-_replace_spaces_with_hyphens_and_add_file_extension')}",
"title": "@{triggerBody()['text']}",
"pageLayout": "article",
"promotionKind": "newsPost",
"showComments": false,
"showRecommendedPages": false,
"titleArea": {
"enableGradientEffect": true,
"imageWebUrl": "/_layouts/15/images/sleektemplateimagetile.jpg",
"layout": "plain",
"showAuthor": false,
"showPublishedDate": true,
"showTextBlockAboveTitle": false,
"textAboveTitle": "",
"textAlignment": "left",
"imageSourceType": 2,
"title": "@{triggerBody()['text']}"
},
"canvasLayout": {
"horizontalSections": [
{
"layout": "oneColumn",
"id": "1",
"emphasis": "none",
"columns": [
{
"id": "1",
"webparts": [
{
"id": "669d4d75-eca0-4e8b-95d7-2e765dd4859a",
"webPartType": "490d7c76-1824-45b2-9de3-676421c997fa",
"data": {
"audiences": [],
"dataVersion": "1.2",
"description": "Embed content from other sites such as Sway, YouTube, Vimeo, and more",
"title": "Embed",
"properties": {
"embedCode": "<iframe src=\"https://player.vimeo.com/video/@{triggerBody()['text_2']}\" width=\"640\" height=\"360\" frameborder=\"0\" allow=\"autoplay; fullscreen; picture-in-picture\" allowfullscreen=\"\"></iframe>",
"cachedEmbedCode": "<iframe src=\"https://player.vimeo.com/video/@{triggerBody()['text_2']}\" width=\"640\" height=\"360\" frameborder=\"0\" allow=\"autoplay; fullscreen; picture-in-picture\" allowfullscreen=\"\"></iframe>",
"shouldScaleWidth": true,
"thumbnailUrl": "",
"cachedEmbedCodeThumbnail": ""
},
"serverProcessedContent": {
"imageSources": [
{
"key": "imageSource",
"value": "/_LAYOUTS/IMAGES/VISUALTEMPLATEIMAGE1.JPG"
}
]
}
}
}
]
}
]
}
]
}
}

Parse JSON

{
"type": "object",
"properties": {
"@@odata.context": {
"type": "string"
},
"@@odata.etag": {
"type": "string"
},
"eTag": {
"type": "string"
},
"id": {
"type": "string"
},
"lastModifiedDateTime": {
"type": "string"
},
"name": {
"type": "string"
},
"webUrl": {
"type": "string"
},
"title": {
"type": "string"
},
"pageLayout": {
"type": "string"
},
"thumbnailWebUrl": {
"type": "string"
},
"promotionKind": {
"type": "string"
},
"showComments": {
"type": "boolean"
},
"showRecommendedPages": {
"type": "boolean"
},
"createdBy": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
}
}
}
}
},
"lastModifiedBy": {
"type": "object",
"properties": {
"user": {
"type": "object",
"properties": {
"displayName": {
"type": "string"
}
}
}
}
},
"parentReference": {
"type": "object",
"properties": {
"siteId": {
"type": "string"
}
}
},
"contentType": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
}
}
},
"publishingState": {
"type": "object",
"properties": {
"level": {
"type": "string"
},
"versionId": {
"type": "string"
}
}
},
"reactions": {
"type": "object",
"properties": {}
},
"titleArea": {
"type": "object",
"properties": {
"enableGradientEffect": {
"type": "boolean"
},
"imageWebUrl": {
"type": "string"
},
"layout": {
"type": "string"
},
"showAuthor": {
"type": "boolean"
},
"showPublishedDate": {
"type": "boolean"
},
"showTextBlockAboveTitle": {
"type": "boolean"
},
"textAboveTitle": {
"type": "string"
},
"textAlignment": {
"type": "string"
},
"title": {
"type": "string"
},
"authors@odata.type": {
"type": "string"
},
"authors": {
"type": "array"
},
"authorByline@odata.type": {
"type": "string"
},
"authorByline": {
"type": "array"
},
"imageSourceType": {
"type": "integer"
},
"serverProcessedContent": {
"type": "object",
"properties": {
"imageSources": {
"type": "array",
"items": {
"type": "object",
"properties": {
"key": {
"type": "string"
},
"value": {
"type": "string"
}
},
"required": [
"key",
"value"
]
}
}
}
}
}
}
}
}

Originally published at Loryan Strant, Microsoft 365 MVP.


Creating a SharePoint page using Microsoft Graph API and Power Automate was originally published in REgarding 365 on Medium, where people are continuing the conversation by highlighting and responding to this story.

❌
❌