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
 install:
17
 install:
18
   - "cd tracim && python setup.py develop; cd -"
18
   - "cd tracim && python setup.py develop; cd -"
19
   - "pip install -r install/requirements.txt; echo"
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
   - "pip install coveralls"
20
   - "pip install coveralls"
24
 
21
 
25
 before_script:
22
 before_script:

+ 40 - 0
README.md View File

291
     website.title = My Company Intranet
291
     website.title = My Company Intranet
292
     website.base_url = http://intranet.mycompany.com:8080
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
 #### Other parameters  ####
334
 #### Other parameters  ####
295
 
335
 
296
 There are other parameters which may be of some interest for you. For example, you can:
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
-Babel==1.3
1
+Babel==2.2
2
 Beaker==1.6.4
2
 Beaker==1.6.4
3
 CherryPy==3.6.0
3
 CherryPy==3.6.0
4
 FormEncode==1.3.0a1
4
 FormEncode==1.3.0a1
38
 tgext.admin==0.6.4
38
 tgext.admin==0.6.4
39
 tgext.asyncjob==0.3.1
39
 tgext.asyncjob==0.3.1
40
 tgext.crud==0.7.3
40
 tgext.crud==0.7.3
41
-tgext.pluggable==0.5.4
41
+tgext.pluggable==0.5.5
42
 transaction==1.4.4
42
 transaction==1.4.4
43
 tw2.core==2.2.2
43
 tw2.core==2.2.2
44
 tw2.forms==2.2.2.1
44
 tw2.forms==2.2.2.1
47
 zope.sqlalchemy==0.7.6
47
 zope.sqlalchemy==0.7.6
48
 tgapp-resetpassword==0.1.3
48
 tgapp-resetpassword==0.1.3
49
 lxml
49
 lxml
50
+python-ldap-test==0.2.0
51
+who-ldap==3.1.0

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

38
 beaker.session.key = tracim
38
 beaker.session.key = tracim
39
 beaker.session.secret = 3283411b-1904-4554-b0e1-883863b53080
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
 #By default session is store in cookies to avoid the overhead
70
 #By default session is store in cookies to avoid the overhead
42
 #of having to manage a session storage. On production you might
71
 #of having to manage a session storage. On production you might
43
 #want to switch to a better session storage.
72
 #want to switch to a better session storage.

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

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
                ]
32
                ]
33
 
33
 
34
 install_requires=[
34
 install_requires=[
35
-    "TurboGears2 >= 2.3.7",
35
+    "TurboGears2==2.3.7",
36
     "Genshi",
36
     "Genshi",
37
     "Mako",
37
     "Mako",
38
     "zope.sqlalchemy >= 0.4",
38
     "zope.sqlalchemy >= 0.4",
39
     "sqlalchemy",
39
     "sqlalchemy",
40
     "alembic",
40
     "alembic",
41
     "repoze.who",
41
     "repoze.who",
42
+    "who-ldap==3.1.0",
43
+    "python-ldap-test==0.2.0",
42
     ]
44
     ]
43
 
45
 
44
 setup(
46
 setup(
67
         ],
69
         ],
68
         'gearbox.plugins': [
70
         'gearbox.plugins': [
69
             'turbogears-devtools = tg.devtools'
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
     dependency_links=[
79
     dependency_links=[
73
-        "http://tg.gy/230"
80
+        "http://tg.gy/230",
74
         ],
81
         ],
75
     zip_safe=False
82
     zip_safe=False
76
 )
83
 )

+ 13 - 0
tracim/test.ini View File

23
 use = main
23
 use = main
24
 skip_authentication = True
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
 # Add additional test specific configuration options as necessary.
39
 # Add additional test specific configuration options as necessary.

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

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

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

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
 # -*- coding: utf-8 -*-
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
 import tg
16
 import tg
17
 from paste.deploy.converters import asbool
17
 from paste.deploy.converters import asbool
18
 
18
 
19
-from tg.configuration import AppConfig
20
 from tgext.pluggable import plug
19
 from tgext.pluggable import plug
21
 from tgext.pluggable import replace_template
20
 from tgext.pluggable import replace_template
22
 
21
 
24
 
23
 
25
 import tracim
24
 import tracim
26
 from tracim import model
25
 from tracim import model
26
+from tracim.config import TracimAppConfig
27
 from tracim.lib import app_globals, helpers
27
 from tracim.lib import app_globals, helpers
28
+from tracim.lib.auth.wrapper import AuthConfigWrapper
28
 from tracim.lib.base import logger
