keycloakRealmManager.ps1

class KeycloakRealmManagerVersion {
    [int]$Major = 1
    [int]$Minor = 5
    [int]$Build = 82
    [int]$Revision = 790

    [string]ToString() {
        return "keycloakRealmManager v$($this.Major).$($this.Minor).$($this.Build).$($this.Revision)"
    }
}

class KeycloakRealmManager {
    hidden [string]$realmClientName = "admin-cli"
    hidden [string]$realmClientSecret = [string]::Empty
    hidden [string]$userName = [string]::Empty
    hidden [string]$password = [string]::Empty
    hidden [array]$clients = @()
    hidden [array]$users = @()
    hidden [array]$groups = @()
    hidden [array]$roles = @()
    hidden [hashtable]$realms = @{}

    [string]$realmName
    [PSCustomObject]$realm = $null

    hidden [KeycloakTokenManager]$tokenManager
    hidden [KeycloakRealmManagerVersion]$version = [KeycloakRealmManagerVersion]::new()

    [string]getVersion() {
        return $this.version.ToString()
    }

    hidden [void]initializeMaster([uri]$realmUri) {
        [string]$local:keycloakBaseUrl = "$($realmUri.Scheme)://$($realmUri.Host)"
        $this.getRealmDetails([string]$keycloakBaseUrl, "master")
    }

    hidden [void]initializeRealm([uri]$realmUri, [bool]$create) {
        [string]$local:keycloakBaseUrl = "$($realmUri.Scheme)://$($realmUri.Host)"
        $local:segments = ($realmUri.Segments)
        [int]$local:realmsIndex = $local:segments.IndexOf("realms/")
        $this.realmName = $local:segments[$local:realmsIndex + 1].TrimEnd("/")

        if ($this.realmName -ne "master") {
            $local:realms = $this.getRealms($keycloakBaseUrl)
            if ($local:realms) {
                if ($local:realms.id.Contains($this.realmName)) {
                    $this.getRealmDetails($local:keycloakBaseUrl, $this.realmName)
                }
                else {
                    if ($create) {
                        if ($this.createRealm($local:keycloakBaseUrl, $this.realmName)) {
                            $this.initializeRealm($realmUri, $false)
                        }
                        else {
                            throw @{ errorMessage = "Failed to create realm '$($this.realmName)'" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
                        }
                    }
                    else {
                        throw @{ errorMessage = "Unknown realm '$($this.realmName)'" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
                    }
                }
            }
            else {
                throw @{ errorMessage = "Unable to get realms from keycloak" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
            }
        }
        else {
            $this.realmName = "master"
        }
    }

    hidden [void]getRealmDetails([string]$keycloakBaseUrl, [string]$realmName) {
        $local:realmDetails = @{
            public_key    = $null
            realmUri      = [uri]("$($keycloakBaseUrl)/realms/$($realmName)")
            realmAdminUri = [uri]("$($keycloakBaseUrl)/admin/realms/$($realmName)")
            realmTokenUri = $null
        }

        $local:result = Invoke-WebRequest -Method Get -Uri $local:realmDetails.realmUri -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
            $local:content = ($local:result.Content | ConvertFrom-Json)

            $local:realmDetails.public_key = [string]$local:content.public_key
            $local:realmDetails.realmTokenUri = [uri]$local:content.'token-service'
            $this.realms[$realmName] = $local:realmDetails
        }
    }

    [uri]getRealmAdminUri() {
        return $this.realms[$this.realmName].realmAdminUri
    }

    [string]getRealmPublicKey() {
        return $this.realms[$this.realmName].public_key
    }

    [uri]getRealmUri() {
        return $this.realms[$this.realmName].realmUri
    }

    [uri]getRealmTokenUri() {
        return $this.realms[$this.realmName].realmTokenUri
    }

    hidden [bool]createRealm([string]$keycloakBaseUrl, [string]$realmName) {
        $local:realm = @{}
        $local:realm.Add("id", $realmName)
        $local:realm.Add("realm", $realmName)
        $local:realm.Add("enabled", $true)

        $local:body = ($local:realm | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

        [string]$local:url = "$keycloakBaseUrl/admin/realms"

        $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Headers $this.tokenManager.getHeader() -Body $local:body -SkipHttpErrorCheck

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
    }

    [bool]updateRealm() {
        $local:body = ($this.getRealm() | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

        [string]$local:url = "$($this.getRealmAdminUri())"

        $local:result = Invoke-WebRequest -Method Put -Uri $local:url -Headers $this.tokenManager.getHeader() -Body $local:body -SkipHttpErrorCheck

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
    }

    [PSCustomObject]getRealm() {
        return $this.getRealm($false)
    }

    [PSCustomObject]getRealm([bool]$force) {
        if (($null -eq $this.realm) -or ($force -eq $true)) {
            [string]$local:url = "$($this.getRealmAdminUri())"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                $this.realm = ($local:result.Content | ConvertFrom-Json -Depth 100)
            }
        }
        return $this.realm
    }

    hidden [array]getRealms([string]$keycloakBaseUrl) {
        [string]$local:url = "$keycloakBaseUrl/admin/realms?briefRepresentation=true"

        $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
            return ($local:result.Content | ConvertFrom-Json)
        }
        return $null
    }

    [bool]deleteRealm() {
        [string]$local:url = "$($this.getRealmAdminUri())"

        $local:result = Invoke-WebRequest -Method Delete -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent) {
            $this.realms.Remove($this.realmName)
            $this.realmName = $null
            $this.clients = @()
            $this.users = @()
            $this.realm = $null
        }
        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
    }

    [array]getRealmComponents() {
        [string]$local:url = "$($this.getRealmAdminUri())/components"

        $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
            return ($local:result.Content | ConvertFrom-Json)
        }
        return $null
    }

