Wie führe ich komplexe API-Autorisierungen in weniger SQL-Abfragen durch?

9

Ich versuche, einer API eine Autorisierungsschicht hinzuzufügen, und das aktuelle Design, das ich habe, führt zu mehr SQL-Abfragen, als es sich anfühlen sollte, also frage ich mich, wie ich das vereinfachen kann.

Kontext

Hier ist das Datenbankschema für diesen Teil des Problems:

%Vor%

Und der fragliche API-Endpunkt ist GET /users/:user/teams , der alle Teams zurückgibt, zu denen ein Benutzer gehört. So sieht der Controller für diese Route aus:

(Hinweis: All dies ist Javascript, aber es wurde aus Gründen der Übersichtlichkeit Pseudocode genannt.)

%Vor%

Diese vier asynchronen Funktionen sind die wichtigsten logischen Schritte, die ausgeführt werden müssen, damit die Autorisierung "vollständig" ist. So sieht jede dieser Funktionen ungefähr aus:

%Vor%

exists überprüft einfach, ob ein Benutzer mit dem userId sogar in der Datenbank existiert und gibt den richtigen Fehlercode aus, falls nicht.

query ist nur Pseudocode zum Ausführen einer SQL-Abfrage mit Escape-Variablen.

%Vor%

canFindTeams stellt sicher, dass entweder der aktuelle Benutzer derjenige ist, der die Anfrage stellt, oder dass der aktuelle Benutzer ein Teammitglied des betreffenden Benutzers ist. Jeder andere sollte nicht berechtigt sein, den betreffenden Benutzer zu finden. In meiner tatsächlichen Implementierung ist es tatsächlich mit roles gemacht worden, die actions zugeordnet haben, so dass ein Teammitglied teams.read kann, aber nicht teams.admin , es sei denn, sie sind ein Eigner. Aber ich habe das für dieses Beispiel vereinfacht.

%Vor%

findTeams fragt tatsächlich die Datenbank nach den Team-Objekten ab.

%Vor%

maskTeams gibt nur die Teams zurück, die ein bestimmter Benutzer sehen sollte. Dies ist erforderlich, da ein Benutzer alle seine Teams sehen kann, aber Teammitglieder sollten nur ihre Teams gemeinsam sehen können, um keine Informationen zu verlieren.

Probleme

Eine der Anforderungen, die dazu geführt haben, dass ich es so aufbringe, ist, dass ich einen Weg finde, diese spezifischen Fehlercodes zu werfen, so dass die Fehler, die an API-Clients zurückgegeben werden, hilfreich sind. Zum Beispiel läuft die Funktion exists vor der Funktion canFindTeams , so dass nicht alle Fehler mit 403 Unauthorized .

auftreten

Ein anderer, der hier im Pseudocode nicht gut kommuniziert wird, ist, dass currentUser tatsächlich ein app (ein Drittanbieter-Client), ein team (ein Zugriffstoken, das sich auf das Team selbst bezieht) oder sein kann a user (der allgemeine Fall). Diese Anforderung macht es schwierig, die Funktion canFindTeams oder maskTeams als einzelne SQL-Anweisungen zu implementieren, da die Logik auf drei Arten verzweigt werden muss ... In meiner Implementierung sind beide Funktionen tatsächlich switch-Anweisungen um die Logik für die Behandlung aller drei Fälle - der Anforderer ist ein app , ein team und ein user .

Aber selbst unter diesen Einschränkungen fühlt sich das wie viel zusätzlicher Code zum Schreiben an, um all diese Authentifizierungsanforderungen zu gewährleisten. Ich bin besorgt über die Leistung, die Wartbarkeit des Codes und auch über die Tatsache, dass diese Abfragen nicht alle in einzelnen Transaktionen sind.

Fragen

  • Beeinflussen die zusätzlichen Abfragen die Leistung?
  • Können sie leicht zu weniger Abfragen kombiniert werden?
  • Gibt es ein besseres Design für die Autorisierung, das dies vereinfacht?
  • Stellt die Verwendung von Transaktionen keine Probleme dar?
  • Hast du noch etwas geändert?

Danke!

    
Ian Storm Taylor 10.03.2016, 07:45
quelle

4 Antworten

0

Ich wollte ein paar Dinge zusammenfassen, nachdem ich über das Problem mehr nachgedacht und eine Lösung implementiert habe ... @ rpys Antwort hat mir sehr geholfen, lies das zuerst!

Es gibt einige Dinge, die dem Autorisierungscode und dem Datenbankabfragecode innewohnen, die ein besseres, zukunftssichereres Design ermöglichen, mit dem Sie zwei dieser Abfragen loswerden können.

404 ist nicht 403

