公共模型
建立檔案common/extends/models.py,将cmdb/models.py裡定義的TimeAbstract、CommonParent移到這裡
from django.db import models
class TimeAbstract(models.Model):
update_time = models.DateTimeField(
auto_now=True, null=True, blank=True, verbose_name='更新時間')
created_time = models.DateTimeField(
auto_now_add=True, null=True, blank=True, verbose_name='建立時間')
class ExtMeta:
related = False
dashboard = False
class Meta:
abstract = True
ordering = ['-id']
class CommonParent(models.Model):
parent = models.ForeignKey(
"self", null=True, blank=True, on_delete=models.SET_NULL, related_name='children')
class Meta:
abstract = True
建立使用者子產品
建立使用者中心子產品
(venv) ➜ ydevops-backend django-admin startapp ucenter
(venv) ➜ ydevops-backend mv ucenter apps
編寫使用者組織架構及rbac模型
from django.db import models
from django.contrib.auth.models import AbstractUser
from common.extends.models import TimeAbstract, CommonParent
# Create your models here.
def org_extra_data():
return {
'leader_user_id': '', # 存儲部門上司ID
'dn': '', # 存儲ldap dn
}
def user_extra_data():
return {
'ding_userid': '', # 釘釘使用者ID
'feishu_userid': '', # 飛書UserID
'feishu_unionid': '', # 飛書UnionID
'feishu_openid': '', # 飛書OpenID
'leader_user_id': '', # 直屬上司ID
'dn': '', # ldap dn
}
class Menu(TimeAbstract, CommonParent):
"""
菜單模型
"""
name = models.CharField(max_length=30, unique=True, verbose_name='菜單名')
title = models.CharField(max_length=30, null=True, blank=True, verbose_name='菜單顯示名')
icon = models.CharField(max_length=50, null=True, blank=True, verbose_name='圖示')
path = models.CharField(max_length=158, null=True, blank=True, verbose_name='路由位址')
redirect = models.CharField(max_length=200, null=True, blank=True, verbose_name='跳轉位址')
is_frame = models.BooleanField(default=False, verbose_name='外部菜單')
hidden = models.BooleanField(default=False, verbose_name='是否隐藏')
spread = models.BooleanField(default=False, verbose_name='是否預設展開')
sort = models.IntegerField(default=0, verbose_name='排序标記')
component = models.CharField(max_length=200, default='Layout', verbose_name='元件')
affix = models.BooleanField(default=False, verbose_name='固定标簽')
single = models.BooleanField(default=False, verbose_name='标簽單開')
activeMenu = models.CharField(max_length=128, blank=True, null=True, verbose_name='激活菜單')
def __str__(self):
return self.name
class Meta:
default_permissions = ()
verbose_name = '菜單'
verbose_name_plural = verbose_name + '管理'
ordering = ['sort', 'name']
class Permission(TimeAbstract, CommonParent):
"""
權限模型
"""
name = models.CharField(max_length=30, unique=True, verbose_name='權限名')
method = models.CharField(max_length=50, null=True, blank=True, verbose_name='方法')
def __str__(self):
return self.name
class Meta:
default_permissions = ()
verbose_name = '權限'
verbose_name_plural = verbose_name + '管理'
class Role(TimeAbstract):
"""
角色模型
"""
name = models.CharField(max_length=32, unique=True, verbose_name='角色')
permissions = models.ManyToManyField(Permission, blank=True, related_name='role_permission', verbose_name='權限')
menus = models.ManyToManyField(Menu, blank=True, verbose_name='菜單')
desc = models.CharField(max_length=50, blank=True, null=True, verbose_name='描述')
def __str__(self):
return self.name
class Meta:
default_permissions = ()
verbose_name = '角色'
verbose_name_plural = verbose_name + '管理'
class Organization(TimeAbstract, CommonParent):
"""
組織架構
"""
organization_type_choices = (
('company', '公司'),
('department', '部門')
)
dept_id = models.CharField(max_length=32, unique=True, verbose_name='部門ID')
name = models.CharField(max_length=60, verbose_name='名稱')
type = models.CharField(max_length=20, choices=organization_type_choices, default='department', verbose_name='類型')
extra_data = models.JSONField(default=org_extra_data, verbose_name='其它資料', help_text=f'資料格式:{org_extra_data()}')
@property
def full(self):
l = []
self.get_parents(l)
return l
def get_parents(self, parent_result: list):
if not parent_result:
parent_result.append(self)
parent_obj = self.parent
if parent_obj:
parent_result.append(parent_obj)
parent_obj.get_parents(parent_result)
def __str__(self):
return self.name
class ExtMeta:
related = True
dashboard = False
class Meta:
default_permissions = ()
verbose_name = '組織架構'
verbose_name_plural = verbose_name + '管理'
class UserProfile(TimeAbstract, AbstractUser):
"""
使用者資訊
"""
mobile = models.CharField(max_length=11, null=True, blank=True, verbose_name='手機号碼')
avatar = models.ImageField(upload_to='static/%Y/%m', default='image/default.png',
max_length=250, null=True, blank=True)
department = models.ManyToManyField(Organization, related_name='org_user', verbose_name='部門')
# 職能:根據職能授權
position = models.CharField(max_length=50, null=True, blank=True, verbose_name='職能')
# 職位:僅展示使用者title資訊
title = models.CharField(max_length=50, null=True, blank=True, verbose_name='職位')
roles = models.ManyToManyField(Role, verbose_name='角色', related_name='user_role', blank=True)
extra_data = models.JSONField(default=user_extra_data, verbose_name='其它資料', help_text=f'資料格式:{user_extra_data()}')
is_ldap = models.BooleanField(default=False, verbose_name='是否ldap使用者')
@property
def name(self):
if self.first_name:
return self.first_name
return self.username
def __str__(self):
return self.name
class ExtMeta:
related = True
dashboard = False
icon = 'peoples'
class Meta:
default_permissions = ()
verbose_name = '使用者資訊'
verbose_name_plural = verbose_name + '管理'
ordering = ['id']
安裝依賴
pip install pillow
添加子產品到settings.py及配置自定義的使用者認證模型
INSTALLED_APPS = [
...
'ucenter.apps.UcenterConfig',
]
...
AUTH_USER_MODEL = 'ucenter.UserProfile'
CMDB模型更新
調整cmdb子產品裡的模型
# 注釋原有使用者模型,替換為自定義的模型
# from django.contrib.auth.models import User
from ucenter.models import UserProfile as User
from .model_assets import Idc, Region
# 從common裡導入TimeAbstract, CommonParent
from common.extends.models import TimeAbstract, CommonParent
遷移資料
(venv) ➜ ydevops-backend python manage.py makemigrations
Migrations for 'ucenter':
apps/ucenter/migrations/0001_initial.py
- Create model Menu
- Create model Permission
- Create model Role
- Create model Organization
- Create model UserProfile
此時建立表時會有異常
(venv) ➜ ydevops-backend python manage.py migrate
...
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration admin.0001_initial is applied before its dependency ucenter.0001_initial on database 'default'.
我們現在開發階段,最省事的就是直接把database删除,重新生成
rm db.sqlite3
python manage.py makemigrations
python manage.py migrate
當然,如果想嘗試解決,我們按如下步驟
- 注釋settings.py->INSTAALLLED_APPS裡的'django.contrib.admin';注釋urls.py->urlpatterns裡的path('admin/', admin.site.urls)。admin是django自帶的管理背景,我們可以不用這個,直接删除也行......
- 删除cmdb裡的遷移檔案apps/cmdb/migrations/0*.py
- 建立ucenter
python manage.py migrate ucenter
- 重新生成遷移檔案
python manage.py makemigrations
- 檢視遷移,確定所有檔案已執行完成
python manage.py showmigrations
由于我們使用了自定義的使用者模型,需要重新建立使用者
(venv) ➜ ydevops-backend python manage.py createsuperuser
編寫序列化器
class UserProfileListSerializers(serializers.ModelSerializer):
user_department = serializers.SerializerMethodField()
user_director = serializers.SerializerMethodField()
def get_user_department(self, instance):
return [{'org_id': i.id, 'org_name': i.name} for i in instance.department.all()]
def get_user_director(self, instance):
leader_ou = [i.extra_data['leader_user_id'] for i in instance.department.all() if i.extra_data.get('leader_user_id', None)]
leaders = UserProfile.objects.filter(extra_data__feishu_openid__in=leader_ou)
return [[{'id': i.id, 'name': i.name} for i in leaders]]
class Meta:
model = UserProfile
exclude = ('password', 'dn')
class UserProfileDetailSerializers(UserProfileListSerializers):
user_roles = serializers.SerializerMethodField()
routers = serializers.SerializerMethodField()
permissions = serializers.SerializerMethodField()
def get_user_roles(self, instance):
try:
qs = instance.roles.all()
return [{'id': i.id, 'name': i.name, 'desc': i.desc} for i in qs]
except BaseException as e:
return []
def get_permissions(self, instance):
perms = instance.roles.values(
'permissions__method',
).distinct()
if instance.is_superuser:
return ['admin']
return [p['permissions__method'] for p in perms if p['permissions__method']]
def get_routers(self, instance):
qs = []
if instance.is_superuser or 'admin' in [p['permissions__method'] for p in
instance.roles.values('permissions__method')]:
qs = Menu.objects.filter(parent__isnull=True)
serializer = MenuListSerializers(instance=qs, many=True)
tree_data = serializer.data
else:
[qs.extend(i.menus.all()) for i in instance.roles.all()]
serializer = UserMenuSerializers(instance=qs, many=True)
# 組織使用者擁有的菜單清單
tree_dict = {}
tree_data = []
try:
for item in serializer.data:
tree_dict[item['id']] = item
for i in tree_dict:
if tree_dict[i]['parent']:
pid = tree_dict[i]['parent']
parent = tree_dict[pid]
parent.setdefault('children', []).append(tree_dict[i])
else:
tree_data.append(tree_dict[i])
except:
tree_data = serializer.data
return tree_data
class Meta:
model = UserProfile
exclude = ('avatar',)
class UserProfileSerializers(serializers.ModelSerializer):
class Meta:
model = UserProfile
exclude = ('avatar',)
def create(self, validated_data):
roles = validated_data.pop('roles')
departments = validated_data.pop('department')
instance = UserProfile.objects.create(**validated_data)
instance.set_password(validated_data['password'])
instance.save()
instance.department.set(departments)
instance.roles.set(roles)
return instance
使用者管理視圖
import shortuuid
from django.db.models import Q
from django.core.cache import cache
from config import USER_AUTH_BACKEND
import logging
logger = logging.getLogger(__name__)
USER_SYNC_KEY = {
'feishu': 'celery_job:feishu_user_sync', # 同步飛書組織架構任務key
'ldap': 'celery_job:ldap_user_sync', # LDAP使用者同步任務KEY
}
class UserViewSet(AutoModelViewSet):
"""
使用者管理視圖
### 使用者管理權限
{'*': ('user_all', '使用者管理')},
{'get': ('user_list', '檢視使用者')},
{'post': ('user_create', '建立使用者')},
{'put': ('user_edit', '編輯使用者')},
{'patch': ('user_edit', '編輯使用者')},
{'delete': ('user_delete', '删除使用者')}
"""
perms_map = (
{'*': ('admin', '管理者')},
{'*': ('user_all', '使用者管理')},
{'get': ('user_list', '檢視使用者')},
{'post': ('user_create', '建立使用者')},
{'put': ('user_edit', '編輯使用者')},
{'patch': ('user_edit', '編輯使用者')},
{'delete': ('user_delete', '删除使用者')}
)
queryset = UserProfile.objects.exclude(Q(username='thirdparty') | Q(is_active=False))
serializer_class = UserProfileSerializers
serializer_list_class = UserProfileListSerializers
def get_serializer_class(self):
if self.action in ['detail', 'retrieve']:
return UserProfileDetailSerializers
return super().get_serializer_class()
def create(self, request, *args, **kwargs):
if self.queryset.filter(username=request.data['username']):
return ops_response({}, success=False, errorCode=40300, errorMessage='%s 賬号已存在!' % request.data['username'])
password = shortuuid.ShortUUID().random(length=8)
request.data['password'] = password
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
data = serializer.data
data['password'] = password
data['status'] = 'success'
data['code'] = 20000
return ops_response(data)
def perform_destroy(self, instance):
# 禁用使用者
instance.is_active = False
instance.save()
@action(methods=['POST'], url_path='password/reset', detail=False)
def password_reset(self, request):
"""
重置使用者密碼
### 重置使用者密碼
"""
data = self.request.data
user = self.queryset.get(pk=data['uid'])
if user.is_superuser:
return ops_response({}, success=False, errorCode=40300, errorMessage='禁止修改管理者密碼!')
user.set_password(data['password'])
user.save()
return ops_response('密碼已更新.')
@action(methods=['GET'], url_path='detail', detail=False)
def detail_info(self, request, pk=None, *args, **kwargs):
"""
使用者詳細清單
### 擷取使用者詳細資訊,使用者管理子產品
"""
return super().list(request, pk, *args, **kwargs)
@action(methods=['POST'], url_path='sync', detail=False)
def user_sync(self, request):
"""
使用者同步
### 傳遞參數:
sync: 1
"""
sync = request.data.get('sync', 0)
is_job_exist = cache.get(USER_SYNC_KEY[USER_AUTH_BACKEND])
if is_job_exist:
return ops_response({}, success=False, errorCode=40300, errorMessage='已經有組織架構同步任務在運作中... 請稍後重新整理頁面檢視')
if sync:
# 同步任務,後面再實作
taskid = None
# 限制隻能有一個同步任務在跑
cache.set(USER_SYNC_KEY[USER_AUTH_BACKEND], taskid, timeout=300)
return ops_response('正在同步組織架構資訊...')
添加路由
加使用者管理的路由加到devops_backend/urls.py
...
router.register('users', UserViewSet)
運作項目
通路http://localhost:9000/apidoc/,輸入users過濾可以看到使用者子產品接口