Skip to content

Commit

Permalink
Merge pull request #1554 from Scifabric/issue-1553
Browse files Browse the repository at this point in the history
Issue 1553
  • Loading branch information
teleyinex committed Jun 14, 2017
2 parents b975dbf + 1daccfc commit 3133bb9
Show file tree
Hide file tree
Showing 58 changed files with 782 additions and 55 deletions.
33 changes: 33 additions & 0 deletions doc/customizing.rst
Expand Up @@ -695,6 +695,39 @@ To:
This feature is disabled by default.


Making extra key/value pairs in info field public
=================================================

By default PYBOSSA protects all the information the info field except for those
values that are public like the url of the image of the project, the container
where that picture is stored and a few extra. While this will be more than enough
for most projects, sometimes, a server will need to expose more information publicly
via the info field for the User and Project Domain Objects.

Imagine that you want to give badges to users. You can store that information in the
User domain object, within the info field in a field named *badges*. While this will
work, the API will hide all that information except for the owner. Thus, it will be impossible
to show user's badges to anonymous people.

With projects it could be the same. You want to highlight some info to anyone, but hide everything else.

As PYBOSSA hides everything by default, you can always turn on which other fields from the
info field can be shown to anonymous users, making them public.

.. note::

WARNING: be very careful. This is your responsibility, and it's not enabled by default. If you
expose your own private data via this field, it's your own responsibility as this is not enabled
by default in PYBOSSA.

If you want to make some key/values public, all you have to do is add to the settings_local.py file
the following config variables::

PROJECT_INFO_PUBLIC_FIELDS = ['key1', 'key2']
USER_INFO_PUBLIC_FIELDS = ['badges', 'key2', ...]

Add as many as you want, need. But please, be careful about which information you disclose.

Adding your own templates
=========================

