# HG changeset patch # User Ludovic Chabant # Date 1515352264 28800 # Node ID 6cd51ea6dfcfffc370856409ac45ee5fc3c40e06 # Parent ab47d3cf5e1e1916d511dd10b2dc4a6984107342 auth: Rewrite permission system and improve support for it. - More proper ACL model for permissions. - Page-level ACL is only specified locally, not inherited anymore. - Protect more API and UI routes with permission checks. - Improve error handling and error pages. diff -r ab47d3cf5e1e -r 6cd51ea6dfcf tests/test_auth.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/test_auth.py Sun Jan 07 11:11:04 2018 -0800 @@ -0,0 +1,98 @@ +import pytest +from configparser import SafeConfigParser +from wikked.auth import ( + UserManager, PERM_NAMES, + NoSuchGroupOrUserError, MultipleGroupMembershipError, + CyclicUserGroupError, InvalidPermissionError) + + +def _user_manager_from_str(txt): + config = SafeConfigParser() + config.read_string(txt) + return UserManager(config) + + +def _p(name): + return PERM_NAMES[name] + + +def test_empty_auth(): + m = _user_manager_from_str("") + assert list(m.getUserNames()) == ['anonymous'] + assert list(m.getGroupNames()) == ['*'] + + +def test_missing_user1(): + with pytest.raises(NoSuchGroupOrUserError): + m = _user_manager_from_str(""" +[permissions] +dorothy = read +""") + + +def test_missing_user2(): + with pytest.raises(NoSuchGroupOrUserError): + m = _user_manager_from_str(""" +[groups] +mygroup = dorothy +""") + + +def test_multiple_group_membership1(): + with pytest.raises(MultipleGroupMembershipError): + m = _user_manager_from_str(""" +[users] +dorothy = pass +[groups] +one = dorothy +two = dorothy +""") + + +def test_multiple_group_membership2(): + with pytest.raises(MultipleGroupMembershipError): + m = _user_manager_from_str(""" +[users] +dorothy = pass +[groups] +one = dorothy +two = one +three = one +""") + + +def test_auth1(): + m = _user_manager_from_str(""" +[users] +dorothy = pass +[permissions] +dorothy = read,edit +""") + assert m.hasPermission('dorothy', _p('read')) + assert m.hasPermission('dorothy', _p('edit')) + assert not m.hasPermission('dorothy', _p('create')) + + +def test_auth2(): + m = _user_manager_from_str(""" +[users] +dorothy = pass +toto = pass +tinman = pass +[groups] +humans = dorothy +others = toto, tinman +[permissions] +humans = read,edit +others = read +tinman = create +""") + assert m.hasPermission('dorothy', _p('read')) + assert m.hasPermission('dorothy', _p('edit')) + assert not m.hasPermission('dorothy', _p('create')) + assert m.hasPermission('toto', _p('read')) + assert not m.hasPermission('toto', _p('edit')) + assert not m.hasPermission('toto', _p('create')) + assert m.hasPermission('tinman', _p('read')) + assert not m.hasPermission('tinman', _p('edit')) + assert m.hasPermission('tinman', _p('create')) diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/api/admin.py --- a/wikked/api/admin.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/api/admin.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,69 +1,12 @@ -from flask import jsonify, abort, request -from flask.ext.login import logout_user, current_user -from wikked.web import app, get_wiki, login_manager -from wikked.webimpl.admin import do_login_user +from flask import jsonify +from wikked.web import app, get_wiki +from wikked.webimpl.decorators import requires_permission @app.route('/api/admin/reindex', methods=['POST']) +@requires_permission('index') def api_admin_reindex(): - if not current_user.is_authenticated() or not current_user.is_admin(): - return login_manager.unauthorized() wiki = get_wiki() wiki.index.reset(wiki.getPages()) result = {'ok': 1} return jsonify(result) - - -@app.route('/api/user/login', methods=['POST']) -def api_user_login(): - if do_login_user(): - username = request.form.get('username') - result = {'username': username, 'logged_in': 1} - return jsonify(result) - abort(401) - - -@app.route('/api/user/is_logged_in') -def api_user_is_logged_in(): - if current_user.is_authenticated(): - result = {'logged_in': True} - return jsonify(result) - abort(401) - - -@app.route('/api/user/logout', methods=['POST']) -def api_user_logout(): - logout_user() - result = {'ok': 1} - return jsonify(result) - - -@app.route('/api/user/info') -def api_current_user_info(): - user = current_user - if user.is_authenticated(): - result = { - 'user': { - 'username': user.username, - 'groups': user.groups - } - } - return jsonify(result) - return jsonify({'user': False}) - - -@app.route('/api/user/info/') -def api_user_info(name): - wiki = get_wiki() - user = wiki.auth.getUser(name) - if user is not None: - result = { - 'user': { - 'username': user.username, - 'groups': user.groups - } - } - return jsonify(result) - abort(404) - - diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/api/edit.py --- a/wikked/api/edit.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/api/edit.py Sun Jan 07 11:11:04 2018 -0800 @@ -2,11 +2,13 @@ from flask.ext.login import current_user from wikked.web import app, get_wiki from wikked.webimpl import url_from_viewarg, split_url_from_viewarg +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.edit import ( get_edit_page, do_edit_page, preview_edited_page) @app.route('/api/edit/', methods=['GET', 'POST']) +@requires_permission('edit') def api_edit_page(url): wiki = get_wiki() user = current_user.get_id() @@ -45,6 +47,7 @@ @app.route('/api/preview', methods=['POST']) +@requires_permission('edit') def api_preview(): url = request.form.get('url') if url == '' or not url[0] == '/': @@ -58,16 +61,19 @@ @app.route('/api/rename/', methods=['POST']) +@requires_permission(['create', 'delete']) def api_rename_page(url): - pass + raise NotImplementedError() @app.route('/api/delete/', methods=['POST']) +@requires_permission('delete') def api_delete_page(url): - pass + raise NotImplementedError() @app.route('/api/validate/newpage', methods=['GET', 'POST']) +@requires_permission('create') def api_validate_newpage(): path = request.form.get('title') if path is None: @@ -84,4 +90,3 @@ except Exception: return '"This page name is invalid or unavailable"' return '"true"' - diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/api/history.py --- a/wikked/api/history.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/api/history.py Sun Jan 07 11:11:04 2018 -0800 @@ -2,12 +2,14 @@ from flask.ext.login import current_user from wikked.web import app, get_wiki from wikked.webimpl import url_from_viewarg +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.history import ( get_site_history, get_page_history, read_page_rev, diff_page_revs) @app.route('/api/site-history') +@requires_permission('wikihistory') def api_site_history(): wiki = get_wiki() user = current_user.get_id() @@ -23,6 +25,7 @@ @app.route('/api/history/') +@requires_permission('history') def api_page_history(url): wiki = get_wiki() user = current_user.get_id() @@ -32,6 +35,7 @@ @app.route('/api/revision/') +@requires_permission('history') def api_read_page_rev(url): wiki = get_wiki() user = current_user.get_id() @@ -44,6 +48,7 @@ @app.route('/api/diff/') +@requires_permission('history') def api_diff_page(url): wiki = get_wiki() user = current_user.get_id() @@ -59,6 +64,7 @@ @app.route('/api/revert/', methods=['POST']) +@requires_permission('revert') def api_revert_page(url): # TODO: only users with write access can revert. if 'rev' not in request.form: @@ -83,4 +89,3 @@ wiki.revertPage(url, page_fields) result = {'reverted': 1} return jsonify(result) - diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/api/read.py --- a/wikked/api/read.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/api/read.py Sun Jan 07 11:11:04 2018 -0800 @@ -4,22 +4,24 @@ from wikked.utils import PageNotFoundError from wikked.web import app, get_wiki from wikked.webimpl import ( - CHECK_FOR_READ, - url_from_viewarg, - get_page_or_raise, - get_page_meta, - PageNotFoundError, RedirectNotFoundError, CircularRedirectError) + url_from_viewarg, + get_page_or_raise, + get_page_meta, + RedirectNotFoundError, CircularRedirectError) +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.read import ( - read_page, get_incoming_links, get_outgoing_links) + read_page, get_incoming_links, get_outgoing_links) from wikked.webimpl.special import list_pages @app.route('/api/list') +@requires_permission('list') def api_list_all_pages(): return api_list_pages(None) @app.route('/api/list/') +@requires_permission('list') def api_list_pages(url): wiki = get_wiki() url = url_from_viewarg(url) @@ -61,9 +63,9 @@ url = url_from_viewarg(url) try: page = get_page_or_raise( - wiki, url, - check_perms=(user, CHECK_FOR_READ), - fields=['raw_text', 'meta']) + wiki, url, + check_perms=(user, 'read'), + fields=['raw_text', 'meta']) except PageNotFoundError as e: app.logger.exception(e) abort(404) @@ -73,14 +75,16 @@ @app.route('/api/query') +@requires_permission('search') def api_query(): wiki = get_wiki() query = dict(request.args) pages = wiki.getPages(meta_query=query) result = { - 'query': query, - 'pages': [get_page_meta(p) for p in pages] - } + 'query': query, + # TODO: filter pages we don't have permission to. + 'pages': [get_page_meta(p) for p in pages] + } return jsonify(result) @@ -96,7 +100,7 @@ user = current_user.get_id() page = get_page_or_raise( wiki, url, - check_perms=(user, CHECK_FOR_READ), + check_perms=(user, 'read'), fields=['url', 'title', 'path', 'meta']) state = page.getState() return jsonify({ diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/api/special.py --- a/wikked/api/special.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/api/special.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,6 +1,7 @@ from flask import jsonify, request, abort from flask.ext.login import current_user from wikked.web import app, get_wiki +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.special import ( get_orphans, get_broken_redirects, get_double_redirects, get_dead_ends, get_search_results, get_search_preview_results) @@ -34,6 +35,7 @@ @app.route('/api/search') +@requires_permission('search') def api_search(): query = request.args.get('q') if query is None or query == '': @@ -42,10 +44,9 @@ @app.route('/api/searchpreview') +@requires_permission('search') def api_search_preview(): query = request.args.get('q') if query is None or query == '': abort(400) return call_api(get_search_preview_results, query=query) - - diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/api/user.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/api/user.py Sun Jan 07 11:11:04 2018 -0800 @@ -0,0 +1,57 @@ +from flask import jsonify, abort, request +from flask.ext.login import logout_user, current_user +from wikked.web import app, get_wiki +from wikked.webimpl.admin import do_login_user + + +@app.route('/api/user/login', methods=['POST']) +def api_user_login(): + if do_login_user(): + username = request.form.get('username') + result = {'username': username, 'logged_in': 1} + return jsonify(result) + abort(401) + + +@app.route('/api/user/is_logged_in') +def api_user_is_logged_in(): + if current_user.is_authenticated(): + result = {'logged_in': True} + return jsonify(result) + abort(401) + + +@app.route('/api/user/logout', methods=['POST']) +def api_user_logout(): + logout_user() + result = {'ok': 1} + return jsonify(result) + + +@app.route('/api/user/info') +def api_current_user_info(): + user = current_user + if user.is_authenticated(): + result = { + 'user': { + 'username': user.username, + 'groups': user.groups + } + } + return jsonify(result) + return jsonify({'user': False}) + + +@app.route('/api/user/info/') +def api_user_info(name): + wiki = get_wiki() + user = wiki.auth.getUser(name) + if user is not None: + result = { + 'user': { + 'username': user.username, + 'groups': user.groups + } + } + return jsonify(result) + abort(404) diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/auth.py --- a/wikked/auth.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/auth.py Sun Jan 07 11:11:04 2018 -0800 @@ -5,12 +5,62 @@ logger = logging.getLogger(__name__) -class User(object): +# Page permissions. +PERM_NONE = 0 +PERM_READ = 2**0 +PERM_EDIT = 2**1 +PERM_CREATE = 2**2 +PERM_DELETE = 2**3 +PERM_HISTORY = 2**4 +PERM_REVERT = 2**5 +PERM_SEARCH = 2**6 +# Site-wide premissions. +PERM_INDEX = 2**7 +PERM_LIST = 2**8 +PERM_LISTREFRESH = 2**9 +PERM_WIKIHISTORY = 2**10 +PERM_USERS = 2**11 + +PERM_NAMES = { + # Page permissions. + 'none': PERM_NONE, + 'read': PERM_READ, + 'edit': PERM_EDIT, + 'create': PERM_CREATE, + 'delete': PERM_DELETE, + 'history': PERM_HISTORY, + 'revert': PERM_REVERT, + 'search': PERM_SEARCH, + # Site-wide permissions. + 'index': PERM_INDEX, + 'list': PERM_LIST, + 'listrefresh': PERM_LISTREFRESH, + 'wikihistory': PERM_WIKIHISTORY, + 'users': PERM_USERS, + # Aliases + 'write': PERM_EDIT, + 'all': 0xffff + } + +ANONYMOUS_USERNAME = 'anonymous' +ALL_USERS_GROUP = '*' + +DEFAULT_USER_ROLES = { + 'reader': (PERM_READ | PERM_HISTORY), + 'contributor': (PERM_READ | PERM_EDIT | PERM_HISTORY), + 'editor': (PERM_READ | PERM_EDIT | PERM_CREATE | PERM_DELETE | + PERM_HISTORY | PERM_REVERT), + 'admin': 0xffff + } + + +class User: """ A user with an account on the wiki. """ def __init__(self, username, password): self.username = username self.password = password + self.permissions = PERM_NONE self.groups = [] def is_authenticated(self): @@ -25,16 +75,225 @@ def get_id(self): return str(self.username) - def is_admin(self): - return 'administrators' in self.groups + +class NoSuchGroupOrUserError(Exception): + pass + + +class MultipleGroupMembershipError(Exception): + pass + + +class CyclicUserGroupError(Exception): + pass + + +class InvalidPermissionError(Exception): + pass + + +class _UserInfo: + def __init__(self, password): + self.password = password + self.allows = PERM_NONE + self.denies = PERM_NONE + self.group_name = None + self.flattened_perms = PERM_NONE + self.flattened_lineage = None + + +class _UserGroupInfo: + def __init__(self): + self.allows = PERM_NONE + self.denies = PERM_NONE + self.parent_group_name = None + + +class _UserRoleInfo: + def __init__(self): + self.allows = PERM_NONE + self.denies = PERM_NONE + + +re_sep = re.compile(r'[,;]') + + +def _parse_permission(perm): + # 'perm' or '+perm' means 'allows'. '-perm' means 'denies'. + is_allow = True + if perm[0] == '-': + perm = perm[1:] + is_allow = False + + p_bit = PERM_NAMES.get(perm) + if p_bit is not None: + return p_bit, is_allow + raise InvalidPermissionError(perm) + + +def _parse_permission_list(permlist): + perms = [p.strip() for p in re_sep.split(permlist)] + allows = PERM_NONE + denies = PERM_NONE + for p in perms: + p_bit, is_allow = _parse_permission(p) + if is_allow: + allows |= p_bit + else: + denies |= p_bit + return allows, denies -class UserManager(object): +def parse_config(config, roles, groups, users): + member_map = {} + + # Pre-populate the default roles, and then read the user-defined + # list of roles. + for name, bits in DEFAULT_USER_ROLES.items(): + ri = _UserRoleInfo() + ri.allows = bits + roles[name] = ri + if config.has_section('roles'): + for role in config.items('roles'): + ri = _UserRoleInfo() + ri.allows, ri.denies = _parse_permission_list(role[1]) + roles[role[0]] = ri + + # Get the list of groups. + if config.has_section('groups'): + for group in config.items('groups'): + # Just create the group for now, and store members in a temp + # map, because some members might only be declared later. + groups[group[0]] = _UserGroupInfo() + member_map[group[0]] = [m.strip() for m in re_sep.split(group[1])] + + # Get the list of users and passwords. + if config.has_section('users'): + for user in config.items('users'): + users[user[0]] = _UserInfo(user[1]) + + # Now resolve group membership -- we should have all the users + # and groups known at this point. + for name, members in member_map.items(): + for m in members: + # Is it a user? + u = users.get(m) + if u is not None: + if u.group_name is not None: + raise MultipleGroupMembershipError( + "User '%s' can't be added to group '%s' " + "because it already belongs to group '%s'." % + (m, name, u.group_name)) + u.group_name = name + continue + + # Is it a group then? + g = groups.get(m) + if g is not None: + if g.parent_group_name is not None: + raise MultipleGroupMembershipError( + "Group '%s' can't be added to group '%s' " + "because it already belongs to group '%s'." % + (m, name, g.parent_group_name)) + g.parent_group_name = name + continue + + # Can't find it! + raise NoSuchGroupOrUserError(m) + + # Add entries for "all known users" and "anonymous users". + # Those are potentially referenced in the 'permissions' section to + # assign broad access levels. + users[ANONYMOUS_USERNAME] = _UserInfo(None) + groups[ALL_USERS_GROUP] = _UserGroupInfo() + + # Assign permissions. + if config.has_section('permissions'): + for perm in config.items('permissions'): + # Get the user or group subject. + subj = users.get(perm[0]) + if subj is None: + subj = groups.get(perm[0]) + if subj is None: + raise NoSuchGroupOrUserError(perm[0]) + + # Get the permission/role list. + allows = PERM_NONE + denies = PERM_NONE + perms = [p.strip() for p in re_sep.split(perm[1])] + for p in perms: + # If it's a role, just combine its allow/deny lists. + role = roles.get(p) + if role is not None: + allows |= role.allows + denies |= role.denies + continue + + # Otherwise, parse actual permissions as usual. + p_bit, is_allow = _parse_permission(p) + if is_allow: + allows |= p_bit + else: + denies |= p_bit + subj.allows |= allows + subj.denies |= denies + else: + # No permissions specified... use the defaults. + users[ANONYMOUS_USERNAME].allows = DEFAULT_USER_ROLES['admin'] + groups[ALL_USERS_GROUP].allows = DEFAULT_USER_ROLES['admin'] + + # Flatten user permissions so we don't have to go through the tree + # all the time, and so we can detect cyclic problems right away. + for username, user_info in users.items(): + group_lineage = _get_group_lineage(user_info, groups) + if username != ANONYMOUS_USERNAME: + group_lineage.append(ALL_USERS_GROUP) + # Walk the lineage the other way, i.e. from the root group down + # to the user itself. + user_info.flattened_lineage = list(reversed(group_lineage)) + for gn in user_info.flattened_lineage: + ginfo = groups[gn] + user_info.flattened_perms |= ginfo.allows + user_info.flattened_perms &= ~ginfo.denies + user_info.flattened_perms |= user_info.allows + user_info.flattened_perms &= ~user_info.denies + + +def _get_group_lineage(user_info, groups): + lineage = [] + if user_info.group_name is not None: + _do_get_group_lineage(groups, user_info.group_name, lineage) + return lineage + + +def _do_get_group_lineage(groups, group_name, lineage): + # Check cycles. + if group_name in lineage: + raise CyclicUserGroupError("Group '%s' is in a parenting cycle: %s" % + (group_name, ' -> '.join(lineage))) + # Check existence. + group_info = groups.get(group_name) + if group_info is None: + raise NoSuchGroupOrUserError(group_name) + + # Yep, it's all good. Add the group to the lineage, and keep walking + # up the parent chain. + lineage.append(group_name) + if group_info.parent_group_name: + _do_get_group_lineage(groups, group_info.parent_group_name, lineage) + + +re_page_acl = re.compile(r'^(?P[^\s]+)\s*\=\s*(?P.*)$') + + +class UserManager: """ A class that keeps track of users and their permissions. """ def __init__(self, config): - self._updatePermissions(config) - self._updateUserInfos(config) + self._roles = {} + self._groups = {} + self._users = {} + parse_config(config, self._roles, self._groups, self._users) def start(self, wiki): pass @@ -46,89 +305,78 @@ pass def getUsers(self): - for user in self._users: - yield self._createUser(user) + for name, info in self._users.items(): + yield self._createUser(name, info) + + def getUserNames(self): + return self._users.keys() def getUser(self, username): - for user in self._users: - if user['username'] == username: - return self._createUser(user) + info = self._users.get(username) + if info is not None: + return self._createUser(username, info) return None - def isPageReadable(self, page, username): - return self._isAllowedForMeta(page, 'readers', username) - - def isPageWritable(self, page, username): - return self._isAllowedForMeta(page, 'writers', username) - - def hasPermission(self, meta_name, username): - perm = self._permissions.get(meta_name) - if perm is not None: - # Permissions are declared at the wiki level. - if username is None and 'anonymous' in perm: - return True - if username is not None and ( - '*' in perm or username in perm): - return True - return False + def getGroupNames(self): + return self._groups.keys() - return True - - def _isAllowedForMeta(self, page, meta_name, username): - perm = page.getMeta(meta_name) - if perm is not None: - # Permissions are declared at the page level. - if isinstance(perm, list): - perm = ','.join(perm) + def hasPagePermission(self, page, username, perm): + extra_acl = None + page_perms = page.getLocalMeta('acl') + if page_perms is not None: + extra_acl = [] + for pp in page_perms: + m = re_page_acl.match(pp) + if m: + name = m.group('name') + perms = [p.strip() + for p in re_sep.split(m.group('perms'))] + extra_acl.append((name, perms)) - allowed = [r.strip() for r in re.split(r'[ ,;]', perm)] - if username is None and 'anonymous' in allowed: - return True - if username is not None and ( - '*' in allowed or username in allowed): - return True + return self.hasPermission(username, perm, extra_acl) - return False - - return self.hasPermission(meta_name, username) + def hasPermission(self, username, perm, extra_acl=None): + username = username or ANONYMOUS_USERNAME + user_info = self._users.get(username) + if user_info is None: + raise NoSuchGroupOrUserError(username) - def _updatePermissions(self, config): - self._permissions = { - 'readers': None, - 'writers': None - } - if config.has_option('permissions', 'readers'): - self._permissions['readers'] = [ - p.strip() - for p in re.split(r'[ ,;]', - config.get('permissions', 'readers'))] - if config.has_option('permissions', 'writers'): - self._permissions['writers'] = [ - p.strip() - for p in re.split( - r'[ ,;]', config.get('permissions', 'writers'))] + # Start with the user permissions, and patch them with whatever + # extra permissions specify. + effective_perms = user_info.flattened_perms + if extra_acl is not None: + for name, perms in extra_acl: + if (name == username or + name in user_info.flattened_lineage): + for p in perms: + if p[0] == '+': + # Add permission. + p_bit = self._getPermissions(p[1:]) + effective_perms |= p_bit + elif p[0] == '-': + # Remove permission. + p_bit = self._getPermissions(p[1:]) + effective_perms &= ~p_bit + else: + # Replace permissions. + p_bit = self._getPermissions(p) + effective_perms = p_bit - def _updateUserInfos(self, config): - self._users = [] - if config.has_section('users'): - groups = [] - if config.has_section('groups'): - groups = config.items('groups') + # Test the effective permissions now! + return (effective_perms & perm) != 0 - for user in config.items('users'): - user_info = { - 'username': user[0], - 'password': user[1], - 'groups': []} - for group in groups: - users_in_group = [ - u.strip() - for u in re.split(r'[ ,;]', group[1])] - if user[0] in users_in_group: - user_info['groups'].append(group[0]) - self._users.append(user_info) + def _getPermissions(self, perm_or_role): + role_info = self._roles.get(perm_or_role) + if role_info is not None: + return (role_info.allows & ~role_info.denies) + p_bit = PERM_NAMES.get(perm_or_role) + if p_bit is not None: + return p_bit + raise InvalidPermissionError( + "'%s' is not a valid permission or role." % perm_or_role) - def _createUser(self, user_info): - user = User(user_info['username'], user_info['password']) - user.groups = list(user_info['groups']) - return user + def _createUser(self, name, info): + u = User(name, info.password) + u.permissions = info.flattened_perms + u.groups += info.flattened_lineage + return u diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/commonroutes.py --- a/wikked/commonroutes.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/commonroutes.py Sun Jan 07 11:11:04 2018 -0800 @@ -5,8 +5,7 @@ from werkzeug.wsgi import wrap_file from wikked.web import app, get_wiki from wikked.webimpl import ( - get_page_or_raise, url_from_viewarg, - CHECK_FOR_READ, mimetype_map) + get_page_or_raise, url_from_viewarg, mimetype_map) @app.route('/pagefiles/') @@ -17,7 +16,7 @@ replace('\\', '/').\ rstrip('/') page = get_page_or_raise(wiki, page_url, fields=['path'], - check_perms=(user, CHECK_FOR_READ)) + check_perms=(user, 'read')) # If no exception was thrown, we're good for reading the file. path_no_ext, _ = os.path.splitext(page.path) diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/page.py --- a/wikked/page.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/page.py Sun Jan 07 11:11:04 2018 -0800 @@ -8,14 +8,19 @@ logger = logging.getLogger(__name__) -def get_meta_value(meta, key, first=False): +class UnexpectedMultipleMetaValuesError(Exception): + pass + + +def get_meta_value(meta, key, *, is_single=False): value = meta.get(key) - if value is not None and isinstance(value, list): - l = len(value) - if l == 0: - return None - if l == 1 or first: - return value[0] + if isinstance(value, list): + if is_single: + lv = len(value) + if lv == 0: + return None + if lv == 1: + return value[0] return value return value @@ -112,15 +117,15 @@ def getFormattedText(self): return self._data.formatted_text - def getMeta(self, name=None, first=False): + def getMeta(self, name=None, is_single=False): if name is None: return self._data.ext_meta - return get_meta_value(self._data.ext_meta, name, first) + return get_meta_value(self._data.ext_meta, name, is_single=is_single) - def getLocalMeta(self, name=None, first=False): + def getLocalMeta(self, name=None, is_single=False): if name is None: return self._data.local_meta - return get_meta_value(self._data.local_meta, name, first) + return get_meta_value(self._data.local_meta, name, is_single=is_single) def getLocalLinks(self): return self._data.local_links diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/resolver.py --- a/wikked/resolver.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/resolver.py Sun Jan 07 11:11:04 2018 -0800 @@ -400,7 +400,7 @@ return page.text if re_wiki_query_local_meta.match(stripped_value): - meta = self.page.getLocalMeta(stripped_value) + meta = self.page.getLocalMeta(stripped_value, is_single=True) if with_url: return (None, meta) return meta diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/templates/error-unauthorized.html --- a/wikked/templates/error-unauthorized.html Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/templates/error-unauthorized.html Sun Jan 07 11:11:04 2018 -0800 @@ -2,13 +2,13 @@ {% block content %}
-

