~ubuntu-branches/ubuntu/natty/moin/natty-updates

« back to all changes in this revision

Viewing changes to MoinMoin/security/__init__.py

  • Committer: Bazaar Package Importer
  • Author(s): Jonas Smedegaard
  • Date: 2008-06-22 21:17:13 UTC
  • mfrom: (0.9.1 upstream)
  • Revision ID: james.westby@ubuntu.com-20080622211713-fpo2zrq3s5dfecxg
Tags: 1.7.0-3
Simplify /etc/moin/wikilist format: "USER URL" (drop unneeded middle
CONFIG_DIR that was wrongly advertised as DATA_DIR).  Make
moin-mass-migrate handle both formats and warn about deprecation of
the old one.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# -*- coding: iso-8859-1 -*-
 
2
"""
 
3
    MoinMoin - Wiki Security Interface and Access Control Lists
 
4
 
 
5
 
 
6
    This implements the basic interface for user permissions and
 
7
    system policy. If you want to define your own policy, inherit
 
8
    from the base class 'Permissions', so that when new permissions
 
9
    are defined, you get the defaults.
 
10
 
 
11
    Then assign your new class to "SecurityPolicy" in wikiconfig;
 
12
    and I mean the class, not an instance of it!
 
13
 
 
14
    @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
 
15
                2003-2008 MoinMoin:ThomasWaldmann,
 
16
                2003 Gustavo Niemeyer,
 
17
                2005 Oliver Graf,
 
18
                2007 Alexander Schremmer
 
19
    @license: GNU GPL, see COPYING for details.
 
20
"""
 
21
 
 
22
import re
 
23
 
 
24
from MoinMoin import wikiutil, user
 
25
from MoinMoin.Page import Page
 
26
 
 
27
#############################################################################
 
28
### Basic Permissions Interface -- most features enabled by default
 
29
#############################################################################
 
30
 
 
31
def _check(request, pagename, username, right):
 
32
    """ Check <right> access permission for user <username> on page <pagename>
 
33
 
 
34
    For cfg.acl_hierarchic=False we just check the page in question.
 
35
 
 
36
    For cfg.acl_hierarchic=True we, we check each page in the hierarchy. We
 
37
    start with the deepest page and recurse to the top of the tree.
 
38
    If one of those permits, True is returned.
 
39
 
 
40
    For both configurations, we check acl_rights_before before the page/default
 
41
    acl and acl_rights_after after the page/default acl, of course.
 
42
 
 
43
    This method should not be called by users, use __getattr__ instead.
 
44
 
 
45
    @param request: the current request object
 
46
    @param pagename: pagename to get permissions from
 
47
    @param username: the user name
 
48
    @param right: the right to check
 
49
 
 
50
    @rtype: bool
 
51
    @return: True if you have permission or False
 
52
    """
 
53
    cache = request.cfg.cache
 
54
    allowed = cache.acl_rights_before.may(request, username, right)
 
55
    if allowed is not None:
 
56
        return allowed
 
57
 
 
58
    if request.cfg.acl_hierarchic:
 
59
        pages = pagename.split('/') # create page hierarchy list
 
60
        some_acl = False
 
61
        for i in range(len(pages), 0, -1):
 
62
            # Create the next pagename in the hierarchy
 
63
            # starting at the leaf, going to the root
 
64
            name = '/'.join(pages[:i])
 
65
            # Get page acl and ask for permission
 
66
            acl = Page(request, name).getACL(request)
 
67
            if acl.acl:
 
68
                some_acl = True
 
69
                allowed = acl.may(request, username, right)
 
70
                if allowed is not None:
 
71
                    return allowed
 
72
        if not some_acl:
 
73
            allowed = cache.acl_rights_default.may(request, username, right)
 
74
            if allowed is not None:
 
75
                return allowed
 
76
    else:
 
77
        if request.page is not None and pagename == request.page.page_name:
 
