Esporta (0) Stampa
Espandi tutto

Introduzione al “Single Sign On”

Di Mauro Servienti - Microsoft MVP

Uno degli argomenti più caldi in questo periodo è la sicurezza, fortunatamente e sempre più spesso, anche gli sviluppatori se ne stanno interessando prendendola come un argomento serio che deve essere affrontato sin dalle prime fasi dell’analisi di una nuova applicazione.

La sicurezza in quanto tale deve essere pensata a tutti i livelli:

  • a livello architetturale: la sicurezza va pensata globalmente, per tutta l’applicazione, vedendo la stessa come un corpo estraneo all’interno del nostro sistema, l’applicazione deve essere in grado di difendersi dagli attacchi che il nostro sistema può, più o meno volontariamente, portarle o, come minimo, deve sapere che determinati attacchi possono arrivare;

  • a livello di codice: ogni singola parte del nostro codice deve partire dal presupposto che i controlli di sicurezza fatti ad un altro livello possano fallire, possano non esserci o possano essere sbagliati. In questa direzione fortunatamente il .NET Framework ci viene incontro fornendoci due strumenti, quali la sicurezza dichiarativa e quella imperativa, veramente eccelsi;

  • a livello sociologico: anche se abbiamo realizzato tutto secondo i canoni e i dettami più rigorosi l’ultimo ostacolo alla vera sicurezza resta l’impatto che la nostra applicazione avrà sull’utilizzatore finale, quali passi quest’ultimo dovrà intraprendere per far si che la sicurezza venga rispettata e mantenuta, quali sforzi dovrà fare per digerire il nostro concetto di sicurezza e per digerire l’approccio e l’impronta che noi abbiamo deciso di dare alla sicurezza nella nostra applicazione.

    Se i passi e gli sforzi che l’utente dovrà fare sono o troppo difficili o comportano tempi di attesa decisamente non accettabili o, peggio, hanno conseguenze quantomeno scomode, allora significa che abbiamo fallito, la sicurezza non verrà rispettata e le falle che si creeranno nella nostra applicazione rischieranno di diventare falle per l’intero sistema ospite, compromettendo, in questo modo, la sicurezza dell’intera infrastruttura.

    Cominceremo ad assistere a fenomeni ben noti a tutti come la scelta di password decisamente troppo semplici o peggio ancora lo scambio di credenziali tra gli utenti per poter eseguire operazioni che con il proprio account o non possono essere fatte o sono troppo complesse da eseguire;

  • a livello amministrativo: quello amministrativo è un’altro punto essenziale, la sicurezza ha un costo anche in termini di manutenzione nel tempo, pensiamo solo al tempo che un amministratore di rete dedica a mantenere aggiornata la struttura di Active Directory che ha in gestione, se l’infrastruttura di sicurezza introdotta dalla nostra applicazione ah un costo di gestione elevato rischiamo che alla lunga venga trascurata perchè troppo onerosa.

Se focalizziamo l’attenzione sugli ultimi due punti ci rendiamo rapidamente conto che uno dei must è quello di cercare di rendere la sicurezza il più integrata possibile con il sistema ospite, cercare di renderla il più trasparente possibile a tutti, sia agli amministratori che dovranno gestirla e ancora di più agli utenti finali che dovranno “subirla” come imposizione.

In questa direzione una risposta e, di conseguenza, una possibile soluzione è il concetto di “Single Sign On”.

In questa pagina

il “Single Sign On” il “Single Sign On”
Pregi e difetti Pregi e difetti
Un sistema custom Un sistema custom
L’architettura L’architettura
“System.DirectoryServices” “System.DirectoryServices”
Il risultato Il risultato
Conclusioni Conclusioni
Risorse: Risorse:
Strumenti Strumenti

il “Single Sign On”

Si parla di sistema basato su “Single Sign On” (SSO) quando le richieste di autenticazione non vengono direttamente gestite dal sistema stesso ma vengono ridirette verso un’altro sistema di autenticazione che ha precedentemente certificato le credenziali dell’utente connesso, senza quindi avere la necessità di richiedere nuovamente le credenziali per l’accesso.

