From 27070123259b124f46f93403f2c56cafa8f7fb52 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sun, 25 Dec 2016 13:15:28 +0800 Subject: [PATCH] Finish access key auth --- apps/assets/api.py | 5 ++- apps/assets/hands.py | 2 +- apps/audits/api.py | 6 +-- apps/audits/hands.py | 2 +- apps/common/compat.py | 79 +++++++++++++++++++++++++++++++++++- apps/common/utils.py | 69 ++++++++++++++++++++++++++++++- apps/jumpserver/settings.py | 5 +-- apps/terminal/api.py | 6 +-- apps/terminal/hands.py | 4 +- apps/users/api.py | 2 +- apps/users/authentication.py | 31 ++++++++++---- apps/users/models/user.py | 4 +- apps/users/permissions.py | 12 +++--- 13 files changed, 190 insertions(+), 37 deletions(-) diff --git a/apps/assets/api.py b/apps/assets/api.py index 15afff6d7..96cb06f42 100644 --- a/apps/assets/api.py +++ b/apps/assets/api.py @@ -8,7 +8,7 @@ from django.shortcuts import get_object_or_404 from common.mixins import IDInFilterMixin from common.utils import get_object_or_none, signer -from .hands import IsSuperUserOrTerminalUser, IsSuperUser +from .hands import IsSuperUserOrAppUser, IsSuperUser from .models import AssetGroup, Asset, IDC, SystemUser, AdminUser from . import serializers @@ -18,6 +18,7 @@ class AssetViewSet(IDInFilterMixin, viewsets.ModelViewSet): queryset = Asset.objects.all() serializer_class = serializers.AssetSerializer filter_fields = ('id', 'ip', 'hostname') + permission_classes = (IsSuperUserOrAppUser,) def get_queryset(self): queryset = super(AssetViewSet, self).get_queryset() @@ -90,7 +91,7 @@ class AssetListUpdateApi(IDInFilterMixin, ListBulkCreateUpdateDestroyAPIView): class SystemUserAuthApi(APIView): - permission_classes = (IsSuperUserOrTerminalUser,) + permission_classes = (IsSuperUserOrAppUser,) def get(self, request, *args, **kwargs): system_user_id = request.query_params.get('system_user_id', -1) diff --git a/apps/assets/hands.py b/apps/assets/hands.py index 94e4a3d77..4536dc1bc 100644 --- a/apps/assets/hands.py +++ b/apps/assets/hands.py @@ -12,5 +12,5 @@ from users.utils import AdminUserRequiredMixin -from users.permissions import IsSuperUserOrTerminalUser, IsSuperUser +from users.permissions import IsSuperUserOrAppUser, IsSuperUser from users.models import User, UserGroup diff --git a/apps/audits/api.py b/apps/audits/api.py index ea906a022..15fe266e7 100644 --- a/apps/audits/api.py +++ b/apps/audits/api.py @@ -7,7 +7,7 @@ from rest_framework import generics, viewsets from rest_framework.views import APIView, Response from . import models, serializers -from .hands import IsSuperUserOrTerminalUser, Terminal +from .hands import IsSuperUserOrAppUser, Terminal class ProxyLogViewSet(viewsets.ModelViewSet): @@ -32,13 +32,13 @@ class ProxyLogViewSet(viewsets.ModelViewSet): queryset = models.ProxyLog.objects.all() serializer_class = serializers.ProxyLogSerializer - permission_classes = (IsSuperUserOrTerminalUser,) + permission_classes = (IsSuperUserOrAppUser,) class CommandLogViewSet(viewsets.ModelViewSet): queryset = models.CommandLog.objects.all() serializer_class = serializers.CommandLogSerializer - permission_classes = (IsSuperUserOrTerminalUser,) + permission_classes = (IsSuperUserOrAppUser,) # class CommandLogTitleApi(APIView): diff --git a/apps/audits/hands.py b/apps/audits/hands.py index 889ec6940..08926d966 100644 --- a/apps/audits/hands.py +++ b/apps/audits/hands.py @@ -4,5 +4,5 @@ from users.utils import AdminUserRequiredMixin from users.models import User from assets.models import Asset, SystemUser -from users.permissions import IsSuperUserOrTerminalUser +from users.permissions import IsSuperUserOrAppUser from terminal.models import Terminal diff --git a/apps/common/compat.py b/apps/common/compat.py index f93d0bec7..f2e757625 100644 --- a/apps/common/compat.py +++ b/apps/common/compat.py @@ -2,6 +2,81 @@ # -*- coding: utf-8 -*- # +""" +兼容Python版本 +""" + +import sys + +is_py2 = (sys.version_info[0] == 2) +is_py3 = (sys.version_info[0] == 3) + + +try: + import simplejson as json +except (ImportError, SyntaxError): + import json + + +if is_py2: + + def to_bytes(data): + """若输入为unicode, 则转为utf-8编码的bytes;其他则原样返回。""" + if isinstance(data, unicode): + return data.encode('utf-8') + else: + return data + + def to_string(data): + """把输入转换为str对象""" + return to_bytes(data) + + def to_unicode(data): + """把输入转换为unicode,要求输入是unicode或者utf-8编码的bytes。""" + if isinstance(data, bytes): + return data.decode('utf-8') + else: + return data + + def stringify(input): + if isinstance(input, dict): + return dict([(stringify(key), stringify(value)) for key,value in input.iteritems()]) + elif isinstance(input, list): + return [stringify(element) for element in input] + elif isinstance(input, unicode): + return input.encode('utf-8') + else: + return input + + builtin_str = str + bytes = str + str = unicode + + +elif is_py3: + + def to_bytes(data): + """若输入为str(即unicode),则转为utf-8编码的bytes;其他则原样返回""" + if isinstance(data, str): + return data.encode(encoding='utf-8') + else: + return data + + def to_string(data): + """若输入为bytes,则认为是utf-8编码,并返回str""" + if isinstance(data, bytes): + return data.decode('utf-8') + else: + return data + + def to_unicode(data): + """把输入转换为unicode,要求输入是unicode或者utf-8编码的bytes。""" + return to_string(data) + + def stringify(input): + return input + + builtin_str = str + bytes = bytes + str = str -if __name__ == '__main__': - pass diff --git a/apps/common/utils.py b/apps/common/utils.py index 269455d09..efc1b769d 100644 --- a/apps/common/utils.py +++ b/apps/common/utils.py @@ -3,12 +3,17 @@ from __future__ import unicode_literals from six import string_types +import base64 import os from itertools import chain import string import logging import datetime -import paramiko +import time +import hashlib +from email.utils import formatdate +import calendar +import threading import paramiko import sshpubkeys @@ -16,7 +21,6 @@ from itsdangerous import TimedJSONWebSignatureSerializer, JSONWebSignatureSerial BadSignature, SignatureExpired from django.shortcuts import reverse as dj_reverse from django.conf import settings -from django.core import signing from django.utils import timezone try: @@ -24,6 +28,8 @@ try: except ImportError: import StringIO +from .compat import to_bytes, to_string + SECRET_KEY = settings.SECRET_KEY @@ -255,4 +261,63 @@ def setattr_bulk(seq, key, value): return map(set_attr, seq) +def content_md5(data): + """计算data的MD5值,经过Base64编码并返回str类型。 + + 返回值可以直接作为HTTP Content-Type头部的值 + """ + m = hashlib.md5(to_bytes(data)) + return to_string(base64.b64encode(m.digest())) + +_STRPTIME_LOCK = threading.Lock() + +_GMT_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" +_ISO8601_FORMAT = "%Y-%m-%dT%H:%M:%S.000Z" + + +def to_unixtime(time_string, format_string): + with _STRPTIME_LOCK: + return int(calendar.timegm(time.strptime(time_string, format_string))) + + +def http_date(timeval=None): + """返回符合HTTP标准的GMT时间字符串,用strftime的格式表示就是"%a, %d %b %Y %H:%M:%S GMT"。 + 但不能使用strftime,因为strftime的结果是和locale相关的。 + """ + return formatdate(timeval, usegmt=True) + + +def http_to_unixtime(time_string): + """把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。 + + HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。 + """ + return to_unixtime(time_string, _GMT_FORMAT) + + +def iso8601_to_unixtime(time_string): + """把ISO8601时间字符串(形如,2012-02-24T06:07:48.000Z)转换为UNIX时间,精确到秒。""" + return to_unixtime(time_string, _ISO8601_FORMAT) + + +def http_to_unixtime(time_string): + """把HTTP Date格式的字符串转换为UNIX时间(自1970年1月1日UTC零点的秒数)。 + + HTTP Date形如 `Sat, 05 Dec 2015 11:10:29 GMT` 。 + """ + return to_unixtime(time_string, "%a, %d %b %Y %H:%M:%S GMT") + + +def make_signature(access_key_secret, date=None): + if isinstance(date, int): + date_gmt = http_date(date) + elif date is None: + date_gmt = http_date(int(time.time())) + else: + date_gmt = date + + data = str(access_key_secret) + "\n" + date_gmt + return content_md5(data) + + signer = Signer() \ No newline at end of file diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 927edfd21..f6b0e3cd7 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -272,13 +272,12 @@ REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. 'DEFAULT_PERMISSION_CLASSES': ( - 'users.permissions.IsValidUser', + 'users.permissions.IsSuperUser', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( - 'users.authentication.TerminalAuthentication', + 'users.authentication.AccessKeyAuthentication', 'users.authentication.AccessTokenAuthentication', 'rest_framework.authentication.TokenAuthentication', - 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', ), 'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',), diff --git a/apps/terminal/api.py b/apps/terminal/api.py index d51563223..b26f1775a 100644 --- a/apps/terminal/api.py +++ b/apps/terminal/api.py @@ -11,7 +11,7 @@ from rest_framework.permissions import AllowAny from common.utils import signer, get_object_or_none from .models import Terminal, TerminalHeatbeat from .serializers import TerminalSerializer, TerminalHeatbeatSerializer -from .hands import IsSuperUserOrTerminalUser, User +from .hands import IsSuperUserOrAppUser, User class TerminalRegister(ListCreateAPIView): @@ -62,13 +62,13 @@ class TerminalViewSet(viewsets.ModelViewSet): class TerminalHeatbeatApi(ListCreateAPIView): queryset = TerminalHeatbeat.objects.all() serializer_class = TerminalHeatbeatSerializer - permission_classes = (IsSuperUserOrTerminalUser,) + permission_classes = (IsSuperUserOrAppUser,) class TerminalHeatbeatViewSet(viewsets.ModelViewSet): queryset = TerminalHeatbeat.objects.all() serializer_class = TerminalHeatbeatSerializer - permission_classes = (IsSuperUserOrTerminalUser,) + permission_classes = (IsSuperUserOrAppUser,) def create(self, request, *args, **kwargs): terminal = request.user diff --git a/apps/terminal/hands.py b/apps/terminal/hands.py index 7cff4eefd..193b548a5 100644 --- a/apps/terminal/hands.py +++ b/apps/terminal/hands.py @@ -2,5 +2,5 @@ # from users.models import User -from users.permissions import IsSuperUserOrTerminalUser -from audits.models import ProxyLog +from users.permissions import IsSuperUserOrAppUser +from audits.models import ProxyLog \ No newline at end of file diff --git a/apps/users/api.py b/apps/users/api.py index 5c1988242..13784f4a6 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -16,7 +16,7 @@ from common.utils import get_logger from .utils import check_user_valid, get_or_refresh_token from .models import User, UserGroup from .hands import write_login_log_async -from .permissions import IsSuperUser, IsTerminalUser, IsValidUser, IsSuperUserOrTerminalUser +from .permissions import IsSuperUser, IsAppUser, IsValidUser, IsSuperUserOrAppUser from . import serializers diff --git a/apps/users/authentication.py b/apps/users/authentication.py index 1c3fffb62..d5cdc2a3b 100644 --- a/apps/users/authentication.py +++ b/apps/users/authentication.py @@ -2,6 +2,8 @@ # import base64 +import hashlib +import time from django.core.cache import cache from django.conf import settings @@ -12,7 +14,7 @@ from django.utils.six import text_type from django.utils.translation import ugettext_lazy as _ from rest_framework import HTTP_HEADER_ENCODING -from common.utils import get_object_or_none +from common.utils import get_object_or_none, make_signature, http_to_unixtime from .utils import get_or_refresh_token from .models import User, AccessKey @@ -22,7 +24,6 @@ def get_request_date_header(request): if isinstance(date, text_type): # Work around django test client oddness date = date.encode(HTTP_HEADER_ENCODING) - return date @@ -54,18 +55,30 @@ class AccessKeyAuthentication(authentication.BaseAuthentication): raise exceptions.AuthenticationFailed(msg) access_key_id = sign[0] - secret = sign[1] - date = + request_signature = sign[1] - return self.authenticate_credentials(sign) - - def authenticate_credentials(self, access_key_id, secret, datetime): - access_key_id = sign[0] - secret = sign[1] + return self.authenticate_credentials(request, access_key_id, request_signature) + def authenticate_credentials(self, request, access_key_id, request_signature): access_key = get_object_or_none(AccessKey, id=access_key_id) + request_date = get_request_date_header(request) if access_key is None or not access_key.user: raise exceptions.AuthenticationFailed(_('Invalid signature.')) + access_key_secret = access_key.secret + + print(request_date) + + 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')) + + 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))) if not access_key.user.is_active: raise exceptions.AuthenticationFailed(_('User disabled.')) diff --git a/apps/users/models/user.py b/apps/users/models/user.py index 504ce403b..c97bf922b 100644 --- a/apps/users/models/user.py +++ b/apps/users/models/user.py @@ -101,8 +101,8 @@ class User(AbstractUser): return False @property - def is_terminal(self): - return False + def is_app(self): + return self.role == 'App' @is_superuser.setter def is_superuser(self, value): diff --git a/apps/users/permissions.py b/apps/users/permissions.py index 7ec7ad1ee..68d446089 100644 --- a/apps/users/permissions.py +++ b/apps/users/permissions.py @@ -23,12 +23,12 @@ class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): and request.user.is_valid -class IsTerminalUser(IsValidUser, permissions.BasePermission): +class IsAppUser(IsValidUser, permissions.BasePermission): """Allows access only to app user """ def has_permission(self, request, view): - return super(IsTerminalUser, self).has_permission(request, view) \ - and isinstance(request.user, Terminal) + return super(IsAppUser, self).has_permission(request, view) \ + and request.user.is_app() class IsSuperUser(IsValidUser, permissions.BasePermission): @@ -39,12 +39,12 @@ class IsSuperUser(IsValidUser, permissions.BasePermission): and request.user.is_superuser -class IsSuperUserOrTerminalUser(IsValidUser, permissions.BasePermission): +class IsSuperUserOrAppUser(IsValidUser, permissions.BasePermission): """Allows access between superuser and app user""" def has_permission(self, request, view): - return super(IsSuperUserOrTerminalUser, self).has_permission(request, view) \ - and (request.user.is_superuser or request.user.is_terminal) + return super(IsSuperUserOrAppUser, self).has_permission(request, view) \ + and (request.user.is_superuser or request.user.is_app) if __name__ == '__main__':