Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Model Permissions Tests #80

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
factory-boy==3.2.0
1 change: 1 addition & 0 deletions template_app/app_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import tornado.web

from baselayer.app import models, model_util
from . import models as blt_models

from .handlers.example_computation import ExampleComputationHandler
from .handlers.push_notification import PushNotificationHandler
Expand Down
84 changes: 84 additions & 0 deletions template_app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import sqlalchemy as sa
from sqlalchemy.orm import relationship

from baselayer.app.models import (
AccessibleIfRelatedRowsAreAccessible,
AccessibleIfUserMatches,
public,
restricted,
Base,
User,
Token
)

class PublicModel(Base):
update = delete = public


class RestrictedModel(Base):
create = read = restricted


class UserAccessibleModel(Base):
create = read = update = delete = AccessibleIfUserMatches('user')

user_id = sa.Column(
sa.Integer, sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True
)
user = relationship('User')


class RelatedModel(Base):
create = read = update = delete = AccessibleIfRelatedRowsAreAccessible(user="delete")

user_id = sa.Column(
sa.Integer, sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True
)
user = relationship('User')


class CompositeAndModel(Base):
create = read = update = delete = (
AccessibleIfUserMatches('user') &
AccessibleIfUserMatches('last_modified_by')
)

user_id = sa.Column(
sa.Integer, sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True
)
user = relationship('User', foreign_keys=[user_id])

last_modified_by_id = sa.Column(
sa.Integer, sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True
)
last_modified_by = relationship('User', foreign_keys=[last_modified_by_id])


class CompositeOrModel(Base):
create = read = update = delete = (
AccessibleIfUserMatches('user') |
AccessibleIfUserMatches('last_modified_by')
)

user_id = sa.Column(
sa.Integer, sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True
)
user = relationship('User', foreign_keys=[user_id])

last_modified_by_id = sa.Column(
sa.Integer, sa.ForeignKey('users.id', ondelete='CASCADE'),
nullable=False,
index=True
)
last_modified_by = relationship('User', foreign_keys=[last_modified_by_id])

58 changes: 58 additions & 0 deletions template_app/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,66 @@
from baselayer.app.test_util import (driver, MyCustomWebDriver, reset_state,
set_server_url)

from baselayer.app import models, model_util
from baselayer.app.models import User, ACL, DBSession
from baselayer.app.env import load_env

from .fixtures import (
PublicModelFactory,
RestrictedModelFactory,
CompositeAndModelFactory,
CompositeOrModelFactory,
UserAccessibleModelFactory,
RelatedModelFactory,
UserFactory
)


print('Loading test configuration from test_config.yaml')
basedir = pathlib.Path(os.path.dirname(__file__))/'../..'
cfg = load_config([basedir/'test_config.yaml'])
set_server_url(f'http://localhost:{cfg["ports.app"]}')
models.init_db(**cfg['database'])

acl = ACL.create_or_get('System admin')
DBSession().add(acl)
DBSession().commit()

@pytest.fixture()
def super_admin_user():
return UserFactory(acls=[acl])


@pytest.fixture()
def user():
return UserFactory()


@pytest.fixture()
def public_model():
return PublicModelFactory()


@pytest.fixture()
def restricted_model():
return RestrictedModelFactory()


@pytest.fixture()
def related_model(user):
return RelatedModelFactory(user=user)


@pytest.fixture()
def user_accessible_model(super_admin_user):
return UserAccessibleModelFactory(user=super_admin_user)


@pytest.fixture()
def composite_and_model(super_admin_user, user):
return CompositeAndModelFactory(last_modified_by=super_admin_user, user=user)


@pytest.fixture()
def composite_or_model(super_admin_user, user):
return CompositeOrModelFactory(last_modified_by=super_admin_user, user=user)
80 changes: 80 additions & 0 deletions template_app/tests/fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from template_app.models import (
PublicModel,
RestrictedModel,
CompositeAndModel,
CompositeOrModel,
UserAccessibleModel,
RelatedModel,
User
)

from baselayer.app.models import DBSession

import factory
import uuid


class BaseMeta:
sqlalchemy_session = DBSession()
sqlalchemy_session_persistence = 'commit'


class UserFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseMeta):
model = User

username = factory.LazyFunction(lambda: str(uuid.uuid4()))
contact_email = factory.LazyFunction(lambda: f'{uuid.uuid4().hex[:10]}@gmail.com')
first_name = factory.LazyFunction(lambda: f'{uuid.uuid4().hex[:4]}')
last_name = factory.LazyFunction(lambda: f'{uuid.uuid4().hex[:4]}')

@factory.post_generation
def acls(obj, create, extracted, **kwargs):
if not create:
return

if extracted:
for acl in extracted:
obj.acls.append(acl)
DBSession().add(obj)
DBSession().commit()


class PublicModelFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseMeta):
model = PublicModel


class RestrictedModelFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseMeta):
model = RestrictedModel


class CompositeAndModelFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseMeta):
model = CompositeAndModel

user = factory.SubFactory(UserFactory)
last_modified_by = factory.SubFactory(UserFactory)


class CompositeOrModelFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseMeta):
model = CompositeOrModel

user = factory.SubFactory(UserFactory)
last_modified_by = factory.SubFactory(UserFactory)


class UserAccessibleModelFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseMeta):
model = UserAccessibleModel

user = factory.SubFactory(UserFactory)


class RelatedModelFactory(factory.alchemy.SQLAlchemyModelFactory):
class Meta(BaseMeta):
model = RelatedModel

user = factory.SubFactory(UserFactory)
48 changes: 48 additions & 0 deletions template_app/tests/models/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
def test_user_can_read_public_model(user, public_model):
assert public_model.is_accessible_by(user)


def test_super_user_can_read_public_model(super_admin_user, public_model):
assert public_model.is_accessible_by(super_admin_user)


def test_user_can_read_restricted_model(user, restricted_model):
assert not restricted_model.is_accessible_by(user) # need System admin


def test_super_user_can_read_restricted_model(super_admin_user, restricted_model):
assert restricted_model.is_accessible_by(super_admin_user)


def test_user_can_read_related_model(user, related_model):
# user must be able to delete related record
assert not related_model.is_accessible_by(user)


def test_super_user_can_read_related_model(super_admin_user, related_model):
assert related_model.is_accessible_by(super_admin_user)


def test_user_can_read_user_accessible_model(user, user_accessible_model):
assert not user_accessible_model.is_accessible_by(user)


def test_super_admin_user_can_read_user_accessible_model(super_admin_user, user_accessible_model):
assert user_accessible_model.is_accessible_by(super_admin_user)


def test_user_can_read_composite_and_model(user, composite_and_model):
# last_modified_by must be the same as user, but is super_admin_user
assert not composite_and_model.is_accessible_by(user)


def test_super_admin_user_can_read_composite_and_model(super_admin_user, composite_and_model):
assert composite_and_model.is_accessible_by(super_admin_user)


def test_user_can_read_composite_or_model(user, composite_or_model):
assert composite_or_model.is_accessible_by(user)


def test_super_admin_user_can_read_composite_or_model(super_admin_user, composite_or_model):
assert composite_or_model.is_accessible_by(super_admin_user)