def _mock_available_roles(mocker): # The code we are testing only cares about the keys so we don't # need to actually create roles for now. roles = dict(get_available_roles(Event), foo=None, bar=None, foobar=None) mocker.patch('fossir.core.db.sqlalchemy.protection.get_available_roles', return_value=roles) mocker.patch('fossir.core.db.sqlalchemy.principals.get_available_roles', return_value=roles)
def has_management_role(cls, role=None, explicit=False): if role is None: if explicit: raise ValueError('role must be specified if explicit=True') return cls.full_access valid_roles = get_available_roles(cls.principal_for_obj).viewkeys() if role == 'ANY': crit = (cls.roles.op('&&')(db.func.cast(valid_roles, ARRAY(db.String)))) else: assert role in valid_roles, "invalid role '{}' for object '{}'".format( role, cls.principal_for_obj) crit = (cls.roles.op('&&')(db.func.cast([role], ARRAY(db.String)))) if explicit: return crit else: return cls.full_access | crit
def has_management_role(self, role=None, explicit=False): """Checks whether a principal has a certain management role. The check always succeeds if the user is a full manager; in that case the list of roles is ignored. :param role: The role to check for or 'ANY' to check for any management role. :param explicit: Whether to check for the role itself even if the user has full management privileges. """ if role is None: if explicit: raise ValueError('role must be specified if explicit=True') return self.full_access elif not explicit and self.full_access: return True valid_roles = get_available_roles(self.principal_for_obj).viewkeys() current_roles = set(self.roles) & valid_roles if role == 'ANY': return bool(current_roles) assert role in valid_roles, "invalid role '{}' for object '{}'".format( role, self.principal_for_obj) return role in current_roles
def _log_acl_changes(sender, obj, principal, entry, is_new, old_data, quiet, **kwargs): if quiet: return available_roles = get_available_roles(Event) def _format_roles(roles): roles = set(roles) return ', '.join( sorted( orig_string(role.friendly_name) for role in available_roles.itervalues() if role.name in roles)) 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 if entry is None: data['Read Access'] = old_data['read_access'] data['Manager'] = old_data['full_access'] data['Roles'] = _format_roles(old_data['roles']) obj.log(EventLogRealm.management, EventLogKind.negative, 'Protection', 'ACL entry removed', session.user, data=data) elif is_new: data['Read Access'] = entry.read_access data['Manager'] = entry.full_access if entry.roles: data['Roles'] = _format_roles(entry.roles) obj.log(EventLogRealm.management, EventLogKind.positive, 'Protection', 'ACL entry added', session.user, data=data) elif entry.current_data != old_data: data['Read Access'] = entry.read_access data['Manager'] = entry.full_access current_roles = set(entry.roles) added_roles = current_roles - old_data['roles'] removed_roles = old_data['roles'] - current_roles if added_roles: data['Roles (added)'] = _format_roles(added_roles) if removed_roles: data['Roles (removed)'] = _format_roles(removed_roles) if current_roles: data['Roles'] = _format_roles(current_roles) obj.log(EventLogRealm.management, EventLogKind.change, 'Protection', 'ACL entry changed', session.user, data=data)
def update_principal(self, principal, read_access=None, full_access=None, roles=None, add_roles=None, del_roles=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 roles: set -- The management roles to grant. Any existing roles will be replaced. :param add_roles: set -- Management roles to add. :param del_roles: set -- Management roles 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 roles is not None and (add_roles or del_roles): raise ValueError('add_roles/del_roles and roles are mutually exclusive') principal = _resolve_principal(principal) principal_class, entry = _get_acl_data(self, principal) new_entry = False if entry is None: if not roles and not add_roles 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, roles=[]) self.acl_entries.add(entry) new_entry = True old_data = entry.current_data # update roles new_roles = set(entry.roles) if roles is not None: new_roles = roles else: if add_roles: new_roles |= add_roles if del_roles: new_roles -= del_roles invalid_roles = new_roles - get_available_roles(type(self)).viewkeys() if invalid_roles: raise ValueError('Invalid roles: {}'.format(', '.join(invalid_roles))) entry.roles = sorted(new_roles) # 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.roles: 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, role=None, allow_admin=True, check_parent=True, explicit_role=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: role: The management role 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 roles. :param allow_admin: If admin users should always have access :param check_parent: If the parent object should be checked. In this case the role is ignored; only full management access is inherited to children. :param explicit_role: If the specified role should be checked explicitly instead of short-circuiting the check for fossir admins or managers. When this option is set to ``True``, the values of `allow_admin` and `check_parent` are ignored. This also applies if `role` is None in which case this argument being set to ``True`` is equivalent to `allow_admin` and `check_parent` being set to ``False``. """ if role is not None and role != 'ANY' and role not in get_available_roles(type(self)): raise ValueError("role '{}' is not valid for '{}' objects".format(role, 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, role=role, allow_admin=allow_admin, check_parent=check_parent, explicit_role=explicit_role), 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_role 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_role(role, explicit=(explicit_role and role is not None))): return True if not check_parent or explicit_role: 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))