Zbyt wiele razy spotkałem się z takim kodem:
1: private void Form1_Load(object sender, EventArgs e) 2: { 3: btnAddUser.Visible = Thread.CurrentPrincipal.IsInRole("can add user"); 4: btnShowPosts.Visible = Thread.CurrentPrincipal.IsInRole("can show posts"); 5: //... 6: //... 7: //... 8: //... 9: 10: }
Naprawdę, pisanie takiego kodu w czasach, gdy programowanie obiektowe nie jest “nowością” (czyli od dobrych kilkunastu lat) to po prostu grzech. Tym bardziej, że w bardziej rozbudowanych systemach powyższe instrukcje mogą składać się z SETEK podobnych linii. Kto zgadnie jak bardzo podatna na błędy programisty jest owa metoda?
Na szczęście można temu zaradzić, pewnie na milion różnych sposobów. Moja propozycja (dla WinForms, ale przy odrobinie chęci można to “zgeneralizować”) jest taka:
1) tworzymy atrybut, który, nałożony na klasę, daje możliwość zadeklarowania nazwy operacji oraz uprawnień wymaganych do jej wykonania:
1: [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] 2: public sealed class UserOperationAttribute : Attribute 3: { 4: private readonly string[] _requiredPrivileges; 5: public readonly string DisplayName; 6: 7: public UserOperationAttribute(string displayName, params string[] requiredPrivileges) 8: { 9: _requiredPrivileges = requiredPrivileges; 10: DisplayName = displayName; 11: } 12: 13: public bool IsSatisfied 14: { 15: get 16: { 17: // current user has all required privileges 18: return _requiredPrivileges.All(Thread.CurrentPrincipal.IsInRole); 19: } 20: } 21: }
2) Specjalna klasa wyszukuje w załadowanych assemblies te typy, które mają ów atrybut zaaplikowany a następnie sprawdza, czy warunek wymagany do udostępnienia operacji użytkownikowi jest spełniony; efektem działania jest słownik zawierający typy będące operacjami oraz instancje przypisanych im atrybutów:
1: public class UserOperationDiscovery 2: { 3: public static Dictionary<Type, UserOperationAttribute> Perform() 4: { 5: var actions = from type in AppDomain.CurrentDomain.GetAssemblies().SelectMany(x => x.GetTypes()) 6: let actionAttribute = type.GetCustomAttributes(typeof(UserOperationAttribute), false) 7: .Cast<UserOperationAttribute>().SingleOrDefault() 8: where actionAttribute != null && actionAttribute.IsSatisfied 9: select new { type, attr = actionAttribute }; 10: 11: return actions.ToDictionary(x => x.type, x => x.attr); 12: } 13: }
3) Z założenia atrybut ten aplikowałem do własnych UserControls. W formie miałem dwa panele: jeden przeznaczony do wyświetlenia aktualnej operacji i jeden służący do prezentowania wszystkich operacji dostępnych dla użytkownika. Operacje uruchamiane były przyciskami (każdej wykrytej operacja tworzyłem przycisk), wciśnięcie przycisku powodowało utworzenie kontrolki i osadzenie jej w odpowiednim placeholderze:
1: private void LoadOperations() 2: { 3: var operations = UserOperationDiscovery.Perform(); 4: operations.ToList().ForEach( 5: o => 6: { 7: // new button for each operation 8: Button btn = new Button 9: { 10: Text = o.Value.DisplayName, 11: Dock = DockStyle.Fill, 12: Tag = o.Key 13: }; 14: 15: btn.Click += (s, e) => 16: { 17: // dispose of currently displayed operation 18: if (Placeholder.Controls.Count > 0) 19: Placeholder.Controls[0].Dispose(); 20: Placeholder.Controls.Clear(); 21: 22: // create new instance of operation on each click 23: var newControl = (Control)Activator.CreateInstance((Type)btn.Tag); 24: newControl.Dock = DockStyle.Fill; 25: Placeholder.Controls.Add(newControl); 26: }; 27: 28: this.buttonsPanel.Controls.Add(btn); 29: }); 30: }
Mechanizm ten jest bardzo prosty, dzięki czemu łatwo można go dostosować do własnych potrzeb (jak np. logiczne grupowanie operacji w dedykowanych miejscach interfejsu użytkownika). Dodanie nowego ekranu sprowadza się do utworzenia kontrolki i zaaplikowaniu jej atrybutu – reszta dzieje się sama. Zaprezentowany na początku posta syf idzie do kosza naprawdę małym nakładem pracy.
Witam, czy będzie można pobrać wkrótce źródła (przykładowy programik)?
@Year:
Wlasciwie to cale zrodla sa zawarte w poscie:). Ale wkrotce zamierzam napisac cos wiekszego: jak polaczyc to z uwierzytelnianiem i autoryzacja w WCF. Tam zrodla beda juz przygotowane w postaci paczki do sciagniecia.
Z tym ze… nie potrafie powiedziec KIEDY. Moze byc w przyszlym tygodniu a moze byc za poltora miesiaca.
Nie podoba mi się ten wpis. Przede wszystkim brak w nim przykładu zastosowania. A jeżeli to rozwiązanie działa tak jak się domyślam, to ma ograniczone możliwości i wprowadza niepotrzebne skomplikowanie.
@Komodo:
dzięki za feedback, skoro już druga osoba o tym pisze to postaram się "pingnąć" wspomniany temat z przykładem z WCF na mojej liście priorytetów:)
Chyba właśnie o coś takiego chodziło Gutkowi tutaj: http://blog.gutek.pl/post/2009/11/16/Oceniane-postow.aspx
Witam,
mi sie tez cos tu nie podoba (podobne odczucia co Komodo), w dodatku uzycie AppDomain.CurrentDomain.GetAssemblies(), nie wiem czemu ale mam mieszane uczucia co do tej funkcji (czy w koncu nie doczeka sie ona, wraz z System.Reflection, wymagan na podwyzszone uprawnienia uzytkowniaka w systemie bo to w koncu wrecz dlubanie w kodzie, tak mi sie wydaje – ale to slaby argument).
Ale temat bardzo ciekawy, sam spedzilem sporo czasu myslac nad rozwiazaniem i wyprodukowalem cos pomiedzy visible = blaa a GetAssemblies. Menadzer, do ktorego dopisuje sie komponenty wraz z uprawnieniami na starcie. Ale tez mi sie to nie podoba zatem czekam na szersze rozwiniecie tematu !
@reichel:
Akurat GetAssemblies() jest najprostszym możliwym działającym przykladem, rownie dobrze mozna zastapic to dowolnym kontenerem IoC, MEFem, System.AddIn czy wlasnym mechanizmem pluginow. Glownym ‘clue’ tego rowiazania jest przeniesienie odpowiedzialnosci ‘pokaz/nie pokazuj’ na poziom atrybutu.
Jeszcze jedno, zapomnialem tego dopisac – jak nie obudowujac dodac atrybut ? Np. mam button1 i nie chce kontenerow. Teoretycznie mozna via run time nadac atrybuty ale wtedy idea jest niemal identyczna jak w przypadku menadzera (no OK, mozna zakladac ze jest to bardziej czyste ale to kwestia wyboru). Jednak istnieje prawdopodobienstwo, ze czegos nie zrozumialem i mozna ?
Swoja droga, ale to pewnie oczywiste gdyz sam temat scisle tyczy sie GUI i nie bylo to tematem, nie nalezy zapomniec ze uprawnienia powinny byc sprawdzone i tak pozniej na poziomie wykonanego kodu (to tak a’propos visible).
@reichel:
W tworzonym systemie nie było potrzeby udostępniania żadnych gołych (tzn: bez reprezentacji "kontrolkowej") operacji, więc rozwiązanie tego nie przewiduje. Jeśli miałbym rozszerzac je w tym kierunku to pewnie stworzyłbym kolejny atrybut nadawany klasom implementującym np interfejs ICommand i proces budowania UI jedynie tworzyłby dla nich przyciski, pod Click podpinając metodę ICommand.Execute() – bez żadnych interakcji z resztą UI.
Dodatkowe zabezpieczenie faktycznie wywoływanej logiki, choćby przez PrincipalPermissionAttribute, to raczej "oczywista oczywistość". Ale masz racje, moglem o tym wspomniec:).