Skip to content

Commit

Permalink
SDAN-703 Apply product permissions to image assets available via the … (
Browse files Browse the repository at this point in the history
#1139)

* SDAN-703 Apply product permissions to image assets available via the Newsroom API

* Add ability to configure API Image permissions
  • Loading branch information
marwoodandrew authored Jun 21, 2021
1 parent 06a2c55 commit 530c3ee
Show file tree
Hide file tree
Showing 9 changed files with 192 additions and 17 deletions.
2 changes: 2 additions & 0 deletions features/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ def before_all(context):
'CORE_APPS': CORE_APPS,
'ELASTICSEARCH_FORCE_REFRESH': True,
'NEWS_API_ENABLED': True,
'NEWS_API_IMAGE_PERMISSIONS_ENABLED': True,
'NEWS_API_TIME_LIMIT_DAYS': 100
}
setup_before_all(context, config, app_factory=get_app)
Expand All @@ -28,6 +29,7 @@ def before_scenario(context, scenario):
'CORE_APPS': CORE_APPS,
'ELASTICSEARCH_FORCE_REFRESH': True,
'NEWS_API_ENABLED': True,
'NEWS_API_IMAGE_PERMISSIONS_ENABLED': True,
'NEWS_API_TIME_LIMIT_DAYS': 100
}

Expand Down
35 changes: 35 additions & 0 deletions features/news_api_atom.feature
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,38 @@ Feature: News API News Search
Then we "get" "<title><![CDATA[headline 1]]></title>" in atom xml response
Then we "get" "5fc5dce16369ab07be3325fa" in atom xml response
Then we "get" "src="http://" in atom xml response
Scenario: Atom request response restricted by featured image product
Given "items"
"""
[{"body_html": "Once upon a time there was a fish who could swim", "headline": "headline 1",
"firstpublished": "#DATE-1#", "versioncreated": "#DATE#",
"associations": {"featuremedia": {"products": [{"code": "1234"}], "renditions": {"original": {}} }}},
{"body_html": "Once upon a time there was a aardvark that could not swim", "headline": "headline 2",
"firstpublished": "#DATE-1#", "versioncreated": "#DATE#",
"associations": {"featuremedia": {"products": [{"code": "4321"}], "renditions": {"original": {}} }}}]
"""
Given "products"
"""
[{"name": "A fishy Product",
"decsription": "a product for those interested in fish",
"companies" : [
"#companies._id#"
],
"query": "Once upon a time",
"product_type": "news_api"
},
{"name": "A fishy superdesk product",
"description": "a superdesk product restricting images in the atom feed",
"companies" : [
"#companies._id#"
],
"sd_product_id": "1234",
"product_type": "news_api"
}
]
"""
When we get "atom"
Then we get OK response
Then we "get" "<title><![CDATA[headline 1]]></title>" in atom xml response
Then we "don't get" "<title><![CDATA[headline 2]]></title>" in atom xml response
49 changes: 49 additions & 0 deletions features/news_api_item.feature
Original file line number Diff line number Diff line change
Expand Up @@ -144,4 +144,53 @@ Feature: News API Item
}
}
}
"""

Scenario: Item request response restricted by featured image product
Given "items"
"""
[{"_id": "111", "body_html": "Once upon a time there was a fish who could swim", "headline": "headline 1",
"firstpublished": "#DATE-1#", "versioncreated": "#DATE#",
"associations": {"featuremedia": {"products": [{"code": "1234"}], "renditions": {"original": {}} }}},
{"_id": "222", "body_html": "Once upon a time there was a aardvark that could not swim", "headline": "headline 2",
"firstpublished": "#DATE-1#", "versioncreated": "#DATE#",
"associations": {"featuremedia": {"products": [{"code": "4321"}], "renditions": {"original": {}} }}}]
"""
Given "products"
"""
[{"name": "A fishy Product",
"decsription": "a product for those interested in fish",
"companies" : [
"#companies._id#"
],
"query": "Once upon a time",
"product_type": "news_api"
},
{"name": "A fishy superdesk product",
"description": "a superdesk product restricting images in the atom feed",
"companies" : [
"#companies._id#"
],
"sd_product_id": "1234",
"product_type": "news_api"
}
]
"""
When we get "/news/item/222?format=NINJSFormatter2"
Then we get existing resource
"""
{
"guid": "222",
"headline": "headline 2",
"associations": "__no_value__"
}
"""
When we get "/news/item/111?format=NINJSFormatter2"
Then we get existing resource
"""
{
"guid": "111",
"headline": "headline 1",
"associations": {"featuremedia": {"products": [{"code": "1234"}], "renditions": {"original": {}} }}
}
"""
47 changes: 47 additions & 0 deletions features/news_api_search.feature
Original file line number Diff line number Diff line change
Expand Up @@ -550,3 +550,50 @@ Feature: News API News Search
"""
{"code": 400, "message": "Bad parameter value for Parameter (timezone)"}
"""

Scenario: Search request response restricted by featured image product
Given "items"
"""
[{"_id": "111", "body_html": "Once upon a time there was a fish who could swim", "headline": "headline 1",
"firstpublished": "#DATE-1#", "versioncreated": "#DATE#",
"associations": {"featuremedia": {"products": [{"code": "1234"}], "renditions": {"original": {}} }}},
{"_id": "222", "body_html": "Once upon a time there was a aardvark that could not swim", "headline": "headline 2",
"firstpublished": "#DATE-1#", "versioncreated": "#DATE#",
"associations": {"featuremedia": {"products": [{"code": "4321"}], "renditions": {"original": {}} }}}]
"""
Given "products"
"""
[{"name": "A fishy Product",
"decsription": "a product for those interested in fish",
"companies" : [
"#companies._id#"
],
"query": "Once upon a time",
"product_type": "news_api"
},
{"name": "A fishy superdesk product",
"description": "a superdesk product restricting images in the atom feed",
"companies" : [
"#companies._id#"
],
"sd_product_id": "1234",
"product_type": "news_api"
}
]
"""
When we get "news/search?q=fish&include_fields=associations"
Then we get list with 1 items
"""
{"_items": [
{"_id": "111",
"associations": {"featuremedia": {"products": [{"code": "1234"}], "renditions": {"original": {}} }}}
]}
"""
When we get "news/search?q=aardvark&include_fields=associations"
Then we get list with 1 items
"""
{"_items": [
{"_id": "222",
"associations": "__no_value__"}
]}
"""
3 changes: 3 additions & 0 deletions newsroom/default_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,3 +321,6 @@
CONTENT_API_EXPIRY_DAYS = os.environ.get('CONTENT_API_EXPIRY_DAYS', 180)

NEWS_API_ENABLED = strtobool(env('NEWS_API_ENABLED', 'false'))

# Enables the application of product filtering to image references in the API and ATOM responses
NEWS_API_IMAGE_PERMISSIONS_ENABLED = strtobool(env('NEWS_API_IMAGE_PERMISSIONS_ENABLED', 'false'))
34 changes: 21 additions & 13 deletions newsroom/news_api/news/atom/atom.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import datetime
import logging
import re
from newsroom.news_api.utils import check_association_permission

blueprint = superdesk.Blueprint('atom', __name__)

Expand Down Expand Up @@ -58,27 +59,33 @@ def _format_update_date(date):

for item in response[0].get('_items'):
try:
complete_item = item = superdesk.get_resource_service('items').find_one(req=None, _id=item.get('_id'))
complete_item = superdesk.get_resource_service('items').find_one(req=None, _id=item.get('_id'))

# If featuremedia is not allowed for the company don't add the item
if ((complete_item.get('associations') or {}).get('featuremedia') or {}).get('renditions'):
if not check_association_permission(complete_item):
continue

entry = SubElement(feed, 'entry')

# If the item has any parents we use the id of the first, this should be constant throught the update
# history
if item.get('ancestors') and len(item.get('ancestors')):
SubElement(entry, 'id').text = item.get('ancestors')[0]
if complete_item.get('ancestors') and len(complete_item.get('ancestors')):
SubElement(entry, 'id').text = complete_item.get('ancestors')[0]
else:
SubElement(entry, 'id').text = item.get('_id')
SubElement(entry, 'id').text = complete_item.get('_id')

SubElement(entry, 'title').text = etree.CDATA(item.get('headline'))
SubElement(entry, 'title').text = etree.CDATA(complete_item.get('headline'))
SubElement(entry, 'published').text = _format_date(complete_item.get('firstpublished'))
SubElement(entry, 'updated').text = _format_update_date(item.get('versioncreated'))
SubElement(entry, 'updated').text = _format_update_date(complete_item.get('versioncreated'))
SubElement(entry, 'link', attrib={'rel': 'self', 'href': flask.url_for('news/item.get_item',
item_id=item.get('_id'),
format='TextFormatter',
_external=True)})
if item.get('byline'):
SubElement(SubElement(entry, 'author'), 'name').text = item.get('byline')
if complete_item.get('byline'):
SubElement(SubElement(entry, 'author'), 'name').text = complete_item.get('byline')

if item.get('pubstatus') == 'usable':
if complete_item.get('pubstatus') == 'usable':
SubElement(entry, etree.QName(_message_nsmap.get('dcterms'), 'valid')).text = \
'start={}; end={}; scheme=W3C-DTF'.format(_format_date(utcnow()),
_format_date(utcnow() + datetime.timedelta(days=30)))
Expand All @@ -88,7 +95,7 @@ def _format_update_date(date):
'start={}; end={}; scheme=W3C-DTF'.format(_format_date(utcnow()),
_format_date(utcnow() - datetime.timedelta(days=30)))

categories = [{'name': s.get('name')} for s in item.get('service', [])]
categories = [{'name': s.get('name')} for s in complete_item.get('service', [])]
for category in categories:
SubElement(entry, 'category', attrib={'term': category.get('name')})

Expand Down Expand Up @@ -118,9 +125,10 @@ def _format_update_date(date):

SubElement(entry, 'content', attrib={'type': 'html'}).text = etree.CDATA(complete_item.get('body_html', ''))

if ((item.get('associations') or {}).get('featuremedia') or {}).get('renditions'):
image = ((item.get('associations') or {}).get('featuremedia') or {}).get('renditions').get("16-9")
metadata = ((item.get('associations') or {}).get('featuremedia') or {})
if ((complete_item.get('associations') or {}).get('featuremedia') or {}).get('renditions'):
image = ((complete_item.get('associations') or {}).get('featuremedia') or {}).get('renditions').get(
"16-9")
metadata = ((complete_item.get('associations') or {}).get('featuremedia') or {})

url = flask.url_for('assets.get_item', _external=True, asset_id=image.get('media'))
media = SubElement(entry, etree.QName(_message_nsmap.get('media'), 'content'),
Expand Down
7 changes: 5 additions & 2 deletions newsroom/news_api/news/search_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
from content_api.errors import BadParameterValueError, UnexpectedParameterError

from newsroom.news_api.settings import ELASTIC_DATETIME_FORMAT
from newsroom.news_api.utils import post_api_audit, remove_internal_renditions
from newsroom.news_api.utils import post_api_audit, remove_internal_renditions, check_association_permission
from newsroom.search import BaseSearchService, query_string
from newsroom.products.products import get_products_by_company

Expand Down Expand Up @@ -69,7 +69,10 @@ def get(self, req, lookup):
doc.pop(field, None)

if 'associations' in orig_request_params.get('include_fields', ''):
remove_internal_renditions(doc)
if not check_association_permission(doc):
doc.pop('associations', None)
else:
remove_internal_renditions(doc)

return resp

Expand Down
28 changes: 27 additions & 1 deletion newsroom/news_api/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from superdesk import get_resource_service
from superdesk.utc import utcnow
from flask import request, g
from flask import request, g, current_app as app
from newsroom.products.products import get_products_by_company


def post_api_audit(doc):
Expand Down Expand Up @@ -53,3 +54,28 @@ def remove_internal_renditions(item):
meta.pop('subscribers', None)

return item


def check_association_permission(item):
"""
Check if any of the products that the passed image item matches are permissioned superdesk products for the
company
:param item:
:return:
"""
if not app.config.get('NEWS_API_IMAGE_PERMISSIONS_ENABLED'):
return True

if ((item.get('associations') or {}).get('featuremedia') or {}).get('products'):
# Extract the products that the image matched in Superdesk
im_products = [p.get('code') for p in
((item.get('associations') or {}).get('featuremedia') or {}).get('products')]

# Check if the one of the companies products that has a superdesk product id matches one of the
# image product id's
sd_products = [p.get('sd_product_id') for p in get_products_by_company(g.user, None, 'news_api') if
p.get('sd_product_id')]

return True if len(set(im_products) & set(sd_products)) else False
else:
return True
4 changes: 3 additions & 1 deletion newsroom/wire/formatters/ninjs2.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .ninjs import NINJSFormatter
from newsroom.news_api.utils import remove_internal_renditions
from newsroom.news_api.utils import remove_internal_renditions, check_association_permission


class NINJSFormatter2(NINJSFormatter):
Expand All @@ -11,4 +11,6 @@ def __init__(self):
self.direct_copy_properties += ('associations',)

def _transform_to_ninjs(self, item):
if not check_association_permission(item):
item.pop('associations', None)
return remove_internal_renditions(super()._transform_to_ninjs(item))

0 comments on commit 530c3ee

Please sign in to comment.