Browse Source

Merge pull request #31 from buxx/feature/ldap

Tracim 9 years ago
parent
commit
3eaf8a4e4d
45 changed files with 1618 additions and 197 deletions
  1. 0 3
      .travis.yml
  2. 40 0
      README.md
  3. 4 2
      install/requirements.txt
  4. 29 0
      tracim/development.ini.base
  5. 22 0
      tracim/migration/versions/b73e57760b36_add_user_field_for_imported_profiles.py
  6. 9 2
      tracim/setup.py
  7. 13 0
      tracim/test.ini
  8. 105 0
      tracim/tracim/command/__init__.py
  9. 22 0
      tracim/tracim/command/ldap_test_server.py
  10. 169 0
      tracim/tracim/command/user.py
  11. 14 0
      tracim/tracim/config/__init__.py
  12. 5 57
      tracim/tracim/config/app_cfg.py
  13. 3 0
      tracim/tracim/config/deployment.ini_tmpl
  14. 2 0
      tracim/tracim/controllers/__init__.py
  15. 1 1
      tracim/tracim/controllers/error.py
  16. 29 1
      tracim/tracim/controllers/user.py
  17. 39 0
      tracim/tracim/fixtures/__init__.py
  18. 58 0
      tracim/tracim/fixtures/ldap.py
  19. 49 0
      tracim/tracim/fixtures/users_and_groups.py
  20. BIN
      tracim/tracim/i18n/fr/LC_MESSAGES/tracim.mo
  21. 79 73
      tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po
  22. 1 0
      tracim/tracim/lib/auth/__init__.py
  23. 66 0
      tracim/tracim/lib/auth/base.py
  24. 47 0
      tracim/tracim/lib/auth/internal.py
  25. 195 0
      tracim/tracim/lib/auth/ldap.py
  26. 24 0
      tracim/tracim/lib/auth/wrapper.py
  27. 4 0
      tracim/tracim/lib/base.py
  28. 21 0
      tracim/tracim/lib/exception.py
  29. 3 0
      tracim/tracim/lib/group.py
  30. 18 0
      tracim/tracim/lib/helpers.py
  31. 7 3
      tracim/tracim/lib/user.py
  32. 1 0
      tracim/tracim/model/auth.py
  33. 6 1
      tracim/tracim/public/assets/css/dashboard.css
  34. 1 1
      tracim/tracim/templates/index.mak
  35. 11 3
      tracim/tracim/templates/user_toolbars.mak
  36. 8 2
      tracim/tracim/templates/user_workspace_forms.mak
  37. 116 14
      tracim/tracim/tests/__init__.py
  38. 27 0
      tracim/tracim/tests/command/commands.py
  39. 80 0
      tracim/tracim/tests/command/user.py
  40. 33 0
      tracim/tracim/tests/command/user_ldap.py
  41. 56 0
      tracim/tracim/tests/functional/test_ldap_authentication.py
  42. 78 0
      tracim/tracim/tests/functional/test_ldap_restrictions.py
  43. 115 0
      tracim/tracim/tests/library/test_ldap_without_ldap_groups.py
  44. 6 33
      tracim/tracim/websetup/bootstrap.py
  45. 2 1
      tracim/tracim/websetup/schema.py

+ 0 - 3
.travis.yml View File

@@ -17,9 +17,6 @@ addons:
17 17
 install:
18 18
   - "cd tracim && python setup.py develop; cd -"
19 19
   - "pip install -r install/requirements.txt; echo"
20
-  - "./bin/tg2env-patch 1 /home/travis/virtualenv/python3.2.5/lib/python3.2/site-packages"
21
-  - "pip install -r install/requirements.txt; echo"
22
-  - "./bin/tg2env-patch 2 /home/travis/virtualenv/python3.2.5/lib/python3.2/site-packages"
23 20
   - "pip install coveralls"
24 21
 
25 22
 before_script:

+ 40 - 0
README.md View File

@@ -291,6 +291,46 @@ You must define general parameters like the base_url and the website title which
291 291
     website.title = My Company Intranet
292 292
     website.base_url = http://intranet.mycompany.com:8080
293 293
 
294
+#### LDAP ####
295
+
296
+To use LDAP authentication, set ``auth_type`` parameter to "ldap":
297
+
298
+    auth_type = ldap
299
+
300
+Then add LDAP parameters
301
+
302
+    # LDAP server address
303
+    ldap_url = ldap://localhost:389
304
+
305
+    # Base dn to make queries
306
+    ldap_base_dn = dc=directory,dc=fsf,dc=org
307
+
308
+    # Bind dn to identify the search
309
+    ldap_bind_dn = cn=admin,dc=directory,dc=fsf,dc=org
310
+
311
+    # The bind password
312
+    ldap_bind_pass = toor
313
+
314
+    # Attribute name of user record who contain user login (email)
315
+    ldap_ldap_naming_attribute = uid
316
+
317
+    # Matching between ldap attribute and ldap user field (ldap_attr1=user_field1,ldap_attr2=user_field2,...)
318
+    ldap_user_attributes = mail=email
319
+
320
+    # TLS usage to communicate with your LDAP server
321
+    ldap_tls = False
322
+
323
+    # If True, LDAP own tracim group managment (not available for now!)
324
+    ldap_group_enabled = False
325
+
326
+You may need an administrator account to manage Tracim. Use the following command (from ``/install/dir/of/tracim/tracim``):
327
+
328
+```
329
+gearbox user create -l admin-email@domain.com -g managers -g administrators
330
+```
331
+
332
+Keep in mind ``admin-email@domain.com`` must match with LDAP user.
333
+
294 334
 #### Other parameters  ####
295 335
 
296 336
 There are other parameters which may be of some interest for you. For example, you can:

+ 4 - 2
install/requirements.txt View File

@@ -1,4 +1,4 @@
1
-Babel==1.3
1
+Babel==2.2
2 2
 Beaker==1.6.4
3 3
 CherryPy==3.6.0
4 4
 FormEncode==1.3.0a1
@@ -38,7 +38,7 @@ tg.devtools==2.3.7
38 38
 tgext.admin==0.6.4
39 39
 tgext.asyncjob==0.3.1
40 40
 tgext.crud==0.7.3
41
-tgext.pluggable==0.5.4
41
+tgext.pluggable==0.5.5
42 42
 transaction==1.4.4
43 43
 tw2.core==2.2.2
44 44
 tw2.forms==2.2.2.1
@@ -47,3 +47,5 @@ zope.interface==4.1.3
47 47
 zope.sqlalchemy==0.7.6
48 48
 tgapp-resetpassword==0.1.3
49 49
 lxml
50
+python-ldap-test==0.2.0
51
+who-ldap==3.1.0

+ 29 - 0
tracim/development.ini.base View File

@@ -38,6 +38,35 @@ cache_dir = %(here)s/data
38 38
 beaker.session.key = tracim
39 39
 beaker.session.secret = 3283411b-1904-4554-b0e1-883863b53080
40 40
 
41
+# Auth type (internal or ldap)
42
+auth_type = internal
43
+
44
+# If auth_type is ldap, uncomment following ldap_* parameters
45
+
46
+# LDAP server address
47
+# ldap_url = ldap://localhost:389
48
+
49
+# Base dn to make queries
50
+# ldap_base_dn = dc=directory,dc=fsf,dc=org
51
+
52
+# Bind dn to identify the search
53
+# ldap_bind_dn = cn=admin,dc=directory,dc=fsf,dc=org
54
+
55
+# The bind password
56
+# ldap_bind_pass = toor
57
+
58
+# Attribute name of user record who contain user login (email)
59
+# ldap_ldap_naming_attribute = uid
60
+
61
+# Matching between ldap attribute and ldap user field (ldap_attr1=user_field1,ldap_attr2=user_field2,...)
62
+# ldap_user_attributes = mail=email
63
+
64
+# TLS usage to communicate with your LDAP server
65
+# ldap_tls = False
66
+
67
+# If True, LDAP own tracim group managment (not available for now!)
68
+# ldap_group_enabled = False
69
+
41 70
 #By default session is store in cookies to avoid the overhead
42 71
 #of having to manage a session storage. On production you might
43 72
 #want to switch to a better session storage.

+ 22 - 0
tracim/migration/versions/b73e57760b36_add_user_field_for_imported_profiles.py View File

@@ -0,0 +1,22 @@
1
+"""Add User field for imported profiles
2
+
3
+Revision ID: b73e57760b36
4
+Revises: 43a323cc661
5
+Create Date: 2016-02-09 11:00:22.694054
6
+
7
+"""
8
+
9
+# revision identifiers, used by Alembic.
10
+revision = 'b73e57760b36'
11
+down_revision = '43a323cc661'
12
+
13
+import sqlalchemy as sa
14
+from alembic import op
15
+
16
+
17
+def upgrade():
18
+    op.add_column('users', sa.Column('imported_from', sa.Unicode(length=32), nullable=True))
19
+
20
+
21
+def downgrade():
22
+    op.drop_column('users', 'imported_from')

+ 9 - 2
tracim/setup.py View File

@@ -32,13 +32,15 @@ testpkgs=['WebTest >= 1.2.3',
32 32
                ]
33 33
 
34 34
 install_requires=[
35
-    "TurboGears2 >= 2.3.7",
35
+    "TurboGears2==2.3.7",
36 36
     "Genshi",
37 37
     "Mako",
38 38
     "zope.sqlalchemy >= 0.4",
39 39
     "sqlalchemy",
40 40
     "alembic",
41 41
     "repoze.who",
42
+    "who-ldap==3.1.0",
43
+    "python-ldap-test==0.2.0",
42 44
     ]
43 45
 
44 46
 setup(
@@ -67,10 +69,15 @@ setup(
67 69
         ],
68 70
         'gearbox.plugins': [
69 71
             'turbogears-devtools = tg.devtools'
72
+        ],
73
+        'gearbox.commands': [
74
+            'ldap_server = tracim.command.ldap_test_server:LDAPTestServerCommand',
75
+            'user_create = tracim.command.user:CreateUserCommand',
76
+            'user_update = tracim.command.user:UpdateUserCommand',
70 77
         ]
71 78
     },
72 79
     dependency_links=[
73
-        "http://tg.gy/230"
80
+        "http://tg.gy/230",
74 81
         ],
75 82
     zip_safe=False
76 83
 )

+ 13 - 0
tracim/test.ini View File

@@ -23,4 +23,17 @@ use = config:development.ini
23 23
 use = main
24 24
 skip_authentication = True
25 25
 
26
+[app:ldap]
27
+sqlalchemy.url = postgresql://postgres:dummy@127.0.0.1:5432/tracim_test?client_encoding=utf8
28
+auth_type = ldap
29
+ldap_url = ldap://localhost:3333
30
+ldap_base_dn = dc=directory,dc=fsf,dc=org
31
+ldap_bind_dn = cn=admin,dc=directory,dc=fsf,dc=org
32
+ldap_bind_pass = toor
33
+ldap_ldap_naming_attribute = uid
34
+ldap_user_attributes = mail=email,pubname=display_name
35
+ldap_tls = False
36
+ldap_group_enabled = False
37
+use = config:development.ini
38
+
26 39
 # Add additional test specific configuration options as necessary.

+ 105 - 0
tracim/tracim/command/__init__.py View File

@@ -0,0 +1,105 @@
1
+# -*- coding: utf-8 -*-
2
+import argparse
3
+import os
4
+import sys
5
+
6
+import transaction
7
+from gearbox.command import Command
8
+from paste.deploy import loadapp
9
+from webtest import TestApp
10
+
11
+from tracim.lib.exception import CommandAbortedError
12
+
13
+
14
+class BaseCommand(Command):
15
+    """ Setup ap at take_action call """
16
+    auto_setup_app = True
17
+
18
+    def run(self, parsed_args):
19
+        try:
20
+            super().run(parsed_args)
21
+        except CommandAbortedError as exc:
22
+            if parsed_args.raise_error:
23
+                raise
24
+            print(exc)
25
+
26
+    def get_parser(self, prog_name):
27
+        parser = super().get_parser(prog_name)
28
+
29
+        parser.add_argument(
30
+            "--raise",
31
+            help='Raise CommandAbortedError errors instead print it\'s message',
32
+            dest='raise_error',
33
+            action='store_true',
34
+        )
35
+
36
+        return parser
37
+
38
+
39
+class AppContextCommand(BaseCommand):
40
+    """
41
+    Command who initialize app context at beginning of take_action method.
42
+    """
43
+
44
+    def __init__(self, *args, **kwargs):
45
+        super(AppContextCommand, self).__init__(*args, **kwargs)
46
+
47
+    @staticmethod
48
+    def _get_initialized_app_context(parsed_args):
49
+        """
50
+        :param parsed_args: parsed args (eg. from take_action)
51
+        :return: (wsgi_app, test_app)
52
+        """
53
+        config_file = parsed_args.config_file
54
+        config_name = 'config:%s' % config_file
55
+        here_dir = os.getcwd()
56
+
57
+        # Load locals and populate with objects for use in shell
58
+        sys.path.insert(0, here_dir)
59
+
60
+        # Load the wsgi app first so that everything is initialized right
61
+        wsgi_app = loadapp(config_name, relative_to=here_dir)
62
+        test_app = TestApp(wsgi_app)
63
+
64
+        # Make available the tg.request and other global variables
65
+        tresponse = test_app.get('/_test_vars')
66
+
67
+        return wsgi_app, test_app
68
+
69
+    def take_action(self, parsed_args):
70
+        super(AppContextCommand, self).take_action(parsed_args)
71
+        if self.auto_setup_app:
72
+            self._get_initialized_app_context(parsed_args)
73
+
74
+    def get_parser(self, prog_name):
75
+        parser = super(AppContextCommand, self).get_parser(prog_name)
76
+
77
+        parser.add_argument("-c", "--config",
78
+                            help='application config file to read (default: development.ini)',
79
+                            dest='config_file', default="development.ini")
80
+        return parser
81
+
82
+    def run(self, parsed_args):
83
+        super().run(parsed_args)
84
+        transaction.commit()
85
+
86
+
87
+class Extender(argparse.Action):
88
+    """
89
+    Copied class from http://stackoverflow.com/a/12461237/801924
90
+    """
91
+    def __call__(self, parser, namespace, values, option_strings=None):
92
+        # Need None here incase `argparse.SUPPRESS` was supplied for `dest`
93
+        dest = getattr(namespace, self.dest, None)
94
+        # print dest,self.default,values,option_strings
95
+        if not hasattr(dest, 'extend') or dest == self.default:
96
+            dest = []
97
+            setattr(namespace, self.dest, dest)
98
+            # if default isn't set to None, this method might be called
99
+            # with the default as `values` for other arguements which
100
+            # share this destination.
101
+            parser.set_defaults(**{self.dest: None})
102
+        try:
103
+            dest.extend(values)
104
+        except ValueError:
105
+            dest.append(values)

