def has_management_permission(self, permission=None, explicit=False): """Checks whether a principal has a certain management permission. The check always succeeds if the user is a full manager; in that case the list of permissions is ignored. :param permission: The permission to check for or 'ANY' to check for any management permission. :param explicit: Whether to check for the permission itself even if the user has full management privileges. """ if permission is None: if explicit: raise ValueError( 'permission must be specified if explicit=True') return self.full_access elif not explicit and self.full_access: return True valid_permissions = get_available_permissions( self.principal_for_obj).viewkeys() current_permissions = set(self.permissions) & valid_permissions if permission == 'ANY': return bool(current_permissions) assert permission in valid_permissions, "invalid permission '{}' for object '{}'".format( permission, self.principal_for_obj) return permission in current_permissions
def update_principals_permissions(obj, current, new): """Handle the updates of permissions and creations/deletions of acl principals. :param obj: The object to update. Must have ``acl_entries`` :param current: A dict mapping principals to a set with its current permissions :param new: A dict mapping principals to a set with its new permissions """ user_selectable_permissions = {v.name for k, v in get_available_permissions(obj.__class__).viewitems() if v.user_selectable} for principal, permissions in current.viewitems(): if principal not in new: permissions_kwargs = { 'full_access': False, 'read_access': False, 'del_permissions': user_selectable_permissions } obj.update_principal(principal, **permissions_kwargs) elif permissions != new[principal]: full_access, read_access, permissions = get_split_permissions(new[principal]) all_user_permissions = [set(entry.permissions) for entry in obj.acl_entries if entry.principal == principal][0] permissions_kwargs = { 'full_access': full_access, 'read_access': read_access, 'permissions': (all_user_permissions - user_selectable_permissions) | permissions } obj.update_principal(principal, **permissions_kwargs) new_principals = set(new) - set(current) for p in new_principals: full_access, read_access, permissions = get_split_permissions(new[p]) permissions_kwargs = { 'full_access': full_access, 'read_access': read_access, 'add_permissions': permissions & user_selectable_permissions } obj.update_principal(p, **permissions_kwargs)
def _process(self): form = EventProtectionForm(obj=FormDefaults(**self._get_defaults()), event=self.event) selectable_permissions = { k for k, v in get_available_permissions(Event).items() if v.user_selectable } user_permissions = [(p.principal, set(p.permissions)) for p in self.event.acl_entries] hidden_permissions = sorted( [(principal, sorted(perms)) for principal, perms in user_permissions if perms and not (perms & selectable_permissions)], key=lambda x: (x[0].principal_order, x[0].name.lower())) form.permissions.hidden_permissions = [ (p.name, perms) for p, perms in hidden_permissions ] if form.validate_on_submit(): update_permissions(self.event, form) update_event_protection( self.event, { 'protection_mode': form.protection_mode.data, 'own_no_access_contact': form.own_no_access_contact.data, 'access_key': form.access_key.data, 'visibility': form.visibility.data, 'public_regform_access': form.public_regform_access.data }) self._update_session_coordinator_privs(form) flash(_('Protection settings have been updated'), 'success') return redirect(url_for('.protection', self.event)) return WPEventProtection.render_template('event_protection.html', self.event, 'protection', form=form)
def hidden_permissions_info(self): all_permissions = get_available_permissions( PermissionsField.type_mapping[self.object_type]) visible_permissions = get_permissions_info( PermissionsField.type_mapping[self.object_type])[0] return { k: all_permissions[k].friendly_name for k in set(all_permissions) - set(visible_permissions) }
def _mock_available_permissions(mocker): permissions = dict(get_available_permissions(Event), foo=MagicMock(), bar=MagicMock(), foobar=MagicMock()) mocker.patch( 'indico.core.db.sqlalchemy.protection.get_available_permissions', return_value=permissions) mocker.patch( 'indico.core.db.sqlalchemy.principals.get_available_permissions', return_value=permissions)
def _mock_available_permissions(mocker): # The code we are testing only cares about the keys so we don't # need to actually create permissions for now. permissions = dict(get_available_permissions(Event), foo=None, bar=None, foobar=None) mocker.patch( 'indico.core.db.sqlalchemy.protection.get_available_permissions', return_value=permissions) mocker.patch( 'indico.core.db.sqlalchemy.principals.get_available_permissions', return_value=permissions)
def _log_acl_changes(sender, obj, principal, entry, is_new, old_data, quiet, **kwargs): if quiet: return user = session.user if session else None # allow acl changes outside request context available_permissions = get_available_permissions(Event) def _format_permissions(permissions): permissions = set(permissions) return ', '.join(sorted(orig_string(p.friendly_name) for p in available_permissions.values() if p.name in permissions)) data = {} if principal.principal_type == PrincipalType.user: data['User'] = principal.full_name elif principal.principal_type == PrincipalType.email: data['Email'] = principal.email elif principal.principal_type == PrincipalType.local_group: data['Group'] = principal.name elif principal.principal_type == PrincipalType.multipass_group: data['Group'] = f'{principal.name} ({principal.provider_title})' elif principal.principal_type == PrincipalType.network: data['IP Network'] = principal.name elif principal.principal_type == PrincipalType.registration_form: data['Registration Form'] = principal.title elif principal.principal_type == PrincipalType.event_role: data['Event Role'] = principal.name if entry is None: data['Read Access'] = old_data['read_access'] data['Manager'] = old_data['full_access'] data['Permissions'] = _format_permissions(old_data['permissions']) obj.log(EventLogRealm.management, EventLogKind.negative, 'Protection', 'ACL entry removed', user, data=data) elif is_new: data['Read Access'] = entry.read_access data['Manager'] = entry.full_access if entry.permissions: data['Permissions'] = _format_permissions(entry.permissions) obj.log(EventLogRealm.management, EventLogKind.positive, 'Protection', 'ACL entry added', user, data=data) elif entry.current_data != old_data: data['Read Access'] = entry.read_access data['Manager'] = entry.full_access current_permissions = set(entry.permissions) added_permissions = current_permissions - old_data['permissions'] removed_permissions = old_data['permissions'] - current_permissions if added_permissions: data['Permissions (added)'] = _format_permissions(added_permissions) if removed_permissions: data['Permissions (removed)'] = _format_permissions(removed_permissions) if current_permissions: data['Permissions'] = _format_permissions(current_permissions) obj.log(EventLogRealm.management, EventLogKind.change, 'Protection', 'ACL entry changed', user, data=data)
def _log_acl_changes(sender, obj, principal, entry, is_new, old_data, quiet, **kwargs): if quiet: return user = session.user if session else None # allow acl changes outside request context available_permissions = get_available_permissions(Event) def _format_permissions(permissions): permissions = set(permissions) return ', '.join(sorted(orig_string(p.friendly_name) for p in available_permissions.itervalues() if p.name in permissions)) data = {} if principal.principal_type == PrincipalType.user: data['User'] = principal.full_name elif principal.principal_type == PrincipalType.email: data['Email'] = principal.email elif principal.principal_type == PrincipalType.local_group: data['Group'] = principal.name elif principal.principal_type == PrincipalType.multipass_group: data['Group'] = '{} ({})'.format(principal.name, principal.provider_title) elif principal.principal_type == PrincipalType.network: data['IP Network'] = principal.name elif principal.principal_type == PrincipalType.event_role: data['Event Role'] = principal.name if entry is None: data['Read Access'] = old_data['read_access'] data['Manager'] = old_data['full_access'] data['Permissions'] = _format_permissions(old_data['permissions']) obj.log(EventLogRealm.management, EventLogKind.negative, 'Protection', 'ACL entry removed', user, data=data) elif is_new: data['Read Access'] = entry.read_access data['Manager'] = entry.full_access if entry.permissions: data['Permissions'] = _format_permissions(entry.permissions) obj.log(EventLogRealm.management, EventLogKind.positive, 'Protection', 'ACL entry added', user, data=data) elif entry.current_data != old_data: data['Read Access'] = entry.read_access data['Manager'] = entry.full_access current_permissions = set(entry.permissions) added_permissions = current_permissions - old_data['permissions'] removed_permissions = old_data['permissions'] - current_permissions if added_permissions: data['Permissions (added)'] = _format_permissions(added_permissions) if removed_permissions: data['Permissions (removed)'] = _format_permissions(removed_permissions) if current_permissions: data['Permissions'] = _format_permissions(current_permissions) obj.log(EventLogRealm.management, EventLogKind.change, 'Protection', 'ACL entry changed', user, data=data)
def has_management_permission(cls, permission=None, explicit=False): if permission is None: if explicit: raise ValueError('permission must be specified if explicit=True') return cls.full_access valid_permissions = get_available_permissions(cls.principal_for_obj).keys() if permission == 'ANY': crit = (cls.permissions.op('&&')(db.func.cast(valid_permissions, ARRAY(db.String)))) else: assert permission in valid_permissions, \ f"invalid permission '{permission}' for object '{cls.principal_for_obj}'" crit = (cls.permissions.op('&&')(db.func.cast([permission], ARRAY(db.String)))) if explicit: return crit else: return cls.full_access | crit
def has_management_permission(cls, permission=None, explicit=False): if permission is None: if explicit: raise ValueError('permission must be specified if explicit=True') return cls.full_access valid_permissions = get_available_permissions(cls.principal_for_obj).viewkeys() if permission == 'ANY': crit = (cls.permissions.op('&&')(db.func.cast(valid_permissions, ARRAY(db.String)))) else: assert permission in valid_permissions, \ "invalid permission '{}' for object '{}'".format(permission, cls.principal_for_obj) crit = (cls.permissions.op('&&')(db.func.cast([permission], ARRAY(db.String)))) if explicit: return crit else: return cls.full_access | crit
def update_principals_permissions(obj, current, new): """Handle the updates of permissions and creations/deletions of acl principals. :param obj: The object to update. Must have ``acl_entries`` :param current: A dict mapping principals to a set with its current permissions :param new: A dict mapping principals to a set with its new permissions """ user_selectable_permissions = { v.name for k, v in get_available_permissions(obj.__class__).viewitems() if v.user_selectable } for principal, permissions in current.viewitems(): if principal not in new: permissions_kwargs = { 'full_access': False, 'read_access': False, 'del_permissions': user_selectable_permissions } obj.update_principal(principal, **permissions_kwargs) elif permissions != new[principal]: full_access, read_access, permissions = get_split_permissions( new[principal]) all_user_permissions = [ set(entry.permissions) for entry in obj.acl_entries if entry.principal == principal ][0] permissions_kwargs = { 'full_access': full_access, 'read_access': read_access, 'permissions': (all_user_permissions - user_selectable_permissions) | permissions } obj.update_principal(principal, **permissions_kwargs) new_principals = set(new) - set(current) for p in new_principals: full_access, read_access, permissions = get_split_permissions(new[p]) permissions_kwargs = { 'full_access': full_access, 'read_access': read_access, 'add_permissions': permissions & user_selectable_permissions } obj.update_principal(p, **permissions_kwargs)
def has_management_permission(self, permission=None, explicit=False): """Checks whether a principal has a certain management permission. The check always succeeds if the user is a full manager; in that case the list of permissions is ignored. :param permission: The permission to check for or 'ANY' to check for any management permission. :param explicit: Whether to check for the permission itself even if the user has full management privileges. """ if permission is None: if explicit: raise ValueError('permission must be specified if explicit=True') return self.full_access elif not explicit and self.full_access: return True valid_permissions = get_available_permissions(self.principal_for_obj).viewkeys() current_permissions = set(self.permissions) & valid_permissions if permission == 'ANY': return bool(current_permissions) assert permission in valid_permissions, "invalid permission '{}' for object '{}'".format(permission, self.principal_for_obj) return permission in current_permissions
def update_principal(self, principal, read_access=None, full_access=None, permissions=None, add_permissions=None, del_permissions=None, quiet=False): """Updates access privileges for the given principal. If the principal is not in the ACL, it will be added if necessary. If the changes remove all its privileges, it will be removed from the ACL. :param principal: A `User`, `GroupProxy` or `EmailPrincipal` instance. :param read_access: If the principal should have explicit read access to the object. This does not grant any management permissions - it simply grants access to an otherwise protected object. :param full_access: If the principal should have full management access. :param permissions: set -- The management permissions to grant. Any existing permissions will be replaced. :param add_permissions: set -- Management permissions to add. :param del_permissions: set -- Management permissions to remove. :param quiet: Whether the ACL change should happen silently. This indicates to acl change signal handlers that the change should not be logged, trigger emails or result in similar notifications. :return: The ACL entry for the given principal or ``None`` if he was removed (or not added). """ if permissions is not None and (add_permissions or del_permissions): raise ValueError('add_permissions/del_permissions and permissions are mutually exclusive') principal = _resolve_principal(principal) principal_class, entry = _get_acl_data(self, principal) new_entry = False if entry is None: if not permissions and not add_permissions and not full_access and not read_access: # not in ACL and no permissions to add return None entry = principal_class(principal=principal, read_access=False, full_access=False, permissions=[]) self.acl_entries.add(entry) new_entry = True old_data = entry.current_data # update permissions new_permissions = set(entry.permissions) if permissions is not None: new_permissions = permissions else: if add_permissions: new_permissions |= add_permissions if del_permissions: new_permissions -= del_permissions invalid_permissions = new_permissions - get_available_permissions(type(self)).viewkeys() if invalid_permissions: raise ValueError('Invalid permissions: {}'.format(', '.join(invalid_permissions))) entry.permissions = sorted(new_permissions) # update read privs if read_access is not None: entry.read_access = read_access # update full management privs if full_access is not None: entry.full_access = full_access # remove entry from acl if no privileges if not entry.read_access and not entry.full_access and not entry.permissions: self.acl_entries.remove(entry) # Flush in case the same principal is added back afterwards. # Not flushing in other cases (adding/modifying) is intentional # as this might happen on a newly created object which is not yet # flushable due to missing data db.session.flush() signals.acl.entry_changed.send(type(self), obj=self, principal=principal, entry=None, is_new=False, old_data=old_data, quiet=quiet) return None signals.acl.entry_changed.send(type(self), obj=self, principal=principal, entry=entry, is_new=new_entry, old_data=old_data, quiet=quiet) return entry
def _mock_available_permissions(mocker): # The code we are testing only cares about the keys so we don't # need to actually create permissions for now. permissions = dict(get_available_permissions(Event), foo=None, bar=None, foobar=None) mocker.patch('indico.core.db.sqlalchemy.protection.get_available_permissions', return_value=permissions) mocker.patch('indico.core.db.sqlalchemy.principals.get_available_permissions', return_value=permissions)
def update_principal(self, principal, read_access=None, full_access=None, permissions=None, add_permissions=None, del_permissions=None, quiet=False): """Update access privileges for the given principal. If the principal is not in the ACL, it will be added if necessary. If the changes remove all its privileges, it will be removed from the ACL. :param principal: A `User`, `GroupProxy` or `EmailPrincipal` instance. :param read_access: If the principal should have explicit read access to the object. This does not grant any management permissions - it simply grants access to an otherwise protected object. :param full_access: If the principal should have full management access. :param permissions: set -- The management permissions to grant. Any existing permissions will be replaced. :param add_permissions: set -- Management permissions to add. :param del_permissions: set -- Management permissions to remove. :param quiet: Whether the ACL change should happen silently. This indicates to acl change signal handlers that the change should not be logged, trigger emails or result in similar notifications. :return: The ACL entry for the given principal or ``None`` if there is no corresponding entry in the end. """ if permissions is not None and (add_permissions or del_permissions): raise ValueError( 'add_permissions/del_permissions and permissions are mutually exclusive' ) principal = _resolve_principal(principal) principal_class, entry = _get_acl_data(self, principal) new_entry = False if entry is None: if not permissions and not add_permissions and not full_access and not read_access: # not in ACL and no permissions to add return None entry = principal_class(principal=principal, read_access=False, full_access=False, permissions=[]) self.acl_entries.add(entry) new_entry = True old_data = entry.current_data # update permissions new_permissions = set(entry.permissions) if permissions is not None: new_permissions = permissions else: if add_permissions: new_permissions |= add_permissions if del_permissions: new_permissions -= del_permissions invalid_permissions = new_permissions - get_available_permissions( type(self)).keys() if invalid_permissions: raise ValueError('Invalid permissions: {}'.format( ', '.join(invalid_permissions))) entry.permissions = sorted(new_permissions) # update read privs if read_access is not None: entry.read_access = read_access # update full management privs if full_access is not None: entry.full_access = full_access # remove entry from acl if no privileges if not entry.read_access and not entry.full_access and not entry.permissions: self.acl_entries.remove(entry) # Flush in case the same principal is added back afterwards. # Not flushing in other cases (adding/modifying) is intentional # as this might happen on a newly created object which is not yet # flushable due to missing data db.session.flush() signals.acl.entry_changed.send(type(self), obj=self, principal=principal, entry=None, is_new=False, old_data=old_data, quiet=quiet) return None signals.acl.entry_changed.send(type(self), obj=self, principal=principal, entry=entry, is_new=new_entry, old_data=old_data, quiet=quiet) return entry
def can_manage(self, user, permission=None, allow_admin=True, check_parent=True, explicit_permission=False): """Check if the user can manage the object. :param user: The :class:`.User` to check. May be None if the user is not logged in. :param: permission: The management permission that is needed for the check to succeed. If not specified, full management privs are required. May be set to the string ``'ANY'`` to check if the user has any management privileges. If the user has `full_access` privileges, he's assumed to have all possible permissions. :param allow_admin: If admin users should always have access :param check_parent: If the parent object should be checked. In this case the permission is ignored; only full management access is inherited to children. :param explicit_permission: If the specified permission should be checked explicitly instead of short-circuiting the check for Indico admins or managers. When this option is set to ``True``, the values of `allow_admin` and `check_parent` are ignored. This also applies if `permission` is None in which case this argument being set to ``True`` is equivalent to `allow_admin` and `check_parent` being set to ``False``. """ if permission is not None and permission != 'ANY' and permission not in get_available_permissions( type(self)): raise ValueError( "permission '{}' is not valid for '{}' objects".format( permission, type(self).__name__)) if user is None: # An unauthorized user is never allowed to perform management operations. # Not even signals may override this since management code generally # expects session.user to be not None. return False if user.is_system: # A system user has no email and thus access checks (against groups) may fail return False # Trigger signals for protection overrides rv = values_from_signal(signals.acl.can_manage.send( type(self), obj=self, user=user, permission=permission, allow_admin=allow_admin, check_parent=check_parent, explicit_permission=explicit_permission), single_value=True) if rv: # in case of contradictory results (shouldn't happen at all) # we stay on the safe side and deny access return all(rv) # Usually admins can access everything, so no need for checks if not explicit_permission and allow_admin and type( self).is_user_admin(user): return True if any(user in entry.principal for entry in iter_acl(self.acl_entries) if entry.has_management_permission( permission, explicit=(explicit_permission and permission is not None))): return True if not check_parent or explicit_permission: return False # the parent can be either an object inheriting from this # mixin or a legacy object with an AccessController parent = self.protection_parent if parent is None: # This should be the case for the top-level object, # i.e. the root category return False elif hasattr(parent, 'can_manage'): return parent.can_manage(user, allow_admin=allow_admin) else: raise TypeError( 'protection_parent of {} is of invalid type {} ({})'.format( self, type(parent), parent))
def can_manage(self, user, permission=None, allow_admin=True, check_parent=True, explicit_permission=False): """Checks if the user can manage the object. :param user: The :class:`.User` to check. May be None if the user is not logged in. :param: permission: The management permission that is needed for the check to succeed. If not specified, full management privs are required. May be set to the string ``'ANY'`` to check if the user has any management privileges. If the user has `full_access` privileges, he's assumed to have all possible permissions. :param allow_admin: If admin users should always have access :param check_parent: If the parent object should be checked. In this case the permission is ignored; only full management access is inherited to children. :param explicit_permission: If the specified permission should be checked explicitly instead of short-circuiting the check for Indico admins or managers. When this option is set to ``True``, the values of `allow_admin` and `check_parent` are ignored. This also applies if `permission` is None in which case this argument being set to ``True`` is equivalent to `allow_admin` and `check_parent` being set to ``False``. """ if permission is not None and permission != 'ANY' and permission not in get_available_permissions(type(self)): raise ValueError("permission '{}' is not valid for '{}' objects".format(permission, type(self).__name__)) if user is None: # An unauthorized user is never allowed to perform management operations. # Not even signals may override this since management code generally # expects session.user to be not None. return False # Trigger signals for protection overrides rv = values_from_signal(signals.acl.can_manage.send(type(self), obj=self, user=user, permission=permission, allow_admin=allow_admin, check_parent=check_parent, explicit_permission=explicit_permission), single_value=True) if rv: # in case of contradictory results (shouldn't happen at all) # we stay on the safe side and deny access return all(rv) # Usually admins can access everything, so no need for checks if not explicit_permission and allow_admin and user.is_admin: return True if any(user in entry.principal for entry in iter_acl(self.acl_entries) if entry.has_management_permission(permission, explicit=(explicit_permission and permission is not None))): return True if not check_parent or explicit_permission: return False # the parent can be either an object inheriting from this # mixin or a legacy object with an AccessController parent = self.protection_parent if parent is None: # This should be the case for the top-level object, # i.e. the root category return False elif hasattr(parent, 'can_manage'): return parent.can_manage(user, allow_admin=allow_admin) else: raise TypeError('protection_parent of {} is of invalid type {} ({})'.format(self, type(parent), parent))