Checking Microsoft 365 Group resources via the Graph API

Ever since the release of Office 365 Groups (now Microsoft 365 Groups), the marketing and product folks at Microsoft have been nagging customers to switch to them, while blissfully ignoring most feedback about potential issues and concerns customers had. In effect, many organizations are now facing challenges dealing with the Group sprawl, and the lack of governance and reporting tools definitely plays a great role in that.

One common question/request is around the lines of “give me a report of which groups are being used, and for what purposes”. Fairly reasonable request, yet no easy way to answer it, as Microsoft hasn’t provided any tools for the task. In fact, the most meaningful way to report on this remains custom scripts, such as this one Tony Redmond has put together, combining different bits and pieces from across the service. While the script does help in reporting group activity and figuring out how the users within your organization are actually leveraging the group(s) functionality, a simpler way to answer the latter question is very much needed. And now, it seems Microsoft is finally doing something meaningful about it, or at least I hope so.

Enter the recently introduced endpoint resource type for the Graph API. Here’s how Microsoft describes it:

Endpoints represent URLs for resources associated with an entity. For example, when a new Microsoft 365 group is created, additional resources are also created as part of the Microsoft 365 group. These include things like a group mailbox for conversations and a group OneDrive folder for documents and files. Further information about these Microsoft 365 group resources, including their associated resource URLs can now be read using the endpoints navigation on the group resource-type. This allows applications to understand these resources, and even embed the resource URL experiences in their own experiences.

Sounds useful, and hopefully it will actually be supported this time around, unlike previous iterations of similar functionality (the resourceProvisioningOptions or creationOptions properties). For the time being though, it’s fairly useless though, as we will see in a second. But here’s hoping!

So, how do we get this new goodness? In a nutshell, one needs to query the /group/{id}/endpoints endpoint… not a pun. This new endpoint is only available under /beta currently, so it goes without saying that things might change before release. In any case, here’s an example of fetching a group’s endpoints via the Graph explorer:

Three useful pieces of information are presented:

  • capability – with values such as Messages (Exchange), Team Collaboration (Teams), Conversations (Yammer)
  • provider – the associated service (Exchange, Microsoft Teams, Yammer)
  • uri – link to the corresponding group resource

So it all sounds good in theory, but in practice the current implementation only seems to return the Team Collaboration and Conversation capabilities, with no trace whatsoever of anything Exchange, SharePoint, Planner, Intune or any other workloads supported by Groups. Effectively, the data obtained from this endpoint is pretty much the same as what’s currently exposed via resourceProvisioningOptions/creationOptions. Of course, this might simply represent the earliest stages of the implementation, so we’ll definitely keep an eye on this functionality.

The other bad news is that we cannot use the $expand operator currently, so if we want to gather this value for all Groups in the tenant, we will have to make a separate call against each group’s endpoints endpoint (yeah!). Here’s a quick and dirty PowerShell snippet that does just that (authentication not included, handle that on your own):

$uri = "https://graph.microsoft.com/v1.0/groups?`$filter=groupTypes/any(c:c+eq+'Unified')"
$result = Invoke-WebRequest -Headers $AuthHeader1 -Uri $uri
$result = ($result.Content | ConvertFrom-Json).Value

foreach ($group in $result) {
$prop = Invoke-WebRequest -Headers $AuthHeader1 -Uri "https://graph.microsoft.com/beta/groups/$($group.id)/endpoints"
$prop = ($prop.Content | ConvertFrom-Json).Value
$group | Add-Member -MemberType NoteProperty -Name "Capabilities" -Value ($prop.capability -join ",")
$group | Add-Member -MemberType NoteProperty -Name "Providers" -Value ($prop.providerName -join ",")
}
$result | ft displayName,resourceProvisioningOptions,creationOptions,Capabilities,Providers

Obviously, this is just a sample code, with no error handling or anything, so use at your own risk, or modify it to best fit your needs. Here’s how the expected output should look like:

