Browse Author

Drew Madelung

This is my site!

Will you be at SharePoint Conference 2019? I will!

I am excited to share that I will be speaking and attending the next SharePoint Conference in Las Vegas during the full week of May 20th in 2019. This is a can’t miss event that will keep you up to speed with all of the great new things coming out in the collaboration and communication space at Microsoft in 2019. Last year we got the introduction into SharePoint spaces! You will want to be there in person to see what’s next.

You can register early to take advantage of some early swag (like an Xbox!) and even save more money using the code MADELUNG when you register!

Why you should try to go

  • Networking & networking & more networking. I love events of this scale because of the opportunities to meet new people and learn from others experiences. There is no better spot than after a great session at a lunch table or after sessions are done and you are out at a great dinner in Vegas! Strike up conversations and who knows what kind of nuggets of great info you might find.
  • It’s not just about SharePoint anymore. Yes the name is SharePoint Conference but this event is really about bringing together all of the Microsoft workloads around communication and collaboration and how they work together. This includes primary focuses outside of SharePoint and OneDrive like Teams, PowerBI, Yammer, Planner, and larger topics like Microsoft Search and Azure.
  • Interactions with Microsoft. What is that super specific question or issue or scenario that has been nagging you or your company? An event like this is a great time to connect directly with people on the product team about how things work and why they were done the way they were. These discussions can provide priceless value that you can take back with you that you can’t find anywhere else.
  • It’s in Vegas and Vegas is awesome.

My sessions

From start to finish: How to create your modern SharePoint site provisioning solution

Creating modern SharePoint sites only takes a second, but what if you want to customize or control that process? It can be a challenge to keep up with all the sites in an organization and can affect support and governance of a SharePoint environment. In this session, learn how to use Microsoft’s latest tools such as Site Designs, PnP Site Provisioning, PowerApps and Flow to create a full site provisioning system with custom templates, custom branding, and easy approval before creation!

This is a session I am doing with Vlad Catrinescu and will be a deeper dive into what we presented at Microsoft Ignite in 2018. Here is a video sneak preview of what to expect!

Kudos to Adam King for help with the video! (and the security guard)

Taking OneDrive for Business administration to the next level

OneDrive for Business is a key workload in Office 365 and is an integral part of your collaboration and content strategy. Whether you are looking to roll-out OneDrive for Business or are already are utilizing it, there are important things that you should know about for administration. Do you know what is possible for admins? Do you know if your content really is secure? Do you know who can and can’t share? You also can’t have a great OneDrive for Business experience without sync but there are things you need to know about deploying and managing sync across your enterprise. This session will go through real world experiences of managing OneDrive for Business and what you really need to know to be successful.


Can’t wait to see everyone at #SPC19!

Drew @ Ignite 2018

I can’t wait for Microsoft Ignite 2018 this year in Orlando. I was honored to be selected as featured speaker earlier this year for this event. The list of speakers and sessions this year is incredible and I can’t believe I am able to join them as a presenter again this year. If you aren’t able to attend, all sessions (including theaters!) will be live streamed. If there is one that you don’t want to miss make sure you make Jeff Teper’s collaboration keynote on Monday at 4:00 p.m.

This is an exciting time to be working in the Microsoft collaboration space. We have seen the explosion of Teams, the resurgence of SharePoint, the overwhelming adoption of the SharePoint Framework and so much more. I am sure the next set of announcements will not disappoint. 

My sessions

Session 1 – BRK3273 – From start to finish: How to create your modern SharePoint site provisioning solution

Tuesday, September 25 – 10:15 AM – 11:00 AM – OCCC W304 A-D

I am lucky to present this session with a good friend and great speaker Vlad Catrinescu.

Details: Creating modern SharePoint sites only takes a second but what if you want to customize or control that process? It can be a challenge to keep up with all the sites in an organization and can affecat support and governance of a SharePoint environment. In this session, learn how to use Microsoft’s latest tools such as Site Designs, Site Scripts, PnP Site Provisioning, PowerApps and Flow to create a full site provisioning system with custom templates, custom branding, and easy approval before creation!

Session 2 – THR2142 – What you need to know about managing OneDrive for Business

Wednesday, September 26 – 4:35 PM – 4:55 PM – Expo Theater #6

