Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add permission manager #158

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 ruddock/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from ruddock.modules import budget
from ruddock.modules import government
from ruddock.modules import hassle
from ruddock.modules import perm_mgr
from ruddock.modules import rotation
from ruddock.modules import users

Expand All @@ -34,6 +35,7 @@
app.register_blueprint(budget.blueprint, url_prefix="/budget")
app.register_blueprint(government.blueprint, url_prefix="/government")
app.register_blueprint(hassle.blueprint, url_prefix="/hassle")
app.register_blueprint(perm_mgr.blueprint, url_prefix="/perm_mgr")
app.register_blueprint(rotation.blueprint, url_prefix="/rotation")
app.register_blueprint(users.blueprint, url_prefix="/users")

Expand Down
3 changes: 3 additions & 0 deletions ruddock/auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,7 @@ def generate_admin_links():
if check_permission(Permissions.BUDGET):
links.append(AdminLink('Budget',
flask.url_for('budget.route_portal', _external=True)))
if check_permission(Permissions.PERMISSION_MANAGER):
links.append(AdminLink('Permission manager',
flask.url_for('perm_mgr.show_permissions', _external=True)))
return links
5 changes: 5 additions & 0 deletions ruddock/modules/perm_mgr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import flask
blueprint = flask.Blueprint('perm_mgr', __name__,
template_folder='templates', static_folder='static')

import ruddock.modules.perm_mgr.routes
187 changes: 187 additions & 0 deletions ruddock/modules/perm_mgr/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import flask
import sqlalchemy
from ruddock.resources import Permissions

def fetch_office_permissions():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the function to do this? Can't we just have it return the permissions of all offices, and also the is_active column?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean I suppose lol, does it matter?

Copy link
Contributor

@HenrySwanson HenrySwanson Apr 13, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh, i tried it out with more thorough test data, and now i see it. you wanna show every office that's got permissions, even if they're not active, in case you need to remove them. likewise, you want to show all active offices, even if they have no permissions, in case you want to manage them.

same with users.

if that's the case, lemme suggest the following (you gotta have the extra any_permissions column because there's a bug where you can't sort on expressions involving aliases: https://bugs.mysql.com/bug.php?id=80802)

SELECT GROUP_CONCAT(permission_id) AS permissions,
  permission_id IS NOT NULL AS any_permissions,
  office_name, office_id, office_order, is_active
FROM offices
  NATURAL LEFT JOIN office_permissions
GROUP BY office_id
HAVING is_active OR any_permissions
ORDER BY
  any_permissions DESC, -- puts nulls after non-nulls
  permissions,
  office_order

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it worked fine on my machine (using MariaDB, maybe they fixed it?). I checked and the Ruddock website is using MariaDB also so this might not be needed?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also using mariadb; maybe I'm just on an older version. In any case, I think that approach makes it much clearer what you're intending to fetch there.

"""Returns the office name and permissions of all active offices.
Returns the permissions of all offices that have permissions,
plus the permissions of all active offices (which may be none)."""
query = sqlalchemy.text("""
SELECT GROUP_CONCAT(permission_id) AS permissions,
office_name, office_id, office_order
FROM office_permissions
NATURAL RIGHT JOIN offices
WHERE is_active=TRUE
GROUP BY office_id

UNION

SELECT GROUP_CONCAT(permission_id) AS permissions,
office_name, office_id, office_order
FROM office_permissions
NATURAL JOIN offices
GROUP BY office_id

ORDER BY
CASE
WHEN permissions IS NULL
THEN 1
ELSE 0
END,
permissions ASC,
office_order ASC
""")

return flask.g.db.execute(query).fetchall()

def fetch_specific_office_permissions(office_id):
"""Returns the office name and permissions of the specified office.
May be none."""
if type(office_id) is not int:
raise TypeError("Must pass office id number to fetch office permissions.")
query = sqlalchemy.text("""
SELECT GROUP_CONCAT(permission_id) AS permissions,
office_name, office_id
FROM office_permissions
NATURAL RIGHT JOIN offices
WHERE office_id = :id
GROUP BY office_id
""")
return flask.g.db.execute(query, id=office_id).fetchone()

def delete_office_permission(office_id, perm_id):
"""Deletes the specified permission from the office."""
if type(perm_id) is str or type(perm_id) is unicode:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

man, py3 makes this much nicer; maybe we should consider that someday

far in the future

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

later later

perm_id = int(perm_id)
if type(office_id) is not int or type(perm_id) is not int:
raise TypeError("Must pass office id number and permission id to fetch office permissions.")
query = sqlalchemy.text("""
DELETE FROM office_permissions
WHERE office_id = :o_id
AND permission_id = :p_id
""")
return flask.g.db.execute(query, o_id=office_id, p_id=perm_id)

def insert_office_permission(office_id, perm_id):
"""Inserts the specified permission for the office."""
if type(perm_id) is str or type(perm_id) is unicode:
perm_id = int(perm_id)
if type(office_id) is not int or type(perm_id) is not int:
raise TypeError("Must pass office id number and permission id to fetch office permissions.")
query = sqlalchemy.text("""
INSERT INTO office_permissions VALUES (:o_id, :p_id)
""")
return flask.g.db.execute(query, o_id=office_id, p_id=perm_id)


def fetch_user_permissions():
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as before, why does this return a table and also a subset of it?

"""Returns the user's name and permissions of all users.
Returns the permissions of all users who have permissions,
and the permissions of all current members (which may be none)"""
query = sqlalchemy.text("""
SELECT GROUP_CONCAT(permission_id) AS permissions, name, user_id
FROM user_permissions
NATURAL RIGHT JOIN members_current
NATURAL JOIN users
NATURAL JOIN members_extra
GROUP BY user_id

UNION

SELECT GROUP_CONCAT(permission_id) AS permissions, name, user_id
FROM user_permissions
NATURAL JOIN members_extra
GROUP BY user_id

ORDER BY
CASE
WHEN permissions IS NULL
THEN 1
ELSE 0
END,
permissions ASC,
name ASC
""")

return flask.g.db.execute(query).fetchall()

def fetch_specific_user_permissions(user_id):
"""Returns the name and permissions of the specified user.
May be none."""
if type(user_id) is not int:
raise TypeError("Must pass user id number to fetch user permissions.")
query = sqlalchemy.text("""
SELECT GROUP_CONCAT(permission_id) AS permissions, name, user_id
FROM user_permissions
NATURAL RIGHT JOIN users
NATURAL JOIN members_extra
WHERE user_id = :id
GROUP BY user_id
""")
return flask.g.db.execute(query, id=user_id).fetchone()

def delete_user_permission(user_id, perm_id):
"""Deletes the specified permission from the user."""
if type(perm_id) is str or type(perm_id) is unicode:
perm_id = int(perm_id)
if type(user_id) is not int or type(perm_id) is not int:
raise TypeError("Must pass user id number and permission id to fetch user permissions.")
query = sqlalchemy.text("""
DELETE FROM user_permissions
WHERE user_id = :o_id
AND permission_id = :p_id
""")
return flask.g.db.execute(query, o_id=user_id, p_id=perm_id)

def insert_user_permission(user_id, perm_id):
"""Inserts the specified permission for the user."""
if type(perm_id) is str or type(perm_id) is unicode:
perm_id = int(perm_id)
if type(user_id) is not int or type(perm_id) is not int:
raise TypeError("Must pass user id number and permission id to fetch user permissions.")
query = sqlalchemy.text("""
INSERT INTO user_permissions VALUES (:o_id, :p_id)
""")
return flask.g.db.execute(query, o_id=user_id, p_id=perm_id)

def decode_perm_string(string, sep=","):
"""Decodes a `sep`-separated string of permissions."""
if string is None:
return ["None"]
elif type(string) is int:
return get_perm_name(string)
elif type(string) is not str:
raise TypeError("decode_perm_string takes int or str only, received "
+ str(type(string)))
return [get_perm_name(int(x)) for x in string.split(sep)]

def decode_perm_string_with_id(string, sep=","):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

"""Decodes a `sep`-separated string of permissions into a dictionary
of ID and name."""
if string is None:
return [{"name": "None", "id": 0}]
elif type(string) is int:
return {"name": get_perm_name(string), "id": string}
elif type(string) is not str:
raise TypeError("decode_perm_string takes int or str only, received "
+ str(type(string)))
return [{"name": get_perm_name(int(x)), "id": x} for x in string.split(sep)]

def get_perm_name(id):
"""Returns the permission name corresponding to the given ID."""
if id is None:
return "None"
perm_name = "";
try:
perm_name = Permissions(id).name.title().replace("_", " ")
except ValueError:
# id is not a valid permission
perm_name = "Invalid permission"
return perm_name

def get_all_perms():
"""Gets a list of all permissions possible to assign."""
x = []
for p in Permissions:
x.append({"name": get_perm_name(p.value), "id": str(p.value)})
return x
75 changes: 75 additions & 0 deletions ruddock/modules/perm_mgr/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import json
import flask

import datetime

from ruddock.resources import Permissions
from ruddock.decorators import login_required
from ruddock.modules.perm_mgr import blueprint, helpers