29
 from tracim.lib.base import logger
29
 from tracim.model.data import ActionDescription
30
 from tracim.model.data import ActionDescription
30
 from tracim.model.data import ContentType
31
 from tracim.model.data import ContentType
31
 
32
 
32
-base_config = AppConfig()
33
+base_config = TracimAppConfig()
33
 base_config.renderers = []
34
 base_config.renderers = []
34
 base_config.use_toscawidgets = False
35
 base_config.use_toscawidgets = False
35
 base_config.use_toscawidgets2 = True
36
 base_config.use_toscawidgets2 = True
50
 base_config.model = tracim.model
51
 base_config.model = tracim.model
51
 base_config.DBSession = tracim.model.DBSession
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
 # base_config.flash.cookie_name
57
 # base_config.flash.cookie_name
55
 # base_config.flash.default_status -> Default message status if not specified (ok by default)
58
 # base_config.flash.default_status -> Default message status if not specified (ok by default)
72
 # YOU MUST CHANGE THIS VALUE IN PRODUCTION TO SECURE YOUR APP 
75
 # YOU MUST CHANGE THIS VALUE IN PRODUCTION TO SECURE YOUR APP 
73
 base_config.sa_auth.cookie_secret = "3283411b-1904-4554-b0e1-883863b53080"
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
 # INFO - This is the way to specialize the resetpassword email properties
78
 # INFO - This is the way to specialize the resetpassword email properties
131
 # plug(base_config, 'resetpassword', None, mail_subject=reset_password_email_subject)
79
 # plug(base_config, 'resetpassword', None, mail_subject=reset_password_email_subject)
132
 plug(base_config, 'resetpassword', 'reset_password')
80
 plug(base_config, 'resetpassword', 'reset_password')

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

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

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

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

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

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

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

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
+from webob.exc import HTTPForbidden
2
 
3
 
3
 from tracim import model  as pm
4
 from tracim import model  as pm
4
 
5
 
93
 
94
 
94
     @tg.expose('tracim.templates.user_password_edit_me')
95
     @tg.expose('tracim.templates.user_password_edit_me')
95
     def edit(self):
96
     def edit(self):
97
+        if not tg.config.get('auth_is_internal'):
98
+            raise HTTPForbidden()
99
+
96
         dictified_user = Context(CTX.USER).toDict(tmpl_context.current_user, 'user')
100
         dictified_user = Context(CTX.USER).toDict(tmpl_context.current_user, 'user')
97
         return DictLikeClass(result = dictified_user)
101
         return DictLikeClass(result = dictified_user)
98
 
102
 
99
     @tg.expose()
103
     @tg.expose()
100
     def put(self, current_password, new_password1, new_password2):
104
     def put(self, current_password, new_password1, new_password2):
105
+        if not tg.config.get('auth_is_internal'):
106
+            raise HTTPForbidden()
107
+
101
         # FIXME - Allow only self password or operation for managers
108
         # FIXME - Allow only self password or operation for managers
102
         current_user = tmpl_context.current_user
109
         current_user = tmpl_context.current_user
103
 
110
 
174
         current_user = tmpl_context.current_user
181
         current_user = tmpl_context.current_user
175
         assert user_id==current_user.user_id
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
         api = UserApi(tmpl_context.current_user)
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
         tg.flash(_('profile updated.'))
192
         tg.flash(_('profile updated.'))
180
         if next_url:
193
         if next_url:
181
             tg.redirect(tg.url(next_url))
194
             tg.redirect(tg.url(next_url))
182
         tg.redirect(self.url())
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

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

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

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
-# 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
 msgid ""
6
 msgid ""
7
 msgstr ""
7
 msgstr ""
8
 "Project-Id-Version: pod 0.1\n"
8
 "Project-Id-Version: pod 0.1\n"
9
 "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
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
 "PO-Revision-Date: 2015-09-08 15:27+0100\n"
11
 "PO-Revision-Date: 2015-09-08 15:27+0100\n"
12
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
12
 "Last-Translator: Damien Accorsi <damien.accorsi@free.fr>\n"
13
+"Language: fr\n"
13
 "Language-Team: fr_FR <LL@li.org>\n"
14
 "Language-Team: fr_FR <LL@li.org>\n"
14
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
15
 "Plural-Forms: nplurals=2; plural=(n > 1)\n"
15
 "MIME-Version: 1.0\n"
16
 "MIME-Version: 1.0\n"
16
 "Content-Type: text/plain; charset=utf-8\n"
17
 "Content-Type: text/plain; charset=utf-8\n"
