diff --git a/.gitignore b/.gitignore index 57ca2bfcc..4e70f9b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,5 @@ ENV/ keys tests/resources/keys/*.pem .DS_Store + +.idea/ diff --git a/fence/blueprints/admin.py b/fence/blueprints/admin.py index b44ca96d0..e539765e6 100644 --- a/fence/blueprints/admin.py +++ b/fence/blueprints/admin.py @@ -64,7 +64,24 @@ def get_all_users(): Retrieve the information regarding the buckets created within a project. Returns a json object. """ - return jsonify(admin.get_all_users(current_session)) + keyword = request.args.get('keyword') + return jsonify(admin.get_all_users(current_session, keyword)) + + +@blueprint.route("/paginated_users", methods=["GET"]) +@admin_login_required +@debug_log +def get_paginated_users(): + """ + Retrieve the information regarding the buckets created within a project. + Returns a json object. + """ + keyword = request.args.get('keyword') + page = request.args.get('page') + page_size = request.args.get('page_size') + return jsonify( + admin.get_paginated_user(current_session, page, page_size, keyword) + ) @blueprint.route("/users", methods=["POST"]) @@ -80,7 +97,8 @@ def create_user(): username = request.get_json().get("name", None) role = request.get_json().get("role", None) email = request.get_json().get("email", None) - return jsonify(admin.create_user(current_session, username, role, email)) + display_name = request.get_json().get("display_name", None) + return jsonify(admin.create_user(current_session, username, role, email, display_name)) @blueprint.route("/users/", methods=["PUT"]) @@ -96,7 +114,11 @@ def update_user(username): name = request.get_json().get("name", None) role = request.get_json().get("role", None) email = request.get_json().get("email", None) - return jsonify(admin.update_user(current_session, username, role, email, name)) + display_name = request.get_json().get("display_name", None) + active = request.get_json().get("active", False) + return jsonify(admin.update_user( + current_session, username, role, email, name, display_name, active + )) @blueprint.route("/users/", methods=["DELETE"]) diff --git a/fence/pagination.py b/fence/pagination.py new file mode 100644 index 000000000..5d8674dbd --- /dev/null +++ b/fence/pagination.py @@ -0,0 +1,147 @@ +from math import ceil + +from flask import request, abort + + +class Pagination(object): + """ + Refs https://github.com/pallets/flask-sqlalchemy/blob/master/flask_sqlalchemy/__init__.py#L310 + Internal helper class returned by :meth:`BaseQuery.paginate`. You + can also construct it from any other SQLAlchemy query object if you are + working with other libraries. Additionally it is possible to pass `None` + as query object in which case the :meth:`prev` and :meth:`next` will + no longer work. + """ + + def __init__(self, query, page, per_page, total, items): + #: the unlimited query object that was used to create this + #: pagination object. + self.query = query + #: the current page number (1 indexed) + self.page = page + #: the number of items to be displayed on a page. + self.per_page = per_page + #: the total number of items matching the query + self.total = total + #: the items for the current page + self.items = items + + @property + def pages(self): + """The total number of pages""" + if self.per_page == 0: + pages = 0 + else: + pages = int(ceil(self.total / float(self.per_page))) + return pages + + @property + def has_next(self): + """True if a next page exists.""" + return self.page < self.pages + + @property + def next_num(self): + """Number of the next page""" + if not self.has_next: + return 0 + return self.page + 1 + + def iter_pages(self, left_edge=2, left_current=2, + right_current=5, right_edge=2): + """Iterates over the page numbers in the pagination. The four + parameters control the thresholds how many numbers should be produced + from the sides. Skipped page numbers are represented as `None`. + This is how you could render such a pagination in the templates: + + .. sourcecode:: html+jinja + + {% macro render_pagination(pagination, endpoint) %} + + {% endmacro %} + """ + last = 0 + for num in range(1, self.pages + 1): + if ( + num <= left_edge or + self.page - left_current - 1 < num < + self.page + right_current + or num > self.pages - right_edge + ): + if last + 1 != num: + yield None + yield num + last = num + + +def paginate(query, page=None, per_page=None, error_out=True): + """ + Refs https://github.com/pallets/flask-sqlalchemy/blob/master/flask_sqlalchemy/__init__.py#L435 + Returns ``per_page`` items from page ``page``. + + If no items are found and ``page`` is greater than 1, or if page is + less than 1, it aborts with 404. + This behavior can be disabled by passing ``error_out=False``. + + If ``page`` or ``per_page`` are ``None``, they will be retrieved from + the request query. + If the values are not ints and ``error_out`` is ``True``, it aborts + with 404. + If there is no request or they aren't in the query, they default to 1 + and 20 respectively. + + Returns a :class:`Pagination` object. + """ + + if request: + if page is None: + try: + page = int(request.args.get('page', 1)) + except (TypeError, ValueError): + if error_out: + abort(404) + + page = 1 + + if per_page is None: + try: + per_page = int(request.args.get('per_page', 10)) + except (TypeError, ValueError): + if error_out: + abort(404) + + per_page = 20 + else: + if page is None: + page = 1 + + if per_page is None: + per_page = 20 + + if error_out and page < 1: + abort(404) + + items = query.limit(per_page).offset((page - 1) * per_page).all() + + if not items and page != 1 and error_out: + abort(404) + + # No need to count if we're on the first page and there are fewer + # items than we expected. + if page == 1 and len(items) < per_page: + total = len(items) + else: + total = query.order_by(None).count() + return Pagination(query, page, per_page, total, items) diff --git a/fence/resources/admin/admin_users.py b/fence/resources/admin/admin_users.py index a5c9e2e51..cae135ad0 100644 --- a/fence/resources/admin/admin_users.py +++ b/fence/resources/admin/admin_users.py @@ -21,6 +21,7 @@ "connect_user_to_project", "get_user_info", "get_all_users", + "get_paginated_user", "get_user_groups", "create_user", "update_user", @@ -70,20 +71,43 @@ def get_user_info(current_session, username): return us.get_user_info(current_session, username) -def get_all_users(current_session): - users = udm.get_all_users(current_session) +def _user_dump(user): + new_user = {"name": user.username} + if user.is_admin: + new_user["role"] = "admin" + else: + new_user["role"] = "user" + new_user["preferred_username"] = user.display_name + new_user["phone_number"] = user.phone_number + new_user["active"] = user.active if user.active is not None else False + new_user["email"] = user.email + return new_user + + +def get_all_users(current_session, keyword=None): + users = udm.get_all_users(current_session, keyword) users_names = [] for user in users: - new_user = {} - new_user["name"] = user.username - if user.is_admin: - new_user["role"] = "admin" - else: - new_user["role"] = "user" - users_names.append(new_user) + users_names.append(_user_dump(user)) return {"users": users_names} +def get_paginated_user(current_session, page, page_size, keyword=None): + pagination = udm.get_paginated_users(current_session, page, page_size, keyword) + users_names = [] + for user in pagination.items: + users_names.append(_user_dump(user)) + return { + "users": users_names, + "pagination": { + "page": pagination.page, + "page_size": pagination.per_page, + "next_page": pagination.next_num, + "total_count": pagination.total, + } + } + + def get_user_groups(current_session, username): user_groups = us.get_user_groups(current_session, username)["groups"] user_groups_info = [] @@ -92,7 +116,7 @@ def get_user_groups(current_session, username): return {"groups": user_groups_info} -def create_user(current_session, username, role, email): +def create_user(current_session, username, role, email, display_name=None): """ Create a user for all the projects or groups in the list. If the user already exists, to avoid unadvertedly changing it, we suggest update @@ -123,11 +147,15 @@ def create_user(current_session, username, role, email): is_admin = role == "admin" email_add = email usr = User(username=username, active=True, is_admin=is_admin, email=email_add) + usr.display_name = display_name current_session.add(usr) return us.get_user_info(current_session, username) -def update_user(current_session, username, role, email, new_name): +def update_user( + current_session, username, role, email, new_name, + display_name=None, active=None +): usr = us.get_user(current_session, username) user_list = [ user["name"].upper() for user in get_all_users(current_session)["users"] @@ -148,6 +176,8 @@ def update_user(current_session, username, role, email, new_name): if role: usr.is_admin = role == "admin" usr.username = new_name or usr.username + usr.display_name = display_name or usr.display_name + usr.active = active if active is not None else usr.active return us.get_user_info(current_session, usr.username) diff --git a/fence/resources/user/__init__.py b/fence/resources/user/__init__.py index a7ed1ea12..a9faff60f 100644 --- a/fence/resources/user/__init__.py +++ b/fence/resources/user/__init__.py @@ -88,6 +88,7 @@ def get_user_info(current_session, username): "resources_granted": [], "groups": groups, "message": "", + "active": user.active, } if hasattr(flask.current_app, "arborist"): diff --git a/fence/resources/userdatamodel/userdatamodel_user.py b/fence/resources/userdatamodel/userdatamodel_user.py index eaee1e03d..55977ba3c 100644 --- a/fence/resources/userdatamodel/userdatamodel_user.py +++ b/fence/resources/userdatamodel/userdatamodel_user.py @@ -1,4 +1,5 @@ -from sqlalchemy import func +import flask +from sqlalchemy import func, or_ from fence.errors import NotFound, UserError from fence.models import ( @@ -13,13 +14,14 @@ UserToGroup, query_for_user, ) - +from fence.pagination import paginate __all__ = [ "get_user", "get_user_accesses", "create_user_by_username_project", "get_all_users", + "get_paginated_users", "get_user_groups", ] @@ -72,8 +74,38 @@ def create_user_by_username_project(current_session, new_user, proj): return {"user": new_user, "project": project, "privileges": priv} -def get_all_users(current_session): - return current_session.query(User).all() +def _get_user_query(current_session, keyword=None): + q = current_session.query(User) + if keyword: + keyword = keyword.replace(' ', '').lower() + q = q.filter( + or_( + func.replace(User.display_name, ' ', '').ilike( + '%{}%'.format(keyword)), + func.replace(User.email, ' ', '').ilike( + '%{}%'.format(keyword)), + ) + ) + return q + + +def get_all_users(current_session, keyword=None): + q = _get_user_query(current_session, keyword) + return q.order_by(User.id.desc()).all() + + +def get_paginated_users(current_session, page, page_size, keyword=None): + q = _get_user_query(current_session, keyword) + q = q.order_by(User.id.desc()) + page = int(page) + page_size = int(page_size) + pagination = paginate( + query=q, + page=page, + per_page=page_size, + error_out=False + ) + return pagination def get_user_groups(current_session, username):