Skip to content

Commit

Permalink
Updating previously updated apps with PowerShell (#41)
Browse files Browse the repository at this point in the history
* Updating apps previously created with WinTuner
* Updating categories with PowerShell
* Assigning apps with PowerShell
  • Loading branch information
svrooij authored Apr 7, 2024
1 parent 6ccc6da commit c9838bc
Show file tree
Hide file tree
Showing 22 changed files with 2,234 additions and 143 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,9 +141,9 @@ jobs:
id: version
run: |
$version = "${{ github.ref_name }}".Substring(1)
$module = Get-Content -Path src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.psd1
$module = Get-Content -Path src/Svrooij.WinTuner.CmdLets/WinTuner.psd1
$module = $module -replace 'ModuleVersion = ''\d+\.\d+\.\d+''', "ModuleVersion = '$version'"
$module | Set-Content -Path src/Svrooij.WinTuner.CmdLets/Svrooij.WinTuner.CmdLets.psd1
$module | Set-Content -Path src/Svrooij.WinTuner.CmdLets/WinTuner.psd1
- name: 🛠️ Build module
shell: pwsh
Expand Down
88 changes: 87 additions & 1 deletion src/Svrooij.WinTuner.CmdLets/Commands/DeployWtWin32App.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.Extensions.Logging;
using Microsoft.Graph.Beta;
using Svrooij.PowerShell.DependencyInjection;
using System;
using System.IO;
Expand All @@ -14,6 +15,7 @@ namespace Svrooij.WinTuner.CmdLets.Commands;
/// <summary>
/// <para type="synopsis">Create a Win32Lob app in Intune</para>
/// <para type="description">Use this command to upload an intunewin package to Microsoft Intune as a new Win32LobApp.</para>
/// <para type="link" uri="https://wintuner.app/docs/wintuner-powershell/Deploy-WtWin32App">Documentation</para>
/// </summary>
/// <example>
/// <para type="description">Upload a pre-packaged application, from just it's folder, using interactive authentication</para>
Expand Down Expand Up @@ -108,6 +110,12 @@ public class DeployWtWin32App : BaseIntuneCmdlet
HelpMessage = "The folder where the package is")]
public string? PackageFolder { get; set; }

/// <summary>
/// <para type="description">The graph id of the app to supersede</para>
/// </summary>
[Parameter(DontShow = true, HelpMessage = "Graph ID of the app to supersede", Mandatory = false)]
public string? GraphId { get; set; }

[ServiceDependency]
private ILogger<DeployWtWin32App>? logger;

Expand Down Expand Up @@ -149,7 +157,85 @@ public override async Task ProcessRecordAsync(CancellationToken cancellationToke
logger?.LogInformation("Uploading Win32App {DisplayName} to Intune with file {IntuneWinFile}", App!.DisplayName, IntuneWinFile);
var graphServiceClient = CreateGraphServiceClient(httpClient!);
var newApp = await graphAppUploader!.CreateNewAppAsync(graphServiceClient, App, IntuneWinFile!, LogoPath, cancellationToken);
logger?.LogInformation("Created Win32App {DisplayName} with id {Id}", newApp!.DisplayName, newApp.Id);
logger?.LogInformation("Created Win32App {DisplayName} with id {appId}", newApp!.DisplayName, newApp.Id);

// Check if we need to supersede an app
if (GraphId is not null)
{
await SupersedeApp(logger!, graphServiceClient, newApp!.Id!, GraphId, cancellationToken);
}

WriteObject(newApp!);
}

/// <summary>
/// Supersede an app.
/// </summary>
/// <remarks>
/// 1. Load the old app
/// 2. Update relationships of the new app to supersede the old app
/// 3. Copy categories from the old app to the new app
/// 4. Copy assignments from the old app to the new app
/// 5. Remove assignments from the old app
/// </remarks>
/// <param name="logger"></param>
/// <param name="graphServiceClient"></param>
/// <param name="newAppId"></param>
/// <param name="oldAppId"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
private static async Task SupersedeApp(ILogger logger, GraphServiceClient graphServiceClient, string newAppId, string oldAppId, CancellationToken cancellationToken)
{
logger?.LogDebug("Loading old app {oldAppId} to superseed", oldAppId);
var oldApp = await graphServiceClient.DeviceAppManagement.MobileApps[oldAppId].GetAsync(req =>
{
req.QueryParameters.Expand = new string[] { "categories", "assignments" };
}, cancellationToken);

if (oldApp is GraphModels.Win32LobApp oldWin32App)
{
logger?.LogInformation("Superseeding app {oldAppId} with {appId}", oldAppId, newAppId);
var batch = new Microsoft.Graph.BatchRequestContentCollection(graphServiceClient);
// Add supersedence relationship to new app
await batch.AddBatchRequestStepAsync(graphServiceClient.DeviceAppManagement.MobileApps[newAppId].UpdateRelationships.ToPostRequestInformation(new Microsoft.Graph.Beta.DeviceAppManagement.MobileApps.Item.UpdateRelationships.UpdateRelationshipsPostRequestBody
{
Relationships = new()
{
new GraphModels.MobileAppSupersedence
{
// TODO Should the SupersedenceType be Update or Replace, maybe configureable?
SupersedenceType = GraphModels.MobileAppSupersedenceType.Update,
TargetId = oldAppId!
}
}
}));

// Copy categories from old app to new app
if (oldWin32App.Categories is not null && oldWin32App.Categories.Count > 0)
{
foreach (var c in oldWin32App.Categories)
{
await batch.AddBatchRequestStepAsync(graphServiceClient.Intune_AddCategoryToApp_RequestInfo(newAppId, c.Id!));
}
}

// Copy assignments from old app to new app
if (oldWin32App.Assignments is not null && oldWin32App.Assignments.Count > 0)
{
await batch.AddBatchRequestStepAsync(graphServiceClient.DeviceAppManagement.MobileApps[newAppId].Assign.ToPostRequestInformation(new Microsoft.Graph.Beta.DeviceAppManagement.MobileApps.Item.Assign.AssignPostRequestBody
{
MobileAppAssignments = oldWin32App.Assignments
}));

// Remove assignments from old app
await batch.AddBatchRequestStepAsync(graphServiceClient.DeviceAppManagement.MobileApps[oldAppId].Assign.ToPostRequestInformation(new Microsoft.Graph.Beta.DeviceAppManagement.MobileApps.Item.Assign.AssignPostRequestBody
{
MobileAppAssignments = new System.Collections.Generic.List<GraphModels.MobileAppAssignment>()
}));
}

// Execute batch
await graphServiceClient.Batch.PostAsync(batch, cancellationToken);
}
}
}
99 changes: 99 additions & 0 deletions src/Svrooij.WinTuner.CmdLets/Commands/GetWtWin32Apps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using Microsoft.Extensions.Logging;
using Svrooij.PowerShell.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using WingetIntune.Graph;

namespace Svrooij.WinTuner.CmdLets.Commands;

/// <summary>
/// <para type="synopsis">Get all apps from Intune packaged by WinTuner</para>
/// <para type="description">Load apps from Tenant and filter based on Update Availabe, pipe to `New-IntuneWinPackage`</para>
/// <para type="link" uri="https://wintuner.app/docs/wintuner-powershell/Get-WtWin32Apps">Documentation</para>
/// </summary>
/// <example>
/// <para type="description">Get all apps that have updates, using interactive authentication</para>
/// <code>Get-WtWin32Apps -Update $true -Username [email protected]</code>
/// </example>
[Cmdlet(VerbsCommon.Get, "WtWin32Apps")]
[OutputType(typeof(Models.WtWin32App[]))]
public class GetWtWin32Apps : BaseIntuneCmdlet
{
/// <summary>
/// <para type="description">Filter based on UpdateAvailable</para>
/// </summary>
[Parameter(Mandatory = false,
HelpMessage = "Filter based on UpdateAvailable")]
public bool? Update { get; set; }

/// <summary>
/// <para type="description">Filter based on SupersedingAppCount</para>
/// </summary>
[Parameter(Mandatory = false,
HelpMessage = "Filter based on SupersedingAppCount")]
public bool? Superseded { get; set; }

/// <summary>
/// <para type="description">Filter based on SupersedingAppCount</para>
/// </summary>
[Parameter(Mandatory = false,
HelpMessage = "Filter based on SupersedingAppCount")]
public bool? Superseding { get; set; }

[ServiceDependency]
private ILogger<GetWtWin32Apps>? logger;

[ServiceDependency]
private HttpClient? httpClient;

[ServiceDependency]
private Winget.CommunityRepository.WingetRepository? repo;

/// <inheritdoc/>
public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
{
ValidateAuthenticationParameters();
logger?.LogInformation("Getting list of published apps");

var graphServiceClient = CreateGraphServiceClient(httpClient!);
var apps = await graphServiceClient.DeviceAppManagement.MobileApps.GetWinTunerAppsAsync(cancellationToken);

List<Models.WtWin32App> result = new();

foreach (var app in apps)
{
var version = await repo!.GetLatestVersion(app.PackageId, cancellationToken);
result.Add(new Models.WtWin32App
{
GraphId = app.GraphId,
PackageId = app.PackageId,
Name = app.Name,
CurrentVersion = app.CurrentVersion,
SupersededAppCount = app.SupersededAppCount,
SupersedingAppCount = app.SupersedingAppCount,
LatestVersion = version,
});
}

if (Update.HasValue)
{
result = result.Where(x => x.IsUpdateAvailable == Update.Value).ToList();
}

if (Superseded.HasValue)
{
result = result.Where(x => x.SupersedingAppCount > 0 == Superseded.Value).ToList();
}

if (Superseding.HasValue)
{
result = Superseding.Value ? result.Where(x => x.SupersededAppCount > 0).ToList() : result.Where(x => x.SupersededAppCount == 0).ToList();
}

WriteObject(result);
}
}
6 changes: 1 addition & 5 deletions src/Svrooij.WinTuner.CmdLets/Commands/NewWtWingetPackage.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,15 @@
using Microsoft.Extensions.Logging;
using Svrooij.PowerShell.DependencyInjection;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Management.Automation;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Svrooij.WinTuner.CmdLets.Commands;
/// <summary>
/// <para type="synopsis">Create intunewin file from Winget installer</para>
/// <para type="description">Downloads the installer for the package and creates an `.intunewin` file for uploading in Intune.</para>
/// <para type="link" uri="https://wintuner.app/">Documentation</para>
/// <para type="link" uri="https://wintuner.app/docs/wintuner-powershell/New-WtWingetPackage">Documentation</para>
/// </summary>
/// <example>
/// <para type="description">Package all files in C:\Temp\Source, with setup file ..\setup.exe to the specified folder</para>
Expand Down
70 changes: 70 additions & 0 deletions src/Svrooij.WinTuner.CmdLets/Commands/RemoveWtWin32App.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Microsoft.Extensions.Logging;
using Svrooij.PowerShell.DependencyInjection;
using System.Collections.Generic;
using System.Linq;
using System.Management.Automation;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace Svrooij.WinTuner.CmdLets.Commands;