    [array]getRealmComponentKeysProviders() {
        $local:components = $this.getRealmComponents()
        if ($local:components) {
            return ($local:components | Where-Object { $_.providerType -eq "org.keycloak.keys.KeyProvider" })
        }
        return $null
    }

    [bool]updateRealmComponentKeysProvider($keysProvider) {
        $local:body = ($keysProvider | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

        [string]$local:url = "$($this.getRealmAdminUri())/components/$($keysProvider.id)"

        $local:result = Invoke-WebRequest -Method Put -Uri $local:url -Body $local:body -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
    }

    [bool]createClient($clientName) {
        $local:client = $this.clients | Where-Object { $_.clientId -eq $clientName }
        if ($local:client) {
            throw @{ errorMessage = "Client with name '$($clientName)' already exists" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        else {
            $local:client = @{}
            $local:client["clientId"] = $clientName
            $local:client["enabled"] = $true

            $local:body = ($local:client | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

            [string]$local:url = "$($this.getRealmAdminUri())/clients"

            $local:result = Invoke-WebRequest -Method Post -Uri $local:url  -Body $local:body -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created) {
                $this.clients = $this.getClients($true)
            }
            return ($result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
        }
    }

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    [bool]createClientJWTCertificate([string]$clientName, [string]$keyPassword, [string]$storePassword) {
        $local:client = $this.getClient($clientName)
        if ($local:client) {
            [string]$local:url = "$($this.getRealmAdminUri())/clients/$($local:client.id)/certificates/jwt.credential/generate-and-download"

            $local:data = @{}
            $local:data["keyAlias"] = $clientName
            $local:data["realmAlias"] = $this.realmName
            $local:data["realmCertificate"] = $false
            $local:data["format"] = "JKS"
            $local:data["keyPassword"] = $keyPassword
            $local:data["storePassword"] = $storePassword

            $local:body = ($local:data | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

            $local:certificateName = "$clientName-JWT-$($this.realmName).jks"
            Remove-Item -Path $local:certificateName -Force -ErrorAction SilentlyContinue

            $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Headers $this.tokenManager.getHeader() -Body $local:body -OutFile $local:certificateName -SkipHttpErrorCheck

            return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
        }
        return $null
    }

    [bool]updateClient([PSCustomObject]$client) {
        $local:existingsClient = $this.getClients() | Where-Object { $_.id -eq $client.id }
        if ($local:existingsClient) {
            $local:body = ($client | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

            [string]$local:url = "$($this.getRealmAdminUri())/clients/$($client.id)"

            $local:result = Invoke-WebRequest -Method Put -Uri $local:url -Headers $this.tokenManager.getHeader() -Body $local:body -SkipHttpErrorCheck

            return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
        }
        else {
            throw @{ errorMessage = "Client with name '$($client.clientId)' already exists" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
    }

    [bool]deleteClient([string]$clientName) {
        $local:client = $this.getClient($clientName)
        if ($local:client) {
            [string]$local:url = "$($this.getRealmAdminUri())/clients/$($local:client.id)"

            $local:result = Invoke-WebRequest -Method Delete -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
        }
        return $null
    }

    [array]getClients() {
        return $this.getClients($false)
    }

    [array]getClients([bool]$force) {
        if (($this.clients.Count -eq 0) -or ($force -eq $true)) {
            [string]$local:url = "$($this.getRealmAdminUri())/clients"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                $this.clients = ($local:result.Content | ConvertFrom-Json -Depth 100)
            }
            else {
                throw throw @{ errorMessage = "Failed to get clients of realm '$($this.realmName)'" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
            }
        }
        return $this.clients
    }

    [PSCustomObject]getClient([string]$clientName) {
        return $this.getClient($clientName, $false)
    }

    [PSCustomObject]getClient([string]$clientName, [bool]$force) {
        $local:clients = $this.getClients($force)
        if ($local:clients) {
            $local:client = $local:clients | Where-Object { $_.clientId -eq $clientName }
            if ($local:client) {
                return ($local:client | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii | ConvertFrom-Json -Depth 100)
            }
            # else {
            # throw @{ errorMessage = "Client with name '$($clientName)' not exists" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
            # }
        }
        return $null
    }

    [string]getClientSecret($clientName) {
        $local:client = $this.getClient($clientName, $false)
        if ($local:client) {
            if ($null -ne $local:client.secret) {
                return ($local:client.secret)
            }
            # else {
            # throw @{ errorMessage = "Client '$($clientName)' has no client secret" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
            # }
        }
        return $null
    }

    [string]getClientJWTCertificate($clientName) {
        $local:client = $this.getClient($clientName, $false)
        if ($local:client) {
            [string]$local:url = "$($this.getRealmAdminUri())/clients/$($local:client.id)/certificates/jwt.credential"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck
            if ($local:result.StatusCode -eq 200) {
                $local:content = ($local:result.Content | ConvertFrom-Json -Depth 100)
                $local:clientJWTCredential = $local:content.certificate
                if ($local:clientJWTCredential) {
                    return $local:clientJWTCredential
                }
            }
        }
        return $null
    }

    [array]getClientProtocolMappers([string]$clientName) {
        $local:client = $this.getClient($clientName, $false)
        if ($local:client) {
            [string]$local:url = "$($this.getRealmAdminUri())/clients/$($local:client.id)/protocol-mappers/protocol/openid-connect"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq 200) {
                $local:mappers = ($local:result.Content | ConvertFrom-Json -Depth 100)
                return $local:mappers
            }
        }
        return $null
    }

    [bool]addClientProtocolMapper([string]$clientName, [PSCustomObject]$clientMapper) {
        $local:client = $this.getClient($clientName, $false)
        if ($local:client) {
            [string]$local:url = "$($this.getRealmAdminUri())/clients/$($local:client.id)/protocol-mappers/models"

            $local:data = ($clientMapper | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii)

            $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Headers $this.tokenManager.getHeader() -Body $local:data -SkipHttpErrorCheck
            return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
        }
        return $false
    }

    [array]getRealmRoles() {
        return $this.getRealmRoles($false)
    }

    [array]getRealmRoles([bool]$force) {
        if (($this.roles.Count -eq 0) -or ($force -eq $true)) {
            [string]$local:url = "$($this.getRealmAdminUri())/roles"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                $this.roles = ($local:result.Content | ConvertFrom-Json)
            }
        }
        return $this.roles
    }

    [PSCustomObject]getRealmRole([string]$roleName) {
        $local:roles = $this.getRealmRoles()
        if ($local:roles) {
            $local:role = $local:roles | Where-Object { $_.name -eq $roleName }
            if ($local:role) {
                return ($local:role | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii | ConvertFrom-Json -Depth 100)
            }
        }
        return $null
    }

    [bool]createRealmRole([string]$roleName) {
        return $this.createRealmRole($roleName, [string]::Empty)
    }

    [bool]createRealmRole([string]$roleName, $description) {
        $local:role = $this.getRealmRole($roleName)
        if ($local:role) {
            throw @{ errorMessage = "Realm role with name '$($roleName)' already exists" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        else {
            [string]$local:url = "$($this.getRealmAdminUri())/roles"

            $local:data = @{}
            $local:data["name"] = $roleName
            if ([string]::IsNullOrEmpty($description) -eq $false) {
                $local:data["description"] = $description
            }

            $local:body = ($local:data | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

            $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Body $local:body -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
        }
    }

    [bool]createGroup([string]$groupName) {
        $local:newGroup = [PSCustomObject]@{
            name = $groupName
        }

        [string]$local:url = "$($this.getRealmAdminUri())/groups"
        $local:body = ($local:newGroup | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

        $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Body $local:body -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created) {
            $this.groups = @() # clear cache
        }

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
    }

    [array]getGroups() {
        return $this.getGroups($false)
    }

    [array]getGroups([bool]$force) {
        if (($this.groups.Count -eq 0) -or ($force -eq $true)) {
            $this.groups = @()
            [int32]$local:groupPageSize = 100
            [Int32]$local:fetchedGroupsCount = 0

            do {
                [string]$local:url = "$($this.getRealmAdminUri())/groups?first=$($this.groups.Count)&max=$($local:groupPageSize)"
                $local:result = Invoke-WebRequest -Method Get -Uri $url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck
                if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                    $local:fetchedGroups = ($local:result.Content | ConvertFrom-Json -Depth 100)
                    $local:fetchedGroupsCount = $local:fetchedGroups.Count
                    $this.groups += $local:fetchedGroups
                }
                # } until ($local:fetchedUsersCount -lt $local:userPageSize)
            } until ($local:fetchedGroupsCount -eq 0)
        }
        return $this.groups
    }

    [PSCustomObject]getGroupByName([string]$groupName) {
        return $this.getGroupByName($groupName, $false)
    }

    [PSCustomObject]getGroupByName([string]$groupName, [bool]$force) {
        $local:groups = $this.getGroups($force)
        if ($local:groups) {
            $local:group = $local:groups | Where-Object { $_.name -eq $groupName }
            if ($local:group) {
                return ($local:group | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii | ConvertFrom-Json -Depth 100)
            }
        }
        return $null
    }

    [bool]deleteGroup([PSCustomObject]$group) {
        [string]$local:url = "$($this.getRealmAdminUri())/groups/$($group.id)"

        $local:result = Invoke-WebRequest -Method Delete -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent) {
            $this.groups = @() # clear cache
        }

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
    }

    [array]getGroupRoles([guid]$groupId) {
        $local:group = $this.getGroups() | Where-Object { $_.id -eq $groupId }
        if ($local:group) {
            [string]$local:url = "$($this.getRealmAdminUri())/groups/$($local:group.id)/role-mappings"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                return [array]($local:result.Content | ConvertFrom-Json)
            }
        }
        return $null
    }

    [array]getAvailableRealmRolesForGroup([guid]$groupId) {
        $local:group = $this.getGroups() | Where-Object { $_.id -eq $groupId }
        if ($local:group) {
            [string]$local:url = "$($this.getRealmAdminUri())/groups/$($groupId)/role-mappings/realm/available"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                return [array]($local:result.Content | ConvertFrom-Json)
            }
        }
        return $null
    }

    [bool]hasGroupRealmRole([guid]$groupId, [string]$roleName) {
        $local:group = $this.getGroups() | Where-Object { $_.id -eq $groupId }
        if ($local:group) {
            $local:groupRoles = $this.getGroupRoles($groupId)
            return ($null -ne ($local:groupRoles.realmMappings | Where-Object { $_.name -eq $roleName }))
        }
        return $null
    }

    [bool]addRealmRoleToGroup([guid]$groupId, [string]$roleName) {
        if ($this.hasGroupRealmRole($groupId, $roleName) -eq $false) {
            $local:realmRoles = $this.getAvailableRealmRolesForGroup($groupId)
            if ($local:realmRoles) {
                $local:realmRole = $local:realmRoles | Where-Object { $_.name -eq $roleName }
                if ($local:realmRole) {
                    $local:body = "[$($local:realmRole | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)]"
                    [string]$local:url = "$($this.getRealmAdminUri())/groups/$($groupId)/role-mappings/realm"

                    $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Headers $script:realmManager.tokenManager.getHeader() -Body $local:body -SkipHttpErrorCheck

                    return ($local:result.StatusCode -eq 204)
                }
                else {
                    throw @{ errorMessage = "Realm role '$($roleName)' does not exist" }  | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
                }
            }
            else {
                throw @{ errorMessage = "No roles for group '$($groupId.TosTring())'" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
            }
        }
        else {
            throw @{ errorMessage = "Group '$($groupId.TosTring())' already has realm role '$($roleName)'" }  | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        return $false
    }

    [bool]createUser([string]$userName) {
        $local:newUser = [PSCustomObject]@{
            username    = $userName
            enabled     = $true
            credentials = @()
        }

        [string]$local:url = "$($this.getRealmAdminUri())/users"
        $local:body = ($local:newUser | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

        $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Body $local:body -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created) {
            $this.users = @() # clear cache
        }

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
    }

    [bool]createUser([PSCustomObject]$user) {
        [string]$local:url = "$($this.getRealmAdminUri())/users"
        $local:body = ($user | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

        $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Body $local:body -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created) {
            $this.users = @() # clear cache
        }

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::Created)
    }

    [bool]updateUser([PSCustomObject]$user) {
        if ($user) {
            [string]$local:url = "$($this.getRealmAdminUri())/users/$($user.id)"
            $local:body = ($user | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)

            $local:result = Invoke-WebRequest -Method Put -Uri $local:url -Body $local:body -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent) {
                $this.users = @() # clear cache
            }

            return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
        }
        return $false
    }

    [array]getUsers() {
        return $this.getUsers($false)
    }

    [array]getUsers([bool]$force) {
        if (($this.users.Count -eq 0) -or ($force -eq $true)) {
            $this.users = @()
            [int32]$local:userPageSize = 100
            [Int32]$local:fetchedUsersCount = 0

            do {
                [string]$local:url = "$($this.getRealmAdminUri())/users?first=$($this.users.Count)&max=$($local:userPageSize)"
                $local:result = Invoke-WebRequest -Method Get -Uri $url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck
                if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                    $local:fetchedUsers = ($local:result.Content | ConvertFrom-Json -Depth 100)
                    $local:fetchedUsersCount = $local:fetchedUsers.Count
                    $this.users += $local:fetchedUsers
                }
                # } until ($local:fetchedUsersCount -lt $local:userPageSize)
            } until ($local:fetchedUsersCount -eq 0)
        }
        return $this.users
    }

    [PSCustomObject]getUserById([guid]$userId) {
        return $this.getUserById($userId, $false)
    }

    [PSCustomObject]getUserById([guid]$userId, [bool]$force) {
        $local:users = $this.getUsers($force)
        if ($local:users) {
            $local:user = $local:users | Where-Object { $_.id -eq $userId.ToString() }
            if ($local:user) {
                return ($local:user | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii | ConvertFrom-Json -Depth 100)
            }
        }
        return $null
    }

    [PSCustomObject]getUserByUserName([string]$userName) {
        return $this.getUserByUserName($userName, $false)
    }

    [PSCustomObject]getUserByUserName([string]$userName, [bool]$force) {
        $local:users = $this.getUsers($force)
        if ($local:users) {
            $local:user = $local:users | Where-Object { $_.username -eq $userName }
            if ($local:user) {
                return ($local:user | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii | ConvertFrom-Json -Depth 100)
            }
        }
        return $null
    }

    [PSCustomObject]getUserByEMail([string]$email, [bool]$force) {
        $local:users = $this.getUsers($force)
        if ($local:users) {
            $local:user = $local:users | Where-Object { $_.email -eq $email }
            if ($local:user) {
                return ($local:user | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii | ConvertFrom-Json -Depth 100)
            }
        }
        return $null
    }

    [PSCustomObject]getUserFederatedIdentity([PSCustomObject]$user) {
        [string]$local:url = "$($this.getRealmAdminUri())/users/$($user.id)/federated-identity"

        $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
            return [array]($local:result.Content | ConvertFrom-Json)
        }

        return $null
    }

    [PSCustomObject]getUserCredentials([PSCustomObject]$user) {
        [string]$local:url = "$($this.getRealmAdminUri())/users/$($user.id)/credentials"

        $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
            return [array]($local:result.Content | ConvertFrom-Json)
        }

        return $null
    }

    [PSCustomObject]deleteUserCredentials([PSCustomObject]$user,[PSCustomObject]$credentials) {
        [string]$local:url = "$($this.getRealmAdminUri())/users/$($user.id)/credentials/$($credentials.id)"

        $local:result = Invoke-WebRequest -Method Delete -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
    }

    [bool]deleteUser([PSCustomObject]$user) {
        [string]$local:url = "$($this.getRealmAdminUri())/users/$($user.id)"

        $local:result = Invoke-WebRequest -Method Delete -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

        if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent) {
            $this.users = @() # clear cache
        }

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
    }

    [array]getUserGroups([guid]$userId) {
        $local:user = $this.getUserById($userId)
        if ($local:user) {
            [string]$local:url = "$($this.getRealmAdminUri())/users/$($local:user.id)/groups"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                return [array]($local:result.Content | ConvertFrom-Json)
            }
        }
        return $null
    }

