diff --git a/.env.default b/.env.default index 0c62acf..9008ecc 100644 --- a/.env.default +++ b/.env.default @@ -1,8 +1,11 @@ -APP_LIFECYCLE="dev" -SENTRY_ENABLED="False" -SENTRY_DSN="" ADMIN_EMAIL="" ADMIN_FIRST_NAME="" +APP_LIFECYCLE="dev" +APPROVED_DOMAINS="example.com,hello.com" +APPROVED_ROOMS="abc123,def456" +APPROVED_USERS="bob@example.com,john@me.com" BOT_NAME="" N8N_WEBHOOK_URL="" +SENTRY_DSN="" +SENTRY_ENABLED="False" WEBEX_API_KEY="" diff --git a/README.md b/README.md index 52be22e..d0349eb 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,14 @@ Add tasks to a Wekan to do list via Webex and n8n. 3. Edit `.env` as required: - `ADMIN_EMAIL` - comma-separated list of admin (who owns the to-do list) email addresses - `ADMIN_FIRST_NAME` - admin first name + - `APP_LIFECYCLE` - for use in Sentry only, set the name of the environment + - `APPROVED_DOMAINS` - comma-separated list of domains that users are allowed to message the bot from + - `APPROVED_ROOMS` - comma-separated list of room IDs that users are allowed to message the bot from + - `APPROVED_USERS` - comma-separated list of email addresses of approved users - `BOT_NAME` - Webex bot name - `N8N_WEBHOOK_URL` - n8n webhook URL + - `SENTRY_DSN` - for use in Sentry only, set the DSN of the Sentry project + - `SENTRY_ENABLED` - for use in Sentry only, enable sending data to Sentry - `WEBEX_API_KEY` - Webex API key ## How to use diff --git a/app/main.py b/app/main.py index 19df55a..c222c89 100644 --- a/app/main.py +++ b/app/main.py @@ -2,14 +2,12 @@ import sentry_sdk from sentry_sdk.integrations.stdlib import StdlibIntegration - from webex_bot.webex_bot import WebexBot from app.commands.exit import ExitCommand from app.commands.submit_task import SubmitTaskCommand from app.utils.config import config - if config.sentry_enabled: apm = sentry_sdk.init( dsn=config.sentry_dsn, @@ -17,7 +15,7 @@ if config.sentry_enabled: environment=config.environment, release=config.version, integrations=[StdlibIntegration()], - spotlight=True + spotlight=True, ) @@ -26,7 +24,9 @@ def create_bot() -> WebexBot: webex_bot: WebexBot = WebexBot( bot_name=config.bot_name, teams_bot_token=config.webex_token, - approved_domains=["cisco.com"], + approved_domains=config.approved_domains, + approved_rooms=config.approved_rooms, + approved_users=config.approved_users, ) webex_bot.commands.clear() webex_bot.add_command(SubmitTaskCommand()) diff --git a/app/utils/config.py b/app/utils/config.py index b9e6231..5cce4b4 100644 --- a/app/utils/config.py +++ b/app/utils/config.py @@ -2,19 +2,19 @@ import os +from app.utils.helpers import validate_email_syntax + class Config: """Configuration module.""" + def __init__(self) -> None: """Configuration module.""" - self.__environment: str = os.environ.get("APP_LIFECYCLE", "DEV").upper() - self.__version: str = os.environ["APP_VERSION"] - self.__bot_name: str = os.environ["BOT_NAME"] - self.__webex_token: str = os.environ["WEBEX_API_KEY"] - self.__admin_first_name: str = os.environ["ADMIN_FIRST_NAME"] - self.__admin_emails: list = os.environ["ADMIN_EMAIL"].split(",") - self.__n8n_webhook_url: str = os.environ["N8N_WEBHOOK_URL"] + + # Sentry config needs to be processed first for loop prevention. + self.__sentry_dsn: str = os.environ.get("SENTRY_DSN", "") + self.__sentry_enabled: bool = bool( os.environ.get("SENTRY_ENABLED", "False").upper() == "TRUE" and self.__sentry_dsn != "" @@ -23,12 +23,12 @@ class Config: @property def environment(self) -> str: """Returns the current app lifecycle.""" - return self.__environment + return os.environ.get("APP_LIFECYCLE", "DEV").upper() @property def version(self) -> str: """Returns the current app version.""" - return self.__version + return os.environ["APP_VERSION"] @property def sentry_enabled(self) -> bool: @@ -46,27 +46,46 @@ class Config: @property def bot_name(self) -> str: """Returns the bot name.""" - return self.__bot_name + return os.environ["BOT_NAME"] @property def webex_token(self) -> str: """Returns the Webex API key.""" - return self.__webex_token + return os.environ["WEBEX_API_KEY"] @property def admin_first_name(self) -> str: """Returns the first name of the bot admin.""" - return self.__admin_first_name + return os.environ["ADMIN_FIRST_NAME"] @property def admin_emails(self) -> list: """Returns a list of admin email addresses.""" - return self.__admin_emails + return os.environ["ADMIN_EMAIL"].split(",") @property def n8n_webhook_url(self) -> str: """Returns the n8n webhook URL.""" - return self.__n8n_webhook_url + return os.environ["N8N_WEBHOOK_URL"] + + @property + def approved_users(self) -> list: + """Returns a list of approved users.""" + emails: list[str] = os.environ.get("APPROVED_USERS", "").split(",") + emails = [i.strip() for i in emails if validate_email_syntax(i.strip())] + return emails + + @property + def approved_rooms(self) -> list: + """Returns a list of approved rooms.""" + rooms: list[str] = os.environ.get("APPROVED_ROOMS", "").split(",") + return [i.strip() for i in rooms] + + @property + def approved_domains(self) -> list: + """Returns a list of approved domains.""" + domains: list[str] = os.environ.get("APPROVED_DOMAINS", "").split(",") + return [i.strip() for i in domains] config: Config = Config() diff --git a/app/utils/helpers.py b/app/utils/helpers.py new file mode 100644 index 0000000..8130666 --- /dev/null +++ b/app/utils/helpers.py @@ -0,0 +1,16 @@ +"""Standalone helper functions.""" + +import re + + +def validate_email_syntax(email: str) -> bool: + """Validate email syntax. + + Args: + email (str): Email address. + + Returns: + bool: True if valid, else False. + """ + pattern = r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$" + return re.match(pattern, email) is not None diff --git a/test.sh b/test.sh index 1fcaf08..ee3861d 100755 --- a/test.sh +++ b/test.sh @@ -1,3 +1,3 @@ -export $(cat .env | xargs) +export $(cat .env.test | xargs) python -B -m app.main -unset APP_LIFECYCLE SENTRY_ENABLED SENTRY_DSN BOT_NAME WEBEX_API_KEY ADMIN_FIRST_NAME ADMIN_EMAIL N8N_WEBHOOK_URL +unset ADMIN_EMAIL ADMIN_FIRST_NAME APP_LIFECYCLE APP_VERSION APPROVED_DOMAINS APPROVED_ROOMS APPROVED_USERS BOT_NAME N8N_WEBHOOK_URL SENTRY_DSN SENTRY_ENABLED WEBEX_API_KEY diff --git a/tests/test_config.py b/tests/test_config.py index b26465f..070f734 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,10 +1,11 @@ #!/usr/bin/env python3 +# ruff: noqa: E402 pylint: disable=wrong-import-position + """Provides test cases for app/utils/config.py.""" import os - vars: dict = { "APP_VERSION": "dev", "BOT_NAME": "TestBot", @@ -13,7 +14,10 @@ vars: dict = { "ADMIN_EMAIL": "test@test.com", "N8N_WEBHOOK_URL": "https://n8n.test.com/webhook/abcdefg", "SENTRY_ENABLED": "false", - "SENTRY_DSN": "http://localhost" + "SENTRY_DSN": "http://localhost", + "APPROVED_USERS": "test@test.com", + "APPROVED_DOMAINS": "test.com", + "APPROVED_ROOMS": "test", } @@ -25,8 +29,18 @@ from app.utils.config import config # pragma: no cover def test_config() -> None: - assert config.bot_name == vars["BOT_NAME"] - assert config.webex_token == vars["WEBEX_API_KEY"] - assert config.admin_first_name == vars["ADMIN_FIRST_NAME"] assert config.admin_emails == vars["ADMIN_EMAIL"].split(",") + assert config.admin_first_name == vars["ADMIN_FIRST_NAME"] + assert config.approved_domains == vars["APPROVED_DOMAINS"].split(",") + assert config.approved_rooms == vars["APPROVED_ROOMS"].split(",") + assert config.approved_users == vars["APPROVED_USERS"].split(",") + assert config.bot_name == vars["BOT_NAME"] assert config.n8n_webhook_url == vars["N8N_WEBHOOK_URL"] + assert config.sentry_enabled == bool(vars["SENTRY_ENABLED"].upper() == "TRUE") + assert config.version == vars["APP_VERSION"] + assert config.webex_token == vars["WEBEX_API_KEY"] + + if config.sentry_enabled: + assert config.sentry_dsn == vars["SENTRY_DSN"] + else: + assert config.sentry_dsn == "" diff --git a/tests/test_helpers.py b/tests/test_helpers.py new file mode 100644 index 0000000..d5ac9a0 --- /dev/null +++ b/tests/test_helpers.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python3 + +"""Provides test cases for app/utils/helpers.py.""" + +from app.utils.helpers import validate_email_syntax # pragma: no cover + + +def test_validate_email_syntax_true() -> None: + """Test validate_email_syntax() with a real email address.""" + email: str = "test@test.com" + assert validate_email_syntax(email) + + +def test_validate_email_syntax_false1() -> None: + """Test validate_email_syntax() with an invalid email address.""" + email: str = "test@test" + assert not validate_email_syntax(email) + + +def test_validate_email_syntax_false2() -> None: + """Test validate_email_syntax() with an invalid email address.""" + email: str = "test" + assert not validate_email_syntax(email)