From af56ddf4615974c6b5f5886520d6abb0781cea80 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 23 Oct 2022 11:07:46 -0500 Subject: graphql: T4574: read config and generate schema with/without key auth --- src/services/vyos-http-api-server | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 632c1e87d..7a35546e5 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -647,11 +647,11 @@ def reset_op(data: ResetModel): ### def graphql_init(fast_api_app): - from api.graphql.bindings import generate_schema - api.graphql.state.init() api.graphql.state.settings['app'] = app + # import after initializaion of state + from api.graphql.bindings import generate_schema schema = generate_schema() in_spec = app.state.vyos_introspection @@ -690,10 +690,13 @@ if __name__ == '__main__': app.state.vyos_origins = server_config.get('cors', {}).get('allow_origin', []) if 'graphql' in server_config: app.state.vyos_graphql = True - if isinstance(server_config['graphql'], dict) and 'introspection' in server_config['graphql']: - app.state.vyos_introspection = True - else: - app.state.vyos_introspection = False + if isinstance(server_config['graphql'], dict): + if 'introspection' in server_config['graphql']: + app.state.vyos_introspection = True + else: + app.state.vyos_introspection = False + # default value is merged in conf_mode http-api.py, if not set + app.state.vyos_auth_type = server_config['graphql']['authentication']['type'] else: app.state.vyos_graphql = False -- cgit v1.2.3 From 28676844e3f4317786e457fcd8651939a05c88ff Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 23 Oct 2022 11:08:06 -0500 Subject: graphql: T4574: add context to read token in queries/mutations --- src/services/api/graphql/graphql/mutations.py | 62 ++++++++++++++++++--------- src/services/api/graphql/graphql/queries.py | 62 ++++++++++++++++++--------- src/services/api/graphql/libs/token_auth.py | 29 +++++++++++++ src/services/vyos-http-api-server | 5 ++- 4 files changed, 116 insertions(+), 42 deletions(-) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py index f0c8b438f..2778feb69 100644 --- a/src/services/api/graphql/graphql/mutations.py +++ b/src/services/api/graphql/graphql/mutations.py @@ -42,32 +42,54 @@ 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: Dict = {})' @mutation.field(mutation_name) @convert_kwargs_to_snake_case @with_signature(func_sig, func_name=resolver_name) async def func_impl(*args, **kwargs): try: - if 'data' not in kwargs: - return { - "success": False, - "errors": ['missing data'] - } - - data = kwargs['data'] - key = data['key'] - - auth = key_auth.auth_required(key) - if auth is None: - return { - "success": False, - "errors": ['invalid API key'] - } - - # We are finished with the 'key' entry, and may remove so as to - # pass the rest of data (if any) to function. - del data['key'] + auth_type = state.settings['app'].state.vyos_auth_type + + if auth_type == 'key': + data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + 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'] + + info = kwargs['info'] + user = info.context.get('user') + if user is None: + return { + "success": False, + "errors": ['not authenticated'] + } + else: + # AtrributeError will have already been raised if no + # vyos_auth_type; validation and defaultValue ensure it is + # one of the previous cases, so this is never reached. + pass session = state.settings['app'].state.vyos_session diff --git a/src/services/api/graphql/graphql/queries.py b/src/services/api/graphql/graphql/queries.py index 13eb59ae4..9c8a4f064 100644 --- a/src/services/api/graphql/graphql/queries.py +++ b/src/services/api/graphql/graphql/queries.py @@ -42,32 +42,54 @@ 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: Dict = {})' @query.field(query_name) @convert_kwargs_to_snake_case @with_signature(func_sig, func_name=resolver_name) async def func_impl(*args, **kwargs): try: - if 'data' not in kwargs: - return { - "success": False, - "errors": ['missing data'] - } - - data = kwargs['data'] - key = data['key'] - - auth = key_auth.auth_required(key) - if auth is None: - return { - "success": False, - "errors": ['invalid API key'] - } - - # We are finished with the 'key' entry, and may remove so as to - # pass the rest of data (if any) to function. - del data['key'] + auth_type = state.settings['app'].state.vyos_auth_type + + if auth_type == 'key': + data = kwargs['data'] + key = data['key'] + + auth = key_auth.auth_required(key) + if auth is None: + return { + "success": False, + "errors": ['invalid API key'] + } + + # We are finished with the 'key' entry, and may remove so as to + # pass the rest of data (if any) to function. + 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'] + + info = kwargs['info'] + user = info.context.get('user') + if user is None: + return { + "success": False, + "errors": ['not authenticated'] + } + else: + # AtrributeError will have already been raised if no + # vyos_auth_type; validation and defaultValue ensure it is + # one of the previous cases, so this is never reached. + pass session = state.settings['app'].state.vyos_session diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index c53e354b1..2d63a1cc7 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -36,3 +36,32 @@ def generate_token(user: str, passwd: str, secret: str) -> dict: 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 diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 7a35546e5..840041b73 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -647,6 +647,7 @@ def reset_op(data: ResetModel): ### def graphql_init(fast_api_app): + from api.graphql.libs.token_auth import get_user_context api.graphql.state.init() api.graphql.state.settings['app'] = app @@ -658,9 +659,9 @@ def graphql_init(fast_api_app): if app.state.vyos_origins: origins = app.state.vyos_origins - app.add_route('/graphql', CORSMiddleware(GraphQL(schema, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS"))) + app.add_route('/graphql', CORSMiddleware(GraphQL(schema, context_value=get_user_context, debug=True, introspection=in_spec), allow_origins=origins, allow_methods=("GET", "POST", "OPTIONS"))) else: - app.add_route('/graphql', GraphQL(schema, debug=True, introspection=in_spec)) + app.add_route('/graphql', GraphQL(schema, context_value=get_user_context, debug=True, introspection=in_spec)) ### -- cgit v1.2.3 From dc37f30a1273c1d3b7949b1d64e60d37da3b9fd4 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Sun, 23 Oct 2022 11:08:19 -0500 Subject: graphql: T4574: set token expiration time in claims --- src/services/api/graphql/graphql/auth_token_mutation.py | 7 ++++++- src/services/api/graphql/libs/token_auth.py | 4 ++-- src/services/vyos-http-api-server | 1 + 3 files changed, 9 insertions(+), 3 deletions(-) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/api/graphql/graphql/auth_token_mutation.py b/src/services/api/graphql/graphql/auth_token_mutation.py index 33779d4f0..21ac40094 100644 --- a/src/services/api/graphql/graphql/auth_token_mutation.py +++ b/src/services/api/graphql/graphql/auth_token_mutation.py @@ -14,6 +14,7 @@ # along with this library. If not, see . import jwt +import datetime from typing import Any, Dict from ariadne import ObjectType, UnionType from graphql import GraphQLResolveInfo @@ -30,7 +31,11 @@ def auth_token_resolver(obj: Any, info: GraphQLResolveInfo, data: Dict): passwd = data['password'] secret = state.settings['secret'] - res = generate_token(user, passwd, secret) + exp_interval = int(state.settings['app'].state.vyos_token_exp) + expiration = (datetime.datetime.now(tz=datetime.timezone.utc) + + datetime.timedelta(seconds=exp_interval)) + + res = generate_token(user, passwd, secret, expiration) if res: data['result'] = res return { diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index 2d63a1cc7..fafb0f5af 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -14,7 +14,7 @@ def init_secret(): secret = token_hex(16) state.settings['secret'] = secret -def generate_token(user: str, passwd: str, secret: str) -> dict: +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): @@ -25,7 +25,7 @@ def generate_token(user: str, passwd: str, secret: str) -> dict: app.state.vyos_token_users = {} users = app.state.vyos_token_users user_id = uuid.uuid1().hex - payload_data = {'iss': user, 'sub': user_id} + payload_data = {'iss': user, 'sub': user_id, 'exp': exp} secret = state.settings.get('secret') if secret is None: return { diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 840041b73..4af27b949 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -698,6 +698,7 @@ if __name__ == '__main__': app.state.vyos_introspection = False # default value is merged in conf_mode http-api.py, if not set app.state.vyos_auth_type = server_config['graphql']['authentication']['type'] + app.state.vyos_token_exp = server_config['graphql']['authentication']['expiration'] else: app.state.vyos_graphql = False -- cgit v1.2.3 From 3db5ba8ef354d80f080cc1baacf33d77ccbb6222 Mon Sep 17 00:00:00 2001 From: John Estabrook Date: Tue, 25 Oct 2022 09:22:50 -0500 Subject: graphql: T4574: set byte length of shared secret from CLI --- src/services/api/graphql/libs/token_auth.py | 3 ++- src/services/vyos-http-api-server | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) (limited to 'src/services/vyos-http-api-server') diff --git a/src/services/api/graphql/libs/token_auth.py b/src/services/api/graphql/libs/token_auth.py index fafb0f5af..3ecd8b855 100644 --- a/src/services/api/graphql/libs/token_auth.py +++ b/src/services/api/graphql/libs/token_auth.py @@ -11,7 +11,8 @@ def _check_passwd_pam(username: str, passwd: str) -> bool: return False def init_secret(): - secret = token_hex(16) + 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: diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 4af27b949..3c390d9dc 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -699,6 +699,7 @@ if __name__ == '__main__': # default value is merged in conf_mode http-api.py, if not set app.state.vyos_auth_type = server_config['graphql']['authentication']['type'] app.state.vyos_token_exp = server_config['graphql']['authentication']['expiration'] + app.state.vyos_secret_len = server_config['graphql']['authentication']['secret_length'] else: app.state.vyos_graphql = False -- cgit v1.2.3