Skip to content

Commit

Permalink
Merge pull request #43 from CESNET/develop
Browse files Browse the repository at this point in the history
Merge 0.8.1 from develop to master
  • Loading branch information
jirivrany authored Nov 27, 2024
2 parents dd2f592 + 36ad3fe commit 49ffee7
Show file tree
Hide file tree
Showing 30 changed files with 689 additions and 911 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ Last part of the system is Guarda service. This systemctl service is running in
* [Local database instalation notes](./docs/DB_LOCAL.md)

## Change Log
- 0.8.1 application is using Flask-Session stored in DB using SQL Alchemy driver. This can be configured for other
drivers, however server side session is required for the application proper function.
- 0.8.0 - API keys update. **Run migration scripts to update your DB**. Keys can now have expiration date and readonly flag. Admin can create special keys for certain machines.
- 0.7.3 - New possibility of external auth proxy.
- 0.7.2 - Dashboard and Main menu are now customizable in config. App is ready to be packaged using setup.py.
- 0.7.0 - ExaAPI now have two options - HTTP or RabbitMQ. ExaAPI process has been renamed, update of ExaBGP process value is needed for this version.
Expand Down
2 changes: 1 addition & 1 deletion flowapp/__about__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.7.3"
__version__ = "0.8.1"
36 changes: 20 additions & 16 deletions flowapp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from flask_sqlalchemy import SQLAlchemy
from flask_wtf.csrf import CSRFProtect
from flask_migrate import Migrate
from flask_session import Session

from .__about__ import __version__
from .instance_config import InstanceConfig
Expand All @@ -14,30 +15,34 @@
db = SQLAlchemy()
migrate = Migrate()
csrf = CSRFProtect()
ext = SSO()
sess = Session()


def create_app():
def create_app(config_object=None):
app = Flask(__name__)
# Map SSO attributes from ADFS to session keys under session['user']
#: Default attribute map

# SSO configuration
SSO_ATTRIBUTE_MAP = {
"eppn": (True, "eppn"),
"cn": (False, "cn"),
}
app.config.setdefault("SSO_ATTRIBUTE_MAP", SSO_ATTRIBUTE_MAP)
app.config.setdefault("SSO_LOGIN_URL", "/login")

# db.init_app(app)
# extension init
migrate.init_app(app, db)
csrf.init_app(app)

# Load the default configuration for dashboard and main menu
app.config.from_object(InstanceConfig)
if config_object:
app.config.from_object(config_object)

app.config.setdefault("VERSION", __version__)
app.config.setdefault("SSO_ATTRIBUTE_MAP", SSO_ATTRIBUTE_MAP)
app.config.setdefault("SSO_LOGIN_URL", "/login")

# This attaches the *flask_sso* login handler to the SSO_LOGIN_URL,
ext = SSO(app=app)
# Init SSO
ext.init_app(app)

from flowapp import models, constants, validators
from .views.admin import admin
Expand Down Expand Up @@ -85,7 +90,7 @@ def logout():

@app.route("/ext-login")
def ext_login():
header_name = app.config.get("AUTH_HEADER_NAME", 'X-Authenticated-User')
header_name = app.config.get("AUTH_HEADER_NAME", "X-Authenticated-User")
if header_name not in request.headers:
return render_template("errors/401.html")

Expand Down Expand Up @@ -148,9 +153,7 @@ def internal_error(exception):
def utility_processor():
def editable_rule(rule):
if rule:
validators.editable_range(
rule, models.get_user_nets(session["user_id"])
)
validators.editable_range(rule, models.get_user_nets(session["user_id"]))
return True
return False

Expand All @@ -174,20 +177,21 @@ def inject_dashboard():

@app.template_filter("strftime")
def format_datetime(value):
format = "y/MM/dd HH:mm"
if value is None:
return app.config.get("MISSING_DATETIME_MESSAGE", "Never")

format = "y/MM/dd HH:mm"
return babel.dates.format_datetime(value, format)

