Skip to content

Commit

Permalink
Some permissions fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
noirbizarre committed May 5, 2015
1 parent 6258c70 commit 3d6bbd9
Show file tree
Hide file tree
Showing 14 changed files with 232 additions and 32 deletions.
12 changes: 8 additions & 4 deletions udata/core/dataset/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,21 +63,23 @@ def post(self):

@ns.route('/<dataset:dataset>/', endpoint='dataset', doc=common_doc)
@api.response(404, 'Dataset not found')
class ModelAPI(SingleObjectAPI, API):
fields = None
form = None

@api.response(410, 'Dataset has been deleted')
class DatasetAPI(API):
@api.doc('get_dataset')
@api.marshal_with(dataset_fields)
def get(self, dataset):
'''Get a dataset given its identifier'''
if dataset.deleted:
api.abort(410, 'Dataset has been deleted')
return dataset

@api.secure
@api.doc('update_dataset')
@api.response(400, 'Validation error')
def put(self, dataset):
'''Update a dataset given its identifier'''
if dataset.deleted:
api.abort(410, 'Dataset has been deleted')
DatasetEditPermission(dataset).test()
form = api.validate(DatasetForm, dataset)
return form.save()
Expand All @@ -87,6 +89,8 @@ def put(self, dataset):
@api.response(204, 'Dataset deleted')
def delete(self, dataset):
'''Delete a dataset given its identifier'''
if dataset.deleted:
api.abort(410, 'Dataset has been deleted')
DatasetEditPermission(dataset).test()
dataset.deleted = datetime.now()
dataset.save()
Expand Down
2 changes: 2 additions & 0 deletions udata/core/dataset/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ def get_context(self):
context = super(DatasetDetailView, self).get_context()
if self.dataset.private and not DatasetEditPermission(self.dataset).can():
abort(404)
elif self.dataset.deleted:
abort(410)
context['reuses'] = Reuse.objects(datasets=self.dataset)
context['can_edit'] = DatasetEditPermission(self.dataset)
context['can_edit_resource'] = CommunityResourceEditPermission
Expand Down
65 changes: 52 additions & 13 deletions udata/core/organization/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

from datetime import datetime

from flask import request
from werkzeug.datastructures import FileStorage

from udata import search
from udata.api import api, ModelAPI, ModelListAPI, API
from udata.auth import current_user
from udata.core.followers.api import FollowAPI
from udata.utils import multi_to_dict

from .forms import OrganizationForm, MembershipRequestForm, MembershipRefuseForm, MemberForm
from .models import Organization, MembershipRequest, Member, FollowOrg
Expand All @@ -33,22 +35,59 @@


@ns.route('/', endpoint='organizations')
@api.doc(get={'id': 'list_organizations', 'model': org_page_fields, 'parser': search_parser})
@api.doc(post={'id': 'create_organization', 'model': org_fields, 'body': org_fields})
class OrganizationListAPI(ModelListAPI):
model = Organization
fields = org_fields
form = OrganizationForm
search_adapter = OrganizationSearch
class OrganizationListAPI(API):
'''Organizations collection endpoint'''
@api.doc('list_organizations', parser=search_parser)
@api.marshal_with(org_page_fields)
def get(self):
'''List or search all organizations'''
return search.query(OrganizationSearch, **multi_to_dict(request.args))

@api.secure
@api.doc('create_organization', responses={400: 'Validation error'})
@api.expect(org_fields)
@api.marshal_with(org_fields)
def post(self):
'''Create a new organization'''
form = api.validate(OrganizationForm)
organization = form.save()
return organization, 201


@ns.route('/<org:org>/', endpoint='organization', doc=common_doc)
@api.doc(model=org_fields, get={'id': 'get_organization'})
@api.doc(put={'id': 'update_organization', 'body': org_fields})
class OrganizationAPI(ModelAPI):
model = Organization
fields = org_fields
form = OrganizationForm
@api.response(404, 'Organization not found')
@api.response(410, 'Organization has been deleted')
class OrganizationAPI(API):
@api.doc('get_organization')
@api.marshal_with(org_fields)
def get(self, org):
'''Get a organization given its identifier'''
if org.deleted:
api.abort(410, 'Organization has been deleted')
return org

@api.secure
@api.doc('update_organization')
@api.response(400, 'Validation error')
def put(self, org):
'''Update a organization given its identifier'''
if org.deleted:
api.abort(410, 'Organization has been deleted')
EditOrganizationPermission(org).test()
form = api.validate(OrganizationForm, org)
return form.save()

@api.secure
@api.doc('delete_organization')
@api.response(204, 'Organization deleted')
def delete(self, org):
'''Delete a organization given its identifier'''
if org.deleted:
api.abort(410, 'Organization has been deleted')
EditOrganizationPermission(org).test()
org.deleted = datetime.now()
org.save()
return '', 204


requests_parser = api.parser()
Expand Down
11 changes: 7 additions & 4 deletions udata/core/organization/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

from datetime import datetime

from flask import request, g, jsonify, redirect, url_for
from flask import request, g, jsonify, redirect, url_for, abort
from flask.ext.security import current_user

from udata import search
Expand Down Expand Up @@ -80,13 +80,16 @@ class OrganizationDetailView(OrgView, DetailView):
def get_context(self):
context = super(OrganizationDetailView, self).get_context()

can_edit = EditOrganizationPermission(self.organization)
can_view = OrganizationPrivatePermission(self.organization)

if self.organization.deleted and not can_view.can():
abort(410)

datasets = Dataset.objects(organization=self.organization).visible().order_by('-created')
supplied_datasets = Dataset.objects(supplier=self.organization).visible().order_by('-created')
reuses = Reuse.objects(organization=self.organization).visible().order_by('-created')
followers = FollowOrg.objects.followers(self.organization).order_by('follower.fullname')

can_edit = EditOrganizationPermission(self.organization)
can_view = OrganizationPrivatePermission(self.organization)
context.update({
'reuses': reuses.paginate(1, self.page_size),
'datasets': datasets.paginate(1, self.page_size),
Expand Down
14 changes: 11 additions & 3 deletions udata/core/reuse/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ def post(self):
return form.save(), 201


@ns.route('/<reuse:reuse>/', endpoint='reuse')
@api.doc(responses={404: 'Object not found'}, **common_doc)
@ns.route('/<reuse:reuse>/', endpoint='reuse', doc=common_doc)
@api.response(404, 'Reuse not found')
@api.response(410, 'Reuse has been deleted')
class ReuseAPI(ModelAPI):
@api.doc('get_reuse')
@api.marshal_with(reuse_fields)
def get(self, reuse):
'''Fetch a given reuse'''
if reuse.deleted and not ReuseEditPermission(reuse).can():
api.abort(410, 'This reuse has been deleted')
return reuse

@api.secure
Expand All @@ -62,14 +65,19 @@ def get(self, reuse):
@api.marshal_with(reuse_fields)
def put(self, reuse):
'''Update a given reuse'''
if reuse.deleted:
api.abort(410, 'This reuse has been deleted')
ReuseEditPermission(reuse).test()
form = api.validate(ReuseForm, reuse)
return form.save()

@api.secure
@api.doc('delete_reuse', responses={204: 'Reuse deleted'})
@api.doc('delete_reuse')
@api.response(204, 'Reuse deleted')
def delete(self, reuse):
'''Delete a given reuse'''
if reuse.deleted:
api.abort(410, 'This reuse has been deleted')
ReuseEditPermission(reuse).test()
reuse.deleted = datetime.now()
reuse.save()
Expand Down
3 changes: 3 additions & 0 deletions udata/core/reuse/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ def get_context(self):
if self.reuse.private and not ReuseEditPermission(self.reuse).can():
abort(404)

if self.reuse.deleted and not ReuseEditPermission(self.reuse).can():
abort(410)

followers = FollowReuse.objects.followers(self.reuse).order_by('follower.fullname')

context.update(
Expand Down
5 changes: 5 additions & 0 deletions udata/frontend/error_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ def page_not_found(error):
return theme.render('errors/404.html', error=error), 404


@front.app_errorhandler(410)
def page_deleted(error):
return theme.render('errors/410.html', error=error), 410


@front.app_errorhandler(500)
def internal_error(error):
return theme.render('errors/500.html', error=error), 500
7 changes: 7 additions & 0 deletions udata/templates/errors/410.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{% extends theme("errors/base.html") %}

{% set title = _('Page Not Found') %}

{% block details %}
<p class="lead">{{ _("The page you're looking has been deleted.") }}</p>
{% endblock %}
30 changes: 29 additions & 1 deletion udata/tests/api/test_datasets_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

import json

from datetime import datetime

from flask import url_for

from udata.models import Dataset, Follow, FollowDataset, Member

from . import APITestCase
from ..factories import DatasetFactory, ResourceFactory, OrganizationFactory, AdminFactory, faker
from ..factories import DatasetFactory, ResourceFactory, OrganizationFactory, AdminFactory, VisibleDatasetFactory, faker


class DatasetAPITest(APITestCase):
Expand Down Expand Up @@ -43,6 +45,13 @@ def test_dataset_api_get(self):
data = json.loads(response.data)
self.assertEqual(len(data['resources']), len(resources))

def test_dataset_api_get_deleted(self):
'''It should not fetch a deleted dataset from the API and raise 410'''
dataset = VisibleDatasetFactory(owner=self.user, deleted=datetime.now())

response = self.get(url_for('api.dataset', dataset=dataset))
self.assertStatus(response, 410)

def test_dataset_api_create(self):
'''It should create a dataset from the API'''
data = DatasetFactory.attributes()
Expand Down Expand Up @@ -122,6 +131,17 @@ def test_dataset_api_update(self):
self.assertEqual(Dataset.objects.count(), 1)
self.assertEqual(Dataset.objects.first().description, 'new description')

def test_dataset_api_update_deleted(self):
'''It should not update a deleted dataset from the API and raise 401'''
user = self.login()
dataset = DatasetFactory(owner=user, deleted=datetime.now())
data = dataset.to_dict()
data['description'] = 'new description'
response = self.put(url_for('api.dataset', dataset=dataset), data)
self.assertStatus(response, 410)
self.assertEqual(Dataset.objects.count(), 1)
self.assertEqual(Dataset.objects.first().description, dataset.description)

def test_dataset_api_delete(self):
'''It should delete a dataset from the API'''
user = self.login()
Expand All @@ -137,6 +157,14 @@ def test_dataset_api_delete(self):
self.assert200(response)
self.assertEqual(len(response.json['data']), 0)

def test_dataset_api_delete_deleted(self):
'''It should delete a deleted dataset from the API and raise 410'''
user = self.login()
dataset = VisibleDatasetFactory(owner=user, deleted=datetime.now())
response = self.delete(url_for('api.dataset', dataset=dataset))

self.assertStatus(response, 410)

def test_dataset_api_feature(self):
'''It should mark the dataset featured on POST'''
self.login(AdminFactory())
Expand Down
68 changes: 63 additions & 5 deletions udata/tests/api/test_organizations_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from datetime import datetime

from flask import url_for

from udata.models import Organization, Member, MembershipRequest, Follow, FollowOrg
Expand Down Expand Up @@ -30,6 +32,12 @@ def test_organization_api_get(self):
response = self.get(url_for('api.organization', org=organization))
self.assert200(response)

def test_organization_api_get_deleted(self):
'''It should not fetch a deleted organization from the API'''
organization = OrganizationFactory(deleted=datetime.now())
response = self.get(url_for('api.organization', org=organization))
self.assertStatus(response, 410)

def test_organization_api_create(self):
'''It should create an organization from the API'''
data = OrganizationFactory.attributes()
Expand All @@ -45,24 +53,74 @@ def test_organization_api_create(self):

def test_dataset_api_update(self):
'''It should update an organization from the API'''
org = OrganizationFactory()
self.login()
member = Member(user=self.user, role='admin')
org = OrganizationFactory(members=[member])
data = org.to_dict()
data['description'] = 'new description'
self.login()
response = self.put(url_for('api.organization', org=org), data)
self.assert200(response)
self.assertEqual(Organization.objects.count(), 1)
self.assertEqual(Organization.objects.first().description, 'new description')

def test_dataset_api_update_deleted(self):
'''It should not update a deleted organization from the API'''
org = OrganizationFactory(deleted=datetime.now())
data = org.to_dict()
data['description'] = 'new description'
self.login()
response = self.put(url_for('api.organization', org=org), data)
self.assertStatus(response, 410)
self.assertEqual(Organization.objects.first().description, org.description)

def test_dataset_api_update_forbidden(self):
'''It should not update an organization from the API if not admin'''
org = OrganizationFactory()
data = org.to_dict()
data['description'] = 'new description'
self.login()
response = self.put(url_for('api.organization', org=org), data)
self.assert403(response)
self.assertEqual(Organization.objects.count(), 1)
self.assertEqual(Organization.objects.first().description, org.description)

def test_organization_api_delete(self):
'''It should delete an organization from the API'''
organization = OrganizationFactory()
with self.api_user():
response = self.delete(url_for('api.organization', org=organization))
self.login()
member = Member(user=self.user, role='admin')
org = OrganizationFactory(members=[member])
response = self.delete(url_for('api.organization', org=org))
self.assertStatus(response, 204)
self.assertEqual(Organization.objects.count(), 1)
self.assertIsNotNone(Organization.objects[0].deleted)

def test_organization_api_delete_deleted(self):
'''It should not delete a deleted organization from the API'''
self.login()
organization = OrganizationFactory(deleted=datetime.now())
response = self.delete(url_for('api.organization', org=organization))
self.assertStatus(response, 410)
self.assertIsNotNone(Organization.objects[0].deleted)

def test_organization_api_delete_as_editor_forbidden(self):
'''It should not delete an organization from the API if not admin'''
self.login()
member = Member(user=self.user, role='editor')
org = OrganizationFactory(members=[member])
response = self.delete(url_for('api.organization', org=org))
self.assert403(response)
self.assertEqual(Organization.objects.count(), 1)
self.assertIsNone(Organization.objects[0].deleted)

def test_organization_api_delete_as_non_member_forbidden(self):
'''It should delete an organization from the API if not member'''
self.login()
org = OrganizationFactory()
response = self.delete(url_for('api.organization', org=org))
self.assert403(response)
self.assertEqual(Organization.objects.count(), 1)
self.assertIsNone(Organization.objects[0].deleted)

# def test_organization_api_delete_not_found(self):
# '''It should raise a 404 on delete from the API if not found'''
# OrganizationFactory()
Expand Down

0 comments on commit 3d6bbd9

Please sign in to comment.