Merge pull request #55 from kaiyou/feat-refactor-permissions
Refactor the access control codemaster
commit
cbc6bb5dd6
@ -0,0 +1,43 @@
|
||||
from freeposte import app
|
||||
|
||||
import sys
|
||||
import tabulate
|
||||
|
||||
|
||||
# Known endpoints without permissions
|
||||
known_missing_permissions = [
|
||||
"index",
|
||||
"static", "bootstrap.static",
|
||||
"admin.static", "admin.login"
|
||||
]
|
||||
|
||||
|
||||
# Compute the permission table
|
||||
missing_permissions = []
|
||||
permissions = {}
|
||||
for endpoint, function in app.view_functions.items():
|
||||
audit = function.__dict__.get("_audit_permissions")
|
||||
if audit:
|
||||
handler, args = audit
|
||||
if args:
|
||||
model = args[0].__name__
|
||||
key = args[1]
|
||||
else:
|
||||
model = key = None
|
||||
permissions[endpoint] = [endpoint, handler.__name__, model, key]
|
||||
elif endpoint not in known_missing_permissions:
|
||||
missing_permissions.append(endpoint)
|
||||
|
||||
|
||||
# Fail if any endpoint is missing a permission check
|
||||
if missing_permissions:
|
||||
print("The following endpoints are missing permission checks:")
|
||||
print(missing_permissions.join(","))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
# Display the permissions table
|
||||
print(tabulate.tabulate([
|
||||
[route, *permissions[route.endpoint]]
|
||||
for route in app.url_map.iter_rules() if route.endpoint in permissions
|
||||
]))
|
@ -0,0 +1,114 @@
|
||||
from freeposte.admin import db, models, forms
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
import functools
|
||||
|
||||
|
||||
def permissions_wrapper(handler):
|
||||
""" Decorator that produces a decorator for checking permissions.
|
||||
"""
|
||||
def callback(function, args, kwargs, dargs, dkwargs):
|
||||
authorized = handler(args, kwargs, *dargs, **dkwargs)
|
||||
if not authorized:
|
||||
flask.abort(403)
|
||||
elif type(authorized) is int:
|
||||
flask.abort(authorized)
|
||||
else:
|
||||
return function(*args, **kwargs)
|
||||
# If the handler has no argument, declare a
|
||||
# simple decorator, otherwise declare a nested decorator
|
||||
# There are at least two mandatory arguments
|
||||
if handler.__code__.co_argcount > 2:
|
||||
def decorator(*dargs, **dkwargs):
|
||||
def inner(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
return callback(function, args, kwargs, dargs, dkwargs)
|
||||
wrapper._audit_permissions = handler, dargs
|
||||
return flask_login.login_required(wrapper)
|
||||
return inner
|
||||
else:
|
||||
def decorator(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
return callback(function, args, kwargs, (), {})
|
||||
wrapper._audit_permissions = handler, []
|
||||
return flask_login.login_required(wrapper)
|
||||
return decorator
|
||||
|
||||
|
||||
@permissions_wrapper
|
||||
def global_admin(args, kwargs):
|
||||
""" The view is only available to global administrators.
|
||||
"""
|
||||
return flask_login.current_user.global_admin
|
||||
|
||||
|
||||
@permissions_wrapper
|
||||
def domain_admin(args, kwargs, model, key):
|
||||
""" The view is only available to specific domain admins.
|
||||
Global admins will still be able to access the resource.
|
||||
|
||||
A model and key must be provided. The model will be queries
|
||||
based on the query parameter named after the key. The model may
|
||||
either be Domain or an Email subclass (or any class with a
|
||||
``domain`` attribute which stores a related Domain instance).
|
||||
"""
|
||||
obj = model.query.get(kwargs[key])
|
||||
if not obj:
|
||||
flask.abort(404)
|
||||
else:
|
||||
domain = obj if type(obj) is models.Domain else obj.domain
|
||||
return domain in flask_login.current_user.get_managed_domains()
|
||||
|
||||
|
||||
@permissions_wrapper
|
||||
def owner(args, kwargs, model, key):
|
||||
""" The view is only available to the resource owner or manager.
|
||||
|
||||
A model and key must be provided. The model will be queries
|
||||
based on the query parameter named after the key. The model may
|
||||
either be User or any model with a ``user`` attribute storing
|
||||
a user instance (like Fetch).
|
||||
|
||||
If the query parameter is empty and the model is User, then
|
||||
the resource being accessed is supposed to be the current
|
||||
logged in user and access is obviously authorized.
|
||||
"""
|
||||
if kwargs[key] is None and model == models.User:
|
||||
return True
|
||||
obj = model.query.get(kwargs[key])
|
||||
if not obj:
|
||||
flask.abort(404)
|
||||
else:
|
||||
user = obj if type(obj) is models.User else obj.user
|
||||
return (
|
||||
user.email == flask_login.current_user.email
|
||||
or user.domain in flask_login.current_user.get_managed_domains()
|
||||
)
|
||||
|
||||
|
||||
@permissions_wrapper
|
||||
def authenticated(args, kwargs):
|
||||
""" The view is only available to logged in users.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
|
||||
def confirmation_required(action):
|
||||
""" View decorator that asks for a confirmation first.
|
||||
"""
|
||||
def inner(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
form = forms.ConfirmationForm()
|
||||
if form.validate_on_submit():
|
||||
return function(*args, **kwargs)
|
||||
return flask.render_template(
|
||||
"confirm.html", action=action.format(*args, **kwargs),
|
||||
form=form
|
||||
)
|
||||
return wrapper
|
||||
return inner
|
@ -1,69 +0,0 @@
|
||||
from freeposte.admin import models, forms
|
||||
|
||||
import flask
|
||||
import flask_login
|
||||
import functools
|
||||
|
||||
|
||||
def confirmation_required(action):
|
||||
""" View decorator that asks for a confirmation first.
|
||||
"""
|
||||
def inner(function):
|
||||
@functools.wraps(function)
|
||||
def wrapper(*args, **kwargs):
|
||||
form = forms.ConfirmationForm()
|
||||
if form.validate_on_submit():
|
||||
return function(*args, **kwargs)
|
||||
return flask.render_template(
|
||||
"confirm.html", action=action.format(*args, **kwargs),
|
||||
form=form
|
||||
)
|
||||
return wrapper
|
||||
return inner
|
||||
|
||||
|
||||
def get_domain_admin(domain_name):
|
||||
domain = models.Domain.query.get(domain_name)
|
||||
if not domain:
|
||||
flask.abort(404)
|
||||
if not domain in flask_login.current_user.get_managed_domains():
|
||||
flask.abort(403)
|
||||
return domain
|
||||
|
||||
|
||||
def require_global_admin():
|
||||
if not flask_login.current_user.global_admin:
|
||||
flask.abort(403)
|
||||
|
||||
|
||||
def get_user(user_email, admin=False):
|
||||
if user_email is None:
|
||||
user_email = flask_login.current_user.email
|
||||
user = models.User.query.get(user_email)
|
||||
if not user:
|
||||
flask.abort(404)
|
||||
if not user.domain in flask_login.current_user.get_managed_domains():
|
||||
if admin:
|
||||
flask.abort(403)
|
||||
elif not user.email == flask_login.current_user.email:
|
||||
flask.abort(403)
|
||||
return user
|
||||
|
||||
|
||||
def get_alias(alias):
|
||||
alias = models.Alias.query.get(alias)
|
||||
if not alias:
|
||||
flask.abort(404)
|
||||
if not alias.domain in flask_login.current_user.get_managed_domains():
|
||||
return 403
|
||||
return alias
|
||||
|
||||
|
||||
def get_fetch(fetch_id):
|
||||
fetch = models.Fetch.query.get(fetch_id)
|
||||
if not fetch:
|
||||
flask.abort(404)
|
||||
if not fetch.user.domain in flask_login.current_user.get_managed_domains():
|
||||
if not fetch.user.email == flask_login.current_user.email:
|
||||
flask.abort(403)
|
||||
return fetch
|
Loading…
Reference in New Issue