mirror of https://github.com/jumpserver/jumpserver
commit
ab8c57894e
|
@ -4,6 +4,9 @@
|
|||
##### 使用版本
|
||||
[请提供你使用的JumpServer版本 如 2.0.1 注: 1.4及以下版本不再提供支持]
|
||||
|
||||
##### 使用浏览器版本
|
||||
[请提供你使用的浏览器版本 如 Chrome 84.0.4147.105 ]
|
||||
|
||||
##### 问题复现步骤
|
||||
1. [步骤1]
|
||||
2. [步骤2]
|
||||
|
|
19
Dockerfile
19
Dockerfile
|
@ -9,18 +9,23 @@ RUN cd utils && bash -ixeu build.sh
|
|||
|
||||
|
||||
FROM registry.fit2cloud.com/public/python:v3
|
||||
ARG PIP_MIRROR=https://pypi.douban.com/simple
|
||||
ENV PIP_MIRROR=$PIP_MIRROR
|
||||
ARG MYSQL_MIRROR=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/
|
||||
ENV MYSQL_MIRROR=$MYSQL_MIRROR
|
||||
|
||||
WORKDIR /opt/jumpserver
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
|
||||
COPY ./requirements ./requirements
|
||||
RUN useradd jumpserver
|
||||
|
||||
RUN yum -y install epel-release && \
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=https://mirrors.tuna.tsinghua.edu.cn/mysql/yum/mysql57-community-el6/\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
|
||||
COPY . .
|
||||
echo -e "[mysql]\nname=mysql\nbaseurl=${MYSQL_MIRROR}\ngpgcheck=0\nenabled=1" > /etc/yum.repos.d/mysql.repo
|
||||
RUN yum -y install $(cat requirements/rpm_requirements.txt)
|
||||
RUN pip install --upgrade pip setuptools && pip install wheel && \
|
||||
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements/requirements.txt || pip install -r requirements/requirements.txt
|
||||
RUN pip install --upgrade pip setuptools wheel -i ${PIP_MIRROR} && \
|
||||
pip config set global.index-url ${PIP_MIRROR}
|
||||
RUN pip install -r requirements/requirements.txt || pip install -r requirements/requirements.txt
|
||||
|
||||
COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver
|
||||
RUN mkdir -p /root/.ssh/ && echo -e "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config
|
||||
|
||||
RUN echo > config.yml
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models
|
||||
from .. import serializers
|
||||
from ..hands import IsOrgAdminOrAppUser
|
||||
|
||||
__all__ = [
|
||||
'K8sAppViewSet',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppViewSet(OrgBulkModelViewSet):
|
||||
model = models.K8sApp
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.K8sAppSerializer
|
|
@ -23,6 +23,7 @@ REMOTE_APP_TYPE_CHROME_FIELDS = [
|
|||
REMOTE_APP_TYPE_MYSQL_WORKBENCH_FIELDS = [
|
||||
{'name': 'mysql_workbench_ip'},
|
||||
{'name': 'mysql_workbench_name'},
|
||||
{'name': 'mysql_workbench_port'},
|
||||
{'name': 'mysql_workbench_username'},
|
||||
{'name': 'mysql_workbench_password', 'write_only': True}
|
||||
]
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 2.2.13 on 2020-08-07 07:13
|
||||
|
||||
from django.db import migrations, models
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('applications', '0004_auto_20191218_1705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='K8sApp',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('type', models.CharField(choices=[('k8s', 'Kubernetes')], default='k8s', max_length=128, verbose_name='Type')),
|
||||
('cluster', models.CharField(max_length=1024, verbose_name='Cluster')),
|
||||
('comment', models.TextField(blank=True, default='', max_length=128, verbose_name='Comment')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'KubernetesApp',
|
||||
'ordering': ('name',),
|
||||
'unique_together': {('org_id', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,2 +1,3 @@
|
|||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.db import models
|
||||
from orgs.mixins.models import OrgModelMixin
|
||||
|
||||
|
||||
class K8sApp(OrgModelMixin, models.JMSModel):
|
||||
class TYPE(models.ChoiceSet):
|
||||
K8S = 'k8s', _('Kubernetes')
|
||||
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
type = models.CharField(
|
||||
default=TYPE.K8S, choices=TYPE.choices,
|
||||
max_length=128, verbose_name=_('Type')
|
||||
)
|
||||
cluster = models.CharField(max_length=1024, verbose_name=_('Cluster'))
|
||||
comment = models.TextField(
|
||||
max_length=128, default='', blank=True, verbose_name=_('Comment')
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name'), ]
|
||||
verbose_name = _('KubernetesApp')
|
||||
ordering = ('name', )
|
|
@ -1,2 +1,3 @@
|
|||
from .remote_app import *
|
||||
from .database_app import *
|
||||
from .k8s_app import *
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'K8sAppSerializer',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppSerializer(BulkOrgResourceModelSerializer):
|
||||
type_display = serializers.CharField(source='get_type_display', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = models.K8sApp
|
||||
fields = [
|
||||
'id', 'name', 'type', 'type_display', 'comment', 'created_by',
|
||||
'date_created', 'date_updated', 'cluster'
|
||||
]
|
||||
read_only_fields = [
|
||||
'id', 'created_by', 'date_created', 'date_updated',
|
||||
]
|
|
@ -12,6 +12,7 @@ app_name = 'applications'
|
|||
router = BulkRouter()
|
||||
router.register(r'remote-apps', api.RemoteAppViewSet, 'remote-app')
|
||||
router.register(r'database-apps', api.DatabaseAppViewSet, 'database-app')
|
||||
router.register(r'k8s-apps', api.K8sAppViewSet, 'k8s-app')
|
||||
|
||||
urlpatterns = [
|
||||
path('remote-apps/<uuid:pk>/connection-info/', api.RemoteAppConnectionInfoApi.as_view(), name='remote-app-connection-info'),
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
# Generated by Django 2.2.10 on 2020-07-23 04:32
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0052_auto_20200715_1535'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='adminuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='authbook',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='gateway',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='username',
|
||||
field=models.CharField(blank=True, db_index=True, max_length=128, validators=[django.core.validators.RegexValidator('^[0-9a-zA-Z_@\\-\\.]*$', 'Special char not allowed')], verbose_name='Username'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.13 on 2020-08-07 02:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0053_auto_20200723_1232'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='token',
|
||||
field=models.TextField(default='', verbose_name='Token'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='systemuser',
|
||||
name='protocol',
|
||||
field=models.CharField(choices=[('ssh', 'ssh'), ('rdp', 'rdp'), ('telnet', 'telnet'), ('vnc', 'vnc'), ('mysql', 'mysql'), ('k8s', 'k8s')], default='ssh', max_length=16, verbose_name='Protocol'),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,23 @@
|
|||
# Generated by Django 2.2.13 on 2020-08-11 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0054_auto_20200807_1032'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='home',
|
||||
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='Home'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='systemuser',
|
||||
name='system_groups',
|
||||
field=models.CharField(blank=True, default='', max_length=4096, verbose_name='System groups'),
|
||||
),
|
||||
]
|
|
@ -230,7 +230,7 @@ class AuthMixin:
|
|||
class BaseUser(OrgModelMixin, AuthMixin, ConnectivityMixin):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, verbose_name=_('Name'))
|
||||
username = models.CharField(max_length=32, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
username = models.CharField(max_length=128, blank=True, verbose_name=_('Username'), validators=[alphanumeric], db_index=True)
|
||||
password = fields.EncryptCharField(max_length=256, blank=True, null=True, verbose_name=_('Password'))
|
||||
private_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH private key'))
|
||||
public_key = fields.EncryptTextField(blank=True, null=True, verbose_name=_('SSH public key'))
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.core.validators import MinValueValidator, MaxValueValidator
|
||||
|
||||
from common.utils import signer
|
||||
from common.fields.model import JsonListCharField
|
||||
from .base import BaseUser
|
||||
from .asset import Asset
|
||||
|
||||
|
@ -91,12 +92,14 @@ class SystemUser(BaseUser):
|
|||
PROTOCOL_TELNET = 'telnet'
|
||||
PROTOCOL_VNC = 'vnc'
|
||||
PROTOCOL_MYSQL = 'mysql'
|
||||
PROTOCOL_K8S = 'k8s'
|
||||
PROTOCOL_CHOICES = (
|
||||
(PROTOCOL_SSH, 'ssh'),
|
||||
(PROTOCOL_RDP, 'rdp'),
|
||||
(PROTOCOL_TELNET, 'telnet'),
|
||||
(PROTOCOL_VNC, 'vnc'),
|
||||
(PROTOCOL_MYSQL, 'mysql'),
|
||||
(PROTOCOL_K8S, 'k8s'),
|
||||
)
|
||||
|
||||
LOGIN_AUTO = 'auto'
|
||||
|
@ -118,6 +121,9 @@ class SystemUser(BaseUser):
|
|||
login_mode = models.CharField(choices=LOGIN_MODE_CHOICES, default=LOGIN_AUTO, max_length=10, verbose_name=_('Login mode'))
|
||||
cmd_filters = models.ManyToManyField('CommandFilter', related_name='system_users', verbose_name=_("Command filter"), blank=True)
|
||||
sftp_root = models.CharField(default='tmp', max_length=128, verbose_name=_("SFTP Root"))
|
||||
token = models.TextField(default='', verbose_name=_('Token'))
|
||||
home = models.CharField(max_length=4096, default='', verbose_name=_('Home'), blank=True)
|
||||
system_groups = models.CharField(default='', max_length=4096, verbose_name=_('System groups'), blank=True)
|
||||
_prefer = 'system_user'
|
||||
|
||||
def __str__(self):
|
||||
|
|
|
@ -33,13 +33,15 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'cmd_filters', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'assets_amount', 'date_created', 'created_by'
|
||||
'auto_generate_key', 'sftp_root', 'token',
|
||||
'assets_amount', 'date_created', 'created_by',
|
||||
'home', 'system_groups'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'password': {"write_only": True},
|
||||
'public_key': {"write_only": True},
|
||||
'private_key': {"write_only": True},
|
||||
'token': {"write_only": True},
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
'assets_amount': {'label': _('Asset')},
|
||||
'login_mode_display': {'label': _('Login mode display')},
|
||||
|
@ -143,13 +145,14 @@ class SystemUserSerializer(AuthSerializerMixin, BulkOrgResourceModelSerializer):
|
|||
|
||||
|
||||
class SystemUserListSerializer(SystemUserSerializer):
|
||||
|
||||
class Meta(SystemUserSerializer.Meta):
|
||||
fields = [
|
||||
'id', 'name', 'username', 'protocol',
|
||||
'login_mode', 'login_mode_display',
|
||||
'priority', "username_same_with_user",
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
"assets_amount",
|
||||
"assets_amount", 'home', 'system_groups',
|
||||
'auto_generate_key',
|
||||
'sftp_root',
|
||||
]
|
||||
|
@ -169,7 +172,7 @@ class SystemUserWithAuthInfoSerializer(SystemUserSerializer):
|
|||
'login_mode', 'login_mode_display',
|
||||
'priority', 'username_same_with_user',
|
||||
'auto_push', 'sudo', 'shell', 'comment',
|
||||
'auto_generate_key', 'sftp_root',
|
||||
'auto_generate_key', 'sftp_root', 'token'
|
||||
]
|
||||
extra_kwargs = {
|
||||
'nodes_amount': {'label': _('Node')},
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
from itertools import groupby
|
||||
from celery import shared_task
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Empty
|
||||
|
||||
from common.utils import encrypt_password, get_logger
|
||||
from orgs.utils import tmp_to_org, org_aware_func
|
||||
from orgs.utils import org_aware_func
|
||||
from . import const
|
||||
from .utils import clean_ansible_task_hosts, group_asset_by_platform
|
||||
|
||||
|
@ -17,20 +18,42 @@ __all__ = [
|
|||
]
|
||||
|
||||
|
||||
def _split_by_comma(raw: str):
|
||||
try:
|
||||
return [i.strip() for i in raw.split(',')]
|
||||
except AttributeError:
|
||||
return []
|
||||
|
||||
|
||||
def _dump_args(args: dict):
|
||||
return ' '.join([f'{k}={v}' for k, v in args.items() if v is not Empty])
|
||||
|
||||
|
||||
def get_push_unixlike_system_user_tasks(system_user, username=None):
|
||||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
public_key = system_user.public_key
|
||||
|
||||
groups = _split_by_comma(system_user.system_groups)
|
||||
|
||||
if groups:
|
||||
groups = '"%s"' % ','.join(groups)
|
||||
|
||||
add_user_args = {
|
||||
'name': username,
|
||||
'shell': system_user.shell or Empty,
|
||||
'state': 'present',
|
||||
'home': system_user.home or Empty,
|
||||
'groups': groups or Empty
|
||||
}
|
||||
|
||||
tasks = [
|
||||
{
|
||||
'name': 'Add user {}'.format(username),
|
||||
'action': {
|
||||
'module': 'user',
|
||||
'args': 'name={} shell={} state=present'.format(
|
||||
username, system_user.shell or '/bin/bash',
|
||||
),
|
||||
'args': _dump_args(add_user_args),
|
||||
}
|
||||
},
|
||||
{
|
||||
|
@ -102,6 +125,11 @@ def get_push_windows_system_user_tasks(system_user, username=None):
|
|||
if username is None:
|
||||
username = system_user.username
|
||||
password = system_user.password
|
||||
groups = {'Users', 'Remote Desktop Users'}
|
||||
if system_user.system_groups:
|
||||
groups.update(_split_by_comma(system_user.system_groups))
|
||||
groups = ','.join(groups)
|
||||
|
||||
tasks = []
|
||||
if not password:
|
||||
return tasks
|
||||
|
@ -116,9 +144,9 @@ def get_push_windows_system_user_tasks(system_user, username=None):
|
|||
'update_password=always '
|
||||
'password_expired=no '
|
||||
'password_never_expires=yes '
|
||||
'groups="Users,Remote Desktop Users" '
|
||||
'groups="{}" '
|
||||
'groups_action=add '
|
||||
''.format(username, username, password),
|
||||
''.format(username, username, password, groups),
|
||||
}
|
||||
}
|
||||
tasks.append(task)
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# coding:utf-8
|
||||
from django.urls import path, re_path
|
||||
from rest_framework_nested import routers
|
||||
# from rest_framework.routers import DefaultRouter
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
from common import api as capi
|
||||
|
|
|
@ -43,7 +43,7 @@ class UserLoginLogViewSet(ListModelMixin, CommonGenericViewSet):
|
|||
|
||||
@staticmethod
|
||||
def get_org_members():
|
||||
users = current_org.get_org_members().values_list('username', flat=True)
|
||||
users = current_org.get_members().values_list('username', flat=True)
|
||||
return users
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -79,7 +79,7 @@ class PasswordChangeLogViewSet(ListModelMixin, CommonGenericViewSet):
|
|||
ordering = ['-datetime']
|
||||
|
||||
def get_queryset(self):
|
||||
users = current_org.get_org_members()
|
||||
users = current_org.get_members()
|
||||
queryset = super().get_queryset().filter(
|
||||
user__in=[user.__str__() for user in users]
|
||||
)
|
||||
|
|
|
@ -20,7 +20,7 @@ class CurrentOrgMembersFilter(filters.BaseFilterBackend):
|
|||
]
|
||||
|
||||
def _get_user_list(self):
|
||||
users = current_org.get_org_members(exclude=('Auditor',))
|
||||
users = current_org.get_members(exclude=('Auditor',))
|
||||
return users
|
||||
|
||||
def filter_queryset(self, request, queryset, view):
|
||||
|
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.13 on 2020-08-11 03:22
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('audits', '0009_auto_20200624_1654'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='operatelog',
|
||||
name='datetime',
|
||||
field=models.DateTimeField(auto_now=True, db_index=True, verbose_name='Datetime'),
|
||||
),
|
||||
]
|
|
@ -58,7 +58,7 @@ class OperateLog(OrgModelMixin):
|
|||
resource_type = models.CharField(max_length=64, verbose_name=_("Resource Type"))
|
||||
resource = models.CharField(max_length=128, verbose_name=_("Resource"))
|
||||
remote_addr = models.CharField(max_length=128, verbose_name=_("Remote addr"), blank=True, null=True)
|
||||
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'))
|
||||
datetime = models.DateTimeField(auto_now=True, verbose_name=_('Datetime'), db_index=True)
|
||||
|
||||
def __str__(self):
|
||||
return "<{}> {} <{}>".format(self.user, self.action, self.resource)
|
||||
|
@ -124,7 +124,7 @@ class UserLoginLog(models.Model):
|
|||
Q(username__contains=keyword)
|
||||
)
|
||||
if not current_org.is_root():
|
||||
username_list = current_org.get_org_members().values_list('username', flat=True)
|
||||
username_list = current_org.get_members().values_list('username', flat=True)
|
||||
login_logs = login_logs.filter(username__in=username_list)
|
||||
return login_logs
|
||||
|
||||
|
|
|
@ -6,3 +6,4 @@ from .token import *
|
|||
from .mfa import *
|
||||
from .access_key import *
|
||||
from .login_confirm import *
|
||||
from .sso import *
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
from uuid import UUID
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.contrib.auth import login
|
||||
from django.conf import settings
|
||||
from django.http.response import HttpResponseRedirect
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.request import Request
|
||||
|
||||
from common.utils.timezone import utcnow
|
||||
from common.const.http import POST, GET
|
||||
from common.drf.api import JmsGenericViewSet
|
||||
from common.drf.serializers import EmptySerializer
|
||||
from common.permissions import IsSuperUser
|
||||
from common.utils import reverse
|
||||
from users.models import User
|
||||
from ..serializers import SSOTokenSerializer
|
||||
from ..models import SSOToken
|
||||
from ..filters import AuthKeyQueryDeclaration
|
||||
from ..mixins import AuthMixin
|
||||
from ..errors import SSOAuthClosed
|
||||
|
||||
NEXT_URL = 'next'
|
||||
AUTH_KEY = 'authkey'
|
||||
|
||||
|
||||
class SSOViewSet(AuthMixin, JmsGenericViewSet):
|
||||
queryset = SSOToken.objects.all()
|
||||
serializer_classes = {
|
||||
'login_url': SSOTokenSerializer,
|
||||
'login': EmptySerializer
|
||||
}
|
||||
|
||||
@action(methods=[POST], detail=False, permission_classes=[IsSuperUser], url_path='login-url')
|
||||
def login_url(self, request, *args, **kwargs):
|
||||
if not settings.AUTH_SSO:
|
||||
raise SSOAuthClosed()
|
||||
|
||||
serializer = self.get_serializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
|
||||
username = serializer.validated_data['username']
|
||||
user = User.objects.get(username=username)
|
||||
next_url = serializer.validated_data.get(NEXT_URL)
|
||||
|
||||
operator = request.user.username
|
||||
# TODO `created_by` 和 `created_by` 可以通过 `ThreadLocal` 统一处理
|
||||
token = SSOToken.objects.create(user=user, created_by=operator, updated_by=operator)
|
||||
query = {
|
||||
AUTH_KEY: token.authkey,
|
||||
NEXT_URL: next_url or ''
|
||||
}
|
||||
login_url = '%s?%s' % (reverse('api-auth:sso-login', external=True), urlencode(query))
|
||||
return Response(data={'login_url': login_url})
|
||||
|
||||
@action(methods=[GET], detail=False, filter_backends=[AuthKeyQueryDeclaration], permission_classes=[])
|
||||
def login(self, request: Request, *args, **kwargs):
|
||||
"""
|
||||
此接口违反了 `Restful` 的规范
|
||||
`GET` 应该是安全的方法,但此接口是不安全的
|
||||
"""
|
||||
authkey = request.query_params.get(AUTH_KEY)
|
||||
next_url = request.query_params.get(NEXT_URL)
|
||||
if not next_url or not next_url.startswith('/'):
|
||||
next_url = reverse('index')
|
||||
|
||||
try:
|
||||
authkey = UUID(authkey)
|
||||
token = SSOToken.objects.get(authkey=authkey, expired=False)
|
||||
# 先过期,只能访问这一次
|
||||
token.expired = True
|
||||
token.save()
|
||||
except (ValueError, SSOToken.DoesNotExist):
|
||||
self.send_auth_signal(success=False, reason='authkey_invalid')
|
||||
return HttpResponseRedirect(reverse('authentication:login'))
|
||||
|
||||
# 判断是否过期
|
||||
if (utcnow().timestamp() - token.date_created.timestamp()) > settings.AUTH_SSO_AUTHKEY_TTL:
|
||||
self.send_auth_signal(success=False, reason='authkey_timeout')
|
||||
return HttpResponseRedirect(reverse('authentication:login'))
|
||||
|
||||
user = token.user
|
||||
login(self.request, user, 'authentication.backends.api.SSOAuthentication')
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
return HttpResponseRedirect(next_url)
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.shortcuts import redirect
|
||||
from rest_framework.permissions import AllowAny
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.generics import CreateAPIView
|
||||
|
@ -40,3 +40,5 @@ class TokenCreateApi(AuthMixin, CreateAPIView):
|
|||
return Response(e.as_data(), status=400)
|
||||
except errors.NeedMoreInfoError as e:
|
||||
return Response(e.as_data(), status=200)
|
||||
except errors.PasswdTooSimple as e:
|
||||
return redirect(e.url)
|
||||
|
|
|
@ -5,14 +5,13 @@ import uuid
|
|||
import time
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.six import text_type
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.contrib.auth.backends import ModelBackend
|
||||
from rest_framework import HTTP_HEADER_ENCODING
|
||||
from rest_framework import authentication, exceptions
|
||||
from common.auth import signature
|
||||
from rest_framework.authentication import CSRFCheck
|
||||
|
||||
from common.utils import get_object_or_none, make_signature, http_to_unixtime
|
||||
from ..models import AccessKey, PrivateToken
|
||||
|
@ -197,3 +196,10 @@ class SignatureAuthentication(signature.SignatureAuthentication):
|
|||
return user, secret
|
||||
except AccessKey.DoesNotExist:
|
||||
return None, None
|
||||
|
||||
|
||||
class SSOAuthentication(ModelBackend):
|
||||
"""
|
||||
什么也不做呀😺
|
||||
"""
|
||||
pass
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
import traceback
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
|
||||
from django.conf import settings
|
||||
|
||||
from pyrad.packet import AccessRequest
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
@ -27,11 +27,22 @@ class CreateUserMixin:
|
|||
user.save()
|
||||
return user
|
||||
|
||||
def _perform_radius_auth(self, client, packet):
|
||||
# TODO: 等待官方库修复这个BUG
|
||||
try:
|
||||
return super()._perform_radius_auth(client, packet)
|
||||
except UnicodeError as e:
|
||||
import sys
|
||||
tb = ''.join(traceback.format_exception(*sys.exc_info(), limit=2, chain=False))
|
||||
if tb.find("cl.decode") != -1:
|
||||
return [], False, False
|
||||
return None
|
||||
|
||||
def authenticate(self, *args, **kwargs):
|
||||
# 校验用户时,会传入public_key参数,父类authentication中不接受public_key参数,所以要pop掉
|
||||
# TODO:需要优化各backend的authenticate方法,django进行调用前会检测各authenticate的参数
|
||||
kwargs.pop('public_key', None)
|
||||
return super().authenticate(*args, *kwargs)
|
||||
return super().authenticate(*args, **kwargs)
|
||||
|
||||
|
||||
class RadiusBackend(CreateUserMixin, RADIUSBackend):
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
RSA_PRIVATE_KEY = 'rsa_private_key'
|
||||
RSA_PUBLIC_KEY = 'rsa_public_key'
|
|
@ -4,6 +4,7 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from django.urls import reverse
|
||||
from django.conf import settings
|
||||
|
||||
from common.exceptions import JMSException
|
||||
from .signals import post_auth_failed
|
||||
from users.utils import (
|
||||
increase_login_failed_count, get_login_failed_count
|
||||
|
@ -205,3 +206,17 @@ class LoginConfirmOtherError(LoginConfirmBaseError):
|
|||
def __init__(self, ticket_id, status):
|
||||
msg = login_confirm_error_msg.format(status)
|
||||
super().__init__(ticket_id=ticket_id, msg=msg)
|
||||
|
||||
|
||||
class SSOAuthClosed(JMSException):
|
||||
default_code = 'sso_auth_closed'
|
||||
default_detail = _('SSO auth closed')
|
||||
|
||||
|
||||
class PasswdTooSimple(JMSException):
|
||||
default_code = 'passwd_too_simple'
|
||||
default_detail = _('Your password is too simple, please change it for security')
|
||||
|
||||
def __init__(self, url, *args, **kwargs):
|
||||
super(PasswdTooSimple, self).__init__(*args, **kwargs)
|
||||
self.url = url
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
from rest_framework import filters
|
||||
from rest_framework.compat import coreapi, coreschema
|
||||
|
||||
|
||||
class AuthKeyQueryDeclaration(filters.BaseFilterBackend):
|
||||
def get_schema_fields(self, view):
|
||||
return [
|
||||
coreapi.Field(
|
||||
name='authkey', location='query', required=True, type='string',
|
||||
schema=coreschema.String(
|
||||
title='authkey',
|
||||
description='authkey'
|
||||
)
|
||||
)
|
||||
]
|
|
@ -2,6 +2,7 @@
|
|||
#
|
||||
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from captcha.fields import CaptchaField
|
||||
|
||||
|
@ -21,9 +22,24 @@ class UserLoginForm(forms.Form):
|
|||
)
|
||||
|
||||
|
||||
class UserLoginCaptchaForm(UserLoginForm):
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
|
||||
|
||||
|
||||
class CaptchaMixin(forms.Form):
|
||||
captcha = CaptchaField()
|
||||
|
||||
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
|
||||
class ChallengeMixin(forms.Form):
|
||||
challenge = forms.CharField(label=_('MFA code'), max_length=6,
|
||||
required=False)
|
||||
|
||||
|
||||
def get_user_login_form_cls(*, captcha=False):
|
||||
bases = []
|
||||
if settings.SECURITY_LOGIN_CAPTCHA_ENABLED and captcha:
|
||||
bases.append(CaptchaMixin)
|
||||
if settings.SECURITY_LOGIN_CHALLENGE_ENABLED:
|
||||
bases.append(ChallengeMixin)
|
||||
bases.append(UserLoginForm)
|
||||
return type('UserLoginForm', tuple(bases), {})
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 2.2.10 on 2020-07-31 08:36
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('authentication', '0003_loginconfirmsetting'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SSOToken',
|
||||
fields=[
|
||||
('created_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Created by')),
|
||||
('updated_by', models.CharField(blank=True, max_length=32, null=True, verbose_name='Updated by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('authkey', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='Token')),
|
||||
('expired', models.BooleanField(default=False, verbose_name='Expired')),
|
||||
('user', models.ForeignKey(db_constraint=False, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -1,7 +1,12 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from urllib.parse import urlencode
|
||||
from functools import partial
|
||||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import authenticate
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger
|
||||
from users.models import User
|
||||
|
@ -9,8 +14,9 @@ from users.utils import (
|
|||
is_block_login, clean_failed_count
|
||||
)
|
||||
from . import errors
|
||||
from .utils import check_user_valid
|
||||
from .utils import rsa_decrypt
|
||||
from .signals import post_auth_success, post_auth_failed
|
||||
from .const import RSA_PRIVATE_KEY
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
@ -50,25 +56,54 @@ class AuthMixin:
|
|||
logger.warn('Ip was blocked' + ': ' + username + ':' + ip)
|
||||
raise errors.BlockLoginError(username=username, ip=ip)
|
||||
|
||||
def check_user_auth(self):
|
||||
def decrypt_passwd(self, raw_passwd):
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
if rsa_private_key is not None:
|
||||
try:
|
||||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error(f'Decrypt password faild: password[{raw_passwd}] rsa_private_key[{rsa_private_key}]')
|
||||
return None
|
||||
return raw_passwd
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
username = request.data.get('username', '')
|
||||
password = request.data.get('password', '')
|
||||
public_key = request.data.get('public_key', '')
|
||||
data = request.data
|
||||
else:
|
||||
username = request.POST.get('username', '')
|
||||
password = request.POST.get('password', '')
|
||||
public_key = request.POST.get('public_key', '')
|
||||
user, error = check_user_valid(
|
||||
request=request, username=username, password=password, public_key=public_key
|
||||
)
|
||||
data = request.POST
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
challenge = data.get('challenge', '')
|
||||
public_key = data.get('public_key', '')
|
||||
ip = self.get_request_ip()
|
||||
|
||||
CredentialError = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
||||
|
||||
if decrypt_passwd:
|
||||
password = self.decrypt_passwd(password)
|
||||
if not password:
|
||||
raise CredentialError(error=errors.reason_password_decrypt_failed)
|
||||
|
||||
user = authenticate(request,
|
||||
username=username,
|
||||
password=password + challenge.strip(),
|
||||
public_key=public_key)
|
||||
|
||||
if not user:
|
||||
raise errors.CredentialError(
|
||||
username=username, error=error, ip=ip, request=request
|
||||
)
|
||||
raise CredentialError(error=errors.reason_password_failed)
|
||||
elif user.is_expired:
|
||||
raise CredentialError(error=errors.reason_user_inactive)
|
||||
elif not user.is_active:
|
||||
raise CredentialError(error=errors.reason_user_inactive)
|
||||
elif user.password_has_expired:
|
||||
raise CredentialError(error=errors.reason_password_expired)
|
||||
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
|
||||
clean_failed_count(username, ip)
|
||||
request.session['auth_password'] = 1
|
||||
request.session['user_id'] = str(user.id)
|
||||
|
@ -76,14 +111,30 @@ class AuthMixin:
|
|||
request.session['auth_backend'] = auth_backend
|
||||
return user
|
||||
|
||||
def check_user_auth_if_need(self):
|
||||
@classmethod
|
||||
def _check_passwd_is_too_simple(cls, user, password):
|
||||
if user.is_superuser and password == 'admin':
|
||||
reset_passwd_url = reverse('authentication:reset-password')
|
||||
query_str = urlencode({
|
||||
'token': user.generate_reset_token()
|
||||
})
|
||||
reset_passwd_url = f'{reset_passwd_url}?{query_str}'
|
||||
|
||||
flash_page_url = reverse('authentication:passwd-too-simple-flash-msg')
|
||||
query_str = urlencode({
|
||||
'redirect_url': reset_passwd_url
|
||||
})
|
||||
|
||||
raise errors.PasswdTooSimple(f'{flash_page_url}?{query_str}')
|
||||
|
||||
def check_user_auth_if_need(self, decrypt_passwd=False):
|
||||
request = self.request
|
||||
if request.session.get('auth_password') and \
|
||||
request.session.get('user_id'):
|
||||
user = self.get_user_from_session()
|
||||
if user:
|
||||
return user
|
||||
return self.check_user_auth()
|
||||
return self.check_user_auth(decrypt_passwd=decrypt_passwd)
|
||||
|
||||
def check_user_mfa_if_need(self, user):
|
||||
if self.request.session.get('auth_mfa'):
|
||||
|
@ -117,7 +168,7 @@ class AuthMixin:
|
|||
|
||||
def get_ticket_or_create(self, confirm_setting):
|
||||
ticket = self.get_ticket()
|
||||
if not ticket or ticket.status == ticket.STATUS_CLOSED:
|
||||
if not ticket or ticket.status == ticket.STATUS.CLOSED:
|
||||
ticket = confirm_setting.create_confirm_ticket(self.request)
|
||||
self.request.session['auth_ticket_id'] = str(ticket.id)
|
||||
return ticket
|
||||
|
@ -126,12 +177,12 @@ class AuthMixin:
|
|||
ticket = self.get_ticket()
|
||||
if not ticket:
|
||||
raise errors.LoginConfirmOtherError('', "Not found")
|
||||
if ticket.status == ticket.STATUS_OPEN:
|
||||
if ticket.status == ticket.STATUS.OPEN:
|
||||
raise errors.LoginConfirmWaitError(ticket.id)
|
||||
elif ticket.action == ticket.ACTION_APPROVE:
|
||||
elif ticket.action == ticket.ACTION.APPROVE:
|
||||
self.request.session["auth_confirm"] = "1"
|
||||
return
|
||||
elif ticket.action == ticket.ACTION_REJECT:
|
||||
elif ticket.action == ticket.ACTION.REJECT:
|
||||
raise errors.LoginConfirmOtherError(
|
||||
ticket.id, ticket.get_action_display()
|
||||
)
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import uuid
|
||||
from django.db import models
|
||||
from functools import partial
|
||||
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext_lazy as _, ugettext as __
|
||||
from rest_framework.authtoken.models import Token
|
||||
from django.conf import settings
|
||||
from django.utils.crypto import get_random_string
|
||||
|
||||
from common.db import models
|
||||
from common.mixins.models import CommonModelMixin
|
||||
from common.utils import get_object_or_none, get_request_ip, get_ip_city
|
||||
|
||||
|
@ -68,7 +71,7 @@ class LoginConfirmSetting(CommonModelMixin):
|
|||
reviewer = self.reviewers.all()
|
||||
ticket = Ticket.objects.create(
|
||||
user=self.user, title=title, body=body,
|
||||
type=Ticket.TYPE_LOGIN_CONFIRM,
|
||||
type=Ticket.TYPE.LOGIN_CONFIRM,
|
||||
)
|
||||
ticket.assignees.set(reviewer)
|
||||
return ticket
|
||||
|
@ -76,3 +79,12 @@ class LoginConfirmSetting(CommonModelMixin):
|
|||
def __str__(self):
|
||||
return '{} confirm'.format(self.user.username)
|
||||
|
||||
|
||||
class SSOToken(models.JMSBaseModel):
|
||||
"""
|
||||
类似腾讯企业邮的 [单点登录](https://exmail.qq.com/qy_mng_logic/doc#10036)
|
||||
出于安全考虑,这里的 `token` 使用一次随即过期。但我们保留每一个生成过的 `token`。
|
||||
"""
|
||||
authkey = models.UUIDField(primary_key=True, default=uuid.uuid4, verbose_name=_('Token'))
|
||||
expired = models.BooleanField(default=False, verbose_name=_('Expired'))
|
||||
user = models.ForeignKey('users.User', on_delete=models.PROTECT, verbose_name=_('User'), db_constraint=False)
|
||||
|
|
|
@ -5,12 +5,12 @@ from rest_framework import serializers
|
|||
from common.utils import get_object_or_none
|
||||
from users.models import User
|
||||
from users.serializers import UserProfileSerializer
|
||||
from .models import AccessKey, LoginConfirmSetting
|
||||
from .models import AccessKey, LoginConfirmSetting, SSOToken
|
||||
|
||||
|
||||
__all__ = [
|
||||
'AccessKeySerializer', 'OtpVerifySerializer', 'BearerTokenSerializer',
|
||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer',
|
||||
'MFAChallengeSerializer', 'LoginConfirmSettingSerializer', 'SSOTokenSerializer',
|
||||
]
|
||||
|
||||
|
||||
|
@ -76,3 +76,9 @@ class LoginConfirmSettingSerializer(serializers.ModelSerializer):
|
|||
model = LoginConfirmSetting
|
||||
fields = ['id', 'user', 'reviewers', 'date_created', 'date_updated']
|
||||
read_only_fields = ['date_created', 'date_updated']
|
||||
|
||||
|
||||
class SSOTokenSerializer(serializers.Serializer):
|
||||
username = serializers.CharField(write_only=True)
|
||||
login_url = serializers.CharField(read_only=True)
|
||||
next = serializers.CharField(write_only=True, allow_blank=True, required=False, allow_null=True)
|
||||
|
|
|
@ -33,6 +33,16 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div class="form-group">
|
||||
<input type="challenge" class="form-control" id="challenge" name="{{ form.challenge.html_name }}" placeholder="{% trans 'MFA code' %}" >
|
||||
{% if form.errors.challenge %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.challenge.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
{{ form.captcha }}
|
||||
</div>
|
||||
|
|
|
@ -67,22 +67,30 @@
|
|||
</div>
|
||||
<div class="box-3">
|
||||
<div style="background-color: white">
|
||||
<div style="margin-top: 30px;padding-top: 40px;padding-left: 20px;padding-right: 20px;height: 80px">
|
||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div style="margin-top: 20px;padding-top: 30px;padding-left: 20px;padding-right: 20px;height: 60px">
|
||||
{% else %}
|
||||
<div style="margin-top: 20px;padding-top: 40px;padding-left: 20px;padding-right: 20px;height: 80px">
|
||||
{% endif %}
|
||||
<span style="font-size: 21px;font-weight:400;color: #151515;letter-spacing: 0;">{{ JMS_TITLE }}</span>
|
||||
</div>
|
||||
<div style="font-size: 12px;color: #999999;letter-spacing: 0;line-height: 18px;margin-top: 18px">
|
||||
{% trans 'Welcome back, please enter username and password to login' %}
|
||||
</div>
|
||||
<div style="margin-bottom: 10px">
|
||||
<div style="margin-bottom: 0px">
|
||||
<div>
|
||||
<div class="col-md-1"></div>
|
||||
<div class="contact-form col-md-10" style="margin-top: 20px;height: 35px">
|
||||
<div class="contact-form col-md-10" style="margin-top: 0px;height: 35px">
|
||||
<form id="contact-form" action="" method="post" role="form" novalidate="novalidate">
|
||||
{% csrf_token %}
|
||||
{% if form.non_field_errors %}
|
||||
<div style="height: 70px;color: red;line-height: 17px;">
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div style="height: 50px;color: red;line-height: 17px;">
|
||||
{% else %}
|
||||
<div style="height: 70px;color: red;line-height: 17px;">
|
||||
{% endif %}
|
||||
<p class="red-fonts">{{ form.non_field_errors.as_text }}</p>
|
||||
</div>
|
||||
{% elif form.errors.captcha %}
|
||||
<p class="red-fonts">{% trans 'Captcha invalid' %}</p>
|
||||
{% else %}
|
||||
|
@ -105,6 +113,16 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if form.challenge %}
|
||||
<div class="form-group">
|
||||
<input type="challenge" class="form-control" id="challenge" name="{{ form.challenge.html_name }}" placeholder="{% trans 'MFA code' %}" >
|
||||
{% if form.errors.challenge %}
|
||||
<div class="help-block field-error">
|
||||
<p class="red-fonts">{{ form.errors.challenge.as_text }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="form-group" style="height: 50px;margin-bottom: 0;font-size: 13px">
|
||||
{{ form.captcha }}
|
||||
</div>
|
||||
|
|
|
@ -8,6 +8,7 @@ from .. import api
|
|||
app_name = 'authentication'
|
||||
router = DefaultRouter()
|
||||
router.register('access-keys', api.AccessKeyViewSet, 'access-key')
|
||||
router.register('sso', api.SSOViewSet, 'sso')
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
@ -21,6 +21,7 @@ urlpatterns = [
|
|||
path('password/forgot/sendmail-success/', users_view.UserForgotPasswordSendmailSuccessView.as_view(),
|
||||
name='forgot-password-sendmail-success'),
|
||||
path('password/reset/', users_view.UserResetPasswordView.as_view(), name='reset-password'),
|
||||
path('password/too-simple-flash-msg/', views.FlashPasswdTooSimpleMsgView.as_view(), name='passwd-too-simple-flash-msg'),
|
||||
path('password/reset/success/', users_view.UserResetPasswordSuccessView.as_view(), name='reset-password-success'),
|
||||
path('password/verify/', users_view.UserVerifyPasswordView.as_view(), name='user-verify-password'),
|
||||
|
||||
|
|
|
@ -4,12 +4,9 @@ import base64
|
|||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_v1_5
|
||||
from Crypto import Random
|
||||
from django.contrib.auth import authenticate
|
||||
|
||||
from common.utils import get_logger
|
||||
|
||||
from . import errors
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
||||
|
@ -41,33 +38,3 @@ def rsa_decrypt(cipher_text, rsa_private_key=None):
|
|||
cipher = PKCS1_v1_5.new(key)
|
||||
message = cipher.decrypt(base64.b64decode(cipher_text.encode()), 'error').decode()
|
||||
return message
|
||||
|
||||
|
||||
def check_user_valid(**kwargs):
|
||||
password = kwargs.pop('password', None)
|
||||
public_key = kwargs.pop('public_key', None)
|
||||
username = kwargs.pop('username', None)
|
||||
request = kwargs.get('request')
|
||||
|
||||
# 获取解密密钥,对密码进行解密
|
||||
rsa_private_key = request.session.get('rsa_private_key')
|
||||
if rsa_private_key is not None:
|
||||
try:
|
||||
password = rsa_decrypt(password, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error('Need decrypt password => {}'.format(password))
|
||||
return None, errors.reason_password_decrypt_failed
|
||||
|
||||
user = authenticate(request, username=username,
|
||||
password=password, public_key=public_key)
|
||||
if not user:
|
||||
return None, errors.reason_password_failed
|
||||
elif user.is_expired:
|
||||
return None, errors.reason_user_inactive
|
||||
elif not user.is_active:
|
||||
return None, errors.reason_user_inactive
|
||||
elif user.password_has_expired:
|
||||
return None, errors.reason_password_expired
|
||||
|
||||
return user, ''
|
||||
|
|
|
@ -22,12 +22,15 @@ from common.utils import get_request_ip, get_object_or_none
|
|||
from users.utils import (
|
||||
redirect_user_first_login_or_index
|
||||
)
|
||||
from .. import forms, mixins, errors, utils
|
||||
from ..const import RSA_PRIVATE_KEY, RSA_PUBLIC_KEY
|
||||
from .. import mixins, errors, utils
|
||||
from ..forms import get_user_login_form_cls
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserLoginView', 'UserLogoutView',
|
||||
'UserLoginGuardView', 'UserLoginWaitConfirmView',
|
||||
'FlashPasswdTooSimpleMsgView',
|
||||
]
|
||||
|
||||
|
||||
|
@ -35,8 +38,6 @@ __all__ = [
|
|||
@method_decorator(csrf_protect, name='dispatch')
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class UserLoginView(mixins.AuthMixin, FormView):
|
||||
form_class = forms.UserLoginForm
|
||||
form_class_captcha = forms.UserLoginCaptchaForm
|
||||
key_prefix_captcha = "_LOGIN_INVALID_{}"
|
||||
redirect_field_name = 'next'
|
||||
|
||||
|
@ -82,15 +83,19 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
if not self.request.session.test_cookie_worked():
|
||||
return HttpResponse(_("Please enable cookies and try again."))
|
||||
try:
|
||||
self.check_user_auth()
|
||||
self.check_user_auth(decrypt_passwd=True)
|
||||
except errors.AuthFailedError as e:
|
||||
form.add_error(None, e.msg)
|
||||
ip = self.get_request_ip()
|
||||
cache.set(self.key_prefix_captcha.format(ip), 1, 3600)
|
||||
new_form = self.form_class_captcha(data=form.data)
|
||||
form_cls = get_user_login_form_cls(captcha=True)
|
||||
new_form = form_cls(data=form.data)
|
||||
new_form._errors = form.errors
|
||||
context = self.get_context_data(form=new_form)
|
||||
return self.render_to_response(context)
|
||||
except errors.PasswdTooSimple as e:
|
||||
return redirect(e.url)
|
||||
self.clear_rsa_key()
|
||||
return self.redirect_to_guard_view()
|
||||
|
||||
def redirect_to_guard_view(self):
|
||||
|
@ -103,18 +108,28 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
def get_form_class(self):
|
||||
ip = get_request_ip(self.request)
|
||||
if cache.get(self.key_prefix_captcha.format(ip)):
|
||||
return self.form_class_captcha
|
||||
return get_user_login_form_cls(captcha=True)
|
||||
else:
|
||||
return self.form_class
|
||||
return get_user_login_form_cls()
|
||||
|
||||
def clear_rsa_key(self):
|
||||
self.request.session[RSA_PRIVATE_KEY] = None
|
||||
self.request.session[RSA_PUBLIC_KEY] = None
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
# 生成加解密密钥对,public_key传递给前端,private_key存入session中供解密使用
|
||||
rsa_private_key, rsa_public_key = utils.gen_key_pair()
|
||||
self.request.session['rsa_private_key'] = rsa_private_key
|
||||
rsa_private_key = self.request.session.get(RSA_PRIVATE_KEY)
|
||||
rsa_public_key = self.request.session.get(RSA_PUBLIC_KEY)
|
||||
if not all((rsa_private_key, rsa_public_key)):
|
||||
rsa_private_key, rsa_public_key = utils.gen_key_pair()
|
||||
rsa_public_key = rsa_public_key.replace('\n', '\\n')
|
||||
self.request.session[RSA_PRIVATE_KEY] = rsa_private_key
|
||||
self.request.session[RSA_PUBLIC_KEY] = rsa_public_key
|
||||
|
||||
context = {
|
||||
'demo_mode': os.environ.get("DEMO_MODE"),
|
||||
'AUTH_OPENID': settings.AUTH_OPENID,
|
||||
'rsa_public_key': rsa_public_key.replace('\n', '\\n')
|
||||
'rsa_public_key': rsa_public_key
|
||||
}
|
||||
kwargs.update(context)
|
||||
return super().get_context_data(**kwargs)
|
||||
|
@ -145,6 +160,8 @@ class UserLoginGuardView(mixins.AuthMixin, RedirectView):
|
|||
return self.format_redirect_url(self.login_confirm_url)
|
||||
except errors.MFAUnsetError as e:
|
||||
return e.url
|
||||
except errors.PasswdTooSimple as e:
|
||||
return e.url
|
||||
else:
|
||||
auth_login(self.request, user)
|
||||
self.send_auth_signal(success=True, user=user)
|
||||
|
@ -216,4 +233,16 @@ class UserLogoutView(TemplateView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
@method_decorator(never_cache, name='dispatch')
|
||||
class FlashPasswdTooSimpleMsgView(TemplateView):
|
||||
template_name = 'flash_message_standalone.html'
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
context = {
|
||||
'title': _('Please change your password'),
|
||||
'messages': _('Your password is too simple, please change it for security'),
|
||||
'interval': 5,
|
||||
'redirect_url': request.GET.get('redirect_url'),
|
||||
'auto_redirect': True,
|
||||
}
|
||||
return self.render_to_response(context)
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.db.models import ChoiceSet
|
||||
|
||||
|
||||
ADMIN = 'Admin'
|
||||
USER = 'User'
|
||||
AUDITOR = 'Auditor'
|
|
@ -3,10 +3,10 @@ from django.db.models import Aggregate
|
|||
|
||||
class GroupConcat(Aggregate):
|
||||
function = 'GROUP_CONCAT'
|
||||
template = '%(function)s(%(distinct)s %(expressions)s %(order_by)s %(separator))'
|
||||
template = '%(function)s(%(expressions)s %(order_by)s %(separator)s)'
|
||||
allow_distinct = False
|
||||
|
||||
def __init__(self, expression, distinct=False, order_by=None, separator=',', **extra):
|
||||
def __init__(self, expression, order_by=None, separator=',', **extra):
|
||||
order_by_clause = ''
|
||||
if order_by is not None:
|
||||
order = 'ASC'
|
||||
|
@ -21,8 +21,7 @@ class GroupConcat(Aggregate):
|
|||
|
||||
super().__init__(
|
||||
expression,
|
||||
distinct='DISTINCT' if distinct else '',
|
||||
order_by=order_by_clause,
|
||||
separator=f'SEPARATOR {separator}',
|
||||
separator=f"SEPARATOR '{separator}'",
|
||||
**extra
|
||||
)
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
"""
|
||||
此文件作为 `django.db.models` 的 shortcut
|
||||
|
||||
这样做的优点与缺点为:
|
||||
优点:
|
||||
- 包命名都统一为 `models`
|
||||
- 用户在使用的时候只导入本文件即可
|
||||
缺点:
|
||||
- 此文件中添加代码的时候,注意不要跟 `django.db.models` 中的命名冲突
|
||||
"""
|
||||
|
||||
import uuid
|
||||
|
||||
from django.db.models import *
|
||||
from django.db.models.functions import Concat
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
|
||||
class Choice(str):
|
||||
def __new__(cls, value, label=''): # `deepcopy` 的时候不会传 `label`
|
||||
self = super().__new__(cls, value)
|
||||
self.label = label
|
||||
return self
|
||||
|
||||
|
||||
class ChoiceSetType(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
_choices = []
|
||||
collected = set()
|
||||
new_attrs = {}
|
||||
for k, v in attrs.items():
|
||||
if isinstance(v, tuple):
|
||||
v = Choice(*v)
|
||||
assert v not in collected, 'Cannot be defined repeatedly'
|
||||
_choices.append(v)
|
||||
collected.add(v)
|
||||
new_attrs[k] = v
|
||||
for base in bases:
|
||||
if hasattr(base, '_choices'):
|
||||
for c in base._choices:
|
||||
if c not in collected:
|
||||
_choices.append(c)
|
||||
collected.add(c)
|
||||
new_attrs['_choices'] = _choices
|
||||
new_attrs['_choices_dict'] = {c: c.label for c in _choices}
|
||||
return type.__new__(cls, name, bases, new_attrs)
|
||||
|
||||
def __contains__(self, item):
|
||||
return self._choices_dict.__contains__(item)
|
||||
|
||||
def __getitem__(self, item):
|
||||
return self._choices_dict.__getitem__(item)
|
||||
|
||||
def get(self, item, default=None):
|
||||
return self._choices_dict.get(item, default)
|
||||
|
||||
@property
|
||||
def choices(self):
|
||||
return [(c, c.label) for c in self._choices]
|
||||
|
||||
|
||||
class ChoiceSet(metaclass=ChoiceSetType):
|
||||
choices = None # 用于 Django Model 中的 choices 配置, 为了代码提示在此声明
|
||||
|
||||
|
||||
class JMSBaseModel(Model):
|
||||
created_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
updated_by = CharField(max_length=32, null=True, blank=True, verbose_name=_('Updated by'))
|
||||
date_created = DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
date_updated = DateTimeField(auto_now=True, verbose_name=_('Date updated'))
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class JMSModel(JMSBaseModel):
|
||||
id = UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
def concated_display(name1, name2):
|
||||
return Concat(F(name1), Value('('), F(name2), Value(')'))
|
|
@ -1,11 +1,42 @@
|
|||
from rest_framework.viewsets import GenericViewSet, ModelViewSet
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from ..mixins.api import SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin
|
||||
from ..mixins.api import (
|
||||
SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, PaginatedResponseMixin,
|
||||
RelationMixin, AllowBulkDestoryMixin
|
||||
)
|
||||
|
||||
|
||||
class JmsGenericViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, GenericViewSet):
|
||||
class JmsGenericViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
GenericViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class JMSModelViewSet(SerializerMixin2, QuerySetMixin, ExtraFilterFieldsMixin, ModelViewSet):
|
||||
class JMSModelViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
ModelViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class JMSBulkModelViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
AllowBulkDestoryMixin,
|
||||
BulkModelViewSet):
|
||||
pass
|
||||
|
||||
|
||||
class JMSBulkRelationModelViewSet(SerializerMixin2,
|
||||
QuerySetMixin,
|
||||
ExtraFilterFieldsMixin,
|
||||
PaginatedResponseMixin,
|
||||
RelationMixin,
|
||||
AllowBulkDestoryMixin,
|
||||
BulkModelViewSet):
|
||||
pass
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
from django.core.exceptions import PermissionDenied, ObjectDoesNotExist as DJObjectDoesNotExist
|
||||
from django.http import Http404
|
||||
from django.utils.translation import gettext
|
||||
|
||||
from rest_framework import exceptions
|
||||
from rest_framework.views import set_rollback
|
||||
from rest_framework.response import Response
|
||||
|
||||
from common.exceptions import JMSObjectDoesNotExist
|
||||
|
||||
|
||||
def extract_object_name(exc, index=0):
|
||||
"""
|
||||
`index` 是从 0 开始数的, 比如:
|
||||
`No User matches the given query.`
|
||||
提取 `User`,`index=1`
|
||||
"""
|
||||
(msg, *_) = exc.args
|
||||
return gettext(msg.split(sep=' ', maxsplit=index + 1)[index])
|
||||
|
||||
|
||||
def common_exception_handler(exc, context):
|
||||
if isinstance(exc, Http404):
|
||||
exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 1))
|
||||
elif isinstance(exc, PermissionDenied):
|
||||
exc = exceptions.PermissionDenied()
|
||||
elif isinstance(exc, DJObjectDoesNotExist):
|
||||
exc = JMSObjectDoesNotExist(object_name=extract_object_name(exc, 0))
|
||||
|
||||
if isinstance(exc, exceptions.APIException):
|
||||
headers = {}
|
||||
if getattr(exc, 'auth_header', None):
|
||||
headers['WWW-Authenticate'] = exc.auth_header
|
||||
if getattr(exc, 'wait', None):
|
||||
headers['Retry-After'] = '%d' % exc.wait
|
||||
|
||||
if isinstance(exc.detail, (list, dict)):
|
||||
data = exc.detail
|
||||
else:
|
||||
data = {'detail': exc.detail}
|
||||
|
||||
set_rollback()
|
||||
return Response(data, status=exc.status_code, headers=headers)
|
||||
|
||||
return None
|
|
@ -0,0 +1,43 @@
|
|||
from uuid import UUID
|
||||
|
||||
from rest_framework.fields import get_attribute
|
||||
from rest_framework.relations import ManyRelatedField, PrimaryKeyRelatedField, MANY_RELATION_KWARGS
|
||||
|
||||
|
||||
class GroupConcatedManyRelatedField(ManyRelatedField):
|
||||
def get_attribute(self, instance):
|
||||
if hasattr(instance, 'pk') and instance.pk is None:
|
||||
return []
|
||||
|
||||
attr = self.source_attrs[-1]
|
||||
|
||||
# `gc` 是 `GroupConcat` 的缩写
|
||||
gc_attr = f'gc_{attr}'
|
||||
if hasattr(instance, gc_attr):
|
||||
gc_value = getattr(instance, gc_attr)
|
||||
if isinstance(gc_value, str):
|
||||
return [UUID(pk) for pk in set(gc_value.split(','))]
|
||||
else:
|
||||
return ''
|
||||
|
||||
relationship = get_attribute(instance, self.source_attrs)
|
||||
return relationship.all() if hasattr(relationship, 'all') else relationship
|
||||
|
||||
|
||||
class GroupConcatedPrimaryKeyRelatedField(PrimaryKeyRelatedField):
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
list_kwargs = {'child_relation': cls(*args, **kwargs)}
|
||||
for key in kwargs:
|
||||
if key in MANY_RELATION_KWARGS:
|
||||
list_kwargs[key] = kwargs[key]
|
||||
return GroupConcatedManyRelatedField(**list_kwargs)
|
||||
|
||||
def to_representation(self, value):
|
||||
if self.pk_field is not None:
|
||||
return self.pk_field.to_representation(value.pk)
|
||||
|
||||
if hasattr(value, 'pk'):
|
||||
return value.pk
|
||||
else:
|
||||
return value
|
|
@ -47,9 +47,9 @@ class JMSCSVParser(BaseParser):
|
|||
yield row
|
||||
|
||||
@staticmethod
|
||||
def _get_fields_map(serializer):
|
||||
def _get_fields_map(serializer_cls):
|
||||
fields_map = {}
|
||||
fields = serializer.fields
|
||||
fields = serializer_cls().fields
|
||||
fields_map.update({v.label: k for k, v in fields.items()})
|
||||
fields_map.update({k: k for k, _ in fields.items()})
|
||||
return fields_map
|
||||
|
@ -101,7 +101,7 @@ class JMSCSVParser(BaseParser):
|
|||
try:
|
||||
view = parser_context['view']
|
||||
meta = view.request.META
|
||||
serializer = view.get_serializer()
|
||||
serializer_cls = view.get_serializer_class()
|
||||
except Exception as e:
|
||||
logger.debug(e, exc_info=True)
|
||||
raise ParseError('The resource does not support imports!')
|
||||
|
@ -121,7 +121,7 @@ class JMSCSVParser(BaseParser):
|
|||
rows = self._gen_rows(binary, charset=encoding)
|
||||
|
||||
header = next(rows)
|
||||
fields_map = self._get_fields_map(serializer)
|
||||
fields_map = self._get_fields_map(serializer_cls)
|
||||
header = [fields_map.get(name.strip('*'), '') for name in header]
|
||||
|
||||
data = []
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
from rest_framework.serializers import Serializer
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework import serializers
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
|
||||
from common.mixins.serializers import BulkSerializerMixin
|
||||
from common.mixins import BulkListSerializerMixin
|
||||
|
||||
__all__ = ['EmptySerializer', 'BulkModelSerializer']
|
||||
|
||||
|
||||
class EmptySerializer(Serializer):
|
||||
pass
|
||||
|
||||
|
||||
class BulkModelSerializer(BulkSerializerMixin, ModelSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class CeleryTaskSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework import status
|
||||
|
||||
|
||||
class JMSException(APIException):
|
||||
pass
|
||||
status_code = status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
class JMSObjectDoesNotExist(APIException):
|
||||
status_code = status.HTTP_404_NOT_FOUND
|
||||
default_code = 'object_does_not_exist'
|
||||
default_detail = _('%s object does not exist.')
|
||||
|
||||
def __init__(self, detail=None, code=None, object_name=None):
|
||||
if detail is None and object_name:
|
||||
detail = self.default_detail % object_name
|
||||
super(JMSObjectDoesNotExist, self).__init__(detail=detail, code=code)
|
||||
|
|
|
@ -11,6 +11,8 @@ from django.core.cache import cache
|
|||
from django.http import JsonResponse
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.settings import api_settings
|
||||
from rest_framework import status
|
||||
from rest_framework_bulk.drf3.mixins import BulkDestroyModelMixin
|
||||
|
||||
from common.drf.filters import IDSpmFilter, CustomFilter, IDInFilter
|
||||
from ..utils import lazyproperty
|
||||
|
@ -67,6 +69,17 @@ class ExtraFilterFieldsMixin:
|
|||
return queryset
|
||||
|
||||
|
||||
class PaginatedResponseMixin:
|
||||
def get_paginated_response_with_query_set(self, queryset):
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
serializer = self.get_serializer(page, many=True)
|
||||
return self.get_paginated_response(serializer.data)
|
||||
|
||||
serializer = self.get_serializer(queryset, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class CommonApiMixin(SerializerMixin, ExtraFilterFieldsMixin):
|
||||
pass
|
||||
|
||||
|
@ -212,10 +225,11 @@ class RelationMixin:
|
|||
self.through = getattr(self.m2m_field.model, self.m2m_field.attname).through
|
||||
|
||||
def get_queryset(self):
|
||||
# 注意,此处拦截了 `get_queryset` 没有 `super`
|
||||
queryset = self.through.objects.all()
|
||||
return queryset
|
||||
|
||||
def send_post_add_signal(self, instances):
|
||||
def send_m2m_changed_signal(self, instances, action):
|
||||
if not isinstance(instances, list):
|
||||
instances = [instances]
|
||||
|
||||
|
@ -228,13 +242,17 @@ class RelationMixin:
|
|||
|
||||
for from_obj, to_ids in from_to_mapper.items():
|
||||
m2m_changed.send(
|
||||
sender=self.through, instance=from_obj, action='post_add',
|
||||
sender=self.through, instance=from_obj, action=action,
|
||||
reverse=False, model=self.to_model, pk_set=to_ids
|
||||
)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
instance = serializer.save()
|
||||
self.send_post_add_signal(instance)
|
||||
self.send_m2m_changed_signal(instance, 'post_add')
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
instance.delete()
|
||||
self.send_m2m_changed_signal(instance, 'post_remove')
|
||||
|
||||
|
||||
class SerializerMixin2:
|
||||
|
@ -264,3 +282,12 @@ class QuerySetMixin:
|
|||
queryset = serializer_class.setup_eager_loading(queryset)
|
||||
|
||||
return queryset
|
||||
|
||||
|
||||
class AllowBulkDestoryMixin:
|
||||
def allow_bulk_destroy(self, qs, filtered):
|
||||
"""
|
||||
我们规定,批量删除的情况必须用 `id` 指定要删除的数据。
|
||||
"""
|
||||
query = str(filtered.query)
|
||||
return '`id` IN (' in query or '`id` =' in query
|
||||
|
|
|
@ -8,7 +8,6 @@ from rest_framework.utils import html
|
|||
from rest_framework.settings import api_settings
|
||||
from rest_framework.exceptions import ValidationError
|
||||
from rest_framework.fields import SkipField, empty
|
||||
|
||||
__all__ = ['BulkSerializerMixin', 'BulkListSerializerMixin', 'CommonSerializerMixin', 'CommonBulkSerializerMixin']
|
||||
|
||||
|
||||
|
@ -50,6 +49,15 @@ class BulkSerializerMixin(object):
|
|||
self.initial_data = data
|
||||
return super().run_validation(data)
|
||||
|
||||
@classmethod
|
||||
def many_init(cls, *args, **kwargs):
|
||||
meta = getattr(cls, 'Meta', None)
|
||||
assert meta is not None, 'Must have `Meta`'
|
||||
if not hasattr(meta, 'list_serializer_class'):
|
||||
from common.drf.serializers import AdaptedBulkListSerializer
|
||||
meta.list_serializer_class = AdaptedBulkListSerializer
|
||||
return super(BulkSerializerMixin, cls).many_init(*args, **kwargs)
|
||||
|
||||
|
||||
class BulkListSerializerMixin(object):
|
||||
"""
|
||||
|
|
|
@ -1,14 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
"""
|
||||
老的代码统一到 `apps/common/drf/serializers.py` 中,
|
||||
之后此文件废弃
|
||||
"""
|
||||
|
||||
from rest_framework_bulk.serializers import BulkListSerializer
|
||||
from rest_framework import serializers
|
||||
from .mixins import BulkListSerializerMixin
|
||||
|
||||
|
||||
class AdaptedBulkListSerializer(BulkListSerializerMixin, BulkListSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class CeleryTaskSerializer(serializers.Serializer):
|
||||
task = serializers.CharField(read_only=True)
|
||||
from common.drf.serializers import AdaptedBulkListSerializer, CeleryTaskSerializer
|
||||
|
|
|
@ -11,6 +11,8 @@ import time
|
|||
import ipaddress
|
||||
import psutil
|
||||
|
||||
from .timezone import dt_formater
|
||||
|
||||
|
||||
UUID_PATTERN = re.compile(r'\w{8}(-\w{4}){3}-\w{12}')
|
||||
ipip_db = None
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
#
|
||||
import re
|
||||
from django.shortcuts import reverse as dj_reverse
|
||||
from django.db.models import Subquery, QuerySet
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
import datetime
|
||||
|
||||
import pytz
|
||||
from django.utils import timezone as dj_timezone
|
||||
from rest_framework.fields import DateTimeField
|
||||
|
||||
max = datetime.datetime.max.replace(tzinfo=datetime.timezone.utc)
|
||||
|
||||
|
||||
def astimezone(dt: datetime.datetime, tzinfo: pytz.tzinfo.DstTzInfo):
|
||||
assert dj_timezone.is_aware(dt)
|
||||
return tzinfo.normalize(dt.astimezone(tzinfo))
|
||||
|
||||
|
||||
def as_china_cst(dt: datetime.datetime):
|
||||
return astimezone(dt, pytz.timezone('Asia/Shanghai'))
|
||||
|
||||
|
||||
def as_current_tz(dt: datetime.datetime):
|
||||
return astimezone(dt, dj_timezone.get_current_timezone())
|
||||
|
||||
|
||||
def utcnow():
|
||||
return dj_timezone.now()
|
||||
|
||||
|
||||
def now():
|
||||
return as_current_tz(utcnow())
|
||||
|
||||
|
||||
_rest_dt_field = DateTimeField()
|
||||
dt_parser = _rest_dt_field.to_internal_value
|
||||
dt_formater = _rest_dt_field.to_representation
|
|
@ -128,7 +128,7 @@ class DatesLoginMetricMixin:
|
|||
|
||||
@lazyproperty
|
||||
def dates_total_count_inactive_users(self):
|
||||
total = current_org.get_org_members().count()
|
||||
total = current_org.get_members().count()
|
||||
active = self.dates_total_count_active_users
|
||||
count = total - active
|
||||
if count < 0:
|
||||
|
@ -137,7 +137,7 @@ class DatesLoginMetricMixin:
|
|||
|
||||
@lazyproperty
|
||||
def dates_total_count_disabled_users(self):
|
||||
return current_org.get_org_members().filter(is_active=False).count()
|
||||
return current_org.get_members().filter(is_active=False).count()
|
||||
|
||||
@lazyproperty
|
||||
def dates_total_count_active_assets(self):
|
||||
|
@ -207,7 +207,7 @@ class DatesLoginMetricMixin:
|
|||
class TotalCountMixin:
|
||||
@staticmethod
|
||||
def get_total_count_users():
|
||||
return current_org.get_org_members().count()
|
||||
return current_org.get_members().count()
|
||||
|
||||
@staticmethod
|
||||
def get_total_count_assets():
|
||||
|
|
|
@ -211,6 +211,9 @@ class Config(dict):
|
|||
'CAS_LOGOUT_COMPLETELY': True,
|
||||
'CAS_VERSION': 3,
|
||||
|
||||
'AUTH_SSO': False,
|
||||
'AUTH_SSO_AUTHKEY_TTL': 60 * 15,
|
||||
|
||||
'OTP_VALID_WINDOW': 2,
|
||||
'OTP_ISSUER_NAME': 'JumpServer',
|
||||
'EMAIL_SUFFIX': 'jumpserver.org',
|
||||
|
@ -238,6 +241,8 @@ class Config(dict):
|
|||
'SECURITY_PASSWORD_LOWER_CASE': False,
|
||||
'SECURITY_PASSWORD_NUMBER': False,
|
||||
'SECURITY_PASSWORD_SPECIAL_CHAR': False,
|
||||
'SECURITY_LOGIN_CHALLENGE_ENABLED': False,
|
||||
'SECURITY_LOGIN_CAPTCHA_ENABLED': True,
|
||||
|
||||
'HTTP_BIND_HOST': '0.0.0.0',
|
||||
'HTTP_LISTEN_PORT': 8080,
|
||||
|
@ -438,6 +443,8 @@ class DynamicConfig:
|
|||
backends.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend')
|
||||
if self.static_config.get('AUTH_RADIUS'):
|
||||
backends.insert(0, 'authentication.backends.radius.RadiusBackend')
|
||||
if self.static_config.get('AUTH_SSO'):
|
||||
backends.insert(0, 'authentication.backends.api.SSOAuthentication')
|
||||
return backends
|
||||
|
||||
def XPACK_LICENSE_IS_VALID(self):
|
||||
|
|
|
@ -92,7 +92,11 @@ CAS_LOGGED_MSG = None
|
|||
CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY
|
||||
CAS_VERSION = CONFIG.CAS_VERSION
|
||||
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
|
||||
CAS_CHECK_NEXT = lambda: lambda _next_page: True
|
||||
|
||||
# SSO Auth
|
||||
AUTH_SSO = CONFIG.AUTH_SSO
|
||||
AUTH_SSO_AUTHKEY_TTL = CONFIG.AUTH_SSO_AUTHKEY_TTL
|
||||
|
||||
# Other setting
|
||||
TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
||||
|
|
|
@ -41,6 +41,8 @@ SECURITY_PASSWORD_RULES = [
|
|||
SECURITY_MFA_VERIFY_TTL = CONFIG.SECURITY_MFA_VERIFY_TTL
|
||||
SECURITY_VIEW_AUTH_NEED_MFA = CONFIG.SECURITY_VIEW_AUTH_NEED_MFA
|
||||
SECURITY_SERVICE_ACCOUNT_REGISTRATION = DYNAMIC.SECURITY_SERVICE_ACCOUNT_REGISTRATION
|
||||
SECURITY_LOGIN_CAPTCHA_ENABLED = CONFIG.SECURITY_LOGIN_CAPTCHA_ENABLED
|
||||
SECURITY_LOGIN_CHALLENGE_ENABLED = CONFIG.SECURITY_LOGIN_CHALLENGE_ENABLED
|
||||
|
||||
# Terminal other setting
|
||||
TERMINAL_PASSWORD_AUTH = DYNAMIC.TERMINAL_PASSWORD_AUTH
|
||||
|
@ -94,3 +96,5 @@ XPACK_LICENSE_IS_VALID = DYNAMIC.XPACK_LICENSE_IS_VALID
|
|||
LOGO_URLS = DYNAMIC.LOGO_URLS
|
||||
|
||||
CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED = CONFIG.CHANGE_AUTH_PLAN_SECURE_MODE_ENABLED
|
||||
|
||||
DATETIME_DISPLAY_FORMAT = '%Y-%m-%d %H:%M:%S'
|
||||
|
|
|
@ -40,6 +40,7 @@ REST_FRAMEWORK = {
|
|||
'DATETIME_FORMAT': '%Y-%m-%d %H:%M:%S %z',
|
||||
'DATETIME_INPUT_FORMATS': ['iso-8601', '%Y-%m-%d %H:%M:%S %z'],
|
||||
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
|
||||
'EXCEPTION_HANDLER': 'common.drf.exc_handlers.common_exception_handler',
|
||||
# 'PAGE_SIZE': 100,
|
||||
# 'MAX_PAGE_SIZE': 5000
|
||||
|
||||
|
|
|
@ -75,8 +75,8 @@ if settings.DEBUG:
|
|||
urlpatterns += [
|
||||
re_path('^api/swagger(?P<format>\.json|\.yaml)$',
|
||||
views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
|
||||
path('api/docs/', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"),
|
||||
path('api/redoc/', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
|
||||
re_path('api/docs/?', views.get_swagger_view().with_ui('swagger', cache_timeout=1), name="docs"),
|
||||
re_path('api/redoc/?', views.get_swagger_view().with_ui('redoc', cache_timeout=1), name='redoc'),
|
||||
|
||||
re_path('^api/v2/swagger(?P<format>\.json|\.yaml)$',
|
||||
views.get_swagger_view().without_ui(cache_timeout=1), name='schema-json'),
|
||||
|
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -93,6 +93,12 @@ def delete_celery_periodic_task(task_name):
|
|||
PeriodicTasks.update_changed()
|
||||
|
||||
|
||||
def get_celery_periodic_task(task_name):
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
task = PeriodicTask.objects.filter(name=task_name).first()
|
||||
return task
|
||||
|
||||
|
||||
def get_celery_task_log_path(task_id):
|
||||
task_id = str(task_id)
|
||||
rel_path = os.path.join(task_id[0], task_id[1], task_id + '.log')
|
||||
|
|
|
@ -15,7 +15,10 @@ from .celery.decorator import (
|
|||
register_as_period_task, after_app_shutdown_clean_periodic,
|
||||
after_app_ready_start
|
||||
)
|
||||
from .celery.utils import create_or_update_celery_periodic_tasks
|
||||
from .celery.utils import (
|
||||
create_or_update_celery_periodic_tasks, get_celery_periodic_task,
|
||||
disable_celery_periodic_task, delete_celery_periodic_task
|
||||
)
|
||||
from .models import Task, CommandExecution, CeleryTask
|
||||
from .utils import send_server_performance_mail
|
||||
|
||||
|
@ -95,6 +98,29 @@ def clean_celery_tasks_period():
|
|||
subprocess.call(command, shell=True)
|
||||
|
||||
|
||||
@shared_task
|
||||
@after_app_ready_start
|
||||
def clean_celery_periodic_tasks():
|
||||
"""清除celery定时任务"""
|
||||
need_cleaned_tasks = [
|
||||
'handle_be_interrupted_change_auth_task_periodic',
|
||||
]
|
||||
logger.info('Start clean celery periodic tasks: {}'.format(need_cleaned_tasks))
|
||||
for task_name in need_cleaned_tasks:
|
||||
logger.info('Start clean task: {}'.format(task_name))
|
||||
task = get_celery_periodic_task(task_name)
|
||||
if task is None:
|
||||
logger.info('Task does not exist: {}'.format(task_name))
|
||||
continue
|
||||
disable_celery_periodic_task(task_name)
|
||||
delete_celery_periodic_task(task_name)
|
||||
task = get_celery_periodic_task(task_name)
|
||||
if task is None:
|
||||
logger.info('Clean task success: {}'.format(task_name))
|
||||
else:
|
||||
logger.info('Clean task failure: {}'.format(task))
|
||||
|
||||
|
||||
@shared_task
|
||||
@after_app_ready_start
|
||||
def create_or_update_registered_periodic_tasks():
|
||||
|
|
|
@ -69,7 +69,7 @@ def send_server_performance_mail(path, usage, usages):
|
|||
from users.models import User
|
||||
subject = _("Disk used more than 80%: {} => {}").format(path, usage.percent)
|
||||
message = subject
|
||||
admins = User.objects.filter(role=User.ROLE_ADMIN)
|
||||
admins = User.objects.filter(role=User.ROLE.ADMIN)
|
||||
recipient_list = [u.email for u in admins if u.email]
|
||||
logger.info(subject)
|
||||
send_mail_async(subject, message, recipient_list, html_message=message)
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import ugettext as _
|
||||
from rest_framework import status, generics
|
||||
from rest_framework.views import Response
|
||||
from rest_framework_bulk import BulkModelViewSet
|
||||
|
||||
from common.permissions import IsSuperUserOrAppUser
|
||||
from .models import Organization
|
||||
from .serializers import OrgSerializer, OrgReadSerializer, \
|
||||
OrgMembershipUserSerializer, OrgMembershipAdminSerializer, \
|
||||
OrgAllUserSerializer, OrgRetrieveSerializer
|
||||
from common.drf.api import JMSBulkRelationModelViewSet
|
||||
from .models import Organization, ROLE
|
||||
from .serializers import (
|
||||
OrgSerializer, OrgReadSerializer,
|
||||
OrgRetrieveSerializer, OrgMemberSerializer
|
||||
)
|
||||
from users.models import User, UserGroup
|
||||
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
|
||||
from perms.models import AssetPermission
|
||||
from orgs.utils import current_org
|
||||
from common.utils import get_logger
|
||||
from .mixins.api import OrgMembershipModelViewSetMixin
|
||||
from .filters import OrgMemberRelationFilterSet
|
||||
|
||||
logger = get_logger(__file__)
|
||||
|
||||
|
@ -39,7 +40,7 @@ class OrgViewSet(BulkModelViewSet):
|
|||
|
||||
def get_data_from_model(self, model):
|
||||
if model == User:
|
||||
data = model.objects.filter(related_user_orgs__id=self.org.id)
|
||||
data = model.objects.filter(orgs__id=self.org.id, m2m_org_members__role=ROLE.USER)
|
||||
else:
|
||||
data = model.objects.filter(org_id=self.org.id)
|
||||
return data
|
||||
|
@ -64,26 +65,13 @@ class OrgViewSet(BulkModelViewSet):
|
|||
return Response({'msg': True}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class OrgMembershipAdminsViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet):
|
||||
serializer_class = OrgMembershipAdminSerializer
|
||||
membership_class = Organization.admins.through
|
||||
permission_classes = (IsSuperUserOrAppUser, )
|
||||
|
||||
|
||||
class OrgMembershipUsersViewSet(OrgMembershipModelViewSetMixin, BulkModelViewSet):
|
||||
serializer_class = OrgMembershipUserSerializer
|
||||
membership_class = Organization.users.through
|
||||
permission_classes = (IsSuperUserOrAppUser, )
|
||||
|
||||
|
||||
class OrgAllUserListApi(generics.ListAPIView):
|
||||
class OrgMemberRelationBulkViewSet(JMSBulkRelationModelViewSet):
|
||||
permission_classes = (IsSuperUserOrAppUser,)
|
||||
serializer_class = OrgAllUserSerializer
|
||||
filter_fields = ("username", "name")
|
||||
search_fields = filter_fields
|
||||
m2m_field = Organization.members.field
|
||||
serializer_class = OrgMemberSerializer
|
||||
filterset_class = OrgMemberRelationFilterSet
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
org = get_object_or_404(Organization, pk=pk)
|
||||
users = org.get_org_users().only(*self.serializer_class.Meta.only_fields)
|
||||
return users
|
||||
def perform_bulk_destroy(self, queryset):
|
||||
objs = list(queryset.all().prefetch_related('user', 'org'))
|
||||
queryset.delete()
|
||||
self.send_m2m_changed_signal(objs, action='post_remove')
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
from django_filters.rest_framework import filterset
|
||||
from django_filters.rest_framework import filters
|
||||
|
||||
from .models import OrganizationMember
|
||||
|
||||
|
||||
class UUIDInFilter(filters.BaseInFilter, filters.UUIDFilter):
|
||||
pass
|
||||
|
||||
|
||||
class OrgMemberRelationFilterSet(filterset.FilterSet):
|
||||
id = UUIDInFilter(field_name='id', lookup_expr='in')
|
||||
|
||||
class Meta:
|
||||
model = OrganizationMember
|
||||
fields = ('org_id', 'user_id', 'role', 'id')
|
|
@ -0,0 +1,33 @@
|
|||
# Generated by Django 2.2.10 on 2020-07-21 11:27
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('orgs', '0003_auto_20190916_1057'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrganizationMember',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('role', models.CharField(choices=[('Admin', 'Administrator'), ('User', 'User'), ('Auditor', 'Auditor')], default='User', max_length=16, verbose_name='Role')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('date_updated', models.DateTimeField(auto_now=True, verbose_name='Date updated')),
|
||||
('created_by', models.CharField(max_length=128, null=True, verbose_name='Created by')),
|
||||
('org', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to='orgs.Organization', verbose_name='Organization')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='m2m_org_members', to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'db_table': 'orgs_organization_members',
|
||||
'unique_together': {('org', 'user', 'role')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -0,0 +1,35 @@
|
|||
# Generated by Django 2.2.10 on 2020-07-21 11:37
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_old_organization_members(apps, schema_editor):
|
||||
org_model = apps.get_model("orgs", "Organization")
|
||||
org_member_model = apps.get_model('orgs', 'OrganizationMember')
|
||||
orgs = org_model.objects.all()
|
||||
|
||||
roles = ['User', 'Auditor', 'Admin']
|
||||
|
||||
for org in orgs:
|
||||
users = org.users.all().only('id')
|
||||
auditors = org.auditors.all().only('id')
|
||||
admins = org.admins.all().only('id')
|
||||
total_members = zip([users, auditors, admins], roles)
|
||||
|
||||
org_members = []
|
||||
for members, role in total_members:
|
||||
for user in members:
|
||||
org_user = org_member_model(user=user, org=org, role=role)
|
||||
org_members.append(org_user)
|
||||
org_member_model.objects.bulk_create(org_members)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orgs', '0004_organizationmember'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(migrate_old_organization_members)
|
||||
]
|
|
@ -0,0 +1,32 @@
|
|||
# Generated by Django 2.2.10 on 2020-07-21 11:37
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('orgs', '0005_auto_20200721_1937'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='organization',
|
||||
name='admins',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organization',
|
||||
name='auditors',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='organization',
|
||||
name='users',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organization',
|
||||
name='members',
|
||||
field=models.ManyToManyField(related_name='orgs', through='orgs.OrganizationMember', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 2.2.10 on 2020-07-28 10:05
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orgs', '0006_auto_20200721_1937'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='organizationmember',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('Admin', 'Organization administrator'), ('User', 'User'), ('Auditor', 'Organization auditor')], default='User', max_length=16, verbose_name='Role'),
|
||||
),
|
||||
]
|
|
@ -55,8 +55,8 @@ class OrgBulkModelViewSet(CommonApiMixin, OrgQuerySetMixin, BulkModelViewSet):
|
|||
filtered_count = filtered.count()
|
||||
if filtered_count == 1:
|
||||
return True
|
||||
if qs_count <= filtered_count:
|
||||
return False
|
||||
if qs_count > filtered_count:
|
||||
return True
|
||||
if self.request.query_params.get('spm', ''):
|
||||
return True
|
||||
return False
|
||||
|
|
|
@ -62,7 +62,6 @@ class OrgModelMixin(models.Model):
|
|||
org = get_current_org()
|
||||
if org is None:
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
if org.is_real() or org.is_system():
|
||||
self.org_id = org.id
|
||||
elif org.is_default():
|
||||
|
|
|
@ -11,8 +11,7 @@ from ..utils import get_current_org_id_for_serializer
|
|||
|
||||
__all__ = [
|
||||
"OrgResourceSerializerMixin", "BulkOrgResourceSerializerMixin",
|
||||
"BulkOrgResourceModelSerializer", "OrgMembershipSerializerMixin",
|
||||
"OrgResourceModelSerializerMixin",
|
||||
"BulkOrgResourceModelSerializer", "OrgResourceModelSerializerMixin",
|
||||
]
|
||||
|
||||
|
||||
|
@ -53,9 +52,3 @@ class BulkOrgResourceSerializerMixin(BulkSerializerMixin, OrgResourceSerializerM
|
|||
|
||||
class BulkOrgResourceModelSerializer(BulkOrgResourceSerializerMixin, serializers.ModelSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class OrgMembershipSerializerMixin:
|
||||
def run_validation(self, initial_data=None):
|
||||
initial_data['organization'] = str(self.context['org'].id)
|
||||
return super().run_validation(initial_data)
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import uuid
|
||||
from django.conf import settings
|
||||
from functools import partial
|
||||
|
||||
from django.db import models
|
||||
from django.db.models import signals
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import is_uuid, lazyproperty
|
||||
from common.utils import is_uuid
|
||||
from common.const import choices
|
||||
from common.db.models import ChoiceSet
|
||||
|
||||
|
||||
class ROLE(ChoiceSet):
|
||||
ADMIN = choices.ADMIN, _('Organization administrator')
|
||||
USER = choices.USER, _('User')
|
||||
AUDITOR = choices.AUDITOR, _("Organization auditor")
|
||||
|
||||
|
||||
class Organization(models.Model):
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
name = models.CharField(max_length=128, unique=True, verbose_name=_("Name"))
|
||||
users = models.ManyToManyField('users.User', related_name='related_user_orgs', blank=True)
|
||||
admins = models.ManyToManyField('users.User', related_name='related_admin_orgs', blank=True)
|
||||
auditors = models.ManyToManyField('users.User', related_name='related_audit_orgs', blank=True)
|
||||
created_by = models.CharField(max_length=32, null=True, blank=True, verbose_name=_('Created by'))
|
||||
date_created = models.DateTimeField(auto_now_add=True, null=True, blank=True, verbose_name=_('Date created'))
|
||||
comment = models.TextField(max_length=128, default='', blank=True, verbose_name=_('Comment'))
|
||||
members = models.ManyToManyField('users.User', related_name='orgs', through='orgs.OrganizationMember',
|
||||
through_fields=('org', 'user'))
|
||||
|
||||
orgs = None
|
||||
CACHE_PREFIX = 'JMS_ORG_{}'
|
||||
|
@ -72,29 +81,24 @@ class Organization(models.Model):
|
|||
org = cls.default() if default else None
|
||||
return org
|
||||
|
||||
# @lazyproperty
|
||||
# lazyproperty 导致用户列表中角色显示出现不稳定的情况, 如果不加会导致数据库操作次数太多
|
||||
def org_users(self):
|
||||
def get_org_members_by_role(self, role):
|
||||
from users.models import User
|
||||
if self.is_real():
|
||||
return self.users.all()
|
||||
users = User.objects.filter(role=User.ROLE_USER)
|
||||
if self.is_default() and not settings.DEFAULT_ORG_SHOW_ALL_USERS:
|
||||
users = users.filter(related_user_orgs__isnull=True)
|
||||
return self.members.filter(m2m_org_members__role=role)
|
||||
users = User.objects.filter(role=role)
|
||||
return users
|
||||
|
||||
def get_org_users(self):
|
||||
return self.org_users()
|
||||
@property
|
||||
def users(self):
|
||||
return self.get_org_members_by_role(ROLE.USER)
|
||||
|
||||
# @lazyproperty
|
||||
def org_admins(self):
|
||||
from users.models import User
|
||||
if self.is_real():
|
||||
return self.admins.all()
|
||||
return User.objects.filter(role=User.ROLE_ADMIN)
|
||||
@property
|
||||
def admins(self):
|
||||
return self.get_org_members_by_role(ROLE.ADMIN)
|
||||
|
||||
def get_org_admins(self):
|
||||
return self.org_admins()
|
||||
@property
|
||||
def auditors(self):
|
||||
return self.get_org_members_by_role(ROLE.AUDITOR)
|
||||
|
||||
def org_id(self):
|
||||
if self.is_real():
|
||||
|
@ -104,87 +108,86 @@ class Organization(models.Model):
|
|||
else:
|
||||
return ''
|
||||
|
||||
# @lazyproperty
|
||||
def org_auditors(self):
|
||||
def get_members(self, exclude=()):
|
||||
from users.models import User
|
||||
if self.is_real():
|
||||
return self.auditors.all()
|
||||
return User.objects.filter(role=User.ROLE_AUDITOR)
|
||||
members = self.members.exclude(m2m_org_members__role__in=exclude)
|
||||
else:
|
||||
members = User.objects.exclude(role__in=exclude)
|
||||
|
||||
def get_org_auditors(self):
|
||||
return self.org_auditors()
|
||||
|
||||
def get_org_members(self, exclude=()):
|
||||
from users.models import User
|
||||
members = User.objects.none()
|
||||
if 'Admin' not in exclude:
|
||||
members |= self.get_org_admins()
|
||||
if 'User' not in exclude:
|
||||
members |= self.get_org_users()
|
||||
if 'Auditor' not in exclude:
|
||||
members |= self.get_org_auditors()
|
||||
return members.exclude(role=User.ROLE_APP).distinct()
|
||||
return members.exclude(role=User.ROLE.APP).distinct()
|
||||
|
||||
def can_admin_by(self, user):
|
||||
if user.is_superuser:
|
||||
return True
|
||||
if self.get_org_admins().filter(id=user.id):
|
||||
if self.admins.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_audit_by(self, user):
|
||||
if user.is_super_auditor:
|
||||
return True
|
||||
if self.get_org_auditors().filter(id=user.id):
|
||||
if self.auditors.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def can_user_by(self, user):
|
||||
if self.get_org_users().filter(id=user.id):
|
||||
if self.users.filter(id=user.id).exists():
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_real(self):
|
||||
return self.id not in (self.DEFAULT_NAME, self.ROOT_ID, self.SYSTEM_ID)
|
||||
|
||||
@classmethod
|
||||
def get_user_orgs_by_role(cls, user, role):
|
||||
if not isinstance(role, (tuple, list)):
|
||||
role = (role, )
|
||||
|
||||
return cls.objects.filter(
|
||||
m2m_org_members__role__in=role,
|
||||
m2m_org_members__user_id=user.id
|
||||
).distinct()
|
||||
|
||||
@classmethod
|
||||
def get_user_all_orgs(cls, user):
|
||||
return [
|
||||
*cls.objects.filter(members=user).distinct(),
|
||||
cls.default()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_user_admin_orgs(cls, user):
|
||||
admin_orgs = []
|
||||
if user.is_anonymous:
|
||||
return admin_orgs
|
||||
elif user.is_superuser:
|
||||
admin_orgs = list(cls.objects.all())
|
||||
admin_orgs.append(cls.default())
|
||||
elif user.is_org_admin:
|
||||
admin_orgs = user.related_admin_orgs.all()
|
||||
return admin_orgs
|
||||
return cls.objects.none()
|
||||
if user.is_superuser:
|
||||
return [*cls.objects.all(), cls.default()]
|
||||
return cls.get_user_orgs_by_role(user, ROLE.ADMIN)
|
||||
|
||||
@classmethod
|
||||
def get_user_user_orgs(cls, user):
|
||||
user_orgs = []
|
||||
if user.is_anonymous:
|
||||
return user_orgs
|
||||
user_orgs = user.related_user_orgs.all()
|
||||
return user_orgs
|
||||
return cls.objects.none()
|
||||
return [
|
||||
*cls.get_user_orgs_by_role(user, ROLE.USER),
|
||||
cls.default()
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_user_audit_orgs(cls, user):
|
||||
audit_orgs = []
|
||||
if user.is_anonymous:
|
||||
return audit_orgs
|
||||
elif user.is_super_auditor:
|
||||
audit_orgs = list(cls.objects.all())
|
||||
audit_orgs.append(cls.default())
|
||||
elif user.is_org_auditor:
|
||||
audit_orgs = user.related_audit_orgs.all()
|
||||
return audit_orgs
|
||||
return cls.objects.none()
|
||||
if user.is_super_auditor:
|
||||
return [*cls.objects.all(), cls.default()]
|
||||
return cls.get_user_orgs_by_role(user, ROLE.AUDITOR)
|
||||
|
||||
@classmethod
|
||||
def get_user_admin_or_audit_orgs(self, user):
|
||||
admin_orgs = self.get_user_admin_orgs(user)
|
||||
audit_orgs = self.get_user_audit_orgs(user)
|
||||
orgs = set(admin_orgs) | set(audit_orgs)
|
||||
return orgs
|
||||
def get_user_admin_or_audit_orgs(cls, user):
|
||||
if user.is_anonymous:
|
||||
return cls.objects.none()
|
||||
if user.is_superuser or user.is_super_auditor:
|
||||
return [*cls.objects.all(), cls.default()]
|
||||
return cls.get_user_orgs_by_role(user, (ROLE.AUDITOR, ROLE.ADMIN))
|
||||
|
||||
@classmethod
|
||||
def default(cls):
|
||||
|
@ -211,8 +214,122 @@ class Organization(models.Model):
|
|||
from .utils import set_current_org
|
||||
set_current_org(self)
|
||||
|
||||
@classmethod
|
||||
def all_orgs(cls):
|
||||
orgs = list(cls.objects.all())
|
||||
orgs.append(cls.default())
|
||||
return orgs
|
||||
|
||||
def _convert_to_uuid_set(users):
|
||||
rst = set()
|
||||
for user in users:
|
||||
if isinstance(user, models.Model):
|
||||
rst.add(user.id)
|
||||
elif not isinstance(user, uuid.UUID):
|
||||
rst.add(uuid.UUID(user))
|
||||
return rst
|
||||
|
||||
|
||||
def _none2list(*args):
|
||||
return ([] if v is None else v for v in args)
|
||||
|
||||
|
||||
class OrgMemeberManager(models.Manager):
|
||||
|
||||
def remove_users_by_role(self, org, users=None, admins=None, auditors=None):
|
||||
if not any((users, admins, auditors)):
|
||||
return
|
||||
users, admins, auditors = _none2list(users, admins, auditors)
|
||||
|
||||
send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False,
|
||||
model=Organization, pk_set=[*users, *admins, *auditors], using=self.db)
|
||||
|
||||
send(action="pre_remove")
|
||||
self.filter(org_id=org.id).filter(
|
||||
Q(user__in=users, role=ROLE.USER) |
|
||||
Q(user__in=admins, role=ROLE.ADMIN) |
|
||||
Q(user__in=auditors, role=ROLE.AUDITOR)
|
||||
).delete()
|
||||
send(action="post_remove")
|
||||
|
||||
def add_users_by_role(self, org, users=None, admins=None, auditors=None):
|
||||
if not any((users, admins, auditors)):
|
||||
return
|
||||
users, admins, auditors = _none2list(users, admins, auditors)
|
||||
|
||||
add_mapper = (
|
||||
(users, ROLE.USER),
|
||||
(admins, ROLE.ADMIN),
|
||||
(auditors, ROLE.AUDITOR)
|
||||
)
|
||||
|
||||
oms_add = []
|
||||
for users, role in add_mapper:
|
||||
for user in users:
|
||||
if isinstance(user, models.Model):
|
||||
user = user.id
|
||||
oms_add.append(self.model(org_id=org.id, user_id=user, role=role))
|
||||
|
||||
send = partial(signals.m2m_changed.send, sender=self.model, instance=org, reverse=False,
|
||||
model=Organization, pk_set=[*users, *admins, *auditors], using=self.db)
|
||||
|
||||
send(action='pre_add')
|
||||
self.bulk_create(oms_add)
|
||||
send(action='post_add')
|
||||
|
||||
def _get_remove_add_set(self, new_users, old_users):
|
||||
if new_users is None:
|
||||
return None, None
|
||||
new_users = _convert_to_uuid_set(new_users)
|
||||
return (old_users - new_users), (new_users - old_users)
|
||||
|
||||
def set_users_by_role(self, org, users=None, admins=None, auditors=None):
|
||||
oms = self.filter(org_id=org.id).values_list('role', 'user_id')
|
||||
|
||||
old_users, old_admins, old_auditors = set(), set(), set()
|
||||
|
||||
mapper = {
|
||||
ROLE.USER: old_users,
|
||||
ROLE.ADMIN: old_admins,
|
||||
ROLE.AUDITOR: old_auditors
|
||||
}
|
||||
|
||||
for role, user_id in oms:
|
||||
if role in mapper:
|
||||
mapper[role].add(user_id)
|
||||
|
||||
users_remove, users_add = self._get_remove_add_set(users, old_users)
|
||||
admins_remove, admins_add = self._get_remove_add_set(admins, old_admins)
|
||||
auditors_remove, auditors_add = self._get_remove_add_set(auditors, old_auditors)
|
||||
|
||||
self.remove_users_by_role(
|
||||
org,
|
||||
users_remove,
|
||||
admins_remove,
|
||||
auditors_remove
|
||||
)
|
||||
|
||||
self.add_users_by_role(
|
||||
org,
|
||||
users_add,
|
||||
admins_add,
|
||||
auditors_add
|
||||
)
|
||||
|
||||
|
||||
class OrganizationMember(models.Model):
|
||||
"""
|
||||
注意:直接调用该 `Model.delete` `Model.objects.delete` 不会触发清理该用户的信号
|
||||
"""
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
org = models.ForeignKey(Organization, related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('Organization'))
|
||||
user = models.ForeignKey('users.User', related_name='m2m_org_members', on_delete=models.CASCADE, verbose_name=_('User'))
|
||||
role = models.CharField(max_length=16, choices=ROLE.choices, default=ROLE.USER, verbose_name=_("Role"))
|
||||
date_created = models.DateTimeField(auto_now_add=True, verbose_name=_("Date created"))
|
||||
date_updated = models.DateTimeField(auto_now=True, verbose_name=_("Date updated"))
|
||||
created_by = models.CharField(max_length=128, null=True, verbose_name=_('Created by'))
|
||||
|
||||
objects = OrgMemeberManager()
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org', 'user', 'role')]
|
||||
db_table = 'orgs_organization_members'
|
||||
|
||||
def __str__(self):
|
||||
return '{} is {}: {}'.format(self.user.name, self.org.name, self.role)
|
||||
|
|
|
@ -1,16 +1,19 @@
|
|||
|
||||
from django.db.models import F
|
||||
from rest_framework.serializers import ModelSerializer
|
||||
from rest_framework import serializers
|
||||
from users.models import UserGroup
|
||||
from assets.models import Asset, Domain, AdminUser, SystemUser, Label
|
||||
from perms.models import AssetPermission
|
||||
|
||||
from users.models.user import User
|
||||
from common.serializers import AdaptedBulkListSerializer
|
||||
from .utils import set_current_org, get_current_org
|
||||
from .models import Organization
|
||||
from .mixins.serializers import OrgMembershipSerializerMixin
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
from common.db.models import concated_display as display
|
||||
from .models import Organization, OrganizationMember, ROLE as ORG_ROLE
|
||||
|
||||
|
||||
class OrgSerializer(ModelSerializer):
|
||||
users = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True)
|
||||
admins = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True)
|
||||
auditors = serializers.PrimaryKeyRelatedField(many=True, queryset=User.objects.all(), write_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Organization
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
|
@ -21,41 +24,48 @@ class OrgSerializer(ModelSerializer):
|
|||
fields_m2m = ['users', 'admins', 'auditors']
|
||||
fields = fields_small + fields_m2m
|
||||
read_only_fields = ['created_by', 'date_created']
|
||||
extra_kwargs = {
|
||||
'admins': {'write_only': True},
|
||||
'users': {'write_only': True},
|
||||
'auditors': {'write_only': True},
|
||||
}
|
||||
|
||||
def create(self, validated_data):
|
||||
members = self._pop_memebers(validated_data)
|
||||
instance = Organization.objects.create(**validated_data)
|
||||
OrganizationMember.objects.add_users_by_role(instance, *members)
|
||||
return instance
|
||||
|
||||
def _pop_memebers(self, validated_data):
|
||||
return (
|
||||
validated_data.pop('users', None),
|
||||
validated_data.pop('admins', None),
|
||||
validated_data.pop('auditors', None)
|
||||
)
|
||||
|
||||
def update(self, instance, validated_data):
|
||||
members = self._pop_memebers(validated_data)
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
OrganizationMember.objects.set_users_by_role(instance, *members)
|
||||
return instance
|
||||
|
||||
|
||||
class OrgReadSerializer(OrgSerializer):
|
||||
pass
|
||||
|
||||
|
||||
class OrgMembershipAdminSerializer(OrgMembershipSerializerMixin, ModelSerializer):
|
||||
class Meta:
|
||||
model = Organization.admins.through
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class OrgMembershipUserSerializer(OrgMembershipSerializerMixin, ModelSerializer):
|
||||
class Meta:
|
||||
model = Organization.users.through
|
||||
list_serializer_class = AdaptedBulkListSerializer
|
||||
fields = '__all__'
|
||||
|
||||
|
||||
class OrgAllUserSerializer(serializers.Serializer):
|
||||
user = serializers.UUIDField(read_only=True, source='id')
|
||||
user_display = serializers.SerializerMethodField()
|
||||
class OrgMemberSerializer(BulkModelSerializer):
|
||||
org_display = serializers.CharField()
|
||||
user_display = serializers.CharField()
|
||||
role_display = serializers.CharField(source='get_role_display')
|
||||
|
||||
class Meta:
|
||||
only_fields = ['id', 'username', 'name']
|
||||
model = OrganizationMember
|
||||
fields = ('id', 'org', 'user', 'role', 'org_display', 'user_display', 'role_display')
|
||||
|
||||
@staticmethod
|
||||
def get_user_display(obj):
|
||||
return str(obj)
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
return queryset.annotate(
|
||||
org_display=F('org__name'),
|
||||
user_display=display('user__name', 'user__username')
|
||||
).distinct()
|
||||
|
||||
|
||||
class OrgRetrieveSerializer(OrgReadSerializer):
|
||||
|
|
|
@ -5,7 +5,7 @@ from django.db.models.signals import m2m_changed
|
|||
from django.db.models.signals import post_save
|
||||
from django.dispatch import receiver
|
||||
|
||||
from .models import Organization
|
||||
from .models import Organization, OrganizationMember
|
||||
from .hands import set_current_org, current_org, Node, get_current_org
|
||||
from perms.models import AssetPermission
|
||||
from users.models import UserGroup
|
||||
|
@ -26,23 +26,31 @@ def on_org_create_or_update(sender, instance=None, created=False, **kwargs):
|
|||
instance.expire_cache()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Organization.users.through)
|
||||
def on_org_user_changed(sender, instance=None, **kwargs):
|
||||
if isinstance(instance, Organization):
|
||||
old_org = current_org
|
||||
set_current_org(instance)
|
||||
if kwargs['action'] == 'pre_remove':
|
||||
users = kwargs['model'].objects.filter(pk__in=kwargs['pk_set'])
|
||||
for user in users:
|
||||
perms = AssetPermission.objects.filter(users=user)
|
||||
user_groups = UserGroup.objects.filter(users=user)
|
||||
for perm in perms:
|
||||
perm.users.remove(user)
|
||||
for user_group in user_groups:
|
||||
user_group.users.remove(user)
|
||||
set_current_org(old_org)
|
||||
def _remove_users(model, users, org, reverse=False):
|
||||
if not isinstance(users, (tuple, list, set)):
|
||||
users = (users, )
|
||||
|
||||
m2m_model = model.users.through
|
||||
if reverse:
|
||||
m2m_field_name = model.users.field.m2m_reverse_field_name()
|
||||
else:
|
||||
m2m_field_name = model.users.field.m2m_field_name()
|
||||
m2m_model.objects.filter(**{'user__in': users, f'{m2m_field_name}__org_id': org.id}).delete()
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=Organization.admins.through)
|
||||
def on_org_admin_change(sender, **kwargs):
|
||||
Organization._user_admin_orgs = None
|
||||
def _clear_users_from_org(org, users):
|
||||
if not users:
|
||||
return
|
||||
|
||||
old_org = current_org
|
||||
set_current_org(org)
|
||||
_remove_users(AssetPermission, users, org)
|
||||
_remove_users(UserGroup, users, org, reverse=True)
|
||||
set_current_org(old_org)
|
||||
|
||||
|
||||
@receiver(m2m_changed, sender=OrganizationMember)
|
||||
def on_org_user_changed(sender, instance=None, action=None, pk_set=None, **kwargs):
|
||||
if action == 'post_remove':
|
||||
leaved_users = set(pk_set) - set(instance.members.values_list('id', flat=True))
|
||||
_clear_users_from_org(instance, leaved_users)
|
||||
|
|
|
@ -1,3 +1,16 @@
|
|||
from django.test import TestCase
|
||||
from django.urls import reverse
|
||||
from rest_framework.test import APITestCase
|
||||
from rest_framework import status
|
||||
|
||||
from users.models.user import User
|
||||
|
||||
|
||||
class OrgTests(APITestCase):
|
||||
def test_create(self):
|
||||
print(User.objects.all())
|
||||
reverse('api-orgs:org-list')
|
||||
|
||||
|
||||
|
||||
{"name":"a-07","admins":["138167d2-6843-4e25-b838-59657157c6c6"],"auditors":["8d4b3ec4-8339-4a2c-b33c-c2633da62c84"],"users":["ea60e8ce-876d-493b-a641-ff836258629c"]}
|
||||
|
||||
# Create your tests here.
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.urls import re_path, path
|
||||
from django.urls import re_path
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
|
||||
from common import api as capi
|
||||
from .. import api
|
||||
|
@ -10,21 +11,13 @@ from .. import api
|
|||
|
||||
app_name = 'orgs'
|
||||
router = DefaultRouter()
|
||||
|
||||
# 将会删除
|
||||
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/admins',
|
||||
api.OrgMembershipAdminsViewSet, 'membership-admins')
|
||||
router.register(r'orgs/(?P<org_id>[0-9a-zA-Z\-]{36})/membership/users',
|
||||
api.OrgMembershipUsersViewSet, 'membership-users'),
|
||||
bulk_router = BulkRouter()
|
||||
|
||||
router.register(r'orgs', api.OrgViewSet, 'org')
|
||||
bulk_router.register(r'org-memeber-relation', api.OrgMemberRelationBulkViewSet, 'org-memeber-relation')
|
||||
|
||||
old_version_urlpatterns = [
|
||||
re_path('(?P<resource>org)/.*', capi.redirect_plural_name_api)
|
||||
]
|
||||
|
||||
urlpatterns = [
|
||||
path('<uuid:pk>/users/all/', api.OrgAllUserListApi.as_view(), name='org-all-users'),
|
||||
]
|
||||
|
||||
urlpatterns += router.urls + old_version_urlpatterns
|
||||
urlpatterns = router.urls + bulk_router.urls + old_version_urlpatterns
|
||||
|
|
|
@ -12,3 +12,6 @@ from .database_app_permission import *
|
|||
from .database_app_permission_relation import *
|
||||
from .user_database_app_permission import *
|
||||
from .system_user_permission import *
|
||||
from .k8s_app_permission import *
|
||||
from .k8s_app_permission_relation import *
|
||||
from .user_k8s_app_permission import *
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
|
||||
from .. import models, serializers
|
||||
from common.permissions import IsOrgAdmin
|
||||
|
||||
|
||||
__all__ = ['K8sAppPermissionViewSet']
|
||||
|
||||
|
||||
class K8sAppPermissionViewSet(OrgBulkModelViewSet):
|
||||
model = models.K8sAppPermission
|
||||
serializer_classes = {
|
||||
'default': serializers.K8sAppPermissionSerializer,
|
||||
'display': serializers.K8sAppPermissionListSerializer
|
||||
}
|
||||
filter_fields = ('name',)
|
||||
search_fields = filter_fields
|
||||
permission_classes = (IsOrgAdmin,)
|
|
@ -0,0 +1,111 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
from rest_framework import generics
|
||||
from django.db.models import F, Value
|
||||
from django.db.models.functions import Concat
|
||||
from django.shortcuts import get_object_or_404
|
||||
|
||||
from common.permissions import IsOrgAdmin
|
||||
from .base import RelationViewSet
|
||||
from .. import models, serializers
|
||||
|
||||
|
||||
class K8sAppPermissionUserRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionUserRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.users.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', 'user', 'k8sapppermission'
|
||||
]
|
||||
search_fields = ('user__name', 'user__username', 'k8sapppermission__name')
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(user_display=F('user__name'))
|
||||
return queryset
|
||||
|
||||
|
||||
class K8sAppPermissionUserGroupRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionUserGroupRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.user_groups.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', "usergroup", "k8sapppermission"
|
||||
]
|
||||
search_fields = ["usergroup__name", "k8sapppermission__name"]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset \
|
||||
.annotate(usergroup_display=F('usergroup__name'))
|
||||
return queryset
|
||||
|
||||
|
||||
class K8sAppPermissionAllUserListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.K8sAppPermissionAllUserSerializer
|
||||
filter_fields = ("username", "name")
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
perm = get_object_or_404(models.K8sAppPermission, pk=pk)
|
||||
users = perm.get_all_users().only(
|
||||
*self.serializer_class.Meta.only_fields
|
||||
)
|
||||
return users
|
||||
|
||||
|
||||
class K8sAppPermissionK8sAppRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionK8sAppRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.k8s_apps.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', 'k8sapp', 'k8sapppermission',
|
||||
]
|
||||
search_fields = [
|
||||
"id", "k8sapp__name", "k8sapppermission__name"
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset \
|
||||
.annotate(k8sapp_display=F('k8sapp__name'))
|
||||
return queryset
|
||||
|
||||
|
||||
class K8sAppPermissionAllK8sAppListApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
serializer_class = serializers.K8sAppPermissionAllK8sAppSerializer
|
||||
filter_fields = ("name",)
|
||||
search_fields = filter_fields
|
||||
|
||||
def get_queryset(self):
|
||||
pk = self.kwargs.get("pk")
|
||||
perm = get_object_or_404(models.K8sAppPermission, pk=pk)
|
||||
database_apps = perm.get_all_k8s_apps().only(
|
||||
*self.serializer_class.Meta.only_fields
|
||||
)
|
||||
return database_apps
|
||||
|
||||
|
||||
class K8sAppPermissionSystemUserRelationViewSet(RelationViewSet):
|
||||
serializer_class = serializers.K8sAppPermissionSystemUserRelationSerializer
|
||||
m2m_field = models.K8sAppPermission.system_users.field
|
||||
permission_classes = (IsOrgAdmin,)
|
||||
filter_fields = [
|
||||
'id', 'systemuser', 'k8sapppermission'
|
||||
]
|
||||
search_fields = [
|
||||
'k8sapppermission__name', 'systemuser__name', 'systemuser__username'
|
||||
]
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
queryset = queryset.annotate(
|
||||
systemuser_display=Concat(
|
||||
F('systemuser__name'), Value('('), F('systemuser__username'),
|
||||
Value(')')
|
||||
)
|
||||
)
|
||||
return queryset
|
|
@ -21,7 +21,7 @@ class UserPermissionMixin:
|
|||
obj = None
|
||||
|
||||
def initial(self, *args, **kwargs):
|
||||
super().initial(*args, *kwargs)
|
||||
super().initial(*args, **kwargs)
|
||||
self.obj = self.get_obj()
|
||||
|
||||
def get_obj(self):
|
||||
|
|
|
@ -0,0 +1,119 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
import uuid
|
||||
from django.shortcuts import get_object_or_404
|
||||
from rest_framework.views import APIView, Response
|
||||
from common.permissions import IsOrgAdminOrAppUser, IsValidUser
|
||||
from common.tree import TreeNodeSerializer
|
||||
from orgs.mixins import generics
|
||||
from users.models import User, UserGroup
|
||||
from applications.serializers import K8sAppSerializer
|
||||
from applications.models import K8sApp
|
||||
from assets.models import SystemUser
|
||||
from .. import utils, serializers
|
||||
from .mixin import UserPermissionMixin
|
||||
|
||||
|
||||
class UserGrantedK8sAppsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = K8sAppSerializer
|
||||
filter_fields = ['id', 'name', 'type', 'comment']
|
||||
search_fields = ['name', 'comment']
|
||||
|
||||
def get_object(self):
|
||||
user_id = self.kwargs.get('pk', '')
|
||||
if user_id:
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
else:
|
||||
user = self.request.user
|
||||
return user
|
||||
|
||||
def get_queryset(self):
|
||||
util = utils.K8sAppPermissionUtil(self.get_object())
|
||||
queryset = util.get_k8s_apps()
|
||||
return queryset
|
||||
|
||||
def get_permissions(self):
|
||||
if self.kwargs.get('pk') is None:
|
||||
self.permission_classes = (IsValidUser,)
|
||||
return super().get_permissions()
|
||||
|
||||
|
||||
class UserGrantedK8sAppsAsTreeApi(UserGrantedK8sAppsApi):
|
||||
serializer_class = TreeNodeSerializer
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def get_serializer(self, k8s_apps, *args, **kwargs):
|
||||
if k8s_apps is None:
|
||||
k8s_apps = []
|
||||
only_k8s_app = self.request.query_params.get('only', '0') == '1'
|
||||
tree_root = None
|
||||
data = []
|
||||
if not only_k8s_app:
|
||||
tree_root = utils.construct_k8s_apps_tree_root()
|
||||
data.append(tree_root)
|
||||
for k8s_app in k8s_apps:
|
||||
node = utils.parse_k8s_app_to_tree_node(tree_root, k8s_app)
|
||||
data.append(node)
|
||||
data.sort()
|
||||
return super().get_serializer(data, many=True)
|
||||
|
||||
|
||||
class UserGrantedK8sAppSystemUsersApi(UserPermissionMixin, generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = serializers.K8sAppSystemUserSerializer
|
||||
only_fields = serializers.K8sAppSystemUserSerializer.Meta.only_fields
|
||||
|
||||
def get_queryset(self):
|
||||
util = utils.K8sAppPermissionUtil(self.obj)
|
||||
k8s_app_id = self.kwargs.get('k8s_app_id')
|
||||
k8s_app = get_object_or_404(K8sApp, id=k8s_app_id)
|
||||
system_users = util.get_k8s_app_system_users(k8s_app)
|
||||
return system_users
|
||||
|
||||
|
||||
# Validate
|
||||
|
||||
class ValidateUserK8sAppPermissionApi(APIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
|
||||
def get(self, request, *args, **kwargs):
|
||||
user_id = request.query_params.get('user_id', '')
|
||||
k8s_app_id = request.query_params.get('k8s_app_id', '')
|
||||
system_user_id = request.query_params.get('system_user_id', '')
|
||||
|
||||
try:
|
||||
user_id = uuid.UUID(user_id)
|
||||
k8s_app_id = uuid.UUID(k8s_app_id)
|
||||
system_user_id = uuid.UUID(system_user_id)
|
||||
except ValueError:
|
||||
return Response({'msg': False}, status=403)
|
||||
|
||||
user = get_object_or_404(User, id=user_id)
|
||||
k8s_app = get_object_or_404(K8sApp, id=k8s_app_id)
|
||||
system_user = get_object_or_404(SystemUser, id=system_user_id)
|
||||
|
||||
util = utils.K8sAppPermissionUtil(user)
|
||||
system_users = util.get_k8s_app_system_users(k8s_app)
|
||||
if system_user in system_users:
|
||||
return Response({'msg': True}, status=200)
|
||||
|
||||
return Response({'msg': False}, status=403)
|
||||
|
||||
|
||||
# UserGroup
|
||||
|
||||
class UserGroupGrantedK8sAppsApi(generics.ListAPIView):
|
||||
permission_classes = (IsOrgAdminOrAppUser,)
|
||||
serializer_class = K8sAppSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = []
|
||||
user_group_id = self.kwargs.get('pk')
|
||||
if not user_group_id:
|
||||
return queryset
|
||||
user_group = get_object_or_404(UserGroup, id=user_group_id)
|
||||
util = utils.K8sAppPermissionUtil(user_group)
|
||||
queryset = util.get_k8s_apps()
|
||||
return queryset
|
|
@ -0,0 +1,28 @@
|
|||
# Generated by Django 2.2.10 on 2020-07-21 09:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
from django.db.models import F
|
||||
from ..models.asset_permission import Action
|
||||
|
||||
|
||||
def migrate_asset_permission(apps, schema_editor):
|
||||
# 已有的资产权限默认拥有剪切板复制粘贴动作
|
||||
AssetPermission = apps.get_model('perms', 'AssetPermission')
|
||||
AssetPermission.objects.all().update(actions=F('actions').bitor(Action.CLIPBOARD_COPY_PASTE))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('perms', '0010_auto_20191218_1705'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='assetpermission',
|
||||
name='actions',
|
||||
field=models.IntegerField(choices=[(255, 'All'), (1, 'Connect'), (2, 'Upload file'), (4, 'Download file'), (6, 'Upload download'), (8, 'Clipboard copy'), (16, 'Clipboard paste'), (24, 'Clipboard copy paste')], default=255, verbose_name='Actions'),
|
||||
),
|
||||
migrations.RunPython(migrate_asset_permission)
|
||||
]
|
|
@ -0,0 +1,44 @@
|
|||
# Generated by Django 2.2.13 on 2020-08-07 07:13
|
||||
|
||||
import common.utils.django
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('assets', '0054_auto_20200807_1032'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('applications', '0005_k8sapp'),
|
||||
('users', '0028_auto_20200728_1805'),
|
||||
('perms', '0011_auto_20200721_1739'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='K8sAppPermission',
|
||||
fields=[
|
||||
('org_id', models.CharField(blank=True, db_index=True, default='', max_length=36, verbose_name='Organization')),
|
||||
('id', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=128, verbose_name='Name')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Active')),
|
||||
('date_start', models.DateTimeField(db_index=True, default=django.utils.timezone.now, verbose_name='Date start')),
|
||||
('date_expired', models.DateTimeField(db_index=True, default=common.utils.django.date_expired_default, verbose_name='Date expired')),
|
||||
('created_by', models.CharField(blank=True, max_length=128, verbose_name='Created by')),
|
||||
('date_created', models.DateTimeField(auto_now_add=True, verbose_name='Date created')),
|
||||
('comment', models.TextField(blank=True, verbose_name='Comment')),
|
||||
('k8s_apps', models.ManyToManyField(blank=True, related_name='granted_by_permissions', to='applications.K8sApp', verbose_name='KubernetesApp')),
|
||||
('system_users', models.ManyToManyField(related_name='granted_by_k8s_app_permissions', to='assets.SystemUser', verbose_name='System user')),
|
||||
('user_groups', models.ManyToManyField(blank=True, to='users.UserGroup', verbose_name='User group')),
|
||||
('users', models.ManyToManyField(blank=True, to=settings.AUTH_USER_MODEL, verbose_name='User')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'KubernetesApp permission',
|
||||
'ordering': ('name',),
|
||||
'unique_together': {('org_id', 'name')},
|
||||
},
|
||||
),
|
||||
]
|
|
@ -4,3 +4,4 @@
|
|||
from .asset_permission import *
|
||||
from .remote_app_permission import *
|
||||
from .database_app_permission import *
|
||||
from .k8s_app_permission import *
|
||||
|
|
|
@ -21,11 +21,15 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
class Action:
|
||||
NONE = 0
|
||||
CONNECT = 0b00000001
|
||||
UPLOAD = 0b00000010
|
||||
DOWNLOAD = 0b00000100
|
||||
|
||||
CONNECT = 0b1
|
||||
UPLOAD = 0b1 << 1
|
||||
DOWNLOAD = 0b1 << 2
|
||||
CLIPBOARD_COPY = 0b1 << 3
|
||||
CLIPBOARD_PASTE = 0b1 << 4
|
||||
ALL = 0xff
|
||||
UPDOWNLOAD = UPLOAD | DOWNLOAD
|
||||
ALL = 0b11111111
|
||||
CLIPBOARD_COPY_PASTE = CLIPBOARD_COPY | CLIPBOARD_PASTE
|
||||
|
||||
DB_CHOICES = (
|
||||
(ALL, _('All')),
|
||||
|
@ -33,6 +37,9 @@ class Action:
|
|||
(UPLOAD, _('Upload file')),
|
||||
(DOWNLOAD, _('Download file')),
|
||||
(UPDOWNLOAD, _("Upload download")),
|
||||
(CLIPBOARD_COPY, _('Clipboard copy')),
|
||||
(CLIPBOARD_PASTE, _('Clipboard paste')),
|
||||
(CLIPBOARD_COPY_PASTE, _('Clipboard copy paste'))
|
||||
)
|
||||
|
||||
NAME_MAP = {
|
||||
|
@ -41,9 +48,12 @@ class Action:
|
|||
UPLOAD: "upload_file",
|
||||
DOWNLOAD: "download_file",
|
||||
UPDOWNLOAD: "updownload",
|
||||
CLIPBOARD_COPY: 'clipboard_copy',
|
||||
CLIPBOARD_PASTE: 'clipboard_paste',
|
||||
CLIPBOARD_COPY_PASTE: 'clipboard_copy_paste'
|
||||
}
|
||||
|
||||
NAME_MAP_REVERSE = dict({v: k for k, v in NAME_MAP.items()})
|
||||
NAME_MAP_REVERSE = {v: k for k, v in NAME_MAP.items()}
|
||||
CHOICES = []
|
||||
for i, j in DB_CHOICES:
|
||||
CHOICES.append((NAME_MAP[i], j))
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from common.utils import lazyproperty
|
||||
from .base import BasePermission
|
||||
|
||||
__all__ = [
|
||||
'K8sAppPermission',
|
||||
]
|
||||
|
||||
|
||||
class K8sAppPermission(BasePermission):
|
||||
k8s_apps = models.ManyToManyField(
|
||||
'applications.K8sApp', related_name='granted_by_permissions',
|
||||
blank=True, verbose_name=_("KubernetesApp")
|
||||
)
|
||||
system_users = models.ManyToManyField(
|
||||
'assets.SystemUser', related_name='granted_by_k8s_app_permissions',
|
||||
verbose_name=_("System user")
|
||||
)
|
||||
|
||||
class Meta:
|
||||
unique_together = [('org_id', 'name')]
|
||||
verbose_name = _('KubernetesApp permission')
|
||||
ordering = ('name',)
|
||||
|
||||
def get_all_k8s_apps(self):
|
||||
return self.k8s_apps.all()
|
||||
|
||||
@lazyproperty
|
||||
def k8s_apps_amount(self):
|
||||
return self.k8s_apps.count()
|
||||
|
||||
@lazyproperty
|
||||
def system_users_amount(self):
|
||||
return self.system_users.count()
|
|
@ -9,3 +9,5 @@ from .asset_permission_relation import *
|
|||
from .database_app_permission import *
|
||||
from .database_app_permission_relation import *
|
||||
from .base import *
|
||||
from .k8s_app_permission import *
|
||||
from .k8s_app_permission_relation import *
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
from django.db.models import Count
|
||||
from rest_framework import serializers
|
||||
|
||||
from orgs.mixins.serializers import BulkOrgResourceModelSerializer
|
||||
from .. import models
|
||||
|
||||
__all__ = [
|
||||
'K8sAppPermissionSerializer', 'K8sAppPermissionListSerializer'
|
||||
]
|
||||
|
||||
|
||||
class AmountMixin:
|
||||
@classmethod
|
||||
def setup_eager_loading(cls, queryset):
|
||||
""" Perform necessary eager loading of data. """
|
||||
queryset = queryset.annotate(
|
||||
users_amount=Count('users', distinct=True), user_groups_amount=Count('user_groups', distinct=True),
|
||||
k8s_apps_amount=Count('k8s_apps', distinct=True),
|
||||
system_users_amount=Count('system_users', distinct=True)
|
||||
)
|
||||
return queryset
|
||||
|
||||
|
||||
class K8sAppPermissionSerializer(AmountMixin, BulkOrgResourceModelSerializer):
|
||||
class Meta:
|
||||
model = models.K8sAppPermission
|
||||
fields = [
|
||||
'id', 'name', 'users', 'user_groups', 'k8s_apps', 'system_users',
|
||||
'comment', 'is_active', 'date_start', 'date_expired', 'is_valid',
|
||||
'created_by', 'date_created', 'users_amount', 'user_groups_amount',
|
||||
'k8s_apps_amount', 'system_users_amount',
|
||||
]
|
||||
read_only_fields = [
|
||||
'created_by', 'date_created', 'users_amount', 'user_groups_amount',
|
||||
'k8s_apps_amount', 'system_users_amount', 'id'
|
||||
]
|
||||
|
||||
|
||||
class K8sAppPermissionListSerializer(AmountMixin, BulkOrgResourceModelSerializer):
|
||||
is_expired = serializers.BooleanField()
|
||||
|
||||
class Meta:
|
||||
model = models.K8sAppPermission
|
||||
fields = [
|
||||
'id', 'name', 'comment', 'is_active', 'users_amount', 'user_groups_amount',
|
||||
'date_start', 'date_expired', 'is_valid', 'k8s_apps_amount', 'system_users_amount',
|
||||
'created_by', 'date_created', 'is_expired'
|
||||
]
|
|
@ -0,0 +1,73 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
from perms.serializers.base import PermissionAllUserSerializer
|
||||
from rest_framework import serializers
|
||||
|
||||
from common.drf.serializers import BulkModelSerializer
|
||||
|
||||
from .. import models
|
||||
|
||||
|
||||
class K8sAppPermissionUserRelationSerializer(BulkModelSerializer):
|
||||
user_display = serializers.ReadOnlyField()
|
||||
k8sapppermission_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = models.K8sAppPermission.users.through
|
||||
fields = [
|
||||
'id', 'user', 'user_display', 'k8sapppermission',
|
||||
'k8sapppermission_display'
|
||||
]
|
||||
|
||||
|
||||
class K8sAppPermissionUserGroupRelationSerializer(BulkModelSerializer):
|
||||
usergroup_display = serializers.ReadOnlyField()
|
||||
k8sapppermission_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = models.K8sAppPermission.user_groups.through
|
||||
fields = [
|
||||
'id', 'usergroup', 'usergroup_display', 'k8sapppermission',
|
||||
'k8sapppermission_display'
|
||||
]
|
||||
|
||||
|
||||
class K8sAppPermissionAllUserSerializer(PermissionAllUserSerializer):
|
||||
class Meta(PermissionAllUserSerializer.Meta):
|
||||
pass
|
||||
|
||||
|
||||
class K8sAppPermissionK8sAppRelationSerializer(BulkModelSerializer):
|
||||
k8sapp_display = serializers.ReadOnlyField()
|
||||
k8sapppermission_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = models.K8sAppPermission.k8s_apps.through
|
||||
fields = [
|
||||
'id', "k8sapp", "k8sapp_display", 'k8sapppermission',
|
||||
'k8sapppermission_display'
|
||||
]
|
||||
|
||||
|
||||
class K8sAppPermissionAllK8sAppSerializer(serializers.Serializer):
|
||||
k8sapp = serializers.UUIDField(read_only=True, source='id')
|
||||
k8sapp_display = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
only_fields = ['id', 'name']
|
||||
|
||||
@staticmethod
|
||||
def get_k8sapp_display(obj):
|
||||
return str(obj)
|
||||
|
||||
|
||||
class K8sAppPermissionSystemUserRelationSerializer(BulkModelSerializer):
|
||||
systemuser_display = serializers.ReadOnlyField()
|
||||
k8sapppermission_display = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = models.K8sAppPermission.system_users.through
|
||||
fields = [
|
||||
'id', 'systemuser', 'systemuser_display', 'k8sapppermission',
|
||||
'k8sapppermission_display'
|
||||
]
|
|
@ -14,6 +14,7 @@ __all__ = [
|
|||
'ActionsSerializer', 'AssetSystemUserSerializer',
|
||||
'RemoteAppSystemUserSerializer',
|
||||
'DatabaseAppSystemUserSerializer',
|
||||
'K8sAppSystemUserSerializer',
|
||||
]
|
||||
|
||||
|
||||
|
@ -53,6 +54,16 @@ class DatabaseAppSystemUserSerializer(serializers.ModelSerializer):
|
|||
read_only_fields = fields
|
||||
|
||||
|
||||
class K8sAppSystemUserSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SystemUser
|
||||
only_fields = (
|
||||
'id', 'name', 'username', 'priority', 'protocol', 'login_mode',
|
||||
)
|
||||
fields = list(only_fields)
|
||||
read_only_fields = fields
|
||||
|
||||
|
||||
class AssetGrantedSerializer(serializers.ModelSerializer):
|
||||
"""
|
||||
被授权资产的数据结构
|
||||
|
|
|
@ -6,6 +6,7 @@ from .asset_permission import asset_permission_urlpatterns
|
|||
from .remote_app_permission import remote_app_permission_urlpatterns
|
||||
from .database_app_permission import database_app_permission_urlpatterns
|
||||
from .system_user_permission import system_users_permission_urlpatterns
|
||||
from .k8s_app_permission import k8s_app_permission_urlpatterns
|
||||
|
||||
app_name = 'perms'
|
||||
|
||||
|
@ -16,5 +17,6 @@ old_version_urlpatterns = [
|
|||
urlpatterns = asset_permission_urlpatterns + \
|
||||
remote_app_permission_urlpatterns + \
|
||||
database_app_permission_urlpatterns + \
|
||||
k8s_app_permission_urlpatterns + \
|
||||
old_version_urlpatterns + \
|
||||
system_users_permission_urlpatterns
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.urls import path, include
|
||||
from rest_framework_bulk.routes import BulkRouter
|
||||
from .. import api
|
||||
|
||||
|
||||
router = BulkRouter()
|
||||
router.register('k8s-app-permissions', api.K8sAppPermissionViewSet, 'k8s-app-permission')
|
||||
router.register('k8s-app-permissions-users-relations', api.K8sAppPermissionUserRelationViewSet, 'k8s-app-permissions-users-relation')
|
||||
router.register('k8s-app-permissions-user-groups-relations', api.K8sAppPermissionUserGroupRelationViewSet, 'k8s-app-permissions-user-groups-relation')
|
||||
router.register('k8s-app-permissions-k8s-apps-relations', api.K8sAppPermissionK8sAppRelationViewSet, 'k8s-app-permissions-k8s-apps-relation')
|
||||
router.register('k8s-app-permissions-system-users-relations', api.K8sAppPermissionSystemUserRelationViewSet, 'k8s-app-permissions-system-users-relation')
|
||||
|
||||
user_permission_urlpatterns = [
|
||||
path('<uuid:pk>/k8s-apps/', api.UserGrantedK8sAppsApi.as_view(), name='user-k8s-apps'),
|
||||
path('k8s-apps/', api.UserGrantedK8sAppsApi.as_view(), name='my-k8s-apps'),
|
||||
|
||||
# k8sApps as tree
|
||||
path('<uuid:pk>/k8s-apps/tree/', api.UserGrantedK8sAppsAsTreeApi.as_view(), name='user-k8ss-apps-tree'),
|
||||
path('k8s-apps/tree/', api.UserGrantedK8sAppsAsTreeApi.as_view(), name='my-k8ss-apps-tree'),
|
||||
|
||||
path('<uuid:pk>/k8s-apps/<uuid:k8s_app_id>/system-users/', api.UserGrantedK8sAppSystemUsersApi.as_view(), name='user-k8s-app-system-users'),
|
||||
path('k8s-apps/<uuid:k8s_app_id>/system-users/', api.UserGrantedK8sAppSystemUsersApi.as_view(), name='user-k8s-app-system-users'),
|
||||
]
|
||||
|
||||
user_group_permission_urlpatterns = [
|
||||
path('<uuid:pk>/k8s-apps/', api.UserGroupGrantedK8sAppsApi.as_view(), name='user-group-k8s-apps'),
|
||||
]
|
||||
|
||||
permission_urlpatterns = [
|
||||
path('<uuid:pk>/users/all/', api.K8sAppPermissionAllUserListApi.as_view(), name='k8s-app-permission-all-users'),
|
||||
path('<uuid:pk>/k8s-apps/all/', api.K8sAppPermissionAllK8sAppListApi.as_view(), name='k8s-app-permission-all-k8s-apps'),
|
||||
|
||||
path('user/validate/', api.ValidateUserK8sAppPermissionApi.as_view(), name='validate-user-k8s-app-permission'),
|
||||
]
|
||||
|
||||
k8s_app_permission_urlpatterns = [
|
||||
path('users/', include(user_permission_urlpatterns)),
|
||||
path('user-groups/', include(user_group_permission_urlpatterns)),
|
||||
path('k8s-app-permissions/', include(permission_urlpatterns))
|
||||
]
|
||||
|
||||
k8s_app_permission_urlpatterns += router.urls
|
|
@ -4,3 +4,4 @@
|
|||
from .asset_permission import *
|
||||
from .remote_app_permission import *
|
||||
from .database_app_permission import *
|
||||
from .k8s_app_permission import *
|
|
@ -0,0 +1,93 @@
|
|||
# coding: utf-8
|
||||
#
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.db.models import Q
|
||||
|
||||
from orgs.utils import set_to_root_org
|
||||
from ..models import K8sAppPermission
|
||||
from common.tree import TreeNode
|
||||
from applications.models import K8sApp
|
||||
from assets.models import SystemUser
|
||||
|
||||
|
||||
def get_user_k8s_app_permissions(user, include_group=True):
|
||||
if include_group:
|
||||
groups = user.groups.all()
|
||||
arg = Q(users=user) | Q(user_groups__in=groups)
|
||||
else:
|
||||
arg = Q(users=user)
|
||||
return K8sAppPermission.objects.all().valid().filter(arg)
|
||||
|
||||
|
||||
def get_user_group_k8s_app_permission(user_group):
|
||||
return K8sAppPermission.objects.all().valid().filter(
|
||||
user_groups=user_group
|
||||
)
|
||||
|
||||
|
||||
class K8sAppPermissionUtil:
|
||||
get_permissions_map = {
|
||||
'User': get_user_k8s_app_permissions,
|
||||
'UserGroup': get_user_group_k8s_app_permission
|
||||
}
|
||||
|
||||
def __init__(self, obj):
|
||||
self.object = obj
|
||||
self.change_org_if_need()
|
||||
|
||||
@staticmethod
|
||||
def change_org_if_need():
|
||||
set_to_root_org()
|
||||
|
||||
@property
|
||||
def permissions(self):
|
||||
obj_class = self.object.__class__.__name__
|
||||
func = self.get_permissions_map[obj_class]
|
||||
_permissions = func(self.object)
|
||||
return _permissions
|
||||
|
||||
def get_k8s_apps(self):
|
||||
k8s_apps = K8sApp.objects.filter(
|
||||
granted_by_permissions__in=self.permissions
|
||||
).distinct()
|
||||
return k8s_apps
|
||||
|
||||
def get_k8s_app_system_users(self, k8s_app):
|
||||
queryset = self.permissions
|
||||
kwargs = {'k8s_apps': k8s_app}
|
||||
queryset = queryset.filter(**kwargs)
|
||||
system_users_ids = queryset.values_list('system_users', flat=True)
|
||||
system_users_ids = system_users_ids.distinct()
|
||||
system_users = SystemUser.objects.filter(id__in=system_users_ids)
|
||||
system_users = system_users.order_by('-priority')
|
||||
return system_users
|
||||
|
||||
|
||||
def construct_k8s_apps_tree_root():
|
||||
tree_root = {
|
||||
'id': 'ID_K8S_APP_ROOT',
|
||||
'name': _('KubernetesApp'),
|
||||
'title': 'K8sApp',
|
||||
'pId': '',
|
||||
'open': False,
|
||||
'isParent': True,
|
||||
'iconSkin': '',
|
||||
'meta': {'type': 'k8s_app'}
|
||||
}
|
||||
return TreeNode(**tree_root)
|
||||
|
||||
|
||||
def parse_k8s_app_to_tree_node(parent, k8s_app):
|
||||
pid = parent.id if parent else ''
|
||||
tree_node = {
|
||||
'id': k8s_app.id,
|
||||
'name': k8s_app.name,
|
||||
'title': k8s_app.name,
|
||||
'pId': pid,
|
||||
'open': False,
|
||||
'isParent': False,
|
||||
'iconSkin': 'file',
|
||||
'meta': {'type': 'k8s_app'}
|
||||
}
|
||||
return TreeNode(**tree_node)
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue