Comme bon nombre de systèmes de messagerie, Lotus (et sa partie serveur, Domino) permettent de définir des droits d'accès, de lecture, d'écriture sur ses différentes boites mail. Domino intègre également une partie de bases documentaire sur lesquelles il est possible d'appliquer le même type d'autorisations. Cependant, le temps faisant, il est possible que certaines autorisations aient oublié d'être enlevées, qu'un audit vous demande de faire une revue de ces droits, ou bien que vous migriez vers un autre système de messagerie, et que vous devez avoir une vue sur les droits actuels pour pouvoir les réimplanter ensuite sur le nouveau système.

Comment faire?

Pour cela, nous aurons besoin de 2 choses :

  1. IBM Notes installé sur un PC et configuré avec un compte Admin
  2. Powershell

La conception de ce script aura été la "bonne mauvaise" surprise pour moi. Ayant automatisé Excel avec Powershell via des objets COM, l'idée d'à nouveau travailler avec ce même type d'objet était pour moi un cauchemar en approche... surtout avec Notes! Et pourtant, le tout s'est fait en douceur! Il y eu quand même quelques GRANDES frustrations, notamment via l’éternel manque de documentation du coté d'IBM, et le fait que leurs développeurs soient encore en 2002 dans leur tête ... Je m'explique ...

Créer un objet COM en soi n'est pas des plus compliqué. Ces informations sont enregistrées dans le registre, et sont aisées à obtenir, par le biais d'un one-liner :

Get-ChildItem HKLM:\Software\Classes -ErrorAction SilentlyContinue | Where-Object {$_.PSChildName -match '^\w+\.\w+$' -and (Test-Path -Path "$($_.PSPath)\CLSID")
} | Select-Object -ExpandProperty PSChildName

