diff --git a/nftables-api.py b/nftables-api.py index b0e80c2..5e69c6f 100644 --- a/nftables-api.py +++ b/nftables-api.py @@ -1,4 +1,15 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + from waitress import serve + from nftables_api.app import app if __name__ == '__main__': diff --git a/nftables_api/__init__.py b/nftables_api/__init__.py index e69de29..104f172 100644 --- a/nftables_api/__init__.py +++ b/nftables_api/__init__.py @@ -0,0 +1,9 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" diff --git a/nftables_api/app.py b/nftables_api/app.py index fc8be70..8fd9649 100644 --- a/nftables_api/app.py +++ b/nftables_api/app.py @@ -1,14 +1,25 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + from falcon import App -from .resources import nft_set -from .config import config + from nftables_api.middlewares.authentication import AuthMiddleWare +from .resources import nft_set + app = App( middleware=[ AuthMiddleWare(), - ] + ], ) -rSet = nft_set.SetResource() +rset = nft_set.SetResource() -app.add_route('/set/{xfamily}/{xtable}/{xset}', rSet) +app.add_route('/set/{xfamily}/{xtable}/{xset}', rset) diff --git a/nftables_api/config.py b/nftables_api/config.py index 21c8028..380a485 100644 --- a/nftables_api/config.py +++ b/nftables_api/config.py @@ -1,6 +1,17 @@ -from yaml import safe_load +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + from os import getenv +from yaml import safe_load + configpath = getenv('NFT-API-CONFIG') if not configpath: raise RuntimeError('NFT-API-CONFIG is not set') diff --git a/nftables_api/middlewares/__init__.py b/nftables_api/middlewares/__init__.py new file mode 100644 index 0000000..104f172 --- /dev/null +++ b/nftables_api/middlewares/__init__.py @@ -0,0 +1,9 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" diff --git a/nftables_api/middlewares/authentication.py b/nftables_api/middlewares/authentication.py index e8e61a6..b7a9147 100644 --- a/nftables_api/middlewares/authentication.py +++ b/nftables_api/middlewares/authentication.py @@ -1,7 +1,19 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + from bcrypt import checkpw from falcon import HTTPUnauthorized + from nftables_api.config import config + class AuthMiddleWare: def _match(self, token_plain, token_hashed): """ @@ -23,7 +35,7 @@ class AuthMiddleWare: return False - def process_request(self, req, resp): + def process_request(self, req, resp): # noqa ARG002, resp is not used but needs to be passed by Falcon """ Rudimentary token validation - check if it is worth walking further down the authorization chain """ @@ -40,7 +52,7 @@ class AuthMiddleWare: ) - def process_resource(self, req, resp, resource, params): + def process_resource(self, req, resp, resource, params): # noqa ARG002, resp is not used but needs to be passed by Falcon """ Fully validate whether a token is authorized to perform the request """ @@ -51,13 +63,15 @@ class AuthMiddleWare: if not self._match(token, config_token): continue - for config_path, methods in config_paths.items(): + for got_config_path, methods in config_paths.items(): if not isinstance(methods, list): - raise RuntimeError(f'Invalid method configured for path {config_path}') + raise RuntimeError(f'Invalid method configured for path {got_config_path}') # a leading slash causes an empty first list entry in the split - if config_path.startswith('/'): - config_path = config_path[1:] + if got_config_path.startswith('/'): + config_path = got_config_path[1:] + else: + config_path = got_config_path path_elements = config_path.split('/') @@ -76,13 +90,13 @@ class AuthMiddleWare: break else: - if req.method in methods: - return - else: + if req.method not in methods: raise HTTPUnauthorized( title='Unauthorized method for path', ) + return + raise HTTPUnauthorized( title='Unauthorized', ) diff --git a/nftables_api/resources/__init__.py b/nftables_api/resources/__init__.py new file mode 100644 index 0000000..104f172 --- /dev/null +++ b/nftables_api/resources/__init__.py @@ -0,0 +1,9 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" diff --git a/nftables_api/resources/nft_set.py b/nftables_api/resources/nft_set.py index b5d1295..9087a72 100644 --- a/nftables_api/resources/nft_set.py +++ b/nftables_api/resources/nft_set.py @@ -1,8 +1,20 @@ -import falcon +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + import json + +import falcon from nftables import Nftables -from nftables_api.utils.parse import parse_nft_response + from nftables_api.utils.output import output_post +from nftables_api.utils.parse import parse_nft_response nft = Nftables() nft.set_json_output(True) @@ -47,7 +59,7 @@ class SetResource: response.text = json.dumps({'status': status, 'error': err_parsed}) - def on_post(self, request, response, xfamily, xtable, xset): + def on_post(self, request, response, xfamily, xtable, xset): # noqa PLR0912, todo: consider moving some of the logic to smaller functions raw = request.get_param_as_bool('raw', default=False) data = request.get_media() @@ -73,7 +85,7 @@ class SetResource: 'prefix': { 'addr': addrsplit[0], 'len': int(addrsplit[1]), - } + }, }) else: @@ -88,19 +100,19 @@ class SetResource: 'family': xfamily, 'name': xset, 'table': xtable, - } - } - } - ] + }, + }, + }, + ], } if not nft.json_validate(nft_payload): response.status = falcon.HTTP_BAD_REQUEST - response.text = output_post(False, 'Payload did not validate.') + response.text = output_post(status=False, message='Payload did not validate.') return rc, out, err = nft.json_cmd(nft_payload) - out_parsed, status, err_parsed = parse_nft_response(rc, out, err, raw) + _, status, err_parsed = parse_nft_response(rc, out, err, raw) if status is True: response.status = falcon.HTTP_CREATED diff --git a/nftables_api/utils/__init__.py b/nftables_api/utils/__init__.py new file mode 100644 index 0000000..104f172 --- /dev/null +++ b/nftables_api/utils/__init__.py @@ -0,0 +1,9 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" diff --git a/nftables_api/utils/output.py b/nftables_api/utils/output.py index 87152c0..7f27676 100644 --- a/nftables_api/utils/output.py +++ b/nftables_api/utils/output.py @@ -1,15 +1,26 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + import json + def output_post(status, message=None): output = { - 'status': status + 'status': status, } if message: output.update( { - 'message': message - } + 'message': message, + }, ) return json.dumps(output) diff --git a/nftables_api/utils/parse.py b/nftables_api/utils/parse.py index 5ed8587..03ad6ca 100644 --- a/nftables_api/utils/parse.py +++ b/nftables_api/utils/parse.py @@ -1,3 +1,13 @@ +""" +A RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + import json diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..8c6f8a1 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,50 @@ +[lint] +# https://docs.astral.sh/ruff/rules/ +extend-select = [ + "A", # flake8-builtins + "ARG", # flake8-unused-arguments + "BLE", # flake8-blind-except + "C4", # flake8-comprehensions + "COM", # flake8-commas + "CPY001",# flake8-copyright + "E", # pycodestyle + "E261", # spaces before inline comments + "ERA", # eradicate + "EXE", # flake8-executable + "FBT", # flake8-boolean-trap + "I", # isort + "INP", # flake8-no-pep420 + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "PL", # Pylint + "RET", # flake8-return + "RSE", # flake8-raise + "RUF", # Ruff-specific rules + "S", # flake8-bandit + "SIM", # flake8-simplify + "T20", # flake8-print + "UP", # pyupgrade + "W", # pycodestyle + "YTT", # flake8-2020 +] +ignore = [ + "E501", # line lengths + "FBT002", # booleans as function arguments + "S603", # https://github.com/astral-sh/ruff/issues/4045 + "S607", # makes subprocess calls in test suite more portable +] +preview = true +explicit-preview-rules = true + +[lint.per-file-ignores] +"tests/*.py" = [ + "INP001", # tests do not need to be part of a package + "S101", # allow "assert" in test suites + "T201", # lazy printing is ok in tests +] + +[lint.pydocstyle] +convention = "pep257" + +[lint.isort] +force-wrap-aliases = true diff --git a/tests/conftest.py b/tests/conftest.py index 68070f5..b792108 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,9 +1,20 @@ +""" +Tests for the RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + from falcon import testing -from pytest import exit, fixture from nftables import Nftables +from pytest import exit, fixture from nftables_api.app import app + def run_nft(nft, cmd): rc, out, err = nft.cmd(cmd) if rc != 0: diff --git a/tests/test_api_set.py b/tests/test_api_set.py index 71abef5..da294fd 100644 --- a/tests/test_api_set.py +++ b/tests/test_api_set.py @@ -1,11 +1,22 @@ -from pytest import mark -from falcon import HTTP_CREATED, HTTP_OK +""" +Tests for the RESTful HTTP API for nftables +Copyright 2024, Georg Pfuetzenreuter + +Licensed under the EUPL, Version 1.2 or - as soon they will be approved by the European Commission - subsequent versions of the EUPL (the "Licence"). +You may not use this work except in compliance with the Licence. +An English copy of the Licence is shipped in a file called LICENSE along with this applications source code. +You may obtain copies of the Licence in any of the official languages at https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12. +""" + from json import dumps, loads +from falcon import HTTP_CREATED, HTTP_OK +from pytest import mark + vs = [4, 6] @mark.parametrize('v', vs) -def test_get_set(client, nft_ruleset_populated_sets, v): +def test_get_set(client, nft_ruleset_populated_sets, v): # noqa ARG001, nft is not needed here want_out = { 4: ["192.168.0.0/24", "127.0.0.1"], 6: ["fd80::/64", "fe80::1"], @@ -22,11 +33,6 @@ def test_append_to_set(client, nft_ruleset_populated_sets, v, plvariant, plforma nft = nft_ruleset_populated_sets # all the matrixes could be moved to parameters - want_out = { - 4: ["192.168.0.0/24", "127.0.0.1", "192.168.5.0/26"], - 6: ["fd80::/64", "fe80::1", "fd10:f00::/128"], - } - if plformat == 'string': if plvariant == 'address': to_add = {