diff --git a/apps/jumpserver/settings.py b/apps/jumpserver/settings.py index 0b2adfa0c..0e509807e 100644 --- a/apps/jumpserver/settings.py +++ b/apps/jumpserver/settings.py @@ -267,10 +267,11 @@ REST_FRAMEWORK = { 'users.backends.IsValidUser', ), 'DEFAULT_AUTHENTICATION_CLASSES': ( + 'users.backends.TerminalAuthentication', + 'users.backends.AccessTokenAuthentication', 'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.TokenAuthentication', - 'users.backends.TerminalAuthentication', ), } # 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 af7b4f0d8..f58cc92ff 100644 --- a/apps/users/api.py +++ b/apps/users/api.py @@ -1,14 +1,20 @@ # ~*~ coding: utf-8 ~*~ # -from django.shortcuts import get_object_or_404 +import base64 +from django.shortcuts import get_object_or_404 +from django.core.cache import cache +from django.conf import settings from rest_framework import generics, status from rest_framework.response import Response +from rest_framework.views import APIView from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView +from rest_framework import authentication from common.mixins import BulkDeleteApiMixin from common.utils import get_logger +from .utils import check_user_valid, token_gen from .models import User, UserGroup from .serializers import UserDetailSerializer, UserAndGroupSerializer, \ GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer @@ -113,21 +119,26 @@ class DeleteUserFromGroupApi(generics.DestroyAPIView): instance.users.remove(user) -class AppUserRegisterApi(generics.CreateAPIView): - """App send a post request to register a app user +class UserTokenApi(APIView): + permission_classes = () + expiration = settings.CONFIG.TOKEN_EXPIRATION or 3600 - request params contains `username_signed`, You can unsign it, - username = unsign(username_signed), if you get the username, - It's present it's a valid request, or return (401, Invalid request), - then your should check if the user exist or not. If exist, - return (200, register success), If not, you should be save it, and - notice admin user, The user default is not active before admin user - unblock it. + def post(self, request, *args, **kwargs): + username = request.data.get('username', '') + password = request.data.get('password', '') + public_key = request.data.get('public_key', '') + remote_addr = request.META.get('REMOTE_ADDR', '') + + remote_addr = base64.b64encode(remote_addr).replace('=', '') + user = check_user_valid(username=username, password=password, public_key=public_key) + if user: + token = cache.get('%s_%s' % (user.id, remote_addr)) + if not token: + token = token_gen(user) + + cache.set(token, user.id, self.expiration) + cache.set('%s_%s' % (user.id, remote_addr), token, self.expiration) + return Response({'token': token}) + else: + return Response({'msg': 'Invalid password or public key or user is not active or expired'}) - Save fields: - username: - name: name + request.ip - email: username + '@app.org' - role: App - """ - pass diff --git a/apps/users/backends.py b/apps/users/backends.py index cb249ed72..bd30cdb75 100644 --- a/apps/users/backends.py +++ b/apps/users/backends.py @@ -1,13 +1,17 @@ # -*- coding: utf-8 -*- # +import base64 + +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 rest_framework.compat import is_authenticated -from django.utils.translation import ugettext as _ from common.utils import unsign, get_object_or_none - from .hands import Terminal +from .models import User class TerminalAuthentication(authentication.BaseAuthentication): @@ -47,6 +51,47 @@ class TerminalAuthentication(authentication.BaseAuthentication): return terminal, None +class AccessTokenAuthentication(authentication.BaseAuthentication): + keyword = 'Token' + model = User + expiration = settings.CONFIG.TOKEN_EXPIRATION or 3600 + + def authenticate(self, request): + auth = authentication.get_authorization_header(request).split() + + if not auth or auth[0].lower() != self.keyword.lower().encode(): + return None + + if len(auth) == 1: + 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.') + raise exceptions.AuthenticationFailed(msg) + + try: + token = auth[1].decode() + except UnicodeError: + msg = _('Invalid token header. Sign string should not contain invalid characters.') + raise exceptions.AuthenticationFailed(msg) + return self.authenticate_credentials(token, request) + + def authenticate_credentials(self, token, request): + user_id = cache.get(token) + user = get_object_or_none(User, id=user_id) + + if not user: + msg = _('Invalid token') + raise exceptions.AuthenticationFailed(msg) + + remote_addr = request.META.get('REMOTE_ADDR', '') + remote_addr = base64.b16encode(remote_addr).replace('=', '') + cache.set(token, user_id, self.expiration) + cache.set('%s_%s' % (user.id, remote_addr), token, self.expiration) + + return user, None + + class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission): """Allows access to valid user, is active and not expired""" diff --git a/apps/users/models.py b/apps/users/models.py index 98bcd3454..2bbaae188 100644 --- a/apps/users/models.py +++ b/apps/users/models.py @@ -193,6 +193,11 @@ class User(AbstractUser): return True return False + def check_public_key(self, public_key): + if self.public_key == public_key: + return True + return False + def generate_reset_token(self): return signing.dumps({'reset': self.id, 'email': self.email}) diff --git a/apps/users/urls.py b/apps/users/urls.py index 4bf42d94a..7a9f94029 100644 --- a/apps/users/urls.py +++ b/apps/users/urls.py @@ -36,6 +36,7 @@ urlpatterns = [ urlpatterns += [ url(r'^v1/users/$', api.UserListUpdateApi.as_view(), name='user-bulk-update-api'), + url(r'^v1/users/token$', api.UserTokenApi.as_view(), name='user-token-api'), url(r'^v1/users/(?P\d+)/$', api.UserDetailApi.as_view(), name='user-patch-api'), url(r'^v1/users/(?P\d+)/reset-password/$', api.UserResetPasswordApi.as_view(), name='user-reset-password-api'), url(r'^v1/users/(?P\d+)/reset-pk/$', api.UserResetPKApi.as_view(), name='user-reset-pk-api'), diff --git a/apps/users/utils.py b/apps/users/utils.py index de21a1539..5291584ae 100644 --- a/apps/users/utils.py +++ b/apps/users/utils.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import logging import os import re +import uuid from django.conf import settings from django.contrib.auth.mixins import UserPassesTestMixin @@ -206,18 +207,20 @@ def validate_ssh_pk(text): return startState([n.strip() for n in text.splitlines()]) -def check_user_is_valid(**kwargs): +def check_user_valid(**kwargs): password = kwargs.pop('password', None) public_key = kwargs.pop('public_key', None) user = get_object_or_none(User, **kwargs) - if password and not user.check_password(password): - user = None - - if public_key and not user.public_key == public_key: - user = None - - if user and user.is_valid: + if user is None or not user.is_valid: + return None + if password and user.check_password(password): + return user + if public_key and user.public_key == public_key: return user - return None + + +def token_gen(*args, **kwargs): + return uuid.uuid4().get_hex() + diff --git a/config_example.py b/config_example.py index 9bae73181..ae8257436 100644 --- a/config_example.py +++ b/config_example.py @@ -10,6 +10,7 @@ import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +LOG_DIR = os.path.join(BASE_DIR, 'logs') class Config: @@ -23,7 +24,6 @@ class Config: # It's used to identify your site, When we send a create mail to user, we only know login url is /login/ # But we should know the absolute url like: http://jms.jumpserver.org/login/, so SITE_URL is # HTTP_PROTOCOL://HOST[:PORT] - # Todo: May be use :method: get_current_site more grace, bug restful api unknown ok or not SITE_URL = 'http://localhost' # Django security setting, if your disable debug model, you should setting that @@ -53,7 +53,7 @@ class Config: # When Django start it will bind this host and port # ./manage.py runserver 127.0.0.1:8080 # Todo: Gunicorn or uwsgi run may be use it - HTTP_LISTEN_HOST = '0.0.0.0' + HTTP_BIND_HOST = '127.0.0.1' HTTP_LISTEN_PORT = 8080 # Use Redis as broker for celery and web socket @@ -61,6 +61,9 @@ class Config: REDIS_PORT = 6379 # REDIS_PASSWORD = '' + # Api token expiration when create + TOKEN_EXPIRATION = 3600 + # Email SMTP setting, we only support smtp send mail # EMAIL_HOST = 'smtp.qq.com' # EMAIL_PORT = 25 @@ -70,14 +73,10 @@ class Config: # EMAIL_USE_TLS = False # If port is 587, set True # EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' - # SSH use password or public key for auth - SSH_PASSWORD_AUTH = False - SSH_PUBLIC_KEY_AUTH = True - def __init__(self): pass - def __getattr__(self, item): + def __getattr__(self, key): return None @@ -86,6 +85,14 @@ class DevelopmentConfig(Config): DISPLAY_PER_PAGE = 20 DB_ENGINE = 'sqlite' DB_NAME = os.path.join(BASE_DIR, 'db.sqlite3') + EMAIL_HOST = 'smtp.exmail.qq.com' + EMAIL_PORT = 465 + EMAIL_HOST_USER = 'ask@jumpserver.org' + EMAIL_HOST_PASSWORD = 'xfDf4x1n' + EMAIL_USE_SSL = True # If port is 465, set True + EMAIL_USE_TLS = False # If port is 587, set True + EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' + SITE_URL = 'http://localhost:8080' class ProductionConfig(Config): @@ -106,3 +113,4 @@ config = { } env = 'development' +