Activité Création automatique de comptes d'utilisateur·rice·s en PowerShell

Consigne

Suivez les instructions ci-après et effectuez les tâches demandées.

Situation

Dans le cadre de l’automatisation d’une partie du processus d’onboarding dans votre entreprise, on vous demande de réaliser un script capable de créer des comptes d’utilisateur·rice·s à partir d’un fichier CSV.

Objectifs

À la fin de ce travail, vous devez :

  1. Connaître les cmdlet Get-Help, Get-Command et Get-Member
  2. Connaître les cmdlet ForEach-Object et Where-Object
  3. Connaître la cmdlet Import-Csv
  4. Être capable de lire et d’utiliser les données d’un fichier CSV.
  5. Connaître les cmdlet New-LocalUser, Get-LocalUser, Set-LocalUser
  6. Connaître les cmdlet New-LocalGroup, Get-LocalGroup, Set-LocalGroup
  7. Connaître les cmdlet Add-LocalGroupMember, Get-LocalGroupMember
  8. Connaître la cmdlet ConvertTo-SecureString

Résultat attendu

  • Un bref rapport qui décrit votre travail et votre démarche.
  • Votre script PowerShell.

Ressources

Documents :

Logiciel :

  • Visual Studio Code

Matériel :

  • Une machine Windows à l’adresse IP 172.20.##.111.

Mise en route

Administrer Windows avec SSH

Pour administrer à distance une machine Windows, on utilise traditionnellement l’interface graphique à l’aide du Remote Desktop protrocol (RDP). Mais il est également possible de le faire en ligne de commande à l’aide du PowerShell Remoting, ou à travers une connexion SSH.

Le PowerShell Remoting est puissant, mais il est conçu pour être utilisé à l’intérieur d’un domaine ActiveDirectory, et est globalement assez difficile à mettre en œuvre.

Dans cette activité, nous allons donc utiliser directement SSH. Pour cela, ouvrez une fenêtre de terminal sans privilège et tapez la commande ci-après, en remplaçant ## par le numéro de votre réseau, et saisissez votre mode passe lorsque vous y êtes invité.

1
ssh Administrateur@172.20.##.111

Activer l’authentification par clé publique

Sous Windows, l’authentification par clé publique fonctionne de la même manière que sous Linux. Pour un compte utilisateur·rice normal (non-administrateur), il suffit d’ajouter votre clé publique dans le fichier $home.ssh\authorized_keys. Toutefois, si votre compte est membre du groupe Administrateurs (comme c’est le cas du compte Administrateur), vous devez ajouter votre clé dans le fichier ‘C:\ProgramData\ssh\administrators_authorized_keys’.

Lorsque c’est fait, quittez votre session sur le serveur et relancez la commande pour vérifier que vous pouvez vous connecter sans mot de passe.

Utiliser le fichier de configuration de SSH

Comme il n’y a pas d’entrée dans le DNS pour ces machines, nous pouvons utiliser la configuration de ssh associer un nom et une configuration à l’adresse IP. Par défaut, la configuration du ssh pour un·e utilisateur·rice se trouver dans le fichier .ssh/config. Sur le système de votre ordinateur personnel, ouvrez le fichier .ssh/config dans un éditeur de texte et ajoutez y les lignes ci-dessous ; créez le fichier s’il n’existe pas déjà.

1
2
3
Host wks-m123
    HostName 172.20.32.111
    User Administrateur

De manière générale le format du fichier de configuration ressemble au contenu de la figure ci-dessous. Les noms hostname1 et hostname2 sont des noms définis par l’utilisateur·rice et ne doivent pas nécessairement correspondre au nom de l’hôte cible. Le FQDN ou l’adresse IP de l’hôte cible est spécifié à l’aide de l’option SSH “HostName”. L’option “User” permet de spécifier le nom du compte d’utilisateur·rice par défaut.

On peut trouver une liste de toutes options dans le manuel de ssh. Enfin, l’entrée “Host *” contient les options par défaut, applicable à tous les hôtes.

1
2
3
4
5
6
7
8
9
Host *
    SSH_OPTION value

Host hostname1
    SSH_OPTION value
    SSH_OPTION value

Host hostname2
    SSH_OPTION value

