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 {
                $principal = $_.principal
                $principalType = $principal.'@odata.type'.Split('.')[-1] #-replace "^(.)", { $_.Groups[1].Value.ToUpper() }
                $principalName = switch ($principalType) {
                    'user' {
                        $principal.userPrincipalName
                        break
                    }
                    'servicePrincipal' {
                        $principal.servicePrincipalNames[0]
                        break
                    }
                    'group' {
                        $principal.mail ?? $principal.displayName
                        break
                    }
                    default { $principal.displayName }
                }
                [PSCustomObject]@{
                    role           = $role.name
                    principalId    = $principal.id
                    principalName  = $principalName
                    displayName    = $principal.displayName
                    roleId         = $role.id
                    roleTemplateId = $role.templateId
                    principalType  = $principalType
                    enabled        = $principalType -eq 'group' ? $true : $principal.accountEnabled
                    assignmentId   = $_.id
                    #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 -join ' '
        }

        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
            ThrowIfNotFound $principalId $UserPrincipalName -PrincipalType 'Usuario'

            $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, ParameterSetName = 'Id')]
        [Alias('PrincipalId')]
        [string]$Id,

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

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

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

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

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

    try {
        Update-CallerPreference

        $applicationSp = GetAppServicePrincipal -ApplicationId $ApplicationId -Properties 'appRoles'
        switch ($PSCmdlet.ParameterSetName) {
            'Id' {
                $principal = Invoke-MsGraph "directoryObjects/$Id"
                ThrowIfNotFound $principal $Id

                $allowedTypes = 'servicePrincipal', 'user', 'group'
                $principalType = $principal.'@odata.type'.Split('.')[-1]
                if($principalType -notin $allowedTypes) {
                    throw "El objecto a asignar es de tipo '$pricipalType'. Solo estan permitidos los tipos ($($allowedTypes -join ', '))"
                }
                $principalId = $principal.id
                $memberType = $principalType -eq 'servicePrincipal' ? 'Application' : 'User'
                break
            }
            'App'   {
                $principalId = (GetAppServicePrincipal -ApplicationId $ClientId).id
                ThrowIfNotFound $principalId $ClientId -PrincipalType 'ApplicationId'
                $memberType = 'Application'
                break
            }
            'User'  {
                $principalId = (Get-EntraUser -UserPrincipalName $UserPrincipalName).id
                ThrowIfNotFound $principalId $UserPrincipalName -PrincipalType 'Usuario'
                $memberType = 'User'
                break
            }
            'Group' {
                $group = Get-EntraGroup -DisplayName $GroupName
                if(!$group.securityEnabled) {
                    throw "El grupo [$GroupName] no existe o no tiene habilitada la propiedad 'SecurityEnabled'"
                }
                $principalId = $group.id
                $memberType = 'User'
                break
            }
        }

        $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 # Este valor se asigna por llamada a cada rol
        }

        Write-Verbose "Asignando roles [$($appRoles.value -join ', ')] de la aplicacion '$($applicationSp.displayName)' ..."
        foreach($r in $appRoles) {
            $params.appRoleId = $r.id
            $response = Invoke-MsGraph $path -Method Post -Body $params -DetailResponse

            if(!$response.Success -and $response.Error.additionalErrors.code -eq 'InvalidUpdate') {
                Write-Warning "El rol [$($r.value)] ya se encuentra asignado."
                continue
            }

            $assign = $response | ConvertFrom-MsGraphResponse
            [PSCustomObject]@{
                principalName = $assign.principalDisplayName
                principalType = $assign.principalType
                appRole       = $r.value
                appName       = $assign.resourceDisplayName
            }
        }
    }
    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()
            Write-Verbose "Registra primeramente la aplicación asociada [$appName]"
            $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, 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)'"
        }
        $roleAssignment =
            Invoke-MsGraph "roleManagement/directory/roleAssignments?`$select=id&`$filter=principalId eq '$PrincipalId' and roleDefinitionId eq '$($role.templateId)'"

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

        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: 'errors.ps1' ###
function ThrowIfNotFound ($Principal, [string]$PrincipalId, [string]$PrincipalType = 'Objeto')
{
    if(!$Principal) {
        $azureCtx = Get-AzContext
        $tenant = $azureCtx.Tenant.Name ?? $azureCtx.Tenant.TenantId
        throw "No se encontro el $PrincipalType [$PrincipalId] en el directorio '$tenant'"
    }
}

### 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
        $tenant = $azureCtx.Tenant.Name ?? $azureCtx.Tenant.TenantId
        throw "No se encontro el Service Principal asociado a la aplicación [$ApplicationId] en el directorio '$tenant'"
    }
    $servicePrincipal
}

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

    $azureCtx = Get-AzContext
    $tenant = $azureCtx.Tenant.Name ?? $azureCtx.Tenant.TenantId

    $sp = Invoke-MsGraph "servicePrincipals(appId='$ApplicationId')"
    if(!$sp) {
        Write-Verbose "Registrando nuevo Service Principal para la aplicación [$ApplicationId] en directorio '$tenant'..."
        $sp = Invoke-MsGraph 'servicePrincipals' -Method POST -Body @{ appId = $ApplicationId }
    }
    else {
        Write-Verbose "Ya existe el Service Principal asignado a la aplicación [$($sp.displayName)] en el directorio '$tenant'"
    }

    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