Skip to content

Commit f0b1c4c

Browse files
authored
fix(api): update unique constraint for Provider model to exclude soft… (#9054)
1 parent a73a79f commit f0b1c4c

File tree

4 files changed

+186
-1
lines changed

4 files changed

+186
-1
lines changed

api/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ All notable changes to the **Prowler API** are documented in this file.
1818

1919
---
2020

21+
## [1.14.2] (Prowler 5.13.2)
22+
23+
### Fixed
24+
- Update unique constraint for `Provider` model to exclude soft-deleted entries, resolving duplicate errors when re-deleting providers.
25+
2126
## [1.14.1] (Prowler 5.13.1)
2227

2328
### Fixed
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 5.1.13 on 2025-11-06 09:20
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("api", "0055_mongodbatlas_provider"),
9+
]
10+
11+
operations = [
12+
migrations.RemoveConstraint(
13+
model_name="provider",
14+
name="unique_provider_uids",
15+
),
16+
migrations.AddConstraint(
17+
model_name="provider",
18+
constraint=models.UniqueConstraint(
19+
condition=models.Q(("is_deleted", False)),
20+
fields=("tenant_id", "provider", "uid"),
21+
name="unique_provider_uids",
22+
),
23+
),
24+
]

api/src/backend/api/models.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,8 @@ class Meta(RowLevelSecurityProtectedModel.Meta):
425425

