user-modify.py 16.8 KB
Newer Older
1
# Copyright 2012 GRNET S.A. All rights reserved.
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
#   1. Redistributions of source code must retain the above
#      copyright notice, this list of conditions and the following
#      disclaimer.
#
#   2. Redistributions in binary form must reproduce the above
#      copyright notice, this list of conditions and the following
#      disclaimer in the documentation and/or other materials
#      provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.

34
import string
35
from datetime import datetime
36

37
38
from optparse import make_option

39
from django.core import management
40
from django.db import transaction
41
from django.core.management.base import BaseCommand, CommandError
42
from django.contrib.auth.models import Group
43
from django.core.exceptions import ValidationError
44
from django.core.validators import validate_email
45

46
from synnefo.util import units
47
from astakos.im.models import AstakosUser, Resource
48
from astakos.im import quotas
49
from astakos.im import activation_backends
50
51
from ._common import (remove_user_permission, add_user_permission, is_uuid,
                      show_resource_value)
52
53

activation_backend = activation_backends.get_backend()
54

55

56
class Command(BaseCommand):
57
    args = "<user ID> (or --all)"
58
    help = "Modify a user's attributes"
59

Olga Brani's avatar
Olga Brani committed
60
    option_list = BaseCommand.option_list + (
61
62
63
64
65
66
67
68
69
        make_option('--all',
                    action='store_true',
                    default=False,
                    help=("Operate on all users. Currently only setting "
                          "base quota is supported in this mode. Can be "
                          "combined with `--exclude'.")),
        make_option('--exclude',
                    help=("If `--all' is given, exclude users given as a "
                          "list of uuids: uuid1,uuid2,uuid3")),
70
        make_option('--invitations',
71
72
73
                    dest='invitations',
                    metavar='NUM',
                    help="Update user's invitations"),
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
74
        make_option('--level',
75
76
77
                    dest='level',
                    metavar='NUM',
                    help="Update user's level"),
78
        make_option('--password',
79
80
81
                    dest='password',
                    metavar='PASSWORD',
                    help="Set user's password"),
82
        make_option('--renew-token',
83
84
85
86
                    action='store_true',
                    dest='renew_token',
                    default=False,
                    help="Renew the user's token"),
87
        make_option('--renew-password',
88
89
90
91
                    action='store_true',
                    dest='renew_password',
                    default=False,
                    help="Renew the user's password"),
92
        make_option('--set-admin',
93
94
95
96
                    action='store_true',
                    dest='admin',
                    default=False,
                    help="Give user admin rights"),
97
        make_option('--set-noadmin',
98
99
100
101
                    action='store_true',
                    dest='noadmin',
                    default=False,
                    help="Revoke user's admin rights"),
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
102
        make_option('--set-active',
103
104
105
                    action='store_true',
                    dest='active',
                    default=False,
106
                    help="Change user's state to active"),
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
107
        make_option('--set-inactive',
108
109
110
111
                    action='store_true',
                    dest='inactive',
                    default=False,
                    help="Change user's state to inactive"),
112
113
114
        make_option('--inactive-reason',
                    dest='inactive_reason',
                    help="Reason user got inactive"),
115
        make_option('--add-group',
116
117
                    dest='add-group',
                    help="Add user group"),
118
        make_option('--delete-group',
119
120
                    dest='delete-group',
                    help="Delete user group"),
121
        make_option('--add-permission',
122
123
                    dest='add-permission',
                    help="Add user permission"),
124
        make_option('--delete-permission',
125
126
                    dest='delete-permission',
                    help="Delete user permission"),
127
128
129
130
        make_option('--accept',
                    dest='accept',
                    action='store_true',
                    help="Accept user"),
131
132
133
134
        make_option('--verify',
                    dest='verify',
                    action='store_true',
                    help="Verify user email"),
135
136
137
138
139
140
141
        make_option('--reject',
                    dest='reject',
                    action='store_true',
                    help="Reject user"),
        make_option('--reject-reason',
                    dest='reject_reason',
                    help="Reason user got rejected"),
142
143
144
145
        make_option('--sign-terms',
                    default=False,
                    action='store_true',
                    help="Sign terms"),
146
        make_option('--base-quota',
147
148
149
150
151
152
153
                    dest='set_base_quota',
                    metavar='<resource> <capacity>',
                    nargs=2,
                    help=("Set base quota for a specified resource. "
                          "The special value 'default' sets the user base "
                          "quota to the default value.")
                    ),
154
155
156
157
158
        make_option('-f', '--no-confirm',
                    action='store_true',
                    default=False,
                    dest='force',
                    help="Do not ask for confirmation"),
159
160
161
162
163
164
165
        make_option('--set-email',
                    dest='set-email',
                    help="Change user's email"),
        make_option('--delete',
                    dest='delete',
                    action='store_true',
                    help="Delete user"),
Olga Brani's avatar
Olga Brani committed
166
167
    )

