KnowIT.MSGraph.psm1


#region === Source functions ===

### Source file: 'Add-EntraDirectoryRoleMember.ps1' ###
function Add-EntraDirectoryRoleMember {

    [Alias('Add-AzAdDirectoryRoleMember')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$PrincipalId,

        [Parameter(Mandatory, ParameterSetName = 'RoleId')]
        [ValidateNotNullOrEmpty()]
        [string]$Id,

        [Parameter(Mandatory, ParameterSetName = 'RoleTemplateId')]
        [ValidateNotNullOrEmpty()]
        [string]$RoleTemplateId,

        [Parameter(Mandatory, ParameterSetName = 'RoleName')]
        [Alias('RoleName')]
        [string]$Name
    )

    try {
        Update-CallerPreference

        [void]$PSBoundParameters.Remove('PrincipalId')
        $role = Get-EntraDirectoryRole @PSBoundParameters
        if(!$role) {
            throw "No esta definido el $($PSCmdlet.ParameterSetName) [$($Id + $RoleTemplateId + $Name)] en el Directorio '$((Get-AzContext).Tenant.Name)'"
        }
        $request = @{
            principalId = $PrincipalId
            roleDefinitionId = $role.templateId
            directoryScopeId = '/'
        }
        Invoke-MsGraph 'roleManagement/directory/roleAssignments' -Method POST -Body $request -ErrorAction Stop
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'ConvertFrom-MsGraphResponse.ps1' ###
filter ConvertFrom-MsGraphResponse {
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [PSTypeNameAttribute('KnowIT.MsGraph.Response')] $InputObject
    )

    $statusCode = $InputObject.StatusCode
    if($InputObject.Success) {
        return $InputObject.Value
    }

    if($statusCode -eq 404) {
        Write-Verbose "Response MsGraph: No se obtuvo ningun registro"
        return
    }

    Write-Error -ErrorRecord ([Management.Automation.ErrorRecord]::new(
        [Exception]$InputObject.Error.message, $InputObject.Error.code,
        [Management.Automation.ErrorCategory]::InvalidResult, $InputObject))
}

### Source file: 'Get-EntraAppExpiringCreds.ps1' ###
function Get-EntraAppExpiringCreds {
    param(
        [int]$Days = 15
    )

    try {
        Update-CallerPreference

        $dateLimit = (Get-Date).AddDays($Days)
        Invoke-MsGraph 'applications?$select=id,appId,displayName,passwordCredentials,keyCredentials' -ErrorAction Stop
        | Map-Object {
            $app = $_
            foreach($credential in $app.passwordCredentials + $app.keyCredentials) {
                if($credential -and $credential.endDateTime -le $dateLimit) {
                    [PSCustomObject]@{
                        Name          = $app.displayName
                        ApplicationId = $app.appId
                        ObjectId      = $app.id
                        Credential    = $credential.customKeyIdentifier ? $credential.displayName : 'Password'
                        Expiration    = $credential.endDateTime
                    }
                }
            }
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Get-EntraApplication.ps1' ###
function Get-EntraApplication {
    param (
        [Parameter(Position = 0, ParameterSetName = 'ById')]
        [ValidateNotNullOrEmpty()]
        [Alias('ObjectId')]
        [string]$Id,

        [Parameter(Mandatory, ParameterSetName = 'ByApplicationId')]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationId,

        [Parameter(Mandatory, ParameterSetName = 'ByUniqueName')]
        [ValidateNotNullOrEmpty()]
        [string]$UniqueName,

        [Parameter(Mandatory, ParameterSetName = 'ByServicePrincipal')]
        [ValidateNotNullOrEmpty()]
        [string]$ServicePrincipalId
    )

    try {
        Update-CallerPreference

        Write-Verbose "Procesando parametros: '$($PSCmdlet.ParameterSetName)'"
        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                Invoke-MsGraph "applications/$Id"
                break
            }
            'ByApplicationId' {
                Invoke-MsGraph "applications(appId='$ApplicationId')"
                break
            }
            'ByUniqueName' {
                Invoke-MsGraph "applications(uniqueName='$UniqueName')"
                break
            }
            'ByServicePrincipal' {
                $sp = Invoke-MsGraph "servicePrincipals/$ServicePrincipalId"
                if($sp) {
                    Invoke-MsGraph "applications(appId='$($sp.appId)')"
                }
                break
            }
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Get-EntraDirectoryRole.ps1' ###
function Get-EntraDirectoryRole {

    [CmdletBinding()]
    [Alias('Get-AzAdDirectoryRole')]
    param(
        [Parameter(ParameterSetName = 'RoleId')]
        [ValidateNotNullOrEmpty()]
        [Alias('RoleId')]
        [string]$Id,

        [Parameter(Mandatory, ParameterSetName = 'RoleTemplateId')]
        [ValidateNotNullOrEmpty()]
        [string]$RoleTemplateId,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'RoleName')]
        [ValidateNotNullOrEmpty()]
        [Alias('DisplayName')]
        [string]$Name
    )

    try {
        Update-CallerPreference

        Write-Verbose "Procesando parametros: '$($PSCmdlet.ParameterSetName)'"
        switch ($PSCmdlet.ParameterSetName) {
            'RoleId' {
                $path = "directoryRoles/$Id"
                break
            }
            'RoleTemplateId' {
                $path = "directoryRoles(roleTemplateId='$RoleTemplateId')"
                break
            }
            'RoleName' {
                $path = "directoryRoles?`$filter=displayName eq '$Name'"
                break
            }
        }

        Invoke-MsGraph $path
        | Map-Object {
            [PSCustomObject]@{
                id          = $_.id
                templateId  = $_.roleTemplateId
                name        = $_.displayName
                description = $_.description
            }
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Get-EntraDirectoryRoleMembers.ps1' ###
function Get-EntraDirectoryRoleMembers {

    [CmdletBinding(DefaultParameterSetName = 'RoleName')]
    [Alias('Get-AzAdDirectoryRoleMembers')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'RoleId')]
        [ValidateNotNullOrEmpty()]
        [Alias('RoleId')]
        [string]$Id,

        [Parameter(Mandatory, ParameterSetName = 'RoleTemplateId')]
        [ValidateNotNullOrEmpty()]
        [string]$RoleTemplateId,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'RoleName')]
        [ValidateNotNullOrEmpty()]
        [Alias('DisplayName')]
        [string]$Name
    )

    try {
        Update-CallerPreference

        $roles = Get-EntraDirectoryRole @PSBoundParameters
        foreach($role in $roles) {
            Invoke-MsGraph "roleManagement/directory/roleAssignments?`$filter=roleDefinitionId eq '$($role.TemplateId)'&`$expand=principal"
            | Map-Object {
                [PSCustomObject]@{
                    role           = $role.name
                    principalId    = $_.principal.id
                    principalName  = $_.principal.'@odata.type' -eq '#microsoft.graph.user' ? $_.principal.userPrincipalName : "appId=$($_.principal.appId)"
                    displayName    = $_.principal.displayName
                    roleId         = $role.id
                    roleTemplateId = $role.templateId
                    assignmentId   = $_.id
                    enabled        = $_.principal.accountEnabled
                    principalType  = $_.principal.'@odata.type'.Split('.')[-1] #-replace "^(.)", { $_.Groups[1].Value.ToUpper() }
                    #Pricipal = $_.principal
                }
            }
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Get-EntraGroup.ps1' ###
function Get-EntraGroup {

    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = 'ById')]
        [ValidateNotNullOrEmpty()]
        [Alias('ObjectId')]
        [string]$Id,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByDisplayName')]
        [ValidateNotNull()]
        [string]$DisplayName
    )

    try {
        Update-CallerPreference

        Write-Verbose "Procesando parametros: '$($PSCmdlet.ParameterSetName)'"
        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                Invoke-MsGraph "groups/$Id"
             }
             'ByDisplayName' {
                Invoke-MsGraph "groups?`$filter=displayName eq '$DisplayName'"
             }
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Get-EntraServicePrincipal.ps1' ###
function Get-EntraServicePrincipal {
    param (
        [Parameter(Position = 0, ParameterSetName = 'ById')]
        [ValidateNotNullOrEmpty()]
        [Alias('ObjectId')]
        [string]$Id,

        [Parameter(Mandatory, ParameterSetName = 'ByApplicationId')]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationId
    )

    try {
        Update-CallerPreference

        Write-Verbose "Procesando parametros: '$($PSCmdlet.ParameterSetName)'"
        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                Invoke-MsGraph "servicePrincipals/$Id"
                break;
            }
            'ByApplicationId' {
                Invoke-MsGraph "servicePrincipals(appId='$ApplicationId')"
                break;
            }
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Get-EntraUser.ps1' ###
function Get-EntraUser {

    [CmdletBinding()]
    param (
        [Parameter(ParameterSetName = 'ById')]
        [Alias('ObjectId')]
        [string]$Id,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByUPN')]
        [string]$UserPrincipalName
    )

    try {
        Update-CallerPreference

        Write-Verbose "Procesando parametros: '$($PSCmdlet.ParameterSetName)'"
        switch ($PSCmdlet.ParameterSetName) {
            'ById' {
                Invoke-MsGraph "users/$Id"
            }
            'ByUPN' {
                Invoke-MsGraph "users?`$filter=userPrincipalName eq '$UserPrincipalName'"
            }
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Grant-EntraAppConsent.ps1' ###

function Grant-EntraAppConsent {

    param(
        [Parameter(Mandatory)]
        [string]$ClientId,

        [Parameter(Mandatory)]
        [ValidateCount(1, 100)]
        [string]$Scopes,

        [Alias('API')]
        [string]$ApplicationId = '00000003-0000-0000-c000-000000000000', # default Microsoft Graph AppId,

        [Parameter(Mandatory, ParameterSetName = 'ByUser')]
        [string]$UserPrincipalName,

        [Parameter(Mandatory, ParameterSetName = 'AllUsers')]
        [switch]$AllUsers
    )

    try {
        Update-CallerPreference

        $clientSp = GetAppServicePrincipal -ApplicationId $ClientId
        $applicationSp = GetAppServicePrincipal -ApplicationId $ApplicationId
        $params = @{
            clientId    = $clientSp.Id
            resourceId  = $applicationSp.Id
            scope       = $Scopes
        }

        if($PSCmdlet.ParameterSetName -eq 'AllUsers') {
            $params.consentType = 'AllPrincipals'
            Write-Verbose "Concede permisos [$Scopes] de '$($applicationSp.displayName)' a todos los usuarios de la aplicación '$($clientSp.displayName)'"
        }
        elseif($PSCmdlet.ParameterSetName -eq 'ByUser') {
            $principalId = (Get-EntraUser -UserPrincipalName $UserPrincipalName).Id
            if(!$principalId) {
                $azureCtx = Get-AzContext
                throw "No se encontro el usuario [$UserPrincipalName] en el directorio '$($azureCtx.Tenant.Name)'"
            }
            $params.consentType = 'Principal'
            $params.principalId = $principalId
            Write-Verbose "Concede permisos [$Scopes] de '$($applicationSp.displayName)' al usuario $UserPrincipalName en la aplicación '$($clientSp.displayName)'"
        }
        Invoke-MsGraph 'oauth2PermissionGrants' -Method Post -Body $params -ErrorAction Stop
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Grant-EntraAppRoleAssignment.ps1' ###

function Grant-EntraAppRoleAssignment {

    param (
        [Parameter(Mandatory)]
        [string]$ApplicationId,

        [Parameter(Mandatory)]
        [ValidateCount(1, 100)]
        [string[]]$Roles,

        [Parameter(Mandatory, ParameterSetName = 'User')]
        [string]$UserPrincipalName,

        [Parameter(Mandatory, ParameterSetName = 'Group')]
        [string]$GroupName,

        [Parameter(Mandatory, ParameterSetName = 'App')]
        [string]$ClientId
    )

    try {
        Update-CallerPreference

        $applicationSp = GetAppServicePrincipal -ApplicationId $ApplicationId -Properties 'appRoles'

        $principalId = switch ($PSCmdlet.ParameterSetName) {
            'App'   { (GetAppServicePrincipal -ApplicationId $ClientId).id }
            'User'  { (Get-EntraUser -UserPrincipalName $UserPrincipalName).id }
            'Group' {
                $group = Get-EntraGroup -DisplayName $GroupName
                if($group -and -not $group.securityEnabled) {
                    throw "El grupo [$GroupName] no existe o no tiene habilitada la propiedad 'SecurityEnabled'"
                }
                $group.id
            }
        }
        if(!$principalId) {
            $azureCtx = Get-AzContext
            throw "No se encontro el objeto [$($ClientId + $UserPrincipalName + $GroupName)] en el directorio '$($azureCtx.Tenant.Name)'"
        }

        $memberType = $PSCmdlet.ParameterSetName -eq 'App' ? 'Application' : 'User'
        $appRoles = $applicationSp.appRoles.Where(
            { $_.value -in $Roles -and $_.allowedMemberTypes -contains $memberType })

        if(!$appRoles) {
            throw "La Aplicacion [$($applicationSp.displayName)] no contiene ninguno de los roles solicitados"
        }

        $path = 'servicePrincipals/{0}/appRoleAssignedTo' -f $applicationSp.id
        $params = @{
            principalId = $principalId
            resourceId  = $applicationSp.id
            appRoleId   = $null
        }

        Write-Verbose "Asignando los roles [$($appRoles.value -join ',')] de la aplicacion '$($applicationSp.displayName)' al PrincipalId: $principalId"
        foreach($roleId in $appRoles.id) {
            $params.appRoleId = $roleId
            Invoke-MsGraph $path -Method Post -Body $params -ErrorAction Stop
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Invoke-MsGraph.ps1' ###
function Invoke-MsGraph {

    [CmdletBinding()]
    [Alias('msgr')]
    param(
        [Parameter(Mandatory, Position = 0)]
        [string]$Path,

        [ValidateSet('GET', 'POST', 'PATCH', 'PUT', 'DELETE')]
        [string]$Method = 'GET',

        [object]$Body,

        [hashtable]$Headers = @{},

        [switch]$DetailResponse,

        [switch]$Beta
    )

    try {
        Update-CallerPreference $PSCmdlet -Skip Verbose

        $token = GetGraphToken
        $Headers.Authorization = "Bearer $token"
        $path = $Path.TrimStart('/')
        $version = $Beta ? 'beta' : 'v1.0'
        $restParams = @{
            Method      = $Method
            Uri         = "https://graph.microsoft.com/$version/$path"
            Headers     = $Headers
            Body        = $Body ? (ConvertTo-Json $Body -Depth 4) : $null
            ContentType = 'application/json'
            Verbose     = $false
        }
        Write-Verbose "Request MsGraph: $($restParams.Method.ToUpper()) /$version/$path"
        if($Body) { Write-Debug "Body: $($restParams.Body)" }

        $result = Invoke-RestMethod @restParams -SkipHttpErrorCheck -StatusCodeVariable statusCode

        if(!$result.error) {
            Write-Verbose "Response MsGraph: [$statusCode] $([Net.HttpStatusCode]$statusCode)"
            $response = [PSCustomObject]@{
                PSTypeName = 'KnowIT.MsGraph.Response'
                Success    = $true
                StatusCode = $statusCode
                Value      = $result.Value ?? $result
            }
        }
        else {
            Write-Debug 'Procesando error en la respuesta del API...'
            $response = [PSCustomObject]@{
                PSTypeName = 'KnowIT.MsGraph.Response'
                Success    = $false
                StatusCode = $statusCode
                Error      = [ordered]@{
                    code = $result.error.innerError?.code ?? $result.error.code
                    message = $result.error.innerError?.message ?? $result.error.message
                    details = $result.error.innerError | Select-Object -Exclude 'code', 'message', 'details'
                    additionalErrors = ($result.error.details ?? @()) + ($result.error.innerError.details ?? @())
                }
            }
            Write-Verbose "Response MsGraph: [$statusCode] $($response.Error.code) - $($response.Error.message)"
        }

        if($DetailResponse) {
            $response
        }
        else {
            $response | ConvertFrom-MsGraphResponse
        }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Register-EntraApplication.ps1' ###
function Register-EntraApplication {
    param (
        [Parameter(Mandatory, Position = 0)]
        [ValidatePattern('^[a-z]+(-[a-z0-9]+)*$')]
        [string]$Name,

        [string]$DisplayName = $Name,

        [hashtable]$AppProperties = @{},

        [switch]$WithServicePrincipal
    )

    try {
        Update-CallerPreference

        $AppProperties.uniqueName = $Name.ToLower()
        $AppProperties.displayName = $DisplayName
        $graphParams = @{
            Method  = 'PATCH'
            Path    = "applications(uniqueName='$Name')"
            Body    = $AppProperties
            Headers = @{ Prefer = 'create-if-missing' }
        }
        $graphResult = Invoke-MsGraph @graphParams -DetailResponse

        $app = switch ($graphResult.StatusCode) {
            201 {
                Write-Verbose "Aplicacion [$Name] creada exitosamente (AppId = '$($graphResult.Value.appId)')"
                $graphResult.Value
                break
            }
            204 {
                Write-Verbose "Aplicacion [$Name] existente. Actualizada de manera exitosa!"
                Invoke-MsGraph $graphParams.Path
                break
            }
            400 {
                foreach($e in $graphResult.Error.additionalErrors) {
                    if($e.code -eq 'ObjectConflict' -and $e.target -eq 'uniqueName') {
                        #TODO: Recuperar? Eliminar permanentemente
                        throw 'La aplicacion ha sido eliminada??'
                    }
                }
                $graphResult | ConvertFrom-MsGraphResponse
            }
            default {
                # En este caso el resultado solo puede ser no exitoso por lo que se generaria el un error de terminacion
                $graphResult | ConvertFrom-MsGraphResponse
                throw # Realmente nunca deberia llegar aqui
            }
        }

        if($WithServicePrincipal){
            $sp = RegisterAppServicePrincipal $app.appId
            $app | Add-Member -MemberType NoteProperty servicePrincipalId -Value $sp.id
        }

        return $app
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Register-EntraServicePrincipal.ps1' ###
function Register-EntraServicePrincipal {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory, ParameterSetName = 'AppId')]
        [string]$ApplicationId,

        [Parameter(Mandatory, Position = 0, ParameterSetName = 'Name')]
        [string]$Name,

        [Parameter(ParameterSetName = 'Name')]
        [hashtable]$AppProperties = @{}
    )

    try {
        Update-CallerPreference

        if($PSCmdlet.ParameterSetName -eq 'Name') {
            $appName = $Name.Replace('/', '-').ToLower()
            $app = Register-EntraApplication $appName -DisplayName $Name -AppProperties $AppProperties
            $ApplicationId = $app.appId
        }

        RegisterAppServicePrincipal $ApplicationId
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Remove-EntraDirectoryRoleMember.ps1' ###
function Remove-EntraDirectoryRoleMember {

    [Alias('Remove-AzAdDirectoryRoleMember')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$PrincipalId,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$RoleTemplateId
    )

    try {
        Update-CallerPreference

        $roleAssignment =
            Invoke-MsGraph "roleManagement/directory/roleAssignments?`$select=id&`$filter=principalId eq '$PrincipalId' and roleDefinitionId eq '$RoleTemplateId'"

        if(!$roleAssignment.id) {
            throw "El PrincipalId '$PrincipalId' no tiene asignado el rol [$RoleTemplateId]"
        }

        Invoke-MsGraph "roleManagement/directory/roleAssignments/$($roleAssignment.id)" -Method DELETE
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Set-EntraAppCredential.ps1' ###
function Set-EntraAppCredential {

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByApplicationId')]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationId,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [int]$ExpireMonths = 12
    )

    try {
        Update-CallerPreference

        $app = GetApplication $ApplicationId -Properties 'passwordCredentials'

        $removePath = "applications/$($app.id)/removePassword"
        $app.passwordCredentials.
            Where({ $_.displayName -eq $Name }).
            ForEach({ Invoke-MsGraph $removePath -Body @{ keyId = $_.keyId } -Method POST })

        $credential = @{
            passwordCredential = @{
                displayName = $Name
                endDateTime = [datetime]::UtcNow.AddMonths($ExpireMonths).ToString('o')
            }
        }

        Write-Verbose "Generando nueva credencial '$Name'"
        Invoke-MsGraph "applications/$($app.id)/addPassword" -Body $credential -Method POST
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Set-EntraAppFederatedIdentity.ps1' ###
function Set-EntraAppFederatedIdentity {
    param (
        [Parameter(Mandatory, Position = 0, ParameterSetName = 'ByApplicationId')]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationId,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Issuer,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Subject,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string[]]$Audience = @('api://AzureADTokenExchange')
    )

    try {
        Update-CallerPreference

        $app = GetApplication $ApplicationId
        $path = "applications/$($app.id)/federatedIdentityCredentials(name='$Name')"
        $body = @{
            name      = $Name
            issuer    = $Issuer
            subject   = $Subject
            audiences = $Audience
        }
        $null = Invoke-MsGraph $path -Method PATCH -Body $body -Headers @{ Prefer = 'create-if-missing' }
    }
    catch {
        $PSCmdlet.WriteError($_)
    }
}

### Source file: 'Applications.ps1' ###

function GetApplication ([string]$ApplicationId, [string[]]$Properties)
{
    $propList = ('id', 'appId', 'displayName' + $Properties) -join ','
    $app = Invoke-MsGraph "applications(appId='$ApplicationId')?`$select=$propList"
    if(!$app) {
        $azureCtx = Get-AzContext
        throw "No se encontro la aplicación [$ApplicationId] en el directorio '$($azureCtx.Tenant.Name)'"
    }
    $app
}



### Source file: 'KnowIT.ModuleHelpers.ps1' ###
function Update-CallerPreference {
    # https://devblogs.microsoft.com/scripting/weekend-scripter-access-powershell-preference-variables/
    param(
        [ValidateNotNull()]
        [PSTypeName('System.Management.Automation.PSScriptCmdlet')]$ScriptCmdlet = (Get-Variable PSCmdlet -Scope 1 -ValueOnly),

        [ValidateSet('ErrorAction', 'Warning', 'Verbose', 'Debug', 'Information', 'Progress', 'Confirm', 'WhatIf')]
        [string[]]$Skip
    )

    $commonParameters = 'ErrorAction', 'Warning', 'Verbose', 'Debug', 'Information', 'Progress', 'Confirm', 'WhatIf'

    $invocation = $ScriptCmdlet.MyInvocation
    $commandDebug = $invocation.BoundParameters.ContainsKey('Debug')

    Write-Debug "Updating [$($invocation.MyCommand)] Preference variables:" -Debug:$commandDebug
    foreach($p in $commonParameters) {
        if($invocation.BoundParameters.ContainsKey($p)) {
            continue
        }
        $var = "${p}Preference"

        if($p -eq 'ErrorAction') {
            $val = 'Stop'
            $scope = 'Forced'
        }
        elseif($p -in $Skip) {
            $val = Get-Variable -Scope Global -Name $var -ValueOnly
            $scope = 'Global'
        }
        else {
            $val = $ScriptCmdlet.GetVariableValue($var)
            $scope = 'Caller'
        }
        Write-Debug " (From $scope scope) $var = $val " -Debug:$commandDebug
        Set-Variable -Scope 1 -Name $var -Value $val
    }
}

function Map-Object {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Justification = 'Internal functions')]
    param([scriptblock]$ScriptBlock)

begin {
    $code = "& { process { $ScriptBlock } }"
    $pipeline = [scriptblock]::Create($code).GetSteppablePipeline()
    $pipeline.Begin($true)
}
process {
    $pipeline.Process($_)
}
end {
    $pipeline.End()
}
}

### Source file: 'ServicePrincipals.ps1' ###

function GetAppServicePrincipal ([string]$ApplicationId, [string[]]$Properties)
{
    $propList = ('id', 'appId', 'displayName' + $Properties) -join ','
    $servicePrincipal = Invoke-MsGraph "servicePrincipals(appId='$ApplicationId')?`$select=$propList"
    if(!$servicePrincipal) {
        $azureCtx = Get-AzContext
        throw "No se encontro el 'Service Principal' asociado a la aplicación [$ApplicationId] en el directorio '$($azureCtx.Tenant.Name)'"
    }
    $servicePrincipal
}

function RegisterAppServicePrincipal ([string]$ApplicationId)
{
    $ErrorActionPreference = 'Stop'

    $sp = Invoke-MsGraph "servicePrincipals(appId='$ApplicationId')"
    if(!$sp) {
        $azureCtx = Get-AzContext
        Write-Verbose "Registrando nuevo Service Principal para la aplicación [$ApplicationId] en directorio '$($azureCtx.Tenant.Name)'..."
        $sp = Invoke-MsGraph 'servicePrincipals' -Method POST -Body @{ appId = $ApplicationId }
    }

    return $sp
}

### Source file: 'Tokens.ps1' ###

function GetGraphToken {
    $token = Get-AzAccessToken -ResourceUrl 'https://graph.microsoft.com' -AsSecureString -Debug:$false
    ConvertFrom-SecureString $token.Token -AsPlainText
}

#endregion