diff --git a/apps/applications/serializers.py b/apps/applications/serializers.py index 31d67418d..56f05e67c 100644 --- a/apps/applications/serializers.py +++ b/apps/applications/serializers.py @@ -14,8 +14,9 @@ class TerminalSerializer(serializers.ModelSerializer): class Meta: model = Terminal - fields = ['id', 'name', 'remote_addr', 'type', 'url', 'comment', 'is_accepted', - 'is_active', 'get_type_display', 'proxy_online', 'is_alive'] + fields = ['id', 'name', 'remote_addr', 'type', 'url', 'comment', + 'is_accepted', 'is_active', 'get_type_display', + 'proxy_online', 'is_alive'] @staticmethod def get_proxy_online(obj): @@ -31,6 +32,7 @@ class TerminalSerializer(serializers.ModelSerializer): class TerminalHeatbeatSerializer(serializers.ModelSerializer): + date_start = serializers.DateTimeField class Meta: model = TerminalHeatbeat diff --git a/apps/assets/serializers.py b/apps/assets/serializers.py index 87fec5767..a9c9c41a8 100644 --- a/apps/assets/serializers.py +++ b/apps/assets/serializers.py @@ -99,6 +99,12 @@ class SystemUserSerializer(serializers.ModelSerializer): return fields +class AssetSystemUserSerializer(serializers.ModelSerializer): + class Meta: + model = SystemUser + fields = ('id', 'name', 'username', 'protocol', 'auth_method', 'comment') + + class SystemUserUpdateAssetsSerializer(serializers.ModelSerializer): assets = serializers.PrimaryKeyRelatedField(many=True, queryset=Asset.objects.all()) @@ -145,13 +151,13 @@ class AssetSerializer(BulkSerializerMixin, serializers.ModelSerializer): class AssetGrantedSerializer(serializers.ModelSerializer): - system_users = SystemUserSerializer(many=True, read_only=True) + system_users_granted = AssetSystemUserSerializer(many=True, read_only=True) is_inherited = serializers.SerializerMethodField() system_users_join = serializers.SerializerMethodField() class Meta(object): model = Asset - fields = ("id", "hostname", "ip", "port", "system_users", "is_inherited", + fields = ("id", "hostname", "ip", "port", "system_users_granted", "is_inherited", "is_active", "system_users_join", "comment") @staticmethod @@ -163,7 +169,7 @@ class AssetGrantedSerializer(serializers.ModelSerializer): @staticmethod def get_system_users_join(obj): - return ', '.join([system_user.username for system_user in obj.system_users.all()]) + return ', '.join([system_user.username for system_user in obj.system_users_granted]) class IDCSerializer(BulkSerializerMixin, serializers.ModelSerializer): diff --git a/apps/audits/models.py b/apps/audits/models.py index 7e8cbeb03..a6b1c1133 100644 --- a/apps/audits/models.py +++ b/apps/audits/models.py @@ -17,10 +17,13 @@ class LoginLog(models.Model): username = models.CharField(max_length=20, verbose_name=_('Username')) name = models.CharField(max_length=20, blank=True, verbose_name=_('Name')) - login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, verbose_name=_('Login type')) + login_type = models.CharField(choices=LOGIN_TYPE_CHOICE, max_length=2, + verbose_name=_('Login type')) login_ip = models.GenericIPAddressField(verbose_name=_('Login ip')) - login_city = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Login city')) - user_agent = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('User agent')) + login_city = models.CharField(max_length=100, blank=True, null=True, + verbose_name=_('Login city')) + user_agent = models.CharField(max_length=100, blank=True, null=True, + verbose_name=_('User agent')) date_login = models.DateTimeField(auto_now_add=True, verbose_name=_('Date login')) class Meta: @@ -66,7 +69,8 @@ class ProxyLog(models.Model): class CommandLog(models.Model): - proxy_log = models.ForeignKey(ProxyLog, on_delete=models.CASCADE, related_name='commands') + proxy_log = models.ForeignKey(ProxyLog, on_delete=models.CASCADE, + related_name='commands') command_no = models.IntegerField() command = models.CharField(max_length=1000, blank=True) output = models.TextField(blank=True) @@ -78,7 +82,8 @@ class CommandLog(models.Model): @property def output_decode(self): try: - return base64.b64decode(self.output).replace('\n', '
') + return base64.b64decode(self.output).decode('utf-8') \ + .replace('\n', '
') except UnicodeDecodeError: return 'UnicodeDecodeError' diff --git a/apps/audits/serializers.py b/apps/audits/serializers.py index 4d02ef5b4..7212c0d97 100644 --- a/apps/audits/serializers.py +++ b/apps/audits/serializers.py @@ -13,8 +13,9 @@ class ProxyLogSerializer(serializers.ModelSerializer): class Meta: model = models.ProxyLog - fields = ['id', 'name', 'username', 'hostname', 'ip', 'system_user', 'login_type', 'terminal', - 'log_file', 'was_failed', 'is_finished', 'date_start', 'date_finished', 'time', + fields = ['id', 'name', 'username', 'hostname', 'ip', 'system_user', + 'login_type', 'terminal', 'log_file', 'was_failed', + 'is_finished', 'date_start', 'date_finished', 'time', 'command_length', "commands_dict"] @staticmethod @@ -32,3 +33,4 @@ class ProxyLogSerializer(serializers.ModelSerializer): class CommandLogSerializer(serializers.ModelSerializer): class Meta: model = models.CommandLog + fields = '__all__' diff --git a/apps/audits/templates/audits/command_log_list.html b/apps/audits/templates/audits/command_log_list.html index 03b786f3f..5e0cf0752 100644 --- a/apps/audits/templates/audits/command_log_list.html +++ b/apps/audits/templates/audits/command_log_list.html @@ -83,7 +83,7 @@ {{ command.proxy_log.system_user }} {{ command.proxy_log.id}} {{ command.datetime }} - {{ command.output_decode |safe }} + {{ command.output_decode|safe }} {% endfor %} diff --git a/apps/audits/templates/audits/proxy_log_detail.html b/apps/audits/templates/audits/proxy_log_detail.html index 8d2724586..05b06071c 100644 --- a/apps/audits/templates/audits/proxy_log_detail.html +++ b/apps/audits/templates/audits/proxy_log_detail.html @@ -52,7 +52,7 @@ {{ command.command_no }} {{ command.command }} - {{ command.output_decode |safe }} + {{ command.output_decode|safe }} {{ command.datetime }} {% endfor %} @@ -68,30 +68,6 @@ -
-
-
- {% trans 'Detail' %} {{ user_object.name }} -
- - - - - - - - - - -
-
-
- -
-
-
-
diff --git a/apps/audits/views.py b/apps/audits/views.py index 61fb1ff97..0c0c7cf55 100644 --- a/apps/audits/views.py +++ b/apps/audits/views.py @@ -14,17 +14,16 @@ from .models import ProxyLog, CommandLog, LoginLog from .hands import User, Asset, SystemUser, AdminUserRequiredMixin -date_now = timezone.localtime(timezone.now()) -now_s = date_now.strftime('%m/%d/%Y') -seven_days_ago_s = (date_now-timezone.timedelta(7)).strftime('%m/%d/%Y') - - class ProxyLogListView(AdminUserRequiredMixin, ListView): model = ProxyLog template_name = 'audits/proxy_log_list.html' context_object_name = 'proxy_log_list' def get_queryset(self): + date_now = timezone.localtime(timezone.now()) + now_s = date_now.strftime('%m/%d/%Y') + seven_days_ago_s = (date_now-timezone.timedelta(7)).strftime('%m/%d/%Y') + self.queryset = super(ProxyLogListView, self).get_queryset() self.keyword = keyword = self.request.GET.get('keyword', '') self.username = username = self.request.GET.get('username', '') @@ -37,7 +36,8 @@ class ProxyLogListView(AdminUserRequiredMixin, ListView): date_from = timezone.datetime.strptime(date_from_s, '%m/%d/%Y') self.queryset = self.queryset.filter(date_start__gt=date_from) if date_to_s: - date_to = timezone.datetime.strptime(date_to_s + ' 23:59:59', '%m/%d/%Y %H:%M:%S') + date_to = timezone.datetime.strptime(date_to_s + ' 23:59:59', + '%m/%d/%Y %H:%M:%S') self.queryset = self.queryset.filter(date_start__lt=date_to) if username: self.queryset = self.queryset.filter(username=username) @@ -54,7 +54,6 @@ class ProxyLogListView(AdminUserRequiredMixin, ListView): return self.queryset def get_context_data(self, **kwargs): - print(self.date_to_s) context = { 'app': _('Audits'), 'action': _('Proxy log list'), @@ -110,6 +109,9 @@ class CommandLogListView(AdminUserRequiredMixin, ListView): context_object_name = 'command_list' def get_queryset(self): + date_now = timezone.localtime(timezone.now()) + now_s = date_now.strftime('%m/%d/%Y') + seven_days_ago_s = (date_now-timezone.timedelta(7)).strftime('%m/%d/%Y') self.queryset = super(CommandLogListView, self).get_queryset() self.keyword = keyword = self.request.GET.get('keyword', '') self.sort = sort = self.request.GET.get('sort', '-datetime') @@ -161,6 +163,9 @@ class LoginLogListView(AdminUserRequiredMixin, ListView): context_object_name = 'login_log_list' def get_queryset(self): + date_now = timezone.localtime(timezone.now()) + now_s = date_now.strftime('%m/%d/%Y') + seven_days_ago_s = (date_now - timezone.timedelta(7)).strftime('%m/%d/%Y') self.queryset = super(LoginLogListView, self).get_queryset() self.keyword = keyword = self.request.GET.get('keyword', '') self.username = username = self.request.GET.get('username', '') diff --git a/apps/common/celery.py b/apps/common/celery.py index f4ea048e5..51de45349 100644 --- a/apps/common/celery.py +++ b/apps/common/celery.py @@ -16,5 +16,6 @@ app = Celery('jumpserver') # Using a string here means the worker will not have to # pickle the object when using Windows. app.config_from_object('django.conf:settings') -app.autodiscover_tasks(lambda: [app_config.split('.')[0] for app_config in settings.INSTALLED_APPS]) +app.autodiscover_tasks(lambda: [app_config.split('.')[0] + for app_config in settings.INSTALLED_APPS]) diff --git a/apps/perms/api.py b/apps/perms/api.py index 7b50c4a9d..ce0193b89 100644 --- a/apps/perms/api.py +++ b/apps/perms/api.py @@ -1,16 +1,19 @@ # ~*~ coding: utf-8 ~*~ # +from django.shortcuts import get_object_or_404 from rest_framework.views import APIView, Response from rest_framework.decorators import api_view from rest_framework.generics import ListAPIView, get_object_or_404 from rest_framework import viewsets -from users.permissions import IsValidUser, IsSuperUser +from users.permissions import IsValidUser, IsSuperUser, IsAppUser from common.utils import get_object_or_none -from .utils import get_user_granted_assets, get_user_granted_asset_groups, get_user_asset_permissions, \ - get_user_group_asset_permissions, get_user_group_granted_assets, get_user_group_granted_asset_groups +from .utils import get_user_granted_assets, get_user_granted_asset_groups, \ + get_user_asset_permissions, get_user_group_asset_permissions, \ + get_user_group_granted_assets, get_user_group_granted_asset_groups from .models import AssetPermission -from .hands import AssetGrantedSerializer, User, UserGroup, AssetGroup, Asset, AssetGroup, AssetGroupSerializer +from .hands import AssetGrantedSerializer, User, UserGroup, AssetGroup, Asset, \ + AssetGroup, AssetGroupSerializer, SystemUser from . import serializers @@ -80,11 +83,12 @@ class UserGrantedAssetsApi(ListAPIView): def get_queryset(self): user_id = self.kwargs.get('pk', '') + queryset = [] if user_id: user = get_object_or_404(User, id=user_id) - queryset = get_user_granted_assets(user) - else: - queryset = [] + for k, v in get_user_granted_assets(user).items(): + k.system_users_granted = v + queryset.append(k) return queryset @@ -104,19 +108,26 @@ class UserGrantedAssetGroupsApi(ListAPIView): class MyGrantedAssetsApi(ListAPIView): + """授权给用户的资产列表 + [{'hostname': 'x','ip': 'x', .., + 'system_users_granted': [{'name': 'x', .}, ...] + """ permission_classes = (IsValidUser,) serializer_class = AssetGrantedSerializer def get_queryset(self): + queryset = [] user = self.request.user if user: - queryset = get_user_granted_assets(user) - else: - queryset = [] + for asset, system_users in get_user_granted_assets(user).items(): + asset.system_users_granted = system_users + queryset.append(asset) return queryset class MyGrantedAssetsGroupsApi(APIView): + """授权给用户的资产组列表, 非直接通过授权规则授权的资产组列表, 而是授权资产的所有 + 资产组之和""" permission_classes = (IsValidUser,) def get(self, request, *args, **kwargs): @@ -141,6 +152,7 @@ class MyGrantedAssetsGroupsApi(APIView): class MyAssetGroupAssetsApi(ListAPIView): + """授权用户资产组下的资产列表, 非该资产组的所有资产,而是被授权的""" permission_classes = (IsValidUser,) serializer_class = AssetGrantedSerializer @@ -152,8 +164,9 @@ class MyAssetGroupAssetsApi(ListAPIView): if user and asset_group: assets = get_user_granted_assets(user) - for asset in assets: - if asset_group in asset.groups.all(): + for asset in asset_group.assets.all(): + if asset in assets: + asset.system_users_granted = assets[asset] queryset.append(asset) return queryset @@ -185,4 +198,24 @@ class UserGroupGrantedAssetGroupsApi(ListAPIView): queryset = get_user_group_granted_asset_groups(user_group) else: queryset = [] - return queryset \ No newline at end of file + return queryset + + +class CheckUserAssetSystemPermission(APIView): + permission_classes = (IsAppUser,) + + def get(self, request): + user_id = request.params.get('user_id', '') + asset_id = request.params.get('asset_id', '') + system_id = request.params.get('system_id', '') + + user = get_object_or_none(User, id=user_id) + asset = get_object_or_none(Asset, id=asset_id) + system_user = get_object_or_none(SystemUser, id=system_id) + + if not (user and asset and system_user): + return Response(status=403) + + assets_granted = get_user_granted_assets(user) + + diff --git a/apps/perms/utils.py b/apps/perms/utils.py index 585b65808..f01ae1779 100644 --- a/apps/perms/utils.py +++ b/apps/perms/utils.py @@ -37,6 +37,8 @@ def get_user_group_granted_assets(user_group): if not asset_permission.is_valid: continue for asset in asset_permission.get_granted_assets(): + if not asset.is_active: + continue if asset in assets: assets[asset] |= set(asset_permission.system_users.all()) else: @@ -127,6 +129,8 @@ def get_user_granted_assets_direct(user): if not asset_permission.is_valid: continue for asset in asset_permission.get_granted_assets(): + if not asset.is_active: + continue if asset in assets: assets[asset] |= set(asset_permission.system_users.all()) else: @@ -147,12 +151,13 @@ def get_user_granted_assets_inherit_from_user_groups(user): for user_group in user_groups: assets_inherited = get_user_group_granted_assets(user_group) for asset in assets_inherited: + if not asset.is_active: + continue if asset in assets: assets[asset] |= assets_inherited[asset] else: setattr(asset, 'inherited', True) assets[asset] = assets_inherited[asset] - return assets @@ -167,6 +172,8 @@ def get_user_granted_assets(user): assets = assets_inherited for asset in assets_direct: + if not asset.is_active: + continue if asset in assets: assets[asset] |= assets_direct[asset] else: diff --git a/apps/users/api.py b/apps/users/api.py index a6c2c24b5..393a5f15e 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -3,23 +3,22 @@ import base64 -from django.core.cache import cache -from django.conf import settings from rest_framework import generics, viewsets from rest_framework.response import Response from rest_framework.views import APIView -from rest_framework.decorators import api_view from rest_framework.permissions import AllowAny -from rest_framework.authentication import SessionAuthentication from rest_framework_bulk import BulkModelViewSet +from rest_framework.authentication import CSRFCheck # from django_filters.rest_framework import DjangoFilterBackend +from django.conf import settings from common.mixins import IDInFilterMixin from common.utils import get_logger from .utils import check_user_valid, generate_token from .models import User, UserGroup from .hands import write_login_log_async -from .permissions import IsSuperUser, IsAppUser, IsValidUser, IsSuperUserOrAppUser +from .permissions import ( + IsSuperUser, IsAppUser, IsValidUser, IsSuperUserOrAppUser) from . import serializers @@ -98,8 +97,9 @@ class UserToken(APIView): password = request.data.get('password', '') public_key = request.data.get('public_key', '') - user, msg = check_user_valid(username=username, email=email, - password=password, public_key=public_key) + user, msg = check_user_valid( + username=username, email=email, + password=password, public_key=public_key) else: user = request.user msg = None @@ -116,24 +116,29 @@ class UserProfile(APIView): def get(self, request): return Response(request.user.to_json()) + def post(self, request): + return Response(request.user.to_json()) + class UserAuthApi(APIView): permission_classes = (AllowAny,) - def post(self, request, *args, **kwargs): + def post(self, request): username = request.data.get('username', '') password = request.data.get('password', '') public_key = request.data.get('public_key', '') login_type = request.data.get('login_type', '') - login_ip = request.data.get('remote_addr', None) or request.META.get('REMOTE_ADDR', '') + login_ip = request.data.get('remote_addr', None) user_agent = request.data.get('HTTP_USER_AGENT', '') - user, msg = check_user_valid(username=username, password=password, public_key=public_key) + user, msg = check_user_valid(username=username, password=password, + public_key=public_key) if user: token = generate_token(request, user) - write_login_log_async.delay(user.username, name=user.name, user_agent=user_agent, - login_ip=login_ip, login_type=login_type) + write_login_log_async.delay(user.username, name=user.name, + user_agent=user_agent, login_ip=login_ip, + login_type=login_type) return Response({'token': token, 'user': user.to_json()}) else: return Response({'msg': msg}, status=401) diff --git a/apps/users/authentication.py b/apps/users/authentication.py index db58b0ed4..203a07e5c 100644 --- a/apps/users/authentication.py +++ b/apps/users/authentication.py @@ -8,10 +8,11 @@ import time from django.core.cache import cache from django.conf import settings from django.utils.translation import ugettext as _ -from rest_framework import authentication, exceptions, permissions from django.utils.six import text_type from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING +from rest_framework import authentication, exceptions, permissions +from rest_framework.authentication import CSRFCheck from common.utils import get_object_or_none, make_signature, http_to_unixtime from .utils import refresh_token @@ -27,6 +28,21 @@ def get_request_date_header(request): class AccessKeyAuthentication(authentication.BaseAuthentication): + """App使用Access key进行签名认证, 目前签名算法比较简单, + app注册或者手动建立后,会生成 access_key_id 和 access_key_secret, + 然后使用 如下算法生成签名: + Signature = md5(access_key_secret + '\n' + Date) + example: Signature = md5('d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc' + '\n' + + 'Thu, 12 Jan 2017 08:19:41 GMT') + 请求时设置请求header + header['Authorization'] = 'Sign access_key_id:Signature' 如: + header['Authorization'] = + 'Sign d32d2b8b-9a10-4b8d-85bb-1a66976f6fdc:OKOlmdxgYPZ9+SddnUUDbQ==' + + 验证时根据相同算法进行验证, 取到access_key_id对应的access_key_id, 从request + headers取到Date, 然后进行md5, 判断得到的结果是否相同, 如果是认证通过, 否则 认证 + 失败 + """ keyword = 'Sign' model = AccessKey @@ -40,22 +56,26 @@ class AccessKeyAuthentication(authentication.BaseAuthentication): msg = _('Invalid signature header. No credentials provided.') raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: - msg = _('Invalid signature header. Signature string should not contain spaces.') + msg = _('Invalid signature header. Signature ' + 'string should not contain spaces.') raise exceptions.AuthenticationFailed(msg) try: sign = auth[1].decode().split(':') if len(sign) != 2: - msg = _('Invalid signature header. Format like AccessKeyId:Signature') + msg = _('Invalid signature header. ' + 'Format like AccessKeyId:Signature') raise exceptions.AuthenticationFailed(msg) except UnicodeError: - msg = _('Invalid signature header. Signature string should not contain invalid characters.') + msg = _('Invalid signature header. ' + 'Signature string should not contain invalid characters.') raise exceptions.AuthenticationFailed(msg) access_key_id = sign[0] request_signature = sign[1] - return self.authenticate_credentials(request, access_key_id, request_signature) + return self.authenticate_credentials( + request, access_key_id, request_signature) @staticmethod def authenticate_credentials(request, access_key_id, request_signature): @@ -68,14 +88,17 @@ class AccessKeyAuthentication(authentication.BaseAuthentication): try: request_unix_time = http_to_unixtime(request_date) except ValueError: - raise exceptions.AuthenticationFailed(_('HTTP header: Date not provide or not %a, %d %b %Y %H:%M:%S GMT')) + raise exceptions.AuthenticationFailed( + _('HTTP header: Date not provide ' + 'or not %a, %d %b %Y %H:%M:%S GMT')) - if int(time.time()) - request_unix_time > 15*60: - raise exceptions.AuthenticationFailed(_('Expired, more than 15 minutes')) + if int(time.time()) - request_unix_time > 15 * 60: + raise exceptions.AuthenticationFailed( + _('Expired, more than 15 minutes')) signature = make_signature(access_key_secret, request_date) if not signature == request_signature: - raise exceptions.AuthenticationFailed(_('Invalid signature. %s: %s' % (signature, request_signature))) + raise exceptions.AuthenticationFailed(_('Invalid signature.')) if not access_key.user.is_active: raise exceptions.AuthenticationFailed(_('User disabled.')) @@ -97,13 +120,15 @@ class AccessTokenAuthentication(authentication.BaseAuthentication): msg = _('Invalid token header. No credentials provided.') raise exceptions.AuthenticationFailed(msg) elif len(auth) > 2: - msg = _('Invalid token header. Sign string should not contain spaces.') + msg = _('Invalid token header. Sign string ' + 'should not contain spaces.') raise exceptions.AuthenticationFailed(msg) try: token = auth[1].decode() except UnicodeError: - msg = _('Invalid token header. Sign string should not contain invalid characters.') + msg = _('Invalid token header. Sign string ' + 'should not contain invalid characters.') raise exceptions.AuthenticationFailed(msg) return self.authenticate_credentials(token) @@ -125,4 +150,6 @@ class PrivateTokenAuthentication(authentication.TokenAuthentication): class SessionAuthentication(authentication.SessionAuthentication): def enforce_csrf(self, request): - return None \ No newline at end of file + reason = CSRFCheck().process_view(request, None, (), {}) + if reason: + raise exceptions.AuthenticationFailed(reason) diff --git a/apps/users/models/authentication.py b/apps/users/models/authentication.py index a0351e466..917bc1114 100644 --- a/apps/users/models/authentication.py +++ b/apps/users/models/authentication.py @@ -16,7 +16,8 @@ class AccessKey(models.Model): default=uuid.uuid4, editable=False) secret = models.UUIDField(verbose_name='AccessKeySecret', default=uuid.uuid4, editable=False) - user = models.ForeignKey(User, verbose_name='User', related_name='access_key') + user = models.ForeignKey(User, verbose_name='User', + related_name='access_key') def get_id(self): return str(self.id) diff --git a/apps/users/urls/api_urls.py b/apps/users/urls/api_urls.py index 212837aff..eaae47556 100644 --- a/apps/users/urls/api_urls.py +++ b/apps/users/urls/api_urls.py @@ -18,9 +18,12 @@ urlpatterns = [ url(r'^v1/token/$', api.UserToken.as_view(), name='user-token'), url(r'^v1/profile/$', api.UserProfile.as_view(), name='user-profile'), url(r'^v1/auth/$', api.UserAuthApi.as_view(), name='user-auth'), - url(r'^v1/users/(?P\d+)/password/reset/$', api.UserResetPasswordApi.as_view(), name='user-reset-password'), - url(r'^v1/users/(?P\d+)/public-key/reset/$', api.UserResetPKApi.as_view(), name='user-public-key-reset'), - url(r'^v1/users/(?P\d+)/public-key/update/$', api.UserUpdatePKApi.as_view(), name='user-public-key-update'), + url(r'^v1/users/(?P\d+)/password/reset/$', + api.UserResetPasswordApi.as_view(), name='user-reset-password'), + url(r'^v1/users/(?P\d+)/public-key/reset/$', + api.UserResetPKApi.as_view(), name='user-public-key-reset'), + url(r'^v1/users/(?P\d+)/public-key/update/$', + api.UserUpdatePKApi.as_view(), name='user-public-key-update'), url(r'^v1/users/(?P\d+)/groups/$', api.UserUpdateGroupApi.as_view(), name='user-update-group'), url(r'^v1/user-groups/(?P\d+)/users/$', diff --git a/apps/users/urls/views_urls.py b/apps/users/urls/views_urls.py index 4741fde60..58c531765 100644 --- a/apps/users/urls/views_urls.py +++ b/apps/users/urls/views_urls.py @@ -6,39 +6,72 @@ from .. import views app_name = 'users' urlpatterns = [ + # Login view url(r'^login$', views.UserLoginView.as_view(), name='login'), url(r'^logout$', views.UserLogoutView.as_view(), name='logout'), - url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), name='forgot-password'), + url(r'^password/forgot$', views.UserForgotPasswordView.as_view(), + name='forgot-password'), url(r'^password/forgot/sendmail-success$', - views.UserForgotPasswordSendmailSuccessView.as_view(), name='forgot-password-sendmail-success'), - url(r'^password/reset$', views.UserResetPasswordView.as_view(), name='reset-password'), - url(r'^password/reset/success$', views.UserResetPasswordSuccessView.as_view(), + views.UserForgotPasswordSendmailSuccessView.as_view(), + name='forgot-password-sendmail-success'), + url(r'^password/reset$', views.UserResetPasswordView.as_view(), + name='reset-password'), + url(r'^password/reset/success$', + views.UserResetPasswordSuccessView.as_view(), name='reset-password-success'), + # User view url(r'^user$', views.UserListView.as_view(), name='user-list'), - url(r'^user/(?P[0-9]+)$', views.UserDetailView.as_view(), name='user-detail'), - url(r'^user/(?P[0-9]+)/asset-permission$', views.UserAssetPermissionView.as_view(), + url(r'^user/(?P[0-9]+)$', views.UserDetailView.as_view(), + name='user-detail'), + url(r'^user/(?P[0-9]+)/asset-permission$', + views.UserAssetPermissionView.as_view(), name='user-asset-permission'), - url(r'^user/(?P[0-9]+)/asset-permission/create$', views.UserAssetPermissionCreateView.as_view(), + url(r'^user/(?P[0-9]+)/asset-permission/create$', + views.UserAssetPermissionCreateView.as_view(), name='user-asset-permission-create'), - url(r'^user/(?P[0-9]+)/assets', views.UserGrantedAssetView.as_view(), name='user-granted-asset'), - url(r'^user/(?P[0-9]+)/login-history', views.UserDetailView.as_view(), name='user-login-history'), - url(r'^user/export/', views.UserExportView.as_view(), name='user-export'), - url(r'^first-login/$', views.UserFirstLoginView.as_view(), name='user-first-login'), - url(r'^user/import/$', views.BulkImportUserView.as_view(), name='user-import'), - # url(r'^user/(?P[0-9]+)/assets-perm$', views.UserDetailView.as_view(), name='user-detail'), - url(r'^user/create$', views.UserCreateView.as_view(), name='user-create'), - url(r'^user/(?P[0-9]+)/update$', views.UserUpdateView.as_view(), name='user-update'), + url(r'^user/(?P[0-9]+)/assets', + views.UserGrantedAssetView.as_view(), + name='user-granted-asset'), + url(r'^user/(?P[0-9]+)/login-history', + views.UserDetailView.as_view(), + name='user-login-history'), + url(r'^user/export/', + views.UserExportView.as_view(), + name='user-export'), + url(r'^first-login/$', + views.UserFirstLoginView.as_view(), + name='user-first-login'), + url(r'^user/import/$', + views.UserBulkImportView.as_view(), + name='user-import'), + url(r'^user/create$', + views.UserCreateView.as_view(), + name='user-create'), + url(r'^user/(?P[0-9]+)/update$', + views.UserUpdateView.as_view(), + name='user-update'), # User group view - url(r'^user-group$', views.UserGroupListView.as_view(), name='user-group-list'), - url(r'^user-group/(?P[0-9]+)$', views.UserGroupDetailView.as_view(), name='user-group-detail'), - url(r'^user-group/create$', views.UserGroupCreateView.as_view(), name='user-group-create'), - url(r'^user-group/(?P[0-9]+)/update$', views.UserGroupUpdateView.as_view(), name='user-group-update'), - url(r'^user-group/(?P[0-9]+)/asset-permission$', views.UserGroupAssetPermissionView.as_view(), + url(r'^user-group$', + views.UserGroupListView.as_view(), + name='user-group-list'), + url(r'^user-group/(?P[0-9]+)$', + views.UserGroupDetailView.as_view(), + name='user-group-detail'), + url(r'^user-group/create$', + views.UserGroupCreateView.as_view(), + name='user-group-create'), + url(r'^user-group/(?P[0-9]+)/update$', + views.UserGroupUpdateView.as_view(), + name='user-group-update'), + url(r'^user-group/(?P[0-9]+)/asset-permission$', + views.UserGroupAssetPermissionView.as_view(), name='user-group-asset-permission'), - url(r'^user-group/(?P[0-9]+)/asset-permission/create$', views.UserGroupAssetPermissionCreateView.as_view(), + url(r'^user-group/(?P[0-9]+)/asset-permission/create$', + views.UserGroupAssetPermissionCreateView.as_view(), name='user-group-asset-permission-create'), - url(r'^user-group/(?P[0-9]+)/assets', views.UserGroupGrantedAssetView.as_view(), + url(r'^user-group/(?P[0-9]+)/assets', + views.UserGroupGrantedAssetView.as_view(), name='user-group-granted-asset'), ] diff --git a/apps/users/views.py b/apps/users/views.py deleted file mode 100644 index c568a4b90..000000000 --- a/apps/users/views.py +++ /dev/null @@ -1,593 +0,0 @@ -# ~*~ coding: utf-8 ~*~ - -from __future__ import unicode_literals -import json -import uuid - -from openpyxl import Workbook -from openpyxl.writer.excel import save_virtual_workbook -from openpyxl import load_workbook -from django import forms -from django.utils import timezone -from django.core.cache import cache -from django.db import IntegrityError -from django.contrib.auth import login as auth_login, logout as auth_logout -from django.contrib.auth.mixins import LoginRequiredMixin -from django.contrib.messages.views import SuccessMessageMixin -from django.core.files.storage import default_storage -from django.http import HttpResponseRedirect, HttpResponse, JsonResponse -from django.shortcuts import reverse, redirect -from django.utils.decorators import method_decorator -from django.utils.translation import ugettext as _ -from django.urls import reverse_lazy -from django.views import View -from django.views.decorators.cache import never_cache -from django.views.decorators.csrf import csrf_protect, csrf_exempt -from django.views.decorators.debug import sensitive_post_parameters -from django.views.generic.base import TemplateView -from django.views.generic.list import ListView -from django.views.generic.edit import CreateView, DeleteView, UpdateView, FormView, SingleObjectMixin, FormMixin -from django.views.generic.detail import DetailView -from formtools.wizard.views import SessionWizardView - -from common.mixins import JSONResponseMixin -from common.utils import get_object_or_none, get_logger -from perms.models import AssetPermission -from .models import User, UserGroup -from .utils import AdminUserRequiredMixin, user_add_success_next, send_reset_password_mail -from .hands import write_login_log_async -from . import forms - -logger = get_logger(__name__) - - -@method_decorator(sensitive_post_parameters(), name='dispatch') -@method_decorator(csrf_protect, name='dispatch') -@method_decorator(never_cache, name='dispatch') -class UserLoginView(FormView): - template_name = 'users/login.html' - form_class = forms.UserLoginForm - redirect_field_name = 'next' - - def get(self, request, *args, **kwargs): - if request.user.is_staff: - return redirect(self.get_success_url()) - return super(UserLoginView, self).get(request, *args, **kwargs) - - def form_valid(self, form): - auth_login(self.request, form.get_user()) - login_ip = self.request.META.get('REMOTE_ADDR', '') - user_agent = self.request.META.get('HTTP_USER_AGENT', '') - write_login_log_async.delay(self.request.user.username, self.request.user.name, - login_type='W', login_ip=login_ip, user_agent=user_agent) - return redirect(self.get_success_url()) - - def get_success_url(self): - if self.request.user.is_first_login: - return reverse('users:user-first-login') - - return self.request.POST.get( - self.redirect_field_name, - self.request.GET.get(self.redirect_field_name, reverse('index'))) - - -@method_decorator(never_cache, name='dispatch') -class UserLogoutView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get(self, request, *args, **kwargs): - auth_logout(request) - return super(UserLogoutView, self).get(request) - - def get_context_data(self, **kwargs): - context = { - 'title': _('Logout success'), - 'messages': _('Logout success, return login page'), - 'redirect_url': reverse('users:login'), - 'auto_redirect': True, - } - kwargs.update(context) - return super(UserLogoutView, self).get_context_data(**kwargs) - - -class UserListView(AdminUserRequiredMixin, TemplateView): - template_name = 'users/user_list.html' - - def get_context_data(self, **kwargs): - context = super(UserListView, self).get_context_data(**kwargs) - context.update({ - 'app': _('Users'), - 'action': _('User list'), - 'groups': UserGroup.objects.all() - }) - return context - - -class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): - model = User - form_class = forms.UserCreateUpdateForm - template_name = 'users/user_create.html' - success_url = reverse_lazy('users:user-list') - success_message = _('Create user %s successfully.') - - def get_context_data(self, **kwargs): - context = super(UserCreateView, self).get_context_data(**kwargs) - context.update({'app': _('Users'), 'action': _('Create user')}) - return context - - def form_valid(self, form): - user = form.save(commit=False) - user.created_by = self.request.user.username or 'System' - user.save() - user_add_success_next(user) - return super(UserCreateView, self).form_valid(form) - - def get_success_message(self, cleaned_data): - return self.success_message % ( - reverse_lazy('users:user-detail', kwargs={'pk': self.object.pk}), - self.object.name, - ) - - -class UserUpdateView(AdminUserRequiredMixin, UpdateView): - model = User - form_class = forms.UserCreateUpdateForm - template_name = 'users/user_update.html' - context_object_name = 'user_object' - success_url = reverse_lazy('users:user-list') - - def form_valid(self, form): - username = self.object.username - user = form.save(commit=False) - user.username = username - user.save() - password = self.request.POST.get('password', '') - if password: - user.set_password(password) - return super(UserUpdateView, self).form_valid(form) - - def get_context_data(self, **kwargs): - context = super(UserUpdateView, self).get_context_data(**kwargs) - context.update({'app': _('Users'), 'action': _('Update user')}) - return context - - -class UserDetailView(AdminUserRequiredMixin, DetailView): - model = User - template_name = 'users/user_detail.html' - context_object_name = "user" - - def get_context_data(self, **kwargs): - groups = UserGroup.objects.exclude(id__in=self.object.groups.all()) - context = {'app': _('Users'), 'action': _('User detail'), 'groups': groups} - kwargs.update(context) - return super(UserDetailView, self).get_context_data(**kwargs) - - -class UserGroupListView(AdminUserRequiredMixin, TemplateView): - template_name = 'users/user_group_list.html' - - def get_context_data(self, **kwargs): - context = super(UserGroupListView, self).get_context_data(**kwargs) - context.update({'app': _('Users'), 'action': _('User group list')}) - return context - - -class UserGroupCreateView(AdminUserRequiredMixin, CreateView): - model = UserGroup - form_class = forms.UserGroupForm - template_name = 'users/user_group_create_update.html' - success_url = reverse_lazy('users:user-group-list') - - def get_context_data(self, **kwargs): - context = super(UserGroupCreateView, self).get_context_data(**kwargs) - users = User.objects.all() - context.update({'app': _('Users'), 'action': _('Create user group'), 'users': users}) - return context - - def form_valid(self, form): - user_group = form.save() - users_id_list = self.request.POST.getlist('users', []) - users = User.objects.filter(id__in=users_id_list) - user_group.created_by = self.request.user.username or 'Admin' - user_group.users.add(*users) - user_group.save() - return super(UserGroupCreateView, self).form_valid(form) - - -class UserGroupUpdateView(AdminUserRequiredMixin, UpdateView): - model = UserGroup - form_class = forms.UserGroupForm - template_name = 'users/user_group_create_update.html' - success_url = reverse_lazy('users:user-group-list') - - def get_context_data(self, **kwargs): - # self.object = self.get_object() - context = super(UserGroupUpdateView, self).get_context_data(**kwargs) - users = User.objects.all() - group_users = [user.id for user in self.object.users.all()] - context.update({ - 'app': _('Users'), - 'action': _('Update User Group'), - 'users': users, - 'group_users': group_users - }) - return context - - def form_valid(self, form): - user_group = form.save() - users_id_list = self.request.POST.getlist('users', []) - users = User.objects.filter(id__in=users_id_list) - user_group.users.clear() - user_group.users.add(*users) - user_group.save() - return super(UserGroupUpdateView, self).form_valid(form) - - -class UserGroupDetailView(AdminUserRequiredMixin, DetailView): - model = UserGroup - context_object_name = 'user_group' - template_name = 'users/user_group_detail.html' - - def get_context_data(self, **kwargs): - users = User.objects.exclude(id__in=self.object.users.all()) - context = { - 'app': _('Users'), - 'action': _('User Group Detail'), - 'users': users, - } - kwargs.update(context) - return super(UserGroupDetailView, self).get_context_data(**kwargs) - - -class UserForgotPasswordView(TemplateView): - template_name = 'users/forgot_password.html' - - def post(self, request): - email = request.POST.get('email') - user = get_object_or_none(User, email=email) - if not user: - return self.get(request, errors=_('Email address invalid, input again')) - else: - send_reset_password_mail(user) - return HttpResponseRedirect(reverse('users:forgot-password-sendmail-success')) - - -class UserForgotPasswordSendmailSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get_context_data(self, **kwargs): - context = { - 'title': _('Send reset password message'), - 'messages': _('Send reset password mail success, login your mail box and follow it '), - 'redirect_url': reverse('users:login'), - } - kwargs.update(context) - return super(UserForgotPasswordSendmailSuccessView, self).get_context_data(**kwargs) - - -class UserResetPasswordSuccessView(TemplateView): - template_name = 'flash_message_standalone.html' - - def get_context_data(self, **kwargs): - context = { - 'title': _('Reset password success'), - 'messages': _('Reset password success, return to login page'), - 'redirect_url': reverse('users:login'), - 'auto_redirect': True, - } - kwargs.update(context) - return super(UserResetPasswordSuccessView, self).get_context_data(**kwargs) - - -class UserResetPasswordView(TemplateView): - template_name = 'users/reset_password.html' - - def get(self, request, *args, **kwargs): - token = request.GET.get('token') - user = User.validate_reset_token(token) - - if not user: - kwargs.update({'errors': _('Token invalid or expired')}) - return super(UserResetPasswordView, self).get(request, *args, **kwargs) - - def post(self, request, *args, **kwargs): - password = request.POST.get('password') - password_confirm = request.POST.get('password-confirm') - token = request.GET.get('token') - - if password != password_confirm: - return self.get(request, errors=_('Password not same')) - - user = User.validate_reset_token(token) - if not user: - return self.get(request, errors=_('Token invalid or expired')) - - user.reset_password(password) - return HttpResponseRedirect(reverse('users:reset-password-success')) - - -class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): - template_name = 'users/first_login.html' - form_list = [forms.UserInfoForm, forms.UserKeyForm] - file_storage = default_storage - - def dispatch(self, request, *args, **kwargs): - if request.user.is_authenticated() and not request.user.is_first_login: - return redirect(reverse('index')) - return super(UserFirstLoginView, self).dispatch(request, *args, **kwargs) - - def done(self, form_list, form_dict, **kwargs): - user = self.request.user - for form in form_list: - for field in form: - if field.value(): - setattr(user, field.name, field.value()) - if field.name == 'enable_otp': - user.enable_otp = field.value() - user.is_first_login = False - user.is_public_key_valid = True - user.save() - return redirect(reverse('index')) - - def get_context_data(self, **kwargs): - context = super(UserFirstLoginView, self).get_context_data(**kwargs) - context.update({'app': _('Users'), 'action': _('First Login')}) - return context - - def get_form_initial(self, step): - user = self.request.user - if step == '0': - return { - 'name': user.name or user.username, - 'enable_otp': user.enable_otp or True, - 'wechat': user.wechat or '', - 'phone': user.phone or '' - } - return super(UserFirstLoginView, self).get_form_initial(step) - - def get_form(self, step=None, data=None, files=None): - form = super(UserFirstLoginView, self).get_form(step, data, files) - - if step is None: - step = self.steps.current - - if step == '1': - form.user = self.request.user - return form - - -class UserAssetPermissionView(AdminUserRequiredMixin, FormMixin, SingleObjectMixin, ListView): - model = User - template_name = 'users/user_asset_permission.html' - context_object_name = 'user' - form_class = forms.UserPrivateAssetPermissionForm - - def get(self, request, *args, **kwargs): - self.object = self.get_object(queryset=User.objects.all()) - return super(UserAssetPermissionView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = { - 'app': 'Users', - 'action': 'User asset permissions', - } - kwargs.update(context) - return super(UserAssetPermissionView, self).get_context_data(**kwargs) - - -class UserGroupAssetPermissionView(AdminUserRequiredMixin, FormMixin, SingleObjectMixin, ListView): - model = UserGroup - template_name = 'users/user_group_asset_permission.html' - context_object_name = 'user_group' - form_class = forms.UserPrivateAssetPermissionForm - - def get(self, request, *args, **kwargs): - self.object = self.get_object(queryset=UserGroup.objects.all()) - return super(UserGroupAssetPermissionView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = { - 'app': 'Users', - 'action': 'User group asset permissions', - } - kwargs.update(context) - return super(UserGroupAssetPermissionView, self).get_context_data(**kwargs) - - -class UserAssetPermissionCreateView(AdminUserRequiredMixin, CreateView): - form_class = forms.UserPrivateAssetPermissionForm - model = AssetPermission - - def get(self, request, *args, **kwargs): - user = self.get_object(queryset=User.objects.all()) - return redirect(reverse('users:user-asset-permission', kwargs={'pk': user.id})) - - def post(self, request, *args, **kwargs): - self.user = self.get_object(queryset=User.objects.all()) - return super(UserAssetPermissionCreateView, self).post(request, *args, **kwargs) - - def get_form(self, form_class=None): - form = super(UserAssetPermissionCreateView, self).get_form(form_class=form_class) - form.user = self.user - return form - - def form_invalid(self, form): - return redirect(reverse('users:user-asset-permission', kwargs={'pk': self.user.id})) - - def get_success_url(self): - return reverse('users:user-asset-permission', kwargs={'pk': self.user.id}) - - -class UserGroupAssetPermissionCreateView(AdminUserRequiredMixin, CreateView): - form_class = forms.UserGroupPrivateAssetPermissionForm - model = AssetPermission - - def get(self, request, *args, **kwargs): - user_group = self.get_object(queryset=UserGroup.objects.all()) - return redirect(reverse('users:user-group-asset-permission', kwargs={'pk': user_group.id})) - - def post(self, request, *args, **kwargs): - self.user_group = self.get_object(queryset=UserGroup.objects.all()) - return super(UserGroupAssetPermissionCreateView, self).post(request, *args, **kwargs) - - def get_form(self, form_class=None): - form = super(UserGroupAssetPermissionCreateView, self).get_form(form_class=form_class) - form.user_group = self.user_group - return form - - def form_invalid(self, form): - return redirect(reverse('users:user-group-asset-permission', kwargs={'pk': self.user_group.id})) - - def get_success_url(self): - return reverse('users:user-group-asset-permission', kwargs={'pk': self.user_group.id}) - - -class UserGrantedAssetView(AdminUserRequiredMixin, DetailView): - model = User - template_name = 'users/user_granted_asset.html' - context_object_name = 'user' - - def get(self, request, *args, **kwargs): - self.object = self.get_object(queryset=User.objects.all()) - return super(UserGrantedAssetView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = { - 'app': 'User', - 'action': 'User granted asset', - } - kwargs.update(context) - return super(UserGrantedAssetView, self).get_context_data(**kwargs) - - -class UserGroupGrantedAssetView(AdminUserRequiredMixin, DetailView): - model = User - template_name = 'users/user_group_granted_asset.html' - context_object_name = 'user_group' - - def get(self, request, *args, **kwargs): - self.object = self.get_object(queryset=UserGroup.objects.all()) - return super(UserGroupGrantedAssetView, self).get(request, *args, **kwargs) - - def get_context_data(self, **kwargs): - context = { - 'app': 'User', - 'action': 'User group granted asset', - } - kwargs.update(context) - return super(UserGroupGrantedAssetView, self).get_context_data(**kwargs) - - -class BulkImportUserView(AdminUserRequiredMixin, JSONResponseMixin, FormView): - form_class = forms.FileForm - - def form_invalid(self, form): - try: - error = form.errors.values()[-1][-1] - except Exception as e: - print e - error = _('Invalid file.') - data = { - 'success': False, - 'msg': error - } - return self.render_json_response(data) - - def form_valid(self, form): - try: - wb = load_workbook(form.cleaned_data['file']) - ws = wb.get_active_sheet() - except Exception as e: - print(e) - data = {'valid': False, 'msg': 'Not a valid Excel file'} - return self.render_json_response(data) - - rows = ws.rows - header_need = ["name", 'username', 'email', 'groups', "role", "phone", "wechat", "comment"] - header = [col.value for col in next(rows)] - print(header) - if header != header_need: - data = {'valid': False, 'msg': 'Must be same format as template or export file'} - return self.render_json_response(data) - - created = [] - updated = [] - failed = [] - for row in rows: - user_dict = dict(zip(header, [col.value for col in row])) - groups_name = user_dict.pop('groups') - if groups_name: - groups_name = groups_name.split(',') - groups = UserGroup.objects.filter(name__in=groups_name) - else: - groups = None - try: - user = User.objects.create(**user_dict) - user_add_success_next(user) - created.append(user_dict['username']) - except IntegrityError as e: - user = User.objects.filter(username=user_dict['username']) - if not user: - failed.append(user_dict['username']) - continue - user.update(**user_dict) - user = user[0] - updated.append(user_dict['username']) - except TypeError as e: - print(e) - failed.append(user_dict['username']) - user = None - - if user and groups: - user.groups.add(*tuple(groups)) - user.save() - - data = { - 'created': created, - 'created_info': 'Created {}'.format(len(created)), - 'updated': updated, - 'updated_info': 'Updated {}'.format(len(updated)), - 'failed': failed, - 'failed_info': 'Failed {}'.format(len(failed)), - 'valid': True, - 'msg': 'Created: {}. Updated: {}, Error: {}'.format(len(created), len(updated), len(failed)) - } - return self.render_json_response(data) - - -@method_decorator(csrf_exempt, name='dispatch') -class UserExportView(View): - def get(self, request, *args, **kwargs): - spm = request.GET.get('spm', '') - users_id = cache.get(spm) - if not users_id and not isinstance(users_id, list): - return HttpResponse('May be expired', status=404) - - users = User.objects.filter(id__in=users_id) - wb = Workbook() - ws = wb.active - ws.title = 'User' - header = ["name", 'username', 'email', 'groups', "role", "phone", "wechat", "comment"] - ws.append(header) - - for user in users: - ws.append([user.name, user.username, user.email, - ','.join([group.name for group in user.groups.all()]), - user.role, user.phone, user.wechat, user.comment]) - - filename = 'users-{}.xlsx'.format(timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')) - response = HttpResponse(save_virtual_workbook(wb), content_type='applications/vnd.ms-excel') - response['Content-Disposition'] = 'attachment; filename="%s"' % filename - return response - - def post(self, request, *args, **kwargs): - try: - users_id = json.loads(request.body).get('users_id', []) - except ValueError: - return HttpResponse('Json object not valid', status=400) - spm = uuid.uuid4().get_hex() - cache.set(spm, users_id, 300) - url = reverse('users:user-export') + '?spm=%s' % spm - return JsonResponse({'redirect': url}) - diff --git a/apps/users/views/__init__.py b/apps/users/views/__init__.py new file mode 100644 index 000000000..b178ac710 --- /dev/null +++ b/apps/users/views/__init__.py @@ -0,0 +1,5 @@ +# ~*~ coding: utf-8 ~*~ + +from .login import * +from .user import * +from .group import * diff --git a/apps/users/views/group.py b/apps/users/views/group.py new file mode 100644 index 000000000..48232b72a --- /dev/null +++ b/apps/users/views/group.py @@ -0,0 +1,169 @@ +# ~*~ coding: utf-8 ~*~ + +from __future__ import unicode_literals +from django import forms +from django.shortcuts import reverse, redirect +from django.utils.translation import ugettext as _ +from django.urls import reverse_lazy +from django.views.generic import ListView +from django.views.generic.base import TemplateView +from django.views.generic.edit import CreateView, UpdateView, FormMixin +from django.views.generic.detail import DetailView, SingleObjectMixin + +from common.utils import get_logger +from perms.models import AssetPermission +from ..models import User, UserGroup +from ..utils import AdminUserRequiredMixin +from .. import forms + +__all__ = ['UserGroupListView', 'UserGroupCreateView', 'UserGroupDetailView', + 'UserGroupUpdateView', 'UserGroupAssetPermissionCreateView', + 'UserGroupAssetPermissionView', 'UserGroupGrantedAssetView'] +logger = get_logger(__name__) + + +class UserGroupListView(AdminUserRequiredMixin, TemplateView): + template_name = 'users/user_group_list.html' + + def get_context_data(self, **kwargs): + context = super(UserGroupListView, self).get_context_data(**kwargs) + context.update({'app': _('Users'), 'action': _('User group list')}) + return context + + +class UserGroupCreateView(AdminUserRequiredMixin, CreateView): + model = UserGroup + form_class = forms.UserGroupForm + template_name = 'users/user_group_create_update.html' + success_url = reverse_lazy('users:user-group-list') + + def get_context_data(self, **kwargs): + context = super(UserGroupCreateView, self).get_context_data(**kwargs) + users = User.objects.all() + context.update({'app': _('Users'), 'action': _('Create user group'), + 'users': users}) + return context + + def form_valid(self, form): + user_group = form.save() + users_id_list = self.request.POST.getlist('users', []) + users = User.objects.filter(id__in=users_id_list) + user_group.created_by = self.request.user.username or 'Admin' + user_group.users.add(*users) + user_group.save() + return super(UserGroupCreateView, self).form_valid(form) + + +class UserGroupUpdateView(AdminUserRequiredMixin, UpdateView): + model = UserGroup + form_class = forms.UserGroupForm + template_name = 'users/user_group_create_update.html' + success_url = reverse_lazy('users:user-group-list') + + def get_context_data(self, **kwargs): + # self.object = self.get_object() + context = super(UserGroupUpdateView, self).get_context_data(**kwargs) + users = User.objects.all() + group_users = [user.id for user in self.object.users.all()] + context.update({ + 'app': _('Users'), + 'action': _('Update User Group'), + 'users': users, + 'group_users': group_users + }) + return context + + def form_valid(self, form): + user_group = form.save() + users_id_list = self.request.POST.getlist('users', []) + users = User.objects.filter(id__in=users_id_list) + user_group.users.clear() + user_group.users.add(*users) + user_group.save() + return super(UserGroupUpdateView, self).form_valid(form) + + +class UserGroupDetailView(AdminUserRequiredMixin, DetailView): + model = UserGroup + context_object_name = 'user_group' + template_name = 'users/user_group_detail.html' + + def get_context_data(self, **kwargs): + users = User.objects.exclude(id__in=self.object.users.all()) + context = { + 'app': _('Users'), + 'action': _('User Group Detail'), + 'users': users, + } + kwargs.update(context) + return super(UserGroupDetailView, self).get_context_data(**kwargs) + + +class UserGroupAssetPermissionView(AdminUserRequiredMixin, FormMixin, + SingleObjectMixin, ListView): + model = UserGroup + template_name = 'users/user_group_asset_permission.html' + context_object_name = 'user_group' + form_class = forms.UserPrivateAssetPermissionForm + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=UserGroup.objects.all()) + return super(UserGroupAssetPermissionView, self)\ + .get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = { + 'app': 'Users', + 'action': 'User group asset permissions', + } + kwargs.update(context) + return super(UserGroupAssetPermissionView, self)\ + .get_context_data(**kwargs) + + +class UserGroupAssetPermissionCreateView(AdminUserRequiredMixin, CreateView): + form_class = forms.UserGroupPrivateAssetPermissionForm + model = AssetPermission + + def get(self, request, *args, **kwargs): + user_group = self.get_object(queryset=UserGroup.objects.all()) + return redirect(reverse('users:user-group-asset-permission', + kwargs={'pk': user_group.id})) + + def post(self, request, *args, **kwargs): + self.user_group = self.get_object(queryset=UserGroup.objects.all()) + return super(UserGroupAssetPermissionCreateView, self)\ + .post(request, *args, **kwargs) + + def get_form(self, form_class=None): + form = super(UserGroupAssetPermissionCreateView, self)\ + .get_form(form_class=form_class) + form.user_group = self.user_group + return form + + def form_invalid(self, form): + return redirect(reverse('users:user-group-asset-permission', + kwargs={'pk': self.user_group.id})) + + def get_success_url(self): + return reverse('users:user-group-asset-permission', + kwargs={'pk': self.user_group.id}) + + +class UserGroupGrantedAssetView(AdminUserRequiredMixin, DetailView): + model = User + template_name = 'users/user_group_granted_asset.html' + context_object_name = 'user_group' + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=UserGroup.objects.all()) + return super(UserGroupGrantedAssetView, self)\ + .get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = { + 'app': 'User', + 'action': 'User group granted asset', + } + kwargs.update(context) + return super(UserGroupGrantedAssetView, self).get_context_data(**kwargs) diff --git a/apps/users/views/login.py b/apps/users/views/login.py new file mode 100644 index 000000000..b81d06f1b --- /dev/null +++ b/apps/users/views/login.py @@ -0,0 +1,200 @@ +# ~*~ coding: utf-8 ~*~ + +from __future__ import unicode_literals +from django import forms +from django.contrib.auth import login as auth_login, logout as auth_logout +from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.files.storage import default_storage +from django.http import HttpResponseRedirect +from django.shortcuts import reverse, redirect +from django.utils.decorators import method_decorator +from django.utils.translation import ugettext as _ +from django.views.decorators.cache import never_cache +from django.views.decorators.csrf import csrf_protect +from django.views.decorators.debug import sensitive_post_parameters +from django.views.generic.base import TemplateView +from django.views.generic.edit import FormView +from formtools.wizard.views import SessionWizardView + +from common.utils import get_object_or_none +from ..models import User +from ..utils import send_reset_password_mail +from ..hands import write_login_log_async +from .. import forms + + +__all__ = ['UserLoginView', 'UserLogoutView', + 'UserForgotPasswordView', 'UserForgotPasswordSendmailSuccessView', + 'UserResetPasswordView', 'UserResetPasswordSuccessView', + 'UserFirstLoginView'] + + +@method_decorator(sensitive_post_parameters(), name='dispatch') +@method_decorator(csrf_protect, name='dispatch') +@method_decorator(never_cache, name='dispatch') +class UserLoginView(FormView): + template_name = 'users/login.html' + form_class = forms.UserLoginForm + redirect_field_name = 'next' + + def get(self, request, *args, **kwargs): + if request.user.is_staff: + return redirect(self.get_success_url()) + return super(UserLoginView, self).get(request, *args, **kwargs) + + def form_valid(self, form): + auth_login(self.request, form.get_user()) + login_ip = self.request.META.get('REMOTE_ADDR', '') + user_agent = self.request.META.get('HTTP_USER_AGENT', '') + write_login_log_async.delay(self.request.user.username, + self.request.user.name, + login_type='W', login_ip=login_ip, + user_agent=user_agent) + return redirect(self.get_success_url()) + + def get_success_url(self): + if self.request.user.is_first_login: + return reverse('users:user-first-login') + + return self.request.POST.get( + self.redirect_field_name, + self.request.GET.get(self.redirect_field_name, reverse('index'))) + + +@method_decorator(never_cache, name='dispatch') +class UserLogoutView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get(self, request, *args, **kwargs): + auth_logout(request) + return super(UserLogoutView, self).get(request) + + def get_context_data(self, **kwargs): + context = { + 'title': _('Logout success'), + 'messages': _('Logout success, return login page'), + 'redirect_url': reverse('users:login'), + 'auto_redirect': True, + } + kwargs.update(context) + return super(UserLogoutView, self).get_context_data(**kwargs) + + +class UserForgotPasswordView(TemplateView): + template_name = 'users/forgot_password.html' + + def post(self, request): + email = request.POST.get('email') + user = get_object_or_none(User, email=email) + if not user: + return self.get(request, errors=_('Email address invalid, input again')) + else: + send_reset_password_mail(user) + return HttpResponseRedirect( + reverse('users:forgot-password-sendmail-success')) + + +class UserForgotPasswordSendmailSuccessView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get_context_data(self, **kwargs): + context = { + 'title': _('Send reset password message'), + 'messages': _('Send reset password mail success, ' + 'login your mail box and follow it '), + 'redirect_url': reverse('users:login'), + } + kwargs.update(context) + return super(UserForgotPasswordSendmailSuccessView, self)\ + .get_context_data(**kwargs) + + +class UserResetPasswordSuccessView(TemplateView): + template_name = 'flash_message_standalone.html' + + def get_context_data(self, **kwargs): + context = { + 'title': _('Reset password success'), + 'messages': _('Reset password success, return to login page'), + 'redirect_url': reverse('users:login'), + 'auto_redirect': True, + } + kwargs.update(context) + return super(UserResetPasswordSuccessView, self).get_context_data(**kwargs) + + +class UserResetPasswordView(TemplateView): + template_name = 'users/reset_password.html' + + def get(self, request, *args, **kwargs): + token = request.GET.get('token') + user = User.validate_reset_token(token) + + if not user: + kwargs.update({'errors': _('Token invalid or expired')}) + return super(UserResetPasswordView, self).get(request, *args, **kwargs) + + def post(self, request, *args, **kwargs): + password = request.POST.get('password') + password_confirm = request.POST.get('password-confirm') + token = request.GET.get('token') + + if password != password_confirm: + return self.get(request, errors=_('Password not same')) + + user = User.validate_reset_token(token) + if not user: + return self.get(request, errors=_('Token invalid or expired')) + + user.reset_password(password) + return HttpResponseRedirect(reverse('users:reset-password-success')) + + +class UserFirstLoginView(LoginRequiredMixin, SessionWizardView): + template_name = 'users/first_login.html' + form_list = [forms.UserInfoForm, forms.UserKeyForm] + file_storage = default_storage + + def dispatch(self, request, *args, **kwargs): + if request.user.is_authenticated() and not request.user.is_first_login: + return redirect(reverse('index')) + return super(UserFirstLoginView, self).dispatch(request, *args, **kwargs) + + def done(self, form_list, form_dict, **kwargs): + user = self.request.user + for form in form_list: + for field in form: + if field.value(): + setattr(user, field.name, field.value()) + if field.name == 'enable_otp': + user.enable_otp = field.value() + user.is_first_login = False + user.is_public_key_valid = True + user.save() + return redirect(reverse('index')) + + def get_context_data(self, **kwargs): + context = super(UserFirstLoginView, self).get_context_data(**kwargs) + context.update({'app': _('Users'), 'action': _('First Login')}) + return context + + def get_form_initial(self, step): + user = self.request.user + if step == '0': + return { + 'name': user.name or user.username, + 'enable_otp': user.enable_otp or True, + 'wechat': user.wechat or '', + 'phone': user.phone or '' + } + return super(UserFirstLoginView, self).get_form_initial(step) + + def get_form(self, step=None, data=None, files=None): + form = super(UserFirstLoginView, self).get_form(step, data, files) + + if step is None: + step = self.steps.current + + if step == '1': + form.user = self.request.user + return form diff --git a/apps/users/views/user.py b/apps/users/views/user.py new file mode 100644 index 000000000..8a2df396e --- /dev/null +++ b/apps/users/views/user.py @@ -0,0 +1,300 @@ +# ~*~ coding: utf-8 ~*~ + +from __future__ import unicode_literals +import uuid +import json + +from django.shortcuts import redirect +from openpyxl import Workbook +from openpyxl.writer.excel import save_virtual_workbook +from openpyxl import load_workbook +from django import forms +from django.core.cache import cache +from django.http import HttpResponse, JsonResponse +from django.contrib.messages.views import SuccessMessageMixin +from django.urls import reverse_lazy, reverse +from django.utils import timezone +from django.utils.translation import ugettext as _ +from django.utils.decorators import method_decorator +from django.views import View +from django.views.generic import ListView +from django.views.generic.base import TemplateView +from django.views.generic.edit import CreateView, UpdateView, FormMixin, \ + FormView +from django.views.generic.detail import DetailView, SingleObjectMixin +from django.views.decorators.csrf import csrf_exempt + +from common.mixins import JSONResponseMixin +from common.utils import get_logger +from perms.models import AssetPermission +from ..models import User, UserGroup +from ..utils import AdminUserRequiredMixin, user_add_success_next +from .. import forms + +__all__ = ['UserListView', 'UserCreateView', 'UserDetailView', + 'UserUpdateView', 'UserAssetPermissionCreateView', + 'UserAssetPermissionView', 'UserGrantedAssetView', + 'UserExportView', 'UserBulkImportView'] +logger = get_logger(__name__) + + +class UserListView(AdminUserRequiredMixin, TemplateView): + template_name = 'users/user_list.html' + + def get_context_data(self, **kwargs): + context = super(UserListView, self).get_context_data(**kwargs) + context.update({ + 'app': _('Users'), + 'action': _('User list'), + 'groups': UserGroup.objects.all() + }) + return context + + +class UserCreateView(AdminUserRequiredMixin, SuccessMessageMixin, CreateView): + model = User + form_class = forms.UserCreateUpdateForm + template_name = 'users/user_create.html' + success_url = reverse_lazy('users:user-list') + success_message = _('Create user %s successfully.') + + def get_context_data(self, **kwargs): + context = super(UserCreateView, self).get_context_data(**kwargs) + context.update({'app': _('Users'), 'action': _('Create user')}) + return context + + def form_valid(self, form): + user = form.save(commit=False) + user.created_by = self.request.user.username or 'System' + user.save() + user_add_success_next(user) + return super(UserCreateView, self).form_valid(form) + + def get_success_message(self, cleaned_data): + return self.success_message % ( + reverse_lazy('users:user-detail', kwargs={'pk': self.object.pk}), + self.object.name, + ) + + +class UserUpdateView(AdminUserRequiredMixin, UpdateView): + model = User + form_class = forms.UserCreateUpdateForm + template_name = 'users/user_update.html' + context_object_name = 'user_object' + success_url = reverse_lazy('users:user-list') + + def form_valid(self, form): + username = self.object.username + user = form.save(commit=False) + user.username = username + user.save() + password = self.request.POST.get('password', '') + if password: + user.set_password(password) + return super(UserUpdateView, self).form_valid(form) + + def get_context_data(self, **kwargs): + context = super(UserUpdateView, self).get_context_data(**kwargs) + context.update({'app': _('Users'), 'action': _('Update user')}) + return context + + +class UserDetailView(AdminUserRequiredMixin, DetailView): + model = User + template_name = 'users/user_detail.html' + context_object_name = "user" + + def get_context_data(self, **kwargs): + groups = UserGroup.objects.exclude(id__in=self.object.groups.all()) + context = { + 'app': _('Users'), + 'action': _('User detail'), + 'groups': groups + } + kwargs.update(context) + return super(UserDetailView, self).get_context_data(**kwargs) + + +@method_decorator(csrf_exempt, name='dispatch') +class UserExportView(View): + def get(self, request, *args, **kwargs): + spm = request.GET.get('spm', '') + users_id = cache.get(spm) + if not users_id and not isinstance(users_id, list): + return HttpResponse('May be expired', status=404) + + users = User.objects.filter(id__in=users_id) + wb = Workbook() + ws = wb.active + ws.title = 'User' + header = ["name", 'username', 'email', 'groups', + "role", "phone", "wechat", "comment"] + ws.append(header) + + for user in users: + ws.append([user.name, user.username, user.email, + ','.join([group.name for group in user.groups.all()]), + user.role, user.phone, user.wechat, user.comment]) + + filename = 'users-{}.xlsx'.format( + timezone.localtime(timezone.now()).strftime('%Y-%m-%d_%H-%M-%S')) + response = HttpResponse(save_virtual_workbook(wb), + content_type='applications/vnd.ms-excel') + response['Content-Disposition'] = 'attachment; filename="%s"' % filename + return response + + def post(self, request, *args, **kwargs): + try: + users_id = json.loads(request.body).get('users_id', []) + except ValueError: + return HttpResponse('Json object not valid', status=400) + spm = uuid.uuid4().get_hex() + cache.set(spm, users_id, 300) + url = reverse('users:user-export') + '?spm=%s' % spm + return JsonResponse({'redirect': url}) + + +class UserBulkImportView(AdminUserRequiredMixin, JSONResponseMixin, FormView): + form_class = forms.FileForm + + def form_invalid(self, form): + try: + error = form.errors.values()[-1][-1] + except Exception as e: + print e + error = _('Invalid file.') + data = { + 'success': False, + 'msg': error + } + return self.render_json_response(data) + + def form_valid(self, form): + try: + wb = load_workbook(form.cleaned_data['file']) + ws = wb.get_active_sheet() + except Exception as e: + print(e) + data = {'valid': False, 'msg': 'Not a valid Excel file'} + return self.render_json_response(data) + + rows = ws.rows + header_need = ["name", 'username', 'email', 'groups', "role", "phone", "wechat", "comment"] + header = [col.value for col in next(rows)] + print(header) + if header != header_need: + data = {'valid': False, 'msg': 'Must be same format as template or export file'} + return self.render_json_response(data) + + created = [] + updated = [] + failed = [] + for row in rows: + user_dict = dict(zip(header, [col.value for col in row])) + groups_name = user_dict.pop('groups') + if groups_name: + groups_name = groups_name.split(',') + groups = UserGroup.objects.filter(name__in=groups_name) + else: + groups = None + try: + user = User.objects.create(**user_dict) + user_add_success_next(user) + created.append(user_dict['username']) + except User.IntegrityError as e: + user = User.objects.filter(username=user_dict['username']) + if not user: + failed.append(user_dict['username']) + continue + user.update(**user_dict) + user = user[0] + updated.append(user_dict['username']) + except TypeError as e: + print(e) + failed.append(user_dict['username']) + user = None + + if user and groups: + user.groups.add(*tuple(groups)) + user.save() + + data = { + 'created': created, + 'created_info': 'Created {}'.format(len(created)), + 'updated': updated, + 'updated_info': 'Updated {}'.format(len(updated)), + 'failed': failed, + 'failed_info': 'Failed {}'.format(len(failed)), + 'valid': True, + 'msg': 'Created: {}. Updated: {}, Error: {}'.format( + len(created), len(updated), len(failed)) + } + return self.render_json_response(data) + + +class UserAssetPermissionView(AdminUserRequiredMixin, FormMixin, + SingleObjectMixin, ListView): + model = User + template_name = 'users/user_asset_permission.html' + context_object_name = 'user' + form_class = forms.UserPrivateAssetPermissionForm + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=User.objects.all()) + return super(UserAssetPermissionView, self).get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = { + 'app': 'Users', + 'action': 'User asset permissions', + } + kwargs.update(context) + return super(UserAssetPermissionView, self).get_context_data(**kwargs) + + +class UserAssetPermissionCreateView(AdminUserRequiredMixin, CreateView): + form_class = forms.UserPrivateAssetPermissionForm + model = AssetPermission + + def get(self, request, *args, **kwargs): + user = self.get_object(queryset=User.objects.all()) + return redirect(reverse('users:user-asset-permission', + kwargs={'pk': user.id})) + + def post(self, request, *args, **kwargs): + self.user = self.get_object(queryset=User.objects.all()) + return super(UserAssetPermissionCreateView, self)\ + .post(request, *args, **kwargs) + + def get_form(self, form_class=None): + form = super(UserAssetPermissionCreateView, self)\ + .get_form(form_class=form_class) + form.user = self.user + return form + + def form_invalid(self, form): + return redirect(reverse('users:user-asset-permission', + kwargs={'pk': self.user.id})) + + def get_success_url(self): + return reverse('users:user-asset-permission', + kwargs={'pk': self.user.id}) + + +class UserGrantedAssetView(AdminUserRequiredMixin, DetailView): + model = User + template_name = 'users/user_granted_asset.html' + context_object_name = 'user' + + def get(self, request, *args, **kwargs): + self.object = self.get_object(queryset=User.objects.all()) + return super(UserGrantedAssetView, self).get(request, *args, **kwargs) + + def get_context_data(self, **kwargs): + context = { + 'app': 'User', + 'action': 'User granted asset', + } + kwargs.update(context) + return super(UserGrantedAssetView, self).get_context_data(**kwargs) \ No newline at end of file