Not too exciting, as only the Team-ified Groups and the single Yammer one I have in my tenant return values for the capability and providerName properties, but hopefully this will change in the future and we will be able to obtain a full list of resources enabled for a given Group, with the corresponding URIs. And who knows, maybe even a way to toggle a specific resource on/off?

Posted in Azure AD, Graph API, Microsoft 365, Office 365 | Leave a comment

Did you know: Search-Mailbox adjusting permissions on target folder

While browsing the Unified audit log in search for events related to some tests I performed the other day, I noticed something interesting. Namely, the “copy search results” workflow initiated by good old Search-Mailbox cmdlet took care of stripping folder-level permissions for people that might have ended up having access to the TargetFolder, where the result of the search was being stored. How very nice!

Here’s the scenario in short. I did some tests with moderation, and wanted to get my hands on the actual messages exchanged between the user and the system mailbox facilitating the moderation functionality. While you can easily grant yourself full access permissions to said mailbox, the messages in question are hard-deleted and can only be found in the Purges folder, to which regular clients do not have access. Enter Search-Mailbox. A quick search allowed me to target the messages in question and save a copy to another mailbox, where I could easily examine them. Which is exactly what I did.

Some time later I was checking the unified audit log for any events that might relate to the moderation tests performed earlier, and the following entry caught my attention:

Since I’m pretty much the only user within my tenant who can play with permissions and I certainly didn’t perform that action, I needed to understand what’s happening here. Interestingly, the Logon User Sid value above did match my user account, however the Client Info String and IP address values give us a clue that the action might be performed server-side, plus External Access is marked as true. Expanding the event details shows additional clues, in particular details about which particular permissions were modified:

Since the “Moderation” folder was the one I designated as value for the -TargetFolder parameter when running the Search-Mailbox cmdlet, the mystery was solved. What in fact was happening is stripping any folder-level permissions that might be inherited from the root folder of the mailbox, in effect ensuring that only users with Full access permissions will be able to access the content copied via the Search-Mailbox cmdlet. Just to confirm this, I checked the root folder permission entries on said mailbox:

Get-MailboxFolderPermission sharednew | ft -AutoSize

FolderName User AccessRights SharingPermissionFlags
---------- ---- ------------ ----------------------
Top of Information Store Default {None}
Top of Information Store Anonymous {None}
Top of Information Store shared {Reviewer}
Top of Information Store Pesho {FolderVisible}

There you have it, two entries in addition to the Default and Anonymous ones. In fact, those two entries are the exact ones that were stripped, as they are effectively inherited from the root upon creation of any new folder, be it client- or server-side. Thus, the good folks at Microsoft have ensured that such permissions will not interfere and potentially allow access to people that should not have it in the first place.

I imagine this behavior is not something new, it’s just something I haven’t noticed before. I do find it useful though, and decided to share it as an example of a nice finishing touch to an already very useful functionality. Oh, the good old days where products were being coded by people that actually knew them in and out 🙂

And yeah, I’m not actually using a Discovery mailbox here, but that only increases the value of this automatic permission removal.

Posted in Exchange Online, Microsoft 365, Office 365, PowerShell | Leave a comment

Review feedback on Microsoft products in the M365 admin center

Announced a while back as part of roadmap item #68899, a new feature that allows tenant admins to review feedback submitted by their users is now rolling out to a tenant near you. The idea is simple – use this as a centralized place to gather, review and act on (eventually) feedback submitted by your users across the various desktop, web and mobile Microsoft 365 applications. This feature complements the previously announced controls for managing feedback across all the platforms (in case you missed it, check out the official documentation).

You can access the list of feedback items under the Microsoft 365 Admin Center > Health > Product feedback or directly via the https://admin.microsoft.com/#/tenantfeedback link. The page itself is very simplistic, basically a short info on top with a list of all feedback items submitted by your users taking the rest of the screen. Filters are available to narrow down the product on which feedback was submitted, the platform, update channel or the type of feedback (in-app feedback or survey). You can also surface additional columns, sort and export the data, as shown below:

Clicking one of the items will bring forth a pane on the right, showing it details. One notable omission here is the lack of access to any of the attachments submitted with the item. Another interesting bit is that the PUID (passport unique ID) value is returned as the user identifier, instead of something human-readable. Other than that, you get pretty much the details you’d expect, all of which you can surface as additional columns in order to avoid the need to click each individual entry.