Details: OneDrive for Business is a key workload in Office 365 and should be an integral part of your collaboration strategy. OneDrive provides a cloud location to store, share, and sync your work files and then work with them from any device. OneDrive for Business management needs to be done to support the user and the administrator to ensure the content is always secure. What happens to OneDrive content when someone leaves? What devices have content synced to them? What limits are there and do the users know about them? Learn more about what management capabilities are available and which ones are needed within your enterprise.

Session 3 – Under the Hood Ignite special: Hub sites with Drew Madelung

Tuesday, September 25 – 2:00 PM – 2:45 PM – Immersion Zone Podcast 2

Details: Are you a fan of Under the Hood with Nick Brattoli, or do you want to learn how SharePoint hub sites are being implemented in the real world? If so, head over to the podcast center and listen to host Nick Brattoli and special guest Drew Madelung as they talk about their experiences. 

They discuss topics such as: 
• Changes organizations can make to their information architecture to take advantage of hub sites 
• Navigation schemas that are intuitive and scale well 
• Security and governance models 
• Tips and tricks to provision sites quickly 
• Challenges and workarounds 

Get Office 365 Groups with Teams via PowerShell and the Microsoft Graph

Office 365 Groups are the backbone of a lot applications in Microsoft. The core principal is that an Office 365 Group is the security model that supports a Team. A good start to learn more about this is from the Microsoft documentation about the two.

Here is a more detailed image about how a Team is a workload that is supported by Office 365 Groups as the identity layer. This means that not all groups have an associated Team but all Teams are supported by a group.

Getting Groups with associated Teams

I had a client ask me recently to get a list of what groups have a Microsoft Teams chat connected vs Office 365 groups that don’t have a team connected. I have done this in the past using the method here on the TechCommunity. I then saw in some updated documentation that the beta Graph API includes a filterable property called resourceProvisioningOptions. The documentation can be found here. Filtering by this property is currently on the beta API so it is not recommended to utilize this in a production solution. 

Using the /groups Graph API we can retrieve all groups in the tenant that have a team. Any group that has a team has a resourceProvisioningOptions property that contains “Team”. 

  • Currently teams that were deleted may be included
  • This property can be changed but don’t do it
  • This also is populated for a group that has a Team added to it after the fact

One of the following permissions is required to call this API. To learn more, including how to choose permissions, see Permissions.

Permission type Permissions (from least to most privileged)
Delegated (work or school account) Group.Read.All, Group.ReadWrite.All
Delegated (personal Microsoft account) Not supported.
Application Group.Read.All, Group.ReadWrite.All

Here is the script and I will break it down below

#Enter scopes or app data - If a scope is entered it will used
#If scopes is empty it will check to run via app 
$scopes = 'Group.Read.All'

$appid = ''
$appsecret = ''
$appaaddomain = ''

#Graph URLs - uncomment one to run

#Get all groups
#$url = "https://graph.microsoft.com/v1.0/groups?`$filter=groupTypes/any(c:c eq 'Unified')&`$select=displayname,resourceProvisioningOptions"
#Get all groups with teams
$url = "https://graph.microsoft.com/beta/groups?`$filter=resourceProvisioningOptions/Any(x:x eq 'Team')"

#Establish connection
If($scopes.Length -gt 0){
    Connect-PnPOnline -Scopes "Group.Read.All"
} elseif($appid.Length -gt 0) {
    Connect-PnPOnline -AppId $appid -AppSecret $appsecret  -AADDomain $appaaddomain
} else {
    write-host 'Connection issue' -ForegroundColor Red
    exit
}

#Get token
$token = Get-PnPAccessToken

#Call graph
if($token){
    $response = Invoke-RestMethod -Uri $url -Headers @{Authorization = "Bearer $token"}
} else {
    write-host 'Token issue' -ForegroundColor Red
    exit
}

#Parse data
if($response){
foreach($r in $response.value){ 
    if($r.resourceProvisioningOptions -eq 'Team'){
        write-host $r.displayname "is a Team enabled Group" -ForegroundColor Yellow
        #Do fancy stuff in here
    } else {
        write-host $r.displayname "is a regular O365 Group" -ForegroundColor Green
    }
}
} else {
    write-host 'Response issue' -ForegroundColor Red
}

Connecting to the Graph via PowerShell