+ 22 - 0
tracim/tracim/command/ldap_test_server.py View File

@@ -0,0 +1,22 @@
1
+from time import sleep
2
+from tracim.fixtures.ldap import ldap_test_server_fixtures
3
+from gearbox.command import Command
4
+from ldap_test import LdapServer
5
+
6
+
7
+class LDAPTestServerCommand(Command):
8
+
9
+    def get_description(self):
10
+        return '''Run a test LDAP server.'''
11
+
12
+    def take_action(self, parsed_args):
13
+        # TODO - B.S. - 20160210: paramètre argv pour preciser les fixtures
14
+        server = LdapServer(ldap_test_server_fixtures)
15
+        print("Starting LDAP server on localhost (port %d)" % ldap_test_server_fixtures.get('port'))
16
+        print("Press CTRL+C to stop it")
17
+        server.start()
18
+        try:
19
+            while True:
20
+                sleep(1)
21
+        except KeyboardInterrupt:
22
+            pass

+ 169 - 0
tracim/tracim/command/user.py View File

@@ -0,0 +1,169 @@
1
+# -*- coding: utf-8 -*-
2
+import transaction
3
+from sqlalchemy.exc import IntegrityError
4
+from tg import config
5
+
6
+from tracim.command import AppContextCommand, Extender
7
+from tracim.lib.auth.ldap import LDAPAuth
8
+from tracim.lib.exception import AlreadyExistError, CommandAbortedError
9
+from tracim.lib.group import GroupApi
10
+from tracim.lib.user import UserApi
11
+from tracim.model import DBSession, User
12
+
13
+
14
+class UserCommand(AppContextCommand):
15
+
16
+    ACTION_CREATE = 'create'
17
+    ACTION_UPDATE = 'update'
18
+
19
+    action = NotImplemented
20
+
21
+    def __init__(self, *args, **kwargs):
22
+        super().__init__(*args, **kwargs)
23
+        self._session = DBSession
24
+        self._transaction = transaction
25
+        self._user_api = UserApi(None)
26
+        self._group_api = GroupApi(None)
27
+
28
+    def get_description(self):
29
+        return '''Create or update user.'''
30
+
31
+    def get_parser(self, prog_name):
32
+        parser = super().get_parser(prog_name)
33
+
34
+        parser.add_argument(
35
+            "-l",
36
+            "--login",
37
+            help='User login (email)',
38
+            dest='login',
39
+            required=True
40
+        )
41
+
42
+        parser.add_argument(
43
+            "-p",
44
+            "--password",
45
+            help='User password',
46
+            dest='password',
47
+            required=False,
48
+            default=None
49
+        )
50
+
51
+        parser.add_argument(
52
+            "-g",
53
+            "--add-to-group",
54
+            help='Add user to group',
55
+            dest='add_to_group',
56
+            nargs='*',
57
+            action=Extender,
58
+            default=[],
59
+        )
60
+
61
+        parser.add_argument(
62
+            "-rmg",
63
+            "--remove-from-group",
64
+            help='Remove user from group',
65
+            dest='remove_from_group',
66
+            nargs='*',
67
+            action=Extender,
68
+            default=[],
69
+        )
70
+
71
+        return parser
72
+
73
+    def _user_exist(self, login):
74
+        return self._user_api.user_with_email_exists(login)
75
+
76
+    def _get_group(self, name):
77
+        return self._group_api.get_one_with_name(name)
78
+
79
+    def _add_user_to_named_group(self, user, group_name):
80
+        group = self._get_group(group_name)
81
+        if user not in group.users:
82
+            group.users.append(user)
83
+        self._session.flush()
84
+
85
+    def _remove_user_from_named_group(self, user, group_name):
86
+        group = self._get_group(group_name)
87
+        if user in group.users:
88
+            group.users.remove(user)
89
+        self._session.flush()
90
+
91
+    def _create_user(self, login, password, **kwargs):
92
+        if not password:
93
+            if self._password_required():
94
+                raise CommandAbortedError("You must provide -p/--password parameter")
95
+            password = ''
96
+
97
+        try:
98
+            user = User(email=login, password=password, **kwargs)
99
+            self._session.add(user)
100
+            self._session.flush()
101
+        except IntegrityError:
102
+            self._session.rollback()
103
+            raise AlreadyExistError()
104
+
105
+        return user
106
+
107
+    def _update_password_for_login(self, login, password):
108
+        user = self._user_api.get_one_by_email(login)
109
+        user.password = password
110
+        self._session.flush()
111
+        transaction.commit()
112
+
113
+    def take_action(self, parsed_args):
114
+        super().take_action(parsed_args)
115
+
116
+        user = self._proceed_user(parsed_args)
117
+        self._proceed_groups(user, parsed_args)
118
+
119
+        print("User created/updated")
120
+
121
+    def _proceed_user(self, parsed_args):
122
+        self._check_context(parsed_args)
123
+
124
+        if self.action == self.ACTION_CREATE:
125
+            try:
126
+                user = self._create_user(login=parsed_args.login, password=parsed_args.password)
127
+            except AlreadyExistError:
128
+                raise CommandAbortedError("Error: User already exist (use `user update` command instead)")
129
+        else:
130
+            if parsed_args.password:
131
+                self._update_password_for_login(login=parsed_args.login, password=parsed_args.password)
132
+            user = self._user_api.get_one_by_email(parsed_args.login)
133
+
134
+        return user
135
+
136
+    def _proceed_groups(self, user, parsed_args):
137
+        # User always in "users" group
138
+        self._add_user_to_named_group(user, 'users')
139
+
140
+        for group_name in parsed_args.add_to_group:
141
+            self._add_user_to_named_group(user, group_name)
142
+
143
+        for group_name in parsed_args.remove_from_group:
144
+            self._remove_user_from_named_group(user, group_name)
145
+
146
+    def _password_required(self):
147
+        if config.get('auth_type') == LDAPAuth.name:
148
+            return False
149
+        return True
150
+
151
+    def _check_context(self, parsed_args):
152
+        if config.get('auth_type') == LDAPAuth.name:
153
+            auth_instance = config.get('auth_instance')
154
+            if not auth_instance.ldap_auth.user_exist(parsed_args.login):
155
+                raise LDAPUserUnknown(
156
+                    "LDAP is enabled and user with login/email \"%s\" not found in LDAP" % parsed_args.login
157
+                )
158
+
159
+
160
+class CreateUserCommand(UserCommand):
161
+    action = UserCommand.ACTION_CREATE
162
+
163
+
164
+class UpdateUserCommand(UserCommand):
165
+    action = UserCommand.ACTION_UPDATE
166
+
167
+
168
+class LDAPUserUnknown(CommandAbortedError):
169
+    pass

+ 14 - 0
tracim/tracim/config/__init__.py View File

@@ -1,2 +1,16 @@
1 1
 # -*- coding: utf-8 -*-
2
+from tg import AppConfig
2 3
 
4
+from tracim.lib.auth.wrapper import AuthConfigWrapper
5
+
6
+
7
+class TracimAppConfig(AppConfig):
8
+    """
9
+    Tracim specific config processes.
10
+    """
11
+
12
+    def after_init_config(self, conf):
13
+        AuthConfigWrapper.wrap(conf)
14
+        #  Fix tg2 problem: https://groups.google.com/forum/#!topic/turbogears/oL_04O6eCQQ
15
+        self.auth_backend = conf.get('auth_backend')
16
+        self.sa_auth = conf.get('sa_auth')

+ 5 - 57
tracim/tracim/config/app_cfg.py View File

@@ -16,7 +16,6 @@ convert them into boolean, for example, you should use the
16 16
 import tg
17 17
 from paste.deploy.converters import asbool
18 18
 
19
-from tg.configuration import AppConfig
20 19
 from tgext.pluggable import plug
21 20
 from tgext.pluggable import replace_template
22 21
 
@@ -24,12 +23,14 @@ from tg.i18n import lazy_ugettext as l_
24 23
 
25 24
 import tracim
26 25
 from tracim import model
26
+from tracim.config import TracimAppConfig
27 27
 from tracim.lib import app_globals, helpers
28
+from tracim.lib.auth.wrapper import AuthConfigWrapper
28 29
 from tracim.lib.base import logger
29 30
 from tracim.model.data import ActionDescription
30 31
 from tracim.model.data import ContentType
31 32
 
32
-base_config = AppConfig()
33
+base_config = TracimAppConfig()
33 34
 base_config.renderers = []
34 35
 base_config.use_toscawidgets = False
35 36
 base_config.use_toscawidgets2 = True
@@ -50,6 +51,8 @@ base_config.use_sqlalchemy = True
50 51
 base_config.model = tracim.model
51 52
 base_config.DBSession = tracim.model.DBSession
52 53
 
54
+# This value can be modified by tracim.lib.auth.wrapper.AuthConfigWrapper but have to be specified before
55
+base_config.auth_backend = 'sqlalchemy'
53 56
 
54 57
 # base_config.flash.cookie_name
55 58
 # base_config.flash.default_status -> Default message status if not specified (ok by default)