Action-wise, the only thing you can do currently is Delete a single or all entries, and the aforementioned Export option. And that’s pretty much all there is to it, currently. In case I missed some detail, here’s a link to the official documentation. Enjoy!

Posted in Microsoft 365 | Leave a comment

Unable to reset the password for a disabled account?!

So apparently, when using the recently introduced /resetPassword endpoint from the Authentication methods Graph API branch, we cannot reset the password for any user whose account is currently disabled (as in BlockCredential is set to True/AccountEnabled is set to False). No idea whether this is some inherent Graph API limitation or someone decided it’s how things should work. In any case, this is what you get currently:

$uri = "https://graph.microsoft.com/beta/users/user@domain.com/authentication/passwordMethods/28c10230-6103-485e-b985-444c60001490/resetPassword"
Invoke-WebRequest -Headers $authHeader -Uri $uri -Method Post

Invoke-WebRequest : The remote server returned an error: (400) Bad Request.
At line:2 char:13
+ Invoke-WebRequest -Headers $authHeader -Uri $uri -Method ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (System.Net.HttpWebRequest:HttpWebRequest) [Invoke-WebRequest], WebException
+ FullyQualifiedErrorId : WebCmdletWebResponseException,Microsoft.PowerShell.Commands.InvokeWebRequestCommand

Note that I didn’t specify a password in the POST request, but that’s perfectly acceptable for a cloud-only account and will result in generating a random password. And you will not get better results by actually providing a password value either. The error message itself is beyond helpful, so I resorted to using the good old StreamReader method to get the actual HTTP error response:

$uri = "https://graph.microsoft.com/beta/users/user@domain.com/authentication/passwordMethods/28c10230-6103-485e-b985-444c60001490/resetPassword"
try { Invoke-WebRequest -Headers $authHeader -Uri $uri -Method Post -ErrorAction Stop -Body ($body | ConvertTo-Json -Depth 6) }
catch [System.Net.WebException] {
if ($_.Exception.Response -eq $null) { throw }

#Get the full error response
$streamReader = [System.IO.StreamReader]::new($_.Exception.Response.GetResponseStream())
$streamReader.BaseStream.Position = 0
$errResp = $streamReader.ReadToEnd() | ConvertFrom-Json
$streamReader.Close()

if ($errResp.error.code -match "ResourceNotFound|Request_ResourceNotFound") { Write-Verbose "Resource $uri not found, skipping..."; return } #404, continue
elseif ($errResp.error.code -eq "BadRequest") { $errResp | return } #400, we should terminate... but stupid Graph sometimes returns 400 instead of 404
elseif ($errResp.error.code -eq "Forbidden") { Write-Verbose "Insufficient permissions to run the Graph API call, aborting..."; throw } #403, terminate
elseif ($errResp.error.code -eq "InvalidAuthenticationToken") { Write-Verbose "Access token is invalid, exiting the script." ; throw }
else { $errResp ; throw }
}
catch { $_ ; return }

which in turn gives us a proper error message:

error
-----
@{code=badRequest; message={"error":{"code":"BadRequest","message":"User is blocked","innerError":{"request-id":"27fcaac5-bc4a-4b77-8152-02742a5dabf6","date":"2021-05-18T16:11:44.908418Z"}}}; innerError=}

Trying the same operation via the Azure AD blade will result in the following message:

Fortunately, we can work around this idiocy by using the good old Set-MsolUserPassword cmdlet or performing the reset via the Microsoft 365 Admin Center. For now.

 

Posted in Azure AD, Graph API, Microsoft 365, Office 365 | Leave a comment

ExchangeOnlineManagement version 2.0.5 released

In case you missed it, a new version of the ExchangeOnlineManagement PowerShell module was released over the weekend, namely 2.0.5. The changelog for this version is rather short and only lists four new added cmdlets:

  • Get-OwnerlessGroupPolicy and Set-OwnerlessGroupPolicy, used to retrieve and manage the Ownerless Group policy object, or in other words the policy which controls what happens to a group without an owner
  • Get-VivaInsightsSettings and Set-VivaInsightsSettings, used to manage the availability of Headspace features in Viva Insights, whatever that is.