    [bool]addUserToGroup([guid]$userId, [guid]$groupId) {
        [string]$local:url = "$($this.getRealmAdminUri())/users/$($userId)/groups/$($groupId)"

        $local:result = Invoke-WebRequest -Method Put -Uri $local:url -Headers $script:realmManager.tokenManager.getHeader() -SkipHttpErrorCheck

        return ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::NoContent)
    }

    [array]getUserRoles([guid]$userId) {
        $local:user = $this.getUserById($userId)
        if ($local:user) {
            [string]$local:url = "$($this.getRealmAdminUri())/users/$($local:user.id)/role-mappings"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                return [array]($local:result.Content | ConvertFrom-Json)
            }
        }
        return $null
    }

    [array]getAvailableRealmRolesForUser([guid]$userId) {
        $local:user = $this.getUserById($userId)
        if ($local:user) {
            [string]$local:url = "$($this.getRealmAdminUri())/users/$($userId)/role-mappings/realm/available"

            $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $this.tokenManager.getHeader() -SkipHttpErrorCheck

            if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                return [array]($local:result.Content | ConvertFrom-Json)
            }
        }
        return $null
    }

    [array]getUserClientRoleMappings([guid]$userId, [string]$clientName) {
        $local:user = $this.getUserById($userId)
        if ($local:user) {
            $local:client = $this.getClient($clientName)
            if ($local:client) {
                [string]$local:url = "$($this.getRealmAdminUri())/users/$($user.id)/role-mappings/clients/$($local:client.id)"
                $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $script:realmManager.tokenManager.getHeader() -SkipHttpErrorCheck
                if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                    return [array]($local:result.Content | ConvertFrom-Json)
                }
            }
        }
        return $null
    }

    [array]getAvailableUserClientRoleMappings([guid]$userId, [string]$clientName) {
        $local:user = $this.getUserById($userId)
        if ($local:user) {
            $local:client = $this.getClient($clientName)
            if ($local:client) {
                [string]$local:url = "$($this.getRealmAdminUri())/users/$($user.id)/role-mappings/clients/$($local:client.id)/available"
                $local:result = Invoke-WebRequest -Method Get -Uri $local:url -Headers $script:realmManager.tokenManager.getHeader() -SkipHttpErrorCheck
                if ($local:result.StatusCode -eq [System.Net.HttpStatusCode]::OK) {
                    return [array]($local:result.Content | ConvertFrom-Json)
                }
            }
        }
        return $null
    }

    [bool]addUserClientRoleMapping([guid]$userId, [string]$clientName, [string]$roleName) {
        $local:client = $this.getClient($clientName)
        if ($local:client) {
            $local:availableClientRoles = $this.getAvailableUserClientRoleMappings($userId, $clientName)
            if ($local:availableClientRoles) {
                $local:availableClientRole = $local:availableClientRoles | Where-Object { $_.name -eq $roleName }
                if ($local:availableClientRole) {
                    [string]$local:url = "$($this.getRealmAdminUri())/users/$($userId)/role-mappings/clients/$($local:client.id)"

                    $local:role = [PSCustomObject]@{
                        description = $local:availableClientRole.description
                        id          = $local:availableClientRole.id
                        name        = $local:availableClientRole.name
                    }
                    $local:body = "[$($local:role | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)]"

                    $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Headers $script:realmManager.tokenManager.getHeader() -Body $local:body -SkipHttpErrorCheck
                    return ($local:result.StatusCode -eq 204)
                }
            }
        }
        return $false
    }

    [bool]hasUserClientRoleMapping([guid]$userId, [string]$clientName, [string]$roleName) {
        $local:user = $this.getUserById($userId)
        if ($local:user) {
            $local:userRoles = $this.getUserRoles($userId)
            return ($null -ne ($local:userRoles.clientMappings.$clientName.mappings | Where-Object { $_.name -eq $roleName }))
        }
        return $null
    }

    [bool]hasUserRealmRole([guid]$userId, [string]$roleName) {
        $local:user = $this.getUserById($userId)
        if ($local:user) {
            $local:userRoles = $this.getUserRoles($userId)
            return ($null -ne ($local:userRoles.realmMappings | Where-Object { $_.name -eq $roleName }))
        }
        return $null
    }

    [bool]addRealmRoleToUser([guid]$userId, [string]$roleName) {
        if ($this.hasUserRealmRole($userId, $roleName) -eq $false) {
            $local:realmRoles = $this.getAvailableRealmRolesForUser($userId)
            if ($local:realmRoles) {
                $local:realmRole = $local:realmRoles | Where-Object { $_.name -eq $roleName }
                if ($local:realmRole) {
                    $local:body = "[$($local:realmRole | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress)]"
                    [string]$local:url = "$($this.getRealmAdminUri())/users/$($userId)/role-mappings/realm"

                    $local:result = Invoke-WebRequest -Method Post -Uri $local:url -Headers $script:realmManager.tokenManager.getHeader() -Body $local:body -SkipHttpErrorCheck

                    return ($local:result.StatusCode -eq 204)
                }
                else {
                    throw @{ errorMessage = "Realm role '$($roleName)' does not exist" }  | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
                }
            }
            else {
                throw @{ errorMessage = "No roles for user '$($userId.TosTring())'" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
            }
        }
        else {
            throw @{ errorMessage = "User '$($userId.TosTring())' already has realm role '$($roleName)'" }  | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        return $false
    }

    [bool]isValid() {
        return (($null -ne $this.realms["master"]) -and ($null -ne $this.realms[$this.realmName]))
    }

    [void]dispose() {
        $this.tokenManager.dispose()

        $this.realmClientName = "admin-cli"
        $this.realmClientSecret = [string]::Empty
        $this.userName = [string]::Empty
        $this.password = [string]::Empty
        $this.clients = @()
        $this.users = @()
        $this.roles = @()
        $this.realms = @{}
    }

    KeycloakRealmManager([uri]$realmUri, [string]$clientName) {
        if ($null -eq $realmUri.Scheme) {
            throw @{ errorMessage = "Invalid realm uri" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        $this.userName = [string]::Empty
        $this.password = [string]::Empty
        $this.realmClientName = $clientName
        $this.initializeMaster($realmUri)

        $this.tokenManager = [KeycloakTokenManager]::new($this.realms["master"].realmTokenUri, $this.realmClientName)
        $this.userName = $this.tokenManager.userName
        $this.initializeRealm($realmUri, $false)
    }

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    KeycloakRealmManager([string]$userName, [string]$password, [uri]$realmUri) {
        if ($null -eq $realmUri.Scheme) {
            throw @{ errorMessage = "Invalid realm uri" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        $this.userName = $userName
        $this.password = $password
        $this.initializeMaster($realmUri)

        $this.tokenManager = [KeycloakTokenManager]::new($userName, $password, $this.realms["master"].realmTokenUri, $this.realmClientName, [string]::Empty)
        $this.initializeRealm($realmUri, $false)
    }

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    KeycloakRealmManager([string]$userName, [string]$password, [uri]$realmUri, [bool]$create) {
        if ($null -eq $realmUri.Scheme) {
            throw @{ errorMessage = "Invalid realm uri" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        $this.userName = $userName
        $this.password = $password
        $this.initializeMaster($realmUri)

        $this.tokenManager = [KeycloakTokenManager]::new($userName, $password, $this.realms["master"].realmTokenUri, $this.realmClientName, [string]::Empty)
        $this.initializeRealm($realmUri, $create)
    }

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPlainTextForPassword', '')]
    KeycloakRealmManager([string]$userName, [string]$password, [uri]$realmUri, [bool]$create, [string]$clientName, [string]$clientSecret) {
        if ($null -eq $realmUri.Scheme) {
            throw @{ errorMessage = "Invalid realm uri" } | ConvertTo-Json -Depth 100 -EscapeHandling EscapeNonAscii -Compress
        }
        $this.realmClientName = $clientName
        $this.realmClientSecret = $clientSecret
        $this.userName = $userName
        $this.password = $password
        $this.initializeMaster($realmUri)

        $this.tokenManager = [KeycloakTokenManager]::new($userName, $password, $this.realms["master"].realmTokenUri, $this.realmClientName, $this.realmClientSecret)
        $this.initializeRealm($realmUri, $create)
    }
}