Bagaimana cara menerapkan Daftar Kontrol Akses di aplikasi MVC Web saya?

96

Pertanyaan pertama

Tolong, bisakah Anda menjelaskan kepada saya bagaimana ACL paling sederhana dapat diterapkan di MVC.

Berikut adalah pendekatan pertama menggunakan Acl di Controller ...

<?php
class MyController extends Controller {

  public function myMethod() {        
    //It is just abstract code
    $acl = new Acl();
    $acl->setController('MyController');
    $acl->setMethod('myMethod');
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...    
  }

}
?>

Ini adalah pendekatan yang sangat buruk, dan minusnya adalah kita harus menambahkan potongan kode Acl ke dalam setiap metode pengontrol, tetapi kita tidak memerlukan dependensi tambahan!

Pendekatan selanjutnya adalah membuat semua metode pengontrol privatedan menambahkan kode ACL ke dalam __callmetode pengontrol .

<?php
class MyController extends Controller {

  private function myMethod() {
    ...
  }

  public function __call($name, $params) {
    //It is just abstract code
    $acl = new Acl();
    $acl->setController(__CLASS__);
    $acl->setMethod($name);
    $acl->getRole();
    if (!$acl->allowed()) die("You're not allowed to do it!");
    ...   
  }

}
?>

Ini lebih baik dari kode sebelumnya, tetapi minus utama adalah ...

  • Semua metode pengontrol harus bersifat pribadi
  • Kita harus menambahkan kode ACL ke dalam metode __call masing-masing pengontrol.

Pendekatan selanjutnya adalah menempatkan kode Acl ke dalam Pengontrol induk, tetapi kita masih perlu merahasiakan semua metode pengontrol anak.

Apa solusinya? Dan apa praktik terbaiknya? Di mana saya harus memanggil fungsi Acl untuk memutuskan mengizinkan atau melarang metode yang akan dijalankan.

Pertanyaan kedua

Pertanyaan kedua adalah tentang mendapatkan peran menggunakan Acl. Bayangkan kita memiliki tamu, pengguna, dan teman pengguna. Pengguna memiliki akses terbatas untuk melihat profilnya yang hanya dapat dilihat oleh teman. Semua tamu tidak dapat melihat profil pengguna ini. Jadi, inilah logikanya ..

  • kita harus memastikan bahwa metode yang dipanggil adalah profil
  • kami harus mendeteksi pemilik profil ini
  • kita harus mendeteksi apakah pemirsa adalah pemilik profil ini atau bukan
  • kita harus membaca aturan pembatasan tentang profil ini
  • kita harus memutuskan untuk mengeksekusi atau tidak menjalankan metode profil

Pertanyaan utamanya adalah tentang mendeteksi pemilik profil. Kita dapat mendeteksi siapa pemilik profil yang hanya menjalankan metode model $ model-> getOwner (), tetapi Acl tidak memiliki akses ke model. Bagaimana kita bisa menerapkan ini?

Saya berharap pikiran saya jernih. Maaf untuk bahasa Inggris saya.

Terima kasih.

