usermanagement.py 12.8 KB
Newer Older
1
2
3
# -*- coding: utf-8 -*-

# imports
4
from passlib.hash import sha256_crypt
5
from vispa import AjaxException
6
from vispa.controller import AbstractController
7
from vispa.models.group import Group, Group_User_Assoc, Group_Group_Assoc
8
from vispa.models.project import Project
Fabian-Andree Heidemann's avatar
GUI:    
Fabian-Andree Heidemann committed
9
from vispa.models.role import Role, Permission
10
from vispa.models.user import User
11
from vispa.models.workgroup import Workgroup
12
import cherrypy
13

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from itertools import izip
from inspect import getargspec
from collections import deque

def DB():
    return cherrypy.request.db

def call_mix(func, *args, **kwargs):
    return func(*args, **{
        arg: kwargs[arg]
        for arg in getargspec(func)[0] if arg in kwargs
    })

class Rights(int):
    levels = [
        "none",
30
31
        "private",
        "protected",
32
33
34
35
        "public",
        "member",
        "manager",
        "admin",
36
        "forbidden"
37
38
    ]

39
40
41
42
43
44
45
    @staticmethod
    def has_user(user, listing):
        return any(
            user.id == (item.id if isinstance(item, User) else item.user_id)
            for item in listing
        )

46
47
48
49
50
    def test(self, obj):
        """
        Tests wether the object interaction is allowed with the given rights.
        """
        user = cherrypy.request.user
51
52
53
54
55
56
57
58
59
60
61
62
63

        if user.serveradmin:
            return True

        if self > self.manager:
            return False
        if obj == user:
            return True
        if user in getattr(obj, "get_managers", list)():
            return True

        if self > self.member:
            return False
64
        if getattr(obj, "is_member", lambda user: False)(user):
65
66
67
68
69
70
71
72
            return True

        return self <= {
            Group.PUBLIC: self.public,
            Group.PROTECTED: self.protected,
            Group.PRIVATE: self.private,
            None: self.none,
        }[getattr(obj, "privacy", None)]
73
74
75
76
77
78
79
80
81
82
83
84
85

    @staticmethod
    def extend(base, extension, delete=[]):
        ret = {}
        ret.update(base)
        ret.update(extension)
        for d in delete:
            del ret[d]
        return ret

for i, name in enumerate(Rights.levels):
    setattr(Rights, name, Rights(i))

86
87
88
89
90
91
92
class PermissionException(AjaxException):
    def __init__(self, obj):
        super(PermissionException, self).__init__(
            "Insufficient permissions to access %s \"%s\" with this operation"
            % (obj.__class__.__name__.lower(), obj.name), 403
        )

93
94
95
96
97
98
99
100
101
102
103
104
class BaseController(object):
    def __init__(self, table, rights={}, assocs=[]):
        self._table = table
        self._rights = rights
        for assoc in assocs:
            setattr(self, assoc._assoc, assoc)
            setattr(assoc, "_master", self)

    def _rga(self, rights, *names):
        # collect the object to work on
        tar = self
        tars = deque([tar])
105
        while tar and hasattr(tar, "_master"):
106
107
108
109
110
111
112
113
114
115
            tar = tar._master
            tars.appendleft(tar)
        rights = self._rights.get(rights, self._rights["__default__"])
        if type(rights) is not list:
            rights = [rights]
        good = [True] * len(rights)
        objs = deque()
        for tar, name, right in izip(tars, names, izip(*rights)):
            obj = tar._table.get_by_name(DB(), name)
            if obj is None:  # TODO: this is a info leak (about existence)
116
117
118
119
                raise AjaxException(
                    "There is no %s \"%s\""
                    % (tar._table.__name__.lower(), name), 404
                )
120
121
            good = [g and Rights(r).test(obj) for g, r in zip(good, right)]
            if True not in good:
122
                raise PermissionException(obj)
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
            objs.append(obj)
        return objs

    def _rt(self, rights):
        rights = self._rights.get(rights, self._rights["__default__"])
        if type(rights) is not list:
            rights = [rights]
        if not any(all(r.test(None) for r in right) for right in rights):
            raise AjaxException(403)

    def _rf(self, rights, obj_iter):
        rights = self._rights.get(rights, self._rights["__default__"])
        rights = Rights(rights[0] if type(rights) is tuple else rights)
        if type(rights) is not list:
            rights = [rights]
        return [obj for obj in obj_iter if any(r.test(obj) for r in rights)]

