Skip to content

Commit

Permalink
views: new users search by role
Browse files Browse the repository at this point in the history
* NEW Adds a search endpoint listing users having a specific role
  assigned.

Signed-off-by: Nicolas Harraudeau <nicolas.harraudeau@cern.ch>
  • Loading branch information
Nicolas Harraudeau committed Feb 17, 2017
1 parent 8ada03d commit c229d1a
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 30 deletions.
5 changes: 4 additions & 1 deletion invenio_accounts_rest/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,11 @@
ACCOUNTS_REST_UNASSIGN_ROLE_PERMISSION_FACTORY = deny_all
"""Default unassign role from user permission factory: reject any request."""

ACCOUNTS_REST_READ_ROLE_USERS_LIST_PERMISSION_FACTORY = deny_all
"""Default list roles' users permission factory: reject any request."""

ACCOUNTS_REST_READ_USER_ROLES_LIST_PERMISSION_FACTORY = deny_all
"""Default list users roles permission factory: reject any request."""
"""Default list users' roles permission factory: reject any request."""

ACCOUNTS_REST_READ_USER_PROPERTIES_PERMISSION_FACTORY = deny_all
"""Default read user properties permission factory: reject any request."""
Expand Down
7 changes: 7 additions & 0 deletions invenio_accounts_rest/ext.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ def unassign_role_permission_factory(self, **kwargs):
return load_or_import_from_config(
'ACCOUNTS_REST_UNASSIGN_ROLE_PERMISSION_FACTORY', app=self.app)

def read_role_users_list_permission_factory(self, **kwargs):
"""Permission factory for reading a role's list of users."""
return load_or_import_from_config(
'ACCOUNTS_REST_READ_ROLE_USERS_LIST_PERMISSION_FACTORY',
app=self.app
)

