Bladeren bron

new authorization mecanism with decorators

Guénaël Muller 7 jaren geleden
bovenliggende
commit
042191d76e
5 gewijzigde bestanden met toevoegingen van 170 en 84 verwijderingen
  1. 14 7
      tracim/__init__.py
  2. 12 0
      tracim/exceptions.py
  3. 79 51
      tracim/lib/utils/auth.py
  4. 51 0
      tracim/lib/utils/request.py
  5. 14 26
      tracim/views/default/default_controller.py

+ 14 - 7
tracim/__init__.py Bestand weergeven

4
 
4
 
5
 from pyramid.config import Configurator
5
 from pyramid.config import Configurator
6
 from pyramid.authentication import BasicAuthAuthenticationPolicy
6
 from pyramid.authentication import BasicAuthAuthenticationPolicy
7
-from pyramid.authorization import ACLAuthorizationPolicy
8
 from hapic.ext.pyramid import PyramidContext
7
 from hapic.ext.pyramid import PyramidContext
9
 
8
 
10
 from tracim.extensions import hapic
9
 from tracim.extensions import hapic
11
 from tracim.config import CFG
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
 from tracim.lib.utils.auth import BASIC_AUTH_WEBUI_REALM
14
 from tracim.lib.utils.auth import BASIC_AUTH_WEBUI_REALM
15
+from tracim.lib.utils.auth import TRACIM_DEFAULT_PERM
15
 from tracim.views.example_api.example_api_controller import ExampleApiController
16
 from tracim.views.example_api.example_api_controller import ExampleApiController
16
 from tracim.views.default.default_controller import DefaultController
17
 from tracim.views.default.default_controller import DefaultController
17
 
18
 
24
     app_config.configure_filedepot()
25
     app_config.configure_filedepot()
25
     settings['CFG'] = app_config
26
     settings['CFG'] = app_config
26
     configurator = Configurator(settings=settings, autocommit=True)
27
     configurator = Configurator(settings=settings, autocommit=True)
27
-    # Add BasicAuthPolicy + ACL AuthorizationPolicy
28
+    # Add BasicAuthPolicy
28
     authn_policy = BasicAuthAuthenticationPolicy(
29
     authn_policy = BasicAuthAuthenticationPolicy(
29
-        check_credentials,
30
+        basic_auth_check_credentials,
30
         realm=BASIC_AUTH_WEBUI_REALM,
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
     configurator.set_authentication_policy(authn_policy)
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
     # Pyramids "plugin" include.
42
     # Pyramids "plugin" include.
36
     configurator.include('pyramid_jinja2')
43
     configurator.include('pyramid_jinja2')
37
     # Add SqlAlchemy DB
44
     # Add SqlAlchemy DB

+ 12 - 0
tracim/exceptions.py Bestand weergeven

59
 
59
 
60
 class SameValueError(ValueError):
60
 class SameValueError(ValueError):
61
     pass
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 Bestand weergeven

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 import typing
2
 import typing
3
+
4
+from pyramid.interfaces import IAuthorizationPolicy
5
+from zope.interface import implementer
6
+
3
 try:
7
 try:
4
     from json.decoder import JSONDecodeError
8
     from json.decoder import JSONDecodeError
5
 except ImportError:  # python3.4
9
 except ImportError:  # python3.4
7
 from sqlalchemy.orm.exc import NoResultFound
11
 from sqlalchemy.orm.exc import NoResultFound
8
 
12
 
9
 from pyramid.request import Request
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
 from tracim.models.auth import User
15
 from tracim.models.auth import User
16
 from tracim.models.data import Workspace
16
 from tracim.models.data import Workspace
17
-from tracim.models.data import UserRoleInWorkspace
18
 from tracim.lib.core.user import UserApi
17
 from tracim.lib.core.user import UserApi
19
 from tracim.lib.core.workspace import WorkspaceApi
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
 BASIC_AUTH_WEBUI_REALM = "tracim"
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
     :param request: pyramid request
32
     :param request: pyramid request
31
-    :return:
33
+    :return: current authenticated user
32
     """
34
     """
33
     app_config = request.registry.settings['CFG']
35
     app_config = request.registry.settings['CFG']
34
     uapi = UserApi(None, session=request.dbsession, config=app_config)
36
     uapi = UserApi(None, session=request.dbsession, config=app_config)
35
     user = None
37
     user = None
36
     try:
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
         user = uapi.get_one_by_email(login)
43
         user = uapi.get_one_by_email(login)
39
     except NoResultFound:
44
     except NoResultFound:
40
-        pass
45
+        raise NotAuthentificated('User not found')
41
     return user
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
     Get current workspace from request
51
     Get current workspace from request
52
+    :param user: User who want to check the workspace
47
     :param request: pyramid request
53
     :param request: pyramid request
48
     :return:
54
     :return:
49
     """
55
     """
50
-    workspace = None
56
+    workspace_id = ''
51
     try:
57
     try:
52
         if 'workspace_id' not in request.json_body:
58
         if 'workspace_id' not in request.json_body:
53
             return None
59
             return None
54
         workspace_id = request.json_body['workspace_id']
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
         workspace = wapi.get_one(workspace_id)
62
         workspace = wapi.get_one(workspace_id)
57
     except JSONDecodeError:
63
     except JSONDecodeError:
58
-        pass
64
+        raise WorkspaceNotFound('Bad json body')
59
     except NoResultFound:
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
     return workspace
70
     return workspace
62
 
71
 
72
+###
73
+# BASIC AUTH
74
+###
75
+
63
 
76
 
64
-def check_credentials(
77
+def basic_auth_check_credentials(
65
         login: str,
78
         login: str,
66
         cleartext_password: str,
79
         cleartext_password: str,
67
-        request: Request
80
+        request: 'TracimRequest'
68
 ) -> typing.Optional[list]:
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
     :param login: login of user
84
     :param login: login of user
73
     :param cleartext_password: user password in cleartext
85
     :param cleartext_password: user password in cleartext
74
     :param request: Pyramid request
86
     :param request: Pyramid request
75
     :return: None if auth failed, list of permissions if auth succeed
87
     :return: None if auth failed, list of permissions if auth succeed
76
     """
88
     """
77
-    user = get_user(request)
78
 
89
 
79
     # Do not accept invalid user
90
     # Do not accept invalid user
91
+    user = _get_basic_auth_unsafe_user(request)
80
     if not user \
92
     if not user \
81
             or user.email != login \
93
             or user.email != login \
82
             or not user.validate_password(cleartext_password):
94
             or not user.validate_password(cleartext_password):
83
         return None
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 Bestand weergeven

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 Bestand weergeven

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