Quellcode durchsuchen

add SQLalchemy model file (not integrated)

Guénaël Muller vor 7 Jahren
Ursprung
Commit
2485a88049
3 geänderte Dateien mit 1799 neuen und 0 gelöschten Zeilen
  1. 315 0
      tracim/models/auth.py
  2. 1431 0
      tracim/models/data.py
  3. 53 0
      tracim/models/organisational.py

+ 315 - 0
tracim/models/auth.py Datei anzeigen

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

Datei-Diff unterdrückt, da er zu groß ist
+ 1431 - 0
tracim/models/data.py


+ 53 - 0
tracim/models/organisational.py Datei anzeigen

1
+from tracim.model import User
2
+from tracim.model.data import UserRoleInWorkspace
3
+
4
+
5
+CALENDAR_PERMISSION_READ = 'r'
6
+CALENDAR_PERMISSION_WRITE = 'w'
7
+
8
+
9
+class Calendar(object):
10
+    def __init__(self, related_object, path):
11
+        self._related_object = related_object
12
+        self._path = path
13
+
14
+    @property
15
+    def related_object(self):
16
+        return self._related_object
17
+
18
+    def user_can_read(self, user: User) -> bool:
19
+        raise NotImplementedError()
20
+
21
+    def user_can_write(self, user: User) -> bool:
22
+        raise NotImplementedError()
23
+
24
+
25
+class UserCalendar(Calendar):
26
+    def user_can_write(self, user: User) -> bool:
27
+        return self._related_object.user_id == user.user_id
28
+
29
+    def user_can_read(self, user: User) -> bool:
30
+        return self._related_object.user_id == user.user_id
31
+
32
+
33
+class WorkspaceCalendar(Calendar):
34
+    _workspace_rights = {
35
+        UserRoleInWorkspace.NOT_APPLICABLE:
36
+            [],
37
+        UserRoleInWorkspace.READER:
38
+            [CALENDAR_PERMISSION_READ],
39
+        UserRoleInWorkspace.CONTRIBUTOR:
40
+            [CALENDAR_PERMISSION_READ, CALENDAR_PERMISSION_WRITE],
41
+        UserRoleInWorkspace.CONTENT_MANAGER:
42
+            [CALENDAR_PERMISSION_READ, CALENDAR_PERMISSION_WRITE],
43
+        UserRoleInWorkspace.WORKSPACE_MANAGER:
44
+            [CALENDAR_PERMISSION_READ, CALENDAR_PERMISSION_WRITE],
45
+    }
46
+
47
+    def user_can_write(self, user: User) -> bool:
48
+        role = user.get_role(self._related_object)
49
+        return CALENDAR_PERMISSION_WRITE in self._workspace_rights[role]
50
+
51
+    def user_can_read(self, user: User) -> bool:
52
+        role = user.get_role(self._related_object)
53
+        return CALENDAR_PERMISSION_READ in self._workspace_rights[role]