diff --git a/ckanext/iaea/logic/__init__.py b/ckanext/iaea/logic/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ckanext/iaea/logic/action.py b/ckanext/iaea/logic/action.py new file mode 100644 index 0000000..e96d6e2 --- /dev/null +++ b/ckanext/iaea/logic/action.py @@ -0,0 +1,45 @@ +import logging +import ckan.lib.datapreview as datapreview +import ckan.logic as l +import ckan.plugins as p +from ckan.logic.schema import default_create_resource_view_schema, default_update_resource_view_schema + +from ckanext.iaea.helpers import get_main_organization + +log = logging.getLogger(__name__) + +ignore_missing = p.toolkit.get_validator('ignore_missing') + + +@p.toolkit.chained_action +def resource_view_create(up_func, context, data_dict): + view_plugin = datapreview.get_view_plugin(data_dict['view_type']) + if not view_plugin: + raise l.ValidationError( + {"view_type": "No plugin found for view_type {view_type}".format( + view_type=data_dict['view_type'] + )} + ) + schema = default_create_resource_view_schema(view_plugin) + schema.update({ + 'suggested_filter_fields': [ignore_missing] + }) + context['schema'] = schema + result = up_func(context, data_dict) + return result + + +@p.toolkit.chained_action +def resource_view_update(up_func, context, data_dict): + model = context['model'] + resource_view = model.ResourceView.get(data_dict['id']) + if not resource_view: + raise l.NotFound + view_plugin = datapreview.get_view_plugin(resource_view.view_type) + schema = default_update_resource_view_schema(view_plugin) + schema.update({ + 'suggested_filter_fields': [ignore_missing] + }) + context['schema'] = schema + result = up_func(context, data_dict) + return result diff --git a/ckanext/iaea/logic/auth.py b/ckanext/iaea/logic/auth.py new file mode 100644 index 0000000..9e42527 --- /dev/null +++ b/ckanext/iaea/logic/auth.py @@ -0,0 +1,61 @@ +from sqlalchemy.sql import exists +from sqlalchemy import or_ + +import ckan.model as model +import ckan.plugins as p +from ckan.common import config, g, _ + + +@p.toolkit.chained_auth_function +def package_create(next_auth, context, data_dict): + return _check_dataset_write_actions_restricted(next_auth, context, data_dict) + + +def _check_dataset_write_actions_restricted(next_auth, context, data_dict): + res = next_auth(context, data_dict) + if not res.get('success'): + return res + + allowed_orgs = config.get('ckanext.iaea.allow_dataset_create_from_organization') or '' + allowed_orgs = [o.strip() for o in allowed_orgs.strip().split(',') if o.strip()] + + if not allowed_orgs: + return res + + user = context.get('auth_user_obj') + if user: + org_ids = get_organization_ids(allowed_orgs) + if not org_ids: + return res + + if not user_has_sufficient_roles_in_org(user.id, org_ids, ['editor', 'admin']): + return { + 'success': False, + 'message': _('User {} cannot create datasets.').format(user.name), + } + + return res + +def get_allowed_organizations_ids(): + allowed_orgs = config.get('ckanext.iaea.allow_dataset_create_from_organization') or '' + allowed_orgs = [o.strip() for o in allowed_orgs.strip().split(',') if o.strip()] + + if not allowed_orgs: + return [] + return get_organization_ids(allowed_orgs) + +def user_has_sufficient_roles_in_org(user_id, org_ids, roles): + q = (model.Session.query(model.Member.group_id) + .filter(model.Member.state=='active') + .filter(model.Member.table_name=='user') + .filter(model.Member.group_id.in_(org_ids)) + .filter(model.Member.table_id == user_id) + .filter(model.Member.capacity.in_(roles))).exists() + return model.Session.query(q).scalar() + +def get_organization_ids(org_names_ids): + q = (model.Session.query(model.Group.id) + .filter(or_(model.Group.id.in_(org_names_ids), model.Group.name.in_(org_names_ids))) + .filter(model.Group.state == 'active') + .filter(model.Group.type == 'organization')) + return [org_id[0] for org_id in q] diff --git a/ckanext/iaea/logic/validators.py b/ckanext/iaea/logic/validators.py new file mode 100644 index 0000000..9b5b9e6 --- /dev/null +++ b/ckanext/iaea/logic/validators.py @@ -0,0 +1,55 @@ +import ckan.plugins.toolkit as tk +import ckan.lib.helpers as h +import ckan.lib.navl.dictization_functions as df +from ckan.common import _ + +from ckanext.iaea.helpers import get_main_organization +from ckanext.iaea.logic.auth import get_allowed_organizations_ids + + +def package_organization_validator(value, context): + '''Validates the organization on create/update package. + The organization must be one of: + - Empty value i.e. no owner organization for the package. + - If set, then it must be set to the main organization of the portal. + - If the package already was owned by an organization, the value must either be that old + organization or the main portal organization (this is for compatibility with older packages). + ''' + if not value: + return value + + main_org = get_main_organization() + package = context.get('package') + + if not package and not main_org: + raise df.Invalid(_('Only datasets without organization are allowed')) + + if not package: + # Check that the current owner_org is the main owner_org + if value.lower() != main_org.get('name').lower() and value.lower() != main_org.get('id').lower(): + # Check if we can allow from additional organizations for load testing, but only if specified + if value not in get_allowed_organizations_ids(): + raise df.Invalid(_('The dataset owner organization must be {} or empty').format(main_org.get('name'))) + + if package and main_org: + new_org = _get_organization(value) + if not new_org: + raise df.Invalid(_('Package owner organization does not exist')) + if new_org.get('id') != package.owner_org: + # The owner organization was updated, so it must be set to the main organization or not at all. + if new_org.get('id') != main_org.get('id'): + # The new owner org is not the main org. + raise df.Invalid(_('You can only update the package owner organization to {} or leave the old one.').format(main_org.get('name'))) + + return value + + +def _get_organization(value): + try: + return tk.get_action('organization_show')({ + 'ignore_auth': True, + }, { + 'id': value, + }) + except tk.ObjectNotFound: + return None diff --git a/ckanext/iaea/plugin.py b/ckanext/iaea/plugin.py index 9bfd9e5..04e5f6d 100644 --- a/ckanext/iaea/plugin.py +++ b/ckanext/iaea/plugin.py @@ -2,12 +2,14 @@ import ckan.plugins.toolkit as toolkit from ckan.lib.plugins import DefaultTranslation from ckanext.iaea.helpers import get_helpers +from ckanext.iaea.logic import action, validators class IaeaPlugin(plugins.SingletonPlugin, DefaultTranslation): plugins.implements(plugins.IConfigurer) plugins.implements(plugins.ITranslation) plugins.implements(plugins.ITemplateHelpers, inherit=True) + plugins.implements(plugins.IValidators) # IConfigurer @@ -28,3 +30,8 @@ def get_helpers(self): iaea_helpers.update(get_helpers()) return iaea_helpers + # IValidators + def get_validators(self): + return { + 'iaea_owner_org_validator': validators.package_organization_validator, + }