Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge 0.8.1 from develop to master #43

Merged
merged 32 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
6913689
Support for authentication using external proxy (#33)
jakubman1 Nov 3, 2023
4c1ece5
version 0.7.3, simple auth mode available, docs for auth created
jirivrany Nov 3, 2023
cb93fbe
version 0.7.3, simple auth mode available, docs for auth created
jirivrany Nov 3, 2023
061f40f
typo in link
jirivrany Nov 3, 2023
e86ac0f
Bugfix/autoescape (#35)
jirivrany Jan 18, 2024
6b020f1
bugfix - V4 table cols, DOCS update
jirivrany Jan 25, 2024
e9cf60c
Merge branch 'master' into develop
jirivrany Jan 25, 2024
fe4ec95
bugfix, closes #38
jirivrany Mar 8, 2024
1b81d2f
Merge branch 'develop' of github.com:CESNET/exafs into develop
jirivrany Mar 8, 2024
4521230
fix typo in user template
jirivrany Mar 27, 2024
02b08ff
update models and api auth for machinekeys and reaonly keys
jirivrany Mar 28, 2024
5f58318
api v1 and v2 are now deprecated
jirivrany Apr 2, 2024
382a83e
updated user apikey form
jirivrany Apr 2, 2024
fb25b20
gui updates for ApiKey and MachineApiKey
jirivrany Apr 4, 2024
fa8ce17
readme updated
jirivrany Apr 4, 2024
9f32ac0
update template filter for strftime
jirivrany Apr 4, 2024
64b3e00
Merge pull request #40 from CESNET/feature/ro-api
jirivrany May 17, 2024
52cae97
quickfix: org adres ranges formatting in template
jirivrany Jul 15, 2024
54d5343
add debug print for rule ids in session
Sep 20, 2024
3868408
session ready to test on server
Sep 20, 2024
a354eb2
debug session init
Sep 20, 2024
7ec5a15
update run example.py
Sep 20, 2024
3002dfa
lets try filesystem cachelib
Sep 20, 2024
372ca44
typo
Sep 20, 2024
78ca1c3
sso map!
Sep 20, 2024
59edcb3
back to SQLAlchemy session
Sep 20, 2024
831f708
version 0.8.1 with server side session
Sep 23, 2024
1da865d
version 0.8.1 with server side session
Sep 23, 2024
cc478f5
Merge pull request #41 from CESNET/bugfix/session
jirivrany Sep 23, 2024
a837a7a
fix missing rule_id parameter in rule get API endpoints
jakubman1 Oct 30, 2024
9dbe60f
Merge pull request #42 from jakubman1/bugfix/rule-get-api
jirivrany Oct 30, 2024
36ad3fe
merge finished
Nov 27, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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