def _register_user_to_session(uuid: str):
print(f"Registering user {uuid} to session")
user = db.session.query(models.User).filter_by(uuid=uuid).first()
session["user_uuid"] = user.uuid
session["user_email"] = user.uuid
session["user_name"] = user.name
session["user_id"] = user.id
session["user_roles"] = [role.name for role in user.role.all()]
session["user_orgs"] = ", ".join(
org.name for org in user.organization.all()
)
session["user_orgs"] = ", ".join(org.name for org in user.organization.all())
session["user_role_ids"] = [role.id for role in user.role.all()]
session["user_org_ids"] = [org.id for org in user.organization.all()]
roles = [i > 1 for i in session["user_role_ids"]]
Expand Down
44 changes: 43 additions & 1 deletion flowapp/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,17 @@ class MultiFormatDateTimeLocalField(DateTimeField):

def __init__(self, *args, **kwargs):
kwargs.setdefault("format", "%Y-%m-%dT%H:%M")
self.unlimited = kwargs.pop('unlimited', False)
self.pref_format = None
super().__init__(*args, **kwargs)

def process_formdata(self, valuelist):
if not valuelist:
return
return None
# with unlimited field we do not need to parse the empty value
if self.unlimited and len(valuelist) == 1 and len(valuelist[0]) == 0:
self.data = None
return None

date_str = " ".join((str(val) for val in valuelist))
result, pref_format = parse_api_time(date_str)
Expand Down Expand Up @@ -119,6 +124,43 @@ class ApiKeyForm(FlaskForm):
validators=[DataRequired(), IPAddress(message="provide valid IP address")],
)

comment = TextAreaField(
"Your comment for this key", validators=[Optional(), Length(max=255)]
)

expires = MultiFormatDateTimeLocalField(
"Key expiration. Leave blank for non expring key (not-recomended).",
format=FORM_TIME_PATTERN, validators=[Optional()], unlimited=True
)

readonly = BooleanField("Read only key", default=False)

key = HiddenField("GeneratedKey")


class MachineApiKeyForm(FlaskForm):
"""
ApiKey for Machines
Each key / machine pair is unique
Only Admin can create new these keys
"""

machine = StringField(
"Machine address",
validators=[DataRequired(), IPAddress(message="provide valid IP address")],
)

comment = TextAreaField(
"Your comment for this key", validators=[Optional(), Length(max=255)]
)

expires = MultiFormatDateTimeLocalField(
"Key expiration. Leave blank for non expring key (not-recomended).",
format=FORM_TIME_PATTERN, validators=[Optional()], unlimited=True
)

readonly = BooleanField("Read only key", default=False)

key = HiddenField("GeneratedKey")