17
 "Content-Transfer-Encoding: 8bit\n"
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
 msgid "Password reset request"
22
 msgid "Password reset request"
22
 msgstr "Réinitialisation du mot de passe"
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
 #, python-format
26
 #, python-format
26
 msgid ""
27
 msgid ""
27
 "\n"
28
 "\n"
44
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
45
 " à l'originie de cette requête, merci d'ignorer et/ou supprimer cet "
45
 "e-mail.\n"
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
 #: tracim/templates/widgets/forms.mak:16 tracim/templates/widgets/forms.mak:39
49
 #: tracim/templates/widgets/forms.mak:16 tracim/templates/widgets/forms.mak:39
49
 #: tracim/templates/widgets/forms.mak:40
50
 #: tracim/templates/widgets/forms.mak:40
50
 msgid "New password"
51
 msgid "New password"
51
 msgstr "Nouveau mot de passe"
52
 msgstr "Nouveau mot de passe"
52
 
53
 
53
-#: tracim/config/app_cfg.py:155
54
+#: tracim/config/app_cfg.py:104
54
 msgid "Confirm new password"
55
 msgid "Confirm new password"
55
 msgstr "Confirmer le nouveau mot de passe"
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
 msgid "Save new password"
59
 msgid "Save new password"
59
 msgstr "Enregistrer le nouveau mot de passe"
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
 msgid "Email address"
63
 msgid "Email address"
63
 msgstr "Adresse email"
64
 msgstr "Adresse email"
64
 
65
 
65
-#: tracim/config/app_cfg.py:158
66
+#: tracim/config/app_cfg.py:107
66
 msgid "Send Request"
67
 msgid "Send Request"
67
 msgstr "Valider"
68
 msgstr "Valider"
68
 
69
 
69
-#: tracim/config/app_cfg.py:161
70
+#: tracim/config/app_cfg.py:110
70
 msgid "Password reset request sent"
71
 msgid "Password reset request sent"
71
 msgstr "Requête de réinitialisation du mot de passe envoyée"
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
 msgid "Invalid password reset request"
75
 msgid "Invalid password reset request"
75
 msgstr "Requête de réinitialisation du mot de passe invalide"
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
 msgid "Password reset request timed out"
79
 msgid "Password reset request timed out"
79
 msgstr "Echec de la requête de réinitialisation du mot de passe"
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
 msgid "Password changed successfully"
83
 msgid "Password changed successfully"
83
 msgstr "Mot de passe changé"
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
 #: tracim/controllers/content.py:426
87
 #: tracim/controllers/content.py:426
87
 msgid "{} updated"
88
 msgid "{} updated"
88
 msgstr "{} mis(e) à jour"
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
 msgid "{} not updated: the content did not change"
92
 msgid "{} not updated: the content did not change"
92
 msgstr "{} non mis à jour : le contenu n'a pas changé"
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
 #: tracim/controllers/content.py:436
96
 #: tracim/controllers/content.py:436
96
 msgid "{} not updated - error: {}"
97
 msgid "{} not updated - error: {}"
97
 msgstr "{} pas mis(e) à jour - erreur : {}"
98
 msgstr "{} pas mis(e) à jour - erreur : {}"
98
 
99
 
99
-#: tracim/controllers/__init__.py:296
100
+#: tracim/controllers/__init__.py:297
100
 msgid "{} status updated"
101
 msgid "{} status updated"
101
 msgstr "Statut de {} mis(e) à jour"
102
 msgstr "Statut de {} mis(e) à jour"
102
 
103
 
103
-#: tracim/controllers/__init__.py:300
104
+#: tracim/controllers/__init__.py:301
104
 msgid "{} status not updated: {}"
105
 msgid "{} status not updated: {}"
105
 msgstr "Statut de {} non mis(e) à jour : {}"
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
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
109
 msgid "{} archived. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
109
 msgstr "{} archivé(e). <a class=\"alert-link\" href=\"{}\">Annuler l'opération</a>"
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
 msgid "{} not archived: {}"
113
 msgid "{} not archived: {}"
113
 msgstr "{} non archivé(e) : {}"
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
 msgid "{} unarchived."
117
 msgid "{} unarchived."
117
 msgstr "{} désarchivé(e)"
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
 msgid "{} not un-archived: {}"
121
 msgid "{} not un-archived: {}"
121
 msgstr "{} non désarchivé(e) : {}"
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
 #: tracim/controllers/content.py:887
125
 #: tracim/controllers/content.py:887
125
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
126
 msgid "{} deleted. <a class=\"alert-link\" href=\"{}\">Cancel action</a>"
126
 msgstr ""
127
 msgstr ""
127
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
128
 "{} supprimé(e). <a class=\"alert-link\" href=\"{}\">Annuler "
128
 "l'opération</a>"
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
 #: tracim/controllers/content.py:896
132
 #: tracim/controllers/content.py:896
132
 msgid "{} not deleted: {}"
133
 msgid "{} not deleted: {}"
133
 msgstr "{} non supprimé(e) : {}"
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
 #: tracim/controllers/content.py:911
137
 #: tracim/controllers/content.py:911
137
 msgid "{} undeleted."
138
 msgid "{} undeleted."
138
 msgstr "{} restauré(e)."
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
 #: tracim/controllers/content.py:921
142
 #: tracim/controllers/content.py:921
142
 msgid "{} not un-deleted: {}"
143
 msgid "{} not un-deleted: {}"
143
 msgstr "{} non restauré(e) : {}"
144
 msgstr "{} non restauré(e) : {}"
144
 
145
 
145
-#: tracim/controllers/__init__.py:428
146
+#: tracim/controllers/__init__.py:429
146
 msgid "{} marked as read."
147
 msgid "{} marked as read."
147
 msgstr "{} marqué \"lu\"."
148
 msgstr "{} marqué \"lu\"."
148
 
149
 
149
-#: tracim/controllers/__init__.py:436
150
+#: tracim/controllers/__init__.py:437
150
 msgid "{} not marked as read: {}"
151
 msgid "{} not marked as read: {}"
151
 msgstr "{} non marqué \"lu\" : {}"
152
 msgstr "{} non marqué \"lu\" : {}"
152
 
153
 
153
-#: tracim/controllers/__init__.py:450
154
+#: tracim/controllers/__init__.py:451
154
 msgid "{} marked unread."
155
 msgid "{} marked unread."
155
 msgstr "{} marqué \"non lu\"."
156
 msgstr "{} marqué \"non lu\"."
156
 
157
 
157
-#: tracim/controllers/__init__.py:458
158
+#: tracim/controllers/__init__.py:459
158
 msgid "{} not marked unread: {}"
159
 msgid "{} not marked unread: {}"
159
 msgstr "{} non marqué \"non lu\" : {}"
160
 msgstr "{} non marqué \"non lu\" : {}"
160
 
161
 
247
 msgid "Successfully logged out. We hope to see you soon!"
248
 msgid "Successfully logged out. We hope to see you soon!"
248
 msgstr "Déconnexion réussie. Nous espérons vous revoir bientôt !"
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
 msgid "Notification enabled for workspace {}"
252
 msgid "Notification enabled for workspace {}"
252
 msgstr "Notifications activées pour l'espace de travail {}"
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
 msgid "Notification disabled for workspace {}"
256
 msgid "Notification disabled for workspace {}"
256
 msgstr "Notifications désactivées pour l'espace de travail {}"
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
 msgid "Empty password is not allowed."
260
 msgid "Empty password is not allowed."
260
 msgstr "Le mot de passe ne doit pas être vide"
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
 msgid "The current password you typed is wrong"
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
 msgid "New passwords do not match."
268
 msgid "New passwords do not match."
268
 msgstr "Les mots de passe ne concordent pas"
269
 msgstr "Les mots de passe ne concordent pas"
269
 
270
 
270
-#: tracim/controllers/user.py:121
271
+#: tracim/controllers/user.py:128
271
 msgid "Your password has been changed"
272
 msgid "Your password has been changed"
272
 msgstr "Votre mot de passe a été changé"
273
 msgstr "Votre mot de passe a été changé"
273
 
274
 
274
-#: tracim/controllers/user.py:179
275
+#: tracim/controllers/user.py:192
275
 msgid "profile updated."
276
 msgid "profile updated."
276
 msgstr "Profil mis à jour"
277
 msgstr "Profil mis à jour"
277
 
278
 
409
 msgid "The content did not changed"
410
 msgid "The content did not changed"
410
 msgstr "Le contenu est inchangé"
411
 msgstr "Le contenu est inchangé"
411
 
412
 
412
-#: tracim/lib/helpers.py:34
413
+#: tracim/lib/helpers.py:33
413
 msgid "{date} at {time}"
414
 msgid "{date} at {time}"
414
 msgstr "{date} à {time}"
415
 msgstr "{date} à {time}"
415
 
416
 
