Finish token access api

pull/530/head
ibuler 2016-10-31 18:58:23 +08:00
parent 92251f2a45
commit 315af35296
7 changed files with 110 additions and 36 deletions

View File

@ -267,10 +267,11 @@ REST_FRAMEWORK = {
'users.backends.IsValidUser', 'users.backends.IsValidUser',
), ),
'DEFAULT_AUTHENTICATION_CLASSES': ( 'DEFAULT_AUTHENTICATION_CLASSES': (
'users.backends.TerminalAuthentication',
'users.backends.AccessTokenAuthentication',
'rest_framework.authentication.BasicAuthentication', 'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.TokenAuthentication', 'rest_framework.authentication.TokenAuthentication',
'users.backends.TerminalAuthentication',
), ),
} }
# This setting is required to override the Django's main loop, when running in # This setting is required to override the Django's main loop, when running in

View File

@ -1,14 +1,20 @@
# ~*~ coding: utf-8 ~*~ # ~*~ 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 import generics, status
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView
from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView
from rest_framework import authentication
from common.mixins import BulkDeleteApiMixin from common.mixins import BulkDeleteApiMixin
from common.utils import get_logger from common.utils import get_logger
from .utils import check_user_valid, token_gen
from .models import User, UserGroup from .models import User, UserGroup
from .serializers import UserDetailSerializer, UserAndGroupSerializer, \ from .serializers import UserDetailSerializer, UserAndGroupSerializer, \
GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer GroupDetailSerializer, UserPKUpdateSerializer, UserBulkUpdateSerializer, GroupBulkUpdateSerializer
@ -113,21 +119,26 @@ class DeleteUserFromGroupApi(generics.DestroyAPIView):
instance.users.remove(user) instance.users.remove(user)
class AppUserRegisterApi(generics.CreateAPIView): class UserTokenApi(APIView):
"""App send a post request to register a app user permission_classes = ()
expiration = settings.CONFIG.TOKEN_EXPIRATION or 3600
request params contains `username_signed`, You can unsign it, def post(self, request, *args, **kwargs):
username = unsign(username_signed), if you get the username, username = request.data.get('username', '')
It's present it's a valid request, or return (401, Invalid request), password = request.data.get('password', '')
then your should check if the user exist or not. If exist, public_key = request.data.get('public_key', '')
return (200, register success), If not, you should be save it, and remote_addr = request.META.get('REMOTE_ADDR', '')
notice admin user, The user default is not active before admin user
unblock it. 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

View File

@ -1,13 +1,17 @@
# -*- coding: utf-8 -*- # -*- 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 import authentication, exceptions, permissions
from rest_framework.compat import is_authenticated from rest_framework.compat import is_authenticated
from django.utils.translation import ugettext as _
from common.utils import unsign, get_object_or_none from common.utils import unsign, get_object_or_none
from .hands import Terminal from .hands import Terminal
from .models import User
class TerminalAuthentication(authentication.BaseAuthentication): class TerminalAuthentication(authentication.BaseAuthentication):
@ -47,6 +51,47 @@ class TerminalAuthentication(authentication.BaseAuthentication):
return terminal, None 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): class IsValidUser(permissions.IsAuthenticated, permissions.BasePermission):
"""Allows access to valid user, is active and not expired""" """Allows access to valid user, is active and not expired"""

View File

@ -193,6 +193,11 @@ class User(AbstractUser):
return True return True
return False return False
def check_public_key(self, public_key):
if self.public_key == public_key:
return True
return False
def generate_reset_token(self): def generate_reset_token(self):
return signing.dumps({'reset': self.id, 'email': self.email}) return signing.dumps({'reset': self.id, 'email': self.email})

View File

@ -36,6 +36,7 @@ urlpatterns = [
urlpatterns += [ urlpatterns += [
url(r'^v1/users/$', api.UserListUpdateApi.as_view(), name='user-bulk-update-api'), 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<pk>\d+)/$', api.UserDetailApi.as_view(), name='user-patch-api'), url(r'^v1/users/(?P<pk>\d+)/$', api.UserDetailApi.as_view(), name='user-patch-api'),
url(r'^v1/users/(?P<pk>\d+)/reset-password/$', api.UserResetPasswordApi.as_view(), name='user-reset-password-api'), url(r'^v1/users/(?P<pk>\d+)/reset-password/$', api.UserResetPasswordApi.as_view(), name='user-reset-password-api'),
url(r'^v1/users/(?P<pk>\d+)/reset-pk/$', api.UserResetPKApi.as_view(), name='user-reset-pk-api'), url(r'^v1/users/(?P<pk>\d+)/reset-pk/$', api.UserResetPKApi.as_view(), name='user-reset-pk-api'),

View File

@ -4,6 +4,7 @@ from __future__ import unicode_literals
import logging import logging
import os import os
import re import re
import uuid
from django.conf import settings from django.conf import settings
from django.contrib.auth.mixins import UserPassesTestMixin 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()]) 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) password = kwargs.pop('password', None)
public_key = kwargs.pop('public_key', None) public_key = kwargs.pop('public_key', None)
user = get_object_or_none(User, **kwargs) user = get_object_or_none(User, **kwargs)
if password and not user.check_password(password): if user is None or not user.is_valid:
user = None return None
if password and user.check_password(password):
if public_key and not user.public_key == public_key: return user
user = None if public_key and user.public_key == public_key:
if user and user.is_valid:
return user return user
return None return None
def token_gen(*args, **kwargs):
return uuid.uuid4().get_hex()

View File

@ -10,6 +10,7 @@
import os import os
BASE_DIR = os.path.dirname(os.path.abspath(__file__)) BASE_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_DIR = os.path.join(BASE_DIR, 'logs')
class Config: 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/ # 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 # But we should know the absolute url like: http://jms.jumpserver.org/login/, so SITE_URL is
# HTTP_PROTOCOL://HOST[:PORT] # 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' SITE_URL = 'http://localhost'
# Django security setting, if your disable debug model, you should setting that # 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 # When Django start it will bind this host and port
# ./manage.py runserver 127.0.0.1:8080 # ./manage.py runserver 127.0.0.1:8080
# Todo: Gunicorn or uwsgi run may be use it # 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 HTTP_LISTEN_PORT = 8080
# Use Redis as broker for celery and web socket # Use Redis as broker for celery and web socket
@ -61,6 +61,9 @@ class Config:
REDIS_PORT = 6379 REDIS_PORT = 6379
# REDIS_PASSWORD = '' # REDIS_PASSWORD = ''
# Api token expiration when create
TOKEN_EXPIRATION = 3600
# Email SMTP setting, we only support smtp send mail # Email SMTP setting, we only support smtp send mail
# EMAIL_HOST = 'smtp.qq.com' # EMAIL_HOST = 'smtp.qq.com'
# EMAIL_PORT = 25 # EMAIL_PORT = 25
@ -70,14 +73,10 @@ class Config:
# EMAIL_USE_TLS = False # If port is 587, set True # EMAIL_USE_TLS = False # If port is 587, set True
# EMAIL_SUBJECT_PREFIX = '[Jumpserver] ' # EMAIL_SUBJECT_PREFIX = '[Jumpserver] '
# SSH use password or public key for auth
SSH_PASSWORD_AUTH = False
SSH_PUBLIC_KEY_AUTH = True
def __init__(self): def __init__(self):
pass pass
def __getattr__(self, item): def __getattr__(self, key):
return None return None
@ -86,6 +85,14 @@ class DevelopmentConfig(Config):
DISPLAY_PER_PAGE = 20 DISPLAY_PER_PAGE = 20
DB_ENGINE = 'sqlite' DB_ENGINE = 'sqlite'
DB_NAME = os.path.join(BASE_DIR, 'db.sqlite3') 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): class ProductionConfig(Config):
@ -106,3 +113,4 @@ config = {
} }
env = 'development' env = 'development'