Expand Down
1 change: 1 addition & 0 deletions flowapp/instance_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ class InstanceConfig:
],
"admin": [
{"name": "Commands Log", "url": "admin.log"},
{"name": "Machine keys", "url": "admin.machine_keys"},
{
"name": "Users",
"url": "admin.users",
Expand Down
27 changes: 27 additions & 0 deletions flowapp/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class User(db.Model):
name = db.Column(db.String(255))
phone = db.Column(db.String(255))
apikeys = db.relationship("ApiKey", back_populates="user", lazy="dynamic")
machineapikeys = db.relationship("MachineApiKey", back_populates="user", lazy="dynamic")
role = db.relationship("Role", secondary=user_role, lazy="dynamic", backref="user")

organization = db.relationship(
Expand Down Expand Up @@ -82,9 +83,35 @@ class ApiKey(db.Model):
id = db.Column(db.Integer, primary_key=True)
machine = db.Column(db.String(255))
key = db.Column(db.String(255))
readonly = db.Column(db.Boolean, default=False)
expires = db.Column(db.DateTime, nullable=True)
comment = db.Column(db.String(255))
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
user = db.relationship("User", back_populates="apikeys")

def is_expired(self):
if self.expires is None:
return False # Non-expiring key
else:
return self.expires < datetime.now()


class MachineApiKey(db.Model):
id = db.Column(db.Integer, primary_key=True)
machine = db.Column(db.String(255))
key = db.Column(db.String(255))
readonly = db.Column(db.Boolean, default=True)
expires = db.Column(db.DateTime, nullable=True)
comment = db.Column(db.String(255))
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
user = db.relationship("User", back_populates="machineapikeys")

def is_expired(self):
if self.expires is None:
return False # Non-expiring key
else:
return self.expires < datetime.now()


class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
Expand Down
29 changes: 19 additions & 10 deletions flowapp/templates/forms/api_key.html
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
{% extends 'layouts/default.html' %}
{% from 'forms/macros.html' import render_field %}
{% from 'forms/macros.html' import render_field, render_checkbox_field %}
{% block title %}Add New Machine with ApiKey{% endblock %}
{% block content %}
<h2>Add new ApiKey for your machine</h2>

<div class="row">

<div class="col-sm-12">
<h6>ApiKey: {{ generated_key }}</h6>
</div>

<form action="{{ action_url }}" method="POST">
{{ form.hidden_tag() if form.hidden_tag }}
<div class="row">
<div class="col-sm-12">
<div class="col-smfut-5">
{{ render_field(form.machine) }}
</div>
</div>

<div class="row">
<div class="col-sm-4">
ApiKey for this machine:
<div class="col-sm-2">
{{ render_checkbox_field(form.readonly) }}
</div>
<div class="col-sm-8">
{{ generated_key }}
<div class="col-sm-5">
{{ render_field(form.expires) }}
</div>
</div>

</div>

<div class="row">
<div class="col-sm-10">
{{ render_field(form.comment) }}
</div>

<div class="row">
<div class="col-sm-10">
Expand Down
44 changes: 44 additions & 0 deletions flowapp/templates/forms/machine_api_key.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{% extends 'layouts/default.html' %}
{% from 'forms/macros.html' import render_field, render_checkbox_field %}
{% block title %}Add New Machine with ApiKey{% endblock %}
{% block content %}
<h2>Add new ApiKey for machine.</h2>
<p>
In general, the keys should be Read Only and with expiration.
If you need to create a full access Read/Write key, consider using usual user form
with your organization settings.
</p>

<div class="row">

<div class="col-sm-12">
<h6>Machine Api Key: {{ generated_key }}</h6>
</div>

<form action="{{ url_for('admin.add_machine_key') }}" method="POST">
{{ form.hidden_tag() if form.hidden_tag }}
<div class="row">
<div class="col-sm-5">
{{ render_field(form.machine) }}
</div>
<div class="col-sm-2">
{{ render_checkbox_field(form.readonly, checked="checked") }}
</div>
<div class="col-sm-5">
{{ render_field(form.expires) }}
</div>
</div>

</div>
<div class="row">
<div class="col-sm-10">
{{ render_field(form.comment) }}
</div>

<div class="row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">Save</button>
</div>
</div>

{% endblock %}
2 changes: 1 addition & 1 deletion flowapp/templates/forms/macros.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{# Renders field for bootstrap 3 standards.
{# Renders field for bootstrap 5 standards.

Params:
field - WTForm field
Expand Down
26 changes: 22 additions & 4 deletions flowapp/templates/pages/api_key.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ <h1>Your machines and ApiKeys</h1>
<tr>
<th>Machine address</th>
<th>ApiKey</th>
<th>Expires</th>
<th>Read only</th>
<th>Action</th>
</tr>
{% for row in keys %}
Expand All @@ -17,10 +19,26 @@ <h1>Your machines and ApiKeys</h1>
{{ row.key }}
</td>
<td>
<a class="btn btn-danger btn-sm" href="{{ url_for('api_keys.delete', key_id=row.id) }}" role="button">
<i class="bi bi-x-lg"></i>
</a>
</td>
{{ row.expires|strftime }}
</td>
<td>
{% if row.readonly %}
<button type="button" class="btn btn-success btn-sm" title="Read Only">
<i class="bi bi-check-lg"></i>
</button>

{% endif %}
</td>
<td>
<a class="btn btn-danger btn-sm" href="{{ url_for('api_keys.delete', key_id=row.id) }}" role="button">
<i class="bi bi-x-lg"></i>
</a>
{% if row.comment %}
<button type="button" class="btn btn-info btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ row.comment }}">
<i class="bi bi-chat-left-text-fill"></i>
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
Expand Down
2 changes: 1 addition & 1 deletion flowapp/templates/pages/dashboard_user.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ <h2>{{ rstate|capitalize }} {{ table_title }} that you can modify</h2>
<table class="table table-hover ip-table">
{{ dashboard_table_editable_head }}
{{ dashboard_table_editable }}
{{ dashboard_table_foot }}}
{{ dashboard_table_foot }}
</table>
</form>
<script type="text/javascript" src="{{ url_for('static', filename='js/check_all.js') }}"></script>
Expand Down
Loading

0 comments on commit 49ffee7

Please sign in to comment.