views.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319
  1. #!/usr/bin/env python
  2. # Licensed to Cloudera, Inc. under one
  3. # or more contributor license agreements. See the NOTICE file
  4. # distributed with this work for additional information
  5. # regarding copyright ownership. Cloudera, Inc. licenses this file
  6. # to you under the Apache License, Version 2.0 (the
  7. # "License"); you may not use this file except in compliance
  8. # with the License. You may obtain a copy of the License at
  9. #
  10. # http://www.apache.org/licenses/LICENSE-2.0
  11. #
  12. # Unless required by applicable law or agreed to in writing, software
  13. # distributed under the License is distributed on an "AS IS" BASIS,
  14. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. # See the License for the specific language governing permissions and
  16. # limitations under the License.
  17. from future import standard_library
  18. standard_library.install_aliases()
  19. try:
  20. import oauth2 as oauth
  21. except:
  22. oauth = None
  23. import cgi
  24. import logging
  25. import sys
  26. from datetime import datetime
  27. from axes.decorators import watch_login
  28. import django.contrib.auth.views
  29. from django.core import urlresolvers
  30. from django.core.exceptions import SuspiciousOperation
  31. from django.contrib.auth import login, get_backends, authenticate
  32. from django.contrib.sessions.models import Session
  33. from django.http import HttpResponseRedirect
  34. from django.utils.translation import ugettext as _
  35. from hadoop.fs.exceptions import WebHdfsException
  36. from notebook.connectors.base import get_api
  37. from useradmin.models import get_profile, UserProfile
  38. from useradmin.views import ensure_home_directory, require_change_password
  39. from desktop.auth import forms as auth_forms
  40. from desktop.auth.backend import OIDCBackend
  41. from desktop.auth.forms import ImpersonationAuthenticationForm, OrganizationUserCreationForm, OrganizationAuthenticationForm
  42. from desktop.conf import OAUTH, ENABLE_ORGANIZATIONS
  43. from desktop.lib.django_util import render
  44. from desktop.lib.django_util import login_notrequired
  45. from desktop.lib.django_util import JsonResponse
  46. from desktop.log.access import access_log, access_warn, last_access_map
  47. from desktop.settings import LOAD_BALANCER_COOKIE
  48. if ENABLE_ORGANIZATIONS.get():
  49. from useradmin.models import User, Group
  50. else:
  51. from django.contrib.auth.models import User, Group
  52. if sys.version_info[0] > 2:
  53. import urllib.request, urllib.error
  54. from urllib.parse import urlencode as urllib_urlencode
  55. else:
  56. from urllib import urlencode as urllib_urlencode
  57. LOG = logging.getLogger(__name__)
  58. def get_current_users():
  59. """Return dictionary of User objects and
  60. a dictionary of the user's IP address and last access time"""
  61. current_users = { }
  62. for session in Session.objects.all():
  63. try:
  64. uid = session.get_decoded().get(django.contrib.auth.SESSION_KEY)
  65. except SuspiciousOperation:
  66. # If secret_key changed, this resolution won't work.
  67. uid = None
  68. if uid is not None:
  69. try:
  70. userobj = User.objects.get(pk=uid)
  71. current_users[userobj] = last_access_map.get(userobj.username, { })
  72. except User.DoesNotExist:
  73. LOG.debug("User with id=%d does not exist" % uid)
  74. return current_users
  75. def first_login_ever():
  76. backends = get_backends()
  77. for backend in backends:
  78. if hasattr(backend, 'is_first_login_ever') and backend.is_first_login_ever():
  79. return True
  80. return False
  81. # We want unique method name to represent HUE-3 vs HUE-4 method call. This is required because of urlresolvers.reverse('desktop.auth.views.dt_login') below which needs uniqueness to work correctly
  82. @login_notrequired
  83. def dt_login_old(request, from_modal=False):
  84. return dt_login(request, from_modal)
  85. @login_notrequired
  86. @watch_login
  87. def dt_login(request, from_modal=False):
  88. if request.method == 'GET':
  89. redirect_to = request.GET.get('next', '/')
  90. else:
  91. redirect_to = request.POST.get('next', '/')
  92. is_first_login_ever = first_login_ever()
  93. backend_names = auth_forms.get_backend_names()
  94. is_active_directory = auth_forms.is_active_directory()
  95. is_ldap_option_selected = 'server' not in request.POST or request.POST.get('server') == 'LDAP' \
  96. or request.POST.get('server') in auth_forms.get_ldap_server_keys()
  97. if is_active_directory and is_ldap_option_selected:
  98. UserCreationForm = auth_forms.LdapUserCreationForm
  99. AuthenticationForm = auth_forms.LdapAuthenticationForm
  100. else:
  101. UserCreationForm = auth_forms.UserCreationForm
  102. if 'ImpersonationBackend' in backend_names:
  103. AuthenticationForm = ImpersonationAuthenticationForm
  104. else:
  105. AuthenticationForm = auth_forms.AuthenticationForm
  106. if ENABLE_ORGANIZATIONS.get():
  107. UserCreationForm = OrganizationUserCreationForm
  108. AuthenticationForm = OrganizationAuthenticationForm
  109. if request.method == 'POST':
  110. request.audit = {
  111. 'operation': 'USER_LOGIN',
  112. 'username': request.POST.get('username', request.POST.get('email'))
  113. }
  114. # For first login, need to validate user info!
  115. first_user_form = is_first_login_ever and UserCreationForm(data=request.POST) or None
  116. first_user = first_user_form and first_user_form.is_valid()
  117. if first_user or not is_first_login_ever:
  118. auth_form = AuthenticationForm(data=request.POST)
  119. if auth_form.is_valid():
  120. # Must login by using the AuthenticationForm. It provides 'backends' on the User object.
  121. user = auth_form.get_user()
  122. userprofile = get_profile(user)
  123. login(request, user)
  124. if request.session.test_cookie_worked():
  125. request.session.delete_test_cookie()
  126. try:
  127. ensure_home_directory(request.fs, user)
  128. except (IOError, WebHdfsException) as e:
  129. LOG.error('Could not create home directory at login for %s.' % user, exc_info=e)
  130. if require_change_password(userprofile):
  131. return HttpResponseRedirect(urlresolvers.reverse('useradmin.views.edit_user', kwargs={'username': user.username}))
  132. userprofile.first_login = False
  133. userprofile.last_activity = datetime.now()
  134. # This is to fix a bug in Hue 4.3
  135. if userprofile.creation_method == UserProfile.CreationMethod.EXTERNAL:
  136. userprofile.creation_method = UserProfile.CreationMethod.EXTERNAL.name
  137. userprofile.save()
  138. msg = 'Successful login for user: %s' % user.username
  139. request.audit['operationText'] = msg
  140. access_warn(request, msg)
  141. if from_modal or request.GET.get('fromModal', 'false') == 'true':
  142. return JsonResponse({'auth': True})
  143. else:
  144. return HttpResponseRedirect(redirect_to)
  145. else:
  146. request.audit['allowed'] = False
  147. msg = 'Failed login for user: %s' % request.POST.get('username')
  148. request.audit['operationText'] = msg
  149. access_warn(request, msg)
  150. if from_modal or request.GET.get('fromModal', 'false') == 'true':
  151. return JsonResponse({'auth': False})
  152. else:
  153. first_user_form = None
  154. auth_form = AuthenticationForm()
  155. # SAML/OIDC user is already authenticated in djangosaml2.views.login
  156. if hasattr(request,'fs') and ('KnoxSpnegoDjangoBackend' in backend_names or 'SpnegoDjangoBackend' in backend_names or 'OIDCBackend' in backend_names or 'SAML2Backend' in backend_names) and request.user.is_authenticated():
  157. try:
  158. ensure_home_directory(request.fs, request.user)
  159. except (IOError, WebHdfsException) as e:
  160. LOG.error('Could not create home directory for %s user %s.' % ('OIDC' if 'OIDCBackend' in backend_names else 'SAML', request.user))
  161. if request.user.is_authenticated():
  162. return HttpResponseRedirect(redirect_to)
  163. if is_active_directory and not is_ldap_option_selected and \
  164. request.method == 'POST' and request.user.username != request.POST.get('username'):
  165. # local user login failed, give the right auth_form with 'server' field
  166. auth_form = auth_forms.LdapAuthenticationForm()
  167. if not from_modal:
  168. request.session.set_test_cookie()
  169. renderable_path = 'login.mako'
  170. if from_modal:
  171. renderable_path = 'login_modal.mako'
  172. response = render(renderable_path, request, {
  173. 'action': urlresolvers.reverse('desktop_auth_views_dt_login'),
  174. 'form': first_user_form or auth_form,
  175. 'next': redirect_to,
  176. 'first_login_ever': is_first_login_ever,
  177. 'login_errors': request.method == 'POST',
  178. 'backend_names': backend_names,
  179. 'active_directory': is_active_directory,
  180. 'user': request.user
  181. })
  182. if not request.user.is_authenticated():
  183. response.delete_cookie(LOAD_BALANCER_COOKIE) # Note: might be re-balanced to another Hue on login.
  184. return response
  185. def dt_logout(request, next_page=None):
  186. """Log out the user"""
  187. username = request.user.get_username()
  188. request.audit = {
  189. 'username': username,
  190. 'operation': 'USER_LOGOUT',
  191. 'operationText': 'Logged out user: %s' % username
  192. }
  193. # Close Impala session on logout
  194. session_app = "impala"
  195. if request.user.has_hue_permission(action='access', app=session_app):
  196. session = {"type":session_app,"sourceMethod":"dt_logout"}
  197. try:
  198. get_api(request, session).close_session(session)
  199. except Exception as e:
  200. LOG.warn("Error closing Impala session: %s" % e)
  201. backends = get_backends()
  202. if backends:
  203. for backend in backends:
  204. if hasattr(backend, 'logout'):
  205. try:
  206. response = backend.logout(request, next_page)
  207. if response:
  208. return response
  209. except Exception as e:
  210. LOG.warn('Potential error on logout for user: %s with exception: %s' % (username, e))
  211. if len([backend for backend in backends if hasattr(backend, 'logout')]) == len(backends):
  212. LOG.warn("Failed to log out from all backends for user: %s" % (username))
  213. response = django.contrib.auth.views.logout(request, next_page)
  214. response.delete_cookie(LOAD_BALANCER_COOKIE)
  215. return response
  216. def profile(request):
  217. """
  218. Dumps JSON for user-profile information.
  219. """
  220. return render(None, request, _profile_dict(request.user))
  221. def _profile_dict(user):
  222. return dict(
  223. username=user.username,
  224. first_name=user.first_name,
  225. last_name=user.last_name,
  226. last_login=str(user.last_login), # datetime object needs to be converted
  227. email=user.email)
  228. # OAuth is based on Twitter as example.
  229. @login_notrequired
  230. def oauth_login(request):
  231. assert oauth is not None
  232. consumer = oauth.Consumer(OAUTH.CONSUMER_KEY.get(), OAUTH.CONSUMER_SECRET.get())
  233. client = oauth.Client(consumer)
  234. resp, content = client.request(OAUTH.REQUEST_TOKEN_URL.get(), "POST", body=urllib_urlencode({
  235. 'oauth_callback': 'http://' + request.get_host() + '/login/oauth_authenticated/'
  236. }))
  237. if resp['status'] != '200':
  238. raise Exception(_("Invalid response from OAuth provider: %s") % resp)
  239. request.session['request_token'] = dict(cgi.parse_qsl(content))
  240. url = "%s?oauth_token=%s" % (OAUTH.AUTHENTICATE_URL.get(), request.session['request_token']['oauth_token'])
  241. return HttpResponseRedirect(url)
  242. @login_notrequired
  243. def oauth_authenticated(request):
  244. consumer = oauth.Consumer(OAUTH.CONSUMER_KEY.get(), OAUTH.CONSUMER_SECRET.get())
  245. token = oauth.Token(request.session['request_token']['oauth_token'], request.session['request_token']['oauth_token_secret'])
  246. client = oauth.Client(consumer, token)
  247. resp, content = client.request(OAUTH.ACCESS_TOKEN_URL.get(), "GET")
  248. if resp['status'] != '200':
  249. raise Exception(_("Invalid response from OAuth provider: %s") % resp)
  250. access_token = dict(cgi.parse_qsl(content))
  251. user = authenticate(access_token=access_token)
  252. login(request, user)
  253. redirect_to = request.GET.get('next', '/')
  254. return HttpResponseRedirect(redirect_to)
  255. @login_notrequired
  256. def oidc_failed(request):
  257. if request.user.is_authenticated():
  258. return HttpResponseRedirect('/')
  259. access_warn(request, "401 Unauthorized by oidc")
  260. return render("oidc_failed.mako", request, dict(uri=request.build_absolute_uri()), status=401)