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; } + } +}