从0到1开辟主动化运维平台-用户模块治理_奇闻趣事网

从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

,如果想尝试解决,我们按如下步骤

  1. 注释settings.py->INSTAALLLED_APPS里的'django.contrib.admin';注释urls.py->urlpatterns里的path('admin/', admin.site.urls)。admin是django自带的管理后台,我们可以不用这个,直接删除也行......
  2. 删除cmdb里的迁移文件apps/cmdb/migrations/0.py
  3. 创建ucenter
python manage.py migrate ucenter
  1. 重新生成迁移文件
python manage.py makemigrations
  1. 查看迁移,确保所有文件已执行完成
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过滤可以看到用户模块接口

Copyright © 2016-2025 www.bnfh.cn 怪异网 版权所有 Power by