Expand Down
2 changes: 2 additions & 0 deletions pybossa/extensions.py
Expand Up @@ -32,6 +32,8 @@
* csrf: for CSRF protection
* newsletter: for subscribing users to Mailchimp newsletter
* assets: for assets management (SASS, etc.)
* JSONEncoder: a custom JSON encoder to handle specific types
* cors: the Flask-Cors library object
"""
__all__ = ['sentinel', 'db', 'signer', 'mail', 'login_manager', 'facebook',
Expand Down
10 changes: 8 additions & 2 deletions pybossa/model/project.py
Expand Up @@ -21,6 +21,7 @@
from sqlalchemy.orm import relationship, backref
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.ext.mutable import MutableDict
from flask import current_app

from pybossa.core import db, signer
from pybossa.model import DomainObject, make_timestamp, make_uuid
Expand Down Expand Up @@ -126,5 +127,10 @@ def public_attributes(self):
@classmethod
def public_info_keys(self):
"""Return a list of public info keys."""
return ['container', 'thumbnail', 'thumbnail_url',
'task_presenter', 'tutorial', 'sched']
default = ['container', 'thumbnail', 'thumbnail_url',
'task_presenter', 'tutorial', 'sched']
extra = current_app.config.get('PROJECT_INFO_PUBLIC_FIELDS')
if extra:
return list(set(default).union(set(extra)))
else:
return default
12 changes: 9 additions & 3 deletions pybossa/model/user.py
Expand Up @@ -17,11 +17,12 @@
# along with PYBOSSA. If not, see <http://www.gnu.org/licenses/>.

from sqlalchemy import Integer, Boolean, Unicode, Text, String, BigInteger
from sqlalchemy.schema import Column, ForeignKey
from sqlalchemy.orm import relationship, backref
from sqlalchemy.schema import Column
from sqlalchemy.orm import relationship
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.ext.mutable import MutableDict
from flask.ext.login import UserMixin
from flask import current_app

from pybossa.core import db, signer
from pybossa.model import DomainObject, make_timestamp, make_uuid
Expand Down Expand Up @@ -92,4 +93,9 @@ def public_attributes(self):
@classmethod
def public_info_keys(self):
"""Return a list of public info keys."""
return ['avatar', 'container', 'extra', 'avatar_url']
default = ['avatar', 'container', 'extra', 'avatar_url']
extra = current_app.config.get('USER_INFO_PUBLIC_FIELDS')
if extra:
return list(set(default).union(set(extra)))
else:
return default
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -62,7 +62,7 @@

setup(
name = 'pybossa',
version = '2.4.1',
version = '2.4.2',
packages = find_packages(),
install_requires = requirements,
# only needed when installing directly from setup.py (PyPi, eggs?) and pointing to e.g. a git repo.
Expand Down
2 changes: 1 addition & 1 deletion test/test_api/test_api_common.py
Expand Up @@ -292,7 +292,7 @@ def test_query_sql_injection(self):
res = self.app.get('/api' + q)
assert res.status_code == 404, res.data


@with_context
def test_jsonpify(self):
"""Test API jsonpify decorator works."""
project = ProjectFactory.create()
Expand Down
4 changes: 4 additions & 0 deletions test/test_api/test_disqus_sso_api.py
Expand Up @@ -23,6 +23,7 @@

class TestDisqusSSO(TestAPI):

@with_context
def test_auth(self):
"""Test with user authenticated."""
url = 'api/disqus/sso'
Expand All @@ -41,6 +42,7 @@ def test_auth(self):
assert data['remote_auth_s3'] is not None, data
assert data['api_key'] is not None, data

@with_context
def test_anon(self):
"""Test with user authenticated."""
url = 'api/disqus/sso'
Expand All @@ -58,6 +60,7 @@ def test_anon(self):
assert data['remote_auth_s3'] is not None, data
assert data['api_key'] is not None, data

@with_context
def test_auth_no_keys(self):
"""Test with user authenticated."""
url = 'api/disqus/sso'
Expand All @@ -70,6 +73,7 @@ def test_auth_no_keys(self):
assert data['status'] == 'failed', data
assert data['exception_msg'] == 'Disqus keys are missing'

@with_context
def test_anon_no_keys(self):
"""Test with user authenticated."""
url = 'api/disqus/sso'
Expand Down
3 changes: 3 additions & 0 deletions test/test_api/test_global_stats_api.py
Expand Up @@ -16,13 +16,15 @@
# You should have received a copy of the GNU Affero General Public License
# along with PYBOSSA. If not, see <http://www.gnu.org/licenses/>.
import json
from default import with_context
from test_api import TestAPI
from factories import ProjectFactory



class TestGlobalStatsAPI(TestAPI):

@with_context
def test_global_stats(self):
"""Test Global Stats works."""
ProjectFactory()
Expand All @@ -35,6 +37,7 @@ def test_global_stats(self):
err_msg = "%s should be in stats JSON object" % k
assert k in stats.keys(), err_msg

@with_context
def test_post_global_stats(self):
"""Test Global Stats Post works."""
res = self.app.post('api/globalstats')
Expand Down
7 changes: 7 additions & 0 deletions test/test_api/test_project_api.py
Expand Up @@ -880,6 +880,7 @@ def test_newtask(self):
res = self.app.get(url)
assert res.data == '{}', res.data

@with_context
@patch('pybossa.repositories.project_repository.uploader')
def test_project_delete_deletes_zip_files(self, uploader):
"""Test API project delete deletes also zip files of tasks and taskruns"""
Expand All @@ -894,6 +895,7 @@ def test_project_delete_deletes_zip_files(self, uploader):
call('1_project1_task_run_csv.zip', 'user_1')]
assert uploader.delete_file.call_args_list == expected

@with_context
def test_project_post_with_reserved_fields_returns_error(self):
user = UserFactory.create()
CategoryFactory.create()
Expand All @@ -916,6 +918,7 @@ def test_project_post_with_reserved_fields_returns_error(self):
error = json.loads(res.data)
assert error['exception_msg'] == "Reserved keys in payload", error

@with_context
def test_project_put_with_reserved_returns_error(self):
user = UserFactory.create()
project = ProjectFactory.create(owner=user)
Expand All @@ -929,6 +932,7 @@ def test_project_put_with_reserved_returns_error(self):
error = json.loads(res.data)
assert error['exception_msg'] == "Reserved keys in payload", error

@with_context
def test_project_post_with_published_attribute_is_forbidden(self):
user = UserFactory.create()
data = dict(
Expand All @@ -947,6 +951,7 @@ def test_project_post_with_published_attribute_is_forbidden(self):
assert res.status_code == 403, res.status_code
assert error_msg == 'You cannot publish a project via the API', res.data

@with_context
def test_project_update_with_published_attribute_is_forbidden(self):
user = UserFactory.create()
project = ProjectFactory.create(owner=user)
Expand All @@ -960,6 +965,7 @@ def test_project_update_with_published_attribute_is_forbidden(self):
assert res.status_code == 403, res.status_code
assert error_msg == 'You cannot publish a project via the API', res.data

@with_context
def test_project_delete_with_results(self):
"""Test API delete project with results cannot be deleted."""
result = self.create_result()
Expand All @@ -970,6 +976,7 @@ def test_project_delete_with_results(self):
res = self.app.delete(url)
assert_equal(res.status, '403 FORBIDDEN', res.status)

@with_context
def test_project_delete_with_results_var(self):
"""Test API delete project with results cannot be deleted by admin."""
root = UserFactory.create(admin=True)
Expand Down
2 changes: 2 additions & 0 deletions test/test_api/test_result.py
Expand Up @@ -322,6 +322,7 @@ def test_result_post(self):
assert err['action'] == 'POST', err
assert err['exception_cls'] == 'TypeError', err

@with_context
def test_result_post_with_reserved_fields_returns_error(self):
user = UserFactory.create()
project = ProjectFactory.create(owner=user)
Expand All @@ -335,6 +336,7 @@ def test_result_post_with_reserved_fields_returns_error(self):
error = json.loads(res.data)
assert error['exception_msg'] == "Reserved keys in payload", error

@with_context
def test_result_put_with_reserved_fields_returns_error(self):
user = UserFactory.create()
result = self.create_result(owner=user)
Expand Down
5 changes: 5 additions & 0 deletions test/test_api/test_task_api.py
Expand Up @@ -380,6 +380,7 @@ def test_task_post(self):
assert err['action'] == 'POST', err
assert err['exception_cls'] == 'TypeError', err

@with_context
def test_task_post_with_reserved_fields_returns_error(self):
user = UserFactory.create()
project = ProjectFactory.create(owner=user)
Expand All @@ -394,6 +395,7 @@ def test_task_post_with_reserved_fields_returns_error(self):
error = json.loads(res.data)
assert error['exception_msg'] == "Reserved keys in payload", error

@with_context
def test_task_post_with_reserved_fav_user_ids(self):
user = UserFactory.create()
project = ProjectFactory.create(owner=user)
Expand All @@ -408,6 +410,7 @@ def test_task_post_with_reserved_fav_user_ids(self):
assert error['exception_msg'] == "Reserved keys in payload", error


@with_context
def test_task_put_with_reserved_fields_returns_error(self):
user = UserFactory.create()
project = ProjectFactory.create(owner=user)
Expand All @@ -423,6 +426,7 @@ def test_task_put_with_reserved_fields_returns_error(self):
error = json.loads(res.data)
assert error['exception_msg'] == "Reserved keys in payload", error

@with_context
def test_task_put_with_fav_user_ids_fields_returns_error(self):
user = UserFactory.create()
project = ProjectFactory.create(owner=user)
Expand Down Expand Up @@ -592,6 +596,7 @@ def test_task_delete(self):
assert root_task not in tasks, tasks


@with_context
@patch('pybossa.repositories.task_repository.uploader')
def test_task_delete_deletes_zip_files(self, uploader):
"""Test API task delete deletes also zip files with tasks and taskruns"""
Expand Down
5 changes: 5 additions & 0 deletions test/test_api/test_taskrun_api.py
Expand Up @@ -582,6 +582,7 @@ def test_taskrun_authenticated_external_uid_post(self, guard):
assert tmp.status_code == 403, tmp.data


@with_context
def test_taskrun_post_requires_newtask_first_anonymous(self):
"""Test API TaskRun post fails if task was not previously requested for
anonymous user"""
Expand All @@ -607,6 +608,7 @@ def test_taskrun_post_requires_newtask_first_anonymous(self):
success = self.app.post('/api/taskrun', data=datajson)
assert success.status_code == 200, success.data

@with_context
def test_taskrun_post_requires_newtask_first_external_uid(self):
"""Test API TaskRun post fails if task was not previously requested for
external user"""
Expand Down Expand Up @@ -882,6 +884,7 @@ def test_taskrun_updates_task_state(self, guard, mock_request):
err_msg = "Task state should be equal to completed"
assert task.state == 'completed', err_msg

@with_context
def test_taskrun_create_with_reserved_fields_returns_error(self):
"""Test API taskrun post with reserved fields raises an error"""
project = ProjectFactory.create()
Expand All @@ -902,6 +905,7 @@ def test_taskrun_create_with_reserved_fields_returns_error(self):
error = json.loads(resp.data)
assert error['exception_msg'] == "Reserved keys in payload", error

@with_context
@patch('pybossa.api.task_run.ContributionsGuard')
def test_taskrun_is_stored_if_project_is_not_published(self, guard):
"""Test API taskrun post stores the taskrun even if project is not published"""
Expand All @@ -923,6 +927,7 @@ def test_taskrun_is_stored_if_project_is_not_published(self, guard):
assert len(task_runs) == 1, task_runs
assert task_runs[0].info == 'my result', task_runs[0]

@with_context
@patch('pybossa.api.task_run.ContributionsGuard')
def test_taskrun_created_with_time_it_was_requested_on_creation(self, guard):
"""Test API taskrun post adds the created timestamp of the moment the task
Expand Down
5 changes: 5 additions & 0 deletions test/test_auditlog.py
Expand Up @@ -155,6 +155,7 @@ def test_project_update_attributes_non_owner(self):

