| # Licensed to the Apache Software Foundation (ASF) under one |
| # or more contributor license agreements. See the NOTICE file |
| # distributed with this work for additional information |
| # regarding copyright ownership. The ASF licenses this file |
| # to you under the Apache License, Version 2.0 (the |
| # "License"); you may not use this file except in compliance |
| # with the License. You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, |
| # software distributed under the License is distributed on an |
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| # KIND, either express or implied. See the License for the |
| # specific language governing permissions and limitations |
| # under the License. |
| |
| import os |
| import logging |
| from urllib import basejoin |
| from cStringIO import StringIO |
| from collections import defaultdict |
| |
| import pkg_resources |
| from tg import expose, redirect, flash, validate |
| from tg.decorators import without_trailing_slash |
| from pylons import request, app_globals as g, tmpl_context as c |
| from paste.deploy.converters import asbool, asint |
| from bson import ObjectId |
| from bson.errors import InvalidId |
| from formencode import validators as V |
| |
| from ming.orm import session |
| from ming.utils import LazyProperty |
| |
| from allura.lib import helpers as h |
| from allura.lib.security import has_access, require_access |
| from allura import model |
| from allura.controllers import BaseController |
| from allura.lib.decorators import require_post, memoize |
| from allura.lib.utils import permanent_redirect, ConfigProxy |
| |
| log = logging.getLogger(__name__) |
| |
| config = ConfigProxy(common_suffix='forgemail.domain') |
| |
| |
| class ConfigOption(object): |
| |
| """Definition of a configuration option for an :class:`Application`. |
| |
| """ |
| |
| def __init__(self, name, ming_type, default, label=None): |
| """Create a new ConfigOption. |
| |
| """ |
| self.name, self.ming_type, self._default, self.label = ( |
| name, ming_type, default, label or name) |
| |
| @property |
| def default(self): |
| """Return the default value for this ConfigOption. |
| |
| """ |
| if callable(self._default): |
| return self._default() |
| return self._default |
| |
| |
| class SitemapEntry(object): |
| |
| """A labeled URL, which may optionally have |
| :class:`children <SitemapEntry>`. |
| |
| Used for generating trees of links. |
| |
| """ |
| |
| def __init__(self, label, url=None, children=None, className=None, |
| ui_icon=None, small=None, tool_name=None, matching_urls=None): |
| """Create a new SitemapEntry. |
| |
| """ |
| self.label = label |
| self.className = className |
| if url is not None: |
| url = url.encode('utf-8') |
| self.url = url |
| self.small = small |
| self.ui_icon = ui_icon |
| self.children = children or [] |
| self.tool_name = tool_name |
| self.matching_urls = matching_urls or [] |
| |
| def __getitem__(self, x): |
| """Automatically expand the list of sitemap child entries with the |
| given items. Example:: |
| |
| SitemapEntry('HelloForge')[ |
| SitemapEntry('foo')[ |
| SitemapEntry('Pages')[pages] |
| ] |
| ] |
| |
| TODO: deprecate this; use a more clear method of building a tree |
| |
| """ |
| if isinstance(x, (list, tuple)): |
| self.children.extend(list(x)) |
| else: |
| self.children.append(x) |
| return self |
| |
| def __repr__(self): |
| l = ['<SitemapEntry '] |
| l.append(' label=%r' % self.label) |
| l.append(' url=%r' % self.url) |
| l.append(' children=%s' % |
| repr(self.children).replace('\n', '\n ')) |
| l.append('>') |
| return '\n'.join(l) |
| |
| def bind_app(self, app): |
| """Recreate this SitemapEntry in the context of |
| :class:`app <Application>`. |
| |
| :returns: :class:`SitemapEntry` |
| |
| """ |
| lbl = self.label |
| url = self.url |
| if callable(lbl): |
| lbl = lbl(app) |
| if url is not None: |
| url = basejoin(app.url, url) |
| return SitemapEntry(lbl, url, [ |
| ch.bind_app(app) for ch in self.children], |
| className=self.className, |
| ui_icon=self.ui_icon, |
| small=self.small, |
| tool_name=self.tool_name, |
| matching_urls=self.matching_urls) |
| |
| def extend(self, sitemap_entries): |
| """Extend our children with ``sitemap_entries``. |
| |
| :param sitemap_entries: list of :class:`SitemapEntry` |
| |
| For each entry, if it doesn't already exist in our children, add it. |
| If it does already exist in our children, recursively extend the |
| children or our copy with the children of the new copy. |
| |
| """ |
| child_index = dict( |
| (ch.label, ch) for ch in self.children) |
| for e in sitemap_entries: |
| lbl = e.label |
| match = child_index.get(e.label) |
| if match and match.url == e.url: |
| match.extend(e.children) |
| else: |
| self.children.append(e) |
| child_index[lbl] = e |
| |
| def matches_url(self, request): |
| """Return True if this SitemapEntry 'matches' the url of ``request``. |
| |
| """ |
| return self.url in request.upath_info or any([ |
| url in request.upath_info for url in self.matching_urls]) |
| |
| |
| class Application(object): |
| |
| """ |
| The base Allura pluggable application |
| |
| After extending this, expose the app by adding an entry point in your |
| setup.py:: |
| |
| [allura] |
| myapp = foo.bar.baz:MyAppClass |
| |
| :cvar str status: One of 'production', 'beta', 'alpha', or 'user'. By |
| default, only 'production' apps are installable in projects. Default |
| is 'production'. |
| :cvar bool searchable: If True, show search box in the left menu of this |
| Application. Default is True. |
| :cvar bool exportable: Default is False, Application can't be exported to json. |
| :cvar list permissions: Named permissions used by instances of this |
| Application. Default is []. |
| :cvar dict permissions_desc: Descriptions of the named permissions. |
| :cvar int max_instances: Specifies the number of tools of this type |
| that can be added to the project. Zero indicates the system tool or one that |
| can not be added to the project by the user. Default value is float("inf"). |
| :cvar bool hidden: Default is False, Application is not hidden from the |
| list of a project's installed tools. |
| :cvar str tool_description: Text description of this Application. |
| :cvar bool relaxed_mount_points: Set to True to relax the default mount point |
| naming restrictions for this Application. Default is False. See |
| :attr:`default mount point naming rules <allura.lib.helpers.re_tool_mount_point>` and |
| :attr:`relaxed mount point naming rules <allura.lib.helpers.re_relaxed_tool_mount_point>`. |
| :cvar Controller root: Serves content at |
| /<neighborhood>/<project>/<app>/. Default is None - subclasses should |
| override. |
| :cvar Controller api_root: Serves API access at |
| /rest/<neighborhood>/<project>/<app>/. Default is None - subclasses |
| should override to expose API access to the Application. |
| :ivar Controller admin: Serves admin functions at |
| /<neighborhood>/<project>/<admin>/<app>/. Default is a |
| :class:`DefaultAdminController` instance. |
| :cvar dict icons: Mapping of icon sizes to application-specific icon paths. |
| """ |
| |
| __version__ = None |
| config_options = [ |
| ConfigOption('mount_point', str, 'app'), |
| ConfigOption('mount_label', str, 'app'), |
| ConfigOption('ordinal', int, '0')] |
| status_map = ['production', 'beta', 'alpha', 'user'] |
| status = 'production' |
| script_name = None |
| root = None # root controller |
| api_root = None |
| permissions = [] |
| permissions_desc = { |
| 'unmoderated_post': 'Post comments without moderation.', |
| 'post': 'Post comments, subject to moderation.', |
| 'moderate': 'Moderate comments.', |
| 'configure': 'Set label and options. Requires admin permission.', |
| 'admin': 'Set permissions.', |
| } |
| max_instances = float("inf") |
| searchable = False |
| exportable = False |
| DiscussionClass = model.Discussion |
| PostClass = model.Post |
| AttachmentClass = model.DiscussionAttachment |
| tool_label = 'Tool' |
| tool_description = "This is a tool for Allura forge." |
| default_mount_label = 'Tool Name' |
| default_mount_point = 'tool' |
| relaxed_mount_points = False |
| ordinal = 0 |
| hidden = False |
| icons = { |
| 24: 'images/admin_24.png', |
| 32: 'images/admin_32.png', |
| 48: 'images/admin_48.png' |
| } |
| |
| def __init__(self, project, app_config_object): |
| """Create an Application instance. |
| |
| :param project: Project to which this Application belongs |
| :type project: :class:`allura.model.project.Project` |
| :param app_config_object: Config describing this Application |
| :type app_config_object: :class:`allura.model.project.AppConfig` |
| |
| """ |
| self.project = project |
| self.config = app_config_object |
| self.admin = DefaultAdminController(self) |
| |
| @LazyProperty |
| def sitemap(self): |
| """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>` |
| describing the page hierarchy provided by this Application. |
| |
| If the list is empty, the Application will not be displayed in the |
| main project nav bar. |
| |
| """ |
| return [SitemapEntry(self.config.options.mount_label, '.')] |
| |
| @LazyProperty |
| def url(self): |
| """Return the URL for this Application. |
| |
| """ |
| return self.config.url(project=self.project) |
| |
| @property |
| def email_address(self): |
| """Return email address for this Application. |
| |
| Email address constructed from Application's url, and looks like this: |
| |
| wiki@test.p.in.sf.net |
| |
| where 'wiki@test.p' comes from app url (in this case /p/test/wiki/) |
| and '.in.sf.net' comes from 'forgemail.domain' config entry. |
| |
| Assumes self.url returns a url path without domain, starting with '/' |
| """ |
| parts = list(reversed(self.url[1:-1].split('/'))) |
| return '%s@%s%s' % (parts[0], '.'.join(parts[1:]), config.common_suffix) |
| |
| @property |
| def acl(self): |
| """Return the :class:`Access Control List <allura.model.types.ACL>` |
| for this Application. |
| |
| """ |
| return self.config.acl |
| |
| @classmethod |
| def describe_permission(cls, permission): |
| """Return help text describing what features ``permission`` controls. |
| |
| Subclasses should define :attr:`permissions_desc`, |
| a ``{permission: description}`` mapping. |
| |
| Returns empty string if there is no description for ``permission``. |
| |
| """ |
| d = {} |
| for t in reversed(cls.__mro__): |
| d = dict(d, **getattr(t, 'permissions_desc', {})) |
| return d.get(permission, '') |
| |
| def parent_security_context(self): |
| """Return the parent of this object. |
| |
| Used for calculating permissions based on trees of ACLs. |
| |
| """ |
| return self.config.parent_security_context() |
| |
| @property |
| def installable(self): |
| """Checks whether to add a tool to the project. |
| |
| Return True if app can be installed. |
| |
| :rtype: bool |
| |
| """ |
| tools_list = [tool.tool_name.lower() |
| for tool in self.project.app_configs] |
| return tools_list.count(self.config.tool_name.lower()) < self.max_instances |
| |
| @classmethod |
| def validate_mount_point(cls, mount_point): |
| """Check if ``mount_point`` is valid for this Application. |
| |
| In general, subclasses should not override this, but rather toggle |
| the strictness of allowed mount point names by toggling |
| :attr:`Application.relaxed_mount_points`. |
| |
| :param mount_point: the mount point to validate |
| :type mount_point: str |
| :rtype: A :class:`regex Match object <_sre.SRE_Match>` if the mount |
| point is valid, else None |
| |
| """ |
| re = (h.re_relaxed_tool_mount_point if cls.relaxed_mount_points |
| else h.re_tool_mount_point) |
| return re.match(mount_point) |
| |
| @classmethod |
| def status_int(self): |
| """Return the :attr:`status` of this Application as an int. |
| |
| Used for sorting available Apps by status in the Admin interface. |
| |
| """ |
| return self.status_map.index(self.status) |
| |
| @classmethod |
| def icon_url(cls, size): |
| """Return URL for icon of the given ``size``. |
| |
| Subclasses can define their own icons by overriding |
| :attr:`icons`. |
| |
| """ |
| resource, url = cls.icons.get(size), '' |
| if resource: |
| resource_path = os.path.join('nf', resource) |
| url = (g.forge_static(resource) if cls.has_resource(resource_path) |
| else g.theme_href(resource)) |
| return url |
| |
| @classmethod |
| @memoize |
| def has_resource(cls, resource_path): |
| """Determine whether this Application has the resource pointed to by |
| ``resource_path``. |
| |
| If the resource is not found for the immediate class, its parents |
| will be searched. The return value is the class that "owns" the |
| resource, or None if the resource is not found. |
| |
| """ |
| for klass in [o for o in cls.__mro__ if issubclass(o, Application)]: |
| if pkg_resources.resource_exists(klass.__module__, resource_path): |
| return klass |
| |
| def has_access(self, user, topic): |
| """Return True if ``user`` can send email to ``topic``. |
| Default is False. |
| |
| :param user: :class:`allura.model.User` instance |
| :param topic: str |
| :rtype: bool |
| |
| """ |
| return False |
| |
| def is_visible_to(self, user): |
| """Return True if ``user`` can view this app. |
| |
| :type user: :class:`allura.model.User` instance |
| :rtype: bool |
| |
| """ |
| return has_access(self, 'read')(user=user) |
| |
| def subscribe_admins(self): |
| """Subscribe all project Admins (for this Application's project) to the |
| :class:`allura.model.notification.Mailbox` for this Application. |
| |
| """ |
| for uid in g.credentials.userids_with_named_role(self.project._id, 'Admin'): |
| model.Mailbox.subscribe( |
| type='direct', |
| user_id=uid, |
| project_id=self.project._id, |
| app_config_id=self.config._id) |
| |
| def subscribe(self, user): |
| """Subscribe :class:`user <allura.model.auth.User>` to the |
| :class:`allura.model.notification.Mailbox` for this Application. |
| |
| """ |
| if user and user != model.User.anonymous(): |
| model.Mailbox.subscribe( |
| type='direct', |
| user_id=user._id, |
| project_id=self.project._id, |
| app_config_id=self.config._id) |
| |
| @classmethod |
| def default_options(cls): |
| """Return a ``(name, default value)`` mapping of this Application's |
| :class:`config_options <ConfigOption>`. |
| |
| :rtype: dict |
| |
| """ |
| return dict( |
| (co.name, co.default) |
| for co in cls.config_options) |
| |
| def install(self, project): |
| 'Whatever logic is required to initially set up a tool' |
| # Create the discussion object |
| discussion = self.DiscussionClass( |
| shortname=self.config.options.mount_point, |
| name='%s Discussion' % self.config.options.mount_point, |
| description='Forum for %s comments' % self.config.options.mount_point) |
| session(discussion).flush() |
| self.config.discussion_id = discussion._id |
| self.subscribe_admins() |
| |
| def uninstall(self, project=None, project_id=None): |
| 'Whatever logic is required to tear down a tool' |
| if project_id is None: |
| project_id = project._id |
| # De-index all the artifacts belonging to this tool in one fell swoop |
| g.solr.delete(q='project_id_s:"%s" AND mount_point_s:"%s"' % ( |
| project_id, self.config.options['mount_point'])) |
| for d in model.Discussion.query.find({ |
| 'project_id': project_id, |
| 'app_config_id': self.config._id}): |
| d.delete() |
| self.config.delete() |
| session(self.config).flush() |
| |
| @property |
| def uninstallable(self): |
| """Return True if this app can be uninstalled. Controls whether the |
| 'Delete' option appears on the admin menu for this app. |
| |
| By default, an app can be uninstalled iff it can be installed, although |
| some apps may want/need to override this (e.g. an app which can |
| not be installed directly by a user, but may be uninstalled). |
| |
| """ |
| return self.installable |
| |
| def main_menu(self): |
| """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>` |
| to display in the main project nav for this Application. |
| |
| Default implementation returns :attr:`sitemap`. |
| |
| """ |
| return self.sitemap |
| |
| def sidebar_menu(self): |
| """Return a list of :class:`SitemapEntries <allura.app.SitemapEntry>` |
| to render in the left sidebar for this Application. |
| |
| """ |
| return [] |
| |
| def sidebar_menu_js(self): |
| """Return Javascript needed by the sidebar menu of this Application. |
| |
| :return: a string of Javascript code |
| |
| """ |
| return "" |
| |
| def admin_menu(self, force_options=False): |
| """Return the admin menu for this Application. |
| |
| Default implementation will return a menu with up to 3 links: |
| |
| - 'Permissions', if the current user has admin access to the |
| project in which this Application is installed |
| - 'Options', if this Application has custom options, or |
| ``force_options`` is True |
| - 'Label', for editing this Application's label |
| |
| Subclasses should override this method to provide additional admin |
| menu items. |
| |
| :param force_options: always include an 'Options' link in the menu, |
| even if this Application has no custom options |
| :return: a list of :class:`SitemapEntries <allura.app.SitemapEntry>` |
| |
| """ |
| admin_url = c.project.url() + 'admin/' + \ |
| self.config.options.mount_point + '/' |
| links = [] |
| if self.permissions and has_access(c.project, 'admin')(): |
| links.append( |
| SitemapEntry('Permissions', admin_url + 'permissions')) |
| if force_options or len(self.config_options) > 3: |
| links.append( |
| SitemapEntry('Options', admin_url + 'options', className='admin_modal')) |
| links.append( |
| SitemapEntry('Label', admin_url + 'edit_label', className='admin_modal')) |
| return links |
| |
| def handle_message(self, topic, message): |
| """Handle incoming email msgs addressed to this tool. |
| Default is a no-op. |
| |
| :param topic: portion of destination email address preceeding the '@' |
| :type topic: str |
| :param message: parsed email message |
| :type message: dict - result of |
| :func:`allura.lib.mail_util.parse_message` |
| :rtype: None |
| |
| """ |
| pass |
| |
| def handle_artifact_message(self, artifact, message): |
| """Handle message addressed to this Application. |
| |
| :param artifact: Specific artifact to which the message is addressed |
| :type artifact: :class:`allura.model.artifact.Artifact` |
| :param message: the message |
| :type message: :class:`allura.model.artifact.Message` |
| |
| Default implementation posts the message to the appropriate discussion |
| thread for the artifact. |
| |
| """ |
| # Find ancestor comment and thread |
| thd, parent_id = artifact.get_discussion_thread(message) |
| # Handle attachments |
| message_id = message['message_id'] |
| if message.get('filename'): |
| # Special case - the actual post may not have been created yet |
| log.info('Saving attachment %s', message['filename']) |
| fp = StringIO(message['payload']) |
| self.AttachmentClass.save_attachment( |
| message['filename'], fp, |
| content_type=message.get( |
| 'content_type', 'application/octet-stream'), |
| discussion_id=thd.discussion_id, |
| thread_id=thd._id, |
| post_id=message_id, |
| artifact_id=message_id) |
| return |
| # Handle duplicates |
| post = self.PostClass.query.get(_id=message_id) |
| if post: |
| log.info( |
| 'Existing message_id %s found - saving this as text attachment' % |
| message_id) |
| fp = StringIO(message['payload']) |
| post.attach( |
| 'alternate', fp, |
| content_type=message.get( |
| 'content_type', 'application/octet-stream'), |
| discussion_id=thd.discussion_id, |
| thread_id=thd._id, |
| post_id=message_id) |
| else: |
| text = message['payload'] or '--no text body--' |
| post = thd.post( |
| message_id=message_id, |
| parent_id=parent_id, |
| text=text, |
| subject=message['headers'].get('Subject', 'no subject')) |
| |
| def bulk_export(self, f): |
| """Export all artifacts in the tool into json file. |
| |
| :param f: File Object to write to |
| |
| Set exportable to True for applications implementing this. |
| """ |
| raise NotImplementedError, 'bulk_export' |
| |
| |
| class DefaultAdminController(BaseController): |
| |
| """Provides basic admin functionality for an :class:`Application`. |
| |
| To add more admin functionality for your Application, extend this |
| class and then assign an instance of it to the ``admin`` attr of |
| your Application:: |
| |
| class MyApp(Application): |
| def __init__(self, *args): |
| super(MyApp, self).__init__(*args) |
| self.admin = MyAdminController(self) |
| |
| """ |
| |
| def __init__(self, app): |
| """Instantiate this controller for an :class:`app <Application>`. |
| |
| """ |
| self.app = app |
| |
| @expose() |
| def index(self, **kw): |
| """Home page for this controller. |
| |
| Redirects to the 'permissions' page by default. |
| |
| """ |
| permanent_redirect('permissions') |
| |
| @expose('json:') |
| @require_post() |
| def block_user(self, username, perm, reason=None): |
| if not username or not perm: |
| return dict(error='Enter username') |
| user = model.User.by_username(username) |
| if not user: |
| return dict(error='User "%s" not found' % username) |
| ace = model.ACE.deny( |
| model.ProjectRole.by_user(user, upsert=True)._id, perm, reason) |
| if not model.ACL.contains(ace, self.app.acl): |
| self.app.acl.append(ace) |
| return dict(user_id=str(user._id), username=user.username, reason=reason) |
| return dict(error='User "%s" already blocked' % user.username) |
| |
| @validate(dict(user_id=V.Set(), |
| perm=V.UnicodeString())) |
| @expose('json:') |
| @require_post() |
| def unblock_user(self, user_id=None, perm=None): |
| try: |
| user_id = map(ObjectId, user_id) |
| except InvalidId: |
| user_id = [] |
| users = model.User.query.find({'_id': {'$in': user_id}}).all() |
| if not users: |
| return dict(error='Select user to unblock') |
| unblocked = [] |
| for user in users: |
| ace = model.ACE.deny(model.ProjectRole.by_user(user)._id, perm) |
| ace = model.ACL.contains(ace, self.app.acl) |
| if ace: |
| self.app.acl.remove(ace) |
| unblocked.append(str(user._id)) |
| return dict(unblocked=unblocked) |
| |
| @expose('jinja:allura:templates/app_admin_permissions.html') |
| @without_trailing_slash |
| def permissions(self): |
| """Render the permissions management web page. |
| |
| """ |
| from ext.admin.widgets import PermissionCard, BlockUser, BlockList |
| c.card = PermissionCard() |
| c.block_user = BlockUser() |
| c.block_list = BlockList() |
| permissions = dict((p, []) for p in self.app.permissions) |
| block_list = defaultdict(list) |
| for ace in self.app.config.acl: |
| if ace.access == model.ACE.ALLOW: |
| try: |
| permissions[ace.permission].append(ace.role_id) |
| except KeyError: |
| # old, unknown permission |
| pass |
| elif ace.access == model.ACE.DENY: |
| role = model.ProjectRole.query.get(_id=ace.role_id) |
| if role.name is None and role.user: |
| block_list[ace.permission].append((role.user, ace.reason)) |
| return dict( |
| app=self.app, |
| allow_config=has_access(c.project, 'admin')(), |
| permissions=permissions, |
| block_list=block_list) |
| |
| @expose('jinja:allura:templates/app_admin_edit_label.html') |
| def edit_label(self): |
| """Renders form to update the Application's ``mount_label``. |
| |
| """ |
| return dict( |
| app=self.app, |
| allow_config=has_access(self.app, 'configure')()) |
| |
| @expose() |
| @require_post() |
| def update_label(self, mount_label): |
| """Handles POST to update the Application's ``mount_label``. |
| |
| """ |
| require_access(self.app, 'configure') |
| self.app.config.options['mount_label'] = mount_label |
| redirect(request.referer) |
| |
| @expose('jinja:allura:templates/app_admin_options.html') |
| def options(self): |
| """Renders form to update the Application's ``config.options``. |
| |
| """ |
| return dict( |
| app=self.app, |
| allow_config=has_access(self.app, 'configure')()) |
| |
| @expose() |
| @require_post() |
| def configure(self, **kw): |
| """Handle POST to delete the Application or update its |
| ``config.options``. |
| |
| """ |
| with h.push_config(c, app=self.app): |
| require_access(self.app, 'configure') |
| is_admin = self.app.config.tool_name == 'admin' |
| if kw.pop('delete', False): |
| if is_admin: |
| flash('Cannot delete the admin tool, sorry....') |
| redirect('.') |
| c.project.uninstall_app(self.app.config.options.mount_point) |
| redirect('..') |
| for opt in self.app.config_options: |
| if opt in Application.config_options: |
| # skip base options (mount_point, mount_label, ordinal) |
| continue |
| val = kw.get(opt.name, '') |
| if opt.ming_type == bool: |
| val = asbool(val or False) |
| elif opt.ming_type == int: |
| val = asint(val or 0) |
| self.app.config.options[opt.name] = val |
| if is_admin: |
| # possibly moving admin mount point |
| redirect('/' |
| + c.project._id |
| + self.app.config.options.mount_point |
| + '/' |
| + self.app.config.options.mount_point |
| + '/') |
| else: |
| redirect(request.referer) |
| |
| @without_trailing_slash |
| @expose() |
| @h.vardec |
| @require_post() |
| def update(self, card=None, **kw): |
| """Handle POST to update permissions for the Application. |
| |
| """ |
| old_acl = self.app.config.acl |
| self.app.config.acl = [] |
| for args in card: |
| perm = args['id'] |
| new_group_ids = args.get('new', []) |
| del_group_ids = [] |
| group_ids = args.get('value', []) |
| if isinstance(new_group_ids, basestring): |
| new_group_ids = [new_group_ids] |
| if isinstance(group_ids, basestring): |
| group_ids = [group_ids] |
| |
| for acl in old_acl: |
| if (acl['permission'] == perm) and (str(acl['role_id']) not in group_ids) and acl['access'] != model.ACE.DENY: |
| del_group_ids.append(str(acl['role_id'])) |
| |
| get_role = lambda _id: model.ProjectRole.query.get( |
| _id=ObjectId(_id)) |
| groups = map(get_role, group_ids) |
| new_groups = map(get_role, new_group_ids) |
| del_groups = map(get_role, del_group_ids) |
| |
| if new_groups or del_groups: |
| model.AuditLog.log('updated "%s" permission: "%s" => "%s" for %s' % ( |
| perm, |
| ', '.join( |
| map(lambda role: role.name, groups + del_groups)), |
| ', '.join( |
| map(lambda role: role.name, groups + new_groups)), |
| self.app.config.options['mount_point'])) |
| |
| role_ids = map(ObjectId, group_ids + new_group_ids) |
| self.app.config.acl += [ |
| model.ACE.allow(r, perm) for r in role_ids] |
| |
| # Add all ACEs for user roles back |
| for ace in old_acl: |
| if (ace.permission == perm) and (ace.access == model.ACE.DENY): |
| self.app.config.acl.append(ace) |
| redirect(request.referer) |