{{error|default("You're not authorized for this")}}

+

{{error|default("User Permission Error")}}

{%if error_details%}

{{error_details}}

{%else%} -

The page you're trying to access is protected.

+

You don't have permission for this.

{%endif%} {%if auth.is_logged_in%}

User {{auth.username}} is not authorized for this. Please log into a different account to have access to it.

diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/templates/error.html --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/templates/error.html Sun Jan 07 11:11:04 2018 -0800 @@ -0,0 +1,11 @@ +{% extends 'index.html' %} +{% block content %} +
+
+

{{error}}

+
+
+

{{error_details}}

+
+
+{% endblock %} diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/templates/page_error.html --- a/wikked/templates/page_error.html Sun Jan 07 11:09:30 2018 -0800 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,1 +0,0 @@ -
{{message}}
diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/utils.py --- a/wikked/utils.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/utils.py Sun Jan 07 11:11:04 2018 -0800 @@ -131,10 +131,10 @@ items = list(meta.items()) for k, v in items: if isinstance(v, list): - l = len(v) - if l == 0: + lv = len(v) + if lv == 0: del meta[k] - elif l == 1: + elif lv == 1: meta[k] = v[0] return meta diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/__init__.py --- a/wikked/views/__init__.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/views/__init__.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,65 +1,7 @@ import urllib.parse -import functools -from flask import request, render_template, url_for +from flask import request, url_for from flask.ext.login import current_user from wikked.utils import get_url_folder -from wikked.web import app, get_wiki -from wikked.webimpl import PermissionError - - -def show_unauthorized_error(error=None, error_details=None, tpl_name=None): - if error is not None: - error = str(error) - - data = {} - if error: - data['error'] = error - if error_details: - data['error_details'] = error_details - - add_auth_data(data) - add_navigation_data(None, data) - tpl_name = tpl_name or 'error-unauthorized.html' - return render_template(tpl_name, **data) - - -def errorhandling_ui(f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - try: - return f(*args, **kwargs) - except PermissionError as ex: - return show_unauthorized_error(ex) - return wrapper - - -def errorhandling_ui2(tpl_name): - def decorator(f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - try: - return f(*args, **kwargs) - except PermissionError as ex: - return show_unauthorized_error(ex, tpl_name=tpl_name) - return wrapper - return decorator - - -def requires_auth(group): - def decorator(f): - @functools.wraps(f) - def wrapper(*args, **kwargs): - wiki = get_wiki() - if not wiki.auth.hasPermission(group, current_user.get_id()): - return show_unauthorized_error() - return f(*args, **kwargs) - return wrapper - return decorator - - -def requires_reader_auth(f): - decorator = requires_auth('readers') - return decorator(f) def add_auth_data(data): @@ -69,7 +11,6 @@ data['auth'] = { 'is_logged_in': True, 'username': username, - 'is_admin': current_user.is_admin(), 'url_logout': '/logout', 'url_profile': '/read/%s' % user_page_url } diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/admin.py --- a/wikked/views/admin.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/views/admin.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,61 +1,12 @@ import urllib.parse -from flask import url_for, request, redirect, render_template -from flask.ext.login import login_user, logout_user, current_user -from wikked.views import ( - add_auth_data, add_navigation_data, requires_reader_auth) +from flask import url_for, render_template +from wikked.views import add_auth_data, add_navigation_data from wikked.web import app, get_wiki - - -@app.route('/login', methods=['GET', 'POST']) -def login(): - wiki = get_wiki() - - data = {} - add_auth_data(data) - add_navigation_data( - None, data, - raw_url='/api/user/login') - - if request.method == 'GET': - if current_user.is_authenticated(): - data['already_logged_in'] = True - return render_template('logout.html', **data) - else: - return render_template('login.html', **data) - - if request.method == 'POST': - username = request.form.get('username') - password = request.form.get('password') - remember = request.form.get('remember') - back_url = request.form.get('back_url') - - user = wiki.auth.getUser(username) - if user is not None and app.bcrypt: - if app.bcrypt.check_password_hash(user.password, password): - login_user(user, remember=bool(remember)) - return redirect(back_url or '/') - - data['has_error'] = True - return render_template('login.html', **data) - - -@app.route('/logout', methods=['GET', 'POST']) -def logout(): - if request.method == 'GET': - data = {} - add_auth_data(data) - add_navigation_data( - None, data, - raw_url='/api/user/logout') - return render_template('logout.html', **data) - - if request.method == 'POST': - logout_user() - return redirect('/') +from wikked.webimpl.decorators import requires_permission @app.route('/special/users') -@requires_reader_auth +@requires_permission('users') def special_users(): wiki = get_wiki() diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/edit.py --- a/wikked/views/edit.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/views/edit.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,26 +1,21 @@ from flask import abort, redirect, url_for, request, render_template from flask.ext.login import current_user -from wikked.views import ( - errorhandling_ui2, show_unauthorized_error, - add_auth_data, add_navigation_data) +from wikked.views import add_auth_data, add_navigation_data from wikked.web import app, get_wiki from wikked.webimpl import url_from_viewarg +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.edit import ( get_edit_page, do_edit_page, preview_edited_page, do_upload_file) -@app.route('/create/', methods=['GET']) +@app.route('/create', methods=['GET']) def create_page_at_root(): return create_page('/') @app.route('/create/') +@requires_permission('create') def create_page(url_folder): - wiki = get_wiki() - if not wiki.auth.hasPermission('writers', current_user.get_id()): - return show_unauthorized_error( - error="You're not authorized to create new pages.") - title_hint = ((url_folder or '') + '/New Page').lstrip('/') data = { 'is_new': True, @@ -38,25 +33,21 @@ @app.route('/create', methods=['POST']) +@requires_permission('create') def create_page_postback(): - wiki = get_wiki() - if not wiki.auth.hasPermission('writers', current_user.get_id()): - return show_unauthorized_error( - error="You're not authorized to create new pages.") - url = request.form['title'] return edit_page(url) @app.route('/edit', methods=['POST']) -@errorhandling_ui2('error-unauthorized-edit.html') +@requires_permission('edit') def edit_new_page(): url = request.form['title'] return edit_page(url) @app.route('/edit/', methods=['GET', 'POST']) -@errorhandling_ui2('error-unauthorized-edit.html') +@requires_permission('edit') def edit_page(url): wiki = get_wiki() user = current_user.get_id() @@ -104,7 +95,7 @@ @app.route('/upload', methods=['GET', 'POST']) -@errorhandling_ui2('error-unauthorized-upload.html') +@requires_permission('create') def upload_file(): p = request.args.get('p') data = { diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/error.py --- a/wikked/views/error.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/views/error.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,30 +1,60 @@ -from flask import jsonify +from flask import request, jsonify, render_template from wikked.scm.base import SourceControlError +from wikked.views import add_auth_data, add_navigation_data from wikked.web import app -from wikked.webimpl import PermissionError +from wikked.webimpl import UserPermissionError + + +def _render_error(error=None, error_details=None, tpl_name=None): + if error is not None: + error = str(error) + + data = {} + if error: + data['error'] = error + if error_details: + data['error_details'] = error_details + + add_auth_data(data) + add_navigation_data(None, data) + tpl_name = tpl_name or 'error.html' + return render_template(tpl_name, **data) + + +def _jsonify_error(error_type='error', error=None, error_details=None, + error_code=500): + resp = { + 'error': { + 'type': error_type, + 'message': error + } + } + if error_details: + resp['error'].update(error_details) + return jsonify(resp), error_code @app.errorhandler(SourceControlError) def handle_source_control_error(error): app.log_exception(error) - resp = { - 'error': { - 'type': 'source_control', - 'operation': error.operation, - 'message': error.message - } - } - return jsonify(resp), 500 + if request.path.startswith('/api/'): + return _jsonify_error('source_control', + error.message, + {'operation': error.operation}) + else: + return _render_error("Source Control Error", error.message) -@app.errorhandler(PermissionError) +@app.errorhandler(UserPermissionError) def handle_permission_error(error): - app.log_exception(error) - resp = { - 'error': { - 'type': 'permission', - 'message': str(error) - } - } - return jsonify(resp), 403 - + if request.path.startswith('/api/'): + return _jsonify_error('user_permission', + str(error)) + else: + perms = error.perm + tpl_name = 'error-unauthorized.html' + if 'edit' in perms or 'create' in perms: + tpl_name = 'error-unauthorized-edit.html' + elif 'upload' in perms: + tpl_name = 'error-unauthorized-upload.html' + return _render_error("User Permission Error", str(error), tpl_name) diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/history.py --- a/wikked/views/history.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/views/history.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,18 +1,17 @@ import urllib.parse from flask import request, abort, render_template from flask.ext.login import current_user -from wikked.views import ( - errorhandling_ui, requires_reader_auth, - add_auth_data, add_navigation_data) +from wikked.views import add_auth_data, add_navigation_data from wikked.web import app, get_wiki from wikked.webimpl import url_from_viewarg +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.history import ( get_site_history, get_page_history, read_page_rev, diff_revs, diff_page_revs) @app.route('/special/history') -@requires_reader_auth +@requires_permission('wikihistory') def site_history(): wiki = get_wiki() user = current_user.get_id() @@ -29,7 +28,7 @@ @app.route('/hist/') -@errorhandling_ui +@requires_permission('history') def page_history(url): wiki = get_wiki() user = current_user.get_id() @@ -44,7 +43,7 @@ @app.route('/rev/') -@errorhandling_ui +@requires_permission('history') def page_rev(url): rev = request.args.get('rev') if rev is None: @@ -67,7 +66,7 @@ @app.route('/diff/') -@errorhandling_ui +@requires_permission('history') def diff_page(url): rev1 = request.args.get('rev1') rev2 = request.args.get('rev2') @@ -95,7 +94,7 @@ @app.route('/diff_rev/') -@errorhandling_ui +@requires_permission('history') def diff_revision(rev): wiki = get_wiki() user = current_user.get_id() diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/read.py --- a/wikked/views/read.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/views/read.py Sun Jan 07 11:11:04 2018 -0800 @@ -3,10 +3,11 @@ render_template, request, abort) from flask.ext.login import current_user from wikked.utils import split_page_url, PageNotFoundError -from wikked.views import add_auth_data, add_navigation_data, errorhandling_ui +from wikked.views import add_auth_data, add_navigation_data from wikked.web import app, get_wiki from wikked.webimpl import ( url_from_viewarg, make_page_title, RedirectNotFoundError) +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.read import ( read_page, get_incoming_links) from wikked.webimpl.special import get_search_results @@ -33,7 +34,6 @@ @app.route('/read/') -@errorhandling_ui def read(url): wiki = get_wiki() url = url_from_viewarg(url) @@ -62,7 +62,7 @@ @app.route('/search') -@errorhandling_ui +@requires_permission('search') def search(): query = request.args.get('q') if query is None or query == '': @@ -85,7 +85,7 @@ @app.route('/inlinks/') -@errorhandling_ui +@requires_permission('read') def incoming_links(url): wiki = get_wiki() user = current_user.get_id() diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/special.py --- a/wikked/views/special.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/views/special.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,9 +1,8 @@ from flask import request, redirect, url_for, render_template, abort from flask.ext.login import current_user -from wikked.views import ( - requires_auth, requires_reader_auth, - add_auth_data, add_navigation_data) +from wikked.views import add_auth_data, add_navigation_data from wikked.web import app, get_wiki +from wikked.webimpl.decorators import requires_permission from wikked.webimpl.special import ( get_orphans, get_broken_redirects, get_double_redirects, get_dead_ends, get_broken_links, get_wanted_pages) @@ -87,7 +86,7 @@ @app.route('/special') -@requires_reader_auth +@requires_permission('read') def special_pages_dashboard(): data = { 'is_special_page': True, @@ -136,42 +135,42 @@ @app.route('/special/list/orphans') -@requires_reader_auth +@requires_permission('read') def special_list_orphans(): return call_api('orphans', get_orphans, raw_url='/api/orphans') @app.route('/special/list/broken-redirects') -@requires_reader_auth +@requires_permission('read') def special_list_broken_redirects(): return call_api('broken-redirects', get_broken_redirects, raw_url='/api/broken-redirects') @app.route('/special/list/double-redirects') -@requires_reader_auth +@requires_permission('read') def special_list_double_redirects(): return call_api('double-redirects', get_double_redirects, raw_url='/api/double-redirects') @app.route('/special/list/dead-ends') -@requires_reader_auth +@requires_permission('read') def special_list_dead_ends(): return call_api('dead-ends', get_dead_ends, raw_url='/api/dead-ends') @app.route('/special/list/broken-links') -@requires_reader_auth +@requires_permission('read') def special_list_broken_links(): return call_api('broken-links', get_broken_links, raw_url='/api/broken-links') @app.route('/special/list/wanted-pages') -@requires_reader_auth +@requires_permission('read') def special_list_wanted_pages(): return call_api('wanted-pages', get_wanted_pages, raw_url='/api/wanted-pages', @@ -179,7 +178,7 @@ @app.route('/special/list-refresh', methods=['POST']) -@requires_auth('administrators') +@requires_permission('listrefresh') def special_list_refresh(): list_name = request.form.get('list_name') postback_name = request.form.get('postback') diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/views/user.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/views/user.py Sun Jan 07 11:11:04 2018 -0800 @@ -0,0 +1,52 @@ +from flask import request, redirect, render_template +from flask.ext.login import login_user, logout_user, current_user +from wikked.views import add_auth_data, add_navigation_data +from wikked.web import app, get_wiki + + +@app.route('/login', methods=['GET', 'POST']) +def login(): + wiki = get_wiki() + + data = {} + add_auth_data(data) + add_navigation_data( + None, data, + raw_url='/api/user/login') + + if request.method == 'GET': + if current_user.is_authenticated(): + data['already_logged_in'] = True + return render_template('logout.html', **data) + else: + return render_template('login.html', **data) + + if request.method == 'POST': + username = request.form.get('username') + password = request.form.get('password') + remember = request.form.get('remember') + back_url = request.form.get('back_url') + + user = wiki.auth.getUser(username) + if user is not None and app.bcrypt: + if app.bcrypt.check_password_hash(user.password, password): + login_user(user, remember=bool(remember)) + return redirect(back_url or '/') + + data['has_error'] = True + return render_template('login.html', **data) + + +@app.route('/logout', methods=['GET', 'POST']) +def logout(): + if request.method == 'GET': + data = {} + add_auth_data(data) + add_navigation_data( + None, data, + raw_url='/api/user/logout') + return render_template('logout.html', **data) + + if request.method == 'POST': + logout_user() + return redirect('/') diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/web.py --- a/wikked/web.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/web.py Sun Jan 07 11:11:04 2018 -0800 @@ -78,12 +78,12 @@ # Customize logging. if app.config['DEBUG']: - l = logging.getLogger('wikked') - l.setLevel(logging.DEBUG) + lg = logging.getLogger('wikked') + lg.setLevel(logging.DEBUG) if app.config['SQL_DEBUG']: - l = logging.getLogger('sqlalchemy') - l.setLevel(logging.DEBUG) + lg = logging.getLogger('sqlalchemy') + lg.setLevel(logging.DEBUG) app.logger.debug("Creating Flask application...") @@ -116,6 +116,7 @@ app.logger.debug("Uncaching all pages because %s was edited." % url) wiki.db.uncachePages(except_url=url, only_required=True) + app.wiki_params.wiki_updater = uncaching_wiki_updater @@ -178,12 +179,14 @@ import wikked.api.history # NOQA import wikked.api.read # NOQA import wikked.api.special # NOQA +import wikked.api.user # NOQA import wikked.views.admin # NOQA import wikked.views.edit # NOQA import wikked.views.error # NOQA import wikked.views.history # NOQA import wikked.views.read # NOQA import wikked.views.special # NOQA +import wikked.views.user # NOQA # Async wiki update. diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/webimpl/__init__.py --- a/wikked/webimpl/__init__.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/webimpl/__init__.py Sun Jan 07 11:11:04 2018 -0800 @@ -3,6 +3,7 @@ import logging import datetime import urllib.parse +from wikked.auth import PERM_READ, PERM_EDIT, PERM_NAMES from wikked.utils import ( get_absolute_url, PageNotFoundError, split_page_url, is_endpoint_url) from wikked.web import app @@ -11,10 +12,6 @@ logger = logging.getLogger(__name__) -CHECK_FOR_READ = 1 -CHECK_FOR_WRITE = 2 - - class CircularRedirectError(Exception): def __init__(self, url, visited): super(CircularRedirectError, self).__init__( @@ -31,8 +28,10 @@ self.url = not_found -class PermissionError(Exception): - pass +class UserPermissionError(Exception): + def __init__(self, perm, message): + super().__init__(message) + self.perm = perm def url_from_viewarg(url): @@ -49,8 +48,7 @@ return (None, '/' + path) -def get_page_or_raise(wiki, url, fields=None, - check_perms=None): +def get_page_or_raise(wiki, url, fields=None, check_perms=None): auto_reload = app.config.get('WIKI_AUTO_RELOAD', False) if auto_reload is True and fields is not None: if 'path' not in fields: @@ -64,8 +62,8 @@ fields.append('is_resolved') if check_perms is not None and fields is not None: - if 'meta' not in fields: - fields.append('meta') + if 'local_meta' not in fields: + fields.append('local_meta') page = wiki.getPage(url, fields=fields) @@ -86,11 +84,17 @@ page = wiki.getPage(url, fields=fields) if check_perms is not None: - user, mode = check_perms - if mode == CHECK_FOR_READ and not is_page_readable(page, user): - raise PermissionError() - elif mode == CHECK_FOR_WRITE and not is_page_writable(page, user): - raise PermissionError() + user, modes = check_perms + has_page_perm = page.wiki.auth.hasPagePermission + for mode in modes.split(','): + if not has_page_perm(page, user, PERM_NAMES[mode]): + if mode == 'read': + msg = "You don't have permissions to read this page." + elif mode == 'edit': + msg = "You don't have permissions to edit this page." + else: + msg = "You don't have the '%s' permission." % mode + raise UserPermissionError(mode, msg) return page @@ -102,12 +106,12 @@ return None -def is_page_readable(page, user): - return page.wiki.auth.isPageReadable(page, user) +def is_page_readable(page, username): + return page.wiki.auth.hasPagePermission(page, username, PERM_READ) -def is_page_writable(page, user): - return page.wiki.auth.isPageWritable(page, user) +def is_page_writable(page, username): + return page.wiki.auth.hasPagePermission(page, username, PERM_EDIT) def get_page_meta(page, local_only=False): diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/webimpl/decorators.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/webimpl/decorators.py Sun Jan 07 11:11:04 2018 -0800 @@ -0,0 +1,25 @@ +import functools +from flask.ext.login import current_user +from wikked.auth import PERM_NAMES +from wikked.web import get_wiki +from wikked.webimpl import UserPermissionError + + +def requires_permission(perm): + if isinstance(perm, str): + perm = [perm] + + p_bit = 0 + for p in perm: + p_bit |= PERM_NAMES[p] + + def decorator(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + wiki = get_wiki() + if not wiki.auth.hasPermission(current_user.get_id(), p_bit): + raise UserPermissionError( + perm, "You don't have permission for this.") + return f(*args, **kwargs) + return wrapper + return decorator diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/webimpl/edit.py --- a/wikked/webimpl/edit.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/webimpl/edit.py Sun Jan 07 11:11:04 2018 -0800 @@ -7,7 +7,6 @@ from wikked.resolver import PageResolver from wikked.utils import PageNotFoundError from wikked.webimpl import ( - CHECK_FOR_WRITE, get_page_or_raise, get_page_meta, make_page_title) @@ -46,7 +45,7 @@ page = None try: page = get_page_or_raise(wiki, url, - check_perms=(user, CHECK_FOR_WRITE)) + check_perms=(user, 'edit')) except PageNotFoundError: # Only catch errors about the page not existing. Permission # errors still go through. @@ -80,7 +79,7 @@ def do_edit_page(wiki, user, url, text, author=None, message=None): try: get_page_or_raise(wiki, url, - check_perms=(user, CHECK_FOR_WRITE)) + check_perms=(user, 'edit')) except PageNotFoundError: # Only catch errors about the page not existing. Permission # errors still go through. diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/webimpl/history.py --- a/wikked/webimpl/history.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/webimpl/history.py Sun Jan 07 11:11:04 2018 -0800 @@ -3,12 +3,8 @@ from pygments import highlight from pygments.formatters import get_formatter_by_name from pygments.lexers import get_lexer_by_name -from wikked.page import PageLoadingError -from wikked.scm.base import ACTION_NAMES -from wikked.utils import PageNotFoundError -from wikked.webimpl import ( - CHECK_FOR_READ, - is_page_readable, get_page_meta, get_page_or_raise) +from wikked.scm.base import ACTION_NAMES, ACTION_ADD, ACTION_EDIT +from wikked.webimpl import get_page_meta, get_page_or_raise def get_history_data(wiki, user, history, needs_files=False): @@ -27,24 +23,19 @@ if needs_files: rev_data['pages'] = [] for f in rev.files: - url = None - path = os.path.join(wiki.root, f['path']) - try: - page = wiki.db.getPage(path=path) - # Hide pages that the user can't see. - if not is_page_readable(page, user): - continue - url = page.url - except PageNotFoundError: - pass - except PageLoadingError: - pass - if not url: - url = os.path.splitext(f['path'])[0] - rev_data['pages'].append({ - 'url': url, - 'action': ACTION_NAMES[f['action']] - }) + path = os.path.join(wiki.root, f.path) + page_info = wiki.fs.getPageInfo(path) + action_name = ACTION_NAMES[f.action] + if page_info is not None: + rev_data['pages'].append({ + 'url': page_info.url, + 'is_add_or_edit': (f.action == ACTION_ADD or + f.action == ACTION_EDIT), + 'action': action_name}) + else: + rev_data['pages'].append({ + 'path': f.path, + 'action': action_name}) rev_data['num_pages'] = len(rev_data['pages']) if len(rev_data['pages']) > 0: hist_data.append(rev_data) @@ -61,7 +52,7 @@ def get_page_history(wiki, user, url): - page = get_page_or_raise(wiki, url, check_perms=(user, CHECK_FOR_READ)) + page = get_page_or_raise(wiki, url, check_perms=(user, 'read,history')) history = page.getHistory() hist_data = get_history_data(wiki, user, history) result = {'url': url, 'meta': get_page_meta(page), 'history': hist_data} @@ -69,7 +60,7 @@ def read_page_rev(wiki, user, url, rev): - page = get_page_or_raise(wiki, url, check_perms=(user, CHECK_FOR_READ)) + page = get_page_or_raise(wiki, url, check_perms=(user, 'read,history')) page_rev = page.getRevision(rev) meta = dict(get_page_meta(page, True), rev=rev) result = {'meta': meta, 'text': page_rev} @@ -77,7 +68,7 @@ def diff_page_revs(wiki, user, url, rev1, rev2=None, raw=False): - page = get_page_or_raise(wiki, url, check_perms=(user, CHECK_FOR_READ)) + page = get_page_or_raise(wiki, url, check_perms=(user, 'read,history')) diff = page.getDiff(rev1, rev2) if not raw: lexer = get_lexer_by_name('diff') @@ -108,4 +99,3 @@ 'message': message } wiki.revertPage(url, page_fields) - diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/webimpl/read.py --- a/wikked/webimpl/read.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/webimpl/read.py Sun Jan 07 11:11:04 2018 -0800 @@ -1,7 +1,6 @@ import os.path import urllib.parse from wikked.webimpl import ( - CHECK_FOR_READ, get_redirect_target, get_page_meta, get_page_or_raise) from wikked.utils import split_page_url, PageNotFoundError @@ -14,7 +13,7 @@ page, visited_paths = get_redirect_target( wiki, path, fields=['url', 'path', 'title', 'text', 'meta'], - check_perms=(user, CHECK_FOR_READ), + check_perms=(user, 'read'), first_only=no_redirect) if no_redirect: @@ -35,7 +34,7 @@ info_page = get_page_or_raise( wiki, meta_page_url, fields=['url', 'path', 'title', 'text', 'meta'], - check_perms=(user, CHECK_FOR_READ)) + check_perms=(user, 'read')) except PageNotFoundError: # Let permissions errors go through, but if the info page is not # found that's OK. @@ -49,7 +48,7 @@ info_page = get_page_or_raise( wiki, endpoint_info.default, fields=['url', 'path', 'title', 'text', 'meta'], - check_perms=(user, CHECK_FOR_READ)) + check_perms=(user, 'read')) ext = None if info_page is not None: @@ -113,14 +112,14 @@ def get_incoming_links(wiki, user, url): page = get_page_or_raise( wiki, url, - check_perms=(user, CHECK_FOR_READ), + check_perms=(user, 'read'), fields=['url', 'title', 'meta']) links = [] for link in page.getIncomingLinks(): try: other = get_page_or_raise( wiki, link, - check_perms=(user, CHECK_FOR_READ), + check_perms=(user, 'read'), fields=['url', 'title', 'meta']) links.append(get_page_meta(other)) except PageNotFoundError: @@ -133,14 +132,14 @@ def get_outgoing_links(wiki, user, url): page = get_page_or_raise( wiki, url, - check_perms=(user, CHECK_FOR_READ), + check_perms=(user, 'read'), fields=['url', 'title', 'links']) links = [] for link in page.links: try: other = get_page_or_raise( wiki, link, - check_perms=(user, CHECK_FOR_READ), + check_perms=(user, 'read'), fields=['url', 'title', 'meta']) links.append(get_page_meta(other)) except PageNotFoundError: diff -r ab47d3cf5e1e -r 6cd51ea6dfcf wikked/webimpl/special.py --- a/wikked/webimpl/special.py Sun Jan 07 11:09:30 2018 -0800 +++ b/wikked/webimpl/special.py Sun Jan 07 11:11:04 2018 -0800 @@ -3,11 +3,10 @@ from wikked.page import WantedPage from wikked.utils import get_absolute_url from wikked.webimpl import ( - CHECK_FOR_READ, get_page_meta, get_page_or_raise, make_page_title, is_page_readable, get_redirect_target, get_or_build_pagelist, get_generic_pagelist_builder, - CircularRedirectError, RedirectNotFoundError) + UserPermissionError, CircularRedirectError, RedirectNotFoundError) def build_pagelist_view_data(pages, user): @@ -183,7 +182,7 @@ def list_pages(wiki, user, url=None): - pages = list(filter(is_page_readable, wiki.getPages(url))) + pages = [p for p in wiki.getPages(url) if is_page_readable(p, user)] page_metas = [get_page_meta(page) for page in pages] result = {'path': url, 'pages': list(page_metas)} return result @@ -195,8 +194,8 @@ for h in hits: try: get_page_or_raise(wiki, h.url, - check_perms=(user, CHECK_FOR_READ)) - except PermissionError: + check_perms=(user, 'read')) + except UserPermissionError: continue readable_hits.append({ @@ -217,8 +216,8 @@ for h in hits: try: get_page_or_raise(wiki, h.url, - check_perms=(user, CHECK_FOR_READ)) - except PermissionError: + check_perms=(user, 'read')) + except UserPermissionError: continue readable_hits.append({'url': h.url, 'title': h.title}) @@ -228,5 +227,3 @@ 'hit_count': len(readable_hits), 'hits': readable_hits} return result - -