Skip to content

Commit 49ffee7

Browse files
authored
Merge pull request #43 from CESNET/develop
Merge 0.8.1 from develop to master
2 parents dd2f592 + 36ad3fe commit 49ffee7

30 files changed

+689
-911
lines changed

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ Last part of the system is Guarda service. This systemctl service is running in
5252
* [Local database instalation notes](./docs/DB_LOCAL.md)
5353

5454
## Change Log
55+
- 0.8.1 application is using Flask-Session stored in DB using SQL Alchemy driver. This can be configured for other
56+
drivers, however server side session is required for the application proper function.
57+
- 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.
5558
- 0.7.3 - New possibility of external auth proxy.
5659
- 0.7.2 - Dashboard and Main menu are now customizable in config. App is ready to be packaged using setup.py.
5760
- 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.

flowapp/__about__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.7.3"
1+
__version__ = "0.8.1"

flowapp/__init__.py

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from flask_sqlalchemy import SQLAlchemy
77
from flask_wtf.csrf import CSRFProtect
88
from flask_migrate import Migrate
9+
from flask_session import Session
910

1011
from .__about__ import __version__
1112
from .instance_config import InstanceConfig
@@ -14,30 +15,34 @@
1415
db = SQLAlchemy()
1516
migrate = Migrate()
1617
csrf = CSRFProtect()
18+
ext = SSO()
19+
sess = Session()
1720

1821

19-
def create_app():
22+
def create_app(config_object=None):
2023
app = Flask(__name__)
21-
# Map SSO attributes from ADFS to session keys under session['user']
22-
#: Default attribute map
24+
25+
# SSO configuration
2326
SSO_ATTRIBUTE_MAP = {
2427
"eppn": (True, "eppn"),
2528
"cn": (False, "cn"),
2629
}
30+
app.config.setdefault("SSO_ATTRIBUTE_MAP", SSO_ATTRIBUTE_MAP)
31+
app.config.setdefault("SSO_LOGIN_URL", "/login")
2732

28-
# db.init_app(app)
33+
# extension init
2934
migrate.init_app(app, db)
3035
csrf.init_app(app)
3136

3237
# Load the default configuration for dashboard and main menu
3338
app.config.from_object(InstanceConfig)
39+
if config_object:
40+
app.config.from_object(config_object)
3441

3542
app.config.setdefault("VERSION", __version__)
36-
app.config.setdefault("SSO_ATTRIBUTE_MAP", SSO_ATTRIBUTE_MAP)
37-
app.config.setdefault("SSO_LOGIN_URL", "/login")
3843

39-
# This attaches the *flask_sso* login handler to the SSO_LOGIN_URL,
40-
ext = SSO(app=app)
44+
# Init SSO
45+
ext.init_app(app)
4146

4247
from flowapp import models, constants, validators
4348
from .views.admin import admin
@@ -85,7 +90,7 @@ def logout():
8590

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

@@ -148,9 +153,7 @@ def internal_error(exception):
148153
def utility_processor():
149154
def editable_rule(rule):
150155
if rule:
151-
validators.editable_range(
152-
rule, models.get_user_nets(session["user_id"])
153-
)
156+
validators.editable_range(rule, models.get_user_nets(session["user_id"]))
154157
return True
155158
return False
156159

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

175178
@app.template_filter("strftime")
176179
def format_datetime(value):
177-
format = "y/MM/dd HH:mm"
180+
if value is None:
181+
return app.config.get("MISSING_DATETIME_MESSAGE", "Never")
178182

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

181186
def _register_user_to_session(uuid: str):
187+
print(f"Registering user {uuid} to session")
182188
user = db.session.query(models.User).filter_by(uuid=uuid).first()
183189
session["user_uuid"] = user.uuid
184190
session["user_email"] = user.uuid
185191
session["user_name"] = user.name
186192
session["user_id"] = user.id
187193
session["user_roles"] = [role.name for role in user.role.all()]
188-
session["user_orgs"] = ", ".join(
189-
org.name for org in user.organization.all()
190-
)
194+
session["user_orgs"] = ", ".join(org.name for org in user.organization.all())
191195
session["user_role_ids"] = [role.id for role in user.role.all()]
192196
session["user_org_ids"] = [org.id for org in user.organization.all()]
193197
roles = [i > 1 for i in session["user_role_ids"]]

flowapp/forms.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,17 @@ class MultiFormatDateTimeLocalField(DateTimeField):
5555

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

