diff options
Diffstat (limited to 'src/services/api')
-rw-r--r-- | src/services/api/graphql/generate/config_session_function.py | 6 | ||||
-rwxr-xr-x | src/services/api/graphql/generate/schema_from_op_mode.py | 6 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/auth_token_mutation.py | 14 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/mutations.py | 25 | ||||
-rw-r--r-- | src/services/api/graphql/graphql/queries.py | 25 | ||||
-rw-r--r-- | src/services/api/graphql/libs/op_mode.py | 14 | ||||
-rw-r--r-- | src/services/api/graphql/libs/token_auth.py | 10 | ||||
-rw-r--r-- | src/services/api/graphql/session/errors/op_mode_errors.py | 12 | ||||
-rw-r--r-- | src/services/api/graphql/session/session.py | 35 |
9 files changed, 95 insertions, 52 deletions
diff --git a/src/services/api/graphql/generate/config_session_function.py b/src/services/api/graphql/generate/config_session_function.py index fc0dd7a87..20fc7cc1d 100644 --- a/src/services/api/graphql/generate/config_session_function.py +++ b/src/services/api/graphql/generate/config_session_function.py @@ -8,8 +8,12 @@ def show_config(path: list[str], configFormat: typing.Optional[str]): def show(path: list[str]): pass +def show_user_info(user: str): + pass + queries = {'show_config': show_config, - 'show': show} + 'show': show, + 'show_user_info': show_user_info} def save_config_file(fileName: typing.Optional[str]): pass diff --git a/src/services/api/graphql/generate/schema_from_op_mode.py b/src/services/api/graphql/generate/schema_from_op_mode.py index 1fd198a37..b320a529e 100755 --- a/src/services/api/graphql/generate/schema_from_op_mode.py +++ b/src/services/api/graphql/generate/schema_from_op_mode.py @@ -25,15 +25,17 @@ from inspect import signature, getmembers, isfunction, isclass, getmro from jinja2 import Template from vyos.defaults import directories +from vyos.opmode import _is_op_mode_function_name as is_op_mode_function_name +from vyos.util import load_as_module if __package__ is None or __package__ == '': sys.path.append("/usr/libexec/vyos/services/api") - from graphql.libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name + from graphql.libs.op_mode import is_show_function_name from graphql.libs.op_mode import snake_to_pascal_case, map_type_name from vyos.config import Config from vyos.configdict import dict_merge from vyos.xml import defaults else: - from .. libs.op_mode import load_as_module, is_op_mode_function_name, is_show_function_name + from .. libs.op_mode import is_show_function_name from .. libs.op_mode import snake_to_pascal_case, map_type_name from .. import state diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py index 21ac40094..603a13758 100644 --- a/src/services/api/graphql/graphql/auth_token_mutation.py +++ b/src/services/api/graphql/graphql/auth_token_mutation.py @@ -20,6 +20,7 @@ from ariadne import ObjectType, UnionType from graphql import GraphQLResolveInfo from .. libs.token_auth import generate_token +from .. session.session import get_user_info from .. import state auth_token_mutation = ObjectType("Mutation") @@ -36,13 +37,24 @@ def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict): datetime.timedelta(seconds=exp_interval)) res = generate_token(user, passwd, secret, expiration) - if res: + try: + res |= get_user_info(user) + except ValueError: + # non-existent user already caught + pass + if 'token' in res: data['result'] = res return { "success": True, "data": data } + if 'errors' in res: + return { + "success": False, + "errors": res['errors'] + } + return { "success": False, "errors": ['token generation failed'] diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index 2778feb69..8254e22b1 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -14,8 +14,8 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict -from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake +from typing import Any, Dict, Optional +from ariadne import ObjectType, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature @@ -42,10 +42,9 @@ def make_mutation_resolver(mutation_name, class_name, session_func): func_base_name = convert_camel_case_to_snake(class_name) resolver_name = f'resolve_{func_base_name}' - func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)' @mutation.field(mutation_name) - @convert_kwargs_to_snake_case @with_signature(func_sig, func_name=resolver_name) async def func_impl(*args, **kwargs): try: @@ -67,20 +66,18 @@ def make_mutation_resolver(mutation_name, class_name, session_func): del data['key'] elif auth_type == 'token': - # there is a subtlety here: with the removal of the key entry, - # some requests will now have empty input, hence no data arg, so - # make it optional in the func_sig. However, it can not be None, - # as the makefun package provides accurate TypeError exceptions; - # hence set it to {}, but now it is a mutable default argument, - # so clear the key 'result', which is added at the end of - # this function. data = kwargs['data'] - if 'result' in data: - del data['result'] - + if data is None: + data = {} info = kwargs['info'] user = info.context.get('user') if user is None: + error = info.context.get('error') + if error is not None: + return { + "success": False, + "errors": [error] + } return { "success": False, "errors": ['not authenticated'] diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 9c8a4f064..daccc19b2 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -14,8 +14,8 @@ # along with this library. If not, see <http://www.gnu.org/licenses/>. from importlib import import_module -from typing import Any, Dict -from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake +from typing import Any, Dict, Optional +from ariadne import ObjectType, convert_camel_case_to_snake from graphql import GraphQLResolveInfo from makefun import with_signature @@ -42,10 +42,9 @@ def make_query_resolver(query_name, class_name, session_func): func_base_name = convert_camel_case_to_snake(class_name) resolver_name = f'resolve_{func_base_name}' - func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict = {})' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Optional[Dict]=None)' @query.field(query_name) - @convert_kwargs_to_snake_case @with_signature(func_sig, func_name=resolver_name) async def func_impl(*args, **kwargs): try: @@ -67,20 +66,18 @@ def make_query_resolver(query_name, class_name, session_func): del data['key'] elif auth_type == 'token': - # there is a subtlety here: with the removal of the key entry, - # some requests will now have empty input, hence no data arg, so - # make it optional in the func_sig. However, it can not be None, - # as the makefun package provides accurate TypeError exceptions; - # hence set it to {}, but now it is a mutable default argument, - # so clear the key 'result', which is added at the end of - # this function. data = kwargs['data'] - if 'result' in data: - del data['result'] - + if data is None: + data = {} info = kwargs['info'] user = info.context.get('user') if user is None: + error = info.context.get('error') + if error is not None: + return { + "success": False, + "errors": [error] + } return { "success": False, "errors": ['not authenticated'] diff --git a/src/services/api/graphql/libs/op_mode.py b/src/services/api/graphql/libs/op_mode.py index 97a26520e..c553bbd67 100644 --- a/src/services/api/graphql/libs/op_mode.py +++ b/src/services/api/graphql/libs/op_mode.py @@ -21,24 +21,14 @@ from typing import Union from humps import decamelize from vyos.defaults import directories +from vyos.util import load_as_module from vyos.opmode import _normalize_field_names -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 @@ -89,7 +79,7 @@ def map_type_name(type_name: type, optional: bool = False) -> str: if type_name == int: return 'Int!' if not optional else 'Int = null' if type_name == bool: - return 'Boolean!' if not optional else 'Boolean = false' + return 'Boolean = false' if typing.get_origin(type_name) == list: if not optional: return f'[{map_type_name(typing.get_args(type_name)[0])}]!' diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index 3ecd8b855..8585485c9 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -29,14 +29,13 @@ def generate_token(user: str, passwd: str, secret: str, exp: int) -> dict: 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'] - } + return {"errors": ['missing secret']} token = jwt.encode(payload=payload_data, key=secret, algorithm="HS256") users |= {user_id: user} return {'token': token} + else: + return {"errors": ['failed pam authentication']} def get_user_context(request): context = {} @@ -54,6 +53,9 @@ def get_user_context(request): user_id: str = payload.get('sub') if user_id is None: return context + except jwt.exceptions.ExpiredSignatureError: + context['error'] = 'expired token' + return context except jwt.PyJWTError: return context try: diff --git a/src/services/api/graphql/session/errors/op_mode_errors.py b/src/services/api/graphql/session/errors/op_mode_errors.py index 7ba75455d..18d555f2d 100644 --- a/src/services/api/graphql/session/errors/op_mode_errors.py +++ b/src/services/api/graphql/session/errors/op_mode_errors.py @@ -1,13 +1,17 @@ - - op_mode_err_msg = { "UnconfiguredSubsystem": "subsystem is not configured or not running", "DataUnavailable": "data currently unavailable", - "PermissionDenied": "client does not have permission" + "PermissionDenied": "client does not have permission", + "InsufficientResources": "insufficient system resources", + "IncorrectValue": "argument value is incorrect", + "UnsupportedOperation": "operation is not supported (yet)", } op_mode_err_code = { "UnconfiguredSubsystem": 2000, "DataUnavailable": 2001, - "PermissionDenied": 1003 + "InsufficientResources": 2002, + "PermissionDenied": 1003, + "IncorrectValue": 1002, + "UnsupportedOperation": 1004, } diff --git a/src/services/api/graphql/session/session.py b/src/services/api/graphql/session/session.py index 0b77b1433..3c5a062b6 100644 --- a/src/services/api/graphql/session/session.py +++ b/src/services/api/graphql/session/session.py @@ -29,6 +29,28 @@ from api.graphql.libs.op_mode import normalize_output op_mode_include_file = os.path.join(directories['data'], 'op-mode-standardized.json') +def get_config_dict(path=[], effective=False, key_mangling=None, + get_first_key=False, no_multi_convert=False, + no_tag_node_value_mangle=False): + config = Config() + return config.get_config_dict(path=path, effective=effective, + key_mangling=key_mangling, + get_first_key=get_first_key, + no_multi_convert=no_multi_convert, + no_tag_node_value_mangle=no_tag_node_value_mangle) + +def get_user_info(user): + user_info = {} + info = get_config_dict(['system', 'login', 'user', user], + get_first_key=True) + if not info: + raise ValueError("No such user") + + user_info['user'] = user + user_info['full_name'] = info.get('full-name', '') + + return user_info + class Session: """ Wrapper for calling configsession functions based on GraphQL requests. @@ -116,6 +138,19 @@ class Session: return res + def show_user_info(self): + session = self._session + data = self._data + + user_info = {} + user = data['user'] + try: + user_info = get_user_info(user) + except Exception as error: + raise error + + return user_info + def system_status(self): import api.graphql.session.composite.system_status as system_status |