import functools
import itertools
from typing import TYPE_CHECKING
import six
from pyramid.security import Everyone
from ziggurat_foundations.permissions import PermissionTuple # noqa
from magpie.utils import ExtendedEnum
if TYPE_CHECKING:
# pylint: disable=W0611,unused-import
from typing import Any, Collection, Dict, List, Optional, Union
from magpie import models
from magpie.typedefs import (
AccessControlEntryType,
AnyPermissionType,
GroupPriority,
PermissionObject,
ResolvablePermissionType,
Str
)
# values employed for special cases of 'PermissionSet.reason' during permission resolution
[docs]PERMISSION_REASON_DEFAULT = "no-permission"
[docs]PERMISSION_REASON_MULTIPLE = "multiple"
[docs]PERMISSION_REASON_ADMIN = "administrator"
[docs]class Permission(ExtendedEnum):
"""
Applicable :term:`Permission` values (names) under certain :term:`Service` and :term:`Resource`.
"""
# file/dir permissions
# WPS permissions
[docs] GET_CAPABILITIES = "getcapabilities"
[docs] GET_FEATURE_INFO = "getfeatureinfo"
[docs] GET_LEGEND_GRAPHIC = "getlegendgraphic"
[docs] GET_FEATURE = "getfeature"
[docs] DESCRIBE_FEATURE_TYPE = "describefeaturetype"
[docs] DESCRIBE_PROCESS = "describeprocess"
[docs] LOCK_FEATURE = "lockfeature"
[docs] TRANSACTION = "transaction"
[docs]class PermissionType(ExtendedEnum):
"""
Applicable types of :term:`Permission` according to context.
"""
[docs] ACCESS = "access" # role based, accessible views
[docs] ALLOWED = "allowed" # available for given service / resource (under service-type)
[docs] APPLIED = "applied" # defined (user|group, service|resource, permission)
[docs] DIRECT = "direct" # applied, only for user situation
[docs] INHERITED = "inherited" # applied, combined for user+group, relative to user member
[docs] EFFECTIVE = "effective" # resolved user+group for resource hierarchy, with access and scope
[docs] OWNED = "owned" # user/group explicitly owns the permission
[docs]class Access(ExtendedEnum):
"""
Applicable access modifier of :term:`Permission` values.
"""
[docs]class Scope(ExtendedEnum):
"""
Applicable access modifier of :term:`Permission` values.
"""
[docs] RECURSIVE = "recursive"
[docs]@functools.total_ordering
class PermissionSet(object):
"""
Explicit definition of a :class:`Permission` with applicable :class:`Access` and :class:`Scope` to resolve it.
The :class:`Permission` is the *name* of the applicable permission on the :class:`magpie.models.Resource`.
The :class:`Scope` defines how the :class:`Permission` should impact the resolution of the perceived
:term:`Effective Permissions` over a :class:`magpie.models.Resource` tree hierarchy.
The :class:`Access` defines how the :class:`Permission` access should be interpreted (granted or denied).
Optionally, a :class:`PermissionType` can be provided to specifically indicate which kind of permission this set
represents. This type is only for informative purposes, and is not saved to database nor displayed by the explicit
string representation. It is returned within JSON representation and can be employed by
:term:`Effective Permissions` resolution to be more verbose about returned results.
On missing :class:`Access` or :class:`Scope` specifications, they default to :attr:`Access.ALLOW` and
:attr:`Scope.RECURSIVE` to handle backward compatible naming convention of plain ``permission_name``.
"""
[docs] __slots__ = ["_name", "_access", "_scope", "_tuple", "_type", "_reason"]
def __init__(self,
permission, # type: AnyPermissionType
access=None, # type: Optional[Union[Access, Str]]
scope=None, # type: Optional[Union[Scope, Str]]
typ=None, # type: Optional[PermissionType]
reason=None, # type: Optional[Str]
): # type: (...) -> None
"""
Initializes the permission definition, possibly using required conversion from other implementations.
:param permission: Name of the permission, or any other implementation from which the name can be inferred.
:param access: Effective behaviour of the permissions. Generally, grant or deny the specified permission.
:param scope: Scope for which the permission affects hierarchical resources. Important for effective resolution.
:param typ: Type of permission being represented. Informative only, does not impact behavior if omitted.
:param reason:
Slightly more indicative information on why the current permission-type has this value.
Value should be either explicitly provided or will be inferred if converted from input PermissionTuple.
.. seealso::
:meth:`PermissionSet._convert`
"""
tup = None
if not isinstance(permission, Permission):
perm_set = PermissionSet._convert(permission)
if isinstance(permission, PermissionTuple):
tup = permission
elif isinstance(permission, PermissionSet):
tup = permission.perm_tuple
permission = perm_set.name
access = perm_set.access if access is None else access
scope = perm_set.scope if scope is None else scope
typ = perm_set.type if perm_set.type is not None else typ
reason = perm_set.reason if perm_set.reason is not None else reason
self.name = permission
self.access = access
self.scope = scope
self.type = typ
self._tuple = tup # type: Optional[PermissionTuple] # reference to original item if available
self._reason = reason
[docs] def __eq__(self, other):
# type: (Any) -> bool
if not isinstance(other, PermissionSet):
other = PermissionSet(other)
return self.name == other.name and self.access == other.access and self.scope == other.scope
[docs] def __ne__(self, other):
# type: (Any) -> bool
return not self.__eq__(other)
[docs] def __lt__(self, other):
# type: (Any) -> bool
"""
Ascending sort of permission according to their name, access and scope modifiers.
First sort by permission name alphabetically, followed by increasing *restrictive access* and increasing
*range of scoped resources*.
Using this sorting methodology, similar permissions by name are grouped together first, and permissions of same
name with modifiers are then ordered, the first having less priority when selecting a single item to display
with conflicting possibilities. Respecting :attr:`Access.DENY` is more important than :attr:`Access.ALLOW`
(to protect the :term:`Resource`), and :attr:`Scope.MATCH` is *closer* to the actual :term:`Resource` than
:attr:`Scope.RECURSIVE` permission received from a *farther* parent in the hierarchy.
Explicitly, sorted explicit string representation becomes::
[name1]-[allow]-[match]
[name1]-[allow]-[recursive]
[name1]-[deny]-[match]
[name1]-[deny]-[recursive]
[name2]-[allow]-[match]
[name2]-[allow]-[recursive]
[name2]-[deny]-[match]
[name2]-[deny]-[recursive]
...
We then obtain two **crucial** ordering results:
1. We can easily pick the last sorted item with highest resolution priority to find the final result of
corresponding permissions.
(note: final result for same user or group, their direct/inherited resolution is not considered here).
2. Picking the first element with lowest priority also displays the permission that impacts the widest
range of resources. For instance in Magpie UI, indicating that a permission as :attr:`Scope.RECURSIVE`
is more verbose as it tell other resources under it are also receive the specified :class:`Access`
modifier rather than only the punctual resource.
.. warning::
Alphabetically sorting permissions by string representation (implicit/explicit) is not equivalent to
sorting them according to :term:`Permission` priority according to how modifiers are resolved. To obtain
the prioritized sorting as strings, a list of :class:`PermissionSet` (with the strings as input) should be
used to convert and correctly interpreted the raw strings, and then be converted back after sorting.
.. code-block:: python
# valid priority-sorted strings
[str(perm) for perm in sorted(PermissionSet(p) for p in permission_strings)]
# not equivalent to raw sorting
list(sorted(permission_strings))
"""
if not isinstance(other, PermissionSet):
other = PermissionSet(other)
if self.name != other.name:
return self.name.value < other.name.value
if self.access != other.access:
return self.access == Access.ALLOW
if self.scope != other.scope:
return self.scope == Scope.MATCH
return False
[docs] def __hash__(self):
# type: () -> int
return hash((self.name, self.access, self.scope))
[docs] def __str__(self):
# type: () -> Str
"""
Obtains the compound literal representation of the :class:`PermissionSet`.
Employed for database storage supporting ``ziggurat`` format.
"""
return "{}-{}-{}".format(self.name.value, self.access.value, self.scope.value)
[docs] def __repr__(self):
# type: () -> Str
"""
Obtains the visual representation of the :class:`PermissionSet`.
"""
perm_repr_template = "PermissionSet(name={}, access={}, scope={}, type={})"
perm_type = self.type.value if self.type is not None else None
return perm_repr_template.format(self.name.value, self.access.value, self.scope.value, perm_type)
[docs] def like(self, other):
"""
Evaluates if one permission is *similar* to another permission definition regardless of *modifiers*.
This is different than ``==`` operator which will evaluate *exactly* equal permission definitions.
"""
if not isinstance(other, PermissionSet):
other = PermissionSet(other)
return self.name == other.name
[docs] def json(self):
# type: () -> PermissionObject
"""
Obtains the JSON representation of this :class:`PermissionSet`.
"""
perm = {
"name": self.name.value,
"access": self.access.value,
"scope": self.scope.value,
"type": self.type.value if self.type is not None else None,
}
if self.reason:
perm.update({"reason": self.reason})
return perm
[docs] def ace(self, user_or_group):
# type: (Optional[Union[models.User, models.Group]]) -> AccessControlEntryType
"""
Converts the :class:`PermissionSet` into an :term:`ACE` that :mod:`pyramid` can understand.
"""
outcome = self.access.value.capitalize() # pyramid: Access/Deny
if user_or_group is None:
target = Everyone
elif self.type == PermissionType.INHERITED:
target = "group:{}".format(user_or_group.id)
else: # both DIRECT and EFFECTIVE (effective is pre-computed with inherited permissions for the user)
target = user_or_group.id
return outcome, target, self.name.value
@property
[docs] def reason(self):
# type: () -> Optional[Str]
"""
Indicative reason of the returned value defined by :meth:`type` or inferred by the :class:`PermissionTuple`.
.. seealso::
:meth:`combine`
:returns:
Single string that describes the reason (source) of the permission, or multiple strings if updated by
combination of multiple permissions.
"""
if self._reason is not None:
return self._reason
if self._tuple is None:
return None
if self._tuple.type == "user":
self._reason = "user:{}:{}".format(self._tuple.user.id, self._tuple.user.user_name)
if self._tuple.type == "group":
self._reason = "group:{}:{}".format(self._tuple.group.id, self._tuple.group.group_name)
return self._reason
@reason.setter
def reason(self, reason):
# type: (Optional[Str]) -> None
self._reason = reason
@classmethod
[docs] def resolve(cls,
permission1, # type: ResolvablePermissionType
permission2, # type: ResolvablePermissionType
context=PermissionType.INHERITED, # type: PermissionType
multiple_choice=None, # type: Optional[ResolvablePermissionType]
): # type: (...) -> ResolvablePermissionType
"""
Resolves provided permissions into a single one considering various modifiers and groups for a resource.
Permissions **MUST** have the same :term:`Permission` name.
By default (using :paramref:`same_resources`), the associated :term:`Resource` on which the two compared
permissions are applied on should also be the same (especially during local :term:`Inherited Permissions`
resolution). This safeguard must be disabled for :term:`Effective Permissions` that specifically handles
multi-level :term:`Resource` resolution.
The comparison considers both the :class:`Access` and :class:`Scope` of :term:`Inherited Permissions` of the
:term:`User`, as well as its :term:`Group` memberships sorted by their priority.
.. seealso::
- :meth:`magpie.services.ServiceInterface.effective_permissions`
- :func:`magpie.api.management.users.user_utils.combine_user_group_permissions`
- :meth:`PermissionSet.__lt__`
"""
if not isinstance(permission1, PermissionSet):
permission1 = PermissionSet(permission1)
if not isinstance(permission2, PermissionSet):
permission2 = PermissionSet(permission2)
# both permissions must contain the user or group reference from the original permission tuple to allow compare
# they must also have the same permission name to actually resolving the one to preserve
if permission1.name != permission2.name or not (permission1.perm_tuple and permission2.perm_tuple):
raise ValueError("Invalid resolution attempt between two incomparable permissions.")
# when resolving (local inherited resolution), only one user permission on same resource is possible (by design)
# hierarchical/effective resolution of resources can differ
if context == PermissionType.INHERITED and (
(permission1.perm_tuple.resource is not permission2.perm_tuple.resource) or
(permission1.type == PermissionType.DIRECT and permission2.type == PermissionType.DIRECT)):
raise ValueError("Invalid inherited resolution attempt expected same resources but contain invalid values.")
# user direct permission always have priority
if permission1.type == PermissionType.DIRECT:
return permission1
if permission2.type == PermissionType.DIRECT:
return permission2
# when only comparing groups, priority dictates the result
priority1 = permission1.group_priority
priority2 = permission2.group_priority
if priority1 > priority2:
return permission1
if priority1 < priority2:
return permission2
# same group priority are resolved according to corresponding permission names/access/scope (__lt__)
if permission1 == permission2:
# if the two different groups have the exact same resolution value,
# indicate that multiple groups resolve into the same access, unless a choice was provided
permission1.reason = multiple_choice.reason if multiple_choice else PERMISSION_REASON_MULTIPLE
return permission1 # preserved group in perm-tuple doesn't matter as they are equivalent
# otherwise return whichever group permission has higher resolution value
return permission2 if permission1 < permission2 else permission1
@property
[docs] def group_priority(self):
# type: () -> Optional[GroupPriority]
"""
Priority accessor in case of group inherited permission resolved by :class:`PermissionTuple`.
"""
if self._tuple is not None and self.type == PermissionType.INHERITED:
return self._tuple.group.priority
return None
@property
[docs] def perm_tuple(self):
# type: () -> Optional[PermissionTuple]
"""
Get the original :class:`PermissionTuple` if available (:class:`PermissionSet` must have been created by one).
"""
return self._tuple
@property
[docs] def implicit_permission(self):
# type: () -> Optional[Str]
"""
Obtain the implicit string representation of the :class:`PermissionSet` as plain :class:`Permission` name.
This representation is backward compatible with prior versions of `Magpie` where explicit representation of
permission names in the database did not exist.
If the contained modifiers of the :class:`PermissionSet` (notably the :attr:`Access.DENY`) result in a string
representation that is *not possible* according to non existing permissions for older `Magpie` instances, the
returned value will be ``None``.
.. seealso::
- :meth:`explicit_permission` for the new representation.
"""
if self.access == Access.ALLOW:
if self.scope == Scope.RECURSIVE:
return self.name.value
if self.scope == Scope.MATCH:
return "{}-{}".format(self.name.value, Scope.MATCH.value)
return None
@property
[docs] def explicit_permission(self):
# type: () -> Str
"""
Obtain the explicit string representation of the :class:`PermissionSet`.
This format is always guaranteed to be completely defined contrary to :meth:`implicit_permission`.
.. seealso::
- :meth:`__str__` (default string value).
- :meth:`implicit_permission` for the old representation.
"""
return str(self)
@property
[docs] def name(self):
# type: () -> Permission
return self._name
@name.setter
def name(self, permission):
# type: (Union[Permission, Str]) -> None
self._name = Permission.get(permission)
if self._name is None:
raise TypeError("Invalid permission: {!s}".format(permission))
[docs] permission = name # synonym to match init parameters
@property
[docs] def access(self):
# type: () -> Access
return self._access
@access.setter
def access(self, access):
# type: (Optional[Union[Access, Str]]) -> None
self._access = Access.get(access, default=Access.ALLOW)
@property
[docs] def scope(self):
# type: () -> Scope
return self._scope
@scope.setter
def scope(self, scope):
# type: (Optional[Union[Scope, Str]]) -> None
self._scope = Scope.get(scope, default=Scope.RECURSIVE)
@property
[docs] def type(self):
# type: () -> Optional[PermissionType]
if self._type is None and self._tuple is not None:
if self._tuple.type == "user":
self._type = PermissionType.DIRECT
if self._tuple.type == "group":
self._type = PermissionType.INHERITED
return self._type
@type.setter
def type(self, typ):
self._type = PermissionType.get(typ)
@classmethod
[docs] def _convert(cls, permission):
# type: (AnyPermissionType) -> Optional[PermissionSet]
"""
Converts any permission representation to the :class:`PermissionSet` with applicable enum members.
Supports older :class:`Permission` representation such that implicit conversion of permission name
without :attr:`access` and :attr:`scope` values are padded with defaults. Also, pre-defined partial or full
definition from literal string representation are parsed to generate the :class:`PermissionSet` instance.
:param permission: implicit or explicit permission name string, or any other known permission implementation
:raises ValueError: when the permission name cannot be identified or parsed
"""
if isinstance(permission, PermissionSet):
return permission
# JSON representation
if isinstance(permission, dict):
name = permission.get(
"name", permission.get("permission_name", permission.get("permission", permission.get("perm_name")))
)
perm = Permission.get(name)
if perm is None:
raise ValueError("Unknown permission name could not be identified: {}".format(name))
access = Access.get(permission.get("access"))
scope = Scope.get(permission.get("scope"))
typ = PermissionType.get(permission.get("type"))
return PermissionSet(perm, access, scope, typ)
# pyramid ACE representation
if isinstance(permission, tuple) and len(permission) == 3:
perm_type = PermissionType.INHERITED if "group" in str(permission[1]) else PermissionType.DIRECT
perm_name = permission[2]
# if permission name represents explicit definition, use it directly and drop Allow/Deny from ACE
# otherwise, use the provided access
access = None
if isinstance(perm_name, six.string_types) and len(perm_name.split("-")) != 3:
access = Access.get(permission[0].lower())
return PermissionSet(perm_name, access=access, scope=None, typ=perm_type)
# ziggurat PermissionTuple or plain string representation
name = getattr(permission, "perm_name", None) or permission
perm = Permission.get(name) # old '-match' variants are not in enum anymore, so they are not found here
perm_type = getattr(permission, "type", None) # ziggurat PermissionTuple
if perm_type == "user":
perm_type = PermissionType.DIRECT
elif perm_type == "group":
perm_type = PermissionType.INHERITED
if perm is not None:
# when matched, either plain permission-name string or Permission enum, or AllPermissionList was passed
# infer the rest of the parameters
return PermissionSet(perm, Access.ALLOW, Scope.RECURSIVE, perm_type)
# only permission-name at this point (with mandatory '-') as without it would be found by above 'Permission.get'
if not isinstance(name, six.string_types):
raise TypeError("Unknown permission object cannot be converted: {!r}".format(name))
if "-" not in name:
raise ValueError("Unknown permission name could not be parsed: {}".format(name))
# plain string representation, either implicit or explicit
perm, modifier = name.rsplit("-", 1)
scope = Scope.get(modifier)
if "-" not in perm: # either compound perm-name or perm-[access|scope] combination
if scope is None:
access = Access.get(modifier, Access.ALLOW)
scope = Scope.RECURSIVE
else:
access = Access.ALLOW
else:
name, access = perm.split("-")
access = Access.get(access)
if access is not None:
perm = name
perm = Permission.get(perm)
return PermissionSet(perm, access, scope, perm_type)
}