Skip to content

Commit

Permalink
feat: add websocket authentication using jwt token (#628)
Browse files Browse the repository at this point in the history
The cli expects to receive websocket-access-token,
websocket-refresh-token, and websocket-token-address.

It does not send the authentication header if above arguments are not
provided, so it works with the old eda-server that does not authenticate
incomming websocket connecitons.

Fixes AAP-17776: ansible-rulebook uses token for authentication
  • Loading branch information
bzwei authored Jan 8, 2024
1 parent 0087c02 commit c2739ad
Show file tree
Hide file tree
Showing 11 changed files with 373 additions and 122 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
- ssl_verify option now also supports "true" or "false" values
- Support for standalone boolean in conditions
- Add basic auth to controller
- Use token for websocket authentication

### Changed
- Generic print as well as printing of events use new banner style
Expand Down
18 changes: 5 additions & 13 deletions ansible_rulebook/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,13 +64,9 @@ def qsize(self):
async def run(parsed_args: argparse.Namespace) -> None:
file_monitor = None

if parsed_args.worker and parsed_args.websocket_address and parsed_args.id:
if parsed_args.worker and parsed_args.websocket_url and parsed_args.id:
logger.info("Starting worker mode")
startup_args = await request_workload(
parsed_args.id,
parsed_args.websocket_address,
parsed_args.websocket_ssl_verify,
)
startup_args = await request_workload(parsed_args.id)
if not startup_args:
logger.error("Error communicating with web socket server")
raise WebSocketExchangeException(
Expand Down Expand Up @@ -102,7 +98,7 @@ async def run(parsed_args: argparse.Namespace) -> None:
if startup_args.check_controller_connection:
await validate_controller_params(startup_args)

if parsed_args.websocket_address:
if parsed_args.websocket_url:
event_log = asyncio.Queue()
else:
event_log = NullQueue()
Expand All @@ -118,13 +114,9 @@ async def run(parsed_args: argparse.Namespace) -> None:
logger.info("Starting rules")

feedback_task = None
if parsed_args.websocket_address:
if parsed_args.websocket_url:
feedback_task = asyncio.create_task(
send_event_log_to_websocket(
event_log,
parsed_args.websocket_address,
parsed_args.websocket_ssl_verify,
)
send_event_log_to_websocket(event_log=event_log)
)
tasks.append(feedback_task)

Expand Down
31 changes: 25 additions & 6 deletions ansible_rulebook/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,16 +97,32 @@ def get_parser() -> argparse.ArgumentParser:
)
parser.add_argument(
"-W",
"--websocket-address",
"--websocket-url",
"--websocket-address",
help="Connect the event log to a websocket",
default=os.environ.get("EDA_WEBSOCKET_URL", ""),
)
parser.add_argument(
"--websocket-ssl-verify",
help="How to verify SSL when connecting to the "
"websocket: (yes|true) | (no|false) | <path to a CA bundle>, "
"default to yes for wss connection.",
default="yes",
default=os.environ.get("EDA_WEBSOCKET_SSL_VERIFY", "yes"),
)
parser.add_argument(
"--websocket-access-token",
help="Token used to autheticate the websocket connection.",
default=os.environ.get("EDA_WEBSOCKET_ACCESS_TOKEN", ""),
)
parser.add_argument(
"--websocket-refresh-token",
help="Token used to renew a websocket access token.",
default=os.environ.get("EDA_WEBSOCKET_REFRESH_TOKEN", ""),
)
parser.add_argument(
"--websocket-token-url",
help="Url to renew websocket access token.",
default=os.environ.get("EDA_WEBSOCKET_TOKEN_URL", ""),
)
parser.add_argument("--id", help="Identifier")
parser.add_argument(
Expand Down Expand Up @@ -215,10 +231,8 @@ def get_version() -> str:


def validate_args(args: argparse.Namespace) -> None:
if args.worker and (not args.id or not args.websocket_address):
raise ValueError(
"Worker mode needs an id and websocket address specfied"
)
if args.worker and (not args.id or not args.websocket_url):
raise ValueError("Worker mode needs an id and websocket url specfied")
if not args.worker and not args.rulebook:
raise ValueError("Rulebook must be specified in non worker mode")

Expand Down Expand Up @@ -255,6 +269,11 @@ def update_settings(args: argparse.Namespace) -> None:
settings.default_execution_strategy = args.execution_strategy

settings.print_events = args.print_events
settings.websocket_url = args.websocket_url
settings.websocket_ssl_verify = args.websocket_ssl_verify
settings.websocket_token_url = args.websocket_token_url
settings.websocket_access_token = args.websocket_access_token
settings.websocket_refresh_token = args.websocket_refresh_token


def main(args: List[str] = None) -> int:
Expand Down
5 changes: 5 additions & 0 deletions ansible_rulebook/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ def __init__(self):
self.default_execution_strategy = "sequential"
self.max_feedback_timeout = 5
self.print_events = False
self.websocket_url = None
self.websocket_ssl_verify = "yes"
self.websocket_token_url = None
self.websocket_access_token = None
self.websocket_refresh_token = None


settings = _Settings()
5 changes: 5 additions & 0 deletions ansible_rulebook/exception.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,8 @@ class InventoryNotFound(Exception):
class MissingArtifactKeyException(Exception):

pass


class TokenNotFound(Exception):

pass
50 changes: 50 additions & 0 deletions ansible_rulebook/token.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Copyright 2024 Red Hat, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import ssl
import typing as tp

import aiohttp

from ansible_rulebook.conf import settings
from ansible_rulebook.exception import TokenNotFound

logger = logging.getLogger(__name__)


async def renew_token() -> str:
logger.info("Renew websocket token from %s", settings.websocket_token_url)
async with aiohttp.ClientSession() as session:
async with session.post(
settings.websocket_token_url,
data={"refresh": settings.websocket_refresh_token},
ssl_context=_sslcontext(),
) as resp:
data = await resp.json()
if "access" not in data:
logger.error(f"Failed to renew token. Error: {str(data)}")
raise TokenNotFound("Response does not contain access token")
return data["access"]


def _sslcontext() -> tp.Optional[ssl.SSLContext]:
if settings.websocket_token_url.startswith("https"):
ssl_verify = settings.websocket_ssl_verify.lower()
if ssl_verify in ["yes", "true"]:
return ssl.create_default_context()
if ssl_verify in ["no", "false"]:
return ssl._create_unverified_context()
return ssl.create_default_context(cafile=ssl_verify)
return None
Loading

0 comments on commit c2739ad

Please sign in to comment.