416
-#: tracim/lib/notifications.py:285
417
+#: tracim/lib/notifications.py:289
417
 msgid "<span id=\"content-intro-username\">{}</span> added a comment:"
418
 msgid "<span id=\"content-intro-username\">{}</span> added a comment:"
418
 msgstr "<span id=\"content-intro-username\">{}</span> a ajouté un commentaire :"
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
 msgid "Answer"
422
 msgid "Answer"
422
 msgstr "Répondre"
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
 msgid "View online"
427
 msgid "View online"
427
 msgstr "Voir en ligne"
428
 msgstr "Voir en ligne"
428
 
429
 
429
-#: tracim/lib/notifications.py:297
430
+#: tracim/lib/notifications.py:301
430
 msgid "<span id=\"content-intro-username\">{}</span> started a thread entitled:"
431
 msgid "<span id=\"content-intro-username\">{}</span> started a thread entitled:"
431
 msgstr ""
432
 msgstr ""
432
 "<span id=\"content-intro-username\">{}</span> a lancé une discussion "
433
 "<span id=\"content-intro-username\">{}</span> a lancé une discussion "
433
 "intitulée :"
434
 "intitulée :"
434
 
435
 
435
-#: tracim/lib/notifications.py:302
436
+#: tracim/lib/notifications.py:306
436
 msgid "<span id=\"content-intro-username\">{}</span> added a file entitled:"
437
 msgid "<span id=\"content-intro-username\">{}</span> added a file entitled:"
437
 msgstr ""
438
 msgstr ""
438
 "<span id=\"content-intro-username\">{}</span> a ajouté un fichier "
439
 "<span id=\"content-intro-username\">{}</span> a ajouté un fichier "
439
 "intitulé :"
440
 "intitulé :"
440
 
441
 
441
-#: tracim/lib/notifications.py:312
442
+#: tracim/lib/notifications.py:316
442
 msgid "<span id=\"content-intro-username\">{}</span> added a page entitled:"
443
 msgid "<span id=\"content-intro-username\">{}</span> added a page entitled:"
443
 msgstr ""
444
 msgstr ""
444
 "<span id=\"content-intro-username\">{}</span> a ajouté une page intitulée"
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
 msgid "<span id=\"content-intro-username\">{}</span> uploaded a new revision."
449
 msgid "<span id=\"content-intro-username\">{}</span> uploaded a new revision."
449
 msgstr ""
450
 msgstr ""
450
 "<span id=\"content-intro-username\">{}</span> a téléchargé une nouvelle "
451
 "<span id=\"content-intro-username\">{}</span> a téléchargé une nouvelle "
451
 "version."
452
 "version."
452
 
453
 
453
-#: tracim/lib/notifications.py:324
454
+#: tracim/lib/notifications.py:328
454
 msgid "<span id=\"content-intro-username\">{}</span> updated this page."
455
 msgid "<span id=\"content-intro-username\">{}</span> updated this page."
455
 msgstr "<span id=\"content-intro-username\">{}</span> a mis à jour cette page."
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
 msgid "<p id=\"content-body-intro\">Here is an overview of the changes:</p>"
459
 msgid "<p id=\"content-body-intro\">Here is an overview of the changes:</p>"
459
 msgstr "<p id=\"content-body-intro\">Voici un aperçu des modifications :</p>"
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
 msgid ""
463
 msgid ""
463
 "<span id=\"content-intro-username\">{}</span> updated the thread "
464
 "<span id=\"content-intro-username\">{}</span> updated the thread "
464
 "description."
465
 "description."
466
 "<span id=\"content-intro-username\">{}</span> a mis à jour la description"
467
 "<span id=\"content-intro-username\">{}</span> a mis à jour la description"
467
 " de la discussion."
468
 " de la discussion."
468
 
469
 
469
-#: tracim/lib/notifications.py:353
470
+#: tracim/lib/notifications.py:357
470
 msgid ""
471
 msgid ""
471
 "<span id=\"content-intro-username\">{}</span> updated the file "
472
 "<span id=\"content-intro-username\">{}</span> updated the file "
472
 "description."
473
 "description."
747
 msgid "Welcome to your home, {username}."
748
 msgid "Welcome to your home, {username}."
748
 msgstr "Bienvenue, {username}."
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
 msgid "Not Read"
752
 msgid "Not Read"
752
 msgstr "Non lus"
753
 msgstr "Non lus"
753
 
754
 
759
 msgid "Last activity: {datetime}"
760
 msgid "Last activity: {datetime}"
760
 msgstr "Dernière activité : {datetime}"
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
 msgid "Recent Activity"
