
Masalah membatasi akses ke data muncul hampir selalu ketika mengembangkan sistem multi-pengguna. Skenario utama adalah sebagai berikut:
- pembatasan akses data untuk pengguna yang diautentikasi
- pembatasan akses ke data untuk diautentikasi tetapi tidak memiliki hak pengguna yang diperlukan
- mencegah akses tidak sah melalui panggilan langsung ke API
- memfilter data dalam permintaan pencarian dan daftar elemen UI (tabel, daftar)
- mencegah perubahan data milik satu pengguna oleh pengguna lain
Skenario 1-3 dijelaskan dengan baik dan biasanya diselesaikan dengan menggunakan alat kerangka kerja bawaan, seperti otorisasi
berbasis peran atau
klaim . Tetapi situasi ketika pengguna yang berwenang dapat secara langsung mengakses data "tetangga" atau mengambil tindakan dalam akunnya terjadi setiap saat. Ini paling sering terjadi karena fakta bahwa programmer lupa untuk menambahkan cek yang diperlukan. Anda dapat mengandalkan tinjauan kode, atau Anda dapat mencegah situasi seperti itu dengan menerapkan aturan penyaringan data global. Mereka akan dibahas dalam artikel.
Daftar dan tabel
Pengontrol khas untuk menerima data dalam ASP.NET MVC mungkin terlihat
seperti ini :
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = _dbContext .Set<TEntity>() .Where() .Count(); var items= _dbContext .Set<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
Dalam hal ini, semua tanggung jawab untuk memfilter data hanya ada pada programmer. Apakah dia akan ingat bahwa perlu untuk menambahkan kondisi di
Where
atau tidak?
Anda dapat memecahkan masalah menggunakan
filter global . Namun, untuk membatasi akses, kami memerlukan informasi tentang pengguna saat ini, yang berarti bahwa konstruksi
DbContext
harus rumit untuk menginisialisasi bidang tertentu.
Jika ada banyak aturan, maka implementasi
DbContext
pasti akan harus belajar "terlalu banyak", yang akan melanggar
prinsip tanggung jawab tunggal .
Arsitektur engah
Masalah dengan akses ke data dan copy-paste terjadi karena dalam contoh kami mengabaikan pembagian menjadi lapisan dan dari pengendali kami segera mencapai lapisan akses data, melewati lapisan logika bisnis. Pendekatan ini bahkan telah dijuluki "
pengendali jelek dan bodoh ." Dalam artikel ini saya tidak ingin menyentuh masalah yang berkaitan dengan repositori, layanan, dan penataan logika bisnis. Filter global melakukan pekerjaan ini dengan baik, Anda hanya perlu menerapkannya pada abstraksi dari lapisan lain.
Tambahkan abstraksi
.NET sudah memiliki
IQueryable
untuk mengakses data. Ganti akses langsung ke
DbContext
dengan akses ke penyedia tersebut:
public interface IQueryableProvider { IQueryable<T> Query<T>() where T: class; IQueryable Query(Type type); }
Dan untuk mengakses data, kami akan membuat filter ini:
public interface IPermissionFilter<T> { IQueryable<T> GetPermitted(IQueryable<T> queryable); }
Kami menerapkan penyedia sedemikian rupa sehingga mencari semua filter yang dinyatakan dan secara otomatis menerapkannya:
public class QueryableProvider: IQueryableProvider {
Kode untuk mendapatkan dan membuat filter dalam contoh ini tidak optimal. Alih-alih
Activator.CreateInstance
lebih baik menggunakan
Pohon Ekspresi yang dikompilasi . Beberapa kontainer IOC mendukung
registrasi obat generik terbuka . Saya akan meninggalkan pertanyaan pengoptimalan di luar cakupan artikel ini.
Kami menyadari filter
Implementasi filter mungkin terlihat seperti ini:
public class EntityPermissionFilter: PermissionFilter<Entity> { public EntityPermissionFilter(DbContext dbContext, IIdentity identity) : base(dbContext, identity) { } public override IQueryable<Practice> GetPermitted( IQueryable<Practice> queryable) { return DbContext .Set<Practice>() .WhereIf(User.OrganizationType == OrganizationType.Client, x => x.Manager.OrganizationId == User.OrganizationId) .WhereIf(User.OrganizationType == OrganizationType.StaffingAgency, x => x.Partners .Select(y => y.OrganizationId) .Contains(User.OrganizationId)); } }
Kami memperbaiki kode pengontrol
[HttpGet] public virtual IActionResult Get([FromQuery]T parameter) { var total = QueryableProvider .Query<TEntity>() .Where() .Count(); var items = QueryableProvider .Query<TEntity>() .Where() .ProjectTo<TDto>() .Skip(parameter.Skip) .Take(parameter.Take) .ToList(); return Ok(new {items, total}); }
Tidak banyak perubahan sama sekali. Tetap melarang akses langsung ke
DbContext
dari pengontrol dan jika filter ditulis dengan benar, maka masalah akses data dapat dianggap tertutup. Filternya cukup kecil, jadi menutupinya dengan tes tidaklah sulit. Selain itu, filter yang sama ini dapat digunakan untuk menulis kode otorisasi yang mencegah akses tidak sah ke data "asing". Saya akan meninggalkan pertanyaan ini untuk artikel selanjutnya.