mirror of https://github.com/jumpserver/jumpserver
Finish token access api
parent
92251f2a45
commit
315af35296
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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"""
|
||||||
|
|
||||||
|
|
|
@ -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 = [
|
||||||
|
|
||||||
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
|
||||||
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()
|
||||||
|
|
||||||
|
|
|
@ -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'
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue