From 00f8c5a775d6c305fd1404250e05dc27214d0b2e Mon Sep 17 00:00:00 2001 From: Liam Steckler Date: Thu, 12 Jun 2025 10:19:07 -0700 Subject: [PATCH] Add Get/Read operations, better credential refreshing --- ABMPS.psd1 | 158 +++++++++++++++++++------------------- AppleBusinessManager.psm1 | 114 ++++++++++++++++++++++++--- 2 files changed, 182 insertions(+), 90 deletions(-) diff --git a/ABMPS.psd1 b/ABMPS.psd1 index 38e80ec..11fafe2 100644 --- a/ABMPS.psd1 +++ b/ABMPS.psd1 @@ -8,50 +8,50 @@ @{ -# Script module or binary module file associated with this manifest. -RootModule = 'AppleBusinessManager.psm1' + # Script module or binary module file associated with this manifest. + RootModule = 'AppleBusinessManager.psm1' -# Version number of this module. -ModuleVersion = '1.1' + # Version number of this module. + ModuleVersion = '1.2' -# Supported PSEditions -# CompatiblePSEditions = @() + # Supported PSEditions + # CompatiblePSEditions = @() -# ID used to uniquely identify this module -GUID = 'ba7191aa-8343-45d4-941b-52ddc74d7cc7' + # ID used to uniquely identify this module + GUID = 'ba7191aa-8343-45d4-941b-52ddc74d7cc7' -# Author of this module -Author = 'Liam Steckler' + # Author of this module + Author = 'Liam Steckler' -# Company or vendor of this module -CompanyName = 'Unknown' + # Company or vendor of this module + CompanyName = 'Unknown' -# Copyright statement for this module -Copyright = '(c) Liam Steckler. All rights reserved.' + # Copyright statement for this module + Copyright = '(c) Liam Steckler. All rights reserved.' -# Description of the functionality provided by this module -Description = 'Authenticates and gets data from the Apple Business Manager APIs' + # Description of the functionality provided by this module + Description = 'Authenticates and gets data from the Apple Business Manager APIs' -# Minimum version of the PowerShell engine required by this module -PowerShellVersion = '7.0.0' + # Minimum version of the PowerShell engine required by this module + PowerShellVersion = '7.0.0' -# Name of the PowerShell host required by this module -# PowerShellHostName = '' + # Name of the PowerShell host required by this module + # PowerShellHostName = '' -# Minimum version of the PowerShell host required by this module -# PowerShellHostVersion = '' + # Minimum version of the PowerShell host required by this module + # PowerShellHostVersion = '' -# Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# DotNetFrameworkVersion = '' + # Minimum version of Microsoft .NET Framework required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # DotNetFrameworkVersion = '' -# Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. -# ClrVersion = '' + # Minimum version of the common language runtime (CLR) required by this module. This prerequisite is valid for the PowerShell Desktop edition only. + # ClrVersion = '' -# Processor architecture (None, X86, Amd64) required by this module -# ProcessorArchitecture = '' + # Processor architecture (None, X86, Amd64) required by this module + # ProcessorArchitecture = '' -# Modules that must be imported into the global environment prior to importing this module -RequiredModules = @( + # Modules that must be imported into the global environment prior to importing this module + RequiredModules = @( @{ ModuleName = "jwtPS" ModuleVersion = "1.1.3" @@ -59,80 +59,80 @@ RequiredModules = @( } ) -# Assemblies that must be loaded prior to importing this module -# RequiredAssemblies = @() + # Assemblies that must be loaded prior to importing this module + # RequiredAssemblies = @() -# Script files (.ps1) that are run in the caller's environment prior to importing this module. -# ScriptsToProcess = @() + # Script files (.ps1) that are run in the caller's environment prior to importing this module. + # ScriptsToProcess = @() -# Type files (.ps1xml) to be loaded when importing this module -# TypesToProcess = @() + # Type files (.ps1xml) to be loaded when importing this module + # TypesToProcess = @() -# Format files (.ps1xml) to be loaded when importing this module -# FormatsToProcess = @() + # Format files (.ps1xml) to be loaded when importing this module + # FormatsToProcess = @() -# Modules to import as nested modules of the module specified in RootModule/ModuleToProcess -# NestedModules = @() + # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess + # NestedModules = @() -# Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. -FunctionsToExport = '*' + # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. + FunctionsToExport = '*' -# Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. -CmdletsToExport = '*' + # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. + CmdletsToExport = '*' -# Variables to export from this module -VariablesToExport = '*' + # Variables to export from this module + VariablesToExport = '*' -# Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. -AliasesToExport = '*' + # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. + AliasesToExport = '*' -# DSC resources to export from this module -# DscResourcesToExport = @() + # DSC resources to export from this module + # DscResourcesToExport = @() -# List of all modules packaged with this module -# ModuleList = @() + # List of all modules packaged with this module + # ModuleList = @() -# List of all files packaged with this module -# FileList = @() + # List of all files packaged with this module + # FileList = @() -# Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. -PrivateData = @{ + # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. + PrivateData = @{ - PSData = @{ + PSData = @{ - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('api', 'apple', 'applebusinessmanager') + # Tags applied to this module. These help with module discovery in online galleries. + Tags = @('api', 'apple', 'applebusinessmanager') - # A URL to the license for this module. - LicenseUri = 'https://scm.gruezi.net/buckbanzai/abmps/src/branch/main/LICENSE' + # A URL to the license for this module. + LicenseUri = 'https://scm.gruezi.net/buckbanzai/abmps/src/branch/main/LICENSE' - # A URL to the main website for this project. - ProjectUri = 'https://scm.gruezi.net/buckbanzai/abmps' + # A URL to the main website for this project. + ProjectUri = 'https://scm.gruezi.net/buckbanzai/abmps' - # A URL to an icon representing this module. - # IconUri = '' + # A URL to an icon representing this module. + # IconUri = '' - # ReleaseNotes of this module - # ReleaseNotes = '' + # ReleaseNotes of this module + # ReleaseNotes = '' - # Prerelease string of this module - # Prerelease = '' + # Prerelease string of this module + # Prerelease = '' - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false + # Flag to indicate whether the module requires explicit user acceptance for install/update/save + # RequireLicenseAcceptance = $false - # External dependent modules of this module - # ExternalModuleDependencies = @() + # External dependent modules of this module + # ExternalModuleDependencies = @() - } # End of PSData hashtable + } # End of PSData hashtable -} # End of PrivateData hashtable + } # End of PrivateData hashtable -# HelpInfo URI of this module -# HelpInfoURI = '' + # HelpInfo URI of this module + # HelpInfoURI = '' -# Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. -# DefaultCommandPrefix = '' + # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. + # DefaultCommandPrefix = '' } diff --git a/AppleBusinessManager.psm1 b/AppleBusinessManager.psm1 index d553bed..0566b07 100644 --- a/AppleBusinessManager.psm1 +++ b/AppleBusinessManager.psm1 @@ -7,14 +7,30 @@ function Connect-AppleBusinessManager { [string][Parameter(ParameterSetName = 'PrivateKeyAsString', Mandatory)]$PrivateKey, [string][Parameter(ParameterSetName = 'PrivateKeyAsString', Mandatory)]$PrivateKeyId ) - if ($PSCmdlet.ParameterSetName -eq 'EnvironmentVariable') { - if (-not $Env:AppleBusinessManagerClientId -or -not $Env:AppleBusinessManagerPrivateKeyId -or -not $Env:AppleBusinessManagerPrivateKey) { - throw "Client ID, Private Key ID and Private Key environment variables were not set for Apple Business Manager" + + if ($PSBoundParameters.Count -eq 0) { + # Parameters were not bound, check to see if they're already set before attempting to authenticate + Write-Verbose "Checking to see if existing credentials were set at a script scope that can be re-used" + if (-not $Script:ClientId -or -not $Script:PrivateKey -or -not $Script:PrivateKeyId) { + Write-Verbose "Existing credentials not loaded, checking for environment variables" + if ($PSCmdlet.ParameterSetName -eq 'EnvironmentVariable') { + if (-not $Env:AppleBusinessManagerClientId -or -not $Env:AppleBusinessManagerPrivateKeyId -or -not $Env:AppleBusinessManagerPrivateKey) { + throw "Client ID, Private Key ID and Private Key environment variables were not set for Apple Business Manager" + } + $Script:ClientId = $Env:AppleBusinessManagerClientId + $Script:PrivateKey = $Env:AppleBusinessManagerPrivateKey + $Script:PrivateKeyId = $Env:AppleBusinessManagerPrivateKeyId + } } - $Script:ClientId = $Env:AppleBusinessManagerClientId - $Script:PrivateKey = $Env:AppleBusinessManagerPrivateKey - $Script:PrivateKeyId = $Env:AppleBusinessManagerPrivateKeyId } + + if ($PSCmdlet.ParameterSetName -eq 'PrivateKeyAsString') { + # Assign the parameters to script scope so we can reuse them later + $Script:ClientId = $ClientId + $Script:PrivateKey = $PrivateKey + $Script:PrivateKeyId = $PrivateKeyId + } + $Header = @{ 'kid' = $Script:PrivateKeyId } @@ -72,17 +88,93 @@ function Invoke-AppleBusinessManagerPagedApiRequest { ) $Results = New-Object System.Collections.ArrayList while ($Uri) { + Write-Verbose "Making request to $Uri" $Result = Invoke-RestMethod $Uri -Authentication Bearer -Token (Get-AppleBusinessManagerBearerToken) -ErrorAction Stop $Uri = $Result.links.next - $Results.AddRange($Result.data) | Out-Null + $Results.AddRange(@($Result.data)) | Out-Null } return $Results } -function Get-AppleBusinessManagerOrgDevices { - return Invoke-AppleBusinessManagerPagedApiRequest -Uri "https://api-business.apple.com/v1/orgDevices" +function Get-AppleBusinessManagerOrgDevice { + [Alias('Get-AppleBusinessManagerOrgDevices')] + param ( + [Parameter(Mandatory, ParameterSetName = 'Read')][string] $OrgDeviceId, + [Parameter(ParameterSetName = 'Read')] + [Parameter(ParameterSetName = 'List')] + [string[]] $Fields, + [Parameter( ParameterSetName = 'Read')] + [Parameter(ParameterSetName = 'List')] + [int] $Limit + ) + $Uri = switch ($PSCmdlet.ParameterSetName) { + 'Read' { "https://api-business.apple.com/v1/orgDevices/$([System.Web.HttpUtility]::UrlEncode($OrgDeviceId))" } + Default { "https://api-business.apple.com/v1/orgDevices" } + } + $UriBuilder = [System.UriBuilder]::new($Uri) + $QueryString = [System.Web.HttpUtility]::ParseQueryString($UriBuilder.Query) + if ($PSBoundParameters.ContainsKey('Fields')) { + $QueryString.Set('fields[orgDevices]', $Fields -join ',') + } + if ($PSBoundParameters.ContainsKey('Limit')) { + $QueryString.Set('limit', $Limit) + } + $UriBuilder.Query = $QueryString.ToString() + return Invoke-AppleBusinessManagerPagedApiRequest -Uri $UriBuilder.Uri } -function Get-AppleBusinessManagerMdmServers { - return Invoke-AppleBusinessManagerPagedApiRequest -Uri "https://api-business.apple.com/v1/mdmServers" +function Get-AppleBusinessManagerOrgDeviceMdmServerId { + param ( + [Parameter(Mandatory)] + [string] $OrgDeviceId + ) + return Invoke-AppleBusinessManagerPagedApiRequest -Uri "https://api-business.apple.com/v1/orgDevices/$([System.Web.HttpUtility]::UrlEncode($OrgDeviceId))/relationships/assignedServer" +} + +function Get-AppleBusinessManagerOrgDeviceMdmServer { + param ( + [Parameter(Mandatory)] + [string] $OrgDeviceId, + [string[]] $Fields + ) + $UriBuilder = [System.UriBuilder]::new("https://api-business.apple.com/v1/orgDevices/$([System.Web.HttpUtility]::UrlEncode($OrgDeviceId))/assignedServer") + $QueryString = [System.Web.HttpUtility]::ParseQueryString($UriBuilder.Query) + if ($PSBoundParameters.ContainsKey('Fields')) { + $QueryString.Set('fields[mdmServers]', $Fields -join ',') + } + $UriBuilder.Query = $QueryString.ToString() + return Invoke-AppleBusinessManagerPagedApiRequest -Uri $UriBuilder.Uri +} + +function Get-AppleBusinessManagerMdmServer { + [Alias('Get-AppleBusinessManagerMdmServers')] + param ( + [string[]] $Fields, + [int] $Limit + ) + $UriBuilder = [System.UriBuilder]::new("https://api-business.apple.com/v1/mdmServers") + $QueryString = [System.Web.HttpUtility]::ParseQueryString($UriBuilder.Query) + if ($PSBoundParameters.ContainsKey('Fields')) { + $QueryString.Set('fields[mdmServers]', $Fields -join ',') + } + if ($PSBoundParameters.ContainsKey('Limit')) { + $QueryString.Set('limit', $Limit) + } + $UriBuilder.Query = $QueryString.ToString() + return Invoke-AppleBusinessManagerPagedApiRequest -Uri $UriBuilder.Uri +} + +function Get-AppleBusinessManagerMdmServerDevice { + param ( + [Parameter(Mandatory)] + [string] $MdmServerId, + [int] $Limit + ) + $UriBuilder = [System.UriBuilder]::new("https://api-business.apple.com/v1/mdmServers/$([System.Web.HttpUtility]::UrlEncode($MdmServerId))/relationships/devices") + $QueryString = [System.Web.HttpUtility]::ParseQueryString($UriBuilder.Query) + if ($PSBoundParameters.ContainsKey('Limit')) { + $QueryString.Set('limit', $Limit) + } + $UriBuilder.Query = $QueryString.ToString() + return Invoke-AppleBusinessManagerPagedApiRequest -Uri $UriBuilder.Uri } \ No newline at end of file