Dans cette liste, vous verrez (si vous avez Notes d'installé) qu'un objet com nommé Lotus.Notessession est présent. Il s'agit de l'objet à appeler pour la suite de ce script... et si vous êtes sur une version de Windows en 64 bits, et que vous appelez l'objet, de cette manière :

notes

Et oui! Erreur!

Pourquoi? Parce qu'IBM n'a pas cru bon de permettre à leur application de s’exécuter en mode 64 bits, ni de renseigner cette spécificité de manière CLAIRE! Après bien des recherches, j'ai lancé cette commande depuis un Powershell en x86 (disponible également sur les Windows 64!) et la... Aucun problème!

L'autre partie "amusante" aura été la encore liée au manque de documentation. La connexion aux bases de données se fait par l'intermédiaire d'une méthode nommée getdatabase() et qui attends 2 arguments. Le premier : le nom du serveur et le second : le nom de la base, avec son chemin.

Rien de terrible en soi, sauf que les messages d'erreurs renvoyés n'indiquent en rien que le nom du serveur doit être ... FORMAT DOMINO! Bien que le client accepte lors de la configuration un nom FQDN (serveur.domaine.fr par exemple), l'objet attends un format domino (Serveur/Domaine fr par exemple).

Quelques petites choses qui font perdre BEAUCOUP de temps à chercher pour pas grand chose...

Au final, le script liste à l'aide de Robocopy (bien plus rapide que Get-Childitem) l'intégralité des fichiers NSF présents sur les serveurs, puis analyse base par base les ACLs présentes sur chaque entrée trouvée. Il crée un fichier CSV récapitulatif avec à chaque entrée les droits de création et suppression de documents.

Il est possible de rajouter d'autres autorisations à retourner, mais j'ai préféré me focaliser uniquement sur les deux là (libre à vous d'en rajouter, il suffit de rajouter quelques informations, lisez les commentaires du code pour être guidé!)

Il crée également un autre fichier CSV contenant les erreurs rencontrées (droits d'accès, base indisponible, etc.).


Script final

Vous pouvez télécharger ce script depuis mon espace git personnel.

if ([System.IntPtr]::Size -ne 4)
{
    Write-Error "Ce script doit être lancé depuis un Powershell 32 bits (disponible dans %windir%\syswow64\WindowsPowershell)"
    Exit 3
}

function Clean-Filename ($Filename=$(throw "Can't be empty"))
{
    $invalidcharsforpath = [System.IO.Path]::GetInvalidFileNameChars()
    foreach ($char in $invalidcharsforpath)
    {
        $Filename = $Filename.replace("$char","")
        return $Filename
    }
}

$aclarray = New-Object System.Collections.ArrayList
$errorarray = New-Object System.Collections.ArrayList

#Le mot de passe à utiliser par l'identité proposée par Notes lorsque vous le lancez. Vous ne pouvez pas changer d'identité depuis ce script
$passwordforactualaccount = "PASSW0RD"
#Creating the Lotus object
$lotus = New-Object -ComObject Lotus.Notessession
try
{
    #Ouverture de Notes avec l'identité par défaut et le mot de passe fourni
    $lotus.Initialize("$passwordforactualaccount")
}
catch
{
    Write-Output "Impossible d'ouvrir Notes, mot de passe incorrect?."
    exit 2
}
Write-Output "Logged as $($lotus.usernamelist.canonical)"

<#
Ce script enregistrera les données sur le Bureau. Vous pouvez changer la valeur pour un chemin régulier ou utiliser une valeur de cette list : 
Desktop
Programs
MyDocuments
Personal
(et bien d'autres...)
#>

$exportfolder = [Environment]::GetFolderPath("Desktop")
Set-Location $exportfolder

#Vous pouvez ajouter autant d'objets que vous le désirez dans la section $databasepath. Respectez juste la syntaxe et tout sera ok :
#"server" : Le chemin vers le serveur Domino. Exemple : "\\DOMINO01"
#"datapath" : Le chemin amenant versle repertoire des données de Domino, là ou sont stockés les fichiers NSF. Si vous utilisez un partage caché, ajoutez un ` avant le $. Cela échappera le caractère. Exemple : "d`$\domino\data\"
#"splitpart" : Utilisé pour séparaer le repertoire racine du restant. Il s'agit souvent de "data", et cette section est utilisée pour Lotus, permettant à l'object COM de trouver la base dans son arborescence. 
# NE TERMINEZ PAS PAR Un \ . Si vous spécifiez une valeur avec un \ dedans, remplacez le par un double backslash \\.
#"lotusdomainname" : Le nom que la methode getdatabase() va utiliser pour selectionner le serveur.

$databasepath = @(

[PSOBJECT]@{
"server"="\\DOMINO01"
"datapath"="c`$\lotus\domino\data\"
"splitpart"="data"
"lotusdomainname"="domino01/Company France"
}

[PSOBJECT]@{
"server"="\\DOMINO03"
"datapath"="d`$\lotus\domino\data\"
"splitpart"="data"
"lotusdomainname"="domino02/Company UK"
}

)

$resultsarray = @()

#<#

$id =0

$databasepath | foreach  {
    #Pour chaque valeur dans databasepath, je joins "server" et "datapah' pour créer un chemin UNC pour l'analyse avec Robocopy
    $analysed = Join-Path $_.server $_.datapath
    #Le nom du fichier de log. $ID est incrementé, permettant de reconnaitre chaque entrée dans la variable $databasepath
    $logname = "robocopylog_$id.log"
    #Ajout de l'ID correspondante à l'entrée actuelle dans $databasepath 
    $_ | Add-Member -MemberType NoteProperty -Name "logid" -Value $id
    $id++
    $logname = Clean-Filename -Filename $logname

    try 
    {
        New-Item -ItemType file -Name "$logname" -Force -ErrorAction Stop
    }
    catch
    {
        Write-Host "Impossible de créer le fichier de log : $logname"
        continue
    }

    #Lancement d'un nouveau processus Robocopy, qui scannera les dossiers à la recherche de nos précieux fichiers NSF, et enregistrant tout dans le fichier de log précedemment crée
    $results = Start-process "robocopy"  -argumentlist "$analysed NULL *.nsf /NDL /NS /L /S /XJ /TS /NJS /NJH /UNILOG:$logname" -PassThru -Wait -WindowStyle Hidden
    #Les codes de sortie de Robocopy sont ok si en dessous de 7. Au dessus, une erreur s'est produite
    if ($results.ExitCode -gt 7)
    {
        Write-Warning "Erreur avec robocopy!"
        Exit 5
    }
}

#>
#On récupère les fichiers de log robocopy
$loglist = Get-item "robocopylog_*.log"

foreach ($log in $loglist)
{
    #On se lance à la recherche du bon serveur et on collecte les valeurs demandées
    $thisserver = $null
    foreach ($server in $databasepath)
    {
        if ($log.name -eq "robocopylog_$($server.logid).log")
        {
            $thisserver = $server
            break
        }
    }
    if ($thisserver -eq $null)
    {
        Write-host "Impossible de trouver le serveur qui a créé $($log.name)"
        continue
    }
    else
    {
        #On récupère le contenu de chaque fichier...
        Get-Content $log | % {
        #Une petite regex pour retrouver le nom
        if ($_.trim() -match "(?[\\].*$)")
            {
                #Et on retrouve le chemin du fichier NSF sous un format que l'objet Lotus pourra comprendre
                $object = $null
                $cutindex = $Matches.file.IndexOf("$($thisserver.splitpart)\")
                $length = $Matches.file.Length
                try
                {
                    $object = [PSOBJECT]@{
                    "server" = $thisserver.lotusdomainname
                    "nsffile" = $Matches.file.Substring($cutindex,$length-$cutindex)
                    }
                }
                catch
                {
                    Write-Warning "Erreur lors de l'obtention des infos de $($matches.file)"
                }
                $resultsarray += $object
            }
        }
    }
}

#Maintenant que nous avons la liste des fichiers NSF, leur chemin, et le serveur domino qui est rattaché, nous pouvons collecter nos ACLs
$progress = 0
foreach ($entry in $resultsarray)
{

    Write-Progress -Activity "Analyse des ACL" -Status $entry.nsffile -PercentComplete $($progress/$($resultsarray.count)*100)
    $progress++
    $index = 1
    $aclobjectlist = @()

    #Vidage de $db à chaque passage, pour pouvoir vérifier si  la propriété .ISOpen est $false (DB manquante) ou si $db n'existe pas (problem avec le chemin)
    $db = $null

    #Connexion à la DB
    $db = $lotus.GetDatabase($entry.server,$entry.nsffile)

    #On verifie que l'ouverture se fait correctement
    if ($db.IsOpen -eq $false -or $db -eq $null)
    {
        $message = $null
        switch ($db.isopen)
        {
            $null  {$message = $($error[0].Exception.Message)}
            $false {$message = "Pas de base de données $($entry.nsffile) sur $($entry.server)"}

        }

        $errorarray.Add([PSCUSTOMOBJECT]@{
        "Fichier" = $entry.nsffile -as [String]
        "Serveur" = $entry.server -as [String]
        "Erreur" = $message
        })
        Write-Host "Impossible d'ouvrir $($entry.nsffile) sur $($entry.server)"
        continue
    }
    #On obtient la première ACL
    $acl = $db.ACL.GetFirstEntry()
    #On boucle tant qu'on trouve une autre ACL
    while ($acl -ne $null)
    {
        #On récupère si ca n'est pas un serveur et si c'est un groupe ou une personne
        if ($acl.IsServer -ne $true -and ($acl.isGroup -eq $true -or $acl.isperson -eq $true))
        {
            #Je vérifie seulement les propriétés de création et suppression de documents. D'autres droits existent, que vous pouvez ajouter à la suite en utilisant la même syntaxe.
            $aclinfo = [PSCUSTOMOBJECT]@{
            "Name" = $acl.Name
            "CanCreateDocuments" = $acl.CanCreateDocuments
            "CanDeleteDocuments" = $acl.CanDeleteDocuments
            }
            #Ajout de l'objet à la liste d'ACL
            $aclobjectlist += [pscustomobject]@{"Nom" =$acl.name;"Droits"=$aclinfo}

            $index++
        }
            #On obtient la prochaine ACL ... et ainsi de suite
            $acl = $db.ACL.GetNextEntry($acl)
    }
    #Lorsque nous avons fini avec les ACL sur cette DB, on ajoute les résultats au tableau récaputilatif
    $aclarray.Add([PSCUSTOMOBJECT]@{
    "Serveur" = $entry.server -as [String]
    "Fichier" = $entry.nsffile -as [String]
    "ACL" = $aclobjectlist | Sort-Object -Property "Nom"}) | Out-Null
#Et on passe à la suivante
}

#On compte le maximum d'ACLs trouvées sur un DB. Cette valeur maximum sera utilisée pour ajouter des membres aux autres objets, d'avoir le même nombre partout afin d'avoir un fichier CSV à la fin.
$max = 0
$aclarray | % {
    $count = $_.acl.count
    if ($count -gt $max)
    {
        $max = $count
    }
}

#Nouvel objet qui conservera toutes les données après formatage
$exportableaclarray = New-Object System.Collections.ArrayList

#On parcourt les résultats
foreach ($entry in $aclarray)
{
    #On crée une nouvelle ligne qui démarrera avec le nom du serveur, puis le nom de la base NSF
    $object = [PSCUSTOMOBJECT]@{
    "Serveur" = $entry.serveur
    "Fichier" = $entry.Fichier
    }

    $index=1
    #On ajoute des colonnes. D'abord le nom, puis les droits de création, ensuite de suppression pour chaque ACL trouvée
    foreach ($acl in $entry.acl)
    {
        $object | Add-Member -Name "Membre_$index" -MemberType NoteProperty -Value $acl.Nom
        $object | Add-Member -Name "Droits_Creation_membre_$index" -MemberType NoteProperty -Value $acl.Droits.CanCreateDocuments
        $object | Add-Member -Name "Droits_Suppression_membre_$index" -MemberType NoteProperty -Value $acl.Droits.CanDeleteDocuments
        $index++
    }
    #Et on remplit le reste avec des valeurs vides jusqu'a ce que nous ayons le même nombre de colonnes que le fichier NSF avec le plus grand nombre d'ACL
    for ($i = $index;$i -le $max;$i++)
    {
        $object | Add-Member -Name "Membre_$i" -MemberType NoteProperty -Value $null
        $object | Add-Member -Name "Droits_Creation_membre_$i" -MemberType NoteProperty -Value $null
        $object | Add-Member -Name "Droits_Suppression_membre_$i" -MemberType NoteProperty -Value $null
    }
    $exportableaclarray.Add($object) | Out-Null

}

#Puis nous exportons les résultats
$exportableaclarray | Export-Csv "$exportfolder\liste_acl_Notes.csv" -NoTypeInformation -Delimiter ";" -Encoding UTF8
#Et enfin les erreurs rencontrées 
$errorarray | Export-Csv "$exportfolder\erreuracl.csv" -NoTypeInformation -Delimiter ";" -Encoding UTF8

Ajouter un commentaire

Article précédent Article suivant