|
12 | 12 | from starlette.status import HTTP_200_OK, HTTP_204_NO_CONTENT, HTTP_404_NOT_FOUND |
13 | 13 |
|
14 | 14 | from fides.api.common_exceptions import ( |
| 15 | + AwaitingAsyncProcessing, |
15 | 16 | AwaitingAsyncTask, |
16 | 17 | ClientUnsuccessfulException, |
17 | 18 | ConnectionException, |
|
22 | 23 | from fides.api.graph.graph import DatasetGraph, Node |
23 | 24 | from fides.api.graph.traversal import Traversal, TraversalNode |
24 | 25 | from fides.api.models.consent_automation import ConsentAutomation |
25 | | -from fides.api.models.policy import Policy |
| 26 | +from fides.api.models.policy import ActionType, Policy, Rule, RuleTarget |
26 | 27 | from fides.api.models.privacy_notice import UserConsentPreference |
27 | 28 | from fides.api.models.privacy_request import PrivacyRequest, RequestTask |
28 | 29 | from fides.api.models.worker_task import ExecutionLogStatus |
@@ -1121,6 +1122,23 @@ def async_graph(self, saas_example_async_dataset_config, db, privacy_request): |
1121 | 1122 | db, privacy_request, traversal_nodes, end_nodes, graph |
1122 | 1123 | ) |
1123 | 1124 |
|
| 1125 | + @pytest.fixture(scope="function") |
| 1126 | + def async_graph_polling( |
| 1127 | + self, saas_async_polling_example_dataset_config, db, privacy_request |
| 1128 | + ): |
| 1129 | + # Build proper async graph with persisted request tasks for polling tests |
| 1130 | + async_graph = saas_async_polling_example_dataset_config.get_graph() |
| 1131 | + graph = DatasetGraph(async_graph) |
| 1132 | + traversal = Traversal(graph, {"email": "customer-1@example.com"}) |
| 1133 | + traversal_nodes = {} |
| 1134 | + end_nodes = traversal.traverse(traversal_nodes, collect_tasks_fn) |
| 1135 | + persist_new_access_request_tasks( |
| 1136 | + db, privacy_request, traversal, traversal_nodes, end_nodes, graph |
| 1137 | + ) |
| 1138 | + persist_initial_erasure_request_tasks( |
| 1139 | + db, privacy_request, traversal_nodes, end_nodes, graph |
| 1140 | + ) |
| 1141 | + |
1124 | 1142 | @mock.patch("fides.api.service.connectors.saas_connector.AuthenticatedClient.send") |
1125 | 1143 | def test_read_request_expects_async_results( |
1126 | 1144 | self, |
@@ -1306,3 +1324,195 @@ def test_callback_succeeded_mask_data( |
1306 | 1324 | ) |
1307 | 1325 | == 5 |
1308 | 1326 | ) |
| 1327 | + |
| 1328 | + @mock.patch( |
| 1329 | + "fides.api.service.connectors.saas_connector.SaaSConnector.create_client" |
| 1330 | + ) |
| 1331 | + def test_guard_access_request_with_access_policy( |
| 1332 | + self, |
| 1333 | + mock_create_client, |
| 1334 | + privacy_request, |
| 1335 | + saas_async_polling_example_connection_config, |
| 1336 | + async_graph_polling, |
| 1337 | + ): |
| 1338 | + """ |
| 1339 | + Test that guard_access_request allows async access requests to run |
| 1340 | + when the policy has access rules (access request scenario). |
| 1341 | + """ |
| 1342 | + connector: SaaSConnector = get_connector( |
| 1343 | + saas_async_polling_example_connection_config |
| 1344 | + ) |
| 1345 | + mock_create_client.return_value = mock.MagicMock() |
| 1346 | + |
| 1347 | + # Get access request task |
| 1348 | + request_task = privacy_request.access_tasks.filter( |
| 1349 | + RequestTask.collection_name == "user" |
| 1350 | + ).first() |
| 1351 | + execution_node = ExecutionNode(request_task) |
| 1352 | + |
| 1353 | + # Policy has access rules, so guard should return True and async_retrieve_data should be called |
| 1354 | + with pytest.raises(AwaitingAsyncProcessing): |
| 1355 | + connector.retrieve_data( |
| 1356 | + execution_node, |
| 1357 | + privacy_request.policy, |
| 1358 | + privacy_request, |
| 1359 | + request_task, |
| 1360 | + {}, |
| 1361 | + ) |
| 1362 | + |
| 1363 | + @mock.patch( |
| 1364 | + "fides.api.service.connectors.saas_connector.SaaSConnector.create_client" |
| 1365 | + ) |
| 1366 | + def test_guard_access_request_with_erasure_only_policy( |
| 1367 | + self, |
| 1368 | + mock_create_client, |
| 1369 | + db, |
| 1370 | + privacy_request, |
| 1371 | + saas_async_polling_example_connection_config, |
| 1372 | + async_graph_polling, |
| 1373 | + oauth_client, |
| 1374 | + ): |
| 1375 | + """ |
| 1376 | + Test that guard_access_request skips async access requests |
| 1377 | + when the policy has no access rules (erasure-only request scenario). |
| 1378 | + This test ensures coverage of the logger.info and return [] lines. |
| 1379 | + Uses polling async strategy to test the guard clause. |
| 1380 | + """ |
| 1381 | + # Create an erasure-only policy (no access rules) |
| 1382 | + erasure_only_policy = Policy.create( |
| 1383 | + db=db, |
| 1384 | + data={ |
| 1385 | + "name": "Erasure Only Policy", |
| 1386 | + "key": "erasure_only_policy_test", |
| 1387 | + "client_id": oauth_client.id, |
| 1388 | + }, |
| 1389 | + ) |
| 1390 | + |
| 1391 | + erasure_rule = Rule.create( |
| 1392 | + db=db, |
| 1393 | + data={ |
| 1394 | + "action_type": ActionType.erasure, |
| 1395 | + "name": "Erasure Rule", |
| 1396 | + "key": "erasure_rule_test", |
| 1397 | + "policy_id": erasure_only_policy.id, |
| 1398 | + "masking_strategy": { |
| 1399 | + "strategy": "null_rewrite", |
| 1400 | + "configuration": {}, |
| 1401 | + }, |
| 1402 | + "client_id": oauth_client.id, |
| 1403 | + }, |
| 1404 | + ) |
| 1405 | + |
| 1406 | + RuleTarget.create( |
| 1407 | + db=db, |
| 1408 | + data={ |
| 1409 | + "data_category": "user.name", |
| 1410 | + "rule_id": erasure_rule.id, |
| 1411 | + "client_id": oauth_client.id, |
| 1412 | + }, |
| 1413 | + ) |
| 1414 | + |
| 1415 | + connector: SaaSConnector = get_connector( |
| 1416 | + saas_async_polling_example_connection_config |
| 1417 | + ) |
| 1418 | + |
| 1419 | + # Get access request task |
| 1420 | + request_task = privacy_request.access_tasks.filter( |
| 1421 | + RequestTask.collection_name == "user" |
| 1422 | + ).first() |
| 1423 | + execution_node = ExecutionNode(request_task) |
| 1424 | + |
| 1425 | + # Verify guard_access_request returns False for erasure-only policy |
| 1426 | + assert connector.guard_access_request(erasure_only_policy) is False |
| 1427 | + |
| 1428 | + result = connector.retrieve_data( |
| 1429 | + execution_node, |
| 1430 | + erasure_only_policy, |
| 1431 | + privacy_request, |
| 1432 | + request_task, |
| 1433 | + {}, |
| 1434 | + ) |
| 1435 | + |
| 1436 | + # Should return empty list without calling async_retrieve_data |
| 1437 | + assert result == [] |
| 1438 | + |
| 1439 | + @mock.patch( |
| 1440 | + "fides.api.service.connectors.saas_connector.SaaSConnector.create_client" |
| 1441 | + ) |
| 1442 | + def test_callback_requests_ignore_guard_clause( |
| 1443 | + self, |
| 1444 | + mock_create_client, |
| 1445 | + db, |
| 1446 | + privacy_request, |
| 1447 | + saas_async_example_connection_config, |
| 1448 | + async_graph, |
| 1449 | + oauth_client, |
| 1450 | + ): |
| 1451 | + """ |
| 1452 | + Test that callback requests ignore the guard clause entirely. |
| 1453 | + Even if guard_access_request returns False (erasure-only policy), |
| 1454 | + callback requests should still proceed and raise AwaitingAsyncTask. |
| 1455 | + """ |
| 1456 | + # Create an erasure-only policy (no access rules) |
| 1457 | + erasure_only_policy = Policy.create( |
| 1458 | + db=db, |
| 1459 | + data={ |
| 1460 | + "name": "Erasure Only Policy Callback Test", |
| 1461 | + "key": "erasure_only_policy_callback_test", |
| 1462 | + "client_id": oauth_client.id, |
| 1463 | + }, |
| 1464 | + ) |
| 1465 | + |
| 1466 | + erasure_rule = Rule.create( |
| 1467 | + db=db, |
| 1468 | + data={ |
| 1469 | + "action_type": ActionType.erasure, |
| 1470 | + "name": "Erasure Rule Callback Test", |
| 1471 | + "key": "erasure_rule_callback_test", |
| 1472 | + "policy_id": erasure_only_policy.id, |
| 1473 | + "masking_strategy": { |
| 1474 | + "strategy": "null_rewrite", |
| 1475 | + "configuration": {}, |
| 1476 | + }, |
| 1477 | + "client_id": oauth_client.id, |
| 1478 | + }, |
| 1479 | + ) |
| 1480 | + |
| 1481 | + RuleTarget.create( |
| 1482 | + db=db, |
| 1483 | + data={ |
| 1484 | + "data_category": "user.name", |
| 1485 | + "rule_id": erasure_rule.id, |
| 1486 | + "client_id": oauth_client.id, |
| 1487 | + }, |
| 1488 | + ) |
| 1489 | + |
| 1490 | + connector: SaaSConnector = get_connector(saas_async_example_connection_config) |
| 1491 | + # Mock the client and its send method to allow async callback flow |
| 1492 | + mock_client = mock.MagicMock() |
| 1493 | + mock_send_response = mock.MagicMock() |
| 1494 | + mock_send_response.json.return_value = {"id": "123"} |
| 1495 | + mock_client.send.return_value = mock_send_response |
| 1496 | + mock_create_client.return_value = mock_client |
| 1497 | + |
| 1498 | + # Get access request task |
| 1499 | + request_task = privacy_request.access_tasks.filter( |
| 1500 | + RequestTask.collection_name == "user" |
| 1501 | + ).first() |
| 1502 | + execution_node = ExecutionNode(request_task) |
| 1503 | + |
| 1504 | + # Verify guard_access_request returns False for erasure-only policy |
| 1505 | + assert connector.guard_access_request(erasure_only_policy) is False |
| 1506 | + |
| 1507 | + # Even though guard_access_request returns False, callback requests |
| 1508 | + # should ignore the guard and always proceed with common requests. |
| 1509 | + connector.retrieve_data( |
| 1510 | + execution_node, |
| 1511 | + erasure_only_policy, |
| 1512 | + privacy_request, |
| 1513 | + request_task, |
| 1514 | + {}, |
| 1515 | + ) |
| 1516 | + |
| 1517 | + # Verify that the async callback flow was triggered (client.send was called) |
| 1518 | + assert mock_client.send.called |
0 commit comments