tracim.models.auth.User.email_address()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 3
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nop 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
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 sha256
16
from typing import TYPE_CHECKING
17
18
from sqlalchemy import Column
19
from sqlalchemy import ForeignKey
20
from sqlalchemy import Sequence
21
from sqlalchemy import Table
22
from sqlalchemy.ext.hybrid import hybrid_property
23
from sqlalchemy.orm import relation
24
from sqlalchemy.orm import relationship
25
from sqlalchemy.orm import synonym
26
from sqlalchemy.types import Boolean
27
from sqlalchemy.types import DateTime
28
from sqlalchemy.types import Integer
29
from sqlalchemy.types import Unicode
30
31
from tracim.lib.utils.translation import fake_translator as l_
0 ignored issues
show
Unused Code introduced by
Unused fake_translator imported from tracim.lib.utils.translation as l_
Loading history...
32
from tracim.models.meta import DeclarativeBase
33
from tracim.models.meta import metadata
34
if TYPE_CHECKING:
35
    from tracim.models.data import Workspace
0 ignored issues
show
Unused Code introduced by
Unused Workspace imported from tracim.models.data
Loading history...
36
    from tracim.models.data import UserRoleInWorkspace
37
__all__ = ['User', 'Group', 'Permission']
38
39
# This is the association table for the many-to-many relationship between
40
# groups and permissions.
41
group_permission_table = Table('group_permission', metadata,
0 ignored issues
show
Coding Style Naming introduced by
The name group_permission_table does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
42
    Column('group_id', Integer, ForeignKey('groups.group_id',
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 27 spaces).
Loading history...
43
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 35 spaces).
Loading history...
44
    Column('permission_id', Integer, ForeignKey('permissions.permission_id',
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 27 spaces).
Loading history...
45
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 40 spaces).
Loading history...
46
)
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 30 spaces).
Loading history...
47
48
# This is the association table for the many-to-many relationship between
49
# groups and members - this is, the memberships.
50
user_group_table = Table('user_group', metadata,
0 ignored issues
show
Coding Style Naming introduced by
The name user_group_table does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
51
    Column('user_id', Integer, ForeignKey('users.user_id',
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 21 spaces).
Loading history...
52
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True),
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 34 spaces).
Loading history...
53
    Column('group_id', Integer, ForeignKey('groups.group_id',
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 21 spaces).
Loading history...
54
        onupdate="CASCADE", ondelete="CASCADE"), primary_key=True)
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 35 spaces).
Loading history...
55
)
0 ignored issues
show
Coding Style introduced by
Wrong continued indentation (add 24 spaces).
Loading history...
56
57
58
class Group(DeclarativeBase):
0 ignored issues
show
Coding Style introduced by
This class should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
59
60
    TIM_NOBODY = 0
61
    TIM_USER = 1
62
    TIM_MANAGER = 2
63
    TIM_ADMIN = 3
64
65
    TIM_NOBODY_GROUPNAME = 'nobody'
66
    TIM_USER_GROUPNAME = 'users'
67
    TIM_MANAGER_GROUPNAME = 'managers'
68
    TIM_ADMIN_GROUPNAME = 'administrators'
69
70
    __tablename__ = 'groups'
71
72
    group_id = Column(Integer, Sequence('seq__groups__group_id'), autoincrement=True, primary_key=True)
