diff --git a/.gitleaks.toml b/.gitleaks.toml index e4a48f1f5..2c19b9f12 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -5,5 +5,5 @@ description = "Global Allowlist" paths = [ '''tests\/unit\/data''', '''tools\/docker\/redis-tls''', - '''tests\/integration\/api\/test_webhook.py''' + '''tests\/integration\/api\/test_event_stream_ecdsa.py''' ] diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000..4cd98a9fc --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,34 @@ +# Complete documentation with many more options at: +# https://docs.sonarqube.org/latest/analysis/analysis-parameters/ + +## The unique project identifier. This is mandatory. +# Do not duplicate or reuse! +# Available characters: [a-zA-Z0-9_:\.\-] +# Must have least one non-digit. +# Recommended format: : +sonar.projectKey=ansible_eda-server + +sonar.organization=ansible + +# Customize what paths to scan. Default is . +sonar.sources=. + +# Verbose name of project displayed in WUI. Default is set to the projectKey. This field is optional. +sonar.projectName=eda-server + +# Version of project. This field is optional. +#sonar.projectVersion=1.0 + +# Tell sonar scanner where coverage files exist +#sonar.python.coverage.reportPaths=coverage.xml + +sonar.issue.ignore.multicriteria=e1 +# Ignore "should be a variable" +#sonar.issue.ignore.multicriteria.e1.ruleKey=python:S1192 +sonar.issue.ignore.multicriteria.e1.resourceKey=**/migrations/**/* + +# Only scan with python3 +sonar.python.version=3.9,3.10,3.11 + +# Ignore code dupe for the migrations +sonar.cpd.exclusions=**/migrations/*.py diff --git a/src/aap_eda/api/webhook_authentication.py b/src/aap_eda/api/event_stream_authentication.py similarity index 93% rename from src/aap_eda/api/webhook_authentication.py rename to src/aap_eda/api/event_stream_authentication.py index ad387bdf6..fa66b5e50 100644 --- a/src/aap_eda/api/webhook_authentication.py +++ b/src/aap_eda/api/event_stream_authentication.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Module providing all webhook authentication types.""" +"""Module providing all event stream authentication types.""" import base64 import hashlib @@ -37,8 +37,8 @@ DEFAULT_TIMEOUT = 30 -class WebhookAuthentication(ABC): - """Base class for Webhook Authentication.""" +class EventStreamAuthentication(ABC): + """Base class for EventStream Authentication.""" @abstractmethod def authenticate(self, body: Optional[bytes]): @@ -46,7 +46,7 @@ def authenticate(self, body: Optional[bytes]): @dataclass -class HMACAuthentication(WebhookAuthentication): +class HMACAuthentication(EventStreamAuthentication): """HMAC Parameters.""" signature: str @@ -87,7 +87,7 @@ def authenticate(self, body: bytes): @dataclass -class TokenAuthentication(WebhookAuthentication): +class TokenAuthentication(EventStreamAuthentication): """Token Authentication.""" token: str @@ -102,7 +102,7 @@ def authenticate(self, _body=None): @dataclass -class MTLSAuthentication(WebhookAuthentication): +class MTLSAuthentication(EventStreamAuthentication): """mTLS Authentication.""" subject: str @@ -117,8 +117,8 @@ def authenticate(self, _body=None): @dataclass -class BasicAuthentication(WebhookAuthentication): - """Token Authentication.""" +class BasicAuthentication(EventStreamAuthentication): + """Basic Authentication.""" password: str username: str @@ -139,7 +139,7 @@ def authenticate(self, _body=None): @dataclass -class Oauth2JwtAuthentication(WebhookAuthentication): +class Oauth2JwtAuthentication(EventStreamAuthentication): """OAuth2 JWT Authentication.""" jwks_url: str @@ -187,7 +187,7 @@ def authenticate(self, _body=None): @dataclass -class Oauth2Authentication(WebhookAuthentication): +class Oauth2Authentication(EventStreamAuthentication): """OAuth2 Authentication.""" introspection_url: str @@ -225,7 +225,7 @@ def authenticate(self, _body=None): @dataclass -class EcdsaAuthentication(WebhookAuthentication): +class EcdsaAuthentication(EventStreamAuthentication): """ECDSA Authentication.""" public_key: str diff --git a/src/aap_eda/api/exceptions.py b/src/aap_eda/api/exceptions.py index 1d982a317..35acd16d0 100644 --- a/src/aap_eda/api/exceptions.py +++ b/src/aap_eda/api/exceptions.py @@ -97,8 +97,9 @@ class InvalidWebsocketHost(APIException): ) -class InvalidWebhookSource(APIException): +class InvalidEventStreamSource(APIException): status_code = status.HTTP_500_INTERNAL_SERVER_ERROR default_detail = ( - "Configuration Error: Webhook source could not be updated in ruleset" + "Configuration Error: Event Stream source could not be " + "updated in ruleset" ) diff --git a/src/aap_eda/api/filters/__init__.py b/src/aap_eda/api/filters/__init__.py index b8cfa4b38..aa0445f3b 100644 --- a/src/aap_eda/api/filters/__init__.py +++ b/src/aap_eda/api/filters/__init__.py @@ -20,6 +20,7 @@ from .credential_type import CredentialTypeFilter from .decision_environment import DecisionEnvironmentFilter from .eda_credential import EdaCredentialFilter +from .event_stream import EventStreamFilter from .organization import OrganizationFilter from .project import ProjectFilter from .rulebook import ( @@ -30,7 +31,6 @@ ) from .team import OrganizationTeamFilter, TeamFilter from .user import UserFilter -from .webhook import WebhookFilter __all__ = ( # project @@ -56,6 +56,6 @@ # team "TeamFilter", "OrganizationTeamFilter", - # Webhook - "WebhookFilter", + # EventStream + "EventStreamFilter", ) diff --git a/src/aap_eda/api/filters/webhook.py b/src/aap_eda/api/filters/event_stream.py similarity index 85% rename from src/aap_eda/api/filters/webhook.py rename to src/aap_eda/api/filters/event_stream.py index b4547c7bf..ee8478e48 100644 --- a/src/aap_eda/api/filters/webhook.py +++ b/src/aap_eda/api/filters/event_stream.py @@ -17,13 +17,13 @@ from aap_eda.core import models -class WebhookFilter(django_filters.FilterSet): +class EventStreamFilter(django_filters.FilterSet): name = django_filters.CharFilter( field_name="name", lookup_expr="istartswith", - label="Filter by webhook name.", + label="Filter by event stream name.", ) class Meta: - model = models.Webhook + model = models.EventStream fields = ["name"] diff --git a/src/aap_eda/api/serializers/__init__.py b/src/aap_eda/api/serializers/__init__.py index ee8b66d81..71fed99fa 100644 --- a/src/aap_eda/api/serializers/__init__.py +++ b/src/aap_eda/api/serializers/__init__.py @@ -38,6 +38,7 @@ EdaCredentialCreateSerializer, EdaCredentialSerializer, ) +from .event_stream import EventStreamInSerializer, EventStreamOutSerializer from .organization import ( OrganizationCreateSerializer, OrganizationRefSerializer, @@ -75,7 +76,6 @@ UserListSerializer, UserSerializer, ) -from .webhook import WebhookInSerializer, WebhookOutSerializer __all__ = ( # auth @@ -129,7 +129,7 @@ "TeamCreateSerializer", "TeamUpdateSerializer", "TeamDetailSerializer", - # webhooks - "WebhookInSerializer", - "WebhookOutSerializer", + # event streams + "EventStreamInSerializer", + "EventStreamOutSerializer", ) diff --git a/src/aap_eda/api/serializers/activation.py b/src/aap_eda/api/serializers/activation.py index 2c5c47341..e172a89f3 100644 --- a/src/aap_eda/api/serializers/activation.py +++ b/src/aap_eda/api/serializers/activation.py @@ -26,11 +26,12 @@ EDA_SERVER_VAULT_LABEL, SOURCE_MAPPING_ERROR_KEY, ) -from aap_eda.api.exceptions import InvalidWebhookSource +from aap_eda.api.exceptions import InvalidEventStreamSource from aap_eda.api.serializers.decision_environment import ( DecisionEnvironmentRefSerializer, ) from aap_eda.api.serializers.eda_credential import EdaCredentialSerializer +from aap_eda.api.serializers.event_stream import EventStreamOutSerializer from aap_eda.api.serializers.organization import OrganizationRefSerializer from aap_eda.api.serializers.project import ( ANSIBLE_VAULT_STRING, @@ -38,7 +39,6 @@ ProjectRefSerializer, ) from aap_eda.api.serializers.rulebook import RulebookRefSerializer -from aap_eda.api.serializers.webhook import WebhookOutSerializer from aap_eda.api.vault import encrypt_string from aap_eda.core import models, validators from aap_eda.core.enums import DefaultCredentialType, ProcessParentType @@ -51,12 +51,17 @@ from aap_eda.core.utils.rulebook import ( build_source_list, get_rulebook_hash, - swap_webhook_sources, + swap_event_stream_sources, ) from aap_eda.core.utils.strings import substitute_variables logger = logging.getLogger(__name__) -REQUIRED_KEYS = ["webhook_id", "webhook_name", "source_name", "rulebook_hash"] +REQUIRED_KEYS = [ + "event_stream_id", + "event_stream_name", + "source_name", + "rulebook_hash", +] @dataclass @@ -65,7 +70,9 @@ class VaultData: password_used: bool = False -def _update_webhook_source(validated_data: dict, vault_data: VaultData) -> str: +def _update_event_stream_source( + validated_data: dict, vault_data: VaultData +) -> str: try: vault_data.password_used = True encrypted_dsn = encrypt_string( @@ -77,8 +84,8 @@ def _update_webhook_source(validated_data: dict, vault_data: VaultData) -> str: source_mappings = yaml.safe_load(validated_data["source_mappings"]) sources_info = {} for source_map in source_mappings: - webhook_id = source_map.get("webhook_id") - obj = models.Webhook.objects.get(id=webhook_id) + event_stream_id = source_map.get("event_stream_id") + obj = models.EventStream.objects.get(id=event_stream_id) sources_info[obj.name] = { "ansible.eda.pg_listener": { @@ -87,13 +94,15 @@ def _update_webhook_source(validated_data: dict, vault_data: VaultData) -> str: }, } - return swap_webhook_sources( + return swap_event_stream_sources( validated_data["rulebook_rulesets"], sources_info, source_mappings ) # TODO: Can we catch a better exception except Exception as e: - logger.error("Failed to update webhook source in rulesets: %s", str(e)) - raise InvalidWebhookSource(e) from e + logger.error( + "Failed to update event stream source in rulesets: %s", str(e) + ) + raise InvalidEventStreamSource(e) from e def _update_k8s_service_name(validated_data: dict) -> str: @@ -205,10 +214,10 @@ class ActivationSerializer(serializers.ModelSerializer): child=EdaCredentialSerializer(), ) - webhooks = serializers.ListField( + event_streams = serializers.ListField( required=False, allow_null=True, - child=WebhookOutSerializer(), + child=EventStreamOutSerializer(), ) class Meta: @@ -236,7 +245,7 @@ class Meta: "awx_token_id", "eda_credentials", "log_level", - "webhooks", + "event_streams", "skip_audit_events", ] read_only_fields = [ @@ -264,10 +273,10 @@ class ActivationListSerializer(serializers.ModelSerializer): allow_blank=True, help_text="Service name of the activation", ) - webhooks = serializers.ListField( + event_streams = serializers.ListField( required=False, allow_null=True, - child=WebhookOutSerializer(), + child=EventStreamOutSerializer(), ) class Meta: @@ -296,7 +305,7 @@ class Meta: "log_level", "eda_credentials", "k8s_service_name", - "webhooks", + "event_streams", "source_mappings", "skip_audit_events", ] @@ -315,9 +324,9 @@ def to_representation(self, activation): if activation.extra_var else None ) - webhooks = [ - WebhookOutSerializer(webhook).data - for webhook in activation.webhooks.all() + event_streams = [ + EventStreamOutSerializer(event_stream).data + for event_stream in activation.event_streams.all() ] return { @@ -344,7 +353,7 @@ def to_representation(self, activation): "log_level": activation.log_level, "eda_credentials": eda_credentials, "k8s_service_name": activation.k8s_service_name, - "webhooks": webhooks, + "event_streams": event_streams, "source_mappings": activation.source_mappings, "skip_audit_events": activation.skip_audit_events, } @@ -412,7 +421,7 @@ class Meta: def validate(self, data): _validate_credentials_and_token_and_rulebook(data=data, creating=True) - _validate_sources_with_webhooks(data=data) + _validate_sources_with_event_streams(data=data) return data def create(self, validated_data): @@ -432,7 +441,7 @@ def create(self, validated_data): vault_data = VaultData() if validated_data.get("source_mappings", []): - validated_data["rulebook_rulesets"] = _update_webhook_source( + validated_data["rulebook_rulesets"] = _update_event_stream_source( validated_data, vault_data ) @@ -519,10 +528,10 @@ class ActivationReadSerializer(serializers.ModelSerializer): allow_blank=True, help_text="Service name of the activation", ) - webhooks = serializers.ListField( + event_streams = serializers.ListField( required=False, allow_null=True, - child=WebhookOutSerializer(), + child=EventStreamOutSerializer(), ) class Meta: @@ -554,7 +563,7 @@ class Meta: "eda_credentials", "log_level", "k8s_service_name", - "webhooks", + "event_streams", "source_mappings", "skip_audit_events", ] @@ -607,9 +616,9 @@ def to_representation(self, activation): if activation.extra_var else None ) - webhooks = [ - WebhookOutSerializer(webhook).data - for webhook in activation.webhooks.all() + event_streams = [ + EventStreamOutSerializer(event_stream).data + for event_stream in activation.event_streams.all() ] return { @@ -641,7 +650,7 @@ def to_representation(self, activation): "log_level": activation.log_level, "eda_credentials": eda_credentials, "k8s_service_name": activation.k8s_service_name, - "webhooks": webhooks, + "event_streams": event_streams, "source_mappings": activation.source_mappings, "skip_audit_events": activation.skip_audit_events, } @@ -679,7 +688,7 @@ class PostActivationSerializer(serializers.ModelSerializer): def validate(self, data): _validate_credentials_and_token_and_rulebook(data=data, creating=False) - _validate_sources_with_webhooks(data=data) + _validate_sources_with_event_streams(data=data) return data class Meta: @@ -725,7 +734,7 @@ def is_activation_valid(activation: models.Activation) -> tuple[bool, str]: data["eda_credentials"] = [ obj.id for obj in activation.eda_credentials.all() ] - data["webhooks"] = [obj.id for obj in activation.webhooks.all()] + data["event_streams"] = [obj.id for obj in activation.event_streams.all()] serializer = PostActivationSerializer(data=data) valid = serializer.is_valid() @@ -913,8 +922,8 @@ def _get_aap_credentials_if_exists( ] -def _validate_sources_with_webhooks(data: dict) -> None: - """Ensure all webhooks have matching source names.""" +def _validate_sources_with_event_streams(data: dict) -> None: + """Ensure all event streams have matching source names.""" source_mappings = data.get("source_mappings") if not source_mappings: return @@ -971,8 +980,8 @@ def _validate_sources_with_webhooks(data: dict) -> None: {SOURCE_MAPPING_ERROR_KEY: [str(msg)]} ) - # validate no duplicate sources/webhooks in provided source mappings - for name in ["source_name", "webhook_name"]: + # validate no duplicate sources/event_streams in provided source mappings + for name in ["source_name", "event_stream_name"]: _check_duplicate_names(name, source_mappings) # validate rulebook is not updated during mapping @@ -997,8 +1006,8 @@ def _validate_sources_with_webhooks(data: dict) -> None: {SOURCE_MAPPING_ERROR_KEY: [msg]} ) - # validate webhook ids and names - data["webhooks"] = _get_webhook_ids(source_mappings) + # validate event_stream ids and names + data["event_streams"] = _get_event_stream_ids(source_mappings) def _validate_source_mappings(mappings: list[dict]) -> None: @@ -1015,35 +1024,37 @@ def _validate_source_mappings(mappings: list[dict]) -> None: ) -def _get_webhook_ids(source_mappings: list[dict]) -> set: - """Get all the webhook ids from source mappings.""" - webhook_ids = set() +def _get_event_stream_ids(source_mappings: list[dict]) -> set: + """Get all the event stream ids from source mappings.""" + event_stream_ids = set() for source_map in source_mappings: try: - webhook = models.Webhook.objects.get(id=source_map["webhook_id"]) - if webhook.name != source_map["webhook_name"]: + event_stream = models.EventStream.objects.get( + id=source_map["event_stream_id"] + ) + if event_stream.name != source_map["event_stream_name"]: msg = ( - f"Event stream {source_map['webhook_name']} did not" - f" match with webhook {webhook.name}" + f"Event stream {source_map['event_stream_name']} did not" + f" match with name {event_stream.name} in database" ) raise serializers.ValidationError( {SOURCE_MAPPING_ERROR_KEY: [msg]} ) - webhook_ids.add(webhook.id) - except models.Webhook.DoesNotExist as exc: - msg = f"Event stream id {source_map['webhook_id']} not found" + event_stream_ids.add(event_stream.id) + except models.EventStream.DoesNotExist as exc: + msg = f"Event stream id {source_map['event_stream_id']} not found" raise serializers.ValidationError( {SOURCE_MAPPING_ERROR_KEY: [msg]} ) from exc - return webhook_ids + return event_stream_ids def _check_duplicate_names( check_name: str, source_mappings: list[dict] ) -> None: - msg = {"source_name": "sources", "webhook_name": "event streams"} + msg = {"source_name": "sources", "event_stream_name": "event streams"} duplicate_names = set() names = set() diff --git a/src/aap_eda/api/serializers/eda_credential.py b/src/aap_eda/api/serializers/eda_credential.py index 485569707..8d5c60a94 100644 --- a/src/aap_eda/api/serializers/eda_credential.py +++ b/src/aap_eda/api/serializers/eda_credential.py @@ -185,7 +185,7 @@ def get_references(eda_credential: models.EdaCredential) -> list[dict]: resources = [] used_activations = eda_credential.activations.all() - used_webhooks = models.Webhook.objects.filter( + used_event_streams = models.EventStream.objects.filter( eda_credential=eda_credential ) used_decision_environments = models.DecisionEnvironment.objects.filter( @@ -205,12 +205,12 @@ def get_references(eda_credential: models.EdaCredential) -> list[dict]: } resources.append(resource) - for webhook in used_webhooks: + for event_stream in used_event_streams: resource = { - "type": "Webhook", - "id": webhook.id, - "name": webhook.name, - "url": (f"api/eda/v1/webhooks/{webhook.id}/"), + "type": "EventStream", + "id": event_stream.id, + "name": event_stream.name, + "url": (f"api/eda/v1/event-streams/{event_stream.id}/"), } resources.append(resource) diff --git a/src/aap_eda/api/serializers/webhook.py b/src/aap_eda/api/serializers/event_stream.py similarity index 88% rename from src/aap_eda/api/serializers/webhook.py rename to src/aap_eda/api/serializers/event_stream.py index f97d76ac7..6c6bfb03a 100644 --- a/src/aap_eda/api/serializers/webhook.py +++ b/src/aap_eda/api/serializers/event_stream.py @@ -20,19 +20,19 @@ from aap_eda.core import models, validators -class WebhookInSerializer(serializers.ModelSerializer): +class EventStreamInSerializer(serializers.ModelSerializer): organization_id = serializers.IntegerField(required=False, allow_null=True) owner = serializers.HiddenField(default=serializers.CurrentUserDefault()) eda_credential_id = serializers.IntegerField( required=True, allow_null=False, validators=[ - validators.check_credential_types_for_webhook, + validators.check_credential_types_for_event_stream, ], ) class Meta: - model = models.Webhook + model = models.EventStream fields = [ "name", "owner", @@ -40,11 +40,11 @@ class Meta: "additional_data_headers", "eda_credential_id", "organization_id", - "webhook_type", + "event_stream_type", ] -class WebhookOutSerializer(serializers.ModelSerializer): +class EventStreamOutSerializer(serializers.ModelSerializer): owner = serializers.SerializerMethodField() organization = serializers.SerializerMethodField() eda_credential = EdaCredentialRefSerializer( @@ -52,7 +52,7 @@ class WebhookOutSerializer(serializers.ModelSerializer): ) class Meta: - model = models.Webhook + model = models.EventStream read_only_fields = [ "id", "owner", @@ -72,7 +72,7 @@ class Meta: "additional_data_headers", "organization", "eda_credential", - "webhook_type", + "event_stream_type", *read_only_fields, ] diff --git a/src/aap_eda/api/urls.py b/src/aap_eda/api/urls.py index dcf68e2df..3cf765176 100644 --- a/src/aap_eda/api/urls.py +++ b/src/aap_eda/api/urls.py @@ -54,11 +54,11 @@ router.register("decision-environments", views.DecisionEnvironmentViewSet) router.register("organizations", views.OrganizationViewSet) router.register("teams", views.TeamViewSet) -router.register("webhooks", views.WebhookViewSet) +router.register("event-streams", views.EventStreamViewSet) router.register( - "external_webhook", - views.ExternalWebhookViewSet, - basename="external_webhook", + "external_event_stream", + views.ExternalEventStreamViewSet, + basename="external_event_stream", ) openapi_urls = [ diff --git a/src/aap_eda/api/views/__init__.py b/src/aap_eda/api/views/__init__.py index 6971d5a37..b7797d27b 100644 --- a/src/aap_eda/api/views/__init__.py +++ b/src/aap_eda/api/views/__init__.py @@ -18,14 +18,14 @@ from .credential_type import CredentialTypeViewSet from .decision_environment import DecisionEnvironmentViewSet from .eda_credential import EdaCredentialViewSet -from .external_webhook import ExternalWebhookViewSet +from .event_stream import EventStreamViewSet +from .external_event_stream import ExternalEventStreamViewSet from .organization import OrganizationViewSet from .project import ProjectViewSet from .root import ApiRootView, ApiV1RootView from .rulebook import AuditRuleViewSet, RulebookViewSet from .team import TeamViewSet from .user import CurrentUserAwxTokenViewSet, CurrentUserView, UserViewSet -from .webhook import WebhookViewSet __all__ = ( # auth @@ -56,8 +56,8 @@ "ApiRootView", # config "ConfigView", - # webhook - "WebhookViewSet", - # External webhook - "ExternalWebhookViewSet", + # Event stream + "EventStreamViewSet", + # External event stream + "ExternalEventStreamViewSet", ) diff --git a/src/aap_eda/api/views/webhook.py b/src/aap_eda/api/views/event_stream.py similarity index 64% rename from src/aap_eda/api/views/webhook.py rename to src/aap_eda/api/views/event_stream.py index e9c681271..421271cad 100644 --- a/src/aap_eda/api/views/webhook.py +++ b/src/aap_eda/api/views/event_stream.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Webhook configuration API Set.""" +"""EventStream configuration API Set.""" import logging from urllib.parse import urljoin @@ -33,40 +33,40 @@ from aap_eda.api import exceptions as api_exc, filters, serializers from aap_eda.core import models -from aap_eda.core.enums import ResourceType, WebhookAuthType +from aap_eda.core.enums import EventStreamAuthType, ResourceType from aap_eda.core.utils import logging_utils logger = logging.getLogger(__name__) -WEBHOOK_EXTERNAL_PATH = "api/eda/v1/external_webhook" +EVENT_STREAM_EXTERNAL_PATH = "api/eda/v1/external_event_stream" resource_name = "EventStream" -class WebhookViewSet( +class EventStreamViewSet( mixins.CreateModelMixin, viewsets.ReadOnlyModelViewSet, mixins.DestroyModelMixin, ): - queryset = models.Webhook.objects.order_by("-created_at") + queryset = models.EventStream.objects.order_by("-created_at") filter_backends = (defaultfilters.DjangoFilterBackend,) - filterset_class = filters.WebhookFilter - rbac_resource_type = ResourceType.WEBHOOK + filterset_class = filters.EventStreamFilter + rbac_resource_type = ResourceType.EVENT_STREAM def get_serializer_class(self): if self.action == "list": - return serializers.WebhookOutSerializer + return serializers.EventStreamOutSerializer if self.action == "destroy": - return serializers.WebhookOutSerializer + return serializers.EventStreamOutSerializer if self.action == "create": - return serializers.WebhookInSerializer + return serializers.EventStreamInSerializer if self.action == "partial_update": - return serializers.WebhookInSerializer + return serializers.EventStreamInSerializer - return serializers.WebhookOutSerializer + return serializers.EventStreamOutSerializer def get_response_serializer_class(self): - return serializers.WebhookOutSerializer + return serializers.EventStreamOutSerializer def filter_queryset(self, queryset): return super().filter_queryset( @@ -74,31 +74,33 @@ def filter_queryset(self, queryset): ) @extend_schema( - description="Get the Webhook by its id", + description="Get the EventStream by its id", responses={ status.HTTP_200_OK: OpenApiResponse( - serializers.WebhookOutSerializer, - description="Return the webhook by its id.", + serializers.EventStreamOutSerializer, + description="Return the event stream by its id.", ), }, ) def retrieve(self, request, *args, **kwargs): - webhook = self.get_object() + event_stream = self.get_object() logger.info( logging_utils.generate_simple_audit_log( "Read", resource_name, - webhook.name, - webhook.id, - webhook.organization, + event_stream.name, + event_stream.id, + event_stream.organization, ) ) - return Response(serializers.WebhookOutSerializer(webhook).data) + return Response( + serializers.EventStreamOutSerializer(event_stream).data + ) @extend_schema( - description="Delete a Webhook by its id", + description="Delete a EventStream by its id", responses={ status.HTTP_204_NO_CONTENT: OpenApiResponse( None, description="Delete successful." @@ -106,40 +108,42 @@ def retrieve(self, request, *args, **kwargs): }, ) def destroy(self, request, *args, **kwargs): - webhook = self.get_object() - ref_count = webhook.activations.count() + event_stream = self.get_object() + ref_count = event_stream.activations.count() if ref_count > 0: raise api_exc.Conflict( - f"Event stream '{webhook.name}' is being referenced by " + f"Event stream '{event_stream.name}' is being referenced by " f"{ref_count} activation(s) and cannot be deleted" ) - self.perform_destroy(webhook) + self.perform_destroy(event_stream) logger.info( logging_utils.generate_simple_audit_log( "Delete", resource_name, - webhook.name, - webhook.id, - webhook.organization, + event_stream.name, + event_stream.id, + event_stream.organization, ) ) return Response(status=status.HTTP_204_NO_CONTENT) @extend_schema( - description="List all webhooks", + description="List all eventstreams", responses={ status.HTTP_200_OK: OpenApiResponse( - serializers.WebhookOutSerializer(many=True), - description="Return a list of webhook.", + serializers.EventStreamOutSerializer(many=True), + description="Return a list of eventstreams.", ), }, ) def list(self, request, *args, **kwargs): - webhooks = models.Webhook.objects.all() - webhooks = self.filter_queryset(webhooks) - serializer = serializers.WebhookOutSerializer(webhooks, many=True) + event_streams = models.EventStream.objects.all() + event_streams = self.filter_queryset(event_streams) + serializer = serializers.EventStreamOutSerializer( + event_streams, many=True + ) result = self.paginate_queryset(serializer.data) logger.info( @@ -154,20 +158,20 @@ def list(self, request, *args, **kwargs): return self.get_paginated_response(result) @extend_schema( - request=serializers.WebhookInSerializer, + request=serializers.EventStreamInSerializer, responses={ status.HTTP_201_CREATED: OpenApiResponse( - serializers.WebhookOutSerializer, - description="Return the new webhook.", + serializers.EventStreamOutSerializer, + description="Return the new event stream.", ), status.HTTP_400_BAD_REQUEST: OpenApiResponse( - description="Invalid data to create webhook." + description="Invalid data to create event stream." ), }, ) def create(self, request, *args, **kwargs): context = {"request": request} - serializer = serializers.WebhookInSerializer( + serializer = serializers.EventStreamInSerializer( data=request.data, context=context, ) @@ -186,13 +190,15 @@ def create(self, request, *args, **kwargs): inputs = yaml.safe_load( response.eda_credential.inputs.get_secret_value() ) - sub_path = f"{WEBHOOK_EXTERNAL_PATH}/{response.uuid}/post/" - if inputs["auth_type"] == WebhookAuthType.MTLS: + sub_path = f"{EVENT_STREAM_EXTERNAL_PATH}/{response.uuid}/post/" + if inputs["auth_type"] == EventStreamAuthType.MTLS: response.url = urljoin( - settings.WEBHOOK_MTLS_BASE_URL, sub_path + settings.EVENT_STREAM_MTLS_BASE_URL, sub_path ) else: - response.url = urljoin(settings.WEBHOOK_BASE_URL, sub_path) + response.url = urljoin( + settings.EVENT_STREAM_BASE_URL, sub_path + ) response.save(update_fields=["url"]) logger.info( @@ -206,65 +212,65 @@ def create(self, request, *args, **kwargs): ) return Response( - serializers.WebhookOutSerializer(response).data, + serializers.EventStreamOutSerializer(response).data, status=status.HTTP_201_CREATED, ) @extend_schema( - request=serializers.WebhookInSerializer, + request=serializers.EventStreamInSerializer, responses={ status.HTTP_200_OK: OpenApiResponse( - serializers.WebhookOutSerializer, - description="Update successful, return the new webhook.", + serializers.EventStreamOutSerializer, + description="Update successful, return the new event stream.", ), status.HTTP_400_BAD_REQUEST: OpenApiResponse( - description="Unable to update webhook." + description="Unable to update event stream." ), }, ) def partial_update(self, request, *args, **kwargs): - webhook = self.get_object() - old_data = model_to_dict(webhook) + event_stream = self.get_object() + old_data = model_to_dict(event_stream) context = {"request": request} - serializer = serializers.WebhookInSerializer( - webhook, + serializer = serializers.EventStreamInSerializer( + event_stream, data=request.data, context=context, partial=True, ) serializer.is_valid(raise_exception=True) - if webhook.test_mode != request.data.get( - "test_mode", webhook.test_mode + if event_stream.test_mode != request.data.get( + "test_mode", event_stream.test_mode ): - webhook.test_content_type = "" - webhook.test_content = "" - webhook.test_headers = "" - webhook.test_error_message = "" + event_stream.test_content_type = "" + event_stream.test_content = "" + event_stream.test_headers = "" + event_stream.test_error_message = "" for key, value in serializer.validated_data.items(): - setattr(webhook, key, value) + setattr(event_stream, key, value) with transaction.atomic(): - webhook.save() + event_stream.save() check_related_permissions( request.user, serializer.Meta.model, old_data, - model_to_dict(webhook), + model_to_dict(event_stream), ) logger.info( logging_utils.generate_simple_audit_log( "Update", resource_name, - webhook.name, - webhook.id, - webhook.organization, + event_stream.name, + event_stream.id, + event_stream.organization, ) ) return Response( - serializers.WebhookOutSerializer(webhook).data, + serializers.EventStreamOutSerializer(event_stream).data, status=status.HTTP_200_OK, ) @@ -292,14 +298,18 @@ def partial_update(self, request, *args, **kwargs): url_path="(?P[^/.]+)/activations", ) def activations(self, request, id): - if not models.Webhook.access_qs(request.user).filter(id=id).exists(): + if ( + not models.EventStream.access_qs(request.user) + .filter(id=id) + .exists() + ): raise api_exc.NotFound( code=status.HTTP_404_NOT_FOUND, detail=f"Event stream with ID={id} does not exist.", ) - webhook = models.Webhook.objects.get(id=id) - activations = webhook.activations.all() + event_stream = models.EventStream.objects.get(id=id) + activations = event_stream.activations.all() filtered_activations = self.filter_queryset(activations) result = self.paginate_queryset(filtered_activations) @@ -309,9 +319,9 @@ def activations(self, request, id): logging_utils.generate_simple_audit_log( "ListActivations", resource_name, - webhook.name, - webhook.id, - webhook.organization, + event_stream.name, + event_stream.id, + event_stream.organization, ) ) return self.get_paginated_response(serializer.data) diff --git a/src/aap_eda/api/views/external_webhook.py b/src/aap_eda/api/views/external_event_stream.py similarity index 79% rename from src/aap_eda/api/views/external_webhook.py rename to src/aap_eda/api/views/external_event_stream.py index 45a4421b0..bc5d0d296 100644 --- a/src/aap_eda/api/views/external_webhook.py +++ b/src/aap_eda/api/views/external_event_stream.py @@ -11,7 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Module providing external webhook post.""" +"""Module providing external event stream post.""" import datetime import logging @@ -19,6 +19,7 @@ import yaml from django.conf import settings +from django.core.exceptions import ValidationError from django.db import transaction from django.db.models import F from django.http.request import HttpHeaders @@ -29,7 +30,7 @@ from rest_framework.permissions import AllowAny from rest_framework.response import Response -from aap_eda.api.webhook_authentication import ( +from aap_eda.api.event_stream_authentication import ( BasicAuthentication, EcdsaAuthentication, HMACAuthentication, @@ -38,28 +39,28 @@ Oauth2JwtAuthentication, TokenAuthentication, ) -from aap_eda.core.enums import Action, ResourceType, WebhookAuthType +from aap_eda.core.enums import Action, EventStreamAuthType, ResourceType from aap_eda.core.exceptions import PGNotifyError -from aap_eda.core.models import Webhook +from aap_eda.core.models import EventStream from aap_eda.services.pg_notify import PGNotify logger = logging.getLogger(__name__) -class ExternalWebhookViewSet(viewsets.GenericViewSet): - """External Webhook View Set.""" +class ExternalEventStreamViewSet(viewsets.GenericViewSet): + """External Event Stream View Set.""" rbac_action = None - rbac_resource_type = ResourceType.WEBHOOK + rbac_resource_type = ResourceType.EVENT_STREAM permission_classes = [AllowAny] authentication_classes = [] def get_rbac_permission(self): """RBAC Permissions.""" - return ResourceType.WEBHOOK, Action.READ + return ResourceType.EVENT_STREAM, Action.READ def __init__(self, *args, **kwargs): - self.webhook = None + self.event_stream = None super().__init__() def _update_test_data( @@ -70,13 +71,14 @@ def _update_test_data( headers: str = "", ): logger.warning( - "The webhook: %s is currently in test mode", self.webhook.name + "The event stream: %s is currently in test mode", + self.event_stream.name, ) - self.webhook.test_error_message = error_message - self.webhook.test_content_type = content_type - self.webhook.test_content = content - self.webhook.test_headers = headers - self.webhook.save( + self.event_stream.test_error_message = error_message + self.event_stream.test_content_type = content_type + self.event_stream.test_content = content + self.event_stream.test_headers = headers + self.event_stream.save( update_fields=[ "test_content_type", "test_content", @@ -103,8 +105,8 @@ def _create_payload( self, headers: HttpHeaders, data: dict, header_key: str, endpoint: str ) -> dict: event_headers = {} - if self.webhook.additional_data_headers: - for key in self.webhook.additional_data_headers.split(","): + if self.event_stream.additional_data_headers: + for key in self.event_stream.additional_data_headers.split(","): value = headers.get(key) if value: event_headers[key] = value @@ -117,18 +119,18 @@ def _create_payload( "payload": data, "meta": { "endpoint": endpoint, - "eda_webhook_name": self.webhook.name, + "eda_event_stream_name": self.event_stream.name, "headers": event_headers, }, } @transaction.atomic def _update_stats(self): - self.webhook.events_received = F("events_received") + 1 - self.webhook.last_event_received_at = datetime.datetime.now( + self.event_stream.events_received = F("events_received") + 1 + self.event_stream.last_event_received_at = datetime.datetime.now( tz=datetime.timezone.utc ) - self.webhook.save( + self.event_stream.save( update_fields=[ "events_received", "last_event_received_at", @@ -137,7 +139,7 @@ def _update_stats(self): def _handle_auth(self, request, inputs): try: - if inputs["auth_type"] == WebhookAuthType.HMAC: + if inputs["auth_type"] == EventStreamAuthType.HMAC: obj = HMACAuthentication( signature_encoding=inputs["signature_encoding"], signature_prefix=inputs.get("signature_prefix", ""), @@ -146,33 +148,33 @@ def _handle_auth(self, request, inputs): secret=inputs["secret"].encode("utf-8"), ) obj.authenticate(request.body) - elif inputs["auth_type"] == WebhookAuthType.MTLS: + elif inputs["auth_type"] == EventStreamAuthType.MTLS: obj = MTLSAuthentication( subject=inputs.get("subject", ""), value=request.headers[inputs["http_header_key"]], ) obj.authenticate() - elif inputs["auth_type"] == WebhookAuthType.TOKEN: + elif inputs["auth_type"] == EventStreamAuthType.TOKEN: obj = TokenAuthentication( token=inputs["token"], value=request.headers[inputs["http_header_key"]], ) obj.authenticate() - elif inputs["auth_type"] == WebhookAuthType.BASIC: + elif inputs["auth_type"] == EventStreamAuthType.BASIC: obj = BasicAuthentication( password=inputs["password"], username=inputs["username"], authorization=request.headers[inputs["http_header_key"]], ) obj.authenticate() - elif inputs["auth_type"] == WebhookAuthType.OAUTH2JWT: + elif inputs["auth_type"] == EventStreamAuthType.OAUTH2JWT: obj = Oauth2JwtAuthentication( jwks_url=inputs["jwks_url"], audience=inputs["audience"], access_token=request.headers[inputs["http_header_key"]], ) obj.authenticate() - elif inputs["auth_type"] == WebhookAuthType.OAUTH2: + elif inputs["auth_type"] == EventStreamAuthType.OAUTH2: obj = Oauth2Authentication( introspection_url=inputs["introspection_url"], token=request.headers[inputs["http_header_key"]], @@ -180,7 +182,7 @@ def _handle_auth(self, request, inputs): client_secret=inputs["client_secret"], ) obj.authenticate() - elif inputs["auth_type"] == WebhookAuthType.ECDSA: + elif inputs["auth_type"] == EventStreamAuthType.ECDSA: if inputs.get("prefix_http_header_key", ""): content_prefix = request.headers[ inputs["prefix_http_header_key"] @@ -202,7 +204,7 @@ def _handle_auth(self, request, inputs): raise ParseError(message) except AuthenticationFailed as err: self._update_stats() - if self.webhook.test_mode: + if self.event_stream.test_mode: self._update_test_data( error_message=err, headers=yaml.dump(dict(request.headers)), @@ -212,21 +214,21 @@ def _handle_auth(self, request, inputs): @extend_schema(exclude=True) @action(detail=True, methods=["POST"], rbac_action=None) def post(self, request, *_args, **kwargs): - """Handle posts from external webhook vendors.""" + """Handle posts from external vendors.""" try: - self.webhook = Webhook.objects.get(uuid=kwargs["pk"]) - except Webhook.DoesNotExist as exc: + self.event_stream = EventStream.objects.get(uuid=kwargs["pk"]) + except (EventStream.DoesNotExist, ValidationError) as exc: raise ParseError("bad uuid specified") from exc logger.debug("Headers %s", request.headers) logger.debug("Body %s", request.body) inputs = yaml.safe_load( - self.webhook.eda_credential.inputs.get_secret_value() + self.event_stream.eda_credential.inputs.get_secret_value() ) if inputs["http_header_key"] not in request.headers: message = f"{inputs['http_header_key']} header is missing" logger.error(message) - if self.webhook.test_mode: + if self.event_stream.test_mode: self._update_test_data( error_message=message, headers=yaml.dump(dict(request.headers)), @@ -254,7 +256,7 @@ def post(self, request, *_args, **kwargs): request.get_full_path(), ) self._update_stats() - if self.webhook.test_mode: + if self.event_stream.test_mode: self._update_test_data( content=yaml.dump(body), content_type=request.headers.get("Content-Type", "unknown"), @@ -264,7 +266,7 @@ def post(self, request, *_args, **kwargs): try: PGNotify( settings.PG_NOTIFY_DSN_SERVER, - self.webhook.channel_name, + self.event_stream.channel_name, payload, )() except PGNotifyError as e: diff --git a/src/aap_eda/core/enums.py b/src/aap_eda/core/enums.py index ff8cb61ee..8cbf59285 100644 --- a/src/aap_eda/core/enums.py +++ b/src/aap_eda/core/enums.py @@ -52,7 +52,7 @@ class ResourceType(DjangoStrEnum): EDA_CREDENTIAL = "eda_credential" ORGANIZATION = "organization" TEAM = "team" - WEBHOOK = "webhook" + EVENT_STREAM = "event_stream" class Action(DjangoStrEnum): @@ -139,8 +139,8 @@ class RulebookProcessLogLevel(DjangoStrEnum): ERROR = "error" -class WebhookAuthType(DjangoStrEnum): - """Types of authentication for Webhook.""" +class EventStreamAuthType(DjangoStrEnum): + """Types of authentication for EventStream.""" HMAC = "hmac" TOKEN = "token" @@ -158,18 +158,18 @@ class SignatureEncodingType(DjangoStrEnum): HEX = "hex" -class WebhookCredentialType(DjangoStrEnum): - HMAC = "HMAC Webhook" - BASIC = "Basic Webhook" - TOKEN = "Token Webhook" - OAUTH2 = "Oauth2 Webhook" - OAUTH2_JWT = "Oauth2 JWT Webhook" - ECDSA = "ECDSA Webhook" - MTLS = "mTLS Webhook" +class EventStreamCredentialType(DjangoStrEnum): + HMAC = "HMAC Event Stream" + BASIC = "Basic Event Stream" + TOKEN = "Token Event Stream" + OAUTH2 = "Oauth2 Event Stream" + OAUTH2_JWT = "Oauth2 JWT Event Stream" + ECDSA = "ECDSA Event Stream" + MTLS = "mTLS Event Stream" -class CustomWebhookCredentialType(DjangoStrEnum): - GITLAB = "GITLAB Webhook" - GITHUB = "GitHub Webhook" - SNOW = "Service Now Webhook" - DYNATRACE = "Dynatrace Webhook" +class CustomEventStreamCredentialType(DjangoStrEnum): + GITLAB = "GITLAB Event Stream" + GITHUB = "GitHub Event Stream" + SNOW = "Service Now Event Stream" + DYNATRACE = "Dynatrace Event Stream" diff --git a/src/aap_eda/core/management/commands/create_initial_data.py b/src/aap_eda/core/management/commands/create_initial_data.py index 8ab529680..9f8fe7196 100644 --- a/src/aap_eda/core/management/commands/create_initial_data.py +++ b/src/aap_eda/core/management/commands/create_initial_data.py @@ -30,7 +30,7 @@ CRUD = ["add", "view", "change", "delete"] LOGGER = logging.getLogger(__name__) AVAILABLE_ALGORITHMS = sorted(hashlib.algorithms_available) -AUTH_TYPE_LABEL = "Webhook Authentication Type" +AUTH_TYPE_LABEL = "Event Stream Authentication Type" SIGNATURE_ENCODING_LABEL = "Signature Encoding" HTTP_HEADER_LABEL = "HTTP Header Key" # FIXME(cutwater): Role descriptions were taken from the RBAC design document @@ -59,7 +59,7 @@ "rulebook": ["view"], "decision_environment": CRUD, "eda_credential": CRUD, - "webhook": CRUD, + "event_stream": CRUD, }, }, { @@ -85,7 +85,7 @@ "rulebook": ["view"], "decision_environment": ["add", "view", "change"], "eda_credential": ["add", "view", "change"], - "webhook": ["add", "view", "change"], + "event_stream": ["add", "view", "change"], }, }, { @@ -111,7 +111,7 @@ "rulebook": ["view"], "decision_environment": ["add", "view", "change"], "eda_credential": ["add", "view", "change"], - "webhook": ["add", "view", "change"], + "event_stream": ["add", "view", "change"], }, }, { @@ -131,7 +131,7 @@ "rulebook": ["view"], "decision_environment": ["view"], "eda_credential": ["view"], - "webhook": ["view"], + "event_stream": ["view"], }, }, { @@ -149,7 +149,7 @@ "rulebook": ["view"], "decision_environment": ["view"], "eda_credential": ["view"], - "webhook": ["view"], + "event_stream": ["view"], }, }, { @@ -167,7 +167,7 @@ "rulebook": ["view"], "decision_environment": ["view"], "eda_credential": ["view"], - "webhook": ["view"], + "event_stream": ["view"], }, }, ] @@ -331,7 +331,7 @@ "required": ["vault_password"], } -WEBHOOK_HMAC_INPUTS = { +EVENT_STREAM_HMAC_INPUTS = { "fields": [ { "id": "auth_type", @@ -346,9 +346,9 @@ "type": "string", "secret": True, "help_text": ( - "The symmetrical shared secret between EDA and the Webhook " - "Server. Please save this value since you would need it on " - "the Webhook Server." + "The symmetrical shared secret between EDA and the " + "Event Stream Server. Please save this value since " + "you would need it on the Event Stream Server." ), }, { @@ -358,8 +358,9 @@ "default": "sha256", "choices": AVAILABLE_ALGORITHMS, "help_text": ( - "The Webhook sender hashes the message being sent using one " - "of these algorithms, which guarantees message integrity." + "The EventStream sender hashes the message being " + "sent using one of these algorithms, which guarantees " + "message integrity." ), }, { @@ -368,7 +369,7 @@ "type": "string", "default": "X-Hub-Signature-256", "help_text": ( - "The webhook sender typically uses a special HTTP header " + "The event stream sender typically uses a special HTTP header " "to send the signature of the payload. e.g X-Hub-Signature-256" ), }, @@ -401,7 +402,7 @@ ], } -WEBHOOK_BASIC_INPUTS = { +EVENT_STREAM_BASIC_INPUTS = { "fields": [ { "id": "auth_type", @@ -415,7 +416,7 @@ "label": "Username", "type": "string", "help_text": ( - "The username used to authenticate the incoming webhook" + "The username used to authenticate the incoming event stream" ), }, { @@ -424,7 +425,7 @@ "type": "string", "secret": True, "help_text": ( - "The password used to authenticate the incoming webhook" + "The password used to authenticate the incoming event stream" ), }, { @@ -438,7 +439,7 @@ "required": ["auth_type", "password", "username", "http_header_key"], } -WEBHOOK_TOKEN_INPUTS = { +EVENT_STREAM_TOKEN_INPUTS = { "fields": [ { "id": "auth_type", @@ -453,9 +454,9 @@ "type": "string", "secret": True, "help_text": ( - "The symmetrical shared token between EDA and the Webhook " + "The symmetrical shared token between EDA and the EventStream " "Server. Please save this value since you would need it on " - "the Webhook Server." + "the EventStream Server." ), }, { @@ -473,7 +474,7 @@ "required": ["auth_type", "token", "http_header_key"], } -WEBHOOK_OAUTH2_INPUTS = { +EVENT_STREAM_OAUTH2_INPUTS = { "fields": [ { "id": "auth_type", @@ -525,7 +526,7 @@ ], } -WEBHOOK_OAUTH2_JWT_INPUTS = { +EVENT_STREAM_OAUTH2_JWT_INPUTS = { "fields": [ { "id": "auth_type", @@ -568,7 +569,7 @@ "required": ["auth_type", "jwks_url", "http_header_key"], } -WEBHOOK_ECDSA_INPUTS = { +EVENT_STREAM_ECDSA_INPUTS = { "fields": [ { "id": "auth_type", @@ -593,7 +594,7 @@ "help_text": ( "Public Key for validating the data, this would be " "available from the sender after you have created the " - "webhook on their side with our URL. This is usually a " + "event stream on their side with our URL. This is usually a " "2 step process" ), "multiline": True, @@ -623,8 +624,9 @@ "default": "sha256", "choices": AVAILABLE_ALGORITHMS, "help_text": ( - "The Webhook sender hashes the message being sent using one " - "of these algorithms, which guarantees message integrity." + "The EventStream sender hashes the message being " + "sent using one of these algorithms, which guarantees " + "message integrity." ), }, ], @@ -637,7 +639,7 @@ ], } -WEBHOOK_MTLS_INPUTS = { +EVENT_STREAM_MTLS_INPUTS = { "fields": [ { "id": "auth_type", @@ -671,7 +673,7 @@ "required": ["auth_type", "http_header_key"], } -WEBHOOK_GITLAB_INPUTS = { +EVENT_STREAM_GITLAB_INPUTS = { "fields": [ { "id": "auth_type", @@ -688,7 +690,7 @@ "help_text": ( "The symmetrical shared token between EDA and the Gitlab " "Server. Please save this value since you would need it on " - "the Webhook Server." + "the EventStream Server." ), }, { @@ -707,7 +709,7 @@ "required": ["auth_type", "token", "http_header_key"], } -WEBHOOK_GITHUB_INPUTS = { +EVENT_STREAM_GITHUB_INPUTS = { "fields": [ { "id": "auth_type", @@ -723,8 +725,8 @@ "secret": True, "help_text": ( "The symmetrical shared secret between EDA and " - "the Webhook Server. Please save this value since " - "you would need it on the Webhook Server." + "the EventStream Server. Please save this value since " + "you would need it on the EventStream Server." ), }, { @@ -734,7 +736,7 @@ "default": "sha256", "choices": ["sha128", "sha256", "sha512", "sha1024"], "help_text": ( - "The Webhook sender hashes the message being sent " + "The EventStream sender hashes the message being sent " "using one of these algorithms, which guarantees " "message integrity." ), @@ -746,7 +748,7 @@ "type": "string", "default": "X-Hub-Signature-256", "help_text": ( - "The webhook sender typically uses a special " + "The event stream sender typically uses a special " "HTTP header to send the signature of the payload. " "e.g X-Hub-Signature-256" ), @@ -786,7 +788,7 @@ ], } -WEBHOOK_SNOW_INPUTS = { +EVENT_STREAM_SNOW_INPUTS = { "fields": [ { "id": "auth_type", @@ -803,7 +805,7 @@ "help_text": ( "The symmetrical shared token between EDA and the ServiceNow " "Server. Please save this value since you would need it on " - "the Webhook Server." + "the EventStream Server." ), }, { @@ -822,7 +824,7 @@ "required": ["auth_type", "token", "http_header_key"], } -WEBHOOK_DYNATRACE_INPUTS = { +EVENT_STREAM_DYNATRACE_INPUTS = { "fields": [ { "id": "auth_type", @@ -836,7 +838,7 @@ "label": "Username", "type": "string", "help_text": ( - "The username used to authenticate the incoming webhook" + "The username used to authenticate the incoming event stream" ), }, { @@ -845,7 +847,7 @@ "type": "string", "secret": True, "help_text": ( - "The password used to authenticate the incoming webhook" + "The password used to authenticate the incoming event stream" ), }, { @@ -905,14 +907,14 @@ "managed": True, }, { - "name": enums.WebhookCredentialType.HMAC, - "namespace": "webhook", + "name": enums.EventStreamCredentialType.HMAC, + "namespace": "event_stream", "kind": "hmac", - "inputs": WEBHOOK_HMAC_INPUTS, + "inputs": EVENT_STREAM_HMAC_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Webhooks that use HMAC. " + "Credential for Event Streams that use HMAC. " "This requires shared secret between the sender and receiver. " "The signature can be sent as hex or base64 strings. " "Most of senders will use a special HTTP header to send " @@ -920,133 +922,133 @@ ), }, { - "name": enums.WebhookCredentialType.BASIC, - "namespace": "webhook", + "name": enums.EventStreamCredentialType.BASIC, + "namespace": "event_stream", "kind": "basic", - "inputs": WEBHOOK_BASIC_INPUTS, + "inputs": EVENT_STREAM_BASIC_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Webhooks that use Basic Authentication. " + "Credential for EventStreams that use Basic Authentication. " "It requires a username and password" ), }, { - "name": enums.WebhookCredentialType.TOKEN, - "namespace": "webhook", + "name": enums.EventStreamCredentialType.TOKEN, + "namespace": "event_stream", "kind": "token", - "inputs": WEBHOOK_TOKEN_INPUTS, + "inputs": EVENT_STREAM_TOKEN_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Webhooks that use Token Authentication. " + "Credential for Event Streams that use Token Authentication. " "Usually the token is sent in the Authorization header. " "Some of the senders will use a special HTTP header to send " "the token." ), }, { - "name": enums.WebhookCredentialType.OAUTH2, - "namespace": "webhook", + "name": enums.EventStreamCredentialType.OAUTH2, + "namespace": "event_stream", "kind": "oauth2", - "inputs": WEBHOOK_OAUTH2_INPUTS, + "inputs": EVENT_STREAM_OAUTH2_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Webhooks that use OAuth2. " + "Credential for Event Streams that use OAuth2. " "This needs a client id and client credential and access " "to an Authorization server so we can introspect the token " "being sent." ), }, { - "name": enums.WebhookCredentialType.OAUTH2_JWT, - "namespace": "webhook", + "name": enums.EventStreamCredentialType.OAUTH2_JWT, + "namespace": "event_stream", "kind": "oauth2_jwt", - "inputs": WEBHOOK_OAUTH2_JWT_INPUTS, + "inputs": EVENT_STREAM_OAUTH2_JWT_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Webhooks that use OAuth2 with JWT. " + "Credential for Event Streams that use OAuth2 with JWT. " "This needs a JWKS URL which will be used to fetch the " "public key and validate the incoming token. If an audience " "is specified we will check the audience in the JWT claims." ), }, { - "name": enums.WebhookCredentialType.ECDSA, - "namespace": "webhook", + "name": enums.EventStreamCredentialType.ECDSA, + "namespace": "event_stream", "kind": "ecdsa", - "inputs": WEBHOOK_ECDSA_INPUTS, + "inputs": EVENT_STREAM_ECDSA_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Webhooks that use Elliptic Curve DSA. " + "Credential for Event Streams that use Elliptic Curve DSA. " "This requires a public key and the headers that carry " "the signature." ), }, { - "name": enums.WebhookCredentialType.MTLS, - "namespace": "webhook", + "name": enums.EventStreamCredentialType.MTLS, + "namespace": "event_stream", "kind": "mtls", - "inputs": WEBHOOK_MTLS_INPUTS, + "inputs": EVENT_STREAM_MTLS_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Webhooks that use mutual TLS. " + "Credential for Event Streams that use mutual TLS. " "The Certificate is installed in the Web Server and " "we can optionally validate the Subject defined in the " "Certificate." ), }, { - "name": enums.CustomWebhookCredentialType.GITLAB, - "namespace": "webhook", + "name": enums.CustomEventStreamCredentialType.GITLAB, + "namespace": "event_stream", "kind": "gitlab", - "inputs": WEBHOOK_GITLAB_INPUTS, + "inputs": EVENT_STREAM_GITLAB_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Gitlab Webhook. This is a specialization of " + "Credential for Gitlab Event Streams. This is a specialization of " "the Token authentication with the X-Gitlab-Token header." ), }, { - "name": enums.CustomWebhookCredentialType.GITHUB, - "namespace": "webhook", + "name": enums.CustomEventStreamCredentialType.GITHUB, + "namespace": "event_stream", "kind": "github", - "inputs": WEBHOOK_GITHUB_INPUTS, + "inputs": EVENT_STREAM_GITHUB_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Github Webhook. This is a specialization of " + "Credential for Github EventStream. This is a specialization of " "the HMAC authentication which only requires a secret to be " "provided." ), }, { - "name": enums.CustomWebhookCredentialType.SNOW, - "namespace": "webhook", + "name": enums.CustomEventStreamCredentialType.SNOW, + "namespace": "event_stream", "kind": "snow", - "inputs": WEBHOOK_SNOW_INPUTS, + "inputs": EVENT_STREAM_SNOW_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for ServiceNow Webhook. This is a specialization of " - "the Token authentication which only requires a token to be " - "provided." + "Credential for ServiceNow Event Stream. This is a " + "specialization of the Token authentication which " + "only requires a token to be provided." ), }, { - "name": enums.CustomWebhookCredentialType.DYNATRACE, - "namespace": "webhook", + "name": enums.CustomEventStreamCredentialType.DYNATRACE, + "namespace": "event_stream", "kind": "dynatrace", - "inputs": WEBHOOK_DYNATRACE_INPUTS, + "inputs": EVENT_STREAM_DYNATRACE_INPUTS, "injectors": {}, "managed": True, "description": ( - "Credential for Dynatrace Webhook. This is a clone of " + "Credential for Dynatrace Event Stream. This is a clone of " "the Basic authentication." ), }, diff --git a/src/aap_eda/core/migrations/0047_eventstream_remove_activation_webhooks_and_more.py b/src/aap_eda/core/migrations/0047_eventstream_remove_activation_webhooks_and_more.py new file mode 100644 index 000000000..ccfcc18da --- /dev/null +++ b/src/aap_eda/core/migrations/0047_eventstream_remove_activation_webhooks_and_more.py @@ -0,0 +1,157 @@ +# Generated by Django 4.2.7 on 2024-08-15 21:11 + +import uuid + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + +import aap_eda.core.models.utils + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0046_remove_activation_event_streams_and_more"), + ] + + operations = [ + migrations.CreateModel( + name="EventStream", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "name", + models.TextField( + help_text="The name of the webhook", unique=True + ), + ), + ( + "event_stream_type", + models.TextField( + default="hmac", + help_text="The type of the event stream based on credential type", + ), + ), + ( + "additional_data_headers", + models.TextField( + blank=True, + help_text="The additional http headers which will be added to the event data. The headers are comma delimited", + ), + ), + ( + "test_mode", + models.BooleanField( + default=False, help_text="Enable test mode" + ), + ), + ( + "test_content_type", + models.TextField( + blank=True, + default="", + help_text="The content type of test data, when in test mode", + ), + ), + ( + "test_content", + models.TextField( + blank=True, + default="", + help_text="The content recieved, when in test mode, stored as a yaml string", + ), + ), + ( + "test_headers", + models.TextField( + blank=True, + default="", + help_text="The headers recieved, when in test mode, stored as a yaml string", + ), + ), + ( + "test_error_message", + models.TextField( + blank=True, + default="", + help_text="The error message, when in test mode", + ), + ), + ("uuid", models.UUIDField(default=uuid.uuid4)), + ( + "url", + models.TextField( + help_text="The URL which will be used to post the data to the event stream" + ), + ), + ("created_at", models.DateTimeField(auto_now_add=True)), + ("modified_at", models.DateTimeField(auto_now=True)), + ( + "events_received", + models.BigIntegerField( + default=0, + help_text="The total number of events received by event stream", + ), + ), + ( + "last_event_received_at", + models.DateTimeField( + help_text="The date/time when the last event was received", + null=True, + ), + ), + ( + "eda_credential", + models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="core.edacredential", + ), + ), + ( + "organization", + models.ForeignKey( + default=aap_eda.core.models.utils.get_default_organization_id, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="core.organization", + ), + ), + ( + "owner", + models.ForeignKey( + help_text="The user who created the webhook", + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + ), + migrations.RemoveField( + model_name="activation", + name="webhooks", + ), + migrations.DeleteModel( + name="Webhook", + ), + migrations.AddField( + model_name="activation", + name="event_streams", + field=models.ManyToManyField( + default=None, related_name="activations", to="core.eventstream" + ), + ), + ] diff --git a/src/aap_eda/core/migrations/0048_alter_eventstream_url.py b/src/aap_eda/core/migrations/0048_alter_eventstream_url.py new file mode 100644 index 000000000..982353d65 --- /dev/null +++ b/src/aap_eda/core/migrations/0048_alter_eventstream_url.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.7 on 2024-08-15 23:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("core", "0047_eventstream_remove_activation_webhooks_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="eventstream", + name="url", + field=models.TextField( + help_text="The URL which will be used to post to the event stream" + ), + ), + ] diff --git a/src/aap_eda/core/models/__init__.py b/src/aap_eda/core/models/__init__.py index 04c9f6eb9..30cf95f36 100644 --- a/src/aap_eda/core/models/__init__.py +++ b/src/aap_eda/core/models/__init__.py @@ -19,6 +19,7 @@ from .credential_type import CredentialType from .decision_environment import DecisionEnvironment from .eda_credential import EdaCredential +from .event_stream import EventStream from .job import ( ActivationInstanceJobInstance, Job, @@ -44,7 +45,6 @@ ) from .team import Team from .user import AwxToken, User -from .webhook import Webhook __all__ = [ "ActivationInstanceJobInstance", @@ -72,7 +72,7 @@ "ActivationRequestQueue", "Organization", "Team", - "Webhook", + "EventStream", ] permission_registry.register( @@ -82,7 +82,7 @@ Project, Organization, Team, - Webhook, + EventStream, parent_field_name="organization", ) permission_registry.register( diff --git a/src/aap_eda/core/models/activation.py b/src/aap_eda/core/models/activation.py index 442a4584c..0e59cfaee 100644 --- a/src/aap_eda/core/models/activation.py +++ b/src/aap_eda/core/models/activation.py @@ -24,9 +24,9 @@ from aap_eda.services.activation.engine.common import ContainerableMixin from .base import BaseOrgModel, UniqueNamedModel +from .event_stream import EventStream from .mixins import OnDeleteProcessParentMixin, StatusHandlerModelMixin from .user import AwxToken, User -from .webhook import Webhook __all__ = ("Activation",) @@ -131,8 +131,8 @@ class Meta: blank=True, help_text="Name of the kubernetes service", ) - webhooks = models.ManyToManyField( - Webhook, related_name="activations", default=None + event_streams = models.ManyToManyField( + EventStream, related_name="activations", default=None ) source_mappings = models.TextField( default="", diff --git a/src/aap_eda/core/models/webhook.py b/src/aap_eda/core/models/event_stream.py similarity index 80% rename from src/aap_eda/core/models/webhook.py rename to src/aap_eda/core/models/event_stream.py index 7c5969d13..c2a2859f3 100644 --- a/src/aap_eda/core/models/webhook.py +++ b/src/aap_eda/core/models/event_stream.py @@ -18,27 +18,19 @@ from .base import BaseOrgModel -__all__ = "Webhook" +__all__ = "EventStream" -EDA_WEBHOOK_CHANNEL_PREFIX = "eda_webhook_" +EDA_EVENT_STREAM_CHANNEL_PREFIX = "eda_event_stream_" -class Webhook(BaseOrgModel): - class Meta: - db_table = "core_webhook" - indexes = [ - models.Index(fields=["id"], name="ix_webhook_id"), - models.Index(fields=["name"], name="ix_webhook_name"), - models.Index(fields=["uuid"], name="ix_webhook_uuid"), - ] - +class EventStream(BaseOrgModel): name = models.TextField( null=False, unique=True, help_text="The name of the webhook" ) - webhook_type = models.TextField( + event_stream_type = models.TextField( null=False, - help_text="The type of the webhook based on credential type", + help_text="The type of the event stream based on credential type", default="hmac", ) @@ -98,12 +90,13 @@ class Meta: uuid = models.UUIDField(default=uuid.uuid4) url = models.TextField( null=False, - help_text="The URL which will be used to post the data to the webhook", + help_text="The URL which will be used to post to the event stream", ) created_at = models.DateTimeField(auto_now_add=True, null=False) modified_at = models.DateTimeField(auto_now=True, null=False) events_received = models.BigIntegerField( - default=0, help_text="The total number of events received by webhook" + default=0, + help_text="The total number of events received by event stream", ) last_event_received_at = models.DateTimeField( null=True, help_text="The date/time when the last event was received" @@ -112,7 +105,7 @@ class Meta: def _get_channel_name(self) -> str: """Generate the channel name based on the UUID and prefix.""" return ( - f"{EDA_WEBHOOK_CHANNEL_PREFIX}" + f"{EDA_EVENT_STREAM_CHANNEL_PREFIX}" f"{str(self.uuid).replace('-','_')}" ) diff --git a/src/aap_eda/core/utils/rulebook.py b/src/aap_eda/core/utils/rulebook.py index efdb157a0..a3e29fbfb 100644 --- a/src/aap_eda/core/utils/rulebook.py +++ b/src/aap_eda/core/utils/rulebook.py @@ -90,10 +90,10 @@ def get_rulebook_hash(rulebook: str) -> str: return sha256.hexdigest() -def swap_webhook_sources( - data: str, webhook_sources: dict, mappings: list[dict] +def swap_event_stream_sources( + data: str, event_stream_sources: dict, mappings: list[dict] ) -> str: - """Swap out the sources with webhook sources that match the name. + """Swap out the sources with event stream sources that match the name. Preserve the filters if they exist for the source. """ @@ -102,7 +102,8 @@ def swap_webhook_sources( current_names = set() mapping_dict = { - mapping["source_name"]: mapping["webhook_name"] for mapping in mappings + mapping["source_name"]: mapping["event_stream_name"] + for mapping in mappings } for ruleset in rulesets: @@ -118,15 +119,16 @@ def swap_webhook_sources( current_names.add(src_name) if src_name in mapping_dict: - webhook_name = mapping_dict[src_name] + event_stream_name = mapping_dict[src_name] - if webhook_name in webhook_sources: - updated_source = _updated_webhook_source( - webhook_name, source, webhook_sources + if event_stream_name in event_stream_sources: + updated_source = _updated_event_stream_source( + event_stream_name, source, event_stream_sources ) new_sources.append(updated_source) LOGGER.debug( - "Source %s updated with Webhook Source", webhook_name + "Source %s updated with Event Stream Source", + event_stream_name, ) else: msg = f"No event stream found for source {src_name}" @@ -141,13 +143,13 @@ def swap_webhook_sources( return yaml.dump(rulesets, sort_keys=False) -def _updated_webhook_source( - name: str, source: dict, webhook_sources: dict +def _updated_event_stream_source( + name: str, source: dict, event_stream_sources: dict ) -> dict: updated_source = {"name": name} - source_type = next(iter(webhook_sources[name])) - updated_source[source_type] = webhook_sources[name][source_type] + source_type = next(iter(event_stream_sources[name])) + updated_source[source_type] = event_stream_sources[name][source_type] if "filters" in source: updated_source["filters"] = source["filters"] - LOGGER.debug("Source %s updated with Webhook Source", name) + LOGGER.debug("Source %s updated with Event Stream Source", name) return updated_source diff --git a/src/aap_eda/core/validators.py b/src/aap_eda/core/validators.py index 4426031c5..b0d8b57cf 100644 --- a/src/aap_eda/core/validators.py +++ b/src/aap_eda/core/validators.py @@ -275,55 +275,55 @@ def valid_hash_format(fmt: str): return fmt -def _validate_webhook_settings(auth_type: str): - """Check webhook settings.""" +def _validate_event_stream_settings(auth_type: str): + """Check event stream settings.""" if ( - auth_type == enums.WebhookCredentialType.MTLS - and not settings.WEBHOOK_MTLS_BASE_URL + auth_type == enums.EventStreamCredentialType.MTLS + and not settings.EVENT_STREAM_MTLS_BASE_URL ): raise serializers.ValidationError( ( f"EventStream of type {auth_type} cannot be used " - "because WEBHOOK_MTLS_BASE_URL is missing in settings." + "because EVENT_STREAM_MTLS_BASE_URL is missing in settings." ) ) if ( - auth_type != enums.WebhookCredentialType.MTLS - and not settings.WEBHOOK_BASE_URL + auth_type != enums.EventStreamCredentialType.MTLS + and not settings.EVENT_STREAM_BASE_URL ): raise serializers.ValidationError( ( f"EventStream of type {auth_type} cannot be used " - "because WEBHOOK_BASE_URL is missing in settings." + "because EVENT_STREAM_BASE_URL is missing in settings." ) ) -def check_if_webhooks_exists(webhook_ids: list[int]) -> list[int]: - """Check a webhook exists.""" - for webhook_id in webhook_ids: +def check_if_event_streams_exists(event_stream_ids: list[int]) -> list[int]: + """Check a event stream exists.""" + for event_stream_id in event_stream_ids: try: - models.Webhook.objects.get(pk=webhook_id) - except models.Webhook.DoesNotExist as exc: + models.EventStream.objects.get(pk=event_stream_id) + except models.EventStream.DoesNotExist as exc: raise serializers.ValidationError( - f"Webhook with id {webhook_id} does not exist" + f"EventStream with id {event_stream_id} does not exist" ) from exc - return webhook_ids + return event_stream_ids -def check_credential_types_for_webhook(eda_credential_id: int) -> int: - """Check the credential types for a webhook.""" +def check_credential_types_for_event_stream(eda_credential_id: int) -> int: + """Check the credential types for a event stream.""" credential = get_credential_if_exists(eda_credential_id) name = credential.credential_type.name names = ( - enums.WebhookCredentialType.values() - + enums.CustomWebhookCredentialType.values() + enums.EventStreamCredentialType.values() + + enums.CustomEventStreamCredentialType.values() ) if name not in names: raise serializers.ValidationError( f"The type of credential can only be one of {names}" ) - _validate_webhook_settings(name) + _validate_event_stream_settings(name) return eda_credential_id diff --git a/src/aap_eda/settings/default.py b/src/aap_eda/settings/default.py index 4a60c9596..ae08f2288 100644 --- a/src/aap_eda/settings/default.py +++ b/src/aap_eda/settings/default.py @@ -742,13 +742,13 @@ def get_rulebook_process_log_level() -> RulebookProcessLogLevel: "PG_NOTIFY_DSN_SERVER", _DEFAULT_PG_NOTIFY_DSN_SERVER ) -WEBHOOK_BASE_URL = settings.get("WEBHOOK_BASE_URL", None) -if WEBHOOK_BASE_URL: - WEBHOOK_BASE_URL = WEBHOOK_BASE_URL.strip("/") + "/" +EVENT_STREAM_BASE_URL = settings.get("EVENT_STREAM_BASE_URL", None) +if EVENT_STREAM_BASE_URL: + EVENT_STREAM_BASE_URL = EVENT_STREAM_BASE_URL.strip("/") + "/" -WEBHOOK_MTLS_BASE_URL = settings.get("WEBHOOK_MTLS_BASE_URL", None) -if WEBHOOK_MTLS_BASE_URL: - WEBHOOK_MTLS_BASE_URL = WEBHOOK_MTLS_BASE_URL.strip("/") + "/" +EVENT_STREAM_MTLS_BASE_URL = settings.get("EVENT_STREAM_MTLS_BASE_URL", None) +if EVENT_STREAM_MTLS_BASE_URL: + EVENT_STREAM_MTLS_BASE_URL = EVENT_STREAM_MTLS_BASE_URL.strip("/") + "/" MAX_PG_NOTIFY_MESSAGE_SIZE = int( settings.get("MAX_PG_NOTIFY_MESSAGE_SIZE", 6144) diff --git a/tests/integration/api/test_activation_with_webhook.py b/tests/integration/api/test_activation_with_event_stream.py similarity index 86% rename from tests/integration/api/test_activation_with_webhook.py rename to tests/integration/api/test_activation_with_event_stream.py index 2a2c194a1..4db055068 100644 --- a/tests/integration/api/test_activation_with_webhook.py +++ b/tests/integration/api/test_activation_with_event_stream.py @@ -152,7 +152,7 @@ def create_activation_related_data( - webhook_names, with_project=True, rulesets=TEST_RULESETS + event_stream_names, with_project=True, rulesets=TEST_RULESETS ): user = models.User.objects.create_user( username="luke.skywalker", @@ -201,14 +201,14 @@ def create_activation_related_data( else None ) - webhooks = [] - for name in webhook_names: - webhook = models.Webhook.objects.create( + event_streams = [] + for name in event_stream_names: + event_stream = models.EventStream.objects.create( uuid=uuid.uuid4(), name=name, owner=user, ) - webhooks.append(webhook) + event_streams.append(event_stream) return { "user_id": user_id, @@ -217,7 +217,7 @@ def create_activation_related_data( "rulebook_id": rulebook_id, "extra_var": TEST_EXTRA_VAR, "credential_id": credential_id, - "webhooks": webhooks, + "event_streams": event_streams, } @@ -229,14 +229,14 @@ def create_activation(fks: dict): activation_data["user_id"] = fks["user_id"] activation = models.Activation(**activation_data) activation.save() - for webhook in fks["webhooks"]: - activation.webhooks.add(webhook) + for event_stream in fks["event_streams"]: + activation.event_streams.add(event_stream) return activation @pytest.mark.django_db -def test_create_activation_with_webhooks( +def test_create_activation_with_event_stream( admin_client: APIClient, preseed_credential_types ): fks = create_activation_related_data(["demo"]) @@ -244,11 +244,11 @@ def test_create_activation_with_webhooks( test_activation["decision_environment_id"] = fks["decision_environment_id"] test_activation["rulebook_id"] = fks["rulebook_id"] source_mappings = [] - for webhook in fks["webhooks"]: + for event_stream in fks["event_streams"]: source_mappings.append( { - "webhook_name": webhook.name, - "webhook_id": webhook.id, + "event_stream_name": event_stream.name, + "event_stream_id": event_stream.id, "rulebook_hash": get_rulebook_hash(TEST_RULESETS), "source_name": "demo", } @@ -276,18 +276,18 @@ def test_create_activation_with_webhooks( activation.status_message == ACTIVATION_STATUS_MESSAGE_MAP[activation.status] ) - assert data["webhooks"][0]["name"] == "demo" + assert data["event_streams"][0]["name"] == "demo" @pytest.mark.django_db -def test_list_activations_by_webhook( +def test_list_activations_by_event_stream( admin_client: APIClient, default_activation: models.Activation, new_activation: models.Activation, - default_webhook: models.Webhook, + default_event_stream: models.EventStream, ): response = admin_client.get( - f"{api_url_v1}/webhooks/{default_webhook.id}/activations/" + f"{api_url_v1}/event-streams/{default_event_stream.id}/activations/" ) data = response.data["results"] @@ -298,11 +298,11 @@ def test_list_activations_by_webhook( activation_1 = default_activation activation_2 = new_activation - activation_1.webhooks.add(default_webhook) - activation_2.webhooks.add(default_webhook) + activation_1.event_streams.add(default_event_stream) + activation_2.event_streams.add(default_event_stream) response = admin_client.get( - f"{api_url_v1}/webhooks/{default_webhook.id}/activations/" + f"{api_url_v1}/event-streams/{default_event_stream.id}/activations/" ) data = response.data["results"] @@ -368,7 +368,7 @@ def test_create_activation_with_missing_keys_in_mappings( test_activation["rulebook_id"] = fks["rulebook_id"] source_mappings = [ { - "webhook_name": "fake", + "event_stream_name": "fake", "source_name": "demo", } ] @@ -382,13 +382,14 @@ def test_create_activation_with_missing_keys_in_mappings( ) assert response.status_code == status.HTTP_400_BAD_REQUEST assert str(response.data[SOURCE_MAPPING_ERROR_KEY][0]) == ( - "The source mapping {'source_name': 'demo', 'webhook_name': 'fake'} " - "is missing the required keys: ['webhook_id', 'rulebook_hash']" + "The source mapping {'event_stream_name': 'fake', " + "'source_name': 'demo'} is missing the required keys: " + "['event_stream_id', 'rulebook_hash']" ) @pytest.mark.django_db -def test_create_activation_with_bad_webhook( +def test_create_activation_with_bad_event_stream( admin_client: APIClient, preseed_credential_types ): fks = create_activation_related_data(["demo"]) @@ -397,8 +398,8 @@ def test_create_activation_with_bad_webhook( test_activation["rulebook_id"] = fks["rulebook_id"] source_mappings = [ { - "webhook_name": "fake", - "webhook_id": 1492, + "event_stream_name": "fake", + "event_stream_id": 1492, "rulebook_hash": get_rulebook_hash(TEST_RULESETS), "source_name": "demo", } @@ -419,7 +420,7 @@ def test_create_activation_with_bad_webhook( @pytest.mark.django_db -def test_create_activation_with_bad_webhook_name( +def test_create_activation_with_bad_event_stream_name( admin_client: APIClient, preseed_credential_types ): fks = create_activation_related_data(["demo"]) @@ -428,8 +429,8 @@ def test_create_activation_with_bad_webhook_name( test_activation["rulebook_id"] = fks["rulebook_id"] source_mappings = [ { - "webhook_name": "missing_name", - "webhook_id": fks["webhooks"][0].id, + "event_stream_name": "missing_name", + "event_stream_id": fks["event_streams"][0].id, "rulebook_hash": get_rulebook_hash(TEST_RULESETS), "source_name": "demo", } @@ -445,7 +446,7 @@ def test_create_activation_with_bad_webhook_name( assert response.status_code == status.HTTP_400_BAD_REQUEST assert ( str(response.data[SOURCE_MAPPING_ERROR_KEY][0]) - == "Event stream missing_name did not match with webhook demo" + == "Event stream missing_name did not match with name demo in database" ) @@ -459,8 +460,8 @@ def test_create_activation_with_bad_rulebook_hash( test_activation["rulebook_id"] = fks["rulebook_id"] source_mappings = [ { - "webhook_name": fks["webhooks"][0].name, - "webhook_id": fks["webhooks"][0].id, + "event_stream_name": fks["event_streams"][0].name, + "event_stream_id": fks["event_streams"][0].id, "rulebook_hash": "abdd", "source_name": "demo", } @@ -492,14 +493,14 @@ def test_create_activation_with_duplicate_source_name( test_activation["rulebook_id"] = fks["rulebook_id"] source_mappings = [ { - "webhook_name": fks["webhooks"][0].name, - "webhook_id": fks["webhooks"][0].id, + "event_stream_name": fks["event_streams"][0].name, + "event_stream_id": fks["event_streams"][0].id, "rulebook_hash": "abdd", "source_name": "demo", }, { - "webhook_name": f"{fks['webhooks'][0].name}_1", - "webhook_id": fks["webhooks"][0].id, + "event_stream_name": f"{fks['event_streams'][0].name}_1", + "event_stream_id": fks["event_streams"][0].id, "rulebook_hash": get_rulebook_hash( LEGACY_TEST_RULESETS_MULTIPLE_SOURCES ), @@ -522,7 +523,7 @@ def test_create_activation_with_duplicate_source_name( @pytest.mark.django_db -def test_create_activation_with_duplicate_webhook_name( +def test_create_activation_with_duplicate_event_stream_name( admin_client: APIClient, preseed_credential_types ): fks = create_activation_related_data( @@ -533,14 +534,14 @@ def test_create_activation_with_duplicate_webhook_name( test_activation["rulebook_id"] = fks["rulebook_id"] source_mappings = [ { - "webhook_name": fks["webhooks"][0].name, - "webhook_id": fks["webhooks"][0].id, + "event_stream_name": fks["event_streams"][0].name, + "event_stream_id": fks["event_streams"][0].id, "rulebook_hash": "abdd", "source_name": "demo", }, { - "webhook_name": fks["webhooks"][0].name, - "webhook_id": fks["webhooks"][0].id, + "event_stream_name": fks["event_streams"][0].name, + "event_stream_id": fks["event_streams"][0].id, "rulebook_hash": get_rulebook_hash( LEGACY_TEST_RULESETS_MULTIPLE_SOURCES ), @@ -562,7 +563,7 @@ def test_create_activation_with_duplicate_webhook_name( ) -webhook_src_test_data = [ +event_stream_src_test_data = [ ( [("missing_source", "demo")], LEGACY_TEST_RULESETS, @@ -613,10 +614,10 @@ def test_create_activation_with_duplicate_webhook_name( @pytest.mark.parametrize( "source_tuples, rulesets, status_code, message, error_key", - webhook_src_test_data, + event_stream_src_test_data, ) @pytest.mark.django_db -def test_bad_src_activation_with_webhooks( +def test_bad_src_activation_with_event_stream( admin_client: APIClient, preseed_credential_types, source_tuples, @@ -625,7 +626,7 @@ def test_bad_src_activation_with_webhooks( message, error_key, ): - names = [webhook_name for _, webhook_name in source_tuples] + names = [event_stream_name for _, event_stream_name in source_tuples] fks = create_activation_related_data(names, True, rulesets) test_activation = TEST_ACTIVATION.copy() test_activation["decision_environment_id"] = fks["decision_environment_id"] @@ -633,14 +634,14 @@ def test_bad_src_activation_with_webhooks( source_mappings = [] - for src, webhook_name in source_tuples: - webhook = models.Webhook.objects.get(name=webhook_name) + for src, event_source_name in source_tuples: + event_stream = models.EventStream.objects.get(name=event_source_name) source_mappings.append( { "source_name": src, "rulebook_hash": get_rulebook_hash(rulesets), - "webhook_name": webhook_name, - "webhook_id": webhook.id, + "event_stream_name": event_stream.name, + "event_stream_id": event_stream.id, } ) test_activation["source_mappings"] = yaml.dump(source_mappings) diff --git a/tests/integration/api/test_credential_type.py b/tests/integration/api/test_credential_type.py index 710e3851d..b2f4d9028 100644 --- a/tests/integration/api/test_credential_type.py +++ b/tests/integration/api/test_credential_type.py @@ -523,9 +523,9 @@ def test_credential_types_based_on_namespace( preseed_credential_types, ): response = admin_client.get( - f"{api_url_v1}/credential-types/?namespace=webhook" + f"{api_url_v1}/credential-types/?namespace=event_stream" ) assert response.status_code == status.HTTP_200_OK data = response.json() for credential_type in data["results"]: - assert credential_type["namespace"] == "webhook" + assert credential_type["namespace"] == "event_stream" diff --git a/tests/integration/api/test_eda_credential.py b/tests/integration/api/test_eda_credential.py index f14de0f52..4a5afc58f 100644 --- a/tests/integration/api/test_eda_credential.py +++ b/tests/integration/api/test_eda_credential.py @@ -523,12 +523,12 @@ def test_delete_credential_with_project_reference( @pytest.mark.django_db -def test_delete_credential_with_webhook_reference( - default_webhook: models.Webhook, +def test_delete_credential_with_event_stream_reference( + default_event_stream: models.EventStream, admin_client: APIClient, preseed_credential_types, ): - eda_credential = default_webhook.eda_credential + eda_credential = default_event_stream.eda_credential response = admin_client.delete( f"{api_url_v1}/eda-credentials/{eda_credential.id}/" ) diff --git a/tests/integration/api/test_event_stream.py b/tests/integration/api/test_event_stream.py new file mode 100644 index 000000000..f2903e3d9 --- /dev/null +++ b/tests/integration/api/test_event_stream.py @@ -0,0 +1,259 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import hmac +import secrets +from typing import List + +import pytest +from django.test import override_settings +from rest_framework import status +from rest_framework.renderers import JSONRenderer +from rest_framework.test import APIClient + +from aap_eda.core import enums, models +from tests.integration.constants import api_url_v1 + + +@pytest.mark.django_db +def test_list_event_streams( + admin_client: APIClient, + default_event_streams: List[models.EventStream], + default_vault_credential, +): + response = admin_client.get(f"{api_url_v1}/event-streams/") + assert response.status_code == status.HTTP_200_OK + assert len(response.data["results"]) == 2 + assert response.data["results"][0]["name"] == default_event_streams[0].name + assert response.data["results"][0]["owner"] == "luke.skywalker" + + +@pytest.mark.django_db +def test_retrieve_event_stream( + admin_client: APIClient, + default_event_streams: List[models.EventStream], + default_vault_credential, +): + event_stream = default_event_streams[0] + response = admin_client.get( + f"{api_url_v1}/event-streams/{event_stream.id}/" + ) + assert response.status_code == status.HTTP_200_OK + assert response.data["name"] == default_event_streams[0].name + assert response.data["url"] == default_event_streams[0].url + assert response.data["owner"] == "luke.skywalker" + + +@pytest.mark.django_db +def test_create_event_stream( + admin_client: APIClient, default_hmac_credential: models.EdaCredential +): + data_in = { + "name": "test_event_stream", + "eda_credential_id": default_hmac_credential.id, + } + event_stream = create_event_stream(admin_client, data_in) + assert event_stream.name == "test_event_stream" + assert event_stream.owner.username == "test.admin" + + +@pytest.mark.django_db +def test_create_event_stream_without_credentials(admin_client: APIClient): + data_in = { + "name": "test_es", + } + response = admin_client.post(f"{api_url_v1}/event-streams/", data=data_in) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == { + "eda_credential_id": ["This field is required."], + } + + +@pytest.mark.django_db +def test_delete_event_stream( + admin_client: APIClient, + default_event_stream: models.EventStream, +): + response = admin_client.delete( + f"{api_url_v1}/event-streams/{default_event_stream.id}/" + ) + assert response.status_code == status.HTTP_204_NO_CONTENT + + +@pytest.mark.django_db +def test_delete_event_stream_with_exception( + admin_client: APIClient, + default_activation: models.Activation, + default_event_stream: models.EventStream, +): + activation = default_activation + activation.event_streams.add(default_event_stream) + + response = admin_client.delete( + f"{api_url_v1}/event-streams/{default_event_stream.id}/" + ) + assert response.status_code == status.HTTP_409_CONFLICT + assert ( + f"Event stream '{default_event_stream.name}' is being referenced by " + "1 activation(s) and cannot be deleted" + ) in response.data["detail"] + + +@pytest.mark.django_db +def test_post_event_stream_with_bad_uuid( + admin_client: APIClient, + default_event_stream: models.EventStream, +): + data = {"a": 1, "b": 2} + response = admin_client.post( + event_stream_post_url("gobble-de-gook"), + data=data, + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + +@pytest.mark.django_db +def test_fetching_event_stream_credential( + admin_client: APIClient, + preseed_credential_types, +): + secret = secrets.token_hex(32) + inputs = { + "auth_type": "token", + "token": secret, + "http_header_key": "Authorization", + } + create_event_stream_credential( + admin_client, + enums.EventStreamCredentialType.TOKEN.value, + inputs, + "demo1", + ) + + inputs = { + "auth_type": "basic", + "password": secret, + "username": "fred", + "http_header_key": "Authorization", + } + create_event_stream_credential( + admin_client, + enums.EventStreamCredentialType.BASIC.value, + inputs, + "demo2", + ) + response = admin_client.get( + f"{api_url_v1}/eda-credentials/?credential_type__kind=token" + ) + assert response.status_code == status.HTTP_200_OK + assert response.json()["results"][0]["name"] == "demo1" + + +@pytest.mark.parametrize( + ("inputs", "cred_type", "settings_key", "error_msg"), + [ + ( + { + "auth_type": "mtls", + "subject": "Subject", + "http_header_key": "Subject", + }, + enums.EventStreamCredentialType.MTLS.value, + "EVENT_STREAM_MTLS_BASE_URL", + ( + "EventStream of type mTLS Event Stream cannot be " + "used because EVENT_STREAM_MTLS_BASE_URL is " + "missing in settings." + ), + ), + ( + { + "auth_type": "basic", + "username": "fred", + "password": secrets.token_hex(32), + "http_header_key": "Authorization", + }, + enums.EventStreamCredentialType.BASIC.value, + "EVENT_STREAM_BASE_URL", + ( + "EventStream of type Basic Event Stream cannot be used " + "because EVENT_STREAM_BASE_URL is missing in settings." + ), + ), + ], +) +@pytest.mark.django_db +def test_post_event_stream_with_missing_settings( + admin_client: APIClient, + preseed_credential_types, + inputs, + cred_type, + settings_key, + error_msg, +): + obj = create_event_stream_credential(admin_client, cred_type, inputs) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + with override_settings(settings_key=None): + response = admin_client.post( + f"{api_url_v1}/event-streams/", data=data_in + ) + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.json() == {"eda_credential_id": [error_msg]} + + +def create_event_stream_credential( + client: APIClient, + credential_type_name: str, + inputs: dict, + name: str = "eda-credential", +) -> dict: + credential_type = models.CredentialType.objects.get( + name=credential_type_name + ) + data_in = { + "name": name, + "inputs": inputs, + "credential_type_id": credential_type.id, + } + response = client.post(f"{api_url_v1}/eda-credentials/", data=data_in) + assert response.status_code == status.HTTP_201_CREATED + return response.json() + + +def create_event_stream( + client: APIClient, data_in: dict +) -> models.EventStream: + with override_settings( + EVENT_STREAM_BASE_URL="https://www.example.com/", + EVENT_STREAM_MTLS_BASE_URL="https://www.example.com/", + ): + response = client.post(f"{api_url_v1}/event-streams/", data=data_in) + assert response.status_code == status.HTTP_201_CREATED + return models.EventStream.objects.get(id=response.data["id"]) + + +def event_stream_post_url(event_stream_uuid: str) -> str: + return f"{api_url_v1}/external_event_stream/{event_stream_uuid}/post/" + + +def hash_digest(data: dict, secret: str, digestmod) -> str: + data_bytes = JSONRenderer().render(data) + hash_object = hmac.new( + secret.encode("utf-8"), msg=data_bytes, digestmod=digestmod + ) + return hash_object.hexdigest() diff --git a/tests/integration/api/test_event_stream_basic.py b/tests/integration/api/test_event_stream_basic.py new file mode 100644 index 000000000..9484645a7 --- /dev/null +++ b/tests/integration/api/test_event_stream_basic.py @@ -0,0 +1,82 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import secrets +from urllib.parse import urlencode + +import pytest +from rest_framework import status +from rest_framework.test import APIClient + +from aap_eda.core import enums +from tests.integration.api.test_event_stream import ( + create_event_stream, + create_event_stream_credential, + event_stream_post_url, +) + + +@pytest.mark.parametrize( + ("auth_status", "bogus_password"), + [ + (status.HTTP_200_OK, None), + (status.HTTP_403_FORBIDDEN, "bogus"), + ], +) +@pytest.mark.django_db +def test_post_event_stream_with_basic_auth( + admin_client: APIClient, + preseed_credential_types, + auth_status, + bogus_password, +): + secret = secrets.token_hex(32) + username = "fred" + inputs = { + "auth_type": "basic", + "username": username, + "password": secret, + "http_header_key": "Authorization", + } + + obj = create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.BASIC.value, inputs + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + if bogus_password: + user_pass = f"{username}:{bogus_password}" + else: + user_pass = f"{username}:{secret}" + + auth_value = f"Basic {base64.b64encode(user_pass.encode()).decode()}" + data = {"a": 1, "b": 2} + content_type = "application/x-www-form-urlencoded" + data_bytes = urlencode(data).encode() + headers = { + "Authorization": auth_value, + "Content-Type": content_type, + } + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == auth_status diff --git a/tests/integration/api/test_event_stream_ecdsa.py b/tests/integration/api/test_event_stream_ecdsa.py new file mode 100644 index 000000000..e08a3748f --- /dev/null +++ b/tests/integration/api/test_event_stream_ecdsa.py @@ -0,0 +1,128 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import hashlib +from datetime import datetime + +import pytest +from ecdsa import SigningKey +from ecdsa.util import sigencode_der +from rest_framework import status +from rest_framework.renderers import JSONRenderer +from rest_framework.test import APIClient + +from aap_eda.core import enums +from tests.integration.api.test_event_stream import ( + create_event_stream, + create_event_stream_credential, + event_stream_post_url, +) + +ECDSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfBis6O6gsIb2Xk6Q82CcEJcwOw+j +hkmGJmzauR5LXaRmek9rwJpO0FhJ01rirMMyVazm0o3S91VS6WEps66UVg== +-----END PUBLIC KEY-----""" # notsecret + +ECDSA_PRIVATE_KEY = """-----BEGIN EC PRIVATE KEY----- +MHcCAQEEID7fDPE0/HavFrQx2F7/+hBTT8mCqI9dn+rSRnce6SwfoAoGCCqGSM49 +AwEHoUQDQgAEfBis6O6gsIb2Xk6Q82CcEJcwOw+jhkmGJmzauR5LXaRmek9rwJpO +0FhJ01rirMMyVazm0o3S91VS6WEps66UVg== +-----END EC PRIVATE KEY-----""" # notsecret + + +@pytest.mark.parametrize( + ("auth_status", "data", "bogus_data", "signature_encoding", "use_prefix"), + [ + (status.HTTP_200_OK, {"a": 1, "b": 2}, None, "hex", False), + (status.HTTP_200_OK, {"a": 1, "b": 2}, None, "base64", False), + (status.HTTP_200_OK, {"a": 1, "b": 2}, None, "hex", True), + (status.HTTP_200_OK, {"a": 1, "b": 2}, None, "base64", True), + (status.HTTP_403_FORBIDDEN, {"a": 1, "b": 2}, {"x": 1}, "hex", False), + ( + status.HTTP_403_FORBIDDEN, + {"a": 1, "b": 2}, + {"x": 1}, + "base64", + False, + ), + ], +) +@pytest.mark.django_db +def test_post_event_stream_with_ecdsa( + admin_client: APIClient, + preseed_credential_types, + auth_status, + data, + bogus_data, + signature_encoding, + use_prefix, +): + signature_header_name = "My-Ecdsa-Sig" + prefix_header_name = "My-Ecdsa-Prefix" + content_prefix = datetime.now().isoformat() + inputs = { + "auth_type": "ecdsa", + "public_key": ECDSA_PUBLIC_KEY, + "http_header_key": signature_header_name, + "signature_encoding": signature_encoding, + "hash_algorithm": "sha256", + } + + if use_prefix: + inputs["prefix_http_header_key"] = prefix_header_name + + obj = create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.ECDSA.value, inputs + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + content_type = "application/json" + + message_bytes = bytearray() + if use_prefix: + message_bytes.extend(content_prefix.encode()) + data_bytes = JSONRenderer().render(data) + message_bytes.extend(data_bytes) + else: + data_bytes = JSONRenderer().render(data) + message_bytes.extend(data_bytes) + + sk = SigningKey.from_pem(ECDSA_PRIVATE_KEY, hashlib.sha256) + signature = sk.sign_deterministic(message_bytes, sigencode=sigencode_der) + if signature_encoding == "base64": + signature_str = base64.b64encode(signature).decode() + else: + signature_str = signature.hex() + + headers = { + signature_header_name: signature_str, + "Content-Type": content_type, + } + if use_prefix: + headers[prefix_header_name] = content_prefix + + if bogus_data: + data_bytes = JSONRenderer().render(bogus_data) + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == auth_status diff --git a/tests/integration/api/test_event_stream_hmac.py b/tests/integration/api/test_event_stream_hmac.py new file mode 100644 index 000000000..f0ccdc92c --- /dev/null +++ b/tests/integration/api/test_event_stream_hmac.py @@ -0,0 +1,283 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import base64 +import hashlib +import hmac +import secrets +from urllib.parse import urlencode + +import pytest +import yaml +from rest_framework import status +from rest_framework.test import APIClient + +from aap_eda.core import enums +from tests.integration.api.test_event_stream import ( + create_event_stream, + create_event_stream_credential, + event_stream_post_url, + hash_digest, +) + +DEFAULT_TEST_HMAC_HEADER = "My-Secret-Header" +DEFAULT_TEST_HMAC_ENCODING = "hex" +DEFAULT_TEST_HMAC_ALGORITHM = "sha256" + + +@pytest.mark.parametrize( + ("hash_algorithm", "digestmod"), + [ + ("sha256", hashlib.sha256), + ("sha512", hashlib.sha512), + ("sha3_224", hashlib.sha3_224), + ("sha3_256", hashlib.sha3_256), + ("sha3_384", hashlib.sha3_384), + ("sha3_512", hashlib.sha3_512), + ("blake2b", hashlib.blake2b), + ("blake2s", hashlib.blake2s), + ], +) +@pytest.mark.django_db +def test_post_event_stream( + admin_client: APIClient, + preseed_credential_types, + hash_algorithm, + digestmod, +): + secret = secrets.token_hex(32) + obj = _create_hmac_credential(admin_client, secret, hash_algorithm) + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + } + + event_stream = create_event_stream(admin_client, data_in) + + data = {"a": 1, "b": 2} + headers = {DEFAULT_TEST_HMAC_HEADER: hash_digest(data, secret, digestmod)} + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data, + ) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_post_event_stream_bad_secret( + admin_client: APIClient, + preseed_credential_types, +): + secret = secrets.token_hex(32) + obj = _create_hmac_credential(admin_client, secret) + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + bad_secret = secrets.token_hex(32) + data = {"a": 1, "b": 2} + headers = { + DEFAULT_TEST_HMAC_HEADER: hash_digest(data, bad_secret, hashlib.sha256) + } + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + event_stream.refresh_from_db() + assert ( + event_stream.test_error_message + == "Signature mismatch, check your payload and secret" + ) + + +@pytest.mark.django_db +def test_post_event_stream_with_prefix( + admin_client: APIClient, + preseed_credential_types, +): + secret = secrets.token_hex(32) + signature_prefix = "sha256=" + obj = _create_hmac_credential( + admin_client, + secret, + DEFAULT_TEST_HMAC_ALGORITHM, + DEFAULT_TEST_HMAC_HEADER, + DEFAULT_TEST_HMAC_ENCODING, + signature_prefix, + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + data = {"a": 1, "b": 2} + digest = hash_digest(data, secret, hashlib.sha256) + headers = {DEFAULT_TEST_HMAC_HEADER: f"{signature_prefix}{digest}"} + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data, + ) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_post_event_stream_with_test_mode( + admin_client: APIClient, + preseed_credential_types, +): + secret = secrets.token_hex(32) + signature_prefix = "sha256=" + obj = _create_hmac_credential( + admin_client, + secret, + DEFAULT_TEST_HMAC_ALGORITHM, + DEFAULT_TEST_HMAC_HEADER, + DEFAULT_TEST_HMAC_ENCODING, + signature_prefix, + ) + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + data = {"a": 1, "b": 2} + digest = hash_digest(data, secret, hashlib.sha256) + headers = {DEFAULT_TEST_HMAC_HEADER: (f"{signature_prefix}{digest}")} + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data, + ) + assert response.status_code == status.HTTP_200_OK + + event_stream.refresh_from_db() + test_data = yaml.safe_load(event_stream.test_content) + assert test_data["a"] == 1 + assert test_data["b"] == 2 + assert event_stream.test_content_type == "application/json" + + +@pytest.mark.django_db +def test_post_event_stream_with_form_urlencoded( + admin_client: APIClient, + preseed_credential_types, +): + secret = secrets.token_hex(32) + signature_prefix = "sha256=" + obj = _create_hmac_credential( + admin_client, + secret, + DEFAULT_TEST_HMAC_ALGORITHM, + DEFAULT_TEST_HMAC_HEADER, + DEFAULT_TEST_HMAC_ENCODING, + signature_prefix, + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + data = {"a": 1, "b": 2} + content_type = "application/x-www-form-urlencoded" + data_bytes = urlencode(data).encode() + hash_object = hmac.new( + secret.encode("utf-8"), msg=data_bytes, digestmod=hashlib.sha256 + ) + headers = { + DEFAULT_TEST_HMAC_HEADER: ( + f"{signature_prefix}{hash_object.hexdigest()}" + ), + "Content-Type": content_type, + } + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_post_event_stream_with_base64_format( + admin_client: APIClient, + preseed_credential_types, +): + secret = secrets.token_hex(32) + signature_prefix = "sha256=" + obj = _create_hmac_credential( + admin_client, + secret, + DEFAULT_TEST_HMAC_ALGORITHM, + DEFAULT_TEST_HMAC_HEADER, + "base64", + signature_prefix, + ) + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + data = {"a": 1, "b": 2} + content_type = "application/x-www-form-urlencoded" + data_bytes = urlencode(data).encode() + hash_object = hmac.new( + secret.encode("utf-8"), msg=data_bytes, digestmod=hashlib.sha256 + ) + b64_signature = base64.b64encode(hash_object.digest()).decode() + headers = { + DEFAULT_TEST_HMAC_HEADER: f"{signature_prefix}{b64_signature}", + "Content-Type": content_type, + } + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == status.HTTP_200_OK + + +def _create_hmac_credential( + admin_client, + secret: str, + hash_algorithm: str = DEFAULT_TEST_HMAC_ALGORITHM, + signature_header_name: str = DEFAULT_TEST_HMAC_HEADER, + signature_encoding: str = DEFAULT_TEST_HMAC_ENCODING, + signature_prefix: str = "", +) -> dict: + inputs = { + "secret": secret, + "http_header_key": signature_header_name, + "auth_type": "hmac", + "signature_encoding": signature_encoding, + "hash_algorithm": hash_algorithm, + } + if signature_prefix: + inputs["signature_prefix"] = signature_prefix + + return create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.HMAC.value, inputs + ) diff --git a/tests/integration/api/test_event_stream_mtls.py b/tests/integration/api/test_event_stream_mtls.py new file mode 100644 index 000000000..fc5b5db17 --- /dev/null +++ b/tests/integration/api/test_event_stream_mtls.py @@ -0,0 +1,76 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest +from rest_framework import status +from rest_framework.renderers import JSONRenderer +from rest_framework.test import APIClient + +from aap_eda.core import enums +from tests.integration.api.test_event_stream import ( + create_event_stream, + create_event_stream_credential, + event_stream_post_url, +) + + +@pytest.mark.parametrize( + ("auth_status", "subject", "bogus_subject"), + [ + (status.HTTP_200_OK, "subject", None), + (status.HTTP_403_FORBIDDEN, "subject", "bogus"), + ], +) +@pytest.mark.django_db +def test_post_event_stream_with_mtls( + admin_client: APIClient, + preseed_credential_types, + subject, + auth_status, + bogus_subject, +): + header_key = "Subject" + inputs = { + "auth_type": "mtls", + "subject": subject, + "http_header_key": header_key, + } + + obj = create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.MTLS.value, inputs + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + data = {"a": 1, "b": 2} + content_type = "application/json" + data_bytes = JSONRenderer().render(data) + if bogus_subject: + subject = bogus_subject + + headers = { + header_key: subject, + "Content-Type": content_type, + } + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == auth_status diff --git a/tests/integration/api/test_event_stream_oauth2.py b/tests/integration/api/test_event_stream_oauth2.py new file mode 100644 index 000000000..a8618bc22 --- /dev/null +++ b/tests/integration/api/test_event_stream_oauth2.py @@ -0,0 +1,89 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import secrets + +import pytest +import requests_mock +from rest_framework import status +from rest_framework.renderers import JSONRenderer +from rest_framework.test import APIClient + +from aap_eda.core import enums +from tests.integration.api.test_event_stream import ( + create_event_stream, + create_event_stream_credential, + event_stream_post_url, +) + + +@pytest.mark.parametrize( + ("auth_status", "payload", "post_status"), + [ + (status.HTTP_200_OK, {"active": True}, status.HTTP_200_OK), + (status.HTTP_403_FORBIDDEN, {"active": False}, status.HTTP_200_OK), + (status.HTTP_403_FORBIDDEN, {"nada": False}, status.HTTP_200_OK), + (status.HTTP_403_FORBIDDEN, "Kaboom", status.HTTP_403_FORBIDDEN), + ], +) +@pytest.mark.django_db +def test_post_event_stream_with_oauth2( + admin_client: APIClient, + preseed_credential_types, + auth_status, + payload, + post_status, +): + header_key = "Authorization" + access_token = "dummy" + introspection_url = ( + "https://fake.com/auth/realms/eda-demo/" + "protocol/openid-connect/token/introspect" + ) + secret = secrets.token_hex(32) + inputs = { + "auth_type": "oauth2", + "client_id": "test", + "client_secret": secret, + "introspection_url": introspection_url, + "http_header_key": header_key, + } + + obj = create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.OAUTH2.value, inputs + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + + data = {"a": 1, "b": 2} + content_type = "application/json" + data_bytes = JSONRenderer().render(data) + headers = { + header_key: access_token, + "Content-Type": content_type, + } + + with requests_mock.Mocker() as m: + m.post(introspection_url, json=payload, status_code=post_status) + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == auth_status diff --git a/tests/integration/api/test_event_stream_oauth2_jwt.py b/tests/integration/api/test_event_stream_oauth2_jwt.py new file mode 100644 index 000000000..8f946a3c3 --- /dev/null +++ b/tests/integration/api/test_event_stream_oauth2_jwt.py @@ -0,0 +1,84 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from unittest.mock import patch + +import jwt +import pytest +from rest_framework import status +from rest_framework.renderers import JSONRenderer +from rest_framework.test import APIClient + +from aap_eda.core import enums +from tests.integration.api.test_event_stream import ( + create_event_stream, + create_event_stream_credential, + event_stream_post_url, +) + + +@pytest.mark.parametrize( + ("auth_status", "side_effect"), + [ + (status.HTTP_200_OK, None), + (status.HTTP_403_FORBIDDEN, jwt.exceptions.PyJWTError("Kaboom")), + ], +) +@pytest.mark.django_db +def test_post_event_stream_with_oauth2_jwt( + admin_client: APIClient, + preseed_credential_types, + auth_status, + side_effect, +): + header_key = "Authorization" + access_token = "dummy" + jwks_url = "https://my_as_server/.well-known/jwks.json" + audience = "dummy" + inputs = { + "auth_type": "oauth2-jwt", + "jwks_url": jwks_url, + "audience": audience, + "http_header_key": header_key, + } + + obj = create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.OAUTH2_JWT.value, inputs + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + + data = {"a": 1, "b": 2} + content_type = "application/json" + data_bytes = JSONRenderer().render(data) + headers = { + header_key: f"Bearer {access_token}", + "Content-Type": content_type, + } + with patch("aap_eda.api.event_stream_authentication.PyJWKClient"): + with patch( + "aap_eda.api.event_stream_authentication.jwt_decode", + side_effect=side_effect, + ): + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == auth_status diff --git a/tests/integration/api/test_event_stream_token.py b/tests/integration/api/test_event_stream_token.py new file mode 100644 index 000000000..91ec883b0 --- /dev/null +++ b/tests/integration/api/test_event_stream_token.py @@ -0,0 +1,138 @@ +# Copyright 2024 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import secrets +from urllib.parse import urlencode + +import pytest +import yaml +from rest_framework import status +from rest_framework.test import APIClient + +from aap_eda.core import enums +from tests.integration.api.test_event_stream import ( + create_event_stream, + create_event_stream_credential, + event_stream_post_url, +) + + +@pytest.mark.parametrize( + ("auth_status", "bogus_token"), + [ + (status.HTTP_200_OK, None), + (status.HTTP_403_FORBIDDEN, "bogus"), + ], +) +@pytest.mark.django_db +def test_post_event_stream_with_token( + admin_client: APIClient, + preseed_credential_types, + auth_status, + bogus_token, +): + secret = secrets.token_hex(32) + signature_header_name = "My-Secret-Header" + inputs = { + "auth_type": "token", + "token": secret, + "http_header_key": signature_header_name, + } + + obj = create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.TOKEN.value, inputs + ) + + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + } + event_stream = create_event_stream(admin_client, data_in) + data = {"a": 1, "b": 2} + content_type = "application/x-www-form-urlencoded" + data_bytes = urlencode(data).encode() + if bogus_token: + secret = bogus_token + headers = { + signature_header_name: secret, + "Content-Type": content_type, + } + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data_bytes, + content_type=content_type, + ) + assert response.status_code == auth_status + + +@pytest.mark.django_db +def test_post_event_stream_with_test_mode_extra_headers( + admin_client: APIClient, + preseed_credential_types, +): + secret = secrets.token_hex(32) + signature_header_name = "X-Gitlab-Token" + inputs = { + "auth_type": "token", + "token": secret, + "http_header_key": signature_header_name, + } + + obj = create_event_stream_credential( + admin_client, enums.EventStreamCredentialType.TOKEN.value, inputs + ) + + additional_data_headers = ( + "X-Gitlab-Event,X-Gitlab-Event-Uuid,X-Gitlab-Uuid" + ) + data_in = { + "name": "test-es-1", + "eda_credential_id": obj["id"], + "test_mode": True, + "additional_data_headers": additional_data_headers, + } + event_stream = create_event_stream(admin_client, data_in) + data = {"a": 1, "b": 2} + headers = { + "X-Gitlab-Event-Uuid": "c2675c66-7e6e-4fe2-9ac3-288534ef34b9", + "X-Gitlab-Instance": "https://gitlab.com", + signature_header_name: secret, + "X-Gitlab-Uuid": "b697868f-3b59-4a1f-985d-47f79e2b05ff", + "X-Gitlab-Event": "Push Hook", + } + + response = admin_client.post( + event_stream_post_url(event_stream.uuid), + headers=headers, + data=data, + ) + assert response.status_code == status.HTTP_200_OK + + event_stream.refresh_from_db() + test_data = yaml.safe_load(event_stream.test_content) + assert test_data["a"] == 1 + assert test_data["b"] == 2 + test_headers = yaml.safe_load(event_stream.test_headers) + assert ( + test_headers["X-Gitlab-Event-Uuid"] + == "c2675c66-7e6e-4fe2-9ac3-288534ef34b9" + ) + assert ( + test_headers["X-Gitlab-Uuid"] == "b697868f-3b59-4a1f-985d-47f79e2b05ff" + ) + assert test_headers["X-Gitlab-Event"] == "Push Hook" + assert event_stream.test_content_type == "application/json" + assert event_stream.events_received == 1 + assert event_stream.last_event_received_at is not None diff --git a/tests/integration/api/test_root.py b/tests/integration/api/test_root.py index 098466335..2f24753c6 100644 --- a/tests/integration/api/test_root.py +++ b/tests/integration/api/test_root.py @@ -44,7 +44,7 @@ "/decision-environments/", "/organizations/", "/teams/", - "/webhooks/", + "/event-streams/", ], True, id="with_shared_resource", @@ -82,7 +82,7 @@ "/decision-environments/", "/organizations/", "/teams/", - "/webhooks/", + "/event-streams/", ], False, id="no_shared_resource", diff --git a/tests/integration/api/test_webhook.py b/tests/integration/api/test_webhook.py deleted file mode 100644 index 82ecca7b2..000000000 --- a/tests/integration/api/test_webhook.py +++ /dev/null @@ -1,1009 +0,0 @@ -# Copyright 2024 Red Hat, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -import base64 -import hashlib -import hmac -import secrets -import uuid -from datetime import datetime -from typing import List -from unittest.mock import patch -from urllib.parse import urlencode - -import jwt -import pytest -import requests_mock -import yaml -from django.test import override_settings -from ecdsa import SigningKey -from ecdsa.util import sigencode_der -from rest_framework import status -from rest_framework.renderers import JSONRenderer -from rest_framework.test import APIClient - -from aap_eda.core import enums, models -from tests.integration.constants import api_url_v1 - -ECDSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- -MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfBis6O6gsIb2Xk6Q82CcEJcwOw+j -hkmGJmzauR5LXaRmek9rwJpO0FhJ01rirMMyVazm0o3S91VS6WEps66UVg== ------END PUBLIC KEY-----""" # notsecret - -ECDSA_PRIVATE_KEY = """-----BEGIN EC PRIVATE KEY----- -MHcCAQEEID7fDPE0/HavFrQx2F7/+hBTT8mCqI9dn+rSRnce6SwfoAoGCCqGSM49 -AwEHoUQDQgAEfBis6O6gsIb2Xk6Q82CcEJcwOw+jhkmGJmzauR5LXaRmek9rwJpO -0FhJ01rirMMyVazm0o3S91VS6WEps66UVg== ------END EC PRIVATE KEY-----""" # notsecret - - -@pytest.mark.django_db -def test_list_webhooks( - admin_client: APIClient, - default_webhooks: List[models.Webhook], - default_vault_credential, -): - response = admin_client.get(f"{api_url_v1}/webhooks/") - assert response.status_code == status.HTTP_200_OK - assert len(response.data["results"]) == 2 - assert response.data["results"][0]["name"] == default_webhooks[0].name - assert response.data["results"][0]["owner"] == "luke.skywalker" - - -@pytest.mark.django_db -def test_retrieve_webhook( - admin_client: APIClient, - default_webhooks: List[models.Webhook], - default_vault_credential, -): - webhook = default_webhooks[0] - response = admin_client.get(f"{api_url_v1}/webhooks/{webhook.id}/") - assert response.status_code == status.HTTP_200_OK - assert response.data["name"] == default_webhooks[0].name - assert response.data["url"] == default_webhooks[0].url - assert response.data["owner"] == "luke.skywalker" - - -@pytest.mark.django_db -def test_create_webhook( - admin_client: APIClient, default_hmac_credential: models.EdaCredential -): - data_in = { - "name": "test_webhook", - "eda_credential_id": default_hmac_credential.id, - } - webhook = _create_webhook(admin_client, data_in) - assert webhook.name == "test_webhook" - assert webhook.owner.username == "test.admin" - - -@pytest.mark.django_db -def test_create_webhook_without_credentials(admin_client: APIClient): - data_in = { - "name": "test_webhook", - } - response = admin_client.post(f"{api_url_v1}/webhooks/", data=data_in) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == { - "eda_credential_id": ["This field is required."], - } - - -@pytest.mark.parametrize( - ("hash_algorithm", "digestmod"), - [ - ("sha256", hashlib.sha256), - ("sha512", hashlib.sha512), - ("sha3_224", hashlib.sha3_224), - ("sha3_256", hashlib.sha3_256), - ("sha3_384", hashlib.sha3_384), - ("sha3_512", hashlib.sha3_512), - ("blake2b", hashlib.blake2b), - ("blake2s", hashlib.blake2s), - ], -) -@pytest.mark.django_db -def test_post_webhook( - admin_client: APIClient, - preseed_credential_types, - hash_algorithm, - digestmod, -): - secret = secrets.token_hex(32) - signature_header_name = "My-Secret-Header" - inputs = { - "secret": secret, - "http_header_key": signature_header_name, - "auth_type": "hmac", - "signature_encoding": "hex", - "hash_algorithm": hash_algorithm, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.HMAC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - } - - webhook = _create_webhook(admin_client, data_in) - - data = {"a": 1, "b": 2} - data_bytes = JSONRenderer().render(data) - hash_object = hmac.new( - secret.encode("utf-8"), msg=data_bytes, digestmod=digestmod - ) - headers = {signature_header_name: hash_object.hexdigest()} - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data, - ) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_post_webhook_bad_secret( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - signature_header_name = "My-Secret-Header" - inputs = { - "secret": secret, - "http_header_key": signature_header_name, - "auth_type": "hmac", - "signature_encoding": "hex", - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.HMAC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - bad_secret = secrets.token_hex(32) - data = {"a": 1, "b": 2} - data_bytes = JSONRenderer().render(data) - hash_object = hmac.new( - bad_secret.encode("utf-8"), msg=data_bytes, digestmod=hashlib.sha256 - ) - headers = {signature_header_name: hash_object.hexdigest()} - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data, - ) - assert response.status_code == status.HTTP_403_FORBIDDEN - webhook.refresh_from_db() - assert ( - webhook.test_error_message - == "Signature mismatch, check your payload and secret" - ) - - -@pytest.mark.django_db -def test_post_webhook_with_prefix( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - signature_header_name = "My-Secret-Header" - hmac_signature_prefix = "sha256=" - inputs = { - "secret": secret, - "http_header_key": signature_header_name, - "auth_type": "hmac", - "signature_encoding": "hex", - "signature_prefix": hmac_signature_prefix, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.HMAC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - data = {"a": 1, "b": 2} - data_bytes = JSONRenderer().render(data) - hash_object = hmac.new( - secret.encode("utf-8"), msg=data_bytes, digestmod=hashlib.sha256 - ) - headers = { - signature_header_name: f"{hmac_signature_prefix}" - f"{hash_object.hexdigest()}" - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data, - ) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_post_webhook_with_test_mode( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - signature_header_name = "My-Secret-Header" - hmac_signature_prefix = "sha256=" - inputs = { - "secret": secret, - "http_header_key": signature_header_name, - "auth_type": "hmac", - "signature_encoding": "hex", - "signature_prefix": hmac_signature_prefix, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.HMAC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - data = {"a": 1, "b": 2} - data_bytes = JSONRenderer().render(data) - hash_object = hmac.new( - secret.encode("utf-8"), msg=data_bytes, digestmod=hashlib.sha256 - ) - headers = { - signature_header_name: ( - f"{hmac_signature_prefix}{hash_object.hexdigest()}" - ) - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data, - ) - assert response.status_code == status.HTTP_200_OK - - webhook.refresh_from_db() - test_data = yaml.safe_load(webhook.test_content) - assert test_data["a"] == 1 - assert test_data["b"] == 2 - assert webhook.test_content_type == "application/json" - - -@pytest.mark.django_db -def test_delete_webhook( - admin_client: APIClient, - default_webhook: models.Webhook, -): - response = admin_client.delete( - f"{api_url_v1}/webhooks/{default_webhook.id}/" - ) - assert response.status_code == status.HTTP_204_NO_CONTENT - - -@pytest.mark.django_db -def test_delete_webhook_with_exception( - admin_client: APIClient, - default_activation: models.Activation, - default_webhook: models.Webhook, -): - activation = default_activation - activation.webhooks.add(default_webhook) - - response = admin_client.delete( - f"{api_url_v1}/webhooks/{default_webhook.id}/" - ) - assert response.status_code == status.HTTP_409_CONFLICT - assert ( - f"Event stream '{default_webhook.name}' is being referenced by " - "1 activation(s) and cannot be deleted" - ) in response.data["detail"] - - -@pytest.mark.django_db -def test_post_webhook_with_bad_uuid( - admin_client: APIClient, - default_webhook: models.Webhook, -): - data = {"a": 1, "b": 2} - response = admin_client.post( - f"{api_url_v1}/external_webhook/{str(uuid.uuid4())}/post/", - data=data, - ) - assert response.status_code == status.HTTP_400_BAD_REQUEST - - -@pytest.mark.django_db -def test_post_webhook_with_form_urlencoded( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - signature_header_name = "My-Secret-Header" - hmac_signature_prefix = "sha256=" - inputs = { - "secret": secret, - "http_header_key": signature_header_name, - "auth_type": "hmac", - "signature_encoding": "hex", - "signature_prefix": hmac_signature_prefix, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.HMAC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - data = {"a": 1, "b": 2} - content_type = "application/x-www-form-urlencoded" - data_bytes = urlencode(data).encode() - hash_object = hmac.new( - secret.encode("utf-8"), msg=data_bytes, digestmod=hashlib.sha256 - ) - headers = { - signature_header_name: ( - f"{hmac_signature_prefix}{hash_object.hexdigest()}" - ), - "Content-Type": content_type, - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_post_webhook_with_base64_format( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - signature_header_name = "My-Secret-Header" - hmac_signature_prefix = "sha256=" - inputs = { - "secret": secret, - "http_header_key": signature_header_name, - "auth_type": "hmac", - "signature_encoding": "base64", - "signature_prefix": hmac_signature_prefix, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.HMAC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - data = {"a": 1, "b": 2} - content_type = "application/x-www-form-urlencoded" - data_bytes = urlencode(data).encode() - hash_object = hmac.new( - secret.encode("utf-8"), msg=data_bytes, digestmod=hashlib.sha256 - ) - b64_signature = base64.b64encode(hash_object.digest()).decode() - headers = { - signature_header_name: f"{hmac_signature_prefix}{b64_signature}", - "Content-Type": content_type, - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.parametrize( - ("auth_status", "bogus_password"), - [ - (status.HTTP_200_OK, None), - (status.HTTP_403_FORBIDDEN, "bogus"), - ], -) -@pytest.mark.django_db -def test_post_webhook_with_basic_auth( - admin_client: APIClient, - preseed_credential_types, - auth_status, - bogus_password, -): - secret = secrets.token_hex(32) - username = "fred" - inputs = { - "auth_type": "basic", - "username": username, - "password": secret, - "http_header_key": "Authorization", - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.BASIC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - if bogus_password: - user_pass = f"{username}:{bogus_password}" - else: - user_pass = f"{username}:{secret}" - - auth_value = f"Basic {base64.b64encode(user_pass.encode()).decode()}" - data = {"a": 1, "b": 2} - content_type = "application/x-www-form-urlencoded" - data_bytes = urlencode(data).encode() - headers = { - "Authorization": auth_value, - "Content-Type": content_type, - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == auth_status - - -@pytest.mark.parametrize( - ("auth_status", "bogus_token"), - [ - (status.HTTP_200_OK, None), - (status.HTTP_403_FORBIDDEN, "bogus"), - ], -) -@pytest.mark.django_db -def test_post_webhook_with_token( - admin_client: APIClient, - preseed_credential_types, - auth_status, - bogus_token, -): - secret = secrets.token_hex(32) - signature_header_name = "My-Secret-Header" - inputs = { - "auth_type": "token", - "token": secret, - "http_header_key": signature_header_name, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.TOKEN.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - data = {"a": 1, "b": 2} - content_type = "application/x-www-form-urlencoded" - data_bytes = urlencode(data).encode() - if bogus_token: - secret = bogus_token - headers = { - signature_header_name: secret, - "Content-Type": content_type, - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == auth_status - - -@pytest.mark.django_db -def test_post_webhook_with_test_mode_extra_headers( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - signature_header_name = "X-Gitlab-Token" - inputs = { - "auth_type": "token", - "token": secret, - "http_header_key": signature_header_name, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.TOKEN.value, inputs - ) - - additional_data_headers = ( - "X-Gitlab-Event,X-Gitlab-Event-Uuid,X-Gitlab-Webhook-Uuid" - ) - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - "additional_data_headers": additional_data_headers, - } - webhook = _create_webhook(admin_client, data_in) - data = {"a": 1, "b": 2} - headers = { - "X-Gitlab-Event-Uuid": "c2675c66-7e6e-4fe2-9ac3-288534ef34b9", - "X-Gitlab-Instance": "https://gitlab.com", - signature_header_name: secret, - "X-Gitlab-Webhook-Uuid": "b697868f-3b59-4a1f-985d-47f79e2b05ff", - "X-Gitlab-Event": "Push Hook", - } - - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data, - ) - assert response.status_code == status.HTTP_200_OK - - webhook.refresh_from_db() - test_data = yaml.safe_load(webhook.test_content) - assert test_data["a"] == 1 - assert test_data["b"] == 2 - test_headers = yaml.safe_load(webhook.test_headers) - assert ( - test_headers["X-Gitlab-Event-Uuid"] - == "c2675c66-7e6e-4fe2-9ac3-288534ef34b9" - ) - assert ( - test_headers["X-Gitlab-Webhook-Uuid"] - == "b697868f-3b59-4a1f-985d-47f79e2b05ff" - ) - assert test_headers["X-Gitlab-Event"] == "Push Hook" - assert webhook.test_content_type == "application/json" - assert webhook.events_received == 1 - assert webhook.last_event_received_at is not None - - -@pytest.mark.parametrize( - ("auth_status", "data", "bogus_data", "signature_encoding"), - [ - (status.HTTP_200_OK, {"a": 1, "b": 2}, None, "hex"), - (status.HTTP_200_OK, {"a": 1, "b": 2}, None, "base64"), - (status.HTTP_403_FORBIDDEN, {"a": 1, "b": 2}, {"x": 1}, "hex"), - (status.HTTP_403_FORBIDDEN, {"a": 1, "b": 2}, {"x": 1}, "base64"), - ], -) -@pytest.mark.django_db -def test_post_webhook_with_ecdsa( - admin_client: APIClient, - preseed_credential_types, - auth_status, - data, - bogus_data, - signature_encoding, -): - signature_header_name = "My-Ecdsa-Sig" - inputs = { - "auth_type": "ecdsa", - "public_key": ECDSA_PUBLIC_KEY, - "http_header_key": signature_header_name, - "signature_encoding": signature_encoding, - "hash_algorithm": "sha256", - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.ECDSA.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - content_type = "application/json" - data_bytes = JSONRenderer().render(data) - sk = SigningKey.from_pem(ECDSA_PRIVATE_KEY, hashlib.sha256) - signature = sk.sign_deterministic(data_bytes, sigencode=sigencode_der) - if signature_encoding == "base64": - signature_str = base64.b64encode(signature).decode() - else: - signature_str = signature.hex() - - headers = { - signature_header_name: signature_str, - "Content-Type": content_type, - } - if bogus_data: - data_bytes = JSONRenderer().render(bogus_data) - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == auth_status - - -@pytest.mark.django_db -def test_post_webhook_with_ecdsa_with_prefix( - admin_client: APIClient, - preseed_credential_types, -): - signature_header_name = "My-Ecdsa-Sig" - prefix_header_name = "My-Ecdsa-Prefix" - content_prefix = datetime.now().isoformat() - signature_encoding = "base64" - inputs = { - "auth_type": "ecdsa", - "public_key": ECDSA_PUBLIC_KEY, - "http_header_key": signature_header_name, - "signature_encoding": signature_encoding, - "hash_algorithm": "sha256", - "prefix_http_header_key": prefix_header_name, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.ECDSA.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - - content_type = "application/json" - data = {"a": 1, "b": 2} - message_bytes = bytearray() - message_bytes.extend(content_prefix.encode()) - data_bytes = JSONRenderer().render(data) - message_bytes.extend(data_bytes) - - sk = SigningKey.from_pem(ECDSA_PRIVATE_KEY, hashlib.sha256) - signature = sk.sign_deterministic(message_bytes, sigencode=sigencode_der) - signature_str = base64.b64encode(signature).decode() - headers = { - signature_header_name: signature_str, - prefix_header_name: content_prefix, - "Content-Type": content_type, - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_fetching_webhook_credential( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - inputs = { - "auth_type": "token", - "token": secret, - "http_header_key": "Authorization", - } - _create_webhook_credential( - admin_client, enums.WebhookCredentialType.TOKEN.value, inputs, "demo1" - ) - - inputs = { - "auth_type": "basic", - "password": secret, - "username": "fred", - "http_header_key": "Authorization", - } - _create_webhook_credential( - admin_client, enums.WebhookCredentialType.BASIC.value, inputs, "demo2" - ) - response = admin_client.get( - f"{api_url_v1}/eda-credentials/?credential_type__kind=token" - ) - assert response.status_code == status.HTTP_200_OK - assert response.json()["results"][0]["name"] == "demo1" - - -@pytest.mark.parametrize( - ("auth_status", "payload", "post_status"), - [ - (status.HTTP_200_OK, {"active": True}, status.HTTP_200_OK), - (status.HTTP_403_FORBIDDEN, {"active": False}, status.HTTP_200_OK), - (status.HTTP_403_FORBIDDEN, {"nada": False}, status.HTTP_200_OK), - (status.HTTP_403_FORBIDDEN, "Kaboom", status.HTTP_403_FORBIDDEN), - ], -) -@pytest.mark.django_db -def test_post_webhook_with_oauth2( - admin_client: APIClient, - preseed_credential_types, - auth_status, - payload, - post_status, -): - header_key = "Authorization" - access_token = "dummy" - introspection_url = ( - "https://fake.com/auth/realms/eda-demo/" - "protocol/openid-connect/token/introspect" - ) - secret = secrets.token_hex(32) - inputs = { - "auth_type": "oauth2", - "client_id": "test", - "client_secret": secret, - "introspection_url": introspection_url, - "http_header_key": header_key, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.OAUTH2.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - - data = {"a": 1, "b": 2} - content_type = "application/json" - data_bytes = JSONRenderer().render(data) - headers = { - header_key: access_token, - "Content-Type": content_type, - } - - with requests_mock.Mocker() as m: - m.post(introspection_url, json=payload, status_code=post_status) - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == auth_status - - -@pytest.mark.parametrize( - ("auth_status", "side_effect"), - [ - (status.HTTP_200_OK, None), - (status.HTTP_403_FORBIDDEN, jwt.exceptions.PyJWTError("Kaboom")), - ], -) -@pytest.mark.django_db -def test_post_webhook_with_oauth2_jwt( - admin_client: APIClient, - preseed_credential_types, - auth_status, - side_effect, -): - header_key = "Authorization" - access_token = "dummy" - jwks_url = "https://my_as_server/.well-known/jwks.json" - audience = "dummy" - inputs = { - "auth_type": "oauth2-jwt", - "jwks_url": jwks_url, - "audience": audience, - "http_header_key": header_key, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.OAUTH2_JWT.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - - data = {"a": 1, "b": 2} - content_type = "application/json" - data_bytes = JSONRenderer().render(data) - headers = { - header_key: f"Bearer {access_token}", - "Content-Type": content_type, - } - with patch("aap_eda.api.webhook_authentication.PyJWKClient"): - with patch( - "aap_eda.api.webhook_authentication.jwt_decode", - side_effect=side_effect, - ): - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == auth_status - - -@pytest.mark.parametrize( - ("auth_status", "subject", "bogus_subject"), - [ - (status.HTTP_200_OK, "subject", None), - (status.HTTP_403_FORBIDDEN, "subject", "bogus"), - ], -) -@pytest.mark.django_db -def test_post_webhook_with_mtls( - admin_client: APIClient, - preseed_credential_types, - subject, - auth_status, - bogus_subject, -): - header_key = "Subject" - inputs = { - "auth_type": "mtls", - "subject": subject, - "http_header_key": header_key, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.MTLS.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - webhook = _create_webhook(admin_client, data_in) - data = {"a": 1, "b": 2} - content_type = "application/json" - data_bytes = JSONRenderer().render(data) - if bogus_subject: - subject = bogus_subject - - headers = { - header_key: subject, - "Content-Type": content_type, - } - response = admin_client.post( - f"{api_url_v1}/external_webhook/{webhook.uuid}/post/", - headers=headers, - data=data_bytes, - content_type=content_type, - ) - assert response.status_code == auth_status - - -@pytest.mark.django_db -def test_post_webhook_with_mtls_missing_settings( - admin_client: APIClient, - preseed_credential_types, -): - header_key = "Subject" - inputs = { - "auth_type": "mtls", - "subject": "Subject", - "http_header_key": header_key, - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.MTLS.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - with override_settings(WEBHOOK_MTLS_BASE_URL=None): - response = admin_client.post(f"{api_url_v1}/webhooks/", data=data_in) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == { - "eda_credential_id": [ - ( - "EventStream of type mTLS Webhook cannot be " - "used because WEBHOOK_MTLS_BASE_URL is " - "missing in settings." - ) - ] - } - - -@pytest.mark.django_db -def test_post_webhook_with_basic_auth_missing_settings( - admin_client: APIClient, - preseed_credential_types, -): - secret = secrets.token_hex(32) - username = "fred" - inputs = { - "auth_type": "basic", - "username": username, - "password": secret, - "http_header_key": "Authorization", - } - - obj = _create_webhook_credential( - admin_client, enums.WebhookCredentialType.BASIC.value, inputs - ) - - data_in = { - "name": "test-webhook-1", - "eda_credential_id": obj["id"], - "test_mode": True, - } - with override_settings(WEBHOOK_BASE_URL=None): - response = admin_client.post(f"{api_url_v1}/webhooks/", data=data_in) - assert response.status_code == status.HTTP_400_BAD_REQUEST - assert response.json() == { - "eda_credential_id": [ - ( - "EventStream of type Basic Webhook cannot be used because " - "WEBHOOK_BASE_URL is missing in settings." - ) - ] - } - - -def _create_webhook_credential( - client: APIClient, - credential_type_name: str, - inputs: dict, - name: str = "eda-credential", -) -> dict: - credential_type = models.CredentialType.objects.get( - name=credential_type_name - ) - data_in = { - "name": name, - "inputs": inputs, - "credential_type_id": credential_type.id, - } - response = client.post(f"{api_url_v1}/eda-credentials/", data=data_in) - assert response.status_code == status.HTTP_201_CREATED - return response.json() - - -def _create_webhook(client: APIClient, data_in: dict) -> models.Webhook: - with override_settings( - WEBHOOK_BASE_URL="https://www.example.com/", - WEBHOOK_MTLS_BASE_URL="https://www.example.com/", - ): - response = client.post(f"{api_url_v1}/webhooks/", data=data_in) - assert response.status_code == status.HTTP_201_CREATED - return models.Webhook.objects.get(id=response.data["id"]) diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index efffe5b28..0e970016a 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -1118,7 +1118,7 @@ def default_hmac_credential( ) -> models.EdaCredential: """Return a default HMAC Credential""" hmac_credential_type = models.CredentialType.objects.get( - name=enums.WebhookCredentialType.HMAC + name=enums.EventStreamCredentialType.HMAC ) return models.EdaCredential.objects.create( name="default-hmac-credential", @@ -1139,24 +1139,24 @@ def default_hmac_credential( @pytest.fixture -def default_webhooks( +def default_event_streams( default_organization: models.Organization, default_user: models.User, default_hmac_credential: models.EdaCredential, -) -> List[models.Webhook]: - return models.Webhook.objects.bulk_create( +) -> List[models.EventStream]: + return models.EventStream.objects.bulk_create( [ - models.Webhook( + models.EventStream( uuid=uuid.uuid4(), - name="test-webhook-1", + name="test-es-1", owner=default_user, organization=default_organization, url=DUMMY_URL, eda_credential=default_hmac_credential, ), - models.Webhook( + models.EventStream( uuid=uuid.uuid4(), - name="test-webhook-2", + name="test-es-2", owner=default_user, organization=default_organization, url=DUMMY_URL, @@ -1167,14 +1167,14 @@ def default_webhooks( @pytest.fixture -def default_webhook( +def default_event_stream( default_organization: models.Organization, default_user: models.User, default_hmac_credential: models.EdaCredential, -) -> models.Webhook: - return models.Webhook.objects.create( +) -> models.EventStream: + return models.EventStream.objects.create( uuid=uuid.uuid4(), - name="test-webhook-1", + name="test-es-1", owner=default_user, organization=default_organization, url=DUMMY_URL, diff --git a/tests/integration/core/test_rulebook.py b/tests/integration/core/test_rulebook.py index 7abb754e9..e33b01f8d 100644 --- a/tests/integration/core/test_rulebook.py +++ b/tests/integration/core/test_rulebook.py @@ -17,7 +17,7 @@ from aap_eda.core.utils.rulebook import ( DEFAULT_SOURCE_NAME_PREFIX, build_source_list, - swap_webhook_sources, + swap_event_stream_sources, ) @@ -187,16 +187,16 @@ def test_multple_rulesets_with_multiple_no_source_names(): name: my source filters: - noop: -- name: Run a webhook listener service +- name: Run a listener service hosts: all sources: - ansible.eda.webhook: rules: - - name: Webhook event + - name: Event Stream event condition: event.payload.ping == "pong" action: debug: - msg: "Webhook triggered!" + msg: "Event Stream triggered!" - name: Shutdown condition: event.payload.shutdown is defined @@ -287,8 +287,8 @@ def test_multple_rulesets_with_duplicate_names(): TEST_RULESETS, [ { - "webhook_id": 1, - "webhook_name": "swap_me", + "event_stream_id": 1, + "event_stream_name": "swap_me", "rulebook_hash": "hash", "source_name": "demo", } @@ -299,8 +299,8 @@ def test_multple_rulesets_with_duplicate_names(): TEST_RULESETS, [ { - "webhook_id": 1, - "webhook_name": "unmatched", + "event_stream_id": 1, + "event_stream_name": "unmatched", "rulebook_hash": "hash", "source_name": "demo", } @@ -311,8 +311,8 @@ def test_multple_rulesets_with_duplicate_names(): TEST_RULESETS, [ { - "webhook_id": 1, - "webhook_name": "unmatched", + "event_stream_id": 1, + "event_stream_name": "unmatched", "rulebook_hash": "hash", "source_name": "unmatched", } @@ -321,19 +321,19 @@ def test_multple_rulesets_with_duplicate_names(): ), ], ) -def test_swap_webhook_sources( +def test_swap_event_stream_sources( input_rulesets, source_mappings, output_rulesets ): - webhook_source = {} + event_stream_source = {} - webhook_source["swap_me"] = { + event_stream_source["swap_me"] = { "ansible.eda.pg_listener": { "dsn": "encrypted_dsn", "channels": ["channel_name"], }, } - result = swap_webhook_sources( - input_rulesets, webhook_source, source_mappings + result = swap_event_stream_sources( + input_rulesets, event_stream_source, source_mappings ) assert yaml.safe_load(result) == yaml.safe_load(output_rulesets) diff --git a/tests/integration/dab_rbac/test_crud_permissions.py b/tests/integration/dab_rbac/test_crud_permissions.py index 1a1422e76..872e344ec 100644 --- a/tests/integration/dab_rbac/test_crud_permissions.py +++ b/tests/integration/dab_rbac/test_crud_permissions.py @@ -57,7 +57,7 @@ def test_add_permissions( pytest.skip("Model has no add permission") url = reverse(f"{get_basename(model)}-list") - with override_settings(WEBHOOK_BASE_URL="https://www.example.com/"): + with override_settings(EVENT_STREAM_BASE_URL="https://www.example.com/"): response = user_client.post(url, data=post_data) prior_ct = model.objects.count() assert response.status_code == 403, response.data @@ -110,7 +110,7 @@ def test_add_permissions( related_perm = "view" give_obj_perm(default_user, related_obj, related_perm) - with override_settings(WEBHOOK_BASE_URL="https://www.example.com/"): + with override_settings(EVENT_STREAM_BASE_URL="https://www.example.com/"): response = user_client.post(url, data=post_data, format="json") assert response.status_code == 201, response.data diff --git a/tests/integration/dab_rbac/test_organization.py b/tests/integration/dab_rbac/test_organization.py index e2e1a750c..03a65e52b 100644 --- a/tests/integration/dab_rbac/test_organization.py +++ b/tests/integration/dab_rbac/test_organization.py @@ -52,7 +52,7 @@ def test_create_with_default_org(cls_factory, model, admin_client, request): except NoReverseMatch: pytest.skip("Not testing model for now") - with override_settings(WEBHOOK_BASE_URL="https://www.example.com/"): + with override_settings(EVENT_STREAM_BASE_URL="https://www.example.com/"): response = admin_client.post(url, data=post_data, format="json") if response.status_code == 405: @@ -89,7 +89,7 @@ def test_create_with_custom_org( except NoReverseMatch: pytest.skip("Not testing model with no list view for now") - with override_settings(WEBHOOK_BASE_URL="https://www.example.com/"): + with override_settings(EVENT_STREAM_BASE_URL="https://www.example.com/"): response = superuser_client.post(url, data=post_data, format="json") if response.status_code == 405: diff --git a/tools/docker/docker-compose-dev.yaml b/tools/docker/docker-compose-dev.yaml index 129325d07..3b7edffbd 100644 --- a/tools/docker/docker-compose-dev.yaml +++ b/tools/docker/docker-compose-dev.yaml @@ -42,6 +42,8 @@ x-environment: &common-env EDA_SERVER_UUID: edgecafe-beef-feed-fade-decadeedgecafe EDA_PG_NOTIFY_DSN: "host=host.containers.internal port=5432 dbname=eda user=postgres password=secret" EDA_PG_NOTIFY_DSN_SERVER: "host=postgres port=5432 dbname=eda user=postgres password=secret" + EDA_EVENT_STREAM_BASE_URL: ${EDA_EVENT_STREAM_BASE_URL:-https://localhost/edgecafe-beef-feed-fade-decadeedgecafe/} + EDA_EVENT_STREAM_MTLS_BASE_URL: ${EDA_EVENT_STREAM_MTLS_BASE_URL:-https://localhost/mtls/edgecafe-beef-feed-fade-decadeedgecafe/} EDA_WEBHOOK_BASE_URL: ${EDA_WEBHOOK_BASE_URL:-https://localhost/edgecafe-beef-feed-fade-decadeedgecafe/} EDA_WEBHOOK_MTLS_BASE_URL: ${EDA_WEBHOOK_MTLS_BASE_URL:-https://localhost/mtls/edgecafe-beef-feed-fade-decadeedgecafe/} EDA_WEBHOOK_HOST: ${EDA_WEBHOOK_HOST:-eda-webhook-api:8000} diff --git a/tools/docker/docker-compose-mac.yml b/tools/docker/docker-compose-mac.yml index 87d8bab46..b01abc600 100644 --- a/tools/docker/docker-compose-mac.yml +++ b/tools/docker/docker-compose-mac.yml @@ -38,8 +38,8 @@ x-environment: - EDA_SERVER_UUID=edgecafe-beef-feed-fade-decadeedgecafe - EDA_PG_NOTIFY_DSN="host=host.containers.internal port=5432 dbname=eda user=postgres password=secret" - EDA_PG_NOTIFY_DSN_SERVER="host=postgres port=5432 dbname=eda user=postgres password=secret" - - EDA_WEBHOOK_BASE_URL=${EDA_WEBHOOK_BASE_URL:-https://localhost/edgecafe-beef-feed-fade-decadeedgecafe/} - - EDA_WEBHOOK_MTLS_BASE_URL=${EDA_WEBHOOK_MTLS_BASE_URL:-https://localhost/mtls/edgecafe-beef-feed-fade-decadeedgecafe/} + - EDA_EVENT_STREAM_BASE_URL=${EDA_EVENT_STREAM_BASE_URL:-https://localhost/edgecafe-beef-feed-fade-decadeedgecafe/} + - EDA_EVENT_STREAM_MTLS_BASE_URL=${EDA_EVENT_STREAM_MTLS_BASE_URL:-https://localhost/mtls/edgecafe-beef-feed-fade-decadeedgecafe/} - EDA_WEBHOOK_HOST=${EDA_WEBHOOK_HOST:-eda-webhook-api:8000} - EDA_WEBHOOK_SERVER=http://${EDA_WEBHOOK_HOST:-eda-webhook-api:8000} - SSL_CERTIFICATE=${SSL_CERTIFICATE:-/certs/cert.pem} diff --git a/tools/docker/docker-compose-stage.yaml b/tools/docker/docker-compose-stage.yaml index 7bc710581..b3a55cb00 100644 --- a/tools/docker/docker-compose-stage.yaml +++ b/tools/docker/docker-compose-stage.yaml @@ -35,8 +35,8 @@ x-environment: - EDA_SERVER_UUID=edgecafe-beef-feed-fade-decadeedgecafe - EDA_PG_NOTIFY_DSN="host=host.containers.internal port=5432 dbname=eda user=postgres password=secret" - EDA_PG_NOTIFY_DSN_SERVER="host=postgres port=5432 dbname=eda user=postgres password=secret" - - EDA_WEBHOOK_BASE_URL=${EDA_WEBHOOK_BASE_URL:-https://localhost/edgecafe-beef-feed-fade-decadeedgecafe/} - - EDA_WEBHOOK_MTLS_BASE_URL=${EDA_WEBHOOK_MTLS_BASE_URL:-https://localhost/mtls/edgecafe-beef-feed-fade-decadeedgecafe/} + - EDA_EVENT_STREAM_BASE_URL=${EDA_EVENT_STREAM_BASE_URL:-https://localhost/edgecafe-beef-feed-fade-decadeedgecafe/} + - EDA_EVENT_STREAM_MTLS_BASE_URL=${EDA_EVENT_STREAM_MTLS_BASE_URL:-https://localhost/mtls/edgecafe-beef-feed-fade-decadeedgecafe/} - EDA_WEBHOOK_HOST=${EDA_WEBHOOK_HOST:-eda-webhook-api:8000} - EDA_WEBHOOK_SERVER=http://${EDA_WEBHOOK_HOST:-eda-webhook-api:8000} - SSL_CERTIFICATE=${SSL_CERTIFICATE:-/certs/cert.pem}