From 623e3ef6c4350b522a5f549346f116c4e0139283 Mon Sep 17 00:00:00 2001 From: Fabian Hutzli Date: Sun, 19 Apr 2026 22:32:48 +0200 Subject: [PATCH] Add Get, Grant, Set and Revoke cmdlets for EntraID app file permissions Implements Files.SelectedOperations.Selected support via the Microsoft Graph Drive API. Files can be addressed by path relative to the library root (e.g. Folder/file.docx) or by Graph drive item ID. The drive is resolved through the list's associated drive endpoint. Permission roles use the correct values (Read, Write, Owner). Documentation included. --- .../Get-PnPEntraIDAppFilePermission.md | 193 +++++++++++++ .../Grant-PnPEntraIDAppFilePermission.md | 176 ++++++++++++ .../Revoke-PnPEntraIDAppFilePermission.md | 163 +++++++++++ .../Set-PnPEntraIDAppFilePermission.md | 157 +++++++++++ .../Apps/GetEntraIDAppFilePermission.cs | 257 ++++++++++++++++++ .../Apps/GrantEntraIDAppFilePermission.cs | 203 ++++++++++++++ .../Apps/RevokeEntraIDAppFilePermission.cs | 174 ++++++++++++ .../Apps/SetEntraIDAppFilePermission.cs | 227 ++++++++++++++++ .../Enums/AzureADNewListPermissionRole.cs | 29 ++ .../Enums/AzureADUpdateListPermissionRole.cs | 29 ++ .../EntraIDAppDrivePermissionInternal.cs | 60 ++++ 11 files changed, 1668 insertions(+) create mode 100644 documentation/Get-PnPEntraIDAppFilePermission.md create mode 100644 documentation/Grant-PnPEntraIDAppFilePermission.md create mode 100644 documentation/Revoke-PnPEntraIDAppFilePermission.md create mode 100644 documentation/Set-PnPEntraIDAppFilePermission.md create mode 100644 src/Commands/Apps/GetEntraIDAppFilePermission.cs create mode 100644 src/Commands/Apps/GrantEntraIDAppFilePermission.cs create mode 100644 src/Commands/Apps/RevokeEntraIDAppFilePermission.cs create mode 100644 src/Commands/Apps/SetEntraIDAppFilePermission.cs create mode 100644 src/Commands/Enums/AzureADNewListPermissionRole.cs create mode 100644 src/Commands/Enums/AzureADUpdateListPermissionRole.cs create mode 100644 src/Commands/Model/EntraIDAppDrivePermissionInternal.cs diff --git a/documentation/Get-PnPEntraIDAppFilePermission.md b/documentation/Get-PnPEntraIDAppFilePermission.md new file mode 100644 index 000000000..4d574dd9b --- /dev/null +++ b/documentation/Get-PnPEntraIDAppFilePermission.md @@ -0,0 +1,193 @@ +--- +Module Name: PnP.PowerShell +schema: 2.0.0 +applicable: SharePoint Online +online version: https://pnp.github.io/powershell/cmdlets/Get-PnPEntraIDAppFilePermission.html +external help file: PnP.PowerShell.dll-Help.xml +title: Get-PnPEntraIDAppFilePermission +--- + +# Get-PnPEntraIDAppFilePermission + +## SYNOPSIS + +**Required Permissions** + + * Microsoft Graph API: Files.ReadWrite.All or Sites.ReadWrite.All + +Returns Entra ID App permissions for a file in a document library. + +## SYNTAX + +### All Permissions +```powershell +Get-PnPEntraIDAppFilePermission -List [-Path ] [-FileId ] [-Site ] [-Connection ] +``` + +### By Permission Id +```powershell +Get-PnPEntraIDAppFilePermission -PermissionId -List [-Path ] [-FileId ] [-Site ] [-Connection ] +``` + +### By App Display Name or App Id +```powershell +Get-PnPEntraIDAppFilePermission -AppIdentity -List [-Path ] [-FileId ] [-Site ] [-Connection ] +``` + +## DESCRIPTION + +This cmdlet returns app permissions for a file in a document library. It is used in conjunction with the Entra ID SharePoint application permission `Files.SelectedOperations.Selected`. + +The file can be identified by either: +- `-Path`: the path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx`) +- `-FileId`: the Graph drive item ID of the file + +Exactly one of `-Path` or `-FileId` must be specified. + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Get-PnPEntraIDAppFilePermission -List "Documents" -Path "Contracts/2024/Agreement.docx" +``` + +Returns all app permissions set on the file at the given path in the Documents library of the currently connected site. + +### EXAMPLE 2 +```powershell +Get-PnPEntraIDAppFilePermission -List "Documents" -Path "Report.xlsx" -Site https://contoso.sharepoint.com/sites/finance +``` + +Returns all app permissions set on the file at the root of the Documents library on the specified site. + +### EXAMPLE 3 +```powershell +Get-PnPEntraIDAppFilePermission -List "Documents" -FileId "01ABC123DEF456GHI789" +``` + +Returns all app permissions set on the file with the specified drive item ID. + +### EXAMPLE 4 +```powershell +Get-PnPEntraIDAppFilePermission -List "Documents" -Path "Report.xlsx" -PermissionId aTowaS50fG1zLnNwLmV4dHxlMzhjZmIzMS00 +``` + +Returns the specific permission details for the given permission id on the file. + +### EXAMPLE 5 +```powershell +Get-PnPEntraIDAppFilePermission -List "Documents" -Path "Report.xlsx" -AppIdentity "My App" +``` + +Returns the specific permission details for the app with the provided display name on the file. + +### EXAMPLE 6 +```powershell +Get-PnPEntraIDAppFilePermission -List "Documents" -Path "Report.xlsx" -AppIdentity "89ea5c94-7736-4e25-95ad-3fa95f62b66e" +``` + +Returns the specific permission details for the app with the provided app id on the file. + +## PARAMETERS + +### -AppIdentity +Specify either the display name or the app id (client id) to filter the returned permissions to a specific app. + +```yaml +Type: String +Parameter Sets: By App Display Name or App Id + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Connection +Optional connection to be used by the cmdlet. Retrieve the value for this parameter by either specifying -ReturnConnection on Connect-PnPOnline or by executing Get-PnPConnection. + +```yaml +Type: PnPConnection +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -FileId +The Graph drive item ID of the file. Use this as an alternative to `-Path` when you already know the drive item ID. Mutually exclusive with `-Path`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -List +The document library containing the file. Accepts a list GUID or display name. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path +The path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx` or just `file.docx` for a file at the root). Mutually exclusive with `-FileId`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PermissionId +If specified, the permission with that id will be retrieved. + +```yaml +Type: String +Parameter Sets: By Permission Id + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Site +Optional url of a site to retrieve the permissions for. Defaults to the currently connected site. + +```yaml +Type: SitePipeBind +Parameter Sets: (All) + +Required: False +Position: Named +Default value: Currently connected site +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) diff --git a/documentation/Grant-PnPEntraIDAppFilePermission.md b/documentation/Grant-PnPEntraIDAppFilePermission.md new file mode 100644 index 000000000..b0ac7028e --- /dev/null +++ b/documentation/Grant-PnPEntraIDAppFilePermission.md @@ -0,0 +1,176 @@ +--- +Module Name: PnP.PowerShell +schema: 2.0.0 +applicable: SharePoint Online +online version: https://pnp.github.io/powershell/cmdlets/Grant-PnPEntraIDAppFilePermission.html +external help file: PnP.PowerShell.dll-Help.xml +title: Grant-PnPEntraIDAppFilePermission +--- + +# Grant-PnPEntraIDAppFilePermission + +## SYNOPSIS + +**Required Permissions** + + * Microsoft Graph API: Files.ReadWrite.All or Sites.ReadWrite.All + +Adds permissions for a given Entra ID application registration on a file in a document library. + +## SYNTAX + +```powershell +Grant-PnPEntraIDAppFilePermission -AppId -DisplayName -Permissions -List [-Path ] [-FileId ] [-Site ] [-Connection ] +``` + +## DESCRIPTION + +This cmdlet adds permissions for a given Entra ID application registration on a file in a document library. It is used in conjunction with the Entra ID SharePoint application permission `Files.SelectedOperations.Selected`. + +The file can be identified by either: +- `-Path`: the path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx`) +- `-FileId`: the Graph drive item ID of the file + +Exactly one of `-Path` or `-FileId` must be specified. + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Grant-PnPEntraIDAppFilePermission -AppId "aa37b89e-75a7-47e3-bdb6-b763851c61b6" -DisplayName "TestApp" -Permissions Read -List "Documents" -Path "Contracts/Agreement.docx" +``` + +Grants the Entra ID application registration Read access on the file at the specified path in the Documents library of the currently connected site. + +### EXAMPLE 2 +```powershell +Grant-PnPEntraIDAppFilePermission -AppId "aa37b89e-75a7-47e3-bdb6-b763851c61b6" -DisplayName "TestApp" -Permissions Write -List "Documents" -FileId "01ABC123DEF456GHI789" +``` + +Grants Write access on the file with the specified drive item ID in the Documents library. + +### EXAMPLE 3 +```powershell +Grant-PnPEntraIDAppFilePermission -AppId "aa37b89e-75a7-47e3-bdb6-b763851c61b6" -DisplayName "TestApp" -Permissions Owner -List "Documents" -Path "Report.xlsx" -Site https://contoso.sharepoint.com/sites/finance +``` + +Grants Owner access on the specified file in the Documents library of the given site collection. + +## PARAMETERS + +### -AppId +The app id (client id) of the Entra ID application registration to grant permission for. + +```yaml +Type: Guid +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Connection +Optional connection to be used by the cmdlet. Retrieve the value for this parameter by either specifying -ReturnConnection on Connect-PnPOnline or by executing Get-PnPConnection. + +```yaml +Type: PnPConnection +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -DisplayName +The display name to associate with the permission. Used for visual reference only; does not need to match the application name in Entra ID. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -FileId +The Graph drive item ID of the file. Use this as an alternative to `-Path` when you already know the drive item ID. Mutually exclusive with `-Path`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -List +The document library containing the file. Accepts a list GUID or display name. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path +The path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx` or just `file.docx` for a file at the root). Mutually exclusive with `-FileId`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Permissions +The permissions to grant for the Entra ID application registration. Can be Read, Write, Owner, or FullControl. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Accepted values: Read, Write, Owner, FullControl +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Site +Optional url of a site to grant the permissions on. Defaults to the currently connected site. + +```yaml +Type: SitePipeBind +Parameter Sets: (All) + +Required: False +Position: Named +Default value: Currently connected site +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) diff --git a/documentation/Revoke-PnPEntraIDAppFilePermission.md b/documentation/Revoke-PnPEntraIDAppFilePermission.md new file mode 100644 index 000000000..a0b01631d --- /dev/null +++ b/documentation/Revoke-PnPEntraIDAppFilePermission.md @@ -0,0 +1,163 @@ +--- +Module Name: PnP.PowerShell +schema: 2.0.0 +applicable: SharePoint Online +online version: https://pnp.github.io/powershell/cmdlets/Revoke-PnPEntraIDAppFilePermission.html +external help file: PnP.PowerShell.dll-Help.xml +title: Revoke-PnPEntraIDAppFilePermission +--- + +# Revoke-PnPEntraIDAppFilePermission + +## SYNOPSIS + +**Required Permissions** + + * Microsoft Graph API: Files.ReadWrite.All or Sites.ReadWrite.All + +Revokes permissions for a given Entra ID application registration on a file in a document library. + +## SYNTAX + +```powershell +Revoke-PnPEntraIDAppFilePermission -PermissionId -List [-Path ] [-FileId ] [-Site ] [-Force] [-Connection ] +``` + +## DESCRIPTION + +This cmdlet revokes an existing permission for an Entra ID application registration on a file in a document library. It is used in conjunction with the Entra ID SharePoint application permission `Files.SelectedOperations.Selected`. + +Use [Get-PnPEntraIDAppFilePermission](Get-PnPEntraIDAppFilePermission.md) to retrieve the `PermissionId` required by this cmdlet. + +The file can be identified by either: +- `-Path`: the path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx`) +- `-FileId`: the Graph drive item ID of the file + +Exactly one of `-Path` or `-FileId` must be specified. + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Revoke-PnPEntraIDAppFilePermission -PermissionId aTowaS50fG1zLnNwLmV4dHxlMzhjZmIzMS00 -List "Documents" -Path "Contracts/Agreement.docx" +``` + +Revokes the permission with the specified id on the file at the given path in the Documents library of the currently connected site. A confirmation prompt will be shown before the permission is removed. + +### EXAMPLE 2 +```powershell +Revoke-PnPEntraIDAppFilePermission -PermissionId aTowaS50fG1zLnNwLmV4dHxlMzhjZmIzMS00 -List "Documents" -FileId "01ABC123DEF456GHI789" -Force +``` + +Revokes the permission on the file with the specified drive item ID without prompting for confirmation. + +### EXAMPLE 3 +```powershell +Revoke-PnPEntraIDAppFilePermission -PermissionId aTowaS50fG1zLnNwLmV4dHxlMzhjZmIzMS00 -List "Documents" -Path "Report.xlsx" -Site https://contoso.sharepoint.com/sites/finance -Force +``` + +Revokes the permission on the specified file in the given site collection without prompting for confirmation. + +## PARAMETERS + +### -Connection +Optional connection to be used by the cmdlet. Retrieve the value for this parameter by either specifying -ReturnConnection on Connect-PnPOnline or by executing Get-PnPConnection. + +```yaml +Type: PnPConnection +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -FileId +The Graph drive item ID of the file. Use this as an alternative to `-Path` when you already know the drive item ID. Mutually exclusive with `-Path`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Force +When specified, no confirmation prompt will be shown before revoking the permission. + +```yaml +Type: SwitchParameter +Parameter Sets: (All) + +Required: False +Position: Named +Default value: False +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -List +The document library containing the file. Accepts a list GUID or display name. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path +The path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx` or just `file.docx` for a file at the root). Mutually exclusive with `-FileId`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PermissionId +The id of the permission to revoke. Use [Get-PnPEntraIDAppFilePermission](Get-PnPEntraIDAppFilePermission.md) to retrieve the id. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Site +Optional url of a site to revoke the permissions on. Defaults to the currently connected site. + +```yaml +Type: SitePipeBind +Parameter Sets: (All) + +Required: False +Position: Named +Default value: Currently connected site +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) diff --git a/documentation/Set-PnPEntraIDAppFilePermission.md b/documentation/Set-PnPEntraIDAppFilePermission.md new file mode 100644 index 000000000..c22a05bd0 --- /dev/null +++ b/documentation/Set-PnPEntraIDAppFilePermission.md @@ -0,0 +1,157 @@ +--- +Module Name: PnP.PowerShell +schema: 2.0.0 +applicable: SharePoint Online +online version: https://pnp.github.io/powershell/cmdlets/Set-PnPEntraIDAppFilePermission.html +external help file: PnP.PowerShell.dll-Help.xml +title: Set-PnPEntraIDAppFilePermission +--- + +# Set-PnPEntraIDAppFilePermission + +## SYNOPSIS + +**Required Permissions** + + * Microsoft Graph API: Files.ReadWrite.All or Sites.ReadWrite.All + +Updates permissions for a given Entra ID application registration on a file in a document library. + +## SYNTAX + +```powershell +Set-PnPEntraIDAppFilePermission -PermissionId -Permissions -List [-Path ] [-FileId ] [-Site ] [-Connection ] +``` + +## DESCRIPTION + +This cmdlet updates an existing permission for an Entra ID application registration on a file in a document library. It is used in conjunction with the Entra ID SharePoint application permission `Files.SelectedOperations.Selected`. + +Use [Get-PnPEntraIDAppFilePermission](Get-PnPEntraIDAppFilePermission.md) to retrieve the `PermissionId` required by this cmdlet. + +The file can be identified by either: +- `-Path`: the path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx`) +- `-FileId`: the Graph drive item ID of the file + +Exactly one of `-Path` or `-FileId` must be specified. + +## EXAMPLES + +### EXAMPLE 1 +```powershell +Set-PnPEntraIDAppFilePermission -PermissionId aTowaS50fG1zLnNwLmV4dHxlMzhjZmIzMS00 -Permissions Read -List "Documents" -Path "Contracts/Agreement.docx" +``` + +Updates the permission to Read access on the file at the specified path in the Documents library of the currently connected site. + +### EXAMPLE 2 +```powershell +Set-PnPEntraIDAppFilePermission -PermissionId aTowaS50fG1zLnNwLmV4dHxlMzhjZmIzMS00 -Permissions Write -List "Documents" -FileId "01ABC123DEF456GHI789" -Site https://contoso.sharepoint.com/sites/finance +``` + +Updates the permission to Write access on the file with the specified drive item ID in the given site collection. + +## PARAMETERS + +### -Connection +Optional connection to be used by the cmdlet. Retrieve the value for this parameter by either specifying -ReturnConnection on Connect-PnPOnline or by executing Get-PnPConnection. + +```yaml +Type: PnPConnection +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -FileId +The Graph drive item ID of the file. Use this as an alternative to `-Path` when you already know the drive item ID. Mutually exclusive with `-Path`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -List +The document library containing the file. Accepts a list GUID or display name. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Path +The path to the file relative to the document library root (e.g. `Folder/SubFolder/file.docx` or just `file.docx` for a file at the root). Mutually exclusive with `-FileId`. + +```yaml +Type: String +Parameter Sets: (All) + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -PermissionId +The id of the permission to update. Use [Get-PnPEntraIDAppFilePermission](Get-PnPEntraIDAppFilePermission.md) to retrieve the id. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Permissions +The updated permissions for the Entra ID application registration. Can be Read, Write, Owner, or FullControl. + +```yaml +Type: String +Parameter Sets: (All) + +Required: True +Accepted values: Read, Write, Owner, FullControl +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + +### -Site +Optional url of a site to update the permissions on. Defaults to the currently connected site. + +```yaml +Type: SitePipeBind +Parameter Sets: (All) + +Required: False +Position: Named +Default value: Currently connected site +Accept pipeline input: False +Accept wildcard characters: False +``` + +## RELATED LINKS + +[Microsoft 365 Patterns and Practices](https://aka.ms/m365pnp) diff --git a/src/Commands/Apps/GetEntraIDAppFilePermission.cs b/src/Commands/Apps/GetEntraIDAppFilePermission.cs new file mode 100644 index 000000000..e2f24675e --- /dev/null +++ b/src/Commands/Apps/GetEntraIDAppFilePermission.cs @@ -0,0 +1,257 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Management.Automation; +using System.Text.Json; +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Base.PipeBinds; +using PnP.PowerShell.Commands.Model; + +namespace PnP.PowerShell.Commands.Apps +{ + [Cmdlet(VerbsCommon.Get, "PnPEntraIDAppFilePermission", DefaultParameterSetName = ParameterSet_ALL)] + [RequiredApiDelegatedOrApplicationPermissions("graph/Sites.FullControl.All")] + [OutputType(typeof(AzureADAppPermission))] + public class GetPnPEntraIDAppFilePermission : PnPGraphCmdlet + { + private const string ParameterSet_ALL = "All Permissions"; + private const string ParameterSet_PERMISSIONID = "By Permission Id"; + private const string ParameterSet_APPIDENTITY = "By App Display Name or App Id"; + + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_PERMISSIONID)] + [ValidateNotNullOrEmpty] + public string PermissionId; + + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_APPIDENTITY)] + [ValidateNotNullOrEmpty] + public string AppIdentity; + + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_ALL)] + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_PERMISSIONID)] + [Parameter(Mandatory = true, ParameterSetName = ParameterSet_APPIDENTITY)] + [ValidateNotNullOrEmpty] + public string List; + + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_ALL)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_PERMISSIONID)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_APPIDENTITY)] + [ValidateNotNullOrEmpty] + public string Path; + + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_ALL)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_PERMISSIONID)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_APPIDENTITY)] + [ValidateNotNullOrEmpty] + public string FileId; + + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_ALL)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_PERMISSIONID)] + [Parameter(Mandatory = false, ParameterSetName = ParameterSet_APPIDENTITY)] + public SitePipeBind Site; + + protected override void ExecuteCmdlet() + { + if (!ParameterSpecified(nameof(Path)) && !ParameterSpecified(nameof(FileId))) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Either -Path or -FileId must be specified."), + "MissingFileIdentifier", ErrorCategory.InvalidArgument, null)); + return; + } + + Guid siteId; + if (ParameterSpecified(nameof(Site))) + { + LogDebug($"Using Microsoft Graph to look up site Id for -{nameof(Site)}"); + siteId = Site.GetSiteIdThroughGraph(Connection, AccessToken); + } + else + { + LogDebug($"No -{nameof(Site)} specified, using currently connected site"); + siteId = new SitePipeBind(Connection.Url).GetSiteIdThroughGraph(Connection, AccessToken); + } + + if (siteId == Guid.Empty) + { + LogWarning("Unable to resolve the site Id. Ensure you pass a valid site via -Site or are connected to a site."); + return; + } + + var listId = ResolveListId(siteId, List); + if (listId == Guid.Empty) + { + LogWarning($"Unable to resolve list '{List}' on site {siteId}. Ensure the list exists and you have access."); + return; + } + + var driveId = ResolveDriveId(siteId, listId); + if (string.IsNullOrEmpty(driveId)) + { + LogWarning($"Unable to resolve the drive for list '{List}'. Ensure the list is a document library."); + return; + } + + string driveItemId; + if (ParameterSpecified(nameof(FileId))) + { + driveItemId = FileId; + LogDebug($"Using provided -{nameof(FileId)} directly as drive item Id"); + } + else + { + driveItemId = ResolveDriveItemId(driveId, Path); + if (string.IsNullOrEmpty(driveItemId)) + { + LogWarning($"Unable to resolve file at path '{Path}' in drive {driveId}. Ensure the path is correct and relative to the library root."); + return; + } + } + + if (ParameterSpecified(nameof(PermissionId))) + { + var cleanPermissionId = Uri.EscapeDataString(PermissionId.Trim().Replace(" ", "").Replace("\t", "").Replace("\r", "").Replace("\n", "")); + var result = GraphRequestHelper.Get($"beta/drives/{driveId}/items/{driveItemId}/permissions/{cleanPermissionId}"); + if (result != null) + { + var converted = result.Convert(); + EnrichWithDisplayNames(converted); + WriteObject(converted); + } + } + else + { + var permissions = GraphRequestHelper.GetResultCollection($"beta/drives/{driveId}/items/{driveItemId}/permissions?$select=id"); + if (permissions != null && permissions.Any()) + { + var results = new List(permissions.Count()); + foreach (var permission in permissions) + { + var detailed = GraphRequestHelper.Get($"beta/drives/{driveId}/items/{driveItemId}/permissions/{permission.Id}"); + if (detailed != null) + { + var converted = detailed.Convert(); + EnrichWithDisplayNames(converted); + results.Add(converted); + } + } + + if (ParameterSpecified(nameof(AppIdentity))) + { + var filtered = results.Where(p => p.Apps.Any(a => a.DisplayName == AppIdentity || a.Id == AppIdentity)); + WriteObject(filtered, true); + } + else + { + WriteObject(results, true); + } + } + } + } + + private void EnrichWithDisplayNames(AzureADAppPermission permission) + { + if (permission?.Apps == null) return; + + foreach (var app in permission.Apps) + { + if (!string.IsNullOrEmpty(app.DisplayName) || string.IsNullOrEmpty(app.Id)) + continue; + + try + { + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/v1.0/servicePrincipals?$filter=appId eq '{Uri.EscapeDataString(app.Id)}'&$select=displayName,appId", + AccessToken); + + if (string.IsNullOrEmpty(raw)) continue; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("value", out JsonElement valueEl)) + { + var first = valueEl.EnumerateArray().FirstOrDefault(); + if (first.ValueKind == JsonValueKind.Object && + first.TryGetProperty("displayName", out JsonElement nameEl)) + { + app.DisplayName = nameEl.GetString(); + LogDebug($"Resolved display name '{app.DisplayName}' for app {app.Id}"); + } + } + } + catch (Exception ex) + { + LogDebug($"Could not resolve display name for app {app.Id}: {ex.Message}"); + } + } + } + + private Guid ResolveListId(Guid siteId, string listIdentifier) + { + if (Guid.TryParse(listIdentifier, out Guid parsedId)) + return parsedId; + + LogDebug($"List identifier '{listIdentifier}' is not a GUID; querying Graph to resolve by display name"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists?$select=id,displayName", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return Guid.Empty; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("value", out JsonElement valueEl)) + { + foreach (var item in valueEl.EnumerateArray()) + { + if (item.TryGetProperty("displayName", out JsonElement nameEl) && + nameEl.GetString().Equals(listIdentifier, StringComparison.OrdinalIgnoreCase) && + item.TryGetProperty("id", out JsonElement idEl)) + { + return Guid.Parse(idEl.GetString()); + } + } + } + + return Guid.Empty; + } + + private string ResolveDriveId(Guid siteId, Guid listId) + { + LogDebug($"Resolving drive Id for list {listId} on site {siteId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists/{listId}/drive?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + + private string ResolveDriveItemId(string driveId, string path) + { + var encodedPath = string.Join("/", path.Trim('/').Split('/').Select(Uri.EscapeDataString)); + LogDebug($"Resolving drive item Id for path '{path}' in drive {driveId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/drives/{driveId}/root:/{encodedPath}?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + } +} diff --git a/src/Commands/Apps/GrantEntraIDAppFilePermission.cs b/src/Commands/Apps/GrantEntraIDAppFilePermission.cs new file mode 100644 index 000000000..0f14bb70b --- /dev/null +++ b/src/Commands/Apps/GrantEntraIDAppFilePermission.cs @@ -0,0 +1,203 @@ +using System; +using System.Linq; +using System.Management.Automation; +using System.Text.Json; +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Base.PipeBinds; +using PnP.PowerShell.Commands.Enums; +using PnP.PowerShell.Commands.Model; +using PnP.PowerShell.Commands.Utilities; + +namespace PnP.PowerShell.Commands.Apps +{ + [Cmdlet(VerbsSecurity.Grant, "PnPEntraIDAppFilePermission")] + [RequiredApiDelegatedPermissions("graph/Sites.FullControl.All")] + [OutputType(typeof(AzureADAppPermission))] + public class GrantPnPEntraIDAppFilePermission : PnPGraphCmdlet + { + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public Guid AppId; + + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string DisplayName; + + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string List; + + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string Path; + + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string FileId; + + [Parameter(Mandatory = false)] + public SitePipeBind Site; + + [Parameter(Mandatory = true)] + [ArgumentCompleter(typeof(EnumAsStringArgumentCompleter))] + public string[] Permissions; + + protected override void ExecuteCmdlet() + { + if (!ParameterSpecified(nameof(Path)) && !ParameterSpecified(nameof(FileId))) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Either -Path or -FileId must be specified."), + "MissingFileIdentifier", ErrorCategory.InvalidArgument, null)); + return; + } + + Guid siteId; + if (ParameterSpecified(nameof(Site))) + { + LogDebug($"Using Microsoft Graph to look up site Id for -{nameof(Site)}"); + siteId = Site.GetSiteIdThroughGraph(Connection, AccessToken); + LogDebug($"Site resolved to Id {siteId}"); + } + else + { + LogDebug($"No -{nameof(Site)} specified, using currently connected site"); + siteId = new SitePipeBind(Connection.Url).GetSiteIdThroughGraph(Connection, AccessToken); + LogDebug($"Currently connected site has Id {siteId}"); + } + + if (siteId == Guid.Empty) + { + LogWarning("Unable to resolve the site Id. Ensure you pass a valid site via -Site or are connected to a site."); + return; + } + + var listId = ResolveListId(siteId, List); + if (listId == Guid.Empty) + { + LogWarning($"Unable to resolve list '{List}' on site {siteId}. Ensure the list exists and you have access."); + return; + } + + var driveId = ResolveDriveId(siteId, listId); + if (string.IsNullOrEmpty(driveId)) + { + LogWarning($"Unable to resolve the drive for list '{List}'. Ensure the list is a document library."); + return; + } + + string driveItemId; + if (ParameterSpecified(nameof(FileId))) + { + driveItemId = FileId; + LogDebug($"Using provided -{nameof(FileId)} directly as drive item Id"); + } + else + { + driveItemId = ResolveDriveItemId(driveId, Path); + if (string.IsNullOrEmpty(driveItemId)) + { + LogWarning($"Unable to resolve file at path '{Path}' in drive {driveId}. Ensure the path is correct and relative to the library root."); + return; + } + } + + // Apply multi-geo fix (same approach as Grant-PnPEntraIDAppSitePermission) + Utilities.REST.RestHelper.Get(Connection.HttpClient, $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}", AccessToken); + + var roles = Permissions.Select(p => p.ToString().ToLowerInvariant()).ToArray(); + + var payload = new + { + grantedToV2 = new + { + application = new + { + id = AppId.ToString(), + displayName = DisplayName + } + }, + roles + }; + + LogDebug($"Granting App {AppId} the permission{(roles.Length != 1 ? "s" : "")} {string.Join(", ", roles)} on drive item {driveItemId} in drive {driveId}"); + + var result = Utilities.REST.RestHelper.Post( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/drives/{driveId}/items/{driveItemId}/permissions", + AccessToken, + payload); + + WriteObject(result?.Convert()); + } + + private Guid ResolveListId(Guid siteId, string listIdentifier) + { + if (Guid.TryParse(listIdentifier, out Guid parsedId)) + return parsedId; + + LogDebug($"List identifier '{listIdentifier}' is not a GUID; querying Graph to resolve by display name"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists?$select=id,displayName", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return Guid.Empty; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("value", out JsonElement valueEl)) + { + foreach (var item in valueEl.EnumerateArray()) + { + if (item.TryGetProperty("displayName", out JsonElement nameEl) && + nameEl.GetString().Equals(listIdentifier, StringComparison.OrdinalIgnoreCase) && + item.TryGetProperty("id", out JsonElement idEl)) + { + return Guid.Parse(idEl.GetString()); + } + } + } + + return Guid.Empty; + } + + private string ResolveDriveId(Guid siteId, Guid listId) + { + LogDebug($"Resolving drive Id for list {listId} on site {siteId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists/{listId}/drive?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + + private string ResolveDriveItemId(string driveId, string path) + { + var encodedPath = string.Join("/", path.Trim('/').Split('/').Select(Uri.EscapeDataString)); + LogDebug($"Resolving drive item Id for path '{path}' in drive {driveId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/drives/{driveId}/root:/{encodedPath}?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + } +} diff --git a/src/Commands/Apps/RevokeEntraIDAppFilePermission.cs b/src/Commands/Apps/RevokeEntraIDAppFilePermission.cs new file mode 100644 index 000000000..574869648 --- /dev/null +++ b/src/Commands/Apps/RevokeEntraIDAppFilePermission.cs @@ -0,0 +1,174 @@ +using System; +using System.Linq; +using System.Management.Automation; +using System.Text.Json; +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Base.PipeBinds; + +namespace PnP.PowerShell.Commands.Apps +{ + [Cmdlet(VerbsSecurity.Revoke, "PnPEntraIDAppFilePermission")] + [RequiredApiDelegatedOrApplicationPermissions("graph/Sites.FullControl.All")] + public class RevokePnPEntraIDAppFilePermission : PnPGraphCmdlet + { + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string PermissionId; + + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string List; + + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string Path; + + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string FileId; + + [Parameter(Mandatory = false)] + public SitePipeBind Site; + + [Parameter(Mandatory = false)] + public SwitchParameter Force; + + protected override void ExecuteCmdlet() + { + if (!ParameterSpecified(nameof(Path)) && !ParameterSpecified(nameof(FileId))) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Either -Path or -FileId must be specified."), + "MissingFileIdentifier", ErrorCategory.InvalidArgument, null)); + return; + } + + Guid siteId; + if (ParameterSpecified(nameof(Site))) + { + LogDebug($"Using Microsoft Graph to look up site Id for -{nameof(Site)}"); + siteId = Site.GetSiteIdThroughGraph(Connection, AccessToken); + } + else + { + LogDebug($"No -{nameof(Site)} specified, using currently connected site"); + siteId = new SitePipeBind(Connection.Url).GetSiteIdThroughGraph(Connection, AccessToken); + } + + if (siteId == Guid.Empty) + { + LogWarning("Unable to resolve the site Id. Ensure you pass a valid site via -Site or are connected to a site."); + return; + } + + var listId = ResolveListId(siteId, List); + if (listId == Guid.Empty) + { + LogWarning($"Unable to resolve list '{List}' on site {siteId}. Ensure the list exists and you have access."); + return; + } + + var driveId = ResolveDriveId(siteId, listId); + if (string.IsNullOrEmpty(driveId)) + { + LogWarning($"Unable to resolve the drive for list '{List}'. Ensure the list is a document library."); + return; + } + + string driveItemId; + if (ParameterSpecified(nameof(FileId))) + { + driveItemId = FileId; + LogDebug($"Using provided -{nameof(FileId)} directly as drive item Id"); + } + else + { + driveItemId = ResolveDriveItemId(driveId, Path); + if (string.IsNullOrEmpty(driveItemId)) + { + LogWarning($"Unable to resolve file at path '{Path}' in drive {driveId}. Ensure the path is correct and relative to the library root."); + return; + } + } + + if (Force || ShouldContinue("Are you sure you want to revoke the file permission?", string.Empty)) + { + var cleanPermissionId = Uri.EscapeDataString(PermissionId.Trim().Replace(" ", "").Replace("\t", "").Replace("\r", "").Replace("\n", "")); + LogDebug($"Revoking permission {cleanPermissionId} from drive item {driveItemId} in drive {driveId} on site {siteId}"); + Utilities.REST.RestHelper.Delete( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/drives/{driveId}/items/{driveItemId}/permissions/{cleanPermissionId}", + AccessToken); + } + } + + private Guid ResolveListId(Guid siteId, string listIdentifier) + { + if (Guid.TryParse(listIdentifier, out Guid parsedId)) + return parsedId; + + LogDebug($"List identifier '{listIdentifier}' is not a GUID; querying Graph to resolve by display name"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists?$select=id,displayName", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return Guid.Empty; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("value", out JsonElement valueEl)) + { + foreach (var item in valueEl.EnumerateArray()) + { + if (item.TryGetProperty("displayName", out JsonElement nameEl) && + nameEl.GetString().Equals(listIdentifier, StringComparison.OrdinalIgnoreCase) && + item.TryGetProperty("id", out JsonElement idEl)) + { + return Guid.Parse(idEl.GetString()); + } + } + } + + return Guid.Empty; + } + + private string ResolveDriveId(Guid siteId, Guid listId) + { + LogDebug($"Resolving drive Id for list {listId} on site {siteId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists/{listId}/drive?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + + private string ResolveDriveItemId(string driveId, string path) + { + var encodedPath = string.Join("/", path.Trim('/').Split('/').Select(Uri.EscapeDataString)); + LogDebug($"Resolving drive item Id for path '{path}' in drive {driveId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/drives/{driveId}/root:/{encodedPath}?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + } +} diff --git a/src/Commands/Apps/SetEntraIDAppFilePermission.cs b/src/Commands/Apps/SetEntraIDAppFilePermission.cs new file mode 100644 index 000000000..fba6d69ba --- /dev/null +++ b/src/Commands/Apps/SetEntraIDAppFilePermission.cs @@ -0,0 +1,227 @@ +using System; +using System.Linq; +using System.Management.Automation; +using System.Text.Json; +using PnP.PowerShell.Commands.Attributes; +using PnP.PowerShell.Commands.Base; +using PnP.PowerShell.Commands.Base.PipeBinds; +using PnP.PowerShell.Commands.Enums; +using PnP.PowerShell.Commands.Model; +using PnP.PowerShell.Commands.Utilities; + +namespace PnP.PowerShell.Commands.Apps +{ + [Cmdlet(VerbsCommon.Set, "PnPEntraIDAppFilePermission")] + [RequiredApiDelegatedOrApplicationPermissions("graph/Sites.FullControl.All")] + [OutputType(typeof(AzureADAppPermission))] + public class SetPnPEntraIDAppFilePermission : PnPGraphCmdlet + { + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string PermissionId; + + [Parameter(Mandatory = true)] + [ValidateNotNullOrEmpty] + public string List; + + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string Path; + + [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty] + public string FileId; + + [Parameter(Mandatory = false)] + public SitePipeBind Site; + + [Parameter(Mandatory = true)] + [ArgumentCompleter(typeof(EnumAsStringArgumentCompleter))] + public string[] Permissions; + + protected override void ExecuteCmdlet() + { + if (!ParameterSpecified(nameof(Path)) && !ParameterSpecified(nameof(FileId))) + { + ThrowTerminatingError(new ErrorRecord( + new PSArgumentException("Either -Path or -FileId must be specified."), + "MissingFileIdentifier", ErrorCategory.InvalidArgument, null)); + return; + } + + Guid siteId; + if (ParameterSpecified(nameof(Site))) + { + LogDebug($"Using Microsoft Graph to look up site Id for -{nameof(Site)}"); + siteId = Site.GetSiteIdThroughGraph(Connection, AccessToken); + } + else + { + LogDebug($"No -{nameof(Site)} specified, using currently connected site"); + siteId = new SitePipeBind(Connection.Url).GetSiteIdThroughGraph(Connection, AccessToken); + } + + if (siteId == Guid.Empty) + { + LogWarning("Unable to resolve the site Id. Ensure you pass a valid site via -Site or are connected to a site."); + return; + } + + var listId = ResolveListId(siteId, List); + if (listId == Guid.Empty) + { + LogWarning($"Unable to resolve list '{List}' on site {siteId}. Ensure the list exists and you have access."); + return; + } + + var driveId = ResolveDriveId(siteId, listId); + if (string.IsNullOrEmpty(driveId)) + { + LogWarning($"Unable to resolve the drive for list '{List}'. Ensure the list is a document library."); + return; + } + + string driveItemId; + if (ParameterSpecified(nameof(FileId))) + { + driveItemId = FileId; + LogDebug($"Using provided -{nameof(FileId)} directly as drive item Id"); + } + else + { + driveItemId = ResolveDriveItemId(driveId, Path); + if (string.IsNullOrEmpty(driveItemId)) + { + LogWarning($"Unable to resolve file at path '{Path}' in drive {driveId}. Ensure the path is correct and relative to the library root."); + return; + } + } + + var payload = new + { + roles = Permissions.Select(p => p.ToLowerInvariant()).ToArray() + }; + + var cleanPermissionId = Uri.EscapeDataString(PermissionId.Trim().Replace(" ", "").Replace("\t", "").Replace("\r", "").Replace("\n", "")); + LogDebug($"Updating permission {cleanPermissionId} on drive item {driveItemId} to {string.Join(", ", payload.roles)}"); + + var result = Utilities.REST.RestHelper.Patch( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/drives/{driveId}/items/{driveItemId}/permissions/{cleanPermissionId}", + AccessToken, + payload); + + if (result != null) + { + var converted = result.Convert(); + EnrichWithDisplayNames(converted); + WriteObject(converted); + } + } + + private void EnrichWithDisplayNames(AzureADAppPermission permission) + { + if (permission?.Apps == null) return; + + foreach (var app in permission.Apps) + { + if (!string.IsNullOrEmpty(app.DisplayName) || string.IsNullOrEmpty(app.Id)) + continue; + + try + { + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/v1.0/servicePrincipals?$filter=appId eq '{Uri.EscapeDataString(app.Id)}'&$select=displayName,appId", + AccessToken); + + if (string.IsNullOrEmpty(raw)) continue; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("value", out JsonElement valueEl)) + { + var first = valueEl.EnumerateArray().FirstOrDefault(); + if (first.ValueKind == JsonValueKind.Object && + first.TryGetProperty("displayName", out JsonElement nameEl)) + { + app.DisplayName = nameEl.GetString(); + LogDebug($"Resolved display name '{app.DisplayName}' for app {app.Id}"); + } + } + } + catch (Exception ex) + { + LogDebug($"Could not resolve display name for app {app.Id}: {ex.Message}"); + } + } + } + + private Guid ResolveListId(Guid siteId, string listIdentifier) + { + if (Guid.TryParse(listIdentifier, out Guid parsedId)) + return parsedId; + + LogDebug($"List identifier '{listIdentifier}' is not a GUID; querying Graph to resolve by display name"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists?$select=id,displayName", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return Guid.Empty; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("value", out JsonElement valueEl)) + { + foreach (var item in valueEl.EnumerateArray()) + { + if (item.TryGetProperty("displayName", out JsonElement nameEl) && + nameEl.GetString().Equals(listIdentifier, StringComparison.OrdinalIgnoreCase) && + item.TryGetProperty("id", out JsonElement idEl)) + { + return Guid.Parse(idEl.GetString()); + } + } + } + + return Guid.Empty; + } + + private string ResolveDriveId(Guid siteId, Guid listId) + { + LogDebug($"Resolving drive Id for list {listId} on site {siteId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/sites/{siteId}/lists/{listId}/drive?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + + private string ResolveDriveItemId(string driveId, string path) + { + var encodedPath = string.Join("/", path.Trim('/').Split('/').Select(Uri.EscapeDataString)); + LogDebug($"Resolving drive item Id for path '{path}' in drive {driveId}"); + + var raw = Utilities.REST.RestHelper.Get( + Connection.HttpClient, + $"https://{Connection.GraphEndPoint}/beta/drives/{driveId}/root:/{encodedPath}?$select=id", + AccessToken); + + if (string.IsNullOrEmpty(raw)) return null; + + var doc = JsonSerializer.Deserialize(raw); + if (doc.TryGetProperty("id", out JsonElement idEl)) + return idEl.GetString(); + + return null; + } + } +} diff --git a/src/Commands/Enums/AzureADNewListPermissionRole.cs b/src/Commands/Enums/AzureADNewListPermissionRole.cs new file mode 100644 index 000000000..2d27b17ae --- /dev/null +++ b/src/Commands/Enums/AzureADNewListPermissionRole.cs @@ -0,0 +1,29 @@ +namespace PnP.PowerShell.Commands.Enums +{ + /// + /// Defines the roles that can be chosen when granting a new list, list item, or file permission + /// See Graph Reference + /// + public enum AzureADNewListPermissionRole + { + /// + /// Provides the ability to read the metadata and contents of the item + /// + Read, + + /// + /// Provides the ability to read and modify the metadata and contents of the item + /// + Write, + + /// + /// Provides owner-level access to the item + /// + Owner, + + /// + /// Provides full control of the resource + /// + FullControl + } +} diff --git a/src/Commands/Enums/AzureADUpdateListPermissionRole.cs b/src/Commands/Enums/AzureADUpdateListPermissionRole.cs new file mode 100644 index 000000000..21bd335bc --- /dev/null +++ b/src/Commands/Enums/AzureADUpdateListPermissionRole.cs @@ -0,0 +1,29 @@ +namespace PnP.PowerShell.Commands.Enums +{ + /// + /// Defines the roles that can be chosen when updating an existing list, list item, or file permission + /// See Graph Reference + /// + public enum AzureADUpdateListPermissionRole + { + /// + /// Provides the ability to read the metadata and contents of the item + /// + Read, + + /// + /// Provides the ability to read and modify the metadata and contents of the item + /// + Write, + + /// + /// Provides owner-level access to the item + /// + Owner, + + /// + /// Provides full control of the resource + /// + FullControl + } +} diff --git a/src/Commands/Model/EntraIDAppDrivePermissionInternal.cs b/src/Commands/Model/EntraIDAppDrivePermissionInternal.cs new file mode 100644 index 000000000..d10b95580 --- /dev/null +++ b/src/Commands/Model/EntraIDAppDrivePermissionInternal.cs @@ -0,0 +1,60 @@ +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace PnP.PowerShell.Commands.Model +{ + internal class EntraIDAppDrivePermissionInternal + { + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("roles")] + public string[] Roles { get; set; } + + [JsonPropertyName("grantedToV2")] + public DrivePermissionGrantedToV2Internal GrantedToV2 { get; set; } + + [JsonPropertyName("grantedToIdentities")] + public List GrantedToIdentities { get; set; } + + internal AzureADAppPermission Convert() + { + var permission = new AzureADAppPermission + { + Id = Id, + Roles = Roles + }; + + if (GrantedToV2?.Application != null) + { + permission.Apps.Add(new AzureADAppIdentity + { + DisplayName = GrantedToV2.Application.DisplayName, + Id = GrantedToV2.Application.Id + }); + } + else if (GrantedToIdentities != null) + { + foreach (var identity in GrantedToIdentities) + { + if (identity?.Application != null) + { + permission.Apps.Add(new AzureADAppIdentity + { + DisplayName = identity.Application.DisplayName, + Id = identity.Application.Id + }); + } + } + } + + return permission; + } + } + + internal class DrivePermissionGrantedToV2Internal + { + [JsonPropertyName("application")] + public AppIdentityInternal Application { get; set; } + } +}