Das erste Problem, auf das @rpy hingewiesen hat, besteht darin, dass Sie aus Sicherheitsgründen Benutzern, die nicht berechtigt sind, ein Objekt zu finden, keine 403-Antwort anzeigen möchten, da Informationen verloren gehen. Stattdessen sollten alle Fehler wie 403: user_find_unauthorized , die aus dem Code geworfen werden, neu zugeordnet werden (wie auch immer das geschehen soll) in 404: user_not_found .

Damit ist es auch ziemlich einfach, den Autorisierungscode so zu ändern, dass er nicht fehlschlägt, wenn ein user -Objekt nicht an erster Stelle existiert. (In meinem Fall war mein Autorisierungscode auf diese Weise bereits strukturiert.)

Damit können Sie die Abfrage exists check-one ablegen.

Denken Sie an die Seitennummerierung

Das zweite Problem ist ein zukünftiges Problem: Was passiert, wenn Sie später die Paginierung zu Ihrer API hinzufügen? Mit meinem Beispielcode wäre die Paginierung sehr schwer zu implementieren, da "Abfragen" und "Maskieren" getrennt sind, so dass Dinge wie LIMIT 10 fast nicht korrekt ausgeführt werden können.

Aus diesem Grund muss der Maskierungscode zwar komplex werden, Sie müssen ihn jedoch in Ihre ursprüngliche find -Abfrage einfügen, um die Seitenumwandlung LIMIT und ORDER BY -Klauseln zu ermöglichen.

Noch eine Abfrage runter.

2 ist besser als 1

Nach all dem glaube ich nicht, dass ich die letzten beiden Abfragen zu einer einzigen Abfrage kombinieren möchte, weil die Trennung von Bedenken zwischen ihnen sehr nützlich ist. Nicht nur das, aber wenn jemand nicht berechtigt ist, auf ein Objekt zuzugreifen, wird das aktuelle Setup schnell fehlschlagen, ohne dass es die Datenbanklast durch unnötige Arbeit negativ beeinflussen kann.

Mit all dem hättest du etwas im Sinne von:

%Vor%

can wird die Autorisierung ausführen und durch die Angabe von users.find zusätzlich zu teams.find wird sichergestellt, dass nicht autorisierte Looks 404 s zurückgeben.

findTeams führt die Nachschlagevorgänge aus, und indem es currentUser übergeben wird, kann es auch die erforderliche Maskierungslogik enthalten.

Ich hoffe, dass all das jedem hilft, der sich darüber Gedanken macht!

    
Ian Storm Taylor 17.03.2016, 21:07
quelle
2

Ich habe es zu einer Funktion gemacht und die Tabellen vereinfacht, um einfacher zu testen. SQL Fiddle . Ich mache Annahmen, da einige der Regeln in den JavaScript-Pseudocode eingebettet sind, den ich nicht ganz verstehe.

%Vor%

Gibt alle aktuellen Benutzerteams sowie die vom Benutzer verwendeten Teams zurück:

%Vor%

Wenn der Benutzer nicht existiert, gibt er die erste Zeile zurück, die Nullen plus alle aktuellen Benutzerteams enthält:

%Vor%

Wenn der aktuelle Benutzer nicht existiert, dann ein leerer Satz:

%Vor%     
Clodoaldo Neto 13.03.2016 13:01
quelle
1

Ihre Absicht / Anforderung, Details zu dem Fehler, der dem Benutzer unterschiedliche Fehler anzeigt, widerzuspiegeln, ist ein Hauptgrund dafür, dass die Abfragen nicht in weniger zusammengefasst werden.

Zur Beantwortung Ihrer expliziten Fragen:

%Vor%

Das hängt wirklich von der Anzahl der Zeilen mit den Tabellen ab. Für die Leistung sollten Sie die Zeiten der Abfragen messen. Dies kann wirklich nicht aus den Abfragen beurteilt werden (allein). Normalerweise haben Abfragen mit "column = VALUE" eine gute Chance, OK auszuführen, wenn die Tabelle klein ist oder ein geeigneter Index vorhanden ist.

%Vor%

Angesichts der von Ihnen gezeigten Abfragen wäre eine Kombination möglich. Dies wird wahrscheinlich die Unterscheidung der tatsächlichen Ursache des Auth-Fehlers verlieren (oder der Abfrage zusätzliche Komplexität hinzufügen). Sie haben jedoch bereits festgestellt, dass die tatsächlichen Abfragen wahrscheinlich etwas komplexer sind. Die Kombination mehrerer Tabellen und (angeblich) vieler Alternativen (ORs, UNIONs, die zur Abdeckung der Varianten benötigt werden) könnte dazu führen, dass der Abfrageoptimierer keinen guten Plan mehr findet. Wenn Sie sich also mit der Leistung befassen, könnte das Kombinieren der Abfragen möglich sein sich negativ auf die Gesamtleistung auswirken (vorbehaltlich der üblichen Messung). Die Gesamtleistung kann auch dadurch verbessert werden, dass weniger Abfragen parallel ausgeführt werden. (Was nur ein Vorteil ist, solange die Anzahl der parallelen Anfragen wirklich niedrig ist).

%Vor%

Dies kann nicht anhand der wenigen Kriterien beantwortet werden, die zu diesem Entwurf geführt haben. Wir brauchen Informationen darüber, was zu erreichen ist und was die Sicherheitsstrategie sein muss. In einigen Fällen z.B. Sie könnten mit der Verwendung der Sicherheit auf Zeilenebene umgehen, die von PG als Version 9.5 verfügbar ist.

%Vor%

Ja, keine Transaktionen können zu inkonsistenten Entscheidungsergebnissen führen, sobald Änderungen an Ihren Autorisierungstabellen während der Ausführung von Abfragen vorgenommen werden. Z.B. Denken Sie daran, dass ein Benutzer entfernt wird und das canFindTeam vor der Abfrage "exists" oder ähnlichen Race-Bedingungen abgeschlossen wird.

Diese Effekte müssen nicht unbedingt schädlich sein, aber sie existieren definitiv. Um ein klareres Bild zu erhalten, beachten Sie bitte die möglichen Änderungen (Einfügen, Löschen, Aktualisieren) der Auth-Tabellen und die Auswirkungen auf Ihre Auth-Abfragen (und gehen Sie nicht davon aus, dass die Abfragen der Reihe nach ausgeführt werden - Sie führen den Async aus!) und die endgültige Entscheidung und Rückkehr zum Benutzer. Wenn all diese Ergebnisse kein Risiko darstellen, bleiben Sie möglicherweise bei der Nichtverwendung von Transaktionen. Ansonsten wird die Verwendung von Transaktionen dringend empfohlen.

%Vor%

Aus Sicherheitsgründen ist die Angabe von Details über einen Fehler eine schlechte Sache. Sie sollten also bei einem Fehler immer ein "nicht autorisiert" zurückgeben oder einfach ein leeres Ergebnis zurückgeben (und nur das detaillierte Ergebnis der Prüfungen zur Analyse oder zum Debuggen protokollieren).

    
rpy 13.03.2016 09:32
quelle
1

Ich könnte (und wahrscheinlich auch) es vorziehen, dies zu vereinfachen, aber beginnen wir mit einer vereinfachten Erläuterung. Sie möchten Informationen zu einem bestimmten Benutzer und zu allen Teams, mit denen sie verbunden sein können. Wenn Sie mit einem bestimmten Benutzer beginnen, erhalten Sie IMMER mindestens die Benutzerkomponenten, wenn es sich um einen gültigen Benutzer handelt. Nur wenn es einen Mitgliedschaftsdatensatz und ein entsprechendes Team gibt, erhalten Sie alle Teaminformationen, mit denen diese eine Person direkt verbunden ist. Wenn diese Abfrage KEINE Datensätze zurückgibt, ist die Benutzer-ID zunächst ungültig und Sie können entsprechend mit 0 Datensätzen antworten.

%Vor%

Es geht also vom Benutzer zur Mitgliedschaft in den Teams, mit denen eine Person direkt verbunden ist und die keine andere Person betrifft. Ich habe nicht gesehen, woher diese "andere Person" stammt, die die Details nur für die üblichen Teams einschränkt. Also, bis zur weiteren Klärung, werde ich diese Antwort erweitern und eine andere Ebene tiefer gehen, um alle Mitgliedschaften eines anderen Benutzers zu erhalten und sie teilen das gleiche Team ... Im Grunde durch die Umkehrung der Verschachtelung von Tabellen auf gemeinsame Mitgliedschaft / Team zurück in die Benutzertabelle .

%Vor%

Ich hoffe, dass das Sinn macht, und lasst mich die Wiederaufnahme von Mitgliedschaften als m2 erklären. Wenn die Person "A" eine Mitgliedschaft in den Teams "X", "Y" und "Z" hat, möchte ich mich der Mitgliedschafts-Tabelle durch die gleiche TEAM - UND einige andere Person ID anschließen. Wenn ein solcher Eintrag vorhanden ist, gehe erneut zur Tabelle des Benutzers (Alias ​​u2) und nimm den Namen und die E-Mail des Mitspielers.

Wenn 50 Teams verfügbar sind, aber Person "A" nur für 3 Teams gilt, sucht sie nur nach anderen MÖGLICHEN Mitgliedern dieser 3 Teams und der Benutzer auf der sekundären (m2-Alias) Mitgliedschaftstabelle ist das " andere "Personen ID.

    
DRapp 17.03.2016 21:01
quelle