168
    @transaction.commit_on_success
169
    def handle(self, *args, **options):
170
171
172
173
174
175
        if options['all']:
            if not args:
                return self.handle_all_users(*args, **options)
            else:
                raise CommandError("Please provide a user ID or --all")

176
        if len(args) != 1:
177
178
179
180
181
            raise CommandError("Please provide a user ID or --all")

        if options["exclude"] is not None:
            m = "Option --exclude is meaningful only combined with --all."
            raise CommandError(m)
182

183
        if args[0].isdigit():
184
            try:
185
186
                user = AstakosUser.objects.select_for_update().\
                    get(id=int(args[0]))
187
188
189
190
191
192
193
            except AstakosUser.DoesNotExist:
                raise CommandError("Invalid user ID")
        elif is_uuid(args[0]):
            try:
                user = AstakosUser.objects.get(uuid=args[0])
            except AstakosUser.DoesNotExist:
                raise CommandError("Invalid user UUID")
194
        else:
195
196
197
            raise CommandError(("Invalid user identification: "
                                "you should provide a valid user ID "
                                "or a valid user UUID"))
198

199
200
201
202
        if options.get('admin'):
            user.is_superuser = True
        elif options.get('noadmin'):
            user.is_superuser = False
203

204
205
206
207
208
209
210
211
212
213
214
215
        if options.get('reject'):
            reject_reason = options.get('reject_reason', None)
            res = activation_backend.handle_moderation(
                user,
                accept=False,
                reject_reason=reject_reason)
            activation_backend.send_result_notifications(res, user)
            if res.is_error():
                print "Failed to reject.", res.message
            else:
                print "Account rejected"

216
217
218
219
220
221
222
223
224
225
        if options.get('verify'):
            res = activation_backend.handle_verification(
                user,
                user.verification_code)
            #activation_backend.send_result_notifications(res, user)
            if res.is_error():
                print "Failed to verify.", res.message
            else:
                print "Account verified (%s)" % res.status_display()

226
227
228
229
230
231
232
233
        if options.get('accept'):
            res = activation_backend.handle_moderation(user, accept=True)
            activation_backend.send_result_notifications(res, user)
            if res.is_error():
                print "Failed to accept.", res.message
            else:
                print "Account accepted and activated"

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
234
        if options.get('active'):
235
236
237
238
239
240
            res = activation_backend.activate_user(user)
            if res.is_error():
                print "Failed to activate.", res.message
            else:
                print "Account %s activated" % user.username

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
241
        elif options.get('inactive'):
242
243
244
245
246
247
248
            res = activation_backend.deactivate_user(
                user,
                reason=options.get('inactive_reason', None))
            if res.is_error():
                print "Failed to deactivate,", res.message
            else:
                print "Account %s deactivated" % user.username
249

250
251
252
        invitations = options.get('invitations')
        if invitations is not None:
            user.invitations = int(invitations)
253

254
        groupname = options.get('add-group')
Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
255
256
        if groupname is not None:
            try:
Olga Brani's avatar
Olga Brani committed
257
258
259
                group = Group.objects.get(name=groupname)
                user.groups.add(group)
            except Group.DoesNotExist, e:
260
261
262
                self.stdout.write(
                    "Group named %s does not exist\n" % groupname)

263
264
265
        groupname = options.get('delete-group')
        if groupname is not None:
            try:
Olga Brani's avatar
Olga Brani committed
266
267
268
                group = Group.objects.get(name=groupname)
                user.groups.remove(group)
            except Group.DoesNotExist, e:
269
270
271
                self.stdout.write(
                    "Group named %s does not exist\n" % groupname)

272
273
274
275
276
        pname = options.get('add-permission')
        if pname is not None:
            try:
                r, created = add_user_permission(user, pname)
                if created:
277
278
                    self.stdout.write(
                        'Permission: %s created successfully\n' % pname)
279
                if r > 0:
280
281
282
283
284
                    self.stdout.write(
                        'Permission: %s added successfully\n' % pname)
                elif r == 0:
                    self.stdout.write(
                        'User has already permission: %s\n' % pname)
285
286
            except Exception, e:
                raise CommandError(e)
287
288

        pname = options.get('delete-permission')
289
290
291
292
        if pname is not None and not user.has_perm(pname):
            try:
                r = remove_user_permission(user, pname)
                if r < 0:
293
294
                    self.stdout.write(
                        'Invalid permission codename: %s\n' % pname)
295
296
297
                elif r == 0:
                    self.stdout.write('User has not permission: %s\n' % pname)
                elif r > 0:
298
299
                    self.stdout.write(
                        'Permission: %s removed successfully\n' % pname)
300
301
            except Exception, e:
                raise CommandError(e)
302

Sofia Papagiannaki's avatar
Sofia Papagiannaki committed
303
304
305
        level = options.get('level')
        if level is not None:
            user.level = int(level)
306

307
308
309
        password = options.get('password')
        if password is not None:
            user.set_password(password)
310

311
312
313
314
        password = None
        if options['renew_password']:
            password = AstakosUser.objects.make_random_password()
            user.set_password(password)
315

316
317
        if options['renew_token']:
            user.renew_token()
318

319
320
321
322
        if options['sign_terms']:
            user.has_signed_terms = True
            user.date_signed_terms = datetime.now()

323
324
325
        try:
            user.save()
        except ValidationError, e:
326
            raise CommandError(e)
327

328
329
        if password:
            self.stdout.write('User\'s new password: %s\n' % password)
330

331
332
        force = options['force']

333
334
        set_base_quota = options.get('set_base_quota')
        if set_base_quota is not None:
335
336
337
            if not user.is_accepted():
                m = "%s is not an accepted user." % user
                raise CommandError(m)
338
            resource, capacity = set_base_quota
339
            self.set_limits([user], resource, capacity, force)
340

341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
        delete = options.get('delete')
        if delete:
            management.call_command('user-show', str(user.pk),
                                    list_quotas=True)
            m = "Are you sure you want to permanently delete the user " \
                "(yes/no) ? "

            self.stdout.write("\n")
            confirm = raw_input(m)
            if confirm == "yes":
                user.delete()

        # Change users email address
        newemail = options.get('set-email', None)
        if newemail is not None:
            newemail = newemail.strip()
            try:
                validate_email(newemail)
            except ValidationError:
                m = "Invalid email address."
                raise CommandError(m)

            if AstakosUser.objects.user_exists(newemail):
                m = "A user with this email address already exists."
                raise CommandError(m)

367
            user.set_email(newemail)
368
369
            user.save()

370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
    def confirm(self):
        self.stdout.write("Confirm? [y/N] ")
        response = raw_input()
        if string.lower(response) not in ['y', 'yes']:
            self.stdout.write("Aborted.\n")
            exit()

    def handle_limits_user(self, user, res, capacity, style):
        default_capacity = res.uplimit
        resource = res.name
        quota = user.get_resource_policy(resource)
        s_default = show_resource_value(default_capacity, resource, style)
        s_current = show_resource_value(quota.capacity, resource, style)
        s_capacity = (show_resource_value(capacity, resource, style)
                      if capacity != 'default' else capacity)
        self.stdout.write("user: %s (%s)\n" % (user.uuid, user.username))
        self.stdout.write("default capacity: %s\n" % s_default)
        self.stdout.write("current capacity: %s\n" % s_current)
        self.stdout.write("new capacity: %s\n" % s_capacity)
        self.confirm()

    def handle_limits_all(self, res, capacity, exclude, style):
        m = "This will set base quota for all users"
        app = (" except %s" % ", ".join(exclude)) if exclude else ""
        self.stdout.write(m+app+".\n")
        resource = res.name
        self.stdout.write("resource: %s\n" % resource)
        s_capacity = (show_resource_value(capacity, resource, style)
                      if capacity != 'default' else capacity)
        self.stdout.write("capacity: %s\n" % s_capacity)
        self.confirm()

    def set_limits(self, users, resource, capacity, force=False, exclude=None):
        try:
            r = Resource.objects.get(name=resource)
        except Resource.DoesNotExist:
            raise CommandError("No such resource '%s'." % resource)

408
        style = None
409
        if capacity != "default":
410
            try:
411
412
                capacity, style = units.parse_with_style(capacity)
            except:
413
414
                m = ("Please specify capacity as a decimal integer or "
                     "'default'")
415
416
417
                raise CommandError(m)

        if not force:
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
            if len(users) == 1:
                self.handle_limits_user(users[0], r, capacity, style)
            else:
                self.handle_limits_all(r, capacity, exclude, style)

        if capacity == "default":
            capacity = r.uplimit
        quotas.update_base_quota(users, resource, capacity)

    def handle_all_users(self, *args, **options):
        force = options["force"]
        exclude = options["exclude"]
        if exclude is not None:
            exclude = exclude.split(',')

        set_base_quota = options.get('set_base_quota')
        if set_base_quota is not None:
            users = AstakosUser.objects.accepted().select_for_update()
            if exclude:
                users = users.exclude(uuid__in=exclude)
            resource, capacity = set_base_quota
            self.set_limits(users, resource, capacity, force, exclude)