6162
def process_formdata(self, valuelist):
6263
if not valuelist:
63-
return
64+
return None
65+
# with unlimited field we do not need to parse the empty value
66+
if self.unlimited and len(valuelist) == 1 and len(valuelist[0]) == 0:
67+
self.data = None
68+
return None
6469

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

127+
comment = TextAreaField(
128+
"Your comment for this key", validators=[Optional(), Length(max=255)]
129+
)
130+
131+
expires = MultiFormatDateTimeLocalField(
132+
"Key expiration. Leave blank for non expring key (not-recomended).",
133+
format=FORM_TIME_PATTERN, validators=[Optional()], unlimited=True
134+
)
135+
136+
readonly = BooleanField("Read only key", default=False)
137+
138+
key = HiddenField("GeneratedKey")
139+
140+
141+
class MachineApiKeyForm(FlaskForm):
142+
"""
143+
ApiKey for Machines
144+
Each key / machine pair is unique
145+
Only Admin can create new these keys
146+
"""
147+
148+
machine = StringField(
149+
"Machine address",
150+
validators=[DataRequired(), IPAddress(message="provide valid IP address")],
151+
)
152+
153+
comment = TextAreaField(
154+
"Your comment for this key", validators=[Optional(), Length(max=255)]
155+
)
156+
157+
expires = MultiFormatDateTimeLocalField(
158+
"Key expiration. Leave blank for non expring key (not-recomended).",
159+
format=FORM_TIME_PATTERN, validators=[Optional()], unlimited=True
160+
)
161+
162+
readonly = BooleanField("Read only key", default=False)
163+
122164
key = HiddenField("GeneratedKey")
123165

124166

