From fc19fbac68f1d37112c3014506204696aef82f27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Fern=C3=A1ndez=20Poyatos?= Date: Thu, 31 Oct 2024 16:25:20 +0100 Subject: [PATCH] feat(ProviderSecret): PRWLR-5128 Implement role assumption provider credentials for AWS (#64) * feat(ProviderSecret): PRWLR-5128 add role assumption logic * feat(Provider): PRWLR-5128 add static kwargs to Prowler provider initialization * chore: PRWLR-5128 update API spec * test(ProviderSecret): PRWLR-5128 add new unit tests * chore: PRWLR-5128 update API spec --- src/backend/api/models.py | 1 + src/backend/api/specs/v1.yaml | 499 +++++++++++++++++++++++++--- src/backend/api/tests/test_utils.py | 73 ++++ src/backend/api/tests/test_views.py | 87 ++++- src/backend/api/utils.py | 29 +- src/backend/api/v1/serializers.py | 181 ++++++++-- src/backend/api/v1/views.py | 35 ++ 7 files changed, 806 insertions(+), 99 deletions(-) diff --git a/src/backend/api/models.py b/src/backend/api/models.py index aa5c14375f..2196073b23 100644 --- a/src/backend/api/models.py +++ b/src/backend/api/models.py @@ -626,6 +626,7 @@ class ResourceFindingMapping(PostgresPartitionedModel, RowLevelSecurityProtected class ProviderSecret(RowLevelSecurityProtectedModel): class TypeChoices(models.TextChoices): STATIC = "static", _("Key-value pairs") + ROLE = "role", _("Role assumption") id = models.UUIDField(primary_key=True, default=uuid4, editable=False) inserted_at = models.DateTimeField(auto_now_add=True, editable=False) diff --git a/src/backend/api/specs/v1.yaml b/src/backend/api/specs/v1.yaml index 64e2eee3db..0358b2abd2 100644 --- a/src/backend/api/specs/v1.yaml +++ b/src/backend/api/specs/v1.yaml @@ -11,12 +11,11 @@ paths: get: operationId: findings_list description: Retrieve a list of all findings with options for filtering by various - criteria. If no `scan` or `inserted_at` filter is provided, a default filter - will be applied that will return all findings for the current day. + criteria. summary: List all findings parameters: - in: query - name: fields[Findings] + name: fields[Finding] schema: type: array items: @@ -452,7 +451,7 @@ paths: summary: Retrieve data from a specific finding parameters: - in: query - name: fields[Findings] + name: fields[Finding] schema: type: array items: @@ -859,6 +858,7 @@ paths: - inserted_at - updated_at - name + - secret_type - provider - url description: endpoint return only specific fields in the response on a per-type @@ -976,6 +976,7 @@ paths: - inserted_at - updated_at - name + - secret_type - provider - url description: endpoint return only specific fields in the response on a per-type @@ -1290,6 +1291,7 @@ paths: type: string enum: - findings + - provider description: include query parameter to allow the client to customize which related resources should be returned. explode: false @@ -1386,6 +1388,7 @@ paths: type: string enum: - findings + - provider description: include query parameter to allow the client to customize which related resources should be returned. explode: false @@ -2609,7 +2612,7 @@ components: properties: uid: type: string - maxLength: 150 + maxLength: 300 delta: enum: - new @@ -2698,25 +2701,30 @@ components: type: object properties: data: - type: object - properties: - id: - type: string - type: - type: string - enum: - - Resource - 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 + 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: + - Resource + 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: The identifier of the related object. - title: Resource Identifier + description: A related resource object from type Resource + title: Resource readOnly: true required: - scan @@ -2730,7 +2738,7 @@ components: FindingTypeEnum: type: string enum: - - Findings + - Finding Membership: type: object required: @@ -2941,7 +2949,118 @@ components: nullable: true maxLength: 100 minLength: 3 + secret_type: + enum: + - static + - role + type: string + description: |- + * `static` - Key-value pairs + * `role` - Role assumption + readOnly: true secret: + oneOf: + - type: object + title: AWS Static Credentials + properties: + aws_access_key_id: + type: string + description: The AWS access key ID. Required for environments + where no IAM role is being assumed and direct AWS access + is needed. + aws_secret_access_key: + type: string + description: The AWS secret access key. Must accompany 'aws_access_key_id' + to authorize access to AWS resources. + aws_session_token: + type: string + description: The session token associated with temporary credentials. + Only needed for session-based or temporary AWS access. + required: + - aws_access_key_id + - aws_secret_access_key + - type: object + title: AWS Assume Role + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to + assume. Required for AWS role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, + if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + external_id: + type: string + description: An optional identifier to enhance security for + role assumption; may be required by the role administrator. + role_session_name: + type: string + description: An identifier for the role session, useful for + tracking sessions in AWS logs. + required: + - role_arn + - type: object + title: Azure Static Credentials + properties: + client_id: + type: string + description: The Azure application (client) ID for authentication + in Azure AD. + client_secret: + type: string + description: The client secret associated with the application + (client) ID, providing secure access. + tenant_id: + type: string + description: The Azure tenant ID, representing the directory + where the application is registered. + required: + - client_id + - client_secret + - tenant_id + - type: object + title: GCP Static Credentials + properties: + client_id: + type: string + description: The client ID from Google Cloud, used to identify + the application for GCP access. + client_secret: + type: string + description: The client secret associated with the GCP client + ID, required for secure access. + refresh_token: + type: string + description: A refresh token that allows the application to + obtain new access tokens for extended use. + required: + - client_id + - client_secret + - refresh_token + - type: object + title: Kubernetes Static Credentials + properties: + kubeconfig_content: + type: string + description: The content of the Kubernetes kubeconfig file, + encoded as a string. + required: + - kubeconfig_content writeOnly: true required: - secret @@ -3132,6 +3251,7 @@ components: type: string format: email minLength: 1 + description: Case insensitive maxLength: 254 company_name: type: string @@ -3364,6 +3484,16 @@ components: nullable: true maxLength: 100 minLength: 3 + secret_type: + enum: + - static + - role + type: string + description: |- + * `static` - Key-value pairs + * `role` - Role assumption + required: + - secret_type relationships: type: object properties: @@ -3424,49 +3554,113 @@ components: secret_type: enum: - static + - role type: string - description: '* `static` - Key-value pairs' + description: |- + * `static` - Key-value pairs + * `role` - Role assumption secret: oneOf: - type: object - title: AWS static credentials + title: AWS Static Credentials properties: aws_access_key_id: type: string + description: The AWS access key ID. Required for environments + where no IAM role is being assumed and direct AWS access is + needed. aws_secret_access_key: type: string + description: The AWS secret access key. Must accompany 'aws_access_key_id' + to authorize access to AWS resources. aws_session_token: type: string + description: The session token associated with temporary credentials. + Only needed for session-based or temporary AWS access. required: - aws_access_key_id - aws_secret_access_key - type: object - title: Azure static credentials + title: AWS Assume Role + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to assume. + Required for AWS role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + external_id: + type: string + description: An optional identifier to enhance security for role + assumption; may be required by the role administrator. + role_session_name: + type: string + description: An identifier for the role session, useful for tracking + sessions in AWS logs. + required: + - role_arn + - type: object + title: Azure Static Credentials properties: client_id: type: string + description: The Azure application (client) ID for authentication + in Azure AD. client_secret: type: string + description: The client secret associated with the application + (client) ID, providing secure access. tenant_id: type: string - description: Azure tenant ID + description: The Azure tenant ID, representing the directory where + the application is registered. required: - client_id - client_secret - tenant_id - type: object - title: GCP static credentials + title: GCP Static Credentials properties: client_id: type: string + description: The client ID from Google Cloud, used to identify + the application for GCP access. client_secret: type: string + description: The client secret associated with the GCP client + ID, required for secure access. refresh_token: type: string + description: A refresh token that allows the application to obtain + new access tokens for extended use. required: - client_id - client_secret - refresh_token + - type: object + title: Kubernetes Static Credentials + properties: + kubeconfig_content: + type: string + description: The content of the Kubernetes kubeconfig file, encoded + as a string. + required: + - kubeconfig_content writeOnly: true required: - secret_type @@ -3535,49 +3729,114 @@ components: secret_type: enum: - static + - role type: string - description: '* `static` - Key-value pairs' + description: |- + * `static` - Key-value pairs + * `role` - Role assumption secret: oneOf: - type: object - title: AWS static credentials + title: AWS Static Credentials properties: aws_access_key_id: type: string + description: The AWS access key ID. Required for environments + where no IAM role is being assumed and direct AWS access + is needed. aws_secret_access_key: type: string + description: The AWS secret access key. Must accompany 'aws_access_key_id' + to authorize access to AWS resources. aws_session_token: type: string + description: The session token associated with temporary credentials. + Only needed for session-based or temporary AWS access. required: - aws_access_key_id - aws_secret_access_key - type: object - title: Azure static credentials + title: AWS Assume Role + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to + assume. Required for AWS role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, + if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + external_id: + type: string + description: An optional identifier to enhance security for + role assumption; may be required by the role administrator. + role_session_name: + type: string + description: An identifier for the role session, useful for + tracking sessions in AWS logs. + required: + - role_arn + - type: object + title: Azure Static Credentials properties: client_id: type: string + description: The Azure application (client) ID for authentication + in Azure AD. client_secret: type: string + description: The client secret associated with the application + (client) ID, providing secure access. tenant_id: type: string - description: Azure tenant ID + description: The Azure tenant ID, representing the directory + where the application is registered. required: - client_id - client_secret - tenant_id - type: object - title: GCP static credentials + title: GCP Static Credentials properties: client_id: type: string + description: The client ID from Google Cloud, used to identify + the application for GCP access. client_secret: type: string + description: The client secret associated with the GCP client + ID, required for secure access. refresh_token: type: string + description: A refresh token that allows the application to + obtain new access tokens for extended use. required: - client_id - client_secret - refresh_token + - type: object + title: Kubernetes Static Credentials + properties: + kubeconfig_content: + type: string + description: The content of the Kubernetes kubeconfig file, + encoded as a string. + required: + - kubeconfig_content writeOnly: true required: - secret_type @@ -3657,7 +3916,117 @@ components: nullable: true maxLength: 100 minLength: 3 + secret_type: + enum: + - static + - role + type: string + description: |- + * `static` - Key-value pairs + * `role` - Role assumption + readOnly: true secret: + oneOf: + - type: object + title: AWS Static Credentials + properties: + aws_access_key_id: + type: string + description: The AWS access key ID. Required for environments + where no IAM role is being assumed and direct AWS access is + needed. + aws_secret_access_key: + type: string + description: The AWS secret access key. Must accompany 'aws_access_key_id' + to authorize access to AWS resources. + aws_session_token: + type: string + description: The session token associated with temporary credentials. + Only needed for session-based or temporary AWS access. + required: + - aws_access_key_id + - aws_secret_access_key + - type: object + title: AWS Assume Role + properties: + role_arn: + type: string + description: The Amazon Resource Name (ARN) of the role to assume. + Required for AWS role assumption. + aws_access_key_id: + type: string + description: The AWS access key ID. Only required if the environment + lacks pre-configured AWS credentials. + aws_secret_access_key: + type: string + description: The AWS secret access key. Required if 'aws_access_key_id' + is provided or if no AWS credentials are pre-configured. + aws_session_token: + type: string + description: The session token for temporary credentials, if applicable. + session_duration: + type: integer + minimum: 900 + maximum: 43200 + default: 3600 + description: The duration (in seconds) for the role session. + external_id: + type: string + description: An optional identifier to enhance security for role + assumption; may be required by the role administrator. + role_session_name: + type: string + description: An identifier for the role session, useful for tracking + sessions in AWS logs. + required: + - role_arn + - type: object + title: Azure Static Credentials + properties: + client_id: + type: string + description: The Azure application (client) ID for authentication + in Azure AD. + client_secret: + type: string + description: The client secret associated with the application + (client) ID, providing secure access. + tenant_id: + type: string + description: The Azure tenant ID, representing the directory where + the application is registered. + required: + - client_id + - client_secret + - tenant_id + - type: object + title: GCP Static Credentials + properties: + client_id: + type: string + description: The client ID from Google Cloud, used to identify + the application for GCP access. + client_secret: + type: string + description: The client secret associated with the GCP client + ID, required for secure access. + refresh_token: + type: string + description: A refresh token that allows the application to obtain + new access tokens for extended use. + required: + - client_id + - client_secret + - refresh_token + - type: object + title: Kubernetes Static Credentials + properties: + kubeconfig_content: + type: string + description: The content of the Kubernetes kubeconfig file, encoded + as a string. + required: + - kubeconfig_content writeOnly: true required: - secret @@ -3781,25 +4150,30 @@ components: type: object properties: data: - type: object - properties: - id: - type: string - type: - type: string - enum: - - Finding - 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 + 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: + - Finding + 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: The identifier of the related object. - title: Resource Identifier + description: A related resource object from type Finding + title: Finding readOnly: true required: - provider @@ -4424,6 +4798,7 @@ components: email: type: string format: email + description: Case insensitive maxLength: 254 company_name: type: string @@ -4492,6 +4867,7 @@ components: email: type: string format: email + description: Case insensitive maxLength: 254 company_name: type: string @@ -4530,6 +4906,7 @@ components: type: string format: email minLength: 1 + description: Case insensitive maxLength: 254 company_name: type: string @@ -4583,6 +4960,7 @@ components: email: type: string format: email + description: Case insensitive maxLength: 254 company_name: type: string @@ -4602,3 +4980,24 @@ components: type: http scheme: bearer bearerFormat: JWT +tags: +- name: User + description: Endpoints for managing user accounts. +- name: Token + description: Endpoints for token management, including obtaining a new token and + refreshing an existing token for authentication purposes. +- name: Tenant + description: Endpoints for managing tenants, along with their memberships. +- name: Provider + description: Endpoints for managing providers (AWS, GCP, Azure, etc...). +- name: Scan + description: Endpoints for triggering manual scans and viewing scan results. +- name: Resource + description: Endpoints for managing resources discovered by scans, allowing retrieval + and filtering of resource information. +- name: Finding + description: Endpoints for managing findings, allowing retrieval and filtering of + findings that result from scans. +- name: Task + description: Endpoints for task management, allowing retrieval of task status and + revoking tasks that have not started. diff --git a/src/backend/api/tests/test_utils.py b/src/backend/api/tests/test_utils.py index 555841908e..5d75b0820d 100644 --- a/src/backend/api/tests/test_utils.py +++ b/src/backend/api/tests/test_utils.py @@ -12,6 +12,7 @@ from api.utils import ( return_prowler_provider, initialize_prowler_provider, prowler_provider_connection_test, + get_prowler_provider_kwargs, ) @@ -136,3 +137,75 @@ class TestProwlerProviderConnectionTest: mock_return_prowler_provider.return_value.test_connection.assert_called_once_with( key="value", provider_id="1234567890", raise_on_exception=False ) + + +class TestGetProwlerProviderKwargs: + @pytest.mark.parametrize( + "provider_type, expected_extra_kwargs", + [ + ( + Provider.ProviderChoices.AWS.value, + {}, + ), + ( + Provider.ProviderChoices.AZURE.value, + {"subscription_ids": ["provider_uid"]}, + ), + ( + Provider.ProviderChoices.GCP.value, + {"project_ids": ["provider_uid"]}, + ), + ( + Provider.ProviderChoices.KUBERNETES.value, + {"context": "provider_uid"}, + ), + ], + ) + def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs): + provider_uid = "provider_uid" + secret_dict = {"key": "value"} + secret_mock = MagicMock() + secret_mock.secret = secret_dict + + provider = MagicMock() + provider.provider = provider_type + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider) + + expected_result = {**secret_dict, **expected_extra_kwargs} + assert result == expected_result + + def test_get_prowler_provider_kwargs_unsupported_provider(self): + # Setup + provider_uid = "provider_uid" + secret_dict = {"key": "value"} + secret_mock = MagicMock() + secret_mock.secret = secret_dict + + provider = MagicMock() + provider.provider = "UNSUPPORTED_PROVIDER" + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider) + + expected_result = secret_dict.copy() + assert result == expected_result + + def test_get_prowler_provider_kwargs_no_secret(self): + # Setup + provider_uid = "provider_uid" + secret_mock = MagicMock() + secret_mock.secret = {} + + provider = MagicMock() + provider.provider = Provider.ProviderChoices.AWS.value + provider.secret = secret_mock + provider.uid = provider_uid + + result = get_prowler_provider_kwargs(provider) + + expected_result = {} + assert result == expected_result diff --git a/src/backend/api/tests/test_views.py b/src/backend/api/tests/test_views.py index 5bcf54f5b9..1c132f5142 100644 --- a/src/backend/api/tests/test_views.py +++ b/src/backend/api/tests/test_views.py @@ -1139,21 +1139,82 @@ class TestProviderSecretViewSet: ) assert response.status_code == status.HTTP_404_NOT_FOUND + @pytest.mark.parametrize( + "provider_type, secret_type, secret_data", + [ + # AWS with STATIC secret + ( + Provider.ProviderChoices.AWS.value, + ProviderSecret.TypeChoices.STATIC, + { + "aws_access_key_id": "value", + "aws_secret_access_key": "value", + "aws_session_token": "value", + }, + ), + # AWS with ROLE secret + ( + Provider.ProviderChoices.AWS.value, + ProviderSecret.TypeChoices.ROLE, + { + "role_arn": "arn:aws:iam::123456789012:role/example-role", + # Optional fields + "external_id": "external-id", + "role_session_name": "session-name", + "session_duration": 3600, + "aws_access_key_id": "value", + "aws_secret_access_key": "value", + "aws_session_token": "value", + }, + ), + # Azure with STATIC secret + ( + Provider.ProviderChoices.AZURE.value, + ProviderSecret.TypeChoices.STATIC, + { + "client_id": "client-id", + "client_secret": "client-secret", + "tenant_id": "tenant-id", + }, + ), + # GCP with STATIC secret + ( + Provider.ProviderChoices.GCP.value, + ProviderSecret.TypeChoices.STATIC, + { + "client_id": "client-id", + "client_secret": "client-secret", + "refresh_token": "refresh-token", + }, + ), + # Kubernetes with STATIC secret + ( + Provider.ProviderChoices.KUBERNETES.value, + ProviderSecret.TypeChoices.STATIC, + { + "kubeconfig_content": "kubeconfig-content", + }, + ), + ], + ) def test_provider_secrets_create_valid( - self, authenticated_client, providers_fixture + self, + authenticated_client, + providers_fixture, + provider_type, + secret_type, + secret_data, ): - provider, *_ = providers_fixture + # Get the provider from the fixture and set its type + provider = Provider.objects.filter(provider=provider_type)[0] + data = { "data": { "type": "ProviderSecret", "attributes": { "name": "My Secret", - "secret_type": "static", - "secret": { - "aws_access_key_id": "value", - "aws_secret_access_key": "value", - "aws_session_token": "value", - }, + "secret_type": secret_type, + "secret": secret_data, }, "relationships": { "provider": {"data": {"type": "Provider", "id": str(provider.id)}} @@ -1167,13 +1228,11 @@ class TestProviderSecretViewSet: ) assert response.status_code == status.HTTP_201_CREATED assert ProviderSecret.objects.count() == 1 - assert ProviderSecret.objects.get().name == data["data"]["attributes"]["name"] + provider_secret = ProviderSecret.objects.first() + assert provider_secret.name == data["data"]["attributes"]["name"] + assert provider_secret.secret_type == data["data"]["attributes"]["secret_type"] assert ( - ProviderSecret.objects.get().secret_type - == data["data"]["attributes"]["secret_type"] - ) - assert ( - str(ProviderSecret.objects.get().provider.id) + str(provider_secret.provider.id) == data["data"]["relationships"]["provider"]["data"]["id"] ) diff --git a/src/backend/api/utils.py b/src/backend/api/utils.py index 86c6c529d8..d42d76922e 100644 --- a/src/backend/api/utils.py +++ b/src/backend/api/utils.py @@ -62,6 +62,31 @@ def return_prowler_provider( return prowler_provider +def get_prowler_provider_kwargs(provider: Provider) -> dict: + """Get the Prowler provider kwargs based on the given provider type. + + Args: + provider (Provider): The provider object containing the provider type and associated secret. + + Returns: + dict: The provider kwargs for the corresponding provider class. + """ + prowler_provider_kwargs = provider.secret.secret + if provider.provider == Provider.ProviderChoices.AZURE.value: + prowler_provider_kwargs = { + **prowler_provider_kwargs, + "subscription_ids": [provider.uid], + } + elif provider.provider == Provider.ProviderChoices.GCP.value: + prowler_provider_kwargs = { + **prowler_provider_kwargs, + "project_ids": [provider.uid], + } + elif provider.provider == Provider.ProviderChoices.KUBERNETES.value: + prowler_provider_kwargs = {**prowler_provider_kwargs, "context": provider.uid} + return prowler_provider_kwargs + + def initialize_prowler_provider( provider: Provider, ) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider: @@ -76,7 +101,7 @@ def initialize_prowler_provider( provider's secrets. """ prowler_provider = return_prowler_provider(provider) - prowler_provider_kwargs = provider.secret.secret + prowler_provider_kwargs = get_prowler_provider_kwargs(provider) return prowler_provider(**prowler_provider_kwargs) @@ -90,7 +115,7 @@ def prowler_provider_connection_test(provider: Provider) -> Connection: Connection: A connection object representing the result of the connection test for the specified provider. """ prowler_provider = return_prowler_provider(provider) - prowler_provider_kwargs = provider.secret.secret + prowler_provider_kwargs = get_prowler_provider_kwargs(provider) return prowler_provider.test_connection( **prowler_provider_kwargs, provider_id=provider.uid, raise_on_exception=False ) diff --git a/src/backend/api/v1/serializers.py b/src/backend/api/v1/serializers.py index ef06724d29..e6b9d532a7 100644 --- a/src/backend/api/v1/serializers.py +++ b/src/backend/api/v1/serializers.py @@ -611,16 +611,27 @@ class FindingSerializer(RLSSerializer): # Provider secrets class BaseWriteProviderSecretSerializer(BaseWriteSerializer): @staticmethod - def validate_secret_based_on_provider(provider_type: str, secret: dict): - if provider_type == Provider.ProviderChoices.AWS.value: - serializer = AwsProviderSecret(data=secret) - elif provider_type == Provider.ProviderChoices.AZURE.value: - serializer = AzureProviderSecret(data=secret) - elif provider_type == Provider.ProviderChoices.GCP.value: - serializer = GCPProviderSecret(data=secret) + def validate_secret_based_on_provider( + provider_type: str, secret_type: ProviderSecret.TypeChoices, secret: dict + ): + if secret_type == ProviderSecret.TypeChoices.STATIC: + if provider_type == Provider.ProviderChoices.AWS.value: + serializer = AwsProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.AZURE.value: + serializer = AzureProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.GCP.value: + serializer = GCPProviderSecret(data=secret) + elif provider_type == Provider.ProviderChoices.KUBERNETES.value: + serializer = KubernetesProviderSecret(data=secret) + else: + raise serializers.ValidationError( + {"provider": f"Provider type not supported {provider_type}"} + ) + elif secret_type == ProviderSecret.TypeChoices.ROLE: + serializer = AWSRoleAssumptionProviderSecret(data=secret) else: raise serializers.ValidationError( - {"provider": f"Provider type not supported {provider_type}"} + {"secret_type": f"Secret type not supported: {secret_type}"} ) try: serializer.is_valid(raise_exception=True) @@ -660,46 +671,148 @@ class GCPProviderSecret(serializers.Serializer): resource_name = "ProviderSecret" +class KubernetesProviderSecret(serializers.Serializer): + kubeconfig_content = serializers.CharField() + + class Meta: + resource_name = "ProviderSecret" + + +class AWSRoleAssumptionProviderSecret(serializers.Serializer): + role_arn = serializers.CharField() + external_id = serializers.CharField(required=False) + role_session_name = serializers.CharField(required=False) + session_duration = serializers.IntegerField( + required=False, min_value=900, max_value=43200 + ) + aws_access_key_id = serializers.CharField(required=False) + aws_secret_access_key = serializers.CharField(required=False) + aws_session_token = serializers.CharField(required=False) + + class Meta: + resource_name = "ProviderSecret" + + @extend_schema_field( { "oneOf": [ { "type": "object", - "title": "AWS static credentials", + "title": "AWS Static Credentials", "properties": { - "aws_access_key_id": {"type": "string"}, - "aws_secret_access_key": {"type": "string"}, - "aws_session_token": {"type": "string"}, + "aws_access_key_id": { + "type": "string", + "description": "The AWS access key ID. Required for environments where no IAM role is being " + "assumed and direct AWS access is needed.", + }, + "aws_secret_access_key": { + "type": "string", + "description": "The AWS secret access key. Must accompany 'aws_access_key_id' to authorize " + "access to AWS resources.", + }, + "aws_session_token": { + "type": "string", + "description": "The session token associated with temporary credentials. Only needed for " + "session-based or temporary AWS access.", + }, }, "required": ["aws_access_key_id", "aws_secret_access_key"], }, { "type": "object", - "title": "Azure static credentials", + "title": "AWS Assume Role", "properties": { - "client_id": {"type": "string"}, - "client_secret": {"type": "string"}, - "tenant_id": {"type": "string", "description": "Azure tenant ID"}, + "role_arn": { + "type": "string", + "description": "The Amazon Resource Name (ARN) of the role to assume. Required for AWS role " + "assumption.", + }, + "aws_access_key_id": { + "type": "string", + "description": "The AWS access key ID. Only required if the environment lacks pre-configured " + "AWS credentials.", + }, + "aws_secret_access_key": { + "type": "string", + "description": "The AWS secret access key. Required if 'aws_access_key_id' is provided or if " + "no AWS credentials are pre-configured.", + }, + "aws_session_token": { + "type": "string", + "description": "The session token for temporary credentials, if applicable.", + }, + "session_duration": { + "type": "integer", + "minimum": 900, + "maximum": 43200, + "default": 3600, + "description": "The duration (in seconds) for the role session.", + }, + "external_id": { + "type": "string", + "description": "An optional identifier to enhance security for role assumption; may be " + "required by the role administrator.", + }, + "role_session_name": { + "type": "string", + "description": "An identifier for the role session, useful for tracking sessions in AWS logs.", + }, }, - "required": [ - "client_id", - "client_secret", - "tenant_id", - ], + "required": ["role_arn"], }, { "type": "object", - "title": "GCP static credentials", + "title": "Azure Static Credentials", "properties": { - "client_id": {"type": "string"}, - "client_secret": {"type": "string"}, - "refresh_token": {"type": "string"}, + "client_id": { + "type": "string", + "description": "The Azure application (client) ID for authentication in Azure AD.", + }, + "client_secret": { + "type": "string", + "description": "The client secret associated with the application (client) ID, providing " + "secure access.", + }, + "tenant_id": { + "type": "string", + "description": "The Azure tenant ID, representing the directory where the application is " + "registered.", + }, }, - "required": [ - "client_id", - "client_secret", - "refresh_token", - ], + "required": ["client_id", "client_secret", "tenant_id"], + }, + { + "type": "object", + "title": "GCP Static Credentials", + "properties": { + "client_id": { + "type": "string", + "description": "The client ID from Google Cloud, used to identify the application for GCP " + "access.", + }, + "client_secret": { + "type": "string", + "description": "The client secret associated with the GCP client ID, required for secure " + "access.", + }, + "refresh_token": { + "type": "string", + "description": "A refresh token that allows the application to obtain new access tokens for " + "extended use.", + }, + }, + "required": ["client_id", "client_secret", "refresh_token"], + }, + { + "type": "object", + "title": "Kubernetes Static Credentials", + "properties": { + "kubeconfig_content": { + "type": "string", + "description": "The content of the Kubernetes kubeconfig file, encoded as a string.", + } + }, + "required": ["kubeconfig_content"], }, ] } @@ -746,15 +859,16 @@ class ProviderSecretCreateSerializer(RLSSerializer, BaseWriteProviderSecretSeria def validate(self, attrs): provider = attrs.get("provider") + secret_type = attrs.get("secret_type") secret = attrs.get("secret") validated_attrs = super().validate(attrs) - self.validate_secret_based_on_provider(provider.provider, secret) + self.validate_secret_based_on_provider(provider.provider, secret_type, secret) return validated_attrs class ProviderSecretUpdateSerializer(BaseWriteProviderSecretSerializer): - secret = serializers.JSONField(write_only=True) + secret = ProviderSecretField(write_only=True) class Meta: model = ProviderSecret @@ -775,8 +889,9 @@ class ProviderSecretUpdateSerializer(BaseWriteProviderSecretSerializer): def validate(self, attrs): provider = self.instance.provider + secret_type = self.instance.secret_type secret = attrs.get("secret") validated_attrs = super().validate(attrs) - self.validate_secret_based_on_provider(provider.provider, secret) + self.validate_secret_based_on_provider(provider.provider, secret_type, secret) return validated_attrs diff --git a/src/backend/api/v1/views.py b/src/backend/api/v1/views.py index a921bbfcdd..c1db0b04c6 100644 --- a/src/backend/api/v1/views.py +++ b/src/backend/api/v1/views.py @@ -139,6 +139,41 @@ class SchemaView(SpectacularAPIView): spectacular_settings.DESCRIPTION = ( "Prowler API specification.\n\nThis file is auto-generated." ) + spectacular_settings.TAGS = [ + {"name": "User", "description": "Endpoints for managing user accounts."}, + { + "name": "Token", + "description": "Endpoints for token management, including obtaining a new token and " + "refreshing an existing token for authentication purposes.", + }, + { + "name": "Tenant", + "description": "Endpoints for managing tenants, along with their memberships.", + }, + { + "name": "Provider", + "description": "Endpoints for managing providers (AWS, GCP, Azure, etc...).", + }, + { + "name": "Scan", + "description": "Endpoints for triggering manual scans and viewing scan results.", + }, + { + "name": "Resource", + "description": "Endpoints for managing resources discovered by scans, allowing " + "retrieval and filtering of resource information.", + }, + { + "name": "Finding", + "description": "Endpoints for managing findings, allowing retrieval and filtering of " + "findings that result from scans.", + }, + { + "name": "Task", + "description": "Endpoints for task management, allowing retrieval of task status and " + "revoking tasks that have not started.", + }, + ] return super().get(request, *args, **kwargs)