Public/Grant-sqmTemporarySysadmin.ps1

<#
.SYNOPSIS
    Vergibt einem Login temporaer sysadmin-Rechte fuer X Tage und entzieht sie
    danach automatisch ueber einen selbstloeschenden SQL-Agent-Job.
 
.DESCRIPTION
    Fuer Patch-/Installationssituationen: macht einen Anwender zeitlich befristet
    zum sysadmin.
 
      - Ohne -StartDate wird SOFORT vergeben (inline) und ein Revoke-Job auf
        heute + X Tage angelegt.
      - Mit -StartDate (in der Zukunft) wird ein Grant-Job auf das Startdatum und
        ein Revoke-Job auf Startdatum + X Tage angelegt.
 
    Beide Jobs sind One-Time-Jobs, die sich bei Erfolg selbst loeschen
    (via Invoke-sqmTempSysadminAction -> sp_delete_job).
 
    Jede Aktion wird im Modul-Logfile UND im Windows Event Log protokolliert -
    inklusive der optionalen Auftragsnummer.
 
.PARAMETER SqlInstance
    SQL Server Instanz. Default: lokaler Computername.
 
.PARAMETER SqlCredential
    PSCredential fuer die SOFORTIGE Vergabe (SQL-Auth). Hinweis: Die Agent-Jobs
    laufen unter dem SQL-Agent-Dienstkonto (Windows, i. d. R. sysadmin) und nutzen
    KEINE gespeicherten Credentials.
 
.PARAMETER Login
    Login / Server-Principal, der temporaer sysadmin werden soll. Muss bereits als
    Login existieren (wird NICHT angelegt).
 
.PARAMETER Days
    Dauer der sysadmin-Rechte in Tagen.
 
.PARAMETER StartDate
    Optionaler Aktivierungszeitpunkt. Fehlt er (oder liegt in der Vergangenheit),
    wird sofort vergeben.
 
.PARAMETER TicketNumber
    Optionale Auftrags-/Ticketnummer fuer die Protokollierung.
 
.PARAMETER Force
    Ueberschreibt bereits vorhandene gleichnamige Grant-/Revoke-Jobs.
 
.EXAMPLE
    Grant-sqmTemporarySysadmin -SqlInstance SQL01 -Login 'DOM\u.maier' -Days 3 -TicketNumber 'INC0012345'
    # Sofort sysadmin fuer 3 Tage, danach automatischer Entzug.
 
.EXAMPLE
    Grant-sqmTemporarySysadmin -Login 'DOM\u.maier' -Days 1 -StartDate '2026-07-01 08:00' -TicketNumber 'CHG7788'
    # Aktivierung am 01.07. 08:00, Entzug am 02.07. 08:00.
 
.EXAMPLE
    Grant-sqmTemporarySysadmin -Login 'DOM\u.maier' -Days 2 -WhatIf
    # Zeigt nur, was passieren wuerde - ohne Vergabe/Job-Anlage.
 
.NOTES
    Requires: dbatools, Invoke-sqmLogging, Invoke-sqmTempSysadminAction.
    Aufrufer braucht fuer die Sofort-Vergabe sysadmin/ALTER auf der Serverrolle.
    Das SQL-Agent-Dienstkonto braucht sysadmin (fuer DROP/Self-Delete) und das
    Modul maschinenweit (AllUsers).
#>

