diff --git a/README.md b/README.md index e6c50587..ba8b1f78 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/flowapp/__about__.py b/flowapp/__about__.py index be0f7d4e..59b7f434 100755 --- a/flowapp/__about__.py +++ b/flowapp/__about__.py @@ -1 +1 @@ -__version__ = "0.7.3" +__version__ = "0.8.0" diff --git a/flowapp/__init__.py b/flowapp/__init__.py index a43d6a6b..f92aeeae 100644 --- a/flowapp/__init__.py +++ b/flowapp/__init__.py @@ -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): diff --git a/flowapp/forms.py b/flowapp/forms.py index 16bdc0f7..ae83a1e2 100644 --- a/flowapp/forms.py +++ b/flowapp/forms.py @@ -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) @@ -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") diff --git a/flowapp/instance_config.py b/flowapp/instance_config.py index 3c8bafbc..9d5a1bfa 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/models.py b/flowapp/models.py index 03c1963e..c274e932 100644 --- a/flowapp/models.py +++ b/flowapp/models.py @@ -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( @@ -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) diff --git a/flowapp/templates/forms/api_key.html b/flowapp/templates/forms/api_key.html index 9d8901cb..0500a39c 100644 --- a/flowapp/templates/forms/api_key.html +++ b/flowapp/templates/forms/api_key.html @@ -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 %}

Add new ApiKey for your machine

+ +
+ +
+
ApiKey: {{ generated_key }}
+
+
{{ form.hidden_tag() if form.hidden_tag }}
-
+
{{ render_field(form.machine) }}
-
- -
-
- ApiKey for this machine: +
+ {{ render_checkbox_field(form.readonly) }}
-
- {{ generated_key }} +
+ {{ render_field(form.expires) }}
+
+
- +
+
+ {{ render_field(form.comment) }} +
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 %} +

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

+ + + + + + + + + + + {% 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/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..f142a4ae 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 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..10c7fcf0 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) @@ -117,6 +120,7 @@ def 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/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 ###