/// <summary>
/// <para type="synopsis">Remove an app from Intune</para>
/// <para type="description">Will remove the relationships (if any) first and then remove the app.</para>
/// <para type="link" uri="https://wintuner.app/docs/wintuner-powershell/Remove-WtWin32App">Documentation</para>
/// </summary>
/// <example>
/// <para type="description">Delete a single app by ID with interactive authentication</para>
/// <code>Remove-WtWin32App -AppId "1450c17d-aee5-4bef-acf9-9e0107d340f2" -Username [email protected]</code>
/// </example>
[Cmdlet(VerbsCommon.Remove, "WtWin32App")]
public class RemoveWtWin32App : BaseIntuneCmdlet
{
/// <summary>
/// <para type="description">Id of the app in Intune</para>
/// </summary>
[Parameter(Mandatory = true,
HelpMessage = "Id of the app in Intune")]
public string? AppId { get; set; }

[ServiceDependency]
private ILogger<RemoveWtWin32App>? logger;

[ServiceDependency]
private HttpClient? httpClient;

/// <inheritdoc/>
public override async Task ProcessRecordAsync(CancellationToken cancellationToken)
{
ValidateAuthenticationParameters();
logger?.LogInformation("Removing app {appId} from Intune", AppId);

var graphServiceClient = CreateGraphServiceClient(httpClient!);

// Load the app to get the relationships
var app = await graphServiceClient.DeviceAppManagement.MobileApps[AppId].GetAsync(cancellationToken: cancellationToken);

if (app?.SupersedingAppCount > 0) // This means deletion will fail
{
// Load the relationships to see if we can remove them
var relationships = await graphServiceClient.DeviceAppManagement.MobileApps[AppId].Relationships.GetAsync(cancellationToken: cancellationToken);

foreach (var relationship in relationships!.Value!.Where(r => r.TargetType == Microsoft.Graph.Beta.Models.MobileAppRelationshipType.Parent))
{
logger?.LogInformation("Updating relations of app {parentAppId} to remove {appId}", relationship.TargetId, AppId);
var parentRelationShips = await graphServiceClient.DeviceAppManagement.MobileApps[relationship.TargetId].Relationships.GetAsync(cancellationToken: cancellationToken);
await graphServiceClient.DeviceAppManagement.MobileApps[relationship.TargetId].UpdateRelationships.PostAsync(new Microsoft.Graph.Beta.DeviceAppManagement.MobileApps.Item.UpdateRelationships.UpdateRelationshipsPostRequestBody
{
Relationships = parentRelationShips?.Value?.Where(r => r.TargetId != AppId).ToList() ?? new List<Microsoft.Graph.Beta.Models.MobileAppRelationship>()
}, cancellationToken: cancellationToken);
}

logger?.LogInformation("Relationship removed, waiting 2 seconds before removing app");
await Task.Delay(2000, cancellationToken);
}

await graphServiceClient.DeviceAppManagement.MobileApps[AppId].DeleteAsync(cancellationToken: cancellationToken);
logger?.LogInformation("App {appId} removed from Intune", AppId);
}
}
Loading

0 comments on commit c9838bc

Please sign in to comment.