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:

You Might Also Like

Vom Suchunternehmen zum praktischen KI-Pionier: Unsere Vision für 2025 und darüber hinaus

CEO Mike Sinoway gibt Einblicke in die Zukunft der KI und stellt...

Read More

Wenn KI schief geht: Fehlschläge in der realen Welt und wie man sie vermeidet

Lassen Sie nicht zu, dass Ihr KI-Chatbot einen 50.000 Dollar teuren Tahoe...

Read More

Lucidworks Kernpakete: Branchenoptimierte KI-Such- und Personalisierungslösungen

Entdecken Sie unsere umfassenden Core Packages, die Analytics Studio, Commerce Studio und...

Read More

Quick Links