@@ -72,61 +75,6 @@ base_config['flash.template'] = '''
72 75
 # YOU MUST CHANGE THIS VALUE IN PRODUCTION TO SECURE YOUR APP 
73 76
 base_config.sa_auth.cookie_secret = "3283411b-1904-4554-b0e1-883863b53080"
74 77
 
75
-base_config.auth_backend = 'sqlalchemy'
76
-
77
-# what is the class you want to use to search for users in the database
78
-base_config.sa_auth.user_class = model.User
79
-
80
-from tg.configuration.auth import TGAuthMetadata
81
-
82
-from sqlalchemy import and_
83
-#This tells to TurboGears how to retrieve the data for your user
84
-class ApplicationAuthMetadata(TGAuthMetadata):
85
-
86
-    def __init__(self, sa_auth):
87
-        self.sa_auth = sa_auth
88
-
89
-    def authenticate(self, environ, identity):
90
-        user = self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(and_(
91
-            self.sa_auth.user_class.is_active==True,
92
-            self.sa_auth.user_class.email==identity['login']
93
-        )).first()
94
-
95
-        if user and user.validate_password(identity['password']):
96
-            return identity['login']
97
-    def get_user(self, identity, userid):
98
-        return self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(and_(self.sa_auth.user_class.is_active==True, self.sa_auth.user_class.email==userid)).first()
99
-    def get_groups(self, identity, userid):
100
-        return [g.group_name for g in identity['user'].groups]
101
-    def get_permissions(self, identity, userid):
102
-        return [p.permission_name for p in identity['user'].permissions]
103
-
104
-base_config.sa_auth.dbsession = model.DBSession
105
-
106
-base_config.sa_auth.authmetadata = ApplicationAuthMetadata(base_config.sa_auth)
107
-
108
-# You can use a different repoze.who Authenticator if you want to
109
-# change the way users can login
110
-#base_config.sa_auth.authenticators = [('myauth', SomeAuthenticator()]
111
-
112
-# You can add more repoze.who metadata providers to fetch
113
-# user metadata.
114
-# Remember to set base_config.sa_auth.authmetadata to None
115
-# to disable authmetadata and use only your own metadata providers
116
-#base_config.sa_auth.mdproviders = [('myprovider', SomeMDProvider()]
117
-
118
-# override this if you would like to provide a different who plugin for
119
-# managing login and logout of your application
120
-base_config.sa_auth.form_plugin = None
121
-
122
-# You may optionally define a page where you want users to be redirected to
123
-# on login:
124
-base_config.sa_auth.post_login_url = '/post_login'
125
-
126
-# You may optionally define a page where you want users to be redirected to
127
-# on logout:
128
-base_config.sa_auth.post_logout_url = '/post_logout'
129
-
130 78
 # INFO - This is the way to specialize the resetpassword email properties
131 79
 # plug(base_config, 'resetpassword', None, mail_subject=reset_password_email_subject)
132 80
 plug(base_config, 'resetpassword', 'reset_password')

+ 3 - 0
tracim/tracim/config/deployment.ini_tmpl View File

@@ -28,6 +28,9 @@ beaker.session.key = pod
28 28
 beaker.session.secret = ${app_instance_secret}
29 29
 app_instance_uuid = ${app_instance_uuid}
30 30
 
31
+# Auth type (internal or ldap)
32
+auth_type = internal
33
+
31 34
 # If you'd like to fine-tune the individual locations of the cache data dirs
32 35
 # for the Cache data, or the Session saves, un-comment the desired settings
33 36
 # here:

+ 2 - 0
tracim/tracim/controllers/__init__.py View File

@@ -126,6 +126,7 @@ class TIMRestController(RestController, BaseController):
126 126
         :param kw:
127 127
         :return:
128 128
         """
129
+        super()._before(*args, **kw)
129 130
         TIMRestPathContextSetup.current_user()
130 131
 
131 132
 
@@ -468,6 +469,7 @@ class StandardController(BaseController):
468 469
         :param kw:
469 470
         :return:
470 471
         """
472
+        super()._before(*args, **kw)
471 473
         TIMRestPathContextSetup.current_user()
472 474
 
473 475
     @classmethod

+ 1 - 1
tracim/tracim/controllers/error.py View File

@@ -30,7 +30,7 @@ class ErrorController(object):
30 30
     @expose('tracim.templates.error')
31 31
     def document(self, *args, **kwargs):
32 32
         """Render the error document"""
33
-        resp = request.environ.get('pylons.original_response')
33
+        resp = request.environ.get('tg.original_response')
34 34
         default_message = ('<p>We\'re sorry but we weren\'t able to process '
35 35
                            ' this request.</p>')
36 36
 

+ 29 - 1
tracim/tracim/controllers/user.py View File

@@ -1,4 +1,5 @@
1 1
 # -*- coding: utf-8 -*-
2
+from webob.exc import HTTPForbidden
2 3
 
3 4
 from tracim import model  as pm
4 5
 
@@ -93,11 +94,17 @@ class UserPasswordRestController(TIMRestController):
93 94
 
94 95
     @tg.expose('tracim.templates.user_password_edit_me')
95 96
     def edit(self):
97
+        if not tg.config.get('auth_is_internal'):
98
+            raise HTTPForbidden()
99
+
96 100
         dictified_user = Context(CTX.USER).toDict(tmpl_context.current_user, 'user')
97 101
         return DictLikeClass(result = dictified_user)
98 102
 
99 103
     @tg.expose()
100 104
     def put(self, current_password, new_password1, new_password2):
105
+        if not tg.config.get('auth_is_internal'):
106
+            raise HTTPForbidden()
107
+
101 108
         # FIXME - Allow only self password or operation for managers
102 109
         current_user = tmpl_context.current_user
103 110
 
@@ -174,9 +181,30 @@ class UserRestController(TIMRestController):
174 181
         current_user = tmpl_context.current_user
175 182
         assert user_id==current_user.user_id
176 183
 
184
+        # Only keep allowed field update
185
+        updated_fields = self._clean_update_fields({
186
+            'name': name,
187
+            'email': email
188
+        })
189
+
177 190
         api = UserApi(tmpl_context.current_user)
178
-        api.update(current_user, name, email, True)
191
+        api.update(current_user, do_save=True, **updated_fields)
179 192
         tg.flash(_('profile updated.'))
180 193
         if next_url:
181 194
             tg.redirect(tg.url(next_url))
182 195
         tg.redirect(self.url())
196
+
197
+    def _clean_update_fields(self, fields: dict):
198
+        """
199
+        Remove field key who are not allowed to be updated
200
+        :param fields: dict with field name key to be cleaned
201
+        :rtype fields: dict
202
+        :return:
203
+        """
204
+        auth_instance = tg.config.get('auth_instance')
205
+        if not auth_instance.is_internal:
206
+            externalized_fields_names = auth_instance.managed_fields
207
+            for externalized_field_name in externalized_fields_names:
208
+                if externalized_field_name in fields:
209
+                    fields.pop(externalized_field_name)
210
+        return fields

+ 39 - 0
tracim/tracim/fixtures/__init__.py View File

@@ -0,0 +1,39 @@
1
+import transaction
2
+
3
+from tracim.model import DBSession
4
+
5
+
6
+class Fixture(object):
7
+
8
+    """ Fixture classes (list) required for this fixtures"""
9
+    require = NotImplemented
10
+
11
+    def __init__(self, session):
12
+        self._session = session
13
+
14
+    def insert(self):
15
+        raise NotImplementedError()
16
+
17
+
18
+class FixturesLoader(object):
19
+    """
20
+    Fixtures loader. Load each fixture once.
21
+    """
22
+
23
+    def __init__(self, loaded=None):
24
+        loaded = [] if loaded is None else loaded
25
+        self._loaded = loaded
26
+
27
+    def loads(self, fixtures_classes):
28
+        for fixture_class in fixtures_classes:
29
+            for required_fixture_class in fixture_class.require:
30
+                self._load(required_fixture_class)
31
+            self._load(fixture_class)
32
+
33
+    def _load(self, fixture_class):
34
+        if fixture_class not in self._loaded:
35
+            fixture = fixture_class(DBSession)
36
+            fixture.insert()
37
+            self._loaded.append(fixture_class)
38
+            DBSession.flush()
39
+            transaction.commit()

+ 58 - 0
tracim/tracim/fixtures/ldap.py View File

@@ -0,0 +1,58 @@
1
+ldap_test_server_fixtures = {
2
+    'port': 3333,
3
+    'password': 'toor',
4
+
5
+    'bind_dn': 'cn=admin,dc=directory,dc=fsf,dc=org',
6
+    'base': {
7
+        'objectclass': ['dcObject', 'organization'],
8
+        'dn': 'dc=directory,dc=fsf,dc=org',
9
+        'attributes': {
10
+            'o': 'Free Software Foundation',
11
+            'dc': 'directory'
12
+        }
13
+    },
14
+
15
+    'entries': [
16
+        {
17
+            'objectclass': ['organizationalRole'],
18
+            'dn': 'cn=admin,dc=directory,dc=fsf,dc=org',
19
+            'attributes': {
20
+                'cn': 'admin'
21
+            }
22
+        },
23
+        {
24
+            'objectclass': ['organizationalUnit'],
25
+            'dn': 'ou=people,dc=directory,dc=fsf,dc=org',
26
+            'attributes': {
27
+                'ou': 'people',
28
+            }
29
+        },
30
+        {
31
+            'objectclass': ['organizationalUnit'],
32
+            'dn': 'ou=groups,dc=directory,dc=fsf,dc=org',
33
+            'attributes': {
34
+                'ou': 'groups',
35
+            }
36
+        },
37
+        {
38
+            'objectclass': ['account', 'top'],
39
+            'dn': 'cn=richard-not-real-email@fsf.org,ou=people,dc=directory,dc=fsf,dc=org',
40
+            'attributes': {
41
+                'uid': 'richard-not-real-email@fsf.org',
42
+                'userPassword': 'rms',
43
+                'mail': 'richard-not-real-email@fsf.org',
44
+                'pubname': 'Richard Stallman',
45
+            }
46
+        },
47
+        {
48
+            'objectclass': ['account', 'top'],
49
+            'dn': 'cn=lawrence-not-real-email@fsf.org,ou=people,dc=directory,dc=fsf,dc=org',
50
+            'attributes': {
51
+                'uid': 'lawrence-not-real-email@fsf.org',
52
+                'userPassword': 'foobarbaz',
53
+                'mail': 'lawrence-not-real-email@fsf.org',
54
+                'pubname': 'Lawrence Lessig',
55
+            }
56
+        },
57
+    ]
58
+}

+ 49 - 0
tracim/tracim/fixtures/users_and_groups.py View File

@@ -0,0 +1,49 @@
1
+# -*- coding: utf-8 -*-
2
+from tracim import model
3
+from tracim.fixtures import Fixture
4
+
5
+
6
+class Base(Fixture):
7
+    require = []
8
+
9
+    def insert(self):
10
+        u = model.User()
11
+        u.display_name = 'Global manager'
12
+        u.email = 'admin@admin.admin'
13
+        u.password = 'admin@admin.admin'
14
+        self._session.add(u)
15
+
16
+        g1 = model.Group()
17
+        g1.group_id = 1
18
+        g1.group_name = 'users'
19
+        g1.display_name = 'Users'
20
+        g1.users.append(u)
21
+        self._session.add(g1)
22
+
23
+        g2 = model.Group()
24
+        g2.group_id = 2
25
+        g2.group_name = 'managers'
26
+        g2.display_name = 'Global Managers'
27
+        g2.users.append(u)
28
+        self._session.add(g2)
29
+
30
+        g3 = model.Group()
31
+        g3.group_id = 3
32
+        g3.group_name = 'administrators'
33
+        g3.display_name = 'Administrators'
34
+        g3.users.append(u)
35
+        self._session.add(g3)
36
+
37
+
38
+class Test(Fixture):
39
+    require = [Base, ]
40
+
41
+    def insert(self):
42
+        g2 = self._session.query(model.Group).filter(model.Group.group_name == 'managers').one()
43
+
44
+        lawrence = model.User()
45
+        lawrence.display_name = 'Lawrence L.'
46
+        lawrence.email = 'lawrence-not-real-email@fsf.org'
47
+        lawrence.password = 'foobarbaz'
48
+        self._session.add(lawrence)
49
+        g2.users.append(lawrence)

BIN
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.mo View File


+ 79 - 73
tracim/tracim/i18n/fr/LC_MESSAGES/tracim.po View File

@@ -1,27 +1,28 @@
1
-# French (France) translations for pod.
2
-# Copyright (C) 2014 ORGANIZATION
3
-# This file is distributed under the same license as the pod project.
4
-# FIRST AUTHOR <EMAIL@ADDRESS>, 2014.
1
+# French translations for tracim.
2
+# Copyright (C) 2016 ORGANIZATION
3
+# This file is distributed under the same license as the tracim project.
4
+# FIRST AUTHOR <EMAIL@ADDRESS>, 2016.
5 5
 #
6 6
 msgid ""
7 7
 msgstr ""
8 8
 "Project-Id-Version: pod 0.1\n"
9 9
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
10
-"POT-Creation-Date: 2015-09-08 17:43+0200\n"
10
+"POT-Creation-Date: 2016-02-18 14:37+0100\n"
11 11
 "PO-Revision-Date: 2015-09-08 15:27+0100\n"
12 12
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
13
+"Language: fr\n"
13 14
 "Language-Team: fr_FR <LL@li.org>\n"
14 15
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
15 16
 "MIME-Version: 1.0\n"
16 17
 "Content-Type: text/plain; charset=utf-8\n"
17 18
 "Content-Transfer-Encoding: 8bit\n"
18
-"Generated-By: Babel 1.3\n"
19
+"Generated-By: Babel 2.2.0\n"
19 20
 
20
-#: tracim/config/app_cfg.py:137
21
+#: tracim/config/app_cfg.py:86
21 22
 msgid "Password reset request"
22 23
 msgstr "Réinitialisation du mot de passe"
23 24
 
24
-#: tracim/config/app_cfg.py:138 tracim/config/app_cfg.py:167
25
+#: tracim/config/app_cfg.py:87 tracim/config/app_cfg.py:116
25 26
 #, python-format
26 27
 msgid ""
27 28
 "\n"
@@ -44,117 +45,117 @@ msgstr ""
44 45
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
45 46
 "e-mail.\n"
46 47
 
47
-#: tracim/config/app_cfg.py:154 tracim/templates/widgets/forms.mak:15
48
+#: tracim/config/app_cfg.py:103 tracim/templates/widgets/forms.mak:15
48 49
 #: tracim/templates/widgets/forms.mak:16 tracim/templates/widgets/forms.mak:39
49 50
 #: tracim/templates/widgets/forms.mak:40
50 51
 msgid "New password"
51 52
 msgstr "Nouveau mot de passe"
52 53
 
53
-#: tracim/config/app_cfg.py:155
54
+#: tracim/config/app_cfg.py:104
54 55
 msgid "Confirm new password"
55 56
 msgstr "Confirmer le nouveau mot de passe"
56 57
 
57
-#: tracim/config/app_cfg.py:156
58
+#: tracim/config/app_cfg.py:105
58 59
 msgid "Save new password"
59 60
 msgstr "Enregistrer le nouveau mot de passe"
60 61
 
61
-#: tracim/config/app_cfg.py:157 tracim/templates/admin/user_getall.mak:48
62
+#: tracim/config/app_cfg.py:106 tracim/templates/admin/user_getall.mak:48
62 63
 msgid "Email address"
63 64
 msgstr "Adresse email"
64 65
 
65
-#: tracim/config/app_cfg.py:158
66
+#: tracim/config/app_cfg.py:107
66 67
 msgid "Send Request"
67 68
 msgstr "Valider"
68 69
 
69
-#: tracim/config/app_cfg.py:161
70
+#: tracim/config/app_cfg.py:110
70 71
 msgid "Password reset request sent"
71 72
 msgstr "Requête de réinitialisation du mot de passe envoyée"
72 73
 
73
-#: tracim/config/app_cfg.py:162 tracim/config/app_cfg.py:164
74
+#: tracim/config/app_cfg.py:111 tracim/config/app_cfg.py:113
74 75
 msgid "Invalid password reset request"
75 76
 msgstr "Requête de réinitialisation du mot de passe invalide"
76 77
 
77
-#: tracim/config/app_cfg.py:163
78
+#: tracim/config/app_cfg.py:112
78 79
 msgid "Password reset request timed out"
79 80
 msgstr "Echec de la requête de réinitialisation du mot de passe"
80 81
 
81
-#: tracim/config/app_cfg.py:165
82
+#: tracim/config/app_cfg.py:114
82 83
 msgid "Password changed successfully"
83 84
 msgstr "Mot de passe changé"
84 85
 
85
-#: tracim/controllers/__init__.py:272 tracim/controllers/content.py:313
86
+#: tracim/controllers/__init__.py:273 tracim/controllers/content.py:313
86 87
 #: tracim/controllers/content.py:426
87 88
 msgid "{} updated"
88 89
 msgstr "{} mis(e) à jour"
89 90
 
90
-#: tracim/controllers/__init__.py:277 tracim/controllers/content.py:431
91
+#: tracim/controllers/__init__.py:278 tracim/controllers/content.py:431
91 92
 msgid "{} not updated: the content did not change"
92 93
 msgstr "{} non mis à jour : le contenu n'a pas changé"
93 94
 
94
-#: tracim/controllers/__init__.py:282 tracim/controllers/content.py:318
95
+#: tracim/controllers/__init__.py:283 tracim/controllers/content.py:318
95 96
 #: tracim/controllers/content.py:436
96 97
 msgid "{} not updated - error: {}"
97 98
 msgstr "{} pas mis(e) à jour - erreur : {}"
98 99
 
99
-#: tracim/controllers/__init__.py:296
100
+#: tracim/controllers/__init__.py:297
100 101
 msgid "{} status updated"
101 102
 msgstr "Statut de {} mis(e) à jour"
102 103
 
103
-#: tracim/controllers/__init__.py:300
104
+#: tracim/controllers/__init__.py:301
104 105
 msgid "{} status not updated: {}"
105 106
 msgstr "Statut de {} non mis(e) à jour : {}"
106 107
 
107
-#: tracim/controllers/__init__.py:332 tracim/controllers/content.py:839
108
+#: tracim/controllers/__init__.py:333 tracim/controllers/content.py:839
108 109
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
109 110
 msgstr "{} archivé(e). <a class=\"alert-link\" href=\"{}\">Annuler l'opération</a>"
110 111
 
111
-#: tracim/controllers/__init__.py:341 tracim/controllers/content.py:848
112
+#: tracim/controllers/__init__.py:342 tracim/controllers/content.py:848
112 113
 msgid "{} not archived: {}"
113 114
 msgstr "{} non archivé(e) : {}"
114 115
 
115
-#: tracim/controllers/__init__.py:354 tracim/controllers/content.py:862
116
+#: tracim/controllers/__init__.py:355 tracim/controllers/content.py:862
116 117
 msgid "{} unarchived."
117 118
 msgstr "{} désarchivé(e)"
118 119
 
119
-#: tracim/controllers/__init__.py:362 tracim/controllers/content.py:870
120
+#: tracim/controllers/__init__.py:363 tracim/controllers/content.py:870
120 121
 msgid "{} not un-archived: {}"
121 122
 msgstr "{} non désarchivé(e) : {}"
122 123
 
123
-#: tracim/controllers/__init__.py:380 tracim/controllers/content.py:90
124
+#: tracim/controllers/__init__.py:381 tracim/controllers/content.py:90
124 125
 #: tracim/controllers/content.py:887
125 126
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
126 127
 msgstr ""
127 128
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
128 129
 "l'opération</a>"
129 130
 
130
-#: tracim/controllers/__init__.py:389 tracim/controllers/content.py:101
131
+#: tracim/controllers/__init__.py:390 tracim/controllers/content.py:101
131 132
 #: tracim/controllers/content.py:896
132 133
 msgid "{} not deleted: {}"
133 134
 msgstr "{} non supprimé(e) : {}"
134 135
 
135
-#: tracim/controllers/__init__.py:404 tracim/controllers/content.py:118
136
+#: tracim/controllers/__init__.py:405 tracim/controllers/content.py:118
136 137
 #: tracim/controllers/content.py:911
137 138
 msgid "{} undeleted."
138 139
 msgstr "{} restauré(e)."
139 140
 
140
-#: tracim/controllers/__init__.py:414 tracim/controllers/content.py:130
141
+#: tracim/controllers/__init__.py:415 tracim/controllers/content.py:130
141 142
 #: tracim/controllers/content.py:921
142 143
 msgid "{} not un-deleted: {}"
143 144
 msgstr "{} non restauré(e) : {}"
144 145
 
145
-#: tracim/controllers/__init__.py:428
146
+#: tracim/controllers/__init__.py:429
146 147
 msgid "{} marked as read."
147 148
 msgstr "{} marqué \"lu\"."
148 149
 
149
-#: tracim/controllers/__init__.py:436
150
+#: tracim/controllers/__init__.py:437
150 151
 msgid "{} not marked as read: {}"
151 152
 msgstr "{} non marqué \"lu\" : {}"
152 153
 
153
-#: tracim/controllers/__init__.py:450
154
+#: tracim/controllers/__init__.py:451
154 155
 msgid "{} marked unread."
155 156
 msgstr "{} marqué \"non lu\"."
156 157
 
157
-#: tracim/controllers/__init__.py:458
158
+#: tracim/controllers/__init__.py:459
158 159
 msgid "{} not marked unread: {}"
159 160
 msgstr "{} non marqué \"non lu\" : {}"
160 161
 
@@ -247,31 +248,31 @@ msgstr "Bonjour %s. Ravis de vous revoir !"
247 248
 msgid "Successfully logged out. We hope to see you soon!"
248 249
 msgstr "Déconnexion réussie. Nous espérons vous revoir bientôt !"
249 250
 
250
-#: tracim/controllers/user.py:54
251
+#: tracim/controllers/user.py:55
251 252
 msgid "Notification enabled for workspace {}"
252 253
 msgstr "Notifications activées pour l'espace de travail {}"
253 254
 
254
-#: tracim/controllers/user.py:67
255
+#: tracim/controllers/user.py:68
255 256
 msgid "Notification disabled for workspace {}"
256 257
 msgstr "Notifications désactivées pour l'espace de travail {}"
257 258
 
258
-#: tracim/controllers/user.py:107 tracim/controllers/admin/user.py:203
259
+#: tracim/controllers/user.py:114 tracim/controllers/admin/user.py:203
259 260
 msgid "Empty password is not allowed."
260 261
 msgstr "Le mot de passe ne doit pas être vide"
261 262
 
262
-#: tracim/controllers/user.py:111
263
+#: tracim/controllers/user.py:118
263 264
 msgid "The current password you typed is wrong"
264
-msgstr "Le mot de passe que vous avez tapé est erroné"
265
+msgstr "Le mot de passe saisi est erroné"
265 266
 
266
-#: tracim/controllers/user.py:115 tracim/controllers/admin/user.py:207
267
+#: tracim/controllers/user.py:122 tracim/controllers/admin/user.py:207
267 268
 msgid "New passwords do not match."
268 269
 msgstr "Les mots de passe ne concordent pas"
269 270
 
270
-#: tracim/controllers/user.py:121
271
+#: tracim/controllers/user.py:128
271 272
 msgid "Your password has been changed"
272 273
 msgstr "Votre mot de passe a été changé"
273 274
 
274
-#: tracim/controllers/user.py:179
275
+#: tracim/controllers/user.py:192
275 276
 msgid "profile updated."
276 277
 msgstr "Profil mis à jour"
277 278
 
@@ -409,56 +410,56 @@ msgstr "Espaces de travail"
409 410
 msgid "The content did not changed"
410 411
 msgstr "Le contenu est inchangé"
411 412
 
412
-#: tracim/lib/helpers.py:34
413
+#: tracim/lib/helpers.py:33
413 414
 msgid "{date} at {time}"
414 415
 msgstr "{date} à {time}"
415 416
 
416
-#: tracim/lib/notifications.py:285
417
+#: tracim/lib/notifications.py:289
417 418
 msgid "<span id=\"content-intro-username\">{}</span> added a comment:"
418 419
 msgstr "<span id=\"content-intro-username\">{}</span> a ajouté un commentaire :"
419 420
 
420
-#: tracim/lib/notifications.py:287 tracim/lib/notifications.py:296
421
+#: tracim/lib/notifications.py:291 tracim/lib/notifications.py:300
421 422
 msgid "Answer"
422 423
 msgstr "Répondre"
423 424
 
424
-#: tracim/lib/notifications.py:293 tracim/lib/notifications.py:317
425
-#: tracim/lib/notifications.py:350
425
+#: tracim/lib/notifications.py:297 tracim/lib/notifications.py:321
426
+#: tracim/lib/notifications.py:354
426 427
 msgid "View online"
427 428
 msgstr "Voir en ligne"
428 429
 
429
-#: tracim/lib/notifications.py:297
430
+#: tracim/lib/notifications.py:301
430 431
 msgid "<span id=\"content-intro-username\">{}</span> started a thread entitled:"
431 432
 msgstr ""
432 433
 "<span id=\"content-intro-username\">{}</span> a lancé une discussion "
433 434
 "intitulée :"
434 435
 
435
-#: tracim/lib/notifications.py:302
436
+#: tracim/lib/notifications.py:306
436 437
 msgid "<span id=\"content-intro-username\">{}</span> added a file entitled:"
437 438
 msgstr ""
438 439
 "<span id=\"content-intro-username\">{}</span> a ajouté un fichier "
439 440
 "intitulé :"
440 441
 
441
-#: tracim/lib/notifications.py:312
442
+#: tracim/lib/notifications.py:316
442 443
 msgid "<span id=\"content-intro-username\">{}</span> added a page entitled:"
443 444
 msgstr ""
444 445
 "<span id=\"content-intro-username\">{}</span> a ajouté une page intitulée"
445 446
 " :"
446 447
 
447
-#: tracim/lib/notifications.py:320
448
+#: tracim/lib/notifications.py:324
448 449
 msgid "<span id=\"content-intro-username\">{}</span> uploaded a new revision."
449 450
 msgstr ""
450 451
 "<span id=\"content-intro-username\">{}</span> a téléchargé une nouvelle "
451 452
 "version."
452 453
 
453
-#: tracim/lib/notifications.py:324
454
+#: tracim/lib/notifications.py:328
454 455
 msgid "<span id=\"content-intro-username\">{}</span> updated this page."
455 456
 msgstr "<span id=\"content-intro-username\">{}</span> a mis à jour cette page."
456 457
 
457
-#: tracim/lib/notifications.py:329 tracim/lib/notifications.py:339
458
+#: tracim/lib/notifications.py:333 tracim/lib/notifications.py:343
458 459
 msgid "<p id=\"content-body-intro\">Here is an overview of the changes:</p>"
459 460
 msgstr "<p id=\"content-body-intro\">Voici un aperçu des modifications :</p>"
460 461
 
461
-#: tracim/lib/notifications.py:334
462
+#: tracim/lib/notifications.py:338
462 463
 msgid ""
463 464
 "<span id=\"content-intro-username\">{}</span> updated the thread "
464 465
 "description."
@@ -466,7 +467,7 @@ msgstr ""
466 467
 "<span id=\"content-intro-username\">{}</span> a mis à jour la description"
467 468
 " de la discussion."
468 469
 
469
-#: tracim/lib/notifications.py:353
470
+#: tracim/lib/notifications.py:357
470 471
 msgid ""
471 472
 "<span id=\"content-intro-username\">{}</span> updated the file "
472 473
 "description."
@@ -747,7 +748,7 @@ msgstr "Tableau de bord"
747 748
 msgid "Welcome to your home, {username}."
748 749
 msgstr "Bienvenue, {username}."
749 750
 
750
-#: tracim/templates/home.mak:50 tracim/templates/user_toolbars.mak:55
751
+#: tracim/templates/home.mak:50 tracim/templates/user_toolbars.mak:63
751 752
 msgid "Not Read"
752 753
 msgstr "Non lus"
753 754
 
@@ -759,7 +760,7 @@ msgstr "Aucun contenu non lu."
759 760
 msgid "Last activity: {datetime}"
760 761
 msgstr "Dernière activité : {datetime}"
761 762
 
762
-#: tracim/templates/home.mak:88 tracim/templates/user_toolbars.mak:56
763
+#: tracim/templates/home.mak:88 tracim/templates/user_toolbars.mak:64
763 764
 msgid "Recent Activity"
764 765
 msgstr "Activité récente"
765 766
 
@@ -940,53 +941,53 @@ msgstr "Modifier l'utilisateur courant"
940 941
 msgid "Edit user"
941 942
 msgstr "Modifier l'utilisateur"
942 943
 
943
-#: tracim/templates/user_toolbars.mak:18 tracim/templates/user_toolbars.mak:43
944
+#: tracim/templates/user_toolbars.mak:20 tracim/templates/user_toolbars.mak:49
944 945
 #: tracim/templates/widgets/forms.mak:7 tracim/templates/widgets/forms.mak:35
945 946
 msgid "Change password"
946 947
 msgstr "Changer le mot de passe"
947 948
 
948
-#: tracim/templates/user_toolbars.mak:18 tracim/templates/user_toolbars.mak:43
949
+#: tracim/templates/user_toolbars.mak:20 tracim/templates/user_toolbars.mak:49
949 950
 #: tracim/templates/admin/user_getall.mak:51
950 951
 msgid "Password"
951 952
 msgstr "Mot de passe"
952 953
 
953
-#: tracim/templates/user_toolbars.mak:24
954
+#: tracim/templates/user_toolbars.mak:28
954 955
 msgid "Disable user"
955 956
 msgstr "Désactiver l'utilisateur"
956 957
 
957
-#: tracim/templates/user_toolbars.mak:24
958
+#: tracim/templates/user_toolbars.mak:28
958 959
 msgid "Disable"
959 960
 msgstr "Désactiver"
960 961
 
961
-#: tracim/templates/user_toolbars.mak:26
962
+#: tracim/templates/user_toolbars.mak:30
962 963
 msgid "Enable user"
963 964
 msgstr "Activer l'utilisateur"
964 965
 
965
-#: tracim/templates/user_toolbars.mak:26
966
+#: tracim/templates/user_toolbars.mak:30
966 967
 msgid "Enable"
967 968
 msgstr "Activer"
968 969
 
969
-#: tracim/templates/user_toolbars.mak:42
970
+#: tracim/templates/user_toolbars.mak:46
970 971
 msgid "Edit my profile"
971 972
 msgstr "Mon profil"
972 973
 
973
-#: tracim/templates/user_toolbars.mak:47
974
+#: tracim/templates/user_toolbars.mak:55
974 975
 msgid "Go to..."
975 976
 msgstr "Voir..."
976 977
 
977
-#: tracim/templates/user_toolbars.mak:55
978
+#: tracim/templates/user_toolbars.mak:63
978 979
 msgid "Not read"
979 980
 msgstr "Non lus"
980 981
 
981
-#: tracim/templates/user_toolbars.mak:56
982
+#: tracim/templates/user_toolbars.mak:64
982 983
 msgid "Activity"
983 984
 msgstr "Activité"
984 985
 
985
-#: tracim/templates/user_toolbars.mak:57
986
+#: tracim/templates/user_toolbars.mak:65
986 987
 msgid "My Workspaces"
987 988
 msgstr "Mes espaces de travail"
988 989
 
989
-#: tracim/templates/user_toolbars.mak:57
990
+#: tracim/templates/user_toolbars.mak:65
990 991
 msgid "Spaces"
991 992
 msgstr "Espaces"
992 993
 
@@ -1006,8 +1007,8 @@ msgstr "Modifier le dossier"
1006 1007
 #: tracim/templates/user_workspace_forms.mak:14
1007 1008
 #: tracim/templates/user_workspace_forms.mak:15
1008 1009
 #: tracim/templates/user_workspace_forms.mak:100
1009
-#: tracim/templates/user_workspace_forms.mak:101
1010
-#: tracim/templates/user_workspace_forms.mak:105
1010
+#: tracim/templates/user_workspace_forms.mak:104
1011
+#: tracim/templates/user_workspace_forms.mak:111
1011 1012
 #: tracim/templates/admin/user_getall.mak:43
1012 1013
 #: tracim/templates/admin/user_getall.mak:44
1013 1014
 #: tracim/templates/admin/workspace_getall.mak:41
@@ -1044,7 +1045,7 @@ msgstr "des pages \"wiki\""
1044 1045
 #: tracim/templates/user_workspace_forms.mak:39
1045 1046
 #: tracim/templates/user_workspace_forms.mak:58
1046 1047
 #: tracim/templates/user_workspace_forms.mak:81
1047
-#: tracim/templates/user_workspace_forms.mak:110
1048
+#: tracim/templates/user_workspace_forms.mak:116
1048 1049
 #: tracim/templates/admin/user_getall.mak:66
1049 1050
 #: tracim/templates/admin/workspace_getall.mak:53
1050 1051
 #: tracim/templates/admin/workspace_getone.mak:93
@@ -1109,7 +1110,12 @@ msgstr "Message"
1109 1110
 msgid "Edit User"
1110 1111
 msgstr "Modifier l'utilisateur"
1111 1112
 
1112
-#: tracim/templates/user_workspace_forms.mak:104
1113
+#: tracim/templates/user_workspace_forms.mak:102
1114
+#: tracim/templates/user_workspace_forms.mak:109
1115
+msgid "This information is managed by an external application"
1116
+msgstr "Cette information est gérée par une application externe"
1117
+
1118
+#: tracim/templates/user_workspace_forms.mak:107
1113 1119
 #: tracim/templates/admin/user_getall.mak:47
1114 1120
 #: tracim/templates/admin/user_getall.mak:106
1115 1121
 msgid "Email"

+ 1 - 0
tracim/tracim/lib/auth/__init__.py View File

@@ -0,0 +1 @@
1
+from tracim.lib.auth.base import Auth

+ 66 - 0
tracim/tracim/lib/auth/base.py View File

@@ -0,0 +1,66 @@
1
+# -*- coding: utf-8 -*-
2
+from tg.util import Bunch
3
+
4
+""" Backup of config.sa_auth """
5
+_original_sa_auth = None
6
+
7
+
8
+def _get_clean_sa_auth(config):
9
+    """
10
+    Return the original sa_auth parameter. Consider Original as it's content before first fill in configuration.
11
+    :param config: tg2 app config
12
+    :return: original sa_auth parameter
13
+    """
14
+    global _original_sa_auth
15
+
16
+    if _original_sa_auth is None:
17
+        _original_sa_auth = dict(config.get('sa_auth'))
18
+
19
+    sa_auth = Bunch()
20
+    sa_auth.update(_original_sa_auth)
21
+    return sa_auth
22
+
23
+
24
+class Auth(object):
25
+    """
26
+    Auth strategy base class
27
+    """
28
+
29
+    """ Auth strategy must be named: .ini config will use this name in auth_type parameter """
30
+    name = NotImplemented
31
+
32
+    """ When Auth is not internal, user account management are disabled (forgotten password, etc.) """
33
+    _internal = NotImplemented
34
+
35
+    def __init__(self, config):
36
+        self._config = config
37
+        self._managed_fields = []
38
+
39
+    @property
40
+    def is_internal(self):
41
+        return bool(self._internal)
42
+
43
+    @property
44
+    def managed_fields(self):
45
+        return self._managed_fields
46
+
47
+    def feed_config(self):
48
+        """
49
+        Fill config with auth needed. You must overload with whild implementation.
50
+        :return:
51
+        """
52
+        self._config['sa_auth'] = _get_clean_sa_auth(self._config)
53
+
54
+        self._config['auth_is_internal'] = self.is_internal
55
+
56
+        # override this if you would like to provide a different who plugin for
57
+        # managing login and logout of your application
58
+        self._config['sa_auth'].form_plugin = None
59
+
60
+        # You may optionally define a page where you want users to be redirected to
61
+        # on login:
62
+        self._config['sa_auth'].post_login_url = '/post_login'
63
+
64
+        # You may optionally define a page where you want users to be redirected to
65
+        # on logout:
66
+        self._config['sa_auth'].post_logout_url = '/post_logout'

+ 47 - 0
tracim/tracim/lib/auth/internal.py View File

@@ -0,0 +1,47 @@
1
+# -*- coding: utf-8 -*-
2
+from sqlalchemy import and_
3
+from tg.configuration.auth import TGAuthMetadata
4
+
5
+from tracim.lib.auth.base import Auth
6
+from tracim.model import DBSession, User
7
+
8
+
9
+class InternalAuth(Auth):
10
+
11
+    name = 'internal'
12
+    _internal = True
13
+
14
+    def feed_config(self):
15
+        """
16
+        Fill config with internal (database) auth information.
17
+        :return:
18
+        """
19
+        super().feed_config()
20
+        self._config['sa_auth'].user_class = User
21
+        self._config['auth_backend'] = 'sqlalchemy'
22
+        self._config['sa_auth'].dbsession = DBSession
23
+        self._config['sa_auth'].authmetadata = InternalApplicationAuthMetadata(self._config.get('sa_auth'))
24
+
25
+
26
+class InternalApplicationAuthMetadata(TGAuthMetadata):
27
+    def __init__(self, sa_auth):
28
+        self.sa_auth = sa_auth
29
+
30
+    def authenticate(self, environ, identity):
31
+        user = self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(and_(
32
+            self.sa_auth.user_class.is_active == True,
33
+            self.sa_auth.user_class.email == identity['login']
34
+        )).first()
35
+
36
+        if user and user.validate_password(identity['password']):
37
+            return identity['login']
38
+
39
+    def get_user(self, identity, userid):
40
+        return self.sa_auth.dbsession.query(self.sa_auth.user_class).filter(
41
+            and_(self.sa_auth.user_class.is_active == True, self.sa_auth.user_class.email == userid)).first()
42
+
43
+    def get_groups(self, identity, userid):
44
+        return [g.group_name for g in identity['user'].groups]
45
+
46
+    def get_permissions(self, identity, userid):
47
+        return [p.permission_name for p in identity['user'].permissions]

+ 195 - 0
tracim/tracim/lib/auth/ldap.py View File

@@ -0,0 +1,195 @@
1
+# -*- coding: utf-8 -*-
2
+import transaction
3
+from tg.configuration.auth import TGAuthMetadata
4
+from who_ldap import LDAPAttributesPlugin as BaseLDAPAttributesPlugin, make_connection
5
+from who_ldap import LDAPGroupsPlugin as BaseLDAPGroupsPlugin
6
+from who_ldap import LDAPSearchAuthenticatorPlugin as BaseLDAPSearchAuthenticatorPlugin
7
+
8
+from tracim.lib.auth.base import Auth
9
+from tracim.lib.exception import ConfigurationError
10
+from tracim.lib.helpers import ini_conf_to_bool
11
+from tracim.lib.user import UserApi
12
+from tracim.model import DBSession, User
13
+
14
+
15
+class LDAPAuth(Auth):
16
+    """
17
+    LDAP auth management.
18
+
19
+    """
20
+    name = 'ldap'
21
+    _internal = False
22
+
23
+    def __init__(self, config):
24
+        super().__init__(config)
25
+        self.ldap_auth = self._get_ldap_auth()
26
+        self.ldap_user_provider = self._get_ldap_user_provider()
27
+        if ini_conf_to_bool(self._config.get('ldap_group_enabled', False)):
28
+            self.ldap_groups_provider = self._get_ldap_groups_provider()
29
+        self._managed_fields = self.ldap_user_provider.local_fields
30
+
31
+    def feed_config(self):
32
+        super().feed_config()
33
+        self._config['auth_backend'] = 'ldapauth'
34
+        self._config['sa_auth'].authenticators = [('ldapauth', self.ldap_auth)]
35
+
36
+        mdproviders = [('ldapuser', self.ldap_user_provider)]
37
+        if ini_conf_to_bool(self._config.get('ldap_group_enabled', False)):
38
+            raise ConfigurationError("ldap_group_enabled is not yet available")
39
+            mdproviders.append(('ldapgroups', self.ldap_groups_provider))
40
+        self._config['sa_auth'].mdproviders = mdproviders
41
+
42
+        self._config['sa_auth'].authmetadata = LDAPApplicationAuthMetadata(self._config.get('sa_auth'))
43
+
44
+    def _get_ldap_auth(self):
45
+        auth_plug = LDAPSearchAuthenticatorPlugin(
46
+            url=self._config.get('ldap_url'),
47
+            base_dn=self._config.get('ldap_base_dn'),
48
+            bind_dn=self._config.get('ldap_bind_dn'),
49
+            bind_pass=self._config.get('ldap_bind_pass'),
50
+            returned_id='login',
51
+            # the LDAP attribute that holds the user name:
52
+            naming_attribute=self._config.get('ldap_naming_attribute'),
53
+            start_tls=ini_conf_to_bool(self._config.get('ldap_tls', False)),
54
+        )
55
+        auth_plug.set_auth(self)
56
+        return auth_plug
57
+
58
+    def _get_ldap_user_provider(self):
59
+        return LDAPAttributesPlugin(
60
+            url=self._config.get('ldap_url'),
61
+            bind_dn=self._config.get('ldap_bind_dn'),
62
+            bind_pass=self._config.get('ldap_bind_pass'),
63
+            name='user',
64
+            # map from LDAP attributes to TurboGears user attributes:
65
+            attributes=self._config.get('ldap_user_attributes', 'mail=email'),
66
+            flatten=True,
67
+            start_tls=ini_conf_to_bool(self._config.get('ldap_tls', False)),
68
+        )
69
+
70
+    def _get_ldap_groups_provider(self):
71
+        return LDAPGroupsPlugin(
72
+            url=self._config.get('ldap_url'),
73
+            base_dn=self._config.get('ldap_base_dn'),
74
+            bind_dn=self._config.get('ldap_bind_dn'),
75
+            bind_pass=self._config.get('ldap_bind_pass'),
76
+            filterstr=self._config.get('ldap_group_filter', '(&(objectClass=group)(member=%(dn)s))'),
77
+            name='groups',
78
+            start_tls=ini_conf_to_bool(self._config.get('ldap_tls', False)),
79
+        )
80
+
81
+
82
+class LDAPSearchAuthenticatorPlugin(BaseLDAPSearchAuthenticatorPlugin):
83
+
84
+    def __init__(self, *args, **kwargs):
85
+        super().__init__(*args, **kwargs)
86
+        self._auth = None
87
+        self._user_api = UserApi(None)
88
+
89
+    def set_auth(self, auth):
90
+        self._auth = auth
91
+
92
+    def authenticate(self, environ, identity):
93
+        # Note: super().authenticate return None if already authenticated or not found
94
+        email = super().authenticate(environ, identity)
95
+        if email:
96
+            self._sync_ldap_user(email, environ, identity)
97
+        return email
98
+
99
+    def _sync_ldap_user(self, email, environ, identity):
100
+        # Create or get user for connected email
101
+        if not self._user_api.user_with_email_exists(email):
102
+            user = User(email=email, imported_from=LDAPAuth.name)
103
+            DBSession.add(user)
104
+        else:
105
+            user = self._user_api.get_one_by_email(email)
106
+
107
+        # Retrieve ldap user attributes
108
+        self._auth.ldap_user_provider.add_metadata_for_auth(environ, identity)
109
+
110
+        # Update user with ldap attributes
111
+        user_ldap_values = identity.get('user').copy()
112
+        for field_name in user_ldap_values:
113
+            setattr(user, field_name, user_ldap_values[field_name])
114
+
115
+        DBSession.flush()
116
+        transaction.commit()
117
+
118
+    def user_exist(self, email):
119
+        with make_connection(self.url, self.bind_dn, self.bind_pass) as conn:
120
+            if self.start_tls:
121
+                conn.start_tls()
122
+
123
+            if not conn.bind():
124
+                return False
125
+
126
+            search = self.search_pattern % email
127
+            conn.search(self.base_dn, search, self.search_scope)
128
+
129
+            if len(conn.response) > 0:
130
+                return True
131
+
132
+            return False
133
+
134
+
135
+class LDAPApplicationAuthMetadata(TGAuthMetadata):
136
+
137
+    def __init__(self, config):
138
+        self.sa_auth = config.get('sa_auth')
139
+        self._config = config
140
+
141
+    def get_user(self, identity, userid):
142
+        return identity.get('user')
143
+
144
+    def get_groups(self, identity, userid):
145
+        if not ini_conf_to_bool(self._config.get('ldap_group_enabled')):
146
+
147
+            # TODO - B.S. - 20160212: récupérer identity['user'].groups directement produit
148
+            # Parent instance XXX is not bound to a Session. Voir avec Damien.
149
+            user = DBSession.query(User).filter(User.email == identity['user'].email).one()
150
+            return [g.group_name for g in user.groups]
151
+
152
+            return [g.group_name for g in identity['user'].groups]
153
+        else:
154
+            raise NotImplementedError()
155
+
156
+    def get_permissions(self, identity, userid):
157
+        if not ini_conf_to_bool(self._config.get('ldap_group_enabled')):
158
+
159
+            # TODO - B.S. - 20160212: récupérer identity['user'].groups directement produit
160
+            # Parent instance XXX is not bound to a Session. Voir avec Damien.
161
+            user = DBSession.query(User).filter(User.email == identity['user'].email).one()
162
+            return [p.permission_name for p in user.permissions]
163
+
164
+            return [p.permission_name for p in identity['user'].permissions]
165
+        else:
166
+            raise NotImplementedError()
167
+
168
+
169
+class LDAPGroupsPlugin(BaseLDAPGroupsPlugin):
170
+
171
+    def add_metadata(self, environ, identity):
172
+        super().add_metadata(environ, identity)
173
+        groups_names = identity[self.name]
174
+        raise NotImplementedError()  # Should sync groups etc ...
175
+
176
+
177
+class LDAPAttributesPlugin(BaseLDAPAttributesPlugin):
178
+
179
+    def __init__(self, *args, **kwargs):
180
+        super().__init__(*args, **kwargs)
181
+        self._user_api = UserApi(None)
182
+
183
+    def add_metadata(self, environ, identity):
184
+        # We disable metadata recuperation, we do it at connection in LDAPSearchAuthenticatorPlugin._sync_ldap_user
185
+        return
186
+
187
+    def add_metadata_for_auth(self, environ, identity):
188
+        super().add_metadata(environ, identity)
189
+
190
+    @property
191
+    def local_fields(self):
192
+        """
193
+        :return: list of ldap side managed field names
194
+        """
195
+        return list(self._attributes_map.values())

+ 24 - 0
tracim/tracim/lib/auth/wrapper.py View File

@@ -0,0 +1,24 @@
1
+# -*- coding: utf-8 -*-
2
+from tracim.lib.auth.internal import InternalAuth
3
+from tracim.lib.auth.ldap import LDAPAuth
4
+
5
+
6
+class AuthConfigWrapper(object):
7
+
8
+    # TODO: Dynamic load, like plugins ?
9
+    AUTH_CLASSES = (InternalAuth, LDAPAuth)
10
+
11
+    @classmethod
12
+    def wrap(cls, config):
13
+        auth_class = cls._get_auth_class(config)
14
+        config['auth_instance'] = auth_class(config)
15
+        config['auth_instance'].feed_config()
16
+
17
+    @classmethod
18
+    def _get_auth_class(cls, config):
19
+        for auth_class in cls.AUTH_CLASSES:
20
+            if auth_class.name is NotImplemented:
21
+                raise Exception("\"name\" attribute of %s is required" % str(auth_class))
22
+            if config.get('auth_type') == auth_class.name:
23
+                return auth_class
24
+        raise Exception("No auth config wrapper found for \"%s\" auth_type config" % config.get('auth_type'))

+ 4 - 0
tracim/tracim/lib/base.py View File

@@ -31,6 +31,10 @@ class BaseController(TGController):
31 31
         tmpl_context.identity = request.identity
32 32
         return TGController.__call__(self, environ, context)
33 33
 
34
+    def _before(self, *args, **kwargs):
35
+        tmpl_context.auth_is_internal = tg.config.get('auth_is_internal', True)
36
+        tmpl_context.auth_instance = tg.config.get('auth_instance')
37
+
34 38
     @property
35 39
     def parent_controller(self):
36 40
         possible_parent = None

+ 21 - 0
tracim/tracim/lib/exception.py View File

@@ -0,0 +1,21 @@
1
+# -*- coding: utf-8 -*-
2
+
3
+
4
+class TracimError(Exception):
5
+    pass
6
+
7
+
8
+class ConfigurationError(TracimError):
9
+    pass
10
+
11
+
12
+class AlreadyExistError(TracimError):
13
+    pass
14
+
15
+
16
+class CommandError(TracimError):
17
+    pass
18
+
19
+
20
+class CommandAbortedError(CommandError):
21
+    pass

+ 3 - 0
tracim/tracim/lib/group.py View File

@@ -17,3 +17,6 @@ class GroupApi(object):
17 17
 
18 18
     def get_one(self, group_id) -> Group:
19 19
         return self._base_query().filter(Group.group_id==group_id).one()
20
+
21
+    def get_one_with_name(self, group_name) -> Group:
22
+        return self._base_query().filter(Group.group_name==group_name).one()

+ 18 - 0
tracim/tracim/lib/helpers.py View File

@@ -198,3 +198,21 @@ def shorten(text: str, maxlength: int, add_three_points=True) -> str:
198 198
             result += '…'
199 199
 
200 200
     return result
201
+
202
+
203
+def ini_conf_to_bool(value):
204
+    """
205
+    Depending INI file interpreter, False values are simple parsed as string,
206
+    so use this function to consider them as boolean
207
+    :param value: value of ini parameter
208
+    :return: bollean value
209
+    """
210
+    if value in ('False', 'false', '0', 'off', 'no'):
211
+        return False
212
+    return bool(value)
213
+
214
+
215
+def is_user_externalized_field(field_name):
216
+    if not tg.config.get('auth_instance').is_internal:
217
+        return field_name in tg.config.get('auth_instance').managed_fields
218
+    return False

+ 7 - 3
tracim/tracim/lib/user.py View File

@@ -27,9 +27,13 @@ class UserApi(object):
27 27
     def get_one_by_email(self, email: str):
28 28
         return self._base_query().filter(User.email==email).one()
29 29
 
30
-    def update(self, user: User, name: str, email: str, do_save):
31
-        user.display_name = name
32
-        user.email = email
30
+    def update(self, user: User, name: str=None, email: str=None, do_save=True):
31
+        if name is not None:
32
+            user.display_name = name
33
+
34
+        if email is not None:
35
+            user.email = email
36
+
33 37
         if do_save:
34 38
             self.save(user)
35 39
 

+ 1 - 0
tracim/tracim/model/auth.py View File

@@ -119,6 +119,7 @@ class User(DeclarativeBase):
119 119
     _password = Column('password', Unicode(128))
120 120
     created = Column(DateTime, default=datetime.now)
121 121
     is_active = Column(Boolean, default=True, nullable=False)
122
+    imported_from = Column(Unicode(32), nullable=True)
122 123
 
123 124
     @hybrid_property
124 125
     def email_address(self):

+ 6 - 1
tracim/tracim/public/assets/css/dashboard.css View File

@@ -361,4 +361,9 @@ tr.t-is-new-content td, div.row.t-is-new-content {
361 361
 
362 362
 .panel-heading > h3 { font-size: 1.5em;}
363 363
 
364
-hr.t-toolbar-btn-group-separator { border-color: #CCC; border-style: dotted; }
364
+hr.t-toolbar-btn-group-separator { border-color: #CCC; border-style: dotted; }
365
+
366
+span.info.readonly {
367
+    font-size: 0.8em;
368
+    opacity: 0.7;
369
+}

+ 1 - 1
tracim/tracim/templates/index.mak View File

@@ -50,7 +50,7 @@
50 50
                                 <button type="submit" class="btn btn-small btn-success text-right">
51 51
                                     <i class="fa fa-check"></i> ${_('Login')}
52 52
                                 </button>
53
-                                % if CFG.EMAIL_NOTIFICATION_ACTIVATED:
53
+                                % if CFG.EMAIL_NOTIFICATION_ACTIVATED and tmpl_context.auth_is_internal:
54 54
                                     <div class="pull-left">
55 55
                                         <a class="btn btn-link" href="${tg.url('/reset_password')}"><i class="fa fa-magic"></i> ${_('Forgot password?')}</a>
56 56
                                     </div>

+ 11 - 3
tracim/tracim/templates/user_toolbars.mak View File

@@ -15,7 +15,11 @@
15 15
                 endif
16 16
             %>
17 17
         <a title="${_('Edit current user')}" class="btn btn-default" data-toggle="modal" data-target="#user-edit-modal-dialog" data-remote="${user_edit_url}" >${ICON.FA('fa-edit t-less-visible')} ${_('Edit user')}</a>
18
-            <a title="${_('Change password')}" class="btn btn-default" data-toggle="modal" data-target="#user-edit-password-modal-dialog" data-remote="${user_password_edit_url}" >${ICON.FA('fa-key t-less-visible')} ${_('Password')}</a>
18
+
19
+            % if tmpl_context.auth_is_internal:
20
+                <a title="${_('Change password')}" class="btn btn-default" data-toggle="modal" data-target="#user-edit-password-modal-dialog" data-remote="${user_password_edit_url}" >${ICON.FA('fa-key t-less-visible')} ${_('Password')}</a>
21
+            % endif
22
+
19 23
         </div>
20 24
         <p></p>
21 25
         % if current_user.profile.id>2 and current_user.id!=user.id:
@@ -39,8 +43,12 @@
39 43
                 user_edit_url = tg.url('/user/{}/edit'.format(current_user.id), {'next_url': '/home'})
40 44
                 user_password_edit_url = tg.url('/user/{}/password/edit'.format(current_user.id))
41 45
             %>
42
-            <a title="${_('Edit my profile')}" class="btn btn-default" data-toggle="modal" data-target="#user-edit-modal-dialog" data-remote="${user_edit_url}" >${ICON.FA('fa-edit t-less-visible')} ${_('Edit my profile')}</a>
43
-            <a title="${_('Change password')}" class="btn btn-default" data-toggle="modal" data-target="#user-edit-password-modal-dialog" data-remote="${user_password_edit_url}" >${ICON.FA('fa-key t-less-visible')} ${_('Password')}</a>
46
+            <a title="${_('Edit my profile')}" class="btn btn-default edit-profile-btn" data-toggle="modal" data-target="#user-edit-modal-dialog" data-remote="${user_edit_url}" >${ICON.FA('fa-edit t-less-visible')} ${_('Edit my profile')}</a>
47
+
48
+            % if tmpl_context.auth_is_internal:
49
+                <a title="${_('Change password')}" class="btn btn-default change-password-btn" data-toggle="modal" data-target="#user-edit-password-modal-dialog" data-remote="${user_password_edit_url}" >${ICON.FA('fa-key t-less-visible')} ${_('Password')}</a>
50
+            % endif
51
+
44 52
         </div>
45 53
         <p></p>
46 54
         <h3 class="t-spacer-above" style="margin-top: 1em;">

+ 8 - 2
tracim/tracim/templates/user_workspace_forms.mak View File

@@ -98,11 +98,17 @@
98 98
         <div class="modal-body">
99 99
             <div class="form-group">
100 100
                 <label for="name">${_('Name')}</label>
101
-                <input name="name" type="text" class="form-control" id="name" placeholder="${_('Name')}" value="${user.name}">
101
+                % if h.is_user_externalized_field('name'):
102
+                    <span class="info readonly">${_('This information is managed by an external application')}</span>
103
+                % endif
104
+                <input name="name" type="text" class="form-control" id="name" placeholder="${_('Name')}" value="${user.name}" ${'readonly="readonly"' if h.is_user_externalized_field('name') else '' | n}>
102 105
             </div>
103 106
             <div class="form-group">
104 107
                 <label for="email">${_('Email')}</label>
105
-                <input name="email" type="text" class="form-control" id="email" placeholder="${_('Name')}" value="${user.email}">
108
+                % if h.is_user_externalized_field('email'):
109
+                    <span class="info readonly">${_('This information is managed by an external application')}</span>
110
+                % endif
111
+                <input name="email" type="text" class="form-control" id="email" placeholder="${_('Name')}" value="${user.email}" ${'readonly="readonly"' if h.is_user_externalized_field('email') else '' | n}>
106 112
             </div>
107 113
         </div>
108 114
         <div class="modal-footer">

+ 116 - 14
tracim/tracim/tests/__init__.py View File

@@ -1,19 +1,18 @@
1 1
 # -*- coding: utf-8 -*-
2 2
 """Unit and functional test suite for tracim."""
3
+import argparse
4
+import os
5
+from os import getcwd
3 6
 
4
-from os import getcwd, path
5
-
6
-from paste.deploy import loadapp
7
-from webtest import TestApp
8
-
9
-from gearbox.commands.setup_app import SetupAppCommand
10
-
7
+import ldap3
11 8
 import tg
12
-from tg import config
13
-from tg.util import Bunch
14
-
9
+import time
10
+import transaction
11
+from gearbox.commands.setup_app import SetupAppCommand
12
+from ldap_test import LdapServer
13
+from nose.tools import ok_
14
+from paste.deploy import loadapp
15 15
 from sqlalchemy.engine import reflection
16
-
17 16
 from sqlalchemy.schema import DropConstraint
18 17
 from sqlalchemy.schema import DropSequence
19 18
 from sqlalchemy.schema import DropTable
@@ -21,16 +20,36 @@ from sqlalchemy.schema import ForeignKeyConstraint
21 20
 from sqlalchemy.schema import MetaData
22 21
 from sqlalchemy.schema import Sequence
23 22
 from sqlalchemy.schema import Table
23
+from tg import config
24
+from tg.util import Bunch
25
+from webtest import TestApp as BaseTestApp, AppError
26
+from who_ldap import make_connection
24 27
 
25
-import transaction
26
-
28
+from tracim.fixtures import FixturesLoader
29
+from tracim.fixtures.users_and_groups import Base as BaseFixture
27 30
 from tracim.lib.base import logger
31
+from tracim.model import DBSession
28 32
 
29 33
 __all__ = ['setup_app', 'setup_db', 'teardown_db', 'TestController']
30 34
 
31 35
 application_name = 'main_without_authn'
32 36
 
33 37
 
38
+class TestApp(BaseTestApp):
39
+    def _check_status(self, status, res):
40
+        """ Simple override to print html content when error"""
41
+        try:
42
+            super()._check_status(status, res)
43
+        except AppError as exc:
44
+            dump_file_path = "/tmp/debug_%d_%s.html" % (time.time() * 1000, res.request.path_qs[1:])
45
+            if os.path.exists("/tmp"):
46
+                with open(dump_file_path, 'w') as dump_file:
47
+                    print(res.ubody, file=dump_file)
48
+                # Update exception message with info about this dumped file
49
+                exc.args = ('%s html error file dump in %s' % (exc.args[0], dump_file_path), ) + exc.args[1:]
50
+            raise exc
51
+
52
+
34 53
 def load_app(name=application_name):
35 54
     """Load the test application."""
36 55
     return TestApp(loadapp('config:test.ini#%s' % name, relative_to=getcwd()))
@@ -131,6 +150,7 @@ def teardown_db():
131 150
 class TestStandard(object):
132 151
 
133 152
     application_under_test = application_name
153
+    fixtures = [BaseFixture, ]
134 154
 
135 155
     def setUp(self):
136 156
         self.app = load_app(self.application_under_test)
@@ -150,12 +170,33 @@ class TestStandard(object):
150 170
         setup_db()
151 171
         logger.debug(self, 'Start Database Setup... -> done')
152 172
 
173
+        logger.debug(self, 'Load extra fixtures...')
174
+        fixtures_loader = FixturesLoader([BaseFixture])  # BaseFixture is already loaded in bootstrap
175
+        fixtures_loader.loads(self.fixtures)
176
+        logger.debug(self, 'Load extra fixtures... -> done')
177
+
153 178
         self.app.get('/_test_vars')  # Allow to create fake context
154 179
         tg.i18n.set_lang('en')  # Set a default lang
155 180
 
156 181
     def tearDown(self):
157 182
         transaction.commit()
158 183
 
184
+
185
+class TestCommand(TestStandard):
186
+    def _execute_command(self, command_class, command_name, sub_argv):
187
+        parser = argparse.ArgumentParser()
188
+        command = command_class(self.app, parser)
189
+        command.auto_setup_app = False
190
+        cmd_parser = command.get_parser(command_name)
191
+        parsed_args = cmd_parser.parse_args(sub_argv)
192
+        return command.run(parsed_args)
193
+
194
+    def setUp(self):
195
+        super().setUp()
196
+        # Ensure app config is loaded
197
+        self.app.get('/_test_vars')
198
+
199
+
159 200
 class TestController(object):
160 201
     """Base functional test case for the controllers.
161 202
 
@@ -173,6 +214,7 @@ class TestController(object):
173 214
     """
174 215
 
175 216
     application_under_test = application_name
217
+    fixtures = [BaseFixture, ]
176 218
 
177 219
     def setUp(self):
178 220
         """Setup test fixture for each functional test method."""
@@ -186,8 +228,68 @@ class TestController(object):
186 228
         setup_app(section_name=self.application_under_test)
187 229
         setup_db()
188 230
 
231
+        fixtures_loader = FixturesLoader([BaseFixture])  # BaseFixture is already loaded in bootstrap
232
+        fixtures_loader.loads(self.fixtures)
233
+
189 234
 
190 235
     def tearDown(self):
191 236
         """Tear down test fixture for each functional test method."""
192
-        # model.DBSession.remove()
237
+        DBSession.close()
193 238
         teardown_db()
239
+
240
+
241
+class TracimTestController(TestController):
242
+
243
+    def _connect_user(self, login, password):
244
+        # Going to the login form voluntarily:
245
+        resp = self.app.get('/', status=200)
246
+        form = resp.form
247
+        # Submitting the login form:
248
+        form['login'] = login
249
+        form['password'] = password
250
+        return form.submit(status=302)
251
+
252
+
253
+class LDAPTest(object):
254
+
255
+    """
256
+    Server fixtures, see https://github.com/zoldar/python-ldap-test
257
+    """
258
+    ldap_server_data = NotImplemented
259
+
260
+    def __init__(self, *args, **kwargs):
261
+        super().__init__(*args, **kwargs)
262
+        self._ldap_test_server = None
263
+        self._ldap_connection = None
264
+
265
+    def setUp(self):
266
+        super().setUp()
267
+        self._ldap_test_server = LdapServer(self.ldap_server_data)
268
+        self._ldap_test_server.start()
269
+        ldap3_server = ldap3.Server('localhost', port=self._ldap_test_server.config['port'])
270
+        self._ldap_connection = ldap3.Connection(
271
+            ldap3_server,
272
+            user=self._ldap_test_server.config['bind_dn'],
273
+            password=self._ldap_test_server.config['password'],
274
+            auto_bind=True
275
+        )
276
+
277
+    def tearDown(self):
278
+        super().tearDown()
279
+        self._ldap_test_server.stop()
280
+
281
+    def test_ldap_connectivity(self):
282
+        with make_connection(
283
+                'ldap://%s:%d' % ('localhost', self._ldap_test_server.config['port']),
284
+                self._ldap_test_server.config['bind_dn'],
285
+                'toor'
286
+        ) as conn:
287
+            if not conn.bind():
288
+                ok_(False, "Cannot establish connection with LDAP test server")
289
+            else:
290
+                ok_(True)
291
+
292
+
293
+class ArgumentParser(argparse.ArgumentParser):
294
+    def exit(self, status=0, message=None):
295
+        raise Exception(message)

+ 27 - 0
tracim/tracim/tests/command/commands.py View File

@@ -0,0 +1,27 @@
1
+# -*- coding: utf-8 -*-
2
+import os
3
+import subprocess
4
+
5
+from nose.tools import ok_
6
+
7
+import tracim
8
+
9
+
10
+class TestCommands(object):
11
+    def test_commands(self):
12
+        """
13
+        Test listing of gearbox command: Tracim commands must be listed
14
+        :return:
15
+        """
16
+        os.chdir(os.path.dirname(tracim.__file__) + '/../')
17
+
18
+        output = subprocess.check_output(["gearbox", "-h"])
19
+        output = output.decode('utf-8')
20
+
21
+        ok_(output.find('not a command') == -1)
22
+
23
+        ok_(output.find('serve') > 0)
24
+
25
+        ok_(output.find('ldap server') > 0)
26
+        ok_(output.find('user create') > 0)
27
+        ok_(output.find('user update') > 0)

+ 80 - 0
tracim/tracim/tests/command/user.py View File

@@ -0,0 +1,80 @@
1
+from nose.tools import eq_
2
+from nose.tools import ok_
3
+
4
+from tracim.command.user import CreateUserCommand, UpdateUserCommand
5
+from tracim.model import DBSession, Group
6
+from tracim.model.auth import User
7
+from tracim.tests import TestCommand
8
+
9
+
10
+class TestUserCommand(TestCommand):
11
+
12
+    def test_create(self):
13
+        self._create_user('new-user@algoo.fr', 'toor')
14
+
15
+    def test_update_password(self):
16
+        self._create_user('new-user@algoo.fr', 'toor')
17
+        self._execute_command(
18
+            CreateUserCommand,
19
+            'gearbox user update',
20
+            ['-l', 'new-user@algoo.fr', '-p', 'new_password']
21
+        )
22
+        user = DBSession.query(User).filter(User.email == 'new-user@algoo.fr').one()
23
+        user.validate_password('new_password')
24
+
25
+    def test_create_with_group(self):
26
+        more_args = ['--add-to-group', 'managers', '--add-to-group', 'administrators']
27
+        self._create_user('new-user@algoo.fr', 'toor', more_args=more_args)
28
+        user = DBSession.query(User).filter(User.email == 'new-user@algoo.fr').one()
29
+        group_managers = DBSession.query(Group).filter(Group.group_name == 'managers').one()
30
+        group_administrators = DBSession.query(Group).filter(Group.group_name == 'administrators').one()
31
+
32
+        ok_(user in group_managers.users)
33
+        ok_(user in group_administrators.users)
34
+
35
+    def test_change_groups(self):
36
+        # create an user in managers group
37
+        more_args = ['--add-to-group', 'managers']
38
+        self._create_user('new-user@algoo.fr', 'toor', more_args=more_args)
39
+        user = DBSession.query(User).filter(User.email == 'new-user@algoo.fr').one()
40
+        group_managers = DBSession.query(Group).filter(Group.group_name == 'managers').one()
41
+        group_administrators = DBSession.query(Group).filter(Group.group_name == 'administrators').one()
42
+
43
+        ok_(user in group_managers.users)
44
+        ok_(user not in group_administrators.users)
45
+
46
+        # Update him and add to administrators group
47
+        add_to_admins_argvs = ['-l', 'new-user@algoo.fr', '--add-to-group', 'administrators']
48
+        self._execute_command(UpdateUserCommand, 'gearbox user update', add_to_admins_argvs)
49
+        user = DBSession.query(User).filter(User.email == 'new-user@algoo.fr').one()
50
+        group_managers = DBSession.query(Group).filter(Group.group_name == 'managers').one()
51
+        group_administrators = DBSession.query(Group).filter(Group.group_name == 'administrators').one()
52
+
53
+        ok_(user in group_managers.users)
54
+        ok_(user in group_administrators.users)
55
+
56
+        # remove him from administrators group
57
+        remove_from_admins_argvs = ['-l', 'new-user@algoo.fr', '--remove-from-group', 'administrators']
58
+        self._execute_command(UpdateUserCommand, 'gearbox user update', remove_from_admins_argvs)
59
+        user = DBSession.query(User).filter(User.email == 'new-user@algoo.fr').one()
60
+        group_managers = DBSession.query(Group).filter(Group.group_name == 'managers').one()
61
+        group_administrators = DBSession.query(Group).filter(Group.group_name == 'administrators').one()
62
+
63
+        ok_(user in group_managers.users)
64
+        ok_(user not in group_administrators.users)
65
+
66
+    def _create_user(self, email, password, more_args=[]):
67
+        args = ['-l', email, '-p', password]
68
+        args.extend(more_args)
69
+
70
+        self._check_user_exist(email, exist=False)
71
+        self._execute_command(CreateUserCommand, 'gearbox user create', args)
72
+        self._check_user_exist(email, exist=True)
73
+
74
+        user = DBSession.query(User).filter(User.email == email).one()
75
+        user.validate_password(password)
76
+
77
+    @staticmethod
78
+    def _check_user_exist(email, exist=True):
79
+        eq_(exist, bool(DBSession.query(User).filter(User.email == email).count()))
80
+

+ 33 - 0
tracim/tracim/tests/command/user_ldap.py View File

@@ -0,0 +1,33 @@
1
+from nose.tools import eq_, raises
2
+from nose.tools import ok_
3
+
4
+from tracim.command.user import CreateUserCommand, LDAPUserUnknown
5
+from tracim.fixtures.ldap import ldap_test_server_fixtures
6
+from tracim.lib.exception import CommandAbortedError
7
+from tracim.tests import TestCommand, LDAPTest
8
+
9
+
10
+class TestLDAPUserCommand(LDAPTest, TestCommand):
11
+    """
12
+    Test LDAP user verification when execute command
13
+    """
14
+    application_under_test = 'ldap'
15
+    ldap_server_data = ldap_test_server_fixtures
16
+
17
+    @raises(LDAPUserUnknown)
18
+    def test_user_not_in_ldap(self):
19
+        self._execute_command(
20
+            CreateUserCommand,
21
+            'gearbox user create',
22
+            ['-l', 'unknown-user@fsf.org', '-p', 'foo', '--raise']
23
+        )
24
+
25
+    def test_user_in_ldap(self):
26
+        try:
27
+            self._execute_command(
28
+                CreateUserCommand,
29
+                'gearbox user create',
30
+                ['-l', 'richard-not-real-email@fsf.org', '-p', 'foo', '--raise']
31
+            )
32
+        except LDAPUserUnknown:
33
+            ok_(False, "Command should not raise LDAPUserUnknown exception")

+ 56 - 0
tracim/tracim/tests/functional/test_ldap_authentication.py View File

@@ -0,0 +1,56 @@
1
+# -*- coding: utf-8 -*-
2
+"""
3
+Integration tests for the ldap authentication sub-system.
4
+"""
5
+from tracim.fixtures.ldap import ldap_test_server_fixtures
6
+from nose.tools import eq_, ok_
7
+
8
+from tracim.fixtures.users_and_groups import Test as TestFixture
9
+from tracim.model import DBSession, User
10
+from tracim.tests import LDAPTest, TracimTestController
11
+
12
+
13
+class TestAuthentication(LDAPTest, TracimTestController):
14
+    application_under_test = 'ldap'
15
+    ldap_server_data = ldap_test_server_fixtures
16
+    fixtures = [TestFixture, ]
17
+
18
+    def test_ldap_auth_fail_no_account(self):
19
+        # User is unknown in tracim database
20
+        eq_(0, DBSession.query(User).filter(User.email == 'unknown-user@fsf.org').count())
21
+
22
+        self._connect_user('unknown-user@fsf.org', 'no-pass')
23
+
24
+        # User is registered in tracim database
25
+        eq_(0, DBSession.query(User).filter(User.email == 'unknown-user@fsf.org').count())
26
+
27
+    def test_ldap_auth_fail_wrong_pass(self):
28
+        # User is unknown in tracim database
29
+        eq_(0, DBSession.query(User).filter(User.email == 'richard-not-real-email@fsf.org').count())
30
+
31
+        self._connect_user('richard-not-real-email@fsf.org', 'wrong-pass')
32
+
33
+        # User is registered in tracim database
34
+        eq_(0, DBSession.query(User).filter(User.email == 'richard-not-real-email@fsf.org').count())
35
+
36
+    def test_ldap_auth_sync(self):
37
+        # User is unknown in tracim database
38
+        eq_(0, DBSession.query(User).filter(User.email == 'richard-not-real-email@fsf.org').count())
39
+
40
+        self._connect_user('richard-not-real-email@fsf.org', 'rms')
41
+
42
+        # User is registered in tracim database
43
+        eq_(1, DBSession.query(User).filter(User.email == 'richard-not-real-email@fsf.org').count())
44
+
45
+    def test_ldap_attributes_sync(self):
46
+        # User is already know in database
47
+        eq_(1, DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').count())
48
+
49
+        # His display name is Lawrence L.
50
+        lawrence = DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').one()
51
+        eq_('Lawrence L.', lawrence.display_name)
52
+
53
+        # After connexion with LDAP, his display_name is updated (see ldap fixtures)
54
+        self._connect_user('lawrence-not-real-email@fsf.org', 'foobarbaz')
55
+        lawrence = DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').one()
56
+        eq_('Lawrence Lessig', lawrence.display_name)

+ 78 - 0
tracim/tracim/tests/functional/test_ldap_restrictions.py View File

@@ -0,0 +1,78 @@
1
+# -*- coding: utf-8 -*-
2
+from collections import OrderedDict
3
+
4
+from bs4 import BeautifulSoup
5
+from nose.tools import eq_, ok_
6
+
7
+from tracim.fixtures.ldap import ldap_test_server_fixtures
8
+from tracim.fixtures.users_and_groups import Test as TestFixture
9
+from tracim.lib.base import current_user
10
+from tracim.model import DBSession, User
11
+from tracim.tests import LDAPTest, TracimTestController
12
+
13
+
14
+class TestAuthentication(LDAPTest, TracimTestController):
15
+    application_under_test = 'ldap'
16
+    ldap_server_data = ldap_test_server_fixtures
17
+    fixtures = [TestFixture, ]
18
+
19
+    def test_password_disabled(self):
20
+        """
21
+        Password change is disabled
22
+        :return:
23
+        """
24
+        lawrence = DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').one()
25
+        self._connect_user('lawrence-not-real-email@fsf.org', 'foobarbaz')
26
+        home = self.app.get('/home/',)
27
+
28
+        # HTML button is not here
29
+        eq_(None, BeautifulSoup(home.body).find(attrs={'class': 'change-password-btn'}))
30
+
31
+        # If we force passwd update, we got 403
32
+        try_post_passwd = self.app.post(
33
+            '/user/%d/password?_method=PUT' % lawrence.user_id,
34
+            OrderedDict([
35
+                ('current_password', 'fooobarbaz'),
36
+                ('new_password1', 'foobar'),
37
+                ('new_password2', 'foobar'),
38
+            ]),
39
+            expect_errors=403
40
+        )
41
+        eq_(try_post_passwd.status_code, 403, "Code should be 403, but is %d" % try_post_passwd.status_code)
42
+
43
+    def test_fields_disabled(self):
44
+        """
45
+        Some fields (email) are not editable on user interface: they are managed by LDAP
46
+        :return:
47
+        """
48
+        lawrence = DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').one()
49
+        self._connect_user('lawrence-not-real-email@fsf.org', 'foobarbaz')
50
+
51
+        edit = self.app.get('/user/5/edit')
52
+
53
+        # email input field is disabled
54
+        email_input = BeautifulSoup(edit.body).find(attrs={'id': 'email'})
55
+        ok_('readonly' in email_input.attrs)
56
+        eq_(email_input.attrs['readonly'], "readonly")
57
+
58
+        # Name is not (see attributes configuration of LDAP fixtures)
59
+        name_input = BeautifulSoup(edit.body).find(attrs={'id': 'name'})
60
+        ok_('readonly' not in name_input.attrs)
61
+
62
+        # If we force edit of user, "email" field will be not updated
63
+        eq_(lawrence.email, 'lawrence-not-real-email@fsf.org')
64
+        eq_(lawrence.display_name, 'Lawrence L.')
65
+
66
+        try_post_user = self.app.post(
67
+            '/user/%d?_method=PUT' % lawrence.user_id,
68
+            OrderedDict([
69
+                ('name', 'Lawrence Lessig YEAH'),
70
+                ('email', 'An-other-email@fsf.org'),
71
+            ])
72
+        )
73
+
74
+        eq_(try_post_user.status_code, 302, "Code should be 302, but is %d" % try_post_user.status_code)
75
+
76
+        lawrence = DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').one()
77
+        eq_(lawrence.email, 'lawrence-not-real-email@fsf.org', "email should be unmodified")
78
+        eq_(lawrence.display_name, 'Lawrence Lessig YEAH', "Name should be updated")

+ 115 - 0
tracim/tracim/tests/library/test_ldap_without_ldap_groups.py View File

@@ -0,0 +1,115 @@
1
+# -*- coding: utf-8 -*-
2
+from nose.tools import eq_
3
+from tg import config
4
+
5
+from tracim.fixtures.ldap import ldap_test_server_fixtures
6
+from tracim.fixtures.users_and_groups import Test as TestFixtures
7
+from tracim.lib.auth.ldap import LDAPAuth
8
+from tracim.lib.helpers import ini_conf_to_bool
9
+from tracim.model import DBSession, User, Group
10
+from tracim.tests import LDAPTest, TestStandard
11
+
12
+
13
+class TestContentApi(LDAPTest, TestStandard):
14
+    """
15
+    This test load test.ini app:ldap config. LDAP groups management must be disabled in this config.
16
+    """
17
+    application_under_test = 'ldap'
18
+    ldap_server_data = ldap_test_server_fixtures
19
+    fixtures = [TestFixtures]
20
+
21
+    def _check_db_user(self, email, count=1):
22
+        eq_(count, DBSession.query(User).filter(User.email == email).count())
23
+
24
+    def test_authenticate_success(self):
25
+        """
26
+        LDAP Auth success
27
+        :return:
28
+        """
29
+        ldap_auth = LDAPAuth(config)
30
+        richard_identity = {'login': 'richard-not-real-email@fsf.org', 'password': 'rms'}
31
+
32
+        self._check_db_user('richard-not-real-email@fsf.org', 0)
33
+        auth_id = ldap_auth.ldap_auth.authenticate(environ={}, identity=richard_identity)
34
+
35
+        assert auth_id == 'richard-not-real-email@fsf.org'
36
+        self._check_db_user('richard-not-real-email@fsf.org', 1)
37
+
38
+    def test_authenticate_fail_wrong_pass(self):
39
+        """
40
+        LDAP Auth fail: wrong password
41
+        :return:
42
+        """
43
+        ldap_auth = LDAPAuth(config)
44
+        richard_identity = {'login': 'richard-not-real-email@fsf.org', 'password': 'wrong pass'}
45
+
46
+        self._check_db_user('richard-not-real-email@fsf.org', 0)
47
+        auth_id = ldap_auth.ldap_auth.authenticate(environ={}, identity=richard_identity)
48
+
49
+        assert auth_id is None
50
+        self._check_db_user('richard-not-real-email@fsf.org', 0)
51
+
52
+    def test_authenticate_fail_wrong_login(self):
53
+        """
54
+        LDAP Auth fail: wrong email
55
+        :return:
56
+        """
57
+        ldap_auth = LDAPAuth(config)
58
+        richard_identity = {'login': 'wrong-email@fsf.org', 'password': 'rms'}
59
+
60
+        self._check_db_user('wrong-email@fsf.org', 0)
61
+        auth_id = ldap_auth.ldap_auth.authenticate(environ={}, identity=richard_identity)
62
+
63
+        assert auth_id is None
64
+        self._check_db_user('wrong-email@fsf.org', 0)
65
+
66
+    def test_internal_groups(self):
67
+        """
68
+        LDAP don't manage groups here: We must retrieve internal groups of tested user
69
+        :return:
70
+        """
71
+        lawrence = DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').one()
72
+        managers = DBSession.query(Group).filter(Group.group_name == 'managers').one()
73
+        lawrence_identity = {'user': lawrence}
74
+
75
+        # Lawrence is in fixtures: he is in managers group
76
+        self._check_db_user('lawrence-not-real-email@fsf.org', 1)
77
+        assert lawrence in managers.users
78
+        assert False is ini_conf_to_bool(config.get('ldap_group_enabled', False))
79
+        assert ['managers'] == config.get('sa_auth').authmetadata.get_groups(
80
+            identity=lawrence_identity,
81
+            userid=lawrence.email
82
+        )
83
+
84
+        should_groups = ['managers']
85
+        are_groups = config.get('sa_auth').authmetadata.get_groups(
86
+            identity=lawrence_identity,
87
+            userid=lawrence.email
88
+        )
89
+        eq_(should_groups,
90
+            are_groups,
91
+            "Permissions should be %s, they are %s" % (should_groups, are_groups))
92
+
93
+    def test_internal_permissions(self):
94
+        """
95
+        LDAP don't manage groups here: We must retrieve internal groups permission of tested user
96
+        :return:
97
+        """
98
+        lawrence = DBSession.query(User).filter(User.email == 'lawrence-not-real-email@fsf.org').one()
99
+        managers = DBSession.query(Group).filter(Group.group_name == 'managers').one()
100
+        lawrence_identity = {'user': lawrence}
101
+
102
+        # Lawrence is in fixtures: he is in managers group
103
+        self._check_db_user('lawrence-not-real-email@fsf.org', 1)
104
+        assert lawrence in managers.users
105
+        assert False is ini_conf_to_bool(config.get('ldap_group_enabled', False))
106
+
107
+        should_permissions = []  # Actually there is no permission
108
+        are_permissions = config.get('sa_auth').authmetadata.get_permissions(
109
+            identity=lawrence_identity,
110
+            userid=lawrence.email
111
+        )
112
+        eq_(should_permissions,
113
+            are_permissions,
114
+            "Permissions should be %s, they are %s" % (should_permissions, are_permissions))
115
+

+ 6 - 33
tracim/tracim/websetup/bootstrap.py View File

@@ -2,47 +2,20 @@
2 2
 """Setup the tracim application"""
3 3
 from __future__ import print_function
4 4
 
5
-import logging
6
-from tg import config
7
-from tracim import model
8 5
 import transaction
9 6
 
7
+from tracim.fixtures import FixturesLoader
8
+from tracim.fixtures.users_and_groups import Base as BaseFixture
9
+
10
+
10 11
 def bootstrap(command, conf, vars):
11 12
     """Place any commands to setup tracim here"""
12 13
 
13 14
     # <websetup.bootstrap.before.auth
14 15
     from sqlalchemy.exc import IntegrityError
15 16
     try:
16
-        u = model.User()
17
-        u.display_name = 'Global manager'
18
-        u.email = 'admin@admin.admin'
19
-        u.password = 'admin@admin.admin'
20
-        model.DBSession.add(u)
21
-
22
-        g1 = model.Group()
23
-        g1.group_id = 1
24
-        g1.group_name = 'users'
25
-        g1.display_name = 'Users'
26
-        g1.users.append(u)
27
-        model.DBSession.add(g1)
28
-
29
-        g2 = model.Group()
30
-        g2.group_id = 2
31
-        g2.group_name = 'managers'
32
-        g2.display_name = 'Global Managers'
33
-        g2.users.append(u)
34
-        model.DBSession.add(g2)
35
-
36
-        g3 = model.Group()
37
-        g3.group_id = 3
38
-        g3.group_name = 'administrators'
39
-        g3.display_name = 'Administrators'
40
-        g3.users.append(u)
41
-        model.DBSession.add(g3)
42
-
43
-        model.DBSession.flush()
44
-        transaction.commit()
45
-
17
+        fixtures_loader = FixturesLoader()
18
+        fixtures_loader.loads([BaseFixture])
46 19
     except IntegrityError:
47 20
         print('Warning, there was a problem adding your auth data, it may have already been added:')
48 21
         import traceback

+ 2 - 1
tracim/tracim/websetup/schema.py View File

@@ -174,7 +174,8 @@ CREATE TABLE users (
174 174
     display_name character varying(255),
175 175
     password character varying(128),
176 176
     created timestamp without time zone,
177
-    is_active boolean DEFAULT true NOT NULL
177
+    is_active boolean DEFAULT true NOT NULL,
178
+    imported_from character varying(32)
178 179
 );
179 180
 
180 181
 CREATE TABLE user_group (