Commit 315af352 authored by ibuler's avatar ibuler

Finish token access api

parent 92251f2a
...@@ -267,10 +267,11 @@ REST_FRAMEWORK = { ...@@ -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
......
# ~*~ 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): ...@@ -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,
username = unsign(username_signed), if you get the username, def post(self, request, *args, **kwargs):
It's present it's a valid request, or return (401, Invalid request), username = request.data.get('username', '')
then your should check if the user exist or not. If exist, password = request.data.get('password', '')
return (200, register success), If not, you should be save it, and public_key = request.data.get('public_key', '')
notice admin user, The user default is not active before admin user remote_addr = request.META.get('REMOTE_ADDR', '')
unblock it.
remote_addr = base64.b64encode(remote_addr).replace('=', '')
Save fields: user = check_user_valid(username=username, password=password, public_key=public_key)
username: if user:
name: name + request.ip token = cache.get('%s_%s' % (user.id, remote_addr))
email: username + '@app.org' if not token:
role: App token = token_gen(user)
"""
pass 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'})
# -*- 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): ...@@ -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"""
......
...@@ -193,6 +193,11 @@ class User(AbstractUser): ...@@ -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})
......
...@@ -36,6 +36,7 @@ urlpatterns = [ ...@@ -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'),
......
...@@ -4,6 +4,7 @@ from __future__ import unicode_literals ...@@ -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): ...@@ -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):
return user
if public_key and user.public_key == public_key:
return user
return None
if public_key and not user.public_key == public_key:
user = None
if user and user.is_valid: def token_gen(*args, **kwargs):
return user return uuid.uuid4().get_hex()
return None
...@@ -10,6 +10,7 @@ ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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): ...@@ -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 = { ...@@ -106,3 +113,4 @@ config = {
} }
env = 'development' env = 'development'
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment