Fixed permissions and referrers

This commit is contained in:
Andrea Mistrali 2024-07-29 13:39:25 +02:00
parent 07ac2edb53
commit c39c3a0ab6
Signed by: andre
SSH Key Fingerprint: SHA256:/D780pZnuHMQ8xFII5lAtXWy8zdowtBhgWjwi88p+lI
9 changed files with 124 additions and 119 deletions

View File

@ -30,7 +30,7 @@ def create_app(environment='development'):
config[env].configure(app) config[env].configure(app)
app.config['APP_TZ'] = os.environ.get('TZ', 'UTC') app.config['APP_TZ'] = os.environ.get('TZ', 'UTC')
app.config['ADMIN_GROUPS'] = list( app.config['ADMIN_GROUPS'] = list(
map(str.strip, app.config['ADMIN_GROUPS'].split(','))) map(str.strip, app.config.get('ADMIN_GROUPS', "").split(',')))
app.logger.debug(f"admin groups: {app.config['ADMIN_GROUPS']}") app.logger.debug(f"admin groups: {app.config['ADMIN_GROUPS']}")
app.logger.info("middleware init: mobility") app.logger.info("middleware init: mobility")
@ -41,12 +41,12 @@ def create_app(environment='development'):
# Register blueprints. # Register blueprints.
from .views import main_blueprint, rest_blueprint from .views import main_blueprint, rest_blueprint
app.logger.info(f"registering main blueprint with prefix '{ app.logger.info(f"register blueprint: 'main' [prefix '{
main_blueprint.url_prefix}'") main_blueprint.url_prefix}']")
app.register_blueprint(main_blueprint) app.register_blueprint(main_blueprint)
app.logger.info(f"registering rest blueprint with prefix '{ app.logger.info(f"register blueprint: 'rest' [prefix '{
rest_blueprint.url_prefix}'") rest_blueprint.url_prefix}']")
app.register_blueprint(rest_blueprint) app.register_blueprint(rest_blueprint)
app.logger.info("jinja2 custom filters loaded") app.logger.info("jinja2 custom filters loaded")

View File

