Field level security

1

Potrzebuję zaprojektować fragment systemu, gdzie użytkownikowi mogą być przypisywane uprawnienia na poziomie każdego pola modelu (brak uprawnień, uprawnienie do odczytu, uprawnienie do edycji).

Obiekt, do którego uprawnienia będą przypisywane posiada podobiekty. Uprawnienie może być przypisane do całego obiektu/podobiektu albo do jego konkretnych pól.
Przykładowo:

public class Case
{
   public string Id { get; set; }
   public string Status { get; set; }

   public string Title { get; set; }
   public string Description { get; set; }
   public string[] Tags { get; set; }

   public Attachment[] Attachments { get; set; }
}

public class Attachment
{
   public string Id { get; set; }

   public string Name { get; set; } 
   public string ContentUrl { get; set; }
}

I tak się zastanawiam jak to sensownie przechowywać w bazie żeby było jako tako elastyczne. Trochę boję się też sytuacji, gdzie refaktor modelu - chociażby prosta zmiana nazwy pola - będzie wymagał update'u wszystkich do tej pory przypisanych uprawnień - bo nagle się okaże że nie mam już pola Title, tylko Name, ale nie mam pomysłu jak mógłbym tego uniknąć.

Samo przechowywanie uprawnień mogłoby wyglądać jakoś tak:

public class Permission
{
   public PermissionType PermissionType { get; set; } // read, edit itd.
   public string Field { get; set; }
}

Gdzie jako Field przechowywałbym wyrażenie JSONPath, a użytkownik do danej sprawy miałby przypisany zbiór takich uprawnień.

Co o tym sądzicie? Jak byście to zrobili?

//Edit:
Chociaż jak teraz myślę, to z JSONPath byłaby jazda po stronie backendu przy określaniu czy użytkownik może edytować/widzieć dane pole. Bo przy uprawnieniu mógłbym sobie trzymać path w postaci $.attachments[?(@.id=="111-222-333")].name, ale w drugą stronę to już nie takie proste, żeby określić czy użytkownik może edytować pole name w danym załączniku :D

5

Ko? Przecież z biznesowego punktu widzenia zawsze dajesz uprawnienia do jakieś akcji użytkownikowi / grupie czy innemu bytowi który sobie utworzysz. Pierwszy raz widzę by ktoś próbował zrobić uprawnienia na właściwości klasy...

To jak dana właściwość jest ustawiana to kwestia logiki biznesowej, a nie uprawnień systemu. Np mówisz "User może edytować tytuły dokumentów" albo "User może edytować tytuły dokumentów które sam wgrał". Zawsze wychodzisz od potrzeby biznesowej.

5

Przede wszystkim to rozdzielił bym model przechowywany/biznesowy od modelu widoku. Model widoku bym spłaszczył, a wtedy powinno być łatwiejsze sprawdzanie czy konkretne pole może być przez użytkownika aktualizowane. W drugą stronę to powinno być jeszcze prostsze przy zastosowaniu takiego oddzielnego modelu.

Dobrze sprawdziłeś wymagania biznesowe i jesteś pewny że nie da się tego trochę ułatwić, np. kontrolować dostęp do konkretnych grup pól, zamiast poszczególnych pól? Wtedy było by znacznie łatwiej.

0
YourFrog2 napisał(a):

To jak dana właściwość jest ustawiana to kwestia logiki biznesowej, a nie uprawnień systemu. [...] Zawsze wychodzisz od potrzeby biznesowej.

Potrzeba biznesowa jest taka, że mam formatkę z 20 polami i przy dodawaniu nowego użytkownika do sprawy mogę na poziomie pojedynczych pól decydować czy użytkownik widzi to pole i czy może je edytować.

YourFrog2 napisał(a):

Np mówisz "User może edytować tytuły dokumentów" albo "User może edytować tytuły dokumentów które sam wgrał". Zawsze wychodzisz od potrzeby biznesowej.

Ok, to zdecydowanie wydaje się sensowniejsze.
Ale nie bez powodu wymyśliłem sobie uprawnienie na poziomie pola - możliwe że formularz będzie konfigurowalny przez administratora per typ sprawy, tzn. będzie mógł on dodawać nowe pola. Do tych pól później również będą przypisywane uprawnienia.

Aventus napisał(a):

Dobrze sprawdziłeś wymagania biznesowe i jesteś pewny że nie da się tego trochę ułatwić, np. kontrolować dostęp do konkretnych grup pól, zamiast poszczególnych pól? Wtedy było by znacznie łatwiej.

