diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index b57a1f07a..13029b815 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -68,7 +68,6 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', - 'ws4redis', ] @@ -264,12 +263,14 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': ( - 'rest_framework.permissions.IsAdminUser', + # 'rest_framework.permissions.IsAuthenticated', + 'users.backends.IsValidUser', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', + 'users.backends.AppSignAuthentication', ), } # This setting is required to override the Django's main loop, when running in diff --git a/apps/users/api.py b/apps/users/api.py index 4302cec6e..67ce7ae4f 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -7,11 +7,12 @@ from rest_framework import generics, status from rest_framework.response import Response from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView +from common.mixins import BulkDeleteApiMixin +from common.utils import get_logger from .models import User, UserGroup from .serializers import UserDetailSerializer, UserAndGroupSerializer, \ GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer -from common.mixins import BulkDeleteApiMixin -from common.utils import get_logger +from .backends import IsSuperUser, IsAppUser, IsValidUser, IsSuperUserOrAppUser logger = get_logger(__name__) @@ -84,6 +85,10 @@ class GroupDetailApi(generics.RetrieveUpdateDestroyAPIView): class UserListUpdateApi(BulkDeleteApiMixin, ListBulkCreateUpdateDestroyAPIView): queryset = User.objects.all() serializer_class = UserBulkUpdateSerializer + permission_classes = (IsSuperUserOrAppUser,) + + def get(self, request, *args, **kwargs): + return super(UserListUpdateApi, self).get(request, *args, **kwargs) class GroupListUpdateApi(BulkDeleteApiMixin, ListBulkCreateUpdateDestroyAPIView): diff --git a/apps/users/backends.py b/apps/users/backends.py index ac4d67ddd..f4b98980a 100644 --- a/apps/users/backends.py +++ b/apps/users/backends.py @@ -1,14 +1,15 @@ # -*- coding: utf-8 -*- # -from rest_framework import authentication, exceptions +from rest_framework import authentication, exceptions, permissions +from rest_framework.compat import is_authenticated from django.utils.translation import ugettext as _ -from common.utils import unsign +from common.utils import unsign, get_object_or_none from .models import User -class APPSignAuthentication(authentication.BaseAuthentication): +class AppSignAuthentication(authentication.BaseAuthentication): keyword = 'Sign' model = User @@ -30,17 +31,50 @@ class APPSignAuthentication(authentication.BaseAuthentication): except UnicodeError: msg = _('Invalid token header. Sign string should not contain invalid characters.') raise exceptions.AuthenticationFailed(msg) - return self.authenticate_credentials(sign) - def authenticate_credentials(self, key): - try: - token = self.model.objects.select_related('user').get(key=key) - except self.model.DoesNotExist: - raise exceptions.AuthenticationFailed(_('Invalid token.')) + def authenticate_credentials(self, sign): + app = unsign(sign, max_age=300) + if app: + user = get_object_or_none(self.model, username=app, role='App') + else: + raise exceptions.AuthenticationFailed(_('Invalid sign.')) - if not token.user.is_active: + if not user.is_active: raise exceptions.AuthenticationFailed(_('User inactive or deleted.')) + return user, None + + +class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): + """Allows access to valid user, is active and not expired""" + + def has_permission(self, request, view): + return super(IsValidUser, self).has_permission(request, view) \ + and request.user.is_valid + + +class IsAppUser(IsValidUser, permissions.BasePermission): + """Allows access only to app user """ + + def has_permission(self, request, view): + return super(IsAppUser, self).has_permission(request, view) \ + and request.user.is_app_user + + +class IsSuperUser(IsValidUser, permissions.BasePermission): + """Allows access only to superuser""" + + def has_permission(self, request, view): + return super(IsSuperUser, self).has_permission(request, view) \ + and request.user.is_superuser + + +class IsSuperUserOrAppUser(IsValidUser, permissions.BasePermission): + """Allows access between superuser and app user""" + + def has_permission(self, request, view): + return super(IsSuperUserOrAppUser, self).has_permission(request, view) \ + and (request.user.is_superuser or request.user.is_app_user) if __name__ == '__main__': diff --git a/apps/users/models.py b/apps/users/models.py index ebe45afaa..c7464f4fb 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -69,6 +69,7 @@ class User(AbstractUser): ROLE_CHOICES = ( ('Admin', _('Administrator')), ('User', _('User')), + ('App', _('Application')), ) username = models.CharField(max_length=20, unique=True, verbose_name=_('Username')) @@ -148,9 +149,18 @@ class User(AbstractUser): else: self.role = 'User' + is_admin = is_superuser + + @property + def is_app_user(self): + if self.role == 'App': + return True + else: + return False + @property def is_staff(self): - if self.is_authenticated and self.is_active and not self.is_expired and self.is_superuser: + if self.is_authenticated and self.is_valid: return True else: return False @@ -185,14 +195,14 @@ class User(AbstractUser): Token.objects.filter(user=self).delete() return Token.objects.create(user=self) - def generate_reset_token(self): - return signing.dumps({'reset': self.id, 'email': self.email}) - def is_member_of(self, user_group): if user_group in self.groups.all(): return True return False + def generate_reset_token(self): + return signing.dumps({'reset': self.id, 'email': self.email}) + @classmethod def validate_reset_token(cls, token, max_age=3600): try: