Skip to content

Commit

Permalink
check user authorization on api calls - backport from newsroom-core (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
marwoodandrew authored Apr 23, 2024
1 parent 096fad4 commit c19c2ee
Show file tree
Hide file tree
Showing 11 changed files with 143 additions and 9 deletions.
15 changes: 15 additions & 0 deletions newsroom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import superdesk
from superdesk import register_resource # noqa
from newsroom.user_roles import UserRole

# reuse content api dbs
MONGO_PREFIX = 'CONTENTAPI_MONGO'
Expand All @@ -17,6 +18,20 @@ class Resource(superdesk.Resource):
mongo_prefix = MONGO_PREFIX
elastic_prefix = ELASTIC_PREFIX

# by default make resources available to internal users/administrators
allowed_roles = [UserRole.ADMINISTRATOR, UserRole.INTERNAL, UserRole.ACCOUNT_MANAGEMENT]
allowed_item_roles = [UserRole.ADMINISTRATOR, UserRole.INTERNAL, UserRole.ACCOUNT_MANAGEMENT]

def __init__(self, endpoint_name, app, service, endpoint_schema=None):
super().__init__(endpoint_name, app, service, endpoint_schema)
config = app.config["DOMAIN"][endpoint_name]
config.update(
{
"allowed_roles": [role.value for role in self.allowed_roles],
"allowed_item_roles": [role.value for role in self.allowed_item_roles],
}
)


class Service(superdesk.Service):
pass
8 changes: 7 additions & 1 deletion newsroom/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import re
import flask

import superdesk
from bson import ObjectId
Expand All @@ -10,7 +11,12 @@

class SessionAuth(BasicAuth):
def authorized(self, allowed_roles, resource, method):
return get_user_id()
if not get_user_id():
return False
if not resource:
return True # list of apis is open
user_role = flask.session.get("user_type") if flask.request else None
return user_role in allowed_roles


def get_user(required=False):
Expand Down
3 changes: 3 additions & 0 deletions newsroom/topics/topics.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@

import newsroom
import superdesk
from newsroom.user_roles import UserRole


class TopicsResource(newsroom.Resource):
Expand All @@ -22,6 +23,8 @@ class TopicsResource(newsroom.Resource):
'schema': {'type': 'string'},
}
}
allowed_roles = [role for role in UserRole]
allowed_item_roles = allowed_roles


class TopicsService(newsroom.Service):
Expand Down
9 changes: 9 additions & 0 deletions newsroom/user_roles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import enum


class UserRole(enum.Enum):
ADMINISTRATOR = "administrator"
INTERNAL = "internal"
PUBLIC = "public"
COMPANY_ADMIN = "company_admin"
ACCOUNT_MANAGEMENT = "account_management"
3 changes: 2 additions & 1 deletion newsroom/users/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from flask import Blueprint
from flask_babel import lazy_gettext
import superdesk
from .users import UsersResource, UsersService
from .users import AuthUserResource, UsersResource, UsersService, AuthUserService

blueprint = Blueprint('users', __name__)

Expand All @@ -10,6 +10,7 @@

def init_app(app):
superdesk.register_resource('users', UsersResource, UsersService, _app=app)
superdesk.register_resource("auth_user", AuthUserResource, AuthUserService, _app=app)
app.add_template_global(views.get_view_data, 'get_user_profile_data')
app.settings_app('users', lazy_gettext('User Management'), weight=200, data=views.get_settings_data,
allow_account_mgr=True)
57 changes: 55 additions & 2 deletions newsroom/users/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,51 @@
from flask import current_app as app, session

import newsroom
import superdesk
from flask import request
from content_api import MONGO_PREFIX
from superdesk.utils import is_hashed, get_hash
from newsroom.auth import get_user_id
from newsroom.auth import get_user, get_user_id, SessionAuth
from newsroom.utils import set_original_creator, set_version_creator
from newsroom.user_roles import UserRole


class UserAuthentication(SessionAuth):
def authorized(self, allowed_roles, resource, method):
if super().authorized(allowed_roles, resource, method):
return True

if not get_user_id():
return False

if not request.view_args or not request.view_args.get("_id"):
# not a request for a specific user, stop
return False

if request.view_args["_id"] == str(get_user_id()):
# current user editing current user
return True

current_user = get_user()
if not current_user.get("company") or current_user.get("user_type") != UserRole.COMPANY_ADMIN.value:
# current user not a company admin
return False

request_user = superdesk.get_resource_service("users").find_one(req=None, _id=request.view_args["_id"])
if request_user.get("company") and request_user["company"] == current_user["company"]:
# if current user is a company admin for request user
return True

return False


class UsersResource(newsroom.Resource):
"""
Users schema
"""

authentication = UserAuthentication()

