mirror of https://github.com/jumpserver/jumpserver
feat: 添加限制用户只能从source登录的功能 (#5592)
* stash it * feat: 添加限制用户只能从source登录的功能 * fix: 修复小错误 Co-authored-by: ibuler <ibuler@qq.com>pull/5652/head
parent
b483f78d52
commit
a7ab7da61c
|
@ -1,10 +1,11 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
from django.dispatch import receiver
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import LazyObject
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
from rest_framework.renderers import JSONRenderer
|
||||
|
@ -34,17 +35,22 @@ MODELS_NEED_RECORD = (
|
|||
)
|
||||
|
||||
|
||||
LOGIN_BACKEND = {
|
||||
'PublicKeyAuthBackend': _('SSH Key'),
|
||||
'RadiusBackend': User.Source.radius.label,
|
||||
'RadiusRealmBackend': User.Source.radius.label,
|
||||
'LDAPAuthorizationBackend': User.Source.ldap.label,
|
||||
'ModelBackend': _('Password'),
|
||||
'SSOAuthentication': _('SSO'),
|
||||
'CASBackend': User.Source.cas.label,
|
||||
'OIDCAuthCodeBackend': User.Source.openid.label,
|
||||
'OIDCAuthPasswordBackend': User.Source.openid.label,
|
||||
}
|
||||
class AuthBackendLabelMapping(LazyObject):
|
||||
@staticmethod
|
||||
def get_login_backends():
|
||||
backend_label_mapping = {}
|
||||
for source, backends in User.SOURCE_BACKEND_MAPPING.items():
|
||||
for backend in backends:
|
||||
backend_label_mapping[backend] = source.label
|
||||
backend_label_mapping[settings.AUTH_BACKEND_PUBKEY] = _('SSH Key')
|
||||
backend_label_mapping[settings.AUTH_BACKEND_MODEL] = _('Password')
|
||||
return backend_label_mapping
|
||||
|
||||
def _setup(self):
|
||||
self._wrapped = self.get_login_backends()
|
||||
|
||||
|
||||
AUTH_BACKEND_LABEL_MAPPING = AuthBackendLabelMapping()
|
||||
|
||||
|
||||
def create_operate_log(action, sender, resource):
|
||||
|
@ -70,6 +76,7 @@ def create_operate_log(action, sender, resource):
|
|||
|
||||
@receiver(post_save)
|
||||
def on_object_created_or_update(sender, instance=None, created=False, update_fields=None, **kwargs):
|
||||
# last_login 改变是最后登录日期, 每次登录都会改变
|
||||
if instance._meta.object_name == 'User' and \
|
||||
update_fields and 'last_login' in update_fields:
|
||||
return
|
||||
|
@ -125,14 +132,13 @@ def on_audits_log_create(sender, instance=None, **kwargs):
|
|||
|
||||
|
||||
def get_login_backend(request):
|
||||
backend = request.session.get('auth_backend', '') or request.session.get(BACKEND_SESSION_KEY, '')
|
||||
backend = request.session.get('auth_backend', '') or \
|
||||
request.session.get(BACKEND_SESSION_KEY, '')
|
||||
|
||||
backend = backend.rsplit('.', maxsplit=1)[-1]
|
||||
if backend in LOGIN_BACKEND:
|
||||
return LOGIN_BACKEND[backend]
|
||||
else:
|
||||
logger.warn(f'LOGIN_BACKEND_NOT_FOUND: {backend}')
|
||||
return ''
|
||||
backend_label = AUTH_BACKEND_LABEL_MAPPING.get(backend, None)
|
||||
if backend_label is None:
|
||||
backend_label = ''
|
||||
return backend_label
|
||||
|
||||
|
||||
def generate_data(username, request):
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
import traceback
|
||||
|
||||
from django.contrib.auth import get_user_model, authenticate
|
||||
from django.contrib.auth import get_user_model
|
||||
from radiusauth.backends import RADIUSBackend, RADIUSRealmBackend
|
||||
from django.conf import settings
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ reason_user_not_exist = 'user_not_exist'
|
|||
reason_password_expired = 'password_expired'
|
||||
reason_user_invalid = 'user_invalid'
|
||||
reason_user_inactive = 'user_inactive'
|
||||
reason_backend_not_match = 'backend_not_match'
|
||||
|
||||
reason_choices = {
|
||||
reason_password_failed: _('Username/password check failed'),
|
||||
|
@ -27,7 +28,8 @@ reason_choices = {
|
|||
reason_user_not_exist: _("Username does not exist"),
|
||||
reason_password_expired: _("Password expired"),
|
||||
reason_user_invalid: _('Disabled or expired'),
|
||||
reason_user_inactive: _("This account is inactive.")
|
||||
reason_user_inactive: _("This account is inactive."),
|
||||
reason_backend_not_match: _("Auth backend not match")
|
||||
}
|
||||
old_reason_choices = {
|
||||
'0': '-',
|
||||
|
|
|
@ -9,7 +9,7 @@ from django.contrib.auth import authenticate
|
|||
from django.shortcuts import reverse
|
||||
from django.contrib.auth import BACKEND_SESSION_KEY
|
||||
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger
|
||||
from common.utils import get_object_or_none, get_request_ip, get_logger, bulk_get
|
||||
from users.models import User
|
||||
from users.utils import (
|
||||
is_block_login, clean_failed_count
|
||||
|
@ -24,6 +24,7 @@ logger = get_logger(__name__)
|
|||
|
||||
class AuthMixin:
|
||||
request = None
|
||||
partial_credential_error = None
|
||||
|
||||
def get_user_from_session(self):
|
||||
if self.request.session.is_empty():
|
||||
|
@ -75,49 +76,75 @@ class AuthMixin:
|
|||
return rsa_decrypt(raw_passwd, rsa_private_key)
|
||||
except Exception as e:
|
||||
logger.error(e, exc_info=True)
|
||||
logger.error(f'Decrypt password faild: password[{raw_passwd}] rsa_private_key[{rsa_private_key}]')
|
||||
logger.error(f'Decrypt password failed: password[{raw_passwd}] '
|
||||
f'rsa_private_key[{rsa_private_key}]')
|
||||
return None
|
||||
return raw_passwd
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
def raise_credential_error(self, error):
|
||||
raise self.partial_credential_error(error=errors.reason_password_decrypt_failed)
|
||||
|
||||
def get_auth_data(self, decrypt_passwd=False):
|
||||
request = self.request
|
||||
if hasattr(request, 'data'):
|
||||
data = request.data
|
||||
else:
|
||||
data = request.POST
|
||||
username = data.get('username', '')
|
||||
password = data.get('password', '')
|
||||
challenge = data.get('challenge', '')
|
||||
public_key = data.get('public_key', '')
|
||||
ip = self.get_request_ip()
|
||||
|
||||
CredentialError = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
||||
items = ['username', 'password', 'challenge', 'public_key']
|
||||
username, password, challenge, public_key = bulk_get(data, *items, default='')
|
||||
password = password + challenge.strip()
|
||||
ip = self.get_request_ip()
|
||||
self.partial_credential_error = partial(errors.CredentialError, username=username, ip=ip, request=request)
|
||||
|
||||
if decrypt_passwd:
|
||||
password = self.decrypt_passwd(password)
|
||||
if not password:
|
||||
raise CredentialError(error=errors.reason_password_decrypt_failed)
|
||||
self.raise_credential_error(errors.reason_password_decrypt_failed)
|
||||
return username, password, public_key, ip
|
||||
|
||||
user = authenticate(request,
|
||||
username=username,
|
||||
password=password + challenge.strip(),
|
||||
public_key=public_key)
|
||||
def _check_only_allow_exists_user_auth(self, username):
|
||||
# 仅允许预先存在的用户认证
|
||||
if settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
exist = User.objects.filter(username=username).exists()
|
||||
if not exist:
|
||||
logger.error(f"Only allow exist user auth, login failed: {username}")
|
||||
self.raise_credential_error(errors.reason_user_not_exist)
|
||||
|
||||
def _check_auth_user_is_valid(self, username, password, public_key):
|
||||
user = authenticate(self.request, username=username, password=password, public_key=public_key)
|
||||
if not user:
|
||||
raise CredentialError(error=errors.reason_password_failed)
|
||||
self.raise_credential_error(errors.reason_password_failed)
|
||||
elif user.is_expired:
|
||||
raise CredentialError(error=errors.reason_user_inactive)
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
elif not user.is_active:
|
||||
raise CredentialError(error=errors.reason_user_inactive)
|
||||
self.raise_credential_error(errors.reason_user_inactive)
|
||||
return user
|
||||
|
||||
def _check_auth_source_is_valid(self, user, auth_backend):
|
||||
# 限制只能从认证来源登录
|
||||
if settings.ONLY_ALLOW_AUTH_FROM_SOURCE:
|
||||
auth_backends_allowed = user.SOURCE_BACKEND_MAPPING.get(user.source)
|
||||
if auth_backend not in auth_backends_allowed:
|
||||
self.raise_credential_error(error=errors.reason_backend_not_match)
|
||||
|
||||
def check_user_auth(self, decrypt_passwd=False):
|
||||
self.check_is_block()
|
||||
request = self.request
|
||||
username, password, public_key, ip = self.get_auth_data(decrypt_passwd=decrypt_passwd)
|
||||
|
||||
self._check_only_allow_exists_user_auth(username)
|
||||
user = self._check_auth_user_is_valid(username, password, public_key)
|
||||
# 限制只能从认证来源登录
|
||||
|
||||
auth_backend = getattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend')
|
||||
self._check_auth_source_is_valid(user, auth_backend)
|
||||
self._check_password_require_reset_or_not(user)
|
||||
self._check_passwd_is_too_simple(user, password)
|
||||
|
||||
clean_failed_count(username, ip)
|
||||
request.session['auth_password'] = 1
|
||||
request.session['user_id'] = str(user.id)
|
||||
auth_backend = getattr(user, 'backend', 'django.contrib.auth.backends.ModelBackend')
|
||||
request.session['auth_backend'] = auth_backend
|
||||
return user
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ def on_user_auth_login_success(sender, user, request, **kwargs):
|
|||
|
||||
|
||||
@receiver(openid_user_login_success)
|
||||
def on_oidc_user_login_success(sender, request, user, **kwargs):
|
||||
def on_oidc_user_login_success(sender, request, user, create=False, **kwargs):
|
||||
request.session[BACKEND_SESSION_KEY] = 'OIDCAuthCodeBackend'
|
||||
post_auth_success.send(sender, user=user, request=request)
|
||||
|
||||
|
|
|
@ -45,9 +45,10 @@ class UserLoginView(mixins.AuthMixin, FormView):
|
|||
|
||||
def get(self, request, *args, **kwargs):
|
||||
if request.user.is_staff:
|
||||
return redirect(redirect_user_first_login_or_index(
|
||||
request, self.redirect_field_name)
|
||||
first_login_url = redirect_user_first_login_or_index(
|
||||
request, self.redirect_field_name
|
||||
)
|
||||
return redirect(first_login_url)
|
||||
request.session.set_test_cookie()
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
|
|
|
@ -272,5 +272,12 @@ class Time:
|
|||
last = timestamp
|
||||
|
||||
|
||||
def bulk_get(d, *keys, default=None):
|
||||
values = []
|
||||
for key in keys:
|
||||
values.append(d.get(key, default))
|
||||
return values
|
||||
|
||||
|
||||
def isinstance_method(attr):
|
||||
return isinstance(attr, type(Time().time))
|
||||
return isinstance(attr, type(Time().time))
|
|
@ -282,6 +282,8 @@ class Config(dict):
|
|||
'REFERER_CHECK_ENABLED': False,
|
||||
'SERVER_REPLAY_STORAGE': {},
|
||||
'CONNECTION_TOKEN_ENABLED': False,
|
||||
'ONLY_ALLOW_EXIST_USER_AUTH': False,
|
||||
'ONLY_ALLOW_AUTH_FROM_SOURCE': True,
|
||||
'DISK_CHECK_ENABLED': True,
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
#
|
||||
import os
|
||||
import ldap
|
||||
from django.utils.translation import ugettext_lazy as _
|
||||
|
||||
from ..const import CONFIG, PROJECT_DIR
|
||||
|
||||
|
@ -94,7 +95,7 @@ CAS_IGNORE_REFERER = True
|
|||
CAS_LOGOUT_COMPLETELY = CONFIG.CAS_LOGOUT_COMPLETELY
|
||||
CAS_VERSION = CONFIG.CAS_VERSION
|
||||
CAS_ROOT_PROXIED_AS = CONFIG.CAS_ROOT_PROXIED_AS
|
||||
CAS_CHECK_NEXT = lambda: lambda _next_page: True
|
||||
CAS_CHECK_NEXT = lambda _next_page: True
|
||||
|
||||
# SSO Auth
|
||||
AUTH_SSO = CONFIG.AUTH_SSO
|
||||
|
@ -105,17 +106,29 @@ TOKEN_EXPIRATION = CONFIG.TOKEN_EXPIRATION
|
|||
LOGIN_CONFIRM_ENABLE = CONFIG.LOGIN_CONFIRM_ENABLE
|
||||
OTP_IN_RADIUS = CONFIG.OTP_IN_RADIUS
|
||||
|
||||
AUTHENTICATION_BACKENDS = [
|
||||
'authentication.backends.pubkey.PublicKeyAuthBackend',
|
||||
'django.contrib.auth.backends.ModelBackend',
|
||||
]
|
||||
|
||||
AUTH_BACKEND_MODEL = 'django.contrib.auth.backends.ModelBackend'
|
||||
AUTH_BACKEND_PUBKEY = 'authentication.backends.pubkey.PublicKeyAuthBackend'
|
||||
AUTH_BACKEND_LDAP = 'authentication.backends.ldap.LDAPAuthorizationBackend'
|
||||
AUTH_BACKEND_OIDC_PASSWORD = 'jms_oidc_rp.backends.OIDCAuthPasswordBackend'
|
||||
AUTH_BACKEND_OIDC_CODE = 'jms_oidc_rp.backends.OIDCAuthCodeBackend'
|
||||
AUTH_BACKEND_RADIUS = 'authentication.backends.radius.RadiusBackend'
|
||||
AUTH_BACKEND_CAS = 'authentication.backends.cas.CASBackend'
|
||||
AUTH_BACKEND_SSO = 'authentication.backends.api.SSOAuthentication'
|
||||
|
||||
|
||||
AUTHENTICATION_BACKENDS = [AUTH_BACKEND_MODEL, AUTH_BACKEND_PUBKEY]
|
||||
|
||||
if AUTH_CAS:
|
||||
AUTHENTICATION_BACKENDS.insert(0, 'authentication.backends.cas.CASBackend')
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_CAS)
|
||||
if AUTH_OPENID:
|
||||
AUTHENTICATION_BACKENDS.insert(0, 'jms_oidc_rp.backends.OIDCAuthPasswordBackend')
|
||||
AUTHENTICATION_BACKENDS.insert(0, 'jms_oidc_rp.backends.OIDCAuthCodeBackend')
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_OIDC_PASSWORD)
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_OIDC_CODE)
|
||||
if AUTH_RADIUS:
|
||||
AUTHENTICATION_BACKENDS.insert(0, 'authentication.backends.radius.RadiusBackend')
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_RADIUS)
|
||||
if AUTH_SSO:
|
||||
AUTHENTICATION_BACKENDS.insert(0, 'authentication.backends.api.SSOAuthentication')
|
||||
AUTHENTICATION_BACKENDS.insert(0, AUTH_BACKEND_SSO)
|
||||
|
||||
|
||||
ONLY_ALLOW_EXIST_USER_AUTH = CONFIG.ONLY_ALLOW_EXIST_USER_AUTH
|
||||
ONLY_ALLOW_AUTH_FROM_SOURCE = CONFIG.ONLY_ALLOW_AUTH_FROM_SOURCE
|
||||
|
|
|
@ -90,7 +90,7 @@ class Setting(models.Model):
|
|||
setting = cls.objects.filter(name='AUTH_LDAP').first()
|
||||
if not setting:
|
||||
return
|
||||
ldap_backend = 'authentication.backends.ldap.LDAPAuthorizationBackend'
|
||||
ldap_backend = settings.AUTH_BACKEND_LABEL_MAPPING['ldap']
|
||||
backends = settings.AUTHENTICATION_BACKENDS
|
||||
has = ldap_backend in backends
|
||||
if setting.cleaned_value and not has:
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from .user import *
|
||||
from .group import *
|
||||
from .profile import *
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
from ..models import User, UserGroup
|
||||
|
||||
__all__ = ['UserGroupForm']
|
||||
|
||||
|
||||
class UserGroupForm(OrgModelForm):
|
||||
users = forms.ModelMultipleChoiceField(
|
||||
queryset=User.objects.none(),
|
||||
label=_("User"),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
'class': 'users-select2',
|
||||
'data-placeholder': _('Select users')
|
||||
}
|
||||
),
|
||||
required=False,
|
||||
)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
self.set_fields_queryset()
|
||||
|
||||
def set_fields_queryset(self):
|
||||
users_field = self.fields.get('users')
|
||||
if self.instance:
|
||||
users_field.initial = self.instance.users.all()
|
||||
users_field.queryset = self.instance.users.all()
|
||||
else:
|
||||
users_field.queryset = User.objects.none()
|
||||
|
||||
def save(self, commit=True):
|
||||
raise Exception("Save by restful api")
|
||||
|
||||
class Meta:
|
||||
model = UserGroup
|
||||
fields = [
|
||||
'name', 'users', 'comment',
|
||||
]
|
|
@ -12,9 +12,21 @@ __all__ = [
|
|||
'UserProfileForm', 'UserMFAForm', 'UserFirstLoginFinishForm',
|
||||
'UserPasswordForm', 'UserPublicKeyForm', 'FileForm',
|
||||
'UserTokenResetPasswordForm', 'UserForgotPasswordForm',
|
||||
'UserCheckPasswordForm', 'UserCheckOtpCodeForm'
|
||||
]
|
||||
|
||||
|
||||
class UserCheckPasswordForm(forms.Form):
|
||||
password = forms.CharField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=128, strip=False
|
||||
)
|
||||
|
||||
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
|
||||
|
||||
|
||||
class UserProfileForm(forms.ModelForm):
|
||||
username = forms.CharField(disabled=True, label=_("Username"))
|
||||
name = forms.CharField(disabled=True, label=_("Name"))
|
||||
|
|
|
@ -1,199 +0,0 @@
|
|||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from common.utils import validate_ssh_public_key
|
||||
from orgs.mixins.forms import OrgModelForm
|
||||
from ..models import User
|
||||
from ..utils import (
|
||||
check_password_rules, get_current_org_members, get_source_choices
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'UserCreateForm', 'UserUpdateForm', 'UserBulkUpdateForm',
|
||||
'UserCheckOtpCodeForm', 'UserCheckPasswordForm'
|
||||
]
|
||||
|
||||
|
||||
class UserCreateUpdateFormMixin(OrgModelForm):
|
||||
role_choices = ((i, n) for i, n in User.ROLE.choices if i != User.ROLE.APP)
|
||||
password = forms.CharField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=128, strip=False, required=False,
|
||||
)
|
||||
role = forms.ChoiceField(
|
||||
choices=role_choices, required=True,
|
||||
initial=User.ROLE.USER, label=_("Role")
|
||||
)
|
||||
source = forms.ChoiceField(
|
||||
choices=get_source_choices, required=True,
|
||||
initial=User.Source.local.value, label=_("Source")
|
||||
)
|
||||
public_key = forms.CharField(
|
||||
label=_('ssh public key'), max_length=5000, required=False,
|
||||
widget=forms.Textarea(attrs={'placeholder': _('ssh-rsa AAAA...')}),
|
||||
help_text=_('Paste user id_rsa.pub here.')
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = [
|
||||
'username', 'name', 'email', 'groups', 'wechat',
|
||||
'source', 'phone', 'role', 'date_expired',
|
||||
'comment', 'mfa_level'
|
||||
]
|
||||
widgets = {
|
||||
'mfa_level': forms.RadioSelect(),
|
||||
'groups': forms.SelectMultiple(
|
||||
attrs={
|
||||
'class': 'select2',
|
||||
'data-placeholder': _('Join user groups')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.request = kwargs.pop("request", None)
|
||||
super(UserCreateUpdateFormMixin, self).__init__(*args, **kwargs)
|
||||
|
||||
roles = []
|
||||
# Super admin user
|
||||
if self.request.user.is_superuser:
|
||||
roles.append((User.ROLE.ADMIN, User.ROLE.ADMIN.label))
|
||||
roles.append((User.ROLE.USER, User.ROLE.USER.label))
|
||||
roles.append((User.ROLE.AUDITOR, User.ROLE.AUDITOR.label))
|
||||
|
||||
# Org admin user
|
||||
else:
|
||||
user = kwargs.get('instance')
|
||||
# Update
|
||||
if user:
|
||||
role = kwargs.get('instance').role
|
||||
roles.append((role, User.ROLE[role]))
|
||||
# Create
|
||||
else:
|
||||
roles.append((User.ROLE.USER, User.ROLE.USER.label))
|
||||
|
||||
field = self.fields['role']
|
||||
field.choices = set(roles)
|
||||
|
||||
def clean_public_key(self):
|
||||
public_key = self.cleaned_data['public_key']
|
||||
if not public_key:
|
||||
return public_key
|
||||
if self.instance.public_key and public_key == self.instance.public_key:
|
||||
msg = _('Public key should not be the same as your old one.')
|
||||
raise forms.ValidationError(msg)
|
||||
|
||||
if not validate_ssh_public_key(public_key):
|
||||
raise forms.ValidationError(_('Not a valid ssh public key'))
|
||||
return public_key
|
||||
|
||||
def clean_password(self):
|
||||
password_strategy = self.data.get('password_strategy')
|
||||
# 创建-不设置密码
|
||||
if password_strategy == '0':
|
||||
return
|
||||
password = self.data.get('password')
|
||||
# 更新-密码为空
|
||||
if password_strategy is None and not password:
|
||||
return
|
||||
if not check_password_rules(password):
|
||||
msg = _('* Your password does not meet the requirements')
|
||||
raise forms.ValidationError(msg)
|
||||
return password
|
||||
|
||||
def save(self, commit=True):
|
||||
password = self.cleaned_data.get('password')
|
||||
mfa_level = self.cleaned_data.get('mfa_level')
|
||||
public_key = self.cleaned_data.get('public_key')
|
||||
user = super().save(commit=commit)
|
||||
if password:
|
||||
user.reset_password(password)
|
||||
if mfa_level:
|
||||
user.mfa_level = mfa_level
|
||||
user.save()
|
||||
if public_key:
|
||||
user.public_key = public_key
|
||||
user.save()
|
||||
return user
|
||||
|
||||
|
||||
class UserCreateForm(UserCreateUpdateFormMixin):
|
||||
EMAIL_SET_PASSWORD = _('Reset link will be generated and sent to the user')
|
||||
CUSTOM_PASSWORD = _('Set password')
|
||||
PASSWORD_STRATEGY_CHOICES = (
|
||||
(0, EMAIL_SET_PASSWORD),
|
||||
(1, CUSTOM_PASSWORD)
|
||||
)
|
||||
password_strategy = forms.ChoiceField(
|
||||
choices=PASSWORD_STRATEGY_CHOICES, required=True, initial=0,
|
||||
widget=forms.RadioSelect(), label=_('Password strategy')
|
||||
)
|
||||
|
||||
|
||||
class UserUpdateForm(UserCreateUpdateFormMixin):
|
||||
pass
|
||||
|
||||
|
||||
class UserBulkUpdateForm(OrgModelForm):
|
||||
users = forms.ModelMultipleChoiceField(
|
||||
required=True,
|
||||
label=_('Select users'),
|
||||
queryset=User.objects.none(),
|
||||
widget=forms.SelectMultiple(
|
||||
attrs={
|
||||
'class': 'users-select2',
|
||||
'data-placeholder': _('Select users')
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.set_fields_queryset()
|
||||
|
||||
def set_fields_queryset(self):
|
||||
users_field = self.fields['users']
|
||||
users_field.queryset = get_current_org_members()
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['users', 'groups', 'date_expired']
|
||||
widgets = {
|
||||
"groups": forms.SelectMultiple(
|
||||
attrs={
|
||||
'class': 'select2',
|
||||
'data-placeholder': _('User group')
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
def save(self, commit=True):
|
||||
changed_fields = []
|
||||
for field in self._meta.fields:
|
||||
if self.data.get(field) is not None:
|
||||
changed_fields.append(field)
|
||||
|
||||
cleaned_data = {k: v for k, v in self.cleaned_data.items()
|
||||
if k in changed_fields}
|
||||
users = cleaned_data.pop('users', '')
|
||||
groups = cleaned_data.pop('groups', [])
|
||||
users = User.objects.filter(id__in=[user.id for user in users])
|
||||
users.update(**cleaned_data)
|
||||
if groups:
|
||||
for user in users:
|
||||
user.groups.set(groups)
|
||||
return users
|
||||
|
||||
|
||||
class UserCheckPasswordForm(forms.Form):
|
||||
password = forms.CharField(
|
||||
label=_('Password'), widget=forms.PasswordInput,
|
||||
max_length=128, strip=False
|
||||
)
|
||||
|
||||
|
||||
class UserCheckOtpCodeForm(forms.Form):
|
||||
otp_code = forms.CharField(label=_('MFA code'), max_length=6)
|
|
@ -514,6 +514,14 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
radius = 'radius', 'Radius'
|
||||
cas = 'cas', 'CAS'
|
||||
|
||||
SOURCE_BACKEND_MAPPING = {
|
||||
Source.local: [settings.AUTH_BACKEND_MODEL, settings.AUTH_BACKEND_PUBKEY],
|
||||
Source.ldap: [settings.AUTH_BACKEND_LDAP],
|
||||
Source.openid: [settings.AUTH_BACKEND_OIDC_PASSWORD, settings.AUTH_BACKEND_OIDC_CODE],
|
||||
Source.radius: [settings.AUTH_BACKEND_RADIUS],
|
||||
Source.cas: [settings.AUTH_BACKEND_CAS],
|
||||
}
|
||||
|
||||
id = models.UUIDField(default=uuid.uuid4, primary_key=True)
|
||||
username = models.CharField(
|
||||
max_length=128, unique=True, verbose_name=_('Username')
|
||||
|
@ -562,7 +570,8 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
max_length=30, default='', blank=True, verbose_name=_('Created by')
|
||||
)
|
||||
source = models.CharField(
|
||||
max_length=30, default=Source.local.value, choices=Source.choices,
|
||||
max_length=30, default=Source.local,
|
||||
choices=Source.choices,
|
||||
verbose_name=_('Source')
|
||||
)
|
||||
date_password_last_updated = models.DateTimeField(
|
||||
|
@ -570,8 +579,6 @@ class User(AuthMixin, TokenMixin, RoleMixin, MFAMixin, AbstractUser):
|
|||
verbose_name=_('Date password last updated')
|
||||
)
|
||||
|
||||
user_cache_key_prefix = '_User_{}'
|
||||
|
||||
def __str__(self):
|
||||
return '{0.name}({0.username})'.format(self)
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
from django.dispatch import receiver
|
||||
from django_auth_ldap.backend import populate_user
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django_cas_ng.signals import cas_user_authenticated
|
||||
|
||||
from jms_oidc_rp.signals import openid_create_or_update_user
|
||||
|
@ -27,6 +28,9 @@ def on_user_create(sender, user=None, **kwargs):
|
|||
|
||||
@receiver(cas_user_authenticated)
|
||||
def on_cas_user_authenticated(sender, user, created, **kwargs):
|
||||
if created and settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
user.delete()
|
||||
raise PermissionDenied(f'Not allow non-exist user auth: {user.username}')
|
||||
if created:
|
||||
user.source = user.Source.cas.value
|
||||
user.save()
|
||||
|
@ -43,6 +47,10 @@ def on_ldap_create_user(sender, user, ldap_user, **kwargs):
|
|||
|
||||
@receiver(openid_create_or_update_user)
|
||||
def on_openid_create_or_update_user(sender, request, user, created, name, username, email, **kwargs):
|
||||
if created and settings.ONLY_ALLOW_EXIST_USER_AUTH:
|
||||
user.delete()
|
||||
raise PermissionDenied(f'Not allow non-exist user auth: {username}')
|
||||
|
||||
if created:
|
||||
logger.debug(
|
||||
"Receive OpenID user created signal: {}, "
|
||||
|
|
|
@ -382,22 +382,6 @@ def get_current_org_members(exclude=()):
|
|||
return current_org.get_members(exclude=exclude)
|
||||
|
||||
|
||||
def get_source_choices():
|
||||
from .models import User
|
||||
choices = [
|
||||
(User.Source.local.value, User.Source.local.label),
|
||||
]
|
||||
if settings.AUTH_LDAP:
|
||||
choices.append((User.Source.ldap.value, User.Source.ldap.label))
|
||||
if settings.AUTH_OPENID:
|
||||
choices.append((User.Source.openid.value, User.Source.openid.label))
|
||||
if settings.AUTH_RADIUS:
|
||||
choices.append((User.Source.radius.value, User.Source.radius.label))
|
||||
if settings.AUTH_CAS:
|
||||
choices.append((User.Source.cas.value, User.Source.cas.label))
|
||||
return choices
|
||||
|
||||
|
||||
def is_auth_time_valid(session, key):
|
||||
return True if session.get(key, 0) > time.time() else False
|
||||
|
||||
|
|
|
@ -51,6 +51,9 @@ class UserOtpEnableInstallAppView(TemplateView):
|
|||
return super().get_context_data(**kwargs)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class UserOtpEnableBindView(AuthMixin, TemplateView, FormView):
|
||||
template_name = 'users/user_otp_enable_bind.html'
|
||||
form_class = forms.UserCheckOtpCodeForm
|
||||
|
|
Loading…
Reference in New Issue