# base tables
class TableController(BaseController):
    """
    Basic controller for accessing items of a table.
    """
    def __init__(self, *args, **kwargs):
        self._setters = kwargs.pop("setters", [])
        super(TableController, self).__init__(*args, **kwargs)

    @cherrypy.expose
150
    def add(self, name):
151
152
153
154
155
        self._rt("create")
        self._table.create(DB(), name)

    @cherrypy.expose
    def list(self):
156
        isAdmin = cherrypy.request.user.serveradmin
157
158
159
        return [
            call_mix(entry.to_dict, user=cherrypy.request.user)
            for entry in self._rf("list", self._table.all(DB()))
160
            if isAdmin or getattr(entry, "status", 1) == 1
161
        ]
162
163
164
165

    @cherrypy.expose
    def info(self, name):
        base, = self._rga("info", name)
166
        return call_mix(base.to_dict, user=cherrypy.request.user)
167
168
169
170

    @cherrypy.expose
    def rename(self, name, new_name):
        base, = self._rga("rename", name)
171
        base.rename(new_name)
172
173

    @cherrypy.expose
174
    def remove(self, name):
175
        base, = self._rga("delete", name)
176
        base.delete()
177
178

    @cherrypy.expose
179
    def update(self, name, **kwargs):
180
181
        for key, value in kwargs.items():
            if key in self._setters:
182
                base, = self._rga("update_%s" % key, name)
183
184
185
186
187
                func = getattr(base, "set_%s" % key)
                func(value)
            else:
                raise AjaxException("Unknown property to updated: %s" % key, 404)

188
189
190
191
192
193
194
class AutojoinTableController(TableController):
    @cherrypy.expose
    def add(self, name):
        self._rt("create")
        entry = self._table.create(DB(), name)
        entry.add_manager(cherrypy.request.user)

195
196
197
198
199
class AssociationController(BaseController):
    def __init__(self, *args, **kwargs):
        self._assoc = kwargs.pop("assoc", [])
        super(AssociationController, self).__init__(*args, **kwargs)

200
201
202
203
204
    def _info(self, item):
        return {
            "name": item.name,
        }

205
206
207
208
    @cherrypy.expose
    def list(self, name):
        base, = self._rga("list", name)
        func = getattr(base, "get_%ss" % self._assoc)
209
        return [self._info(obj) for obj in call_mix(func, recursion_depth=0)]
210
211
212
213
214

    @cherrypy.expose
    def add(self, name, assoc_name):
        base, target = self._rga("add", name, assoc_name)
        func = getattr(base, "add_%s" % self._assoc)
215
        func(target)
216
217
218
219
220

    @cherrypy.expose
    def remove(self, name, assoc_name):
        base, target = self._rga("remove", name, assoc_name)
        func = getattr(base, "remove_%s" % self._assoc)
221
        func(target)
222
223

class AssociationToGroupController(AssociationController):
224
225
226
227
228
229
    def _info(self, item):
        return {
            "name": getattr(item, self._assoc).name,
            "confirmed": item.status == item.CONFIRMED,
        }

230
231
232
    @cherrypy.expose
    def add(self, name, assoc_name, password=""):
        try:
233
            base, target = self._rga("add", name, assoc_name)
234
235
            conf = True
        except PermissionException:
236
237
            base, target = self._rga("add_self", name, assoc_name)
            if base.privacy == Group.PUBLIC:
238
239
                conf = True
            elif base.password and password:
240
241
                if not sha256_crypt.verify(password, base.password):
                    raise AjaxException(403)
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
                conf = True
            elif base.privacy == Group.PROTECTED:
                conf = False
            else:
                raise PermissionException(base)

        func = getattr(base, "add_%s" % self._assoc)
        conft = Group_User_Assoc if self._table.__name__ == "User" else Group_Group_Assoc
        try:
            func(target, conft.CONFIRMED if conf else conft.UNCONFIRMED)
        except Exception as e:
            if not conf:
                raise
            if "already in group" not in e.message:  # TODO: this is fugly
                raise
            func = getattr(base, "confirm_%s" % self._assoc)
            func(target)
259
260
261
262
263

    @cherrypy.expose
    def confirm(self, name, assoc_name):
        base, target = self._rga("confirm", name, assoc_name)
        func = getattr(base, "confirm_%s" % self._assoc)
264
265
266
267
268
269
270
271
272
273
        func(target)

class AssociationToProjectController(AssociationController):
    def _info(self, item):
        return {
            "name": getattr(item, self._assoc).name,
            "roles": [
                role.name for role in item.roles
            ]
        }