Vous pouvez maintenant vous connecter à votre serveur avec la commande ci-dessous. Si vous ne spécifiez pas le nom d’utilisateur·rice, la connexion est effectuée avec le compte Administrateur.

1
ssh wks-m123

Modifier le shell par défaut du service sshd

Par défaut, le shell par défaut du service sshd est cmd.exe. Lorsqu’on se connecte en SSH sur une machine Windows et que l’on souhaite utiliser PowerShell, on doit lancer manuellement la commande PowerShell.

Une autre option est de modifier le shell par défaut dans la configuration du service sshd avec la commande suivante :

1
New-ItemProperty -Path "HKLM:\SOFTWARE\OpenSSH" -Name DefaultShell -Value "C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -PropertyType String -Force

Éditer du texte

Éditer du texte en ligne de commande

Par défaut, Windows n’a pas d’éditeur de texte en ligne de commande. L’éditeur de texte standard est “notepade.exe”, dont les fonctionnalités sont particulièrement limitées, nécessite l’environnement graphique de Windows.

Toutefois, nous pouvons également utiliser vim sous Windows à condition de l’installer. Pour cela, connectez-vous en SSH à votre machine Windows avec le compte Administrateur, et lancez la commande suivante :

1
choco install vim

Vous pouvez maintenant utiliser l’éditeur vi comme vous le faites sous Linux.

Éditer du texte à distance avec Visual Studio Code

Une autre option est d’utiliser l’extension Remote SSH de Visual Studio Code, qui permet d’éditer des fichiers qui se trouvent sur une machine distante.

Pour cela, ouvrez une nouvelle fenêtre de VSCode et installez le Remote Development Extension Pack, si ce n’est pas déjà fait. Ce pack d’extension permet, entre autres, de connecter VSCode à des machines distantes via le protocole SSH et de développer à l’aide de conteneurs (container).

Pour vous connecter à une machine distante, pressez les touches Ctrl+Maj+P ou Maj+Commande+P pour afficher la palette des commandes. Tapez “ssh connect” et sélectionnez “Remote-SSH: Connect To Host”. En principe, vous devriez voir le nom que vous avez donné à votre machine Windows dans le fichier *.ssh/config. Si c’est le cas, sélectionnez l’entrée pour ouvrir une connexion. Si ce n’est pas le cas, demandez de l’aide à votre enseignant.

VSCode Remote SSH

Si la connexion a été effectuée avec succès, vous devriez voir le nom de votre machine dans la zone verte en bas à gauche comme sur la figure ci-dessus. Si c’est le cas, VSCode est connecté à votre machine et si vous cliquez “Open Folder…”, vous pouvez parcourir le système de fichier de cette machine. Vous pouvez également ouvrir une fenêtre de terminal intégré à partir du menu ou en utilisant la palette de commande (voir plus haut). Avec la palette de commande, commencez à taper “Create New Terminal” et sélectionnez l’entrée “Terminal: Create New Terminal” lorsqu’elle apparaît. Vous pouvez maintenant manipuler votre système de fichier avec PowerShell.

On peut, par exemple, utiliser cette fenêtre de terminal pour créer une structure de répertoire $home\test\m122\powershell, avec la commande ci-dessous. Cette commande est l’équivalent PowerShell de la commande Bash mkdir -p ~/test/m122/powershell. Observez le nom des commandes et les résultats affichés, nous y reviendrons.

1
New-Item -ItemType "directory" -Path "$home\test\m122\powershell"

Pour afficher le contenu d’un répertoire, on utilise la commande Get-ChildItem. Par exemple, on peut afficher le contenu du répertoire courant, avec la commande suivante :

1
Get-ChildItem -Path "./"

Pour supprimer un répertoire ou un fichier, on utilise la commande Remove-Item, et la commande Move-Item pour déplacer un fichier ou un répertoire. Par exemple, pour supprimer le répertoire test, vous pouvez lancer la commande ci-après. L’option -Recurse indique à la commande qu’elle doit supprimer récursivement le contenu du répertoire.

1
Remove-Item -Recurse -Path "$home\test" 

Introduction à PowerShell

Qu’est-ce que PowerShell ?

Sous Windows, le shell en ligne de commande historique est le programme CMD.EXE. Ce shell est un descendant direct du programme COMMAND.COM de MSDOS.Même si ce shell est toujours utilisé, on lui préfère aujourd’hui un shell plus moderne apparu plus ou moins en même temps que Windows Vista au milieu des années 2000.

Comme nous l’avons vu, un shell est une interface utilisateur pour les fonctionnalités du système d’exploitation. Comme il s’agit d’un programme qui s’exécute dans l’espace utilisateur (par opposition à l’espace noyau), il dépend de l’interface de programmation d’application (API) du système.

Dans le cas du Bash et des autres shells des systèmes POSIX, l’API est essentiellement la bibliothèque standard du C (C standard library). Dans le cas, du CMD.EXE, l’API est Windows API, également connu sous le nom Win32 API. Comme pour l’API des systèmes POSIX, cette API a été conçue pour le langage C.

L’API sur laquelle repose PowerShell est le .NET Framewwork pour les versions du shell intégré à Winwows, et le .NET Core pour les versions multiplateformes. Ces API ne sont pas directement des API de Windows, mais d’un environnement d’exécution comparable à la JVM (Java Virtual Machine), appelé CLR (Common Langage Runtime).

Flux de caractères et flux d’objets

Les shells POSIX et le cmd.exe sont essentiellement basés sur des flux de caractères (stdio) et des variables d’environnements dont le contenu est du texte.

En PowerShell en revanche, même si les commandes et les arguments sont toujours saisis sous forme de texte, les flux d’entrée et de sortie des commandes (appelées cmdlet) ne véhiculent pas du texte, mais des objets, et les variables sont typées. Par exemple, lorsque vous avez créé votre répertoire test, la commande a affiché le résultat suivant :

1
2
3
4
5
    Directory: C:\Users\Administrateur\test\m122        

Mode                 LastWriteTime         Length Name      
----                 -------------         ------ ----      
d----          18.06.2024    10:28                powershell

Il s’agit de la représentation par défaut de l’objet de type System.IO.DirectoryInfo renvoyé par la commande New-Item. Voyons cela de plus près.

Si vous ne l’avez pas déjà fait, commencez par supprimer le répertoire test avec la commande Remove-Item. Puis lancez la commande suivante :

1
$myTestDirectory = New-Item -ItemType "directory" -Path "$home\test\m122\powershell"

Cette fois la commande n’affiche rien, car nous avons affecté le résultat à la variable $myTestDirectory. Remarquez au passage, de quelle manière sont définies les variables en PowerShell. Le nom d’une variable commence toujours par un $; contrairement au Bash, le signe $ fait partie du nom, et il peut y avoir des caractères blancs avant et après le signe égal.

Essayez maintenant, tapez la ligne ci-dessous, pressez la touche Entrée, et observez ce qu’il se passe.

1
$myTestDirectory

Le shell affiche de nouveau une représentation par défaut de l’objet. Le principe est le suivant : le shell interprète la ligne de commande. Si la ligne de commande contient une expression (une commande qui renvoie un objet, une variable, une opération, ou simplement une constante), le shell écrit la valeur de l’expression dans la sortie standard. Si la sortie standard ne supporte pas un flux d’objets (p. ex. le terminal), le shell produit une représentation par défaut de la valeur de l’expression, sous forme de texte.

Voyons ce qu’il se passe avec la commande suivante :

1
$myTestDirectory.GetType()
1
2
3
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     False    DirectoryInfo                            System.IO.FileSystemInfo

La méthode GetType() est une fonction qui renvoie le type de la variable. Le shell affiche donc une représentation par défaut de l’objet de type DirectoryInfo qui représente ce type (en PowerShell, le type d’une variable correspond au type de sa valeur).

Essayons encore la commande suivante :

1
$myTestDirectory.FullName
1
C:\Users\Administrateur\test\m122\powershell

Cette fois, la commande utilise la propriété FullName de l’objet qui représente le répertoire pour obtenir une chaine de caractère qui contient le nom complet du type.

Utilisation des tubes

Puisque PowerShell écrit des objets dans la sortie standard, on peut utiliser un tube (pipe |) pour passer un objet à une autre commande. Par exemple, lancez la commande suivante :

1
$myTestDirectory | Get-Member -Type Properties
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
   TypeName: System.IO.DirectoryInfo

