مجوز های دسترسی

امنیت یکی از مهمترین مفاهیم در خصوص وبسایت ها هستش که در رابطه با web API ها اهمیتش دو چندان میشه. در حال حاضر API ما به همه دسترسی کامل میده. هیچ محدودیتی نیست؛ هر کاربری میتونه دست به هر کاری بزنه که بشدت خطرناکه! بعنوان مثال: یک کاربر ناشناس میتونه هر پستی رو بخونه، ایجاد کنه، ویرایش و یا حذف کنه. حتی پستی رو که خودش ایجاد نکرده باشه! مسلمه که همچین چیزی اصلا باب میل ما نیست.
خوشبختانه DRF یکسری تنظیمات در رابطه با مجوز های دسترسی داره که میتونیم برای حفاظت از API خودمون بکار بگیریمشون. این تنظیمات رو میشه در سه سطح اعمالشون کرد:

  • project-level
  • view-level
  • model-level

در این بخش، هر سه مورد رو بررسی میکنیم و نهایتا یک مجوز دسترسی سفارشی برای خودمون ایجاد میکنیم تا فقط نویسنده ی یک پست بتونه ویرایش یا حذفش کنه.

مجوز های Project-Level

خود DRF مجموعه ای از پیکربندیها رو داره که در تنظیمات جنگو و در قسمت REST_FRAMEWORK قرار میگیرن. ما قبلا یکیشون رو صراحتا در فایل django_project/setting.py آوردیم:

Code


# django_project/settings.py


REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.AllowAny",
],
}


در اصل چهار تا از این تیپ پیکربندی های توکار داریم که در دسته ی project-level قرار میگیرن:

  • AllowAny : هر کاربری، چه احراز هویت شده باشه و چه نشده باشه، دسترسی کامل داره
  • IsAuthenticated : فقط کاربرانی دسترسی دارن که ثبت نام کردن و احراز هویت شدن
  • IsAdminUser : فقط اَدمین ها یا superuser ها دسترسی دارن
  • IsAuthenticatedOrReadOnly : کاربرانی که احراز هویت نشدن میتونن هر صفحه ای رو ببینن ولی فقط اونهایی که احراز هویت شده باشن از امتیازات ایجاد، ویرایش یا حذف کردن برخوردارن.

بمنظور پیاده سازی هر کدوم از موارد بالا کافیه که تنظیمات DEFAULT_PERMISSION_CLASSES رو بروزرسانی کنیم و مرورگر خودمون رو refresh کنیم. همین!
از این بین، ما فقط سوییچ میکنیم روی IsAuthenticated تا فقط کاربرانی که احراز هویت شدن یا وارد شدن، بتونند از API ما دیدن کنن.

این بخش از تنظیمات در فایل django_project/settings.py رو به شکل زیر در میاریم:

Code


# django_project/settings.py


REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
"rest_framework.permissions.IsAuthenticated", # new
],
}


حالا اگه مرورگر خودتون رو refresh کنید میبیند که هیچ اتفاقی نمیفته و علتش اینه که ما با حساب superuser وارد شدیم که اگه به گوشه بالا-راست صفحه نگاه کنید میتونید از این بابت مطمئن بشید. بمنظور خروج از حساب فعلی به آدرس: " /http://.127.0.0.1:8000/admin " برید و روی لینک " Log Out " در گوشه بالا-راست کلیک کنید.
حالا اگه دوباره به آدرس: " /http://127.0.0.1:8000/api/v1 " برگردید، با کد 403 مواجه میشید که از عدم ورود مشخصات لازم بمنظور احراز هویت کاربر حکایت داره و این دقیقا همون چیزیه که میخواستیم!

Django Blog API 403 Error

ایجاد کاربر جدید

لازمه که شرایط دسترسی به API خودمون رو از دید یک کاربر عادی بررسی کنیم چون بالاخره قرار نیست که همه اَدمین باشن! برای ایجاد کاربر جدید دوباره به آدرس: " /http://127.0.0.1:8000/admin " برگردید و وارد حساب superuser خودتون بشید. بعدش روی لینک " +Add " روبه روی Users کلیک کنید. سپس یک نام کاربری و رمزعبور برای کاربر جدید وارد کنید و روی دکمه ی " Save " کلیک کنید. من از نام کاربری: testuser و رمزعبور: testpass1234 کردم ولی شما میتونید از مشخصات دلخواه خودتون استفاده کنید. همچنین توجه داشته باشید که یک فیلد با عنوان " Name " هم داریم که پرکردنش اختیاریه و به لطف مدل کاربری سفارشی خودمون در اینجا حضور داره.

