Browse Source

new authorization mecanism with decorators

Guénaël Muller 7 years ago
parent
commit
042191d76e

+ 14 - 7
tracim/__init__.py View File

@@ -4,14 +4,15 @@ import time
4 4
 
5 5
 from pyramid.config import Configurator
6 6
 from pyramid.authentication import BasicAuthAuthenticationPolicy
7
-from pyramid.authorization import ACLAuthorizationPolicy
8 7
 from hapic.ext.pyramid import PyramidContext
9 8
 
10 9
 from tracim.extensions import hapic
11 10
 from tracim.config import CFG
12
-from tracim.lib.utils.auth import check_credentials
13
-from tracim.lib.utils.auth import Root
11
+from tracim.lib.utils.auth import basic_auth_check_credentials
12
+from tracim.lib.utils.request import TracimRequest
13
+from tracim.lib.utils.auth import AcceptAllAuthorizationPolicy
14 14
 from tracim.lib.utils.auth import BASIC_AUTH_WEBUI_REALM
15
+from tracim.lib.utils.auth import TRACIM_DEFAULT_PERM
15 16
 from tracim.views.example_api.example_api_controller import ExampleApiController
16 17
 from tracim.views.default.default_controller import DefaultController
17 18
 
@@ -24,14 +25,20 @@ def main(global_config, **settings):
24 25
     app_config.configure_filedepot()
25 26
     settings['CFG'] = app_config
26 27
     configurator = Configurator(settings=settings, autocommit=True)
27
-    # Add BasicAuthPolicy + ACL AuthorizationPolicy
28
+    # Add BasicAuthPolicy
28 29
     authn_policy = BasicAuthAuthenticationPolicy(
29
-        check_credentials,
30
+        basic_auth_check_credentials,
30 31
         realm=BASIC_AUTH_WEBUI_REALM,
31 32
     )
32
-    configurator.set_authorization_policy(ACLAuthorizationPolicy())
33
+    # Default authorization : Accept anything.
34
+    configurator.set_authorization_policy(AcceptAllAuthorizationPolicy())
33 35
     configurator.set_authentication_policy(authn_policy)
34
-    configurator.set_root_factory(lambda request: Root())
36
+    # INFO - GM - 11-04-2018 - set default perm
37
+    # setting default perm is needed to force authentification
38
+    # mecanism in all views.
39
+    configurator.set_default_permission(TRACIM_DEFAULT_PERM)
40
+    # Override default request
41
+    configurator.set_request_factory(TracimRequest)
35 42
     # Pyramids "plugin" include.
36 43
     configurator.include('pyramid_jinja2')
37 44
     # Add SqlAlchemy DB

+ 12 - 0
tracim/exceptions.py View File

@@ -59,3 +59,15 @@ class NotFound(TracimException):
59 59
 
60 60
 class SameValueError(ValueError):
61 61
     pass
62
+
63
+
64
+class NotAuthentificated(TracimException):
65
+    pass
66
+
67
+
68
+class WorkspaceNotFound(NotFound):
69
+    pass
70
+
71
+
72
+class InsufficientUserWorkspaceRole(TracimException):
73
+    pass

+ 79 - 51
tracim/lib/utils/auth.py View File

@@ -1,5 +1,9 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 import typing
3
+
4
+from pyramid.interfaces import IAuthorizationPolicy
5
+from zope.interface import implementer
6
+
3 7
 try:
4 8
     from json.decoder import JSONDecodeError
5 9
 except ImportError:  # python3.4
@@ -7,112 +11,136 @@ except ImportError:  # python3.4
7 11
 from sqlalchemy.orm.exc import NoResultFound
8 12
 
9 13
 from pyramid.request import Request
10
-from pyramid.security import ALL_PERMISSIONS
11
-from pyramid.security import Allow
12
-from pyramid.security import unauthenticated_userid
13 14
 
14
-from tracim.models.auth import Group
15 15
 from tracim.models.auth import User
16 16
 from tracim.models.data import Workspace
17
-from tracim.models.data import UserRoleInWorkspace
18 17
 from tracim.lib.core.user import UserApi
19 18
 from tracim.lib.core.workspace import WorkspaceApi
20
-from tracim.lib.core.userworkspace import RoleApi
21 19
 