def read_user_roles_list_permission_factory(self, **kwargs):
"""Permission factory for reading the list of roles."""
return load_or_import_from_config(
Expand Down
99 changes: 93 additions & 6 deletions invenio_accounts_rest/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,74 @@ def inner(self, user_id, *args, **kwargs):
return pass_user_decorator


class RoleUsersListResource(ContentNegotiatedMethodView):
"""Resource listing users having a specific role assigned."""

view_name = 'role_users_list'

def __init__(self, max_result_window=None, **kwargs):
"""Constructor."""
default_serializer = users_list_serializer \
if 'invenio-userprofiles' not in current_app.extensions else \
users_with_profile_list_serializer
kwargs.setdefault(
'method_serializers',
current_app.config.get(
'ACCOUNTS_REST_ACCOUNTS_LIST_SERIALIZERS', {
'GET': {
'application/json': default_serializer,
},
})
)
kwargs.setdefault(
'default_method_media_type',
current_app.config.get(
'ACCOUNTS_REST_ACCOUNTS_LIST_DEFAULT_MEDIA_TYPE', {
'GET': 'application/json',
},
)
)
super(RoleUsersListResource, self).__init__(**kwargs)
self.max_result_window = max_result_window or 10000

@pass_role
@need_role_permission('read_role_users_list_permission_factory')
def get(self, role):
"""Get a list of the user's roles."""
page = request.values.get('page', 1, type=int)
size = request.values.get('size', 10, type=int)
if page * size >= self.max_result_window:
raise MaxResultWindowRESTError()

query_string = request.args.get('q')
users_query = db.session.query(User).join(userrole).filter_by(
role_id=role.id
).order_by(User.email)
total_query = db.session.query(
func.count(User.id)).join(userrole).filter_by(
role_id=role.id
)
if query_string is not None:
query_filter = User.email.like('%{}%'.format(query_string))
users_query = users_query.filter(query_filter)
total_query = total_query.filter(query_filter)
role_users = users_query.slice((page - 1) * size, page * size).all()
total = total_query.scalar()

result = self.make_response(
users=role_users,
total=total,
links=paginated_query_links(
'invenio_accounts_rest.role_users_list',
total, page, size,
self.max_result_window,
role_id=role.id),
code=200,
)

return result


def verify_reassign_role_permission(permission_factory, role, user):
"""Check that the current user has permissions on reassigning a role.
Expand Down Expand Up @@ -492,13 +560,24 @@ class UserRolesListResource(ContentNegotiatedMethodView):

def __init__(self, max_result_window=None, **kwargs):
"""Constructor."""
super(UserRolesListResource, self).__init__(
serializers={
'application/json': roles_list_serializer
},
default_media_type='application/json',
**kwargs
kwargs.setdefault(
'method_serializers',
current_app.config.get(
'ACCOUNTS_REST_ROLES_LIST_SERIALIZERS', {
'GET': {
'application/json': roles_list_serializer,
}
})
)
kwargs.setdefault(
'default_method_media_type',
current_app.config.get(
'ACCOUNTS_REST_ROLES_LIST_DEFAULT_MEDIA_TYPE', {
'GET': 'application/json',
}
)
)
super(UserRolesListResource, self).__init__(**kwargs)
self.max_result_window = max_result_window or 10000

@pass_user()
Expand Down Expand Up @@ -720,6 +799,14 @@ def get(self):
)


blueprint.add_url_rule(
'/roles/<string:role_id>/users',
view_func=RoleUsersListResource.as_view(
RoleUsersListResource.view_name
)
)


blueprint.add_url_rule(
'/roles/<string:role_id>/users/<string:user_id>',
view_func=AssignRoleResource.as_view(
Expand Down
7 changes: 6 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ def accounts_rest_permission_factory():
'create_role': [],
'assign_role': {},
'unassign_role': {},
'read_role_users_list': {},
'read_user_roles_list': {},
'read_user_properties': {},
'update_user_properties': {},
Expand Down Expand Up @@ -123,6 +124,8 @@ def u_permission_factory(user):
'create_role': list_permission_factory_sub('create_role'),
'assign_role': reassign_role_permission_factory_sub('assign_role'),
'unassign_role': reassign_role_permission_factory_sub('unassign_role'),
'read_role_users_list': role_permission_factory_sub(
'read_role_users_list'),
'read_user_roles_list': user_permission_factory_sub(
'read_user_roles_list'),
'read_user_properties': user_permission_factory_sub(
Expand Down Expand Up @@ -175,6 +178,7 @@ def app(request, accounts_rest_permission_factory):
create_role = accounts_rest_permission_factory['create_role']
assign_role = accounts_rest_permission_factory['assign_role']
unassign_role = accounts_rest_permission_factory['unassign_role']
role_users = accounts_rest_permission_factory['read_role_users_list']
user_roles = accounts_rest_permission_factory['read_user_roles_list']
read_user_prop = accounts_rest_permission_factory['read_user_properties']
mod_user_prop = accounts_rest_permission_factory['update_user_properties']
Expand All @@ -188,6 +192,7 @@ def app(request, accounts_rest_permission_factory):
ACCOUNTS_REST_CREATE_ROLE_PERMISSION_FACTORY=create_role,
ACCOUNTS_REST_ASSIGN_ROLE_PERMISSION_FACTORY=assign_role,
ACCOUNTS_REST_UNASSIGN_ROLE_PERMISSION_FACTORY=unassign_role,
ACCOUNTS_REST_READ_ROLE_USERS_LIST_PERMISSION_FACTORY=role_users,
ACCOUNTS_REST_READ_USER_ROLES_LIST_PERMISSION_FACTORY=user_roles,
ACCOUNTS_REST_READ_USER_PROPERTIES_PERMISSION_FACTORY=read_user_prop,
ACCOUNTS_REST_UPDATE_USER_PROPERTIES_PERMISSION_FACTORY=mod_user_prop,
Expand Down Expand Up @@ -247,7 +252,7 @@ def user_data(idx):
return data

users = {
'user{}'.format(idx): user_data(idx) for idx in range(1, 5)
'user{}'.format(idx): user_data(idx) for idx in range(1, 8)
}
users.update({
'inactive': dict(
Expand Down
149 changes: 127 additions & 22 deletions tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def normalize(link):
"""Parse a link in order to make the query string deterministic."""
parsed_link = urlparse(link)._asdict()
parsed_link['query'] = parse_qs(parsed_link['query'])
return parsed_link
return {
name: normalize(link) for name, link in iteritems(links)
}
Expand Down Expand Up @@ -1392,26 +1393,6 @@ def test_user_search(app, with_profiles, users, create_roles, roles_data,
response_data = json.loads(res.get_data(as_text=True))
response_data['links'] = normalize_links(response_data['links'])

def expected_user(user):
expected_user = {
'id': user.id,
'email': user.data['email'],
'active': True,
'links': {
'self': url_for(
'invenio_accounts_rest.user',
user_id=user.id,
_external=True
)
}
}
if with_profiles:
expected_user.update({
'full_name': user.data['profile']['full_name'],
'username': user.data['profile']['username'],
})
return expected_user

assert response_data == {
'links': normalize_links({
'prev': url_for(
Expand All @@ -1424,9 +1405,133 @@ def expected_user(user):
),
}),
'hits': {
'hits': [expected_user(user) for user in
'hits': [expected_user(user, with_profiles) for user in
sorted(users.values(),
key=lambda user: user.data['email'])[2:4]],
'total': 6
'total': 9
}
}


@pytest.mark.parametrize('app', [
{'with_profiles': True}, {'with_profiles': False}
], indirect=['app'])
def test_role_users_search(app, with_profiles, users, create_roles, roles_data,
accounts_rest_permission_factory):
"""Test searching users having a specific role assigned."""
headers = [('Content-Type', 'application/json'),
('Accept', 'application/json')]

# assign the role to 'user1, user2 and user3'
with app.app_context():
role = Role.query.filter(Role.id == create_roles[0]['id']).one()
assigned_users = [users[username] for username in
['user{}'.format(idx) for idx in range(1, 6)]]
user_models = User.query.filter(
User.id.in_(user.id for user in assigned_users)
).all()
for model in user_models:
if role not in model.roles:
model.roles.append(role)
db.session.commit()

url = url_for(
'invenio_accounts_rest.role_users_list',
role_id=role.id,
access_token=users['user1'].allowed_token, page=2, size=2
)
accounts_rest_permission_factory['allowed_users'][
'read_role_users_list'][users['user1'].id] = [role.id]

with app.test_client() as client:
res = client.get(url, headers=headers)
assert res.status_code == 200
response_data = json.loads(res.get_data(as_text=True))

response_data['links'] = normalize_links(response_data['links'])

with app.app_context():
assert response_data == {
'links': normalize_links({
'prev': url_for(
'invenio_accounts_rest.role_users_list',
role_id=role.id,
page=1, size=2, _external=True
),
'next': url_for(
'invenio_accounts_rest.role_users_list',
role_id=role.id,
page=3, size=2, _external=True
),
}),
'hits': {
'hits': [expected_user(user, with_profiles) for user in
sorted(assigned_users,
key=lambda user: user.data['email'])[2:4]],
'total': 5
}
}

with app.app_context():
url = url_for(
'invenio_accounts_rest.role_users_list',
role_id=role.id, q='user1',
access_token=users['user1'].allowed_token
)
# test filtering by name
with app.test_client() as client:
res = client.get(url, headers=headers)
assert res.status_code == 200
response_data = json.loads(res.get_data(as_text=True))
assert response_data['hits']['total'] == 1
assert response_data['hits']['hits'][0]['id'] == users['user1'].id


def test_role_users_search_permissions(app, users, create_roles, roles_data,
accounts_rest_permission_factory):
"""Test permissions of searching users having a specific role assigned."""
headers = [('Content-Type', 'application/json'),
('Accept', 'application/json')]
with app.app_context():
role = Role.query.filter(Role.id == create_roles[0]['id']).one()

def test_role_user_search(user, expected_code):
url = url_for(
'invenio_accounts_rest.role_users_list',
role_id=role.id,
access_token=user.allowed_token if user is not None else None
)

with app.test_client() as client:
res = client.get(url, headers=headers)
assert res.status_code == expected_code

with app.app_context():
accounts_rest_permission_factory['allowed_users'][
'read_role_users_list'][users['user1'].id] = [role.id]

test_role_user_search(None, 401)
test_role_user_search(users['user2'], 403)
test_role_user_search(users['user1'], 200)


def expected_user(user, with_profiles):
"""Serialize user as expected."""
expected_user = {
'id': user.id,
'email': user.data['email'],
'active': True,
'links': {
'self': url_for(
'invenio_accounts_rest.user',
user_id=user.id,
_external=True
)
}
}
if with_profiles:
expected_user.update({
'full_name': user.data['profile']['full_name'],
'username': user.data['profile']['username'],
})
return expected_user

0 comments on commit c229d1a

Please sign in to comment.