426426
constraints = [
427427
models.UniqueConstraint(
428-
fields=("tenant_id", "provider", "uid", "is_deleted"),
428+
fields=("tenant_id", "provider", "uid"),
429+
condition=Q(is_deleted=False),
429430
name="unique_provider_uids",
430431
),
431432
RowLevelSecurityConstraint(

api/src/backend/api/tests/test_views.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,161 @@ def test_providers_create_valid(self, authenticated_client, provider_json_payloa
11711171
assert Provider.objects.get().uid == provider_json_payload["uid"]
11721172
assert Provider.objects.get().alias == provider_json_payload["alias"]
11731173

1174+
@pytest.mark.parametrize(
1175+
"provider_json_payload",
1176+
(
1177+
[
1178+
{"provider": "aws", "uid": "111111111111", "alias": "test"},
1179+
{"provider": "gcp", "uid": "a12322-test54321", "alias": "test"},
1180+
{
1181+
"provider": "kubernetes",
1182+
"uid": "kubernetes-test-123456789",
1183+
"alias": "test",
1184+
},
1185+
{
1186+
"provider": "kubernetes",
1187+
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
1188+
"alias": "EKS",
1189+
},
1190+
{
1191+
"provider": "kubernetes",
1192+
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
1193+
"alias": "GKE",
1194+
},
1195+
{
1196+
"provider": "kubernetes",
1197+
"uid": "gke_project/cluster-name",
1198+
"alias": "GKE",
1199+
},
1200+
{
1201+
"provider": "kubernetes",
1202+
"uid": "admin@k8s-demo",
1203+
"alias": "test",
1204+
},
1205+
{
1206+
"provider": "azure",
1207+
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
1208+
"alias": "test",
1209+
},
1210+
{
1211+
"provider": "m365",
1212+
"uid": "TestingPro.onmicrosoft.com",
1213+
"alias": "test",
1214+
},
1215+
{
1216+
"provider": "m365",
1217+
"uid": "subdomain.domain.es",
1218+
"alias": "test",
1219+
},
1220+
{
1221+
"provider": "m365",
1222+
"uid": "microsoft.net",
1223+
"alias": "test",
1224+
},
1225+
{
1226+
"provider": "m365",
1227+
"uid": "subdomain1.subdomain2.subdomain3.subdomain4.domain.net",
1228+
"alias": "test",
1229+
},
1230+
{
1231+
"provider": "github",
1232+
"uid": "test-user",
1233+
"alias": "test",
1234+
},
1235+
{
1236+
"provider": "github",
1237+
"uid": "test-organization",
1238+
"alias": "GitHub Org",
1239+
},
1240+
{
1241+
"provider": "github",
1242+
"uid": "prowler-cloud",
1243+
"alias": "Prowler",
1244+
},
1245+
{
1246+
"provider": "github",
1247+
"uid": "microsoft",
1248+
"alias": "Microsoft",
1249+
},
1250+
{
1251+
"provider": "github",
1252+
"uid": "a12345678901234567890123456789012345678",
1253+
"alias": "Long Username",
1254+
},
1255+
]
1256+
),
1257+
)
1258+
@patch("api.v1.views.Task.objects.get")
1259+
@patch("api.v1.views.delete_provider_task.delay")
1260+
def test_providers_soft_delete(
1261+
self,
1262+
mock_delete_task,
1263+
mock_task_get,
1264+
authenticated_client,
1265+
provider_json_payload,
1266+
tasks_fixture,
1267+
):
1268+
# Mock the Celery task response
1269+
prowler_task = tasks_fixture[0]
1270+
task_mock = Mock()
1271+
task_mock.id = prowler_task.id
1272+
mock_delete_task.return_value = task_mock
1273+
mock_task_get.return_value = prowler_task
1274+
1275+
# 1.Create a provider
1276+
response = authenticated_client.post(
1277+
reverse("provider-list"), data=provider_json_payload, format="json"
1278+
)
1279+
assert response.status_code == status.HTTP_201_CREATED
1280+
assert Provider.objects.count() == 1
1281+
provider_id = response.json()["data"]["id"]
1282+
1283+
# 2. Soft delete the provider using the actual API endpoint
1284+
response = authenticated_client.delete(
1285+
reverse("provider-detail", kwargs={"pk": provider_id})
1286+
)
1287+
assert response.status_code == status.HTTP_202_ACCEPTED
1288+
assert Provider.objects.count() == 0
1289+
assert Provider.all_objects.count() == 1
1290+
1291+
mock_delete_task.assert_called_once_with(
1292+
provider_id=str(provider_id), tenant_id=ANY
1293+
)
1294+
1295+
# 3. Create a provider with the same UID should succeed (since the old one is soft deleted)
1296+
response = authenticated_client.post(
1297+
reverse("provider-list"), data=provider_json_payload, format="json"
1298+
)
1299+
assert response.status_code == status.HTTP_201_CREATED
1300+
assert Provider.objects.count() == 1
1301+
assert Provider.all_objects.count() == 2
1302+
provider_id = response.json()["data"]["id"]
1303+
1304+
# 4. Creating another provider with the same UID should fail (duplicate)
1305+
response = authenticated_client.post(
1306+
reverse("provider-list"), data=provider_json_payload, format="json"
1307+
)
1308+
assert response.status_code == status.HTTP_400_BAD_REQUEST
1309+
1310+
mock_delete_task.reset_mock()
1311+
mock_delete_task.return_value = task_mock
1312+
1313+
# 5. Delete the second provider
1314+
response = authenticated_client.delete(
1315+
reverse("provider-detail", kwargs={"pk": provider_id})
1316+
)
1317+
assert response.status_code == status.HTTP_202_ACCEPTED
1318+
assert Provider.objects.count() == 0
1319+
assert Provider.all_objects.count() == 2
1320+
1321+
# 6. Creating a provider with the same UID should succeed again
1322+
response = authenticated_client.post(
1323+
reverse("provider-list"), data=provider_json_payload, format="json"
1324+
)
1325+
assert response.status_code == status.HTTP_201_CREATED
1326+
assert Provider.objects.count() == 1
1327+
assert Provider.all_objects.count() == 3
1328+
11741329
@pytest.mark.parametrize(
11751330
"provider_json_payload, error_code, error_pointer",
11761331
(

0 commit comments

Comments
 (0)