Skip to content

Commit

Permalink
Tests on permissions
Browse files Browse the repository at this point in the history
  • Loading branch information
Etienne Jodry authored and Etienne Jodry committed May 14, 2024
1 parent 38c6d57 commit 627528a
Show file tree
Hide file tree
Showing 17 changed files with 169 additions and 144 deletions.
5 changes: 2 additions & 3 deletions src/biodm/components/controllers/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,9 @@ def validate(cls, data: bytes) -> (Any | list | dict | None):
:param data: some request body
:type data: bytes
"""
try:
try:
json_data = json.load(io.BytesIO(data))
cls.schema.many = isinstance(json_data, list)
return cls.schema.loads(json_data=data)
return cls.schema.loads(json_data=json_data, many=isinstance(json_data, list))

except ValidationError as e:
raise PayloadValidationError() from e
Expand Down
57 changes: 50 additions & 7 deletions src/biodm/components/table.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
from __future__ import annotations
from typing import TYPE_CHECKING, List
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, List, Dict
import datetime

from sqlalchemy import (
inspect, Column, Integer, text, String, TIMESTAMP, ForeignKey, UUID
)
import sqlalchemy
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import DeclarativeBase, relationship, Mapped
import sqlalchemy.orm
from sqlalchemy.orm.relationships import Relationship

if TYPE_CHECKING:
Expand All @@ -16,10 +19,34 @@


class Base(DeclarativeBase, AsyncAttrs):
"""Base class for ORM declarative Tables."""
# Enable entity - service linkage.
"""Base class for ORM declarative Tables.
:param svc: Enable entity - service linkage
:type svc: DatabaseService
:param __permissions: Stores rules for user defined permissions on hierarchical entities
:type __permissions: Dict
"""
svc: DatabaseService
# perm: Permission
__permissions: Dict = {}

def __init_subclass__(cls, **kw: Any) -> None:
"""Sets up the rules according to permissions objects set on tables."""
if hasattr(cls, "permissions"):
Base.__permissions[cls.__name__] = Base.__permissions.get(cls.__name__, (cls, []))
Base.__permissions[cls.__name__][1].append(cls.permissions)

for table in cls.permissions.propagates_to or []:
Base.__permissions[table.__name__][1].append(cls.permissions)
return super().__init_subclass__(**kw)

@classmethod
def setup_permissions(cls):
"""After tables have been added to Base, you may call this method to factor in changes.
-- Temporary for dev mode --."""
d = {}
for name, (table, permission) in Base.__permissions.items():
print(name, table, permission)
Base.__permissions = d

@declared_attr
def __tablename__(cls) -> str:
Expand Down Expand Up @@ -79,10 +106,26 @@ def user(cls) -> Mapped["User"]:
)
validated_at = Column(TIMESTAMP(timezone=True))


@dataclass
class Permission:
def __init__(self, propagates_to: List[Base]) -> None:
pass
"""Holds permissions for a given entity's attributes."""
field: Column
# Verbs.
create: bool=False
read: bool=False
update: bool=False
download: bool=False
visualize: bool=False
# Flags.
# propagates: bool=True # ?
use_same: bool=True

# TODO: check inputs
# def __init__(
# self,

# ) -> None:
# pass