@blueprint.route('/')
@login_required(Permissions.PERMISSION_MANAGER)
def show_permissions():
"""Displays a list of permissions for current users and offices."""
office_perms = [{"name": x["office_name"],
"perms": helpers.decode_perm_string(x['permissions']),
"id": x["office_id"]}
for x in helpers.fetch_office_permissions()]
user_perms = [{"name": x["name"],
"perms": helpers.decode_perm_string(x['permissions']),
"id": x["user_id"]}
for x in helpers.fetch_user_permissions()]
return flask.render_template('perm_list.html', office_perms=office_perms,
user_perms=user_perms)

@blueprint.route('/edit_user/<int:user_id>')
@login_required(Permissions.PERMISSION_MANAGER)
def edit_user_permissions(user_id):
x = helpers.fetch_specific_user_permissions(user_id)
user_perms = {"name": x["name"],
"perms": helpers.decode_perm_string_with_id(x['permissions']),
"id": x["user_id"]}
all_perms = helpers.get_all_perms()
diff_perms = [p for p in all_perms if not any(p["id"] == u["id"] for u in user_perms["perms"])]
return flask.render_template('edit_user.html', info=user_perms,
all_perms=diff_perms)

@blueprint.route('/edit_office/<int:office_id>')
@login_required(Permissions.PERMISSION_MANAGER)
def edit_office_permissions(office_id):
x = helpers.fetch_specific_office_permissions(office_id)
office_perms = {"name": x["office_name"],
"perms": helpers.decode_perm_string_with_id(x['permissions']),
"id": x["office_id"]}
all_perms = helpers.get_all_perms()
diff_perms = [p for p in all_perms if not any(p["id"] == o["id"] for o in office_perms["perms"])]
return flask.render_template('edit_office.html', info=office_perms,
all_perms=diff_perms)

@blueprint.route('/delete_user_perm/<int:user_id>', methods=["POST"])
@login_required(Permissions.PERMISSION_MANAGER)
def delete_user_perm(user_id):
perm_id = flask.request.form.get("perm_id")
helpers.delete_user_permission(user_id, perm_id)
return flask.redirect(flask.url_for("perm_mgr.edit_user_permissions", user_id=user_id))

@blueprint.route('/delete_office_perm/<int:office_id>', methods=["POST"])
@login_required(Permissions.PERMISSION_MANAGER)
def delete_office_perm(office_id):
perm_id = flask.request.form.get("perm_id")
helpers.delete_office_permission(office_id, perm_id)
return flask.redirect(flask.url_for("perm_mgr.edit_office_permissions", office_id=office_id))

@blueprint.route('/add_user_perm/<int:user_id>', methods=["POST"])
@login_required(Permissions.PERMISSION_MANAGER)
def add_user_perm(user_id):
perm_id = flask.request.form.get("perm_id")
helpers.insert_user_permission(user_id, perm_id)
return flask.redirect(flask.url_for("perm_mgr.edit_user_permissions", user_id=user_id))

@blueprint.route('/add_office_perm/<int:office_id>', methods=["POST"])
@login_required(Permissions.PERMISSION_MANAGER)
def add_office_perm(office_id):
perm_id = flask.request.form.get("perm_id")
helpers.insert_office_permission(office_id, perm_id)
return flask.redirect(flask.url_for("perm_mgr.edit_office_permissions", office_id=office_id))
58 changes: 58 additions & 0 deletions ruddock/modules/perm_mgr/templates/edit_office.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
{% extends "layout.html" %}

{% block body %}
<center>
{# Name and subtitle #}
<h2>{{ info["name"] }}</h2>
<h3>
Editing office permissions.
</h3>

<br>
<table>
<thead>
<tr>
<th>Permissions</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for perm in info["perms"] %}
<tr>
<form action={{ url_for("perm_mgr.delete_office_perm", office_id=info["id"]) }} method="post">
<input type="hidden" name="perm_id" value={{ perm["id"] }}>
<td>
{% if perm["id"] == 0 %}
<em>
{% endif %}
{{ perm["name"] }}
{% if perm["id"] == 0 %}
</em>
{% endif %}
</td>
<td>
{% if perm["id"] != 0 %}
<input type="submit" value="Delete">
{% endif %}
</td>
</form>
</tr>
{% endfor %}
<tr>
<form action={{ url_for("perm_mgr.add_office_perm", office_id=info["id"]) }} method="post">
<td>Add:
<select name="perm_id">
{% for perm in all_perms %}
<option value={{ perm["id"] }}>{{ perm["name"] }}</option>
{% endfor %}
</select>
</td>
<td>
<input type="submit" value="Submit">
</td>
</form>
</tr>
</tbody>
</table>
</center>
{% endblock body %}
Loading