Implement authorization

Read a configuration file mapping bcrypt hashed tokens to authorized
paths and methods to decide whether a request should be pursued.

Signed-off-by: Georg Pfuetzenreuter <mail@georg-pfuetzenreuter.net>
This commit is contained in:
Georg Pfuetzenreuter 2024-09-28 16:42:46 +02:00
parent 218c9122d1
commit 75c2411dd2
Signed by: Georg
GPG Key ID: 1ED2F138E7E6FF57
4 changed files with 112 additions and 1 deletions

View File

@ -1,7 +1,13 @@
from falcon import App
from .resources import nft_set
from .config import config
from nftables_api.middlewares.authentication import AuthMiddleWare
app = App()
app = App(
middleware=[
AuthMiddleWare(),
]
)
rSet = nft_set.SetResource()

14
nftables_api/config.py Normal file
View File

@ -0,0 +1,14 @@
from yaml import safe_load
from os import getenv
configpath = getenv('NFT-API-CONFIG')
if not configpath:
raise RuntimeError('NFT-API-CONFIG is not set')
with open(configpath) as fh:
configdata = safe_load(fh)
config = configdata.get('nft-api', {})
if not config:
raise RuntimeError('Invalid configuration data')

View File

@ -0,0 +1,88 @@
from bcrypt import checkpw
from falcon import HTTPUnauthorized
from nftables_api.config import config
class AuthMiddleWare:
def _match(self, token_plain, token_hashed):
"""
Check plain token against bcrypt hash
"""
try:
return checkpw(token_plain.encode(), token_hashed.encode())
except ValueError:
return False
def _valid(self, token):
"""
Check if token is contained in the configuration
"""
for config_token, config_paths in config.get('tokens', {}).items():
if self._match(token, config_token):
return True
return False
def process_request(self, req, resp):
"""
Rudimentary token validation - check if it is worth walking further down the authorization chain
"""
token = req.get_header('X-NFT-API-TOKEN')
if token is None:
raise HTTPUnauthorized(
title='Authentication required',
)
if not self._valid(token):
raise HTTPUnauthorized(
title='Unauthorized',
)
def process_resource(self, req, resp, resource, params):
"""
Fully validate whether a token is authorized to perform the request
"""
token = req.get_header('X-NFT-API-TOKEN')
resource_name = resource._name()
for config_token, config_paths in config.get('tokens', {}).items():
if not self._match(token, config_token):
continue
for config_path, methods in config_paths.items():
if not isinstance(methods, list):
raise RuntimeError(f'Invalid method configured for path {config_path}')
# a leading slash causes an empty first list entry in the split
if config_path.startswith('/'):
config_path = config_path[1:]
path_elements = config_path.split('/')
if path_elements[0] != resource_name:
continue
path_elements = path_elements[1:]
if resource_name == 'set':
need_elements = ['xfamily', 'xtable', 'xset']
for i, need_element in enumerate(need_elements):
if path_elements[i] == '*':
continue
if path_elements[i] != params.get(need_element):
break
else:
if req.method in methods:
return
else:
raise HTTPUnauthorized(
title='Unauthorized method for path',
)
raise HTTPUnauthorized(
title='Unauthorized',
)

View File

@ -8,6 +8,9 @@ nft = Nftables()
nft.set_json_output(True)
class SetResource:
def _name(self):
return 'set'
def on_get(self, request, response, xfamily, xtable, xset):
raw = request.get_param_as_bool('raw', default=False)