Browse Source

Add endpoints to create role/update role

Guénaël Muller 6 years ago
parent
commit
64b5a92d33

+ 25 - 1
tracim/lib/core/user.py View File

13
 from tracim.models.auth import User
13
 from tracim.models.auth import User
14
 from tracim.models.auth import Group
14
 from tracim.models.auth import Group
15
 from sqlalchemy.orm.exc import NoResultFound
15
 from sqlalchemy.orm.exc import NoResultFound
16
-from tracim.exceptions import WrongUserPassword, UserDoesNotExist
16
+from tracim.exceptions import UserDoesNotExist
17
+from tracim.exceptions import WrongUserPassword
17
 from tracim.exceptions import AuthenticationFailed
18
 from tracim.exceptions import AuthenticationFailed
18
 from tracim.models.context_models import UserInContext
19
 from tracim.models.context_models import UserInContext
19
 
20
 
68
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
69
             raise UserDoesNotExist('User "{}" not found in database'.format(email)) from exc  # nopep8
69
         return user
70
         return user
70
 
71
 
72
+    def get_one_by_public_name(self, public_name: str) -> User:
73
+        """
74
+        Get one user by public_name
75
+        """
76
+        try:
77
+            user = self._base_query().filter(User.display_name == public_name).one()
78
+        except NoResultFound as exc:
79
+            raise UserDoesNotExist('User "{}" not found in database'.format(public_name)) from exc  # nopep8
80
+        return user
71
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
81
     # FIXME - G.M - 24-04-2018 - Duplicate method with get_one.
82
+
72
     def get_one_by_id(self, id: int) -> User:
83
     def get_one_by_id(self, id: int) -> User:
73
         return self.get_one(user_id=id)
84
         return self.get_one(user_id=id)
74
 
85
 
83
     def get_all(self) -> typing.Iterable[User]:
94
     def get_all(self) -> typing.Iterable[User]:
84
         return self._session.query(User).order_by(User.display_name).all()
95
         return self._session.query(User).order_by(User.display_name).all()
85
 
96
 
97
+    def find_one(self, user_email_or_public_name: str) -> User:
98
+        user = None
99
+        try:
100
+            user = self.get_one_by_email(email=user_email_or_public_name)
101
+        except UserDoesNotExist:
102
+            # INFO - G.M - 2018-07-183 - we discard this exception in order
103
+            # allow secondary check (public_name)
104
+            pass
105
+
106
+        if not user:
107
+            user = self.get_one_by_public_name(user_email_or_public_name)
108
+
109
+        return user
86
     # Check methods
110
     # Check methods
87
 
111
 
88
     def user_with_email_exists(self, email: str) -> bool:
112
     def user_with_email_exists(self, email: str) -> bool:

+ 24 - 0
tracim/lib/core/userworkspace.py View File

3
 
3
 
4
 from tracim import CFG
4
 from tracim import CFG
5
 from tracim.models.context_models import UserRoleWorkspaceInContext
5
 from tracim.models.context_models import UserRoleWorkspaceInContext
6
+from tracim.models.roles import WorkspaceRoles
6
 
7
 
7
 __author__ = 'damien'
8
 __author__ = 'damien'
8
 
9
 
93
     def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
94
     def get_one(self, user_id: int, workspace_id: int) -> UserRoleInWorkspace:
94
         return self._get_one_rsc(user_id, workspace_id).one()
95
         return self._get_one_rsc(user_id, workspace_id).one()
95
 
96
 