22
-# INFO - G.M - 06-04-2018 - Auth for pyramid
23
-# based on this tutorial : https://docs.pylonsproject.org/projects/pyramid-cookbook/en/latest/auth/basic.html  # nopep8
20
+from tracim.exceptions import NotAuthentificated
21
+from tracim.exceptions import WorkspaceNotFound
22
+from tracim.exceptions import InsufficientUserWorkspaceRole
24 23
 BASIC_AUTH_WEBUI_REALM = "tracim"
24
+TRACIM_DEFAULT_PERM = 'tracim'
25 25
 
26 26
 
27
-def get_user(request: Request) -> typing.Optional[User]:
27
+def get_safe_user(
28
+        request: Request,
29
+) -> User:
28 30
     """
29
-    Get current pyramid user from request
31
+    Get current pyramid authenticated user from request
30 32
     :param request: pyramid request
31
-    :return:
33
+    :return: current authenticated user
32 34
     """
33 35
     app_config = request.registry.settings['CFG']
34 36
     uapi = UserApi(None, session=request.dbsession, config=app_config)
35 37
     user = None
36 38
     try:
37
-        login = unauthenticated_userid(request)
39
+        login = request.authenticated_userid
40
+        if not login:
41
+            raise NotAuthentificated('not authenticated user_id,'
42
+                                     'Failed Authentification ?')
38 43
         user = uapi.get_one_by_email(login)
39 44
     except NoResultFound:
40
-        pass
45
+        raise NotAuthentificated('User not found')
41 46
     return user
42 47
 
43 48
 
44
-def get_workspace(request: Request) -> typing.Optional[Workspace]:
49
+def get_workspace(user: User, request: Request) -> typing.Optional[Workspace]:
45 50
     """
46 51
     Get current workspace from request
52
+    :param user: User who want to check the workspace
47 53
     :param request: pyramid request
48 54
     :return:
49 55
     """
50
-    workspace = None
56
+    workspace_id = ''
51 57
     try:
52 58
         if 'workspace_id' not in request.json_body:
53 59
             return None
54 60
         workspace_id = request.json_body['workspace_id']
55
-        wapi = WorkspaceApi(current_user=None, session=request.dbsession)
61
+        wapi = WorkspaceApi(current_user=user, session=request.dbsession)
56 62
         workspace = wapi.get_one(workspace_id)
57 63
     except JSONDecodeError:
58
-        pass
64
+        raise WorkspaceNotFound('Bad json body')
59 65
     except NoResultFound:
60
-        pass
66
+        raise WorkspaceNotFound(
67
+            'Workspace {} does not exist '
68
+            'or is not visible for this user'.format(workspace_id)
69
+        )
61 70
     return workspace
62 71
 
72
+###
73
+# BASIC AUTH
74
+###
75
+
63 76
 
