models.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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. The core of this module adds permissions functionality to Hue applications.
  19. A "Hue Permission" (colloquially, appname.action, but stored in the HuePermission model) is a way to specify some action whose
  20. control may be restricted. Every Hue application, by default, has an "access" action. To specify extra actions, applications
  21. can specify them in appname.settings.PERMISSION_ACTIONS, as pairs of (action_name, description).
  22. Several mechanisms enforce permission. First of all, the "access" permission
  23. is controlled by LoginAndPermissionMiddleware. For eligible views within an application, the access permission is checked. Second,
  24. views may use @desktop.decorators.hue_permission_required("action", "app") to annotate their function, and this decorator will
  25. check a permission. Thirdly, you may wish to do so manually, by using something akin to:
  26. app = desktop.lib.apputil.get_current_app() # a string
  27. dp = HuePermission.objects.get(app=pp, action=action)
  28. request.user.has_hue_permission(dp)
  29. Permissions may be granted to groups, but not, currently, to users. A user's abilities is the union of all permissions the group
  30. has access to.
  31. Note that Django itself has a notion of users, groups, and permissions. We re-use Django's notion of users and groups, but ignore its notion of
  32. permissions. The permissions notion in Django is strongly tied to what models you may or may not edit, and there are elaborations (especially
  33. in Django 1.2) to manipulate this row by row. This does not map nicely onto actions which may not relate to database models.
  34. """
  35. import collections
  36. import json
  37. import logging
  38. from datetime import datetime
  39. from enum import Enum
  40. from django.db import connection, models, transaction
  41. from django.contrib.auth import models as auth_models
  42. from django.contrib.auth.models import AbstractUser, BaseUserManager
  43. from django.core.cache import cache
  44. from django.utils import timezone as dtz
  45. from django.utils.translation import ugettext_lazy as _t
  46. from desktop import appmanager
  47. from desktop.conf import ENABLE_ORGANIZATIONS, ENABLE_CONNECTORS
  48. from desktop.lib.connectors.models import _get_installed_connectors, Connector
  49. from desktop.lib.exceptions_renderable import PopupException
  50. from desktop.lib.idbroker.conf import is_idbroker_enabled
  51. from desktop.monkey_patches import monkey_patch_username_validator
  52. from useradmin.conf import DEFAULT_USER_GROUP
  53. if ENABLE_ORGANIZATIONS.get():
  54. from useradmin.models2 import OrganizationUser as User, OrganizationGroup as Group, Organization, get_organization
  55. from useradmin.organization import _fitered_queryset, get_user_request_organization, default_organization
  56. else:
  57. from django.contrib.auth.models import User, Group
  58. class Organization(): pass
  59. def default_organization(): pass
  60. def get_organization(): pass
  61. def _fitered_queryset(queryset): return queryset
  62. def get_user_request_organization(): pass
  63. monkey_patch_username_validator()
  64. LOG = logging.getLogger(__name__)
  65. class UserProfile(models.Model):
  66. """
  67. Extra settings / properties to store for each user.
  68. """
  69. class CreationMethod(Enum):
  70. HUE = 1
  71. EXTERNAL = 2
  72. user = models.OneToOneField(User, unique=True)
  73. home_directory = models.CharField(editable=True, max_length=1024, null=True)
  74. creation_method = models.CharField(editable=True, null=False, max_length=64, default=CreationMethod.HUE.name)
  75. first_login = models.BooleanField(default=True, verbose_name=_t('First Login'), help_text=_t('If this is users first login.'))
  76. last_activity = models.DateTimeField(auto_now=True, db_index=True)
  77. json_data = models.TextField(default='{}')
  78. def get_groups(self):
  79. return self.user.groups.all()
  80. def _lookup_permission(self, app, action):
  81. # We cache it instead of doing HuePermission.objects.get(app=app, action=action). To revert with Django 1.6
  82. perms = cache.get('perms')
  83. if not perms:
  84. perms = dict([('%s:%s' % (p.app, p.action), p) for p in HuePermission.objects.all()])
  85. cache.set('perms', perms, 60 * 60)
  86. return perms.get('%s:%s' % (app, action))
  87. def has_hue_permission(self, action=None, app=None, perm=None):
  88. if perm is None:
  89. try:
  90. perm = self._lookup_permission(app, action)
  91. except HuePermission.DoesNotExist:
  92. LOG.exception("Permission object %s - %s not available. Was Django migrate command run after installation?" % (app, action))
  93. return self.user.is_superuser
  94. if self.user.is_superuser:
  95. return True
  96. if ENABLE_CONNECTORS.get() and app in ('jobbrowser', 'metastore', 'filebrowser', 'indexer', 'useradmin'):
  97. return True
  98. group_ids = self.user.groups.values_list('id', flat=True)
  99. return GroupPermission.objects.filter(group__id__in=group_ids, hue_permission=perm).exists()
  100. def get_permissions(self):
  101. return HuePermission.objects.filter(groups__user=self.user)
  102. def check_hue_permission(self, perm=None, app=None, action=None):
  103. """
  104. Raises a PopupException if permission is denied.
  105. Either perm or both app and action are required.
  106. """
  107. if perm is None:
  108. perm = self._lookup_permission(app, action)
  109. if self.has_hue_permission(perm):
  110. return
  111. else:
  112. raise PopupException(_t("You do not have permissions to %(description)s.") % {'description': perm.description})
  113. @property
  114. def data(self):
  115. if not self.json_data:
  116. self.json_data = json.dumps({})
  117. return json.loads(self.json_data)
  118. def update_data(self, val):
  119. data_dict = self.data
  120. data_dict.update(val)
  121. self.json_data = json.dumps(data_dict)
  122. def get_profile(user):
  123. """
  124. Caches the profile, to avoid DB queries at every call.
  125. """
  126. if hasattr(user, "_cached_userman_profile"):
  127. return user._cached_userman_profile
  128. else:
  129. # Lazily create profile.
  130. try:
  131. profile = UserProfile.objects.get(user=user)
  132. except UserProfile.DoesNotExist as e:
  133. profile = create_profile_for_user(user)
  134. user._cached_userman_profile = profile
  135. return profile
  136. def group_has_permission(group, perm):
  137. return GroupPermission.objects.filter(group=group, hue_permission=perm).exists()
  138. def group_permissions(group):
  139. return HuePermission.objects.filter(grouppermission__group=group).all()
  140. # Create a user profile for the given user
  141. def create_profile_for_user(user):
  142. p = UserProfile()
  143. p.user = user
  144. p.last_activity = dtz.now()
  145. p.home_directory = "/user/%s" % p.user.username
  146. try:
  147. p.save()
  148. return p
  149. except:
  150. LOG.exception("Failed to automatically create user profile.")
  151. return None
  152. class LdapGroup(models.Model):
  153. """
  154. Groups that come from LDAP originally will have an LdapGroup
  155. record generated at creation time.
  156. """
  157. group = models.ForeignKey(Group, related_name="group")
  158. class GroupPermission(models.Model):
  159. """
  160. Represents the permissions a group has.
  161. """
  162. group = models.ForeignKey(Group)
  163. hue_permission = models.ForeignKey("HuePermission")
  164. class BasePermission(models.Model):
  165. """
  166. Set of non-object specific permissions that an app supports.
  167. Currently only assign permissions to groups (not users or roles).
  168. Could someday support external permissions of Apache Ranger permissions, AWS IAM... This could be done via subclasses or creating new types
  169. of connectors.
  170. """
  171. app = models.CharField(max_length=30)
  172. action = models.CharField(max_length=100)
  173. description = models.CharField(max_length=255)
  174. groups = models.ManyToManyField(Group, through=GroupPermission)
  175. def __str__(self):
  176. return "%s.%s:%s(%d)" % (self.app, self.action, self.description, self.pk)
  177. @classmethod
  178. def get_app_permission(cls, hue_app, action):
  179. return BasePermission.objects.get(app=hue_app, action=action)
  180. class Meta(object):
  181. abstract = True
  182. class ConnectorPermission(BasePermission):
  183. connector = models.ForeignKey(Connector)
  184. class Meta(object):
  185. abstract = True
  186. verbose_name = _t('Connector permission')
  187. verbose_name_plural = _t('Connector permissions')
  188. unique_together = ('connector', 'action',)
  189. class OrganizationConnectorPermissionManager(models.Manager):
  190. def get_queryset(self):
  191. """Restrict to only organization"""
  192. queryset = super(OrganizationConnectorPermissionManager, self).get_queryset()
  193. return _fitered_queryset(queryset)
  194. class OrganizationConnectorPermission(ConnectorPermission):
  195. organization = models.ForeignKey(Organization)
  196. objects = OrganizationConnectorPermissionManager()
  197. def __init__(self, *args, **kwargs):
  198. if not kwargs.get('organization'):
  199. kwargs['organization'] = get_user_request_organization()
  200. super(OrganizationConnectorPermission, self).__init__(*args, **kwargs)
  201. class Meta(ConnectorPermission.Meta):
  202. abstract = True
  203. unique_together = ('connector', 'action', 'organization',)
  204. if ENABLE_CONNECTORS.get():
  205. if ENABLE_ORGANIZATIONS.get():
  206. class HuePermission(OrganizationConnectorPermission): pass
  207. else:
  208. class HuePermission(ConnectorPermission): pass
  209. else:
  210. class HuePermission(BasePermission): pass
  211. def get_default_user_group(**kwargs):
  212. default_user_group = DEFAULT_USER_GROUP.get()
  213. if default_user_group is None:
  214. return None
  215. attributes = {
  216. 'name': default_user_group
  217. }
  218. if ENABLE_ORGANIZATIONS.get() and kwargs.get('user'):
  219. attributes['organization'] = kwargs['user'].organization
  220. group, created = Group.objects.get_or_create(**attributes)
  221. if created:
  222. group.save()
  223. return group
  224. def update_app_permissions(**kwargs):
  225. """
  226. Keep in sync apps and connectors permissions into the database table.
  227. Map app + action to a HuePermission.
  228. v2
  229. Based on the connectors.
  230. Permissions are either based on connectors instances or Hue specific actions.
  231. Permissions can be deleted or added dynamically.
  232. v1
  233. This is a 'migrate' callback.
  234. We never delete permissions automatically, because apps might come and go.
  235. Note that signing up to the "migrate" signal is not necessarily the best thing we can do, since some apps might not
  236. have models, but nonetheless, "migrate" is typically run when apps are installed.
  237. """
  238. created_tables = connection.introspection.table_names()
  239. if ENABLE_ORGANIZATIONS.get() and 'useradmin_organization' not in created_tables:
  240. return
  241. if u'useradmin_huepermission' in created_tables: # Check if Useradmin has been installed.
  242. current = {}
  243. try:
  244. for dp in HuePermission.objects.all():
  245. current.setdefault(dp.app, {})[dp.action] = dp
  246. except:
  247. LOG.exception('failed to get permissions')
  248. return
  249. updated = 0
  250. uptodate = 0
  251. added = []
  252. if ENABLE_CONNECTORS.get():
  253. old_apps = list(current.keys())
  254. ConnectorPerm = collections.namedtuple('ConnectorPerm', 'name nice_name settings')
  255. apps = [
  256. ConnectorPerm(name=connector['name'], nice_name=connector['nice_name'], settings=[])
  257. for connector in _get_installed_connectors()
  258. ]
  259. else:
  260. old_apps = []
  261. apps = appmanager.DESKTOP_APPS
  262. for app in apps:
  263. app_name = app.name
  264. permission_description = "Access the %s connection" % app.nice_name if ENABLE_CONNECTORS.get() else "Launch this application"
  265. actions = set([("access", permission_description)])
  266. actions.update(getattr(app.settings, "PERMISSION_ACTIONS", []))
  267. if app_name not in current:
  268. current[app_name] = {}
  269. if app_name in old_apps:
  270. old_apps.remove(app_name)
  271. for action, description in actions:
  272. c = current[app_name].get(action)
  273. if c:
  274. if c.description != description:
  275. c.description = description
  276. c.save()
  277. updated += 1
  278. else:
  279. uptodate += 1
  280. else:
  281. new_dp = HuePermission(app=app_name, action=action, description=description)
  282. new_dp.save()
  283. added.append(new_dp)
  284. # Only with v2
  285. deleted, _ = HuePermission.objects.filter(app__in=old_apps).delete()
  286. # Add all permissions to default group except some.
  287. default_group = get_default_user_group()
  288. if default_group:
  289. for new_dp in added:
  290. if not (new_dp.app == 'useradmin' and new_dp.action == 'access') and \
  291. not (new_dp.app == 'useradmin' and new_dp.action == 'superuser') and \
  292. not (new_dp.app == 'metastore' and new_dp.action == 'write') and \
  293. not (new_dp.app == 'hbase' and new_dp.action == 'write') and \
  294. not (new_dp.app == 'security' and new_dp.action == 'impersonate') and \
  295. not (new_dp.app == 'filebrowser' and new_dp.action == 's3_access' and not is_idbroker_enabled('s3a')) and \
  296. not (new_dp.app == 'filebrowser' and new_dp.action == 'gs_access' and not is_idbroker_enabled('gs')) and \
  297. not (new_dp.app == 'filebrowser' and new_dp.action == 'adls_access') and \
  298. not (new_dp.app == 'filebrowser' and new_dp.action == 'abfs_access') and \
  299. not (new_dp.app == 'oozie' and new_dp.action == 'disable_editor_access'):
  300. GroupPermission.objects.create(group=default_group, hue_permission=new_dp)
  301. available = HuePermission.objects.count()
  302. stale = available - len(added) - updated - uptodate
  303. if len(added) or updated or stale or deleted:
  304. LOG.info("HuePermissions: %d added, %d updated, %d up to date, %d stale, %d deleted" % (
  305. len(added), updated, uptodate, stale, deleted
  306. )
  307. )
  308. models.signals.post_migrate.connect(update_app_permissions)
  309. # models.signals.post_migrate.connect(get_default_user_group)
  310. def install_sample_user(django_user=None):
  311. """
  312. Setup the de-activated sample user with a certain id. Do not create a user profile.
  313. """
  314. from desktop.models import SAMPLE_USER_ID, get_sample_user_install
  315. from hadoop import cluster
  316. user = None
  317. django_username = get_sample_user_install(django_user)
  318. if ENABLE_ORGANIZATIONS.get():
  319. lookup = {'email': django_username}
  320. django_username_short = django_user.username_short
  321. else:
  322. lookup = {'username': django_username}
  323. django_username_short = django_username
  324. try:
  325. if User.objects.filter(id=SAMPLE_USER_ID).exists():
  326. user = User.objects.get(id=SAMPLE_USER_ID)
  327. LOG.info('Sample user found with username "%s" and User ID: %s' % (user.username, user.id))
  328. elif User.objects.filter(**lookup).exists():
  329. user = User.objects.get(**lookup)
  330. LOG.info('Sample user found: %s' % lookup)
  331. else:
  332. user_attributes = lookup.copy()
  333. if ENABLE_ORGANIZATIONS.get():
  334. user_attributes['organization'] = get_organization(user=django_user)
  335. user_attributes.update({
  336. 'password': '!',
  337. 'is_active': False,
  338. 'is_superuser': False,
  339. 'id': SAMPLE_USER_ID,
  340. 'pk': SAMPLE_USER_ID
  341. })
  342. user, created = User.objects.get_or_create(**user_attributes)
  343. if created:
  344. LOG.info('Installed a user "%s"' % lookup)
  345. if user.username != django_username and not ENABLE_ORGANIZATIONS.get():
  346. LOG.warn('Sample user does not have username "%s", will attempt to modify the username.' % django_username)
  347. with transaction.atomic():
  348. user = User.objects.get(id=SAMPLE_USER_ID)
  349. user.username = django_username
  350. user.save()
  351. except Exception as ex:
  352. LOG.exception('Failed to get or create sample user')
  353. # If sample user doesn't belong to default group, add to default group
  354. default_group = get_default_user_group()
  355. if user is not None and default_group is not None and default_group not in user.groups.all():
  356. user.groups.add(default_group)
  357. user.save()
  358. fs = cluster.get_hdfs()
  359. # If home directory doesn't exist for sample user, create it
  360. try:
  361. if not fs.do_as_user(django_username_short, fs.get_home_dir):
  362. fs.do_as_user(django_username_short, fs.create_home_dir)
  363. LOG.info('Created home directory for user: %s' % django_username_short)
  364. else:
  365. LOG.info('Home directory already exists for user: %s' % django_username)
  366. except Exception as ex:
  367. LOG.exception('Failed to create home directory for user %s: %s' % (django_username, str(ex)))
  368. return user