""""""
"""Class that produces necessary fields to declare ressource permissions for an entity.
Expand Down
1 change: 1 addition & 0 deletions src/biodm/managers/dbmanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ async def session(self) -> AsyncGenerator[AsyncSession, None]:

async def init_db(self) -> None:
"""Drop all tables and create them."""
Base.setup_permissions()
async with self.engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
await conn.run_sync(Base.metadata.create_all)
Expand Down
11 changes: 3 additions & 8 deletions src/biodm/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,14 +137,9 @@ def __init__(self, app):

@pytest.fixture()
def client():
with TestClient(**
{
"app": app,
'backend_options': {
"use_uvloop": True
}
}
) as c:
with TestClient(app=app, backend_options={
"use_uvloop": True
}) as c:
yield c


Expand Down
89 changes: 21 additions & 68 deletions src/biodm/tests/test_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ def test_resource_schema(client):

def test_create_unary_resource(client):
""""""
response = client.post('/bs/', content=json_bytes({'name': 'test'}))
item = {'name': 'test'}
response = client.post('/bs/', content=json_bytes(item))

assert response.status_code == 201
assert "id" in response.text
Expand Down Expand Up @@ -59,11 +60,7 @@ def test_create_wrong_data(client):


def test_read_resource(client):
item = {
'x': 1,
'y': 2,
'c': {'data': '1234'},
}
item = {'x': 1, 'y': 2, 'c': {'data': '1234'},}

_ = client.post('/as/', content=json_bytes(item))
response = client.get('/cs/1')
Expand All @@ -84,16 +81,8 @@ def test_missing_resource(client):


def test_readall_resource(client):
item1 = {
'x': 1,
'y': 2,
'bs': [{'name': 'bip'},{'name': 'bap'},]
}
item2 = {
'x': 3,
'y': 4,
'bs': [{'name': 'tit'},{'name': 'tat'},]
}
item1 = {'x': 1, 'y': 2, 'bs': [{'name': 'bip'},{'name': 'bap'},]}
item2 = {'x': 3, 'y': 4, 'bs': [{'name': 'tit'},{'name': 'tat'},]}

_ = client.post('/as/', content=json_bytes([item1, item2]))
response = client.get('/bs/')
Expand All @@ -106,16 +95,8 @@ def test_readall_resource(client):


def test_filter_resource_wildcard(client):
item1 = {
'x': 1,
'y': 2,
'bs': [{'name': 'bip'},{'name': 'bap'},]
}
item2 = {
'x': 3,
'y': 4,
'bs': [{'name': 'tit'},{'name': 'tat'},]
}
item1 = {'x': 1, 'y': 2, 'bs': [{'name': 'bip'},{'name': 'bap'},]}
item2 = {'x': 3, 'y': 4, 'bs': [{'name': 'tit'},{'name': 'tat'},]}

_ = client.post('/as/', content=json_bytes([item1, item2]))
response = client.get('/bs?name=b*')
Expand All @@ -130,16 +111,8 @@ def test_filter_resource_wildcard(client):


def test_filter_resource_values(client):
item1 = {
'x': 1,
'y': 2,
'bs': [{'name': 'bip'},{'name': 'bap'},]
}
item2 = {
'x': 3,
'y': 4,
'bs': [{'name': 'tit'},{'name': 'tat'},]
}
item1 = {'x': 1, 'y': 2, 'bs': [{'name': 'bip'},{'name': 'bap'},]}
item2 = {'x': 3, 'y': 4, 'bs': [{'name': 'tit'},{'name': 'tat'},]}

_ = client.post('/as/', content=json_bytes([item1, item2]))
response = client.get('/bs?name=bip,tat')
Expand All @@ -152,16 +125,8 @@ def test_filter_resource_values(client):


def test_filter_resource_op(client):
item1 = {
'x': 1,
'y': 2,
'bs': [{'name': 'bip'},{'name': 'bap'},]
}
item2 = {
'x': 3,
'y': 4,
'bs': [{'name': 'tit'},{'name': 'tat'},]
}
item1 = {'x': 1, 'y': 2, 'bs': [{'name': 'bip'},{'name': 'bap'},]}
item2 = {'x': 3, 'y': 4, 'bs': [{'name': 'tit'},{'name': 'tat'},]}

_ = client.post('/as/', content=json_bytes([item1, item2]))
response = client.get('/as?x.lt(2)')
Expand Down Expand Up @@ -201,42 +166,30 @@ def test_filter_resource_with_fields(client):

@pytest.mark.xfail(raises=ValueError)
def test_filter_wrong_op(client):
item1 = {
'x': 1,
'y': 2,
'bs': [{'name': 'bip'},{'name': 'bap'},]
}
client.post('/as/', content=json_bytes(item1))
item = {'x': 1, 'y': 2, 'bs': [{'name': 'bip'}, {'name': 'bap'},]}

client.post('/as/', content=json_bytes(item))
client.get('/as?x.lt=2')


@pytest.mark.xfail(raises=ValueError)
def test_filter_wrong_wildcard(client):
item1 = {
'x': 1,
'y': 2,
'bs': [{'name': 'bip'},{'name': 'bap'},]
}
client.post('/as/', content=json_bytes(item1))
item = {'x': 1, 'y': 2, 'bs': [{'name': 'bip'}, {'name': 'bap'},]}

client.post('/as/', content=json_bytes(item))
client.get('/as?y=2*')


@pytest.mark.xfail(raises=ValueError)
def test_filter_op_on_string(client):
item1 = {
'x': 1,
'y': 2,
'bs': [{'name': 'bip'},{'name': 'bap'},]
}
client.post('/as/', content=json_bytes(item1))
item = {'x': 1, 'y': 2, 'bs': [{'name': 'bip'}, {'name': 'bap'},]}

client.post('/as/', content=json_bytes(item))
client.get('/bs?name.gt(2)')


def test_delete_resource(client):
item = {
'x': 1,
'y': 2,
}
item = {'x': 1, 'y': 2,}

_ = client.post('/as/', content=json_bytes(item))
response = client.delete('/as/1')
Expand Down
4 changes: 2 additions & 2 deletions src/example/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@

from biodm.api import Api

from entities.controllers import CONTROLLERS
from entities import tables, schemas
from entities import controllers
# from example import manifests
import config


def main():
app = Api(
debug=config.DEBUG,
controllers=CONTROLLERS,
controllers=controllers.CONTROLLERS,
instance={
'config': config,
'tables': tables,
Expand Down
3 changes: 2 additions & 1 deletion src/example/entities/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from .tag import TagController
from .project import ProjectController
from .file import FileController
from .dataset import DatasetController




CONTROLLERS = [TagController, FileController, DatasetController]
CONTROLLERS = [TagController, FileController, ProjectController, DatasetController]
5 changes: 5 additions & 0 deletions src/example/entities/controllers/project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from biodm.components.controllers import ResourceController


class ProjectController(ResourceController):
pass
3 changes: 2 additions & 1 deletion src/example/entities/schemas/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .project import ProjectSchema
from .tag import TagSchema
from .dataset import DatasetSchema
from .file import FileSchema
from .file import FileSchema
35 changes: 13 additions & 22 deletions src/example/entities/schemas/dataset.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,8 @@
from marshmallow import Schema, validate, pre_load, ValidationError
from marshmallow.fields import String, Date, List, Nested, Integer, UUID

from biodm.tables import User, Group, ListGroup
from biodm.schemas import UserSchema, GroupSchema, ListGroupSchema
from entities.tables import Dataset
# from controllers import schemas
# from .group import GroupSchema
# from .user import UserSchema
# from .tag import TagSchema


class DatasetSchema(Schema):
class Meta:
model = Dataset

id = Integer()
version = Integer()
name = String(required=True)
Expand All @@ -30,9 +19,11 @@ class Meta:
# [u.id for u in User]
# )
)
id_project = Integer()

owner_group = Nested('GroupSchema') # , only=('name', 'n_members',)
contact = Nested('UserSchema', only=('username', ))
# owner_group = Nested('GroupSchema')
contact = Nested('UserSchema')
project = Nested('ProjectSchema')
tags = List(Nested('TagSchema'))

id_ls_download = Integer()
Expand All @@ -52,13 +43,13 @@ def pre_load_process(self, data, many, **kwargs):
elif not id_uc:
raise ValidationError("Need one of username_user_contact or contact fields.")

name_group = data.get('name_owner_group')
group_name = data.get('owner_group', {}).get('name')
if name_group and group_name:
assert(name_group == group_name)
elif group_name and not name_group:
ret["name_owner_group"] = group_name
elif not name_group:
raise ValidationError("Need one of name_owner_group or group fields.")
# name_group = data.get('name_owner_group')
# group_name = data.get('owner_group', {}).get('name')
# if name_group and group_name:
# assert(name_group == group_name)
# elif group_name and not name_group:
# ret["name_owner_group"] = group_name
# elif not name_group:
# raise ValidationError("Need one of name_owner_group or group fields.")

return ret
# return ret
8 changes: 1 addition & 7 deletions src/example/entities/schemas/file.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,12 @@
from marshmallow import Schema, validate
from marshmallow.fields import String, List, Nested, Integer, Bool

from entities.tables import File
from .dataset import DatasetSchema


class FileSchema(Schema):
class Meta:
model = File

id = Integer()
filename = String(required=True)
url = String(required=False)
ready = Bool(required=False)
id_dataset = Integer(required=True)
version_dataset = Integer(required=True)
dataset = Nested(DatasetSchema, required=True, load_only=True)
dataset = Nested('DatasetSchema', required=True, load_only=True)
Loading

0 comments on commit 627528a

Please sign in to comment.