Il nodo cruciale è l’ultima parte della frase: “senza avere la necessità di richiedere le credenziali all’utente”, quante volte abbiamo osservato utenti che lanciano l’applicazione, magari il gestionale aziendale, questa chiede, giustamente, delle credenziali e l’utente fornisce come username “Admin” e lascia la password vuota...e alla nostra richiesta di delucidazioni ci sentiamo rispondere che non è un problema li fanno tutti così perché se no il sistema non funziona.

Se indagassimo a fondo, probabilmente scopriremmo che il sistema funziona perfettamente ma la gestione e la manutenzione della sicurezza sono un compito che nessuno si è mai voluto accollare perché troppo oneroso, portando all’inevitabile conseguenza che la sicurezza viene vissuta come una fatica, come un’ulteriore ostacolo verso la realizzazione del proprio lavoro quotidiano.

L’obiettivo principe del Single Sign On è proprio quello di rendere la sicurezza trasparente all’utente finale, facilmente manutenibile e gestibile per gli amministratori; l’utente deve rendersi conto di lavorare in un sistema sicuro, ma non deve assolutamente vivere la sicurezza come un onere aggiuntivo.

In che modo il “Single Sign On” ci può aiutare?

Facciamo innanzitutto una premessa doverosa: di qualcuno ci dobbiamo necessariamente fidare, che questo qualcuno sia l’utente finale che digita la combinazione di username e password o sia Windows che ha in precedenza autenticato l’utente connesso fa poca differenza, dobbiamo scegliere di chi fidarci e l’esperienza mi ha fatto dire che è meglio fidarsi di Windows piuttosto che chiedere nuovamente le credenziali all’utente; tutto ciò per i motivi che abbiamo appena elencato.

Se osserviamo il comportamento dello stesso sistema operativo notiamo che fa largo uso del concetto di “Single Sign On”; ad esempio ogni volta che lanciamo una Management Console per eseguire un compito amministrativo non ci vengono nuovamente chieste le credenziali ma, il sistema semplicemente verifica che l’utente appartenga ad un determinato gruppo di sicurezza e, in caso affermativo, consentirà l’uso della Console, mentre, in caso negativo, semplicemente lo bloccherà negando l’accesso.

Perché quindi non sfruttare la stessa logica all’interno delle nostre applicazioni?

In questa direzione fortunatamente il .NET Framework ci aiuta, facilitandoci di molto il compito, per realizzare un semplice, ma decisamente efficace, sistema basato su “Single Sign On” è sufficiente, all’avvio dell’applicazione, fare qualcosa del tipo:

//Recuperiamo l'identità corrente.
WindowsIdentity wi = WindowsIdentity.GetCurrent();

/*
 * Costruiamo un principal per contenere l'identità
 * di windows appena recuperata.
 */
WindowsPrincipal wp = new WindowsPrincipal( wi );

/*
 * Assegniamo il WindowsPrincipal creato al Thread 
 * principale della nostra applicazione.
 */
Thread.CurrentPrincipal = wp;

A seguito di questa assegnazione possiamo semplicemente scrivere:

/*
 * Verifichimao che l'utente appartenga ad un gruppo
 * tra quelli predefiniti e solo in caso affermativo 
 * procediamo.
 */
if( Thread.CurrentPrincipal.IsInRole( "Users" ) )
{
//L’utente appartiene al gruppo Users di Windows.
}

Questo semplice esempio ci fa capire come possa essere facile integrare un sistema di “Single Sign On” all’interno delle nostre applicazioni appoggiandoci semplicemente alla gestione dei gruppi di Windows e paragonando “Identity” e “Role” del .NET Framework con “Utenti” e “Gruppi” del sistema operativo.

E’ altrettanto semplice utilizzare la sicurezza dichiarativa attraverso gli attributi messi a disposizione dal .NET Framework:

[ PrincipalPermission( SecurityAction.Demand, Role="Users" ) ]
public void MySecureMethod()
{
/*
 * Se il CurrentPrincipal del Thread che esegue
 * questo metodo non appartiene (IsInRole == false)
 * al gruppo Users il metodo fallirà con una
 * SecurityException
 */
}

Un’altra considerazione importante è che il sistema si integra perfettamente anche con Active Directory, quindi il seguente blocco di codice funziona tanto quanto il precedente:

[ PrincipalPermission( SecurityAction.Demand, Role="Domain Admins" ) ]
public void MySecureMethod()
{
//...
}

Negli esempi riportati ho sempre usato dei gruppi cosiddetti builtin, cioè predefiniti, ma nulla ci vieta di creare i nostri gruppi di sicurezza e usare quelli.

Pregi e difetti

A prima vista questo sistema sembra soddisfare tutte le possibili esigenze:

  • si integra con Active Directory e ci permette di centralizzare al massimo la gestione della security in un ambiente familiare agli amministratori, inoltre rende totalmente trasparente all’utente finale la gestione della sicurezza, non richiede di digitare una nuova combinazione di utente e password e ci garantisce che l’applicazione “giri” sempre con le credenziali che vogliamo, cioè quelle dell’utente connesso.

    Questo ci protegge anche dallo scambio di credenziali tra gli utenti perché è sufficiente sensibilizzare l’utente facendogli notare che nel momento in cui dovesse “cedere” le sue credenziali il nuovo possessore avrebbe accesso, ad esempio, a tutti i sui documenti privati e alla posta elettronica.

  • L’utilizzo di un sistema di “Single Sign On” come questo ha un’ulteriore vantaggio, per eseguire l’applicazione con credenziali diverse è sufficiente utilizzare “Run As” e inserire le credenziali di un’altro utente di Active Directory o di Windows senza la necessità di cambiare l’utente corrente del sistema operativo eseguendo un logoff e un nuovo logon.

Purtroppo però non è tutto così semplice e nella vita reale le necessità di un utente non sono sempre soddisfabili con poche righe di codice.

Procediamo con ordine:

  • se l’utilizzo di “Run As” ha si un indubbio vantaggio porta anche qualche complicazione difficilmente aggirabile. “Run As” esegue l’applicazione in una nuova sessione, eseguendo prima il logon del nuovo utente e quindi lanciando il processo; quindi se abbiamo bisogno di accedere, dalla nostra applicazione che gira in una sessione diversa, alla cartella “Documenti” dell’utente corrente il risultato sarebbe un doveroso accesso negato; certo la soluzione è quella di garantire l’accesso anche al “nuovo” utente ma non è detto che questo sia sempre possibile o non è detto che le policy aziendali lo consentano. Questa soluzione ci obbliga inoltre a modificare le policy di sicurezza di ogni singola macchina su cui l’applicazione è installata; policy di sicurezza che poi sono da gestire e mantenere nel tempo.

  • Se l’applicazione deve essere utilizzata anche da utenti mobili, quindi installata su dei computer portatili, Windows non esegue il caching dell’appartenenza a un gruppo di Active Directory rendendo vano tutto il lavoro svolto. Se infatti cerchiamo di usare IPrincipal.IsInRole() o la sicurezza dichiarativa, passando come argomento un gruppo di Active Direcotry, quando la macchina è disconnessa dal dominio, verrà generata un’eccezione che ci informa che la relazione di trust tra la macchina client e il suo controller di dominio non può essere stabilita.

  • Questa soluzione ci impedisce di realizzare una sorta “Fast User Switching” all’interno della nostra applicazione: pensiamo ad esempio ad uno scenario in cui un utente normale è autorizzato a gestire le commesse all’interno del gestionale aziendale, ma non ha i permessi necessari per creare una nuova commessa. Si potrebbe pensare a un sistema che, alla richiesta di creazione di una nuova commessa, chieda all’utente le credenziali per effettuare quella specifica operazione, e solo quella, senza la necessità di chiudere l’applicazione e avviarla con credenziali diverse o peggio ancora effettuare un logon con un altro utente semplicemente per fare quella determinata operazione.

  • Se avessimo la necessità di distribuire l’applicazione su macchine che non appartengono al nostro dominio Active Directory o che non appartengono affatto ad un dominio, perché ad esempio montano Windows XP Home Edition o perché appartengono ad un dominio diverso come nel caso della macchina di un consulente, non avremmo nessun modo di gestire la sicurezza nella nostra applicazione.

  • Se per le nostre necessità i concetti di “Identity” e “Role” non fossero sufficienti, ma avessimo bisogno di inserire il concetto di “Right”, cioè un utente appartenente ad un determinato gruppo può o non può (esplicitamente o implicitamente) eseguire una determinata operazione, allora il modello di sicurezza basato solo su utenti e gruppi non sarebbe più sufficiente. In uno scenario di questo tipo sarebbe decisamente complesso integrare, ad esempio in Active Directory, nuovi concetti quali quello di “diritto”; saremmo obbligati ad estendere lo schema di Active Directory, a realizzare l’interfaccia utente per amministrare i nuovi attributi introdotti e a mantenere allineato il nostro codice con le versioni future di Active Directory stessa.

Un sistema custom

Se i limiti che abbiamo esposto fossero dei limiti bloccanti allo sviluppo della nostra applicazione ci ritroveremmo nella scomoda situazione di dover realizzare un nostro sistema di autenticazione. Fortunatamente questo non ci impedirà di sfruttare comunque a pieno i concetti che stanno alla base del “Single Sign On” integrando perfettamente la sicurezza del nostro applicativo e rendendola allo stesso modo totalmente trasparente all’utente finale.

L’architettura

Realizzare un sistema che soddisfi tutte le nostre esigenze non è certo un compito semplice ma neanche impossibile, procediamo per gradi.

Cominciamo con l’introdurre la struttura che ha la base dati che ospiterà i dati relativi alla sicurezza:

schema

Abbiamo una tabella “Users” dedicata a contenere i dati relativi agli utenti, una tabella “Roles” che conterrà l’elenco dei ruoli, che abbiamo detto essere paragonabili ai gruppi di sicurezza del sistema operativo, e infine una tabella “RolesRelations” che è dedicata a mantenere le relazioni molti a molti tra gli utenti e i ruoli e, nel caso specifico, tra i ruoli e i ruoli stessi permettendoci di gestire quindi l’inserimento di un ruolo all’interno di un altro ruolo semplificando, in questo modo, di molto la manutenzione nel tempo delle policy di sicurezza dell’applicazione stessa.

Un campo chiave nella base dati è, nella tabella “Users”, il campo “AccountSID”, che come si evince facilmente dal nome, è dedicato a contenere il SID (Security Identifier) dell’utente di Windows o di Active Directory che desideriamo possa accedere alla nostra applicazione. La tabella però non contiene solo un riferimento al SID dell’utente perché è strutturata per permetterci di inserire anche utenti personalizzati che non appartengono al dominio e permetterci quindi di far accedere all’applicazione anche utenti e/o macchine che non fanno direttamente parte della nostra struttura di Active Directory.

L’applicazione all’avvio esegue i seguenti passi:

  • recupera il SID dell’utente corrente di Windows, quello cioè della macchina locale su cui è in esecuzione:

    //L'Identity che rappresenta l'utente corrente di Windows
    WindowsIdentity wi = WindowsIdentity.GetCurrent();
    
    //Il SID dell'utente corrente di Windows
    SecurityIdentifier sid = wi.User;
    
  • verifica se il SID, così recuperato, sia presente nella tabella “Users” della nostra base dati:

    • in caso affermativo: Costruisce una GenericIdentity e un GenericPrincipal con le informazioni presenti nella tabella “Users”, assegnando al GenericPrincipal i ruoli corretti sulla base delle relazioni impostate nella tabella “RolesRelations”. Infine assegna il GenericPrincipal ottenuto alla proprietà statica CurrentPrincipal della classe Thread.

    • In caso negativo:Ci limitiamo a mostrare all’utente una bella maschera di richiesta di credenziali e procediamo allo stesso modo, verifichiamo cioè se le credenziali immesse dall’utente, che sappiamo non appartenere al dominio o non essere tra quelli abilitati al “Single Sign On”, siano corrette e in caso affermativo procediamo creando una GenericIdentity e un GenericPrincipal. La verifica viene fatta sempre a fronte dei dati contenuti nella tabella “Users” ma questa volta confrontando la combinazione di Username e Password immessi dall’utente. Nel caso specifico non viene verificata direttamente la password ma un hash della stessa al fine di evitare di persistere dati sensibili in chiaro.

