Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
fe9b42e8a2
|
|||
| 14b09a0fcf | |||
|
c39c3a0ab6
|
|||
|
07ac2edb53
|
|||
|
50097ce5b3
|
|||
|
4b28db6a13
|
22
README.md
22
README.md
@@ -16,17 +16,17 @@ You can run the Flask application as any other Flask app, using `flask run` insi
|
|||||||
|
|
||||||
There are some settings that must/can be provided to the application:
|
There are some settings that must/can be provided to the application:
|
||||||
|
|
||||||
| Variable | Usage | Default |
|
| Variable | Usage | Default |
|
||||||
| -------------------------- | ---------------------------------------- | :-----: |
|
| -------------------------- | -------------------------------------------------------------- | :-----: |
|
||||||
| `APPLICATION_ROOT` | Base URI path for the app | `/` |
|
| `APPLICATION_ROOT` | Base URI path for the app | `/` |
|
||||||
| `HSMAN_SECRET_KEY` | Flask app secret key | |
|
| `HSMAN_SECRET_KEY` | Flask app secret key | |
|
||||||
| `HSMAN_ADMIN_GROUPS` | User groups that are considered admins | |
|
| `HSMAN_ADMIN_GROUPS` | Comma separated list of user groups that are considered admins | |
|
||||||
| `HSMAN_OIDC_CLIENT_ID` | OIDC client ID | |
|
| `HSMAN_OIDC_CLIENT_ID` | OIDC client ID | |
|
||||||
| `HSMAN_OIDC_CLIENT_SECRET` | OIDC clietn secret | |
|
| `HSMAN_OIDC_CLIENT_SECRET` | OIDC clietn secret | |
|
||||||
| `HSMAN_OIDC_URL` | OIDC server URL | |
|
| `HSMAN_OIDC_URL` | OIDC server URL | |
|
||||||
| `HSMAN_OIDC_REDIRECT_URI` | OIDC redirect URI | |
|
| `HSMAN_OIDC_REDIRECT_URI` | OIDC redirect URI | |
|
||||||
| `HSAPI_SERVER` | Headscale server URL | |
|
| `HSAPI_SERVER` | Headscale server URL | |
|
||||||
| `HSAPI_API_TOKEN` | API token/key to access headscale server | |
|
| `HSAPI_API_TOKEN` | API token/key to access headscale server | |
|
||||||
|
|
||||||
The last two variables are then fed to `hsapi-client`, the module that we use to interact with Headscale APIs.
|
The last two variables are then fed to `hsapi-client`, the module that we use to interact with Headscale APIs.
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ def create_app(environment='development'):
|
|||||||
app.config.from_prefixed_env(prefix="HSMAN")
|
app.config.from_prefixed_env(prefix="HSMAN")
|
||||||
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(
|
||||||
|
map(str.strip, app.config.get('ADMIN_GROUPS', "").split(',')))
|
||||||
|
app.logger.debug(f"admin groups: {app.config['ADMIN_GROUPS']}")
|
||||||
|
|
||||||
app.logger.info("middleware init: mobility")
|
app.logger.info("middleware init: mobility")
|
||||||
mobility.init_app(app)
|
mobility.init_app(app)
|
||||||
@@ -38,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")
|
||||||
|
|||||||
82
app/lib.py
82
app/lib.py
@@ -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)
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h3>Welcome, {{ session.userinfo.name }}</h3>
|
<h3>
|
||||||
|
Welcome, {{ session.userinfo.name }}
|
||||||
|
</h3>
|
||||||
<hr>
|
<hr>
|
||||||
<h4>authentication info</h4>
|
<h4>authentication info</h4>
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
@@ -27,7 +29,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
<i class="fas fa-angle-right"></i>
|
<i class="fas fa-angle-right"></i>
|
||||||
|
{% if session.userinfo.groups[0] in config['ADMIN_GROUPS'] %}
|
||||||
|
<span class="badge badge-pill badge-warning">
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-pill badge-dark">
|
||||||
|
{% endif %}
|
||||||
{{ session.userinfo.groups[0]}}
|
{{ session.userinfo.groups[0]}}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for group in session.userinfo.groups[1:] |sort %}
|
{% for group in session.userinfo.groups[1:] |sort %}
|
||||||
@@ -36,10 +44,29 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
<i class="fas fa-angle-right"></i> {{ group }}
|
<i class="fas fa-angle-right"></i>
|
||||||
|
{% if group in config['ADMIN_GROUPS'] %}
|
||||||
|
<span class="badge badge-pill badge-warning">
|
||||||
|
{% else %}
|
||||||
|
<span class="badge badge-pill badge-dark">
|
||||||
|
{% endif %}
|
||||||
|
{{ group }}
|
||||||
|
</span>
|
||||||
</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">
|
||||||
@@ -52,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 }}">
|
||||||
@@ -69,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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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(
|
||||||
@@ -118,8 +120,8 @@ def user(userName):
|
|||||||
def routes():
|
def routes():
|
||||||
routes = Route().list()
|
routes = Route().list()
|
||||||
|
|
||||||
prefixes = set(
|
prefixes = sorted(set(
|
||||||
(r.prefix for r in routes.routes if not r.prefix.endswith('/0')))
|
(r.prefix for r in routes.routes if not r.prefix.endswith('/0'))))
|
||||||
|
|
||||||
exitNodes = [r.node for r in routes.routes if r.prefix.endswith(
|
exitNodes = [r.node for r in routes.routes if r.prefix.endswith(
|
||||||
'0/0') and r.enabled]
|
'0/0') and r.enabled]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "hsman"
|
name = "hsman"
|
||||||
version = "0.9.5"
|
version = "0.9.8"
|
||||||
description = "Flask Admin webui for Headscale"
|
description = "Flask Admin webui for Headscale"
|
||||||
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
authors = ["Andrea Mistrali <andrea@mistrali.pw>"]
|
||||||
license = "BSD"
|
license = "BSD"
|
||||||
|
|||||||
Reference in New Issue
Block a user