274
275
276
277

class AssociationDeepController(AssociationController):
    @cherrypy.expose
    def list(self, name, mid_name):
278
        base, mid, = self._rga("list", name, mid_name)
279
        func = getattr(base, "get_%ss" % self._assoc)
280
        return [obj.name for obj in call_mix(func, mid, recursion_depth=0)]
281
282
283

    @cherrypy.expose
    def add(self, name, mid_name, assoc_name):
284
        base, mid, target = self._rga("add", name, mid_name, assoc_name)
285
        func = getattr(base, "add_%s" % self._assoc)
286
        func(mid, target)
287
288
289

    @cherrypy.expose
    def remove(self, name, mid_name, assoc_name):
290
        base, mid, target = self._rga("remove", name, mid_name, assoc_name)
291
        func = getattr(base, "remove_%s" % self._assoc)
292
        func(mid, target)
293

294
295
class UMAjaxController(AbstractController):

296
297
298
299
300
301
302
303
304
305
306
307
    def __init__(self):
        super(UMAjaxController, self).__init__()

        # some commonly used rights
        rights_admin = {"__default__": (Rights.admin,) * 3}
        rights_manager = {"__default__": (Rights.manager, Rights.none)}
        rights_assoc = {
            "list": (Rights.member,),
            "add": (Rights.manager, Rights.none),
            "remove": [
                (Rights.manager, Rights.none),  # managers can kick members
                (Rights.none, Rights.manager),  # anyone can leave on their own
308
309
            ],
            "__default__": (Rights.manager,),
310
311
312
313
        }

        # now define the controllers
        self.permission = TableController(table=Permission, rights=rights_admin)
314
315
316
        self.role = TableController(table=Role, rights=Rights.extend(rights_admin, {
            "list": (Rights.none,),
        }), assocs=[
317
318
            AssociationController(table=Permission, assoc="permission", rights=rights_admin),
        ])
319
        self.workgroup = AutojoinTableController(table=Workgroup, rights={
320
321
322
323
324
325
326
327
328
329
330
            "list": (Rights.member,),
            "info": (Rights.member,),
            "create": (Rights.none,),
            "delete": (Rights.manager,),
            "__default__": (Rights.manager,)
        }, assocs=[
            AssociationController(table=User, assoc="manager", rights=rights_manager),
            AssociationController(table=User, assoc="user", rights=rights_assoc)
        ])

        self.group = TableController(table=Group, rights={
331
332
            "list": (Rights.protected,),
            "info": (Rights.protected,),
333
334
335
336
337
338
            "create": (Rights.admin,),
            "delete": (Rights.admin,),
            "update_status": (Rights.admin,),
            "__default__": (Rights.manager,),
        }, assocs=[
            AssociationController(table=User, assoc="manager", rights=rights_manager),
339
            AssociationToGroupController(table=Group, assoc="parent_group", rights={
340
                "list": (Rights.public,),
341
342
                "__default__": (Rights.forbidden,),
            }),
343
344
345
        ] + [
            AssociationToGroupController(table=t, assoc=a, rights=Rights.extend(rights_assoc, {
                "list": (Rights.public,),
346
                "confirm": (Rights.manager, Rights.none),
347
348
349
350
                "add": (Rights.manager, Rights.none),
                "add_self": (Rights.none, Rights.manager),
            })) for t, a in [
                (User, "user"),
351
                (Group, "child_group"),
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
            ]
        ], setters=[
            "privacy",
            "status",
            "password",
        ])

        self.project = TableController(table=Project, rights={
            "list": (Rights.member,),
            "info": (Rights.member,),
            "create": (Rights.admin,),
            "delete": (Rights.admin,),
            "update_status": (Rights.admin,),
            "__default__": (Rights.manager,),
        }, assocs=[
            AssociationController(table=User, assoc="manager", rights=rights_manager),
        ] + [
369
            AssociationToProjectController(table=t, assoc=a, rights=rights_assoc, assocs=[
370
371
372
373
374
375
376
377
378
                AssociationDeepController(table=Role, assoc="%s_role" % a, rights=rights_admin)
            ]) for t, a in [
                (User, "user"),
                (Group, "group")
            ]
        ], setters=[
            "status"
        ])

379
380
381
382
383
384
385
    @cherrypy.expose
    def user_groups(self):
        return [grp.name for grp in cherrypy.request.user.get_groups()]

    @cherrypy.expose
    def user_projects(self):
        return [pro.name for pro in cherrypy.request.user.get_projects()]