Let’s take a quick look at the Ownerless group policy cmdlets first. Some rough edges are immediately visible, for example if you run the Get-OwnerlessGroupPolicy cmdlet when connected via CBA, you get the following error:

Get-OwnerlessGroupPolicy
Get-OwnerlessGroupPolicy: Error while requesting REST service. HttpStatusCode=Unauthorized

The CBA-enabled module has traditionally had issues with Groups-related functionality, due to the nature of the group (and group policy) objects, so that’s not that surprising. What is surprising though is that running the same cmdlet with a regular user account also throws an error:

Get-OwnerlessGroupPolicy
Get-OwnerlessGroupPolicy : Object reference not set to an instance of an object.
At line:1 char:1
+ Get-OwnerlessGroupPolicy
+ ~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ProtocolError: (:) [Get-OwnerlessGroupPolicy], NullReferenceException
+ FullyQualifiedErrorId : Object reference not set to an instance of an object.,Microsoft.Exchange.Management.RestApiClient.OwnerlessGroups.GetOwnerlessGroupPolicy

A simple “no policy is configured” output would’ve been better. Things are a bit messy when using the Set-OwnerlessGroupPolicy cmdlet too, running it without any parameters will prompt you to provide a value for the Enabled parameter and proceed with creating the policy object:

Set-OwnerlessGroupPolicy

cmdlet Set-OwnerlessGroupPolicy at command pipeline position 1
Supply values for the following parameters:
Enabled:

Enabled : False
SenderEmailAddress :
NoOfWeeksToNotify : 0
MaxNoOfMembersToNotify : 0
EnabledGroupIds : {}

As a result, we can now run the Get-OwnerlessGroupPolicycmdlet with success:

Get-OwnerlessGroupPolicy

Enabled : False
SenderEmailAddress :
NoOfWeeksToNotify : 0
MaxNoOfMembersToNotify : 0
EnabledGroupIds : {}

Putting the cmdlet oddities aside, the Ownerless group policy is a nice addition that will help address scenarios where the last owner of a group is removed for some reason (for example when leaving the company). Once you enabled the policy, an email will be sent to the current members of a given ownerless group, asking them to “volunteer” as a group owner. The policy settings allow you to specify how long the emails should be sent for (as the parameter name, NoOfWeeksToNotify, suggests, value is in weeks), and how many members should be probed (MaxNoOfMembersToNotify). In addition, you can also control the sender email address, via the SenderEmailAddress parameter. And should you want to only enable this feature for specific set of groups, instead of all, you can provide a comma-separated list of up to 10 group ID entries via the EnabledGroupIds parameter. Here’s an example of running the cmdlet with some settings that make sense:

Set-OwnerlessGroupPolicy -Enabled $true -SenderEmailAddress GroupGovernance@michev.info -NoOfWeeksToNotify 4 -MaxNoOfMembersToNotify 5

Let’s also briefly cover the *-VivaInsightsSettings cmdlets. Run the Get-VivaInsightsSettings to check whether a given user is enabled for Viva Insights. Make sure to specify the UPN/email address of the user though, as with other Graph-based cmdlets:

Get-VivaInsightsSettings -Identity user@domain.com

UserId IsInsightsHeadspaceEnabled
------ --------------------------
user@domain.com True

To enable (or disable) a given user for Viva Insights, use the Set-VivaInsightsSettings cmdlet:

Set-VivaInsightsSettings -Identity gosho@michev.info -Enabled $false -Feature headspace

UserId IsInsightsHeadspaceEnabled
------ --------------------------
gosho@michev.info False

Note that you do need to provide the Feature parameter, which only accepts the headspace value currently. The Enabled parameter control the user state for the given feature. And that’s pretty much all there is to it.

 

Posted in Exchange Online, Microsoft 365, Office 365, PowerShell | Leave a comment