1# Copyright (c) 2012-2016 Seafile Ltd. 2import hashlib 3import logging 4from datetime import datetime 5from django.conf import settings 6# Avoid shadowing the login() view below. 7from django.views.decorators.csrf import csrf_protect 8from django.urls import reverse 9from django.contrib import messages 10from django.shortcuts import render 11from django.contrib.sites.shortcuts import get_current_site 12from django.http import HttpResponseRedirect, Http404 13 14from django.utils.http import urlquote, base36_to_int, is_safe_url 15from django.utils.translation import ugettext as _ 16from django.views.decorators.cache import never_cache 17from seaserv import seafile_api 18 19from seahub.auth import REDIRECT_FIELD_NAME, get_backends 20from seahub.auth import login as auth_login 21from seahub.auth.decorators import login_required 22from seahub.auth.forms import AuthenticationForm, CaptchaAuthenticationForm, \ 23 PasswordResetForm, SetPasswordForm, PasswordChangeForm, \ 24 SetContactEmailPasswordForm 25from seahub.auth.signals import user_logged_in_failed 26from seahub.auth.tokens import default_token_generator 27from seahub.auth.utils import ( 28 get_login_failed_attempts, incr_login_failed_attempts, 29 clear_login_failed_attempts) 30from seahub.base.accounts import User, UNUSABLE_PASSWORD 31from seahub.options.models import UserOptions 32from seahub.profile.models import Profile 33from seahub.two_factor.views.login import is_device_remembered 34from seahub.utils import is_ldap_user, get_site_name 35from seahub.utils.ip import get_remote_ip 36from seahub.utils.file_size import get_quota_from_string 37from seahub.utils.two_factor_auth import two_factor_auth_enabled, handle_two_factor_auth 38from seahub.utils.user_permissions import get_user_role 39from seahub.utils.auth import get_login_bg_image_path 40 41from constance import config 42 43from seahub.password_session import update_session_auth_hash 44 45# Get an instance of a logger 46logger = logging.getLogger(__name__) 47 48 49def log_user_in(request, user, redirect_to): 50 # Ensure the user-originating redirection url is safe. 51 if not is_safe_url(url=redirect_to, allowed_hosts=request.get_host()): 52 redirect_to = settings.LOGIN_REDIRECT_URL 53 54 if request.session.test_cookie_worked(): 55 request.session.delete_test_cookie() 56 57 clear_login_failed_attempts(request, user.username) 58 59 if two_factor_auth_enabled(user): 60 if is_device_remembered(request.COOKIES.get('S2FA', ''), user): 61 from seahub.two_factor.models import default_device 62 user.otp_device = default_device(user) 63 else: 64 return handle_two_factor_auth(request, user, redirect_to) 65 66 # Okay, security checks complete. Log the user in. 67 auth_login(request, user) 68 69 return HttpResponseRedirect(redirect_to) 70 71def _handle_login_form_valid(request, user, redirect_to, remember_me): 72 if UserOptions.objects.passwd_change_required( 73 user.username): 74 redirect_to = reverse('auth_password_change') 75 request.session['force_passwd_change'] = True 76 77 if user.permissions.role_quota(): 78 user_role = get_user_role(user) 79 quota = get_quota_from_string(user.permissions.role_quota()) 80 seafile_api.set_role_quota(user_role, quota) 81 82 # password is valid, log user in 83 request.session['remember_me'] = remember_me 84 return log_user_in(request, user, redirect_to) 85 86@csrf_protect 87@never_cache 88def login(request, template_name='registration/login.html', 89 redirect_if_logged_in='libraries', 90 redirect_field_name=REDIRECT_FIELD_NAME, 91 authentication_form=AuthenticationForm): 92 """Displays the login form and handles the login action.""" 93 94 redirect_to = request.GET.get(redirect_field_name, '') 95 if request.user.is_authenticated: 96 if redirect_to: 97 return HttpResponseRedirect(redirect_to) 98 else: 99 return HttpResponseRedirect(reverse(redirect_if_logged_in)) 100 101 ip = get_remote_ip(request) 102 103 if request.method == "POST": 104 login = request.POST.get('login', '').strip() 105 failed_attempt = get_login_failed_attempts(username=login, ip=ip) 106 remember_me = True if request.POST.get('remember_me', 107 '') == 'on' else False 108 redirect_to = request.POST.get(redirect_field_name, '') or redirect_to 109 110 # check the form 111 used_captcha_already = False 112 if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True: 113 form = authentication_form(data=request.POST) 114 else: 115 if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT: 116 form = CaptchaAuthenticationForm(data=request.POST) 117 used_captcha_already = True 118 else: 119 form = authentication_form(data=request.POST) 120 121 if form.is_valid(): 122 return _handle_login_form_valid(request, form.get_user(), 123 redirect_to, remember_me) 124 125 # form is invalid 126 user_logged_in_failed.send(sender=None, request=request) 127 failed_attempt = incr_login_failed_attempts(username=login, 128 ip=ip) 129 130 if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT: 131 if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True: 132 # log user in if password is valid otherwise freeze account 133 logger.warn('Login attempt limit reached, try freeze the user, email/username: %s, ip: %s, attemps: %d' % 134 (login, ip, failed_attempt)) 135 email = Profile.objects.get_username_by_login_id(login) 136 if email is None: 137 email = login 138 try: 139 user = User.objects.get(email) 140 if user.is_active: 141 user.freeze_user(notify_admins=True) 142 logger.warn('Login attempt limit reached, freeze the user email/username: %s, ip: %s, attemps: %d' % 143 (login, ip, failed_attempt)) 144 except User.DoesNotExist: 145 logger.warn('Login attempt limit reached with invalid email/username: %s, ip: %s, attemps: %d' % 146 (login, ip, failed_attempt)) 147 pass 148 form.errors['freeze_account'] = _('This account has been frozen due to too many failed login attempts.') 149 else: 150 # use a new form with Captcha 151 logger.warn('Login attempt limit reached, show Captcha, email/username: %s, ip: %s, attemps: %d' % 152 (login, ip, failed_attempt)) 153 if not used_captcha_already: 154 form = CaptchaAuthenticationForm() 155 156 else: 157 ### GET 158 failed_attempt = get_login_failed_attempts(ip=ip) 159 if failed_attempt >= config.LOGIN_ATTEMPT_LIMIT: 160 if bool(config.FREEZE_USER_ON_LOGIN_FAILED) is True: 161 form = authentication_form() 162 else: 163 logger.warn('Login attempt limit reached, show Captcha, ip: %s, attempts: %d' % 164 (ip, failed_attempt)) 165 form = CaptchaAuthenticationForm() 166 else: 167 form = authentication_form() 168 169 request.session.set_test_cookie() 170 current_site = get_current_site(request) 171 172 multi_tenancy = getattr(settings, 'MULTI_TENANCY', False) 173 174 if config.ENABLE_SIGNUP: 175 if multi_tenancy: 176 org_account_only = getattr(settings, 'FORCE_ORG_REGISTER', False) 177 if org_account_only: 178 signup_url = reverse('org_register') 179 else: 180 signup_url = reverse('choose_register') 181 else: 182 signup_url = reverse('registration_register') 183 else: 184 signup_url = '' 185 186 enable_sso = getattr(settings, 'ENABLE_SHIB_LOGIN', False) or \ 187 getattr(settings, 'ENABLE_KRB5_LOGIN', False) or \ 188 getattr(settings, 'ENABLE_ADFS_LOGIN', False) or \ 189 getattr(settings, 'ENABLE_OAUTH', False) or \ 190 getattr(settings, 'ENABLE_DINGTALK', False) or \ 191 getattr(settings, 'ENABLE_CAS', False) or \ 192 getattr(settings, 'ENABLE_REMOTE_USER_AUTHENTICATION', False) or \ 193 getattr(settings, 'ENABLE_WORK_WEIXIN', False) 194 195 login_bg_image_path = get_login_bg_image_path() 196 197 return render(request, template_name, { 198 'form': form, 199 redirect_field_name: redirect_to, 200 'site': current_site, 201 'site_name': get_site_name(), 202 'remember_days': config.LOGIN_REMEMBER_DAYS, 203 'signup_url': signup_url, 204 'enable_sso': enable_sso, 205 'login_bg_image_path': login_bg_image_path, 206 }) 207 208def login_simple_check(request): 209 """A simple check for login called by thirdpart systems(OA, etc). 210 211 Token generation: MD5(secret_key + foo@foo.com + 2014-1-1).hexdigest() 212 Token length: 32 hexadecimal digits. 213 """ 214 username = request.GET.get('user', '') 215 random_key = request.GET.get('token', '') 216 217 if not username or not random_key: 218 raise Http404 219 220 today = datetime.now().strftime('%Y-%m-%d') 221 expect = hashlib.md5((settings.SECRET_KEY+username+today).encode('utf-8')).hexdigest() 222 if expect == random_key: 223 try: 224 user = User.objects.get(email=username) 225 except User.DoesNotExist: 226 raise Http404 227 228 for backend in get_backends(): 229 user.backend = "%s.%s" % (backend.__module__, backend.__class__.__name__) 230 231 auth_login(request, user) 232 233 # Ensure the user-originating redirection url is safe. 234 if REDIRECT_FIELD_NAME in request.GET: 235 next_page = request.GET[REDIRECT_FIELD_NAME] 236 if not is_safe_url(url=next_page, host=request.get_host()): 237 next_page = settings.LOGIN_REDIRECT_URL 238 else: 239 next_page = settings.SITE_ROOT 240 241 return HttpResponseRedirect(next_page) 242 else: 243 raise Http404 244 245 246def logout(request, next_page=None, 247 template_name='registration/logged_out.html', 248 redirect_field_name=REDIRECT_FIELD_NAME): 249 "Logs out the user and displays 'You are logged out' message." 250 from seahub.auth import logout 251 logout(request) 252 253 # Local logout for shibboleth user. 254 shib_logout_url = getattr(settings, 'SHIBBOLETH_LOGOUT_URL', '') 255 if getattr(settings, 'ENABLE_SHIB_LOGIN', False) and shib_logout_url: 256 shib_logout_return = getattr(settings, 'SHIBBOLETH_LOGOUT_RETURN', '') 257 if shib_logout_return: 258 shib_logout_url += shib_logout_return 259 response = HttpResponseRedirect(shib_logout_url) 260 response.delete_cookie('seahub_auth') 261 return response 262 263 # Local logout for cas user. 264 if getattr(settings, 'ENABLE_CAS', False): 265 response = HttpResponseRedirect(reverse('cas_ng_logout')) 266 response.delete_cookie('seahub_auth') 267 return response 268 269 from seahub.settings import LOGOUT_REDIRECT_URL 270 if LOGOUT_REDIRECT_URL: 271 response = HttpResponseRedirect(LOGOUT_REDIRECT_URL) 272 response.delete_cookie('seahub_auth') 273 return response 274 275 if redirect_field_name in request.GET: 276 next_page = request.GET[redirect_field_name] 277 # Security check -- don't allow redirection to a different host. 278 if not is_safe_url(url=next_page, allowed_hosts=request.get_host()): 279 next_page = request.path 280 281 if next_page is None: 282 redirect_to = request.GET.get(redirect_field_name, '') 283 if redirect_to: 284 response = HttpResponseRedirect(redirect_to) 285 else: 286 response = render(request, template_name, { 287 'title': _('Logged out') 288 }) 289 else: 290 # Redirect to this page until the session has been cleared. 291 response = HttpResponseRedirect(next_page or request.path) 292 293 response.delete_cookie('seahub_auth') 294 return response 295 296def logout_then_login(request, login_url=None): 297 "Logs out the user if he is logged in. Then redirects to the log-in page." 298 if not login_url: 299 login_url = settings.LOGIN_URL 300 return logout(request, login_url) 301 302def redirect_to_login(next, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME): 303 "Redirects the user to the login page, passing the given 'next' page" 304 if not login_url: 305 login_url = settings.LOGIN_URL 306 return HttpResponseRedirect('%s?%s=%s' % (login_url, urlquote(redirect_field_name), urlquote(next))) 307 308# 4 views for password reset: 309# - password_reset sends the mail 310# - password_reset_done shows a success message for the above 311# - password_reset_confirm checks the link the user clicked and 312# prompts for a new password 313# - password_reset_complete shows a success message for the above 314 315@csrf_protect 316def password_reset(request, is_admin_site=False, template_name='registration/password_reset_form.html', 317 email_template_name='registration/password_reset_email.html', 318 password_reset_form=PasswordResetForm, token_generator=default_token_generator, 319 post_reset_redirect=None): 320 if post_reset_redirect is None: 321 post_reset_redirect = reverse('auth_password_reset_done') 322 if request.method == "POST": 323 form = password_reset_form(request.POST) 324 if form.is_valid(): 325 opts = {} 326 opts['use_https'] = request.is_secure() 327 opts['token_generator'] = token_generator 328 if is_admin_site: 329 opts['domain_override'] = request.META['HTTP_HOST'] 330 else: 331 opts['email_template_name'] = email_template_name 332 opts['domain_override'] = get_current_site(request).domain 333 try: 334 form.save(**opts) 335 except Exception as e: 336 logger.error(str(e)) 337 messages.error(request, _('Failed to send email, please contact administrator.')) 338 return render(request, template_name, { 339 'form': form, 340 }) 341 else: 342 return HttpResponseRedirect(post_reset_redirect) 343 else: 344 form = password_reset_form() 345 return render(request, template_name, { 346 'form': form, 347 }) 348 349def password_reset_done(request, template_name='registration/password_reset_done.html'): 350 return render(request, template_name) 351 352# Doesn't need csrf_protect since no-one can guess the URL 353def password_reset_confirm(request, uidb36=None, token=None, template_name='registration/password_reset_confirm.html', 354 token_generator=default_token_generator, set_password_form=SetPasswordForm, 355 post_reset_redirect=None): 356 """ 357 View that checks the hash in a password reset link and presents a 358 form for entering a new password. 359 """ 360 assert uidb36 is not None and token is not None # checked by URLconf 361 if post_reset_redirect is None: 362 post_reset_redirect = reverse('auth_password_reset_complete') 363 try: 364 uid_int = base36_to_int(uidb36) 365 user = User.objects.get(id=uid_int) 366 except (ValueError, User.DoesNotExist): 367 user = None 368 369 context_instance = {} 370 if token_generator.check_token(user, token): 371 context_instance['validlink'] = True 372 if request.method == 'POST': 373 form = set_password_form(user, request.POST) 374 if form.is_valid(): 375 form.save() 376 return HttpResponseRedirect(post_reset_redirect) 377 else: 378 form = set_password_form(None) 379 else: 380 context_instance['validlink'] = False 381 form = None 382 context_instance['form'] = form 383 return render(request, template_name, context_instance) 384 385def password_reset_complete(request, template_name='registration/password_reset_complete.html'): 386 return render(request, template_name, {'login_url': settings.LOGIN_URL}) 387 388@csrf_protect 389@login_required 390def password_change(request, template_name='registration/password_change_form.html', 391 post_change_redirect=None, password_change_form=PasswordChangeForm): 392 if post_change_redirect is None: 393 post_change_redirect = reverse('auth_password_change_done') 394 395 if is_ldap_user(request.user): 396 messages.error(request, _("Can not update password, please contact LDAP admin.")) 397 398 if settings.ENABLE_USER_SET_CONTACT_EMAIL: 399 user_profile = Profile.objects.get_profile_by_user(request.user.username) 400 if user_profile is None or not user_profile.contact_email: 401 # set contact email and password 402 password_change_form = SetContactEmailPasswordForm 403 template_name = 'registration/password_set_form.html' 404 405 elif request.user.enc_password == UNUSABLE_PASSWORD: 406 # set password only 407 password_change_form = SetPasswordForm 408 template_name = 'registration/password_set_form.html' 409 410 if request.method == "POST": 411 form = password_change_form(user=request.user, data=request.POST) 412 if form.is_valid(): 413 form.save() 414 415 if request.session.get('force_passwd_change', False): 416 del request.session['force_passwd_change'] 417 UserOptions.objects.unset_force_passwd_change( 418 request.user.username) 419 420 update_session_auth_hash(request, request.user) 421 return HttpResponseRedirect(post_change_redirect) 422 else: 423 form = password_change_form(user=request.user) 424 425 return render(request, template_name, { 426 'form': form, 427 'min_len': config.USER_PASSWORD_MIN_LENGTH, 428 'strong_pwd_required': config.USER_STRONG_PASSWORD_REQUIRED, 429 'level': config.USER_PASSWORD_STRENGTH_LEVEL, 430 'force_passwd_change': request.session.get('force_passwd_change', False), 431 }) 432 433def password_change_done(request, template_name='registration/password_change_done.html'): 434 return render(request, template_name) 435