Molto spesso, quando si sviluppa un’applicazione (soprattutto in ambito gestionale), è necessario gestire diversi livelli di accesso degli utenti, prevedendo la possibilità di “bloccare” alcune funzionalità a seconda dei privilegi che un utente ha o non ha. Il posto corretto per fare questa operazione sicuramente non è l’interfaccia utente, ma un qualche strato inferiore che gestisca la sicurezza, negando l’accesso alle operazioni non consentite. Ciò non toglie però che per rendere l’applicazione più elegante e user friendly è utile risolvere il problema anche a monte, evitando che l’utente possa accedere ad una funzionalità per la quale non ha accesso, rendendola inaccessibile dall’interfaccia utente. A questo scopo ho sviluppato una libreria per WPF che, sfruttando alcune caratteristiche della piattaforma (in particolare le Dependency Properties), permette di gestire i privilegi utente a livello di interfaccia senza la necessità di scrivere tonnellate di codice. Di seguito vado ad illustrare il funzionamento di questa libreria, che potete scaricare assieme ad un esempio nella pagina dei downloads.
Gestire la sicurezza attraverso le attached properties
Una delle tante caratteristiche di WPF che a mio parere rivoluzionano il modo di sviluppare le applicazioni rispetto a Windows Form, è quella delle attached properties, ovvero la possibilità di definire delle proprietà in una classe esterna che vanno ad arricchire le funzionalità degli elementi grafici, senza bisogno di estendere gli elementi stessi. In UIPrivilegesManager ho definito delle attached properties che estendono i controlli WPF per poter eseguire il check dei privilegi dell’utente e modificare lo stato o l’apparenza dei controlli in caso di fallimento del check. Attenzione, parlo di controlli non ha caso. In WPF, infatti, non tutti gli elementi che compongono l’interfaccia (che derivano dalla classe UIElement) sono accessibili all’utente, ma solo quelli che derivano dalla classe Control. Quindi è sensato che queste attached properties siano definite solo per gli oggetti di tipo Control.
Le quattro attached properties sono definite nella classe ControlBehaviors e sono:
- RequiredPrivileges
- CheckPrivilegesMode
- CheckPrivilegesBehavior
- CheckPrivilegesType
RequiredPrivileges
Si tratta della lista dei privilegi che l’utente deve possedere per poter interagire con quel controllo. Un privilegio è un’entità molto generica descritta semplicemente da un nome (Name) e può essere quindi una singola operazione, un gruppo, un ruolo, o altro, a seconda di come è definita la sicurezza nell’applicazione.
1: <Button ...>
2: <pm:ControlBehaviors.RequiredPrivileges>
3: <pm:PrivilegeCollection>
4: <pm:Privilege Name="..." />
5: ...
6: </pm:PrivilegeCollection>
7: </pm:ControlBehaviors.RequiredPrivileges>
8: </Button>
pm è il nome del namespace importato nello xaml in questo modo:
1: xmlns:pm="clr-namespace:UIPrivilegesManager;assembly=UIPrivilegesManager"
CheckPrivilegesMode
Descrive il modo in cui viene eseguito il check dei privilegi e può prendere due valori: “All” e “Any”. Nel primo caso significa che l’utente deve avere tutti i privilegi richiesti dal controllo, mentre nel secondo caso ne deve avere almeno uno. Il valore di default è “All”.
1: <Button pm:ControlBehaviors.CheckPrivilegesMode="All" ...>
2: ...
3: </Button>
CheckPrivilegesBehavior
Questa proprietà definisce la modalità con la quale il manager gestisce il controllo dei privilegi per quel controllo. Può avere uno dei tre valori:
- DisableControl: in caso l’utente non abbia i privilegi richiesti il controllo viene disabilitato (IsEnabled = false)
- HideControl: in caso l’utente non abbia i privilegi richiesti il controllo viene nascosto (Visibility = Collapsed)
- Custom: il manager lancia semplicemente il metodo CheckPrivileges della classe impostata con la proprietà CheckPrivilegesType
Il valore di default è “DisableControl”.
1: <Button pm:ControlBehaviors.CheckPrivilegesBehavior="DisableControl" ...>
2: ...
3: </Button>
CheckPrivilegesType
Deve essere settata quando la proprietà CheckPrivilegesBehavior è impostata su “Custom”. Accetta un oggetto che implementa l’interfaccia ICheckPrivileges che fornisce il metodo CheckPrivileges (un esempio di classe custom lo trovate nella demo):
1: public interface ICheckPrivileges
2: {
3: void CheckPrivileges(Control control, PrivilegeCollection userPrivileges, PrivilegeCollection requiredPrivileges);
4: }
Se la proprietà non è impostata il manager solleva un’eccezione di tipo PrivilegesManagerException.
1: ...
2: <local:CustomCheckPrivileges x:Key="CustomCheckPrivileges " />
3: ...
4: <Button pm:ControlBehaviors.CheckPrivilegesBehavior="Custom"
5: pm:ControlBehaviors.CheckPrivilegesType="{StaticResource CustomCheckPrivileges}">
6: ...
7: </Button>
Control.Loaded
In fase di registrazione dell’attached property RequiredPrivileges ho definito nei metadata la PropertyChangedCallback che OnRequiredPrivilegesChanged. In questa callback, vado ad abbonarmi all’evento Loaded del controllo, che è il punto di ingresso in cui viene legato il controllo al PrivilegesManager.
1: private static void control_Loaded(object sender, RoutedEventArgs e)
2: {
3: Control c = sender as Control;
4: switch (GetCheckPrivilegesBehavior(c))
5: {
6: case CheckPrivilegesBehavior.DisableControl:
7: {
8: PrivilegesManager.ControlsToDisable.Add(c);
9: c.InvalidateProperty(Control.IsEnabledProperty);
10: break;
11: }
12: case CheckPrivilegesBehavior.HideControl:
13: {
14: PrivilegesManager.ControlsToHide.Add(c);
15: c.InvalidateProperty(Control.VisibilityProperty);
16: break;
17: }
18: case CheckPrivilegesBehavior.Custom:
19: {
20: PrivilegesManager.RunCustomCheckMethod(c);
21: break;
22: }
23: }
24: }
Come si può vedere dal codice, nel metodo control_Loaded eseguo uno switch sulla proprietà CheckPrivilegesBehavior e delego alla classe PrivilegesManager il compito di eseguire il check dei privilegi.
DisableControl e HideControl
Vale la pena di mostrare il funzionamento di questi due behavior perché non è così banale come potrebbe sembrare. Partiamo dalla cosa ovvia: “lavorano” rispettivamente sulle proprietà IsEnabled e Visibility del controllo. Il check viene eseguito una prima volta all’evento loaded del controllo (grazie al metodo InvalidateProperty(proprietà), vedi metodo control_Loaded) ma successivamente occorre tenere traccia se durante l’esecuzione dell’applicazione queste proprietà vengono modificate, magari via codice o perchè associate ad un binding, e in caso rilanciare il check. Ma entrambe queste proprietà sono dependency properties e quindi ci viene in aiuto la classe DependencyPropertyDescriptor, più precisamente la possibilità di settare attraverso questa classe la proprietà DesignerCoerceValueCallback. Questa callback viene lanciata ogni volta che viene modificato il valore della dependency property e fornisce la possibilità di forzare il valore della proprietà al valore desiderato PRIMA che la proprietà venga effettivamente settata. In questa callback possiamo quindi eseguire il check dei privilegi ogni volta che la proprietà sta per essere modificata e quindi forzarne il valore (a false, per la proprietà IsEnabled e a Collapsed per la proprietà Visibility). Ad esempio, per la proprietà IsEnabled (per la proprietà Visibility è esattamente la stessa cosa) abbiamo:
1: ...
2: DependencyPropertyDescriptor isEnabledPD = DependencyPropertyDescriptor.FromProperty(Control.IsEnabledProperty, typeof(Control));
3: isEnabledPD.DesignerCoerceValueCallback = new CoerceValueCallback(CoerceIsEnabled);
4: ...
5: private static object CoerceIsEnabled(DependencyObject obj, object value)
6: {
7: Control c = obj as Control;
8: if (c != null)
9: {
10: if (ControlsToDisable.Contains(c) && (bool)value)
11: {
12: return UserHasRequiredPrivileges(ControlBehaviors.GetRequiredPrivileges(c), ControlBehaviors.GetCheckPrivilegesMode(c));
13: }
14: }
15: return value;
16: }
La callback DesignerCoerceValueCallback è generica per ogni controllo all’interno dell’applicazione quindi viene lanciata anche per i controlli per i quali non sono settati i privilegi richiesti. Per questo motivo ho memorizzato i controlli che hanno come behavior DisableControl o HideControl all’interno di due liste (ControlsToDisable e ControlsToHide) per poter eseguire i check solamente per i controlli corretti.
Le callback di coerce richiamano un metodo che esegue il check a seconda della modalità impostata sul controllo:
1: private static bool UserHasRequiredPrivileges(PrivilegeCollection requiredPrivileges, CheckPrivilegesMode mode)
2: {
3: switch (mode)
4: {
5: case CheckPrivilegesMode.All:
6: {
7: return _userPrivileges.Intersect(requiredPrivileges).Count() == requiredPrivileges.Count;
8: }
9: case CheckPrivilegesMode.Any:
10: {
11: return _userPrivileges.Intersect(requiredPrivileges).Count() > 0;
12: }
13: }
14: return false;
15: }
Configurazione di UIPrivilegesManager
Fino a questo momento ho parlato di un confronto tra i privilegi richiesti da un controllo e quelli in possesso dell’utente. Ma come fa il manager ha sapere quali sono i privilegi dell’utente? Nell’applicazione è possibile definire una classe che implementi l’interfaccia IGetUserPrivileges, che fornisce il metodo GetUserPrivileges, attraverso il quale caricare i privilegi degli utenti:
1: public interface IGetUserPrivileges
2: {
3: PrivilegeCollection GetUserPrivileges();
4: }
Questa classe deve essere specificata nel file di configurazione dell’applicazione, in una sezione ad hoc:
1: <configuration>
2: <configSections>
3: ...
4: <section name="PrivilegesManager" type="UIPrivilegesManager.PrivilegesManagerConfiguration, UIPrivilegesManager" allowLocation="true" allowDefinition="Everywhere" />
5: </configSections>
6: ...
7: <PrivilegesManager EnableCheckPrivileges="True" LoadUserPrivilegesType="PrivilegesManagerSample.LoadUserPrivileges, PrivilegesManagerSample" />
8: </configuration>
La sezione fornisce anche la proprietà EnableCheckPrivileges, di tipo booleano, che permette di disabilitare la sicurezza in modo semplice, utile in fase di sviluppo e di test (il valore di default è true).
Questa è una soluzione per configurare in modo semplice la libreria ma se per qualche ragione non è possibile utilizzare il file di configurazione (ad esempio se l’utente finale purtroppo ha i privilegi di modificare il file e quindi disabilitare il check, cosa che in linea teorica non dovrebbe poter fare!), la classe PrivilegesManager fornisce un metodo statico pubblico per poter inizializzare i privilegi utente, da chiamare allo startup dell’applicazione:
1: public partial class App : Application
2: {
3: protected override void OnStartup(StartupEventArgs e)
4: {
5: base.OnStartup(e);
6:
7: PrivilegeCollection privileges = ...
8: UIPrivilegesManager.PrivilegesManager.SetUserPrivileges(privileges);
9: }
10: }
Provalo!
Potete scaricare i sorgenti e/o i binari qui. Assieme troverete una demo che mostra come utilizzare la libreria e fornisce anche un esempio di implementazione di una classe custom per il check dei privilegi su controlli di tipo Button, a mio parere molto interessante perché permette di avere un comportamento simile a quello di UAC, ovvero di chiedere credenziali con privilegi più alti quando l’utente corrente non ha i privilegi richiesti dal bottone.
Currently rated 1.8 by 39 people
- Currently 1.846153/5 Stars.
- 1
- 2
- 3
- 4
- 5
WPF
wpf, attached properties, security, sicurezza, privileges, privilegi, dependency properties