64
-def check_credentials(
77
+def basic_auth_check_credentials(
65 78
         login: str,
66 79
         cleartext_password: str,
67
-        request: Request
80
+        request: 'TracimRequest'
68 81
 ) -> typing.Optional[list]:
69 82
     """
70
-    Check credential for pyramid basic_auth, checks also for
71
-    global and Workspace related permissions.
83
+    Check credential for pyramid basic_auth
72 84
     :param login: login of user
73 85
     :param cleartext_password: user password in cleartext
74 86
     :param request: Pyramid request
75 87
     :return: None if auth failed, list of permissions if auth succeed
76 88
     """
77
-    user = get_user(request)
78 89
 
79 90
     # Do not accept invalid user
91
+    user = _get_basic_auth_unsafe_user(request)
80 92
     if not user \
81 93
             or user.email != login \
82 94
             or not user.validate_password(cleartext_password):
83 95
         return None
84
-    permissions = []
85
-
86
-    # Global groups
87
-    for group in user.groups:
88
-        permissions.append(group.group_id)
89
-
90
-    # Current workspace related group
91
-    workspace = get_workspace(request)
92
-    if workspace:
93
-        roleapi = RoleApi(current_user=user, session=request.dbsession)
94
-        role = roleapi.get_one(
95
-            user_id=user.user_id,
96
-            workspace_id=workspace.workspace_id,
97
-        )
98
-        permissions.append(role)
96
+    return []
99 97
 
100
-    return permissions
101 98
 
99
+def _get_basic_auth_unsafe_user(
100
+    request: Request,
101
+) -> typing.Optional[User]:
102
+    """
103
+    :param request: pyramid request
104
+    :return: User or None
105
+    """
106
+    app_config = request.registry.settings['CFG']
107
+    uapi = UserApi(None, session=request.dbsession, config=app_config)
108
+    try:
109
+        login = request.unauthenticated_userid
110
+        if not login:
111
+            return None
112
+        user = uapi.get_one_by_email(login)
113
+    except NoResultFound:
114
+        return None
115
+    return user
116
+
117
+####
102 118
 
103
-# Global Permissions
104
-ADMIN_PERM = 'admin'
105
-MANAGE_GLOBAL_PERM = 'manage_global'
106
-USER_PERM = 'user'
107
-# Workspace-specific permission
108
-READ_PERM = 'read'
109
-CONTRIBUTE_PERM = 'contribute'
110
-MANAGE_CONTENT_PERM = 'manage_content'
111
-MANAGE_WORKSPACE_PERM = 'manage_workspace'
112 119
 
120
+def require_workspace_role(minimal_required_role):
121
+    def decorator(func):
113 122
 
114
-class Root(object):
123
+        def wrapper(self, request: 'TracimRequest'):
124
+            user = request.current_user
125
+            workspace = request.current_workspace
126
+            if workspace.get_user_role(user) >= minimal_required_role:
127
+                return func(self, request)
128
+            raise InsufficientUserWorkspaceRole()
129
+
130
+        return wrapper
131
+    return decorator
132
+
133
+###
134
+
135
+
136
+@implementer(IAuthorizationPolicy)
137
+class AcceptAllAuthorizationPolicy(object):
115 138
     """
116
-    Root of all Pyramid requests, used to store global acl
139
+    Simple AuthorizationPolicy to avoid trouble with pyramid.
140
+    Acceot any request.
117 141
     """
118
-    __acl__ = ()
142
+    def permits(self, context, principals, permision):
143
+        return True
144
+
145
+    def principals_allowed_by_permission(self, context, permission):
146
+        raise NotImplementedError()

+ 51 - 0
tracim/lib/utils/request.py View File

@@ -0,0 +1,51 @@
1
+from contextlib import contextmanager
2
+
3
+from pyramid.decorator import reify
4
+from pyramid.request import Request
5
+
6
+from tracim.models import User
7
+from tracim.models.data import Workspace
8
+from tracim.lib.utils.auth import get_safe_user
9
+from tracim.lib.utils.auth import get_workspace
10
+
11
+
12
+class TracimRequest(Request):
13
+    def __init__(
14
+            self,
15
+            environ,
16
+            charset=None,
17
+            unicode_errors=None,
18
+            decode_param_names=None,
19
+            **kw
20
+    ):
21
+        super().__init__(
22
+            environ,
23
+            charset,
24
+            unicode_errors,
25
+            decode_param_names,
26
+            **kw
27
+        )
28
+        self._current_workspace = None  # type: Workspace
29
+        self._current_user = None  # type: User
30
+
31
+    @property
32
+    def current_workspace(self) -> Workspace:
33
+        if self._current_workspace is None:
34
+            self.current_workspace = get_workspace(self.current_user, self)
35
+        return self._current_workspace
36
+
37
+    @current_workspace.setter
38
+    def current_workspace(self, workspace: Workspace) -> None:
39
+        assert self._current_workspace is None
40
+        self._current_workspace = workspace
41
+
42
+    @property
43
+    def current_user(self) -> User:
44
+        if self._current_user is None:
45
+            self.current_user = get_safe_user(self)
46
+        return self._current_user
47
+
48
+    @current_user.setter
49
+    def current_user(self, user: User) -> None:
50
+        assert self._current_user is None
51
+        self._current_user = user

+ 14 - 26
tracim/views/default/default_controller.py View File

@@ -1,30 +1,25 @@
1 1
 # coding=utf-8
2
+from pyramid.request import Request
3
+
4
+from tracim.models.data import UserRoleInWorkspace
2 5
 from tracim.views.controllers import Controller
3 6
 from pyramid.config import Configurator
4 7
 from pyramid.response import Response
5 8
 from pyramid.exceptions import NotFound
6 9
 from pyramid.httpexceptions import HTTPUnauthorized
7 10
 from pyramid.httpexceptions import HTTPForbidden
8
-from pyramid.security import forget
11
+from pyramid.security import forget, authenticated_userid
9 12
 
10
-from tracim.lib.utils.auth import MANAGE_CONTENT_PERM
11
-from tracim.lib.utils.auth import MANAGE_WORKSPACE_PERM
12
-from tracim.lib.utils.auth import MANAGE_GLOBAL_PERM
13
-from tracim.lib.utils.auth import READ_PERM
14
-from tracim.lib.utils.auth import CONTRIBUTE_PERM
15
-from tracim.lib.utils.auth import ADMIN_PERM
16
-from tracim.lib.utils.auth import USER_PERM
13
+from tracim.lib.utils.auth import require_workspace_role
17 14
 
18 15
 
19 16
 class DefaultController(Controller):
20 17
 
21
-    @classmethod
22
-    def notfound_view(cls, request):
18
+    def notfound_view(self, request):
23 19
         request.response.status = 404
24 20
         return {}
25 21
 
26
-    @classmethod
27
-    def forbidden_view(cls, request):
22
+    def forbidden_view(self, request):
28 23
         if request.authenticated_userid is None:
29 24
             response = HTTPUnauthorized()
30 25
             response.headers.update(forget(request))
@@ -35,8 +30,8 @@ class DefaultController(Controller):
35 30
         return response
36 31
 
37 32
     # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
38
-    @classmethod
39
-    def test_config(cls, request):
33
+    @require_workspace_role(UserRoleInWorkspace.READER)
34
+    def test_config(self, request: Request):
40 35
         try:
41 36
             app_config = request.registry.settings['CFG']
42 37
             project = app_config.WEBSITE_TITLE
@@ -45,8 +40,8 @@ class DefaultController(Controller):
45 40
         return {'project': project}
46 41
 
47 42
     # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
48
-    @classmethod
49
-    def test_contributor_page(cls, request):
43
+    @require_workspace_role(UserRoleInWorkspace.CONTRIBUTOR)
44
+    def test_contributor_page(self, request):
50 45
         try:
51 46
             app_config = request.registry.settings['CFG']
52 47
             project = 'contributor'
@@ -55,8 +50,7 @@ class DefaultController(Controller):
55 50
         return {'project': project}
56 51
 
57 52
     # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
58
-    @classmethod
59
-    def test_admin_page(cls, request):
53
+    def test_admin_page(self, request):
60 54
         try:
61 55
             app_config = request.registry.settings['CFG']
62 56
             project = 'admin'
@@ -65,8 +59,7 @@ class DefaultController(Controller):
65 59
         return {'project': project}
66 60
 
67 61
     # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
68
-    @classmethod
69
-    def test_manager_page(cls, request):
62
+    def test_manager_page(self, request):
70 63
         try:
71 64
             app_config = request.registry.settings['CFG']
72 65
             project = 'manager'
@@ -75,8 +68,7 @@ class DefaultController(Controller):
75 68
         return {'project': project}
76 69
 
77 70
     # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
78
-    @classmethod
79
-    def test_user_page(cls, request):
71
+    def test_user_page(self, request):
80 72
         try:
81 73
             app_config = request.registry.settings['CFG']
82 74
             project = 'user'
@@ -109,7 +101,6 @@ class DefaultController(Controller):
109 101
             self.test_contributor_page,
110 102
             route_name='test_contributor',
111 103
             renderer='tracim:templates/mytemplate.jinja2',
112
-            permission=CONTRIBUTE_PERM,
113 104
         )
114 105
 
115 106
         # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
@@ -118,7 +109,6 @@ class DefaultController(Controller):
118 109
             self.test_admin_page,
119 110
             route_name='test_admin',
120 111
             renderer='tracim:templates/mytemplate.jinja2',
121
-            permission=ADMIN_PERM,
122 112
         )
123 113
 
124 114
         # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
@@ -127,7 +117,6 @@ class DefaultController(Controller):
127 117
             self.test_user_page,
128 118
             route_name='test_manager',
129 119
             renderer='tracim:templates/mytemplate.jinja2',
130
-            permission=MANAGE_GLOBAL_PERM,
131 120
         )
132 121
 
133 122
         # TODO - G.M - 10-04-2018 - [cleanup][tempExample] - Drop this method
@@ -136,7 +125,6 @@ class DefaultController(Controller):
136 125
             self.test_user_page,
137 126
             route_name='test_user',
138 127
             renderer='tracim:templates/mytemplate.jinja2',
139
-            permission=USER_PERM,
140 128
         )
141 129
 
142 130
         configurator.add_forbidden_view(self.forbidden_view)