Browse Source

Merge pull request #19 from tracim/feature/699_endpoint_to_delete_user

Bastien Sevajol 6 years ago
parent
commit
a6eabea2d8
No account linked to committer's email

+ 4 - 0
backend/README.md View File

77
 
77
 
78
     tracimcli db init
78
     tracimcli db init
79
 
79
 
80
+Stamp current version of database to last (useful for migration):
81
+
82
+    alembic -c development.ini stamp head
83
+
80
 create wsgidav configuration file for webdav:
84
 create wsgidav configuration file for webdav:
81
 
85
 
82
     cp wsgidav.conf.sample wsgidav.conf
86
     cp wsgidav.conf.sample wsgidav.conf

+ 4 - 0
backend/doc/migration.md View File

28
 
28
 
29
     alembic -c development.ini current
29
     alembic -c development.ini current
30
 
30
 
31
+## Set Alembic stamp to last version (first time use) ##
32
+
33
+    alembic -c development.ini stamp head
34
+
31
 ### Creating new schema migration ###
35
 ### Creating new schema migration ###
32
 
36
 
33
 This creates a new auto-generated python migration file 
37
 This creates a new auto-generated python migration file 

+ 2 - 1
backend/doc/roles.md View File

23
 | delete workspace              |  no         | yes, if manager of a given workspace         | yes     |
23
 | delete workspace              |  no         | yes, if manager of a given workspace         | yes     |
24
 |-------------------------------|-------------|-------------|---------|
24
 |-------------------------------|-------------|-------------|---------|
25
 | set user global profile rights|  no         | no          | yes     |
25
 | set user global profile rights|  no         | no          | yes     |
26
-| deactivate user               |  no         | no          | yes     |
26
+| activate/deactivate user      |  no         | no          | yes     |
27
+| delete user/ undelete user    |  no         | no          | yes     |
27
 |-------------------------------|-------------|-------------|---------|
28
 |-------------------------------|-------------|-------------|---------|
28
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
29
 | access to all user data (/users/{user_id} endpoints) |personal-only|personal-only| yes     |
29
 
30
 

+ 16 - 1
backend/tracim_backend/lib/core/user.py View File

34
             current_user: typing.Optional[User],
34
             current_user: typing.Optional[User],
35
             session: Session,
35
             session: Session,
36
             config: CFG,
36
             config: CFG,
37
+            show_deleted: bool = False,
37
     ) -> None:
38
     ) -> None:
38
         self._session = session
39
         self._session = session
39
         self._user = current_user
40
         self._user = current_user
40
         self._config = config
41
         self._config = config
42
+        self._show_deleted = show_deleted
41
 
43
 
42
     def _base_query(self):
44
     def _base_query(self):
43
-        return self._session.query(User)
45
+        query = self._session.query(User)
46
+        if not self._show_deleted:
47
+            query = query.filter(User.is_deleted == False)
48
+        return query
44
 
49
 
45
     def get_user_with_context(self, user: User) -> UserInContext:
50
     def get_user_with_context(self, user: User) -> UserInContext:
46
         """
51
         """
382
         if do_save:
387
         if do_save:
383
             self.save(user)
388
             self.save(user)
384
 
389
 
390
+    def delete(self, user: User, do_save=False):
391
+        user.is_deleted = True
392
+        if do_save:
393
+            self.save(user)
394
+
395
+    def undelete(self, user: User, do_save=False):
396
+        user.is_deleted = False
397
+        if do_save:
398
+            self.save(user)
399
+
385
     def save(self, user: User):
400
     def save(self, user: User):
386
         self._session.flush()
401
         self._session.flush()
387
 
402
 

+ 1 - 1
backend/tracim_backend/lib/utils/request.py View File

293
         :return: user found from header/body
293
         :return: user found from header/body
294
         """
294
         """
295
         app_config = request.registry.settings['CFG']
295
         app_config = request.registry.settings['CFG']
296
-        uapi = UserApi(None, session=request.dbsession, config=app_config)
296
+        uapi = UserApi(None, show_deleted=True, session=request.dbsession, config=app_config)
297
         login = ''
297
         login = ''
298
         try:
298
         try:
299
             login = None
299
             login = None

+ 26 - 0
backend/tracim_backend/migration/versions/78b52ca39419_add_is_deleted_to_user.py View File

