From 7d5f18f53acb20a29f502df2d8c89106dfb68565 Mon Sep 17 00:00:00 2001 From: "riyaz.shiraguppi" Date: Tue, 25 Aug 2026 14:53:29 -0500 Subject: [PATCH] feat(monitors): add restricted roles skip flag --- datadog_sync/commands/shared/options.py | 11 ++++++ datadog_sync/model/monitors.py | 16 +++++++++ datadog_sync/utils/configuration.py | 3 ++ tests/unit/test_monitors.py | 47 +++++++++++++++++++++++++ 4 files changed, 77 insertions(+) diff --git a/datadog_sync/commands/shared/options.py b/datadog_sync/commands/shared/options.py index 54446826..1e768ba1 100644 --- a/datadog_sync/commands/shared/options.py +++ b/datadog_sync/commands/shared/options.py @@ -576,6 +576,17 @@ def click_config_file_provider(ctx: Context, opts: CustomOptionClass, value: Non help="Override num_flex_logs_retention_days on logs indexes where the field is present.", cls=CustomOptionClass, ), + option( + "--skip-monitors-with-restricted-roles", + required=False, + is_flag=True, + default=False, + show_default=True, + help="Filter out monitors whose source payload has a non-empty restricted_roles list. " + "This is an explicit access-control escape hatch for DDR destinations where " + "role/user activation is not ready yet; filtered monitors are not created or updated.", + cls=CustomOptionClass, + ), option( "--create-global-downtime", required=False, diff --git a/datadog_sync/model/monitors.py b/datadog_sync/model/monitors.py index e8e86c27..bdc155ea 100644 --- a/datadog_sync/model/monitors.py +++ b/datadog_sync/model/monitors.py @@ -239,6 +239,22 @@ async def delete_resource(self, _id: str) -> None: params={"force": "true"}, ) + def filter(self, resource: Dict) -> bool: + if not super().filter(resource): + return False + + if getattr(self.config, "skip_monitors_with_restricted_roles", False) is True and resource.get( + "restricted_roles" + ): + self.config.logger.info( + "filtering monitor with restricted_roles because --skip-monitors-with-restricted-roles is enabled", + resource_type=self.resource_type, + _id=str(resource.get("id", "")), + ) + return False + + return True + def connect_resources(self, _id: str, resource: Dict) -> ResourceConnectionResult: """Drop-aware override. diff --git a/datadog_sync/utils/configuration.py b/datadog_sync/utils/configuration.py index fc7249cd..d7157dcd 100644 --- a/datadog_sync/utils/configuration.py +++ b/datadog_sync/utils/configuration.py @@ -126,6 +126,7 @@ class Configuration(object): prune_force: bool = False prune_dry_run: bool = False destination_logs_intake_url: Optional[str] = None + skip_monitors_with_restricted_roles: bool = False async def init_async(self, cmd: Command): await self.source_client._init_session() @@ -510,6 +511,7 @@ def build_config(cmd: Command, **kwargs: Optional[Any]) -> Configuration: skip_failed_resource_connections = kwargs.get("skip_failed_resource_connections") drop_unresolvable_principals = kwargs.get("drop_unresolvable_principals") or False refresh_destination_state_before_apply = kwargs.get("refresh_destination_state_before_apply") or False + skip_monitors_with_restricted_roles = kwargs.get("skip_monitors_with_restricted_roles") or False max_workers = kwargs.get("max_workers") max_workers_per_type_raw = kwargs.get("max_workers_per_type") # Parse --max-workers-per-type early so malformed input fails BEFORE any @@ -849,6 +851,7 @@ def build_config(cmd: Command, **kwargs: Optional[Any]) -> Configuration: max_concurrent_reads=max_concurrent_reads, transient_failure_threshold_pct=transient_failure_threshold_pct, destination_logs_intake_url=destination_logs_intake_url, + skip_monitors_with_restricted_roles=skip_monitors_with_restricted_roles, ) # Initialize resource classes diff --git a/tests/unit/test_monitors.py b/tests/unit/test_monitors.py index bdc7dc0a..6a17df99 100644 --- a/tests/unit/test_monitors.py +++ b/tests/unit/test_monitors.py @@ -16,6 +16,7 @@ from unittest.mock import MagicMock from datadog_sync.model.monitors import Monitors +from datadog_sync.utils.filter import process_filters from datadog_sync.utils.resource_utils import SkipResource, ResourceConnectionError from datadog_sync.utils.workers import Counter @@ -118,6 +119,52 @@ def test_service_check_null_options_raises_skip(self): asyncio.run(monitors.pre_resource_action_hook("44444", resource)) +class TestMonitorsFilter: + """Filtering behavior specific to monitor access-control escape hatches.""" + + def _make_monitors(self, skip_restricted=False, filters=None, filter_operator="OR"): + config = MagicMock() + config.filters = filters or {} + config.filter_operator = filter_operator + config.skip_monitors_with_restricted_roles = skip_restricted + config.logger = MagicMock() + return Monitors(config) + + def test_restricted_roles_allowed_by_default(self): + monitors = self._make_monitors() + resource = {"id": 269290576, "restricted_roles": ["role-src"]} + + assert monitors.filter(resource) is True + + def test_restricted_roles_filtered_when_flag_enabled(self): + monitors = self._make_monitors(skip_restricted=True) + resource = {"id": 269290576, "restricted_roles": ["role-src"]} + + assert monitors.filter(resource) is False + monitors.config.logger.info.assert_called_once() + + @pytest.mark.parametrize( + "resource", + [ + {"id": 1}, + {"id": 2, "restricted_roles": []}, + {"id": 3, "restricted_roles": None}, + ], + ) + def test_empty_or_missing_restricted_roles_are_not_filtered(self, resource): + monitors = self._make_monitors(skip_restricted=True) + + assert monitors.filter(resource) is True + + def test_existing_filters_still_apply_before_restricted_role_skip(self): + filters = process_filters(["Type=monitors;Name=id;Value=123;Operator=ExactMatch"]) + monitors = self._make_monitors(skip_restricted=True, filters=filters) + resource = {"id": 456, "restricted_roles": ["role-src"]} + + assert monitors.filter(resource) is False + monitors.config.logger.info.assert_not_called() + + class TestMonitorsSchemaMigrations: """Schema migrations that adapt us1-accepted payloads to us3-required shapes."""