class PermissionSet(models.Model):
    """A permission set represents the complete set of permissions
    resulting from the composition of a sequence of policy instances.
    The sequence of policy instances is recorded using the
    ``PolicyInstance`` model and the permission tree is constructed
    lazily from this information when the permission set is read from
    the database.

    """
    # Ordered set of policies used to generate this permission set.
    policy_assign = models.ManyToManyField(Policy, through=PolicyInstance)

    # Users to which this permission set is attached: a user has only
    # one permission set, so this is really an 1:m relation, not an
    # n:m relation.
    users = models.ManyToManyField(settings.AUTH_USER_MODEL,
                                   related_name='permissionset')
    anonymous_user = models.BooleanField(default=False)

    # Custom manager to deal with folding together permission sets
    # generated from identical sequences of policies.
    objects = PermissionSetManager()

    def cache_key(self):
        return 'tutelary:ptree:' + str(self.pk)

    def tree(self):
        key = self.cache_key()
        cached = cache.get(key)
        if cached is None:
            ptree = engine.PermissionTree(
                policies=[engine.PolicyBody(json=pi.policy.body,
                                            variables=json.loads(pi.variables))
                          for pi in (PolicyInstance.objects
                                     .select_related('policy')
                                     .filter(pset=self))]
            )
            cache.set(key, ptree)
            cached = ptree
        return cached

    def refresh(self):
        cache.set(self.cache_key(), None)

    def __str__(self):
        return str(self.pk) class Policy(models.Model):
    """An individual policy has a name and a JSON policy body.  Changes to
    policies are audited.

    """

    name = models.CharField(max_length=200)
    """Policy name field."""

    body = models.TextField()
    """Policy JSON body."""

    audit_log = AuditLog()

    def __str__(self):
        return self.name

    def variable_names(self):
        pat = re.compile(r'\$(?:([_a-z][_a-z0-9]*)|{([_a-z][_a-z0-9]*)})')
        return set([m[0] for m in re.findall(pat, self.body)])

    def save(self, *args, **kwargs):
        super().save(*args, **kwargs)
        self.refresh()

    def refresh(self):
        for pset in _policy_psets([self]):
            pset.refresh() class Role(models.Model):
    """A policy role has a name, a sequence of policies and a set of
    variable assignments.  Changes to roles are audited.

    """

    name = models.CharField(max_length=200)
    """Role name field."""

    policies = models.ManyToManyField(Policy, through=RolePolicyAssign)

    variables = models.TextField()
    """JSON dump of dictionary giving variable assignments for role.

    """

    audit_log = AuditLog()

    objects = RoleManager()

    def __str__(self):
        return self.name

    def variable_names(self):
        return set().union(*[p.variable_names() for p in self.policies.all()])

    def delete(self, *args, **kwargs):
        RolePolicyAssign.objects.filter(role=self).delete()
        super().delete(*args, **kwargs) class PolicyInstance(models.Model):
    """An instance of a policy provides fixed values for any variables
    used in the policy's body.  An ordered sequence of policy
    instances defines a permission set.

    """

    policy = models.ForeignKey(Policy, on_delete=models.PROTECT)

    pset = models.ForeignKey('PermissionSet', on_delete=models.CASCADE)

    index = models.IntegerField()
    """Integer index used to order the sequence of policies composing a
    permission set.

    """

    role = models.ForeignKey(Role, null=True, on_delete=models.PROTECT)
    """The role that this policy instance is associated with, if any."""

    variables = models.TextField()
    """JSON dump of dictionary giving variable assignments for policy
    instance.

    """

    class Meta:
        ordering = ['index']

    def __str__(self):
        return "policy={} pset={} index={} role={} variables={}".format(
            self.policy.name, self.pset.pk, self.index,
            self.role.name if self.role else "NULL",
            self.variables
        ) class RolePolicyAssign(models.Model):
    """A role is defined by an ordered sequence of policies.

    """

    policy = models.ForeignKey('Policy', on_delete=models.PROTECT)

    role = models.ForeignKey('Role', on_delete=models.CASCADE)

    index = models.IntegerField()
    """Integer index used to order the sequence of policies composing a
    role.

    """

    class Meta:
        ordering = ['index']

    def __str__(self):
        return "role={} policy={}".format(self.role.name, self.policy.name) class Object(EscapeSeparated):
    """Objects are represented by slash-separated sequences of elements
    (e.g. ``Cadasta/Batangas/parcel/123``, ``H4H/PaP/party/118``) with
    wildcard elements indicated by ``*``
    (e.g. ``Cadasta/*/parcel/*``).  Slashes can be backslash-escaped,
    as can backslashes (e.g. ``Cadasta/Village-X\/Y/parcel/943``).

    """
    separator = '/' class UserFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = User class PolicyFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Policy

    @classmethod
    def set_directory(cls, dir):
        cls.directory = dir

    @classmethod
    def _adjust_kwargs(cls, **kwargs):
        body_file = os.path.join(cls.directory, kwargs.pop('file', None))
        kwargs['body'] = open(body_file).read()
        return kwargs class RoleFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Role @pytest.fixture(scope="function")
