diff --git a/flowapp/templates/forms/machine_api_key.html b/flowapp/templates/forms/machine_api_key.html
new file mode 100644
index 00000000..be6fcaad
--- /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 %}
+
+ 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.
+
+
+ {{ render_field(form.comment) }}
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/flowapp/templates/forms/macros.html b/flowapp/templates/forms/macros.html
index 12d8e435..cb79a11b 100644
--- a/flowapp/templates/forms/macros.html
+++ b/flowapp/templates/forms/macros.html
@@ -1,4 +1,4 @@
-{# Renders field for bootstrap 3 standards.
+{# Renders field for bootstrap 5 standards.
Params:
field - WTForm field
diff --git a/flowapp/templates/pages/api_key.html b/flowapp/templates/pages/api_key.html
index 0b3ee32b..cc645887 100644
--- a/flowapp/templates/pages/api_key.html
+++ b/flowapp/templates/pages/api_key.html
@@ -6,6 +6,8 @@
Your machines and ApiKeys
Machine address |
ApiKey |
+ Expires |
+ Read only |
Action |
{% for row in keys %}
@@ -17,10 +19,26 @@
Your machines and ApiKeys
{{ row.key }}
-
-
-
- |
+ {{ row.expires|strftime }}
+
+
+ {% if row.readonly %}
+
+
+ {% endif %}
+ |
+
+
+
+
+ {% if row.comment %}
+
+ {% endif %}
+ |
{% endfor %}
diff --git a/flowapp/templates/pages/dashboard_user.html b/flowapp/templates/pages/dashboard_user.html
index 683f5436..f3c777fa 100644
--- a/flowapp/templates/pages/dashboard_user.html
+++ b/flowapp/templates/pages/dashboard_user.html
@@ -16,7 +16,7 @@
{{ rstate|capitalize }} {{ table_title }} that you can modify
{{ dashboard_table_editable_head }}
{{ dashboard_table_editable }}
- {{ dashboard_table_foot }}}
+ {{ dashboard_table_foot }}
diff --git a/flowapp/templates/pages/machine_api_key.html b/flowapp/templates/pages/machine_api_key.html
new file mode 100644
index 00000000..52eb478a
--- /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.
+
+
+
+ Machine address |
+ ApiKey |
+ Created by |
+ Created for |
+ Expires |
+ Read/Write ? |
+ Action |
+
+ {% for row in keys %}
+
+
+ {{ row.machine }}
+ |
+
+ {{ row.key }}
+ |
+
+ {{ row.user.name }}
+ |
+
+ {{ row.comment }}
+ |
+ {{ row.expires|strftime }}
+ |
+
+ {% if not row.readonly %}
+
+
+ {% endif %}
+ |
+
+
+
+
+ |
+
+ {% endfor %}
+
+
+ Add new Machine ApiKey
+
+{% endblock %}
\ No newline at end of file
diff --git a/flowapp/templates/pages/orgs.html b/flowapp/templates/pages/orgs.html
index 3184160a..9d2b2cb2 100644
--- a/flowapp/templates/pages/orgs.html
+++ b/flowapp/templates/pages/orgs.html
@@ -4,7 +4,7 @@
Name |
- Adress Range |
+ Adress Ranges |
action |
{% for org in orgs %}
@@ -12,7 +12,11 @@
{{ org.name }} |
{% set rows = org.arange.split() %}
- {{ rows|join(" ") }}
+
+ {% for row in rows %}
+ - {{ row }}
+ {% endfor %}
+
|
diff --git a/flowapp/tests/conftest.py b/flowapp/tests/conftest.py
index ed3c378d..1d5436db 100644
--- a/flowapp/tests/conftest.py
+++ b/flowapp/tests/conftest.py
@@ -9,6 +9,7 @@
from flowapp import create_app
from flowapp import db as _db
+from datetime import datetime
import flowapp.models
TESTDB = "test_project.db"
@@ -121,11 +122,6 @@ def db(app, request):
print("#: inserting users")
flowapp.models.insert_users(users)
- model = flowapp.models.ApiKey(machine="127.0.0.1", key="testkey", user_id=1)
-
- _db.session.add(model)
- _db.session.commit()
-
def teardown():
_db.session.commit()
_db.drop_all()
@@ -136,12 +132,54 @@ def teardown():
@pytest.fixture(scope="session")
-def jwt_token(client, db, request):
+def jwt_token(client, app, db, request):
"""
Get the test_client from the app, for the whole test session.
"""
+ mkey = "testkey"
+
+ with app.app_context():
+ model = flowapp.models.ApiKey(machine="127.0.0.1", key=mkey, user_id=1)
+ db.session.add(model)
+ db.session.commit()
+
+ print("\n----- GET JWT TEST TOKEN\n")
+ url = "/api/v3/auth"
+ headers = {"x-api-key": mkey}
+ token = client.get(url, headers=headers)
+ data = json.loads(token.data)
+ return data["token"]
+
+
+@pytest.fixture(scope="session")
+def expired_auth_token(client, app, db, request):
+ """
+ Get the test_client from the app, for the whole test session.
+ """
+ test_key = "expired_test_key"
+ expired_date = datetime.strptime("2019-01-01", "%Y-%m-%d")
+ with app.app_context():
+ model = flowapp.models.ApiKey(machine="127.0.0.1", key=test_key, user_id=1, expires=expired_date)
+ db.session.add(model)
+ db.session.commit()
+
+ return test_key
+
+
+@pytest.fixture(scope="session")
+def readonly_jwt_token(client, app, db, request):
+ """
+ Get the test_client from the app, for the whole test session.
+ """
+ readonly_key = "readonly-testkey"
+ with app.app_context():
+ model = flowapp.models.ApiKey(machine="127.0.0.1", key=readonly_key, user_id=1, readonly=True)
+ db.session.add(model)
+ db.session.commit()
+
print("\n----- GET JWT TEST TOKEN\n")
- url = "/api/v1/auth/testkey"
- token = client.get(url)
+ url = "/api/v3/auth"
+ headers = {"x-api-key": readonly_key}
+ token = client.get(url, headers=headers)
data = json.loads(token.data)
return data["token"]
diff --git a/flowapp/tests/test_api_auth.py b/flowapp/tests/test_api_auth.py
new file mode 100644
index 00000000..5733346b
--- /dev/null
+++ b/flowapp/tests/test_api_auth.py
@@ -0,0 +1,62 @@
+# Test for api authorization
+import json
+
+
+def test_token(client, jwt_token):
+ """
+ test that token authorization works
+ """
+ req = client.get("/api/v3/test_token", headers={"x-access-token": jwt_token})
+
+ assert req.status_code == 200
+
+
+def test_expired_token(client, expired_auth_token):
+ """
+ test that expired token authorization return 401
+ """
+ req = client.get("/api/v3/auth", headers={"x-api-key": expired_auth_token})
+
+ assert req.status_code == 401
+
+
+def test_withnout_token(client):
+ """
+ test that without token authorization return 401
+ """
+ req = client.get("/api/v3/test_token")
+
+ assert req.status_code == 401
+
+
+def test_readonly_token(client, readonly_jwt_token):
+ """
+ test that readonly flag is set correctly
+ """
+ req = client.get("/api/v3/test_token", headers={"x-access-token": readonly_jwt_token})
+
+ assert req.status_code == 200
+ data = json.loads(req.data)
+ assert data['readonly']
+
+
+def test_readonly_token_ipv4_create(client, db, readonly_jwt_token):
+ """
+ test that readonly token can't create ipv4 rule
+ """
+ headers = {"x-access-token": readonly_jwt_token}
+
+ req = client.post(
+ "/api/v3/rules/ipv4",
+ headers=headers,
+ json={
+ "action": 2,
+ "protocol": "tcp",
+ "source": "147.230.17.117",
+ "source_mask": 32,
+ "source_port": "",
+ "expires": "1444913400",
+ },
+ )
+
+ assert req.status_code == 403
diff --git a/flowapp/tests/test_api_deprecated.py b/flowapp/tests/test_api_deprecated.py
new file mode 100644
index 00000000..fca94148
--- /dev/null
+++ b/flowapp/tests/test_api_deprecated.py
@@ -0,0 +1,28 @@
+V_PREFIX = "/api/v1"
+
+
+def test_token(client, jwt_token):
+ """
+ test that token authorization works
+ """
+ req = client.get(f"{V_PREFIX}/test_token", headers={"x-access-token": jwt_token})
+
+ assert req.status_code == 400
+
+
+def test_withnout_token(client):
+ """
+ test that without token authorization return 401
+ """
+ req = client.get(f"{V_PREFIX}/test_token")
+
+ assert req.status_code == 400
+
+
+def test_rules(client, db, jwt_token):
+ """
+ test that there is one ipv4 rule created in the first test
+ """
+ req = client.get(f"{V_PREFIX}/rules", headers={"x-access-token": jwt_token})
+
+ assert req.status_code == 400
diff --git a/flowapp/tests/test_api_v1.py b/flowapp/tests/test_api_v1.py
deleted file mode 100644
index 1fd27b1f..00000000
--- a/flowapp/tests/test_api_v1.py
+++ /dev/null
@@ -1,430 +0,0 @@
-import json
-from flowapp.output import announce_route
-
-
-def test_token(client, jwt_token):
- """
- test that token authorization works
- """
- req = client.get("/api/v1/test_token", headers={"x-access-token": jwt_token})
-
- assert req.status_code == 200
-
-
-def test_withnout_token(client):
- """
- test that without token authorization return 401
- """
- req = client.get("/api/v1/test_token")
-
- assert req.status_code == 401
-
-
-def test_list_actions(client, db, jwt_token):
- """
- test that endpoint returns all action in db
- """
- req = client.get("/api/v1/actions", headers={"x-access-token": jwt_token})
-
- assert req.status_code == 200
- data = json.loads(req.data)
- assert len(data) == 4
-
-
-def test_list_communities(client, db, jwt_token):
- """
- test that endpoint returns all action in db
- """
- req = client.get("/api/v1/communities", headers={"x-access-token": jwt_token})
-
- assert req.status_code == 200
- data = json.loads(req.data)
- assert len(data) == 3
-
-
-def test_create_v4rule(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- """
- req = client.post(
- "/api/v1/rules/ipv4",
- headers={"x-access-token": jwt_token},
- json={
- "action": 2,
- "protocol": "tcp",
- "source": "147.230.17.17",
- "source_mask": 32,
- "source_port": "",
- "expires": "10/15/2050 14:46",
- },
- )
-
- assert req.status_code == 201
- data = json.loads(req.data)
- assert data["rule"]
- assert data["rule"]["id"] == 1
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
-
-
-def test_delete_v4rule(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- that time in the past creates expired rule (state 2)
- and that the rule deletion works as expected
- """
- req = client.post(
- "/api/v1/rules/ipv4",
- headers={"x-access-token": jwt_token},
- json={
- "action": 2,
- "protocol": "tcp",
- "source": "147.230.17.12",
- "source_mask": 32,
- "source_port": "",
- "expires": "10/15/2015 14:46",
- },
- )
-
- assert req.status_code == 201
- data = json.loads(req.data)
- assert data["rule"]["id"] == 2
- assert data["rule"]["rstate"] == "withdrawed rule"
-
- req2 = client.delete(
- "/api/v1/rules/ipv4/{}".format(data["rule"]["id"]),
- headers={"x-access-token": jwt_token},
- )
- assert req2.status_code == 201
-
-
-def test_create_rtbh_rule(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- """
- req = client.post(
- "/api/v1/rules/rtbh",
- headers={"x-access-token": jwt_token},
- json={
- "community": 1,
- "ipv4": "147.230.17.17",
- "ipv4_mask": 32,
- "expires": "10/25/2050 14:46",
- },
- )
- data = json.loads(req.data)
- assert req.status_code == 201
- assert data["rule"]
- assert data["rule"]["id"] == 1
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
-
-
-def test_delete_rtbh_rule(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- """
- req = client.post(
- "/api/v1/rules/rtbh",
- headers={"x-access-token": jwt_token},
- json={
- "community": 2,
- "ipv4": "147.230.17.177",
- "ipv4_mask": 32,
- "expires": "10/25/2050 14:46",
- },
- )
-
- assert req.status_code == 201
- data = json.loads(req.data)
- assert data["rule"]["id"] == 2
- req2 = client.delete(
- "/api/v1/rules/rtbh/{}".format(data["rule"]["id"]),
- headers={"x-access-token": jwt_token},
- )
- assert req2.status_code == 201
-
-
-def test_create_v6rule(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- """
- req = client.post(
- "/api/v1/rules/ipv6",
- headers={"x-access-token": jwt_token},
- json={
- "action": 3,
- "next_header": "tcp",
- "source": "2001:718:1C01:1111::",
- "source_mask": 64,
- "source_port": "",
- "expires": "10/25/2050 14:46",
- },
- )
- data = json.loads(req.data)
- assert req.status_code == 201
- assert data["rule"]
- assert data["rule"]["id"] == "1"
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
-
-
-def test_validation_v4rule(client, db, jwt_token):
- """
- test that creating with invalid data returns 400 and errors
- """
- req = client.post(
- "/api/v1/rules/ipv4",
- headers={"x-access-token": jwt_token},
- json={
- "action": 2,
- "dest": "200.200.200.32",
- "dest_mask": 16,
- "protocol": "tcp",
- "source": "1.1.1.1",
- "source_mask": 32,
- "source_port": "",
- "expires": "10/15/2050 14:46",
- },
- )
-
- assert req.status_code == 400
- data = json.loads(req.data)
- assert len(data["validation_errors"]) > 0
- assert sorted(data["validation_errors"].keys()) == sorted(["dest", "source"])
- assert len(data["validation_errors"]["dest"]) == 2
- assert data["validation_errors"]["dest"][0].startswith("This is not")
- assert data["validation_errors"]["dest"][1].startswith("Source or des")
- assert len(data["validation_errors"]["source"]) == 1
- assert data["validation_errors"]["source"][0].startswith("Source or des")
-
-
-def test_validation_v4rule_fragment(client, db, jwt_token):
- """
- test that creating with invalid fragment values returns 400 and errors
- """
- bad_string = "bad-and-ugly"
- req = client.post(
- "/api/v1/rules/ipv4",
- headers={"x-access-token": jwt_token},
- json={
- "action": 2,
- "protocol": "tcp",
- "source": "147.230.17.12",
- "source_mask": 32,
- "source_port": "",
- "expires": "10/15/2050 14:46",
- "fragment": bad_string,
- },
- )
-
- assert req.status_code == 400
- data = json.loads(req.data)
- assert len(data["validation_errors"]) > 0
- assert "fragment" in data["validation_errors"].keys()
- assert bad_string in data["validation_errors"]["fragment"][0]
-
-
-def test_all_validation_errors(client, db, jwt_token):
- """
- test that creating with invalid data returns 400 and errors
- """
- req = client.post(
- "/api/v1/rules/ipv4", headers={"x-access-token": jwt_token}, json={"action": 2}
- )
- data = json.loads(req.data)
- assert req.status_code == 400
-
-
-def test_validate_v6rule(client, db, jwt_token):
- """
- test that creating with invalid data returns 400 and errors
- """
- req = client.post(
- "/api/v1/rules/ipv6",
- headers={"x-access-token": jwt_token},
- json={
- "action": 32,
- "next_header": "abc",
- "source": "2011:78:1C01:1111::",
- "source_mask": 64,
- "source_port": "",
- "expires": "10/25/2050 14:46",
- },
- )
- data = json.loads(req.data)
- assert req.status_code == 400
- assert len(data["validation_errors"]) > 0
- assert sorted(data["validation_errors"].keys()) == sorted(
- ["action", "next_header", "dest", "source"]
- )
- # assert data['validation_errors'][0].startswith('Error in the Action')
- # assert data['validation_errors'][1].startswith('Error in the Source')
- # assert data['validation_errors'][2].startswith('Error in the Next Header')
-
-
-def test_rules(client, db, jwt_token):
- """
- test that there is one ipv4 rule created in the first test
- """
- req = client.get("/api/v1/rules", headers={"x-access-token": jwt_token})
-
- assert req.status_code == 200
-
- data = json.loads(req.data)
- assert len(data["ipv4_rules"]) == 1
- assert len(data["ipv6_rules"]) == 1
-
-
-def test_timestamp_param(client, db, jwt_token):
- """
- test that url param for time format works as expected
- """
- req = client.get(
- "/api/v1/rules?time_format=timestamp", headers={"x-access-token": jwt_token}
- )
-
- assert req.status_code == 200
-
- data = json.loads(req.data)
- assert data["ipv4_rules"][0]["expires"] == 2549451000
- assert data["ipv6_rules"][0]["expires"] == 2550315000
-
-
-def test_update_existing_v4rule_with_timestamp(client, db, jwt_token):
- """
- test that update with different data passes
- """
- req = client.post(
- "/api/v1/rules/ipv4",
- headers={"x-access-token": jwt_token},
- json={
- "action": 2,
- "protocol": "tcp",
- "source": "147.230.17.17",
- "source_mask": 32,
- "source_port": "",
- "expires": "1444913400",
- },
- )
-
- assert req.status_code == 201
- data = json.loads(req.data)
- assert data["rule"]
- assert data["rule"]["id"] == 1
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
- assert data["rule"]["expires"] == 1444913400
-
-
-def test_create_v4rule_with_timestamp(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- """
- req = client.post(
- "/api/v1/rules/ipv4",
- headers={"x-access-token": jwt_token},
- json={
- "action": 2,
- "protocol": "tcp",
- "source": "147.230.17.117",
- "source_mask": 32,
- "source_port": "",
- "expires": "1444913400",
- },
- )
-
- assert req.status_code == 201
- data = json.loads(req.data)
- assert data["rule"]
- assert data["rule"]["id"] == 2
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
- assert data["rule"]["expires"] == 1444913400
-
-
-def test_update_existing_v6rule_with_timestamp(client, db, jwt_token):
- """
- test that update with different data passes
- """
- req = client.post(
- "/api/v1/rules/ipv6",
- headers={"x-access-token": jwt_token},
- json={
- "action": 3,
- "next_header": "tcp",
- "source": "2001:718:1C01:1111::",
- "source_mask": 64,
- "source_port": "",
- "expires": "1444913400",
- },
- )
- data = json.loads(req.data)
- assert req.status_code == 201
- assert data["rule"]
- assert data["rule"]["id"] == "1"
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
- assert data["rule"]["expires"] == 1444913400
-
-
-def test_create_v6rule_with_timestamp(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- """
- req = client.post(
- "/api/v1/rules/ipv6",
- headers={"x-access-token": jwt_token},
- json={
- "action": 2,
- "next_header": "udp",
- "source": "2001:718:1C01:1111::",
- "source_mask": 64,
- "source_port": "",
- "expires": "1444913400",
- },
- )
- data = json.loads(req.data)
- assert req.status_code == 201
- assert data["rule"]
- assert data["rule"]["id"] == "2"
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
- assert data["rule"]["expires"] == 1444913400
-
-
-def test_update_existing_rtbh_rule_with_timestamp(client, db, jwt_token):
- """
- test that update with different data passes
- """
- req = client.post(
- "/api/v1/rules/rtbh",
- headers={"x-access-token": jwt_token},
- json={
- "community": 1,
- "ipv4": "147.230.17.17",
- "ipv4_mask": 32,
- "expires": "1444913400",
- },
- )
- data = json.loads(req.data)
- assert req.status_code == 201
- assert data["rule"]
- assert data["rule"]["id"] == 1
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
- assert data["rule"]["expires"] == 1444913400
-
-
-def test_create_rtbh_rule_with_timestamp(client, db, jwt_token):
- """
- test that creating with valid data returns 201
- """
- req = client.post(
- "/api/v1/rules/rtbh",
- headers={"x-access-token": jwt_token},
- json={
- "community": 1,
- "ipv4": "147.230.17.117",
- "ipv4_mask": 32,
- "expires": "1444913400",
- },
- )
- data = json.loads(req.data)
- assert req.status_code == 201
- assert data["rule"]
- assert data["rule"]["id"] == 2
- assert data["rule"]["user"] == "jiri.vrany@tul.cz"
- assert data["rule"]["expires"] == 1444913400
diff --git a/flowapp/tests/test_api_v2.py b/flowapp/tests/test_api_v3.py
similarity index 88%
rename from flowapp/tests/test_api_v2.py
rename to flowapp/tests/test_api_v3.py
index 0ffb54f8..75abb73c 100644
--- a/flowapp/tests/test_api_v2.py
+++ b/flowapp/tests/test_api_v3.py
@@ -1,11 +1,13 @@
import json
+V_PREFIX = "/api/v3"
+
def test_token(client, jwt_token):
"""
test that token authorization works
"""
- req = client.get("/api/v2/test_token", headers={"x-access-token": jwt_token})
+ req = client.get(f"{V_PREFIX}/test_token", headers={"x-access-token": jwt_token})
assert req.status_code == 200
@@ -14,7 +16,7 @@ def test_withnout_token(client):
"""
test that without token authorization return 401
"""
- req = client.get("/api/v2/test_token")
+ req = client.get(f"{V_PREFIX}/test_token")
assert req.status_code == 401
@@ -23,7 +25,7 @@ def test_list_actions(client, db, jwt_token):
"""
test that endpoint returns all action in db
"""
- req = client.get("/api/v2/actions", headers={"x-access-token": jwt_token})
+ req = client.get(f"{V_PREFIX}/actions", headers={"x-access-token": jwt_token})
assert req.status_code == 200
data = json.loads(req.data)
@@ -34,7 +36,7 @@ def test_list_communities(client, db, jwt_token):
"""
test that endpoint returns all action in db
"""
- req = client.get("/api/v2/communities", headers={"x-access-token": jwt_token})
+ req = client.get(f"{V_PREFIX}/communities", headers={"x-access-token": jwt_token})
assert req.status_code == 200
data = json.loads(req.data)
@@ -46,7 +48,7 @@ def test_create_v4rule(client, db, jwt_token):
test that creating with valid data returns 201
"""
req = client.post(
- "/api/v2/rules/ipv4",
+ f"{V_PREFIX}/rules/ipv4",
headers={"x-access-token": jwt_token},
json={
"action": 2,
@@ -62,7 +64,7 @@ def test_create_v4rule(client, db, jwt_token):
assert req.status_code == 201
data = json.loads(req.data)
assert data["rule"]
- assert data["rule"]["id"] == 3
+ assert data["rule"]["id"] == 1
assert data["rule"]["user"] == "jiri.vrany@tul.cz"
@@ -73,7 +75,7 @@ def test_delete_v4rule(client, db, jwt_token):
and that the rule deletion works as expected
"""
req = client.post(
- "/api/v2/rules/ipv4",
+ f"{V_PREFIX}/rules/ipv4",
headers={"x-access-token": jwt_token},
json={
"action": 2,
@@ -87,11 +89,11 @@ def test_delete_v4rule(client, db, jwt_token):
assert req.status_code == 201
data = json.loads(req.data)
- assert data["rule"]["id"] == 4
+ assert data["rule"]["id"] == 2
assert data["rule"]["rstate"] == "withdrawed rule"
req2 = client.delete(
- "/api/v2/rules/ipv4/{}".format(data["rule"]["id"]),
+ f'{V_PREFIX}/rules/ipv4/{data["rule"]["id"]}',
headers={"x-access-token": jwt_token},
)
assert req2.status_code == 201
@@ -102,7 +104,7 @@ def test_create_rtbh_rule(client, db, jwt_token):
test that creating with valid data returns 201
"""
req = client.post(
- "/api/v2/rules/rtbh",
+ f"{V_PREFIX}/rules/rtbh",
headers={"x-access-token": jwt_token},
json={
"community": 1,
@@ -123,7 +125,7 @@ def test_delete_rtbh_rule(client, db, jwt_token):
test that creating with valid data returns 201
"""
req = client.post(
- "/api/v2/rules/rtbh",
+ f"{V_PREFIX}/rules/rtbh",
headers={"x-access-token": jwt_token},
json={
"community": 2,
@@ -135,9 +137,9 @@ def test_delete_rtbh_rule(client, db, jwt_token):
assert req.status_code == 201
data = json.loads(req.data)
- assert data["rule"]["id"] == 3
+ assert data["rule"]["id"] == 2
req2 = client.delete(
- "/api/v2/rules/rtbh/{}".format(data["rule"]["id"]),
+ f'{V_PREFIX}/rules/rtbh/{data["rule"]["id"]}',
headers={"x-access-token": jwt_token},
)
assert req2.status_code == 201
@@ -148,7 +150,7 @@ def test_validation_rtbh_rule(client, db, jwt_token):
test that creating with invalid data returns 400 and errors
"""
req = client.post(
- "/api/v2/rules/rtbh",
+ f"{V_PREFIX}/rules/rtbh",
headers={"x-access-token": jwt_token},
json={
"community": 1,
@@ -172,7 +174,7 @@ def test_create_v6rule(client, db, jwt_token):
test that creating with valid data returns 201
"""
req = client.post(
- "/api/v2/rules/ipv6",
+ f"{V_PREFIX}/rules/ipv6",
headers={"x-access-token": jwt_token},
json={
"action": 3,
@@ -195,7 +197,7 @@ def test_validation_v4rule(client, db, jwt_token):
test that creating with invalid data returns 400 and errors
"""
req = client.post(
- "/api/v2/rules/ipv4",
+ f"{V_PREFIX}/rules/ipv4",
headers={"x-access-token": jwt_token},
json={
"action": 2,
@@ -225,7 +227,7 @@ def test_all_validation_errors(client, db, jwt_token):
test that creating with invalid data returns 400 and errors
"""
req = client.post(
- "/api/v2/rules/ipv4", headers={"x-access-token": jwt_token}, json={"action": 2}
+ f"{V_PREFIX}/rules/ipv4", headers={"x-access-token": jwt_token}, json={"action": 2}
)
data = json.loads(req.data)
assert req.status_code == 400
@@ -236,7 +238,7 @@ def test_validate_v6rule(client, db, jwt_token):
test that creating with invalid data returns 400 and errors
"""
req = client.post(
- "/api/v2/rules/ipv6",
+ f"{V_PREFIX}/rules/ipv6",
headers={"x-access-token": jwt_token},
json={
"action": 32,
@@ -262,13 +264,13 @@ def test_rules(client, db, jwt_token):
"""
test that there is one ipv4 rule created in the first test
"""
- req = client.get("/api/v2/rules", headers={"x-access-token": jwt_token})
+ req = client.get(f"{V_PREFIX}/rules", headers={"x-access-token": jwt_token})
assert req.status_code == 200
data = json.loads(req.data)
- assert len(data["flowspec_ipv4_rw"]) == 3
- assert len(data["flowspec_ipv6_rw"]) == 2
+ assert len(data["flowspec_ipv4_rw"]) == 1
+ assert len(data["flowspec_ipv6_rw"]) == 1
def test_timestamp_param(client, db, jwt_token):
@@ -276,7 +278,7 @@ def test_timestamp_param(client, db, jwt_token):
test that url param for time format works as expected
"""
req = client.get(
- "/api/v2/rules?time_format=timestamp", headers={"x-access-token": jwt_token}
+ f"{V_PREFIX}/rules?time_format=timestamp", headers={"x-access-token": jwt_token}
)
assert req.status_code == 200
@@ -291,7 +293,7 @@ def test_update_existing_v4rule_with_timestamp(client, db, jwt_token):
test that update with different data passes
"""
req = client.post(
- "/api/v2/rules/ipv4",
+ f"{V_PREFIX}/rules/ipv4",
headers={"x-access-token": jwt_token},
json={
"action": 2,
@@ -306,7 +308,7 @@ def test_update_existing_v4rule_with_timestamp(client, db, jwt_token):
assert req.status_code == 201
data = json.loads(req.data)
assert data["rule"]
- assert data["rule"]["id"] == 1
+ assert data["rule"]["id"] == 2
assert data["rule"]["user"] == "jiri.vrany@tul.cz"
assert data["rule"]["expires"] == 1444913400
@@ -316,7 +318,7 @@ def test_create_v4rule_with_timestamp(client, db, jwt_token):
test that creating with valid data returns 201
"""
req = client.post(
- "/api/v2/rules/ipv4",
+ f"{V_PREFIX}/rules/ipv4",
headers={"x-access-token": jwt_token},
json={
"action": 2,
@@ -331,7 +333,7 @@ def test_create_v4rule_with_timestamp(client, db, jwt_token):
assert req.status_code == 201
data = json.loads(req.data)
assert data["rule"]
- assert data["rule"]["id"] == 4
+ assert data["rule"]["id"] == 3
assert data["rule"]["user"] == "jiri.vrany@tul.cz"
assert data["rule"]["expires"] == 1444913400
@@ -341,7 +343,7 @@ def test_update_existing_v6rule_with_timestamp(client, db, jwt_token):
test that update with different data passes
"""
req = client.post(
- "/api/v2/rules/ipv6",
+ f"{V_PREFIX}/rules/ipv6",
headers={"x-access-token": jwt_token},
json={
"action": 3,
@@ -365,7 +367,7 @@ def test_create_v6rule_with_timestamp(client, db, jwt_token):
test that creating with valid data returns 201
"""
req = client.post(
- "/api/v2/rules/ipv6",
+ f"{V_PREFIX}/rules/ipv6",
headers={"x-access-token": jwt_token},
json={
"action": 2,
@@ -379,7 +381,7 @@ def test_create_v6rule_with_timestamp(client, db, jwt_token):
data = json.loads(req.data)
assert req.status_code == 201
assert data["rule"]
- assert data["rule"]["id"] == "3"
+ assert data["rule"]["id"] == "2"
assert data["rule"]["user"] == "jiri.vrany@tul.cz"
assert data["rule"]["expires"] == 1444913400
@@ -389,7 +391,7 @@ def test_update_existing_rtbh_rule_with_timestamp(client, db, jwt_token):
test that update with different data passes
"""
req = client.post(
- "/api/v2/rules/rtbh",
+ f"{V_PREFIX}/rules/rtbh",
headers={"x-access-token": jwt_token},
json={
"community": 1,
@@ -411,7 +413,7 @@ def test_create_rtbh_rule_with_timestamp(client, db, jwt_token):
test that creating with valid data returns 201
"""
req = client.post(
- "/api/v2/rules/rtbh",
+ f"{V_PREFIX}/rules/rtbh",
headers={"x-access-token": jwt_token},
json={
"community": 1,
@@ -423,6 +425,6 @@ def test_create_rtbh_rule_with_timestamp(client, db, jwt_token):
data = json.loads(req.data)
assert req.status_code == 201
assert data["rule"]
- assert data["rule"]["id"] == 3
+ assert data["rule"]["id"] == 2
assert data["rule"]["user"] == "jiri.vrany@tul.cz"
assert data["rule"]["expires"] == 1444913400
diff --git a/flowapp/tests/test_forms.py b/flowapp/tests/test_forms.py
index 4418eb6c..481354c9 100644
--- a/flowapp/tests/test_forms.py
+++ b/flowapp/tests/test_forms.py
@@ -4,6 +4,7 @@
@pytest.fixture()
def ip_form(field_class):
+
form = flowapp.forms.IPForm()
form.source = field_class()
form.dest = field_class()
diff --git a/flowapp/views/admin.py b/flowapp/views/admin.py
index 4b8cc66b..b90daa40 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,17 +44,78 @@ 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
def user():
form = UserForm(request.form)
- form.role_ids.choices = [
- (g.id, g.name) for g in db.session.query(Role).order_by("name")
- ]
- form.org_ids.choices = [
- (g.id, g.name) for g in db.session.query(Organization).order_by("name")
- ]
+ form.role_ids.choices = [(g.id, g.name) for g in db.session.query(Role).order_by("name")]
+ form.org_ids.choices = [(g.id, g.name) for g in db.session.query(Organization).order_by("name")]
if request.method == "POST" and form.validate():
# test if user is unique
@@ -87,12 +150,8 @@ def user():
def edit_user(user_id):
user = db.session.query(User).get(user_id)
form = UserForm(request.form, obj=user)
- form.role_ids.choices = [
- (g.id, g.name) for g in db.session.query(Role).order_by("name")
- ]
- form.org_ids.choices = [
- (g.id, g.name) for g in db.session.query(Organization).order_by("name")
- ]
+ form.role_ids.choices = [(g.id, g.name) for g in db.session.query(Role).order_by("name")]
+ form.org_ids.choices = [(g.id, g.name) for g in db.session.query(Organization).order_by("name")]
if request.method == "POST" and form.validate():
user.update(form)
@@ -163,9 +222,7 @@ def organization():
flash("Organization saved")
return redirect(url_for("admin.organizations"))
else:
- flash(
- "Organization {} already exists".format(form.name.data), "alert-danger"
- )
+ flash("Organization {} already exists".format(form.name.data), "alert-danger")
action_url = url_for("admin.organization")
return render_template(
@@ -321,9 +378,7 @@ def action():
return redirect(url_for("admin.actions"))
else:
flash(
- "Action with name {} or command {} already exists".format(
- form.name.data, form.command.data
- ),
+ "Action with name {} or command {} already exists".format(form.name.data, form.command.data),
"alert-danger",
)
@@ -410,9 +465,7 @@ def community():
flash("Community saved", "alert-success")
return redirect(url_for("admin.communities"))
else:
- flash(
- f"Community with name {form.name.data} already exists", "alert-danger"
- )
+ flash(f"Community with name {form.name.data} already exists", "alert-danger")
community_url = url_for("admin.community")
return render_template(
@@ -457,9 +510,7 @@ def delete_community(community_id):
try:
db.session.commit()
except IntegrityError:
- message = "Community {} is in use in some rules, can not be deleted!".format(
- aname
- )
+ message = "Community {} is in use in some rules, can not be deleted!".format(aname)
alert_type = "alert-danger"
flash(message, alert_type)
diff --git a/flowapp/views/api_common.py b/flowapp/views/api_common.py
index 515e0c90..163449ce 100644
--- a/flowapp/views/api_common.py
+++ b/flowapp/views/api_common.py
@@ -11,6 +11,7 @@
Flowspec4,
Flowspec6,
ApiKey,
+ MachineApiKey,
Community,
get_user_nets,
get_user_actions,
@@ -59,7 +60,7 @@ def decorated(*args, **kwargs):
except jwt.ExpiredSignatureError:
return jsonify({"message": "auth token expired"}), 401
- return f(current_user, *args, **kwargs)
+ return f(current_user=current_user, *args, **kwargs)
return decorated
@@ -71,7 +72,20 @@ def authorize(user_key):
"""
jwt_key = current_app.config.get("JWT_SECRET")
+ # try normal user key first
model = db.session.query(ApiKey).filter_by(key=user_key).first()
+ # if not found try machine key
+ if not model:
+ model = db.session.query(MachineApiKey).filter_by(key=user_key).first()
+ # if key is not found return 403
+ if not model:
+ return jsonify({"message": "auth token is invalid"}), 403
+
+ # check if the key is not expired
+ if model.is_expired():
+ return jsonify({"message": "auth token is expired"}), 401
+
+ # check if the key is not used by different machine
if model and ipaddress.ip_address(model.machine) == ipaddress.ip_address(
request.remote_addr
):
@@ -79,6 +93,7 @@ def authorize(user_key):
"user": {
"uuid": model.user.uuid,
"id": model.user.id,
+ "readonly": model.readonly,
"roles": [role.name for role in model.user.role.all()],
"org": [org.name for org in model.user.organization.all()],
"role_ids": [role.id for role in model.user.role.all()],
@@ -94,6 +109,26 @@ def authorize(user_key):
return jsonify({"message": "auth token is invalid"}), 403
+def check_readonly(func):
+ """
+ Check if the token is readonly
+ Used in api endpoints
+ """
+ @wraps(func)
+ def decorated_function(*args, **kwargs):
+ # Access read only flag from first of the args
+ print("ARGS", args)
+ print("KWARGS", kwargs)
+ current_user = kwargs.get("current_user", False)
+ read_only = current_user.get("readonly", False)
+ if read_only:
+ return jsonify({"message": "read only token can't perform this action"}), 403
+ return func(*args, **kwargs)
+ return decorated_function
+
+
+# endpints
+
def index(current_user, key_map):
prefered_tf = (
request.args.get(TIME_FORMAT_ARG) if request.args.get(TIME_FORMAT_ARG) else ""
@@ -455,7 +490,7 @@ def delete_rule(current_user, rule_id, model_name, route_model, rule_type):
:param route_model:
:return:
"""
- model = db.session.query(model_name).get(rule_id)
+ model = db.session.get(model_name, rule_id)
if model:
if check_access_rights(current_user, model.user_id):
# withdraw route
@@ -486,10 +521,12 @@ def token_test_get(current_user):
:param rule_id:
:return:
"""
- return (
- jsonify({"message": "token works as expected", "uuid": current_user["uuid"]}),
- 200,
- )
+ my_response = {
+ "message": "token works as expected",
+ "uuid": current_user["uuid"],
+ "readonly": current_user["readonly"],
+ }
+ return jsonify(my_response), 200
def get_form_errors(form):
diff --git a/flowapp/views/api_keys.py b/flowapp/views/api_keys.py
index a40c1ec5..45cc276b 100644
--- a/flowapp/views/api_keys.py
+++ b/flowapp/views/api_keys.py
@@ -57,7 +57,12 @@ def add():
if request.method == "POST" and form.validate():
model = ApiKey(
- machine=form.machine.data, key=form.key.data, user_id=session["user_id"]
+ 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)
diff --git a/flowapp/views/api_v1.py b/flowapp/views/api_v1.py
index 51e4cdc9..1dab2c3d 100644
--- a/flowapp/views/api_v1.py
+++ b/flowapp/views/api_v1.py
@@ -1,156 +1,12 @@
-from flask import Blueprint
-from flowapp import csrf
-from flowapp.views import api_common
+from flask import Blueprint, jsonify
-api = Blueprint("api_v1", __name__, template_folder="templates")
-
-
-@api.route("/auth/", methods=["GET"])
-def authorize(user_key):
- return api_common.authorize(user_key)
-
-
-@api.route("/rules")
-@api_common.token_required
-def index(current_user):
- key_map = {
- "ipv4_rules": "ipv4_rules",
- "ipv6_rules": "ipv6_rules",
- "rtbh_rules": "rtbh_rules",
- "ipv4_rules_readonly": "ipv4_rules_readonly",
- "ipv6_rules_readonly": "ipv6_rules_readonly",
- "rtbh_rules_readonly": "rtbh_rules_readonly",
- }
- return api_common.index(current_user, key_map)
-
-
-@api.route("/actions")
-@api_common.token_required
-def all_actions(current_user):
- """
- Returns Actions allowed for current user
- :param current_user:
- :return: json response
- """
- return api_common.all_actions(current_user)
-
-
-@api.route("/communities")
-@api_common.token_required
-def all_communities(current_user):
- """
- Returns RTHB communites allowed for current user
- :param current_user:
- :return: json response
- """
- return api_common.all_communities(current_user)
-
-
-@api.route("/rules/ipv4", methods=["POST"])
-@api_common.token_required
-def create_ipv4(current_user):
- """
- Api method for new IPv4 rule
- :param data: parsed json request
- :param current_user: data from jwt token
- :return: json response
- """
- return api_common.create_ipv4(current_user)
-
-
-@api.route("/rules/ipv6", methods=["POST"])
-@csrf.exempt
-@api_common.token_required
-def create_ipv6(current_user):
- """
- Create new IPv6 rule
- :param data: parsed json request
- :param current_user: data from jwt token
- :return:
- """
- return api_common.create_ipv6(current_user)
-
-
-@api.route("/rules/rtbh", methods=["POST"])
-@csrf.exempt
-@api_common.token_required
-def create_rtbh(current_user):
- return api_common.create_rtbh(current_user)
-
-@api.route("/rules/ipv4/", methods=["GET"])
-@api_common.token_required
-def ipv4_rule_get(current_user, rule_id):
- """
- Return IPv4 rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.ipv4_rule_get(current_user)
-
-
-@api.route("/rules/ipv6/", methods=["GET"])
-@api_common.token_required
-def ipv6_rule_get(current_user, rule_id):
- """
- Return IPv6 rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.ipv6_rule_get(current_user)
-
-
-@api.route("/rules/rtbh/", methods=["GET"])
-@api_common.token_required
-def rtbh_rule_get(current_user, rule_id):
- """
- Return RTBH rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.rtbh_rule_get(current_user)
-
-
-@api.route("/rules/ipv4/", methods=["DELETE"])
-@api_common.token_required
-def delete_v4_rule(current_user, rule_id):
- """
- Delete rule with given id and type
- :param rule_id: integer - rule id
- """
- return api_common.delete_v4_rule(current_user, rule_id)
-
-
-@api.route("/rules/ipv6/", methods=["DELETE"])
-@api_common.token_required
-def delete_v6_rule(current_user, rule_id):
- """
- Delete rule with given id and type
- :param rule_id: integer - rule id
- """
- return api_common.delete_v6_rule(current_user, rule_id)
-
-
-@api.route("/rules/rtbh/", methods=["DELETE"])
-@api_common.token_required
-def delete_rtbh_rule(current_user, rule_id):
- """
- Delete rule with given id and type
- :param rule_id: integer - rule id
- """
- return api_common.delete_rtbh_rule(current_user, rule_id)
-
-
-@api.route("/test_token", methods=["GET"])
-@api_common.token_required
-def token_test_get(current_user):
- """
- Return IPv4 rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.token_test_get(current_user)
+api = Blueprint("api_v1", __name__, template_folder="templates")
+METHODS = ["GET", "POST", "PUT", "DELETE"]
+
+@api.route("/", defaults={"path": ""}, methods=METHODS)
+@api.route("/", methods=METHODS)
+def deprecated_warning(path):
+ """Catch all routes and return a deprecated warning message."""
+ message = "Warning: This API version is deprecated. Please use /api/v3/ instead."
+ return jsonify({"message": message}), 400
diff --git a/flowapp/views/api_v2.py b/flowapp/views/api_v2.py
index 561ba36e..12d99b21 100644
--- a/flowapp/views/api_v2.py
+++ b/flowapp/views/api_v2.py
@@ -1,156 +1,12 @@
-from flask import Blueprint
-from flowapp import csrf
-from flowapp.views import api_common
+from flask import Blueprint, jsonify
-api = Blueprint("api_v2", __name__, template_folder="templates")
-
-
-@api.route("/auth/", methods=["GET"])
-def authorize(user_key):
- return api_common.authorize(user_key)
-
-
-@api.route("/rules")
-@api_common.token_required
-def index(current_user):
- key_map = {
- "ipv4_rules": "flowspec_ipv4_rw",
- "ipv6_rules": "flowspec_ipv6_rw",
- "rtbh_rules": "rtbh_any_rw",
- "ipv4_rules_readonly": "flowspec_ipv4_ro",
- "ipv6_rules_readonly": "flowspec_ipv6_ro",
- "rtbh_rules_readonly": "rtbh_any_ro",
- }
- return api_common.index(current_user, key_map)
-
-
-@api.route("/actions")
-@api_common.token_required
-def all_actions(current_user):
- """
- Returns Actions allowed for current user
- :param current_user:
- :return: json response
- """
- return api_common.all_actions(current_user)
-
-
-@api.route("/communities")
-@api_common.token_required
-def all_communities(current_user):
- """
- Returns RTHB communites allowed for current user
- :param current_user:
- :return: json response
- """
- return api_common.all_communities(current_user)
-
-
-@api.route("/rules/ipv4", methods=["POST"])
-@api_common.token_required
-def create_ipv4(current_user):
- """
- Api method for new IPv4 rule
- :param data: parsed json request
- :param current_user: data from jwt token
- :return: json response
- """
- return api_common.create_ipv4(current_user)
-
-
-@api.route("/rules/ipv6", methods=["POST"])
-@csrf.exempt
-@api_common.token_required
-def create_ipv6(current_user):
- """
- Create new IPv6 rule
- :param data: parsed json request
- :param current_user: data from jwt token
- :return:
- """
- return api_common.create_ipv6(current_user)
-
-
-@api.route("/rules/rtbh", methods=["POST"])
-@csrf.exempt
-@api_common.token_required
-def create_rtbh(current_user):
- return api_common.create_rtbh(current_user)
-
-@api.route("/rules/ipv4/", methods=["GET"])
-@api_common.token_required
-def ipv4_rule_get(current_user, rule_id):
- """
- Return IPv4 rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.ipv4_rule_get(current_user)
-
-
-@api.route("/rules/ipv6/", methods=["GET"])
-@api_common.token_required
-def ipv6_rule_get(current_user, rule_id):
- """
- Return IPv6 rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.ipv6_rule_get(current_user)
-
-
-@api.route("/rules/rtbh/", methods=["GET"])
-@api_common.token_required
-def rtbh_rule_get(current_user, rule_id):
- """
- Return RTBH rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.rtbh_rule_get(current_user)
-
-
-@api.route("/rules/ipv4/", methods=["DELETE"])
-@api_common.token_required
-def delete_v4_rule(current_user, rule_id):
- """
- Delete rule with given id and type
- :param rule_id: integer - rule id
- """
- return api_common.delete_v4_rule(current_user, rule_id)
-
-
-@api.route("/rules/ipv6/", methods=["DELETE"])
-@api_common.token_required
-def delete_v6_rule(current_user, rule_id):
- """
- Delete rule with given id and type
- :param rule_id: integer - rule id
- """
- return api_common.delete_v6_rule(current_user, rule_id)
-
-
-@api.route("/rules/rtbh/", methods=["DELETE"])
-@api_common.token_required
-def delete_rtbh_rule(current_user, rule_id):
- """
- Delete rule with given id and type
- :param rule_id: integer - rule id
- """
- return api_common.delete_rtbh_rule(current_user, rule_id)
-
-
-@api.route("/test_token", methods=["GET"])
-@api_common.token_required
-def token_test_get(current_user):
- """
- Return IPv4 rule
- :param current_user:
- :param rule_id:
- :return:
- """
- return api_common.token_test_get(current_user)
+api = Blueprint("api_v2", __name__, template_folder="templates")
+METHODS = ["GET", "POST", "PUT", "DELETE"]
+
+@api.route("/", defaults={"path": ""}, methods=METHODS)
+@api.route("/", methods=METHODS)
+def deprecated_warning(path):
+ """Catch all routes and return a deprecated warning message."""
+ message = "Warning: This API version is deprecated. Please use /api/v3/ instead."
+ return jsonify({"message": message}), 400
diff --git a/flowapp/views/api_v3.py b/flowapp/views/api_v3.py
index 094b2a52..8f7b2cb2 100644
--- a/flowapp/views/api_v3.py
+++ b/flowapp/views/api_v3.py
@@ -49,6 +49,7 @@ def all_communities(current_user):
@api.route("/rules/ipv4", methods=["POST"])
@api_common.token_required
+@api_common.check_readonly
def create_ipv4(current_user):
"""
Api method for new IPv4 rule
@@ -62,6 +63,7 @@ def create_ipv4(current_user):
@api.route("/rules/ipv6", methods=["POST"])
@csrf.exempt
@api_common.token_required
+@api_common.check_readonly
def create_ipv6(current_user):
"""
Create new IPv6 rule
@@ -75,6 +77,7 @@ def create_ipv6(current_user):
@api.route("/rules/rtbh", methods=["POST"])
@csrf.exempt
@api_common.token_required
+@api_common.check_readonly
def create_rtbh(current_user):
return api_common.create_rtbh(current_user)
@@ -88,7 +91,7 @@ def ipv4_rule_get(current_user, rule_id):
:param rule_id:
:return:
"""
- return api_common.ipv4_rule_get(current_user)
+ return api_common.ipv4_rule_get(current_user, rule_id)
@api.route("/rules/ipv6/", methods=["GET"])
@@ -100,7 +103,7 @@ def ipv6_rule_get(current_user, rule_id):
:param rule_id:
:return:
"""
- return api_common.ipv6_rule_get(current_user)
+ return api_common.ipv6_rule_get(current_user, rule_id)
@api.route("/rules/rtbh/", methods=["GET"])
@@ -112,11 +115,12 @@ def rtbh_rule_get(current_user, rule_id):
:param rule_id:
:return:
"""
- return api_common.rtbh_rule_get(current_user)
+ return api_common.rtbh_rule_get(current_user, rule_id)
@api.route("/rules/ipv4/", methods=["DELETE"])
@api_common.token_required
+@api_common.check_readonly
def delete_v4_rule(current_user, rule_id):
"""
Delete rule with given id and type
@@ -127,6 +131,7 @@ def delete_v4_rule(current_user, rule_id):
@api.route("/rules/ipv6/", methods=["DELETE"])
@api_common.token_required
+@api_common.check_readonly
def delete_v6_rule(current_user, rule_id):
"""
Delete rule with given id and type
@@ -137,6 +142,7 @@ def delete_v6_rule(current_user, rule_id):
@api.route("/rules/rtbh/", methods=["DELETE"])
@api_common.token_required
+@api_common.check_readonly
def delete_rtbh_rule(current_user, rule_id):
"""
Delete rule with given id and type
diff --git a/flowapp/views/rules.py b/flowapp/views/rules.py
index b2e29565..714f6221 100644
--- a/flowapp/views/rules.py
+++ b/flowapp/views/rules.py
@@ -73,9 +73,7 @@ def reactivate_rule(rule_type, rule_id):
form.net_ranges = get_user_nets(session["user_id"])
if rule_type > 2:
- form.action.choices = [
- (g.id, g.name) for g in db.session.query(Action).order_by("name")
- ]
+ form.action.choices = [(g.id, g.name) for g in db.session.query(Action).order_by("name")]
form.action.data = model.action_id
if rule_type == 1:
@@ -256,9 +254,7 @@ def group_delete():
"{} / {}".format(session["user_email"], session["user_orgs"]),
)
- db.session.query(model_name).filter(model_name.id.in_(to_delete)).delete(
- synchronize_session=False
- )
+ db.session.query(model_name).filter(model_name.id.in_(to_delete)).delete(synchronize_session=False)
db.session.commit()
flash("Rules {} deleted".format(to_delete), "alert-success")
@@ -309,9 +305,7 @@ def group_update():
form = form_name(request.form)
form.net_ranges = get_user_nets(session["user_id"])
if rule_type_int > 2:
- form.action.choices = [
- (g.id, g.name) for g in db.session.query(Action).order_by("name")
- ]
+ form.action.choices = [(g.id, g.name) for g in db.session.query(Action).order_by("name")]
if rule_type_int == 1:
form.community.choices = get_user_communities(session["user_role_ids"])
@@ -429,9 +423,7 @@ def ipv4_rule():
if model:
model.expires = round_to_ten_minutes(form.expires.data)
- flash_message = (
- "Existing IPv4 Rule found. Expiration time was updated to new value."
- )
+ flash_message = "Existing IPv4 Rule found. Expiration time was updated to new value."
else:
model = Flowspec4(
source=form.source.data,
@@ -473,17 +465,12 @@ def ipv4_rule():
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)
- )
+ print("Error in the %s field - %s" % (getattr(form, field).label.text, error))
default_expires = datetime.now() + timedelta(days=7)
form.expires.data = default_expires
- return render_template(
- "forms/ipv4_rule.html", form=form, action_url=url_for("rules.ipv4_rule")
- )
+ return render_template("forms/ipv4_rule.html", form=form, action_url=url_for("rules.ipv4_rule"))
@rules.route("/add_ipv6_rule", methods=["GET", "POST"])
@@ -507,9 +494,7 @@ def ipv6_rule():
if model:
model.expires = round_to_ten_minutes(form.expires.data)
- flash_message = (
- "Existing IPv4 Rule found. Expiration time was updated to new value."
- )
+ flash_message = "Existing IPv4 Rule found. Expiration time was updated to new value."
else:
model = Flowspec6(
source=form.source.data,
@@ -550,17 +535,12 @@ def ipv6_rule():
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)
- )
+ print("Error in the %s field - %s" % (getattr(form, field).label.text, error))
default_expires = datetime.now() + timedelta(days=7)
form.expires.data = default_expires
- return render_template(
- "forms/ipv6_rule.html", form=form, action_url=url_for("rules.ipv6_rule")
- )
+ return render_template("forms/ipv6_rule.html", form=form, action_url=url_for("rules.ipv6_rule"))
@rules.route("/add_rtbh_rule", methods=["GET", "POST"])
@@ -586,9 +566,7 @@ def rtbh_rule():
if model:
model.expires = round_to_ten_minutes(form.expires.data)
- flash_message = (
- "Existing RTBH Rule found. Expiration time was updated to new value."
- )
+ flash_message = "Existing RTBH Rule found. Expiration time was updated to new value."
else:
model = RTBH(
ipv4=form.ipv4.data,
@@ -622,17 +600,12 @@ def rtbh_rule():
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)
- )
+ print("Error in the %s field - %s" % (getattr(form, field).label.text, error))
default_expires = datetime.now() + timedelta(days=7)
form.expires.data = default_expires
- return render_template(
- "forms/rtbh_rule.html", form=form, action_url=url_for("rules.rtbh_rule")
- )
+ return render_template("forms/rtbh_rule.html", form=form, action_url=url_for("rules.rtbh_rule"))
@rules.route("/export")
diff --git a/migrations/versions/4af5ae4bae1c_.py b/migrations/versions/4af5ae4bae1c_.py
new file mode 100644
index 00000000..15017fb6
--- /dev/null
+++ b/migrations/versions/4af5ae4bae1c_.py
@@ -0,0 +1,38 @@
+"""empty message
+
+Revision ID: 4af5ae4bae1c
+Revises: 67bb6c1b3898
+Create Date: 2024-03-27 18:19:35.721215
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '4af5ae4bae1c'
+down_revision = '67bb6c1b3898'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('api_key', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('comment', sa.String(length=255), nullable=True))
+
+ with op.batch_alter_table('machine_api_key', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('readonly', sa.Boolean(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('machine_api_key', schema=None) as batch_op:
+ batch_op.drop_column('readonly')
+
+ with op.batch_alter_table('api_key', schema=None) as batch_op:
+ batch_op.drop_column('comment')
+
+ # ### end Alembic commands ###
diff --git a/migrations/versions/67bb6c1b3898_.py b/migrations/versions/67bb6c1b3898_.py
new file mode 100644
index 00000000..ec0d3e08
--- /dev/null
+++ b/migrations/versions/67bb6c1b3898_.py
@@ -0,0 +1,45 @@
+"""empty message
+
+Revision ID: 67bb6c1b3898
+Revises: 2bd0e800ab1c
+Create Date: 2024-03-27 18:13:10.688958
+
+"""
+from alembic import op
+import sqlalchemy as sa
+
+
+# revision identifiers, used by Alembic.
+revision = '67bb6c1b3898'
+down_revision = '2bd0e800ab1c'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ op.create_table('machine_api_key',
+ sa.Column('id', sa.Integer(), nullable=False),
+ sa.Column('machine', sa.String(length=255), nullable=True),
+ sa.Column('key', sa.String(length=255), nullable=True),
+ sa.Column('expires', sa.DateTime(), nullable=True),
+ sa.Column('comment', sa.String(length=255), nullable=True),
+ sa.Column('user_id', sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(['user_id'], ['user.id'], ),
+ sa.PrimaryKeyConstraint('id')
+ )
+ with op.batch_alter_table('api_key', schema=None) as batch_op:
+ batch_op.add_column(sa.Column('readonly', sa.Boolean(), nullable=True))
+ batch_op.add_column(sa.Column('expires', sa.DateTime(), nullable=True))
+
+ # ### end Alembic commands ###
+
+
+def downgrade():
+ # ### commands auto generated by Alembic - please adjust! ###
+ with op.batch_alter_table('api_key', schema=None) as batch_op:
+ batch_op.drop_column('expires')
+ batch_op.drop_column('readonly')
+
+ op.drop_table('machine_api_key')
+ # ### end Alembic commands ###
diff --git a/requirements.txt b/requirements.txt
index c929dbd7..876a5c61 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,14 +1,15 @@
-Flask>=2.0.2
+Flask<3
Flask-SQLAlchemy>=2.2
Flask-SSO>=0.4.0
Flask-WTF>=1.0.0
Flask-Migrate>=3.0.0
Flask-Script>=2.0.0
+Flask-Session
PyJWT>=2.4.0
PyMySQL>=1.0.0
pytest>=7.0.0
requests>=2.20.0
babel>=2.7.0
-mysqlclient>=2.0.0
email_validator>=1.1
pika>=1.3.0
+mysqlclient>=2.0.0
diff --git a/run.example.py b/run.example.py
index 2911b5bd..e19c804e 100644
--- a/run.example.py
+++ b/run.example.py
@@ -2,40 +2,42 @@
This is an example of how to run the application.
First copy the file as run.py (or whatever you want)
Then edit the file to match your needs.
+
+From version 0.8.1 the 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.
+
In general you should not need to edit this example file.
Only if you want to configure the application main menu and
-dashboard. Or in case that you want to add extensions etc.
+dashboard.
+
+Or in case that you want to add extensions etc.
"""
from os import environ
-from flowapp import create_app, db
+from flowapp import create_app, db, sess
import config
-# Call app factory
-app = create_app()
-
# Configurations
-env = environ.get('EXAFS_ENV', 'Production')
+env = environ.get("EXAFS_ENV", "Production")
-if env == 'devel':
- app.config.from_object(config.DevelopmentConfig)
- app.config.update(
- DEVEL=True
- )
+# Call app factory
+if env == "devel":
+ app = create_app(config.DevelopmentConfig)
else:
- app.config.from_object(config.ProductionConfig)
- app.config.update(
- SESSION_COOKIE_SECURE=True,
- SESSION_COOKIE_HTTPONLY=True,
- SESSION_COOKIE_SAMESITE='Lax',
- DEVEL=False
- )
+ app = create_app(config.ProductionConfig)
# init database object
db.init_app(app)
+# init session
+app.config.update(SESSION_TYPE="sqlalchemy")
+app.config.update(SESSION_SQLALCHEMY=db)
+sess.init_app(app)
+
+
# run app
-if __name__ == '__main__':
- app.run(host='::', port=8080, debug=True)
+if __name__ == "__main__":
+ app.run(host="127.0.0.1", port=8000, debug=True)
|