Manajemen akses di Expressjs dengan CASL



Dalam aplikasi modern yang mendukung otentikasi, kami sering ingin mengubah apa yang terlihat oleh pengguna, tergantung pada perannya. Misalnya, pengguna tamu dapat melihat artikel, tetapi hanya pengguna atau administrator terdaftar yang melihat tombol untuk menghapus artikel ini.


Mengelola visibilitas ini bisa menjadi mimpi buruk lengkap dengan meningkatnya peran. Anda mungkin telah menulis atau melihat kode seperti ini:


if (user.role === ADMIN || user.auth && post.author === user.id) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) } 

Kode tersebut didistribusikan di seluruh aplikasi dan biasanya menjadi masalah besar ketika pelanggan mengubah persyaratan atau meminta untuk menambahkan peran tambahan. Pada akhirnya, Anda harus melalui semua if-s dan menambahkan cek tambahan.


Pada artikel ini, saya akan menunjukkan cara alternatif untuk menerapkan manajemen izin di Expressjs API menggunakan perpustakaan yang disebut CASL . Ini sangat menyederhanakan kontrol akses dan memungkinkan Anda untuk menulis ulang contoh sebelumnya menjadi seperti ini:


 if (req.ability.can('read', post)) { res.send(post) } else { res.status(403).send({ message: 'You are not allowed to do this!' }) } 

Baru mengenal CASL? Saya sarankan membaca Apa itu CASL?


Catatan: Artikel ini awalnya diterbitkan di Medium.


Aplikasi demo


Sebagai aplikasi pengujian, saya membuat REST API yang cukup sederhana untuk blog . Aplikasi terdiri dari 3 entitas ( User , Post dan Comment ) dan 4 modul (satu modul untuk setiap entitas dan satu lagi untuk verifikasi otorisasi). Semua modul dapat ditemukan di folder src/modules .


Aplikasi ini menggunakan model luwak , otentikasi paspor dan otorisasi berbasis CASL (atau kontrol akses). Menggunakan API, pengguna dapat:


  • baca semua artikel dan komentar
  • buat pengguna (mis., daftar)
  • kelola artikel Anda sendiri (buat, edit, hapus), jika diotorisasi
  • perbarui informasi pribadi jika diizinkan
  • kelola komentar Anda sendiri jika diotorisasi

Untuk menginstal aplikasi ini, cukup clone dari github dan jalankan npm install dan npm start . Anda juga perlu memulai server MongoDB, aplikasi terhubung ke mongodb://localhost:27017/blog . Setelah semuanya siap, Anda dapat bermain sedikit, dan untuk membuatnya lebih menyenangkan, impor data dasar dari db/ folder:


 mongorestore ./db 

Atau, Anda dapat mengikuti instruksi di file proyek README atau menggunakan koleksi tukang pos saya.


Apa masalahnya?


Pertama, keuntungan besar CASL adalah memungkinkan Anda menentukan hak akses di satu tempat, untuk semua pengguna! Kedua, CASL berfokus bukan pada siapa pengguna itu, tetapi pada apa yang bisa ia lakukan, yaitu. pada kemampuannya. Ini memungkinkan Anda untuk mendistribusikan kapabilitas ini ke berbagai peran atau grup pengguna tanpa upaya yang tidak perlu. Ini berarti bahwa kami dapat mendaftarkan hak akses untuk pengguna yang sah dan tidak sah:


 const { AbilityBuilder, Ability } = require('casl') function defineAbilitiesFor(user) { const { rules, can } = AbilityBuilder.extract() can('read', ['Post', 'Comment']) can('create', 'User') if (user) { can(['update', 'delete', 'create'], ['Post', 'Comment'], { author: user._id }) can(['read', 'update'], 'User', { _id: user._id }) } return new Ability(rules) } const ANONYMOUS_ABILITY = defineAbilitiesFor(null) module.exports = function createAbilities(req, res, next) { req.ability = req.user.email ? defineAbilitiesFor(req.user) : ANONYMOUS_ABILITY next() } 

Sekarang mari kita parsing kode yang ditulis di atas. Sebuah instance dari AbilityBuilder -a dibuat dalam fungsi defineAbilitiesFor(user) , metode ekstraknya membagi objek ini menjadi 2 fungsi sederhana can dan cannot dan sebuah array rules ( cannot dapat digunakan dalam kode ini). Selanjutnya, dengan menggunakan panggilan ke fungsi can , kami menentukan apa yang dapat dilakukan pengguna: argumen pertama melewati tindakan (atau array tindakan), argumen kedua adalah jenis objek di mana tindakan (atau array jenis) dilakukan, dan objek kondisi dapat dilewati sebagai argumen opsional ketiga. Objek kondisi digunakan ketika memeriksa hak akses pada instance kelas, yaitu ia memeriksa apakah properti author objek post dan user._id sama, jika ya, maka true akan dikembalikan, jika tidak false . Untuk kejelasan, saya akan memberikan contoh:


 // Post is a mongoose model const post = await Post.findOne() const user = await User.findOne() const ability = defineAbilitiesFor(user) console.log(ability.can('update', post)) //  post.author === user._id,   true 

