From db7edd5dae98f0b0ab5e1b1f2514890a96b38bbf Mon Sep 17 00:00:00 2001 From: Christopher Arndt Date: Sun, 23 Jul 2023 19:34:25 +0200 Subject: [PATCH] feat: add environment white-list config setting and cmdline option See description in readme for details Signed-off-by: Christopher Arndt --- README.md | 11 ++++++- matrixchat-notify.py | 69 ++++++++++++++++++++++++++++++++++++-------- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 05efb3a..7801411 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,14 @@ steps: substtution is considered to be in Markdown format and will be rendered to HTML and sent as a formatted message with `org.matrix.custom.html` format. +* `pass_environment` *(default:* `DRONE_*`*)* + + Comma-separated white-list of environment variable names or name patterns. + Patterns are shell-glob style patterns and case-sensitive. + + Only environment variables matching any of the given names or patterns will + be available as valid placeholders in the message template. + * `password` Password to use for authenticating the user set with `userid`. Either a @@ -64,7 +72,8 @@ steps: * `template` *(default:* `${DRONE_BUILD_STATUS}`*)* The message template. Valid placeholders of the form `${PLACEHOLDER}` will - be substituted with the values of the matching environment variables. + be substituted with the values of the matching environment variables + (subject to filtering according to the `pass_environment` setting). See this [reference] for environment variables available in drone.io CI pipelines. diff --git a/matrixchat-notify.py b/matrixchat-notify.py index 6c30f0a..0033d89 100755 --- a/matrixchat-notify.py +++ b/matrixchat-notify.py @@ -10,6 +10,7 @@ Requires: import argparse import asyncio +import fnmatch import json import logging import os @@ -22,14 +23,16 @@ from nio import AsyncClient, LoginResponse PROG = "matrixchat-notify" CONFIG_FILENAME = f"{PROG}-config.json" -DEFAULT_TEMPLATE = "${DRONE_BUILD_STATUS}" DEFAULT_HOMESERVER = "https://matrix.org" +DEFAULT_PASS_ENVIRONMENT = ["DRONE_*"] +DEFAULT_TEMPLATE = "${DRONE_BUILD_STATUS}" SETTINGS_KEYS = ( "accesstoken", "deviceid", "devicename", "homeserver", "markdown", + "pass_environment", "password", "roomid", "template", @@ -110,6 +113,38 @@ async def send_notification(config, message): await client.close() +def render_message(config): + pass_environment = config.get("pass_environment", "") + + if not isinstance(pass_environment, list): + pass_environment = [pass_environment] + + patterns = [] + for value in pass_environment: + # expand any comma-separetd names/patterns + if "," in value: + patterns.extend([p.strip() for p in value.split(",") if p.strip()]) + else: + patterns.append(value) + + env_names = tuple(os.environ) + filtered_names = set() + + for pattern in patterns: + filtered_names.update(fnmatch.filter(env_names, pattern)) + + context = {name: os.environ[name] for name in tuple(filtered_names)} + template = config.get("template", DEFAULT_TEMPLATE) + return Template(template).safe_substitute(context) + + +def render_markdown(message): + import markdown + + formatted = markdown.markdown(message) + return {"formatted_body": formatted, "body": message, "format": "org.matrix.custom.html"} + + def main(args=None): ap = argparse.ArgumentParser(prog=PROG, description=__doc__.splitlines()[0]) ap.add_argument( @@ -125,6 +160,17 @@ def main(args=None): action="store_true", help="Don't send notification message, only print it.", ) + ap.add_argument( + "-e", + "--pass-environment", + nargs="*", + help=( + "Comma-separated white-list of environment variable names or name patterns. Only " + "environment variables matching any of the given names or patterns will be available " + "as valid placeholders in the message template. " + "Accepts shell glob patterns and may be passed more than once (default: 'DRONE_*')." + ), + ) ap.add_argument( "-m", "--render-markdown", @@ -150,23 +196,22 @@ def main(args=None): except Exception as exc: return f"Could not parse configuration: {exc}" - template = config.get("template", DEFAULT_TEMPLATE) - message = Template(template).safe_substitute(os.environ) + if args.pass_environment is not None: + # Security feature: if any environment names/patterns are passed via -e|--pass-environment + # options, they completely replace any given via the config or environment. + config["pass_environment"] = args.pass_environment + + if "pass_environment" not in config: + config["pass_environment"] = DEFAULT_PASS_ENVIRONMENT + + message = render_message(config) if tobool(config.get("markdown")) or args.render_markdown: log.debug("Rendering markdown message to HTML.") try: - import markdown - - formatted = markdown.markdown(message) + message = render_markdown(message) except: ## noqa log.exception("Failed to render message with markdown.") - return 1 - - body = message - message = {"formatted_body": formatted} - message["body"] = body - message["format"] = "org.matrix.custom.html" if not args.dry_run: if not config.get("userid"):