flowapp/instance_config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ class InstanceConfig:
7878
],
7979
"admin": [
8080
{"name": "Commands Log", "url": "admin.log"},
81+
{"name": "Machine keys", "url": "admin.machine_keys"},
8182
{
8283
"name": "Users",
8384
"url": "admin.users",

flowapp/models.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class User(db.Model):
3434
name = db.Column(db.String(255))
3535
phone = db.Column(db.String(255))
3636
apikeys = db.relationship("ApiKey", back_populates="user", lazy="dynamic")
37+
machineapikeys = db.relationship("MachineApiKey", back_populates="user", lazy="dynamic")
3738
role = db.relationship("Role", secondary=user_role, lazy="dynamic", backref="user")
3839

3940
organization = db.relationship(
@@ -82,9 +83,35 @@ class ApiKey(db.Model):
8283
id = db.Column(db.Integer, primary_key=True)
8384
machine = db.Column(db.String(255))
8485
key = db.Column(db.String(255))
86+
readonly = db.Column(db.Boolean, default=False)
87+
expires = db.Column(db.DateTime, nullable=True)
88+
comment = db.Column(db.String(255))
8589
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
8690
user = db.relationship("User", back_populates="apikeys")
8791

92+
def is_expired(self):
93+
if self.expires is None:
94+
return False # Non-expiring key
95+
else:
96+
return self.expires < datetime.now()
97+
98+
99+
class MachineApiKey(db.Model):
100+
id = db.Column(db.Integer, primary_key=True)
101+
machine = db.Column(db.String(255))
102+
key = db.Column(db.String(255))
103+
readonly = db.Column(db.Boolean, default=True)
104+
expires = db.Column(db.DateTime, nullable=True)
105+
comment = db.Column(db.String(255))
106+
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False)
107+
user = db.relationship("User", back_populates="machineapikeys")
108+
109+
def is_expired(self):
110+
if self.expires is None:
111+
return False # Non-expiring key
112+
else:
113+
return self.expires < datetime.now()
114+
88115

89116
class Role(db.Model):
90117
id = db.Column(db.Integer, primary_key=True)

flowapp/templates/forms/api_key.html

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,34 @@
11
{% extends 'layouts/default.html' %}
2-
{% from 'forms/macros.html' import render_field %}
2+
{% from 'forms/macros.html' import render_field, render_checkbox_field %}
33
{% block title %}Add New Machine with ApiKey{% endblock %}
44
{% block content %}
55
<h2>Add new ApiKey for your machine</h2>
6+
7+
<div class="row">
8+
9+
<div class="col-sm-12">
10+
<h6>ApiKey: {{ generated_key }}</h6>
11+
</div>
12+
613
<form action="{{ action_url }}" method="POST">
714
{{ form.hidden_tag() if form.hidden_tag }}
815
<div class="row">
9-
<div class="col-sm-12">
16+
<div class="col-smfut-5">
1017
{{ render_field(form.machine) }}
1118
</div>
12-
</div>
13-
14-
<div class="row">
15-
<div class="col-sm-4">
16-
ApiKey for this machine:
19+
<div class="col-sm-2">
20+
{{ render_checkbox_field(form.readonly) }}
1721
</div>
18-
<div class="col-sm-8">
19-
{{ generated_key }}
22+
<div class="col-sm-5">
23+
{{ render_field(form.expires) }}
2024
</div>
25+
</div>
26+
2127
</div>
22-
28+
<div class="row">
29+
<div class="col-sm-10">
30+
{{ render_field(form.comment) }}
31+
</div>
2332

2433
<div class="row">
2534
<div class="col-sm-10">
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{% extends 'layouts/default.html' %}
2+
{% from 'forms/macros.html' import render_field, render_checkbox_field %}
3+
{% block title %}Add New Machine with ApiKey{% endblock %}
4+
{% block content %}
5+
<h2>Add new ApiKey for machine.</h2>
6+
<p>
7+
In general, the keys should be Read Only and with expiration.
8+
If you need to create a full access Read/Write key, consider using usual user form
9+
with your organization settings.
10+
</p>
11+
12+
<div class="row">
13+
14+
<div class="col-sm-12">
15+
<h6>Machine Api Key: {{ generated_key }}</h6>
16+
</div>
17+
18+
<form action="{{ url_for('admin.add_machine_key') }}" method="POST">
19+
{{ form.hidden_tag() if form.hidden_tag }}
20+
<div class="row">
21+
<div class="col-sm-5">
22+
{{ render_field(form.machine) }}
23+
</div>
24+
<div class="col-sm-2">
25+
{{ render_checkbox_field(form.readonly, checked="checked") }}
26+
</div>
27+
<div class="col-sm-5">
28+
{{ render_field(form.expires) }}
29+
</div>
30+
</div>
31+
32+
</div>
33+
<div class="row">
34+
<div class="col-sm-10">
35+
{{ render_field(form.comment) }}
36+
</div>
37+
38+
<div class="row">
39+
<div class="col-sm-10">
40+
<button type="submit" class="btn btn-primary">Save</button>
41+
</div>
42+
</div>
43+
44+
{% endblock %}

flowapp/templates/forms/macros.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
{# Renders field for bootstrap 3 standards.
1+
{# Renders field for bootstrap 5 standards.
22

33
Params:
44
field - WTForm field

flowapp/templates/pages/api_key.html

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ <h1>Your machines and ApiKeys</h1>
66
<tr>
77
<th>Machine address</th>
88
<th>ApiKey</th>
9+
<th>Expires</th>
10+
<th>Read only</th>
911
<th>Action</th>
1012
</tr>
1113
{% for row in keys %}
@@ -17,10 +19,26 @@ <h1>Your machines and ApiKeys</h1>
1719
{{ row.key }}
1820
</td>
1921
<td>
20-
<a class="btn btn-danger btn-sm" href="{{ url_for('api_keys.delete', key_id=row.id) }}" role="button">
21-
<i class="bi bi-x-lg"></i>
22-
</a>
23-
</td>
22+
{{ row.expires|strftime }}
23+
</td>
24+
<td>
25+
{% if row.readonly %}
26+
<button type="button" class="btn btn-success btn-sm" title="Read Only">
27+
<i class="bi bi-check-lg"></i>
28+
</button>
29+
30+
{% endif %}
31+
</td>
32+
<td>
33+
<a class="btn btn-danger btn-sm" href="{{ url_for('api_keys.delete', key_id=row.id) }}" role="button">
34+
<i class="bi bi-x-lg"></i>
35+
</a>
36+
{% if row.comment %}
37+
<button type="button" class="btn btn-info btn-sm" data-bs-toggle="tooltip" data-bs-placement="top" title="{{ row.comment }}">
38+
<i class="bi bi-chat-left-text-fill"></i>
39+
</button>
40+
{% endif %}
41+
</td>
2442
</tr>
2543
{% endfor %}
2644
</table>

flowapp/templates/pages/dashboard_user.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ <h2>{{ rstate|capitalize }} {{ table_title }} that you can modify</h2>
1616
<table class="table table-hover ip-table">
1717
{{ dashboard_table_editable_head }}
1818
{{ dashboard_table_editable }}
19-
{{ dashboard_table_foot }}}
19+
{{ dashboard_table_foot }}
2020
</table>
2121
</form>
2222
<script type="text/javascript" src="{{ url_for('static', filename='js/check_all.js') }}"></script>

0 commit comments

Comments
 (0)