97
+    def update_role(
98
+        self,
99
+        role: UserRoleInWorkspace,
100
+        role_level: int,
101
+        with_notif: typing.Optional[bool] = None,
102
+        save_now: bool=False,
103
+    ):
104
+        """
105
+        Update role of user in this workspace
106
+        :param role: UserRoleInWorkspace object
107
+        :param role_level: level of new role wanted
108
+        :param with_notif: is user notification enabled in this workspace ?
109
+        :param save_now: database flush
110
+        :return: updated role
111
+        """
112
+        role.role = role_level
113
+        if with_notif is not None:
114
+            role.do_notify == with_notif
115
+        if save_now:
116
+            self.save(role)
117
+
118
+        return role
119
+
96
     def create_one(
120
     def create_one(
97
         self,
121
         self,
98
         user: User,
122
         user: User,

+ 37 - 1
tracim/models/context_models.py View File

9
 from tracim.models.auth import Profile
9
 from tracim.models.auth import Profile
10
 from tracim.models.data import Content
10
 from tracim.models.data import Content
11
 from tracim.models.data import ContentRevisionRO
11
 from tracim.models.data import ContentRevisionRO
12
-from tracim.models.data import Workspace, UserRoleInWorkspace
12
+from tracim.models.data import Workspace
13
+from tracim.models.data import UserRoleInWorkspace
13
 from tracim.models.roles import WorkspaceRoles
14
 from tracim.models.roles import WorkspaceRoles
14
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
15
 from tracim.models.workspace_menu_entries import default_workspace_menu_entry
15
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
16
 from tracim.models.workspace_menu_entries import WorkspaceMenuEntry
44
         self.workspace_id = workspace_id
45
         self.workspace_id = workspace_id
45
 
46
 
46
 
47
 
48
+class WorkspaceAndUserPath(object):
49
+    """
50
+    Paths params with workspace id and user_id
51
+    """
52
+    def __init__(self, workspace_id: int, user_id: int):
53
+        self.workspace_id = workspace_id
54
+        self.user_id = workspace_id
55
+
56
+
47
 class CommentPath(object):
57
 class CommentPath(object):
48
     """
58
     """
49
     Paths params with workspace id and content_id and comment_id model
59
     Paths params with workspace id and content_id and comment_id model
76
         self.show_active = bool(show_active)
86
         self.show_active = bool(show_active)
77
 
87
 
78
 
88
 
89
+class RoleUpdate(object):
90
+    """
91
+    Update role
92
+    """
93
+    def __init__(
94
+        self,
95
+        role: str,
96
+    ):
97
+        self.role = role
98
+
99
+
100
+class WorkspaceMemberInvitation(object):
101
+    """
102
+    Workspace Member Invitation
103
+    """
104
+    def __init__(
105
+        self,
106
+        user_id: int,
107
+        user_email_or_public_name: str,
108
+        role: str,
109
+    ):
110
+        self.role = role
111
+        self.user_email_or_public_name = user_email_or_public_name
112
+        self.user_id = user_id
113
+
114
+
79
 class WorkspaceUpdate(object):
115
 class WorkspaceUpdate(object):
80
     """
116
     """
81
     Update workspace
117
     Update workspace

+ 1 - 1
tracim/models/roles.py View File

28
         """ Return valid label associated to role"""
28
         """ Return valid label associated to role"""
29
         # TODO - G.M - 2018-06-180 - Make this work correctly
29
         # TODO - G.M - 2018-06-180 - Make this work correctly
30
         return self.slug
30
         return self.slug
31
-    
31
+
32
     @classmethod
32
     @classmethod
33
     def get_all_valid_role(cls) -> typing.List['WorkspaceRoles']:
33
     def get_all_valid_role(cls) -> typing.List['WorkspaceRoles']:
34
         """
34
         """

+ 223 - 1
tracim/tests/functional/test_workspaces.py View File

307
         assert 'message' in res.json.keys()
307
         assert 'message' in res.json.keys()
308
         assert 'details' in res.json.keys()
308
         assert 'details' in res.json.keys()
309
 
309
 
310
+    def test_api__create_workspace_member_role__ok_200__user_id(self):
311
+        """
312
+        Create workspace member role
313
+        :return:
314
+        """
315
+        self.testapp.authorization = (
316
+            'Basic',
317
+            (
318
+                'admin@admin.admin',
319
+                'admin@admin.admin'
320
+            )
321
+        )
322
+        # create workspace role
323
+        params = {
324
+            'user_id': 2,
325
+            'user_email_or_public_name': None,
326
+            'role': 'content-manager',
327
+        }
328
+        res = self.testapp.post_json(
329
+            '/api/v2/workspaces/1/members',
330
+            status=200,
331
+            params=params,
332
+        )
333
+        user_role_found = res.json_body
334
+        assert user_role_found['role'] == 'content-manager'
335
+        assert user_role_found['user_id'] == 2
336
+        assert user_role_found['workspace_id'] == 1
337
+
338
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
339
+        assert len(res) == 2
340
+        user_role = res[0]
341
+        assert user_role['role'] == 'workspace-manager'
342
+        assert user_role['user_id'] == 1
343
+        assert user_role['workspace_id'] == 1
344
+        user_role = res[1]
345
+        assert user_role_found == user_role
346
+
347
+    def test_api__create_workspace_member_role__ok_200__user_email(self):
348
+        """
349
+        Create workspace member role
350
+        :return:
351
+        """
352
+        self.testapp.authorization = (
353
+            'Basic',
354
+            (
355
+                'admin@admin.admin',
356
+                'admin@admin.admin'
357
+            )
358
+        )
359
+        # create workspace role
360
+        params = {
361
+            'user_id': None,
362
+            'user_email_or_public_name': 'lawrence-not-real-email@fsf.local',
363
+            'role': 'content-manager',
364
+        }
365
+        res = self.testapp.post_json(
366
+            '/api/v2/workspaces/1/members',
367
+            status=200,
368
+            params=params,
369
+        )
370
+        user_role_found = res.json_body
371
+        assert user_role_found['role'] == 'content-manager'
372
+        assert user_role_found['user_id'] == 2
373
+        assert user_role_found['workspace_id'] == 1
374
+
375
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
376
+        assert len(res) == 2
377
+        user_role = res[0]
378
+        assert user_role['role'] == 'workspace-manager'
379
+        assert user_role['user_id'] == 1
380
+        assert user_role['workspace_id'] == 1
381
+        user_role = res[1]
382
+        assert user_role_found == user_role
383
+
384
+    def test_api__create_workspace_member_role__ok_200__user_public_name(self):
385
+        """
386
+        Create workspace member role
387
+        :return:
388
+        """
389
+        self.testapp.authorization = (
390
+            'Basic',
391
+            (
392
+                'admin@admin.admin',
393
+                'admin@admin.admin'
394
+            )
395
+        )
396
+        # create workspace role
397
+        params = {
398
+            'user_id': None,
399
+            'user_email_or_public_name': 'Lawrence L.',
400
+            'role': 'content-manager',
401
+        }
402
+        res = self.testapp.post_json(
403
+            '/api/v2/workspaces/1/members',
404
+            status=200,
405
+            params=params,
406
+        )
407
+        user_role_found = res.json_body
408
+        assert user_role_found['role'] == 'content-manager'
409
+        assert user_role_found['user_id'] == 2
410
+        assert user_role_found['workspace_id'] == 1
411
+
412
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
413
+        assert len(res) == 2
414
+        user_role = res[0]
415
+        assert user_role['role'] == 'workspace-manager'
416
+        assert user_role['user_id'] == 1
417
+        assert user_role['workspace_id'] == 1
418
+        user_role = res[1]
419
+        assert user_role_found == user_role
420
+
421
+    def test_api__create_workspace_member_role__err_400__nothing(self):
422
+        """
423
+        Create workspace member role
424
+        :return:
425
+        """
426
+        self.testapp.authorization = (
427
+            'Basic',
428
+            (
429
+                'admin@admin.admin',
430
+                'admin@admin.admin'
431
+            )
432
+        )
433
+        # create workspace role
434
+        params = {
435
+            'user_id': None,
436
+            'user_email_or_public_name': None,
437
+            'role': 'content-manager',
438
+        }
439
+        res = self.testapp.post_json(
440
+            '/api/v2/workspaces/1/members',
441
+            status=400,
442
+            params=params,
443
+        )
444
+
445
+    def test_api__create_workspace_member_role__err_400__wrong_user_id(self):
446
+        """
447
+        Create workspace member role
448
+        :return:
449
+        """
450
+        self.testapp.authorization = (
451
+            'Basic',
452
+            (
453
+                'admin@admin.admin',
454
+                'admin@admin.admin'
455
+            )
456
+        )
457
+        # create workspace role
458
+        params = {
459
+            'user_id': 47,
460
+            'user_email_or_public_name': None,
461
+            'role': 'content-manager',
462
+        }
463
+        res = self.testapp.post_json(
464
+            '/api/v2/workspaces/1/members',
465
+            status=400,
466
+            params=params,
467
+        )
468
+
469
+    def test_api__create_workspace_member_role__err_400__wrong_user_email_or_public_name(self):  # nopep8
470
+        """
471
+        Create workspace member role
472
+        :return:
473
+        """
474
+        self.testapp.authorization = (
475
+            'Basic',
476
+            (
477
+                'admin@admin.admin',
478
+                'admin@admin.admin'
479
+            )
480
+        )
481
+        # create workspace role
482
+        params = {
483
+            'user_id': None,
484
+            'user_email_or_public_name': 'nothing@nothing.nothing',
485
+            'role': 'content-manager',
486
+        }
487
+        res = self.testapp.post_json(
488
+            '/api/v2/workspaces/1/members',
489
+            status=400,
490
+            params=params,
491
+        )
492
+
493
+    def test_api__update_workspace_member_role__ok_200__nominal_case(self):
494
+        """
495
+        Update worskpace member role
496
+        """
497
+        # before
498
+        self.testapp.authorization = (
499
+            'Basic',
500
+            (
501
+                'admin@admin.admin',
502
+                'admin@admin.admin'
503
+            )
504
+        )
505
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
506
+        assert len(res) == 1
507
+        user_role = res[0]
508
+        assert user_role['role'] == 'workspace-manager'
509
+        assert user_role['user_id'] == 1
510
+        assert user_role['workspace_id'] == 1
511
+        # update workspace role
512
+        params = {
513
+            'role': 'content-manager',
514
+        }
515
+        res = self.testapp.put_json(
516
+            '/api/v2/workspaces/1/members/1',
517
+            status=200,
518
+            params=params,
519
+        )
520
+        user_role = res.json_body
521
+        assert user_role['role'] == 'content-manager'
522
+        assert user_role['user_id'] == 1
523
+        assert user_role['workspace_id'] == 1
524
+        # after
525
+        res = self.testapp.get('/api/v2/workspaces/1/members', status=200).json_body   # nopep8
526
+        assert len(res) == 1
527
+        user_role = res[0]
528
+        assert user_role['role'] == 'content-manager'
529
+        assert user_role['user_id'] == 1
530
+        assert user_role['workspace_id'] == 1
531
+
310
 
532
 
311
 class TestWorkspaceContents(FunctionalTest):
533
 class TestWorkspaceContents(FunctionalTest):
312
     """
534
     """
364
         assert content['workspace_id'] == 1
586
         assert content['workspace_id'] == 1
365
 
587
 
366
     # Root related
588
     # Root related
367
-    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):
589
+    def test_api__get_workspace_content__ok_200__get_all_root_content__legacy_html_slug(self):  # nopep8
368
         """
590
         """
369
         Check obtain workspace all root contents
591
         Check obtain workspace all root contents
370
         """
592
         """

+ 41 - 0
tracim/views/core_api/schemas.py View File

10
 from tracim.models.contents import ContentTypeLegacy as ContentType
10
 from tracim.models.contents import ContentTypeLegacy as ContentType
11
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
11
 from tracim.models.contents import ContentStatusLegacy as ContentStatus
12
 from tracim.models.context_models import ContentCreation
12
 from tracim.models.context_models import ContentCreation
13
+from tracim.models.context_models import WorkspaceMemberInvitation
13
 from tracim.models.context_models import WorkspaceUpdate
14
 from tracim.models.context_models import WorkspaceUpdate
15
+from tracim.models.context_models import RoleUpdate
14
 from tracim.models.context_models import CommentCreation
16
 from tracim.models.context_models import CommentCreation
15
 from tracim.models.context_models import TextBasedContentUpdate
17
 from tracim.models.context_models import TextBasedContentUpdate
16
 from tracim.models.context_models import SetContentStatus
18
 from tracim.models.context_models import SetContentStatus
17
 from tracim.models.context_models import CommentPath
19
 from tracim.models.context_models import CommentPath
18
 from tracim.models.context_models import MoveParams
20
 from tracim.models.context_models import MoveParams
19
 from tracim.models.context_models import WorkspaceAndContentPath
21
 from tracim.models.context_models import WorkspaceAndContentPath
22
+from tracim.models.context_models import WorkspaceAndUserPath
20
 from tracim.models.context_models import ContentFilter
23
 from tracim.models.context_models import ContentFilter
21
 from tracim.models.context_models import LoginCredentials
24
 from tracim.models.context_models import LoginCredentials
22
 from tracim.models.data import UserRoleInWorkspace
25
 from tracim.models.data import UserRoleInWorkspace
91
     content_id = marshmallow.fields.Int(example=6, required=True)
94
     content_id = marshmallow.fields.Int(example=6, required=True)
92
 
95
 
93
 
96
 
97
+class WorkspaceAndUserIdPathSchema(
98
+    UserIdPathSchema,
99
+    WorkspaceIdPathSchema
100
+):
101
+    @post_load
102
+    def make_path_object(self, data):
103
+        return WorkspaceAndUserPath(**data)
104
+
105
+
94
 class WorkspaceAndContentIdPathSchema(
106
 class WorkspaceAndContentIdPathSchema(
95
     WorkspaceIdPathSchema,
107
     WorkspaceIdPathSchema,
96
     ContentIdPathSchema
108
     ContentIdPathSchema
106
         description='id of a comment related to content content_id',
118
         description='id of a comment related to content content_id',
107
         required=True
119
         required=True
108
     )
120
     )
121
+
109
     @post_load
122
     @post_load
110
     def make_path_object(self, data):
123
     def make_path_object(self, data):
111
         return CommentPath(**data)
124
         return CommentPath(**data)
150
 ###
163
 ###
151
 
164
 
152
 
165
 
166
+class RoleUpdateSchema(marshmallow.Schema):
167
+    role = marshmallow.fields.String(
168
+        example='contributor',
169
+        validate=OneOf(UserRoleInWorkspace.get_all_role_slug())
170
+    )
171
+
172
+    @post_load
173
+    def make_role(self, data):
174
+        return RoleUpdate(**data)
175
+
176
+
177
+class WorkspaceMemberInviteSchema(RoleUpdateSchema):
178
+    user_id = marshmallow.fields.Int(
179
+        example=5,
180
+        default=None,
181
+        allow_none=True,
182
+    )
183
+    user_email_or_public_name = marshmallow.fields.String(
184
+        example='suri@cate.fr',
185
+        default=None,
186
+        allow_none=True,
187
+    )
188
+
189
+    @post_load
190
+    def make_role(self, data):
191
+        return WorkspaceMemberInvitation(**data)
192
+
193
+
153
 class BasicAuthSchema(marshmallow.Schema):
194
 class BasicAuthSchema(marshmallow.Schema):
154
 
195
 
155
     email = marshmallow.fields.Email(
196
     email = marshmallow.fields.Email(

+ 91 - 0
tracim/views/core_api/workspace_controller.py View File

1
 import typing
1
 import typing
2
 import transaction
2
 import transaction
3
 from pyramid.config import Configurator
3
 from pyramid.config import Configurator
4
+
5
+from tracim.lib.core.user import UserApi
6
+from tracim.models.roles import WorkspaceRoles
7
+
4
 try:  # Python 3.5+
8
 try:  # Python 3.5+
5
     from http import HTTPStatus
9
     from http import HTTPStatus
6
 except ImportError:
10
 except ImportError:
20
 from tracim.models.context_models import UserRoleWorkspaceInContext
24
 from tracim.models.context_models import UserRoleWorkspaceInContext
21
 from tracim.models.context_models import ContentInContext
25
 from tracim.models.context_models import ContentInContext
22
 from tracim.exceptions import EmptyLabelNotAllowed
26
 from tracim.exceptions import EmptyLabelNotAllowed
27
+from tracim.exceptions import UserDoesNotExist
23
 from tracim.exceptions import WorkspacesDoNotMatch
28
 from tracim.exceptions import WorkspacesDoNotMatch
24
 from tracim.views.controllers import Controller
29
 from tracim.views.controllers import Controller
25
 from tracim.views.core_api.schemas import FilterContentQuerySchema
30
 from tracim.views.core_api.schemas import FilterContentQuerySchema
31
+from tracim.views.core_api.schemas import WorkspaceMemberInviteSchema
32
+from tracim.views.core_api.schemas import RoleUpdateSchema
26
 from tracim.views.core_api.schemas import WorkspaceCreationSchema
33
 from tracim.views.core_api.schemas import WorkspaceCreationSchema
27
 from tracim.views.core_api.schemas import WorkspaceModifySchema
34
 from tracim.views.core_api.schemas import WorkspaceModifySchema
35
+from tracim.views.core_api.schemas import WorkspaceAndUserIdPathSchema
28
 from tracim.views.core_api.schemas import ContentMoveSchema
36
 from tracim.views.core_api.schemas import ContentMoveSchema
29
 from tracim.views.core_api.schemas import NoContentSchema
37
 from tracim.views.core_api.schemas import NoContentSchema
30
 from tracim.views.core_api.schemas import ContentCreationSchema
38
 from tracim.views.core_api.schemas import ContentCreationSchema
128
         ]
136
         ]
129
 
137
 
130
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
138
     @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
139
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
140
+    @hapic.input_path(WorkspaceAndUserIdPathSchema())
141
+    @hapic.input_body(RoleUpdateSchema())
142
+    @hapic.output_body(WorkspaceMemberSchema())
143
+    def update_workspaces_members_role(
144
+            self,
145
+            context,
146
+            request: TracimRequest,
147
+            hapic_data=None
148
+    ) -> UserRoleWorkspaceInContext:
149
+        """
150
+        Update Members to this workspace
151
+        """
152
+        app_config = request.registry.settings['CFG']
153
+        rapi = RoleApi(
154
+            current_user=request.current_user,
155
+            session=request.dbsession,
156
+            config=app_config,
157
+        )
158
+
159
+        role = rapi.get_one(
160
+            user_id=hapic_data.path.user_id,
161
+            workspace_id=hapic_data.path.workspace_id,
162
+        )
163
+        workspace_role = WorkspaceRoles.get_role_from_slug(hapic_data.body.role)
164
+        role = rapi.update_role(
165
+            role,
166
+            role_level=workspace_role.level
167
+        )
168
+        return rapi.get_user_role_workspace_with_context(role)
169
+
170
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
171
+    @require_workspace_role(UserRoleInWorkspace.WORKSPACE_MANAGER)
172
+    @hapic.input_path(WorkspaceIdPathSchema())
173
+    @hapic.input_body(WorkspaceMemberInviteSchema())
174
+    @hapic.output_body(WorkspaceMemberSchema())
175
+    def create_workspaces_members_role(
176
+            self,
177
+            context,
178
+            request: TracimRequest,
179
+            hapic_data=None
180
+    ) -> UserRoleWorkspaceInContext:
181
+        """
182
+        Add Members to this workspace
183
+        """
184
+        app_config = request.registry.settings['CFG']
185
+        rapi = RoleApi(
186
+            current_user=request.current_user,
187
+            session=request.dbsession,
188
+            config=app_config,
189
+        )
190
+        uapi = UserApi(
191
+            current_user=request.current_user,
192
+            session=request.dbsession,
193
+            config=app_config,
194
+        )
195
+        if hapic_data.body.user_id is not None:
196
+            user = uapi.get_one(user_id=hapic_data.body.user_id)
197
+        elif hapic_data.body.user_email_or_public_name:
198
+            user = uapi.find_one(
199
+                user_email_or_public_name=hapic_data.body.user_email_or_public_name  # nopep8
200
+            )
201
+        else:
202
+            raise UserDoesNotExist(
203
+                'user_id or user_email_or_public_name should have '
204
+                'valid value.'
205
+            )
206
+        role = rapi.create_one(
207
+            user=user,
208
+            workspace=request.current_workspace,
209
+            role_level=WorkspaceRoles.get_role_from_slug(hapic_data.body.role).level,  # nopep8
210
+            with_notif=False,
211
+            flush=True,
212
+        )
213
+        return rapi.get_user_role_workspace_with_context(role)
214
+
215
+    @hapic.with_api_doc(tags=[WORKSPACE_ENDPOINTS_TAG])
131
     @require_workspace_role(UserRoleInWorkspace.READER)
216
     @require_workspace_role(UserRoleInWorkspace.READER)
132
     @hapic.input_path(WorkspaceIdPathSchema())
217
     @hapic.input_path(WorkspaceIdPathSchema())
133
     @hapic.input_query(FilterContentQuerySchema())
218
     @hapic.input_query(FilterContentQuerySchema())
388
         # Workspace Members (Roles)
473
         # Workspace Members (Roles)
389
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
474
         configurator.add_route('workspace_members', '/workspaces/{workspace_id}/members', request_method='GET')  # nopep8
390
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
475
         configurator.add_view(self.workspaces_members, route_name='workspace_members')  # nopep8
476
+        # Update Workspace Members roles
477
+        configurator.add_route('update_workspace_member', '/workspaces/{workspace_id}/members/{user_id}', request_method='PUT')  # nopep8
478
+        configurator.add_view(self.update_workspaces_members_role, route_name='update_workspace_member')  # nopep8
479
+        # Create Workspace Members roles
480
+        configurator.add_route('create_workspace_member', '/workspaces/{workspace_id}/members', request_method='POST')  # nopep8
481
+        configurator.add_view(self.create_workspaces_members_role, route_name='create_workspace_member')  # nopep8
391
         # Workspace Content
482
         # Workspace Content
392
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
483
         configurator.add_route('workspace_content', '/workspaces/{workspace_id}/contents', request_method='GET')  # nopep8
393
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8
484
         configurator.add_view(self.workspace_content, route_name='workspace_content')  # nopep8