764
 msgid "Recent Activity"
764
 msgstr "Activité récente"
765
 msgstr "Activité récente"
765
 
766
 
940
 msgid "Edit user"
941
 msgid "Edit user"
941
 msgstr "Modifier l'utilisateur"
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
 #: tracim/templates/widgets/forms.mak:7 tracim/templates/widgets/forms.mak:35
945
 #: tracim/templates/widgets/forms.mak:7 tracim/templates/widgets/forms.mak:35
945
 msgid "Change password"
946
 msgid "Change password"
946
 msgstr "Changer le mot de passe"
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
 #: tracim/templates/admin/user_getall.mak:51
950
 #: tracim/templates/admin/user_getall.mak:51
950
 msgid "Password"
951
 msgid "Password"
951
 msgstr "Mot de passe"
952
 msgstr "Mot de passe"
952
 
953
 
953
-#: tracim/templates/user_toolbars.mak:24
954
+#: tracim/templates/user_toolbars.mak:28
954
 msgid "Disable user"
955
 msgid "Disable user"
955
 msgstr "Désactiver l'utilisateur"
956
 msgstr "Désactiver l'utilisateur"
956
 
957
 
957
-#: tracim/templates/user_toolbars.mak:24
958
+#: tracim/templates/user_toolbars.mak:28
958
 msgid "Disable"
959
 msgid "Disable"
959
 msgstr "Désactiver"
960
 msgstr "Désactiver"
960
 
961
 
961
-#: tracim/templates/user_toolbars.mak:26
962
+#: tracim/templates/user_toolbars.mak:30
962
 msgid "Enable user"
963
 msgid "Enable user"
963
 msgstr "Activer l'utilisateur"
964
 msgstr "Activer l'utilisateur"
964
 
965
 
965
-#: tracim/templates/user_toolbars.mak:26
966
+#: tracim/templates/user_toolbars.mak:30
966
 msgid "Enable"
967
 msgid "Enable"
967
 msgstr "Activer"
968
 msgstr "Activer"
968
 
969
 
969
-#: tracim/templates/user_toolbars.mak:42
970
+#: tracim/templates/user_toolbars.mak:46
970
 msgid "Edit my profile"
971
 msgid "Edit my profile"
971
 msgstr "Mon profil"
972
 msgstr "Mon profil"
972
 
973
 
973
-#: tracim/templates/user_toolbars.mak:47
974
+#: tracim/templates/user_toolbars.mak:55
974
 msgid "Go to..."
975
 msgid "Go to..."
975
 msgstr "Voir..."
976
 msgstr "Voir..."
976
 
977
 
977
-#: tracim/templates/user_toolbars.mak:55
978
+#: tracim/templates/user_toolbars.mak:63
978
 msgid "Not read"
979
 msgid "Not read"
979
 msgstr "Non lus"
980
 msgstr "Non lus"
980
 
981
 
981
-#: tracim/templates/user_toolbars.mak:56
982
+#: tracim/templates/user_toolbars.mak:64
982
 msgid "Activity"
983
 msgid "Activity"
983
 msgstr "Activité"
984
 msgstr "Activité"
984
 
985
 
985
-#: tracim/templates/user_toolbars.mak:57
986
+#: tracim/templates/user_toolbars.mak:65
986
 msgid "My Workspaces"
987
 msgid "My Workspaces"
987
 msgstr "Mes espaces de travail"
988
 msgstr "Mes espaces de travail"
988
 
989
 
989
-#: tracim/templates/user_toolbars.mak:57
990
+#: tracim/templates/user_toolbars.mak:65
990
 msgid "Spaces"
991
 msgid "Spaces"
991
 msgstr "Espaces"
992
 msgstr "Espaces"
992
 
993
 
1006
 #: tracim/templates/user_workspace_forms.mak:14
1007
 #: tracim/templates/user_workspace_forms.mak:14
1007
 #: tracim/templates/user_workspace_forms.mak:15
1008
 #: tracim/templates/user_workspace_forms.mak:15
1008
 #: tracim/templates/user_workspace_forms.mak:100
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
 #: tracim/templates/admin/user_getall.mak:43
1012
 #: tracim/templates/admin/user_getall.mak:43
1012
 #: tracim/templates/admin/user_getall.mak:44
1013
 #: tracim/templates/admin/user_getall.mak:44
1013
 #: tracim/templates/admin/workspace_getall.mak:41
1014
 #: tracim/templates/admin/workspace_getall.mak:41
