2011-07-08 16 views
30

Podczas korzystania z implementacji listy ACL w Symfony2 w aplikacji sieciowej, natrafiliśmy na przypadek użycia, w którym sugerowany sposób korzystania z list ACL (sprawdzanie uprawnień użytkowników dla pojedynczego obiektu domeny) staje się niewykonalny. Zastanawiamy się więc, czy istnieje część API ACL, którą możemy wykorzystać do rozwiązania naszego problemu.Jak używać listy ACL do filtrowania listy obiektów-domen zgodnie z uprawnieniami określonego użytkownika (np. EDYCJA)?

Przypadek użycia znajduje się w kontrolerze, który przygotowuje listę obiektów domen, które mają być prezentowane w szablonie, aby użytkownik mógł wybrać, które z jej obiektów chce edytować. Użytkownik nie ma uprawnień do edycji wszystkich obiektów w bazie danych, więc lista musi zostać odpowiednio przefiltrowana.

Mogłoby (wśród innych rozwiązań) odbywać według dwóch strategii:

1) filtru zapytania, dołącza danego zapytania z prawidłowych identyfikatorów obiektów z ACL aktualny użytkownik jest na obiekt (lub obiekty). Tj:

WHERE <other conditions> AND u.id IN(<list of legal object ids here>) 

2) Filtr po kwerendy, która usuwa obiekty użytkownik nie posiada odpowiednich uprawnień do po pełna lista została pobrana z bazy danych. Np.:

