├── ckanext ├── privatedatasets │ ├── parsers │ │ ├── __init__.py │ │ └── fiware.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_db.py │ │ ├── test_views.py │ │ ├── test_fiware_parser.py │ │ ├── test_helpers.py │ │ ├── test_converters_validators.py │ │ ├── test_auth.py │ │ ├── test_actions.py │ │ ├── test_plugin.py │ │ └── test_selenium.py │ ├── fanstatic │ │ ├── custom.css │ │ └── allowed_users.js │ ├── templates │ │ ├── snippets │ │ │ ├── acquire_button.html │ │ │ └── package_item.html │ │ ├── user │ │ │ ├── dashboard_acquired.html │ │ │ └── dashboard.html │ │ └── package │ │ │ └── snippets │ │ │ └── package_basic_fields.html │ ├── templates_2.8 │ │ ├── snippets │ │ │ ├── acquire_button.html │ │ │ └── package_item.html │ │ ├── user │ │ │ ├── dashboard_acquired.html │ │ │ └── dashboard.html │ │ └── package │ │ │ └── snippets │ │ │ └── package_basic_fields.html │ ├── __init__.py │ ├── constants.py │ ├── db.py │ ├── views.py │ ├── helpers.py │ ├── converters_validators.py │ ├── auth.py │ ├── actions.py │ └── plugin.py └── __init__.py ├── bin ├── travis-run.sh └── travis-build.bash ├── MANIFEST.in ├── setup.cfg ├── .gitignore ├── .travis.yml ├── test.ini ├── setup.py └── README.md /ckanext/privatedatasets/parsers/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /bin/travis-run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "Starting Jetty" 4 | sudo service jetty8 restart 5 | 6 | sudo netstat -ntlp 7 | 8 | python setup.py nosetests -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include ckanext/privatedatasets/templates * 2 | recursive-include ckanext/privatedatasets/templates_2.8 * 3 | recursive-include ckanext/privatedatasets/fanstatic * 4 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/fanstatic/custom.css: -------------------------------------------------------------------------------- 1 | .label-acquired { 2 | background-color: #55a1ce; 3 | } 4 | 5 | .label-owner { 6 | background-color: #e0051e; 7 | } 8 | 9 | .divider { 10 | margin-left:10px; 11 | height:auto; 12 | display:inline-block; 13 | } -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | 4 | [flake8] 5 | ignore=E501 6 | 7 | [metadata] 8 | description-file = README.md 9 | 10 | [nosetests] 11 | ckan=1 12 | with-pylons=test.ini 13 | with-xunit=1 14 | with-coverage=1 15 | cover-package=ckanext.privatedatasets 16 | cover-inclusive=1 17 | cover-erase=1 18 | cover-xml=1 19 | 20 | [pep8] 21 | ignore=E501 22 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates/snippets/acquire_button.html: -------------------------------------------------------------------------------- 1 | {# 2 | 3 | Displays a Get Access button to request access to a private dataset. 4 | 5 | ulr_dest - target url 6 | 7 | Example: 8 | 9 | {% snippet 'snippets/acquire_button.html', url_dest=url %} 10 | 11 | #} 12 | 13 | 14 | {{ _('Acquire') }} 15 | 16 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates_2.8/snippets/acquire_button.html: -------------------------------------------------------------------------------- 1 | {# 2 | 3 | Displays a Get Access button to request access to a private dataset. 4 | 5 | ulr_dest - target url 6 | 7 | Example: 8 | 9 | {% snippet 'snippets/acquire_button.html', url_dest=url %} 10 | 11 | #} 12 | 13 | 14 | {{ _('Acquire') }} 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | eggs/ 15 | lib/ 16 | lib64/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | *.egg-info/ 21 | .installed.cfg 22 | *.egg 23 | .eggs 24 | # Installer logs 25 | pip-log.txt 26 | pip-delete-this-directory.txt 27 | 28 | # Unit test / coverage reports 29 | htmlcov/ 30 | .tox/ 31 | .coverage 32 | .cache 33 | nosetests.xml 34 | coverage.xml 35 | 36 | # Translations 37 | *.mo 38 | 39 | # Mr Developer 40 | .mr.developer.cfg 41 | .project 42 | .pydevproject 43 | 44 | # Rope 45 | .ropeproject 46 | 47 | # Django stuff: 48 | *.log 49 | *.pot 50 | 51 | # Sphinx documentation 52 | docs/_build/ 53 | 54 | .idea 55 | venv -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates/user/dashboard_acquired.html: -------------------------------------------------------------------------------- 1 | {% extends "user/dashboard.html" %} 2 | 3 | {% block dashboard_activity_stream_context %}{% endblock %} 4 | 5 | {% block page_primary_action %} 6 | {% link_for _('Acquire Dataset'), controller='package', action='search', class_="btn btn-primary", icon="shopping-cart" %} 7 | {% endblock %} 8 | 9 | {% block primary_content_inner %} 10 |

{{ _('Acquired Datasets') }}

11 | {% if acquired_datasets %} 12 | {% snippet 'snippets/package_list.html', packages=acquired_datasets %} 13 | {% else %} 14 |

15 | {{ _('You haven\'t acquired any datasets.') }} 16 | {% link_for _('Acquire one now?'), controller='package', action='search' %} 17 |

18 | {% endif %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates_2.8/user/dashboard_acquired.html: -------------------------------------------------------------------------------- 1 | {% extends "user/dashboard.html" %} 2 | 3 | {% block dashboard_activity_stream_context %}{% endblock %} 4 | 5 | {% block page_primary_action %} 6 | {% link_for _('Acquire Dataset'), controller='package', action='search', class_="btn btn-primary", icon="shopping-cart" %} 7 | {% endblock %} 8 | 9 | {% block primary_content_inner %} 10 |

{{ _('Acquired Datasets') }}

11 | {% if acquired_datasets %} 12 | {% snippet 'snippets/package_list.html', packages=acquired_datasets %} 13 | {% else %} 14 |

15 | {{ _('You haven\'t acquired any datasets.') }} 16 | {% link_for _('Acquire one now?'), controller='package', action='search' %} 17 |

18 | {% endif %} 19 | {% endblock %} 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | language: python 3 | python: 4 | - "2.7" 5 | env: 6 | - CKANVERSION=2.7.3 7 | - CKANVERSION=2.8.1 8 | - CKANVERSION=2.8.2 9 | services: 10 | - redis-server 11 | - postgresql 12 | - xvfb 13 | addons: 14 | firefox: "60.1.0esr" 15 | before_install: 16 | - wget https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-linux64.tar.gz 17 | - mkdir geckodriver 18 | - tar -xzf geckodriver-v0.21.0-linux64.tar.gz -C geckodriver 19 | - export PATH=$PATH:$PWD/geckodriver 20 | install: 21 | - bash bin/travis-build.bash 22 | before_script: 23 | - "export DISPLAY=:99.0" 24 | - sleep 3 # give xvfb some time to start 25 | script: 26 | - sh bin/travis-run.sh 27 | after_success: coveralls 28 | branches: 29 | only: 30 | - master 31 | -------------------------------------------------------------------------------- /ckanext/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | # this is a namespace package 21 | try: 22 | import pkg_resources 23 | pkg_resources.declare_namespace(__name__) 24 | except ImportError: 25 | import pkgutil 26 | __path__ = pkgutil.extend_path(__path__, __name__) 27 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | # this is a namespace package 21 | try: 22 | import pkg_resources 23 | pkg_resources.declare_namespace(__name__) 24 | except ImportError: 25 | import pkgutil 26 | __path__ = pkgutil.extend_path(__path__, __name__) 27 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/constants.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | ALLOWED_USERS = 'allowed_users' 21 | ACQUISITIONS_LIST = 'acquisitions_list' 22 | ALLOWED_USERS_STR = 'allowed_users_str' 23 | SEARCHABLE = 'searchable' 24 | ACQUIRE_URL = 'acquire_url' 25 | CONTEXT_CALLBACK = 'updating_via_cb' 26 | PACKAGE_ACQUIRED = 'package_acquired' 27 | PACKAGE_DELETED = 'revoke_access' 28 | -------------------------------------------------------------------------------- /bin/travis-build.bash: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | echo "This is travis-build.bash..." 6 | 7 | echo "Installing the packages that CKAN requires..." 8 | sudo apt-get update -qq 9 | sudo apt-get install solr-jetty 10 | 11 | echo "Installing CKAN and its Python dependencies..." 12 | git clone https://github.com/ckan/ckan 13 | cd ckan 14 | git checkout ckan-$CKANVERSION 15 | python setup.py develop 16 | 17 | sed -i "s|psycopg2==2.4.5|psycopg2==2.7.1|g" requirements.txt 18 | 19 | pip install -r requirements.txt 20 | pip install -r dev-requirements.txt 21 | cd - 22 | 23 | echo "Checking solr" 24 | ls -la /etc/ 25 | 26 | echo "Setting up Solr..." 27 | # solr is multicore for tests on ckan master now, but it's easier to run tests 28 | # on Travis single-core still. 29 | # see https://github.com/ckan/ckan/issues/2972 30 | sed -i -e 's/solr_url.*/solr_url = http:\/\/127.0.0.1:8080\/solr/' ckan/test-core.ini 31 | printf "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8080\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty 32 | sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml 33 | sudo service jetty8 restart 34 | 35 | echo "Creating the PostgreSQL user and database..." 36 | sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';" 37 | sudo -u postgres psql -c "CREATE USER datastore_default WITH PASSWORD 'pass';" 38 | sudo -u postgres psql -c "CREATE DATABASE ckan_test WITH OWNER ckan_default;" 39 | sudo -u postgres psql -c "CREATE DATABASE datastore_test WITH OWNER ckan_default;" 40 | 41 | 42 | echo "Initialising the database..." 43 | cd ckan 44 | paster db init -c test-core.ini 45 | cd - 46 | 47 | echo "Installing ckanext-privatedatasets and its requirements..." 48 | python setup.py develop 49 | 50 | echo "travis-build.bash is done." -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates_2.8/user/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "user/edit_base.html" %} 2 | 3 | {% set user = g.userobj %} 4 | 5 | {% block breadcrumb_content %} 6 |
  • {{ _('Dashboard') }}
  • 7 | {% endblock %} 8 | 9 | {% block secondary %}{% endblock %} 10 | 11 | {% block primary %} 12 |
    13 | {% block page_header %} 14 | 26 | {% endblock %} 27 |
    28 | {% if self.page_primary_action() | trim %} 29 |
    30 | {% block page_primary_action %}{% endblock %} 31 |
    32 | {% endif %} 33 | {% block primary_content_inner %} 34 |
    35 | {% snippet 'user/snippets/followee_dropdown.html', context=dashboard_activity_stream_context, followees=followee_list %} 36 |

    37 | {% block page_heading %} 38 | {{ _('News feed') }} 39 | {% endblock %} 40 | {{ _("Activity from items that I'm following") }} 41 |

    42 | {% block activity_stream %} 43 | {{ dashboard_activity_stream|safe }} 44 | {% endblock %} 45 |
    46 | {% endblock %} 47 |
    48 |
    49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | import unittest 21 | import ckanext.privatedatasets.db as db 22 | 23 | from mock import MagicMock 24 | 25 | 26 | class DBTest(unittest.TestCase): 27 | 28 | def setUp(self): 29 | # Restart databse initial status 30 | db.AllowedUser = None 31 | 32 | # Create mocks 33 | self._sa = db.sa 34 | db.sa = MagicMock() 35 | 36 | def tearDown(self): 37 | db.AllowedUser = None 38 | db.sa = self._sa 39 | 40 | def test_initdb_not_initialized(self): 41 | 42 | # Call the function 43 | model = MagicMock() 44 | db.init_db(model) 45 | 46 | # Assert that table method has been called 47 | db.sa.Table.assert_called_once() 48 | model.meta.mapper.assert_called_once() 49 | 50 | def test_initdb_initialized(self): 51 | db.AllowedUser = MagicMock() 52 | 53 | # Call the function 54 | model = MagicMock() 55 | db.init_db(model) 56 | 57 | # Assert that table method has been called 58 | self.assertEquals(0, db.sa.Table.call_count) 59 | self.assertEquals(0, model.meta.mapper.call_count) 60 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates/user/dashboard.html: -------------------------------------------------------------------------------- 1 | {% extends "user/edit_base.html" %} 2 | 3 | {% set user = c.userobj %} 4 | 5 | {% block breadcrumb_content %} 6 |
  • {{ _('Dashboard') }}
  • 7 | {% endblock %} 8 | 9 | {% block secondary %}{% endblock %} 10 | 11 | {% block primary %} 12 |
    13 | {% block page_header %} 14 | 26 | {% endblock %} 27 |
    28 | {% if self.page_primary_action() | trim %} 29 |
    30 | {% block page_primary_action %}{% endblock %} 31 |
    32 | {% endif %} 33 | {% block primary_content_inner %} 34 |
    35 | {% snippet 'user/snippets/followee_dropdown.html', context=c.dashboard_activity_stream_context, followees=c.followee_list %} 36 |

    37 | {% block page_heading %} 38 | {{ _('News feed') }} 39 | {% endblock %} 40 | {{ _("Activity from items that I'm following") }} 41 |

    42 | {% block activity_stream %} 43 | {{ c.dashboard_activity_stream }} 44 | {% endblock %} 45 |
    46 | {% endblock %} 47 |
    48 |
    49 | {% endblock %} 50 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/db.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | from __future__ import absolute_import 21 | 22 | import sqlalchemy as sa 23 | 24 | AllowedUser = None 25 | 26 | 27 | def init_db(model): 28 | 29 | global AllowedUser 30 | if AllowedUser is None: 31 | 32 | class _AllowedUser(model.DomainObject): 33 | 34 | @classmethod 35 | def get(cls, **kw): 36 | '''Finds all the instances required.''' 37 | query = model.Session.query(cls).autoflush(False) 38 | return query.filter_by(**kw).all() 39 | 40 | AllowedUser = _AllowedUser 41 | 42 | # FIXME: Maybe a default value should not be included... 43 | package_allowed_users_table = sa.Table( 44 | 'package_allowed_users', 45 | model.meta.metadata, 46 | sa.Column('package_id', sa.types.UnicodeText, primary_key=True, default=u''), 47 | sa.Column('user_name', sa.types.UnicodeText, primary_key=True, default=u''), 48 | ) 49 | 50 | # Create the table only if it does not exist 51 | package_allowed_users_table.create(checkfirst=True) 52 | 53 | model.meta.mapper(AllowedUser, package_allowed_users_table,) 54 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | from __future__ import absolute_import, unicode_literals 21 | 22 | from ckan import logic, model 23 | from ckan.common import _, g 24 | from ckan.lib import base 25 | from ckan.plugins import toolkit 26 | 27 | from ckanext.privatedatasets import constants 28 | 29 | 30 | def acquired_datasets(): 31 | context = {'auth_user_obj': g.userobj, 'for_view': True, 'model': model, 'session': model.Session, 'user': g.user} 32 | data_dict = {'user_obj': g.userobj} 33 | try: 34 | user_dict = toolkit.get_action('user_show')(context, data_dict) 35 | acquired_datasets = toolkit.get_action(constants.ACQUISITIONS_LIST)(context, None) 36 | except logic.NotFound: 37 | base.abort(404, _('User not found')) 38 | except logic.NotAuthorized: 39 | base.abort(403, _('Not authorized to see this page')) 40 | 41 | extra_vars = { 42 | 'user_dict': user_dict, 43 | 'acquired_datasets': acquired_datasets, 44 | } 45 | return base.render('user/dashboard_acquired.html', extra_vars) 46 | 47 | 48 | class AcquiredDatasetsControllerUI(base.BaseController): 49 | 50 | def acquired_datasets(self): 51 | return acquired_datasets() 52 | -------------------------------------------------------------------------------- /test.ini: -------------------------------------------------------------------------------- 1 | [server:main] 2 | use = egg:Paste#http 3 | host = 0.0.0.0 4 | port = 5000 5 | 6 | [app:main] 7 | # use = config:/usr/lib/ckan/default/src/ckan/test-core.ini 8 | use = config:./ckan/test-core.ini 9 | 10 | ckan.site_id = ckanext.privatedatasets.test 11 | ckan.site_url = http://localhost:5000 12 | package_new_return_url = http://localhost:5000/dataset/?test=new 13 | package_edit_return_url = http://localhost:5000/dataset/?test=edit 14 | 15 | ckan.cache_validation_enabled = True 16 | ckan.cache_enabled = False 17 | ckan.tests.functional.test_cache.expires = 1800 18 | ckan.tests.functional.test_cache.TestCacheBasics.test_get_cache_expires.expires = 3600 19 | 20 | # Disable test css as we aren't using it and creates requests to URLs 21 | # raising ValidationErrors. See #46 22 | ckan.template_head_end = 23 | ckan.legacy_templates = no 24 | ckan.plugins = privatedatasets 25 | 26 | ckan.auth.create_unowned_dataset = true 27 | ckan.auth.create_dataset_if_not_in_organization = true 28 | ckan.auth.user_create_groups = true 29 | ckan.auth.user_create_organizations = true 30 | 31 | ckan.privatedatasets.parser = ckanext.privatedatasets.parsers.fiware:FiWareNotificationParser 32 | ckan.privatedatasets.show_acquire_url_on_create = True 33 | ckan.privatedatasets.show_acquire_url_on_edit = True 34 | 35 | sqlalchemy.url = postgresql://ckan_default:pass@127.0.0.1:5432/ckan_test 36 | ckan.datastore.write_url = postgresql://ckan_default:pass@127.0.0.1:5432/datastore_test 37 | ckan.datastore.read_url = postgresql://datastore_default:pass@127.0.0.1:5432/datastore_test 38 | 39 | ckan.storage_path=data/storage 40 | 41 | # Logging configuration 42 | [loggers] 43 | keys = root, ckan, ckanext, sqlalchemy 44 | 45 | [handlers] 46 | keys = console 47 | 48 | [formatters] 49 | keys = generic 50 | 51 | [logger_root] 52 | level = WARN 53 | handlers = console 54 | 55 | [logger_ckan] 56 | qualname = ckan 57 | handlers = console 58 | level = INFO 59 | propagate = 0 60 | 61 | [logger_ckanext] 62 | qualname = ckanext 63 | handlers = console 64 | level = DEBUG 65 | propagate = 0 66 | 67 | [logger_sqlalchemy] 68 | handlers = console 69 | qualname = sqlalchemy.engine 70 | level = WARN 71 | 72 | [handler_console] 73 | class = StreamHandler 74 | args = (sys.stdout,) 75 | level = NOTSET 76 | formatter = generic 77 | 78 | [formatter_generic] 79 | format = %(asctime)s %(levelname)-5.5s [%(name)s] %(message)s 80 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | from setuptools import setup, find_packages 22 | 23 | version = '0.4.1' 24 | 25 | setup( 26 | name='ckanext-privatedatasets', 27 | version=version, 28 | description='CKAN Extension - Private Datasets', 29 | long_description=''' 30 | This CKAN extension allows a user to create private datasets that only certain users will be able to see. When a dataset is being created, it's possible to specify the list of users that can see this dataset. In addition, the extension provides an HTTP API that allows to add users programatically. 31 | ''', 32 | classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers 33 | keywords='ckan, private, datasets', 34 | author='Aitor Magan, Francisco de la Vega', 35 | author_email='fdelavega@ficodes.com', 36 | url='https://conwet.fi.upm.es', 37 | download_url='https://github.com/conwetlab/ckanext-privatedatasets/tarball/v' + version, 38 | license='', 39 | packages=find_packages(exclude=['ez_setup', 'examples', 'tests']), 40 | namespace_packages=['ckanext', 'ckanext.privatedatasets'], 41 | include_package_data=True, 42 | zip_safe=False, 43 | setup_requires=[ 44 | 'nose>=1.3.0' 45 | ], 46 | install_requires=[ 47 | # -*- Extra requirements: -*- 48 | ], 49 | tests_require=[ 50 | 'parameterized', 51 | 'selenium==3.13.0' 52 | ], 53 | test_suite='nosetests', 54 | entry_points=''' 55 | [ckan.plugins] 56 | # Add plugins here, e.g. 57 | privatedatasets=ckanext.privatedatasets.plugin:PrivateDatasets 58 | ''', 59 | ) 60 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/fanstatic/allowed_users.js: -------------------------------------------------------------------------------- 1 | /* 2 | * (C) Copyright 2014 CoNWeT Lab., Universidad Politécnica de Madrid 3 | * 4 | * This file is part of CKAN Private Dataset Extension. 5 | * 6 | * CKAN Private Dataset Extension is free software: you can redistribute it and/or 7 | * modify it under the terms of the GNU Affero General Public License as 8 | * published by the Free Software Foundation, either version 3 of the 9 | * License, or (at your option) any later version. 10 | * 11 | * CKAN Private Dataset Extension is distributed in the hope that it will be useful, but 12 | * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public 14 | * License for more details. 15 | * 16 | * You should have received a copy of the GNU Affero General Public License 17 | * along with CKAN Private Dataset Extension. If not, see 18 | * . 19 | * 20 | */ 21 | 22 | /* Dataset allowed_users, searchable and acquire_url toggler 23 | * allowed_users, acquire_url and searchable can only be active when a 24 | * user attempts to create a private dataset 25 | */ 26 | 27 | this.ckan.module('allowed-users', function ($, _) { 28 | return { 29 | initialize: function() { 30 | this.original_acquire_url = $('[name=acquire_url]').val(); 31 | $('#field-private').on('change', this._onChange); 32 | this._onChange(); //Initial 33 | }, 34 | _onChange: function() { 35 | var ds_private = $('#field-private').val(); 36 | 37 | if (ds_private == 'True') { 38 | $('#field-allowed_users_str').prop('disabled', false); //Enable 39 | $('#field-acquire_url').prop('disabled', false); //Enable 40 | $('#field-searchable').prop('disabled', false); //Enable 41 | $('[name=acquire_url]').val(this.original_acquire_url); //Set previous acquire URL 42 | } else { 43 | $('#field-allowed_users_str').prop('disabled', true); //Disable 44 | $('#field-acquire_url').prop('disabled', true); //Disable 45 | $('#field-searchable').prop('disabled', true); //Disable 46 | 47 | //Remove previous values 48 | $('#field-allowed_users_str').select2('val', ''); 49 | this.original_acquire_url = $('[name=acquire_url]').val(); //Get previous value 50 | $('[name=acquire_url]').val(''); //Acquire URL should be reseted 51 | $('#field-searchable').val('True'); 52 | } 53 | } 54 | }; 55 | }); 56 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/parsers/fiware.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | import re 21 | from urlparse import urlparse 22 | 23 | from ckan.common import request 24 | import ckan.plugins.toolkit as tk 25 | import six 26 | 27 | 28 | class FiWareNotificationParser(object): 29 | 30 | def parse_notification(self, request_data): 31 | my_host = request.host 32 | 33 | fields = ['customer_name', 'resources'] 34 | 35 | for field in fields: 36 | if field not in request_data: 37 | raise tk.ValidationError({'message': '%s not found in the request' % field}) 38 | 39 | # Parse the body 40 | resources = request_data['resources'] 41 | user_name = request_data['customer_name'] 42 | datasets = [] 43 | 44 | if not isinstance(user_name, six.string_types): 45 | raise tk.ValidationError({'message': 'Invalid customer_name format'}) 46 | 47 | if not isinstance(resources, list): 48 | raise tk.ValidationError({'message': 'Invalid resources format'}) 49 | 50 | for resource in resources: 51 | if isinstance(resource, dict) and 'url' in resource: 52 | parsed_url = urlparse(resource['url']) 53 | dataset_name = re.findall('^/dataset/([^/]+).*$', parsed_url.path) 54 | 55 | resource_url = parsed_url.netloc 56 | if ':' in my_host and ':' not in resource_url: 57 | # Add the default port depending on the protocol 58 | default_port = '80' if parsed_url.protocol == 'http' else '443' 59 | resource_url = resource_url + default_port 60 | 61 | if len(dataset_name) == 1: 62 | if resource_url == my_host: 63 | datasets.append(dataset_name[0]) 64 | else: 65 | raise tk.ValidationError({'message': 'Dataset %s is associated with the CKAN instance located at %s, expected %s' 66 | % (dataset_name[0], resource_url, my_host)}) 67 | else: 68 | raise tk.ValidationError({'message': 'Invalid resource format'}) 69 | 70 | return {'users_datasets': [{'user': user_name, 'datasets': datasets}]} 71 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014-2015 CoNWeT Lab., Universidad Politécnica de Madrid 4 | # Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | from __future__ import absolute_import 22 | 23 | import logging 24 | import os 25 | 26 | from ckan.common import request 27 | import ckan.model as model 28 | import ckan.plugins.toolkit as tk 29 | 30 | from ckanext.privatedatasets import db 31 | 32 | 33 | log = logging.getLogger(__name__) 34 | 35 | 36 | def is_dataset_acquired(pkg_dict): 37 | 38 | db.init_db(model) 39 | 40 | if tk.c.user: 41 | return len(db.AllowedUser.get(package_id=pkg_dict['id'], user_name=tk.c.user)) > 0 42 | else: 43 | return False 44 | 45 | 46 | def is_owner(pkg_dict): 47 | if tk.c.userobj is not None: 48 | return tk.c.userobj.id == pkg_dict['creator_user_id'] 49 | else: 50 | return False 51 | 52 | 53 | def get_allowed_users_str(users): 54 | if users: 55 | return ','.join([user for user in users]) 56 | else: 57 | return '' 58 | 59 | 60 | def can_read(pkg_dict): 61 | try: 62 | context = {'user': tk.c.user, 'userobj': tk.c.userobj, 'model': model} 63 | return tk.check_access('package_show', context, pkg_dict) 64 | except tk.NotAuthorized: 65 | return False 66 | 67 | 68 | def get_config_bool_value(config_name, default_value=False): 69 | env_name = config_name.upper().replace('.', '_') 70 | value = os.environ.get(env_name, tk.config.get(config_name, default_value)) 71 | return value if type(value) == bool else value.strip().lower() in ('true', '1', 'on') 72 | 73 | 74 | def show_acquire_url_on_create(): 75 | return get_config_bool_value('ckan.privatedatasets.show_acquire_url_on_create') 76 | 77 | 78 | def show_acquire_url_on_edit(): 79 | return get_config_bool_value('ckan.privatedatasets.show_acquire_url_on_edit') 80 | 81 | 82 | def acquire_button(package): 83 | ''' 84 | Return a Get Access button for the given package id when the dataset has 85 | an acquisition URL. 86 | 87 | :param package: the the package to request access when the get access 88 | button is clicked 89 | :type package: Package 90 | 91 | :returns: a get access button as an HTML snippet 92 | :rtype: string 93 | 94 | ''' 95 | 96 | if 'acquire_url' in package and request.path.startswith('/dataset')\ 97 | and package['acquire_url'] != '': 98 | url_dest = package['acquire_url'] 99 | data = {'url_dest': url_dest} 100 | return tk.render_snippet('snippets/acquire_button.html', data) 101 | else: 102 | return '' 103 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates/snippets/package_item.html: -------------------------------------------------------------------------------- 1 | {# 2 | Displays a single of dataset. 3 | 4 | package - A package to display. 5 | item_class - The class name to use on the list item. 6 | hide_resources - If true hides the resources (default: false). 7 | banner - If true displays a popular banner (default: false). 8 | truncate - The length to trucate the description to (default: 180) 9 | truncate_title - The length to truncate the title to (default: 80). 10 | 11 | Example: 12 | 13 | {% snippet 'snippets/package_item.html', package=c.datasets[0] %} 14 | 15 | #} 16 | {% set truncate = truncate or 180 %} 17 | {% set truncate_title = truncate_title or 80 %} 18 | {% set title = package.title or package.name %} 19 | {% set notes = h.markdown_extract(package.notes, extract_length=truncate) %} 20 | {% set acquired = h.is_dataset_acquired(package) %} 21 | {% set owner = h.is_owner(package) %} 22 | 23 | {% resource 'privatedatasets/custom.css' %} 24 | 25 |
  • 26 | {% block package_item_content %} 27 |
    28 |

    29 | {% if package.private and not owner and not acquired %} 30 | 31 | 32 | {{ _('Private') }} 33 | 34 | {% endif %} 35 | {% if acquired and not owner %} 36 | 37 | 38 | {{ _('Acquired') }} 39 | 40 | {% endif %} 41 | {% if owner %} 42 | 43 | 44 | {{ _('Owner') }} 45 | 46 | {% endif %} 47 | 48 | 49 | {% if package.private and not owner and not acquired %} 50 | {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} 51 |
    52 | {{ h.acquire_button(package) }} 53 | {% else %} 54 | {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} 55 | {% endif %} 56 | 57 | 58 | {% if package.get('state', '').startswith('draft') %} 59 | {{ _('Draft') }} 60 | {% elif package.get('state', '').startswith('deleted') %} 61 | {{ _('Deleted') }} 62 | {% endif %} 63 | {{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }} 64 |

    65 | {% if banner %} 66 | 67 | {% endif %} 68 | {% if notes %} 69 |
    {{ notes|urlize }}
    70 | {% endif %} 71 |
    72 | {% if package.resources and not hide_resources %} 73 |
      74 | {% for resource in h.dict_list_reduce(package.resources, 'format') %} 75 |
    • 76 | {{ resource }} 77 |
    • 78 | {% endfor %} 79 |
    80 | {% endif %} 81 | {% endblock %} 82 |
  • 83 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates_2.8/snippets/package_item.html: -------------------------------------------------------------------------------- 1 | {# 2 | Displays a single of dataset. 3 | 4 | package - A package to display. 5 | item_class - The class name to use on the list item. 6 | hide_resources - If true hides the resources (default: false). 7 | banner - If true displays a popular banner (default: false). 8 | truncate - The length to trucate the description to (default: 180) 9 | truncate_title - The length to truncate the title to (default: 80). 10 | 11 | Example: 12 | 13 | {% snippet 'snippets/package_item.html', package=c.datasets[0] %} 14 | 15 | #} 16 | {% set truncate = truncate or 180 %} 17 | {% set truncate_title = truncate_title or 80 %} 18 | {% set title = package.title or package.name %} 19 | {% set notes = h.markdown_extract(package.notes, extract_length=truncate) %} 20 | {% set acquired = h.is_dataset_acquired(package) %} 21 | {% set owner = h.is_owner(package) %} 22 | 23 | {% resource 'privatedatasets/custom.css' %} 24 | 25 |
  • 26 | {% block package_item_content %} 27 |
    28 |

    29 | {% if package.private and not owner and not acquired%} 30 | 31 | 32 | {{ _('Private') }} 33 | 34 | {% endif %} 35 | {% if acquired and not owner %} 36 | 37 | 38 | {{ _('Acquired') }} 39 | 40 | {% endif %} 41 | {% if owner %} 42 | 43 | 44 | {{ _('Owner') }} 45 | 46 | {% endif %} 47 | 48 | 49 | {% if package.private and not owner and not acquired %} 50 | {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} 51 |
    52 | {{ h.acquire_button(package) }} 53 | {% else %} 54 | {{ h.link_to(h.truncate(title, truncate_title), h.url_for(controller='package', action='read', id=package.name)) }} 55 | {% endif %} 56 | 57 | 58 | {% if package.get('state', '').startswith('draft') %} 59 | {{ _('Draft') }} 60 | {% elif package.get('state', '').startswith('deleted') %} 61 | {{ _('Deleted') }} 62 | {% endif %} 63 | {{ h.popular('recent views', package.tracking_summary.recent, min=10) if package.tracking_summary }} 64 |

    65 | {% if banner %} 66 | 67 | {% endif %} 68 | {% if notes %} 69 |
    {{ notes|urlize }}
    70 | {% endif %} 71 |
    72 | {% if package.resources and not hide_resources %} 73 |
      74 | {% for resource in h.dict_list_reduce(package.resources, 'format') %} 75 |
    • 76 | {{ resource }} 77 |
    • 78 | {% endfor %} 79 |
    80 | {% endif %} 81 | {% endblock %} 82 |
  • 83 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_views.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | # Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | import unittest 22 | 23 | from mock import ANY, DEFAULT, MagicMock, patch 24 | from parameterized import parameterized 25 | 26 | from ckanext.privatedatasets import views 27 | 28 | 29 | class ViewsTest(unittest.TestCase): 30 | 31 | @parameterized.expand([ 32 | ('NotFound', 404), 33 | ('NotAuthorized', 403), 34 | ]) 35 | @patch.multiple("ckanext.privatedatasets.views", base=DEFAULT, toolkit=DEFAULT, model=DEFAULT, g=DEFAULT, logic=DEFAULT, _=DEFAULT) 36 | def test_exceptions_loading_users(self, exception, expected_status, base, toolkit, model, g, logic, _): 37 | 38 | # Configure the mocks 39 | setattr(logic, exception, ValueError) 40 | toolkit.get_action().side_effect = getattr(logic, exception) 41 | base.abort.side_effect = TypeError 42 | 43 | # Call the function 44 | with self.assertRaises(TypeError): 45 | views.acquired_datasets() 46 | 47 | # Assertations 48 | expected_context = { 49 | 'auth_user_obj': g.userobj, 50 | 'for_view': True, 51 | 'model': model, 52 | 'session': model.Session, 53 | 'user': g.user, 54 | } 55 | 56 | toolkit.get_action().assert_called_once_with(expected_context, {'user_obj': g.userobj}) 57 | base.abort.assert_called_once_with(expected_status, ANY) 58 | 59 | @patch.multiple("ckanext.privatedatasets.views", base=DEFAULT, toolkit=DEFAULT, model=DEFAULT, g=DEFAULT, logic=DEFAULT) 60 | def test_no_error_loading_users(self, base, toolkit, model, g, logic): 61 | 62 | # actions 63 | default_user = {'user_name': 'test', 'another_val': 'example value'} 64 | user_show = MagicMock(return_value=default_user) 65 | acquisitions_list = MagicMock() 66 | 67 | toolkit.get_action = MagicMock(side_effect=lambda action: user_show if action == 'user_show' else acquisitions_list) 68 | 69 | # Call the function 70 | returned = views.acquired_datasets() 71 | 72 | # User_show called correctly 73 | expected_context = { 74 | 'auth_user_obj': g.userobj, 75 | 'for_view': True, 76 | 'model': model, 77 | 'session': model.Session, 78 | 'user': g.user, 79 | } 80 | 81 | user_show.assert_called_once_with(expected_context, {'user_obj': g.userobj}) 82 | acquisitions_list.assert_called_with(expected_context, None) 83 | 84 | # Check that the render method has been called 85 | base.render.assert_called_once_with('user/dashboard_acquired.html', {'user_dict': default_user, 'acquired_datasets': acquisitions_list()}) 86 | self.assertEqual(returned, base.render()) 87 | 88 | @patch("ckanext.privatedatasets.views.acquired_datasets") 89 | def test_there_is_a_controller_for_ckan_27(self, acquired_datasets): 90 | controller = views.AcquiredDatasetsControllerUI() 91 | 92 | response = controller.acquired_datasets() 93 | 94 | acquired_datasets.assert_called_once_with() 95 | self.assertEqual(response, acquired_datasets()) 96 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/converters_validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | # Copyright (c) 2019 Future Internet Consulting and Development Solutions S.L. 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | from __future__ import absolute_import 22 | 23 | from itertools import count 24 | import re 25 | 26 | from ckan.plugins import toolkit 27 | from ckan.common import _ 28 | import six 29 | 30 | from ckanext.privatedatasets import constants, db 31 | 32 | 33 | def private_datasets_metadata_checker(key, data, errors, context): 34 | 35 | dataset_id = data.get(('id',)) 36 | private_val = data.get(('private',)) 37 | 38 | # Avoid missing value 39 | # "if not private_val:" is not valid because private_val can be False 40 | if not isinstance(private_val, six.string_types) and not isinstance(private_val, bool): 41 | private_val = None 42 | 43 | # If the private field is not included in the data dict, we must check the current value 44 | if private_val is None and dataset_id: 45 | dataset_dict = toolkit.get_action('package_show')({'ignore_auth': True}, {'id': dataset_id}) 46 | private_val = dataset_dict.get('private') 47 | 48 | private = private_val is True if isinstance(private_val, bool) else private_val == 'True' 49 | metadata_value = data[key] 50 | 51 | # If allowed users are included and the dataset is not private outside and organization, an error will be raised. 52 | if metadata_value and not private: 53 | errors[key].append(_('This field is only valid when you create a private dataset')) 54 | 55 | 56 | def allowed_users_convert(key, data, errors, context): 57 | 58 | # By default, all the fileds are in the data dictionary even if they contains nothing. In this case, 59 | # the value is 'ckan.lib.navl.dictization_functions.Missing' and for this reason the type is checked 60 | 61 | # Get the allowed user list 62 | if (constants.ALLOWED_USERS,) in data and isinstance(data[(constants.ALLOWED_USERS,)], list): 63 | allowed_users = data[(constants.ALLOWED_USERS,)] 64 | elif (constants.ALLOWED_USERS_STR,) in data and isinstance(data[(constants.ALLOWED_USERS_STR,)], six.string_types): 65 | allowed_users_str = data[(constants.ALLOWED_USERS_STR,)].strip() 66 | allowed_users = [allowed_user for allowed_user in allowed_users_str.split(',') if allowed_user.strip() != ''] 67 | else: 68 | allowed_users = None 69 | 70 | if allowed_users is not None: 71 | current_index = max([int(k[1]) for k in data.keys() if len(k) == 2 and k[0] == key[0]] + [-1]) 72 | 73 | if len(allowed_users) == 0: 74 | data[(constants.ALLOWED_USERS,)] = [] 75 | else: 76 | for num, allowed_user in zip(count(current_index + 1), allowed_users): 77 | allowed_user = allowed_user.strip() 78 | data[(key[0], num)] = allowed_user 79 | 80 | 81 | def get_allowed_users(key, data, errors, context): 82 | pkg_id = data[('id',)] 83 | 84 | db.init_db(context['model']) 85 | 86 | users = db.AllowedUser.get(package_id=pkg_id) 87 | 88 | for i, user in enumerate(users): 89 | data[(key[0], i)] = user.user_name 90 | 91 | 92 | def url_checker(key, data, errors, context): 93 | url = data.get(key, None) 94 | 95 | if url: 96 | # DJango Regular Expression to check URLs 97 | regex = re.compile( 98 | r'^https?://' # scheme is validated separately 99 | r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}(? 10 | {% endif %} 11 | 12 | {% set dataset_is_draft = data.get('state', 'draft').startswith('draft') or data.get('state', 'none') == 'none' %} 13 | {% set dataset_has_organization = data.owner_org or data.group_id %} 14 | {% set organizations_available = h.organizations_available('create_dataset') %} 15 | {% set user_is_sysadmin = h.check_access('sysadmin') %} 16 | {% set show_organizations_selector = organizations_available and (user_is_sysadmin or dataset_is_draft) %} 17 | {% set editing = 'id' in data %} 18 | 19 | {% if show_organizations_selector and show_visibility_selector %} 20 |
    21 | {% endif %} 22 | 23 | {% if show_organizations_selector %} 24 | {% set existing_org = data.owner_org or data.group_id %} 25 |
    26 | 27 |
    28 | 38 |
    39 |
    40 | {% endif %} 41 | 42 | {% block package_metadata_fields_visibility %} 43 |
    44 | 45 |
    46 | 51 | 52 | 53 | {% trans %} 54 | Private datasets can only be accessed by certain users, while public datasets can be accessed by anyone. 55 | {% endtrans %} 56 | 57 |
    58 |
    59 | {% endblock %} 60 | 61 | {% block package_metadata_fields_protected %} 62 |
    63 | 64 |
    65 | 70 | 71 | 72 | {% trans %} 73 | Searchable datasets can be searched by anyone, while not-searchable datasets can only be accessed by entering directly its URL. 74 | {% endtrans %} 75 | 76 |
    77 |
    78 | {% endblock %} 79 | 80 | 81 | {% if show_organizations_selector and show_visibility_selector %} 82 |
    83 | {% endif %} 84 | 85 | {% set users_attrs = {'data-module': 'autocomplete', 'data-module-tags': '', 'data-module-source': '/api/2/util/user/autocomplete?q=?'} %} 86 | {{ form.input('allowed_users_str', label=_('Allowed Users'), id='field-allowed_users_str', placeholder=_('Allowed Users'), value=h.get_allowed_users_str(data.allowed_users), error=errors.custom_text, classes=['control-full'], attrs=users_attrs) }} 87 | 88 | 89 | {% if editing and h.show_acquire_url_on_edit() or not editing and h.show_acquire_url_on_create() %} 90 | {{ form.input('acquire_url', label=_('Acquire URL'), id='field-acquire_url', placeholder=_('http://example.com/acquire/'), value=data.acquire_url, error=errors.custom_text, classes=['control-medium']) }} 91 | {% else %} 92 | 93 | {% endif %} 94 | 95 | {% if data.id and h.check_access('package_delete', {'id': data.id}) and data.state != 'active' %} 96 |
    97 | 98 |
    99 | 103 |
    104 |
    105 | {% endif %} 106 | 107 | {% endblock %} 108 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/templates_2.8/package/snippets/package_basic_fields.html: -------------------------------------------------------------------------------- 1 | {% ckan_extends %} 2 | 3 | {% block package_basic_fields_org %} 4 | 5 | {% resource 'privatedatasets/allowed_users.js' %} 6 | 7 | {# if we have a default group then this wants remembering #} 8 | {% if data.group_id %} 9 | 10 | {% endif %} 11 | 12 | {% set dataset_is_draft = data.get('state', 'draft').startswith('draft') or data.get('state', 'none') == 'none' %} 13 | {% set dataset_has_organization = data.owner_org or data.group_id %} 14 | {% set organizations_available = h.organizations_available('create_dataset') %} 15 | {% set user_is_sysadmin = h.check_access('sysadmin') %} 16 | {% set show_organizations_selector = organizations_available and (user_is_sysadmin or dataset_is_draft) %} 17 | {% set editing = 'id' in data %} 18 | 19 | {% if show_organizations_selector and show_visibility_selector %} 20 |
    21 | {% endif %} 22 | 23 | {% if show_organizations_selector %} 24 | {% set existing_org = data.owner_org or data.group_id %} 25 |
    26 | 27 |
    28 | 38 |
    39 |
    40 | {% endif %} 41 | 42 | {% block package_metadata_fields_visibility %} 43 |
    44 | 45 |
    46 | 51 | 52 | 53 | {% trans %} 54 | Private datasets can only be accessed by certain users, while public datasets can be accessed by anyone. 55 | {% endtrans %} 56 | 57 |
    58 |
    59 | {% endblock %} 60 | 61 | {% block package_metadata_fields_protected %} 62 |
    63 | 64 |
    65 | 70 | 71 | 72 | {% trans %} 73 | Searchable datasets can be searched by anyone, while not-searchable datasets can only be accessed by entering directly its URL. 74 | {% endtrans %} 75 | 76 |
    77 |
    78 | {% endblock %} 79 | 80 | 81 | {% if show_organizations_selector and show_visibility_selector %} 82 |
    83 | {% endif %} 84 | 85 | {% set users_attrs = {'data-module': 'autocomplete', 'data-module-tags': '', 'data-module-source': '/api/2/util/user/autocomplete?q=?'} %} 86 | {{ form.input('allowed_users_str', label=_('Allowed Users'), id='field-allowed_users_str', placeholder=_('Allowed Users'), value=h.get_allowed_users_str(data.allowed_users), error=errors.custom_text, classes=['control-full'], attrs=users_attrs) }} 87 | 88 | 89 | {% if editing and h.show_acquire_url_on_edit() or not editing and h.show_acquire_url_on_create() %} 90 | {{ form.input('acquire_url', label=_('Acquire URL'), id='field-acquire_url', placeholder=_('http://example.com/acquire/'), value=data.acquire_url, error=errors.custom_text, classes=['control-medium']) }} 91 | {% else %} 92 | 93 | {% endif %} 94 | 95 | {% if data.id and h.check_access('package_delete', {'id': data.id}) and data.state != 'active' %} 96 |
    97 | 98 |
    99 | 103 |
    104 |
    105 | {% endif %} 106 | 107 | {% endblock %} 108 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_fiware_parser.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | import unittest 21 | import ckanext.privatedatasets.parsers.fiware as fiware 22 | 23 | from mock import MagicMock 24 | from parameterized import parameterized 25 | 26 | 27 | TEST_CASES = { 28 | 'one_ds': { 29 | 'host': 'localhost', 30 | 'json': {"customer_name": "test", "resources": [{"url": "http://localhost/dataset/ds1"}]}, 31 | 'result': {'users_datasets': [{'user': 'test', 'datasets': ['ds1']}]} 32 | }, 33 | 'two_ds': { 34 | 'host': 'localhost', 35 | 'json': {"customer_name": "test", "resources": [{"url": "http://localhost/dataset/ds1"}, 36 | {"url": "http://localhost/dataset/ds2"}]}, 37 | 'result': {'users_datasets': [{'user': 'test', 'datasets': ['ds1', 'ds2']}]} 38 | }, 39 | 'error': { 40 | 'host': 'localhost', 41 | 'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"}]}, 42 | 'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected localhost', 43 | }, 44 | 'error_one_ds': { 45 | 'host': 'localhost', 46 | 'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"}, 47 | {"url": "http://localhost/dataset/ds2"}]}, 48 | 'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected localhost', 49 | }, 50 | 'two_errors': { 51 | 'host': 'localhost', 52 | 'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"}, 53 | {"url": "http://localhostb/dataset/ds2"}]}, 54 | 'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected localhost', 55 | }, 56 | 'two_errors_two_ds': { 57 | 'host': 'example.com', 58 | 'json': {"customer_name": "test", "resources": [{"url": "http://localhosta/dataset/ds1"}, 59 | {"url": "http://example.es/dataset/ds2"}, {"url": "http://example.com/dataset/ds3"}, 60 | {"url": "http://example.com/dataset/ds4"}]}, 61 | 'error': 'Dataset ds1 is associated with the CKAN instance located at localhosta, expected example.com', 62 | }, 63 | 'no_customer_name': { 64 | 'host': 'localhost', 65 | 'json': {"resources": [{"url": "http://localhost/dataset/ds1"}]}, 66 | 'error': 'customer_name not found in the request' 67 | }, 68 | 'no_resources': { 69 | 'host': 'localhost', 70 | 'json': {"customer_name": "test"}, 71 | 'error': 'resources not found in the request' 72 | }, 73 | 'no_customer_name_and_resources': { 74 | 'host': 'localhost', 75 | 'json': {"customer": "test"}, 76 | 'error': 'customer_name not found in the request' 77 | }, 78 | 'invalid_customer_name': { 79 | 'host': 'localhost', 80 | 'json': {"customer_name": 974, "resources": [{"url": "http://localhost/dataset/ds1"}]}, 81 | 'error': 'Invalid customer_name format' 82 | }, 83 | 'invalid_resources': { 84 | 'host': 'localhost', 85 | 'json': {"customer_name": "test", "resources": "http://localhost/dataset/ds1"}, 86 | 'error': 'Invalid resources format' 87 | }, 88 | 'missing_url_resource': { 89 | 'host': 'localhost', 90 | 'json': {"customer_name": "test", "resources": [{"urla": "http://localhost/dataset/ds1"}]}, 91 | 'error': 'Invalid resource format' 92 | }, 93 | 94 | 95 | } 96 | 97 | 98 | class FiWareParserTest(unittest.TestCase): 99 | 100 | def setUp(self): 101 | # Parser 102 | self.parser = fiware.FiWareNotificationParser() 103 | 104 | # Mock functions 105 | self._request = fiware.request 106 | fiware.request = MagicMock() 107 | 108 | def tearDown(self): 109 | # Unmock functions 110 | fiware.request = self._request 111 | 112 | @parameterized.expand([ 113 | ('one_ds',), 114 | ('two_ds',), 115 | ('error',), 116 | ('error_one_ds',), 117 | ('two_errors',), 118 | ('two_errors_two_ds',), 119 | ('no_customer_name',), 120 | ('no_resources',), 121 | ('no_customer_name_and_resources',), 122 | ('invalid_customer_name',), 123 | ('invalid_resources',), 124 | ('missing_url_resource',) 125 | ]) 126 | def test_parse_notification(self, case): 127 | 128 | # Configure 129 | fiware.request.host = TEST_CASES[case]['host'] 130 | 131 | # Call the function 132 | if 'error' in TEST_CASES[case]: 133 | with self.assertRaises(fiware.tk.ValidationError) as cm: 134 | self.parser.parse_notification(TEST_CASES[case]['json']) 135 | self.assertEqual(cm.exception.error_dict['message'], TEST_CASES[case]['error']) 136 | else: 137 | result = self.parser.parse_notification(TEST_CASES[case]['json']) 138 | # Assert that the result is what we expected to be 139 | self.assertEquals(TEST_CASES[case]['result'], result) 140 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | from __future__ import absolute_import 21 | 22 | import ckan.authz as authz 23 | from ckan.common import _, request 24 | import ckan.lib.helpers as helpers 25 | import ckan.logic.auth as logic_auth 26 | import ckan.plugins.toolkit as tk 27 | 28 | from ckanext.privatedatasets import db 29 | 30 | 31 | @tk.auth_allow_anonymous_access 32 | def package_show(context, data_dict): 33 | user = context.get('user') 34 | user_obj = context.get('auth_user_obj') 35 | package = logic_auth.get_package_object(context, data_dict) 36 | 37 | # datasets can be read by its creator 38 | if package and user_obj and package.creator_user_id == user_obj.id: 39 | return {'success': True} 40 | 41 | # Not active packages can only be seen by its owners 42 | if package.state == 'active': 43 | # anyone can see a public package 44 | if package.private: 45 | 46 | acquired = False 47 | 48 | if package.owner_org: 49 | acquired = authz.has_user_permission_for_group_or_org( 50 | package.owner_org, user, 'read') 51 | 52 | if not acquired: 53 | # Init the model 54 | db.init_db(context['model']) 55 | 56 | # Branch not executed if the database return an empty list 57 | if db.AllowedUser.get(package_id=package.id, user_name=user): 58 | acquired = True 59 | 60 | if not acquired: 61 | 62 | # Show a flash message with the URL to acquire the dataset 63 | # This message only can be shown when the user tries to access the dataset via its URL (/dataset/...) 64 | # The message cannot be displayed in other pages that uses the package_show function such as 65 | # the user profile page 66 | 67 | if hasattr(package, 'extras') and 'acquire_url' in package.extras and request.path.startswith( 68 | '/dataset/') \ 69 | and package.extras['acquire_url'] != '': 70 | helpers.flash_notice(_('This private dataset can be acquired. To do so, please click ' + 71 | 'here') % package.extras['acquire_url'], 72 | allow_html=True) 73 | 74 | return {'success': True} 75 | else: 76 | return {'success': False, 'msg': _('User %s not authorized to read package %s') % (user, package.id)} 77 | 78 | 79 | def package_update(context, data_dict): 80 | user = context.get('user') 81 | user_obj = context.get('auth_user_obj') 82 | package = logic_auth.get_package_object(context, data_dict) 83 | 84 | # Only the package creator can update it 85 | if package and user_obj and package.creator_user_id == user_obj.id: 86 | return {'success': True} 87 | 88 | # if the user has rights to update a dataset in the organization or in the group 89 | if package and package.owner_org: 90 | authorized = authz.has_user_permission_for_group_or_org( 91 | package.owner_org, user, 'update_dataset') 92 | else: 93 | authorized = False 94 | 95 | if not authorized: 96 | return {'success': False, 'msg': _('User %s is not authorized to edit package %s') % (user, package.id)} 97 | else: 98 | return {'success': True} 99 | 100 | 101 | @tk.auth_allow_anonymous_access 102 | def resource_show(context, data_dict): 103 | 104 | user = context.get('user') 105 | user_obj = context.get('auth_user_obj') 106 | resource = logic_auth.get_resource_object(context, data_dict) 107 | # check authentication against package 108 | package_dict = {'id': resource.package_id} 109 | package = logic_auth.get_package_object(context, package_dict) 110 | if not package: 111 | raise tk.ObjectNotFound(_('No package found for this resource, cannot check auth.')) 112 | 113 | if package and user_obj and package.creator_user_id == user_obj.id: 114 | return {'success': True} 115 | 116 | # active packages can only be seen by its owners 117 | if package.state == 'active': 118 | 119 | # anyone can see a public package 120 | if not package.private: 121 | return {'success': True} 122 | 123 | # if the user has rights to read in the organization or in the group 124 | if package.owner_org: 125 | authorized = authz.has_user_permission_for_group_or_org( 126 | package.owner_org, user, 'read') 127 | else: 128 | authorized = False 129 | 130 | if not authorized: 131 | # Init the model 132 | db.init_db(context['model']) 133 | 134 | # Branch not executed if the database return an empty list 135 | if db.AllowedUser.get(package_id=package.id, user_name=user): 136 | authorized = True 137 | 138 | if not authorized: 139 | return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)} 140 | 141 | else: 142 | return {'success': True} 143 | 144 | else: 145 | return {'success': False, 'msg': _('User %s not authorized to read resource %s') % (user, resource.id)} 146 | 147 | @tk.auth_allow_anonymous_access 148 | def package_acquired(context, data_dict): 149 | # TODO: Improve security 150 | return {'success': True} 151 | 152 | 153 | def acquisitions_list(context, data_dict): 154 | # Users can get only their acquisitions list 155 | return {'success': context['user'] == data_dict['user']} 156 | 157 | 158 | @tk.auth_allow_anonymous_access 159 | def revoke_access(context, data_dict): 160 | # TODO: Check functionality and improve security(if needed) 161 | return {'success': True} 162 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | # Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | import unittest 22 | import ckanext.privatedatasets.helpers as helpers 23 | 24 | from mock import patch, MagicMock 25 | from parameterized import parameterized 26 | 27 | 28 | class HelpersTest(unittest.TestCase): 29 | 30 | def setUp(self): 31 | # Create mocks 32 | self._model = helpers.model 33 | helpers.model = MagicMock() 34 | 35 | self._tk = helpers.tk 36 | helpers.tk = MagicMock() 37 | helpers.tk.config = {} 38 | 39 | self._db = helpers.db 40 | helpers.db = MagicMock() 41 | 42 | self._request = helpers.request 43 | helpers.request = MagicMock() 44 | 45 | def tearDown(self): 46 | helpers.model = self._model 47 | helpers.tk = self._tk 48 | helpers.db = self._db 49 | helpers.request = self._request 50 | 51 | @parameterized.expand([ 52 | (False, 'user', False), 53 | (True, 'user', True), 54 | (False, None, False), 55 | (True, None, False), 56 | ]) 57 | def test_is_dataset_acquired(self, db_acquired, user, acquired): 58 | # Configure test 59 | helpers.tk.c.user = user 60 | pkg_dict = {'id': 'package_id'} 61 | 62 | db_response = [] 63 | if db_acquired is True: 64 | out = helpers.db.AllowedUser() 65 | out.package_id = 'package_id' 66 | out.user_name = user 67 | db_response.append(out) 68 | 69 | helpers.db.AllowedUser.get = MagicMock(return_value=db_response) 70 | 71 | # Check the function returns the expected result 72 | self.assertEquals(acquired, helpers.is_dataset_acquired(pkg_dict)) 73 | 74 | # Check that the database has been initialized properly 75 | helpers.db.init_db.assert_called_once_with(helpers.model) 76 | 77 | @parameterized.expand([ 78 | (1, 1, True), 79 | (1, 2, False), 80 | (1, None, False) 81 | ]) 82 | def test_is_owner(self, creator_user_id, user_id, owner): 83 | # Configure test 84 | if user_id: 85 | user = MagicMock() 86 | user.id = user_id 87 | helpers.tk.c.userobj = user 88 | else: 89 | helpers.tk.c.userobj = None 90 | 91 | pkg_dict = {'creator_user_id': creator_user_id} 92 | 93 | # Check that the functions return the expected result 94 | self.assertEquals(owner, helpers.is_owner(pkg_dict)) 95 | 96 | @parameterized.expand([ 97 | ([], ''), 98 | (['a'], 'a'), 99 | (['a', 'b'], 'a,b'), 100 | (['a', 'b', 'c', 'd'], 'a,b,c,d'), 101 | ]) 102 | def test_get_allowed_users_str(self, allowed_users, expected_result): 103 | self.assertEquals(expected_result, helpers.get_allowed_users_str(allowed_users)) 104 | 105 | @parameterized.expand([ 106 | (False,), 107 | (True,) 108 | ]) 109 | def test_can_read(self, auth): 110 | # Recover exception 111 | helpers.tk.NotAuthorized = self._tk.NotAuthorized 112 | 113 | def _check_access(function_name, context, data_dict): 114 | if not auth: 115 | raise helpers.tk.NotAuthorized() 116 | else: 117 | return True 118 | 119 | helpers.tk.check_access = MagicMock(side_effect=_check_access) 120 | 121 | # Call the function and check the result 122 | package = {'id': 1} 123 | self.assertEquals(auth, helpers.can_read(package)) 124 | 125 | # Assert called with 126 | context = {'user': helpers.tk.c.user, 'userobj': helpers.tk.c.userobj, 'model': helpers.model} 127 | helpers.tk.check_access.assert_called_once_with('package_show', context, package) 128 | 129 | @parameterized.expand([ 130 | (None, False, None), 131 | ('True', True, None), 132 | ('False', False, None), 133 | ('afa ', False, None), 134 | (True, True, None), 135 | (False, False, None), 136 | (False, True , 'true'), 137 | (False, True , 'on'), 138 | (False, True , '1'), 139 | (True, False, '0'), 140 | (True, False, 'off'), 141 | (True, False, 'fAlsE'), 142 | (True, False, 'fAlsE'), 143 | ]) 144 | @patch("ckanext.privatedatasets.helpers.os.environ", new={}) 145 | def test_show_acquire_url_on_create(self, config_value, expected_value, env_val): 146 | # {} is shared between tests, so we have clear it each time 147 | helpers.os.environ.clear() 148 | 149 | if config_value is not None: 150 | helpers.tk.config['ckan.privatedatasets.show_acquire_url_on_create'] = config_value 151 | 152 | if env_val: 153 | helpers.os.environ['CKAN_PRIVATEDATASETS_SHOW_ACQUIRE_URL_ON_CREATE'] = env_val 154 | 155 | # Call the function 156 | self.assertEquals(expected_value, helpers.show_acquire_url_on_create()) 157 | 158 | @parameterized.expand([ 159 | (None, False, None), 160 | ('True', True, None), 161 | (' tRUe', True, None), 162 | ('False', False, None), 163 | (True, True, None), 164 | (False, False, None), 165 | (False, True , 'trUe'), 166 | (False, True , 'on'), 167 | (False, True , '1'), 168 | (True, False, '0'), 169 | (True, False, 'off'), 170 | (True, False, 'fAlsE'), 171 | (True, False, 'potato'), 172 | ]) 173 | @patch("ckanext.privatedatasets.helpers.os.environ", new={}) 174 | def test_show_acquire_url_on_edit(self, config_value, expected_value, env_val): 175 | # {} is shared between tests, so we have clear it each time 176 | helpers.os.environ.clear() 177 | 178 | if config_value is not None: 179 | helpers.tk.config['ckan.privatedatasets.show_acquire_url_on_edit'] = config_value 180 | 181 | if env_val: 182 | helpers.os.environ['CKAN_PRIVATEDATASETS_SHOW_ACQUIRE_URL_ON_EDIT'] = env_val 183 | 184 | # Call the function 185 | self.assertEquals(expected_value, helpers.show_acquire_url_on_edit()) 186 | 187 | @parameterized.expand([ 188 | ({}, '/dataset', False), 189 | ({'acquire_url': 'http://fiware.org'}, '/dataset', True), 190 | ({'acquire_url': ''}, '/dataset', False), 191 | ({'acquire_url': 'http://fiware.org'}, '/user', False), 192 | ]) 193 | def test_acquire_button(self, package, path, button_expected): 194 | 195 | # Mocking 196 | helpers.request.path = path 197 | 198 | # Call the function and check response 199 | result = helpers.acquire_button(package) 200 | 201 | if button_expected: 202 | helpers.tk.render_snippet.assert_called_once_with('snippets/acquire_button.html', 203 | {'url_dest': package['acquire_url']}) 204 | self.assertEquals(result, helpers.tk.render_snippet.return_value) 205 | else: 206 | self.assertEquals(result, '') 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | CKAN Private Datasets [![Build Status](https://travis-ci.org/conwetlab/ckanext-privatedatasets.svg?branch=master)](https://travis-ci.org/conwetlab/ckanext-privatedatasets) [![Coverage Status](https://coveralls.io/repos/github/conwetlab/ckanext-privatedatasets/badge.svg?branch=master)](https://coveralls.io/github/conwetlab/ckanext-privatedatasets?branch=develop) 2 | ===================== 3 | 4 | This CKAN extension allows a user to create private datasets that only certain users will be able to see. When a dataset is being created, it's possible to specify the list of users that can see this dataset. In addition, the extension provides an HTTP API that allows to add users programmatically. 5 | 6 | This project is part of [FIWARE](http://www.fiware.org). 7 | 8 | Installation 9 | ------------ 10 | Install this extension in your CKAN instance is as easy as install any other CKAN extension. 11 | 12 | * Activate your virtual environment 13 | ``` 14 | . /usr/lib/ckan/default/bin/activate 15 | ``` 16 | * Install the extension by running 17 | ``` 18 | pip install ckanext-privatedatasets 19 | ``` 20 | > **Note**: If you prefer, you can also download the source code and install the extension manually. To do so, execute the following commands: 21 | > ``` 22 | > $ git clone https://github.com/conwetlab/ckanext-privatedatasets.git 23 | > $ cd ckanext-privatedatasets 24 | > $ python setup.py install 25 | > ``` 26 | 27 | * Modify your configuration file (generally in `/etc/ckan/default/production.ini`) and add `privatedatasets` in the `ckan.plugins` property. 28 | ``` 29 | ckan.plugins = privatedatasets 30 | ``` 31 | * In the same config file, specify the location of your parser by adding the `ckan.privatedatasets.parser` setting. For example, to set the [FiWareNotificationParser](https://github.com/conwetlab/ckanext-privatedatasets/blob/master/ckanext/privatedatasets/parsers/fiware.py) as notification parser, add the following line: `ckan.privatedatasets.parser = ckanext.privatedatasets.parsers.fiware:FiWareNotificationParser`. 32 | * If you want you can also add some preferences to set if the Acquire URL should be shown when the user is to create and/or editing a dataset: 33 | * To show the Acquire URL when the user is **creating** a dataset, you should set the following preference: `ckan.privatedatasets.show_acquire_url_on_create = True`. By default, the value of this preference is set to `False`. 34 | * To show the Acquire URL when the user is **editing** a dataset, you should set the following preference: `ckan.privatedatasets.show_acquire_url_on_edit = True`. By default, the value of this preference is set to `False`. 35 | * In some cases you will want to secure the notification callback in order to filter the entities (user, machines...) that can send them. To do so, you can follow the instructions in the section [Securing the Notification Callback](#securing-the-notification-callback). 36 | * Restart your apache2 server 37 | ``` 38 | sudo service apache2 restart 39 | ``` 40 | * That's All! 41 | 42 | Creating a notification parser 43 | ------------------------------ 44 | Since each service can send notifications in a different way, the extension allows developers to create their own notifications parser. As default, we provide you a basic parser based on the notifications sent by the [FiWare Store](https://github.com/conwetlab/wstore/). 45 | 46 | If you want to create your own parser, you have to: 47 | 48 | 1. Create a class with a method called `parse_notification`. This method will receive one argument that will include the notification body. 49 | 2. Parse the notification as you like. You can raise a CKAN's default exception (`ValidationError`, `ObjectNotFound`, `NotAuthorized`, `ValidationError`, `SearchError`, `SearchQueryError` or `SearchIndexError`) if you find an error parsing the notification. 50 | 3. Return a dictionary with the structure attached below. The `users_datasets` is the lists of datasets available for each user (each element of this list is a dictionary with two fields: `user` and `datasets`). 51 | 52 | ``` 53 | {'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, 54 | {'user': 'user_name2', 'datasets': ['ds1', 'ds4', ...] }]} 55 | ``` 56 | 57 | Finally, you have to modify your config file and specify in the `ckan.privatedatasets.parser` the location of your own parser. 58 | 59 | At this point, you will be able to add users via API by accessing the following URL: 60 | 61 | ``` 62 | http://:/api/action/dataset_acquired 63 | ``` 64 | 65 | Securing the Notification Callback 66 | ----------------------------------- 67 | In some cases, you are required to filter the entities (users, machines...) that can send notifications to the notification callback. To do so, you must relay on Client Side Verification over HTTPs, so the first step here is to deploy your CKAN instance over HTTPs. If you haven't already done it, you can use the following tutorial: [Starting CKAN over HTTPs](https://github.com/conwetlab/ckanext-oauth2/wiki/Starting-CKAN-over-HTTPs). 68 | 69 | Once that your CKAN instance is running over HTTPs, you have to configure the Client Side Verification. To achieve this, the first thing that you must do is creating an OpenSSL config file. You can use the following one or modify it to your liking: 70 | 71 | ``` 72 | [ req ] 73 | default_md = sha1 74 | distinguished_name = req_distinguished_name 75 | 76 | [ req_distinguished_name ] 77 | countryName = Country 78 | countryName_default = SP 79 | countryName_min = 2 80 | countryName_max = 2 81 | localityName = Locality 82 | localityName_default = Madrid 83 | organizationName = Organization 84 | organizationName_default = FIWARE 85 | commonName = Common Name 86 | commonName_max = 64 87 | 88 | [ certauth ] 89 | subjectKeyIdentifier = hash 90 | authorityKeyIdentifier = keyid:always,issuer:always 91 | basicConstraints = CA:true 92 | crlDistributionPoints = @crl 93 | 94 | [ server ] 95 | basicConstraints = CA:FALSE 96 | keyUsage = digitalSignature, keyEncipherment, dataEncipherment 97 | extendedKeyUsage = serverAuth 98 | nsCertType = server 99 | crlDistributionPoints = @crl 100 | 101 | [ client ] 102 | basicConstraints = CA:FALSE 103 | keyUsage = digitalSignature, keyEncipherment, dataEncipherment 104 | extendedKeyUsage = clientAuth 105 | nsCertType = client 106 | crlDistributionPoints = @crl 107 | 108 | [ crl ] 109 | URI=http://testca.local/ca.crl 110 | ``` 111 | 112 | Then, you should create your CA using the previous config file. To do so, you can execute the following line (replace `` by the real path of your OpenSSL config file): 113 | 114 | ``` 115 | $ openssl req -config -newkey rsa:2048 -nodes -keyform PEM -keyout ca.key -x509 -days 3650 -extensions certauth -outform PEM -out ca.cer 116 | ``` 117 | 118 | Afterwards, you will need to filter the notification callback to be callable only by those entities that use a valid certificate (the one signed by the CA created previously). To achieve this, edit the file `/etc/apache2/sites-available/ckan_default` and add the following lines immediately after the SSL configuration (replace `` by the real path of your OpenSSL config file): 119 | 120 | ``` 121 | 122 | SSLCACertificateFile 123 | SSLVerifyClient require 124 | 125 | ``` 126 | 127 | Finally, you must restart your Apache server. To do so, execute the following command: 128 | 129 | ``` 130 | $ sudo service apache2 restart 131 | ``` 132 | 133 | From now own, you should consider that a valid certificate will be required to call the notification callback. To generate a new certificate you can execute the following lines (replace `` by the real path of your OpenSSL config file): 134 | 135 | ``` 136 | $ openssl genrsa -out client.key 2048 137 | $ openssl req -config -new -key client.key -out client.req 138 | $ openssl x509 -req -in client.req -CA ca.cer -CAkey ca.key -set_serial 101 -extfile -extensions client -days 365 -outform PEM -out client.cer 139 | $ openssl pkcs12 -export -inkey client.key -in client.cer -out client.p12 140 | ``` 141 | 142 | That's all! You notification callback is completely secure now! Enjoy it :) 143 | 144 | Tests 145 | ----- 146 | This sofware contains a set of test to detect errors and failures. You can run this tests by running the following command (this command will generate coverage reports): 147 | ``` 148 | python setup.py nosetests 149 | ``` 150 | **Note:** The `test.ini` file contains a link to the CKAN `test-core.ini` file. You will need to change that link to the real path of the file in your system (generally `/usr/lib/ckan/default/src/ckan/test-core.ini`). 151 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_converters_validators.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | import unittest 21 | import ckanext.privatedatasets.converters_validators as conv_val 22 | 23 | from mock import MagicMock 24 | from parameterized import parameterized 25 | 26 | 27 | class ConvertersValidatorsTest(unittest.TestCase): 28 | 29 | def setUp(self): 30 | # Create mocks 31 | self._toolkit = conv_val.toolkit 32 | conv_val.toolkit = MagicMock() 33 | 34 | self._db = conv_val.db 35 | conv_val.db = MagicMock() 36 | 37 | def tearDown(self): 38 | conv_val.db = self._db 39 | conv_val.toolkit = self._toolkit 40 | 41 | @parameterized.expand([ 42 | # When no data is present, no errors should be returned 43 | (True, True, 'conwet', '', False), 44 | ('True', True, 'conwet', '', False), 45 | (False, True, 'conwet', '', False), 46 | ('False', True, 'conwet', '', False), 47 | (None, True, 'conwet', '', False), 48 | (None, False, 'conwet', '', False), 49 | (True, True, None, '', False), 50 | ('True', True, None, '', False), 51 | (False, True, None, '', False), 52 | ('False', True, None, '', False), 53 | (None, True, None, '', False), 54 | (None, False, None, '', False), 55 | (True, True, 'conwet', [], False), 56 | ('True', True, 'conwet', [], False), 57 | (False, True, 'conwet', [], False), 58 | ('False', True, 'conwet', [], False), 59 | (None, True, 'conwet', [], False), 60 | (None, False, 'conwet', [], False), 61 | (True, True, None, [], False), 62 | ('True', True, None, [], False), 63 | (False, True, None, [], False), 64 | ('False', True, None, [], False), 65 | (None, True, None, [], False), 66 | (None, False, None, [], False), 67 | # When data is present, the field is only valid when the 68 | # the private field is set to true (organization should 69 | # not be taken into account anymore) 70 | (True, True, 'conwet', 'test', False), 71 | ('True', True, 'conwet', 'test', False), 72 | (True, False, 'conwet', 'test', False), 73 | ('True', False, 'conwet', 'test', False), 74 | (False, True, 'conwet', 'test', True), 75 | ('False', True, 'conwet', 'test', True), 76 | (False, False, 'conwet', 'test', True), 77 | ('False', False, 'conwet', 'test', True), 78 | (None, True, 'conwet', 'test', False), 79 | (None, False, 'conwet', 'test', True), 80 | (True, True, None, 'test', False), 81 | ('True', True, None, 'test', False), 82 | (True, False, None, 'test', False), 83 | ('True', False, None, 'test', False), 84 | (False, True, None, 'test', True), 85 | ('False', True, None, 'test', True), 86 | (False, False, None, 'test', True), 87 | ('False', False, None, 'test', True), 88 | (None, True, None, 'test', False), 89 | (None, False, None, 'test', True), 90 | ]) 91 | def test_metadata_checker(self, received_private, package_private, owner_org, metada_val, error_set): 92 | 93 | # Configure the mocks 94 | package_show = MagicMock(return_value={'private': package_private, 'id': 'package_id'}) 95 | conv_val.toolkit.get_action = MagicMock(return_value=package_show) 96 | 97 | KEY = ('test',) 98 | errors = {} 99 | errors[KEY] = [] 100 | 101 | data = {} 102 | data[('id',)] = 'package_id' 103 | data[('owner_org',)] = owner_org 104 | if received_private is not None: 105 | data[('private',)] = received_private 106 | data[KEY] = metada_val 107 | 108 | conv_val.private_datasets_metadata_checker(KEY, data, errors, {}) 109 | 110 | if error_set: 111 | self.assertEquals(1, len(errors[KEY])) 112 | else: 113 | self.assertEquals(0, len(errors[KEY])) 114 | 115 | @parameterized.expand([ 116 | ('', 0, []), 117 | ('', 2, []), 118 | ('a', 0, ['a']), 119 | ('a', 2, ['a']), 120 | (',,, , , ', 0, []), 121 | (',,, , , ', 2, []), 122 | ('a,z, d', 0, ['a', 'z', 'd']), 123 | ('a,z, d', 2, ['a', 'z', 'd']), 124 | (['a','z', 'd'], 0, ['a', 'z', 'd']), 125 | (['a','z', 'd'], 2, ['a', 'z', 'd']), 126 | ]) 127 | def test_allowed_user_convert(self, users, previous_users, expected_users): 128 | key_str = 'allowed_users_str' 129 | key = 'allowed_users' 130 | 131 | # Fullfill the data dictionary 132 | # * list should be included in the allowed_users filed 133 | # * strings should be included in the allowed_users_str field 134 | if isinstance(users, basestring): 135 | data_key = key_str 136 | else: 137 | data_key = key 138 | 139 | data = {(data_key,): users} 140 | 141 | for i in range(0, previous_users): 142 | data[(key, i)] = i 143 | 144 | # Call the function 145 | context = {'user': 'test', 'auth_obj_id': {'id': 1}} 146 | conv_val.allowed_users_convert((key,), data, {}, context) 147 | 148 | # Check that the users are set properly 149 | for i in range(previous_users, previous_users + len(expected_users)): 150 | self.assertEquals(expected_users[i - previous_users], data[(key, i)]) 151 | 152 | @parameterized.expand([ 153 | ([],), 154 | (['a'],), 155 | (['a', 'b'],), 156 | (['a', 'b', 'c'],), 157 | (['a', 'b', 'c', 'd', 'e'],) 158 | ]) 159 | def test_get_allowed_users(self, users): 160 | key = 'allowed_users' 161 | data = {('id',): 'package_id'} 162 | 163 | # Create the users 164 | db_res = [] 165 | for user in users: 166 | db_row = MagicMock() 167 | db_row.package_id = 'package_id' 168 | db_row.user_name = user 169 | db_res.append(db_row) 170 | 171 | conv_val.db.AllowedUser.get = MagicMock(return_value=db_res) 172 | 173 | # Call the function 174 | context = {'model': MagicMock()} 175 | conv_val.get_allowed_users((key,), data, {}, context) 176 | 177 | # Check that the users are set properly 178 | for i, user in enumerate(users): 179 | self.assertEquals(user, data[(key, i)]) 180 | 181 | # Check that the table has been initialized properly 182 | conv_val.db.init_db.assert_called_once_with(context['model']) 183 | 184 | @parameterized.expand([ 185 | (None, False), 186 | ('', False), 187 | ('http://google.es', False), 188 | ('https://google.es', False), 189 | ('http://google.es:80', False), 190 | ('https://google.es:443', False), 191 | ('http://google.es/path/path2/path3', False), 192 | ('https://google.es/path/path2/path3', False), 193 | ('http://google.es/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 194 | ('https://google.es/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 195 | ('http://google.es:80/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 196 | ('https://google.es:443/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 197 | ('http://goo-gl3.epa.es:80/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 198 | ('https://go-ogl2.epa.es:443/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 199 | ('http://192.168.0.1:80/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 200 | ('https://192.168.0.1:443/path/path2/path3?aaaa=bbbb&cccc=dddd', False), 201 | ('ftp://google.es', True), 202 | ('http://google*.com', True), 203 | ('http://google+.com', True), 204 | ('http://google/.com', True), 205 | ('google', True), 206 | ('http://google', True), 207 | ('http://google:es', True), 208 | ('www.google.es', True) 209 | ]) 210 | def test_url_validator(self, url, expected_error): 211 | key = ('url',) 212 | data = {key: url} 213 | 214 | # Call the function 215 | errors = {key: []} 216 | conv_val.url_checker(key, data, errors, {}) 217 | 218 | # Check the errors array 219 | if expected_error: 220 | self.assertEquals('The URL "%s" is not valid.' % url, errors[key][0]) 221 | expected_length = 1 222 | else: 223 | expected_length = 0 224 | 225 | self.assertEquals(expected_length, len(errors[key])) 226 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/actions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid 4 | # Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | from __future__ import absolute_import 22 | 23 | import importlib 24 | import logging 25 | import os 26 | 27 | import ckan.plugins as plugins 28 | 29 | from ckanext.privatedatasets import constants, db 30 | 31 | 32 | log = logging.getLogger(__name__) 33 | 34 | PARSER_CONFIG_PROP = 'ckan.privatedatasets.parser' 35 | 36 | 37 | def package_acquired(context, request_data): 38 | ''' 39 | API action to be called every time a user acquires a dataset in an external service. 40 | 41 | This API should be called to add the user to the list of allowed users. 42 | 43 | Since each service can provide a different way of pushing the data, the received 44 | data will be forwarded to the parser set in the preferences. This parser should 45 | return a dict similar to the following one: 46 | {'errors': ["...", "...", ...] 47 | 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]} 48 | 1) 'errors' contains the list of errors. It should be empty if no errors arised 49 | while the notification is parsed 50 | 2) 'users_datasets' is the lists of datasets available for each user (each element 51 | of this list is a dictionary with two fields: user and datasets). 52 | 53 | :parameter request_data: Depends on the parser 54 | :type request_data: dict 55 | 56 | :return: A list of warnings or None if the list of warnings is empty 57 | :rtype: dict 58 | 59 | ''' 60 | context['method'] = 'grant' 61 | return _process_package(context, request_data) 62 | 63 | 64 | def acquisitions_list(context, data_dict): 65 | ''' 66 | API to retrieve the list of datasets that have been acquired by a certain user 67 | 68 | :parameter user: The user whose acquired dataset you want to retrieve. This parameter 69 | is optional. If you don't include this identifier, the system will use the one 70 | of the user that is performing the request 71 | :type user: string 72 | 73 | :return: The list of datarequest that has been acquired by the specified user 74 | :rtype: list 75 | ''' 76 | 77 | if data_dict is None: 78 | data_dict = {} 79 | 80 | if 'user' not in data_dict and 'user' in context: 81 | data_dict['user'] = context['user'] 82 | 83 | plugins.toolkit.check_access(constants.ACQUISITIONS_LIST, context.copy(), data_dict) 84 | 85 | # Init db 86 | db.init_db(context['model']) 87 | 88 | # Init the result array 89 | result = [] 90 | 91 | # Check that the user exists 92 | try: 93 | plugins.toolkit.get_validator('user_name_exists')(data_dict['user'], context.copy()) 94 | except Exception: 95 | raise plugins.toolkit.ValidationError('User %s does not exist' % data_dict['user']) 96 | 97 | # Get the datasets acquired by the user 98 | query = db.AllowedUser.get(user_name=data_dict['user']) 99 | 100 | # Get the datasets 101 | for dataset in query: 102 | try: 103 | dataset_show_func = 'package_show' 104 | func_data_dict = {'id': dataset.package_id} 105 | internal_context = context.copy() 106 | 107 | # Check that the the dataset can be accessed and get its data 108 | # FIX: If the check_access function is not called, an exception is risen. 109 | plugins.toolkit.check_access(dataset_show_func, internal_context, func_data_dict) 110 | dataset_dict = plugins.toolkit.get_action(dataset_show_func)(internal_context, func_data_dict) 111 | 112 | # Only packages with state == 'active' can be shown 113 | if dataset_dict.get('state', None) == 'active': 114 | result.append(dataset_dict) 115 | except Exception: 116 | pass 117 | 118 | return result 119 | 120 | 121 | def revoke_access(context, request_data): 122 | ''' 123 | API action to be called in order to revoke access grants of an user. 124 | 125 | This API should be called to delete the user from the list of allowed users. 126 | 127 | Since each service can provide a different way of pushing the data, the received 128 | data will be forwarded to the parser set in the preferences. This parser should 129 | return a dict similar to the following one: 130 | {'errors': ["...", "...", ...] 131 | 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]} 132 | 1) 'errors' contains the list of errors. It should be empty if no errors arised 133 | while the notification is parsed 134 | 2) 'users_datasets' is the lists of datasets available for each user (each element 135 | of this list is a dictionary with two fields: user and datasets). 136 | 137 | :parameter request_data: Depends on the parser 138 | :type request_data: dict 139 | 140 | :return: A list of warnings or None if the list of warnings is empty 141 | :rtype: dict 142 | 143 | ''' 144 | context['method'] = 'revoke' 145 | return _process_package(context, request_data) 146 | 147 | 148 | def _process_package(context, request_data): 149 | log.info('Notification received: %s' % request_data) 150 | 151 | # Check access 152 | method = constants.PACKAGE_ACQUIRED if context.get('method') == 'grant' else constants.PACKAGE_DELETED 153 | plugins.toolkit.check_access(method, context, request_data) 154 | 155 | # Get the parser from the configuration 156 | class_path = os.environ.get(PARSER_CONFIG_PROP.upper().replace('.', '_'), plugins.toolkit.config.get(PARSER_CONFIG_PROP, '')) 157 | 158 | if class_path != '': 159 | try: 160 | cls = class_path.split(':') 161 | class_package = cls[0] 162 | class_name = cls[1] 163 | parser_cls = getattr(importlib.import_module(class_package), class_name) 164 | parser = parser_cls() 165 | except Exception as e: 166 | raise plugins.toolkit.ValidationError({'message': '%s: %s' % (type(e).__name__, str(e))}) 167 | else: 168 | raise plugins.toolkit.ValidationError({'message': '%s not configured' % PARSER_CONFIG_PROP}) 169 | 170 | # Parse the result using the parser set in the configuration 171 | # Expected result: {'errors': ["...", "...", ...] 172 | # 'users_datasets': [{'user': 'user_name', 'datasets': ['ds1', 'ds2', ...]}, ...]} 173 | result = parser.parse_notification(request_data) 174 | 175 | warns = [] 176 | 177 | for user_info in result['users_datasets']: 178 | for dataset_id in user_info['datasets']: 179 | 180 | try: 181 | context_pkg_show = context.copy() 182 | context_pkg_show['ignore_auth'] = True 183 | context_pkg_show[constants.CONTEXT_CALLBACK] = True 184 | dataset = plugins.toolkit.get_action('package_show')(context_pkg_show, {'id': dataset_id}) 185 | 186 | # This operation can only be performed with private datasets 187 | # This check is redundant since the package_update function will throw an exception 188 | # if a list of allowed users is included in a public dataset. However, this check 189 | # should be performed in order to avoid strange future exceptions 190 | if dataset.get('private', None) is True: 191 | 192 | # Create the array if it does not exist 193 | if constants.ALLOWED_USERS not in dataset or dataset[constants.ALLOWED_USERS] is None: 194 | dataset[constants.ALLOWED_USERS] = [] 195 | 196 | method = context['method'] == 'grant' 197 | present = user_info['user'] in dataset[constants.ALLOWED_USERS] 198 | # Deletes the user only if it is in the list 199 | if (not method and present) or (method and not present): 200 | if method: 201 | dataset[constants.ALLOWED_USERS].append(user_info['user']) 202 | else: 203 | dataset[constants.ALLOWED_USERS].remove(user_info['user']) 204 | 205 | context_pkg_update = context.copy() 206 | context_pkg_update['ignore_auth'] = True 207 | 208 | # Set creator as the user who is performing the changes 209 | user_show = plugins.toolkit.get_action('user_show') 210 | creator_user_id = dataset.get('creator_user_id', '') 211 | user_show_context = {'ignore_auth': True} 212 | user = user_show(user_show_context, {'id': creator_user_id}) 213 | context_pkg_update['user'] = user.get('name', '') 214 | 215 | plugins.toolkit.get_action('package_update')(context_pkg_update, dataset) 216 | log.info('Action %s access to dataset ended successfully' % context['method']) 217 | else: 218 | log.warn('Action %s access to dataset not completed. The dataset %s already %s access to the user %s' % (context['method'], dataset_id, context['method'], user_info['user'])) 219 | else: 220 | log.warn('Dataset %s is public. Cannot %s access to users' % (dataset_id, context['method'])) 221 | warns.append('Unable to upload the dataset %s: It\'s a public dataset' % dataset_id) 222 | 223 | except plugins.toolkit.ObjectNotFound: 224 | # If a dataset does not exist in the instance, an error message will be returned to the user. 225 | # However the process won't stop and the process will continue with the remaining datasets. 226 | log.warn('Dataset %s was not found in this instance' % dataset_id) 227 | warns.append('Dataset %s was not found in this instance' % dataset_id) 228 | except plugins.toolkit.ValidationError as e: 229 | # Some datasets does not allow to introduce the list of allowed users since this property is 230 | # only valid for private datasets outside an organization. In this case, a wanr will return 231 | # but the process will continue 232 | # WARN: This exception should not be risen anymore since public datasets are not updated. 233 | message = '%s(%s): %s' % (dataset_id, constants.ALLOWED_USERS, e.error_dict[constants.ALLOWED_USERS][0]) 234 | log.warn(message) 235 | warns.append(message) 236 | 237 | # Return warnings that inform about non-existing datasets 238 | if len(warns) > 0: 239 | return {'warns': warns} 240 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_auth.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | import unittest 21 | import ckanext.privatedatasets.auth as auth 22 | 23 | from mock import MagicMock 24 | from parameterized import parameterized 25 | 26 | 27 | class AuthTest(unittest.TestCase): 28 | 29 | def setUp(self): 30 | # Create mocks 31 | self._logic_auth = auth.logic_auth 32 | auth.logic_auth = MagicMock() 33 | 34 | self._request = auth.request 35 | auth.request = MagicMock() 36 | 37 | self._helpers = auth.helpers 38 | auth.helpers = MagicMock() 39 | 40 | self._authz = auth.authz 41 | auth.authz = MagicMock() 42 | 43 | self._tk = auth.tk 44 | auth.tk = MagicMock() 45 | 46 | self._db = auth.db 47 | auth.db = MagicMock() 48 | 49 | def tearDown(self): 50 | auth.logic_auth = self._logic_auth 51 | auth.request = self._request 52 | auth.helpers = self._helpers 53 | auth.authz = self._authz 54 | auth.tk = self._tk 55 | auth.db = self._db 56 | if hasattr(self, '_package_show'): 57 | auth.package_show = self._package_show 58 | 59 | def test_decordators(self): 60 | self.assertEquals(True, getattr(auth.package_show, 'auth_allow_anonymous_access', False)) 61 | self.assertEquals(True, getattr(auth.resource_show, 'auth_allow_anonymous_access', False)) 62 | self.assertEquals(True, getattr(auth.package_acquired, 'auth_allow_anonymous_access', False)) 63 | 64 | @parameterized.expand([ 65 | # Anonymous user (public) 66 | (None, None, None, False, 'active', None, None, None, None, None, True), 67 | # Anonymous user (private) 68 | (None, None, None, True, 'active', None, None, None, None, '/', True), 69 | (None, None, '', True, 'active', None, None, '', None, '/', True), 70 | # Anonymous user (private). Buy URL not shown 71 | (None, None, None, True, 'active', None, None, None, 'google.es', '/', True), 72 | # Anonymous user (private). Buy URL show 73 | (None, None, None, True, 'active', None, None, None, 'google.es', '/dataset/testds', True), 74 | # The creator can always see the dataset 75 | (1, 1, None, False, 'active', None, None, None, None, None, True), 76 | (1, 1, None, True, 'active', 'conwet', None, None, None, None, True), 77 | (1, 1, None, True, 'active', None, None, None, None, None, True), 78 | (1, 1, None, False, 'draft', None, None, None, None, None, True), 79 | # Other user (no organizations) 80 | (1, 2, 'test', False, 'active', None, None, None, None, None, True), 81 | (1, 2, 'test', True, 'active', None, None, None, 'google.es', '/', True), # Buy MSG not shown 82 | (1, 2, 'test', True, 'active', None, None, None, None, '/dataset/testds', True), # Buy MSG not shown 83 | (1, 2, 'test', True, 'active', None, None, None, 'google.es', '/dataset/testds', True), # Buy MSG shown 84 | (1, 2, 'test', False, 'draft', None, None, None, None, None, False), 85 | # Other user but authorized in the list of authorized users 86 | (1, 2, 'test', True, 'active', None, None, True, None, None, True), 87 | # Other user and not authorized in the list of authorized users 88 | (1, 2, 'test', True, 'active', None, None, False, 'google.es', '/', True), 89 | (1, 2, 'test', True, 'active', None, None, False, 'google.es', '/dataset/testds', True), 90 | # Other user with organizations 91 | (1, 2, 'test', False, 'active', 'conwet', False, None, None, None, True), 92 | (1, 2, 'test', True, 'active', 'conwet', False, None, None, None, True), 93 | (1, 2, 'test', True, 'active', 'conwet', True, None, None, None, True), 94 | (1, 2, 'test', True, 'draft', 'conwet', True, None, None, None, False), 95 | # Other user with organizations (user is not in the organization) 96 | (1, 2, 'test', True, 'active', 'conwet', False, True, None, None, True), 97 | (1, 2, 'test', True, 'active', 'conwet', False, False, None, None, True), 98 | (1, 2, 'test', True, 'active', 'conwet', False, False, 'google.es', '/dataset/testds', True), 99 | (1, 2, 'test', True, 'active', 'conwet', False, False, 'google.es', '/', True) 100 | ]) 101 | def test_auth_package_show(self, creator_user_id, user_obj_id, user, private, state, owner_org, 102 | owner_member, db_auth, acquire_url, request_path, authorized): 103 | 104 | # Configure the mocks 105 | returned_package = MagicMock() 106 | returned_package.creator_user_id = creator_user_id 107 | returned_package.private = private 108 | returned_package.state = state 109 | returned_package.owner_org = owner_org 110 | returned_package.extras = {} 111 | 112 | # Configure the database 113 | db_response = [] 114 | if db_auth is True: 115 | out = auth.db.AllowedUser() 116 | out.package_id = 'package_id' 117 | out.user_name = user 118 | db_response.append(out) 119 | 120 | auth.db.AllowedUser.get = MagicMock(return_value=db_response) 121 | 122 | if acquire_url: 123 | returned_package.extras['acquire_url'] = acquire_url 124 | 125 | auth.logic_auth.get_package_object = MagicMock(return_value=returned_package) 126 | auth.authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member) 127 | auth.request.path = request_path 128 | 129 | # Prepare the context 130 | context = {'model': MagicMock()} 131 | if user is not None: 132 | context['user'] = user 133 | if user_obj_id is not None: 134 | context['auth_user_obj'] = MagicMock() 135 | context['auth_user_obj'].id = user_obj_id 136 | 137 | # Function to be tested 138 | result = auth.package_show(context, {}) 139 | 140 | # Check the result 141 | self.assertEquals(authorized, result['success']) 142 | 143 | # Permissions for organization are checked when the dataset is private, it belongs to an organization 144 | # and when the dataset has not been created by the user who is asking for it 145 | if private and owner_org and state == 'active' and creator_user_id != user_obj_id: 146 | auth.authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'read') 147 | else: 148 | self.assertEquals(0, auth.authz.has_user_permission_for_group_or_org.call_count) 149 | 150 | # The database is only initialized when: 151 | # * the dataset is private AND 152 | # * the dataset is active AND 153 | # * the dataset has no organization OR the user does not belong to that organization AND 154 | # * the dataset has not been created by the user who is asking for it OR the user is not specified 155 | if private and state == 'active' and (not owner_org or not owner_member) and (creator_user_id != user_obj_id or user_obj_id is None): 156 | # Check that the database has been initialized properly 157 | auth.db.init_db.assert_called_once_with(context['model']) 158 | else: 159 | self.assertEquals(0, auth.db.init_db.call_count) 160 | 161 | # Conditions to buy a dataset; It should be private, active and should not belong to any organization 162 | if authorized and state == 'active' and request_path and request_path.startswith('/dataset/') and acquire_url: 163 | auth.helpers.flash_notice.assert_called_once() 164 | else: 165 | self.assertEquals(0, auth.helpers.flash_notice.call_count) 166 | 167 | @parameterized.expand([ 168 | (None, None, None, None, None, False), # Anonymous user 169 | (1, 1, None, None, None, True), # A user can edit its dataset 170 | (1, 2, None, None, None, False), # A user cannot edit a dataset belonging to another user 171 | (1, 2, 'test', 'conwet', False, False), # User without rights to update a dataset 172 | (1, 2, 'test', 'conwet', True, True), # User with rights to update a dataset 173 | ]) 174 | def test_auth_package_update(self, creator_user_id, user_obj_id, user, owner_org, owner_member, authorized): 175 | 176 | # Configure the mocks 177 | returned_package = MagicMock() 178 | returned_package.creator_user_id = creator_user_id 179 | returned_package.owner_org = owner_org 180 | 181 | auth.logic_auth.get_package_object = MagicMock(return_value=returned_package) 182 | auth.authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member) 183 | 184 | # Prepare the context 185 | context = {} 186 | if user is not None: 187 | context['user'] = user 188 | if user_obj_id is not None: 189 | context['auth_user_obj'] = MagicMock() 190 | context['auth_user_obj'].id = user_obj_id 191 | 192 | # Function to be tested 193 | result = auth.package_update(context, {}) 194 | 195 | # Check the result 196 | self.assertEquals(authorized, result['success']) 197 | 198 | # Permissions for organization are checked when the user asking to update the dataset is not the creator 199 | # and when the dataset has organization 200 | if creator_user_id != user_obj_id and owner_org: 201 | auth.authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'update_dataset') 202 | else: 203 | self.assertEquals(0, auth.authz.has_user_permission_for_group_or_org.call_count) 204 | 205 | @parameterized.expand([ 206 | # if package dont exist 207 | (False, None, None, None, False, 'active', None, None, None, False), 208 | # Anonymous user only can view resources of a public and active package 209 | (True, None, None, None, False, 'active', None, None, None, True), 210 | (True, None, None, None, True, 'active', None, None, None, False), 211 | (True, None, None, '', True, 'active', None, None, None, False), 212 | # The creator can always see the resource 213 | (True, 1, 1, None, False, 'active', None, None, None, True), 214 | (True, 1, 1, None, True, 'active', None, None, None, True), 215 | (True, 1, 1, None, True, 'draft', None, None, None, True), 216 | (True, 1, 1, None, False, 'draft', None, None, None, True), 217 | # Other user (no organizations) 218 | (True, 1, 2, 'test', False, 'active', None, None, None, True), 219 | (True, 1, 2, 'test', True, 'active', None, None, None, False), 220 | (True, 1, 2, 'test', True, 'draft', None, None, None, False), 221 | # Other user but authorized in the list of authorized users 222 | (True, 1, 2, 'test', True, 'active', None, None, True, True), 223 | # Other user and not authorized in the list of authorized users 224 | (True, 1, 2, 'test', True, 'active', None, None, False, False), 225 | # Other user with organizations 226 | (True, 1, 2, 'test', True, 'active', 'conwet', False, None, False), 227 | (True, 1, 2, 'test', True, 'active', 'conwet', True, None, True), 228 | 229 | ]) 230 | def test_auth_resource_show(self, exist_pkg, creator_user_id, user_obj_id, user, private, state, owner_org, 231 | owner_member, db_auth, authorized): 232 | #Recover the exception 233 | auth.tk.ObjectNotFound = self._tk.ObjectNotFound 234 | 235 | # Configure the mocks 236 | if exist_pkg: 237 | returned_package = MagicMock() 238 | returned_package.creator_user_id = creator_user_id 239 | returned_package.private = private 240 | returned_package.state = state 241 | returned_package.owner_org = owner_org 242 | returned_package.extras = {} 243 | else: 244 | returned_package = None 245 | 246 | returned_resource = MagicMock() 247 | returned_resource.package_id = 1 248 | 249 | # Configure the database 250 | db_response = [] 251 | if db_auth is True: 252 | out = auth.db.AllowedUser() 253 | out.package_id = 'package_id' 254 | out.user_name = user 255 | db_response.append(out) 256 | 257 | # Prepare the context 258 | context = {'model': MagicMock()} 259 | if user is not None: 260 | context['user'] = user 261 | if user_obj_id is not None: 262 | context['auth_user_obj'] = MagicMock() 263 | context['auth_user_obj'].id = user_obj_id 264 | 265 | auth.db.AllowedUser.get = MagicMock(return_value=db_response) 266 | auth.logic_auth.get_resource_object = MagicMock(return_value=returned_resource) 267 | auth.logic_auth.get_package_object = MagicMock(return_value=returned_package) 268 | auth.authz.has_user_permission_for_group_or_org = MagicMock(return_value=owner_member) 269 | 270 | # Prepare the context 271 | context = {'model': MagicMock()} 272 | if user is not None: 273 | context['user'] = user 274 | if user_obj_id is not None: 275 | context['auth_user_obj'] = MagicMock() 276 | context['auth_user_obj'].id = user_obj_id 277 | 278 | if not exist_pkg: 279 | self.assertRaises(self._tk.ObjectNotFound, auth.resource_show, context, {}) 280 | else: 281 | result = auth.resource_show(context, {}) 282 | self.assertEquals(authorized, result['success']) 283 | 284 | if private and owner_org and state == 'active' and creator_user_id != user_obj_id: 285 | auth.authz.has_user_permission_for_group_or_org.assert_called_once_with(owner_org, user, 'read') 286 | else: 287 | self.assertEquals(0, auth.authz.has_user_permission_for_group_or_org.call_count) 288 | 289 | if private and state == 'active' and (not owner_org or not owner_member) and (creator_user_id != user_obj_id or user_obj_id is None): 290 | # Check that the database has been initialized properly 291 | auth.db.init_db.assert_called_once_with(context['model']) 292 | else: 293 | self.assertEquals(0, auth.db.init_db.call_count) 294 | 295 | 296 | def test_package_acquired(self): 297 | self.assertTrue(auth.package_acquired({}, {})['success']) 298 | 299 | def test_package_deleted(self): 300 | self.assertTrue(auth.revoke_access({}, {})['success']) 301 | 302 | @parameterized.expand([ 303 | ({'user': 'user_1'}, {'user': 'user_1'}, True), 304 | ({'user': 'user_2'}, {'user': 'user_1'}, False), 305 | ]) 306 | def test_acquisitions_list(self, context, data_dict, expected_result): 307 | self.assertEquals(expected_result, auth.acquisitions_list(context, data_dict)['success']) 308 | 309 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid 4 | # Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | from __future__ import absolute_import, unicode_literals 22 | 23 | from ckan import model, plugins as p 24 | from ckan.lib import search 25 | from ckan.lib.plugins import DefaultPermissionLabels 26 | from ckan.plugins import toolkit as tk 27 | from flask import Blueprint 28 | 29 | from ckanext.privatedatasets import auth, actions, constants, converters_validators as conv_val, db, helpers 30 | from ckanext.privatedatasets.views import acquired_datasets 31 | 32 | 33 | HIDDEN_FIELDS = [constants.ALLOWED_USERS, constants.SEARCHABLE] 34 | 35 | 36 | class PrivateDatasets(p.SingletonPlugin, tk.DefaultDatasetForm, DefaultPermissionLabels): 37 | 38 | p.implements(p.IDatasetForm) 39 | p.implements(p.IAuthFunctions) 40 | p.implements(p.IConfigurer) 41 | p.implements(p.IBlueprint) 42 | p.implements(p.IRoutes, inherit=True) 43 | p.implements(p.IActions) 44 | p.implements(p.IPackageController, inherit=True) 45 | p.implements(p.ITemplateHelpers) 46 | p.implements(p.IPermissionLabels) 47 | p.implements(p.IResourceController) 48 | 49 | ###################################################################### 50 | ############################ DATASET FORM ############################ 51 | ###################################################################### 52 | 53 | def __init__(self, name=None): 54 | self.indexer = search.PackageSearchIndex() 55 | 56 | def _modify_package_schema(self): 57 | return { 58 | # remove datasets_with_no_organization_cannot_be_private validator 59 | 'private': [tk.get_validator('ignore_missing'), 60 | tk.get_validator('boolean_validator')], 61 | constants.ALLOWED_USERS_STR: [tk.get_validator('ignore_missing'), 62 | conv_val.private_datasets_metadata_checker], 63 | constants.ALLOWED_USERS: [conv_val.allowed_users_convert, 64 | tk.get_validator('ignore_missing'), 65 | conv_val.private_datasets_metadata_checker], 66 | constants.ACQUIRE_URL: [tk.get_validator('ignore_missing'), 67 | conv_val.private_datasets_metadata_checker, 68 | conv_val.url_checker, 69 | tk.get_converter('convert_to_extras')], 70 | constants.SEARCHABLE: [tk.get_validator('ignore_missing'), 71 | conv_val.private_datasets_metadata_checker, 72 | tk.get_converter('convert_to_extras'), 73 | tk.get_validator('boolean_validator')] 74 | } 75 | 76 | def create_package_schema(self): 77 | # grab the default schema in our plugin 78 | schema = super(PrivateDatasets, self).create_package_schema() 79 | schema.update(self._modify_package_schema()) 80 | return schema 81 | 82 | def update_package_schema(self): 83 | # grab the default schema in our plugin 84 | schema = super(PrivateDatasets, self).update_package_schema() 85 | schema.update(self._modify_package_schema()) 86 | return schema 87 | 88 | def show_package_schema(self): 89 | schema = super(PrivateDatasets, self).show_package_schema() 90 | schema.update({ 91 | constants.ALLOWED_USERS: [conv_val.get_allowed_users, 92 | tk.get_validator('ignore_missing')], 93 | constants.ACQUIRE_URL: [tk.get_converter('convert_from_extras'), 94 | tk.get_validator('ignore_missing')], 95 | constants.SEARCHABLE: [tk.get_converter('convert_from_extras'), 96 | tk.get_validator('ignore_missing')] 97 | }) 98 | return schema 99 | 100 | def is_fallback(self): 101 | # Return True to register this plugin as the default handler for 102 | # package types not handled by any other IDatasetForm plugin. 103 | return True 104 | 105 | def package_types(self): 106 | # This plugin doesn't handle any special package types, it just 107 | # registers itself as the default (above). 108 | return [] 109 | 110 | ###################################################################### 111 | ########################### AUTH FUNCTIONS ########################### 112 | ###################################################################### 113 | 114 | def get_auth_functions(self): 115 | auth_functions = {'package_show': auth.package_show, 116 | 'package_update': auth.package_update, 117 | 'resource_show': auth.resource_show, 118 | constants.PACKAGE_ACQUIRED: auth.package_acquired, 119 | constants.ACQUISITIONS_LIST: auth.acquisitions_list, 120 | constants.PACKAGE_DELETED: auth.revoke_access} 121 | 122 | return auth_functions 123 | 124 | ###################################################################### 125 | ############################ ICONFIGURER ############################# 126 | ###################################################################### 127 | 128 | def update_config(self, config): 129 | # Add this plugin's templates dir to CKAN's extra_template_paths, so 130 | # that CKAN will use this plugin's custom templates. 131 | if p.toolkit.check_ckan_version(min_version='2.8'): 132 | tk.add_template_directory(config, 'templates_2.8') 133 | else: 134 | tk.add_template_directory(config, 'templates') 135 | 136 | # Register this plugin's fanstatic directory with CKAN. 137 | tk.add_resource(b'fanstatic', b'privatedatasets') 138 | 139 | ###################################################################### 140 | ############################# IBLUEPRINT ############################# 141 | ###################################################################### 142 | 143 | # Deprecated but Required for CKAN 2.7 144 | def before_map(self, m): 145 | if p.toolkit.check_ckan_version(max_version='2.7.99'): 146 | m.connect('user_acquired_datasets', '/dashboard/acquired', ckan_icon='shopping-cart', 147 | controller='ckanext.privatedatasets.views:AcquiredDatasetsControllerUI', 148 | action='acquired_datasets', conditions=dict(method=['GET'])) 149 | return m 150 | 151 | def get_blueprint(self): 152 | blueprint = Blueprint('privatedatasets', self.__module__) 153 | if p.toolkit.check_ckan_version(min_version='2.8'): 154 | blueprint.add_url_rule('/dashboard/acquired', 'acquired_datasets', acquired_datasets) 155 | return blueprint 156 | 157 | ###################################################################### 158 | ############################## IACTIONS ############################## 159 | ###################################################################### 160 | 161 | def get_actions(self): 162 | action_functions = {constants.PACKAGE_ACQUIRED: actions.package_acquired, 163 | constants.ACQUISITIONS_LIST: actions.acquisitions_list, 164 | constants.PACKAGE_DELETED: actions.revoke_access} 165 | 166 | return action_functions 167 | 168 | ###################################################################### 169 | ######################### IPACKAGECONTROLLER ######################### 170 | ###################################################################### 171 | 172 | def _delete_pkg_atts(self, pkg_dict, attrs): 173 | for attr in attrs: 174 | if attr in pkg_dict: 175 | del pkg_dict[attr] 176 | 177 | def before_index(self, pkg_dict): 178 | 179 | if 'extras_' + constants.SEARCHABLE in pkg_dict: 180 | if pkg_dict['extras_searchable'] == 'False': 181 | pkg_dict['capacity'] = 'private' 182 | else: 183 | pkg_dict['capacity'] = 'public' 184 | 185 | return pkg_dict 186 | 187 | def after_create(self, context, pkg_dict): 188 | session = context['session'] 189 | update_cache = False 190 | 191 | db.init_db(context['model']) 192 | 193 | # Get the users and the package ID 194 | if constants.ALLOWED_USERS in pkg_dict: 195 | 196 | allowed_users = pkg_dict[constants.ALLOWED_USERS] 197 | package_id = pkg_dict['id'] 198 | 199 | # Get current users 200 | users = db.AllowedUser.get(package_id=package_id) 201 | 202 | # Delete users and save the list of current users 203 | current_users = [] 204 | for user in users: 205 | current_users.append(user.user_name) 206 | if user.user_name not in allowed_users: 207 | session.delete(user) 208 | update_cache = True 209 | 210 | # Add non existing users 211 | for user_name in allowed_users: 212 | if user_name not in current_users: 213 | out = db.AllowedUser() 214 | out.package_id = package_id 215 | out.user_name = user_name 216 | out.save() 217 | session.add(out) 218 | update_cache = True 219 | 220 | session.commit() 221 | 222 | # The cache should be updated. Otherwise, the system may return 223 | # outdated information in future requests 224 | if update_cache: 225 | new_pkg_dict = tk.get_action('package_show')( 226 | {'model': context['model'], 227 | 'ignore_auth': True, 228 | 'validate': False, 229 | 'use_cache': False}, 230 | {'id': package_id}) 231 | 232 | # Prevent acquired datasets jumping to the first position 233 | revision = tk.get_action('revision_show')({'ignore_auth': True}, {'id': new_pkg_dict['revision_id']}) 234 | new_pkg_dict['metadata_modified'] = revision.get('timestamp', '') 235 | self.indexer.update_dict(new_pkg_dict) 236 | 237 | return pkg_dict 238 | 239 | def after_update(self, context, pkg_dict): 240 | return self.after_create(context, pkg_dict) 241 | 242 | def after_show(self, context, pkg_dict): 243 | 244 | void = False; 245 | 246 | for resource in pkg_dict['resources']: 247 | if resource == {}: 248 | void = True 249 | 250 | if void: 251 | del pkg_dict['resources'] 252 | del pkg_dict['num_resources'] 253 | 254 | user_obj = context.get('auth_user_obj') 255 | updating_via_api = context.get(constants.CONTEXT_CALLBACK, False) 256 | 257 | # allowed_users and searchable fileds can be only viewed by (and only if the dataset is private): 258 | # * the dataset creator 259 | # * the sysadmin 260 | # * users allowed to update the allowed_users list via the notification API 261 | if pkg_dict.get('private') is False or not updating_via_api and (not user_obj or (pkg_dict['creator_user_id'] != user_obj.id and not user_obj.sysadmin)): 262 | # The original list cannot be modified 263 | attrs = list(HIDDEN_FIELDS) 264 | self._delete_pkg_atts(pkg_dict, attrs) 265 | 266 | return pkg_dict 267 | 268 | def after_delete(self, context, pkg_dict): 269 | session = context['session'] 270 | package_id = pkg_dict['id'] 271 | 272 | # Get current users 273 | db.init_db(context['model']) 274 | users = db.AllowedUser.get(package_id=package_id) 275 | 276 | # Delete all the users 277 | for user in users: 278 | session.delete(user) 279 | session.commit() 280 | 281 | return pkg_dict 282 | 283 | def after_search(self, search_results, search_params): 284 | for result in search_results['results']: 285 | # Extra fields should not be returned 286 | # The original list cannot be modified 287 | attrs = list(HIDDEN_FIELDS) 288 | 289 | # Additionally, resources should not be included if the user is not allowed 290 | # to show the resource 291 | context = { 292 | 'model': model, 293 | 'session': model.Session, 294 | 'user': tk.c.user, 295 | 'user_obj': tk.c.userobj 296 | } 297 | 298 | try: 299 | tk.check_access('package_show', context, result) 300 | except tk.NotAuthorized: 301 | # NotAuthorized exception is risen when the user is not allowed 302 | # to read the package. 303 | attrs.append('resources') 304 | # Delete 305 | self._delete_pkg_atts(result, attrs) 306 | 307 | return search_results 308 | 309 | #### 310 | def before_view(self, pkg_dict): 311 | 312 | for resource in pkg_dict['resources']: 313 | 314 | context = { 315 | 'model': model, 316 | 'session': model.Session, 317 | 'user': tk.c.user, 318 | 'user_obj': tk.c.userobj 319 | } 320 | 321 | try: 322 | tk.check_access('resource_show', context, resource) 323 | except tk.NotAuthorized: 324 | pkg_dict['resources'].remove(resource) 325 | pkg_dict = self.before_view(pkg_dict) 326 | return pkg_dict 327 | 328 | def get_dataset_labels(self, dataset_obj): 329 | labels = super(PrivateDatasets, self).get_dataset_labels( 330 | dataset_obj) 331 | 332 | if getattr(dataset_obj, 'searchable', False): 333 | labels.append('searchable') 334 | 335 | return labels 336 | 337 | def get_user_dataset_labels(self, user_obj): 338 | labels = super(PrivateDatasets, self).get_user_dataset_labels( 339 | user_obj) 340 | 341 | labels.append('searchable') 342 | return labels 343 | 344 | ###################################################################### 345 | ######################### IRESOURCECONTROLLER ######################## 346 | ###################################################################### 347 | 348 | def before_create(self, context, resource): 349 | pass 350 | 351 | def before_update(self, context, current, resource): 352 | pass 353 | 354 | def before_delete(self, context, resource, resources): 355 | pass 356 | 357 | def before_show(self, resource_dict): 358 | 359 | context = { 360 | 'model': model, 361 | 'session': model.Session, 362 | 'user': tk.c.user, 363 | 'user_obj': tk.c.userobj 364 | } 365 | 366 | try: 367 | tk.check_access('resource_show', context, resource_dict) 368 | except tk.NotAuthorized: 369 | resource_dict.clear() 370 | return resource_dict 371 | 372 | ###################################################################### 373 | ######################### ITEMPLATESHELPER ########################### 374 | ###################################################################### 375 | 376 | def get_helpers(self): 377 | return {'is_dataset_acquired': helpers.is_dataset_acquired, 378 | 'get_allowed_users_str': helpers.get_allowed_users_str, 379 | 'is_owner': helpers.is_owner, 380 | 'can_read': helpers.can_read, 381 | 'show_acquire_url_on_create': helpers.show_acquire_url_on_create, 382 | 'show_acquire_url_on_edit': helpers.show_acquire_url_on_edit, 383 | 'acquire_button': helpers.acquire_button 384 | } 385 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_actions.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | import ckanext.privatedatasets.actions as actions 21 | import unittest 22 | 23 | from mock import MagicMock 24 | from parameterized import parameterized 25 | 26 | PARSER_CONFIG_PROP = 'ckan.privatedatasets.parser' 27 | IMPORT_ERROR_MSG = 'Unable to load the module' 28 | CLASS_NAME = 'parser_class' 29 | ADD_USERS_ERROR = 'Error updating the dataset' 30 | 31 | 32 | class ActionsTest(unittest.TestCase): 33 | 34 | def setUp(self): 35 | 36 | # Load the mocks 37 | self._importlib = actions.importlib 38 | actions.importlib = MagicMock() 39 | 40 | self._plugins = actions.plugins 41 | actions.plugins = MagicMock() 42 | 43 | self._db = actions.db 44 | actions.db = MagicMock() 45 | 46 | def tearDown(self): 47 | # Unmock 48 | actions.importlib = self._importlib 49 | actions.plugins = self._plugins 50 | actions.db = self._db 51 | 52 | @parameterized.expand([ 53 | ('', None, False, False, '%s not configured' % PARSER_CONFIG_PROP), 54 | ('INVALID_CLASS', None, False, False, 'IndexError: list index out of range'), 55 | ('INVALID.CLASS', None, False, False, 'IndexError: list index out of range'), 56 | ('valid.path', CLASS_NAME, False, False, 'ImportError: %s' % IMPORT_ERROR_MSG), 57 | ('valid.path', CLASS_NAME, False, True, 'ImportError: %s' % IMPORT_ERROR_MSG), 58 | ('valid.path', CLASS_NAME, True, False, 'AttributeError: %s' % CLASS_NAME), 59 | ('valid.path', CLASS_NAME, True, True, None) 60 | 61 | ]) 62 | def test_class_cannot_be_loaded(self, class_path, class_name, path_exist, class_exist, expected_error): 63 | class_package = class_path 64 | class_package += ':' + class_name if class_name else '' 65 | actions.plugins.toolkit.config = {PARSER_CONFIG_PROP: class_package} 66 | 67 | # Recover exception 68 | actions.plugins.toolkit.ValidationError = self._plugins.toolkit.ValidationError 69 | 70 | # Configure the mock 71 | package = MagicMock() 72 | if class_name and not class_exist: 73 | delattr(package, class_name) 74 | 75 | actions.importlib.import_module = MagicMock(side_effect=ImportError(IMPORT_ERROR_MSG) if not path_exist else None, 76 | return_value=package if path_exist else None) 77 | 78 | if expected_error: 79 | with self.assertRaises(actions.plugins.toolkit.ValidationError) as cm: 80 | actions.package_acquired({}, {}) 81 | self.assertEqual(cm.exception.error_dict['message'], expected_error) 82 | else: 83 | # Exception is not risen 84 | self.assertEquals(None, actions.package_acquired({}, {})) 85 | 86 | # Checks 87 | self.assertEquals(0, actions.plugins.toolkit.get_action.call_count) 88 | 89 | def configure_mocks(self, parse_result, datasets_not_found=[], not_updatable_datasets=[], 90 | allowed_users=None, creator_user={'id': '1234', 'name': 'ckan'}): 91 | 92 | actions.plugins.toolkit.config = {PARSER_CONFIG_PROP: 'valid.path:%s' % CLASS_NAME} 93 | 94 | # Configure mocks 95 | parser_instance = MagicMock() 96 | parser_instance.parse_notification = MagicMock(return_value=parse_result) 97 | package = MagicMock() 98 | package.parser_class = MagicMock(return_value=parser_instance) 99 | 100 | actions.importlib.import_module = MagicMock(return_value=package) 101 | 102 | # We should use the real exceptions 103 | actions.plugins.toolkit.ObjectNotFound = self._plugins.toolkit.ObjectNotFound 104 | actions.plugins.toolkit.ValidationError = self._plugins.toolkit.ValidationError 105 | 106 | def _package_show(context, data_dict): 107 | if data_dict['id'] in datasets_not_found: 108 | raise actions.plugins.toolkit.ObjectNotFound() 109 | else: 110 | dataset = {'id': data_dict['id'], 'private': data_dict['id'] not in not_updatable_datasets, 111 | 'creator_user_id': creator_user['id']} 112 | if allowed_users is not None: 113 | dataset['allowed_users'] = list(allowed_users) 114 | return dataset 115 | 116 | def _package_update(context, data_dict): 117 | if data_dict['id'] in not_updatable_datasets: 118 | raise actions.plugins.toolkit.ValidationError({'allowed_users': [ADD_USERS_ERROR]}) 119 | 120 | def _user_show(context, data_dict): 121 | return {'name': creator_user['name'], 'id': data_dict['id']} 122 | 123 | package_show = MagicMock(side_effect=_package_show) 124 | package_update = MagicMock(side_effect=_package_update) 125 | user_show = MagicMock(side_effect=_user_show) 126 | 127 | def _get_action(action): 128 | if action == 'package_update': 129 | return package_update 130 | elif action == 'package_show': 131 | return package_show 132 | elif action == 'user_show': 133 | return user_show 134 | 135 | actions.plugins.toolkit.get_action = _get_action 136 | 137 | return parser_instance.parse_notification, package_show, package_update, user_show 138 | 139 | @parameterized.expand([ 140 | # Simple Test: one user and one dataset 141 | ({'user1': ['ds1']}, [], [], None), 142 | ({'user2': ['ds1']}, [], [], []), 143 | ({'user3': ['ds1']}, [], [], ['another_user']), 144 | ({'user4': ['ds1']}, [], [], ['another_user', 'another_one']), 145 | ({'user5': ['ds1']}, [], [], ['another_user', 'user_name']), 146 | ({'user6': ['ds1']}, ['ds1'], [], None), 147 | ({'user7': ['ds1']}, [], ['ds1'], []), 148 | ({'user8': ['ds1']}, [], ['ds1'], ['another_user']), 149 | ({'user9': ['ds1']}, [], ['ds1'], ['another_user', 'another_one']), 150 | ({'user1': ['ds1']}, [], ['ds1'], ['another_user', 'user_name']), 151 | 152 | # # Complex test: some users and some datasets 153 | ({'user1': ['ds1', 'ds2', 'ds3', 'ds4'], 'user2': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], []), 154 | ({'user3': ['ds1', 'ds2', 'ds3', 'ds4'], 'user4': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user']), 155 | ({'user5': ['ds1', 'ds2', 'ds3', 'ds4'], 'user6': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user', 'another_one']), 156 | ({'user7': ['ds1', 'ds2', 'ds3', 'ds4'], 'user8': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user', 'another_one', 'user7']) 157 | ]) 158 | def test_add_users(self, users_info, datasets_not_found, not_updatable_datasets, allowed_users=[]): 159 | 160 | parse_result = {'users_datasets': [{'user': user, 'datasets': users_info[user]} for user in users_info]} 161 | creator_user = {'name': 'ckan', 'id': '1234'} 162 | 163 | parse_notification, package_show, package_update, user_show = self.configure_mocks(parse_result, 164 | datasets_not_found, not_updatable_datasets, allowed_users, creator_user) 165 | 166 | # Call the function 167 | context = {'user': 'user1', 'model': 'model', 'auth_obj': {'id': 1}, 'method': 'grant'} 168 | result = actions.package_acquired(context, users_info) 169 | 170 | # Calculate the list of warns 171 | warns = [] 172 | for user_datasets in parse_result['users_datasets']: 173 | for dataset_id in user_datasets['datasets']: 174 | if dataset_id in datasets_not_found: 175 | warns.append('Dataset %s was not found in this instance' % dataset_id) 176 | elif dataset_id in not_updatable_datasets: 177 | # warns.append('%s(%s): %s' % (dataset_id, 'allowed_users', ADD_USERS_ERROR)) 178 | warns.append('Unable to upload the dataset %s: It\'s a public dataset' % dataset_id) 179 | 180 | expected_result = {'warns': warns} if len(warns) > 0 else None 181 | 182 | # Check that the returned result is as expected 183 | self.assertEquals(expected_result, result) 184 | 185 | # Check that the initial functions (check_access and parse_notification) has been called properly 186 | parse_notification.assert_called_once_with(users_info) 187 | actions.plugins.toolkit.check_access.assert_called_once_with('package_acquired', context, users_info) 188 | 189 | for user_datasets in parse_result['users_datasets']: 190 | for dataset_id in user_datasets['datasets']: 191 | # The show function is always called 192 | context_show = context.copy() 193 | context_show['ignore_auth'] = True 194 | context_show['updating_via_cb'] = True 195 | package_show.assert_any_call(context_show, {'id': dataset_id}) 196 | 197 | # The update function is called only when the show function does not throw an exception and 198 | # when the user is not in the list of allowed users. 199 | if dataset_id not in datasets_not_found and allowed_users is not None and user_datasets['user'] not in allowed_users and dataset_id not in not_updatable_datasets: 200 | # Calculate the list of allowed_users 201 | expected_allowed_users = list(allowed_users) 202 | expected_allowed_users.append(user_datasets['user']) 203 | 204 | context_update = context.copy() 205 | context_update['ignore_auth'] = True 206 | context_update['user'] = creator_user['name'] 207 | 208 | package_update.assert_any_call(context_update, {'id': dataset_id, 'allowed_users': expected_allowed_users, 'private': True, 'creator_user_id': creator_user['id']}) 209 | 210 | 211 | @parameterized.expand([ 212 | (None, {},), 213 | ({}, {2: actions.plugins.toolkit.ObjectNotFound},), 214 | ({'user': 'fiware'}, {1: actions.plugins.toolkit.NotAuthorized},), 215 | (None, {1: actions.plugins.toolkit.NotAuthorized, 2: actions.plugins.toolkit.ObjectNotFound},), 216 | ({}, {}, [1]), 217 | ({'user': 'fiware'}, {}, [3, 2]), 218 | (None, {1: actions.plugins.toolkit.NotAuthorized}, [2]), 219 | ({}, {1: actions.plugins.toolkit.NotAuthorized, 2: actions.plugins.toolkit.ObjectNotFound}, [1, 3]), 220 | ]) 221 | def test_acquisitions_list(self, data_dict, package_errors={}, deleted_packages=[]): 222 | 223 | pkgs_ids = [0, 1, 2, 3] 224 | user = 'example_user_test' 225 | actions.plugins.toolkit.c.user = user 226 | 227 | # get_action mock 228 | default_package = {'pkg_id': 0, 'test': 'ok', 'res': 'ta'} 229 | 230 | def _package_show(context, data_dict): 231 | if data_dict['id'] in package_errors: 232 | raise package_errors[data_dict['id']]('ERROR') 233 | else: 234 | pkg = default_package.copy() 235 | pkg['pkg_id'] = data_dict['id'] 236 | pkg['state'] = 'deleted' if data_dict['id'] in deleted_packages else 'active' 237 | return pkg 238 | 239 | package_show = MagicMock(side_effect=_package_show) 240 | actions.plugins.toolkit.get_action.return_value = package_show 241 | 242 | # query mock 243 | query_res = [] 244 | for i in pkgs_ids: 245 | pkg = MagicMock() 246 | pkg.package_id = i 247 | pkg.user_name = user 248 | query_res.append(pkg) 249 | 250 | actions.db.AllowedUser.get = MagicMock(return_value=query_res) 251 | 252 | # Context 253 | context = { 254 | 'model': MagicMock(), 255 | 'user': 'default_user' 256 | } 257 | 258 | # Call the function 259 | result = actions.acquisitions_list(context, data_dict) 260 | 261 | # Asset that check_access has been called 262 | actions.plugins.toolkit.chec_access(actions.constants.ACQUISITIONS_LIST, context, data_dict) 263 | 264 | # Check that the database has been initialized properly 265 | actions.db.init_db.assert_called_once_with(context['model']) 266 | 267 | # Set expected user 268 | expected_user = data_dict['user'] if data_dict is not None and 'user' in data_dict else context['user'] 269 | 270 | # Query called correctry 271 | actions.db.AllowedUser.get.assert_called_once_with(user_name=expected_user) 272 | 273 | # Assert that the package_show has been called properly 274 | self.assertEquals(len(pkgs_ids), package_show.call_count) 275 | for i in pkgs_ids: 276 | package_show.assert_any_call(context, {'id': i}) 277 | 278 | # Check that the template receives the correct datasets 279 | expected_acquired_datasets = [] 280 | for i in pkgs_ids: 281 | if i not in package_errors and i not in deleted_packages: 282 | pkg = default_package.copy() 283 | pkg['pkg_id'] = i 284 | pkg['state'] = 'deleted' if i in deleted_packages else 'active' 285 | expected_acquired_datasets.append(pkg) 286 | 287 | self.assertEquals(expected_acquired_datasets, result) 288 | 289 | @parameterized.expand([ 290 | # Simple Test: one user and one dataset 291 | ({'user1': ['ds1']}, [], [], None), #Test with and non-existing list of allowed users 292 | ({'user2': ['ds1']}, [], [], []), #Test remove a non-existing user 293 | ({'user3': ['ds1']}, [], [], ['user3']), #Test remove an existing user 294 | ({'user4': ['ds1']}, [], [], ['another_user']), #Test remove non-existing from an already populated list 295 | ({'user5': ['ds1']}, [], [], ['another_user', 'user5']), 296 | ({'user6': ['ds1']}, ['ds1'], [], None), #Test remove from an unknown place 297 | ({'user61': ['ds1']}, ['ds1'], [], []), 298 | ({'user62': ['ds1']}, ['ds1'], [], ['user6']), 299 | ({'user7': ['ds1']}, [], ['ds1'], []), #Tests deleting from a public dataset 300 | ({'user8': ['ds1']}, [], ['ds1'], ['another_user']), 301 | ({'user9': ['ds1']}, [], ['ds1'], ['another_user', 'another_one']), 302 | ({'user91': ['ds1']}, ['ds1'], ['ds1'], ['another_user', 'another_one']), # Checking the behaviour when the unknown dataset is public 303 | ({'user92': ['ds1']}, ['ds1'], ['ds1'], ['another_user', 'another_one','user92']), 304 | 305 | # Complex test: some users and some datasets 306 | ({'user1': ['ds1', 'ds2', 'ds3', 'ds4'], 'user2': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], []), 307 | ({'user3': ['ds1', 'ds2', 'ds3', 'ds4'], 'user4': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user']), 308 | ({'user5': ['ds1', 'ds2', 'ds3', 'ds4'], 'user6': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user', 'another_one']), 309 | ({'user7': ['ds1', 'ds2', 'ds3', 'ds4'], 'user8': ['ds5', 'ds6', 'ds7']}, ['ds3', 'ds6'], ['ds4', 'ds7'], ['another_user', 'another_one', 'user7']) 310 | ]) 311 | def test_delete_users(self, users_info, datasets_not_found, not_updatable_datasets, allowed_users=[]): 312 | parse_result = {'users_datasets': []} 313 | creator_user = {'name': 'ckan', 'id': '1234'} 314 | 315 | # Transform user_info 316 | for user in users_info: 317 | parse_result['users_datasets'].append({'user': user, 'datasets': users_info[user]}) 318 | 319 | parse_delete, package_show, package_update, user_show = self.configure_mocks(parse_result, 320 | datasets_not_found, not_updatable_datasets, allowed_users, creator_user) 321 | 322 | # Call the function 323 | context = {'user': 'user1', 'model': 'model', 'auth_obj': {'id': 1}, 'method': 'revoke'} 324 | result = actions.revoke_access(context, users_info) 325 | 326 | # Calculate the list of warns 327 | warns = [] 328 | for user_datasets in parse_result['users_datasets']: 329 | for dataset_id in user_datasets['datasets']: 330 | if dataset_id in datasets_not_found: 331 | warns.append('Dataset %s was not found in this instance' % dataset_id) 332 | elif dataset_id in not_updatable_datasets: 333 | # warns.append('%s(%s): %s' % (dataset_id, 'allowed_users', ADD_USERS_ERROR)) 334 | warns.append('Unable to upload the dataset %s: It\'s a public dataset' % dataset_id) 335 | 336 | expected_result = {'warns': warns} if len(warns) > 0 else None 337 | 338 | # Check that the returned result is as expected 339 | self.assertEquals(expected_result, result) 340 | 341 | # Check that the initial functions (check_access and parse_notification) has been called properly 342 | parse_delete.assert_called_once_with(users_info) 343 | actions.plugins.toolkit.check_access.assert_called_once_with('revoke_access', context, users_info) 344 | 345 | for user_datasets in parse_result['users_datasets']: 346 | for dataset_id in user_datasets['datasets']: 347 | # The show function is always called 348 | context_show = context.copy() 349 | context_show['ignore_auth'] = True 350 | context_show['updating_via_cb'] = True 351 | package_show.assert_any_call(context_show, {'id': dataset_id}) 352 | 353 | # The update function is called only when the show function does not throw an exception and 354 | # when the user is not in the list of allowed users. 355 | if dataset_id not in datasets_not_found and allowed_users is not None and user_datasets['user'] in allowed_users and dataset_id not in not_updatable_datasets: 356 | # Calculate the list of allowed_users 357 | expected_allowed_users = list(allowed_users) 358 | expected_allowed_users.remove(user_datasets['user']) 359 | 360 | context_update = context.copy() 361 | context_update['ignore_auth'] = True 362 | context_update['user'] = creator_user['name'] 363 | 364 | package_update.assert_any_call(context_update, {'id': dataset_id, 'allowed_users': expected_allowed_users, 'private': True, 'creator_user_id': creator_user['id']}) 365 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_plugin.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid 4 | 5 | # This file is part of CKAN Private Dataset Extension. 6 | 7 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 8 | # modify it under the terms of the GNU Affero General Public License as published by 9 | # the Free Software Foundation, either version 3 of the License, or 10 | # (at your option) any later version. 11 | 12 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 13 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | # GNU Affero General Public License for more details. 16 | 17 | # You should have received a copy of the GNU Affero General Public License 18 | # along with CKAN Private Dataset Extension. If not, see . 19 | 20 | import unittest 21 | import copy 22 | 23 | from flask import Blueprint 24 | from mock import MagicMock 25 | from parameterized import parameterized 26 | 27 | import ckanext.privatedatasets.plugin as plugin 28 | 29 | 30 | class PluginTest(unittest.TestCase): 31 | 32 | def setUp(self): 33 | # Create mocks 34 | self._tk = plugin.tk 35 | plugin.tk = MagicMock() 36 | plugin.tk.NotAuthorized = self._tk.NotAuthorized 37 | 38 | self._db = plugin.db 39 | plugin.db = MagicMock() 40 | 41 | self._search = plugin.search 42 | plugin.search = MagicMock() 43 | 44 | # Create the plugin 45 | self.privateDatasets = plugin.PrivateDatasets() 46 | 47 | def tearDown(self): 48 | plugin.tk = self._tk 49 | plugin.db = self._db 50 | plugin.search = self._search 51 | 52 | @parameterized.expand([ 53 | (plugin.p.IDatasetForm,), 54 | (plugin.p.IAuthFunctions,), 55 | (plugin.p.IConfigurer,), 56 | (plugin.p.IBlueprint,), 57 | (plugin.p.IActions,), 58 | (plugin.p.IPackageController,), 59 | (plugin.p.ITemplateHelpers,) 60 | ]) 61 | def test_implementation(self, interface): 62 | self.assertTrue(interface.implemented_by(plugin.PrivateDatasets)) 63 | 64 | @parameterized.expand([ 65 | ('package_show', plugin.auth.package_show), 66 | ('package_update', plugin.auth.package_update), 67 | ('resource_show', plugin.auth.resource_show), 68 | ('package_acquired', plugin.auth.package_acquired), 69 | ('acquisitions_list', plugin.auth.acquisitions_list), 70 | ('revoke_access', plugin.auth.revoke_access) 71 | ]) 72 | def test_auth_function(self, function_name, expected_function): 73 | auth_functions = self.privateDatasets.get_auth_functions() 74 | self.assertEquals(auth_functions[function_name], expected_function) 75 | 76 | def test_update_config(self): 77 | # Call the method 78 | config = {'test': 1234, 'another': 'value'} 79 | self.privateDatasets.update_config(config) 80 | 81 | # Test that functions are called as expected 82 | if self._tk.check_ckan_version(min_version='2.8'): 83 | plugin.tk.add_template_directory.assert_called_once_with(config, 'templates_2.8') 84 | else: 85 | plugin.tk.add_template_directory.assert_called_once_with(config, 'templates') 86 | plugin.tk.add_resource('fanstatic', 'privatedatasets') 87 | 88 | def test_get_blueprint(self): 89 | # Call the method 90 | self.assertIsInstance(self.privateDatasets.get_blueprint(), Blueprint) 91 | 92 | @parameterized.expand([ 93 | ('package_acquired', plugin.actions.package_acquired), 94 | ('acquisitions_list', plugin.actions.acquisitions_list), 95 | ('revoke_access', plugin.actions.revoke_access) 96 | ]) 97 | def test_actions_function(self, function_name, expected_function): 98 | actions = self.privateDatasets.get_actions() 99 | self.assertEquals(actions[function_name], expected_function) 100 | 101 | def test_fallback(self): 102 | self.assertEquals(True, self.privateDatasets.is_fallback()) 103 | 104 | def test_package_types(self): 105 | self.assertEquals([], self.privateDatasets.package_types()) 106 | 107 | @parameterized.expand([ 108 | ('is_dataset_acquired', plugin.helpers.is_dataset_acquired), 109 | ('get_allowed_users_str', plugin.helpers.get_allowed_users_str), 110 | ('is_owner', plugin.helpers.is_owner), 111 | ('can_read', plugin.helpers.can_read) 112 | ]) 113 | def test_helpers_functions(self, function_name, expected_function): 114 | helpers_functions = self.privateDatasets.get_helpers() 115 | self.assertEquals(helpers_functions[function_name], expected_function) 116 | 117 | ###################################################################### 118 | ############################## SCHEMAS ############################### 119 | ###################################################################### 120 | 121 | def _check_fields(self, schema, fields): 122 | for field in fields: 123 | for checker_validator in fields[field]: 124 | self.assertTrue(checker_validator in schema[field]) 125 | self.assertEquals(len(fields[field]), len(schema[field])) 126 | 127 | @parameterized.expand([ 128 | ('create_package_schema'), 129 | ('update_package_schema'), 130 | ]) 131 | def test_schema_create_update(self, function_name): 132 | 133 | function = getattr(self.privateDatasets, function_name) 134 | returned_schema = function() 135 | 136 | fields = { 137 | 'private': [plugin.tk.get_validator('ignore_missing'), plugin.tk.get_validator('boolean_validator')], 138 | 'acquire_url': [plugin.tk.get_validator('ignore_missing'), plugin.tk.get_converter('convert_to_extras'), 139 | plugin.conv_val.url_checker, plugin.conv_val.private_datasets_metadata_checker], 140 | 'searchable': [plugin.tk.get_validator('ignore_missing'), plugin.tk.get_validator('boolean_validator'), 141 | plugin.tk.get_converter('convert_to_extras'), plugin.conv_val.private_datasets_metadata_checker], 142 | 'allowed_users_str': [plugin.tk.get_validator('ignore_missing'), plugin.conv_val.private_datasets_metadata_checker], 143 | 'allowed_users': [plugin.conv_val.allowed_users_convert, plugin.tk.get_validator('ignore_missing'), 144 | plugin.conv_val.private_datasets_metadata_checker] 145 | } 146 | 147 | self._check_fields(returned_schema, fields) 148 | 149 | def test_schema_show(self): 150 | 151 | returned_schema = self.privateDatasets.show_package_schema() 152 | 153 | fields = ['searchable', 'acquire_url'] 154 | 155 | fields = { 156 | 'acquire_url': [plugin.tk.get_validator('ignore_missing'), plugin.tk.get_converter('convert_from_extras')], 157 | 'searchable': [plugin.tk.get_validator('ignore_missing'), plugin.tk.get_converter('convert_from_extras')], 158 | 'allowed_users': [plugin.tk.get_validator('ignore_missing'), plugin.conv_val.get_allowed_users] 159 | } 160 | 161 | self._check_fields(returned_schema, fields) 162 | 163 | ###################################################################### 164 | ############################## PACKAGE ############################### 165 | ###################################################################### 166 | 167 | @parameterized.expand([ 168 | ('True', []), 169 | ('False', []), 170 | ('True', ['abc']), 171 | ('False', ['abc']), 172 | ('True', ['abc', 'def', 'ghi']), 173 | ('False', ['abc', 'def', 'ghi']), 174 | ]) 175 | def test_packagecontroller_after_delete(self, private, allowed_users): 176 | pkg_id = '29472' 177 | pkg_dict = {'test': 'a', 'id': pkg_id, 'private': private, 'allowed_users': allowed_users} 178 | expected_pkg_dict = pkg_dict.copy() 179 | 180 | # Configure the database mock 181 | db_current_users = [] 182 | for user in allowed_users: 183 | db_user = MagicMock() 184 | db_user.package_id = pkg_id 185 | db_user.user_name = user 186 | db_current_users.append(db_user) 187 | 188 | # Allowed users 189 | plugin.db.AllowedUser.get = MagicMock(return_value=db_current_users) 190 | 191 | context = {'user': 'test', 'auth_user_obj': {'id': 1}, 'session': MagicMock(), 'model': MagicMock()} 192 | result = self.privateDatasets.after_delete(context, pkg_dict) # Call the function 193 | self.assertEquals(expected_pkg_dict, result) # Check the result 194 | 195 | # Assert that the get method has been called 196 | plugin.db.init_db.assert_called_once_with(context['model']) 197 | plugin.db.AllowedUser.get.assert_called_once_with(package_id=pkg_id) 198 | 199 | # Check that all the users has been deleted 200 | for user in allowed_users: 201 | found = False 202 | for call in context['session'].delete.call_args_list: 203 | call_user = call[0][0] 204 | 205 | if call_user.package_id == pkg_id and call_user.user_name == user: 206 | found = True 207 | break 208 | 209 | self.assertTrue(found) 210 | 211 | @parameterized.expand([ 212 | (True, 1, 1, False, True, True, [{'id': 1}, {'id': 2}], True), 213 | (True, 1, 2, False, True, True, [{'id': 1}, {'id': 2}], True), 214 | (True, 1, 1, True, True, True, [{'id': 1}, {'id': 2}], True), 215 | (True, 1, 2, True, True, True, [{'id': 1}, {'id': 2}], True), 216 | (True, 1, None, None, True, True, [{'id': 1}, {'id': 2}], True), 217 | (True, 1, 1, None, True, True, [{'id': 1}, {'id': 2}], True), 218 | (True, 1, None, True, True, True, [{'id': 1}, {'id': 2}], True), 219 | (True, 1, None, False, True, True, [{'id': 1}, {'id': 2}], True), 220 | (False, 1, 1, False, True, True, [{'id': 1}, {'id': 2}], True), 221 | (False, 1, 2, False, True, False, [{'id': 1}, {'id': 2}], True), 222 | (False, 1, 1, True, True, True, [{'id': 1}, {'id': 2}], True), 223 | (False, 1, 2, True, True, True, [{'id': 1}, {'id': 2}], True), 224 | (False, 1, None, None, True, False, [{'id': 1}, {'id': 2}], True), 225 | (False, 1, 1, None, True, True, [{'id': 1}, {'id': 2}], True), 226 | (False, 1, None, True, True, True, [{'id': 1}, {'id': 2}], True), 227 | (False, 1, None, False, True, False, [{'id': 1}, {'id': 2}], True), 228 | (True, 1, 1, False, False, False, [{'id': 1}, {'id': 2}], True), 229 | (True, 1, 2, False, False, False, [{'id': 1}, {'id': 2}], True), 230 | (True, 1, 1, True, False, False, [{'id': 1}, {'id': 2}], True), 231 | (True, 1, 2, True, False, False, [{'id': 1}, {'id': 2}], True), 232 | (True, 1, None, None, False, False, [{'id': 1}, {'id': 2}], True), 233 | (True, 1, 1, None, False, False, [{'id': 1}, {'id': 2}], True), 234 | (True, 1, None, True, False, False, [{'id': 1}, {'id': 2}], True), 235 | (True, 1, None, False, False, False, [{'id': 1}, {'id': 2}], True), 236 | (False, 1, 1, False, False, False, [{'id': 1}, {'id': 2}], True), 237 | (False, 1, 2, False, False, False, [{'id': 1}, {'id': 2}], True), 238 | (False, 1, 1, True, False, False, [{'id': 1}, {'id': 2}], True), 239 | (False, 1, 2, True, False, False, [{'id': 1}, {'id': 2}], True), 240 | (False, 1, None, None, False, False, [{'id': 1}, {'id': 2}], True), 241 | (False, 1, 1, None, False, False, [{'id': 1}, {'id': 2}], True), 242 | (False, 1, None, True, False, False, [{'id': 1}, {'id': 2}], True), 243 | (False, 1, None, False, False, False, [{'id': 1}, {'id': 2}], True), 244 | (True, 1, 1, False, True, True, [{}, {}], False), 245 | (True, 1, 2, False, True, True, [{}, {}], False), 246 | (True, 1, 1, True, True, True, [{}, {}], False), 247 | (True, 1, 2, True, True, True, [{}, {}], False), 248 | (True, 1, None, None, True, True, [{}, {}], False), 249 | (True, 1, 1, None, True, True, [{}, {}], False), 250 | (True, 1, None, True, True, True, [{}, {}], False), 251 | (True, 1, None, False, True, True, [{}, {}], False), 252 | (False, 1, 1, False, True, True, [{}, {}], False), 253 | (False, 1, 2, False, True, False, [{}, {}], False), 254 | (False, 1, 1, True, True, True, [{}, {}], False), 255 | (False, 1, 2, True, True, True, [{}, {}], False), 256 | (False, 1, None, None, True, False, [{}, {}], False), 257 | (False, 1, 1, None, True, True, [{}, {}], False), 258 | (False, 1, None, True, True, True, [{}, {}], False), 259 | (False, 1, None, False, True, False, [{}, {}], False), 260 | (True, 1, 1, False, False, False, [{}, {}], False), 261 | (True, 1, 2, False, False, False, [{}, {}], False), 262 | (True, 1, 1, True, False, False, [{}, {}], False), 263 | (True, 1, 2, True, False, False, [{}, {}], False), 264 | (True, 1, None, None, False, False, [{}, {}], False), 265 | (True, 1, 1, None, False, False, [{}, {}], False), 266 | (True, 1, None, True, False, False, [{}, {}], False), 267 | (True, 1, None, False, False, False, [{}, {}], False), 268 | (False, 1, 1, False, False, False, [{}, {}], False), 269 | (False, 1, 2, False, False, False, [{}, {}], False), 270 | (False, 1, 1, True, False, False, [{}, {}], False), 271 | (False, 1, 2, True, False, False, [{}, {}], False), 272 | (False, 1, None, None, False, False, [{}, {}], False), 273 | (False, 1, 1, None, False, False, [{}, {}], False), 274 | (False, 1, None, True, False, False, [{}, {}], False), 275 | (False, 1, None, False, False, False, [{}, {}], False), 276 | ]) 277 | def test_packagecontroller_after_show(self, update_via_api, creator_id, user_id, sysadmin, private, fields_expected, resources, resources_fields): 278 | 279 | context = {'updating_via_cb': update_via_api} 280 | 281 | if creator_id is not None or sysadmin is not None: 282 | user = MagicMock() 283 | user.id = user_id 284 | user.sysadmin = sysadmin 285 | context['auth_user_obj'] = user 286 | 287 | pkg_dict = {'creator_user_id': creator_id, 'allowed_users': ['a', 'b', 'c'], 'searchable': True, 'acquire_url': 'http://google.es', 'private': private, 'resources': resources, 'num_resources': 2} 288 | 289 | # Call the function 290 | result = self.privateDatasets.after_show(context, pkg_dict) # Call the function 291 | 292 | # Check the final result 293 | fields = ['allowed_users', 'searchable'] 294 | for field in fields: 295 | if fields_expected: 296 | self.assertTrue(field in result) 297 | else: 298 | self.assertFalse(field in result) 299 | 300 | fields = ['resources', 'num_resources'] 301 | for field in fields: 302 | if resources_fields: 303 | self.assertTrue(field in result) 304 | else: 305 | self.assertFalse(field in result) 306 | 307 | 308 | @parameterized.expand([ 309 | ('public', None, 'public'), 310 | ('public', 'False', 'private'), 311 | ('public', 'True', 'public'), 312 | ('private', None, 'private'), 313 | ('private', 'False', 'private'), 314 | ('public', 'True', 'public') 315 | ]) 316 | def test_packagecontroller_before_index(self, initialCapacity, searchable, finalCapacity): 317 | pkg_dict = {'capacity': initialCapacity, 'name': 'a', 'description': 'This is a test'} 318 | if searchable is not None: 319 | pkg_dict['extras_searchable'] = searchable 320 | 321 | expected_result = pkg_dict.copy() 322 | expected_result['capacity'] = finalCapacity 323 | 324 | self.assertEquals(expected_result, self.privateDatasets.before_index(pkg_dict)) 325 | 326 | def _aux_test_after_create_update(self, function, new_users, current_users, users_to_add, users_to_delete): 327 | package_id = 'package_id' 328 | 329 | # Configure mocks 330 | revision = {'timestamp': '7888'} 331 | default_dict = {'a': '0', 'b': 1, 'm': True, 'revision_id': 'revision_id_uuidv4'} 332 | expected_dict = default_dict.copy() 333 | expected_dict['metadata_modified'] = revision['timestamp'] 334 | package_show = MagicMock(return_value=default_dict.copy()) 335 | revision_show = MagicMock(return_value=revision.copy()) 336 | 337 | def _get_action(action): 338 | if action == 'package_show': 339 | return package_show 340 | elif action == 'revision_show': 341 | return revision_show 342 | 343 | plugin.tk.get_action = MagicMock(side_effect=_get_action) 344 | 345 | # Each time 'AllowedUser' is called, we must get a new instance 346 | # and this is the way to get this behaviour 347 | def constructor(): 348 | return MagicMock() 349 | 350 | plugin.db.AllowedUser = MagicMock(side_effect=constructor) 351 | 352 | # Configure the database mock 353 | db_current_users = [] 354 | for user in current_users: 355 | db_user = MagicMock() 356 | db_user.package_id = package_id 357 | db_user.user_name = user 358 | db_current_users.append(db_user) 359 | 360 | plugin.db.AllowedUser.get = MagicMock(return_value=db_current_users) 361 | 362 | # Call the method 363 | context = {'user': 'test', 'auth_user_obj': {'id': 1}, 'session': MagicMock(), 'model': MagicMock()} 364 | pkg_dict = {'id': 'package_id', 'allowed_users': new_users} 365 | function(context, pkg_dict) 366 | 367 | # Check that the database has been called 368 | plugin.db.init_db.assert_called_once_with(context['model']) 369 | plugin.db.AllowedUser.get.assert_called_once_with(package_id=pkg_dict['id']) 370 | 371 | def _test_calls(user_list, function): 372 | self.assertEquals(len(user_list), function.call_count) 373 | for user in user_list: 374 | found = False 375 | for call in function.call_args_list: 376 | call_user = call[0][0] 377 | 378 | if call_user.package_id == package_id and call_user.user_name == user: 379 | found = True 380 | break 381 | 382 | self.assertTrue(found) 383 | 384 | # Check that the method has deleted the appropriate users 385 | _test_calls(users_to_delete, context['session'].delete) 386 | 387 | # Check that the method has added the appropiate users 388 | _test_calls(users_to_add, context['session'].add) 389 | 390 | if len(users_to_add) == 0 and len(users_to_delete) == 0: 391 | # Check that the cache has not been updated 392 | self.assertEquals(0, self.privateDatasets.indexer.update_dict.call_count) 393 | else: 394 | # Check that the cache has been updated 395 | self.privateDatasets.indexer.update_dict.assert_called_once_with(expected_dict) 396 | 397 | @parameterized.expand([ 398 | # One element 399 | (['a'], [], ['a'], []), 400 | (['a'], ['a'], [], []), 401 | ([], ['a'], [], ['a']), 402 | # Two elements 403 | (['a', 'b'], [], ['a', 'b'], []), 404 | (['a', 'b'], ['b'], ['a'], []), 405 | (['a'], ['a', 'b'], [], ['b']), 406 | ([], ['a', 'b'], [], ['a', 'b']), 407 | (['a', 'b'], ['a', 'b'], [], []), 408 | # Three or more elements 409 | (['c'], ['a', 'b'], ['c'], ['a', 'b']), 410 | (['a', 'b', 'c'], ['a', 'b'], ['c'], []), 411 | (['a', 'b', 'c'], ['a'], ['b', 'c'], []), 412 | (['a', 'b', 'c'], ['a', 'b', 'c'], [], []), 413 | (['a', 'b', 'c'], [], ['a', 'b', 'c'], []), 414 | (['a', 'b'], ['a', 'b', 'c'], [], ['c']) 415 | ]) 416 | def test_packagecontroller_after_create(self, new_users, current_users, users_to_add, users_to_delete): 417 | self._aux_test_after_create_update(self.privateDatasets.after_create, new_users, current_users, users_to_add, users_to_delete) 418 | 419 | @parameterized.expand([ 420 | # One element 421 | (['a'], [], ['a'], []), 422 | (['a'], ['a'], [], []), 423 | ([], ['a'], [], ['a']), 424 | # Two elements 425 | (['a', 'b'], [], ['a', 'b'], []), 426 | (['a', 'b'], ['b'], ['a'], []), 427 | (['a'], ['a', 'b'], [], ['b']), 428 | ([], ['a', 'b'], [], ['a', 'b']), 429 | (['a', 'b'], ['a', 'b'], [], []), 430 | # Three or more elements 431 | (['c'], ['a', 'b'], ['c'], ['a', 'b']), 432 | (['a', 'b', 'c'], ['a', 'b'], ['c'], []), 433 | (['a', 'b', 'c'], ['a'], ['b', 'c'], []), 434 | (['a', 'b', 'c'], ['a', 'b', 'c'], [], []), 435 | (['a', 'b', 'c'], [], ['a', 'b', 'c'], []), 436 | (['a', 'b'], ['a', 'b', 'c'], [], ['c']) 437 | ]) 438 | def test_packagecontroller_after_update(self, new_users, current_users, users_to_add, users_to_delete): 439 | self._aux_test_after_create_update(self.privateDatasets.after_update, new_users, current_users, users_to_add, users_to_delete) 440 | 441 | @parameterized.expand([ 442 | (1, True), 443 | (1, False), 444 | # Complex results 445 | (3, True), 446 | (3, False) 447 | ]) 448 | def test_packagecontroller_after_search(self, num_seach_results, user_allowed): 449 | 450 | # Create the list with the 451 | remaining_fields = ['other_id', 'name', 'author'] 452 | # Resources field should be in the result when the user is allowed to show the dataset 453 | if user_allowed: 454 | remaining_fields.append('resources') 455 | 456 | search_results = {'facets': ['facet1', 'facet2'], 'results': [], 'elements': num_seach_results} 457 | # Add resources 458 | for _ in range(num_seach_results): 459 | search_results['results'].append({ 460 | 'allowed_users': ['user1', 'user2'], 461 | 'seearchable': True, 462 | 'acquire_url': 'https://upm.es', 463 | 'resources': ['resource1', 'resource2'], 464 | remaining_fields[0]: 'value1', 465 | remaining_fields[1]: 'value2', 466 | remaining_fields[2]: 'value3' 467 | }) 468 | 469 | # Mocking 470 | plugin.tk.check_access.side_effect = None if user_allowed else plugin.tk.NotAuthorized 471 | 472 | # Call the function 473 | final_search_results = self.privateDatasets.after_search(copy.deepcopy(search_results), None) 474 | 475 | # Assertations 476 | for result in final_search_results['results']: 477 | self.assertNotIn('allowed_users', result) 478 | self.assertNotIn('searchable', result) 479 | self.assertIn('acquire_url', result) 480 | 481 | for remaining_field in remaining_fields: 482 | self.assertIn(remaining_field, result) 483 | 484 | self.assertEquals(final_search_results['facets'], search_results['facets']) 485 | self.assertEquals(final_search_results['elements'], search_results['elements']) 486 | 487 | @parameterized.expand([ 488 | (True,), 489 | (False,) 490 | ]) 491 | def test_package_controller_before_view(self, user_allowed): 492 | 493 | pkg_dict = {'resources': [{'id': 1}, {'id': 2}, {'id': 3}]} 494 | pkg_dict_not_allowed = {'resources': []} 495 | 496 | plugin.tk.check_access.side_effect = None if user_allowed else plugin.tk.NotAuthorized 497 | 498 | result = self.privateDatasets.before_view(pkg_dict) 499 | 500 | if user_allowed: 501 | self.assertEquals(result['resources'], pkg_dict['resources']) 502 | else: 503 | self.assertEquals(result['resources'], pkg_dict_not_allowed['resources']) 504 | 505 | @parameterized.expand([ 506 | (True,), 507 | (False,) 508 | ]) 509 | def test_resource_controller_before_show(self, user_allowed): 510 | 511 | resource_dict = {'id': 1, 'resource_name': 'resource_test'} 512 | 513 | plugin.tk.check_access.side_effect = None if user_allowed else plugin.tk.NotAuthorized 514 | 515 | result = self.privateDatasets.before_show(resource_dict) 516 | 517 | if user_allowed: 518 | self.assertEquals(result['id'], resource_dict['id']) 519 | self.assertEquals(result['resource_name'], resource_dict['resource_name']) 520 | else: 521 | self.assertNotIn('id', result) 522 | self.assertNotIn('resource_name', result) 523 | -------------------------------------------------------------------------------- /ckanext/privatedatasets/tests/test_selenium.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Copyright (c) 2014 - 2017 CoNWeT Lab., Universidad Politécnica de Madrid 4 | # Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. 5 | 6 | # This file is part of CKAN Private Dataset Extension. 7 | 8 | # CKAN Private Dataset Extension is free software: you can redistribute it and/or 9 | # modify it under the terms of the GNU Affero General Public License as published by 10 | # the Free Software Foundation, either version 3 of the License, or 11 | # (at your option) any later version. 12 | 13 | # CKAN Private Dataset Extension is distributed in the hope that it will be useful, 14 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | # GNU Affero General Public License for more details. 17 | 18 | # You should have received a copy of the GNU Affero General Public License 19 | # along with CKAN Private Dataset Extension. If not, see . 20 | 21 | from __future__ import unicode_literals, print_function 22 | 23 | import json 24 | import os 25 | import unittest 26 | import re 27 | from subprocess import Popen 28 | import time 29 | 30 | import ckan.lib.search.index as search_index 31 | import ckan.model as model 32 | from parameterized import parameterized 33 | import requests 34 | from selenium import webdriver 35 | from selenium.common.exceptions import NoAlertPresentException 36 | from selenium.webdriver.common.by import By 37 | from selenium.webdriver.common.keys import Keys 38 | from selenium.webdriver.support import expected_conditions as EC 39 | from selenium.webdriver.support.ui import Select, WebDriverWait 40 | 41 | import ckanext.privatedatasets.db as db 42 | 43 | 44 | def get_dataset_url(dataset_name): 45 | return dataset_name.replace(' ', '-').lower() 46 | 47 | 48 | class TestSelenium(unittest.TestCase): 49 | 50 | @classmethod 51 | def setUpClass(cls): 52 | # Run CKAN 53 | env = os.environ.copy() 54 | env['DEBUG'] = 'False' 55 | cls._process = Popen(['paster', 'serve', 'test.ini'], env=env) 56 | 57 | # Init Selenium 58 | cls.driver = webdriver.Firefox() 59 | cls.base_url = 'http://localhost:5000/' 60 | cls.driver.set_window_size(1024, 768) 61 | 62 | @classmethod 63 | def tearDownClass(cls): 64 | cls._process.terminate() 65 | cls.driver.quit() 66 | 67 | def clearBBDD(self): 68 | # Clean Solr 69 | search_index.clear_index() 70 | 71 | # Clean the database 72 | model.repo.rebuild_db() 73 | 74 | # Delete previous users 75 | db.init_db(model) 76 | users = db.AllowedUser.get() 77 | for user in users: 78 | model.Session.delete(user) 79 | model.Session.commit() 80 | 81 | def setUp(self): 82 | self.clearBBDD() 83 | 84 | def tearDown(self): 85 | self.driver.get(self.base_url) 86 | try: # pragma: no cover 87 | # Accept any "Are you sure to leave?" alert 88 | self.driver.switch_to.alert.accept() 89 | self.driver.switch_to.default_content() 90 | except NoAlertPresentException: 91 | pass 92 | WebDriverWait(self.driver, 10).until(lambda driver: self.base_url == driver.current_url) 93 | self.driver.delete_all_cookies() 94 | self.clearBBDD() 95 | 96 | def assert_fields_disabled(self, fields): 97 | for field in fields: 98 | self.assertFalse(self.driver.find_element_by_id(field).is_enabled()) 99 | 100 | def logout(self): 101 | self.driver.delete_all_cookies() 102 | self.driver.get(self.base_url) 103 | 104 | def register(self, username, fullname, mail): 105 | driver = self.driver 106 | driver.get(self.base_url) 107 | driver.find_element_by_link_text('Register').click() 108 | WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-username"))).clear() 109 | driver.find_element_by_id('field-username').send_keys(username) 110 | driver.find_element_by_id('field-fullname').clear() 111 | driver.find_element_by_id('field-fullname').send_keys(fullname) 112 | driver.find_element_by_id('field-email').clear() 113 | driver.find_element_by_id('field-email').send_keys(mail) 114 | driver.find_element_by_id('field-password').clear() 115 | driver.find_element_by_id('field-password').send_keys("1234" + username) 116 | driver.find_element_by_id('field-confirm-password').clear() 117 | driver.find_element_by_id('field-confirm-password').send_keys("1234" + username) 118 | driver.find_element_by_name('save').click() 119 | self.logout() 120 | 121 | def login(self, username): 122 | driver = self.driver 123 | driver.get(self.base_url) 124 | login_btn = WebDriverWait(driver, 15).until( 125 | EC.element_to_be_clickable((By.LINK_TEXT, 'Log in')) 126 | ) 127 | login_btn.click() 128 | 129 | WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-login"))).clear() 130 | driver.find_element_by_id('field-login').send_keys(username) 131 | driver.find_element_by_id('field-password').clear() 132 | driver.find_element_by_id('field-password').send_keys("1234" + username) 133 | driver.find_element_by_id('field-remember').click() 134 | driver.find_element_by_css_selector('button.btn.btn-primary').click() 135 | 136 | def create_organization(self, name, description, users): 137 | driver = self.driver 138 | driver.get(self.base_url) 139 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Organizations'))).click() 140 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Organization'))).click() 141 | WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, 'field-name'))) 142 | 143 | # Wait a bit to let ckan add javascript hooks 144 | time.sleep(0.2) 145 | 146 | driver.find_element_by_id('field-name').clear() 147 | driver.find_element_by_id('field-name').send_keys(name) 148 | driver.find_element_by_id('field-description').clear() 149 | driver.find_element_by_id('field-description').send_keys(description) 150 | driver.find_element_by_name('save').click() 151 | 152 | # Add users 153 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Manage'))).click() 154 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Members'))).click() 155 | for user in users: 156 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Member'))).click() 157 | WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "s2id_autogen1"))).send_keys(user + Keys.RETURN) 158 | driver.find_element_by_name('submit').click() 159 | 160 | def fill_ds_general_info(self, name, description, tags, private, searchable, allowed_users, acquire_url): 161 | # FIRST PAGE: Dataset properties 162 | driver = self.driver 163 | WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-title"))) 164 | 165 | # Wait a bit to let ckan add javascript hooks 166 | time.sleep(0.2) 167 | 168 | driver.find_element_by_id('field-title').clear() 169 | driver.find_element_by_id('field-title').send_keys(name) 170 | driver.find_element_by_id('field-notes').clear() 171 | driver.find_element_by_id('field-notes').send_keys(description) 172 | # field-tags 173 | for tag in tags: 174 | driver.find_element_by_id('s2id_autogen1').send_keys(tag + Keys.RETURN) 175 | Select(driver.find_element_by_id('field-private')).select_by_visible_text('Private' if private else 'Public') 176 | # WARN: The organization is set by default 177 | 178 | # If the dataset is private, we should complete the fields 179 | # If the dataset is public, these fields will be disabled (we'll check it) 180 | if private: 181 | Select(driver.find_element_by_id('field-searchable')).select_by_visible_text('True' if searchable else 'False') 182 | # field-allowed_users 183 | for user in allowed_users: 184 | driver.find_element_by_css_selector('#s2id_field-allowed_users_str .select2-input').send_keys(user + Keys.RETURN) 185 | driver.find_element_by_id('field-acquire_url').clear() 186 | if acquire_url: 187 | driver.find_element_by_id('field-acquire_url').send_keys(acquire_url) 188 | else: 189 | self.assert_fields_disabled(['field-searchable', 'field-allowed_users_str', 'field-acquire_url']) 190 | 191 | driver.find_element_by_name('save').click() 192 | 193 | def create_ds(self, name, description, tags, private, searchable, allowed_users, acquire_url, resource_url, resource_name, resource_description, resource_format): 194 | driver = self.driver 195 | driver.get(self.base_url) 196 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Datasets'))).click() 197 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Dataset'))).click() 198 | self.fill_ds_general_info(name, description, tags, private, searchable, allowed_users, acquire_url) 199 | 200 | # SECOND PAGE: Add Resources 201 | WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.ID, "field-name"))) 202 | 203 | # Wait a bit to let ckan add javascript hooks 204 | time.sleep(0.2) 205 | 206 | try: 207 | # The link button is only clicked if it's present 208 | driver.find_element_by_link_text('Link').click() 209 | except Exception: # pragma: no cover 210 | pass 211 | 212 | driver.find_element_by_id('field-image-url').clear() 213 | driver.find_element_by_id('field-image-url').send_keys(resource_url) 214 | driver.find_element_by_id('field-name').clear() 215 | driver.find_element_by_id('field-name').send_keys(resource_name) 216 | driver.find_element_by_id('field-description').clear() 217 | driver.find_element_by_id('field-description').send_keys(resource_description) 218 | driver.find_element_by_id('s2id_autogen1').send_keys(resource_format + Keys.RETURN) 219 | driver.find_element_by_css_selector('button.btn.btn-primary').click() 220 | 221 | def modify_ds(self, url, name, description, tags, private, searchable, allowed_users, acquire_url): 222 | driver = self.driver 223 | driver.get('%sdataset/edit/%s' % (self.base_url, url)) 224 | self.fill_ds_general_info(name, description, tags, private, searchable, allowed_users, acquire_url) 225 | 226 | def check_ds_values(self, url, private, searchable, allowed_users, acquire_url): 227 | driver = self.driver 228 | driver.get(self.base_url + 'dataset/edit/' + url) 229 | self.assertEqual('Private' if private else 'Public', Select(driver.find_element_by_id('field-private')).first_selected_option.text) 230 | 231 | if private: 232 | acquire_url_final = '' if acquire_url is None else acquire_url 233 | self.assertEqual(acquire_url_final, driver.find_element_by_id('field-acquire_url').get_attribute('value')) 234 | self.assertEqual('True' if searchable else 'False', Select(driver.find_element_by_id('field-searchable')).first_selected_option.text) 235 | 236 | # Test that the allowed users lists is as expected (order is not important) 237 | current_users = driver.find_element_by_css_selector('#s2id_field-allowed_users_str > ul.select2-choices').text.split('\n') 238 | current_users = current_users[0:-1] 239 | # ''.split('\n') ==> [''] 240 | # if len(current_users) == 1 and current_users[0] == '': 241 | # current_users = [] 242 | # Check the array 243 | self.assertEqual(len(allowed_users), len(current_users)) 244 | for user in current_users: 245 | self.assertIn(user, allowed_users) 246 | else: 247 | self.assert_fields_disabled(['field-searchable', 'field-allowed_users_str', 'field-acquire_url']) 248 | 249 | def check_user_access(self, dataset, dataset_url, owner, acquired, in_org, private, searchable, acquire_url=None): 250 | driver = self.driver 251 | driver.find_element_by_link_text('Datasets').click() 252 | 253 | if searchable or owner or in_org: 254 | xpath = '//div[@id=\'content\']/div[3]/div/section/div/ul/li/div/h3/span' 255 | 256 | # Check the label 257 | if owner: 258 | self.assertEqual('OWNER', driver.find_element_by_xpath(xpath).text) 259 | if not acquired and private and not in_org: 260 | self.assertEqual('PRIVATE', driver.find_element_by_xpath(xpath).text) 261 | elif acquired and not owner and private: 262 | self.assertEqual('ACQUIRED', driver.find_element_by_xpath(xpath).text) 263 | 264 | # When a user cannot access a dataset, the link is no longer provided 265 | else: 266 | # If the dataset is not searchable and the user is not the owner, a link to it could not be found in the dataset search page 267 | self.assertEqual(None, re.search(dataset_url, driver.page_source)) 268 | 269 | # Access the dataset 270 | driver.get(self.base_url + 'dataset/' + dataset_url) 271 | 272 | if not acquired and private and not in_org: 273 | # If the dataset is private and the user hasnt access to the resources, the field resources dont appear 274 | 275 | self.assertEquals('empty', driver.find_element_by_class_name('empty').get_attribute('class')) 276 | self.assertEqual(self.base_url + 'dataset/%s' % dataset_url, driver.current_url) 277 | 278 | else: 279 | self.assertEquals('resource-list', driver.find_element_by_class_name('resource-list').get_attribute('class')) 280 | self.assertEqual(self.base_url + 'dataset/%s' % dataset_url, driver.current_url) 281 | 282 | def check_acquired(self, dataset, dataset_url, acquired, private): 283 | driver = self.driver 284 | driver.get(self.base_url + 'dashboard') 285 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Acquired Datasets'))).click() 286 | 287 | if acquired and private: 288 | # This message could not be shown when the user has acquired at least one dataset 289 | self.assertEqual(None, re.search('You haven\'t acquired any datasets.', driver.page_source)) 290 | # Access the dataset 291 | driver.find_element_by_link_text(dataset).click() 292 | self.assertEqual(self.base_url + 'dataset/%s' % dataset_url, driver.current_url) 293 | else: 294 | # If the user has not acquired the dataset, a link to this dataset could not be in the acquired dataset list 295 | self.assertEqual(None, re.search(dataset_url, driver.page_source)) 296 | # When a user has not acquired any dataset, a message will be shown to inform the user 297 | self.assertNotEquals(None, re.search('You haven\'t acquired any datasets.', driver.page_source)) 298 | 299 | def default_register(self, user): 300 | self.register(user, user, '%s@conwet.com' % user) 301 | 302 | @parameterized.expand([ 303 | (['user1', 'user2', 'user3'], True, True, [], 'http://store.conwet.com/'), 304 | (['user1', 'user2', 'user3'], True, True, []), 305 | (['user1', 'user2', 'user3'], False, True, []), 306 | (['user1', 'user2', 'user3'], True, False, []), 307 | (['user1', 'user2', 'user3', 'user4'], True, True, ['user2', 'user4'], 'http://store.conwet.com/'), 308 | (['user1', 'user2', 'user3', 'user4'], True, True, ['user3', 'user4']), 309 | (['user1', 'user2', 'user3', 'user4'], False, True, ['user3', 'user4']), 310 | (['user1', 'user2', 'user3', 'user4'], True, False, ['user2', 'user4']), 311 | ]) 312 | def test_basic(self, users, private, searchable, allowed_users, acquire_url=None): 313 | # Create users 314 | for user in users: 315 | self.default_register(user) 316 | 317 | # The first user creates a dataset 318 | self.login(users[0]) 319 | pkg_name = 'Dataset 1' 320 | url = get_dataset_url(pkg_name) 321 | self.create_ds(pkg_name, 'Example description', ['tag1', 'tag2', 'tag3'], private, searchable, 322 | allowed_users, acquire_url, 'http://upm.es', 'UPM Main', 'Example Description', 'CSV') 323 | 324 | self.check_ds_values(url, private, searchable, allowed_users, acquire_url) 325 | 326 | self.check_user_access(pkg_name, url, True, True, False, private, searchable, acquire_url) 327 | self.check_acquired(pkg_name, url, False, private) 328 | 329 | # Rest of users 330 | rest_users = users[1:] 331 | for user in rest_users: 332 | self.logout() 333 | self.login(user) 334 | acquired = user in allowed_users 335 | self.check_user_access(pkg_name, url, False, acquired, False, private, searchable, acquire_url) 336 | 337 | self.check_acquired(pkg_name, url, acquired, private) 338 | 339 | @parameterized.expand([ 340 | (['conwet'], 'ftp://google.es', 'Acquire URL: The URL "ftp://google.es" is not valid.'), 341 | (['conwet'], 'google', 'Acquire URL: The URL "google" is not valid.'), 342 | (['conwet'], 'http://google', 'Acquire URL: The URL "http://google" is not valid.'), 343 | (['conwet'], 'www.google.es', 'Acquire URL: The URL "www.google.es" is not valid.') 344 | 345 | ]) 346 | def test_invalid_fields(self, allowed_users, acquire_url, expected_msg): 347 | 348 | # Create a default user 349 | user = 'user1' 350 | self.default_register(user) 351 | 352 | # Create the dataset 353 | self.login(user) 354 | pkg_name = 'Dataset 2' 355 | 356 | # Go the page to create the dataset 357 | driver = self.driver 358 | driver.get(self.base_url) 359 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Datasets'))).click() 360 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, 'Add Dataset'))).click() 361 | 362 | # Fill the requested information 363 | self.fill_ds_general_info(pkg_name, 'Example description', ['tag1'], True, True, allowed_users, acquire_url) 364 | 365 | # Check the error message 366 | msg_error = WebDriverWait(self.driver, 10).until(EC.presence_of_element_located((By.XPATH, '//div[@id=\'content\']/div[3]/div/section/div/form/div/ul/li'))).text 367 | self.assertEqual(expected_msg, msg_error) 368 | 369 | @parameterized.expand([ 370 | ('Acquire Dataset', 'dataset'), 371 | ('Acquire one now?', 'dataset') 372 | ]) 373 | def test_dashboard_basic_links(self, link, expected_url): 374 | # Create a default user 375 | user = 'user1' 376 | self.default_register(user) 377 | self.login(user) 378 | 379 | # Enter the acquired dataset tab 380 | driver = self.driver 381 | driver.get(self.base_url + 'dashboard/acquired') 382 | WebDriverWait(self.driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, link))).click() 383 | self.assertEqual(self.base_url + 'dataset', self.base_url + expected_url) 384 | 385 | @parameterized.expand([ 386 | 387 | # Allowed users contains just one user 388 | # ([{'private': True, 'searchable': True, 'allowed_users': ['user1']}], ['user2']), 389 | # ([{'private': False, 'searchable': True, 'allowed_users': ['user1']}], ['user2']), 390 | # ([{'private': True, 'searchable': False, 'allowed_users': ['user1']}], ['user2']), 391 | # ([{'private': False, 'searchable': False, 'allowed_users': ['user1']}], ['user2']), 392 | 393 | # Allowed users contains more than one user 394 | # ([{'private': True, 'searchable': True, 'allowed_users': ['user1', 'user2']}], ['user3']), 395 | # ([{'private': False, 'searchable': True, 'allowed_users': ['user1', 'user2']}], ['user3']), 396 | # ([{'private': True, 'searchable': False, 'allowed_users': ['user1', 'user2']}], ['user3']), 397 | # ([{'private': False, 'searchable': False, 'allowed_users': ['user1', 'user2']}], ['user3']), 398 | 399 | # User added is already in the list 400 | ([{'private': True, 'searchable': True, 'allowed_users': ['user1', 'user2']}], ['user2']), 401 | ([{'private': True, 'searchable': False, 'allowed_users': ['user1', 'user2']}], ['user2']), 402 | 403 | # Some users 404 | ([{'private': True, 'searchable': True, 'allowed_users': ['user1', 'user2']}], ['user3', 'user4']), 405 | ([{'private': False, 'searchable': True, 'allowed_users': ['user1', 'user2']}], ['user3', 'user4']), 406 | ([{'private': True, 'searchable': False, 'allowed_users': ['user1', 'user2']}], ['user3', 'user4']), 407 | ([{'private': False, 'searchable': False, 'allowed_users': ['user1', 'user2']}], ['user3', 'user4']), 408 | # Complex test 409 | ([{'private': True, 'searchable': False, 'allowed_users': ['user1', 'user2']}, 410 | {'private': True, 'searchable': True, 'allowed_users': ['user5', 'user6']}, 411 | {'private': True, 'searchable': True, 'allowed_users': ['user7', 'user8']}, 412 | {'private': False, 'searchable': True, 'allowed_users': ['user9', 'user1']}], ['user3', 'user4']) 413 | 414 | ]) 415 | def test_add_users_via_api_action(self, datasets, users_via_api): 416 | # Create a default user 417 | user = 'user1' 418 | self.default_register(user) 419 | self.login(user) 420 | 421 | acquire_url = 'http://upm.es' 422 | dataset_default_name = 'Dataset %d' 423 | 424 | # Create the dataset 425 | for i, dataset in enumerate(datasets): 426 | pkg_name = dataset_default_name % i 427 | self.create_ds(pkg_name, 'Example description', ['tag1'], dataset['private'], dataset['searchable'], 428 | dataset['allowed_users'], acquire_url, 'http://upm.es', 'UPM Main', 'Example Description', 'CSV') 429 | 430 | # Make the requests 431 | for user in users_via_api: 432 | 433 | resources = [] 434 | for i, dataset in enumerate(datasets): 435 | resources.append({'url': self.base_url + 'dataset/' + get_dataset_url(dataset_default_name % i)}) 436 | 437 | content = {'customer_name': user, 'resources': resources} 438 | req = requests.post(self.base_url + 'api/action/package_acquired', data=json.dumps(content), 439 | headers={'content-type': 'application/json'}) 440 | 441 | result = json.loads(req.text)['result'] 442 | for i, dataset in enumerate(datasets): 443 | if not dataset['private']: 444 | url_path = get_dataset_url(dataset_default_name % i) 445 | self.assertIn('Unable to upload the dataset %s: It\'s a public dataset' % url_path, result['warns']) 446 | 447 | # Check the dataset 448 | for i, dataset in enumerate(datasets): 449 | 450 | if dataset['private']: 451 | final_users = set(dataset['allowed_users']) 452 | final_users.update(users_via_api) 453 | else: 454 | final_users = [] 455 | 456 | url_path = get_dataset_url(dataset_default_name % i) 457 | self.check_ds_values(url_path, dataset['private'], dataset['searchable'], final_users, acquire_url) 458 | 459 | @parameterized.expand([ 460 | # Even if user6 is in another organization, he/she won't be able to access the dataset 461 | (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}, 462 | {'name': 'UPM', 'users': ['user6']}], True, True, ['user4', 'user5'], 'http://store.conwet.com/'), 463 | (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}, 464 | {'name': 'UPM', 'users': ['user6']}], True, True, ['user4', 'user5']), 465 | (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}, 466 | {'name': 'UPM', 'users': ['user6']}], True, False, ['user4', 'user5']), 467 | (['user1', 'user2', 'user3', 'user4', 'user5', 'user6'], [{'name': 'CoNWeT', 'users': ['user2', 'user3']}, 468 | {'name': 'UPM', 'users': ['user6']}], False, True, ['user4', 'user5']), 469 | ]) 470 | def test_organization(self, users, orgs, private, searchable, adquiring_users, acquire_url=None): 471 | # Create users 472 | for user in users: 473 | self.default_register(user) 474 | 475 | self.login(users[0]) 476 | 477 | # Create the organizations 478 | for org in orgs: 479 | self.create_organization(org['name'], 'Example Description', org['users']) 480 | 481 | # Create the dataset 482 | pkg_name = 'Dataset 1' 483 | url = get_dataset_url(pkg_name) 484 | self.create_ds(pkg_name, 'Example description', ['tag1', 'tag2', 'tag3'], private, searchable, 485 | adquiring_users, acquire_url, 'http://upm.es', 'UPM Main', 'Example Description', 'CSV') 486 | self.check_ds_values(url, private, searchable, adquiring_users, acquire_url) 487 | self.check_user_access(pkg_name, url, True, True, True, private, searchable, acquire_url) 488 | self.check_acquired(pkg_name, url, False, private) 489 | 490 | # Rest of users 491 | rest_users = users[1:] 492 | for user in rest_users: 493 | self.logout() 494 | self.login(user) 495 | acquired = user in adquiring_users 496 | in_org = user in orgs[0]['users'] 497 | self.check_user_access(pkg_name, url, False, acquired, in_org, private, searchable, acquire_url) 498 | 499 | self.check_acquired(pkg_name, url, acquired, private) 500 | 501 | def test_bug_16(self): 502 | """ 503 | Private datasets cannot be turned to public datasets when the Acquisition URL is set 504 | """ 505 | user = 'user1' 506 | self.default_register(user) 507 | 508 | # The user creates a dataset 509 | self.login(user) 510 | pkg_name = 'Dataset 1' 511 | description = 'Example Description' 512 | tags = ['tag1', 'tag2', 'tag3'] 513 | url = get_dataset_url(pkg_name) 514 | self.create_ds(pkg_name, 'Example description', [], True, True, 515 | [], 'http://example.com', 'http://upm.es', 'UPM Main', 'Example Description', 'CSV') 516 | 517 | self.modify_ds(url, pkg_name, description, tags, False, None, None, None) 518 | expected_url = 'dataset/%s' % url 519 | WebDriverWait(self.driver, 20).until(lambda driver: expected_url in driver.current_url) 520 | --------------------------------------------------------------------------------