1044
 #: tracim/templates/user_workspace_forms.mak:39
1045
 #: tracim/templates/user_workspace_forms.mak:39
1045
 #: tracim/templates/user_workspace_forms.mak:58
1046
 #: tracim/templates/user_workspace_forms.mak:58
1046
 #: tracim/templates/user_workspace_forms.mak:81
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
 #: tracim/templates/admin/user_getall.mak:66
1049
 #: tracim/templates/admin/user_getall.mak:66
1049
 #: tracim/templates/admin/workspace_getall.mak:53
1050
 #: tracim/templates/admin/workspace_getall.mak:53
1050
 #: tracim/templates/admin/workspace_getone.mak:93
1051
 #: tracim/templates/admin/workspace_getone.mak:93
1109
 msgid "Edit User"
1110
 msgid "Edit User"
1110
 msgstr "Modifier l'utilisateur"
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
 #: tracim/templates/admin/user_getall.mak:47
1119
 #: tracim/templates/admin/user_getall.mak:47
1114
 #: tracim/templates/admin/user_getall.mak:106
1120
 #: tracim/templates/admin/user_getall.mak:106
1115
 msgid "Email"
1121
 msgid "Email"

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

1
+from tracim.lib.auth.base import Auth

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

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

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

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

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
         tmpl_context.identity = request.identity
31
         tmpl_context.identity = request.identity
32
         return TGController.__call__(self, environ, context)
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
     @property
38
     @property
35
     def parent_controller(self):
39
     def parent_controller(self):
36
         possible_parent = None
40
         possible_parent = None

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

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
 
17
 
18
     def get_one(self, group_id) -> Group:
18
     def get_one(self, group_id) -> Group:
19
         return self._base_query().filter(Group.group_id==group_id).one()
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
             result += '…'
198
             result += '…'
199
 
199
 
200
     return result
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
     def get_one_by_email(self, email: str):
27
     def get_one_by_email(self, email: str):
28
         return self._base_query().filter(User.email==email).one()
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
         if do_save:
37
         if do_save:
34
             self.save(user)
38
             self.save(user)
35
 
39
 

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

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

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

361
 
361
 
362
 .panel-heading > h3 { font-size: 1.5em;}
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
                                 <button type="submit" class="btn btn-small btn-success text-right">
50
                                 <button type="submit" class="btn btn-small btn-success text-right">
51
                                     <i class="fa fa-check"></i> ${_('Login')}
51
                                     <i class="fa fa-check"></i> ${_('Login')}
52
                                 </button>
52
                                 </button>
53
-                                % if CFG.EMAIL_NOTIFICATION_ACTIVATED:
53
+                                % if CFG.EMAIL_NOTIFICATION_ACTIVATED and tmpl_context.auth_is_internal:
54
                                     <div class="pull-left">
54
                                     <div class="pull-left">
55
                                         <a class="btn btn-link" href="${tg.url('/reset_password')}"><i class="fa fa-magic"></i> ${_('Forgot password?')}</a>
55
                                         <a class="btn btn-link" href="${tg.url('/reset_password')}"><i class="fa fa-magic"></i> ${_('Forgot password?')}</a>
56
                                     </div>
56
                                     </div>

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

15
                 endif
15
                 endif
16
             %>
16
             %>
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>
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
         </div>
23
         </div>
20
         <p></p>
24
         <p></p>
21
         % if current_user.profile.id>2 and current_user.id!=user.id:
25
         % if current_user.profile.id>2 and current_user.id!=user.id:
39
                 user_edit_url = tg.url('/user/{}/edit'.format(current_user.id), {'next_url': '/home'})
43
                 user_edit_url = tg.url('/user/{}/edit'.format(current_user.id), {'next_url': '/home'})
40
                 user_password_edit_url = tg.url('/user/{}/password/edit'.format(current_user.id))
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
         </div>
52
         </div>
45
         <p></p>
53
         <p></p>
46
         <h3 class="t-spacer-above" style="margin-top: 1em;">
54
         <h3 class="t-spacer-above" style="margin-top: 1em;">

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

98
         <div class="modal-body">
98
         <div class="modal-body">
99
             <div class="form-group">
99
             <div class="form-group">
100
                 <label for="name">${_('Name')}</label>
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
             </div>
105
             </div>
103
             <div class="form-group">
106
             <div class="form-group">
104
                 <label for="email">${_('Email')}</label>
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
             </div>
112
             </div>
107
         </div>
113
         </div>
108
         <div class="modal-footer">
114
         <div class="modal-footer">

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