Name                MemberType     Definition
----                ----------     ----------
Target              AliasProperty  Target = LinkTarget
LinkType            CodeProperty   System.String LinkType{get=GetLinkType;}
Mode                CodeProperty   System.String Mode{get=Mode;}
ModeWithoutHardLink CodeProperty   System.String ModeWithoutHardLink{get=ModeWithoutHardLink;}
ResolvedTarget      CodeProperty   System.String ResolvedTarget{get=ResolvedTarget;}
PSChildName         NoteProperty   string PSChildName=powershell
PSDrive             NoteProperty   PSDriveInfo PSDrive=C
PSIsContainer       NoteProperty   bool PSIsContainer=True
PSParentPath        NoteProperty   string PSParentPath=Microsoft.PowerShell.Core\FileSystem::C:\Users\Administrateur\test\m122
PSPath              NoteProperty   string PSPath=Microsoft.PowerShell.Core\FileSystem::C:\Users\Administrateur\test\m122\powershell
PSProvider          NoteProperty   ProviderInfo PSProvider=Microsoft.PowerShell.Core\FileSystem
Attributes          Property       System.IO.FileAttributes Attributes {get;set;}
CreationTime        Property       datetime CreationTime {get;set;}
CreationTimeUtc     Property       datetime CreationTimeUtc {get;set;}
Exists              Property       bool Exists {get;}
Extension           Property       string Extension {get;}
FullName            Property       string FullName {get;}
LastAccessTime      Property       datetime LastAccessTime {get;set;}
LastAccessTimeUtc   Property       datetime LastAccessTimeUtc {get;set;}
LastWriteTime       Property       datetime LastWriteTime {get;set;}
LastWriteTimeUtc    Property       datetime LastWriteTimeUtc {get;set;}
LinkTarget          Property       string LinkTarget {get;}
Name                Property       string Name {get;}
Parent              Property       System.IO.DirectoryInfo Parent {get;}
Root                Property       System.IO.DirectoryInfo Root {get;}
UnixFileMode        Property       System.IO.UnixFileMode UnixFileMode {get;set;}
BaseName            ScriptProperty System.Object BaseName {get=$this.Name;}

La commande Get-Member renvoie tous les membres (méthodes, propriété, etc.) d’un objet. L’option -Type permet de limiter l’affichage à un type de membre particulier (ici, uniquement les propriétés).

Liste et itérateur

Pour créer un tableau de valeur en PowerShell, il suffit d’écrire une série de valeurs séparées par des virgules. Par exemple, la ligne suivante écrit une List d’objet dans la sortie standard.

1
3, 10, 22, 11

Contrairement à Java, les éléments de la liste peuvent être de différents types. Par exemple :

1
145, "Une chaîne.", 3.14, "Une autre chaîne.", $true

En PowerShell, pour itérer les éléments d’un tableau, on utilise rarement une structure de boucle « for » comme en Java ou en Bash. On préfère utiliser la cmdlet ForEach-Object. Par exemple, lancez la commande ci-après pour afficher le type de chaque élément de cette liste. La commande qui se trouve dans l’accolade est exécutée une fois pour chaque élément de la liste. La variable $_ représente l’élément courant.

1
145, "Une chaîne.", 3.14, "Une autre chaîne.", $true | ForEach-Object { $_.GetType().FullName }
1
2
3
4
5
System.Int32
System.String
System.Double
System.String
System.Boolean

Pour créer une liste de valeurs contiguës, par exemple, une liste d’entiers de 5 à 20, on peut utiliser la syntaxe suivante :

1
5..20

La cmdlet Where-Object de filter les éléments d’une liste. Par exemple, lancez la commande suivante pour produire une liste de tous les multiples de 10 entre 1 et 100.

1
1..100 | Where-Object { $_ % 10 -eq 0 }

Entre les accolades de la cmdlet Where-Object, vous reconnaissez une expression booléenne, composée d’une opération avec l’opérateur % (modulo) et d’une comparaison avec l’opérateur -eq pour tester l’égalité. La commande renvoie dans la sortie standard toutes les valeurs divisibles par 10.

Comme toutes les commandes précédentes sont des expressions, vous pouvez affecter le résultat de ces commandes à des variables. Par exemple :