assert len(logs) == 0, logs

@with_context
def test_project_update_task_presenter(self):
"""Test Auditlog API project update info task_presenter works."""
project = ProjectFactory.create()
Expand All @@ -177,6 +178,7 @@ def test_project_update_task_presenter(self):
msg = "%s != %s" % (data['info']['task_presenter'], log.new_value)
assert data['info']['task_presenter'] == log.new_value, msg

@with_context
def test_project_update_scheduler(self):
"""Test Auditlog API project update info scheduler works."""
project = ProjectFactory.create()
Expand All @@ -199,6 +201,7 @@ def test_project_update_scheduler(self):
msg = "%s != %s" % (data['info']['sched'], log.new_value)
assert data['info']['sched'] == log.new_value, msg

@with_context
def test_project_update_two_info_objects(self):
"""Test Auditlog API project update two info objects works."""
project = ProjectFactory.create()
Expand Down Expand Up @@ -647,6 +650,7 @@ def test_project_task_redundancy(self):
assert log.user_name == 'johndoe', log.user_name
assert log.user_id == 1, log.user_id

@with_context
def test_project_auditlog_autoimporter_create(self):
self.register()
self.new_project()
Expand Down Expand Up @@ -675,6 +679,7 @@ def test_project_auditlog_autoimporter_create(self):
assert log.user_name == 'johndoe', log.user_name
assert log.user_id == 1, log.user_id

@with_context
def test_project_auditlog_autoimporter_delete(self):
self.register()
owner = user_repo.get(1)
Expand Down

0 comments on commit 3133bb9

Please sign in to comment.