1
 # -*- coding: utf-8 -*-
1
 # -*- coding: utf-8 -*-
2
 """Unit and functional test suite for tracim."""
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
 import tg
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
 from sqlalchemy.engine import reflection
15
 from sqlalchemy.engine import reflection
16
-
17
 from sqlalchemy.schema import DropConstraint
16
 from sqlalchemy.schema import DropConstraint
18
 from sqlalchemy.schema import DropSequence
17
 from sqlalchemy.schema import DropSequence
19
 from sqlalchemy.schema import DropTable
18
 from sqlalchemy.schema import DropTable
21
 from sqlalchemy.schema import MetaData
20
 from sqlalchemy.schema import MetaData
22
 from sqlalchemy.schema import Sequence
21
 from sqlalchemy.schema import Sequence
23
 from sqlalchemy.schema import Table
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
 from tracim.lib.base import logger
30
 from tracim.lib.base import logger
31
+from tracim.model import DBSession
28
 
32
 
29
 __all__ = ['setup_app', 'setup_db', 'teardown_db', 'TestController']
33
 __all__ = ['setup_app', 'setup_db', 'teardown_db', 'TestController']
30
 
34
 
31
 application_name = 'main_without_authn'
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
 def load_app(name=application_name):
53
 def load_app(name=application_name):
35
     """Load the test application."""
54
     """Load the test application."""
36
     return TestApp(loadapp('config:test.ini#%s' % name, relative_to=getcwd()))
55
     return TestApp(loadapp('config:test.ini#%s' % name, relative_to=getcwd()))
131
 class TestStandard(object):
150
 class TestStandard(object):
132
 
151
 
133
     application_under_test = application_name
152
     application_under_test = application_name
153
+    fixtures = [BaseFixture, ]
134
 
154
 
135
     def setUp(self):
155
     def setUp(self):
136
         self.app = load_app(self.application_under_test)
156
         self.app = load_app(self.application_under_test)
150
         setup_db()
170
         setup_db()
151
         logger.debug(self, 'Start Database Setup... -> done')
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
         self.app.get('/_test_vars')  # Allow to create fake context
178
         self.app.get('/_test_vars')  # Allow to create fake context
154
         tg.i18n.set_lang('en')  # Set a default lang
179
         tg.i18n.set_lang('en')  # Set a default lang
155
 
180
 
156
     def tearDown(self):
181
     def tearDown(self):
157
         transaction.commit()
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
 class TestController(object):
200
 class TestController(object):
160
     """Base functional test case for the controllers.
201
     """Base functional test case for the controllers.
161
 
202
 
173
     """
214
     """
174
 
215
 
175
     application_under_test = application_name
216
     application_under_test = application_name
217
+    fixtures = [BaseFixture, ]
176
 
218
 
177
     def setUp(self):
219
     def setUp(self):
178
         """Setup test fixture for each functional test method."""
220
         """Setup test fixture for each functional test method."""
186
         setup_app(section_name=self.application_under_test)
228
         setup_app(section_name=self.application_under_test)
187
         setup_db()
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
     def tearDown(self):
235
     def tearDown(self):
191
         """Tear down test fixture for each functional test method."""
236
         """Tear down test fixture for each functional test method."""
192
-        # model.DBSession.remove()
237
+        DBSession.close()
193
         teardown_db()
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

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

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

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

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

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

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
 """Setup the tracim application"""
2
 """Setup the tracim application"""
3
 from __future__ import print_function
3
 from __future__ import print_function
4
 
4
 
5
-import logging
6
-from tg import config
7
-from tracim import model
8
 import transaction
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
 def bootstrap(command, conf, vars):
11
 def bootstrap(command, conf, vars):
11
     """Place any commands to setup tracim here"""
12
     """Place any commands to setup tracim here"""
12
 
13
 
13
     # <websetup.bootstrap.before.auth
14
     # <websetup.bootstrap.before.auth
14
     from sqlalchemy.exc import IntegrityError
15
     from sqlalchemy.exc import IntegrityError
15
     try:
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
     except IntegrityError:
19
     except IntegrityError:
47
         print('Warning, there was a problem adding your auth data, it may have already been added:')
20
         print('Warning, there was a problem adding your auth data, it may have already been added:')
48
         import traceback
21
         import traceback

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

174
     display_name character varying(255),
174
     display_name character varying(255),
175
     password character varying(128),
175
     password character varying(128),
176
     created timestamp without time zone,
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
 CREATE TABLE user_group (
181
 CREATE TABLE user_group (