Django Blog API Adding 1st test user


در صفحه ی بعدی میتونیم علاوه بر نام کاربری و رمزعبور، یکسری اطلاعات دیگه هم راجع به کاربر جدیدمون اضافه کنیم ولی لازم نیست و همینی که هست کفایت میکنه.

Django Blog API Admin user change page

به انتهای صفحه برید و روی دکمه ی " Save " کلیک کنید. بعدش به یک صفحه جدید هدایت میشید که میتونید لیستی از کاربرها رو مشاهده کنید.

Django Blog API Users list

در این صفحه اگر به " Staff Status " دقت کنید متوجه میشید که فقط یکی از حساب ها superuser هستش و اون یکی صرفا یه حساب عادیه.
نهایتا روی لینک " Log Out " در گوشه بالا-راست صفحه کلیک میکنیم تا از پنل اَدمین خارج بشیم.

Django Blog API Admin logout page

اضافه کردن قابلیت های Log In و Log Out

حالا سوالی که پیش میاد اینه که این کاربر جدید چجوری قراره لاگین کنه (Log In) ؟ خوشبختانه DRF تقریبا ترتیب همه چیز رو از این بابت داده و ما میتونیم فقط با اضافه کردن یک خط در فایل django_project/urls.py ، یک لینک " Log In " داشته باشیم.

Code


# django_project/urls.py

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
path('admin/', admin.site.urls),
path("api/v1/", include("posts.urls")),
path("api-auth/", include("rest_framework.urls")), # new
]


اجباری نبود که از الگوی " /api-auth " استفاده کنیم و میتونستیم هر چیز دیگه ای هم بجاش وارد کنیم ولی ترجیحا طبق مستندات DRF جلو رفتیم.

حالا اگه در مرورگر خودتون به آدرس: " /http://127.0.0.1:8000/api/v1 " برید، یک تغییر جزئی میبیند:
یک لینک با عنوان " Log In " در گوشه بالا- راست صفحه اضافه شده.

Django Blog API Adding login link

روی این لینک جدید کلیک کنید و با وارد کردن مشخصات حساب جدیدی که ایجاد کردیم وارد بشید.

Django Blog API Login page

بعد از ورود، به صفحه لیست بندی پست ها هدایت میشید. اگه به گوشه بالا-راست صفحه نگاه کنید نام کاربری حساب جدیدتون رو مشاهده میکنید که کنارش یک فلش رو به پایین وجود داره که اگه روش کلیک کنید، لینک " Log Out " براتون نمایش داده میشه.

Django Blog API Accessing logout link through dropdown menu

مجوز های View-Level

مجوز هایی در این سطح، امکان کنترل جزئی تر و دقیق تری رو برامون فراهم میکنن.
در حال حاضر دو تا view در فایل posts/views.py داریم که ما قصد داریم دومیش یعنی: " PostDetail " رو به نحوی بروزرسانی کنیم که فقط اَدمین ها بتونن ببیننش. در صورتی که اینکار بدرستی پیاده بشه: کاربری که لاگین نکرده کلا نمیتونه API ما رو ببینه، کاربری که لاگین کرده فقط میتونه لیست پست ها رو ببینه، و فقط اَدمین میتونه صفحه ی اختصاصی هر پست رو ببینه.

پس در ابتدای فایل مذکور میایم و permissions رو از DRF فراخوانی میکنیم. سپس فیلد permission_classes رو به کلاسِ PostDetail اضافه اش میکنیم و این فیلد جدید رو روی IsAdminUser تنظیمش میکنیم.

Code


# posts/views.py

from rest_framework import generics, permissions # new

from .models import Post
from .serializers import PostSerializer


