Skip to content

Commit

Permalink
Merge pull request #40 from CESNET/feature/ro-api
Browse files Browse the repository at this point in the history
Feature/ro api
  • Loading branch information
jirivrany committed May 17, 2024
2 parents 4521230 + 9f32ac0 commit 64b3e00
Show file tree
Hide file tree
Showing 25 changed files with 618 additions and 805 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ 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.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 machinnes.
- 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.0"
4 changes: 3 additions & 1 deletion flowapp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,10 @@ def inject_dashboard():

@app.template_filter("strftime")
def format_datetime(value):
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):
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-sm-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
55 changes: 55 additions & 0 deletions flowapp/templates/pages/machine_api_key.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{% extends 'layouts/default.html' %}
{% block title %}ExaFS - ApiKeys{% endblock %}
{% block content %}
<h1>Machines and ApiKeys</h1>
<p>
This is the list of all machines and their API keys, created by admin(s).
In general, the keys should be Read Only and with expiration.
If you need to create a full access Read/Write key, use usual user form with your organization settings.
</p>
<table class="table table-hover">
<tr>
<th>Machine address</th>
<th>ApiKey</th>
<th>Created by</th>
<th>Created for</th>
<th>Expires</th>
<th>Read/Write ?</th>
<th>Action</th>
</tr>
{% for row in keys %}
<tr>
<td>
{{ row.machine }}
</td>
<td>
{{ row.key }}
</td>
<td>
{{ row.user.name }}
</td>
<td>
{{ row.comment }}
<td>
{{ row.expires|strftime }}
</td>
<td>
{% if not row.readonly %}
<button type="button" class="btn btn-warning btn-sm" title="Read Only">
<i class="bi bi-exclamation-lg"></i>
</button>

{% endif %}
</td>
<td>
<a class="btn btn-danger btn-sm" href="{{ url_for('admin.delete_machine_key', key_id=row.id) }}" role="button">
<i class="bi bi-x-lg"></i>
</a>
</td>
</tr>
{% endfor %}
</table>
<a class="btn btn-primary" href="{{ url_for('admin.add_machine_key') }}" role="button">
Add new Machine ApiKey
</a>
{% endblock %}
Loading

0 comments on commit 64b3e00

Please sign in to comment.