Aller au contenu

Exécuter du code Powershell au travers d'un service WCF

·890 mots·5 mins
Sommaire

PowerShell permet d’écrire et d’exécuter des scripts avec un modèle objet plutôt complet, et se basant sur le framework .Net. En gros, cela remplace (avantageusement) les scripts .bat, tout en se rapprochant de ce que les shells Unix permettent de faire depuis 20 ans.

Comme toute autre librairie, les dépendances PowerShell sont spécifiques à la machine qui les héberge. Il est évidemment impossible d’accéder à certaines fonctionnalités depuis n’importe quel poste client.

L’idée ci-dessous est d’implémenter un pont entre certaines fonctionnalités PowerShell spécifiques à un Active Directory, pour pouvoir y accéder depuis un poste client. De cette manière, nous nous orientons vers une solution typée micro-services, où nous avons:

  1. Un premier micro-service qui tourne sur un serveur de management Active Directory et dont la fonction sera de créer ou modifier des comptes utilisateurs, en fonction de paramètres spécifiques - le gestionnaire AD,
  2. Un second micro-service qui s’occupera de construire les informations demandées, pour ensuite les envoyer au gestionnaire AD.

Le premier service sera déployé sur le serveur de gestion, et accessible au travers d’IIS, grâce à un service WCF

La difficulté principale consiste donc à

  1. Ecrire les scripts PowerShell de gestion des comptes utilisateurs,
  2. Construire les paramètres corrects à envoyer au premier service
  3. Et récupérer le résultat.

Interfaces de communication
#

Le service WCF ne présente pas vraiment de difficulté, puisqu’il ne fait qu’exposer les paramètres et les différentes méthodes au travers du protocole existant. Une interface de management ([ServiceContract]) expose toutes les méthodes disponibles; celles-ci sont taggées par un attribut [OperationContract]:

[ServiceContract]
public interface IManagement
{
	[OperationContract]
	Result CreateAdAccount(string samaccountname, string password);

	[OperationContract]
	Result CreateHomeFolder(string samaccountname);

	[OperationContract]
	Result CreateEmailAddress(string samaccountname, string emailAddress);

	[OperationContract]
	Result GetADUser(string samaccountname);
}

Chacune de ces méthodes retourne un résultat de type Result, décrit de la manière suivante:

public class Result() {
	public string Content { get; set; }
	public string Status { get; set; }
}

Côté client, il nous sera possible d’appeller ces différentes méthodes grâce à une instance d’un client WCF:

var client = new ADServiceReference.ManagementClient();

var result = client.GetADUser('james_bond');

/* ... */

Implémentation des scripts PowerShell
#

Je ne détaillerai pas l’ensemble des scripts de gestion. Le plus simple à mettre en place concerne le détail de la récupération d’un utilisateur grâce au script get-aduser.ps1 ci-dessous, que l’on placera dans le répertoire Scripts de notre projet WCF:

<#
Récupère l'ensemble des propriétés d'un compte AD.
#>

param(
	[Parameter(Mandatory = $true)] [string] $SAMAccount
)

Import-Module ActiveDirectory

$user = Get-ADUser -Identity $SAMAccount -Properties *
$MaxPasswordAge = (Get-ADDefaultDomainPasswordPolicy).MaxPasswordAge.TotalDays
$passwordExpirationDate = $user.PassWordLastSet + $MaxPasswordAge

Write-Output "DisplayName:", $user.DisplayName
Write-Output "Password last set: ", $user.PasswordLastSet
Write-Output "Password Expiration: ", $passwordExpirationDate
Write-Output ""
Write-Output "Member of: ", $user.MemberOf
Write-Output ""
Write-Output "when changed:", $user.whenChanged
Write-Output "When created:", $user.whenCreated

Le script récupère un compte AD sur base du paramètre $SAMAccount obligatoire; la sortie console est ensuite utilisée pour afficher simplement les informations le caractérisant.