To connect to the Graph via PowerShell I am using the PnP PowerShell module. SharePoint Patterns and Practices (PnP) contains a library of PowerShell commands (PnP PowerShell) that allows you to perform complex provisioning and artifact management actions towards SharePoint. The commands use CSOM and can work against both SharePoint Online as SharePoint On-Premises. Details about how to work with this module and its cmdlets can be found here.

The cmdlet that is used to connect is Connect-PnPOnline. This cmdlet can be used to connect to multiple entry points. When connecting to the Graph you can connect through Azure AD and declare permissions scopes with the -Scopes parameter or connect with app level permissions using the -AppId, -AppSecret, and -AADDomain parameters.

I have setup the script to handle either depending on what you enter at the top for the variables. Details for different types of permissions can be found here.

Calling the Graph via PowerShell

To call the Graph I am using the Invoke-RestMethod cmdlet to make the REST request. To handle the Graph call we need to pass along a bearer token. I am getting the token through the PnP cmdlet Get-PnPAccessToken. The data will then be returned as an object. You could convert the data into JSON to utilize it if necessary.

Along with the token we need to pass along the Graph Uri call. I have setup 2 different options to get the data. Swap the comment (#) tags for either $url line. 

Here is a breakdown of each option:

  • Get all groups which have Teams
    • This will return the filtered list of Groups
      • /v1.0/groups?$filter=groupTypes/any(c:c eq ‘Unified’)&`$select=displayname,resourceProvisioningOptions
  • Get all groups
    • This will return all groups and then go through all returned groups and perform an action for ones that have a connected Team
      • /beta/groups?$filter=resourceProvisioningOptions/Any(x:x eq ‘Team’)
      • Currently this only does a Write-Host but any business logic could be added here.

Make sure you copy and paste from the code block for proper formatting.

The best way to test Graph calls before working with them is through the Graph Explorer. I highly recommend this one!

Securing the app permissions

One idea that I did not put in here but would be a good idea if you wanted to set up a recurring solution around this would be to protect the app data through the Azure Key Vault.  Here are details on how this can be completed – 

Using Azure Key Vault with PowerShell – Part 1

More information about setting up an Azure AD app can be found here:

Interact with Graph and make O365 Groups with AzureFunctions PowerShell

Removing Permissions for Viewing Modern Personal Blogs in Office 365

A personal blog can be a great tool for you to contribute your thoughts and ideas. Office 365 provides the capability for everyone to have a personal blog that can be accessed via your profile page. 

When you create a new blog post this will be automatically be view-able by all employees. If you do not want to have this capability or manage this in any way it can be done via PowerShell. The example I put together will remove viewers access from all existing blogs so they can only be seen by the owner.

To get started we need a high level understanding of what these blogs are and how they work. I won’t go into all of the details of this because Benjamin Niaulin has already put it together in this great post:

The highlights to support this post are:

  • When a user follows the links to create a new blog post a new site collection is built with the managed path of /portals/personal with a site name of your user account
    • i.e. tenant.sharepoint.com/portals/personal/dmadelung
    • These are not viewable in any SP Admin center and Get-SPOSite will not work
  • Site collections are only built after a user initiates the creation so not all users will have one
  • Blog posts (stories) are creates at pages in the pages library on your site collection
  • Permissions are handled with SharePoint permissions and inherited down with a Contributors, Creators, and Viewers SharePoint Group
    • The viewers group includes “Everyone except external users” by default
  • The blogs are NOT deleted when a user leaves like their OneDrive site collection

And here are details the details from Microsoft around personal blog posts in Office 365:

Removing existing permissions via PowerShell

As this is all hosted in SharePoint there could be multiple ways that we can control these. Unfortunately I couldn’t find a way to control things as scale but there is a small UserVoice submission for it. What I wanted to ensure was that creators could still get to their content but no one else could view anything. The path I took to manage these was through PowerShell and CSOM (Client Side Object Model)

Here is link to the GitHub repo and I will break it down below along with the script.

Here are some key things to note:

  • I can not confirm that doing this is the best practice but it was the easiest way I found to control these without a any administrator controls available to us. 
  • This is currently built to run on demand but could be updated to run on a schedule via something like Azure Automation.
    • To catch everything it will need to run on a schedule because any future sites will not be caught.
  • This could be updated to be used as a reporting tool or identification tool for cleanup.
  • I would comment out the actual removal of the permissions and put some logging in to test before fully running.
    • Also if you have any changes please update the repo!
  • This queries the user profile service in SharePoint Online to get the full list of users which could be huge.
    • I didn’t test this on a very large environment so this could take awhile to run or need to be enhanced for scale.
  • All of the user profile gathering was copied from this post from Microsoft on how to display a list of OneDrive for Business site collections

To get started with CSOM & PowerShell with SharePoint Online here is a good blog post from Chris O’Brien. You can get the latest version of SharePoint Online CSOM here. If you download the nuget file you can change the file extension to .zip and extract the .dlls.

To utilize the script make sure you fill out the appropriate variables and more information about what this will do is below the script. Make sure you test any script you get online before you really run it!

# Use this script to remove viewer permissions from all user delve blogs that have been created
# A user will still be able to view their existing blogs and create blogs but people will not be able to see them
# This would allow you to choose in the future if you want to make them live
# 
# This could be updated to run on a schedule as this will not remove any new blogs that are created

### ENTER YOU VARIABLES HERE ###

#Path to the SP CSOM files 
$csomPath = "C:\...." 
################

#Prompt for parameters
#TenantDomain is beginning of "tenantdomain.sharepoint.com.."
$TenantDomain = Read-Host -Prompt "Tenant domain"
$AdminAccount = Read-Host -Prompt "Admin account"
$AdminPass = Read-Host -Prompt "Password for $AdminAccount" –AsSecureString

#Set SharePoint admin url
$AdminURI = "https://" + $TenantDomain + "-admin.sharepoint.com"

#Get CSOM files
Add-type -Path "$csomPath\Microsoft.SharePoint.Client.dll"
Add-type -Path "$csomPath\Microsoft.SharePoint.Client.Runtime.dll"

#Begin the process
$loadInfo1 = [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client")
$loadInfo2 = [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.Runtime")
$loadInfo3 = [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint.Client.UserProfiles")

#Set credentials for CSOM
$creds = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($AdminAccount, $AdminPass)

#Add the path of the User Profile Service to the SPO admin URL, then create a new webservice proxy to access it
$proxyaddr = "$AdminURI/_vti_bin/UserProfileService.asmx?wsdl"
$UserProfileService= New-WebServiceProxy -Uri $proxyaddr -UseDefaultCredential False
$UserProfileService.Credentials = $creds

#Set variables for authentication cookies
$strAuthCookie = $creds.GetAuthenticationCookie($AdminURI)
$uri = New-Object System.Uri($AdminURI)
$container = New-Object System.Net.CookieContainer
$container.SetCookies($uri, $strAuthCookie)
$UserProfileService.CookieContainer = $container

#Sets the first User profile, at index -1
$UserProfileResult = $UserProfileService.GetUserProfileByIndex(-1)

Write-Host "Starting- This could take a while."

#Getting total number of profiles
$NumProfiles = $UserProfileService.GetUserProfileCount()
$i = 1

#Create array to track users
$users = @()

#As long as the next User profile is NOT the one we started with (at -1)...
While ($UserProfileResult.NextValue -ne -1) 
{
    Write-Host "Reviewing profile $i of $NumProfiles"

    #Look for the Point Publishing Blog url object in the User Profile and retrieve it
    #It will be empty for users which it has not been created for

    #Get personal blog publishing URL
    $Prop = $UserProfileResult.UserProfile | Where-Object { $_.Name -eq "SPS-PointPublishingUrl" } 
    $Url= $Prop.Values[0].Value

    #Get user UPN - Can be used for reporting
    #$Prop = $userProfileResult.UserProfile | Where-Object { $_.Name -eq "SPS-UserPrincipalName"}
    #$Upn= $Prop.Values[0].Value

    #If the blog site exists then add it to an array to review
    if ($Url) {
        $users += $Url
    }

    #And now we check the next profile the same way...
    $UserProfileResult = $UserProfileService.GetUserProfileByIndex($UserProfileResult.NextValue)
    $i++
}

#Loop through all identified sites to remove blog viewers
foreach($user in $users){
    #Set blog site url
    $siteurl = "https://" + $TenantDomain + ".sharepoint.com" + $user

    #Connect to blog site collection
    $ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteurl)
    $ctx.Credentials = $Creds
 
    #Connect to web and get site groups
    $web = $ctx.Web
    $groups = $ctx.Web.SiteGroups
    $ctx.Load($web)
    $ctx.Load($groups)
    $ctx.ExecuteQuery()
    
    #Get the viewers group
    $group = $groups | where { $_.Title -eq "Viewers"}
    if($group){
        #Get the users in the viewers group
        $users = $group.Users
        $ctx.Load($users)
        $ctx.ExecuteQuery()

        #Remove all users from the viewers group
        foreach($u in $users){
            $group.Users.RemoveByLoginName($u.LoginName)
            $web.Update()
            $ctx.ExecuteQuery()
        }
    }
}

The end result will be that all existing blog sites will have anyone in the Viewers SharePoint Group removed

Before…

After..

Sharing a File in SharePoint Online or OneDrive with PowerShell

I have been diving into doing larger scale operations in SharePoint Online using the Client Side Object Model (CSOM) utilizing PowerShell and ran into a scenario that I couldn’t easily find documented anywhere. What I wanted to do was technically “share” a file with a specific user and have that user receive an email just like if it was done through the GUI. What I didn’t want to see is just the breaking of permissions. What I found was the Web.ShareObject method and this great blog post from Vesa Juvonen in 2015

Once I found this I started working on putting this into a useful PowerShell format. To get started with CSOM & PowerShell with SharePoint Online here is a good blog post from Chris O’Brien. You can get the latest version of SharePoint Online CSOM here. If you download the nuget file you can change the file extension to .zip and extract the .dlls.

Here is link to the GitHub rep and I will break it down below along with the script. Here are some key things to note:

  • The Web.ShareObject method has been updated since the Vesa blog post with a parameter called useSimplifiedRoles that can be used for utilizing modern sharing
  • SharePoint PnP has extended the sharing APIs and built a sample that can be used
  • This script is built to share a file based on filename within a site to a single user
  • This works on SharePoint Online and OneDrive for Business
  • It will share as the user who runs the script
  • This script could be updated to share a site or to multiple people
  • You can share with Edit or View permission based on the roleValue
  • It doesn’t replicate the modern sharing UI in capabilities exactly (more of what occurs details below)

To utilize the script make sure you fill out the appropriate variables and more information about what this will do is below the script. 

# Use this script to share a file via CSOM and PowerShell
# ShareObject https://msdn.microsoft.com/en-us/library/office/mt684216.aspx
# External sharing blog https://blogs.msdn.microsoft.com/vesku/2015/10/02/external-sharing-api-for-sharepoint-and-onedrive-for-business/

### ENTER YOU VARIABLES HERE ###

#path to the SP CSOM files 
$csomPath = "C:\...." 

#Email of person running the script
$adminEmail = "user@domain.com"

#Site collection to be connected to
$siteUrl = "https://site.sharepoint.com/sites/site"

#Library title where the file exists
$libraryTitle = "Documents" 

#Filename including file type
$fileName = "Test Document 1.docx"

#Email of who the document is being shared to
$emailSharedTo = "user2@domain.com"

#UNVALIDATED_EMAIL_ADDRESS if they are in AD or GUEST_USER if they are not
$principalType = "UNVALIDATED_EMAIL_ADDRESS"  

#role:1073741826 = View, role:1073741827 = Edit
$roleValue = "role:1073741827"

#A flag to determine if permissions should be pushed to items with unique permissions.
$propageAcl = $true

#Flag to determine if an e-mail notification should to sent, if e-mail is configured.
$sendEmail = $true  

#If an e-mail is being sent, this determines if an anonymous link should be added to the message.
$includedAnonymousLinkInEmail = $false  

#The ID of the group to be added to. Use zero if not adding to a permissions group. Not actually used by the code even when user is added to existing group. 
$groupId = 0

#Doesn't matter as it isn't sent in current email format
$emailSubject = ""

#Text for the body of the e-mail.
$emailBody = "Check out my email body"  

#Use modern sharing links instead of directly granting access
$useSimplifiedRoles = $true
################

# Get CSOM files
Add-type -Path "$csomPath\Microsoft.SharePoint.Client.dll"
Add-type -Path "$csomPath\Microsoft.SharePoint.Client.Runtime.dll"

# Connnect to site
$ss = Read-Host -Prompt "Enter admin password" -AsSecureString
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($siteUrl)
$creds = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($adminEmail, $ss)
$ctx.Credentials = $creds
if(!$ctx.ServerObjectIsNull.Value) { 
    Write-Host "Connected to site:" $siteUrl -ForegroundColor Green 
} 
# Get web
$web = $ctx.Web

# Connect to library
$list = $web.Lists.GetByTitle($libraryTitle)
$ctx.Load($web)
$ctx.Load($list)
$ctx.Load($list.RootFolder)
$ctx.ExecuteQuery()

# Get doc
$query = New-Object Microsoft.SharePoint.Client.CamlQuery
$caml ="<View Scope='RecursiveAll'><Query><Where><Eq><FieldRef Name='FileLeafRef'/><Value Type='File'>" + $fileName + "</Value></Eq></Where></Query></View>"
$query.ViewXml = $caml
$item = $list.GetItems($query)
$ctx.Load($item)
$ctx.ExecuteQuery()
if (!$item) {
    Write-Host "Could not find the file:" $fileName -ForegroundColor Yellow 
} else {
    Write-Host "Sharing the the file:" $item.FieldValues.FileLeafRef -ForegroundColor Green 
}

# Get doc url
$itemUrl = $item.FieldValues.FileRef
$split = $web.Url -split '/'
$itemUrl = "https://" + $split[2] + $itemUrl

# Build user object to be shared to
$jsonPerson = "[{`"Key`":`"$emailSharedTo`",
`"Description`":`"$emailSharedTo`",
`"DisplayText`":`"$emailSharedTo`",
`"EntityType`":`"`",
`"ProviderDisplayName`":`"`",
`"ProviderName`":`"`",
`"IsResolved`":true,
`"EntityData`":{`"Email`":`"$emailSharedTo`",
    `"AccountName`":`"$emailSharedTo`",
    `"Title`":`"$emailSharedTo`",
    `"PrincipalType`":`"$principalType`"},
`"MultipleMatches`":[]}]"

