Skip to content

Commit

Permalink
first version
Browse files Browse the repository at this point in the history
  • Loading branch information
Ronald Krist committed Jul 4, 2024
1 parent 9c2a897 commit bf04d17
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 59 deletions.
14 changes: 14 additions & 0 deletions oarepo_workflows/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,22 @@ class OARepoWorkflows(object):

def __init__(self, app=None):
if app:
self.init_config(app)
self.init_app(app)

def init_config(self, app):
"""Initialize configuration."""
from . import ext_config

if "OAREPO_PERMISSIONS_PRESETS" not in app.config:
app.config["OAREPO_PERMISSIONS_PRESETS"] = {}

for k in ext_config.OAREPO_PERMISSIONS_PRESETS:
if k not in app.config["OAREPO_PERMISSIONS_PRESETS"]:
app.config["OAREPO_PERMISSIONS_PRESETS"][k] = (
ext_config.OAREPO_PERMISSIONS_PRESETS[k]
)

@cached_property
def state_changed_notifiers(self):
group_name = "oarepo_workflows.state_changed_notifiers"
Expand Down
5 changes: 5 additions & 0 deletions oarepo_workflows/ext_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from oarepo_workflows.permissions.policy import WorkflowPermissionPolicy

OAREPO_PERMISSIONS_PRESETS = {
"workflow": WorkflowPermissionPolicy,
}
82 changes: 69 additions & 13 deletions oarepo_workflows/permissions/generators.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,42 @@
import operator
from functools import reduce
from itertools import chain

from invenio_records.dictutils import dict_lookup
from invenio_records_permissions.generators import ConditionalGenerator, Generator
from invenio_search.engine import dsl

from oarepo_workflows.proxies import current_oarepo_workflows


def needs_from_generators(generators, *args, **kwargs):
def needs_from_generators(generators, **kwargs):
if not generators:
return []
needs = [
g.needs(
*args,
**kwargs,
)
for g in generators
]
return set(chain.from_iterable(needs))


def reference_receiver_from_generators(generators, **kwargs):
if not generators:
return None
for generator in generators:
if hasattr(generator, 'reference_receiver'):
ref = generator.reference_receiver(**kwargs)
if ref:
return ref


def query_filters_from_generators(generators, **kwargs):
queries = [g.query_filter(**kwargs) for g in generators]
queries = [q for q in queries if q]
return reduce(operator.or_, queries) if queries else None


