mirror of
https://github.com/jumpserver/jumpserver.git
synced 2026-07-02 07:01:30 +00:00
perf: Login asset otp
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
@@ -18,7 +19,7 @@ from authentication.permissions import UserConfirmation, ConfirmType
|
||||
from common.api.mixin import ExtraFilterFieldsMixin
|
||||
from common.drf.filters import AttrRulesFilterBackend
|
||||
from common.permissions import IsValidUser
|
||||
from common.utils import lazyproperty, get_logger
|
||||
from common.utils import lazyproperty, get_logger, generate_otp_code
|
||||
from orgs.mixins.api import OrgBulkModelViewSet
|
||||
from orgs.utils import tmp_to_root_org
|
||||
from rbac.permissions import RBACPermission
|
||||
@@ -46,6 +47,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
'clear_secret': 'accounts.change_account',
|
||||
'move_to_assets': 'accounts.delete_account',
|
||||
'copy_to_assets': 'accounts.add_account',
|
||||
'generate_otp': 'accounts.add_account',
|
||||
'chat': 'accounts.view_account',
|
||||
}
|
||||
export_as_zip = True
|
||||
@@ -173,7 +175,7 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
asset_ids = request.data.get('assets', [])
|
||||
assets = Asset.objects.filter(id__in=asset_ids)
|
||||
field_names = [
|
||||
'name', 'username', 'secret_type', 'secret',
|
||||
'name', 'username', 'secret_type', 'secret', 'otp_secret_key',
|
||||
'privileged', 'is_active', 'source', 'source_id', 'comment'
|
||||
]
|
||||
account_data = {field: getattr(account, field) for field in field_names}
|
||||
@@ -207,6 +209,12 @@ class AccountViewSet(OrgBulkModelViewSet):
|
||||
def copy_to_assets(self, request, *args, **kwargs):
|
||||
return self._copy_or_move_to_assets(request, move=False)
|
||||
|
||||
@action(methods=['post'], detail=False, url_path='generate-otp')
|
||||
def generate_otp(self, request, *args, **kwargs):
|
||||
serializer = serializers.AccountOTPGenerateSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
return Response(serializer.get_payload(), status=HTTP_200_OK)
|
||||
|
||||
@action(methods=['get'], detail=False, url_path='chat')
|
||||
def chat(self, request, *args, **kwargs):
|
||||
with tmp_to_root_org():
|
||||
@@ -227,8 +235,27 @@ class AccountSecretsViewSet(AccountRecordViewLogMixin, AccountViewSet):
|
||||
rbac_perms = {
|
||||
'list': 'accounts.view_accountsecret',
|
||||
'retrieve': 'accounts.view_accountsecret',
|
||||
'otp_code': 'accounts.view_accountsecret',
|
||||
}
|
||||
|
||||
@action(methods=['get'], detail=True, url_path='otp-code', permission_classes=[RBACPermission])
|
||||
def otp_code(self, request, *args, **kwargs):
|
||||
account = self.get_object()
|
||||
if not settings.SECURITY_ACCOUNT_SECRET_READ:
|
||||
return Response(
|
||||
{'detail': _('Account secret reading has been disabled by administrator')},
|
||||
status=HTTP_400_BAD_REQUEST
|
||||
)
|
||||
if not account.otp_secret_key:
|
||||
return Response(
|
||||
{'detail': _('Account OTP secret key not configured')},
|
||||
status=HTTP_400_BAD_REQUEST
|
||||
)
|
||||
return Response({
|
||||
'otp_code': generate_otp_code(account.otp_secret_key),
|
||||
'has_otp_secret_key': True,
|
||||
}, status=HTTP_200_OK)
|
||||
|
||||
|
||||
class AssetAccountBulkCreateApi(CreateAPIView):
|
||||
serializer_class = serializers.AssetAccountBulkSerializer
|
||||
|
||||
19
apps/accounts/migrations/0008_account_otp_secret_key.py
Normal file
19
apps/accounts/migrations/0008_account_otp_secret_key.py
Normal file
@@ -0,0 +1,19 @@
|
||||
# Generated by Django 4.1.13 on 2026-06-08 00:00
|
||||
|
||||
import common.db.fields
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0007_alter_account_connectivity'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='account',
|
||||
name='otp_secret_key',
|
||||
field=common.db.fields.EncryptCharField(blank=True, max_length=128, null=True, verbose_name='OTP secret key'),
|
||||
),
|
||||
]
|
||||
@@ -5,6 +5,7 @@ from simple_history.models import HistoricalRecords
|
||||
|
||||
from assets.models import Asset
|
||||
from assets.models.base import AbsConnectivity
|
||||
from common.db import fields
|
||||
from common.utils import lazyproperty, get_logger
|
||||
from labels.mixins import LabeledMixin
|
||||
from .base import BaseAccount
|
||||
@@ -102,6 +103,9 @@ class Account(AbsConnectivity, LabeledMixin, BaseAccount, JSONFilterMixin):
|
||||
change_secret_status = models.CharField(
|
||||
max_length=16, null=True, blank=True, verbose_name=_('Change secret status')
|
||||
)
|
||||
otp_secret_key = fields.EncryptCharField(
|
||||
max_length=128, blank=True, null=True, verbose_name=_('OTP secret key')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _('Account')
|
||||
|
||||
@@ -15,13 +15,25 @@ from accounts.tasks import push_accounts_to_assets_task
|
||||
from assets.const import Category, AllTypes
|
||||
from assets.models import Asset
|
||||
from common.serializers import SecretReadableMixin, SecretReadableCheckMixin, CommonBulkModelSerializer
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField
|
||||
from common.utils import get_logger
|
||||
from common.serializers.fields import ObjectRelatedField, LabeledChoiceField, EncryptedField
|
||||
from common.utils import (
|
||||
get_logger, generate_otp_secret_key, generate_otp_code,
|
||||
normalize_otp_secret_key, is_otp_secret_key_valid,
|
||||
)
|
||||
from .base import BaseAccountSerializer, AuthValidateMixin
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
def validate_otp_secret_key_value(value):
|
||||
otp_secret_key = normalize_otp_secret_key(value)
|
||||
if not otp_secret_key:
|
||||
return ''
|
||||
if not is_otp_secret_key_valid(otp_secret_key):
|
||||
raise serializers.ValidationError(_('OTP secret key invalid'))
|
||||
return otp_secret_key
|
||||
|
||||
|
||||
class AccountCreateUpdateSerializerMixin(serializers.Serializer):
|
||||
template = serializers.PrimaryKeyRelatedField(
|
||||
queryset=AccountTemplate.objects, required=False,
|
||||
@@ -234,6 +246,17 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
label=_('Su from'), attrs=('id', 'name', 'username')
|
||||
)
|
||||
ds = ObjectRelatedField(read_only=True, label=_('Directory service'), attrs=('id', 'name', 'domain_name'))
|
||||
has_otp_secret_key = serializers.SerializerMethodField(
|
||||
label=_('Has OTP secret key'), read_only=True
|
||||
)
|
||||
otp_secret_key = EncryptedField(
|
||||
label=_('OTP secret key'),
|
||||
required=False,
|
||||
max_length=128,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
class Meta(BaseAccountSerializer.Meta):
|
||||
model = Account
|
||||
@@ -243,9 +266,9 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
]
|
||||
fields = BaseAccountSerializer.Meta.fields + [
|
||||
'su_from', 'asset', 'version', 'ds',
|
||||
'source', 'source_id', 'secret_reset',
|
||||
'source', 'source_id', 'has_otp_secret_key', 'otp_secret_key', 'secret_reset',
|
||||
] + AccountCreateUpdateSerializerMixin.Meta.fields + automation_fields
|
||||
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields
|
||||
read_only_fields = BaseAccountSerializer.Meta.read_only_fields + automation_fields + ['has_otp_secret_key']
|
||||
fields = [f for f in fields if f not in ['spec_info']]
|
||||
extra_kwargs = {
|
||||
**BaseAccountSerializer.Meta.extra_kwargs,
|
||||
@@ -280,6 +303,13 @@ class AccountSerializer(AccountCreateUpdateSerializerMixin, BaseAccountSerialize
|
||||
raise serializers.ValidationError(field_errors)
|
||||
return attrs
|
||||
|
||||
def validate_otp_secret_key(self, value):
|
||||
return validate_otp_secret_key_value(value)
|
||||
|
||||
@staticmethod
|
||||
def get_has_otp_secret_key(obj):
|
||||
return bool(getattr(obj, 'otp_secret_key', ''))
|
||||
|
||||
|
||||
class AccountDetailSerializer(AccountSerializer):
|
||||
has_secret = serializers.BooleanField(label=_("Has secret"), read_only=True)
|
||||
@@ -305,11 +335,19 @@ class AssetAccountBulkSerializer(
|
||||
max_length=128, required=False, write_only=True, allow_null=True, label=_("Su from"),
|
||||
allow_blank=True,
|
||||
)
|
||||
otp_secret_key = EncryptedField(
|
||||
label=_('OTP secret key'),
|
||||
required=False,
|
||||
max_length=128,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
write_only=True,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
'name', 'username', 'secret', 'secret_type', 'secret_reset',
|
||||
'name', 'username', 'secret', 'secret_type', 'otp_secret_key', 'secret_reset',
|
||||
'passphrase', 'privileged', 'is_active', 'comment', 'template',
|
||||
'on_invalid', 'push_now', 'params',
|
||||
'su_from_username', 'source', 'source_id',
|
||||
@@ -473,6 +511,31 @@ class AssetAccountBulkSerializer(
|
||||
res['asset'] = str(res['asset'])
|
||||
return results
|
||||
|
||||
def validate_otp_secret_key(self, value):
|
||||
return validate_otp_secret_key_value(value)
|
||||
|
||||
|
||||
class AccountOTPGenerateSerializer(serializers.Serializer):
|
||||
otp_secret_key = EncryptedField(
|
||||
label=_('OTP secret key'),
|
||||
required=False,
|
||||
max_length=128,
|
||||
allow_blank=True,
|
||||
allow_null=True,
|
||||
write_only=True,
|
||||
)
|
||||
otp_code = serializers.CharField(label=_('OTP code'), read_only=True)
|
||||
|
||||
def validate_otp_secret_key(self, value):
|
||||
return validate_otp_secret_key_value(value)
|
||||
|
||||
def get_payload(self):
|
||||
otp_secret_key = self.validated_data.get('otp_secret_key') or generate_otp_secret_key()
|
||||
return {
|
||||
'otp_secret_key': otp_secret_key,
|
||||
'otp_code': generate_otp_code(otp_secret_key),
|
||||
}
|
||||
|
||||
|
||||
class AccountSecretSerializer(SecretReadableCheckMixin, SecretReadableMixin, AccountSerializer):
|
||||
spec_info = serializers.DictField(label=_('Spec info'), read_only=True)
|
||||
|
||||
@@ -18,7 +18,7 @@ from accounts.const import AliasAccount
|
||||
from acls.notifications import AssetLoginReminderMsg
|
||||
from common.api import JMSModelViewSet
|
||||
from common.exceptions import JMSException
|
||||
from common.utils import random_string, get_logger, get_request_ip_or_data
|
||||
from common.utils import random_string, get_logger, get_request_ip_or_data, check_otp_code
|
||||
from common.utils.django import get_request_os
|
||||
from common.utils.http import is_true, is_false
|
||||
from orgs.mixins.api import RootOrgViewMixin
|
||||
@@ -412,8 +412,9 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
|
||||
account_name = data.get('account')
|
||||
protocol = data.get('protocol')
|
||||
connect_method = data.get('connect_method')
|
||||
otp_code = data.pop('otp_code', '')
|
||||
self.input_username = self.get_input_username(data)
|
||||
_data = self._validate(user, asset, account_name, protocol, connect_method)
|
||||
_data = self._validate(user, asset, account_name, protocol, connect_method, otp_code=otp_code)
|
||||
data.update(_data)
|
||||
return serializer
|
||||
|
||||
@@ -426,7 +427,15 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
|
||||
setattr(token, k, v)
|
||||
return token
|
||||
|
||||
def _validate(self, user, asset, account_alias, protocol, connect_method):
|
||||
@staticmethod
|
||||
def _validate_account_otp(account, otp_code):
|
||||
otp_secret_key = getattr(account, 'otp_secret_key', '')
|
||||
if not otp_secret_key:
|
||||
return
|
||||
if not otp_code or not check_otp_code(otp_secret_key, otp_code):
|
||||
raise ValidationError({'otp_code': _('OTP code invalid, or server time error')})
|
||||
|
||||
def _validate(self, user, asset, account_alias, protocol, connect_method, otp_code=''):
|
||||
data = dict()
|
||||
data['org_id'] = asset.org_id
|
||||
data['user'] = user
|
||||
@@ -436,6 +445,7 @@ class ConnectionTokenViewSet(AuthFaceMixin, ExtraActionApiMixin, RootOrgViewMixi
|
||||
raise ValidationError(_('Anonymous account is not supported for this asset'))
|
||||
|
||||
account = self._validate_perm(user, asset, account_alias, protocol)
|
||||
self._validate_account_otp(account, otp_code)
|
||||
if account.has_secret:
|
||||
data['input_secret'] = ''
|
||||
data['input_secret_type'] = account.secret_type
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import os
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import gettext as _
|
||||
@@ -112,7 +113,8 @@ class SessionAuthentication(authentication.SessionAuthentication):
|
||||
if not user or not user.is_active or not user.is_valid:
|
||||
return None
|
||||
|
||||
ignore_csrf_check = os.environ.get("DOMAINS", "") == "*"
|
||||
domains = getattr(settings, 'DOMAINS', '') or os.environ.get("DOMAINS", "")
|
||||
ignore_csrf_check = '*' in [domain.strip() for domain in domains.split(',') if domain.strip()]
|
||||
if not ignore_csrf_check:
|
||||
try:
|
||||
self.enforce_csrf(request)
|
||||
|
||||
@@ -19,6 +19,9 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
input_secret = EncryptedField(
|
||||
label=_("Input secret"), max_length=40960, required=False, allow_blank=True
|
||||
)
|
||||
otp_code = EncryptedField(
|
||||
label=_("OTP code"), max_length=16, required=False, allow_blank=True, write_only=True
|
||||
)
|
||||
from_ticket_info = serializers.SerializerMethodField(label=_("Ticket info"))
|
||||
actions = ActionChoicesField(read_only=True, label=_("Actions"))
|
||||
|
||||
@@ -26,7 +29,7 @@ class ConnectionTokenSerializer(OrgResourceModelSerializerMixin):
|
||||
model = ConnectionToken
|
||||
fields_mini = ['id', 'value']
|
||||
fields_small = fields_mini + [
|
||||
'user', 'asset', 'account', 'input_username', 'input_secret', 'input_secret_type',
|
||||
'user', 'asset', 'account', 'input_username', 'input_secret', 'otp_code', 'input_secret_type',
|
||||
'connect_method', 'connect_options', 'protocol', 'actions',
|
||||
'is_active', 'is_reusable', 'from_ticket', 'from_ticket_info',
|
||||
'date_expired', 'date_created', 'date_updated', 'created_by',
|
||||
|
||||
@@ -8,5 +8,6 @@ from .encode import *
|
||||
from .http import *
|
||||
from .ip import *
|
||||
from .jumpserver import *
|
||||
from .otp import *
|
||||
from .random import *
|
||||
from .translate import *
|
||||
|
||||
57
apps/common/utils/otp.py
Normal file
57
apps/common/utils/otp.py
Normal file
@@ -0,0 +1,57 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
import pyotp
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
def get_otp_digest():
|
||||
return hashlib.sha256 if settings.OTP_DIGEST == 'sha256' else hashlib.sha1
|
||||
|
||||
|
||||
def normalize_otp_secret_key(otp_secret_key):
|
||||
if not otp_secret_key:
|
||||
return ''
|
||||
return str(otp_secret_key).replace(' ', '').upper()
|
||||
|
||||
|
||||
def generate_otp_secret_key():
|
||||
return base64.b32encode(os.urandom(10)).decode('utf-8')
|
||||
|
||||
|
||||
def generate_otp_uri(username, otp_secret_key=None, issuer="JumpServer"):
|
||||
otp_secret_key = normalize_otp_secret_key(otp_secret_key) or generate_otp_secret_key()
|
||||
totp = pyotp.TOTP(otp_secret_key, digest=get_otp_digest())
|
||||
otp_issuer_name = settings.OTP_ISSUER_NAME or issuer
|
||||
uri = totp.provisioning_uri(name=username, issuer_name=otp_issuer_name)
|
||||
return uri, otp_secret_key
|
||||
|
||||
|
||||
def generate_otp_code(otp_secret_key):
|
||||
otp_secret_key = normalize_otp_secret_key(otp_secret_key)
|
||||
totp = pyotp.TOTP(otp_secret_key, digest=get_otp_digest())
|
||||
return totp.now()
|
||||
|
||||
|
||||
def check_otp_code(otp_secret_key, otp_code):
|
||||
if not otp_secret_key or not otp_code:
|
||||
return False
|
||||
|
||||
otp_secret_key = normalize_otp_secret_key(otp_secret_key)
|
||||
totp = pyotp.TOTP(otp_secret_key, digest=get_otp_digest())
|
||||
otp_valid_window = settings.OTP_VALID_WINDOW or 0
|
||||
return totp.verify(otp=str(otp_code), valid_window=otp_valid_window)
|
||||
|
||||
|
||||
def is_otp_secret_key_valid(otp_secret_key):
|
||||
if not otp_secret_key:
|
||||
return True
|
||||
try:
|
||||
generate_otp_code(otp_secret_key)
|
||||
except Exception:
|
||||
return False
|
||||
return True
|
||||
@@ -21,7 +21,10 @@ from rest_framework import status
|
||||
from .utils import set_current_request
|
||||
from common.utils.common import text_hmac_sha256
|
||||
|
||||
IGNORE_CSRF_CHECK = '*' in os.getenv("DOMAINS", "").split(',')
|
||||
|
||||
def ignore_csrf_check():
|
||||
domains = getattr(settings, 'DOMAINS', '') or os.getenv("DOMAINS", "")
|
||||
return '*' in [domain.strip() for domain in domains.split(',') if domain.strip()]
|
||||
|
||||
|
||||
class TimezoneMiddleware:
|
||||
@@ -198,8 +201,12 @@ class SafeRedirectMiddleware:
|
||||
|
||||
|
||||
class CsrfCheckMiddleware(CsrfViewMiddleware):
|
||||
def _origin_verified(self, request):
|
||||
if IGNORE_CSRF_CHECK:
|
||||
def process_view(self, request, callback, callback_args, callback_kwargs):
|
||||
if ignore_csrf_check():
|
||||
request._dont_enforce_csrf_checks = True
|
||||
return super().process_view(request, callback, callback_args, callback_kwargs)
|
||||
|
||||
def _origin_verified(self, request):
|
||||
if ignore_csrf_check():
|
||||
return True
|
||||
return super()._origin_verified(request)
|
||||
|
||||
@@ -75,6 +75,7 @@ ALLOWED_DOMAINS = [host.strip() for host in ALLOWED_DOMAINS]
|
||||
ALLOWED_DOMAINS = [host.replace('http://', '').replace('https://', '') for host in ALLOWED_DOMAINS if host]
|
||||
ALLOWED_DOMAINS = [host.split('/')[0] for host in ALLOWED_DOMAINS if host]
|
||||
ALLOWED_DOMAINS = [re.sub(':80$|:443$', '', host) for host in ALLOWED_DOMAINS]
|
||||
ALLOW_ALL_DOMAINS = '*' in ALLOWED_DOMAINS
|
||||
|
||||
DEBUG_HOSTS = ('127.0.0.1', 'localhost', 'core')
|
||||
DEBUG_PORT = ['8080', '80', ]
|
||||
@@ -92,8 +93,7 @@ ALLOWED_HOSTS = ['*']
|
||||
# https://docs.djangoproject.com/en/4.1/ref/settings/#std-setting-CSRF_TRUSTED_ORIGINS
|
||||
CSRF_TRUSTED_ORIGINS = []
|
||||
for host_port in ALLOWED_DOMAINS:
|
||||
if '*' in ALLOWED_DOMAINS:
|
||||
CSRF_TRUSTED_ORIGINS = ['http://*', 'https://*']
|
||||
if ALLOW_ALL_DOMAINS:
|
||||
break
|
||||
origin = host_port.strip('.')
|
||||
|
||||
@@ -111,13 +111,23 @@ for host_port in ALLOWED_DOMAINS:
|
||||
for schema in ['https', 'http']:
|
||||
if is_local_origin and schema == 'https':
|
||||
continue
|
||||
CSRF_TRUSTED_ORIGINS.append('{}://*.{}'.format(schema, origin))
|
||||
exact_origin = '{}://{}'.format(schema, origin)
|
||||
wildcard_origin = '{}://*.{}'.format(schema, origin)
|
||||
CSRF_TRUSTED_ORIGINS.append(exact_origin)
|
||||
if not is_local_origin:
|
||||
CSRF_TRUSTED_ORIGINS.append(wildcard_origin)
|
||||
|
||||
CORS_ALLOWED_ORIGINS = [o.replace('*.', '') for o in CSRF_TRUSTED_ORIGINS]
|
||||
CSRF_TRUSTED_ORIGINS = list(dict.fromkeys(CSRF_TRUSTED_ORIGINS))
|
||||
if ALLOW_ALL_DOMAINS:
|
||||
CORS_ALLOW_ALL_ORIGINS = True
|
||||
CORS_ALLOWED_ORIGINS = []
|
||||
else:
|
||||
CORS_ALLOW_ALL_ORIGINS = False
|
||||
CORS_ALLOWED_ORIGINS = list(dict.fromkeys(o.replace('*.', '') for o in CSRF_TRUSTED_ORIGINS))
|
||||
CSRF_FAILURE_VIEW = 'jumpserver.views.other.csrf_failure'
|
||||
# print("CSRF_TRUSTED_ORIGINS: ")
|
||||
# for origin in CSRF_TRUSTED_ORIGINS:
|
||||
# print(' - ' + origin)
|
||||
# print(' - ' + origin)
|
||||
# Max post update field num
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10000
|
||||
|
||||
|
||||
@@ -61,16 +61,21 @@ class NodePermedSerializer(serializers.ModelSerializer):
|
||||
class AccountsPermedSerializer(serializers.ModelSerializer):
|
||||
actions = ActionChoicesField(read_only=True)
|
||||
username = serializers.CharField(source='full_username', read_only=True)
|
||||
has_otp_secret_key = serializers.SerializerMethodField()
|
||||
date_expired = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Account
|
||||
fields = [
|
||||
'id', 'alias', 'name', 'username', 'has_username',
|
||||
'has_secret', 'secret_type', 'actions', 'date_expired'
|
||||
'has_secret', 'has_otp_secret_key', 'secret_type', 'actions', 'date_expired'
|
||||
]
|
||||
read_only_fields = fields
|
||||
|
||||
@staticmethod
|
||||
def get_has_otp_secret_key(obj) -> bool:
|
||||
return bool(getattr(obj, 'otp_secret_key', ''))
|
||||
|
||||
def get_date_expired(self, obj) -> str:
|
||||
dt = obj.date_expired
|
||||
if dt:
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
# ~*~ coding: utf-8 ~*~
|
||||
#
|
||||
import base64
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from contextlib import contextmanager
|
||||
from urllib.parse import unquote
|
||||
import hashlib
|
||||
|
||||
import pyotp
|
||||
from django.conf import settings
|
||||
from django.core.cache import cache
|
||||
from django.utils import translation
|
||||
|
||||
from common.tasks import send_mail_async
|
||||
from common.utils import reverse, get_object_or_none, ip, safe_next_url
|
||||
from common.utils import reverse, get_object_or_none, ip, safe_next_url, generate_otp_uri, check_otp_code
|
||||
from .models import User
|
||||
|
||||
logger = logging.getLogger('jumpserver.users')
|
||||
otp_digest = hashlib.sha256 if settings.OTP_DIGEST == 'sha256' else hashlib.sha1
|
||||
|
||||
|
||||
def send_user_created_mail(user):
|
||||
@@ -68,25 +62,6 @@ def redirect_user_first_login_or_index(request, redirect_field_name):
|
||||
return url
|
||||
|
||||
|
||||
def generate_otp_uri(username, otp_secret_key=None, issuer="JumpServer"):
|
||||
if otp_secret_key is None:
|
||||
otp_secret_key = base64.b32encode(os.urandom(10)).decode('utf-8')
|
||||
|
||||
totp = pyotp.TOTP(otp_secret_key, digest=otp_digest)
|
||||
otp_issuer_name = settings.OTP_ISSUER_NAME or issuer
|
||||
uri = totp.provisioning_uri(name=username, issuer_name=otp_issuer_name)
|
||||
return uri, otp_secret_key
|
||||
|
||||
|
||||
def check_otp_code(otp_secret_key, otp_code):
|
||||
if not otp_secret_key or not otp_code:
|
||||
return False
|
||||
|
||||
totp = pyotp.TOTP(otp_secret_key, digest=otp_digest)
|
||||
otp_valid_window = settings.OTP_VALID_WINDOW or 0
|
||||
return totp.verify(otp=otp_code, valid_window=otp_valid_window)
|
||||
|
||||
|
||||
def get_password_check_rules(user):
|
||||
check_rules = []
|
||||
for rule in settings.SECURITY_PASSWORD_RULES:
|
||||
|
||||
Reference in New Issue
Block a user