$objs = <query for objects> 
$objIds = <getting all the permitted obj ids from the ACL> 
for ($obj in $objs) { 
    if (in_array($obj.id, $objIds) { $result[] = $obj; } 
} 
return $result; 

Pierwsza strategia jest lepsza, ponieważ baza danych wykonuje wszystkie prace filtrujące, a obie wymagają dwóch zapytań do bazy danych. Jeden dla list ACL i jeden dla faktycznego zapytania, ale prawdopodobnie jest to nieuniknione.

Czy jest jakaś implementacja jednej z tych strategii (lub czegoś, co przynosi pożądane rezultaty) w Symfony2?

Odpowiedz

18

Zakładając, że masz zbiór obiektów domeny, które chcesz sprawdzić, można korzystać z usługi za security.acl.providerfindAcls() sposób załadować wsadowym z góry zaproszeń isGranted().

Warunki:

Baza danych została wypełniona jednostek badawczych, z uprawnieniami obiektowych MaskBuilder::MASK_OWNER random użytkownika z mojej bazy danych i uprawnienia klasowych MASK_VIEW do roli IS_AUTHENTICATED_ANONYMOUSLY; MASK_CREATE dla ROLE_USER; i i MASK_DELETE dla .

Code Test:

$repo = $this->getDoctrine()->getRepository('Foo\Bundle\Entity\Bar'); 
$securityContext = $this->get('security.context'); 
$aclProvider = $this->get('security.acl.provider'); 

$barCollection = $repo->findAll(); 

$oids = array(); 
foreach ($barCollection as $bar) { 
    $oid = ObjectIdentity::fromDomainObject($bar); 
    $oids[] = $oid; 
} 

$aclProvider->findAcls($oids); // preload Acls from database 

foreach ($barCollection as $bar) { 
    if ($securityContext->isGranted('EDIT', $bar)) { 
     // permitted 
    } else { 
     // denied 
    } 
} 

WYNIKI:

Z wezwanie do $aclProvider->findAcls($oids);, profiler pokazuje, że mój wniosek zawierał 3 zapytań do bazy danych (jako gość).

Bez połączenia z numerem findAcls(), to samo żądanie zawierało 51 zapytań.

Należy pamiętać, że metoda ładuje się w partiach po 30 (z 2 zapytaniami na partię), więc liczba zapytań wzrośnie wraz z większymi zbiorami danych. Test ten przeprowadzono w ciągu około 15 minut pod koniec dnia pracy; kiedy będę miał szansę, przejdę i przejrzę dokładniejsze metody, aby sprawdzić, czy są jakieś inne przydatne zastosowania systemu ACL i zgłoś się tutaj.

+8

Ale nie rozumiem, jak chcesz obsługiwać większe zbiory danych z tym. Wyobraź sobie, że masz 10000 rekordów, nawet z paginacją w miejscu, musiałbym wiedzieć, ile rekordów jest własnością użytkownika. Czy istnieje sposób bez przechodzenia przez WSZYSTKIE encje w tabeli? – stoefln

+2

Całkowicie zgadzam się z @stoefln, takie rozwiązanie jest niepraktyczne w przypadku większych zbiorów danych. [Odpowiedź Diego] (http://stackoverflow.com/a/7452467/539560) wygląda lepiej, ale nie jestem tego pewien ... – Xuni

+0

+1 Po tym jak wpatrywałem się beznadziejnie w dokumentację, w końcu miałem trochę (nieco niepowiązane), ale bardzo pomocne objawienie ACL czytające tę odpowiedź. Dziękuję za to :) –

-3

Użyj sprzężeń, a jeśli używasz Doctrine, przygotuj dla nich połączenia, ponieważ prawie zawsze są one szybsze. Dlatego powinieneś zaprojektować swój schemat ACL, że wykonanie tych szybkich filtrów jest wykonalne.

+2

Używamy implementacji ACL w Symfony2, więc nie mamy wpływu na schemat ACL i jego złożoność. Biorąc pod uwagę strukturę schematu ACL Symonfy'2, nie byłoby możliwe dołączenie każdej kwerendy z tym typem sprzężenia (głównie ze względu na dwuznaczność tożsamości, hierarchię ACE i prezencję ACE). Będę edytować pytanie, aby wyraźniej stwierdzić, że używamy implementacji ACL Symfony2. –

9

Ominięcie jednostek nie jest wykonalne, jeśli masz kilka tysięcznych bytów - będzie wolniej i zużywa więcej pamięci, co zmusi cię do użycia funkcji dozowania doktryny, co sprawi, że twój kod będzie bardziej złożony (i inne, ponieważ przecież potrzebujesz tylko identyfikatorów do wykonania kwerendy - nie całego acl/encji w pamięci)

To, co zrobiliśmy, aby rozwiązać ten problem, to zastąpienie usługi acl.provider naszym własnym, a w tej usłudze dodać metodę, aby bezpośrednie zapytanie do bazy danych:

private function _getEntitiesIdsMatchingRoleMaskSql($className, array $roles, $requiredMask) 
{ 
    $rolesSql = array(); 
    foreach($roles as $role) { 
     $rolesSql[] = 's.identifier = ' . $this->connection->quote($role); 
    } 
    $rolesSql = '(' . implode(' OR ', $rolesSql) . ')'; 

    $sql = <<<SELECTCLAUSE 
     SELECT 
      oid.object_identifier 
     FROM 
      {$this->options['entry_table_name']} e 
     JOIN 
      {$this->options['oid_table_name']} oid ON (
      oid.class_id = e.class_id 
     ) 
     JOIN {$this->options['sid_table_name']} s ON (
      s.id = e.security_identity_id 
     )  
     JOIN {$this->options['class_table_nambe']} class ON (
      class.id = e.class_id 
     ) 
     WHERE 
      {$this->connection->getDatabasePlatform()->getIsNotNullExpression('e.object_identity_id')} AND 
      (e.mask & %d) AND 
      $rolesSql AND 
      class.class_type = %s 
     GROUP BY 
      oid.object_identifier  
SELECTCLAUSE; 

    return sprintf(
     $sql, 
     $requiredMask, 
     $this->connection->quote($role), 
     $this->connection->quote($className) 
    ); 

} 

Następnie wywołanie tej metody z rzeczywistego p Metoda ublic że dostaje podmiotów identyfikatory:

/** 
* Get the entities Ids for the className that match the given role & mask 
* 
* @param string $className 
* @param string $roles 
* @param integer $mask 
* @param bool $asString - Return a comma-delimited string with the ids instead of an array 
* 
* @return bool|array|string - True if its allowed to all entities, false if its not 
*   allowed, array or string depending on $asString parameter. 
*/ 
public function getAllowedEntitiesIds($className, array $roles, $mask, $asString = true) 
{ 

    // Check for class-level global permission (its a very similar query to the one 
    // posted above 
    // If there is a class-level grant permission, then do not query object-level 
    if ($this->_maskMatchesRoleForClass($className, $roles, $requiredMask)) { 
     return true; 
    }   

    // Query the database for ACE's matching the mask for the given roles 
    $sql = $this->_getEntitiesIdsMatchingRoleMaskSql($className, $roles, $mask); 
    $ids = $this->connection->executeQuery($sql)->fetchAll(\PDO::FETCH_COLUMN); 

    // No ACEs found 
    if (!count($ids)) { 
     return false; 
    } 

    if ($asString) { 
     return implode(',', $ids); 
    } 

    return $ids; 
} 

ten sposób możemy teraz użyć kodu, aby dodać filtry do DQL zapytania:

// Some action in a controller or form handler... 

// This service is our own aclProvider version with the methods mentioned above 
$aclProvider = $this->get('security.acl.provider'); 

$ids = $aclProvider->getAllowedEntitiesIds('SomeEntityClass', array('role1'), MaskBuilder::VIEW, true); 

if (is_string($ids)) { 
    $queryBuilder->andWhere("entity.id IN ($ids)"); 
} 
// No ACL found: deny all 
elseif ($ids===false) { 
    $queryBuilder->andWhere("entity.id = 0") 
} 
elseif ($ids===true) { 
    // Global-class permission: allow all 
} 

// Run query...etc 

wady: Te metody mają ulec poprawie, aby wziąć pod uwzględnij złożoność dziedziczenia i strategii ACL, ale w prostych przypadkach działa dobrze. Również pamięć podręczna musi zostać zaimplementowana, aby uniknąć powtarzania podwójnej kwerendy (jedna z poziomem klasy, druga z poziomem objetc).

0

Łączenie Symfony ACL z powrotem do aplikacji i używanie jej jako sortowania, nie jest dobrym podejściem. Mieszamy i łączymy 2 lub 3 warstwy aplikacji razem. Funkcja ACL polega na odpowiadaniu "TAK/NIE" na pytanie "Czy wolno mi to zrobić?" Jeśli potrzebujesz jakichś własnych/edytowalnych artykułów, możesz użyć niektórych kolumn, takich jak CreatedBy lub group CreatedBy, według kryteriów z innej tabeli. Niektóre grupy użytkowników lub konta.

Powiązane problemy