def _needs_from_workflow(workflow_id, action, record, **kwargs):
try:
generators = dict_lookup(
Expand All @@ -27,23 +47,50 @@ def _needs_from_workflow(workflow_id, action, record, **kwargs):
return needs_from_generators(generators, record, **kwargs)


def get_workflow_from_record(record, **kwargs):
if hasattr(record, "parent"):
record = record.parent
if hasattr(record, "workflow") and record.workflow:
return record.workflow
else:
if "record" not in kwargs:
return current_oarepo_workflows.get_default_workflow(
record=record, **kwargs
)
else:
return current_oarepo_workflows.get_default_workflow(**kwargs)


def get_permission_class_from_workflow(record=None, action_name=None, **kwargs):
if record:
workflow_id = get_workflow_from_record(record)
else:
workflow_id = current_oarepo_workflows.get_default_workflow(**kwargs)

policy = dict_lookup(
current_oarepo_workflows.record_workflows, f"{workflow_id}.permissions"
)
return policy(action_name, **kwargs)


class WorkflowPermission(Generator):
def __init__(self, action):
super().__init__()
self._action = action

def needs(self, record=None, **kwargs):
if not record: # invenio requests service does not have a way to input these
return []
workflow_id = getattr(record.parent, "workflow", None)
if not workflow_id:
return []
return _needs_from_workflow(
workflow_id,
self._action,
record,
**kwargs,
def _get_generators(self, record, **kwargs):
permission_class = get_permission_class_from_workflow(
record, action_name=self._action, **kwargs
)
return getattr(permission_class, self._action, None)

def needs(self, record=None, **kwargs):
generators = self._get_generators(record, **kwargs)
return needs_from_generators(generators, record=record, **kwargs)

def query_filter(self, record=None, **kwargs):
generators = self._get_generators(record, **kwargs)
return query_filters_from_generators(generators, record=record, **kwargs)


class IfInState(ConditionalGenerator):
Expand All @@ -57,3 +104,12 @@ def _condition(self, record, **kwargs):
except AttributeError:
return False
return state == self.state

def query_filter(self, **kwargs):
"""Filters for queries."""
field = "state"

q_instate = dsl.Q("match", **{field: self.state})
then_query = self._make_query(self.then_, **kwargs)

return q_instate & then_query
85 changes: 45 additions & 40 deletions oarepo_workflows/permissions/policy.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,33 @@
from invenio_records.dictutils import dict_lookup

from invenio_records_permissions import RecordPermissionPolicy
from invenio_records_permissions.generators import AuthenticatedUser, SystemProcess
from oarepo_runtime.services.generators import RecordOwners

from oarepo_workflows.proxies import current_oarepo_workflows
import copy
from .generators import IfInState

from oarepo_workflows.permissions.generators import WorkflowPermission

# todo this must be used as permission_policy_cls in model's service config and for now is not compatible with permissions presets - the mixin must be deleted
def workflow_permission_set_getter(service, action_name=None, **kwargs):
if "record" in kwargs: # todo should the input to get_default_workflow be always parent? it should be unified somewhere
kwargs_copy = copy.deepcopy(kwargs)
record = kwargs_copy.pop("record")
parent = record.parent
if "workflow" in parent:
workflow_id = parent["workflow"]
else:
workflow_id = current_oarepo_workflows.get_default_workflow(record=parent, **kwargs_copy)
else:
# todo hook in communities to get default for community
workflow_id = current_oarepo_workflows.get_default_workflow(**kwargs)
try:
policy = dict_lookup(
current_oarepo_workflows.record_workflows, f"{workflow_id}.permissions"
)
except:
#todo dev debug
print()
return policy(action_name, **kwargs)
from .generators import IfInState


# todo this is just for testing purposes now
class WorkflowPermissionPolicy(RecordPermissionPolicy):
class DefaultWorkflowPermissionPolicy(RecordPermissionPolicy):

PERMISSIONS_REMAP = {
"read_draft": "read",
"update_draft": "update",
"delete_draft": "delete",
"draft_create_files": "create_files",
"draft_set_content_files": "set_content_files",
"draft_get_content_files": "get_content_files",
"draft_commit_files": "commit_files",
"draft_read_files": "read_files",
"draft_update_files": "update_files",
"search_drafts": "search"
"can_read_draft": "can_read",
"can_update_draft": "can_update",
"can_delete_draft": "can_delete",
"can_draft_create_files": "can_create_files",
"can_draft_set_content_files": "can_set_content_files",
"can_draft_get_content_files": "can_get_content_files",
"can_draft_commit_files": "can_commit_files",
"can_draft_read_files": "can_read_files",
"can_draft_update_files": "can_update_files",
"can_search_drafts": "can_search",
}

def __init__(self, action_name=None, **over):
action_name = WorkflowPermissionPolicy.PERMISSIONS_REMAP.get(action_name, action_name)
can = getattr(self, f"can_{action_name}")
action_name = DefaultWorkflowPermissionPolicy.PERMISSIONS_REMAP.get(
action_name, action_name
)
can = getattr(self, action_name)
can.append(SystemProcess())
super().__init__(action_name, **over)

Expand All @@ -66,3 +44,30 @@ def __init__(self, action_name=None, **over):
]
can_create = [AuthenticatedUser()]
can_publish = [AuthenticatedUser()]


class WorkflowPermissionPolicy(RecordPermissionPolicy):

can_create = [WorkflowPermission("can_create")]
can_publish = [WorkflowPermission("can_publish")]
can_search = [WorkflowPermission("can_search")]
can_read = [WorkflowPermission("can_read")]
can_update = [WorkflowPermission("can_update")]
can_delete = [WorkflowPermission("can_delete")]
can_create_files = [WorkflowPermission("can_create_files")]
can_set_content_files = [WorkflowPermission("can_set_content_files")]
can_get_content_files = [WorkflowPermission("can_get_content_files")]
can_commit_files = [WorkflowPermission("can_commit_files")]
can_read_files = [WorkflowPermission("can_read_files")]
can_update_files = [WorkflowPermission("can_update_files")]

can_search_drafts = [WorkflowPermission("can_search")]
can_read_draft = [WorkflowPermission("can_read")]
can_update_draft = [WorkflowPermission("can_update")]
can_delete_draft = [WorkflowPermission("can_delete")]
can_draft_create_files = [WorkflowPermission("can_create_files")]
can_draft_set_content_files = [WorkflowPermission("can_set_content_files")]
can_draft_get_content_files = [WorkflowPermission("can_get_content_files")]
can_draft_commit_files = [WorkflowPermission("can_commit_files")]
can_draft_read_files = [WorkflowPermission("can_read_files")]
can_draft_update_files = [WorkflowPermission("can_update_files")]
4 changes: 1 addition & 3 deletions oarepo_workflows/records/models.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
from invenio_communities.communities.records.models import CommunityMetadata
from invenio_db import db
from sqlalchemy import String
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy_utils.types import UUIDType




class RecordWorkflowModelMixin:
__record_model__ = None

@declared_attr
def record_id(cls):
return db.Column(
Expand Down
4 changes: 3 additions & 1 deletion oarepo_workflows/records/systemfields/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ def pre_commit(self, record):
super().pre_commit(record)
saved_workflow = self._get_workflow_from_parent_db(record)
if not saved_workflow:
default = self._get_workflow(record) # todo or use the intialized one from pre_create or ?
default = self._get_workflow(
record
)
if default:
new = self._record_workflow_model(
workflow=default, record_id=str(record.id)
Expand Down
26 changes: 24 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,36 @@
from invenio_accounts.testutils import login_user_via_session
from invenio_app.factory import create_api
from invenio_i18n import lazy_gettext as _
from invenio_records_permissions.generators import AuthenticatedUser, SystemProcess
from invenio_users_resources.records import UserAggregate
from oarepo_runtime.services.generators import RecordOwners

from oarepo_workflows.permissions.generators import IfInState
from oarepo_workflows.permissions.policy import DefaultWorkflowPermissionPolicy

# tests should not depend on specified default configuration


class TestWorkflowPermissionPolicy(DefaultWorkflowPermissionPolicy):
can_search = [AuthenticatedUser()]
can_read = [
IfInState("draft", [RecordOwners()]),
IfInState("published", [AuthenticatedUser()]),
]
can_update = [IfInState("draft", RecordOwners())]
can_delete = [
IfInState("draft", RecordOwners()),
# published record can not be deleted directly by anyone else than system
SystemProcess(),
]
can_create = [AuthenticatedUser()]
can_publish = [AuthenticatedUser()]

from oarepo_workflows.permissions.policy import WorkflowPermissionPolicy

WORKFLOWS = {
"default": {
"label": _("Default workflow"),
"permissions": WorkflowPermissionPolicy,
"permissions": DefaultWorkflowPermissionPolicy,
"requests": {},
}
}
Expand Down
22 changes: 22 additions & 0 deletions tests/test_workflow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
from flask_security import logout_user

from thesis.resources.records.config import ThesisResourceConfig
from thesis.thesis.records.api import ThesisRecord


def test_workflow_read(users, logged_client, search_clear):
Expand Down Expand Up @@ -45,6 +48,25 @@ def test_workflow_publish(users, logged_client, search_clear):
assert other_response.status_code == 200


def test_query_filter(users, client, logged_client, search_clear):
# todo complete; turns out this is a bit more complicated. needs to make muy own test generators
user_client1 = logged_client(users[0])

create_response = user_client1.post(ThesisResourceConfig.url_prefix, json={})
draft_json = create_response.json
user_client1.post(
f"{ThesisResourceConfig.url_prefix}{draft_json['id']}/draft/actions/publish"
)
ThesisRecord.index.refresh()

owner_response = user_client1.get(ThesisResourceConfig.url_prefix).json

logout_user()
anon_response = client.get(ThesisResourceConfig.url_prefix).json

print()


def test_state_change(users, record_service, state_change_function, search_clear):
record = record_service.create(users[0].identity, {})._record
state_change_function(users[0].identity, record, "approving")
Expand Down
3 changes: 3 additions & 0 deletions tests/thesis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ record:
module:
qualified: thesis

permissions:
presets: [ 'workflow' ]

record:
fields:
state: "{{oarepo_workflows.records.systemfields.state.RecordStateField}}(initial='published')"
Expand Down

0 comments on commit bf04d17

Please sign in to comment.