Raczej nie, biorąc pod uwagę to co napisałem wyżej - czyli możliwość dodawania pól przez admina.

3

Po prostu masz formularz z wieloma polami w którym nie każde pole jest "edytowalne" dla osoby która się zalogowała do systemu. Jak dla mnie to klasyczny ACL gdzie osoba dodająca dynamiczne pole do formularza zmuszona jest do podania grup osób które mogą zarządzać ów polem, a to jak sobie to w kodzie przechowujesz NIE MA nic do rzeczy. Bo ja pewnie bym pchał json'a na bazę razem z numerem wersji według którego należy formularz odtwarzać / upgradować by pasował do nowych wersji oprogramowania.

Prosty przykład działania takiej aplikacji. Tworzę "ankietę" i decyduje czy osoby zalogowane / anonimowe mogą dodawać nowe opcje oraz że osoby które dodały nową opcje mogą ją usunąć jeśli są jej właścicielem.

https://pl.wikipedia.org/wiki/Access-control_list Ewentualnie coś podobnego jak RBAC czy inne podobne których nikt normalny nie pamięta czym się od siebie różnią

0
YourFrog2 napisał(a):

Po prostu masz formularz z wieloma polami w którym nie każde pole jest "edytowalne" dla osoby która się zalogowała do systemu.

No tak, ale w przypadku dynamicznego pola nie mogę sobie z góry przygotować biznesowych uprawnień tak jak sugerowałeś wcześniej - "Użytkownik może edytować tytuł", "Użytkownik może edytować tagi". Więc jak to przechowuję rzeczywiście nie ma znaczenia, ale w przypadku dynamicznych pól i tak muszę jakoś powiązać uprawnienie z dodanym polem - pewnie za pomocą jakiegoś identyfikatora pola.

YourFrog2 napisał(a):

Jak dla mnie to klasyczny ACL gdzie osoba dodająca dynamiczne pole do formularza zmuszona jest do podania grup osób które mogą zarządzać ów polem

Prawie, bo pola konfiguruje administrator, a uprawnienia dla nowych osób - osoba do której trafiła sprawa (to jest określane na podstawie reguł biznesowych). Ale to rzecz wtórna :P

3

Definiujesz podstawowe uprawnienia które dziedziczy z automatu każde nowe pole podczas dodawania, a administrator / inna osoba uprawniona ma możliwość ich modyfikacji. Zalogowana osoba zawsze należy do jakieś grupy / grup które mają swoje własne uprawnienia do danej akcji. NIE MA znaczenia co to za akcja. Zawsze sprawdzasz sobie czy ktoś ma uprawnienia do zapisu danego pola i tyle. A to że jest inne miejsce w panelu do definiowania uprawnień to who care? Toć to zwykłe GUI na matryce.

Wyobraź sobie taka sytuacje że masz typy pól "Numeryczne, Tekstowe, Pole wyboru, Data z przeszłości" i ludzie z tych pól budują w twoim systemie formularz gdzie dla każdego pola mają różne konfiguracje. Co dla nich zawsze będzie wspólne? Jakieś pole "unique_name" które jednoznacznie rozpozna pole na liście obiektów. I teraz w okół tego budujesz ACL. Podajesz jako zasób nazwę formularza (który też jest unikalny w ramach wszystkich formularzy w systemie) oraz nazwę pola i do niego przypisujesz grupy użytkowników.

Tak wygląda prawie zawsze scieżka sukcesu dla aktualizacji formularza

Weź wszystkie pola z formularza
 - Dla każdego pola sprawdź czy podczas zapisu / aktualizacji nadeszły dane z formularza
 - Jeżeli dane nadeszły to sprawdź uprawnienia dla tego pola po unique_name
 - Zapisz w formularzu dane nadesłane przez użytkownika

Weź formularz zapisz go w systemie.

Musisz zmienić po prostu sposób przechowywania formularza z ładnego modelu biznesowego na byt Map<String, FormObject> gdzie FormObject jest konfigurowalnym elementem z tych wybranych, a String to UniqueName FormObject'a. Inna sprawa że możesz mieć jakiś mapper pomiędzy formularzem, a modelem biznesowym no ale albo dynamicznie albo nie. Zauważ że NIE MA znaczenia czy budujesz formularz za pomocą kodu czy też odtwarzasz na podstawie konfiguracji z bazy danych.

