Skip to content

Commit c58be57

Browse files
committed
fix(api): update unique constraint for Provider model to exclude soft… (#9054)
(cherry picked from commit f0b1c4c) # Conflicts: # api/CHANGELOG.md
1 parent 28eb1ee commit c58be57

4 files changed

Lines changed: 202 additions & 1 deletion

File tree

api/CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,27 @@
22

33
All notable changes to the **Prowler API** are documented in this file.
44

5+
## [1.15.0] (Prowler UNRELEASED)
6+
7+
### Added
8+
- IaC (Infrastructure as Code) provider support for remote repositories [(#8751)](https://github.com/prowler-cloud/prowler/pull/8751)
9+
- Extend `GET /api/v1/providers` with provider-type filters and optional pagination disable to support the new Overview filters [(#8975)](https://github.com/prowler-cloud/prowler/pull/8975)
10+
- New endpoint to retrieve the number of providers grouped by provider type [(#8975)](https://github.com/prowler-cloud/prowler/pull/8975)
11+
- Support for configuring multiple LLM providers [(#8772)](https://github.com/prowler-cloud/prowler/pull/8772)
12+
- Support C5 compliance framework for Azure provider [(#9081)](https://github.com/prowler-cloud/prowler/pull/9081)
13+
- Support for Oracle Cloud Infrastructure (OCI) provider [(#8927)](https://github.com/prowler-cloud/prowler/pull/8927)
14+
- Support muting findings based on simple rules with custom reason [(#9051)](https://github.com/prowler-cloud/prowler/pull/9051)
15+
- Support C5 compliance framework for the GCP provider [(#9097)](https://github.com/prowler-cloud/prowler/pull/9097)
16+
- Support for Amazon Bedrock and OpenAI compatible providers in Lighthouse AI [(#8957)](https://github.com/prowler-cloud/prowler/pull/8957)
17+
- Support for MongoDB Atlas provider [(#9167)](https://github.com/prowler-cloud/prowler/pull/9167)
18+
19+
---
20+
21+
## [1.14.2] (Prowler 5.14.0)
22+
23+
### Fixed
24+
- Update unique constraint for `Provider` model to exclude soft-deleted entries, resolving duplicate errors when re-deleting providers.
25+
526
## [1.14.1] (Prowler 5.13.1)
627

728
### 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
@@ -388,7 +388,8 @@ class Meta(RowLevelSecurityProtectedModel.Meta):
388388

389389
constraints = [
390390
models.UniqueConstraint(
391-
fields=("tenant_id", "provider", "uid", "is_deleted"),
391+
fields=("tenant_id", "provider", "uid"),
392+
condition=Q(is_deleted=False),
392393
name="unique_provider_uids",
393394
),
394395
RowLevelSecurityConstraint(

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

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

1084+
@pytest.mark.parametrize(
1085+
"provider_json_payload",
1086+
(
1087+
[
1088+
{"provider": "aws", "uid": "111111111111", "alias": "test"},
1089+
{"provider": "gcp", "uid": "a12322-test54321", "alias": "test"},
1090+
{
1091+
"provider": "kubernetes",
1092+
"uid": "kubernetes-test-123456789",
1093+
"alias": "test",
1094+
},
1095+
{
1096+
"provider": "kubernetes",
1097+
"uid": "arn:aws:eks:us-east-1:111122223333:cluster/test-cluster-long-name-123456789",
1098+
"alias": "EKS",
1099+
},
1100+
{
1101+
"provider": "kubernetes",
1102+
"uid": "gke_aaaa-dev_europe-test1_dev-aaaa-test-cluster-long-name-123456789",
1103+
"alias": "GKE",
1104+
},
1105+
{
1106+
"provider": "kubernetes",
1107+
"uid": "gke_project/cluster-name",
1108+
"alias": "GKE",
1109+
},
1110+
{
1111+
"provider": "kubernetes",
1112+
"uid": "admin@k8s-demo",
1113+
"alias": "test",
1114+
},
1115+
{
1116+
"provider": "azure",
1117+
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
1118+
"alias": "test",
1119+
},
1120+
{
1121+
"provider": "m365",
1122+
"uid": "TestingPro.onmicrosoft.com",
1123+
"alias": "test",
1124+
},
1125+
{
1126+
"provider": "m365",
1127+
"uid": "subdomain.domain.es",
1128+
"alias": "test",
1129+
},
1130+
{
1131+
"provider": "m365",
1132+
"uid": "microsoft.net",
1133+
"alias": "test",
1134+
},
1135+
{
1136+
"provider": "m365",
1137+
"uid": "subdomain1.subdomain2.subdomain3.subdomain4.domain.net",
1138+
"alias": "test",
1139+
},
1140+
{
1141+
"provider": "github",
1142+
"uid": "test-user",
1143+
"alias": "test",
1144+
},
1145+
{
1146+
"provider": "github",
1147+
"uid": "test-organization",
1148+
"alias": "GitHub Org",
1149+
},
1150+
{
1151+
"provider": "github",
1152+
"uid": "prowler-cloud",
1153+
"alias": "Prowler",
1154+
},
1155+
{
1156+
"provider": "github",
1157+
"uid": "microsoft",
1158+
"alias": "Microsoft",
1159+
},
1160+
{
1161+
"provider": "github",
1162+
"uid": "a12345678901234567890123456789012345678",
1163+
"alias": "Long Username",
1164+
},
1165+
]
1166+
),
1167+
)
1168+
@patch("api.v1.views.Task.objects.get")
1169+
@patch("api.v1.views.delete_provider_task.delay")
1170+
def test_providers_soft_delete(
1171+
self,
1172+
mock_delete_task,
1173+
mock_task_get,
1174+
authenticated_client,
1175+
provider_json_payload,
1176+
tasks_fixture,
1177+
):
1178+
# Mock the Celery task response
1179+
prowler_task = tasks_fixture[0]
1180+
task_mock = Mock()
1181+
task_mock.id = prowler_task.id
1182+
mock_delete_task.return_value = task_mock
1183+
mock_task_get.return_value = prowler_task
1184+
1185+
# 1.Create a provider
1186+
response = authenticated_client.post(
1187+
reverse("provider-list"), data=provider_json_payload, format="json"
1188+
)
1189+
assert response.status_code == status.HTTP_201_CREATED
1190+
assert Provider.objects.count() == 1
1191+
provider_id = response.json()["data"]["id"]
1192+
1193+
# 2. Soft delete the provider using the actual API endpoint
1194+
response = authenticated_client.delete(
1195+
reverse("provider-detail", kwargs={"pk": provider_id})
1196+
)
1197+
assert response.status_code == status.HTTP_202_ACCEPTED
1198+
assert Provider.objects.count() == 0
1199+
assert Provider.all_objects.count() == 1
1200+
1201+
mock_delete_task.assert_called_once_with(
1202+
provider_id=str(provider_id), tenant_id=ANY
1203+
)
1204+
1205+
# 3. Create a provider with the same UID should succeed (since the old one is soft deleted)
1206+
response = authenticated_client.post(
1207+
reverse("provider-list"), data=provider_json_payload, format="json"
1208+
)
1209+
assert response.status_code == status.HTTP_201_CREATED
1210+
assert Provider.objects.count() == 1
1211+
assert Provider.all_objects.count() == 2
1212+
provider_id = response.json()["data"]["id"]
1213+
1214+
# 4. Creating another provider with the same UID should fail (duplicate)
1215+
response = authenticated_client.post(
1216+
reverse("provider-list"), data=provider_json_payload, format="json"
1217+
)
1218+
assert response.status_code == status.HTTP_400_BAD_REQUEST
1219+
1220+
mock_delete_task.reset_mock()
1221+
mock_delete_task.return_value = task_mock
1222+
1223+
# 5. Delete the second provider
1224+
response = authenticated_client.delete(
1225+
reverse("provider-detail", kwargs={"pk": provider_id})
1226+
)
1227+
assert response.status_code == status.HTTP_202_ACCEPTED
1228+
assert Provider.objects.count() == 0
1229+
assert Provider.all_objects.count() == 2
1230+
1231+
# 6. Creating a provider with the same UID should succeed again
1232+
response = authenticated_client.post(
1233+
reverse("provider-list"), data=provider_json_payload, format="json"
1234+
)
1235+
assert response.status_code == status.HTTP_201_CREATED
1236+
assert Provider.objects.count() == 1
1237+
assert Provider.all_objects.count() == 3
1238+
10841239
@pytest.mark.parametrize(
10851240
"provider_json_payload, error_code, error_pointer",
10861241
(

0 commit comments

Comments
 (0)