def datadir(request, tmpdir):
    """
    Fixture responsible for searching a folder with the same name of test
    module and, if available, moving all contents to a temporary directory so
    tests can use them freely.

    Adapted from http://stackoverflow.com/questions/29627341
    """
    test_dir = py.path.local(request.module.__file__).new(ext='')
    try:
        for f in test_dir.listdir():
            f.copy(tmpdir)
    except:
        pass
    return tmpdir
from tutelary.models import (
    PermissionSet, Policy, Role, PolicyInstance, RolePolicyAssign
)
from tutelary.engine import Object
from django.contrib.auth.models import User
from .factories import UserFactory, PolicyFactory, RoleFactory
from .datadir import datadir  # noqa
from .settings import DEBUG
import pytest


@pytest.fixture(scope="function")  # noqa
def setup(datadir, db):
    user1 = UserFactory.create(username='user1')
    user2 = UserFactory.create(username='user2')
    user3 = UserFactory.create(username='user3')

    PolicyFactory.set_directory(str(datadir))
    def_pol = PolicyFactory.create(name='def', file='default-policy.json')
    org_pol = PolicyFactory.create(name='org', file='org-policy-1.json')
    prj_pol = PolicyFactory.create(name='prj', file='project-policy-1.json')

    org_role = RoleFactory.create(
        name='cadasta_org', policies=[def_pol, org_pol],
        variables={'organisation': 'Cadasta'}
    )
    prj_role = RoleFactory.create(
        name='testproj_proj', policies=[def_pol, org_pol, prj_pol],
        variables={'organisation': 'Cadasta', 'project': 'TestProj'}
    )

    user1.assign_policies(def_pol)
    user2.assign_policies((org_role, {'organisation': 'Cadasta'}))
    user3.assign_policies((prj_role, {'organisation': 'Cadasta',
                                      'project': 'TestProj'}))

    return (user1, user2, user3, def_pol, org_pol, prj_pol, org_role, prj_role)


def check(nuser=None, npol=None, nrole=None, npolin=None, npset=None):
    if nuser is not None:
        assert User.objects.count() == nuser
    if npol is not None:
        assert Policy.objects.count() == npol
    if nrole is not None:
        assert Role.objects.count() == nrole
    if npolin is not None:
        assert PolicyInstance.objects.count() == npolin
    if npset is not None:
        assert PermissionSet.objects.count() == npset


def test_role_policy_update(datadir, setup):  # noqa
    u1, u2, u3, def_pol, org_pol, prj_pol, org_role, prj_role = setup

    obj1 = Object('parcel/Cadasta/TestProj/123')
    obj2 = Object('parcel/Cadasta/Proj2/114')
    obj3 = Object('parcel/SkunkWorks/SR-71/Area51')

    check(nuser=3, npol=3, nrole=2, npolin=6, npset=3)

    assert not u1.has_perm('parcel.edit', obj1)
    assert u2.has_perm('parcel.edit', obj1)
    assert u3.has_perm('parcel.edit', obj1)

    assert not u1.has_perm('parcel.view', obj2)
    assert u2.has_perm('parcel.view', obj2)
    assert u3.has_perm('parcel.view', obj2)

    assert not u1.has_perm('parcel.view', obj3)
    assert not u2.has_perm('parcel.view', obj3)
    assert not u3.has_perm('parcel.view', obj3)

    org_pol.body = datadir.join('org-policy-2.json').read()
    org_pol.save()

    check(nuser=3, npol=3, nrole=2, npolin=6, npset=3)

    assert not u1.has_perm('parcel.edit', obj1)
    assert not u2.has_perm('parcel.edit', obj1)
    assert not u3.has_perm('parcel.edit', obj1)

    assert not u1.has_perm('parcel.view', obj2)
    assert u2.has_perm('parcel.view', obj2)
    assert u3.has_perm('parcel.view', obj2)

    assert not u1.has_perm('parcel.view', obj3)
    assert not u2.has_perm('parcel.view', obj3)
    assert not u3.has_perm('parcel.view', obj3)


def debug(s):
    if DEBUG:
        print(s)
        print()
        for p in Policy.objects.all():
            print(repr(p))
        print()
        for r in Role.objects.all():
            print(repr(r))
        print()
        for rpa in RolePolicyAssign.objects.all():
            print(repr(rpa))
        print()

        for pi in PolicyInstance.objects.all():