MS Graph maar nu met Perl en LWP
Peter @2026-02-18 17:03:26
Work in progress
Dit artikel wordt momenteel geschreven
Waarom doe ik dit?
In een eerder artikel heb ik met Curl in een Bash script een lijst met groepen opgehaald uit onze Azure omgeving. Dit heb ik oorspronkelijk gedaan om OAuth2 te begrijpen. Ik was destijds bezig om een koppeling te maken tussen ons leerling volg systeem (Magister) en onze Teams omgeving. Ik had al eerder (in 2018) een dergelijke koppeling gemaakt, In die koppeling was MS SDS de interface naar Azure, en dat beviel eigenlijk helemaal niet. Tot overmaat van ramp zou SDS overgaan van versie 1 naar 2, hierbij was volledig onduidelijk of de toenmalige interface nog wel zou werken. Ik besloot daarom om SDS uit de vergelijking te halen en zelf de interface naar Azure te schrijven. Hoe moeilijk kon het zijn toch?
Nou dat viel eigenlijk best wel tegen. Voor mijn voorkeur script taal Perl kon ik eigenlijk geen methode/module vinden waarmee ik op een handige manier API calls kon maken. Voorbeelden ging meestal in op Powershell en Node JS. Zonder waardeoordeel, maar dat zijn niet de talen waarin ik mij thuis voel. Wat ook veelvuldig genoemd werd was simpelweg HTTP request gebruiken. En dat ben ik verder gaan onderzoeken.
Waar ik in eerste instantie op stuk liep was de OAuth2 flow om een applicatie token te krijgen. Het token was noodzakelijk om API calls te kunnen maken. Vandaar dat ik met Curl aan de gang ben gegaan om dit voor elkaar te krijgen. Daarna ben ik verder gegaan met het maken van een Perl implementatie.
Waarom LWP?
Curl is een geweldige tool om vanaf de command line HTTP request te kunnen maken. Ik had er voor kunnen kiezen om ook in Perl Curl te gebruiken. De Perl eigen methode is echter LWP, als je in Perl tenminste kunt spreken van een “eigen”. Op cpan zijn legio modules te vinden voor het maken van HTTP request. De meest Perlish, meest volwassen hiervan leek mij LWP te zijn. Bovendien had ik LWP eerder gebruikt voor de cheat van een spelletje (Omerta). LWP dus. # Een lelijk begin Don’t try this at home. Onderstaand Perl script is een soort van 1 op 1 vertaling van het eerdere Bash script. Daar waar dat Bash script eigenlijk heel elegant was, is dit script oerlelijk. Ik heb het dan ook alleen maar gemaakt om een punt te maken: Je kunt gewoon een lineair script maken die ongeveer hetzelfde doet als het Bash script met Curl. Je zult echter ook met mij eens zijn dat dit lelijk is, dat je het niet zo moet doen. Dat gaan we ook veranderen.
#! /usr/bin/env perl
# Default module imports
use strict; # Altijd goed om strict en warnings te gebruiken
use warnings;
use v5.11; # Gebruik v5.11 voor onder andere de 'say' functie
# Specifieke module imports voor dit script
use Data::Dumper; # Handig voor debuggen, hiermee kun je complexe datastructuren mooi printen
use Config::Simple; # Om makkelijk configuratiebestanden te kunnen lezen
use Time::Piece; # Voor het werken met datums en tijden
use FindBin; # Om het pad van het script te kunnen gebruiken, handig voor het vinden van config bestanden en libraries
use LWP::UserAgent; # Voor het maken van HTTP requests
use JSON; # Voor het werken met JSON data, zoals de responses van de Microsoft Graph API
# Initialisatie van de configuratie
my %config; # Hash om de configuratie in op te slaan
# Lees de configuratie uit azure.cfg, als dat niet lukt, geef een foutmelding en stop het script
Config::Simple->import_from("$FindBin::Bin/azure.cfg",\%config) or die("No config: $!");
# Maak een nieuwe UserAgent aan voor het maken van HTTP requests
my $ua = LWP::UserAgent->new;
# De URL voor het ophalen van een access token van de Microsoft Identity Platform
# Gebruik hier de login endpoint.
my $token_url = $config{'LOGIN_ENDPOINT'}."/".$config{'TENANT_ID'}."/oauth2/token";
# Bereid een request voor om een access token op te halen van de Microsoft Identity Platform
my $token_request = HTTP::Request->new(
POST => $token_url,
[
'Accept' => '*/*',
'User-Agent' => 'Perl LWP',
'Content-Type' => 'application/x-www-form-urlencoded'
],
"grant_type=client_credentials".
"&client_id=" . $config{'APP_ID'} .
"&client_secret=". $config{'APP_PASS'} .
"&scope=" . $config{'GRAPH_ENDPOINT'} . "/.default" .
"&resource=" . $config{'GRAPH_ENDPOINT'},
);
# Execute de request met de LWP UserAgent
my $result = $ua->request($token_request);
# Verwerk het resultaat
my $access_token;
if ($result->is_success){
my $reply = decode_json($result->decoded_content);
$access_token = $reply->{'access_token'};
}else{
print Dumper $result;
die $result->status_line;
}
# We hebben nu een access token,
# dat kunnen we gebruiken om API calls te maken naar de Microsoft Graph API
# Stel een URL samen voor een API call, laten we de groepen weer opvragen als voorbeeld
# Pagina grootte is 1, en we selecteren alleen de id en displayName van de groepen om het overzichtelijk te houden
my $api_url = $config{'GRAPH_ENDPOINT'}.'/v1.0/groups?$top=1&$select=id,displayName';
# Bereid een request voor om de groepen op te halen, gebruik het access token in de Authorization header
my $api_request = HTTP::Request->new(
GET => $api_url,
[
'Accept' => 'application/json',
'User-Agent' => 'Perl LWP',
'Authorization' => "Bearer $access_token"
],
);
# Execute de API request
my $api_result = $ua->request($api_request);
# Verwerk het resultaat van de API call
if ($api_result->is_success){
my $api_reply = decode_json($api_result->decoded_content);
print Dumper $api_reply; # Print de response van de API call, dit zal een lijst van groepen bevatten
}else{
print Dumper $api_result;
die $api_result->status_line;
}Ik heb wel een paar kleine dingen veranderd ten opzichte van het Bash script: - Ik vraag nog maar 1 groep op - En van die groep wil ik alleen de displayName en de ID zien
Dit om het een beetje overzichtelijk te houden, en je ziet gelijk een beetje wat voor mogelijkheden een oData filter geeft.
Lelijk als het is, het werkt wel:
{
'@odata.nextLink' => 'https://graph.microsoft.com/v1.0/groups?$top=1&$select=id%2cdisplayName&$skiptoken=[skip token]',
'value' => [
{
'id' => '[group id]',
'displayName' => '[group displayName]'
}
],
'@odata.context' => 'https://graph.microsoft.com/v1.0/$metadata#groups(id,displayName)'
}De property value bevat een array met een
hash voor elke gevonden groep. Omdat ik een
top=1 gevraagd heb is dit er maar één. Aangezien er meer
groepen zijn is er ook hier weer de @odata.nextlink, daar
zal ik later op terug komen.
Deze code is natuurlijk vreselijk. Lelijk en niet te onderhouden. Door functies te gebruiken zou je het al een stuk netter kunnen maken. Maar ik gebruik tientallen scripts die allemaal de een of andere API query doen, dat is een onderhouds nachtmerrie als ik iets wil wijzigen. Ik heb er daarom voor gekozen om de functionaliteit onder te brengen in modules, en eigenlijk in (Moose) objecten.