Kirzilla
sumber
1
Saya bahkan tidak mengerti mengapa Anda memerlukan "Daftar kontrol akses" untuk interaksi pengguna. Bukankah Anda akan mengatakan sesuatu seperti if($user->hasFriend($other_user) || $other_user->profileIsPublic()) $other_user->renderProfile()(jika tidak, tampilkan "Anda tidak memiliki akses ke profil pengguna ini" atau sesuatu seperti itu? Saya tidak mengerti.
Buttle Butkus
2
Mungkin, karena Kirzilla ingin mengelola semua kondisi untuk akses di satu tempat - terutama di konfigurasi. Jadi, setiap perubahan izin dapat dilakukan di Admin alih-alih mengubah kode.
Mariyo

Jawaban:

185

Bagian / jawaban pertama (implementasi ACL)

Menurut pendapat saya, cara terbaik untuk mendekati ini adalah dengan menggunakan pola dekorator . Pada dasarnya, ini berarti Anda mengambil objek Anda, dan meletakkannya di dalam objek lain, yang akan bertindak seperti cangkang pelindung. Ini TIDAK mengharuskan Anda untuk memperluas kelas asli. Berikut ini contohnya:

class SecureContainer
{

    protected $target = null;
    protected $acl = null;

    public function __construct( $target, $acl )
    {
        $this->target = $target;
        $this->acl = $acl;
    }

    public function __call( $method, $arguments )
    {
        if ( 
             method_exists( $this->target, $method )
          && $this->acl->isAllowed( get_class($this->target), $method )
        ){
            return call_user_func_array( 
                array( $this->target, $method ),
                $arguments
            );
        }
    }

}

Dan inilah cara Anda menggunakan struktur semacam ini:

// assuming that you have two objects already: $currentUser and $controller
$acl = new AccessControlList( $currentUser );

$controller = new SecureContainer( $controller, $acl );
// you can execute all the methods you had in previous controller 
// only now they will be checked against ACL
$controller->actionIndex();

Seperti yang mungkin Anda perhatikan, solusi ini memiliki beberapa keunggulan:

  1. penahanan dapat digunakan pada objek apa pun, bukan hanya contoh Controller
  2. pemeriksaan otorisasi terjadi di luar objek target, yang berarti:
    • objek asli tidak bertanggung jawab untuk kontrol akses, mengikuti SRP
    • ketika Anda mendapatkan "izin ditolak", Anda tidak terkunci di dalam pengontrol, lebih banyak opsi
  3. Anda dapat menyuntikkan contoh aman ini ke objek lain, itu akan mempertahankan perlindungan
  4. bungkus & lupakan .. Anda bisa berpura - pura bahwa itu adalah objek asli, itu akan bereaksi sama

Namun , ada satu masalah besar dengan metode ini juga - Anda tidak dapat memeriksa secara asli apakah objek yang diamankan mengimplementasikan dan antarmuka (yang juga berlaku untuk mencari metode yang ada) atau merupakan bagian dari beberapa rantai warisan.

Bagian / jawaban kedua (RBAC untuk objek)

Dalam hal ini, perbedaan utama yang harus Anda kenali adalah bahwa Objek Domain Anda (misalnya Profile:) itu sendiri berisi detail tentang pemiliknya. Artinya, bagi Anda untuk memeriksa, jika (dan pada level mana) pengguna memiliki akses ke sana, Anda akan diminta untuk mengubah baris ini:

$this->acl->isAllowed( get_class($this->target), $method )

Pada dasarnya Anda memiliki dua opsi:

  • Berikan ACL objek yang dimaksud. Tetapi Anda harus berhati-hati untuk tidak melanggar Law of Demeter :

    $this->acl->isAllowed( get_class($this->target), $method )
  • Minta semua detail yang relevan dan berikan ACL hanya dengan apa yang dibutuhkannya, yang juga akan membuatnya sedikit lebih ramah untuk pengujian unit:

    $command = array( get_class($this->target), $method );
    /* -- snip -- */
    $this->acl->isAllowed( $this->target->getPermissions(), $command )

Beberapa video yang mungkin membantu Anda membuat penerapan sendiri:

Catatan sampingan

Anda tampaknya memiliki pemahaman yang cukup umum (dan sepenuhnya salah) tentang apa Model di MVC itu. Model bukanlah kelas . Jika Anda memiliki nama kelas FooBarModelatau sesuatu yang mewarisi AbstractModelmaka Anda salah melakukannya.

Dalam MVC yang tepat, Model adalah lapisan, yang berisi banyak kelas. Sebagian besar kelas dapat dipisahkan dalam dua kelompok, berdasarkan tanggung jawab:

- Logika Bisnis Domain

( baca lebih lanjut : di sini dan di sini ):

Contoh dari grup kelas ini menangani penghitungan nilai, memeriksa kondisi yang berbeda, menerapkan aturan penjualan, dan melakukan semua yang Anda sebut "logika bisnis". Mereka tidak memiliki petunjuk bagaimana data disimpan, di mana disimpan atau bahkan jika penyimpanan itu ada di tempat pertama.

Objek Bisnis Domain tidak bergantung pada database. Saat Anda membuat faktur, tidak masalah dari mana data berasal. Ini bisa dari SQL atau dari REST API jarak jauh, atau bahkan tangkapan layar dari dokumen MSWord. Logika bisnis tidak berubah.

- Akses dan Penyimpanan Data

Contoh yang dibuat dari grup kelas ini terkadang disebut Objek Akses Data. Biasanya struktur yang mengimplementasikan pola Data Mapper (jangan bingung dengan ORM dengan nama yang sama .. tidak ada hubungan). Di sinilah pernyataan SQL Anda (atau mungkin DomDocument Anda, karena Anda menyimpannya dalam XML).

Di samping dua bagian utama, ada satu lagi kelompok instance / kelas, yang harus disebutkan:

- Layanan

Di sinilah komponen Anda dan pihak ke-3 berperan. Misalnya, Anda dapat menganggap "otentikasi" sebagai layanan, yang dapat disediakan oleh Anda sendiri, atau beberapa kode eksternal. Juga "pengirim email" akan menjadi layanan, yang mungkin menggabungkan beberapa objek domain dengan PHPMailer atau SwiftMailer, atau komponen pengirim email Anda sendiri.

Sumber layanan lain adalah abstraksi pada domain dan lapisan akses data. Mereka dibuat untuk menyederhanakan kode yang digunakan oleh pengontrol. Misalnya: membuat akun pengguna baru mungkin perlu bekerja dengan beberapa objek domain dan pembuat peta . Tapi, dengan menggunakan layanan, itu hanya membutuhkan satu atau dua baris di pengontrol.

Apa yang harus Anda ingat saat membuat layanan, adalah bahwa seluruh lapisan seharusnya tipis . Tidak ada logika bisnis dalam layanan. Mereka hanya ada di sana untuk mengatur objek domain, komponen, dan pembuat peta.

Salah satu kesamaan dari mereka semua adalah bahwa layanan tidak memengaruhi lapisan View dengan cara apa pun secara langsung, dan bersifat otonom sedemikian rupa, sehingga mereka dapat (dan sering berhenti) digunakan di luar struktur MVC itu sendiri. Selain itu, struktur mandiri seperti itu membuat migrasi ke kerangka kerja / arsitektur yang berbeda jauh lebih mudah, karena kopling yang sangat rendah antara layanan dan aplikasi lainnya.

tereško
sumber
34
Saya baru belajar lebih banyak dalam 5 menit membaca ulang ini, daripada yang saya miliki dalam beberapa bulan. Apakah Anda setuju dengan: pengontrol tipis mengirimkan ke layanan yang mengumpulkan data tampilan? Juga, jika Anda pernah menerima pertanyaan secara langsung, kirimkan saya pesan.
Stephane
2
Saya setuju sebagian. Pengumpulan data dari tampilan terjadi di luar MVC triad, ketika Anda menginisialisasi Requestinstance (atau analoginya). Pengontrol hanya mengekstrak data dari Requestinstance dan meneruskan sebagian besar ke layanan yang sesuai (beberapa di antaranya juga akan ditampilkan). Layanan melakukan operasi yang Anda perintahkan. Kemudian, saat tampilan menghasilkan respons, itu meminta data dari layanan, dan berdasarkan informasi itu, menghasilkan respons. Respons tersebut dapat berupa HTML yang dibuat dari beberapa template atau hanya header lokasi HTTP. Tergantung pada status yang diatur oleh pengontrol.
tereško
4
Untuk menggunakan penjelasan yang disederhanakan: controller "menulis" ke model dan view, view "reads" dari model. Lapisan model adalah struktur pasif di semua pola terkait Web yang telah terinspirasi oleh MVC.
tereško
@Stephane, untuk mengajukan pertanyaan secara langsung, Anda selalu dapat mengirimi saya pesan di twitter. Atau apakah Anda pertanyaannya agak "bentuk panjang", yang tidak bisa dijejali 140 karakter?
tereško
Bacaan dari model: apakah itu berarti beberapa peran aktif model? Saya belum pernah mendengar itu sebelumnya. Saya selalu dapat mengirimi Anda tautan melalui twitter jika itu yang Anda inginkan. Seperti yang Anda lihat, tanggapan ini berubah menjadi percakapan dengan cepat dan saya berusaha untuk menghormati situs ini dan pengikut twitter Anda.
Stephane
16

ACL dan Pengontrol

Pertama-tama: Ini adalah hal / lapisan yang paling sering berbeda. Saat Anda mengkritik kode pengontrol yang patut dicontoh, kode ini menggabungkan keduanya - jelas sekali terlalu ketat.

tereško telah menjelaskan cara bagaimana Anda dapat lebih memisahkan ini dengan pola dekorator.

Saya akan mundur selangkah terlebih dahulu untuk mencari masalah asli yang Anda hadapi dan membahasnya sebentar lagi.

Di satu sisi Anda ingin memiliki pengontrol yang hanya melakukan pekerjaan yang diperintahkan (perintah atau tindakan, sebut saja perintah).

Di sisi lain, Anda ingin dapat menempatkan ACL di aplikasi Anda. Bidang kerja ACL ini harus - jika saya memahami pertanyaan Anda dengan benar - untuk mengontrol akses ke perintah tertentu dari aplikasi Anda.

Kontrol akses semacam ini oleh karena itu membutuhkan sesuatu yang lain yang menyatukan keduanya. Berdasarkan konteks di mana sebuah perintah dieksekusi, ACL masuk dan keputusan perlu dilakukan apakah perintah tertentu dapat dijalankan oleh subjek tertentu (misalnya pengguna).

Mari kita rangkum sampai titik ini apa yang kita miliki:

  • Perintah
  • ACL
  • Pengguna

Komponen ACL sangat penting di sini: Ia perlu mengetahui setidaknya sesuatu tentang perintah (untuk mengidentifikasi perintah dengan tepat) dan harus dapat mengidentifikasi pengguna. Pengguna biasanya mudah diidentifikasi dengan ID unik. Namun seringkali dalam aplikasi web ada pengguna yang tidak teridentifikasi sama sekali, sering disebut tamu, anonim, semua orang, dll. Untuk contoh ini kami berasumsi bahwa ACL dapat menggunakan objek pengguna dan merangkum detail ini. Objek pengguna terikat ke objek permintaan aplikasi dan ACL dapat mengkonsumsinya.

Bagaimana dengan mengidentifikasi perintah? Interpretasi Anda tentang pola MVC menunjukkan bahwa sebuah perintah adalah gabungan dari nama kelas dan nama metode. Jika kita melihat lebih dekat bahkan ada argumen (parameter) untuk sebuah perintah. Jadi valid untuk menanyakan apa sebenarnya yang mengidentifikasi sebuah perintah? Nama kelas, nama metode, jumlah atau nama argumen, bahkan data di dalam salah satu argumen atau campuran dari semua ini?

Bergantung pada tingkat detail yang Anda perlukan untuk mengidentifikasi perintah di ACL Anda, ini bisa sangat bervariasi. Sebagai contoh, mari kita buat sederhana dan tentukan bahwa perintah diidentifikasi oleh nama kelas dan nama metode.

Jadi konteks bagaimana ketiga bagian ini (ACL, Command dan User) menjadi milik satu sama lain sekarang lebih jelas.

Bisa dibilang, dengan komponen ACL imajiner kita sudah bisa melakukan hal berikut:

$acl->commandAllowedForUser($command, $user);

Lihat saja apa yang terjadi di sini: Dengan membuat perintah dan pengguna dapat diidentifikasi, ACL dapat melakukan tugasnya. Pekerjaan ACL tidak terkait dengan pekerjaan objek pengguna dan perintah konkret.

Hanya ada satu bagian yang hilang, ini tidak bisa hidup di udara. Dan ternyata tidak. Jadi, Anda perlu menemukan tempat di mana kontrol akses perlu dijalankan. Mari kita lihat apa yang terjadi di aplikasi web standar:

User -> Browser -> Request (HTTP)
   -> Request (Command) -> Action (Command) -> Response (Command) 
   -> Response(HTTP) -> Browser -> User

Untuk menemukan tempat itu, kita tahu itu harus sebelum perintah konkret dijalankan, jadi kita bisa mengurangi daftar itu dan hanya perlu melihat ke tempat (potensial) berikut:

User -> Browser -> Request (HTTP)
   -> Request (Command)

Di beberapa titik dalam aplikasi Anda, Anda tahu bahwa pengguna tertentu telah meminta untuk melakukan perintah konkret. Anda sudah melakukan semacam ACL di sini: Jika pengguna meminta perintah yang tidak ada, Anda tidak mengizinkan perintah tersebut untuk dijalankan. Jadi, di mana pun yang terjadi dalam aplikasi Anda mungkin merupakan tempat yang baik untuk menambahkan pemeriksaan ACL "nyata":

Perintah telah ditemukan dan kita dapat membuat identifikasi sehingga ACL dapat mengatasinya. Jika perintah tidak diizinkan untuk pengguna, perintah tersebut tidak akan dijalankan (tindakan). Mungkin CommandNotAllowedResponsebukan CommandNotFoundResponseuntuk kasus permintaan tidak dapat diselesaikan ke perintah konkret.

Tempat di mana pemetaan HTTPRequest beton dipetakan ke sebuah perintah sering disebut Routing . Sebagai Perutean sudah memiliki tugas untuk menemukan perintah, mengapa tidak memperluasnya untuk memeriksa apakah perintah tersebut benar-benar diizinkan per ACL? Misalnya dengan memperluas Router ke router menyadari ACL: RouterACL. Jika router Anda belum mengetahui User, maka Routeritu bukan tempat yang tepat, karena agar ACL bekerja tidak hanya perintah tetapi juga pengguna harus diidentifikasi. Jadi tempat ini dapat bervariasi, tetapi saya yakin Anda dapat dengan mudah menemukan tempat yang perlu diperluas, karena ini adalah tempat yang memenuhi persyaratan pengguna dan perintah:

User -> Browser -> Request (HTTP)
   -> Request (Command)

Pengguna tersedia sejak awal, Perintah dulu dengan Request(Command).

Jadi, alih-alih menempatkan pemeriksaan ACL Anda di dalam implementasi konkret setiap perintah, Anda menempatkannya sebelumnya. Anda tidak memerlukan pola yang berat, sihir atau apapun, ACL melakukan tugasnya, pengguna melakukan tugasnya dan terutama perintah yang melakukan tugasnya: Hanya perintah, tidak ada yang lain. Perintah tersebut tidak memiliki kepentingan untuk mengetahui apakah peran tersebut berlaku atau tidak, apakah itu dijaga atau tidak.

Jadi pisahkan saja hal-hal yang bukan milik satu sama lain. Gunakan sedikit penulisan ulang Prinsip Tanggung Jawab Tunggal (SRP) : Seharusnya hanya ada satu alasan untuk mengubah perintah - karena perintah telah berubah. Bukan karena Anda sekarang memperkenalkan ACL dalam aplikasi Anda. Bukan karena Anda mengalihkan objek Pengguna. Bukan karena Anda bermigrasi dari antarmuka HTTP / HTML ke SOAP atau antarmuka baris perintah.

ACL dalam kasus Anda mengontrol akses ke perintah, bukan perintah itu sendiri.

hakre
sumber
Dua pertanyaan: CommandNotFoundResponse & CommandNotAllowedResponse: apakah Anda akan meneruskan ini dari kelas ACL ke Router atau Controller dan mengharapkan respons universal? 2: Jika Anda ingin menyertakan metode + atribut, bagaimana Anda akan menanganinya?
Stephane
1: Respon adalah respon, disini bukan dari ACL tapi dari router, ACL membantu router untuk mengetahui tipe respon (tidak ditemukan, terutama: terlarang). 2: Tergantung. Jika yang Anda maksud adalah atribut sebagai parameter dari tindakan, dan Anda membutuhkan ACL dengan parameter, letakkan di bawah ACL.
hakre
13

Salah satu kemungkinannya adalah menggabungkan semua pengontrol Anda di kelas lain yang memperluas Kontroler dan membuatnya mendelegasikan semua panggilan fungsi ke instance yang dibungkus setelah memeriksa otorisasi.

Anda juga dapat melakukannya lebih upstream, di petugas operator (jika aplikasi Anda memang memilikinya) dan mencari izin berdasarkan URL, bukan metode kontrol.

edit : Apakah Anda perlu mengakses database, server LDAP, dll. adalah ortogonal untuk pertanyaan tersebut. Maksud saya adalah bahwa Anda dapat menerapkan otorisasi berdasarkan URL, bukan metode pengontrol. Ini lebih kuat karena Anda biasanya tidak akan mengubah URL Anda (jenis area URL antarmuka publik), tetapi Anda mungkin juga mengubah implementasi pengontrol Anda.

Biasanya, Anda memiliki satu atau beberapa file konfigurasi tempat Anda memetakan pola URL tertentu ke metode otentikasi dan arahan otorisasi tertentu. Petugas operator, sebelum mengirimkan permintaan ke pengontrol, menentukan apakah pengguna berwenang dan membatalkan pengiriman jika tidak.

Artefacto
sumber
Tolong, bisakah Anda memperbarui jawaban Anda dan menambahkan lebih banyak detail tentang Dispatcher. Saya memiliki dispatcher - ini mendeteksi metode pengontrol apa yang harus saya panggil dengan URL. Tapi saya tidak mengerti bagaimana saya bisa mendapatkan peran (saya perlu mengakses DB untuk melakukannya) di Dispatcher. Berharap untuk mendengar Anda segera.
Kirzilla
Aha, dapatkan idemu. Saya harus memutuskan mengizinkan mengeksekusi atau tidak tanpa mengakses metode! Jempolan! Pertanyaan terakhir yang belum terselesaikan - bagaimana mengakses model dari ACL. Ada ide?
Kirzilla
@Kirz Saya memiliki masalah yang sama dengan Pengendali. Sepertinya dependensi harus ada di sana. Bahkan jika ACL tidak, bagaimana dengan lapisan model? Bagaimana Anda bisa mencegahnya menjadi ketergantungan?
Stephane