Selanjutnya, menggunakan if (user) kami menentukan hak akses untuk pengguna yang diotorisasi (jika pengguna tidak berwenang, kami tidak tahu siapa dia dan kami tidak memiliki objek dengan informasi tentang pengguna). Pada akhirnya, kami mengembalikan instance kelas Ability , dengan bantuan yang akan kami periksa hak aksesnya.


Selanjutnya, kita membuat konstanta ANONYMOUS_ABILITY , ini adalah turunan dari kelas Ability untuk pengguna yang tidak sah. Pada akhirnya, kami mengekspor middleware ekspres, yang bertanggung jawab untuk membuat instance Ability untuk pengguna tertentu.


Pengujian API


Mari kita uji apa yang terjadi dengan tukang pos . Pertama, Anda perlu mendapatkan accessToken, untuk ini mengirim permintaan:


 POST /session { "session": { "email": "casl@medium.com", "password": "password" } } 

Anda mendapatkan respons seperti ini sebagai berikut:


 { "accessToken": "...." } 

Token ini harus dimasukkan ke dalam Authorization header dan dikirim dengan semua permintaan berikutnya.


Sekarang mari kita coba perbarui artikelnya.


 PATCH http://localhost:3030/posts/597649a88679237e6f411ae6 { "post": { "title": "[UPDATED] my post title" } } 200 Ok { "post": { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } } 

Semuanya bekerja dengan baik. Tetapi bagaimana jika kita memperbarui artikel orang lain?


 PATCH http://localhost:3030/posts/59761ba80203fb638e9bd85c { "post": { "title": "[EVIL ACTION] my post title" } } 403 { "status": "forbidden", "message": "Cannot execute \"update\" on \"Post\"" } 

Ada kesalahan! Seperti yang diharapkan :)


Sekarang mari kita bayangkan bahwa untuk penulis blog kami, kami ingin membuat halaman di mana mereka dapat melihat semua posting yang dapat mereka perbarui. Dari sudut pandang logika tertentu, ini tidak sulit, Anda hanya perlu memilih semua artikel di mana penulis sama dengan user._id . Tapi kami sudah mendaftarkan logika seperti itu dengan bantuan CASL, akan sangat mudah untuk mendapatkan semua artikel seperti itu dari database tanpa menulis permintaan tambahan, dan jika hak berubah, maka Anda harus mengubah permintaan - kerja ekstra :).


Untungnya, CASL memiliki paket npm tambahan - @ casl / luwak . Paket ini memungkinkan Anda untuk meminta catatan dari MongoDB sesuai dengan izin khusus! Untuk luwak, paket ini menyediakan plugin yang menambahkan metode yang accessibleBy(ability, action) model. Dengan menggunakan metode ini, kami juga akan meminta catatan dari database (baca lebih lanjut tentang ini di dokumentasi CASL dan file paket README ).


Inilah tepatnya cara handler /posts diimplementasikan (Saya juga menambahkan kemampuan untuk menentukan tindakan mana yang Anda perlukan untuk memeriksa izin):


 Post.accessibleBy(req.ability, req.query.action) 

Jadi, untuk menyelesaikan masalah yang dijelaskan sebelumnya, cukup tambahkan parameter action=update :


 GET http://localhost:3030/posts?action=update 200 Ok { "posts": [ { "_id": "597649a88679237e6f411ae6", "updatedAt": "2017-07-24T19:53:09.693Z", "createdAt": "2017-07-24T19:25:28.766Z", "title": "[UPDATED] my post title", "text": "very long and interesting text", "author": "597648b99d24c87e51aecec3", "__v": 0 } ] } 

Kesimpulannya


Berkat CASL, kami memiliki cara yang sangat bagus untuk mengelola hak akses. Saya lebih dari yakin bahwa jenisnya membangun


 if (ability.can('read', post)) ... 

jauh lebih jelas dan lebih mudah daripada


 if (user.role === ADMIN || user.auth && todo.author === user.id) ... 

Dengan CASL, kita bisa lebih jelas tentang apa yang kode kita lakukan. Selain itu, pemeriksaan semacam itu pasti akan digunakan di tempat lain dalam aplikasi kita, dan di sinilah CASL akan membantu menghindari duplikasi kode.


Saya harap Anda tertarik untuk membaca tentang CASL karena saya tertarik untuk membuatnya. CASL memiliki dokumentasi yang cukup bagus , Anda pasti akan menemukan banyak informasi berguna di sana, tetapi jangan ragu untuk bertanya jika dalam obrolan gotik dan menambahkan tanda bintang di github ;)

Source: https://habr.com/ru/post/id414951/


All Articles