78
            p = request.page # reuse is good
 
79
        else:
 
80
            p = Page(request, pagename)
 
81
        acl = p.getACL(request) # this will be fast in a reused page obj
 
82
        allowed = acl.may(request, username, right)
 
83
        if allowed is not None:
 
84
            return allowed
 
85
 
 
86
    allowed = cache.acl_rights_after.may(request, username, right)
 
87
    if allowed is not None:
 
88
        return allowed
 
89
 
 
90
    return False
 
91
 
 
92
 
 
93
class Permissions:
 
94
    """ Basic interface for user permissions and system policy.
 
95
 
 
96
    Note that you still need to allow some of the related actions, this
 
97
    just controls their behavior, not their activation.
 
98
 
 
99
    When sub classing this class, you must extend the class methods, not
 
100
    replace them, or you might break the acl in the wiki. Correct sub
 
101
    classing looks like this:
 
102
 
 
103
    def read(self, pagename):
 
104
        # Your special security rule
 
105
        if something:
 
106
            return false
 
107
 
 
108
        # Do not return True or you break acl!
 
109
        # This call will use the default acl rules
 
110
        return Permissions.read(pagename)
 
111
    """
 
112
 
 
113
    def __init__(self, user):
 
114
        self.name = user.name
 
115
        self.request = user._request
 
116
 
 
117
    def save(self, editor, newtext, rev, **kw):
 
118
        """ Check whether user may save a page.
 
119
 
 
120
        `editor` is the PageEditor instance, the other arguments are
 
121
        those of the `PageEditor.saveText` method.
 
122
 
 
123
        @param editor: PageEditor instance.
 
124
        @param newtext: new page text, you can enable of disable saving according
 
125
            to the content of the text, e.g. prevent link spam.
 
126
        @param rev: new revision number? XXX
 
127
        @param kw: XXX
 
128
        @rtype: bool
 
129
        @return: True if you can save or False
 
130
        """
 
131
        return self.write(editor.page_name)
 
132
 
 
133
    def __getattr__(self, attr):
 
134
        """ Shortcut to export getPermission function for all known ACL rights
 
135
 
 
136
        if attr is one of the rights in acl_rights_valid, then return a
 
137
        checking function for it. Else raise an AttributeError.
 
138
 
 
139
        @param attr: one of ACL rights as defined in acl_rights_valid
 
140
        @rtype: function
 
141
        @return: checking function for that right, accepting a pagename
 
142
        """
 
143
        request = self.request
 
144
        if attr not in request.cfg.acl_rights_valid:
 
145
            raise AttributeError, attr
 
146
        return lambda pagename: _check(self.request, pagename, self.name, attr)
 
147
 
 
148
 
 
149
# make an alias for the default policy
 
150
Default = Permissions
 
151
 
 
152
 
 
153
class AccessControlList:
 
