PageRenderTime 29ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/ckan/model/user.py

https://gitlab.com/iislod/ckan
Python | 292 lines | 219 code | 41 blank | 32 comment | 28 complexity | 2ae86b6bd130fe1413b930e9b96f58ca MD5 | raw file
  1. import datetime
  2. import re
  3. import os
  4. from hashlib import sha1, md5
  5. import passlib.utils
  6. from passlib.hash import pbkdf2_sha512
  7. from sqlalchemy.sql.expression import or_
  8. from sqlalchemy.orm import synonym
  9. from sqlalchemy import types, Column, Table
  10. import vdm.sqlalchemy
  11. import meta
  12. import core
  13. import types as _types
  14. import domain_object
  15. user_table = Table('user', meta.metadata,
  16. Column('id', types.UnicodeText, primary_key=True,
  17. default=_types.make_uuid),
  18. Column('name', types.UnicodeText, nullable=False, unique=True),
  19. Column('openid', types.UnicodeText),
  20. Column('password', types.UnicodeText),
  21. Column('fullname', types.UnicodeText),
  22. Column('email', types.UnicodeText),
  23. Column('apikey', types.UnicodeText, default=_types.make_uuid),
  24. Column('created', types.DateTime, default=datetime.datetime.now),
  25. Column('reset_key', types.UnicodeText),
  26. Column('about', types.UnicodeText),
  27. Column('activity_streams_email_notifications', types.Boolean,
  28. default=False),
  29. Column('sysadmin', types.Boolean, default=False),
  30. )
  31. vdm.sqlalchemy.make_table_stateful(user_table)
  32. class User(vdm.sqlalchemy.StatefulObjectMixin,
  33. domain_object.DomainObject):
  34. VALID_NAME = re.compile(r"^[a-zA-Z0-9_\-]{3,255}$")
  35. DOUBLE_SLASH = re.compile(':\/([^/])')
  36. @classmethod
  37. def by_openid(cls, openid):
  38. obj = meta.Session.query(cls).autoflush(False)
  39. return obj.filter_by(openid=openid).first()
  40. @classmethod
  41. def by_email(cls, email):
  42. return meta.Session.query(cls).filter_by(email=email).all()
  43. @classmethod
  44. def get(cls, user_reference):
  45. query = meta.Session.query(cls).autoflush(False)
  46. query = query.filter(or_(cls.name == user_reference,
  47. cls.id == user_reference))
  48. return query.first()
  49. @classmethod
  50. def all(cls):
  51. '''Return all users in this CKAN instance.
  52. :rtype: list of ckan.model.user.User objects
  53. '''
  54. q = meta.Session.query(cls)
  55. return q.all()
  56. @property
  57. def display_name(self):
  58. if self.fullname is not None and len(self.fullname.strip()) > 0:
  59. return self.fullname
  60. return self.name
  61. @property
  62. def email_hash(self):
  63. e = ''
  64. if self.email:
  65. e = self.email.strip().lower().encode('utf8')
  66. return md5(e).hexdigest()
  67. def get_reference_preferred_for_uri(self):
  68. '''Returns a reference (e.g. name, id, openid) for this user
  69. suitable for the user\'s URI.
  70. When there is a choice, the most preferable one will be
  71. given, based on readability. This is expected when repoze.who can
  72. give a more friendly name for an openid user.
  73. The result is not escaped (will get done in url_for/redirect_to).
  74. '''
  75. if self.name:
  76. ref = self.name
  77. elif self.openid:
  78. ref = self.openid
  79. else:
  80. ref = self.id
  81. return ref
  82. def _set_password(self, password):
  83. '''Hash using pbkdf2
  84. Use passlib to hash the password using pkbdf2, upgrading
  85. passlib will also upgrade the number of rounds and salt of the
  86. hash as the user logs in automatically. Changing hashing
  87. algorithm will require this code to be changed (perhaps using
  88. passlib's CryptContext)
  89. '''
  90. hashed_password = pbkdf2_sha512.encrypt(password)
  91. if not isinstance(hashed_password, unicode):
  92. hashed_password = hashed_password.decode('utf-8')
  93. self._password = hashed_password
  94. def _get_password(self):
  95. return self._password
  96. def _verify_and_upgrade_from_sha1(self, password):
  97. if isinstance(password, unicode):
  98. password_8bit = password.encode('ascii', 'ignore')
  99. else:
  100. password_8bit = password
  101. hashed_pass = sha1(password_8bit + self.password[:40])
  102. current_hash = passlib.utils.to_native_str(self.password[40:])
  103. if passlib.utils.consteq(hashed_pass.hexdigest(), current_hash):
  104. #we've passed the old sha1 check, upgrade our password
  105. self._set_password(password)
  106. self.save()
  107. return True
  108. else:
  109. return False
  110. def _verify_and_upgrade_pbkdf2(self, password):
  111. if pbkdf2_sha512.verify(password, self.password):
  112. self._set_password(password)
  113. self.save()
  114. return True
  115. else:
  116. return False
  117. def validate_password(self, password):
  118. '''
  119. Check the password against existing credentials.
  120. :param password: the password that was provided by the user to
  121. try and authenticate. This is the clear text version that we will
  122. need to match against the hashed one in the database.
  123. :type password: unicode object.
  124. :return: Whether the password is valid.
  125. :rtype: bool
  126. '''
  127. if not password or not self.password:
  128. return False
  129. if not pbkdf2_sha512.identify(self.password):
  130. return self._verify_and_upgrade_from_sha1(password)
  131. else:
  132. current_hash = pbkdf2_sha512.from_string(self.password)
  133. if (current_hash.rounds < pbkdf2_sha512.default_rounds or
  134. len(current_hash.salt) < pbkdf2_sha512.default_salt_size):
  135. return self._verify_and_upgrade_pbkdf2(password)
  136. else:
  137. return pbkdf2_sha512.verify(password, self.password)
  138. password = property(_get_password, _set_password)
  139. @classmethod
  140. def check_name_valid(cls, name):
  141. if not name \
  142. or not len(name.strip()) \
  143. or not cls.VALID_NAME.match(name):
  144. return False
  145. return True
  146. @classmethod
  147. def check_name_available(cls, name):
  148. return cls.by_name(name) == None
  149. def as_dict(self):
  150. _dict = domain_object.DomainObject.as_dict(self)
  151. del _dict['password']
  152. return _dict
  153. def number_of_edits(self):
  154. # have to import here to avoid circular imports
  155. import ckan.model as model
  156. revisions_q = meta.Session.query(model.Revision)
  157. revisions_q = revisions_q.filter_by(author=self.name)
  158. return revisions_q.count()
  159. def number_created_packages(self, include_private_and_draft=False):
  160. # have to import here to avoid circular imports
  161. import ckan.model as model
  162. q = meta.Session.query(model.Package)\
  163. .filter_by(creator_user_id=self.id)
  164. if include_private_and_draft:
  165. q = q.filter(model.Package.state != 'deleted')
  166. else:
  167. q = q.filter_by(state='active')\
  168. .filter_by(private=False)
  169. return q.count()
  170. def activate(self):
  171. ''' Activate the user '''
  172. self.state = core.State.ACTIVE
  173. def set_pending(self):
  174. ''' Set the user as pending '''
  175. self.state = core.State.PENDING
  176. def is_deleted(self):
  177. return self.state == core.State.DELETED
  178. def is_pending(self):
  179. return self.state == core.State.PENDING
  180. def is_in_group(self, group_id):
  181. return group_id in self.get_group_ids()
  182. def is_in_groups(self, group_ids):
  183. ''' Given a list of group ids, returns True if this user is in
  184. any of those groups '''
  185. guser = set(self.get_group_ids())
  186. gids = set(group_ids)
  187. return len(guser.intersection(gids)) > 0
  188. def get_group_ids(self, group_type=None, capacity=None):
  189. ''' Returns a list of group ids that the current user belongs to '''
  190. return [g.id for g in
  191. self.get_groups(group_type=group_type, capacity=capacity)]
  192. def get_groups(self, group_type=None, capacity=None):
  193. import ckan.model as model
  194. q = meta.Session.query(model.Group)\
  195. .join(model.Member, model.Member.group_id == model.Group.id and \
  196. model.Member.table_name == 'user').\
  197. join(model.User, model.User.id == model.Member.table_id).\
  198. filter(model.Member.state == 'active').\
  199. filter(model.Member.table_id == self.id)
  200. if capacity:
  201. q = q.filter(model.Member.capacity == capacity)
  202. return q.all()
  203. if '_groups' not in self.__dict__:
  204. self._groups = q.all()
  205. groups = self._groups
  206. if group_type:
  207. groups = [g for g in groups if g.type == group_type]
  208. return groups
  209. @classmethod
  210. def search(cls, querystr, sqlalchemy_query=None, user_name=None):
  211. '''Search name, fullname, email and openid. '''
  212. if sqlalchemy_query is None:
  213. query = meta.Session.query(cls)
  214. else:
  215. query = sqlalchemy_query
  216. qstr = '%' + querystr + '%'
  217. filters = [
  218. cls.name.ilike(qstr),
  219. cls.fullname.ilike(qstr),
  220. cls.openid.ilike(qstr),
  221. ]
  222. # sysadmins can search on user emails
  223. import ckan.authz as authz
  224. if user_name and authz.is_sysadmin(user_name):
  225. filters.append(cls.email.ilike(qstr))
  226. query = query.filter(or_(*filters))
  227. return query
  228. @classmethod
  229. def user_ids_for_name_or_id(self, user_list=[]):
  230. '''
  231. This function returns a list of ids from an input that can be a list of
  232. names or ids
  233. '''
  234. query = meta.Session.query(self.id)
  235. query = query.filter(or_(self.name.in_(user_list),
  236. self.id.in_(user_list)))
  237. return [user.id for user in query.all()]
  238. meta.mapper(User, user_table,
  239. properties={'password': synonym('_password', map_column=True)},
  240. order_by=user_table.c.name)