Support for policy upload/download
Plus a small bugfix for groups
This commit is contained in:
parent
7fa17adfb1
commit
31910dc034
|
@ -74,7 +74,7 @@ class OIDCAuthentication(_OIDCAuth):
|
||||||
@property
|
@property
|
||||||
def groups(self) -> list:
|
def groups(self) -> list:
|
||||||
userinfo = flask_session['userinfo']
|
userinfo = flask_session['userinfo']
|
||||||
return userinfo.get('groups')
|
return userinfo.get('groups') or []
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def isAdmin(self) -> bool:
|
def isAdmin(self) -> bool:
|
||||||
|
|
|
@ -64,7 +64,7 @@ a:active {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle {
|
a.nodeco, a.nav-link, a.navbar-brand, a.routeToggle, a.plain {
|
||||||
text-decoration-line: none;
|
text-decoration-line: none;
|
||||||
text-decoration-style: unset;
|
text-decoration-style: unset;
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,3 +89,39 @@ function backfillips(obj) {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function uploadACL(obj) {
|
||||||
|
var fd = new FormData();
|
||||||
|
var files = $("#upload")[0].files[0];
|
||||||
|
fd.append("file", files);
|
||||||
|
|
||||||
|
// When we close the modal, we reload the page
|
||||||
|
$("#uploadACL").on("hidden.bs.modal", function (event) {
|
||||||
|
location.reload();
|
||||||
|
});
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/policy/upload",
|
||||||
|
type: "POST",
|
||||||
|
xhrFields: {
|
||||||
|
withCredentials: true,
|
||||||
|
},
|
||||||
|
data: fd,
|
||||||
|
contentType: false,
|
||||||
|
processData: false,
|
||||||
|
success: function (response) {
|
||||||
|
if (response != 0) {
|
||||||
|
$("#output").html("acl updated");
|
||||||
|
} else {
|
||||||
|
$("#output").html("acl not updated");
|
||||||
|
}
|
||||||
|
setTimeout(() => {
|
||||||
|
$("#uploadACL").modal("hide");
|
||||||
|
}, 5000);
|
||||||
|
},
|
||||||
|
error: function (response) {
|
||||||
|
console.log(response.responseJSON.message);
|
||||||
|
$("#output").html(response.responseJSON.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -41,16 +41,16 @@
|
||||||
</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 auth.groups[0] in config['ADMIN_GROUPS'] %}
|
{% if not auth.groups[0] or auth.groups[0] in config['ADMIN_GROUPS'] %}
|
||||||
<span class="badge badge-pill badge-warning">
|
<span class="badge badge-pill badge-warning">
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge badge-pill badge-dark">
|
<span class="badge badge-pill badge-dark">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{{ auth.groups[0]}}
|
{{ auth.groups[0] | default('no group')}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% for group in auth.groups[1:] |sort %}
|
{% for group in auth.groups[1:] | default([]) |sort %}
|
||||||
<div class="row data">
|
<div class="row data">
|
||||||
<div class="col col-2">
|
<div class="col col-2">
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
<div class="col col-4">
|
<div class="col col-4">
|
||||||
<h3>nodes</h3>
|
<h3>nodes</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="col col-2">
|
<div class="col col-4">
|
||||||
|
<div class="float-right">
|
||||||
<span data-toggle="tooltip" data-placement="right" title="Recheck all IP addresses of all nodes">
|
<span data-toggle="tooltip" data-placement="right" title="Recheck all IP addresses of all nodes">
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm" onClick="backfillips(this);">Backfill IP addresses</button>
|
<button type="button" class="btn btn-outline-primary btn-sm" onClick="backfillips(this);">Backfill IP addresses</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
<table id="nodes" class="display" style="width:100%">
|
<table id="nodes" class="display" style="width:100%">
|
||||||
<thead>
|
<thead>
|
||||||
|
|
|
@ -1,13 +1,30 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
<div class="row data justify-content-between">
|
||||||
|
<div class="col col-4">
|
||||||
<h3>policy</h3>
|
<h3>policy</h3>
|
||||||
<footer class="blockquote-footer">
|
<footer class="blockquote-footer">
|
||||||
for more info see <a href="https://tailscale.com/kb/1337/acl-syntax" target="_blank">tailscale docs</a>
|
for more info see <a href="https://tailscale.com/kb/1337/acl-syntax" target="_blank">tailscale docs</a>
|
||||||
</footer>
|
</footer>
|
||||||
|
</div>
|
||||||
|
<div class="col col-4">
|
||||||
|
<div class="float-right">
|
||||||
|
last update:
|
||||||
|
<span data-toggle="tooltip"
|
||||||
|
data-placement="right"
|
||||||
|
title="{{ policy.updatedAt | fmt_datetime }}">
|
||||||
|
{{policy.updatedAt | htime_dt }}
|
||||||
|
</span>
|
||||||
|
<div>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm plain"><a class="plain" href="/policy/view" target="_blank">View</a></button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm plain"><a class="plain" href="/policy/download">Download</a></button>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm plain" data-toggle="modal" data-target="#uploadACL">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col col-6">
|
<div class="col col-6">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
|
@ -79,8 +96,29 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- <textarea readonly rows="30" style="width: 80%; height: 80%;">
|
<!-- upload acl modal -->
|
||||||
{{ policy.json }}
|
<div class="modal fade" id="uploadACL" tabindex="-1" role="dialog" aria-labelledby="uploadACL" aria-hidden="true">
|
||||||
</textarea> -->
|
<div class="modal-dialog" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="renameModalLabel">upload ACL</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
<input class="form-check-input" type="file" name="upload" id="upload">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span id="output"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-primary" onClick="uploadACL(this);">Upload</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
@ -8,7 +8,7 @@ from app import auth
|
||||||
|
|
||||||
# from ..lib import username
|
# from ..lib import username
|
||||||
|
|
||||||
from flask import jsonify
|
from flask import jsonify, make_response
|
||||||
from flask_pyoidc.user_session import UserSession
|
from flask_pyoidc.user_session import UserSession
|
||||||
|
|
||||||
from hsapi_client import Node, User, Route, PreAuthKey, Policy
|
from hsapi_client import Node, User, Route, PreAuthKey, Policy
|
||||||
|
@ -132,9 +132,24 @@ def routes():
|
||||||
routes=final)
|
routes=final)
|
||||||
|
|
||||||
|
|
||||||
@main_blueprint.route('/policy', methods=['GET'])
|
@main_blueprint.route('/policy', defaults={'action': None}, methods=['GET'])
|
||||||
|
@main_blueprint.route('/policy/<action>', methods=['GET'])
|
||||||
@auth.authorize_admins('default')
|
@auth.authorize_admins('default')
|
||||||
def policy():
|
def policy(action):
|
||||||
|
log.debug(f"action: {action}")
|
||||||
policy = Policy().get()
|
policy = Policy().get()
|
||||||
|
if action == "view":
|
||||||
|
return policy.json
|
||||||
|
|
||||||
|
elif action == "download":
|
||||||
|
updateStr = policy.updatedAt.strftime(format='%Y%m%d-%H%M')
|
||||||
|
log.debug(updateStr)
|
||||||
|
filename = f"acl-{updateStr}.json"
|
||||||
|
response = make_response(policy.json)
|
||||||
|
response.headers['Content-Disposition'] = f'attachment; filename={
|
||||||
|
filename}'
|
||||||
|
|
||||||
|
return response
|
||||||
|
else:
|
||||||
return render_template("policy.html",
|
return render_template("policy.html",
|
||||||
policy=policy)
|
policy=policy)
|
||||||
|
|
|
@ -4,11 +4,14 @@ 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 flask import jsonify
|
from flask import jsonify, make_response
|
||||||
|
|
||||||
from hsapi_client import Node, User, Route, PreAuthKey
|
from hsapi_client import Node, User, Route, PreAuthKey, Policy
|
||||||
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
|
from hsapi_client.preauthkeys import (v1CreatePreAuthKeyRequest,
|
||||||
v1ExpirePreAuthKeyRequest)
|
v1ExpirePreAuthKeyRequest)
|
||||||
|
from hsapi_client.policies import v1Policy
|
||||||
|
|
||||||
|
from hsapi_client.config import HTTPException
|
||||||
|
|
||||||
from app.lib import remote_ip
|
from app.lib import remote_ip
|
||||||
|
|
||||||
|
@ -111,3 +114,21 @@ def expirePKA(userName: str, key: str):
|
||||||
def backfillips():
|
def backfillips():
|
||||||
response = Node().backfillips()
|
response = Node().backfillips()
|
||||||
return jsonify(response.changes)
|
return jsonify(response.changes)
|
||||||
|
|
||||||
|
|
||||||
|
@rest_blueprint.route('/policy/upload', methods=['POST'])
|
||||||
|
@auth.authorize_admins('default')
|
||||||
|
def policyUpload():
|
||||||
|
file = request.files['file']
|
||||||
|
try:
|
||||||
|
acl = ''.join(map(bytes.decode, file.readlines()))
|
||||||
|
except UnicodeDecodeError as e:
|
||||||
|
log.debug(e)
|
||||||
|
return jsonify(message=str(e)), 422
|
||||||
|
|
||||||
|
policy = Policy()
|
||||||
|
try:
|
||||||
|
policy.put(acl)
|
||||||
|
except HTTPException as e:
|
||||||
|
return jsonify(message=str(e.message)), 422
|
||||||
|
return jsonify(message="acl updated"), 200
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -15,7 +15,8 @@ flask-mobility = "^2.0.1"
|
||||||
humanize = "^4.9.0"
|
humanize = "^4.9.0"
|
||||||
flask-pydantic = "^0.12.0"
|
flask-pydantic = "^0.12.0"
|
||||||
uvicorn = "^0.30.1"
|
uvicorn = "^0.30.1"
|
||||||
hsapi-client = "^0.9.8"
|
hsapi-client = "^0.9.9"
|
||||||
|
# hsapi_client = { path = "../hsapi-client", develop = true }
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
Loading…
Reference in New Issue