154
    ''' Access Control List
 
155
 
 
156
    Control who may do what on or with a wiki page.
 
157
 
 
158
    Syntax of an ACL string:
 
159
 
 
160
        [+|-]User[,User,...]:[right[,right,...]] [[+|-]SomeGroup:...] ...
 
161
        ... [[+|-]Known:...] [[+|-]All:...]
 
162
 
 
163
        "User" is a user name and triggers only if the user matches. Up
 
164
        to version 1.2 only WikiNames were supported, as of version 1.3,
 
165
        any name can be used in acl lines, including name with spaces
 
166
        using esoteric languages.
 
167
 
 
168
        "SomeGroup" is a page name matching cfg.page_group_regex with
 
169
         some lines in the form " * Member", defining the group members.
 
170
 
 
171
        "Known" is a group containing all valid / known users.
 
172
 
 
173
        "All" is a group containing all users (Known and Anonymous users).
 
174
 
 
175
        "right" may be an arbitrary word like read, write, delete, admin.
 
176
        Only words in cfg.acl_validrights are accepted, others are
 
177
        ignored. It is allowed to specify no rights, which means that no
 
178
        rights are given.
 
179
 
 
180
    How ACL is processed
 
181
 
 
182
        When some user is trying to access some ACL-protected resource,
 
183
        the ACLs will be processed in the order they are found. The first
 
184
        matching ACL will tell if the user has access to that resource
 
185
        or not.
 
186
 
 
187
        For example, the following ACL tells that SomeUser is able to
 
188
        read and write the resources protected by that ACL, while any
 
189
        member of SomeGroup (besides SomeUser, if part of that group)
 
190
        may also admin that, and every other user is able to read it.
 
191
 
 
192
            SomeUser:read,write SomeGroup:read,write,admin All:read
 
193
 
 
194
        In this example, SomeUser can read and write but can not admin,
 
195
        revert or delete pages. Rights that are NOT specified on the
 
196
        right list are automatically set to NO.
 
197
 
 
198
    Using Prefixes
 
199
 
 
200
        To make the system more flexible, there are also two modifiers:
 
201
        the prefixes "+" and "-".
 
202
 
 
203
            +SomeUser:read -OtherUser:write
 
204
 
 
205
        The acl line above will grant SomeUser read right, and OtherUser
 
206
        write right, but will NOT block automatically all other rights
 
207
        for these users. For example, if SomeUser ask to write, the
 
208
        above acl line does not define if he can or can not write. He
 
209
        will be able to write if acl_rights_before or acl_rights_after
 
210
        allow this (see configuration options).
 
211
 
 
212
        Using prefixes, this acl line:
 
213
 
 
214
            SomeUser:read,write SomeGroup:read,write,admin All:read
 
215
 
 
216
        Can be written as:
 
217
 
 
218
            -SomeUser:admin SomeGroup:read,write,admin All:read
 
219
 
 
220
        Or even:
 
221
 
 
222
            +All:read -SomeUser:admin SomeGroup:read,write,admin
 
223
 
 
224
        Notice that you probably will not want to use the second and
 
225
        third examples in ACL entries of some page. They are very
 
226
        useful on the moin configuration entries though.
 
227
 
 
228
   Configuration options
 
229
 
 
230
       cfg.acl_rights_default
 
231
           It is is ONLY used when no other ACLs are given.
 
232
           Default: "Known:read,write,delete All:read,write",
 
233
 
 
234
       cfg.acl_rights_before
 
235
           When the page has ACL entries, this will be inserted BEFORE
 
236
           any page entries.
 
237
           Default: ""
 
238
 
 
239
       cfg.acl_rights_after
 
240
           When the page has ACL entries, this will be inserted AFTER
 
241
           any page entries.
 
242
           Default: ""
 
243
 
 
244
       cfg.acl_rights_valid
 
245
           These are the acceptable (known) rights (and the place to
 
246
           extend, if necessary).
 
247
           Default: ["read", "write", "delete", "admin"]
 
248
    '''
 
249
 
 
250
    special_users = ["All", "Known", "Trusted"] # order is important
 
251
 
 
252
    def __init__(self, cfg, lines=[]):
 
253
        """Initialize an ACL, starting from <nothing>.
 
254
        """
 
255
        if lines:
 
256
            self.acl = [] # [ ('User', {"read": 0, ...}), ... ]
 
257
            self.acl_lines = []
 
258
            for line in lines:
 
259
                self._addLine(cfg, line)
 
260
        else:
 
261
            self.acl = None
 
262
            self.acl_lines = None
 
263
 
 
264
    def _addLine(self, cfg, aclstring, remember=1):
 
265
        """ Add another ACL line
 
266
 
 
267
        This can be used in multiple subsequent calls to process longer lists.
 
268
 
 
269
        @param cfg: current config
 
270
        @param aclstring: acl string from page or cfg
 
271
        @param remember: should add the line to self.acl_lines
 
272
        """
 
273
 
 
274
        # Remember lines
 
275
        if remember:
 
276
            self.acl_lines.append(aclstring)
 
277
 
 
278
        # Iterate over entries and rights, parsed by acl string iterator
 
279
        acliter = ACLStringIterator(cfg.acl_rights_valid, aclstring)
 
280
        for modifier, entries, rights in acliter:
 
281
            if entries == ['Default']:
 
282
                self._addLine(cfg, cfg.acl_rights_default, remember=0)
 
283
            else:
 
284
                for entry in entries:
 
285
                    rightsdict = {}
 
286
                    if modifier:
 
287
                        # Only user rights are added to the right dict.
 
288
                        # + add rights with value of 1
 
289
                        # - add right with value of 0
 
290
                        for right in rights:
 
291
                            rightsdict[right] = (modifier == '+')
 
292
                    else:
 
293
                        # All rights from acl_rights_valid are added to the
 
294
                        # dict, user rights with value of 1, and other with
 
295
                        # value of 0
 
296
                        for right in cfg.acl_rights_valid:
 
297
                            rightsdict[right] = (right in rights)
 
298
                    self.acl.append((entry, rightsdict))
 
299
 
 
300
    def may(self, request, name, dowhat):
 
301
        """ May <name> <dowhat>? Returns boolean answer.
 
302
 
 
303
            Note: this check does NOT include the acl_rights_before / _after ACL,
 
304
                  but it WILL use acl_rights_default if there is no (page) ACL.
 
305
        """
 
306
        if self.acl is None: # no #acl used on Page
 
307
            acl = request.cfg.cache.acl_rights_default.acl
 
308
        else: # we have a #acl on the page (self.acl can be [] if #acl is empty!)
 
309
            acl = self.acl
 
310
        is_group_member = request.dicts.has_member
 
311
        group_re = request.cfg.cache.page_group_regexact
 
312
        allowed = None
 
313
        for entry, rightsdict in acl:
 
314
            if entry in self.special_users:
 
315
                handler = getattr(self, "_special_"+entry, None)
 
316
                allowed = handler(request, name, dowhat, rightsdict)
 
317
            elif group_re.search(entry):
 
318
                if is_group_member(entry, name):
 
319
                    allowed = rightsdict.get(dowhat)
 
320
                else:
 
321
                    for special in self.special_users:
 
322
                        if is_group_member(entry, special):
 
323
                            handler = getattr(self, "_special_" + special, None)
 
324
                            allowed = handler(request, name, dowhat, rightsdict)
 
325
                            break # order of self.special_users is important
 
326
            elif entry == name:
 
327
                allowed = rightsdict.get(dowhat)
 
328
            if allowed is not None:
 
329
                return allowed
 
330
        return allowed # should be None
 
331
 
 
332
    def getString(self, b='#acl ', e='\n'):
 
333
        """print the acl strings we were fed with"""
 
334
        if self.acl_lines:
 
335
            acl_lines = ''.join(["%s%s%s" % (b, l, e) for l in self.acl_lines])
 
336
        else:
 
337
            acl_lines = ''
 
338
        return acl_lines
 
339
 
 
340
    def _special_All(self, request, name, dowhat, rightsdict):
 
341
        return rightsdict.get(dowhat)
 
342
 
 
343
    def _special_Known(self, request, name, dowhat, rightsdict):
 
344
        """ check if user <name> is known to us,
 
345
            that means that there is a valid user account present.
 
346
            works for subscription emails.
 
347
        """
 
348
        if user.getUserId(request, name): # is a user with this name known?
 
349
            return rightsdict.get(dowhat)
 
350
        return None
 
351
 
 
352
    def _special_Trusted(self, request, name, dowhat, rightsdict):
 
353
        """ check if user <name> is known AND has logged in using a trusted
 
354
            authentication method.
 
355
            Does not work for subsription emails that should be sent to <user>,
 
356
            as he is not logged in in that case.
 
357
        """
 
