Browse Source

USer management commands

Bastien Sevajol 9 years ago
parent
commit
d7314f6458

+ 2 - 0
tracim/setup.py View File

@@ -72,6 +72,8 @@ setup(
72 72
         ],
73 73
         'gearbox.commands': [
74 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 79
     dependency_links=[

+ 91 - 1
tracim/tracim/command/__init__.py View File

@@ -1 +1,91 @@
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 View File

@@ -0,0 +1,151 @@
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 View File

@@ -0,0 +1,17 @@
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 View File

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

+ 46 - 13
tracim/tracim/tests/__init__.py View File

@@ -1,22 +1,16 @@
1 1
 # -*- coding: utf-8 -*-
2 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 6
 import ldap3
7
+import tg
8
+import transaction
9
+from gearbox.commands.setup_app import SetupAppCommand
7 10
 from ldap_test import LdapServer
8 11
 from nose.tools import ok_
9 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 13
 from sqlalchemy.engine import reflection
19
-
20 14
 from sqlalchemy.schema import DropConstraint
21 15
 from sqlalchemy.schema import DropSequence
22 16
 from sqlalchemy.schema import DropTable
@@ -24,10 +18,12 @@ from sqlalchemy.schema import ForeignKeyConstraint
24 18
 from sqlalchemy.schema import MetaData
25 19
 from sqlalchemy.schema import Sequence
26 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 24
 from who_ldap import make_connection
30 25
 
26
+from tracim.command import BaseCommand
31 27
 from tracim.lib.base import logger
32 28
 from tracim.model import DBSession
33 29
 
@@ -161,6 +157,38 @@ class TestStandard(object):
161 157
     def tearDown(self):
162 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 192
 class TestController(object):
165 193
     """Base functional test case for the controllers.
166 194
 
@@ -248,3 +276,8 @@ class LDAPTest:
248 276
                 ok_(False, "Cannot establish connection with LDAP test server")
249 277
             else:
250 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 View File

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