Benutzerdefinierte Sicherheitsfilterung in Solr 5.x
Ein Kunde, der benutzerdefinierte Sicherheitsfilter in Solr 3.x implementiert hatte und dann auf 4.x umgestiegen ist, hat vor kurzem mit…
Ein Kunde, der benutzerdefinierte Sicherheitsfilter in Solr 3.x implementiert hatte und dann auf 4.x umgestiegen ist, hat vor kurzem mit uns zusammengearbeitet, um seinen Filtercode auf Solr 5.x zu portieren. Lucene wurde in 5.x überarbeitet (siehe LUCENE-5666 für die schmutzigen Details), so dass der nicht invertierte Zugriff, der zum Sortieren, Facettieren, Gruppieren usw. verwendet wird, die DocValues API anstelle von FieldCache verwendet. Nachfolgend finden Sie einen Auffrischungskurs darüber, wie Solr benutzerdefinierte Filter implementiert und was sich mit Solr 5.x geändert hat.(Hier ist unser früherer Beitrag über benutzerdefinierte Sicherheitsfilter für Solr 3.x und Solr 4.x)
Zusammenfassung der Filterung und des Caching von Solr
Lassen Sie uns zunächst die Filter- und Caching-Funktionen von Solr betrachten. Abfragen an Solr beinhalten eine Volltextabfrage mit Relevanzbewertung (der berüchtigte q-Parameter ). Während der Benutzer navigiert, durchsucht er die Facetten. Die Suchanwendung generiert Filterabfrageparameter(fq) für die facettierte Navigation (z. B. fq=color:red). Die Filterabfragen sind nicht an der Bewertung der Dokumente beteiligt, sondern dienen lediglich der Reduzierung des Suchraums. Solr verfügt über einen Filter-Cache, in dem die Dokumentensätze jeder einzelnen Filterabfrage zwischengespeichert werden. Diese Dokumentensätze werden im Voraus generiert, zwischengespeichert und reduzieren die von der Hauptabfrage berücksichtigten Dokumente. Wenn die Filter nicht zwischengespeichert werden, werden sie parallel zur Hauptabfrage verwendet, um die zu berücksichtigenden Dokumente zu „überspringen“. Jedem Filter können Kosten zugeordnet werden, um das Überspringen zu priorisieren (der kleinste Satz zuerst würde die Anzahl der für den Abgleich berücksichtigten Dokumente minimieren).
Post-Filterung
Auch ohne Caching werden Filtersätze standardmäßig im Voraus generiert. In einigen Fällen kann die Erstellung eines Filtersatzes extrem teuer und unerschwinglich sein. Ein Beispiel hierfür ist die Filterung der Zugriffskontrolle, bei der der Kontext der Benutzerabfrage berücksichtigt werden muss, um zu wissen, welche Dokumente zurückgegeben werden dürfen und welche nicht. Idealerweise sollten nur Dokumente, die mit der Abfrage und den einfachen Filtern übereinstimmen, für die Sicherheitszugriffskontrolle ausgewertet werden. Es wäre Verschwendung, andere Dokumente auszuwerten, die ohnehin nicht passen würden. Lassen Sie uns also ein Beispiel durchspielen… ein erfundenes Beispiel, um zu zeigen, wie die Post-Filterung von Solr funktioniert. Hier ist das Design:
Dokumente sind mit einer „Zugriffskontrollliste“ verknüpft, in der erlaubte und nicht erlaubte Benutzer sowie erlaubte und nicht erlaubte Gruppen angegeben sind. Die Zugriffskontrollliste ist eine geordnete Liste von erlaubten/verbotenen Benutzern und Gruppen. Die Reihenfolge ist wichtig, so dass die erste übereinstimmende Regel den Zugriff bestimmt. Wenn kein erlaubter Zugriff gefunden wird, ist das Dokument nicht erlaubt.
Ein Dokument könnte zum Beispiel die Zugriffskontrolle „+u:user1 +g:group1 -g:group2 +u:user2 -u:user3“ enthalten. Abfrageanfragen an Solr enthalten den Benutzernamen und die Gruppenzugehörigkeit des Benutzers. Anhand dieser Beispiel-Zugriffskontrollzeichenkette sehen Sie, wie dieses ausgeklügelte Design reagieren sollte:
user='user1', groups=null: allowed user='user2', groups=null: allowed user='user1', groups=[group1]: allowed user='user2', groups=[group2]: NOT ALLOWED user='user3', groups=[group1]: allowed user='user3', groups=[group2]: NOT ALLOWED user='user3', groups=[group1, group2]: allowed
Das heißt, wenn Benutzer2 als Mitglied von Gruppe2 sucht, sollte er dieses bestimmte Dokument nicht finden dürfen (-g:Gruppe2 steht in den Regeln vor +u:Benutzer2, und die Reihenfolge ist wichtig). Ich weiß, ich weiß, das ist ziemlich konstruiert, aber es spiegelt die Art von Komplexität wider, die in einigen sehr realen Umgebungen benötigt wird.
Da diese Regeln von der Reihenfolge und der Abfrage abhängig sind, ist es nicht möglich, eine einfache Lucene-Abfrage durchzuführen, um erlaubte Dokumente zu filtern. Folgen Sie mir bei diesem Beispiel. Ich habe versucht, es so kompliziert zu gestalten, dass ein benutzerdefinierter Filter erforderlich ist. Wenn Sie für Ihre Filterung die fq-Funktion von Solr verwenden können, sollten Sie diese stattdessen nutzen. Im Abschnitt „Kleingedrucktes“ weiter unten finden Sie eine Erinnerung und eine Erläuterung zu diesem Punkt.
Solr verfügt über eine PostFilter-Funktion, die diese letzte Prüfung beim Filtern von Dokumenten im laufenden Betrieb ermöglicht. Es erfordert einiges an Know-how, um einen PostFilter angemessen zu implementieren. Das Codebeispiel hier ist daher ein guter Ausgangspunkt für Ihre eigene benutzerdefinierte Post-Filterung. Die Nutzung eines PostFilters erfolgt über ein Solr QParserPlugin. Hier ist mein AccessControlQParserPlugin, das nur eine einfache Fabrik zur Erstellung einer AccessControlQuery ist:
public class AccessControlQParserPlugin extends QParserPlugin { public static String NAME = "acl"; @Override public void init(NamedList args) { } @Override public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) { return new QParser(qstr, localParams, params, req) { @Override public Query parse() throws SyntaxError { return new AccessControlQuery(localParams.get("user"), localParams.get("groups")); } }; } }
Hier ist AccessControlQuery:
public class AccessControlQuery extends ExtendedQueryBase implements PostFilter { private String user; private String[] groups; public AccessControlQuery(String user, String groups) { this.user = user; this.groups = groups.split(","); } /** * acl is in the form of a series of whitespace separated [+|-][u|g]:name * allowed is determined by any explicit user or group mentions, plus or minus * order matters * if nothing matches, it is not allowed */ public static boolean isAllowed(String acl, String user, String[] groups) { if (user == null && groups == null) return false; String[] permissions = acl.split(" "); for(String p : permissions) { boolean allowed = p.charAt(0) == '+'; String name = p.substring(3); if (p.charAt(1) == 'u') { // user if (user != null && user.equals(name)) return allowed; } else { // group if (groups != null) { for (String g : groups) { if (g.equals(name)) return allowed; } } } } return false; } @Override public boolean getCache() { return false; // never cache } @Override public int getCost() { return Math.max(super.getCost(), 100); // never return less than 100 since we only support post filtering } @Override public DelegatingCollector getFilterCollector(IndexSearcher searcher) { return new DelegatingCollector() { SortedDocValues acls; @Override protected void doSetNextReader(LeafReaderContext context) throws IOException { acls = context.reader().getSortedDocValues("acl"); super.doSetNextReader(context); } @Override public void collect(int doc) throws IOException { if (isAllowed(acls.get(doc).utf8ToString(), user, groups)) super.collect(doc); } }; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; AccessControlQuery that = (AccessControlQuery) o; if (!Arrays.equals(groups, that.groups)) return false; if (user != null ? !user.equals(that.user) : that.user != null) return false; return true; } @Override public int hashCode() { int result = super.hashCode(); result = 31 * result + (user != null ? user.hashCode() : 0); result = 31 * result + (groups != null ? Arrays.hashCode(groups) : 0); return result; } public static void main(String[] args) { String acl = "+u:user1 +g:group1 -g:group2 +u:user2 -u:user3"; System.out.println("acl = " + acl); test(acl, "user1", null); test(acl, "user2", null); test(acl, "user1", new String[] {"group1"}); test(acl, "user2", new String[] {"group2"}); test(acl, "user3", new String[] {"group1"}); test(acl, "user3", new String[] {"group2"}); test(acl, "user3", new String[] {"group1","group2"}); } private static void test(String acl, String user, String[] groups) { System.out.println("user='" + user + "'" + ", groups=" + (groups == null ? null : Arrays.asList(groups)) + ": " + (isAllowed(acl, user, groups) ? "allowed" : "NOT ALLOWED")); } }
Die Methode main() wurde verwendet, um die obigen Ergebnisse der Regelverarbeitung zu erzeugen. Ein paar Anmerkungen zu diesem Code sind wichtig:
- Diese Implementierung kann nur als Parameter für eine Filterabfrage (fq) verwendet werden, nicht als q-Parameter.
- hashCode/equals sind sehr wichtig, da es sonst zu unerwarteten/falschen Ergebnissen kommen kann.
- Die Zwischenspeicherung ist explizit deaktiviert, daher ist es nicht notwendig, cache=false zu setzen.
Solr verfügt über eine Logik, die PostFilter nur dann einsetzt, wenn die Kosten >= 100 sind, daher ist die Methode getCost so, wie sie ist. - Die benutzerdefinierte Filterlogik befindet sich in der einzigen isAllowed()-Methode.
- Dieses Beispiel wurde mit der Lucene/Solr 5.x Codebasis erstellt. Es ist mit keiner früheren Version von Solr kompatibel.
In dieser Implementierung werden die Zugriffskontrollregeln für jedes Dokument vollständig im Feld acl angegeben. Um zur Abfragezeit effizient nach diesen Regeln zu filtern, werden die DocValues von Lucene verwendet.
Nachdem wir die Implementierung hinter uns haben, können wir sie nun endlich verwenden: Indizieren Sie einige Dokumente und erstellen Sie Abfragen, die mit dem Abfrageparser „acl“ filtern. Hier sind die Dokumente, im CSV-Format:
acl_docs.csv
id,acl 1,+u:bob 2,-g:sales +g:engineering 3,+g:hr -g:engineering 4,-u:alice +g:hr 5,+g:hr -u:alice 6,+g:sales +g:engineering -u:bob 7,+g:hr -u:alice +g:sales 8,+g:sales 9,+g:engineering 10,+g:hr
Es wurde eine Sammlung „acl_example“ erstellt und zwei wichtige Konfigurationsänderungen vorgenommen: Registrierung des „acl“-Abfrageparsers und Anpassung der „acl“-Felddefinition. Um den „acl“-Abfrageparser zu registrieren, fügen Sie dies der Datei solrconfig.xml hinzu:
<queryParser name="acl" class="AccessControlQParserPlugin"/>
Das Feld „acl“ ist im Schema definiert als:
<field name="acl" type="string" indexed="true" stored="true" multiValued="false" docValues="true"/>
Beachten Sie docValues=“true“, eine wichtige Einstellung hier.
Die Dokumente wurden mit dem bin/post-Tool von Solr indiziert:
bin/post -c acl_example acl_docs.csv
Und schließlich sehen wir uns die Ergebnisse an, indem wir die Basisabfrage http://localhost:8983/solr/select?q=*:* verwenden, die an sich alle Dokumente zurückgibt. Durch Anhängen eines fq-Parameters mit der Syntax &fq={!acl user=’username‘ groups=’group1,group2′} wird der Sicherheitsfilter angewendet. Hier sehen Sie verschiedene Variationen von Benutzern und Gruppen und die Ergebnisse:
&fq={!acl user='alice' groups=''}: Matching ids: None &fq={!acl user='bob' groups=''}: Matching ids: 1 &fq={!acl user='alice' groups='hr'}: Matching ids: 3 5 7 10 &fq={!acl user='alice' groups='hr,sales'}: Matching ids: 3 5 6 7 8 10 &fq={!acl user='alice' groups='hr,sales,engineering'}: Matching ids: 3 5 6 7 8 9 10 &fq={!acl user='bob' groups='hr'}: Matching ids: 1 3 4 5 7 10
Kleingedrucktes
Es ist wichtig zu beachten, dass PostFilter ein letzter Ausweg für die Implementierung der Dokumentenfilterung ist. Machen Sie die Lösung nicht komplizierter, als sie sein muss. In den meisten Fällen kann sogar die Filterung der Zugriffskontrolle mit einfachen Suchtechniken implementiert werden, indem zugelassene Benutzer und Gruppen auf Dokumente indiziert werden und der Lucene (oder ein anderer) Query Parser verwendet wird, um die Aufgabe zu erfüllen. Nur wenn die Regeln zu kompliziert sind oder externe Informationen benötigt werden, ist ein benutzerdefinierter PostFilter sinnvoll. Die interne Methode #collect() wird für jedes übereinstimmende Dokument aufgerufen. In diesem Beispiel wurde eine *:*-Abfrage verwendet, die dazu führt, dass jedes Dokument im Index mit dem PostFilter ausgewertet werden muss. Was in #collect geschieht, muss stark optimiert werden.
Code
Hier ist der Code als Textdatei, was ihn sauberer und einfacher zu speichern macht als das Kopieren und Einfügen von oben: