auth.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. # -*- coding: utf-8 -*-
  2. """
  3. Auth* related model.
  4. This is where the models used by the authentication stack are defined.
  5. It's perfectly fine to re-use this definition in the tracim application,
  6. though.
  7. """
  8. import os
  9. import time
  10. import uuid
  11. from datetime import datetime
  12. from hashlib import sha256
  13. from typing import TYPE_CHECKING
  14. import sqlalchemy
  15. from sqlalchemy import Column
  16. from sqlalchemy import ForeignKey
  17. from sqlalchemy import Sequence
  18. from sqlalchemy import Table
  19. from sqlalchemy.ext.hybrid import hybrid_property
  20. from sqlalchemy.orm import relation
  21. from sqlalchemy.orm import relationship
  22. from sqlalchemy.orm import synonym
  23. from sqlalchemy.types import Boolean
  24. from sqlalchemy.types import DateTime
  25. from sqlalchemy.types import Integer
  26. from sqlalchemy.types import Unicode
  27. from tracim_backend.lib.utils.translation import fake_translator as l_
  28. from tracim_backend.models.meta import DeclarativeBase
  29. from tracim_backend.models.meta import metadata
  30. if TYPE_CHECKING:
  31. from tracim_backend.models.data import Workspace
  32. from tracim_backend.models.data import UserRoleInWorkspace
  33. __all__ = ['User', 'Group', 'Permission']
  34. # This is the association table for the many-to-many relationship between
  35. # groups and permissions.
  36. group_permission_table = Table('group_permission', metadata,
  37. Column('group_id', Integer, ForeignKey('groups.group_id',
  38. onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
  39. Column('permission_id', Integer, ForeignKey('permissions.permission_id',
  40. onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
  41. )
  42. # This is the association table for the many-to-many relationship between
  43. # groups and members - this is, the memberships.
  44. user_group_table = Table('user_group', metadata,
  45. Column('user_id', Integer, ForeignKey('users.user_id',
  46. onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
  47. Column('group_id', Integer, ForeignKey('groups.group_id',
  48. onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
  49. )
  50. class Group(DeclarativeBase):
  51. TIM_NOBODY = 0
  52. TIM_USER = 1
  53. TIM_MANAGER = 2
  54. TIM_ADMIN = 3
  55. TIM_NOBODY_GROUPNAME = 'nobody'
  56. TIM_USER_GROUPNAME = 'users'
  57. TIM_MANAGER_GROUPNAME = 'managers'
  58. TIM_ADMIN_GROUPNAME = 'administrators'
  59. __tablename__ = 'groups'
  60. group_id = Column(Integer, Sequence('seq__groups__group_id'), autoincrement=True, primary_key=True)
  61. group_name = Column(Unicode(16), unique=True, nullable=False)
  62. display_name = Column(Unicode(255))
  63. created = Column(DateTime, default=datetime.utcnow)
  64. users = relationship('User', secondary=user_group_table, backref='groups')
  65. def __repr__(self):
  66. return '<Group: name=%s>' % repr(self.group_name)
  67. def __unicode__(self):
  68. return self.group_name
  69. @classmethod
  70. def by_group_name(cls, group_name, dbsession):
  71. """Return the user object whose email address is ``email``."""
  72. return dbsession.query(cls).filter_by(group_name=group_name).first()
  73. class Profile(object):
  74. """This model is the "max" group associated to a given user."""
  75. _NAME = [
  76. Group.TIM_NOBODY_GROUPNAME,
  77. Group.TIM_USER_GROUPNAME,
  78. Group.TIM_MANAGER_GROUPNAME,
  79. Group.TIM_ADMIN_GROUPNAME,
  80. ]
  81. _IDS = [
  82. Group.TIM_NOBODY,
  83. Group.TIM_USER,
  84. Group.TIM_MANAGER,
  85. Group.TIM_ADMIN,
  86. ]
  87. # TODO - G.M - 18-04-2018 [Cleanup] Drop this
  88. # _LABEL = [l_('Nobody'),
  89. # l_('Users'),
  90. # l_('Global managers'),
  91. # l_('Administrators')]
  92. def __init__(self, profile_id):
  93. assert isinstance(profile_id, int)
  94. self.id = profile_id
  95. self.name = Profile._NAME[profile_id]
  96. # TODO - G.M - 18-04-2018 [Cleanup] Drop this
  97. # self.label = Profile._LABEL[profile_id]
  98. class User(DeclarativeBase):
  99. """
  100. User definition.
  101. This is the user definition used by :mod:`repoze.who`, which requires at
  102. least the ``email`` column.
  103. """
  104. __tablename__ = 'users'
  105. user_id = Column(Integer, Sequence('seq__users__user_id'), autoincrement=True, primary_key=True)
  106. email = Column(Unicode(255), unique=True, nullable=False)
  107. display_name = Column(Unicode(255))
  108. _password = Column('password', Unicode(128))
  109. created = Column(DateTime, default=datetime.utcnow)
  110. is_active = Column(Boolean, default=True, nullable=False)
  111. is_deleted = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.sql.expression.literal(False))
  112. imported_from = Column(Unicode(32), nullable=True)
  113. # timezone as tz database format
  114. timezone = Column(Unicode(255), nullable=False, server_default='')
  115. # lang in iso639 format
  116. lang = Column(Unicode(3), nullable=True, default=None)
  117. # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed
  118. # with new auth system
  119. auth_token = Column(Unicode(255))
  120. auth_token_created = Column(DateTime)
  121. @hybrid_property
  122. def email_address(self):
  123. return self.email
  124. def __repr__(self):
  125. return '<User: email=%s, display=%s>' % (
  126. repr(self.email), repr(self.display_name))
  127. def __unicode__(self):
  128. return self.display_name or self.email
  129. @property
  130. def permissions(self):
  131. """Return a set with all permissions granted to the user."""
  132. perms = set()
  133. for g in self.groups:
  134. perms = perms | set(g.permissions)
  135. return perms
  136. @property
  137. def profile(self) -> Profile:
  138. profile_id = 0
  139. if len(self.groups) > 0:
  140. profile_id = max(group.group_id for group in self.groups)
  141. return Profile(profile_id)
  142. # TODO - G-M - 20-04-2018 - [Calendar] Replace this in context model object
  143. # @property
  144. # def calendar_url(self) -> str:
  145. # # TODO - 20160531 - Bastien: Cyclic import if import in top of file
  146. # from tracim.lib.calendar import CalendarManager
  147. # calendar_manager = CalendarManager(None)
  148. #
  149. # return calendar_manager.get_user_calendar_url(self.user_id)
  150. @classmethod
  151. def by_email_address(cls, email, dbsession):
  152. """Return the user object whose email address is ``email``."""
  153. return dbsession.query(cls).filter_by(email=email).first()
  154. @classmethod
  155. def by_user_name(cls, username, dbsession):
  156. """Return the user object whose user name is ``username``."""
  157. return dbsession.query(cls).filter_by(email=username).first()
  158. @classmethod
  159. def _hash_password(cls, cleartext_password: str) -> str:
  160. salt = sha256()
  161. salt.update(os.urandom(60))
  162. salt = salt.hexdigest()
  163. hash = sha256()
  164. # Make sure password is a str because we cannot hash unicode objects
  165. hash.update((cleartext_password + salt).encode('utf-8'))
  166. hash = hash.hexdigest()
  167. ciphertext_password = salt + hash
  168. # Make sure the hashed password is a unicode object at the end of the
  169. # process because SQLAlchemy _wants_ unicode objects for Unicode cols
  170. # FIXME - D.A. - 2013-11-20 - The following line has been removed since using python3. Is this normal ?!
  171. # password = password.decode('utf-8')
  172. return ciphertext_password
  173. def _set_password(self, cleartext_password: str) -> None:
  174. """
  175. Set ciphertext password from cleartext password.
  176. Hash cleartext password on the fly,
  177. Store its ciphertext version,
  178. """
  179. self._password = self._hash_password(cleartext_password)
  180. def _get_password(self) -> str:
  181. """Return the hashed version of the password."""
  182. return self._password
  183. password = synonym('_password', descriptor=property(_get_password,
  184. _set_password))
  185. def validate_password(self, cleartext_password: str) -> bool:
  186. """
  187. Check the password against existing credentials.
  188. :param cleartext_password: the password that was provided by the user
  189. to try and authenticate. This is the clear text version that we
  190. will need to match against the hashed one in the database.
  191. :type cleartext_password: unicode object.
  192. :return: Whether the password is valid.
  193. :rtype: bool
  194. """
  195. result = False
  196. if self.password:
  197. hash = sha256()
  198. hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
  199. result = self.password[64:] == hash.hexdigest()
  200. return result
  201. def get_display_name(self, remove_email_part: bool=False) -> str:
  202. """
  203. Get a name to display from corresponding member or email.
  204. :param remove_email_part: If True and display name based on email,
  205. remove @xxx.xxx part of email in returned value
  206. :return: display name based on user name or email.
  207. """
  208. if self.display_name:
  209. return self.display_name
  210. else:
  211. if remove_email_part:
  212. at_pos = self.email.index('@')
  213. return self.email[0:at_pos]
  214. return self.email
  215. def get_role(self, workspace: 'Workspace') -> int:
  216. for role in self.roles:
  217. if role.workspace == workspace:
  218. return role.role
  219. return UserRoleInWorkspace.NOT_APPLICABLE
  220. def get_active_roles(self) -> ['UserRoleInWorkspace']:
  221. """
  222. :return: list of roles of the user for all not-deleted workspaces
  223. """
  224. roles = []
  225. for role in self.roles:
  226. if not role.workspace.is_deleted:
  227. roles.append(role)
  228. return roles
  229. # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed
  230. # with new auth system
  231. def ensure_auth_token(self, validity_seconds, session) -> None:
  232. """
  233. Create auth_token if None, regenerate auth_token if too much old.
  234. auth_token validity is set in
  235. :return:
  236. """
  237. if not self.auth_token or not self.auth_token_created:
  238. self.auth_token = str(uuid.uuid4())
  239. self.auth_token_created = datetime.utcnow()
  240. session.flush()
  241. return
  242. now_seconds = time.mktime(datetime.utcnow().timetuple())
  243. auth_token_seconds = time.mktime(self.auth_token_created.timetuple())
  244. difference = now_seconds - auth_token_seconds
  245. if difference > validity_seconds:
  246. self.auth_token = str(uuid.uuid4())
  247. self.auth_token_created = datetime.utcnow()
  248. session.flush()
  249. class Permission(DeclarativeBase):
  250. """
  251. Permission definition.
  252. Only the ``permission_name`` column is required.
  253. """
  254. __tablename__ = 'permissions'
  255. permission_id = Column(
  256. Integer,
  257. Sequence('seq__permissions__permission_id'),
  258. autoincrement=True,
  259. primary_key=True
  260. )
  261. permission_name = Column(Unicode(63), unique=True, nullable=False)
  262. description = Column(Unicode(255))
  263. groups = relation(Group, secondary=group_permission_table,
  264. backref='permissions')
  265. def __repr__(self):
  266. return '<Permission: name=%s>' % repr(self.permission_name)
  267. def __unicode__(self):
  268. return self.permission_name