1
+"""add_is_deleted_to_user
2
+
3
+Revision ID: 78b52ca39419
4
+Revises: ad79f58ec2bf
5
+Create Date: 2018-08-09 15:50:49.656925
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = '78b52ca39419'
11
+down_revision = 'ad79f58ec2bf'
12
+
13
+from alembic import op
14
+import sqlalchemy as sa
15
+
16
+
17
+def upgrade():
18
+    # ### commands auto generated by Alembic - please adjust! ###
19
+    op.add_column('users', sa.Column('is_deleted', sa.Boolean(), server_default=sa.sql.expression.literal(False), nullable=False))
20
+    # ### end Alembic commands ###
21
+
22
+
23
+def downgrade():
24
+    # ### commands auto generated by Alembic - please adjust! ###
25
+    op.drop_column('users', 'is_deleted')
26
+    # ### end Alembic commands ###

+ 2 - 0
backend/tracim_backend/models/auth.py View File

15
 from hashlib import sha256
15
 from hashlib import sha256
16
 from typing import TYPE_CHECKING
16
 from typing import TYPE_CHECKING
17
 
17
 
18
+import sqlalchemy
18
 from sqlalchemy import Column
19
 from sqlalchemy import Column
19
 from sqlalchemy import ForeignKey
20
 from sqlalchemy import ForeignKey
20
 from sqlalchemy import Sequence
21
 from sqlalchemy import Sequence
135
     _password = Column('password', Unicode(128))
136
     _password = Column('password', Unicode(128))
136
     created = Column(DateTime, default=datetime.utcnow)
137
     created = Column(DateTime, default=datetime.utcnow)
137
     is_active = Column(Boolean, default=True, nullable=False)
138
     is_active = Column(Boolean, default=True, nullable=False)
139
+    is_deleted = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.sql.expression.literal(False))
138
     imported_from = Column(Unicode(32), nullable=True)
140
     imported_from = Column(Unicode(32), nullable=True)
139
     timezone = Column(Unicode(255), nullable=False, server_default='')
141
     timezone = Column(Unicode(255), nullable=False, server_default='')
140
     # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed
142
     # TODO - G.M - 04-04-2018 - [auth] Check if this is already needed

+ 4 - 0
backend/tracim_backend/models/context_models.py View File

406
     def profile(self) -> Profile:
406
     def profile(self) -> Profile:
407
         return self.user.profile.name
407
         return self.user.profile.name
408
 
408
 
409
+    @property
410
+    def is_deleted(self) -> bool:
411
+        return self.user.is_deleted
412
+
409
     # Context related
413
     # Context related
410
 
414
 
411
     @property
415
     @property

+ 48 - 0
backend/tracim_backend/tests/functional/test_user.py View File

2533
         assert res['email'] == 'test@test.test'
2533
         assert res['email'] == 'test@test.test'
2534
         assert res['public_name'] == 'bob'
2534
         assert res['public_name'] == 'bob'
2535
         assert res['timezone'] == 'Europe/Paris'
2535
         assert res['timezone'] == 'Europe/Paris'
2536
+        assert res['is_deleted'] is False
2536
 
2537
 
2537
     def test_api__get_user__ok_200__user_itself(self):
2538
     def test_api__get_user__ok_200__user_itself(self):
2538
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2539
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2582
         assert res['email'] == 'test@test.test'
2583
         assert res['email'] == 'test@test.test'
2583
         assert res['public_name'] == 'bob'
2584
         assert res['public_name'] == 'bob'
2584
         assert res['timezone'] == 'Europe/Paris'
2585
         assert res['timezone'] == 'Europe/Paris'
2586
+        assert res['is_deleted'] is False
2585
 
2587
 
2586
     def test_api__get_user__err_403__other_normal_user(self):
2588
     def test_api__get_user__err_403__other_normal_user(self):
2587
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2589
         dbsession = get_tm_session(self.session_factory, transaction.manager)
2634
             status=403
2636
             status=403
2635
         )
2637
         )
2636
 
2638
 
2639
+    def test_api_delete_user__ok_200__admin(self):
2640
+        dbsession = get_tm_session(self.session_factory, transaction.manager)
2641
+        admin = dbsession.query(models.User) \
2642
+            .filter(models.User.email == 'admin@admin.admin') \
2643
+            .one()
2644
+        uapi = UserApi(
2645
+            current_user=admin,
2646
+            session=dbsession,
2647
+            config=self.app_config,
2648
+        )
2649
+        gapi = GroupApi(
2650
+            current_user=admin,
2651
+            session=dbsession,
2652
+            config=self.app_config,
2653
+        )
2654
+        groups = [gapi.get_one_with_name('users')]
2655
+        test_user = uapi.create_user(
2656
+            email='test@test.test',
2657
+            password='pass',
2658
+            name='bob',
2659
+            groups=groups,
2660
+            timezone='Europe/Paris',
2661
+            do_save=True,
2662
+            do_notify=False,
2663
+        )
2664
+        uapi.save(test_user)
2665
+        transaction.commit()
2666
+        user_id = int(test_user.user_id)
2667
+
2668
+        self.testapp.authorization = (
2669
+            'Basic',
2670
+            (
2671
+                'admin@admin.admin',
2672
+                'admin@admin.admin'
2673
+            )
2674
+        )
2675
+        self.testapp.put(
2676
+            '/api/v2/users/{}/delete'.format(user_id),
2677
+            status=204
2678
+        )
2679
+        res = self.testapp.get(
2680
+            '/api/v2/users/{}'.format(user_id),
2681
+            status=200
2682
+        ).json_body
2683
+        assert res['is_deleted'] is True
2684
+
2637
 
2685
 
2638
 class TestUsersEndpoint(FunctionalTest):
2686
 class TestUsersEndpoint(FunctionalTest):
2639
     # -*- coding: utf-8 -*-
2687
     # -*- coding: utf-8 -*-

+ 4 - 0
backend/tracim_backend/views/core_api/schemas.py View File

75
         example=True,
75
         example=True,
76
         description='Is user account activated ?'
76
         description='Is user account activated ?'
77
     )
77
     )
78
+    is_deleted = marshmallow.fields.Bool(
79
+        example=False,
80
+        description='Is user account deleted ?'
81
+    )
78
     # TODO - G.M - 17-04-2018 - Restrict timezone values
82
     # TODO - G.M - 17-04-2018 - Restrict timezone values
79
     timezone = marshmallow.fields.String(
83
     timezone = marshmallow.fields.String(
80
         example="Europe/Paris",
84
         example="Europe/Paris",

+ 43 - 0
backend/tracim_backend/views/core_api/user_controller.py View File

244
     @require_profile(Group.TIM_ADMIN)
244
     @require_profile(Group.TIM_ADMIN)
245
     @hapic.input_path(UserIdPathSchema())
245
     @hapic.input_path(UserIdPathSchema())
246
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
246
     @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
247
+    def delete_user(self, context, request: TracimRequest, hapic_data=None):
248
+        """
249
+        delete user
250
+        """
251
+        app_config = request.registry.settings['CFG']
252
+        uapi = UserApi(
253
+            current_user=request.current_user,  # User
254
+            session=request.dbsession,
255
+            config=app_config,
256
+        )
257
+        uapi.delete(user=request.candidate_user, do_save=True)
258
+        return
259
+
260
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
261
+    @require_profile(Group.TIM_ADMIN)
262
+    @hapic.input_path(UserIdPathSchema())
263
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
264
+    def undelete_user(self, context, request: TracimRequest, hapic_data=None):
265
+        """
266
+        undelete user
267
+        """
268
+        app_config = request.registry.settings['CFG']
269
+        uapi = UserApi(
270
+            current_user=request.current_user,  # User
271
+            session=request.dbsession,
272
+            config=app_config,
273
+            show_deleted=True,
274
+        )
275
+        uapi.undelete(user=request.candidate_user, do_save=True)
276
+        return
277
+
278
+    @hapic.with_api_doc(tags=[SWAGGER_TAG__USER_ENDPOINTS])
279
+    @require_profile(Group.TIM_ADMIN)
280
+    @hapic.input_path(UserIdPathSchema())
281
+    @hapic.output_body(NoContentSchema(), default_http_code=HTTPStatus.NO_CONTENT)  # nopep8
247
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
282
     def disable_user(self, context, request: TracimRequest, hapic_data=None):
248
         """
283
         """
249
         disable user
284
         disable user
464
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
499
         configurator.add_route('disable_user', '/users/{user_id}/disable', request_method='PUT')  # nopep8
465
         configurator.add_view(self.disable_user, route_name='disable_user')
500
         configurator.add_view(self.disable_user, route_name='disable_user')
466
 
501
 
502
+        # delete user
503
+        configurator.add_route('delete_user', '/users/{user_id}/delete', request_method='PUT')  # nopep8
504
+        configurator.add_view(self.delete_user, route_name='delete_user')
505
+
506
+        # undelete user
507
+        configurator.add_route('undelete_user', '/users/{user_id}/undelete', request_method='PUT')  # nopep8
508
+        configurator.add_view(self.undelete_user, route_name='undelete_user')
509
+
467
         # set user profile
510
         # set user profile
468
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
511
         configurator.add_route('set_user_profile', '/users/{user_id}/profile', request_method='PUT')  # nopep8
469
         configurator.add_view(self.set_profile, route_name='set_user_profile')
512
         configurator.add_view(self.set_profile, route_name='set_user_profile')