diff --git a/flowapp/__about__.py b/flowapp/__about__.py index be0f7d4..59b7f43 100755 --- a/flowapp/__about__.py +++ b/flowapp/__about__.py @@ -1 +1 @@ -__version__ = "0.7.3" +__version__ = "0.8.0" diff --git a/flowapp/forms.py b/flowapp/forms.py index 3cf3555..ae83a1e 100644 --- a/flowapp/forms.py +++ b/flowapp/forms.py @@ -138,6 +138,32 @@ class ApiKeyForm(FlaskForm): 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") + + class OrganizationForm(FlaskForm): """ Organization form object diff --git a/flowapp/instance_config.py b/flowapp/instance_config.py index 3c8bafb..9d5a1bf 100644 --- a/flowapp/instance_config.py +++ b/flowapp/instance_config.py @@ -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", diff --git a/flowapp/templates/forms/machine_api_key.html b/flowapp/templates/forms/machine_api_key.html new file mode 100644 index 0000000..be6fcaa --- /dev/null +++ b/flowapp/templates/forms/machine_api_key.html @@ -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 %} +

Add new ApiKey for machine.

+

+ 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. +

+ +
+ +
+
Machine Api Key: {{ generated_key }}
+
+ +
+ {{ form.hidden_tag() if form.hidden_tag }} +
+
+ {{ render_field(form.machine) }} +
+
+ {{ render_checkbox_field(form.readonly, checked="checked") }} +
+
+ {{ render_field(form.expires) }} +
+
+ +
+
+
+ {{ render_field(form.comment) }} +
+ +
+
+ +
+
+ +{% endblock %} \ No newline at end of file diff --git a/flowapp/templates/pages/api_key.html b/flowapp/templates/pages/api_key.html index 2532d91..cc64588 100644 --- a/flowapp/templates/pages/api_key.html +++ b/flowapp/templates/pages/api_key.html @@ -19,7 +19,7 @@

Your machines and ApiKeys

{{ row.key }} - {{ row.expires }} + {{ row.expires|strftime }} {% if row.readonly %} diff --git a/flowapp/templates/pages/machine_api_key.html b/flowapp/templates/pages/machine_api_key.html new file mode 100644 index 0000000..52eb478 --- /dev/null +++ b/flowapp/templates/pages/machine_api_key.html @@ -0,0 +1,55 @@ +{% extends 'layouts/default.html' %} +{% block title %}ExaFS - ApiKeys{% endblock %} +{% block content %} +

Machines and ApiKeys

+

+ 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. +

+ + + + + + + + + + + {% for row in keys %} + + + + + + + + + {% endfor %} +
Machine addressApiKeyCreated byCreated forExpiresRead/Write ?Action
+ {{ row.machine }} + + {{ row.key }} + + {{ row.user.name }} + + {{ row.comment }} + + {{ row.expires|strftime }} + + {% if not row.readonly %} + + + {% endif %} + + + + +
+ + Add new Machine ApiKey + +{% endblock %} \ No newline at end of file diff --git a/flowapp/views/admin.py b/flowapp/views/admin.py index 4b8cc66..f142a4a 100644 --- a/flowapp/views/admin.py +++ b/flowapp/views/admin.py @@ -1,12 +1,14 @@ # flowapp/views/admin.py from datetime import datetime, timedelta +import secrets -from flask import Blueprint, render_template, redirect, flash, request, url_for +from flask import Blueprint, render_template, redirect, flash, request, session, url_for from sqlalchemy.exc import IntegrityError -from ..forms import ASPathForm, UserForm, ActionForm, OrganizationForm, CommunityForm +from ..forms import ASPathForm, MachineApiKeyForm, UserForm, ActionForm, OrganizationForm, CommunityForm from ..models import ( ASPath, + MachineApiKey, User, Action, Organization, @@ -42,6 +44,74 @@ def log(page): return render_template("pages/logs.html", logs=logs) +@admin.route("/machine_keys", methods=["GET"]) +@auth_required +@admin_required +def machine_keys(): + """ + Display all machine keys, from all admins + """ + keys = db.session.query(MachineApiKey).all() + + return render_template("pages/machine_api_key.html", keys=keys) + + +@admin.route("/add_machine_key", methods=["GET", "POST"]) +@auth_required +@admin_required +def add_machine_key(): + """ + Add new MachnieApiKey + :return: form or redirect to list of keys + """ + generated = secrets.token_hex(24) + form = MachineApiKeyForm(request.form, key=generated) + + if request.method == "POST" and form.validate(): + print("Form validated") + # import ipdb; ipdb.set_trace() + model = MachineApiKey( + machine=form.machine.data, + key=form.key.data, + expires=form.expires.data, + readonly=form.readonly.data, + comment=form.comment.data, + user_id=session["user_id"] + ) + + db.session.add(model) + db.session.commit() + flash("NewKey saved", "alert-success") + + return redirect(url_for("admin.machine_keys")) + else: + for field, errors in form.errors.items(): + for error in errors: + print( + "Error in the %s field - %s" + % (getattr(form, field).label.text, error) + ) + + return render_template("forms/machine_api_key.html", form=form, generated_key=generated) + + +@admin.route("/delete_machine_key/", methods=["GET"]) +@auth_required +@admin_required +def delete_machine_key(key_id): + """ + Delete api_key and machine + :param key_id: integer + """ + model = db.session.query(MachineApiKey).get(key_id) + # delete from db + db.session.delete(model) + db.session.commit() + flash("Key deleted", "alert-success") + + return redirect(url_for("admin.machine_keys")) + + @admin.route("/user", methods=["GET", "POST"]) @auth_required @admin_required