schema = {
'password': {
'type': 'string',
Expand Down Expand Up @@ -96,7 +130,7 @@ class UsersResource(newsroom.Resource):
mongo_prefix = MONGO_PREFIX
datasource = {
'source': 'users',
'projection': {'password': 0},
'projection': {'password': 0, 'token': 0},
'default_sort': [('last_name', 1)]
}
mongo_indexes = {
Expand Down Expand Up @@ -144,3 +178,22 @@ def password_match(self, password, hashed_password):

def on_deleted(self, doc):
app.cache.delete(str(doc.get('_id')))


class AuthUserResource(newsroom.Resource):
internal_resource = True

schema = {
"email": UsersResource.schema["email"],
"password": UsersResource.schema["password"],
"token": UsersResource.schema["token"],
"token_expiry_date": UsersResource.schema["token_expiry_date"],
}

datasource = {
"source": "users",
}


class AuthUserService(newsroom.Service):
pass
4 changes: 4 additions & 0 deletions newsroom/wire/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from newsroom.auth import get_user
from newsroom.companies import get_user_company
from newsroom.products.products import get_products_by_company
from newsroom.user_roles import UserRole


logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -53,6 +54,9 @@ class WireSearchResource(newsroom.Resource):
item_methods = ['GET']
resource_methods = ['GET']

allowed_roles = [role for role in UserRole]
allowed_item_roles = allowed_roles


def versioncreated_range(created):
_range = {}
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ werkzeug>=0.9.4,<=0.11.15
urllib3<1.26

-e .
git+https://github.com/superdesk/superdesk-planning.git@support/1.33#egg=superdesk-planning
git+https://github.com/superdesk/superdesk-planning.git@v1.33.3#egg=superdesk-planning
git+https://github.com/superdesk/superdesk-core.git@hotfix/1.33.17#egg=Superdesk-Core
38 changes: 38 additions & 0 deletions tests/test_api_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from flask import url_for
from bson import ObjectId


def test_public_user_api(app, client):
company_ids = app.data.insert('companies', [{
'phone': '2132132134',
'sd_subscriber_id': '12345',
'name': 'Press Co.',
'is_enabled': True,
'contact_name': 'Tom'
}])
user = {
'_id': ObjectId("5c5914275f627d5885fee6a8"),
'first_name': 'Normal',
'last_name': 'User',
'email': '[email protected]',
'password': '$2b$12$HGyWCf9VNfnVAwc2wQxQW.Op3Ejk7KIGE6urUXugpI0KQuuK6RWIG',
'user_type': 'public',
'is_validated': True,
'is_enabled': True,
'is_approved': True,
'receive_email': True,
'phone': '2132132134',
'expiry_alert': True,
'company': company_ids[0]}
app.data.insert('users', [user])
client.post(
url_for('auth.login'),
data={'email': '[email protected]', 'password': 'admin'},
follow_redirects=True
)

resp = client.get("/api")
assert 200 == resp.status_code

resp = client.get("/api/users")
assert resp.status_code == 401
7 changes: 6 additions & 1 deletion tests/test_topics.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from flask import json
from newsroom.topics.views import get_topic_url
from .fixtures import init_company, PUBLIC_USER_ID, TEST_USER_ID # noqa
from unittest import mock
from .utils import mock_send_email
from newsroom.topics.views import get_topic_url


topic = {
'label': 'Foo',
Expand Down Expand Up @@ -36,6 +37,7 @@ def test_post_topic_user(client):
with client as app:
with client.session_transaction() as session:
session['user'] = user_id
session['user_type'] = 'administrator'
resp = app.post(
topics_url,
data=json.dumps(topic),
Expand All @@ -52,6 +54,7 @@ def test_update_topic_fails_for_different_user(client):
with client as app:
with client.session_transaction() as session:
session['user'] = user_id
session['user_type'] = 'administrator'
resp = app.post(
topics_url,
data=json.dumps(topic),
Expand All @@ -74,6 +77,7 @@ def test_update_topic(client):
with client as app:
with client.session_transaction() as session:
session['user'] = user_id
session['user_type'] = 'administrator'
resp = app.post(
topics_url,
data=json.dumps(topic),
Expand All @@ -99,6 +103,7 @@ def test_delete_topic(client):
with client as app:
with client.session_transaction() as session:
session['user'] = user_id
session['user_type'] = 'administrator'
resp = app.post(
topics_url,
data=json.dumps(topic),
Expand Down
6 changes: 3 additions & 3 deletions tests/test_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime, timedelta
from superdesk import get_resource_service

from newsroom.auth import get_user_by_email
from newsroom.auth import get_auth_user_by_email
from newsroom.utils import get_user_dict, get_company_dict, is_valid_login
from .utils import mock_send_email
from unittest import mock
Expand Down Expand Up @@ -79,7 +79,7 @@ def test_reset_password_token_sent_for_user_succeeds(app, client):
response = client.post('/users/59b4c5c61d41c8d736852fbf/reset_password')
assert response.status_code == 200
assert '"success": true' in response.get_data(as_text=True)
user = get_resource_service('users').find_one(req=None, email='[email protected]')
user = get_resource_service('auth_user').find_one(req=None, email='[email protected]')
assert user.get('token') is not None


Expand Down Expand Up @@ -185,7 +185,7 @@ def test_create_new_user_succeeds(app, client):
assert 'account created' in outbox[0].subject

# get reset password token
user = get_user_by_email('[email protected]')
user = get_auth_user_by_email('[email protected]')
client.get(url_for('auth.reset_password', token=user['token']))

# change the password
Expand Down

0 comments on commit c19c2ee

Please sign in to comment.