0 ignored issues
show
Coding Style introduced by
This line is too long as per the coding-style (103/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
73
    group_name = Column(Unicode(16), unique=True, nullable=False)
74
    display_name = Column(Unicode(255))
75
    created = Column(DateTime, default=datetime.utcnow)
76
77
    users = relationship('User', secondary=user_group_table, backref='groups')
78
79
    def __repr__(self):
80
        return '<Group: name=%s>' % repr(self.group_name)
81
82
    def __unicode__(self):
83
        return self.group_name
84
85
    @classmethod
86
    def by_group_name(cls, group_name, dbsession):
87
        """Return the user object whose email address is ``email``."""
88
        return dbsession.query(cls).filter_by(group_name=group_name).first()
89
90
91
class Profile(object):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
92
    """This model is the "max" group associated to a given user."""
93
94
    _NAME = [Group.TIM_NOBODY_GROUPNAME,
95
             Group.TIM_USER_GROUPNAME,
96
             Group.TIM_MANAGER_GROUPNAME,
97
             Group.TIM_ADMIN_GROUPNAME]
98
99
    # TODO - G.M - 18-04-2018 [Cleanup] Drop this
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
100
    # _LABEL = [l_('Nobody'),
101
    #           l_('Users'),
102
    #           l_('Global managers'),
103
    #           l_('Administrators')]
104
105
    def __init__(self, profile_id):
106
        assert isinstance(profile_id, int)
107
        self.id = profile_id
0 ignored issues
show
Coding Style Naming introduced by
The name id does not conform to the attribute naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
108
        self.name = Profile._NAME[profile_id]
109
        # TODO - G.M - 18-04-2018 [Cleanup] Drop this
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
110
        # self.label = Profile._LABEL[profile_id]
111
112
113
class User(DeclarativeBase):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
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
    # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
132
    # with new auth system
133
    auth_token = Column(Unicode(255))
134
    auth_token_created = Column(DateTime)
135
136
    @hybrid_property
137
    def email_address(self):
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
138
        return self.email
139
140
    def __repr__(self):
141
        return '<User: email=%s, display=%s>' % (
142
                repr(self.email), repr(self.display_name))
0 ignored issues
show
Coding Style introduced by
Wrong hanging indentation (remove 4 spaces).
Loading history...
143
144
    def __unicode__(self):
145
        return self.display_name or self.email
146
147
    @property
148
    def permissions(self):
149
        """Return a set with all permissions granted to the user."""
150
        perms = set()
151
        for g in self.groups:
0 ignored issues
show
Coding Style Naming introduced by
The name g does not conform to the variable naming conventions ((([a-z][a-z0-9_]{2,30})|(_[a-z0-9_]*))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
152
            perms = perms | set(g.permissions)
153
        return perms
154
155
    @property
156
    def profile(self) -> Profile:
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
157
        profile_id = 0
158
        if len(self.groups) > 0:
0 ignored issues
show
Unused Code introduced by
Do not use len(SEQUENCE) as condition value
Loading history...
159
            profile_id = max(group.group_id for group in self.groups)
160
        return Profile(profile_id)
161
162
    # TODO - G-M - 27-03-2018 - [Calendar] Check about calendar code
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
163
    # @property
164
    # def calendar_url(self) -> str:
165
    #     # TODO - 20160531 - Bastien: Cyclic import if import in top of file
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
166
    #     from tracim.lib.calendar import CalendarManager
167
    #     calendar_manager = CalendarManager(None)
168
    #
169
    #     return calendar_manager.get_user_calendar_url(self.user_id)
170
171
    @classmethod
172
    def by_email_address(cls, email, dbsession):
173
        """Return the user object whose email address is ``email``."""
174
        return dbsession.query(cls).filter_by(email=email).first()
175
176
    @classmethod
177
    def by_user_name(cls, username, dbsession):
178
        """Return the user object whose user name is ``username``."""
179
        return dbsession.query(cls).filter_by(email=username).first()
180
181
    @classmethod
182
    def _hash_password(cls, cleartext_password: str) -> str:
183
        salt = sha256()
184
        salt.update(os.urandom(60))
185
        salt = salt.hexdigest()
186
187
        hash = sha256()
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in hash.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
188
        # Make sure password is a str because we cannot hash unicode objects
189
        hash.update((cleartext_password + salt).encode('utf-8'))
190
        hash = hash.hexdigest()
191
192
        ciphertext_password = salt + hash
193
194
        # Make sure the hashed password is a unicode object at the end of the
195
        # process because SQLAlchemy _wants_ unicode objects for Unicode cols
196
        # FIXME - D.A. - 2013-11-20 - The following line has been removed since using python3. Is this normal ?!
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
Coding Style introduced by
This line is too long as per the coding-style (112/100).

This check looks for lines that are too long. You can specify the maximum line length.

Loading history...
197
        # password = password.decode('utf-8')
198
199
        return ciphertext_password
200
201
    def _set_password(self, cleartext_password: str) -> None:
202
        """
203
        Set ciphertext password from cleartext password.
204
205
        Hash cleartext password on the fly,
206
        Store its ciphertext version,
207
        """
208
        self._password = self._hash_password(cleartext_password)
209
210
    def _get_password(self) -> str:
211
        """Return the hashed version of the password."""
212
        return self._password
213
214
    password = synonym('_password', descriptor=property(_get_password,
215
                                                        _set_password))
216
217
    def validate_password(self, cleartext_password: str) -> bool:
218
        """
219
        Check the password against existing credentials.
220
221
        :param cleartext_password: the password that was provided by the user
222
            to try and authenticate. This is the clear text version that we
223
            will need to match against the hashed one in the database.
224
        :type cleartext_password: unicode object.
225
        :return: Whether the password is valid.
226
        :rtype: bool
227
228
        """
229
        result = False
230
        if self.password:
231
            hash = sha256()
0 ignored issues
show
Bug Best Practice introduced by
This seems to re-define the built-in hash.

It is generally discouraged to redefine built-ins as this makes code very hard to read.

Loading history...
232
            hash.update((cleartext_password + self.password[:64]).encode('utf-8'))
233
            result = self.password[64:] == hash.hexdigest()
234
        return result
235
236
    def get_display_name(self, remove_email_part: bool=False) -> str:
0 ignored issues
show
Coding Style introduced by
Exactly one space required around keyword argument assignment
Loading history...
237
        """
238
        Get a name to display from corresponding member or email.
239
240
        :param remove_email_part: If True and display name based on email,
241
            remove @xxx.xxx part of email in returned value
242
        :return: display name based on user name or email.
243
        """
244
        if self.display_name:
245
            return self.display_name
246
        else:
247
            if remove_email_part:
248
                at_pos = self.email.index('@')
249
                return self.email[0:at_pos]
250
            return self.email
251
252
    def get_role(self, workspace: 'Workspace') -> int:
0 ignored issues
show
Coding Style introduced by
This method should have a docstring.

The coding style of this project requires that you add a docstring to this code element. Below, you find an example for methods:

class SomeClass:
    def some_method(self):
        """Do x and return foo."""

If you would like to know more about docstrings, we recommend to read PEP-257: Docstring Conventions.

Loading history...
253
        for role in self.roles:
254
            if role.workspace == workspace:
255
                return role.role
256
257
        return UserRoleInWorkspace.NOT_APPLICABLE
0 ignored issues
show
introduced by
The variable UserRoleInWorkspace does not seem to be defined in case TYPE_CHECKING on line 34 is False. Are you sure this can never be the case?
Loading history...
258
259
    def get_active_roles(self) -> ['UserRoleInWorkspace']:
260
        """
261
        :return: list of roles of the user for all not-deleted workspaces
262
        """
263
        roles = []
264
        for role in self.roles:
265
            if not role.workspace.is_deleted:
266
                roles.append(role)
267
        return roles
268
269
    # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed
0 ignored issues
show
Coding Style introduced by
TODO and FIXME comments should generally be avoided.
Loading history...
270
    # with new auth system
271
    def ensure_auth_token(self, validity_seconds, session) -> None:
272
        """
273
        Create auth_token if None, regenerate auth_token if too much old.
274
275
        auth_token validity is set in
276
        :return:
277
        """
278
279
        if not self.auth_token or not self.auth_token_created:
280
            self.auth_token = str(uuid.uuid4())
281
            self.auth_token_created = datetime.utcnow()
282
            session.flush()
283
            return
284
285
        now_seconds = time.mktime(datetime.utcnow().timetuple())
286
        auth_token_seconds = time.mktime(self.auth_token_created.timetuple())
287
        difference = now_seconds - auth_token_seconds
288
289
        if difference > validity_seconds:
290
            self.auth_token = str(uuid.uuid4())
291
            self.auth_token_created = datetime.utcnow()
292
            session.flush()
293
294
295
class Permission(DeclarativeBase):
0 ignored issues
show
Unused Code introduced by
The variable __class__ seems to be unused.
Loading history...
296
    """
297
    Permission definition.
298
299
    Only the ``permission_name`` column is required.
300
301
    """
302
303
    __tablename__ = 'permissions'
304
305
    permission_id = Column(
306
        Integer,
307
        Sequence('seq__permissions__permission_id'),
308
        autoincrement=True,
309
        primary_key=True
310
    )
311
    permission_name = Column(Unicode(63), unique=True, nullable=False)
312
    description = Column(Unicode(255))
313
314
    groups = relation(Group, secondary=group_permission_table,
315
                      backref='permissions')
316
317
    def __repr__(self):
318
        return '<Permission: name=%s>' % repr(self.permission_name)
319
320
    def __unicode__(self):
321
        return self.permission_name
322