Django內建許可權擴充套件案例

運維咖啡吧發表於2019-02-28

當Django的內建許可權無法滿足需求的時候就自己擴充套件吧~

背景介紹

overmind專案使用了Django內建的許可權系統,Django內建許可權系統基於model層做控制,新的model建立後會預設新建三個許可權,分別為:add、change、delete,如果給使用者或組賦予delete的許可權,那麼使用者將可以刪除這個model下的所有資料。

原本overmind只管理了我們自己部門的資料庫,許可權設定只針對具體的功能不針對細粒度的資料庫例項,例如使用者A 有稽核的許可權,那麼使用者A 可以稽核所有的DB,此時使用內建的許可權系統就可以滿足需求了,但隨著系統的不斷完善要接入其他部門的資料庫管理,這就要求針對不同使用者開放不同DB的許可權了,例如A部門的使用者只能操作A部門的DB,Django內建基於model的許可權無法滿足需求了。

實現過程

先來確定下需求:

  1. 保持原本的基於功能的許可權控制不變,例如使用者A有查詢許可權,B有稽核許可權
  2. 增加針對DB例項的許可權控制,例如使用者A只能查詢特定的DB,B只能稽核特定的DB

對於上邊需求1用內建的許可權系統已經可以實現,這裡不贅述,重點看下需求2,DB資訊都存放在同一個表裡,不同使用者能操作不同的DB,也就是需要把每一條DB資訊與有許可權操作的使用者進行關聯,為了方便操作,我們考慮把DB跟使用者組關聯,在使用者組裡的使用者都有許可權,而操作型別經過分析主要有兩類讀和寫,那麼需要給每個MySQL例項新增兩個欄位分別記錄對此例項有讀和寫許可權的使用者組

如下程式碼在原來的model基礎上新增read_groupswrite_groups欄位,DB例項跟使用者組應是ManyToManyField多對多關係,一個例項可以關聯多個使用者組,一個使用者組也可以屬於多個例項

class Mysql(models.Model):
    Env = (
        (1, 'Dev'),
        (2, 'Qa'),
        (3, 'Prod'),
    )
    create_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
    update_time = models.DateTimeField(auto_now=True, verbose_name='更新時間')

    project_id = models.IntegerField(verbose_name='專案')
    project_tmp = models.CharField(max_length=128, default='')
    environment = models.IntegerField(choices=Env, verbose_name='環境')

    master_host = models.GenericIPAddressField(verbose_name='master主機')
    master_port = models.IntegerField(default=3306, verbose_name='master埠')

    slave_host = models.GenericIPAddressField(null=True, verbose_name='slave主機')
    slave_port = models.IntegerField(null=True, default=3306, verbose_name='slave埠')

    database = models.CharField(max_length=64, verbose_name='資料庫')

    read_groups = models.ManyToManyField(Group, related_name='read', verbose_name='讀許可權')
    write_groups = models.ManyToManyField(Group, related_name='write', verbose_name='寫許可權')

    description = models.TextField(null=True, verbose_name='備註')
複製程式碼

model確定了,接下來我們分三部分詳細介紹下許可權驗證的具體實現

列表頁許可權控制

Django內建許可權擴充套件案例

如上圖列表頁,每個使用者進入系統後只能檢視自己有讀許可權的MySQL例項列表,管理員能檢視所有,程式碼如下:

def mysql(request):
    if request.method == 'GET':
        if request.user.is_superuser:
            _lists = Mysql.objects.all().order_by('id')
        else:
            # 獲取登入使用者的所有組
            _user_groups = request.user.groups.all()

            # 構造一個空的QuerySet然後合併
            _lists = Mysql.objects.none()
            for group in _user_groups:
                _lists = _lists | group.read.all()

        return render(request, 'overmind/mysql.index.html', {'request': request, 'lPage': _lists})
複製程式碼

實現的思路是:獲取登入使用者的所有組,然後迴圈查詢每個組有讀取許可權的資料庫例項,最後把每個組有許可權讀的資料庫例項進行合併返回

獲取登入使用者的所有組用到了ManyToMany的查詢方法:request.user.groups.all()

最終返回的一個結果是QuerySet,所以我們需要先構造一個空的Queryset:Mysql.objects.none()

QuerySet合併不能用簡單的相加,應為:QuerySet-1 | QuerySet-2

查詢介面許可權控制

Django內建許可權擴充套件案例

如上圖系統中有很多功能是需要根據專案、環境查詢對應的DB資訊的,對於此類介面也需要控制使用者只能查詢自己有許可權讀的DB例項,管理員能檢視所有,程式碼如下:

def get_project_database(request, project, environment):
    if request.method == 'GET':
        _jsondata = {}

        if request.user.is_superuser:
            # 返回所有專案和環境匹配的DB
            _lists = Mysql.objects.filter(
                project_id=int(project),
                environment=int(environment)
            )

            _jsondata = {i.id: i.database for i in _lists}
        else:
            # 只返回使用者有許可權查詢的DB
            _user_groups = request.user.groups.all()

            for group in _user_groups:
                # 迴圈mysql表中有read_groups許可權的所有組
                for mysql in group.read.all():
                    if mysql.project_id == int(project) and mysql.environment == int(environment):
                        _jsondata[mysql.id] = mysql.database

        return JsonResponse(_jsondata)
複製程式碼

實現思路與上邊類似,只是多了一步根據專案和環境再進行判斷

需要根據group去反查都有哪些DB例項包含了該組,這裡用到了M2M的related_name屬性:group.read.all()

更多關於Django ORM查詢的內容可以看這篇文章Django model select的各種用法詳解有詳細的總結

執行操作許可權控制

除了上邊的兩個場景之外我們還需要在執行具體的操作之前去判斷是否有許可權,例如執行稽核操作前判斷使用者是否對此DB有寫許可權

有很多地方都需要做這個判斷,所以把這個許可權判斷單獨寫個方法來處理,程式碼如下:

def check_permission(perm, mysql, user):
    # 如果使用者是超級管理員則有許可權
    if user.is_superuser:
        return True

    # 取出使用者所屬的所有組
    _user_groups = user.groups.all()

    # 取出Mysql對應許可權的所有組
    if perm == 'read':
        _mysql_groups = mysql.read_groups.all()
    if perm == 'write':
        _mysql_groups = mysql.write_groups.all()

    # 使用者組和DB許可權組取交集,有則表示有許可權,否則沒有許可權
    group_list = list(set(_user_groups).intersection(set(_mysql_groups)))

    return False if len(group_list) == 0 else True
複製程式碼

實現思路是:根據傳入的第三個使用者引數,來獲取到使用者所有的組,然後根據傳入的第一個引數型別讀取或寫入和第二個引數DB例項來獲取到有許可權的所有組,然後對兩個組取交集,交集不為空則表示有許可權,為空則沒有

M2M的.all()取出來的結果是個list,兩個list取交集的方法為:list(set(list-A).intersection(set(list-B)))

view中使用就很簡單了,如下:

def query(request):
    if request.method == 'POST':
        postdata = request.body.decode('utf-8')
        _host = get_object_or_404(Mysql, id=int(postdata.get('database')))

        # 檢查使用者是否有DB的查詢許可權
        if check_permission('read', _host, request.user) == False:
            return JsonResponse({'state': 0, 'message': '當前使用者沒有查詢此DB的許可權'})
複製程式碼

寫在最後

  1. Django有第三方的基於object的許可權管理模組Django-guardian,本專案沒有使用主要是因為一來許可權需求並不複雜,自己實現也很方便,二來個人在非必要的情況下並不喜歡引用過多第三方的包,後續升級維護都是負擔
  2. 方案和程式碼不盡完美,各位有更好的方案建議或更優雅的程式碼寫法歡迎與我交流

Django內建許可權擴充套件案例

如果你覺得文章不錯,請點右下角【好看】。如果你覺得讀的不盡興,推薦閱讀以下文章:

相關文章