Runspace .Net et exécution des scripts en détails
#

Retour au code .Net pour l’implémentation des méthodes du service WCF: nous allons construire un runspace, qui est une sorte de bac-à-sable d’exécution, en lui passant les paramètres définis dans la signature du script.

La première étape consiste à forcer le chargement des modules et des snapins PowerShell, pour ensuite ouvrir le pipeline et y enquiller les paramètres, avant de lancer l’exécution proprement dite.

En détails, comme nous utilisons la sortie console grâce à la commande Write-Output pour notre script, nous allons utiliser une instance de StringBuilder pour en récupérer le contenu.

Les snapins et modules PowerShell sont chargés grâce au snippet ci-dessous. Une instance de PSSnapInException permet de gérer les éventuelles exceptions qui auraient été levées durant l’import:

InitialSessionState initial = InitialSessionState.CreateDefault();
PSSnapInException snapinException = new PSSnapInException();

initial.ImportPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out snapinException);
initial.ImportPSModule(new string[] { "ActiveDirectory" });

Une fois que le runspace aura été instancié, nous pourrons créer un pipeline (pour nos paramètres) grâce à la méthode runspace.CreatePipeline();.

Chaque paramètre peut être ajouté à l’appel de la fonctio PowerShell grâce à une instance de Command, ayant une correspondance vers l’emplacement de notre script sur le serveur (souvenez-vous, le fichier .ps1 était stocké dans le répertoire Scripts de notre projet):

var cmd = new Command(
	System.Web.HttpContext.Current.Server.MapPath(SCRIPT_PATH)
);

cmd.Parameters.Add(new CommandParameter("SAMAccount", samAccountName));
pipeline.Commands.Add(cmd);

Et finalement, nous pouvons récupérer la sortie console du script PowerShell directement au travers de l’invocation du pipeline:

var result = pipeline.Invoke();

Au complet
#

using System.Management.Automation;
using System.Management.Automation.Runspaces;
using System.ServiceModel.Activation;

public class Management : IManagement
{
	public Result GetADUser(string samAccountName)
	{
		string SCRIPT_PATH = @"Scripts/get-aduser.ps1";

		StringBuilder stringBuilder = new StringBuilder();

		try
		{
			InitialSessionState initial = InitialSessionState.CreateDefault();
			PSSnapInException snapinException = new PSSnapInException();

			initial.ImportPSSnapIn("Microsoft.Exchange.Management.PowerShell.Admin", out snapinException);
			initial.ImportPSModule(new string[] { "ActiveDirectory" });

			using (Runspace runspace = RunspaceFactory.CreateRunspace(initial))
			{
				runspace.ApartmentState = System.Threading.ApartmentState.STA;
				runspace.ThreadOptions = PSThreadOptions.UseNewThread;

				runspace.Open();

				Pipeline pipeline = runspace.CreatePipeline();

				Command cmd = new Command(
					System.Web.HttpContext.Current.Server.MapPath(SCRIPT_PATH)
				);

				cmd.Parameters.Add(new CommandParameter("SAMAccount", samAccountName));

				pipeline.Commands.Add(cmd);

				var result = pipeline.Invoke();

				foreach (var res in result)
				{
					stringBuilder.AppendLine(res.ToString());
				}

				ManageErrors(pipeline);
			}
		}
		catch (Exception ex)
		{
			stringBuilder.Append(ex.ToString());

			return new Result() { Status = "NOK", Content = stringBuilder.ToString() };
		}

		return new Result() { Status = "OK", Content = stringBuilder.ToString() };
	}
}

En conclusion
#

La version présentée ci-dessous est relativement simpliste: il n’y a aucune correspondance effectuée entre un modèle orienté objet et les types définis en PowerShell. Elle permet cependant de se faire une idée générale d’un appel dynamique de script PowerShell hébergé sur un serveur au travers d’un service WCF hébergé sur IIS.