# Initiate share
$result = [Microsoft.SharePoint.Client.Web]::ShareObject($web.Context,$itemUrl,$jsonPerson,$roleValue,$groupid,$propageAcl,$sendEmail,$includedAnonymousLinkInEmail,$emailSubject,$emailBody,$useSimplifiedRoles)
$web.Context.Load($result)
$web.Context.ExecuteQuery()

Write-Host "Status of the share:" $result.StatusCode -ForegroundColor Green

Starting from a non shared file this is what you will see based on different configurations:

Sharing with useSimplifiedRoles set to $true and sendEmail set to $true

  • The file does not have inheritance broken

  • After initiating the ShareObject, inheritance is broken but you don’t see any changes

  • The person being shared to receives an email that the person who ran the script wants to share a file with you and you will see the email subject is preset but the email body is included

  • Once the person being shared to clicks on the link you can see a new ‘Managed Links’ section in the item permissions

  • If you follow that link you will see the item is now shared with that individual

Sharing with useSimplifiedRoles set to $true and sendEmail set to $false

  • The file does not have inheritance broken
  • After initiating the ShareObject, inheritance is broken but you don’t see any changes if the user tries to access the file through the document library
  • There is a new link viewable in the modern manage access section showing a new sharing link and that someone can access via that link

  • If the user accesses the file via that link you can see a new ‘Managed Links’ section in the item permissions and you can see that user in the Shared with section

 

Sharing with useSimplifiedRoles set to $false and sendEmail set to $false

  • The file does not have inheritance broken
  • After initiating the ShareObject, inheritance is broken but you don’t see any changes even after a user accesses the file, that means this does nothing but break inheritance

Sharing with useSimplifiedRoles set to $false and sendEmail set to $true

  • The file does not have inheritance broken
  • After initiating the ShareObject, inheritance is broken but you don’t see any changes
  • Once a user accesses the file via the link in the email they are granted permissions directly to the file (contribute instead of edit)

Ending…

After putting this together I realized I don’t really have a great use case to actually use this. Either way it was a good learning experience for me as I am just getting started into this kind of CSOM & PowerShell work and maybe it will come in handy for someone else in the future.