358
        if (request.user.name == name and
 
359
            request.user.auth_method in request.cfg.auth_methods_trusted):
 
360
            return rightsdict.get(dowhat)
 
361
        return None
 
362
 
 
363
    def __eq__(self, other):
 
364
        return self.acl_lines == other.acl_lines
 
365
 
 
366
    def __ne__(self, other):
 
367
        return self.acl_lines != other.acl_lines
 
368
 
 
369
 
 
370
class ACLStringIterator:
 
371
    """ Iterator for acl string
 
372
 
 
373
    Parse acl string and return the next entry on each call to
 
374
    next. Implement the Iterator protocol.
 
375
 
 
376
    Usage:
 
377
        iter = ACLStringIterator(cfg.acl_rights_valid, 'user name:right')
 
378
        for modifier, entries, rights in iter:
 
379
            # process data
 
380
    """
 
381
 
 
382
    def __init__(self, rights, aclstring):
 
383
        """ Initialize acl iterator
 
384
 
 
385
        @param rights: the acl rights to consider when parsing
 
386
        @param aclstring: string to parse
 
387
        """
 
388
        self.rights = rights
 
389
        self.rest = aclstring.strip()
 
390
        self.finished = 0
 
391
 
 
392
    def __iter__(self):
 
393
        """ Required by the Iterator protocol """
 
394
        return self
 
395
 
 
396
    def next(self):
 
397
        """ Return the next values from the acl string
 
398
 
 
399
        When the iterator is finished and you try to call next, it
 
400
        raises a StopIteration. The iterator finish as soon as the
 
401
        string is fully parsed or can not be parsed any more.
 
402
 
 
403
        @rtype: 3 tuple - (modifier, [entry, ...], [right, ...])
 
404
        @return: values for one item in an acl string
 
405
        """
 
406
        # Handle finished state, required by iterator protocol
 
407
        if self.rest == '':
 
408
            self.finished = 1
 
409
        if self.finished:
 
410
            raise StopIteration
 
411
 
 
412
        # Get optional modifier [+|-]entries:rights
 
413
        modifier = ''
 
414
        if self.rest[0] in ('+', '-'):
 
415
            modifier, self.rest = self.rest[0], self.rest[1:]
 
416
 
 
417
        # Handle the Default meta acl
 
418
        if self.rest.startswith('Default ') or self.rest == 'Default':
 
419
            self.rest = self.rest[8:]
 
420
            entries, rights = ['Default'], []
 
421
 
 
422
        # Handle entries:rights pairs
 
423
        else:
 
424
            # Get entries
 
425
            try:
 
426
                entries, self.rest = self.rest.split(':', 1)
 
427
            except ValueError:
 
428
                self.finished = 1
 
429
                raise StopIteration("Can't parse rest of string")
 
430
            if entries == '':
 
431
                entries = []
 
432
            else:
 
433
                # TODO strip each entry from blanks?
 
434
                entries = entries.split(',')
 
435
 
 
436
            # Get rights
 
437
            try:
 
438
                rights, self.rest = self.rest.split(' ', 1)
 
439
                # Remove extra white space after rights fragment,
 
440
                # allowing using multiple spaces between items.
 
441
                self.rest = self.rest.lstrip()
 
442
            except ValueError:
 
443
                rights, self.rest = self.rest, ''
 
444
            rights = [r for r in rights.split(',') if r in self.rights]
 
445
 
 
446
        return modifier, entries, rights
 
447
 
 
448
 
 
449
def parseACL(request, text):
 
450
    """ Parse acl lines from text and return ACL object """
 
451
    pi, dummy = wikiutil.get_processing_instructions(text)
 
452
    acl_lines = [args for verb, args in pi if verb == 'acl']
 
453
    return AccessControlList(request.cfg, acl_lines)
 
454