function Grant-sqmTemporarySysadmin
{
    [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
    [OutputType([PSCustomObject])]
    param (
        [Parameter(Mandatory = $false)]
        [string]$SqlInstance = $env:COMPUTERNAME,
        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$SqlCredential,
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$Login,
        [Parameter(Mandatory = $true)]
        [ValidateRange(1, 3650)]
        [int]$Days,
        [Parameter(Mandatory = $false)]
        [datetime]$StartDate,
        [Parameter(Mandatory = $false)]
        [string]$TicketNumber,
        [Parameter(Mandatory = $false)]
        [switch]$Force
    )

    begin
    {
        $functionName = $MyInvocation.MyCommand.Name
        $connParams = @{ SqlInstance = $SqlInstance }
        if ($SqlCredential) { $connParams['SqlCredential'] = $SqlCredential }
    }

    process
    {
        # --- Zeiten bestimmen ---
        $now = Get-Date
        $immediate = (-not $PSBoundParameters.ContainsKey('StartDate')) -or ($StartDate -le $now)
        $activation = if ($immediate) { $now } else { $StartDate }
        $revocation = $activation.AddDays($Days)

        # --- Login-Existenz pruefen (read-only) ---
        $loginLit = $Login -replace "'", "''"
        try
        {
            $exists = Invoke-DbaQuery @connParams -Database master -EnableException -ErrorAction Stop `
                -Query "SELECT COUNT(*) AS Cnt FROM sys.server_principals WHERE name = N'$loginLit' AND type IN ('U','G','S','K');"
            if (-not $exists -or [int]$exists.Cnt -eq 0)
            {
                throw "Login '$Login' existiert auf '$SqlInstance' nicht. Bitte den Login zuerst anlegen."
            }
        }
        catch
        {
            Invoke-sqmLogging -Message "Login-Pruefung fehlgeschlagen: $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR"
            throw
        }

        # --- Jobnamen ---
        $sani      = ($Login -replace '[^A-Za-z0-9._-]', '_')
        $jobBase   = "sqmTempSysadmin_$sani`_$($activation.ToString('yyyyMMddHHmm'))"
        $revokeJob = "${jobBase}_Revoke"
        $grantJob  = "${jobBase}_Grant"
        $psExe     = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe'

        # Werte fuer die inline -Command-Strings (Single-Quote-Escape)
        $instEsc   = $SqlInstance -replace "'", "''"
        $loginEsc  = $Login -replace "'", "''"
        $ticketEsc = ($TicketNumber -replace "'", "''")

        # --- lokale Hilfe: One-Time-Job mit CmdExec-Step + Schedule anlegen ---
        function New-sqmOneTimeJob
        {
            param([string]$Name, [string]$Command, [datetime]$When, [string]$Description)

            $existing = Get-DbaAgentJob -SqlInstance $SqlInstance -Job $Name -ErrorAction SilentlyContinue
            if ($existing -and -not $Force) { throw "Job '$Name' existiert bereits. -Force zum Ueberschreiben." }
            if ($existing -and $Force) { Remove-DbaAgentJob -SqlInstance $SqlInstance -Job $Name -Confirm:$false -ErrorAction Stop }

            $null = New-DbaAgentJob -SqlInstance $SqlInstance -Job $Name -Description $Description -ErrorAction Stop
            $null = New-DbaAgentJobStep -SqlInstance $SqlInstance -Job $Name -StepName 'Run' `
                -Subsystem 'CmdExec' -Command $Command -ErrorAction Stop

            $schedName = "sch_$Name"
            $startDateInt = [int]$When.ToString('yyyyMMdd')
            $startTimeInt = [int]$When.ToString('HHmmss')
            $schedSql = @"
DECLARE @sid INT;
WHILE EXISTS (SELECT 1 FROM msdb.dbo.sysschedules WHERE name = N'$schedName')
BEGIN
    SELECT TOP (1) @sid = schedule_id FROM msdb.dbo.sysschedules WHERE name = N'$schedName';
    EXEC msdb.dbo.sp_delete_schedule @schedule_id = @sid, @force_delete = 1;
END
EXEC msdb.dbo.sp_add_schedule
    @schedule_name = N'$schedName',
    @enabled = 1,
    @freq_type = 1,
    @active_start_date = $startDateInt,
    @active_start_time = $startTimeInt;
EXEC msdb.dbo.sp_attach_schedule @job_name = N'$Name', @schedule_name = N'$schedName';
"@

            $null = Invoke-DbaQuery -SqlInstance $SqlInstance -Database msdb -Query $schedSql -EnableException -ErrorAction Stop
        }

        $result = [PSCustomObject]@{
            SqlInstance    = $SqlInstance
            Login          = $Login
            Days           = $Days
            ActivationTime = $activation
            RevocationTime = $revocation
            TicketNumber   = $TicketNumber
            GrantJob       = if ($immediate) { $null } else { $grantJob }
            RevokeJob      = $revokeJob
            Immediate      = $immediate
            Status         = 'Planned'
            Message        = $null
        }

        $desc = "sqmSQLTool: temporaerer sysadmin fuer '$Login' bis $($revocation.ToString('yyyy-MM-dd HH:mm')). Auftragsnummer: $(if ($TicketNumber){$TicketNumber}else{'(keine)'})"

        if (-not $PSCmdlet.ShouldProcess($SqlInstance, "sysadmin fuer '$Login' $(if($immediate){'SOFORT'}else{"ab $($activation.ToString('yyyy-MM-dd HH:mm'))"}) fuer $Days Tage (Entzug $($revocation.ToString('yyyy-MM-dd HH:mm')))"))
        {
            $result.Status = 'WhatIf'
            $result.Message = "WhatIf: keine Aenderung durchgefuehrt."
            return $result
        }

        try
        {
            # --- Revoke-Job IMMER anlegen ---
            $revokeCmd = "$psExe -NoProfile -ExecutionPolicy Bypass -Command `"Import-Module sqmSQLTool; Invoke-sqmTempSysadminAction -SqlInstance '$instEsc' -Login '$loginEsc' -Action Revoke -TicketNumber '$ticketEsc' -JobName '$revokeJob'`""
            New-sqmOneTimeJob -Name $revokeJob -Command $revokeCmd -When $revocation -Description $desc

            # --- Grant ---
            if ($immediate)
            {
                Invoke-sqmTempSysadminAction @connParams -Login $Login -Action Grant -TicketNumber $TicketNumber | Out-Null
            }
            else
            {
                $grantCmd = "$psExe -NoProfile -ExecutionPolicy Bypass -Command `"Import-Module sqmSQLTool; Invoke-sqmTempSysadminAction -SqlInstance '$instEsc' -Login '$loginEsc' -Action Grant -TicketNumber '$ticketEsc' -JobName '$grantJob'`""
                New-sqmOneTimeJob -Name $grantJob -Command $grantCmd -When $activation -Description $desc
            }

            $result.Status = 'Success'
            $result.Message = if ($immediate)
            {
                "sysadmin sofort vergeben; automatischer Entzug am $($revocation.ToString('yyyy-MM-dd HH:mm')) via Job '$revokeJob'."
            }
            else
            {
                "Vergabe am $($activation.ToString('yyyy-MM-dd HH:mm')) (Job '$grantJob'), Entzug am $($revocation.ToString('yyyy-MM-dd HH:mm')) (Job '$revokeJob')."
            }

            Invoke-sqmLogging -Message "$($result.Message) Login '$Login', Auftragsnummer: $(if($TicketNumber){$TicketNumber}else{'(keine)'})" -FunctionName $functionName -Level "INFO"
            return $result
        }
        catch
        {
            $result.Status = 'Error'
            $result.Message = $_.Exception.Message
            Invoke-sqmLogging -Message "Fehler bei temporaerer sysadmin-Vergabe fuer '$Login': $($_.Exception.Message)" -FunctionName $functionName -Level "ERROR"
            throw
        }
    }
}