diff options
author | John Estabrook <jestabro@vyos.io> | 2022-10-25 12:08:42 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-10-25 12:08:42 -0500 |
commit | 1265b15ffc8baa05177c707f30205d70528c5dc6 (patch) | |
tree | 8cb9715666250f8599147d066bd93a9a06df4962 /src/services/api/graphql/libs | |
parent | ec82d60828500a56b6fe8357970bf839053ac0af (diff) | |
parent | 3db5ba8ef354d80f080cc1baacf33d77ccbb6222 (diff) | |
download | vyos-1x-1265b15ffc8baa05177c707f30205d70528c5dc6.tar.gz vyos-1x-1265b15ffc8baa05177c707f30205d70528c5dc6.zip |
Merge pull request #1613 from jestabro/graphql-hybrid-auth
graphql: T4574: add JWT token authentication
Diffstat (limited to 'src/services/api/graphql/libs')
-rw-r--r-- | src/services/api/graphql/libs/key_auth.py | 18 | ||||
-rw-r--r-- | src/services/api/graphql/libs/op_mode.py | 100 | ||||
-rw-r--r-- | src/services/api/graphql/libs/token_auth.py | 68 |
3 files changed, 186 insertions, 0 deletions
diff --git a/src/services/api/graphql/libs/key_auth.py b/src/services/api/graphql/libs/key_auth.py new file mode 100644 index 000000000..2db0f7d48 --- /dev/null +++ b/src/services/api/graphql/libs/key_auth.py @@ -0,0 +1,18 @@ + +from .. import state + +def check_auth(key_list, key): + if not key_list: + return None + key_id = None + for k in key_list: + if k['key'] == key: + key_id = k['id'] + return key_id + +def auth_required(key): + api_keys = None + api_keys = state.settings['app'].state.vyos_keys + key_id = check_auth(api_keys, key) + state.settings['app'].state.vyos_id = key_id + return key_id diff --git a/src/services/api/graphql/libs/op_mode.py b/src/services/api/graphql/libs/op_mode.py new file mode 100644 index 000000000..da2bcdb5b --- /dev/null +++ b/src/services/api/graphql/libs/op_mode.py @@ -0,0 +1,100 @@ +# Copyright 2022 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +import os +import re +import typing +import importlib.util + +from vyos.defaults import directories + +def load_as_module(name: str, path: str): + spec = importlib.util.spec_from_file_location(name, path) + mod = importlib.util.module_from_spec(spec) + spec.loader.exec_module(mod) + return mod + +def load_op_mode_as_module(name: str): + path = os.path.join(directories['op_mode'], name) + name = os.path.splitext(name)[0].replace('-', '_') + return load_as_module(name, path) + +def is_op_mode_function_name(name): + if re.match(r"^(show|clear|reset|restart)", name): + return True + return False + +def is_show_function_name(name): + if re.match(r"^show", name): + return True + return False + +def _nth_split(delim: str, n: int, s: str): + groups = s.split(delim) + l = len(groups) + if n > l-1 or n < 1: + return (s, '') + return (delim.join(groups[:n]), delim.join(groups[n:])) + +def _nth_rsplit(delim: str, n: int, s: str): + groups = s.split(delim) + l = len(groups) + if n > l-1 or n < 1: + return (s, '') + return (delim.join(groups[:l-n]), delim.join(groups[l-n:])) + +# Since we have mangled possible hyphens in the file name while constructing +# the snake case of the query/mutation name, we will need to recover the +# file name by searching with mangling: +def _filter_on_mangled(test): + def func(elem): + mangle = os.path.splitext(elem)[0].replace('-', '_') + return test == mangle + return func + +# Find longest name in concatenated string that matches the basename of an +# op-mode script. Should one prefer to concatenate in the reverse order +# (script_name + '_' + function_name), use _nth_rsplit. +def split_compound_op_mode_name(name: str, files: list): + for i in range(1, name.count('_') + 1): + pair = _nth_split('_', i, name) + f = list(filter(_filter_on_mangled(pair[1]), files)) + if f: + pair = (pair[0], f[0]) + return pair + return (name, '') + +def snake_to_pascal_case(name: str) -> str: + res = ''.join(map(str.title, name.split('_'))) + return res + +def map_type_name(type_name: type, optional: bool = False) -> str: + if type_name == str: + return 'String!' if not optional else 'String = null' + if type_name == int: + return 'Int!' if not optional else 'Int = null' + if type_name == bool: + return 'Boolean!' if not optional else 'Boolean = false' + if typing.get_origin(type_name) == list: + if not optional: + return f'[{map_type_name(typing.get_args(type_name)[0])}]!' + return f'[{map_type_name(typing.get_args(type_name)[0])}]' + # typing.Optional is typing.Union[_, NoneType] + if (typing.get_origin(type_name) is typing.Union and + typing.get_args(type_name)[1] == type(None)): + return f'{map_type_name(typing.get_args(type_name)[0], optional=True)}' + + # scalar 'Generic' is defined in schema.graphql + return 'Generic' diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py new file mode 100644 index 000000000..3ecd8b855 --- /dev/null +++ b/src/services/api/graphql/libs/token_auth.py @@ -0,0 +1,68 @@ +import jwt +import uuid +import pam +from secrets import token_hex + +from .. import state + +def _check_passwd_pam(username: str, passwd: str) -> bool: + if pam.authenticate(username, passwd): + return True + return False + +def init_secret(): + length = int(state.settings['app'].state.vyos_secret_len) + secret = token_hex(length) + state.settings['secret'] = secret + +def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict: + if user is None or passwd is None: + return {} + if _check_passwd_pam(user, passwd): + app = state.settings['app'] + try: + users = app.state.vyos_token_users + except AttributeError: + app.state.vyos_token_users = {} + users = app.state.vyos_token_users + user_id = uuid.uuid1().hex + payload_data = {'iss': user, 'sub': user_id, 'exp': exp} + secret = state.settings.get('secret') + if secret is None: + return { + "success": False, + "errors": ['failed secret generation'] + } + token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256") + + users |= {user_id: user} + return {'token': token} + +def get_user_context(request): + context = {} + context['request'] = request + context['user'] = None + if 'Authorization' in request.headers: + auth = request.headers['Authorization'] + scheme, token = auth.split() + if scheme.lower() != 'bearer': + return context + + try: + secret = state.settings.get('secret') + payload = jwt.decode(token, secret, algorithms=["HS256"]) + user_id: str = payload.get('sub') + if user_id is None: + return context + except jwt.PyJWTError: + return context + try: + users = state.settings['app'].state.vyos_token_users + except AttributeError: + return context + + user = users.get(user_id) + if user is not None: + context['user'] = user + + return context |