feat(security): add approved rooms/users/domains as env variables #277
| @@ -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="" | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -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()) | ||||
|   | ||||
| @@ -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() | ||||
|   | ||||
							
								
								
									
										16
									
								
								app/utils/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								app/utils/helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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 | ||||
							
								
								
									
										4
									
								
								test.sh
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								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 | ||||
|   | ||||
| @@ -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 == "" | ||||
|   | ||||
							
								
								
									
										23
									
								
								tests/test_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								tests/test_helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -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) | ||||
		Reference in New Issue
	
	Block a user