REST strategi otorisasi API

8

Ada banyak pertanyaan di sini yang berhubungan dengan mekanisme otentikasi dan otorisasi API ISTIRAHAT tetapi tidak satupun dari mereka tampaknya masuk ke detail tentang bagaimana menerapkan layanan aman di tingkat aplikasi.

Sebagai contoh, misalkan webapp saya (saya ingat Java tetapi ini berlaku untuk backend mana pun) memiliki sistem otentikasi aman yang memungkinkan pengguna API untuk login dengan nama pengguna dan kata sandi. Ketika pengguna membuat permintaan, pada titik mana pun selama permintaan memproses pipa saya bisa memanggil getAuthenticatedUser()metode yang akan mengembalikan pengguna nol jika pengguna tidak masuk, atau objek domain pengguna yang mewakili pengguna yang masuk.

API memungkinkan pengguna terotentikasi untuk mengakses data mereka, mis. GET untuk /api/orders/akan mengembalikan daftar pesanan pengguna tersebut. Demikian pula, GET untuk /api/tasks/{task_id}akan mengembalikan data yang berkaitan dengan tugas tertentu.

Mari kita asumsikan bahwa ada sejumlah objek domain berbeda yang dapat dikaitkan dengan akun pengguna (pesanan dan tugas adalah dua contoh, kami juga dapat memiliki pelanggan, faktur, dll.). Kami hanya ingin pengguna yang diautentikasi dapat mengakses data tentang objek mereka sendiri sehingga ketika pengguna melakukan panggilan ke /api/invoices/{invoice_id}kami perlu memeriksa bahwa pengguna berwenang untuk mengakses sumber daya itu sebelum kami menyajikannya.

Pertanyaan saya adalah, apakah ada pola atau strategi untuk menangani masalah otorisasi ini? Salah satu opsi yang saya pertimbangkan adalah membuat antarmuka pembantu (yaitu SecurityUtils.isUserAuthorized(user, object)), yang dapat dipanggil selama pemrosesan permintaan untuk memastikan bahwa pengguna berwenang untuk mengambil objek. Ini tidak ideal karena mencemari kode titik akhir aplikasi dengan banyak panggilan ini, mis

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

... dan kemudian ada pertanyaan tentang penerapan metode ini untuk setiap jenis domain yang bisa sedikit menyebalkan. Ini mungkin satu-satunya pilihan tetapi saya tertarik mendengar saran Anda!

HJCee
sumber
Ketika Anda mengatakan pengguna "masuk", maksud Anda bahwa Anda mempertahankan sesi?
JimmyJames

Jawaban:

8

Tolong karena cinta Tuhan jangan buat SecurityUtilskelas!

Kelas Anda akan menjadi 10rb baris kode spageti dalam hitungan bulan! Anda perlu memiliki Actionjenis (buat, baca, perbarui, hancurkan, daftar, dll.) Diteruskan ke isUserAuthorized()metode Anda , yang dengan cepat akan menjadi switchpernyataan panjang seribu baris dengan logika yang semakin kompleks yang akan sulit untuk unit test. Jangan lakukan itu.


Secara umum apa yang saya lakukan, setidaknya di Ruby on Rails, adalah meminta setiap objek domain bertanggung jawab atas hak aksesnya sendiri dengan memiliki kelas kebijakan untuk setiap model . Kemudian, pengontrol menanyakan kelas kebijakan apakah pengguna saat ini untuk permintaan memiliki akses ke sumber daya atau tidak. Berikut ini contoh di Ruby, karena saya belum pernah menerapkan hal seperti itu di Jawa, tetapi gagasan itu harus dijabarkan dengan jelas:

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

Bahkan jika Anda memiliki sumber daya bersarang yang kompleks, beberapa sumber daya harus 'memiliki' sumber daya bersarang, sehingga logika tingkat atas secara inheren melebur turun. Namun, sumber daya bersarang tersebut membutuhkan kelas kebijakan mereka sendiri jika mereka dapat diperbarui secara terpisah dari sumber daya 'induk'.

Dalam aplikasi saya, yang untuk departemen universitas saya, semuanya berputar di sekitar Courseobjek. Ini bukan Useraplikasi sentris. Namun, Usersudah terdaftar Course, jadi saya bisa memastikan bahwa:

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

untuk sumber daya apa pun yang Userperlu dimodifikasi, karena hampir semua sumber daya terkait dengan a Course. Ini dilakukan di kelas kebijakan dalam owns_whatevermetode.

Saya belum melakukan banyak arsitektur Java, tetapi tampaknya Anda bisa membuat Policyantarmuka, di mana sumber daya yang berbeda yang perlu diautentikasi harus mengimplementasikan antarmuka. Kemudian, Anda memiliki semua metode yang diperlukan yang dapat menjadi serumit yang Anda perlukan untuk per objek domain . Yang penting adalah untuk mengikat logika dengan model itu sendiri, tetapi pada saat yang sama menyimpannya di kelas yang terpisah (prinsip tanggung jawab tunggal (SRP)).

Tindakan pengontrol Anda bisa terlihat seperti:

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
sumber
1

Solusi yang lebih mudah adalah dengan menggunakan anotasi untuk menandai metode yang memerlukan beberapa bentuk otorisasi. Ini menonjol dari kode bisnis Anda dan dapat ditangani oleh Spring Security atau kode AOP khusus. Jika Anda menggunakan anotasi ini pada metode bisnis Anda daripada titik akhir, Anda dapat memastikan untuk mendapatkan pengecualian ketika pengguna yang tidak sah mencoba memanggil mereka terlepas dari titik masuk.

Michał Kosmulski
sumber
Bukan itu yang aku tanyakan. Anotasi musim semi memungkinkan Anda untuk menjamin bahwa pengguna memiliki tingkat otorisasi tertentu (mis. Bahwa pengguna adalah admin) tetapi saya tidak percaya mereka membantu membatasi akses ke entitas tertentu. Katakanlah logika bisnis mengambil dan mengembalikan faktur berdasarkan id. Saya masuk dan memberikan id faktur orang lain ke titik akhir. Saya tidak percaya anotasi Spring mencegah akses horizontal dari formulir itu?
HJCee
1
Penjelasan AOP @HJCee Spring Securities cukup ekspresif . Anda dapat mendefinisikan @PreAuthorization("hasRole('ADMIN') and #requestingUser.company.uuid == authentication.details.companyUuid")anotasi di mana #requestingUsersegmen mereferensikan objek yang dipetakan dengan fieldName requestingUseryang memiliki metode getCompany()yang mengembalikan objek memiliki metode lebih lanjut getUuid(). The authenticationmengacu pada Authenticationobjek yang tersimpan dalam konteks keamanan.
Roman Vottner
1
@RomanVottner Apa yang terjadi jika Anda memerlukan otorisasi yang sangat rumit? misalnya, hanya moderator di Stack Exchange dengan lencana X emas di tag Y yang dapat melakukan pengeditan untuk menghapus pertanyaan (atau apa pun). Saya akan meneruskan anotasi garis tunggal 300 karakter.
Chris Cirefice
1
@ChrisCirefice Baik menggunakan @PreAuthorize("hasPermission(#user, 'allowDoSomething')")dan mengimplementasikan evaluator izin khusus Anda atau menulis handler dan root ekspresi kustom . Jika Anda ingin mengubah perilaku anotasi yang tersedia, silakan lihat utas ini
Roman Vottner
0

Gunakan keamanan berbasis kemampuan.

Kemampuan adalah objek yang tidak dapat dilupakan yang bertindak sebagai bukti bahwa seseorang dapat melakukan tindakan tertentu. Pada kasus ini:

  • Jadikan setiap peran (serangkaian tindakan yang diizinkan) menjadi antarmuka.
  • Apakah operasi yang memerlukan otentikasi menjadi metode pada antarmuka masing-masing. Ini harus mengeluarkan pengecualian jika penerima bukan pengguna permintaan saat ini, jika memungkinkan.

Ini membuat tidak mungkin untuk mencoba melakukan sesuatu yang tidak diizinkan dilakukan oleh pengguna saat ini.

Dengan begitu tidak mungkin

Demi
sumber
1
Bukan untuk menjadi kritikus, tetapi bukankah ini TL; DR dari jawaban saya? Jika ya, lebih baik mengomentari jawaban saya daripada menulis sendiri :)
Chris Cirefice
Tidak terlalu. Idenya di sini adalah untuk mengekspresikan peran berbeda yang dapat dimiliki pengguna dalam sistem tipe Java, sehingga Anda tidak dapat memanggil metode pada pengguna yang membutuhkan hak istimewa yang tidak dimiliki pengguna.
Demi
Lebih jauh dari komentar Chris, pertanyaan saya bukan tentang pembatasan akses berbasis peran (yang sepele untuk diterapkan dengan kerangka kerja web yang bagus) tetapi tentang pembatasan akses berdasarkan asosiasi antara pengguna dan data ('adalah objek X yang dimiliki oleh pengguna Y' adalah contoh yang sangat sederhana dari asosiasi seperti itu tetapi mereka bisa sangat kompleks). Itulah masalahnya saya benar-benar mencoba untuk mendapatkan saran.
HJCee