class PostList(generics.ListCreateAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer


class PostDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (permissions.IsAdminUser,) # new
queryset = Post.objects.all()
serializer_class = PostSerializer


حالا اگه مرورگر خودتون رو refresh کنید میبیند که لیست پست ها هنوزم براتون نمایش داده میشه ولی اگر به صفحه اختصاصی پست اول در آدرس: " /http://127.0.0.1:8000/api/v1/1 " برید، با کد وضعیت 403 روبه رو میشید!

Django Blog API 403 Error in detail

اگر از حساب فعلی خارج بشید و این دفعه با حساب اَدمین وارد بشید، دیگه با این پیام مواجه نمیشید چون دسترسی کامل دارید. پس میشه گفت که ما بطرز صحیحی یک مجوزِ view-level رو پیاده کردیم.

بطور کلی، انواع استاندارد مجوز ها عبارتند از: دسترسی کامل برای همه، دسترسی کامل برای کاربران احراز هویت شده، دسترسی محدود شده برای کاربران احراز هویت شده، و دسترسی کامل برای اَدمین ها. پیکربندی مجوز ها، بستگی به نیازهای پروژه شما داره.

حالا قبل از اینکه ادامه بدیم، دوباره به فایل posts/views.py برگردید و permissions رو از ابتدای فایل، به همراه فیلد permission_classes از کلاسِ PostDetail حذف کنید. چون برای کاری که ما میخواهیم بکنیم، همین که فقط کاربران احرازهویت شده اجازه دسترسی داشته باشند کافیه. ما اینکار رو قبلا در فایل django_project/settings.py و در تنظیمات DEFAULT_PERMISSON انجامش دادیم.

مجوز سفارشی

حالا میخواهیم اولین مجوز سفارشی خودمون رو درست کنیم تا فقط نویسنده ی یک پست اجازه ی ویرایش یا حذف همون پست رو داشته باشه؛ به عبارت دیگه قراره که یک کاربر عادی فقط مجاز به ویرایش و حذف پست های خودش باشه، اما اَدمین همچنان مجاز به انجام هر کاری خواهد بود.
بصورت توکار، DRF متکی به کلاسی به نام BasePermission هستش که همه ی کلاس های permission بصورت وراثتی ازش استفاده میکنن و کد مربوطه اش رو هم میتونید در Github ببینید:

Code


class BasePermission(metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""

def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True

def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True


بمنظور ایجاد یک مجوز سفارشی میشه از هر دو تا متد بالا استفاده کرد.
تا اینجای فصل و در پروژه های مختلف بارها پیش اومده که بخواهیم اطلاعات ورودی رو بصورت لیست شده نمایش بدیم و برای اینکار از view هایی استفاده کردیم که به اصطلاح به: " list-view " معروف هستند (مثل ListAPIView) ؛ حالا اگه بخواهیم برای این دسته از view ها مجوز دسترسی سفارشی تعریف کنیم، میایم و از مِتد has_permission استفاده میکنیم. مشابه همین قضیه، ما یک دسته دیگه هم داریم که به: " detail-view " معروف هستند (مثل RetrieveAPIView) و اگه بخواهیم براشون مجوز سفارشی تعیین کنیم باید از هر دو مِتد has_permission و has_object_permission استفاده کنیم اما نکته اش در اینه که باید اول has_permission اجرا بشه و True برگردونه تا بعدش has_object_permission اجرا بشه.

حالا ما در این پروژه میخواهیم یک مجوز سفارشی ایجاد کنیم که اجازه ی ویرایش یا حذف یک پست رو فقط به نویسنده اش بده و مابقی کاربر ها فقط اجازه ی مشاهده اون پست رو داشته باشن. بدین منظور یک فایل جدید با نام: " permissions.py " در دایرکتوری posts ایجاد میکنیم و کد های زیر رو داخلش مینویسیم:

Code


# posts/permissions.py

from rest_framework import permissions


class IsAuthorOrReadOnly(permissions.BasePermission):
def has_permission(self, request, view):
# کاربرانی که احراز هویت شده باشند، امکان خواندن لیست داده ها را دارند
if request.user.is_authenticated:
return True
return False

def has_object_permission(self, request, view, obj):
# همه ی کاربران امکان خواندن پست مورد نظر را دارند
if request.method in permissions.SAFE_METHODS:
return True

# امکان ویرایش یا حذف پست فقط برای نویسنده ی آن مقدور است
return obj.author == request.user


در ابتدای فایل میایم و permissions رو فراخوانی میکنیم و بعدش یک کلاس با نام " IsAuthorOrReadOnly " تعریف میکنیم که حاوی دو متد هستش. متد اول (has_permission) امکان دسترسی به محتوای درخواستی رو فقط برای کاربرانی که لاگین کرده باشند میده. متد دوم (has_object_permission) امکان مشاهده و خواندن محتوای درخواستی رو به همه کاربرها میده ولی اجازه ی ویرایش یا حذفش رو فقط به مالک یا همون نویسنده ی محتوا میده.

***: در قطعه کد بالا ، SAFE_METHODS اشاره داره به متد های GET ، HEAD و OPTIONS .

در ادامه کار میریم سراغ فایل posts/views.py و مجوز جدیدی که ایجاد کردیم رو در ابتدای فایل فراخوانی میکنیم و سپس این مجوز رو با استفاده از فیلدِ permission_classes ، به کلاسهای PostList و PostDetail اضافه میکنیم.

Code


# posts/views.py

from rest_framework import generics, permissions

from .models import Post
from .permissions import IsAuthorOrReadOnly # new
from .serializers import PostSerializer


class PostList(generics.ListCreateAPIView):
permission_classes = (IsAuthorOrReadOnly,) # new
queryset = Post.objects.all()
serializer_class = PostSerializer


class PostDetail(generics.RetrieveUpdateDestroyAPIView):
permission_classes = (IsAuthorOrReadOnly,) # new
queryset = Post.objects.all()
serializer_class = PostSerializer


خب کار تمومه. برای اینکه خروجی مورد انتظارمون رو در عمل ببینیم، با استفاده از حسابِ testuser یک پست جدید ایجاد میکنیم و مطمئن میشیم که این کاربر به عنوان نویسنده ی پست مورد نظر میتونه بهش دسترسی داشته باشه. مشخصه که ما اگر از حساب superuser برای اینکار استفاده کنیم نمیتونیم از خروجی مورد نظر اطمینان داشته باشیم، چون اَدمین بصورت پیشفرض بدون نیاز به هیچگونه مجوزی به همه ی محتوای حاضر دسترسی داره و میتونه هر کاری بکنه.

در مرورگر به آدرس: " /http://127.0.0.1:8000/admin " برید و وارد حساب superuser خودتون بشید و سپس یک پست جدید ایجاد کنید و testuser رو بعنوان نویسنده ی این پست جدید انتخاب کنید. (یادتون نره که روی دکمه ی Save کلیک کنید.)

Django Blog API Adding new post as Testuser

بعدش به آدرس: " /http://127.0.0.1:8000/api/v1/2 " برید و با استفاده از لینک " Log Out " در منوی بالا-راست صفحه، از حساب superuser خارج بشید و این دفعه با استفاده از حساب testuser وارد بشید.

Django Blog API Logging in as Testuser

این همون خروجی مورد انتظار ماست که میتونیم پست مورد نظر رو به عنوان testuser یا نویسنده اش، ویرایش یا حذف کنیم. اما اگر به پست اول در آدرس: " /http://127.0.0.1:8000/api/v1/1 " سر بزنید، میبینید که فقط امکان مشاهده پست رو داریم و نمیتونیم ویرایش یا حذفش کنیم چون توسط یه کاربر دیگه (superuser) ایجاد شده.

Django Blog API Restricted access for non-Admin users

حالا اگه میخواهید که کاملا از بابت همه چیز خیالتون راحت بشه میتونید از حساب testuser فعلی هم خارج بشید و سعی کنید که به اِندپوینت لیست پست ها یا هر کدوم از پست ها سربزنید تا مطمئن بشید که کاربرانی که لاگین نکرده باشند، نمیتونن به چیزی هم دسترسی داشته باشن.

Git

نهایتا هم پیشرفت خودمون در این بخش از پروژه رو ثبت میکنیم:

Shell


(blogapi-JvRm-7-t) > git status
(blogapi-JvRm-7-t) > git add -A
(blogapi-JvRm-7-t) > git commit -m "add permissions"


جمع بندی

دست و پا کردن مجوز های دسترسی مناسب و سنجیده، بخش مهمی از هر API هستش. ما در این بخش با انواع این مجوز ها آشنا شدیم و یک مجوز متناسب با نیاز های پروژه خودمون رو پیاده کردیم.

در بخش بعدی راجع به احراز هویت کاربران صحبت میکنیم.