从0到1开辟主动化运维平台-用户模块治理
奇闻趣事 2023-05-04 17:40www.bnfh.cn奇闻趣事
从0到1开发自动化运维平台-用户模块管理
公共模型
新建文件mon/extends/models.py,将cmdb/models.py里定义的TimeAbstract、CommonParent移到这里
from django.db import models
class TimeAbstract(models.Model):
update_time = models.DateTimeField(
auto_no=True, null=True, blank=True, verbose_name='更新时间')
created_time = models.DateTimeField(
auto_no_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 mon.extends.models import TimeAbstract, CommonParent
# Create your models here.
def _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='排序标记')
ponent = 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):
"""
组织架构
"""
anization_type_choices = (
('pany', '公司'),
('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=anization_type_choices, default='department', verbose_name='类型')
extra_data = models.JSONField(default=_extra_data, verbose_name='其它数据', help_text=f'数据格式{_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='_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 pillo
添加模块到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
# 从mon里导入TimeAbstract, CommonParent
from mon.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 shomigrations
由于我们使用了自定义的用户模型,需要重新创建用户
(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 [{'_id': i.id, '_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 = ('passord', '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_passord(validated_data['passord'])
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 UserVieSet(AutoModelVieSet):
"""
用户管理视图
### 用户管理权限
{'': ('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, kargs):
if self.queryset.filter(username=request.data['username']):
return ops_response({}, suess=False, errorCode=40300, errorMessage='%s 账号已存在!' % request.data['username'])
passord = shortuuid.ShortUUID().random(length=8)
request.data['passord'] = passord
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
data = serializer.data
data['passord'] = passord
data['status'] = 'suess'
data['code'] = 20000
return ops_response(data)
def perform_destroy(self, instance):
# 禁用用户
instance.is_active = False
instance.save()
@action(methods=['POST'], url_path='passord/reset', detail=False)
def passord_reset(self, request):
"""
重置用户密码
### 重置用户密码
"""
data = self.request.data
user = self.queryset.get(pk=data['uid'])
if user.is_superuser:
return ops_response({}, suess=False, errorCode=40300, errorMessage='禁止修改管理员密码!')
user.set_passord(data['passord'])
user.save()
return ops_response('密码已更新.')
@action(methods=['GET'], url_path='detail', detail=False)
def detail_info(self, request, pk=None, args, kargs):
"""
用户详细列表
### 获取用户详细信息,用户管理模块
"""
return super().list(request, pk, args, kargs)
@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({}, suess=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', UserVieSet)
运行项目
访问http://localhost:9000/apidoc/,输入users过滤可以看到用户模块接口
上一篇:新爱获法网独家新媒体版权,深圳建首家皇马室
下一篇:半夜惊梦_半夜惊梦是那么真切