浏览代码

USer management commands

Bastien Sevajol 9 年前
父节点
当前提交
d7314f6458

+ 2 - 0
tracim/setup.py 查看文件

72
         ],
72
         ],
73
         'gearbox.commands': [
73
         'gearbox.commands': [
74
             'ldap_server = tracim.command.ldap_test_server:LDAPTestServerCommand',
74
             'ldap_server = tracim.command.ldap_test_server:LDAPTestServerCommand',
75
+            'user_create = tracim.command.user:CreateUserCommand',
76
+            'user_update = tracim.command.user:UpdateUserCommand',
75
         ]
77
         ]
76
     },
78
     },
77
     dependency_links=[
79
     dependency_links=[

+ 91 - 1
tracim/tracim/command/__init__.py 查看文件

1
-__author__ = 'bux'
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
+            print(exc)
23
+
24
+
25
+class AppContextCommand(BaseCommand):
26
+    """
27
+    Command who initialize app context at beginning of take_action method.
28
+    """
29
+
30
+    def __init__(self, *args, **kwargs):
31
+        super(AppContextCommand, self).__init__(*args, **kwargs)
32
+
33
+    @staticmethod
34
+    def _get_initialized_app_context(parsed_args):
35
+        """
36
+        :param parsed_args: parsed args (eg. from take_action)
37
+        :return: (wsgi_app, test_app)
38
+        """
39
+        config_file = parsed_args.config_file
40
+        config_name = 'config:%s' % config_file
41
+        here_dir = os.getcwd()
42
+
43
+        # Load locals and populate with objects for use in shell
44
+        sys.path.insert(0, here_dir)
45
+
46
+        # Load the wsgi app first so that everything is initialized right
47
+        wsgi_app = loadapp(config_name, relative_to=here_dir)
48
+        test_app = TestApp(wsgi_app)
49
+
50
+        # Make available the tg.request and other global variables
51
+        tresponse = test_app.get('/_test_vars')
52
+
53
+        return wsgi_app, test_app
54
+
55
+    def take_action(self, parsed_args):
56
+        super(AppContextCommand, self).take_action(parsed_args)
57
+        if self.auto_setup_app:
58
+            self._get_initialized_app_context(parsed_args)
59
+
60
+    def get_parser(self, prog_name):
61
+        parser = super(AppContextCommand, self).get_parser(prog_name)
62
+
63
+        parser.add_argument("-c", "--config",
64
+                            help='application config file to read (default: development.ini)',
65
+                            dest='config_file', default="development.ini")
66
+        return parser
67
+
68
+    def run(self, parsed_args):
69
+        super().run(parsed_args)
70
+        transaction.commit()
71
+
72
+
73
+class Extender(argparse.Action):
74
+    """
75
+    Copied class from http://stackoverflow.com/a/12461237/801924
76
+    """
77
+    def __call__(self, parser, namespace, values, option_strings=None):
78
+        # Need None here incase `argparse.SUPPRESS` was supplied for `dest`
79
+        dest = getattr(namespace, self.dest, None)
80
+        # print dest,self.default,values,option_strings
81
+        if not hasattr(dest, 'extend') or dest == self.default:
82
+            dest = []
83
+            setattr(namespace, self.dest, dest)
84
+            # if default isn't set to None, this method might be called
85
+            # with the default as `values` for other arguements which
86
+            # share this destination.
87
+            parser.set_defaults(**{self.dest: None})
88
+        try:
89
+            dest.extend(values)
90
+        except ValueError:
91
+            dest.append(values)

+ 151 - 0
tracim/tracim/command/user.py 查看文件

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

+ 17 - 0
tracim/tracim/lib/exception.py 查看文件

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

+ 3 - 0
tracim/tracim/lib/group.py 查看文件

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()

+ 46 - 13
tracim/tracim/tests/__init__.py 查看文件

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
-
4
-from os import getcwd, path
3
+import argparse
4
+from os import getcwd
5
 
5
 
6
 import ldap3
6
 import ldap3
7
+import tg
8
+import transaction
9
+from gearbox.commands.setup_app import SetupAppCommand
7
 from ldap_test import LdapServer
10
 from ldap_test import LdapServer
8
 from nose.tools import ok_
11
 from nose.tools import ok_
9
 from paste.deploy import loadapp
12
 from paste.deploy import loadapp
10
-from webtest import TestApp
11
-
12
-from gearbox.commands.setup_app import SetupAppCommand
13
-
14
-import tg
15
-from tg import config
16
-from tg.util import Bunch
17
-
18
 from sqlalchemy.engine import reflection
13
 from sqlalchemy.engine import reflection
19
-
20
 from sqlalchemy.schema import DropConstraint
14
 from sqlalchemy.schema import DropConstraint
21
 from sqlalchemy.schema import DropSequence
15
 from sqlalchemy.schema import DropSequence
22
 from sqlalchemy.schema import DropTable
16
 from sqlalchemy.schema import DropTable
24
 from sqlalchemy.schema import MetaData
18
 from sqlalchemy.schema import MetaData
25
 from sqlalchemy.schema import Sequence
19
 from sqlalchemy.schema import Sequence
26
 from sqlalchemy.schema import Table
20
 from sqlalchemy.schema import Table
27
-
28
-import transaction
21
+from tg import config
22
+from tg.util import Bunch
23
+from webtest import TestApp
29
 from who_ldap import make_connection
24
 from who_ldap import make_connection
30
 
25
 
26
+from tracim.command import BaseCommand
31
 from tracim.lib.base import logger
27
 from tracim.lib.base import logger
32
 from tracim.model import DBSession
28
 from tracim.model import DBSession
33
 
29
 
161
     def tearDown(self):
157
     def tearDown(self):
162
         transaction.commit()
158
         transaction.commit()
163
 
159
 
160
+
161
+class TestCommand(TestStandard):
162
+    def __init__(self, *args, **kwargs):
163
+        super().__init__(*args, **kwargs)
164
+        # We disable app loading from commands classes
165
+        BaseCommand.auto_setup_app = False
166
+        # Hack parser object to test conditions
167
+        BaseCommand.get_parser = self._get_test_parser()
168
+
169
+    def _get_test_parser(self):
170
+        def get_parser(self, prog_name):
171
+            parser = ArgumentParser(
172
+                description=self.get_description(),
173
+                prog=prog_name,
174
+                add_help=False
175
+            )
176
+            return parser
177
+        return get_parser
178
+
179
+    def _execute_command(self, command_class, command_name, sub_argv):
180
+        parser = argparse.ArgumentParser()
181
+        command = command_class(self.app, parser)
182
+        cmd_parser = command.get_parser(command_name)
183
+        parsed_args = cmd_parser.parse_args(sub_argv)
184
+        return command.run(parsed_args)
185
+
186
+    def setUp(self):
187
+        super().setUp()
188
+        # Ensure app config is loaded
189
+        self.app.get('/_test_vars')
190
+
191
+
164
 class TestController(object):
192
 class TestController(object):
165
     """Base functional test case for the controllers.
193
     """Base functional test case for the controllers.
166
 
194
 
248
                 ok_(False, "Cannot establish connection with LDAP test server")
276
                 ok_(False, "Cannot establish connection with LDAP test server")
249
             else:
277
             else:
250
                 ok_(True)
278
                 ok_(True)
279
+
280
+
281
+class ArgumentParser(argparse.ArgumentParser):
282
+    def exit(self, status=0, message=None):
283
+        raise Exception(message)

+ 80 - 0
tracim/tracim/tests/command/user.py 查看文件

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
+