Al termine del processo di autenticazione dell’utente sia che il sistema di “Single Sign On” sia entrato in funzione sia che l’utente sia stato autenticato con un processo custom otteniamo lo stesso effetto, abbiamo cioè un GenericPrincipal assegnato al thread corrente e questo ci garantisce che il motore che gestisce la sicurezza all’interno del .NET Framework funzioni come ci aspettiamo.

“System.DirectoryServices”

Arrivati questo punto è più che lecito chiedersi quali siano i passi da compiere perché sia possibile amministrare una struttura del genere, quali strumenti deve cioè avere chi amministra la sicurezza della nostra infrastruttura.

Molto semplicemente ci possiamo limitare a fornire uno strumento che permetta di sfogliare Active Directory, o gli utenti locali di Windows, e che permetta di estrarre le informazioni, che fondamentalmente sono il SID e il nome dell’account (necessario solo ed esclusivamente per una più rapida identificazione dell’utente durante le fasi di manutenzione) che ci serve persistere nella nostra base dati.

Per fare tutto questo il .NET Framework ci mette a disposizione un interno namespace: System.DirectoryServices.

Questo namespace contiene un set di classi che ci permettono di comunicare con un server LDAP o, attraverso il provider “WinNT”, con lo storage degli utenti di Windows.

Il set di classi fornito con la versione 1.x del .NET Framework permetteva di eseguire solo alcune operazioni, per andare oltre questo limite era necessario interagire direttamente con ADSI via P/Invoke; con l’introduzione del .NET Framework 2.0 le possibilità offerte dal set di classi contenuto in System.DirectoryServices sono state estese introducendo nuove funzionalità e semplificando quei compiti che prima richiedevano parecchie righe di codice per essere eseguiti.

Cominciamo quindi con il realizzare un DTO (Data Transport Object) che ci permetta di mappare un utente così come lo vede Active Directory su un oggetto con le caratteristiche che servono a noi, non facciamo altro che realizzare una semplice classe che faccia da “Adapter”.

Cominciamo quindi con il realizzare un DTO (Data Transport Object) che ci permetta di mappare un utente così come lo vede Active Directory su un oggetto con le caratteristiche che servono a noi, non facciamo altro che realizzare una semplice classe che faccia da “Adapter”.

public class ActiveDirectoryUser
{
public ActiveDirectoryUser( 
String commonName, String distinguishedName,
String sAMAccountName, Byte[] objectSID )
{
this._commonName = commonName;
this._distinguishedName = distinguishedName;
this._sAMAccountName = sAMAccountName;
this._objectSID = objectSID;
}

private String _commonName;
public String CommonName
{
/*
 * l’attributo commonName di ADSI
 * Nel mio dominio: CN=Mauro Servienti
 */
get { return this._commonName; }
}

private String _distinguishedName;
public String DistinguishedName
{
/*
 * l’attributo distinguishedName di ADSI
 * Nel mio dominio una cosa del tipo:
 * DC=local, DC=topics, DC=m,OU=Users,CN=Mauro Servienti
 */
get { return this._distinguishedName; }
}

private String _sAMAccountName;
public String SAMAccountName
{
/*
 * il nome account per l’accesso, nel mio
 * caso: TOPICS\Mauro
 */
get { return this._sAMAccountName; }
}

private Byte[] _objectSID;
public Byte[] ObjectSID
{
/*
 * il SID del mio utente
  */
get { return this._objectSID; }
}
}

Nella classe di esempio appena esposta ci siamo limitati ad inserire le proprietà, che per Active Directory sono attributi, che ci interessavano, nulla comunque ci vieta di estendere il tutto per recuperare altre informazioni, relative all’utente, che Active Directory ci mette a disposizione.

Realizziamo quindi un semplice Data Provider che data una query LDAP ci ritorni un insieme di oggetti a noi noti.

public sealed class ActiveDirectoryDataProvider : IDisposable
{
#region IDisposable Members
/*
 * Omissis
  */
#endregion

//Alcune costanti
const String CN = "cn";
const String SAMACCOUNTNAME = "samaccountname";
const String OBJECTSID = "objectsid";
const String OBJECTCLASS = "objectClass";

//Query base LDAP per filtrare gli utenti in un sistema SBS
const String DEFAULT_LDAP_QUERY = @”(&(objectCategory=user) ” + 
“ (!objectclass=contact)(cn=*)(!cn=aspnet*)” + 
“ (!cn=*User Template)(!cn=helpassistant_*)” + 
“(!cn=iusr_*)(!cn=iwam_*)(!cn=SystemMailbox{*)” + 
“(!cn=support_*)(!cn=krbtgt*)(!cn=SQLDebugger) ” + 
“(!cn=sbsmonacct)(!cn=Backup User)(!cn=SBSAgentUser) ” + 
“ (!cn=STS Worker)(!(&(memberOf=CN=Administrator” + 
“ Templates,OU=Security Groups,OU=MyBusiness,DC=*)” + 
“ (userAccountControl:1.2.840.113556.1.4.803:=2))))";

LdapDirectoryIdentifier directoryIdentifier = null;
LdapConnection connection;

public ActiveDirectoryDataProvider()
{
/*
 * Costruttore di default che inizializza un
 * DirectoryIdentifier “puntandolo” al dominio
 * a cui è connessa la macchina su cui è in
 * esecuzione il codice
 */
this.directoryIdentifier = new LdapDirectoryIdentifier( null );
}

public ActiveDirectoryDataProvider( String fullDnsDomainName )
{
/*
 * Costruttore parametrico che inizializza un
 * DirectoryIdentifier “puntandolo” al dominio
 * specificato nel parametro ‘fullDnsDomainName’.
 * E’ quindi possibile specificare un nome dns completo
 * per un dominio e lasciare rispondere il primo GC 
 * disponibile oppure è possibile specificare il nome
 * DNS completo di un server di AD e connettersi a quello
 */
this.directoryIdentifier = new LdapDirectoryIdentifier
( 
fullDnsDomainName 
);
}

Boolean _isBinded = false;

//Determina se ci siamo connessi o meno ad AD
public Boolean IsBinded
{
get { return this._isBinded; }
private set { this._isBinded = value; }
}

public void Bind()
{
this.Bind( null );
}

//Si connette ad Active Directory, utilizzando le credenziali specificate
public void Bind( NetworkCredential credentials )
{
if( this.IsBinded )
{
/*
 * non è possibile connettersi più di una volta è necessario
 * prima chiudere la connessione attiva
 */
throw new InvalidOperationException( 
"Ldap connection already binded, call” + 
“Close() method before calling Bind() once again" 
);
}

try
{
//Creaiamo la connessione LDAP
this.connection = new LdapConnection( directoryIdentifier );
if( credentials != null )
{
//Se ci sono credenziali le usiamo
this.connection.Bind( credentials );
}
else
{
this.connection.Bind();
}

//Impostiamo il flag
this.IsBinded = true;
}
catch( Exception ex )
{
//In caso di problemi esguiamo un po’ di pulizia
this.Close();

//Rispediamo all’esterno l’errore
throw ex;
}
}

public void Close()
{
if( this.connection != null )
{
this.connection.Dispose();
this.connection = null;
}

this.IsBinded = false;
}

Fin qui nulla di particolare, abbiamo una semplice classe “wrapper” a LdapDirecotryIdentifier e LdapConnnection, l’unica cosa degna di nota è la costante DEFAULT_LDAP_QUERY che ci da una chiara idea della sintassi LDAP per eseguire una query a fronte di un server Active Directory. Nello specifico quella query serve per estrarre tutti gli utenti definiti in una installazione di Active Directory su un server Microsoft Windows Server 2003 SBS.

Vediamo infine la parte più importante, come eseguire la query su Active Directory ed estrarre i dati che ci interessano.

public ActiveDirectoryUser[] FindUsers( 
String distinguishedName, 
String ldapQuery )
{
if( !this.IsBinded )
{
//Ci assicuriamo che la connessione sia aperta
throw new InvalidOperationException( 
"The underlying ldap connection is not binded" );
}

//Prepariamo la lista che conterrà il risultato
List<ActiveDirectoryUser> users = new List<ActiveDirectoryUser>();

/*
 * Prepariamo una istanza di SearchRequest:
 * SearchRequest è il contenitore della query LDAP
 * e di tutte le informazioni accessorie necessarie
 * perchè il server LDAP sia in grado di darci un risposta
 */
SearchRequest request = new SearchRequest();

/*
 * impostiamo il DistinguishedName, il DistinguishedName
 * dice al server LDAP quela sia il punto in cui iniziare
 * la ricerca, è quindi possibile passare un valore tipo:
 * DC=local, DC=topics, DC=m per far si che la ricerca inizi
 * alla root di Active Directory, oppure un valore del tipo:
 * DC=local, DC=topics, DC=m, OU=My Organizational Unit per 
 * specificare che vogliamo iniziare la ricerca dalla unità 
 * organizzativa specificata
 */
request.DistinguishedName = distinguishedName;

//la query ldap da usare
request.Filter = ldapQuery;

/*
 * aggiungiamo gli attributi che desideriamo ottenere 
 * da Active Directory, per impostazione predefinita non
 * viene caricato nessun dato, questo perchè l’operazione
 * in AD è onerosa. E’ quindi buona norma specificare il set
 * di attributi di cui si ha veramente bisogno evitando di
 * caricare informazioni inutili
 */ 
request.Attributes.Add( CN );
request.Attributes.Add( SAMACCOUNTNAME );
request.Attributes.Add( OBJECTSID );
request.Attributes.Add( OBJECTCLASS );

/*
 * lo _scope_ della ricerca, in questo caso diciamo ad AD
 * che vogliamo eseguire la ricerca sul nodo rappresentato dal
 * distinguishedName dato e su tutti i nodi figli, è possibile 
 * specificare che la ricerca debba essere eseguita sui soli 
 * nodi figli (SearchScope.OneLevel) o sul solo nodo specificato
 * (SearchScope.Base)
 */
request.Scope = SearchScope.Subtree;

/*
 * Inviamo la richiesta attraverso la connessione 
 * e aspettiamo una risposta
 */
SearchResponse response = ( SearchResponse )this.connection.SendRequest
( 
request 
);

foreach( SearchResultEntry entry in response.Entries )
{
/*
 * Scorriamo tutte le SearchResultEntry che troviamo 
 * nella risposta ottenuta da AD, siccome siamo interessati
 * ai soli utenti, cerchiamo di capire se quello che 
 * abbiamo in mano sia un utento o no, per fare questo analizziamo
 * l’attributo ‘objectClass’ che memorizza i suoi valori sotto forma
 * di Byte[], una nota importante è che gli attributi di AD sono 
 * multi valore, memorizzano cioè un array di valori, ad un 
 * attributo possono corrispondere quindi n valori.
 */
Boolean isUser = false;
foreach( Byte[] x in entry.Attributes[ OBJECTCLASS ] )
{
/*
 * Scorriamo i valori di objectClass li convertiamo in
 * stringa e se uno di essi coincide con ‘User’ siamo 
 * certi che l’oggetto AD che abbiamo in mano sia un
 * Account Utente
 */
String val = this.GetStringValue( x );
if( String.Compare( 
val, 
"user",
StringComparison.InvariantCultureIgnoreCase 
) == 0 )
{
isUser = true;
break;
}
}

if( isUser )
{
/*
 * certi che quello che abbiamo sia un utente, procediamo
 * con l’estrazione dei valori degli attributi che abbiamo
 * caricato, in questo caso ignoriamo il fatto che gli 
 * attributi siano multi valore e prendiamo il primo
 * 
 * Il ‘commonName’
 */
String cn = ( String )entry.Attributes
[ CN ][ 0 ];

//Il ‘samAccountName’
String samaccountname = ( String )entry.Attributes
[ SAMACCOUNTNAME ][ 0 ];

//l’objectSid, il nostro SecurityIdentifier
Byte[] objectsid = ( Byte[] )entry.Attributes
[ OBJECTSID ][ 0 ];
/*
 * Il ‘distinguishedName’ che è per AD il nome
 * completo dell’oggetto. Utilissimo per recuperare nuovamente
 * un riferimento allo stesso senza dover rieseguire la Query
 */
String entryDn = entry.DistinguishedName;

//Creiamo il nostro DTO
ActiveDirectoryUser user = new ActiveDirectoryUser( 
cn, 
entryDn, 
samaccountname, 
objectsid 
);

//Lo aggiungiamo alla lista
users.Add( user );
}
}

//Ritorniamo un array dei nostri DTO
return users.ToArray();
}

public ActiveDirectoryUser[] FindUsers( String distinguishedName )
{
/*
 * Un semplice overload che usa la query LDAP predefinita
 */
return this.FindUsers( distinguishedName, DEFAULT_LDAP_QUERY );
}

Per completezza riporto di seguito il codice di due metodi “helper” necessari per convertire il valore di un attributo, memorizzato come Byte[ ], in una stringa.

UTF8Encoding utf8 = null;
String GetStringValue( Byte[] attributeValue )
{
if( utf8 == null )
{
utf8 = new UTF8Encoding( false, true );
}

try
{
return utf8.GetString( attributeValue );
}
catch( ArgumentException )
{
return this.ByteArrayToHexString( attributeValue );
}
}

String ByteArrayToHexString( Byte[] bytes )
{
if( bytes == null )
{
return String.Empty;
}

StringBuilder sb = new StringBuilder( bytes.Length / 2 );
sb.Append( "0x" );
foreach( Byte b in bytes )
{
sb.Append( String.Format( "{0:X2}", b ) );
}

return sb.ToString();
}

Il risultato

Siamo, a questo punto, in grado di estrarre una lista di utenti da Active Directory, siamo in grado di salvare le informazioni che ci interessano nella nostra base dati e siamo riusciti ad attivare per uno specifico utente di Active Directory il sistema di “Single Sign On”, da questo momento l’utente non vedrà più la maschera per l’immisione delle credenziali ma accederà direttamente all’applicazione.

Ma quali altri vantaggi abbiamo ottenuto?

Siamo in grado con molta semplicità di realizzare un sistema di “Fast User Switching” all’interno della nostra applicazione che adesso supporta si l’autenticazione integrata di Windows sia un sistema di autenticazione personalizzato. Possiamo quindi in qualsiasi momento proporre una maschera all’utente che gli permette, inserendo delle nuove credenziali, di cercare di connettersi con una nuova identità, o nel caso lo sia già di cercare di tornare all’identità di default di Windows/Active Directory.

Volendo potremmo anche realizzare un complesso sistema che mimi lo “User Account Control” di Windows Vista, eseguendo dei blocchi di codice in AppDomain(s) separati a cui abbiamo, in fase di creazione, assegnato una identità diversa, e quindi ruoli diversi, da quella dell’AppDomain principale.

Conclusioni

Abbiamo visto cosa significhi implementare un sistema di “Single Sign On”, abbiamo evidenziato i vantaggi e gli svantaggi che un’implementazione semplice, ma comunque effiicace comporta, abbiamo visto che non è poi così difficile realizzare un sistema personalizzato per ottenere un risultato molto simile che annulla anche molti dei limiti che abbiamo sottolineato.

Abbiamo infine visto nel dettaglio quali siano i passi da seguire per dialogare con Active Directory al fine di estrarre le informazioni che ci interessano.

Il codice proposto da largo spazio a idee e a implementazioni decisamente più complesse di quella proposta che però possono adattarsi e risolvere tutte le possibili esigenze.

Risorse:

Strumenti

Un “tool” fondamentale per lo studio di Active Directory e per non perdersi nei meandri delle sue ramificazioni e AdsiEdit, uno snap-in per Microsoft Management Console, disponibile con tutte le versioni Server dei sistemi operativi Microsoft e installabile dalla directory SupportTools del CD di installazione. Lo strumento una volta installato, può anche essere installato su una workstation, e configurato permette di accedere ad Active Directory in lettura e scrittura, permette di studiarne lo schema e ove necessario di modificarlo. Le modifiche allo schema di Active Directory non sono reversibili e un’errata manipolazione dei valori può portare al blocco dell’intera infrastruttura di Active Directory stessa, lo strumento è quindi da utilizzare con moltissima cautela.


Mostra:
© 2015 Microsoft