Skip to content
This repository has been archived by the owner on Aug 17, 2023. It is now read-only.

Admin Portal Related #5

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,5 @@ ENV/
keys
tests/resources/keys/*.pem
.DS_Store

.idea/
28 changes: 25 additions & 3 deletions fence/blueprints/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand All @@ -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/<username>", methods=["PUT"])
Expand All @@ -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/<username>", methods=["DELETE"])
Expand Down
147 changes: 147 additions & 0 deletions fence/pagination.py
Original file line number Diff line number Diff line change
@@ -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) %}
<div class=pagination>
{%- for page in pagination.iter_pages() %}
{% if page %}
{% if page != pagination.page %}
<a href="{{ url_for(endpoint, page=page) }}">{{ page }}</a>
{% else %}
<strong>{{ page }}</strong>
{% endif %}
{% else %}
<span class=ellipsis>…</span>
{% endif %}
{%- endfor %}
</div>
{% 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)
52 changes: 41 additions & 11 deletions fence/resources/admin/admin_users.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"connect_user_to_project",
"get_user_info",
"get_all_users",
"get_paginated_user",
"get_user_groups",
"create_user",
"update_user",
Expand Down Expand Up @@ -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 = []
Expand All @@ -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
Expand Down Expand Up @@ -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"]
Expand All @@ -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)


Expand Down
1 change: 1 addition & 0 deletions fence/resources/user/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"):
Expand Down
40 changes: 36 additions & 4 deletions fence/resources/userdatamodel/userdatamodel_user.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -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",
]

Expand Down Expand Up @@ -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):
Expand Down