ref(rbac): enable relationship creation when objects is created (#6238)

This commit is contained in:
Adrián Jesús Peña Rodríguez
2024-12-18 16:45:32 +01:00
committed by GitHub
parent 8cc8f76204
commit 33857109c9
6 changed files with 930 additions and 156 deletions

View File

@@ -309,7 +309,7 @@ class ProviderGroup(RowLevelSecurityProtectedModel):
]
class JSONAPIMeta:
resource_name = "provider-group"
resource_name = "provider-groups"
class ProviderGroupMembership(RowLevelSecurityProtectedModel):
@@ -926,7 +926,7 @@ class Role(RowLevelSecurityProtectedModel):
]
class JSONAPIMeta:
resource_name = "role"
resource_name = "roles"
class RoleProviderGroupRelationship(RowLevelSecurityProtectedModel):

View File

@@ -1696,7 +1696,7 @@ paths:
summary: List all provider groups
parameters:
- in: query
name: fields[provider-group]
name: fields[provider-groups]
schema:
type: array
items:
@@ -1833,13 +1833,13 @@ paths:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/ProviderGroupRequest'
$ref: '#/components/schemas/ProviderGroupCreateRequest'
application/x-www-form-urlencoded:
schema:
$ref: '#/components/schemas/ProviderGroupRequest'
$ref: '#/components/schemas/ProviderGroupCreateRequest'
multipart/form-data:
schema:
$ref: '#/components/schemas/ProviderGroupRequest'
$ref: '#/components/schemas/ProviderGroupCreateRequest'
required: true
security:
- jwtAuth: []
@@ -1848,7 +1848,7 @@ paths:
content:
application/vnd.api+json:
schema:
$ref: '#/components/schemas/ProviderGroupResponse'
$ref: '#/components/schemas/ProviderGroupCreateResponse'
description: ''
/api/v1/provider-groups/{id}:
get:
@@ -1858,7 +1858,7 @@ paths:
summary: Retrieve data from a provider group
parameters:
- in: query
name: fields[provider-group]
name: fields[provider-groups]
schema:
type: array
items:
@@ -2939,7 +2939,7 @@ paths:
summary: List all roles
parameters:
- in: query
name: fields[role]
name: fields[roles]
schema:
type: array
items:
@@ -3138,7 +3138,7 @@ paths:
summary: Retrieve data from a role
parameters:
- in: query
name: fields[role]
name: fields[roles]
schema:
type: array
items:
@@ -3750,6 +3750,7 @@ paths:
name: filter[state]
schema:
type: string
title: Task State
enum:
- available
- cancelled
@@ -3758,6 +3759,8 @@ paths:
- failed
- scheduled
description: |-
Current state of the task being run
* `available` - Available
* `scheduled` - Scheduled
* `executing` - Executing
@@ -5536,7 +5539,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -5546,8 +5549,8 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
description: A related resource object from type roles
title: roles
inviter:
type: object
properties:
@@ -5689,7 +5692,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -5699,8 +5702,8 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
description: A related resource object from type roles
title: roles
required:
- roles
InvitationCreateRequest:
@@ -5796,7 +5799,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
@@ -5806,8 +5809,8 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
description: A related resource object from type roles
title: roles
required:
- roles
required:
@@ -5887,7 +5890,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -5897,8 +5900,8 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
description: A related resource object from type roles
title: roles
required:
- roles
InvitationUpdateResponse:
@@ -6422,7 +6425,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
@@ -6432,8 +6435,8 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
description: A related resource object from type roles
title: roles
required:
- roles
required:
@@ -6486,7 +6489,7 @@ components:
member is used to describe resource objects that share common attributes
and relationships.
enum:
- provider-group
- provider-groups
id:
type: string
format: uuid
@@ -6497,8 +6500,75 @@ components:
type: string
minLength: 1
maxLength: 255
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
required:
- name
relationships:
type: object
properties:
providers:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- providers
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
common attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type providers
title: providers
roles:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
common attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type roles
title: roles
required:
- data
PatchedProviderSecretUpdateRequest:
@@ -6765,7 +6835,7 @@ components:
member is used to describe resource objects that share common attributes
and relationships.
enum:
- role
- roles
id:
type: string
format: uuid
@@ -6788,10 +6858,109 @@ components:
type: boolean
manage_scans:
type: boolean
permission_state:
type: string
readOnly: true
unlimited_visibility:
type: boolean
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
required:
- name
relationships:
type: object
properties:
provider_groups:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- provider-groups
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
common attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type provider-groups
title: provider-groups
users:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- users
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
common attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type users
title: users
invitations:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- invitations
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
common attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type invitations
title: invitations
readOnly: true
required:
- data
PatchedScanUpdateRequest:
@@ -6962,6 +7131,37 @@ components:
required:
- name
- email
relationships:
type: object
properties:
roles:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
common attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type roles
title: roles
required:
- data
Provider:
@@ -7068,7 +7268,7 @@ components:
type:
type: string
enum:
- provider-group
- provider-groups
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -7078,8 +7278,8 @@ components:
- type
required:
- data
description: A related resource object from type provider-group
title: provider-group
description: A related resource object from type provider-groups
title: provider-groups
readOnly: true
required:
- secret
@@ -7183,7 +7383,7 @@ components:
properties:
type:
allOf:
- $ref: '#/components/schemas/ProviderGroupTypeEnum'
- $ref: '#/components/schemas/Type34dEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
@@ -7237,7 +7437,6 @@ components:
- data
description: A related resource object from type providers
title: providers
readOnly: true
roles:
type: object
properties:
@@ -7254,7 +7453,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -7264,38 +7463,96 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
readOnly: true
ProviderGroupMembershipRequest:
description: A related resource object from type roles
title: roles
ProviderGroupCreate:
type: object
required:
- type
additionalProperties: false
properties:
data:
type:
allOf:
- $ref: '#/components/schemas/Type34dEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
attributes:
type: object
required:
- type
additionalProperties: false
properties:
type:
name:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- provider_groups-provider
attributes:
maxLength: 255
inserted_at:
type: string
format: date-time
readOnly: true
updated_at:
type: string
format: date-time
readOnly: true
required:
- name
relationships:
type: object
properties:
providers:
type: object
properties:
providers:
data:
type: array
items:
$ref: '#/components/schemas/ProviderResourceIdentifierRequest'
description: List of resource identifier objects representing providers.
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- providers
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
attributes and relationships.
required:
- id
- type
required:
- providers
required:
- data
ProviderGroupRequest:
- data
description: A related resource object from type providers
title: providers
roles:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type roles
title: roles
ProviderGroupCreateRequest:
type: object
properties:
data:
@@ -7310,7 +7567,7 @@ components:
member is used to describe resource objects that share common attributes
and relationships.
enum:
- provider-group
- provider-groups
attributes:
type: object
properties:
@@ -7359,7 +7616,6 @@ components:
- data
description: A related resource object from type providers
title: providers
readOnly: true
roles:
type: object
properties:
@@ -7376,7 +7632,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
@@ -7386,9 +7642,43 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
readOnly: true
description: A related resource object from type roles
title: roles
required:
- data
ProviderGroupCreateResponse:
type: object
properties:
data:
$ref: '#/components/schemas/ProviderGroupCreate'
required:
- data
ProviderGroupMembershipRequest:
type: object
properties:
data:
type: object
required:
- type
additionalProperties: false
properties:
type:
type: string
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
enum:
- provider_groups-provider
attributes:
type: object
properties:
providers:
type: array
items:
$ref: '#/components/schemas/ProviderResourceIdentifierRequest'
description: List of resource identifier objects representing providers.
required:
- providers
required:
- data
ProviderGroupResourceIdentifierRequest:
@@ -7428,10 +7718,6 @@ components:
$ref: '#/components/schemas/ProviderGroup'
required:
- data
ProviderGroupTypeEnum:
type: string
enum:
- provider-group
ProviderResourceIdentifierRequest:
type: object
properties:
@@ -8234,7 +8520,7 @@ components:
properties:
type:
allOf:
- $ref: '#/components/schemas/Type1aaEnum'
- $ref: '#/components/schemas/Type6bbEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
@@ -8293,7 +8579,7 @@ components:
type:
type: string
enum:
- provider-group
- provider-groups
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -8303,8 +8589,8 @@ components:
- type
required:
- data
description: A related resource object from type provider-group
title: provider-group
description: A related resource object from type provider-groups
title: provider-groups
users:
type: object
properties:
@@ -8333,7 +8619,6 @@ components:
- data
description: A related resource object from type users
title: users
readOnly: true
invitations:
type: object
properties:
@@ -8363,8 +8648,6 @@ components:
description: A related resource object from type invitations
title: invitations
readOnly: true
required:
- provider_groups
RoleCreate:
type: object
required:
@@ -8373,7 +8656,7 @@ components:
properties:
type:
allOf:
- $ref: '#/components/schemas/Type1aaEnum'
- $ref: '#/components/schemas/Type6bbEnum'
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common attributes
and relationships.
@@ -8429,7 +8712,7 @@ components:
type:
type: string
enum:
- provider-group
- provider-groups
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -8439,8 +8722,8 @@ components:
- type
required:
- data
description: A related resource object from type provider-group
title: provider-group
description: A related resource object from type provider-groups
title: provider-groups
users:
type: object
properties:
@@ -8469,7 +8752,6 @@ components:
- data
description: A related resource object from type users
title: users
readOnly: true
invitations:
type: object
properties:
@@ -8499,8 +8781,6 @@ components:
description: A related resource object from type invitations
title: invitations
readOnly: true
required:
- provider_groups
RoleCreateRequest:
type: object
properties:
@@ -8516,7 +8796,7 @@ components:
member is used to describe resource objects that share common attributes
and relationships.
enum:
- role
- roles
attributes:
type: object
properties:
@@ -8570,7 +8850,7 @@ components:
type:
type: string
enum:
- provider-group
- provider-groups
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share
@@ -8580,8 +8860,8 @@ components:
- type
required:
- data
description: A related resource object from type provider-group
title: provider-group
description: A related resource object from type provider-groups
title: provider-groups
users:
type: object
properties:
@@ -8610,7 +8890,6 @@ components:
- data
description: A related resource object from type users
title: users
readOnly: true
invitations:
type: object
properties:
@@ -8640,8 +8919,6 @@ components:
description: A related resource object from type invitations
title: invitations
readOnly: true
required:
- provider_groups
required:
- data
RoleCreateResponse:
@@ -9319,14 +9596,18 @@ components:
type: string
enum:
- provider-secrets
Type1aaEnum:
type: string
enum:
- role
Type227Enum:
type: string
enum:
- providers
Type34dEnum:
type: string
enum:
- provider-groups
Type6bbEnum:
type: string
enum:
- roles
Type7f7Enum:
type: string
enum:
@@ -9429,7 +9710,7 @@ components:
type:
type: string
enum:
- role
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
@@ -9439,8 +9720,8 @@ components:
- type
required:
- data
description: A related resource object from type role
title: role
description: A related resource object from type roles
title: roles
readOnly: true
UserCreate:
type: object
@@ -9596,6 +9877,37 @@ components:
required:
- name
- email
relationships:
type: object
properties:
roles:
type: object
properties:
data:
type: array
items:
type: object
properties:
id:
type: string
format: uuid
title: Resource Identifier
description: The identifier of the related object.
type:
type: string
enum:
- roles
title: Resource Type Name
description: The [type](https://jsonapi.org/format/#document-resource-object-identification)
member is used to describe resource objects that share common
attributes and relationships.
required:
- id
- type
required:
- data
description: A related resource object from type roles
title: roles
UserUpdateResponse:
type: object
properties:

View File

@@ -815,7 +815,7 @@ class TestProviderViewSet:
@pytest.mark.parametrize(
"include_values, expected_resources",
[
("provider_groups", ["provider-group"]),
("provider_groups", ["provider-groups"]),
],
)
def test_providers_list_include(
@@ -1200,7 +1200,7 @@ class TestProviderGroupViewSet:
def test_provider_group_create(self, authenticated_client):
data = {
"data": {
"type": "provider-group",
"type": "provider-groups",
"attributes": {
"name": "Test Provider Group",
},
@@ -1219,7 +1219,7 @@ class TestProviderGroupViewSet:
def test_provider_group_create_invalid(self, authenticated_client):
data = {
"data": {
"type": "provider-group",
"type": "provider-groups",
"attributes": {
# Name is missing
},
@@ -1241,7 +1241,7 @@ class TestProviderGroupViewSet:
data = {
"data": {
"id": str(provider_group.id),
"type": "provider-group",
"type": "provider-groups",
"attributes": {
"name": "Updated Provider Group Name",
},
@@ -1263,7 +1263,7 @@ class TestProviderGroupViewSet:
data = {
"data": {
"id": str(provider_group.id),
"type": "provider-group",
"type": "provider-groups",
"attributes": {
"name": "", # Invalid name
},
@@ -1327,6 +1327,170 @@ class TestProviderGroupViewSet:
response = authenticated_client.put(reverse("providergroup-list"))
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
def test_provider_group_create_with_relationships(
self, authenticated_client, providers_fixture, roles_fixture
):
provider1, provider2, *_ = providers_fixture
role1, role2, *_ = roles_fixture
data = {
"data": {
"type": "provider-groups",
"attributes": {"name": "Test Provider Group with relationships"},
"relationships": {
"providers": {
"data": [
{"type": "providers", "id": str(provider1.id)},
{"type": "providers", "id": str(provider2.id)},
]
},
"roles": {
"data": [
{"type": "roles", "id": str(role1.id)},
{"type": "roles", "id": str(role2.id)},
]
},
},
}
}
response = authenticated_client.post(
reverse("providergroup-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_201_CREATED
response_data = response.json()["data"]
group = ProviderGroup.objects.get(id=response_data["id"])
assert group.name == "Test Provider Group with relationships"
assert set(group.providers.all()) == {provider1, provider2}
assert set(group.roles.all()) == {role1, role2}
def test_provider_group_update_relationships(
self,
authenticated_client,
provider_groups_fixture,
providers_fixture,
roles_fixture,
):
group = provider_groups_fixture[0]
provider3 = providers_fixture[2]
provider4 = providers_fixture[3]
role3 = roles_fixture[2]
role4 = roles_fixture[3]
data = {
"data": {
"id": str(group.id),
"type": "provider-groups",
"relationships": {
"providers": {
"data": [
{"type": "providers", "id": str(provider3.id)},
{"type": "providers", "id": str(provider4.id)},
]
},
"roles": {
"data": [
{"type": "roles", "id": str(role3.id)},
{"type": "roles", "id": str(role4.id)},
]
},
},
}
}
response = authenticated_client.patch(
reverse("providergroup-detail", kwargs={"pk": group.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
group.refresh_from_db()
assert set(group.providers.all()) == {provider3, provider4}
assert set(group.roles.all()) == {role3, role4}
def test_provider_group_clear_relationships(
self, authenticated_client, providers_fixture, provider_groups_fixture
):
group = provider_groups_fixture[0]
provider3 = providers_fixture[2]
provider4 = providers_fixture[3]
data = {
"data": {
"id": str(group.id),
"type": "provider-groups",
"relationships": {
"providers": {
"data": [
{"type": "providers", "id": str(provider3.id)},
{"type": "providers", "id": str(provider4.id)},
]
}
},
}
}
response = authenticated_client.patch(
reverse("providergroup-detail", kwargs={"pk": group.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
data = {
"data": {
"id": str(group.id),
"type": "provider-groups",
"relationships": {
"providers": {
"data": [] # Removing all providers
},
"roles": {
"data": [] # Removing all roles
},
},
}
}
response = authenticated_client.patch(
reverse("providergroup-detail", kwargs={"pk": group.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
group.refresh_from_db()
assert group.providers.count() == 0
assert group.roles.count() == 0
def test_provider_group_create_with_invalid_relationships(
self, authenticated_client
):
invalid_provider_id = "non-existent-id"
data = {
"data": {
"type": "provider-groups",
"attributes": {"name": "Invalid relationships test"},
"relationships": {
"providers": {
"data": [{"type": "providers", "id": invalid_provider_id}]
}
},
}
}
response = authenticated_client.post(
reverse("providergroup-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code in [status.HTTP_400_BAD_REQUEST]
@pytest.mark.django_db
class TestProviderSecretViewSet:
@@ -2571,7 +2735,7 @@ class TestInvitationViewSet:
},
"relationships": {
"roles": {
"data": [{"type": "role", "id": str(roles_fixture[0].id)}]
"data": [{"type": "roles", "id": str(roles_fixture[0].id)}]
}
},
}
@@ -2670,9 +2834,10 @@ class TestInvitationViewSet:
)
def test_invitations_partial_update_valid(
self, authenticated_client, invitations_fixture
self, authenticated_client, invitations_fixture, roles_fixture
):
invitation, *_ = invitations_fixture
role1, role2, *_ = roles_fixture
new_email = "new_email@prowler.com"
new_expires_at = datetime.now(timezone.utc) + timedelta(days=7)
new_expires_at_iso = new_expires_at.isoformat()
@@ -2684,6 +2849,14 @@ class TestInvitationViewSet:
"email": new_email,
"expires_at": new_expires_at_iso,
},
"relationships": {
"roles": {
"data": [
{"type": "roles", "id": str(role1.id)},
{"type": "roles", "id": str(role2.id)},
]
},
},
}
}
assert invitation.email != new_email
@@ -2702,6 +2875,7 @@ class TestInvitationViewSet:
assert invitation.email == new_email
assert invitation.expires_at == new_expires_at
assert invitation.roles.count() == 2
@pytest.mark.parametrize(
"email",
@@ -3121,7 +3295,7 @@ class TestRoleViewSet:
def test_role_create(self, authenticated_client):
data = {
"data": {
"type": "role",
"type": "roles",
"attributes": {
"name": "Test Role",
"manage_users": "false",
@@ -3150,7 +3324,7 @@ class TestRoleViewSet:
):
data = {
"data": {
"type": "role",
"type": "roles",
"attributes": {
"name": "Test Role",
"manage_users": "false",
@@ -3164,7 +3338,7 @@ class TestRoleViewSet:
"relationships": {
"provider_groups": {
"data": [
{"type": "provider-group", "id": str(provider_group.id)}
{"type": "provider-groups", "id": str(provider_group.id)}
for provider_group in provider_groups_fixture[:2]
]
}
@@ -3190,7 +3364,7 @@ class TestRoleViewSet:
def test_role_create_invalid(self, authenticated_client):
data = {
"data": {
"type": "role",
"type": "roles",
"attributes": {
# Name is missing
},
@@ -3210,7 +3384,7 @@ class TestRoleViewSet:
data = {
"data": {
"id": str(role.id),
"type": "role",
"type": "roles",
"attributes": {
"name": "Updated Provider Group Name",
},
@@ -3230,7 +3404,7 @@ class TestRoleViewSet:
data = {
"data": {
"id": str(role.id),
"type": "role",
"type": "roles",
"attributes": {
"name": "", # Invalid name
},
@@ -3290,6 +3464,162 @@ class TestRoleViewSet:
response = authenticated_client.put(reverse("role-list"))
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED
def test_role_create_with_users_and_provider_groups(
self, authenticated_client, users_fixture, provider_groups_fixture
):
user1, user2, *_ = users_fixture
pg1, pg2, *_ = provider_groups_fixture
data = {
"data": {
"type": "roles",
"attributes": {
"name": "Role with Users and PGs",
"manage_users": "true",
"manage_account": "false",
"manage_billing": "true",
"manage_providers": "true",
"manage_integrations": "false",
"manage_scans": "false",
"unlimited_visibility": "false",
},
"relationships": {
"users": {
"data": [
{"type": "users", "id": str(user1.id)},
{"type": "users", "id": str(user2.id)},
]
},
"provider_groups": {
"data": [
{"type": "provider-groups", "id": str(pg1.id)},
{"type": "provider-groups", "id": str(pg2.id)},
]
},
},
}
}
response = authenticated_client.post(
reverse("role-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_201_CREATED
created_role = Role.objects.get(name="Role with Users and PGs")
assert created_role.users.count() == 2
assert set(created_role.users.all()) == {user1, user2}
assert created_role.provider_groups.count() == 2
assert set(created_role.provider_groups.all()) == {pg1, pg2}
def test_role_update_relationships(
self,
authenticated_client,
roles_fixture,
users_fixture,
provider_groups_fixture,
):
role = roles_fixture[0]
user3 = users_fixture[2]
pg3 = provider_groups_fixture[2]
data = {
"data": {
"id": str(role.id),
"type": "roles",
"relationships": {
"users": {
"data": [
{"type": "users", "id": str(user3.id)},
]
},
"provider_groups": {
"data": [
{"type": "provider-groups", "id": str(pg3.id)},
]
},
},
}
}
response = authenticated_client.patch(
reverse("role-detail", kwargs={"pk": role.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
role.refresh_from_db()
assert role.users.count() == 1
assert role.users.first() == user3
assert role.provider_groups.count() == 1
assert role.provider_groups.first() == pg3
def test_role_clear_relationships(self, authenticated_client, roles_fixture):
role = roles_fixture[0]
data = {
"data": {
"id": str(role.id),
"type": "roles",
"relationships": {
"users": {
"data": [] # Clearing all users
},
"provider_groups": {
"data": [] # Clearing all provider groups
},
},
}
}
response = authenticated_client.patch(
reverse("role-detail", kwargs={"pk": role.id}),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code == status.HTTP_200_OK
role.refresh_from_db()
assert role.users.count() == 0
assert role.provider_groups.count() == 0
def test_role_create_with_invalid_user_relationship(
self, authenticated_client, provider_groups_fixture
):
invalid_user_id = "non-existent-user-id"
pg = provider_groups_fixture[0]
data = {
"data": {
"type": "roles",
"attributes": {
"name": "Invalid Users Role",
"manage_users": "false",
"manage_account": "false",
"manage_billing": "false",
"manage_providers": "true",
"manage_integrations": "true",
"manage_scans": "true",
"unlimited_visibility": "true",
},
"relationships": {
"users": {"data": [{"type": "users", "id": invalid_user_id}]},
"provider_groups": {
"data": [{"type": "provider-groups", "id": str(pg.id)}]
},
},
}
}
response = authenticated_client.post(
reverse("role-list"),
data=json.dumps(data),
content_type="application/vnd.api+json",
)
assert response.status_code in [status.HTTP_400_BAD_REQUEST]
@pytest.mark.django_db
class TestUserRoleRelationshipViewSet:
@@ -3297,7 +3627,9 @@ class TestUserRoleRelationshipViewSet:
self, authenticated_client, roles_fixture, create_test_user
):
data = {
"data": [{"type": "role", "id": str(role.id)} for role in roles_fixture[:2]]
"data": [
{"type": "roles", "id": str(role.id)} for role in roles_fixture[:2]
]
}
response = authenticated_client.post(
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
@@ -3314,7 +3646,9 @@ class TestUserRoleRelationshipViewSet:
self, authenticated_client, roles_fixture, create_test_user
):
data = {
"data": [{"type": "role", "id": str(role.id)} for role in roles_fixture[:2]]
"data": [
{"type": "roles", "id": str(role.id)} for role in roles_fixture[:2]
]
}
authenticated_client.post(
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
@@ -3324,7 +3658,7 @@ class TestUserRoleRelationshipViewSet:
data = {
"data": [
{"type": "role", "id": str(roles_fixture[0].id)},
{"type": "roles", "id": str(roles_fixture[0].id)},
]
}
response = authenticated_client.post(
@@ -3341,7 +3675,7 @@ class TestUserRoleRelationshipViewSet:
):
data = {
"data": [
{"type": "role", "id": str(roles_fixture[2].id)},
{"type": "roles", "id": str(roles_fixture[2].id)},
]
}
response = authenticated_client.patch(
@@ -3356,8 +3690,8 @@ class TestUserRoleRelationshipViewSet:
data = {
"data": [
{"type": "role", "id": str(roles_fixture[1].id)},
{"type": "role", "id": str(roles_fixture[2].id)},
{"type": "roles", "id": str(roles_fixture[1].id)},
{"type": "roles", "id": str(roles_fixture[2].id)},
]
}
response = authenticated_client.patch(
@@ -3385,7 +3719,7 @@ class TestUserRoleRelationshipViewSet:
def test_invalid_provider_group_id(self, authenticated_client, create_test_user):
invalid_id = "non-existent-id"
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
data = {"data": [{"type": "provider-groups", "id": invalid_id}]}
response = authenticated_client.post(
reverse("user-roles-relationship", kwargs={"pk": create_test_user.id}),
data=data,
@@ -3403,7 +3737,7 @@ class TestRoleProviderGroupRelationshipViewSet:
):
data = {
"data": [
{"type": "provider-group", "id": str(provider_group.id)}
{"type": "provider-groups", "id": str(provider_group.id)}
for provider_group in provider_groups_fixture[:2]
]
}
@@ -3429,7 +3763,7 @@ class TestRoleProviderGroupRelationshipViewSet:
):
data = {
"data": [
{"type": "provider-group", "id": str(provider_group.id)}
{"type": "provider-groups", "id": str(provider_group.id)}
for provider_group in provider_groups_fixture[:2]
]
}
@@ -3443,7 +3777,7 @@ class TestRoleProviderGroupRelationshipViewSet:
data = {
"data": [
{"type": "provider-group", "id": str(provider_groups_fixture[0].id)},
{"type": "provider-groups", "id": str(provider_groups_fixture[0].id)},
]
}
response = authenticated_client.post(
@@ -3462,7 +3796,7 @@ class TestRoleProviderGroupRelationshipViewSet:
):
data = {
"data": [
{"type": "provider-group", "id": str(provider_groups_fixture[1].id)},
{"type": "provider-groups", "id": str(provider_groups_fixture[1].id)},
]
}
response = authenticated_client.patch(
@@ -3483,8 +3817,8 @@ class TestRoleProviderGroupRelationshipViewSet:
data = {
"data": [
{"type": "provider-group", "id": str(provider_groups_fixture[1].id)},
{"type": "provider-group", "id": str(provider_groups_fixture[2].id)},
{"type": "provider-groups", "id": str(provider_groups_fixture[1].id)},
{"type": "provider-groups", "id": str(provider_groups_fixture[2].id)},
]
}
response = authenticated_client.patch(
@@ -3520,7 +3854,7 @@ class TestRoleProviderGroupRelationshipViewSet:
def test_invalid_provider_group_id(self, authenticated_client, roles_fixture):
invalid_id = "non-existent-id"
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
data = {"data": [{"type": "provider-groups", "id": invalid_id}]}
response = authenticated_client.post(
reverse(
"role-provider-groups-relationship", kwargs={"pk": roles_fixture[1].id}
@@ -3681,7 +4015,7 @@ class TestProviderGroupMembershipViewSet:
):
provider_group, *_ = provider_groups_fixture
invalid_id = "non-existent-id"
data = {"data": [{"type": "provider-group", "id": invalid_id}]}
data = {"data": [{"type": "provider-groups", "id": invalid_id}]}
response = authenticated_client.post(
reverse(
"provider_group-providers-relationship",

View File

@@ -445,7 +445,12 @@ class MembershipSerializer(serializers.ModelSerializer):
# Provider Groups
class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
providers = serializers.ResourceRelatedField(many=True, read_only=True)
providers = serializers.ResourceRelatedField(
queryset=Provider.objects.all(), many=True, required=False
)
roles = serializers.ResourceRelatedField(
queryset=Role.objects.all(), many=True, required=False
)
def validate(self, attrs):
if ProviderGroup.objects.filter(name=attrs.get("name")).exists():
@@ -475,21 +480,93 @@ class ProviderGroupSerializer(RLSSerializer, BaseWriteSerializer):
}
class ProviderGroupIncludedSerializer(RLSSerializer, BaseWriteSerializer):
class ProviderGroupIncludedSerializer(ProviderGroupSerializer):
class Meta:
model = ProviderGroup
fields = ["id", "name"]
class ProviderGroupUpdateSerializer(RLSSerializer, BaseWriteSerializer):
"""
Serializer for updating the ProviderGroup model.
Only allows "name" field to be updated.
"""
class ProviderGroupCreateSerializer(ProviderGroupSerializer):
providers = serializers.ResourceRelatedField(
queryset=Provider.objects.all(), many=True, required=False
)
roles = serializers.ResourceRelatedField(
queryset=Role.objects.all(), many=True, required=False
)
class Meta:
model = ProviderGroup
fields = ["id", "name"]
fields = [
"id",
"name",
"inserted_at",
"updated_at",
"providers",
"roles",
]
def create(self, validated_data):
providers = validated_data.pop("providers", [])
roles = validated_data.pop("roles", [])
tenant_id = self.context.get("tenant_id")
provider_group = ProviderGroup.objects.create(
tenant_id=tenant_id, **validated_data
)
through_model_instances = [
ProviderGroupMembership(
provider_group=provider_group,
provider=provider,
tenant_id=tenant_id,
)
for provider in providers
]
ProviderGroupMembership.objects.bulk_create(through_model_instances)
through_model_instances = [
RoleProviderGroupRelationship(
provider_group=provider_group,
role=role,
tenant_id=tenant_id,
)
for role in roles
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
return provider_group
class ProviderGroupUpdateSerializer(ProviderGroupSerializer):
def update(self, instance, validated_data):
tenant_id = self.context.get("tenant_id")
if "providers" in validated_data:
providers = validated_data.pop("providers")
instance.providers.clear()
through_model_instances = [
ProviderGroupMembership(
provider_group=instance,
provider=provider,
tenant_id=tenant_id,
)
for provider in providers
]
ProviderGroupMembership.objects.bulk_create(through_model_instances)
if "roles" in validated_data:
roles = validated_data.pop("roles")
instance.roles.clear()
through_model_instances = [
RoleProviderGroupRelationship(
provider_group=instance,
role=role,
tenant_id=tenant_id,
)
for role in roles
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
return super().update(instance, validated_data)
class ProviderResourceIdentifierSerializer(serializers.Serializer):
@@ -1235,6 +1312,10 @@ class InvitationCreateSerializer(InvitationBaseWriteSerializer, RLSSerializer):
class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
roles = serializers.ResourceRelatedField(
required=False, many=True, queryset=Role.objects.all()
)
class Meta:
model = Invitation
fields = ["id", "email", "expires_at", "state", "token", "roles"]
@@ -1247,15 +1328,19 @@ class InvitationUpdateSerializer(InvitationBaseWriteSerializer):
}
def update(self, instance, validated_data):
roles = validated_data.pop("roles", [])
tenant_id = self.context.get("tenant_id")
invitation = super().update(instance, validated_data)
if roles:
if "roles" in validated_data:
roles = validated_data.pop("roles")
instance.roles.clear()
for role in roles:
InvitationRoleRelationship.objects.create(
role=role, invitation=invitation, tenant_id=tenant_id
new_relationships = [
InvitationRoleRelationship(
role=r, invitation=instance, tenant_id=tenant_id
)
for r in roles
]
InvitationRoleRelationship.objects.bulk_create(new_relationships)
invitation = super().update(instance, validated_data)
return invitation
@@ -1274,12 +1359,15 @@ class InvitationAcceptSerializer(RLSSerializer):
class RoleSerializer(RLSSerializer, BaseWriteSerializer):
provider_groups = serializers.ResourceRelatedField(
many=True, queryset=ProviderGroup.objects.all()
)
permission_state = serializers.SerializerMethodField()
users = serializers.ResourceRelatedField(
queryset=User.objects.all(), many=True, required=False
)
provider_groups = serializers.ResourceRelatedField(
queryset=ProviderGroup.objects.all(), many=True, required=False
)
def get_permission_state(self, obj):
def get_permission_state(self, obj) -> str:
return obj.permission_state
def validate(self, attrs):
@@ -1323,12 +1411,18 @@ class RoleSerializer(RLSSerializer, BaseWriteSerializer):
"id": {"read_only": True},
"inserted_at": {"read_only": True},
"updated_at": {"read_only": True},
"users": {"read_only": True},
"url": {"read_only": True},
}
class RoleCreateSerializer(RoleSerializer):
provider_groups = serializers.ResourceRelatedField(
many=True, queryset=ProviderGroup.objects.all(), required=False
)
users = serializers.ResourceRelatedField(
many=True, queryset=User.objects.all(), required=False
)
def create(self, validated_data):
provider_groups = validated_data.pop("provider_groups", [])
users = validated_data.pop("users", [])
@@ -1347,7 +1441,7 @@ class RoleCreateSerializer(RoleSerializer):
through_model_instances = [
UserRoleRelationship(
role=user,
role=role,
user=user,
tenant_id=tenant_id,
)
@@ -1358,20 +1452,37 @@ class RoleCreateSerializer(RoleSerializer):
return role
class RoleUpdateSerializer(RLSSerializer, BaseWriteSerializer):
class Meta:
model = Role
fields = [
"id",
"name",
"manage_users",
"manage_account",
"manage_billing",
"manage_providers",
"manage_integrations",
"manage_scans",
"unlimited_visibility",
]
class RoleUpdateSerializer(RoleSerializer):
def update(self, instance, validated_data):
tenant_id = self.context.get("tenant_id")
if "provider_groups" in validated_data:
provider_groups = validated_data.pop("provider_groups")
instance.provider_groups.clear()
through_model_instances = [
RoleProviderGroupRelationship(
role=instance,
provider_group=provider_group,
tenant_id=tenant_id,
)
for provider_group in provider_groups
]
RoleProviderGroupRelationship.objects.bulk_create(through_model_instances)
if "users" in validated_data:
users = validated_data.pop("users")
instance.users.clear()
through_model_instances = [
UserRoleRelationship(
role=instance,
user=user,
tenant_id=tenant_id,
)
for user in users
]
UserRoleRelationship.objects.bulk_create(through_model_instances)
return super().update(instance, validated_data)
class ProviderGroupResourceIdentifierSerializer(serializers.Serializer):

View File

@@ -100,6 +100,7 @@ from api.v1.serializers import (
ProviderCreateSerializer,
ProviderGroupMembershipSerializer,
ProviderGroupSerializer,
ProviderGroupCreateSerializer,
ProviderGroupUpdateSerializer,
ProviderSecretCreateSerializer,
ProviderSecretSerializer,
@@ -741,7 +742,9 @@ class ProviderGroupViewSet(BaseRLSViewSet):
return user_roles.provider_groups.all()
def get_serializer_class(self):
if self.action == "partial_update":
if self.action == "create":
return ProviderGroupCreateSerializer
elif self.action == "partial_update":
return ProviderGroupUpdateSerializer
return super().get_serializer_class()

View File

@@ -322,6 +322,20 @@ def invitations_fixture(create_test_user, tenants_fixture):
return valid_invitation, expired_invitation
@pytest.fixture
def users_fixture(django_user_model):
user1 = User.objects.create_user(
name="user1", email="test_unit0@prowler.com", password="S3cret"
)
user2 = User.objects.create_user(
name="user2", email="test_unit1@prowler.com", password="S3cret"
)
user3 = User.objects.create_user(
name="user3", email="test_unit2@prowler.com", password="S3cret"
)
return user1, user2, user3
@pytest.fixture
def providers_fixture(tenants_fixture):
tenant, *_ = tenants_fixture