1
2
$a1 = 145, "Une chaîne.", 3.14, "Une autre chaîne.", $true
$a2 = 1..100 | Where-Object { $_ % 10 -eq 0 }

Utiliser une collection renvoyée par une commande

Bon nombre de cmdlet renvoient des collections d’objets. C’est le cas par exemple de la valeur renvoyée par la commande Get-ChildItem :

1
2
$test = Get-ChildItem -Path "$home"
$test.GetType()
1
2
3
IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     Object[]                                 System.Array

La méthode GetType() nous informe qu’il s’agit d’un tableau d’objet.

Lorsque l’on doit traiter un tableau d’objet, on peut passer ce tableau la cmdlet ForEach-Objet pour effectuer une action avec chaque élément. Par exemple, lancez la commande ci-après pour afficher le nom complet de chaque élément du répertoire $home dans la fenêtre du terminal avec la commande Write-Host.

1
Get-ChildItem $home | ForEach-Object { Write-Host $_.FullName }

On peut également utiliser la commande Where-Object pour filtrer le contenu d’une collection avant de l’itérer. Par exemple :

1
Get-ChildItem $home | Where-Object { $_.Name.StartsWith("D") } | ForEach-Object { Write-Host $_.FullName } 

Aggrégation de valeurs

Pour finir, voyons encore une fonctionnalité de la commande ForEach-Objet. Avec trois blocs cette commande permet d’aggréger les objets d’une façon ou d’une autre. Par exemple:

1
1..10 | ForEach-Object { $sum = 0 } { $sum += $_ } { Write-Output $sum }

Le premier bloc est exécuté une seule fois avant de commencer l’itération, le second bloc est exécuté une fois pour chaque élément de la collection, et le troisième bloc est exéctuté une fois, à la fin de l’itération.

En principe, une commande Powershell s’écrit sur une seule ligne. Pour écrire une commande sur plusieurs ligne on peut utiliser le caractère backtick (`) pour indiquer que la commande se poursuit sur la ligne suivante. Par exemple:

1
2
3
4
5
6
Get-ChildItem $home `
  | Where-Object { $_.Name -like "D*" } `
  | ForEach-Object { Write-Output $_.Name.ToUpper() } `
  | ForEach-Object { $fileNames = "" } `
    { $fileNames += " " + $_ } `
    { "Fichier: $($filenames.Trim())" }

Informations complémentaires

Après avoir discuté avec votre supérieur, vous avez obtenu les informations ci-après.

  • La première ligne du fichier contient les en-têtes
  • Le caractère de séparation est le point-virgule (;).
  • Les champs Group1, Group2, Group3 et Group4 sont les noms des groupes supplémentaires de l’utilisateur·rice. Ces champs sont optionnels et peuvent donc être vides.
  • Si un groupe n’existe pas, il doit être créé.
  • Si un compte existe déjà, le script ne doit pas échouer et le compte ne doit pas être modifié.
  • Le script doit écrire les informations ci-après dans stderr :
    • la liste des comptes créés,
    • la liste des comptes qui n’ont pas été modifiés (s’il y en a),
    • la liste des groupes crée (s’il y en a).

Votre supérieur vous demande encore de regarder les cmdlet suivantes :

  • Import-Csv : lit un fichier CSV et renvoie une collection d’objets. Chaque objet représente une ligne du fichier. Si le fichier a des entêtes, les noms des entêtes sont utilisés pour les noms des propriétés.
  • ConvertTo-SecureString : convertir une chaîne de caractère en une chaîne sécurisée requise par certaines cmdlets.

Tâches

Réaliser le script

On vous demande de réaliser un script en bas nommé Add-Users qui s’utilise de la manière suivante : addusers <chemin du fichier csv>

Si le script est exécuté sans paramètre ou si le premier paramètre est -Help, il doit afficher la syntaxe de la commande et le format du fichier CSV, dans la fenêtre du terminal.

Si le fichier passé en paramètre n’existe pas, le script doit afficher un message d’information dans la sortie d’erreurs.

Si le script est lancé sans privilège, il doit se terminer immédiatement avec un message d’information dans la sortie d’erreurs.

N’hésitez pas à interroger un chatbot pour vous aider, mais il est important de comprendre ce qu’il vous propose et d’expérimenter par vous-même pour bien comprendre ce que fait chaque partie du script.