Ejemplo n.º 1
0
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('indico.core.db.sqlalchemy.protection.get_available_roles',
                 return_value=roles)
    mocker.patch('indico.core.db.sqlalchemy.principals.get_available_roles',
                 return_value=roles)
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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)
Ejemplo n.º 4
0
 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
Ejemplo n.º 5
0
 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
Ejemplo n.º 6
0
    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
Ejemplo n.º 7
0
    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
Ejemplo n.º 8
0
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('indico.core.db.sqlalchemy.protection.get_available_roles', return_value=roles)
    mocker.patch('indico.core.db.sqlalchemy.principals.get_available_roles', return_value=roles)
Ejemplo n.º 9
0
    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
Ejemplo n.º 10
0
    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 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 `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.
            # XXX: Legacy modification keys are checked outside
            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)
        elif hasattr(parent, 'canUserModify'):
            return parent.canUserModify(user.as_avatar)
        else:
            raise TypeError(
                'protection_parent of {} is of invalid type {} ({})'.format(
                    self, type(parent), parent))
Ejemplo n.º 11
0
    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
Ejemplo n.º 12
0
    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 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 `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.
            # XXX: Legacy modification keys are checked outside
            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)
        elif hasattr(parent, 'canUserModify'):
            return parent.canUserModify(user.as_avatar)
        else:
            raise TypeError('protection_parent of {} is of invalid type {} ({})'.format(self, type(parent), parent))
Ejemplo n.º 13
0
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)
    if entry is None:
        data["Manager"] = old_data["full_access"]
        data["Roles"] = _format_roles(old_data["roles"])
        # TODO: add item for read_access once we store it in the ACL
        obj.log(
            EventLogRealm.management, EventLogKind.negative, "Protection", "ACL entry removed", session.user, data=data
        )
    else:
        if is_new:
            # TODO: add item for read_access once we store it in the ACL
            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,
            )
        else:
            # TODO: add item for read_access once we store it in the ACL
            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,
            )