backend.py 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909
  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. """
  18. Authentication backend classes for Desktop.
  19. These classes should implement the interface described at:
  20. http://docs.djangoproject.com/en/1.0/topics/auth/#writing-an-authentication-backend
  21. In addition, the User classes they return must support:
  22. - get_groups() (returns a list of strings)
  23. - get_home_directory() (returns None or a string)
  24. - has_hue_permission(action, app) -> boolean
  25. Because Django's models are sometimes unfriendly, you'll want
  26. User to remain a django.contrib.auth.models.User object.
  27. """
  28. from builtins import object
  29. from importlib import import_module
  30. import ldap
  31. import logging
  32. import pam
  33. import requests
  34. import django.contrib.auth.backends
  35. from django.contrib import auth
  36. from django.core.urlresolvers import reverse
  37. from django.core.exceptions import ImproperlyConfigured, PermissionDenied
  38. from django.http import HttpResponseRedirect
  39. from django.forms import ValidationError
  40. from django_auth_ldap.backend import LDAPBackend
  41. from django_auth_ldap.config import LDAPSearch
  42. from liboauth.metrics import oauth_authentication_time
  43. from mozilla_django_oidc.auth import OIDCAuthenticationBackend, default_username_algo
  44. from mozilla_django_oidc.utils import absolutify, import_from_settings
  45. from desktop import metrics
  46. from desktop.conf import AUTH, LDAP, OIDC, ENABLE_ORGANIZATIONS
  47. from desktop.settings import LOAD_BALANCER_COOKIE
  48. from useradmin import ldap_access
  49. from useradmin.models import get_profile, get_default_user_group, UserProfile, User
  50. from useradmin.organization import get_organization
  51. LOG = logging.getLogger(__name__)
  52. # TODO: slowly move those utils to the useradmin module
  53. def load_augmentation_class():
  54. """
  55. Loads the user augmentation class.
  56. Similar in spirit to django.contrib.auth.load_backend
  57. """
  58. try:
  59. class_name = AUTH.USER_AUGMENTOR.get()
  60. i = class_name.rfind('.')
  61. module, attr = class_name[:i], class_name[i + 1:]
  62. mod = import_module(module)
  63. klass = getattr(mod, attr)
  64. LOG.info("Augmenting users with class: %s" % (klass,))
  65. return klass
  66. except:
  67. LOG.exception('failed to augment class')
  68. raise ImproperlyConfigured("Could not find user_augmentation_class: %s" % (class_name,))
  69. _user_augmentation_class = None
  70. def get_user_augmentation_class():
  71. global _user_augmentation_class
  72. if _user_augmentation_class is None:
  73. _user_augmentation_class = load_augmentation_class()
  74. return _user_augmentation_class
  75. def rewrite_user(user):
  76. """
  77. Rewrites the user according to the augmentation class.
  78. We currently only re-write specific attributes, though this could be generalized.
  79. """
  80. if user is None:
  81. LOG.warn('Failed to rewrite user, user is None.')
  82. else:
  83. augment = get_user_augmentation_class()(user)
  84. for attr in ('get_groups', 'get_home_directory', 'has_hue_permission', 'get_permissions'):
  85. setattr(user, attr, getattr(augment, attr))
  86. setattr(user, 'profile', get_profile(user))
  87. setattr(user, 'auth_backend', user.profile.data.get('auth_backend'))
  88. return user
  89. def is_admin(user):
  90. """
  91. Admin of the Organization. Typically can edit users, connectors...
  92. To rename to is_org_admin at some point.
  93. If ENABLE_ORGANIZATIONS is false:
  94. - Hue superusers are automatically also admin
  95. If ENABLE_ORGANIZATIONS is true:
  96. - Hue superusers might not be admin of the organization
  97. """
  98. is_admin = False
  99. if hasattr(user, 'is_superuser') and not ENABLE_ORGANIZATIONS.get():
  100. is_admin = user.is_superuser
  101. if not is_admin and user.is_authenticated():
  102. try:
  103. user = rewrite_user(user)
  104. is_admin = user.is_admin if ENABLE_ORGANIZATIONS.get() else user.has_hue_permission(action="superuser", app="useradmin")
  105. except Exception:
  106. LOG.exception("Could not validate if %s is a superuser, assuming False." % user)
  107. return is_admin
  108. def is_hue_admin(user):
  109. """
  110. Hue service super user. Can manage global settings of the services used by all the organization.
  111. Independent of ENABLE_ORGANIZATIONS.
  112. """
  113. return hasattr(user, 'is_superuser') and user.is_superuser
  114. class DefaultUserAugmentor(object):
  115. def __init__(self, parent):
  116. self._parent = parent
  117. def _get_profile(self):
  118. return get_profile(self._parent)
  119. def get_groups(self):
  120. return self._get_profile().get_groups()
  121. def get_home_directory(self):
  122. return self._get_profile().home_directory
  123. def has_hue_permission(self, action, app):
  124. return self._get_profile().has_hue_permission(action=action, app=app)
  125. def get_permissions(self):
  126. return self._get_profile().get_permissions()
  127. def find_user(username):
  128. lookup = {'email': username} if ENABLE_ORGANIZATIONS.get() else {'username': username}
  129. try:
  130. user = User.objects.get(**lookup)
  131. LOG.debug("Found user %s" % user)
  132. except User.DoesNotExist:
  133. user = None
  134. return user
  135. def create_user(username, password, is_superuser=True):
  136. if ENABLE_ORGANIZATIONS.get():
  137. organization = get_organization(email=username)
  138. attrs = {'email': username, 'organization': organization}
  139. else:
  140. attrs = {'username': username}
  141. user = User(**attrs)
  142. if password is None:
  143. user.set_unusable_password()
  144. else:
  145. user.set_password(password)
  146. user.is_superuser = is_superuser
  147. if ENABLE_ORGANIZATIONS.get():
  148. user.is_admin = is_superuser or not organization.organizationuser_set.exists() or not organization.is_multi_user
  149. user.save()
  150. ensure_has_a_group(user)
  151. user.save()
  152. LOG.info("User %s was created." % username)
  153. return user
  154. def find_or_create_user(username, password=None, is_superuser=True):
  155. user = find_user(username)
  156. if user is None:
  157. user = create_user(username, password, is_superuser)
  158. return user
  159. def ensure_has_a_group(user):
  160. default_group = get_default_user_group(user=user)
  161. if not user.groups.exists() and default_group is not None:
  162. user.groups.add(default_group)
  163. user.save()
  164. def force_username_case(username):
  165. if AUTH.FORCE_USERNAME_LOWERCASE.get():
  166. username = username.lower()
  167. elif AUTH.FORCE_USERNAME_UPPERCASE.get():
  168. username = username.upper()
  169. return username
  170. class DesktopBackendBase(object):
  171. """
  172. Abstract base class for providing external authentication schemes.
  173. Extend this class and implement check_auth
  174. """
  175. def authenticate(self, username, password):
  176. if self.check_auth(username, password):
  177. user = find_or_create_user(username)
  178. user = rewrite_user(user)
  179. return user
  180. else:
  181. return None
  182. def get_user(self, user_id):
  183. try:
  184. user = User.objects.get(pk=user_id)
  185. user = rewrite_user(user)
  186. return user
  187. except User.DoesNotExist:
  188. return None
  189. def check_auth(self, username, password):
  190. """
  191. Implementors should return a boolean value which determines
  192. whether the given username and password pair is valid.
  193. """
  194. raise NotImplemented("Abstract class - must implement check_auth")
  195. class AllowFirstUserDjangoBackend(django.contrib.auth.backends.ModelBackend):
  196. """
  197. Allows the first user in, but otherwise delegates to Django's
  198. ModelBackend.
  199. """
  200. def authenticate(self, username=None, email=None, password=None):
  201. if email is not None:
  202. username = email
  203. username = force_username_case(username)
  204. request = None
  205. user = super(AllowFirstUserDjangoBackend, self).authenticate(request, username=username, password=password)
  206. if user is not None:
  207. if user.is_active:
  208. user = rewrite_user(user)
  209. return user
  210. return user
  211. if self.is_first_login_ever():
  212. user = find_or_create_user(username, password)
  213. user = rewrite_user(user)
  214. userprofile = get_profile(user)
  215. userprofile.first_login = False
  216. userprofile.save()
  217. ensure_has_a_group(user)
  218. return user
  219. return None
  220. def get_user(self, user_id):
  221. user = super(AllowFirstUserDjangoBackend, self).get_user(user_id)
  222. user = rewrite_user(user)
  223. return user
  224. def is_first_login_ever(self):
  225. """ Return true if no one has ever logged in."""
  226. return User.objects.count() == 0
  227. class ImpersonationBackend(django.contrib.auth.backends.ModelBackend):
  228. """
  229. Authenticate with a proxy user username/password but then login as another user.
  230. Does not support a multiple backends setup.
  231. """
  232. def authenticate(self, username=None, password=None, login_as=None):
  233. if not login_as:
  234. return
  235. request = None
  236. authenticated = super(ImpersonationBackend, self).authenticate(request, username, password)
  237. if not authenticated:
  238. raise PermissionDenied()
  239. user = find_or_create_user(login_as, password=None, is_superuser=False)
  240. ensure_has_a_group(user)
  241. return rewrite_user(user)
  242. def get_user(self, user_id):
  243. user = super(ImpersonationBackend, self).get_user(user_id)
  244. user = rewrite_user(user)
  245. return user
  246. class OAuthBackend(DesktopBackendBase):
  247. """
  248. Deprecated, use liboauth.backend.OAuthBackend instead
  249. Heavily based on Twitter Oauth: https://github.com/simplegeo/python-oauth2#logging-into-django-w-twitter
  250. Requires: python-oauth2 and httplib2
  251. build/env/bin/python setup.py install https://github.com/simplegeo/python-oauth2
  252. build/env/bin/pip install httplib2
  253. """
  254. @oauth_authentication_time
  255. def authenticate(self, access_token):
  256. username = access_token['screen_name']
  257. password = access_token['oauth_token_secret']
  258. # Could save oauth_token detail in the user profile here
  259. user = find_or_create_user(username, password)
  260. user.is_superuser = False
  261. user.save()
  262. ensure_has_a_group(user)
  263. return user
  264. @classmethod
  265. def manages_passwords_externally(cls):
  266. return True
  267. class AllowAllBackend(DesktopBackendBase):
  268. """
  269. Authentication backend that allows any user to login as long
  270. as they have a username. The users will be added to the 'default_user_group'.
  271. We want to ensure that already created users (e.g., from other backends)
  272. retain their superuser status, and any new users are not super users by default.
  273. """
  274. def check_auth(self, username, password):
  275. user = find_user(username)
  276. if user is None:
  277. user = create_user(username, password)
  278. user.is_superuser = False
  279. user.save()
  280. ensure_has_a_group(user)
  281. return user
  282. @classmethod
  283. def manages_passwords_externally(cls):
  284. return True
  285. class PamBackend(DesktopBackendBase):
  286. """
  287. Authentication backend that uses PAM to authenticate logins. The first user to
  288. login will become the superuser.
  289. """
  290. @metrics.pam_authentication_time
  291. def authenticate(self, request=None, username=None, password=None):
  292. username = force_username_case(username)
  293. if pam.authenticate(username, password, AUTH.PAM_SERVICE.get()):
  294. is_super = False
  295. if User.objects.count() == 0:
  296. is_super = True
  297. try:
  298. if AUTH.IGNORE_USERNAME_CASE.get():
  299. user = User.objects.get(username__iexact=username)
  300. else:
  301. user = User.objects.get(username=username)
  302. except User.DoesNotExist:
  303. user = find_or_create_user(username, None)
  304. if user is not None and user.is_active:
  305. profile = get_profile(user)
  306. profile.creation_method = UserProfile.CreationMethod.EXTERNAL.name
  307. profile.save()
  308. user.is_superuser = is_super
  309. ensure_has_a_group(user)
  310. user.save()
  311. user = rewrite_user(user)
  312. return user
  313. return None
  314. @classmethod
  315. def manages_passwords_externally(cls):
  316. return True
  317. class LdapBackend(object):
  318. """
  319. Authentication backend that uses LDAP to authenticate logins.
  320. The first user to login will become the superuser.
  321. """
  322. def __init__(self):
  323. # Delegate to django_auth_ldap.LDAPBackend
  324. class _LDAPBackend(LDAPBackend):
  325. def get_or_create_user(self, username, ldap_user):
  326. username = force_username_case(username)
  327. try:
  328. #Avoid circular import from is_admin
  329. from useradmin.forms import validate_username
  330. validate_username(username)
  331. if LDAP.IGNORE_USERNAME_CASE.get():
  332. try:
  333. return User.objects.get(username__iexact=username), False
  334. except User.DoesNotExist:
  335. return User.objects.get_or_create(username=username)
  336. else:
  337. return User.objects.get_or_create(username=username)
  338. except ValidationError as e:
  339. LOG.exception("LDAP username is invalid: %s" % username)
  340. self._backend = _LDAPBackend()
  341. def add_ldap_config(self, ldap_config):
  342. ldap_url = ldap_config.LDAP_URL.get()
  343. if ldap_url is None:
  344. LOG.warn("Could not find LDAP URL required for authentication.")
  345. return None
  346. else:
  347. setattr(self._backend.settings, 'SERVER_URI', ldap_config.LDAP_URL.get())
  348. if ldap_url.lower().startswith('ldaps') and ldap_config.USE_START_TLS.get():
  349. LOG.warn("Cannot configure LDAP with SSL and enable STARTTLS.")
  350. if ldap_config.SEARCH_BIND_AUTHENTICATION.get():
  351. # New Search/Bind Auth
  352. base_dn = ldap_config.BASE_DN.get()
  353. user_name_attr = ldap_config.USERS.USER_NAME_ATTR.get()
  354. user_filter = ldap_config.USERS.USER_FILTER.get()
  355. if not user_filter.startswith('('):
  356. user_filter = '(' + user_filter + ')'
  357. if ldap_config.BIND_DN.get():
  358. bind_dn = ldap_config.BIND_DN.get()
  359. setattr(self._backend.settings, 'BIND_DN', bind_dn)
  360. bind_password = ldap_config.BIND_PASSWORD.get()
  361. if not bind_password:
  362. bind_password = ldap_config.BIND_PASSWORD_SCRIPT.get()
  363. setattr(self._backend.settings, 'BIND_PASSWORD', bind_password)
  364. if user_filter is None:
  365. search_bind_results = LDAPSearch(base_dn,
  366. ldap.SCOPE_SUBTREE, "(" + user_name_attr + "=%(user)s)")
  367. else:
  368. search_bind_results = LDAPSearch(base_dn,
  369. ldap.SCOPE_SUBTREE, "(&(" + user_name_attr + "=%(user)s)" + user_filter + ")")
  370. setattr(self._backend.settings, 'USER_SEARCH', search_bind_results)
  371. else:
  372. nt_domain = ldap_config.NT_DOMAIN.get()
  373. if nt_domain is None:
  374. pattern = ldap_config.LDAP_USERNAME_PATTERN.get()
  375. pattern = pattern.replace('<username>', '%(user)s')
  376. setattr(self._backend.settings, 'USER_DN_TEMPLATE', pattern)
  377. else:
  378. # %(user)s is a special string that will get replaced during the authentication process
  379. setattr(self._backend.settings, 'USER_DN_TEMPLATE', "%(user)s@" + nt_domain)
  380. # If Secure ldaps is specified in hue.ini then ldap code automatically use SSL/TLS communication
  381. if not ldap_url.lower().startswith('ldaps'):
  382. setattr(self._backend.settings, 'START_TLS', ldap_config.USE_START_TLS.get())
  383. # Certificate-related config settings
  384. if ldap_config.LDAP_CERT.get():
  385. ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_DEMAND)
  386. ldap.set_option(ldap.OPT_X_TLS_CACERTFILE, ldap_config.LDAP_CERT.get())
  387. else:
  388. ldap.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_NEVER)
  389. if ldap_config.FOLLOW_REFERRALS.get():
  390. ldap.set_option(ldap.OPT_REFERRALS, 1)
  391. else:
  392. ldap.set_option(ldap.OPT_REFERRALS, 0)
  393. def add_ldap_config_for_server(self, server):
  394. if LDAP.LDAP_SERVERS.get():
  395. # Choose from multiple server configs
  396. if server in LDAP.LDAP_SERVERS.get():
  397. self.add_ldap_config(LDAP.LDAP_SERVERS.get()[server])
  398. else:
  399. self.add_ldap_config(LDAP)
  400. @metrics.ldap_authentication_time
  401. def authenticate(self, request=None, username=None, password=None, server=None):
  402. self.add_ldap_config_for_server(server)
  403. username_filter_kwargs = ldap_access.get_ldap_user_kwargs(username)
  404. # Do this check up here, because the auth call creates a django user upon first login per user
  405. is_super = False
  406. if not UserProfile.objects.filter(creation_method=UserProfile.CreationMethod.EXTERNAL.name).exists():
  407. # If there are no LDAP users already in the system, the first one will
  408. # become a superuser
  409. is_super = True
  410. elif User.objects.filter(**username_filter_kwargs).exists():
  411. # If the user already exists, we shouldn't change its superuser
  412. # privileges. However, if there's a naming conflict with a non-external
  413. # user, we should do the safe thing and turn off superuser privs.
  414. existing_user = User.objects.get(**username_filter_kwargs)
  415. existing_profile = get_profile(existing_user)
  416. if existing_profile.creation_method == UserProfile.CreationMethod.EXTERNAL.name:
  417. is_super = User.objects.get(**username_filter_kwargs).is_superuser
  418. elif not LDAP.CREATE_USERS_ON_LOGIN.get():
  419. LOG.warn("Create users when they login with their LDAP credentials is turned off")
  420. return None
  421. try:
  422. allowed_group = self.check_ldap_access_groups(server, username)
  423. if allowed_group:
  424. user = self._backend.authenticate(username, password)
  425. else:
  426. LOG.warn("%s not in an allowed login group" % username)
  427. return None
  428. except ImproperlyConfigured as detail:
  429. LOG.warn("LDAP was not properly configured: %s", detail)
  430. return None
  431. if user is not None and user.is_active:
  432. profile = get_profile(user)
  433. profile.creation_method = UserProfile.CreationMethod.EXTERNAL.name
  434. profile.save()
  435. user.is_superuser = is_super
  436. user = rewrite_user(user)
  437. ensure_has_a_group(user)
  438. if LDAP.SYNC_GROUPS_ON_LOGIN.get():
  439. self.import_groups(server, user)
  440. return user
  441. def get_user(self, user_id):
  442. user = self._backend.get_user(user_id)
  443. user = rewrite_user(user)
  444. return user
  445. def check_ldap_access_groups(self, server, username):
  446. #Avoid circular import from is_admin
  447. from useradmin.views import get_find_groups_filter
  448. allowed_group = False
  449. if LDAP.LOGIN_GROUPS.get() and LDAP.LOGIN_GROUPS.get() != ['']:
  450. login_groups = LDAP.LOGIN_GROUPS.get()
  451. connection = ldap_access.get_connection_from_server(server)
  452. try:
  453. user_info = connection.find_users(username, find_by_dn=False)
  454. except Exception as e:
  455. LOG.warn("Failed to find LDAP user: %s" % e)
  456. if not user_info:
  457. LOG.warn("Could not get LDAP details for users with pattern %s" % username)
  458. return False
  459. ldap_info = user_info[0]
  460. group_ldap_info = connection.find_groups("*", group_filter=get_find_groups_filter(ldap_info, server))
  461. for group in group_ldap_info:
  462. if group['name'] in login_groups:
  463. return True
  464. else:
  465. #Login groups not set default to True
  466. allowed_group = True
  467. return allowed_group
  468. def import_groups(self, server, user):
  469. connection = ldap_access.get_connection_from_server(server)
  470. #Avoid circular import from is_admin
  471. from useradmin.views import import_ldap_users
  472. import_ldap_users(connection, user.username, sync_groups=True, import_by_dn=False, server=server)
  473. @classmethod
  474. def manages_passwords_externally(cls):
  475. return True
  476. class SpnegoDjangoBackend(django.contrib.auth.backends.ModelBackend):
  477. """
  478. A note about configuration:
  479. The HTTP/_HOST@REALM principal (where _HOST is the fully qualified domain
  480. name of the server running Hue) needs to be exported to a keytab file.
  481. The keytab file can either be located in /etc/krb5.keytab or you can set
  482. the KRB5_KTNAME environment variable to point to another location
  483. (e.g. /etc/hue/hue.keytab).
  484. """
  485. @metrics.spnego_authentication_time
  486. def authenticate(self, request=None, username=None):
  487. username = self.clean_username(username)
  488. username = force_username_case(username)
  489. is_super = False
  490. if User.objects.count() == 0:
  491. is_super = True
  492. try:
  493. if AUTH.IGNORE_USERNAME_CASE.get():
  494. user = User.objects.get(username__iexact=username)
  495. else:
  496. user = User.objects.get(username=username)
  497. except User.DoesNotExist:
  498. user = find_or_create_user(username, None)
  499. if user is not None and user.is_active:
  500. profile = get_profile(user)
  501. profile.creation_method = UserProfile.CreationMethod.EXTERNAL.name
  502. profile.save()
  503. user.is_superuser = is_super
  504. ensure_has_a_group(user)
  505. user.save()
  506. if user is not None:
  507. user = rewrite_user(user)
  508. return user
  509. def clean_username(self, username):
  510. if '@' in username:
  511. return username.split('@')[0]
  512. return username
  513. def get_user(self, user_id):
  514. user = super(SpnegoDjangoBackend, self).get_user(user_id)
  515. user = rewrite_user(user)
  516. return user
  517. class KnoxSpnegoDjangoBackend(django.contrib.auth.backends.ModelBackend):
  518. """
  519. Knox Trusted proxy passes GET parameter doAs
  520. """
  521. @metrics.spnego_authentication_time
  522. def authenticate(self, request=None, username=None):
  523. username = self.clean_username(username, request)
  524. username = force_username_case(username)
  525. is_super = False
  526. if User.objects.count() == 0:
  527. is_super = True
  528. try:
  529. if AUTH.IGNORE_USERNAME_CASE.get():
  530. user = User.objects.get(username__iexact=username)
  531. else:
  532. user = User.objects.get(username=username)
  533. except User.DoesNotExist:
  534. user = find_or_create_user(username, None)
  535. if user is not None and user.is_active:
  536. profile = get_profile(user)
  537. profile.creation_method = UserProfile.CreationMethod.EXTERNAL.name
  538. profile.save()
  539. user.is_superuser = is_super
  540. ensure_has_a_group(user)
  541. user.save()
  542. if user is not None:
  543. user = rewrite_user(user)
  544. return user
  545. def clean_username(self, username, request):
  546. # Grab doAs parameter here
  547. doas_user = request.GET.get("doAs", "")
  548. return doas_user
  549. def get_user(self, user_id):
  550. user = super(KnoxSpnegoDjangoBackend, self).get_user(user_id)
  551. user = rewrite_user(user)
  552. return user
  553. class RemoteUserDjangoBackend(django.contrib.auth.backends.RemoteUserBackend):
  554. """
  555. Delegates to Django's RemoteUserBackend and requires HueRemoteUserMiddleware
  556. """
  557. def authenticate(self, remote_user=None):
  558. username = self.clean_username(remote_user)
  559. username = force_username_case(username)
  560. is_super = False
  561. if User.objects.count() == 0:
  562. is_super = True
  563. try:
  564. if AUTH.IGNORE_USERNAME_CASE.get():
  565. user = User.objects.get(username__iexact=username)
  566. else:
  567. user = User.objects.get(username=username)
  568. except User.DoesNotExist:
  569. user = find_or_create_user(username, None)
  570. if user is not None and user.is_active:
  571. profile = get_profile(user)
  572. profile.creation_method = UserProfile.CreationMethod.EXTERNAL.name
  573. profile.save()
  574. user.is_superuser = is_super
  575. ensure_has_a_group(user)
  576. user.save()
  577. user = rewrite_user(user)
  578. return user
  579. def get_user(self, user_id):
  580. user = super(RemoteUserDjangoBackend, self).get_user(user_id)
  581. user = rewrite_user(user)
  582. return user
  583. class OIDCBackend(OIDCAuthenticationBackend):
  584. def authenticate(self, **kwargs):
  585. self.request = kwargs.pop('request', None)
  586. if not self.request:
  587. return None
  588. state = self.request.GET.get('state')
  589. code = self.request.GET.get('code')
  590. nonce = kwargs.pop('nonce', None)
  591. if not code or not state:
  592. return None
  593. reverse_url = import_from_settings('OIDC_AUTHENTICATION_CALLBACK_URL', 'oidc_authentication_callback')
  594. token_payload = {
  595. 'client_id': self.OIDC_RP_CLIENT_ID,
  596. 'client_secret': self.OIDC_RP_CLIENT_SECRET,
  597. 'grant_type': 'authorization_code',
  598. 'code': code,
  599. 'redirect_uri': absolutify(
  600. self.request,
  601. reverse(reverse_url)
  602. ),
  603. }
  604. # Get the token
  605. token_info = self.get_token(token_payload)
  606. id_token = token_info.get('id_token')
  607. access_token = token_info.get('access_token')
  608. refresh_token = token_info.get('refresh_token')
  609. # Validate the token
  610. verified_id = self.verify_token(id_token, nonce=nonce)
  611. if verified_id:
  612. self.save_refresh_tokens(refresh_token)
  613. user = self.get_or_create_user(access_token, id_token, verified_id)
  614. user = rewrite_user(user)
  615. return user
  616. return None
  617. def filter_users_by_claims(self, claims):
  618. username = claims.get(import_from_settings('OIDC_USERNAME_ATTRIBUTE', 'preferred_username'))
  619. if not username:
  620. return self.UserModel.objects.none()
  621. return self.UserModel.objects.filter(username__iexact=username)
  622. def save_refresh_tokens(self, refresh_token):
  623. session = self.request.session
  624. if import_from_settings('OIDC_STORE_REFRESH_TOKEN', False):
  625. session['oidc_refresh_token'] = refresh_token
  626. def create_user(self, claims):
  627. """Return object for a newly created user account."""
  628. # Overriding lib's logic, use preferred_username from oidc as username
  629. username = claims.get(import_from_settings('OIDC_USERNAME_ATTRIBUTE', 'preferred_username'), '')
  630. email = claims.get('email', '')
  631. first_name = claims.get('given_name', '')
  632. last_name = claims.get('family_name', '')
  633. if not username:
  634. if not email:
  635. LOG.debug("OpenID Connect no username and email while creating new user")
  636. return None
  637. username = default_username_algo(email)
  638. return self.UserModel.objects.create_user(
  639. username=username,
  640. email=email,
  641. first_name=first_name,
  642. last_name=last_name,
  643. is_superuser=self.is_hue_superuser(claims)
  644. )
  645. def get_or_create_user(self, access_token, id_token, verified_id):
  646. user = super(OIDCBackend, self).get_or_create_user(access_token, id_token, verified_id)
  647. if not user and not import_from_settings('OIDC_CREATE_USER', True):
  648. # in this case, user is login from Keycloak, but not allow create
  649. self.logout(self.request, next_page=import_from_settings('LOGIN_REDIRECT_URL_FAILURE', '/'))
  650. return user
  651. def get_user(self, user_id):
  652. user = super(OIDCBackend, self).get_user(user_id)
  653. user = rewrite_user(user)
  654. return user
  655. def update_user(self, user, claims):
  656. if user.is_superuser != self.is_hue_superuser(claims):
  657. user.is_superuser = self.is_hue_superuser(claims)
  658. user.save()
  659. return user
  660. def is_hue_superuser(self, claims):
  661. """
  662. To use this feature, setup in Keycloak:
  663. 1. add the name of Hue superuser group to superuser_group in hue.ini
  664. 2. in Keycloak, go to your_realm --> your_clients --> Mappers, add a mapper
  665. Mapper Type: Group Membership (this is predefined mapper type)
  666. Token Claim Name: group_membership (required exact string)
  667. """
  668. sueruser_group = '/' + OIDC.SUPERUSER_GROUP.get()
  669. if sueruser_group:
  670. return sueruser_group in claims.get('group_membership', [])
  671. return False
  672. def logout(self, request, next_page):
  673. # https://stackoverflow.com/questions/46689034/logout-user-via-keycloak-rest-api-doesnt-work
  674. if request.session.get('_auth_user_backend', '') != 'desktop.auth.backend.OIDCBackend':
  675. return None
  676. session = request.session
  677. access_token = session.get('oidc_access_token', '')
  678. refresh_token = session.get('oidc_refresh_token', '')
  679. if access_token and refresh_token:
  680. oidc_logout_url = OIDC.LOGOUT_REDIRECT_URL.get()
  681. client_id = import_from_settings('OIDC_RP_CLIENT_ID')
  682. client_secret = import_from_settings('OIDC_RP_CLIENT_SECRET')
  683. oidc_verify_ssl = import_from_settings('OIDC_VERIFY_SSL')
  684. form = {
  685. 'client_id': client_id,
  686. 'client_secret': client_secret,
  687. 'refresh_token': refresh_token,
  688. }
  689. headers = {
  690. 'Authorization': 'Bearer {0}'.format(access_token),
  691. 'Content-Type': 'application/x-www-form-urlencoded'
  692. }
  693. resp = requests.post(oidc_logout_url, data=form, headers=headers, verify=oidc_verify_ssl)
  694. if resp.status_code >= 200 and resp.status_code < 300:
  695. LOG.debug("OpenID Connect logout succeed!")
  696. delete_oidc_session_tokens(session)
  697. auth.logout(request)
  698. response = HttpResponseRedirect(next_page)
  699. response.delete_cookie(LOAD_BALANCER_COOKIE)
  700. return response
  701. else:
  702. LOG.error("OpenID Connect logout failed: %s" % resp.content)
  703. else:
  704. LOG.warn("OpenID Connect tokens are not available, logout skipped!")
  705. return None
  706. # def filter_users_by_claims(self, claims):
  707. # def verify_claims(self, claims):
  708. def delete_oidc_session_tokens(session):
  709. if session:
  710. if 'oidc_access_token' in session:
  711. del session['oidc_access_token']
  712. if 'oidc_id_token' in session:
  713. del session['oidc_id_token']
  714. if 'oidc_id_token_expiration' in session:
  715. del session['oidc_id_token_expiration']
  716. if 'oidc_login_next' in session:
  717. del session['oidc_login_next']
  718. if 'oidc_refresh_token' in session:
  719. del session['oidc_refresh_token']
  720. if 'oidc_state' in session:
  721. del session['oidc_state']