REST-API-Autorisierungsstrategien

8

Hier gibt es viele Fragen, die sich mit den Mechanismen der Authentifizierung und Autorisierung von RESTful-APIs befassen, aber keine von ihnen scheint Einzelheiten zur Implementierung sicherer Dienste auf Anwendungsebene zu enthalten.

Angenommen, meine Webanwendung (ich habe Java im Sinn, dies gilt jedoch für jedes Backend) verfügt über ein sicheres Authentifizierungssystem, mit dem sich Benutzer der API mit einem Benutzernamen und einem Kennwort anmelden können. Wenn der Benutzer eine Anfrage stellt, kann ich zu jedem Zeitpunkt während der Pipeline für die Anforderungsverarbeitung eine getAuthenticatedUser()Methode aufrufen , die entweder den Nullbenutzer zurückgibt, wenn der Benutzer nicht angemeldet ist, oder ein Benutzerdomänenobjekt, das den angemeldeten Benutzer darstellt.

Die API ermöglicht authentifizierten Benutzern den Zugriff auf ihre Daten, z. B. ein GET, /api/orders/um die Auftragsliste dieses Benutzers zurückzugeben. In ähnlicher Weise gibt ein GET to /api/tasks/{task_id}Daten zurück, die sich auf diese bestimmte Aufgabe beziehen.

Nehmen wir an, dass es eine Reihe verschiedener Domänenobjekte gibt, die einem Benutzerkonto zugeordnet werden können (Bestellungen und Aufgaben sind zwei Beispiele, wir könnten auch Kunden, Rechnungen usw. haben). Wir möchten nur, dass authentifizierte Benutzer auf Daten zu ihren eigenen Objekten zugreifen können. Wenn ein Benutzer einen Anruf tätigt, müssen /api/invoices/{invoice_id}wir überprüfen, ob der Benutzer berechtigt ist, auf diese Ressource zuzugreifen, bevor wir sie bereitstellen.

Meine Frage ist dann, ob es Muster oder Strategien gibt, um dieses Autorisierungsproblem zu lösen. Eine Option, die ich in Betracht ziehe, ist das Erstellen einer Hilfsschnittstelle (dh SecurityUtils.isUserAuthorized(user, object)), die während der Anforderungsverarbeitung aufgerufen werden kann, um sicherzustellen, dass der Benutzer zum Abrufen des Objekts berechtigt ist. Dies ist nicht ideal, da es den Anwendungsendpunktcode mit vielen dieser Aufrufe verschmutzt, z

Object someEndpoint(int objectId) {
    if (!SecurityUtils.isUserAuthorized(loggedInUser, objectDAO.get(objectId)) {
        throw new UnauthorizedException();
    }
    ...
}

... und dann stellt sich die Frage, ob diese Methode für jeden Domänentyp implementiert werden kann, was ein bisschen schmerzhaft sein kann. Dies könnte die einzige Option sein, aber ich würde mich über Ihre Vorschläge freuen!

HJCee
quelle
Wenn Sie sagen, dass sich die Benutzer "anmelden", meinen Sie damit, dass Sie eine Sitzung aufrechterhalten?
JimmyJames

Antworten:

8

Bitte für die Liebe Gottes keine SecurityUtilsKlasse schaffen !

Ihre Klasse wird in wenigen Monaten zu 10.000 Zeilen Spaghetti-Code! Sie müssten einen ActionTyp (Erstellen, Lesen, Aktualisieren, Zerstören, Auflisten usw.) an Ihre isUserAuthorized()Methode übergeben, der schnell zu einer tausend Zeilen langen switchAnweisung mit immer komplexer werdender Logik wird, die sich nur schwer in einem Unit-Test testen lässt. Tu es nicht.


Im Allgemeinen muss zumindest in Ruby on Rails jedes Domänenobjekt für seine eigenen Zugriffsrechte verantwortlich sein, indem für jedes Modell eine Richtlinienklasse festgelegt wird . Anschließend fragt der Controller die Richtlinienklasse, ob der aktuelle Benutzer für die Anforderung Zugriff auf die Ressource hat oder nicht. Hier ist ein Beispiel in Ruby, da ich so etwas noch nie in Java implementiert habe, aber die Idee sollte sauber rüberkommen:

class OrderPolicy

    class Scope < Struct.new(:user, :scope)

        def resolve

            # A user must be logged in to interact with this resource at all
            raise NotAuthorizedException unless user

            # Admin/moderator can see all orders
            if (user.admin? || user.moderator?)
                scope.all
            else
                # Only allow the user to see their own orders
                scope.where(orderer_id: user.id)
            end
        end
    end

    # Constructor, if you don't know Ruby
    def initialize(user, order)
        raise NotAuthorizedException unless user
        @user = user
        @order= order
    end

    # Whitelist what data can be manipulated by each type of user
    def valid_attributes
        if @user.admin?
            [:probably, :want, :to, :let, :admin, :update, :everything]
        elsif @user.moderator?
            [:fewer, :attributes, :but, :still, :most]
        else
            [:regualar, :user, :attributes]
        end
    end

    # Maybe restrict updatable attributes further
    def valid_update_attributes
    end

    # Who can create new orders
    def create?
        true # anyone, and they would have been authenticated already by #initialize
    end

    # Read operation
    def show?
        @user.admin? || @user.moderator? || owns_order
    end

    # Only superusers can update resources
    def update?
        @user.admin? || @user.moderator?
    end

    # Only admins can delete, because it's extremely destructive or whatever
    def destroy?
        @user.admin?
    end

    private

    # A user 'owns' an order if they were the person who submitted the order
    # E.g. superusers can access the order, but they didn't create it
    def owns_order
        @order.orderer_id == @user.id
    end
end

Selbst wenn Sie über komplexe verschachtelte Ressourcen verfügen, muss eine Ressource die verschachtelten Ressourcen "besitzen", damit die Logik der obersten Ebene von Natur aus heruntersprudelt. Diese verschachtelten Ressourcen benötigen jedoch ihre eigenen Richtlinienklassen, falls sie unabhängig von der übergeordneten Ressource aktualisiert werden können.

In meiner Bewerbung für die Abteilung meiner Universität dreht sich alles um das CourseObjekt. Es ist keine Userzentrierte Anwendung. Da jedoch Users eingeschrieben sind Course, kann ich einfach sicherstellen, dass:

@course.users.include? current_user && (whatever_other_logic_I_need)

für jede Ressource, die eine bestimmte UserPerson ändern muss, da fast alle Ressourcen an a gebunden sind Course. Dies erfolgt in der Richtlinienklasse in der owns_whateverMethode.

Ich habe nicht viel Java-Architektur gemacht, aber es scheint, dass Sie eine PolicySchnittstelle erstellen könnten , in der die verschiedenen Ressourcen, die authentifiziert werden müssen, die Schnittstelle implementieren müssen. Dann haben Sie alle notwendigen Methoden , die so komplex werden können , wie Sie sie brauchen , um pro Domain - Objekt . Wichtig ist, die Logik an das Modell selbst zu binden, es aber gleichzeitig in einer separaten Klasse zu halten (Single Responsibility Principle (SRP)).

Ihre Controller-Aktionen könnten ungefähr so ​​aussehen:

public List<Order> index(OrderQuery query) {

    authorize(Order.class)
    // you should be auto-rescuing the NotAuthorizedException thrown by
    //the policy class at the controller level (or application level)

    // if the authorization didn't fail/rescue from exception, just render the resource
    List<Order> orders = db.search(query);
    return renderJSON(orders);
}

public Order show(int orderId) {

    authorize(Order.class)
    Order order = db.find(orderId);
    return renderJSON(order);
}
Chris Cirefice
quelle
1

Eine bequemere Lösung besteht darin, Anmerkungen zu verwenden, um Methoden zu markieren, für die eine Autorisierung erforderlich ist. Dies unterscheidet sich von Ihrem Geschäftscode und kann von Spring Security oder benutzerdefiniertem AOP-Code verarbeitet werden. Wenn Sie diese Anmerkungen für Ihre Geschäftsmethoden anstelle von Endpunkten verwenden, können Sie sicher sein, dass eine Ausnahme auftritt, wenn ein nicht autorisierter Benutzer versucht, sie unabhängig vom Einstiegspunkt aufzurufen.

Michał Kosmulski
quelle
Nicht was ich frage. Mit Spring Annotations können Sie sicherstellen, dass der Benutzer über eine bestimmte Berechtigungsstufe verfügt (z. B. dass der Benutzer ein Administrator ist), aber ich glaube nicht, dass sie dazu beitragen, den Zugriff auf bestimmte Entitäten einzuschränken. Angenommen, die Geschäftslogik ruft eine Rechnung basierend auf einer ID ab und gibt sie zurück. Ich melde mich an und gebe dem Endpunkt die Rechnungs-ID einer anderen Person. Ich glaube nicht, dass die Frühlingsanmerkungen den horizontalen Zugriff auf dieses Formular verhindern.
HJCee
1
@ HJCee Spring Securities AOP-Anmerkungen sind sehr ausdrucksstark . Sie können eine @PreAuthorization("hasRole('ADMIN') and #requestingUser.company.uuid == authentication.details.companyUuid")Annotation definieren, in der das #requestingUserSegment auf ein pased Objekt mit dem Feldnamen verweist, requestingUserder eine Methode hat, für die getCompany()das zurückgegebene Objekt eine weitere Methode hat getUuid(). Das authenticationbezieht sich auf das Authenticationim Sicherheitskontext gespeicherte Objekt.
Roman Vottner
1
@RomanVottner Was passiert, wenn Sie eine wirklich komplexe Autorisierung benötigen? Beispielsweise können nur Moderatoren in Stack Exchange mit X-Gold-Abzeichen im Y-Tag Änderungen vornehmen, um Fragen (oder was auch immer) zu löschen. Ich werde die einzeilige Annotation mit 300 Zeichen weitergeben.
Chris Cirefice
1
@ChrisCirefice Verwenden @PreAuthorize("hasPermission(#user, 'allowDoSomething')")und implementieren Sie entweder Ihren benutzerdefinierten Berechtigungsauswerter oder schreiben Sie einen benutzerdefinierten Ausdruckshandler und root . Wenn Sie das Verhalten der verfügbaren Anmerkungen ändern möchten, schauen Sie sich diesen Thread an
Roman Vottner
0

Verwenden Sie funktionsbasierte Sicherheit.

Eine Fähigkeit ist ein nicht fälschbares Objekt, das als Beweis dafür dient, dass man eine bestimmte Aktion ausführen kann. In diesem Fall:

  • Machen Sie jede Rolle (Satz zulässiger Aktionen) zu einer Schnittstelle.
  • Lassen Sie die Operationen, die eine Authentifizierung erfordern, Methoden auf ihren jeweiligen Schnittstellen sein. Diese sollten nach Möglichkeit eine Ausnahme auslösen, wenn der Empfänger nicht der aktuelle Benutzer der Anforderung ist.

Dies macht es unmöglich, etwas zu versuchen, zu dem der aktuelle Benutzer nicht berechtigt ist.

Auf diese Weise ist es unmöglich

Demi
quelle
1
Nicht der Kritiker zu sein, aber ist das nicht ein TL; DR meiner Antwort? Wenn ja, wäre es vorzuziehen, einfach meine Antwort zu kommentieren, anstatt Ihre eigene zu schreiben :)
Chris Cirefice
Nicht ganz. Hier geht es darum, die verschiedenen Rollen auszudrücken, die Benutzer im Java-Typsystem haben können, sodass Sie keine Methode für einen Benutzer aufrufen können, für den ein Benutzer keine Berechtigung benötigt.
Demi
Neben Chris 'Kommentar geht es bei meiner Frage nicht um rollenbasierte Zugriffsbeschränkungen (die mit einem guten Webframework trivial zu implementieren sind), sondern um Zugriffsbeschränkungen, die auf Assoziationen zwischen Benutzern und Daten basieren (' gehört Objekt X dem Benutzer Y '). ist ein wirklich einfaches Beispiel für eine solche Assoziation, aber sie könnte sehr komplex sein. Das ist das Problem, über das ich wirklich Rat bekommen möchte.
HJCee