0
YourFrog2 napisał(a):

Jakieś pole "unique_name" które jednoznacznie rozpozna pole na liście obiektów. I teraz w okół tego budujesz ACL.

O tym samym myślałem.

Czyli podsumowując - dla pól, które są stałe dla każdego typu sprawy buduje sobie stałą listę biznesowych uprawnień, a dla pól dynamicznych uprawnienia określam na podstawie niezmienialnego identyfikatora pola.

0
Aventus napisał(a):

Dobrze sprawdziłeś wymagania biznesowe i jesteś pewny że nie da się tego trochę ułatwić, np. kontrolować dostęp do konkretnych grup pól, zamiast poszczególnych pól? Wtedy było by znacznie łatwiej.

Dokładnie, poziom grup to jest to.
Np koronne przykłady w programach biznesowych:

  • widzenie kwot/wypłat w tematyce kadrowo-płacowej (zarówno umownych, jak i realnie uzyskanych)
  • widzenie kosztów zakupu w handlu
some_ONE napisał(a):

Raczej nie, biorąc pod uwagę to co napisałem wyżej - czyli możliwość dodawania pól przez admina.

To niech dodaje i do grupy (tak, czuję w czym będziesz protestował: jak się dodaje na poziomie "fizycznym", np kolumny w bazie, to ma się problem - leczące jest dodawanie nowych pól oparte na wyższym poziomie logicznym, jakieś repozytorium)

0

zastanawiałem się trochę nad tym i w sumie nie wiem jaki poziom dynamiczności i zautomatyzowania tego chciałeś osiągnąć, więc wyjdźmy od początku

jak unikatowo identyfikować property nawet gdy zajdzie refactor? mój pomysł jest taki:

public class Case
{
    [Identity("6aa0847e-e7ef-4db7-8c71-8c37b32d2c29")]
    public string Id { get; set; }

    [Identity("2a810d01-6e74-435d-8b50-314108f83532")]
    public string Name { get; set; }

    public Attachment[] Attachments { get; set; }
}

public class Attachment
{
    [Identity("1aa0847e-e7ef-4db7-9c71-8c37b32d2c29")]
    public string Id { get; set; }
}
public static void Main()
{
    var user_permission = new Dictionary<string, int>
    {
        {"6aa0847e-e7ef-4db7-8c71-8c37b32d2c29", 1 }, // can read
        {"1aa0847e-e7ef-4db7-9c71-8c37b32d2c29", 1 }, // can read
        {"2a810d01-6e74-435d-8b50-314108f83532", 0 }, // cannot read
    };

    var @case = new Case
    {
        Id = "Id1",
        Name = "Case1",
        Attachments = new Attachment[]
        {
            new Attachment
            {
                Id = "Att_Id"
            }
        }
    };

    var result = Extract(user_permission, @case);

    foreach (var entry in result)
    {
        Console.WriteLine($"{entry.Key} - {entry.Value}");
    }
}

private static Dictionary<string, object> Extract(Dictionary<string, int> user_permission, Case @case)
{
    var result = InternalExtract<Case>(user_permission, @case);

    foreach (var attr in @case.Attachments)
    {
        var extracted = InternalExtract<Attachment>(user_permission, attr).ToList();

        extracted.ForEach(x => result.Add(x.Key, x.Value));
    }

    return result;
}

private static Dictionary<string, object> InternalExtract<T>(Dictionary<string, int> user_permission, T obj)
{
    var props = obj
        .GetType()
        .GetProperties()
        .Select(x => new { Property = x, Attributes = x.GetCustomAttributes(false).OfType<IdentityAttribute>().ToList() })
        .Where(x => x.Attributes.Any())
        .ToList();

    var result = new Dictionary<string, object>();

    foreach (var prop in props)
    {
        var key = prop.Attributes[0].Value;
        if (user_permission.TryGetValue(key, out var access_level) && access_level == 1)
        {
            result.Add(key, prop.Property.GetValue(obj));
        }
    }

    return result;
}

Result:

6aa0847e-e7ef-4db7-8c71-8c37b32d2c29 - Id1
1aa0847e-e7ef-4db7-9c71-8c37b32d2c29 - Att_Id

Jak widać nie ma Name

Ale znając życie poszedłbym w ifki bo innym łatwiej byłoby się odnaleźć

1 użytkowników online, w tym zalogowanych: 0, gości: 1