@ -2,7 +2,7 @@ import os
import functools import functools
from flask import request, abort, current_app from flask import request, abort, current_app
from flask import session as flask_session from flask import session as flask_session, jsonify
from flask_pyoidc import OIDCAuthentication as _OIDCAuth from flask_pyoidc import OIDCAuthentication as _OIDCAuth
from flask_pyoidc.user_session import UserSession from flask_pyoidc.user_session import UserSession
from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata from flask_pyoidc.provider_configuration import ProviderConfiguration, ClientMetadata
@ -21,19 +21,6 @@ def remote_ip() -> str:
return str(request.environ.get('REMOTE_ADDR')) return str(request.environ.get('REMOTE_ADDR'))
def username() -> str:
userinfo = flask_session['userinfo']
return userinfo['email'].split('@')[0]
def login_name() -> str:
userinfo = flask_session['userinfo']
if 'preferred_username' in userinfo:
return userinfo['preferred_username']
else:
return username()
def webMode() -> bool: def webMode() -> bool:
is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '') is_gunicorn = "gunicorn" in os.environ.get('SERVER_SOFTWARE', '')
is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true" is_werkzeug = os.environ.get('WERKZEUG_RUN_MAIN', False) == "true"
@ -64,6 +51,53 @@ class OIDCAuthentication(_OIDCAuth):
super().init_app(app) super().init_app(app)
app.auth = self app.auth = self
@property
def username(self) -> str:
userinfo = flask_session['userinfo']
return userinfo['email'].split('@')[0]
@property
def login_name(self) -> str:
userinfo = flask_session['userinfo']
return userinfo.get('preferred_username', self.username)
@property
def isAdmin(self) -> bool:
userinfo = flask_session['userinfo']
user_groups = userinfo.get('groups', [])
with current_app.app_context():
admin_groups = current_app.config.get('ADMIN_GROUPS', [])
admin_users = current_app.config.get('ADMIN_USERS', [])
authorized_groups = set(admin_groups).intersection(user_groups)
if len(authorized_groups):
log.debug(f"'{self.username}' is a member of {
authorized_groups}")
return True
if self.username in admin_users:
log.debug(f"'{self.username}' is an admin user")
return True
return False
@property
def unathorized(self):
response = jsonify(
{'message': f"not authorized",
'comment': 'nice try, info logged',
'logged': f"'{self.username}@{remote_ip()}",
'result': 'GO AWAY!'})
log.warning(
f"user '{self.username}' attempted denied operation from {remote_ip()}")
return response, 403
def userOrAdmin(self, username: str):
"""
Check is the current user is an admin OR the username passed as argument
"""
return self.isAdmin or self.username == username
def authorize(self, provider_name: str, authz_fn: Callable, **kwargs): def authorize(self, provider_name: str, authz_fn: Callable, **kwargs):
if provider_name not in self._provider_configurations: if provider_name not in self._provider_configurations:
raise ValueError( raise ValueError(
@ -76,7 +110,7 @@ class OIDCAuthentication(_OIDCAuth):
# Decorator # Decorator
def oidc_decorator(view_func): def oidc_decorator(view_func):
@ functools.wraps(view_func) @functools.wraps(view_func)
def wrapper(*args, **kwargs): def wrapper(*args, **kwargs):
# Retrieve session and client # Retrieve session and client
session = UserSession(flask_session, provider_name) session = UserSession(flask_session, provider_name)
@ -165,23 +199,7 @@ class OIDCAuthentication(_OIDCAuth):
""" """
def _authz_fn(session) -> bool: def _authz_fn(session) -> bool:
user_groups = session.userinfo.get('groups', []) return self.isAdmin
username = session.userinfo.get('preferred_username', "")
with current_app.app_context():
admin_groups = current_app.config.get('ADMIN_GROUPS', [])
admin_users = current_app.config.get('ADMIN_USERS', [])
authorized_groups = set(admin_groups).intersection(user_groups)
if len(authorized_groups):
log.debug(f"'{username}' is a member of {
authorized_groups}")
return True
if username in admin_users:
log.debug(f"'{username}' is an admin user")
return True
return False
return self.authorize(provider_name, return self.authorize(provider_name,
authz_fn=_authz_fn) authz_fn=_authz_fn)

View File

@ -55,6 +55,18 @@
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
<div class="row data">
<div class="col col-2">
<strong>access level</strong>
</div>
<div class="col col-6">
{% if auth.isAdmin %}
<span class="badge badge-pill badge-danger">ADMIN</span>
{% else %}
<span class="badge badge-pill badge-info">USER</span>
{% endif %}
</div>
</div>
<hr> <hr>
<h4>your devices</h4> <h4>your devices</h4>
<div class="row strong"> <div class="row strong">
@ -67,7 +79,7 @@
{% for node in userNodeList %} {% for node in userNodeList %}
<div class="row data"> <div class="row data">
<div class="col col-2"> <div class="col col-2">
{{ node.givenName}} <a href="{{url_for('main.node', nodeId=node.id) }}">{{ node.givenName}}</a>
</div> </div>
<div class="col col-2"> <div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}"> <span data-toggle="tooltip" data-placement="right" title="{{ node.createdAt | fmt_datetime }}">
@ -84,7 +96,7 @@
</div> </div>
<div class="col col-2"> <div class="col col-2">
<span data-toggle="tooltip" data-placement="right" title="delete"> <span data-toggle="tooltip" data-placement="right" title="delete">
<a class="nodeco" href="{{ url_for('rest.deleteOwnNode', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.deleteNode', nodeId=node.id) }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</span> </span>

View File

@ -74,7 +74,10 @@
{% for ip in node.ipAddresses %} {% for ip in node.ipAddresses %}
<div class="row data"> <div class="row data">
<div class="col col-3"> <div class="col col-3">
{{ ip }} <span class="address copy"
value="{{ ip }}">
{{ ip }}
</spanundefined>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
@ -210,3 +213,13 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block scripts %}
<script>
$(function () {
$('.address.copy').on('click', function() {
copyToClipboard(this)
})
})
</script>
{% endblock %}

View File

@ -52,7 +52,7 @@
<td class="no-sort"> <td class="no-sort">
{% if node.expireDate and not node.expired %} {% if node.expireDate and not node.expired %}
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect"> <span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
<a class="nodeco" href="{{ url_for('rest.expireNodeList', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.expireNode', nodeId=node.id) }}">
<i class="fas fa-plug"></i> <i class="fas fa-plug"></i>
</a> </a>
</span> </span>

View File

@ -49,7 +49,7 @@
<td class="no-sort"> <td class="no-sort">
{% if node.expireDate and not node.expired %} {% if node.expireDate and not node.expired %}
<span data-toggle="tooltip" data-placement="right" title="expire/disconnect"> <span data-toggle="tooltip" data-placement="right" title="expire/disconnect">
<a class="nodeco" href="{{ url_for('rest.expireNodeUser', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.expireNode', nodeId=node.id) }}">
<i class="fas fa-plug"></i> <i class="fas fa-plug"></i>
</a> </a>
</span> </span>
@ -57,7 +57,7 @@
<i class="fas fa-plug disabled"></i> <i class="fas fa-plug disabled"></i>
{% endif %} {% endif %}
<span data-toggle="tooltip" data-placement="right" title="delete"> <span data-toggle="tooltip" data-placement="right" title="delete">
<a class="nodeco" href="{{ url_for('rest.deleteNodeUser', nodeId=node.id) }}"> <a class="nodeco" href="{{ url_for('rest.deleteNode', nodeId=node.id) }}">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
</span> </span>

