Skip to content

Allow the possibility of an "Always deny" Permission to facilitate "safe by default" designs. #92

Description

@sean102

In my Flask application, I use class based Views. All views are derived from a base class.
The base class does not have a registered url rule and never receives requests directly.
My desire was to put an "Always Deny" permission on this base class to force any derived view
to explicitly declare permissions, thus creating a "safe by default" pattern.

I was surprised to find that Flask-Principal does not seem to have any (convenient) way of creating such a Permission.
The fundamental problem is that a Permission() without any Needs defaults to 'allow'
This is a little unintuitive given "A Permission is a set of requirements, any of which should be present for access to a resource."
But the chosen design is defensible, so probably an extra note in the documentation is sufficient to specify this corner case.

What is more problematic is that a Denial() without any Needs ALSO defaults to 'allow'. So we have a situation where
Permission() means 'allow' and Denial() also means 'allow'.

PROPOSAL

Part 1) Allow the default disposition (i.e. if permission.perms is an empty set) to be specified.

e.g.

class Permission(object):

    def __init__(self, *needs, default_permit=True):
        self.perms = {n: True for n in needs}
        self.default = default_permit

    def allows(self, identity):
        if self.needs and not self.needs.intersection(identity.provides):
            return False

        if self.excludes and self.excludes.intersection(identity.provides):
            return False

        if self.perms:
            return True

        return self.default

Part 2) Denial() should default to 'deny'.
This is more a restoration of sanity rather than a necessity. The change of Part 1, makes this less necessary.
Alternately, This change would make the change in Part 1 unnecessary as Denial() would be the missing "Always deny" permission.

e.g.

class Denial(Permission):

    def __init__(self, *needs, default_permit=False):  # default_permit=True would be backward compatible

This is a potentially breaking change. However.
The documentation does not say what will happen in this case.
There are no instances of Denial() in the unit tests (There ARE instances of Permission() in the unit tests)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions