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_EMAIL="" | ||||||
| ADMIN_FIRST_NAME="" | 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="" | BOT_NAME="" | ||||||
| N8N_WEBHOOK_URL="" | N8N_WEBHOOK_URL="" | ||||||
|  | SENTRY_DSN="" | ||||||
|  | SENTRY_ENABLED="False" | ||||||
| WEBEX_API_KEY="" | WEBEX_API_KEY="" | ||||||
|   | |||||||
| @@ -9,8 +9,14 @@ Add tasks to a Wekan to do list via Webex and n8n. | |||||||
| 3. Edit `.env` as required: | 3. Edit `.env` as required: | ||||||
|     - `ADMIN_EMAIL` - comma-separated list of admin (who owns the to-do list) email addresses |     - `ADMIN_EMAIL` - comma-separated list of admin (who owns the to-do list) email addresses | ||||||
|     - `ADMIN_FIRST_NAME` - admin first name |     - `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 |     - `BOT_NAME` - Webex bot name | ||||||
|     - `N8N_WEBHOOK_URL` - n8n webhook URL |     - `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 |     - `WEBEX_API_KEY` - Webex API key | ||||||
|  |  | ||||||
| ## How to use | ## How to use | ||||||
|   | |||||||
| @@ -2,14 +2,12 @@ | |||||||
|  |  | ||||||
| import sentry_sdk | import sentry_sdk | ||||||
| from sentry_sdk.integrations.stdlib import StdlibIntegration | from sentry_sdk.integrations.stdlib import StdlibIntegration | ||||||
|  |  | ||||||
| from webex_bot.webex_bot import WebexBot | from webex_bot.webex_bot import WebexBot | ||||||
|  |  | ||||||
| from app.commands.exit import ExitCommand | from app.commands.exit import ExitCommand | ||||||
| from app.commands.submit_task import SubmitTaskCommand | from app.commands.submit_task import SubmitTaskCommand | ||||||
| from app.utils.config import config | from app.utils.config import config | ||||||
|  |  | ||||||
|  |  | ||||||
| if config.sentry_enabled: | if config.sentry_enabled: | ||||||
|     apm = sentry_sdk.init( |     apm = sentry_sdk.init( | ||||||
|         dsn=config.sentry_dsn, |         dsn=config.sentry_dsn, | ||||||
| @@ -17,7 +15,7 @@ if config.sentry_enabled: | |||||||
|         environment=config.environment, |         environment=config.environment, | ||||||
|         release=config.version, |         release=config.version, | ||||||
|         integrations=[StdlibIntegration()], |         integrations=[StdlibIntegration()], | ||||||
|         spotlight=True |         spotlight=True, | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -26,7 +24,9 @@ def create_bot() -> WebexBot: | |||||||
|     webex_bot: WebexBot = WebexBot( |     webex_bot: WebexBot = WebexBot( | ||||||
|         bot_name=config.bot_name, |         bot_name=config.bot_name, | ||||||
|         teams_bot_token=config.webex_token, |         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.commands.clear() | ||||||
|     webex_bot.add_command(SubmitTaskCommand()) |     webex_bot.add_command(SubmitTaskCommand()) | ||||||
|   | |||||||
| @@ -2,9 +2,12 @@ | |||||||
|  |  | ||||||
| import os | import os | ||||||
|  |  | ||||||
|  | from app.utils.helpers import validate_email_syntax | ||||||
|  |  | ||||||
|  |  | ||||||
| class Config: | class Config: | ||||||
|     """Configuration module.""" |     """Configuration module.""" | ||||||
|  |  | ||||||
|     def __init__(self) -> None: |     def __init__(self) -> None: | ||||||
|         """Configuration module.""" |         """Configuration module.""" | ||||||
|         self.__environment: str = os.environ.get("APP_LIFECYCLE", "DEV").upper() |         self.__environment: str = os.environ.get("APP_LIFECYCLE", "DEV").upper() | ||||||
| @@ -68,5 +71,24 @@ class Config: | |||||||
|         """Returns the n8n webhook URL.""" |         """Returns the n8n webhook URL.""" | ||||||
|         return self.__n8n_webhook_url |         return self.__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() | config: Config = Config() | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								app/utils/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								app/utils/helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | 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 | 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 | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ | |||||||
|  |  | ||||||
| import os | import os | ||||||
|  |  | ||||||
|  |  | ||||||
| vars: dict = { | vars: dict = { | ||||||
|     "APP_VERSION": "dev", |     "APP_VERSION": "dev", | ||||||
|     "BOT_NAME": "TestBot", |     "BOT_NAME": "TestBot", | ||||||
| @@ -13,7 +12,10 @@ vars: dict = { | |||||||
|     "ADMIN_EMAIL": "test@test.com", |     "ADMIN_EMAIL": "test@test.com", | ||||||
|     "N8N_WEBHOOK_URL": "https://n8n.test.com/webhook/abcdefg", |     "N8N_WEBHOOK_URL": "https://n8n.test.com/webhook/abcdefg", | ||||||
|     "SENTRY_ENABLED": "false", |     "SENTRY_ENABLED": "false", | ||||||
|     "SENTRY_DSN": "http://localhost" |     "SENTRY_DSN": "http://localhost", | ||||||
|  |     "APPROVED_USERS": "test@test.com", | ||||||
|  |     "APPROVED_DOMAINS": "test.com", | ||||||
|  |     "APPROVED_ROOMS": "test", | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -21,12 +23,22 @@ for var, value in vars.items(): | |||||||
|     os.environ[var] = value |     os.environ[var] = value | ||||||
|  |  | ||||||
| # needs to be imported AFTER environment variables are set | # needs to be imported AFTER environment variables are set | ||||||
| from app.utils.config import config  # pragma: no cover | from app.utils.config import config  # pragma: no cover  # noqa: E402 | ||||||
|  |  | ||||||
|  |  | ||||||
| def test_config() -> None: | 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_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.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 == "" | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								tests/test_helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								tests/test_helpers.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | #!/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: | ||||||
|  |     email: str = "test@test.com" | ||||||
|  |     assert validate_email_syntax(email) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_validate_email_syntax_false1() -> None: | ||||||
|  |     email: str = "test@test" | ||||||
|  |     assert not validate_email_syntax(email) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def test_validate_email_syntax_false2() -> None: | ||||||
|  |     email: str = "test" | ||||||
|  |     assert not validate_email_syntax(email) | ||||||
		Reference in New Issue
	
	Block a user