View File

@ -26,6 +26,15 @@ def health():
return jsonify(dict(status="OK", version=current_app.config['APP_VERSION'])) return jsonify(dict(status="OK", version=current_app.config['APP_VERSION']))
@main_blueprint.route('/token', methods=['GET', 'POST'])
@auth.authorize_admins('default')
def token():
user_session = UserSession(session)
return jsonify(access_token=user_session.access_token,
id_token=user_session.id_token,
userinfo=user_session.userinfo)
@main_blueprint.route('/', methods=['GET', 'POST']) @main_blueprint.route('/', methods=['GET', 'POST'])
@auth.access_control('default') @auth.access_control('default')
def index(): def index():
@ -34,17 +43,8 @@ def index():
userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user] userNodeList = [n for n in Node().list().nodes if n.user.name == hs_user]
return render_template('index.html', return render_template('index.html',
userNodeList=userNodeList, userNodeList=userNodeList,
session=user_session) session=user_session,
auth=auth)
@main_blueprint.route('/token', methods=['GET', 'POST'])
@auth.authorize_admins('default')
def token():
user_session = UserSession(session)
# return jsonify(user_session.userinfo)
return jsonify(access_token=user_session.access_token,
id_token=user_session.id_token,
userinfo=user_session.userinfo)
@main_blueprint.route('/logout') @main_blueprint.route('/logout')
@ -62,12 +62,14 @@ def nodes():
@main_blueprint.route('/node/<int:nodeId>', methods=['GET']) @main_blueprint.route('/node/<int:nodeId>', methods=['GET'])
@auth.authorize_admins('default') @auth.access_control('default')
def node(nodeId): def node(nodeId):
# There is a bug in HS api with retrieving a single node # There is a bug in HS api with retrieving a single node
# and we added a workaround to hsapi, so node.get() returns a # and we added a workaround to hsapi, so node.get() returns a
# v1Node object instead of v1NodeResponse, so we access directly # v1Node object instead of v1NodeResponse, so we access directly
# `node`, instead of `node.node` # `node`, instead of `node.node`
if not auth.userOrAdmin(auth.username):
return auth.unathorized
node = Node().get(nodeId) node = Node().get(nodeId)
routes = Node().routes(nodeId) routes = Node().routes(nodeId)
isExitNode = any( isExitNode = any(

View File

@ -4,7 +4,7 @@ from flask import Blueprint, request
from flask import redirect, url_for from flask import redirect, url_for
from app import auth from app import auth
from ..lib import login_name, username # from ..lib import login_name, username
from flask import jsonify from flask import jsonify
@ -35,86 +35,46 @@ def routeToggle(routeId: int):
action = 'enabled' action = 'enabled'
log.info( log.info(
f"route '{route.prefix}' via '{route.node.givenName}'" f"route '{route.prefix}' via '{route.node.givenName}'"
f"{action} by '{username()}'") f"{action} by '{auth.username}'")
return redirect(url_for("main.routes")) return redirect(request.referrer)
@rest_blueprint.route('/node/<int:nodeId>/expire', methods=['GET']) @rest_blueprint.route('/node/<int:nodeId>/expire', methods=['GET'])
@auth.authorize_admins('default') @auth.access_control('default')
def expireNode(nodeId: int): def expireNode(nodeId: int):
"""
This expires a node from the node page.
The difference from above is that it returns to the /node/nodeId page
"""
Node().expire(nodeId)
log.info(f"node '{nodeId}' expired by '{username()}'")
return redirect(url_for("main.node", nodeId=nodeId))
@rest_blueprint.route('/node/<int:nodeId>/user-expire', methods=['GET'])
@auth.authorize_admins('default')
def expireNodeUser(nodeId: int):
""" """
This expires a node from the node page. This expires a node from the node page.
The difference from above is that it returns to the /node/nodeId page The difference from above is that it returns to the /node/nodeId page
""" """
node = Node().get(nodeId) node = Node().get(nodeId)
userName = node.user.name if not auth.userOrAdmin(node.user.name):
return auth.unathorized
Node().expire(nodeId) Node().expire(nodeId)
log.info(f"node '{nodeId}' expired by '{username()}'") log.info(f"node '{nodeId}' expired by '{auth.username}'")
return redirect(url_for("main.user", userName=userName)) return redirect(request.referrer)
@rest_blueprint.route('/node/<int:nodeId>/list-expire', methods=['GET']) @rest_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
@auth.authorize_admins('default') @auth.access_control('default')
def expireNodeList(nodeId: int):
"""
This expires a node from the node list.
The difference from above is that it returns to the /nodes page
"""
Node().expire(nodeId)
log.info(f"node '{nodeId}' expired by '{username()}'")
return redirect(url_for("main.nodes"))
@ rest_blueprint.route('/node/<int:nodeId>/delete', methods=['GET'])
@ auth.authorize_admins('default')
def deleteNode(nodeId: int): def deleteNode(nodeId: int):
Node().delete(nodeId)
log.info(f"node '{nodeId}' deleted by '{username()}'")
return redirect(url_for("main.nodes"))
@rest_blueprint.route('/node/<int:nodeId>/delete-own', methods=['GET'])
@auth.access_control('default')
def deleteOwnNode(nodeId: int):
node = Node().get(nodeId) node = Node().get(nodeId)
if node.user.name != username(): if not auth.userOrAdmin(node.user.name):
response = jsonify({'message': 'not authorized'}) return auth.unathorized
return response, 401 Node().expire(nodeId)
Node().delete(nodeId) Node().delete(nodeId)
log.info(f"'{username()}' delete their own node '{nodeId}'") log.info(f"node '{nodeId}' deleted by '{auth.username}'")
return redirect(url_for("main.index")) return redirect(request.referrer)
@rest_blueprint.route('/node/<int:nodeId>/delete-user', methods=['GET']) @rest_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
@auth.access_control('default') @auth.authorize_admins('default')
def deleteNodeUser(nodeId: int):
node = Node().get(nodeId)
Node().delete(nodeId)
log.info(f"'{username()}' delete their own node '{nodeId}'")
return redirect(url_for("main.user", userName=node.user.name))
@ rest_blueprint.route('/node/<int:nodeId>/rename/<newName>', methods=['GET'])
@ auth.authorize_admins('default')
def renameNode(nodeId: int, newName: str): def renameNode(nodeId: int, newName: str):
Node().rename(nodeId, newName) Node().rename(nodeId, newName)
return jsonify(dict(newName=newName)) return jsonify(dict(newName=newName))
@ rest_blueprint.route('/user/<userName>/delete', methods=['GET']) @rest_blueprint.route('/user/<userName>/delete', methods=['GET'])
@ auth.authorize_admins('default') @auth.authorize_admins('default')
def deleteUser(userName: str): def deleteUser(userName: str):
nodes = Node().byUser(userName) nodes = Node().byUser(userName)
for node in nodes.nodes: for node in nodes.nodes:
@ -124,8 +84,8 @@ def deleteUser(userName: str):
return redirect(url_for("main.users")) return redirect(url_for("main.users"))
@ rest_blueprint.route('/user/<userName>/pakcreate', methods=['POST']) @rest_blueprint.route('/user/<userName>/pakcreate', methods=['POST'])
@ auth.authorize_admins('default') @auth.authorize_admins('default')
def createPKA(userName: str): def createPKA(userName: str):
data = request.json data = request.json
log.debug(data) log.debug(data)
@ -138,8 +98,8 @@ def createPKA(userName: str):
return jsonify(dict(key=pak.preAuthKey.key)) return jsonify(dict(key=pak.preAuthKey.key))
@ rest_blueprint.route('/user/<userName>/expire/<key>', methods=['GET']) @rest_blueprint.route('/user/<userName>/expire/<key>', methods=['GET'])
@ auth.authorize_admins('default') @auth.authorize_admins('default')
def expirePKA(userName: str, key: str): def expirePKA(userName: str, key: str):
log.debug(key) log.debug(key)
req = v1ExpirePreAuthKeyRequest(user=userName, key=key) req = v1ExpirePreAuthKeyRequest(user=userName, key=key)

View File

@ -17,7 +17,7 @@ class BaseConfig(object):
# All the followinf vars can be overriden # All the followinf vars can be overriden
# in the environment, using `HSMAN_` prefix # in the environment, using `HSMAN_` prefix
SECRET_KEY = "secreto" SECRET_KEY = "secreto"
ADMIN_GROUPS = ["adminGroup"] ADMIN_GROUPS = "adminGroup"
OIDC_CLIENT_ID = 'client-id' OIDC_CLIENT_ID = 'client-id'
OIDC_CLIENT_SECRET = 'client-secreto' OIDC_CLIENT_SECRET = 'client-secreto'
OIDC_URL = "https://myidp.example.com/auth" OIDC_URL = "https://myidp.example.com/auth"