diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index f40d6a7cc215..4d231f47dcb3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -246,7 +246,7 @@ # AzureSdkOwners: @rajeshka @shankarsama # ServiceLabel: %Event Grid # PRLabel: %Event Grid -/sdk/eventgrid/ @rajeshka @shankarsama +/sdk/eventgrid/ @shankarsama @Nishanth-MS # PRLabel: %Event Hubs /sdk/eventhub/ @axisc @hmlam @j7nw4r @SwayGom @sagar0207 @sjkwak diff --git a/sdk/eventgrid/azure-eventgrid/CHANGELOG.md b/sdk/eventgrid/azure-eventgrid/CHANGELOG.md index d0f5965f119f..0926c164587c 100644 --- a/sdk/eventgrid/azure-eventgrid/CHANGELOG.md +++ b/sdk/eventgrid/azure-eventgrid/CHANGELOG.md @@ -1,5 +1,16 @@ # Release History +## 4.22.1 (2026-07-28) + +### Bugs Fixed + +- Fixed a security issue where the legacy `EventGridPublisherClient` (sync and async) could re-send credential headers to a different host when following an HTTP 3xx redirect. A `SensitiveHeaderCleanupPolicy` is now added to the pipeline to strip `Authorization`, `aeg-sas-key` and `aeg-sas-token` headers on cross-host redirects. + +### Other Changes + +- Raised the minimum `azure-core` dependency to `1.38.3` so that credential headers remain stripped across retries following a cross-host redirect (`insecure_domain_change` flag persistence). +- Increased the minimum supported Python version to 3.10. Python 3.9 and earlier are no longer supported. + ## 4.22.0 (2025-05-14) ### Features Added diff --git a/sdk/eventgrid/azure-eventgrid/README.md b/sdk/eventgrid/azure-eventgrid/README.md index 40d37fe346dc..aae9bdd28f3f 100644 --- a/sdk/eventgrid/azure-eventgrid/README.md +++ b/sdk/eventgrid/azure-eventgrid/README.md @@ -17,7 +17,7 @@ This is a GA release of Azure Event Grid's `EventGridPublisherClient` and `Event ## Getting started ### Prerequisites -* Python 3.8 or later is required to use this package. +* Python 3.10 or later is required to use this package. * You must have an [Azure subscription][azure_subscription] and at least one of the following: * an Event Grid Namespace resource. To create an Event Grid Namespace resource follow [this tutorial](https://learn.microsoft.com/azure/event-grid/create-view-manage-namespaces). * an Event Grid Basic resource. To create an Event Grid Basic resource via the Azure portal follow this [step-by-step tutorial](https://learn.microsoft.com/azure/event-grid/custom-event-quickstart-portal). To create an Event Grid Basic resource via the [Azure CLI](https://learn.microsoft.com/cli/azure) follow this [tutorial](https://learn.microsoft.com/azure/event-grid/custom-event-quickstart) diff --git a/sdk/eventgrid/azure-eventgrid/api.md b/sdk/eventgrid/azure-eventgrid/api.md new file mode 100644 index 000000000000..ffcc8fb45c65 --- /dev/null +++ b/sdk/eventgrid/azure-eventgrid/api.md @@ -0,0 +1,672 @@ +```py +namespace azure.eventgrid + + def azure.eventgrid.generate_sas( + endpoint: str, + shared_access_key: str, + expiration_date_utc: datetime, + *, + api_version: str = constants.DEFAULT_API_VERSION + ) -> str: ... + + + class azure.eventgrid.EventGridConsumerClient(InternalEventGridConsumerClient): implements ContextManager + + def __init__( + self, + endpoint: str, + credential: Union[AzureKeyCredential, TokenCredential], + *, + api_version: Optional[str] = ..., + namespace_topic: str, + subscription: str, + **kwargs: Any + ) -> None: ... + + def __repr__(self) -> str: ... + + @distributed_trace + def acknowledge( + self, + *, + lock_tokens: List[str], + **kwargs: Any + ) -> AcknowledgeResult: ... + + def close(self) -> None: ... + + @distributed_trace + def receive( + self, + *, + max_events: Optional[int] = ..., + max_wait_time: Optional[int] = ..., + **kwargs: Any + ) -> List[ReceiveDetails]: ... + + @distributed_trace + def reject( + self, + *, + lock_tokens: List[str], + **kwargs: Any + ) -> RejectResult: ... + + @api_version_validation(params_added_on={'2023-10-01-preview': ['release_delay']}) + def release( + self, + *, + lock_tokens: List[str], + release_delay: Optional[Union[int, ReleaseDelay]] = ..., + **kwargs: Any + ) -> ReleaseResult: ... + + @distributed_trace + @api_version_validation(method_added_on='2023-10-01-preview', params_added_on={'2023-10-01-preview': ['api_version', 'content_type', 'accept']}) + def renew_locks( + self, + *, + lock_tokens: List[str], + **kwargs: Any + ) -> RenewLocksResult: ... + + def send_request( + self, + request: HttpRequest, + *, + stream: bool = False, + **kwargs: Any + ) -> HttpResponse: ... + + + class azure.eventgrid.EventGridEvent(InternalEventGridEvent): + data: object + data_version: str + event_time: datetime + event_type: str + id: str + metadata_version: str + subject: str + topic: str + + def __eq__(self, other: Any) -> bool: ... + + def __init__( + self, + subject: str, + event_type: str, + data: Any, + data_version: str, + *, + event_time: Optional[datetime] = ..., + id: Optional[str] = ..., + metadata_version: Optional[str] = ..., + topic: Optional[str] = ..., + **kwargs: Any + ) -> None: ... + + def __ne__(self, other: Any) -> bool: ... + + def __repr__(self) -> str: ... + + def __str__(self) -> str: ... + + @classmethod + def deserialize( + cls: Type[ModelType], + data: Any, + content_type: Optional[str] = None + ) -> ModelType: ... + + @classmethod + def enable_additional_properties_sending(cls) -> None: ... + + @classmethod + def from_dict( + cls: Type[ModelType], + data: Any, + key_extractors: Optional[Callable[[str, Dict[str, Any], Any], Any]] = None, + content_type: Optional[str] = None + ) -> ModelType: ... + + @classmethod + def from_json(cls, event: Any) -> EventGridEvent: ... + + @classmethod + def is_xml_model(cls) -> bool: ... + + def as_dict( + self, + keep_readonly: bool = True, + key_transformer: Callable[[str, Dict[str, Any], Any], Any] = attribute_transformer, + **kwargs: Any + ) -> JSON: ... + + def serialize( + self, + keep_readonly: bool = False, + **kwargs: Any + ) -> JSON: ... + + + class azure.eventgrid.EventGridPublisherClient(InternalEventGridPublisherClient): implements ContextManager + + def __init__( + self, + endpoint: str, + credential: Union[AzureKeyCredential, AzureSasCredential, TokenCredential], + *, + api_version: Optional[str] = ..., + namespace_topic: Optional[str] = ..., + **kwargs: Any + ) -> None: ... + + def __repr__(self) -> str: ... + + def close(self) -> None: ... + + @distributed_trace + def send( + self, + events: Union[CloudEvent, List[CloudEvent], Dict[str, Any], List[Dict[str, Any]], CNCFCloudEvent, List[CNCFCloudEvent], EventGridEvent, List[EventGridEvent]], + *, + channel_name: Optional[str] = ..., + content_type: Optional[str] = ..., + **kwargs: Any + ) -> None: ... + + def send_request( + self, + request: HttpRequest, + *, + stream: bool = False, + **kwargs: Any + ) -> HttpResponse: ... + + + class azure.eventgrid.SystemEventNames(str, Enum, metaclass=CaseInsensitiveEnumMeta): + AcsAdvancedMessageDeliveryStatusUpdatedEventName = "Microsoft.Communication.AdvancedMessageDeliveryStatusUpdated" + AcsAdvancedMessageReceivedEventName = "Microsoft.Communication.AdvancedMessageReceived" + AcsCallEndedEventName = "Microsoft.Communication.CallEnded" + AcsCallParticipantAddedEventName = "Microsoft.Communication.CallParticipantAdded" + AcsCallParticipantRemovedEventName = "Microsoft.Communication.CallParticipantRemoved" + AcsCallStartedEventName = "Microsoft.Communication.CallStarted" + AcsChatAzureBotCommandReceivedInThreadEventName = "Microsoft.Communication.ChatAzureBotCommandReceivedInThread" + AcsChatMemberAddedToThreadWithUserEventName = "Microsoft.Communication.ChatMemberAddedToThreadWithUser" + AcsChatMemberRemovedFromThreadWithUserEventName = "Microsoft.Communication.ChatMemberRemovedFromThreadWithUser" + AcsChatMessageDeletedEventName = "Microsoft.Communication.ChatMessageDeleted" + AcsChatMessageDeletedInThreadEventName = "Microsoft.Communication.ChatMessageDeletedInThread" + AcsChatMessageEditedEventName = "Microsoft.Communication.ChatMessageEdited" + AcsChatMessageEditedInThreadEventName = "Microsoft.Communication.ChatMessageEditedInThread" + AcsChatMessageReceivedEventName = "Microsoft.Communication.ChatMessageReceived" + AcsChatMessageReceivedInThreadEventName = "Microsoft.Communication.ChatMessageReceivedInThread" + AcsChatParticipantAddedToThreadEventName = "Microsoft.Communication.ChatThreadParticipantAdded" + AcsChatParticipantAddedToThreadWithUserEventName = "Microsoft.Communication.ChatParticipantAddedToThreadWithUser" + AcsChatParticipantRemovedFromThreadEventName = "Microsoft.Communication.ChatThreadParticipantRemoved" + AcsChatParticipantRemovedFromThreadWithUserEventName = "Microsoft.Communication.ChatParticipantRemovedFromThreadWithUser" + AcsChatThreadCreatedEventName = "Microsoft.Communication.ChatThreadCreated" + AcsChatThreadCreatedWithUserEventName = "Microsoft.Communication.ChatThreadCreatedWithUser" + AcsChatThreadDeletedEventName = "Microsoft.Communication.ChatThreadDeleted" + AcsChatThreadParticipantAddedEventName = "Microsoft.Communication.ChatThreadParticipantAdded" + AcsChatThreadParticipantRemovedEventName = "Microsoft.Communication.ChatThreadParticipantRemoved" + AcsChatThreadPropertiesUpdatedEventName = "Microsoft.Communication.ChatThreadPropertiesUpdated" + AcsChatThreadPropertiesUpdatedPerUserEventName = "Microsoft.Communication.ChatThreadPropertiesUpdatedPerUser" + AcsChatThreadWithUserDeletedEventName = "Microsoft.Communication.ChatThreadWithUserDeleted" + AcsChatTypingIndicatorReceivedInThreadEventName = "Microsoft.Communication.ChatTypingIndicatorReceivedInThread" + AcsEmailDeliveryReportReceivedEventName = "Microsoft.Communication.EmailDeliveryReportReceived" + AcsEmailEngagementTrackingReportReceivedEventName = "Microsoft.Communication.EmailEngagementTrackingReportReceived" + AcsIncomingCallEventName = "Microsoft.Communication.IncomingCall" + AcsRecordingFileStatusUpdatedEventName = "Microsoft.Communication.RecordingFileStatusUpdated" + AcsRouterJobCancelledEventName = "Microsoft.Communication.RouterJobCancelled" + AcsRouterJobClassificationFailedEventName = "Microsoft.Communication.RouterJobClassificationFailed" + AcsRouterJobClassifiedEventName = "Microsoft.Communication.RouterJobClassified" + AcsRouterJobClosedEventName = "Microsoft.Communication.RouterJobClosed" + AcsRouterJobCompletedEventName = "Microsoft.Communication.RouterJobCompleted" + AcsRouterJobDeletedEventName = "Microsoft.Communication.RouterJobDeleted" + AcsRouterJobExceptionTriggeredEventName = "Microsoft.Communication.RouterJobExceptionTriggered" + AcsRouterJobQueuedEventName = "Microsoft.Communication.RouterJobQueued" + AcsRouterJobReceivedEventName = "Microsoft.Communication.RouterJobReceived" + AcsRouterJobSchedulingFailedEventName = "Microsoft.Communication.RouterJobSchedulingFailed" + AcsRouterJobUnassignedEventName = "Microsoft.Communication.RouterJobUnassigned" + AcsRouterJobWaitingForActivationEventName = "Microsoft.Communication.RouterJobWaitingForActivation" + AcsRouterJobWorkerSelectorsExpiredEventName = "Microsoft.Communication.RouterJobWorkerSelectorsExpired" + AcsRouterWorkerDeletedEventName = "Microsoft.Communication.RouterWorkerDeleted" + AcsRouterWorkerDeregisteredEventName = "Microsoft.Communication.RouterWorkerDeregistered" + AcsRouterWorkerOfferAcceptedEventName = "Microsoft.Communication.RouterWorkerOfferAccepted" + AcsRouterWorkerOfferDeclinedEventName = "Microsoft.Communication.RouterWorkerOfferDeclined" + AcsRouterWorkerOfferExpiredEventName = "Microsoft.Communication.RouterWorkerOfferExpired" + AcsRouterWorkerOfferIssuedEventName = "Microsoft.Communication.RouterWorkerOfferIssued" + AcsRouterWorkerOfferRevokedEventName = "Microsoft.Communication.RouterWorkerOfferRevoked" + AcsRouterWorkerRegisteredEventName = "Microsoft.Communication.RouterWorkerRegistered" + AcsRouterWorkerUpdatedEventName = "Microsoft.Communication.RouterWorkerUpdated" + AcsSmsDeliveryReportReceivedEventName = "Microsoft.Communication.SMSDeliveryReportReceived" + AcsSmsReceivedEventName = "Microsoft.Communication.SMSReceived" + AcsUserDisconnectedEventName = "Microsoft.Communication.UserDisconnected" + ApiCenterApiDefinitionAddedEventName = "Microsoft.ApiCenter.ApiDefinitionAdded" + ApiCenterApiDefinitionUpdatedEventName = "Microsoft.ApiCenter.ApiDefinitionUpdated" + ApiManagementApiCreatedEventName = "Microsoft.ApiManagement.APICreated" + ApiManagementApiDeletedEventName = "Microsoft.ApiManagement.APIDeleted" + ApiManagementApiReleaseCreatedEventName = "Microsoft.ApiManagement.APIReleaseCreated" + ApiManagementApiReleaseDeletedEventName = "Microsoft.ApiManagement.APIReleaseDeleted" + ApiManagementApiReleaseUpdatedEventName = "Microsoft.ApiManagement.APIReleaseUpdated" + ApiManagementApiUpdatedEventName = "Microsoft.ApiManagement.APIUpdated" + ApiManagementCircuitBreakerClosedEventName = "Microsoft.ApiManagement.CircuitBreaker.Closed" + ApiManagementCircuitBreakerOpenedEventName = "Microsoft.ApiManagement.CircuitBreaker.Opened" + ApiManagementGatewayApiAddedEventName = "Microsoft.ApiManagement.GatewayAPIAdded" + ApiManagementGatewayApiRemovedEventName = "Microsoft.ApiManagement.GatewayAPIRemoved" + ApiManagementGatewayCertificateAuthorityCreatedEventName = "Microsoft.ApiManagement.GatewayCertificateAuthorityCreated" + ApiManagementGatewayCertificateAuthorityDeletedEventName = "Microsoft.ApiManagement.GatewayCertificateAuthorityDeleted" + ApiManagementGatewayCertificateAuthorityUpdatedEventName = "Microsoft.ApiManagement.GatewayCertificateAuthorityUpdated" + ApiManagementGatewayCreatedEventName = "Microsoft.ApiManagement.GatewayCreated" + ApiManagementGatewayDeletedEventName = "Microsoft.ApiManagement.GatewayDeleted" + ApiManagementGatewayHostnameConfigurationCreatedEventName = "Microsoft.ApiManagement.GatewayHostnameConfigurationCreated" + ApiManagementGatewayHostnameConfigurationDeletedEventName = "Microsoft.ApiManagement.GatewayHostnameConfigurationDeleted" + ApiManagementGatewayHostnameConfigurationUpdatedEventName = "Microsoft.ApiManagement.GatewayHostnameConfigurationUpdated" + ApiManagementGatewayTokenExpiredEventName = "Microsoft.ApiManagement.GatewayTokenExpired" + ApiManagementGatewayTokenNearExpiryEventName = "Microsoft.ApiManagement.GatewayTokenNearExpiry" + ApiManagementGatewayUpdatedEventName = "Microsoft.ApiManagement.GatewayUpdated" + ApiManagementProductCreatedEventName = "Microsoft.ApiManagement.ProductCreated" + ApiManagementProductDeletedEventName = "Microsoft.ApiManagement.ProductDeleted" + ApiManagementProductUpdatedEventName = "Microsoft.ApiManagement.ProductUpdated" + ApiManagementSubscriptionCreatedEventName = "Microsoft.ApiManagement.SubscriptionCreated" + ApiManagementSubscriptionDeletedEventName = "Microsoft.ApiManagement.SubscriptionDeleted" + ApiManagementSubscriptionUpdatedEventName = "Microsoft.ApiManagement.SubscriptionUpdated" + ApiManagementUserCreatedEventName = "Microsoft.ApiManagement.UserCreated" + ApiManagementUserDeletedEventName = "Microsoft.ApiManagement.UserDeleted" + ApiManagementUserUpdatedEventName = "Microsoft.ApiManagement.UserUpdated" + AppConfigurationKeyValueDeletedEventName = "Microsoft.AppConfiguration.KeyValueDeleted" + AppConfigurationKeyValueModifiedEventName = "Microsoft.AppConfiguration.KeyValueModified" + AppConfigurationSnapshotCreatedEventName = "Microsoft.AppConfiguration.SnapshotCreated" + AppConfigurationSnapshotModifiedEventName = "Microsoft.AppConfiguration.SnapshotModified" + AvsClusterCreatedEventName = "Microsoft.AVS.ClusterCreated" + AvsClusterDeletedEventName = "Microsoft.AVS.ClusterDeleted" + AvsClusterFailedEventName = "Microsoft.AVS.ClusterFailed" + AvsClusterUpdatedEventName = "Microsoft.AVS.ClusterUpdated" + AvsClusterUpdatingEventName = "Microsoft.AVS.ClusterUpdating" + AvsPrivateCloudFailedEventName = "Microsoft.AVS.PrivateCloudFailed" + AvsPrivateCloudUpdatedEventName = "Microsoft.AVS.PrivateCloudUpdated" + AvsPrivateCloudUpdatingEventName = "Microsoft.AVS.PrivateCloudUpdating" + AvsScriptExecutionCancelledEventName = "Microsoft.AVS.ScriptExecutionCancelled" + AvsScriptExecutionFailedEventName = "Microsoft.AVS.ScriptExecutionFailed" + AvsScriptExecutionFinishedEventName = "Microsoft.AVS.ScriptExecutionFinished" + AvsScriptExecutionStartedEventName = "Microsoft.AVS.ScriptExecutionStarted" + ContainerRegistryArtifactEventName = "Microsoft.AppConfiguration.KeyValueModified" + ContainerRegistryChartDeletedEventName = "Microsoft.ContainerRegistry.ChartDeleted" + ContainerRegistryChartPushedEventName = "Microsoft.ContainerRegistry.ChartPushed" + ContainerRegistryEventName = "Microsoft.ContainerRegistry.ChartPushed" + ContainerRegistryImageDeletedEventName = "Microsoft.ContainerRegistry.ImageDeleted" + ContainerRegistryImagePushedEventName = "Microsoft.ContainerRegistry.ImagePushed" + ContainerServiceClusterSupportEndedEventName = "Microsoft.ContainerService.ClusterSupportEnded" + ContainerServiceClusterSupportEndingEventName = "Microsoft.ContainerService.ClusterSupportEnding" + ContainerServiceNewKubernetesVersionAvailableEventName = "Microsoft.ContainerService.NewKubernetesVersionAvailable" + ContainerServiceNodePoolRollingFailedEventName = "Microsoft.ContainerService.NodePoolRollingFailed" + ContainerServiceNodePoolRollingStartedEventName = "Microsoft.ContainerService.NodePoolRollingStarted" + ContainerServiceNodePoolRollingSucceededEventName = "Microsoft.ContainerService.NodePoolRollingSucceeded" + DataBoxCopyCompletedEventName = "Microsoft.DataBox.CopyCompleted" + DataBoxCopyStartedEventName = "Microsoft.DataBox.CopyStarted" + DataBoxOrderCompletedEventName = "Microsoft.DataBox.OrderCompleted" + EdgeSolutionVersionPublishedEventName = "Microsoft.Edge.SolutionVersionPublished" + EventGridMQTTClientCreatedOrUpdatedEventName = "Microsoft.EventGrid.MQTTClientCreatedOrUpdated" + EventGridMQTTClientDeletedEventName = "Microsoft.EventGrid.MQTTClientDeleted" + EventGridMQTTClientSessionConnectedEventName = "Microsoft.EventGrid.MQTTClientSessionConnected" + EventGridMQTTClientSessionDisconnectedEventName = "Microsoft.EventGrid.MQTTClientSessionDisconnected" + EventGridSubscriptionDeletedEventName = "Microsoft.EventGrid.SubscriptionDeletedEvent" + EventGridSubscriptionValidationEventName = "Microsoft.EventGrid.SubscriptionValidationEvent" + EventHubCaptureFileCreatedEventName = "Microsoft.EventHub.CaptureFileCreated" + HealthcareDicomImageCreatedEventName = "Microsoft.HealthcareApis.DicomImageCreated" + HealthcareDicomImageDeletedEventName = "Microsoft.HealthcareApis.DicomImageDeleted" + HealthcareDicomImageUpdatedEventName = "Microsoft.HealthcareApis.DicomImageUpdated" + HealthcareFhirResourceCreatedEventName = "Microsoft.HealthcareApis.FhirResourceCreated" + HealthcareFhirResourceDeletedEventName = "Microsoft.HealthcareApis.FhirResourceDeleted" + HealthcareFhirResourceUpdatedEventName = "Microsoft.HealthcareApis.FhirResourceUpdated" + IoTHubDeviceConnectedEventName = "Microsoft.Devices.DeviceConnected" + IoTHubDeviceCreatedEventName = "Microsoft.Devices.DeviceCreated" + IoTHubDeviceDeletedEventName = "Microsoft.Devices.DeviceDeleted" + IoTHubDeviceDisconnectedEventName = "Microsoft.Devices.DeviceDisconnected" + IotHubDeviceConnectedEventName = "Microsoft.Devices.DeviceConnected" + IotHubDeviceCreatedEventName = "Microsoft.Devices.DeviceCreated" + IotHubDeviceDeletedEventName = "Microsoft.Devices.DeviceDeleted" + IotHubDeviceDisconnectedEventName = "Microsoft.Devices.DeviceDisconnected" + IotHubDeviceTelemetryEventName = "Microsoft.Devices.DeviceTelemetry" + KeyVaultAccessPolicyChangedEventName = "Microsoft.KeyVault.VaultAccessPolicyChanged" + KeyVaultCertificateExpiredEventName = "Microsoft.KeyVault.CertificateExpired" + KeyVaultCertificateNearExpiryEventName = "Microsoft.KeyVault.CertificateNearExpiry" + KeyVaultCertificateNewVersionCreatedEventName = "Microsoft.KeyVault.CertificateNewVersionCreated" + KeyVaultKeyExpiredEventName = "Microsoft.KeyVault.KeyExpired" + KeyVaultKeyNearExpiryEventName = "Microsoft.KeyVault.KeyNearExpiry" + KeyVaultKeyNewVersionCreatedEventName = "Microsoft.KeyVault.KeyNewVersionCreated" + KeyVaultSecretExpiredEventName = "Microsoft.KeyVault.SecretExpired" + KeyVaultSecretNearExpiryEventName = "Microsoft.KeyVault.SecretNearExpiry" + KeyVaultSecretNewVersionCreatedEventName = "Microsoft.KeyVault.SecretNewVersionCreated" + KeyVaultVaultAccessPolicyChangedEventName = "Microsoft.KeyVault.VaultAccessPolicyChanged" + MachineLearningServicesDatasetDriftDetectedEventName = "Microsoft.MachineLearningServices.DatasetDriftDetected" + MachineLearningServicesModelDeployedEventName = "Microsoft.MachineLearningServices.ModelDeployed" + MachineLearningServicesModelRegisteredEventName = "Microsoft.MachineLearningServices.ModelRegistered" + MachineLearningServicesRunCompletedEventName = "Microsoft.MachineLearningServices.RunCompleted" + MachineLearningServicesRunStatusChangedEventName = "Microsoft.MachineLearningServices.RunStatusChanged" + MapsGeofenceEnteredEventName = "Microsoft.Maps.GeofenceEntered" + MapsGeofenceExitedEventName = "Microsoft.Maps.GeofenceExited" + MapsGeofenceResultEventName = "Microsoft.Maps.GeofenceResult" + MediaJobCanceledEventName = "Microsoft.Media.JobCanceled" + MediaJobCancelingEventName = "Microsoft.Media.JobCanceling" + MediaJobErroredEventName = "Microsoft.Media.JobErrored" + MediaJobFinishedEventName = "Microsoft.Media.JobFinished" + MediaJobOutputCanceledEventName = "Microsoft.Media.JobOutputCanceled" + MediaJobOutputCancelingEventName = "Microsoft.Media.JobOutputCanceling" + MediaJobOutputErroredEventName = "Microsoft.Media.JobOutputErrored" + MediaJobOutputFinishedEventName = "Microsoft.Media.JobOutputFinished" + MediaJobOutputProcessingEventName = "Microsoft.Media.JobOutputProcessing" + MediaJobOutputProgressEventName = "Microsoft.Media.JobOutputProgress" + MediaJobOutputScheduledEventName = "Microsoft.Media.JobOutputScheduled" + MediaJobOutputStateChangeEventName = "Microsoft.Media.JobOutputStateChange" + MediaJobProcessingEventName = "Microsoft.Media.JobProcessing" + MediaJobScheduledEventName = "Microsoft.Media.JobScheduled" + MediaJobStateChangeEventName = "Microsoft.Media.JobStateChange" + MediaLiveEventChannelArchiveHeartbeatEventName = "Microsoft.Media.LiveEventChannelArchiveHeartbeat" + MediaLiveEventConnectionRejectedEventName = "Microsoft.Media.LiveEventConnectionRejected" + MediaLiveEventEncoderConnectedEventName = "Microsoft.Media.LiveEventEncoderConnected" + MediaLiveEventEncoderDisconnectedEventName = "Microsoft.Media.LiveEventEncoderDisconnected" + MediaLiveEventIncomingDataChunkDroppedEventName = "Microsoft.Media.LiveEventIncomingDataChunkDropped" + MediaLiveEventIncomingStreamReceivedEventName = "Microsoft.Media.LiveEventIncomingStreamReceived" + MediaLiveEventIncomingStreamsOutOfSyncEventName = "Microsoft.Media.LiveEventIncomingStreamsOutOfSync" + MediaLiveEventIncomingVideoStreamsOutOfSyncEventName = "Microsoft.Media.LiveEventIncomingVideoStreamsOutOfSync" + MediaLiveEventIngestHeartbeatEventName = "Microsoft.Media.LiveEventIngestHeartbeat" + MediaLiveEventTrackDiscontinuityDetectedEventName = "Microsoft.Media.LiveEventTrackDiscontinuityDetected" + PolicyInsightsPolicyStateChangedEventName = "Microsoft.PolicyInsights.PolicyStateChanged" + PolicyInsightsPolicyStateCreatedEventName = "Microsoft.PolicyInsights.PolicyStateCreated" + PolicyInsightsPolicyStateDeletedEventName = "Microsoft.PolicyInsights.PolicyStateDeleted" + RedisExportRDBCompletedEventName = "Microsoft.Cache.ExportRDBCompleted" + RedisImportRDBCompletedEventName = "Microsoft.Cache.ImportRDBCompleted" + RedisPatchingCompletedEventName = "Microsoft.Cache.PatchingCompleted" + RedisScalingCompletedEventName = "Microsoft.Cache.ScalingCompleted" + ResourceActionCancelEventName = "Microsoft.Resources.ResourceActionCancel" + ResourceActionCancelName = "Microsoft.Resources.ResourceActionCancel" + ResourceActionFailureEventName = "Microsoft.Resources.ResourceActionFailure" + ResourceActionFailureName = "Microsoft.Resources.ResourceActionFailure" + ResourceActionSuccessEventName = "Microsoft.Resources.ResourceActionSuccess" + ResourceActionSuccessName = "Microsoft.Resources.ResourceActionSuccess" + ResourceDeleteCancelEventName = "Microsoft.Resources.ResourceDeleteCancel" + ResourceDeleteCancelName = "Microsoft.Resources.ResourceDeleteCancel" + ResourceDeleteFailureEventName = "Microsoft.Resources.ResourceDeleteFailure" + ResourceDeleteFailureName = "Microsoft.Resources.ResourceDeleteFailure" + ResourceDeleteSuccessEventName = "Microsoft.Resources.ResourceDeleteSuccess" + ResourceDeleteSuccessName = "Microsoft.Resources.ResourceDeleteSuccess" + ResourceNotificationsContainerServiceEventResourcesScheduledEventName = "Microsoft.ResourceNotifications.ContainerServiceEventResources.ScheduledEventEmitted" + ResourceNotificationsHealthResourcesAnnotatedEventName = "Microsoft.ResourceNotifications.HealthResources.ResourceAnnotated" + ResourceNotificationsHealthResourcesAvailabilityStatusChangedEventName = "Microsoft.ResourceNotifications.HealthResources.AvailabilityStatusChanged" + ResourceNotificationsResourceManagementCreatedOrUpdatedEventName = "Microsoft.ResourceNotifications.Resources.CreatedOrUpdated" + ResourceNotificationsResourceManagementDeletedEventName = "Microsoft.ResourceNotifications.Resources.Deleted" + ResourceWriteCancelEventName = "Microsoft.Resources.ResourceWriteCancel" + ResourceWriteCancelName = "Microsoft.Resources.ResourceWriteCancel" + ResourceWriteFailureEventName = "Microsoft.Resources.ResourceWriteFailure" + ResourceWriteFailureName = "Microsoft.Resources.ResourceWriteFailure" + ResourceWriteSuccessEventName = "Microsoft.Resources.ResourceWriteSuccess" + ResourceWriteSuccessName = "Microsoft.Resources.ResourceWriteSuccess" + ServiceBusActiveMessagesAvailablePeriodicNotificationsEventName = "Microsoft.ServiceBus.ActiveMessagesAvailablePeriodicNotifications" + ServiceBusActiveMessagesAvailableWithNoListenersEventName = "Microsoft.ServiceBus.ActiveMessagesAvailableWithNoListeners" + ServiceBusDeadletterMessagesAvailablePeriodicNotificationsEventName = "Microsoft.ServiceBus.DeadletterMessagesAvailablePeriodicNotifications" + ServiceBusDeadletterMessagesAvailableWithNoListenerEventName = "Microsoft.ServiceBus.DeadletterMessagesAvailableWithNoListeners" + ServiceBusDeadletterMessagesAvailableWithNoListenersEventName = "Microsoft.ServiceBus.DeadletterMessagesAvailableWithNoListeners" + SignalRServiceClientConnectionConnectedEventName = "Microsoft.SignalRService.ClientConnectionConnected" + SignalRServiceClientConnectionDisconnectedEventName = "Microsoft.SignalRService.ClientConnectionDisconnected" + StorageAsyncOperationInitiatedEventName = "Microsoft.Storage.AsyncOperationInitiated" + StorageBlobCreatedEventName = "Microsoft.Storage.BlobCreated" + StorageBlobDeletedEventName = "Microsoft.Storage.BlobDeleted" + StorageBlobInventoryPolicyCompletedEventName = "Microsoft.Storage.BlobInventoryPolicyCompleted" + StorageBlobRenamedEventName = "Microsoft.Storage.BlobRenamed" + StorageBlobTierChangedEventName = "Microsoft.Storage.BlobTierChanged" + StorageDirectoryCreatedEventName = "Microsoft.Storage.DirectoryCreated" + StorageDirectoryDeletedEventName = "Microsoft.Storage.DirectoryDeleted" + StorageDirectoryRenamedEventName = "Microsoft.Storage.DirectoryRenamed" + StorageLifecyclePolicyCompletedEventName = "Microsoft.Storage.LifecyclePolicyCompleted" + StorageTaskAssignmentCompletedEventName = "Microsoft.Storage.StorageTaskAssignmentCompleted" + StorageTaskAssignmentQueuedEventName = "Microsoft.Storage.StorageTaskAssignmentQueued" + StorageTaskCompletedEventName = "Microsoft.Storage.StorageTaskCompleted" + StorageTaskQueuedEventName = "Microsoft.Storage.StorageTaskQueued" + SubscriptionDeletedEventName = "Microsoft.EventGrid.SubscriptionDeletedEvent" + SubscriptionValidationEventName = "Microsoft.EventGrid.SubscriptionValidationEvent" + WebAppServicePlanUpdatedEventName = "Microsoft.Web.AppServicePlanUpdated" + WebAppUpdatedEventName = "Microsoft.Web.AppUpdated" + WebBackupOperationCompletedEventName = "Microsoft.Web.BackupOperationCompleted" + WebBackupOperationFailedEventName = "Microsoft.Web.BackupOperationFailed" + WebBackupOperationStartedEventName = "Microsoft.Web.BackupOperationStarted" + WebRestoreOperationCompletedEventName = "Microsoft.Web.RestoreOperationCompleted" + WebRestoreOperationFailedEventName = "Microsoft.Web.RestoreOperationFailed" + WebRestoreOperationStartedEventName = "Microsoft.Web.RestoreOperationStarted" + WebSlotSwapCompletedEventName = "Microsoft.Web.SlotSwapCompleted" + WebSlotSwapFailedEventName = "Microsoft.Web.SlotSwapFailed" + WebSlotSwapStartedEventName = "Microsoft.Web.SlotSwapStarted" + WebSlotSwapWithPreviewCancelledEventName = "Microsoft.Web.SlotSwapWithPreviewCancelled" + WebSlotSwapWithPreviewStartedEventName = "Microsoft.Web.SlotSwapWithPreviewStarted" + + +namespace azure.eventgrid.aio + + class azure.eventgrid.aio.EventGridConsumerClient(InternalEventGridConsumerClient): implements AsyncContextManager + + def __init__( + self, + endpoint: str, + credential: Union[AzureKeyCredential, AsyncTokenCredential], + *, + api_version: Optional[str] = ..., + namespace_topic: str, + subscription: str, + **kwargs: Any + ) -> None: ... + + def __repr__(self) -> str: ... + + @distributed_trace_async + async def acknowledge( + self, + *, + lock_tokens: List[str], + **kwargs: Any + ) -> AcknowledgeResult: ... + + async def close(self) -> None: ... + + @distributed_trace_async + async def receive( + self, + *, + max_events: Optional[int] = ..., + max_wait_time: Optional[int] = ..., + **kwargs: Any + ) -> List[ReceiveDetails]: ... + + @distributed_trace_async + async def reject( + self, + *, + lock_tokens: List[str], + **kwargs: Any + ) -> RejectResult: ... + + @distributed_trace_async + @api_version_validation(params_added_on={'2023-10-01-preview': ['release_delay']}) + async def release( + self, + *, + lock_tokens: List[str], + release_delay: Optional[Union[int, ReleaseDelay]] = ..., + **kwargs: Any + ) -> ReleaseResult: ... + + @distributed_trace_async + @api_version_validation(method_added_on='2023-10-01-preview', params_added_on={'2023-10-01-preview': ['api_version', 'content_type', 'accept']}) + async def renew_locks( + self, + *, + lock_tokens: List[str], + **kwargs: Any + ) -> RenewLocksResult: ... + + def send_request( + self, + request: HttpRequest, + *, + stream: bool = False, + **kwargs: Any + ) -> Awaitable[AsyncHttpResponse]: ... + + + class azure.eventgrid.aio.EventGridPublisherClient(InternalEventGridPublisherClient): implements AsyncContextManager + + def __init__( + self, + endpoint: str, + credential: Union[AzureKeyCredential, AzureSasCredential, AsyncTokenCredential], + *, + api_version: Optional[str] = ..., + namespace_topic: Optional[str] = ..., + **kwargs: Any + ) -> None: ... + + def __repr__(self) -> str: ... + + async def close(self) -> None: ... + + @distributed_trace_async + async def send( + self, + events: Union[CloudEvent, List[CloudEvent], Dict[str, Any], List[Dict[str, Any]], CNCFCloudEvent, List[CNCFCloudEvent], EventGridEvent, List[EventGridEvent]], + *, + channel_name: Optional[str] = ..., + content_type: Optional[str] = ..., + **kwargs: Any + ) -> None: ... + + def send_request( + self, + request: HttpRequest, + *, + stream: bool = False, + **kwargs: Any + ) -> Awaitable[AsyncHttpResponse]: ... + + +namespace azure.eventgrid.models + + class azure.eventgrid.models.AcknowledgeResult(Model): + failed_lock_tokens: List[FailedLockToken] + succeeded_lock_tokens: List[str] + + @overload + def __init__( + self, + *, + failed_lock_tokens: List[FailedLockToken], + succeeded_lock_tokens: List[str] + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: ... + + + class azure.eventgrid.models.BrokerProperties(InternalBrokerProperties): + delivery_count: int + lock_token: str + + @overload + def __init__( + self, + *, + delivery_count: int, + lock_token: str + ): ... + + @overload + def __init__(self, mapping: Mapping[str, Any]): ... + + + class azure.eventgrid.models.FailedLockToken(Model): + error: ODataV4Format + lock_token: str + + @overload + def __init__( + self, + *, + error: ODataV4Format, + lock_token: str + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: ... + + + class azure.eventgrid.models.ReceiveDetails(InternalReceiveDetails, Generic[DataType]): + broker_properties: BrokerProperties + event: CloudEvent + + @overload + def __init__( + self, + *, + broker_properties: BrokerProperties, + event: CloudEvent[DataType] + ): ... + + @overload + def __init__(self, mapping: Mapping[str, Any]): ... + + + class azure.eventgrid.models.RejectResult(Model): + failed_lock_tokens: List[FailedLockToken] + succeeded_lock_tokens: List[str] + + @overload + def __init__( + self, + *, + failed_lock_tokens: List[FailedLockToken], + succeeded_lock_tokens: List[str] + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: ... + + + class azure.eventgrid.models.ReleaseDelay(str, Enum, metaclass=CaseInsensitiveEnumMeta): + NO_DELAY = "0" + ONE_HOUR = "3600" + ONE_MINUTE = "60" + TEN_MINUTES = "600" + TEN_SECONDS = "10" + + + class azure.eventgrid.models.ReleaseResult(Model): + failed_lock_tokens: List[FailedLockToken] + succeeded_lock_tokens: List[str] + + @overload + def __init__( + self, + *, + failed_lock_tokens: List[FailedLockToken], + succeeded_lock_tokens: List[str] + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: ... + + + class azure.eventgrid.models.RenewLocksResult(Model): + failed_lock_tokens: List[FailedLockToken] + succeeded_lock_tokens: List[str] + + @overload + def __init__( + self, + *, + failed_lock_tokens: List[FailedLockToken], + succeeded_lock_tokens: List[str] + ) -> None: ... + + @overload + def __init__(self, mapping: Mapping[str, Any]) -> None: ... + + +``` \ No newline at end of file diff --git a/sdk/eventgrid/azure-eventgrid/api.metadata.yml b/sdk/eventgrid/azure-eventgrid/api.metadata.yml new file mode 100644 index 000000000000..d03a8016fb72 --- /dev/null +++ b/sdk/eventgrid/azure-eventgrid/api.metadata.yml @@ -0,0 +1,3 @@ +apiMdSha256: dcc804237da14b8d346f7881e23a570a9639c4784861a319246e65309b46b724 +parserVersion: 0.3.30 +pythonVersion: 3.13.14 diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/_publisher_client.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/_publisher_client.py index 9250a614c0a6..2706ccc215ee 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/_publisher_client.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/_publisher_client.py @@ -20,6 +20,7 @@ DistributedTracingPolicy, HttpLoggingPolicy, UserAgentPolicy, + SensitiveHeaderCleanupPolicy, ) from azure.core.exceptions import ( ClientAuthenticationError, @@ -121,6 +122,16 @@ def _policies( **kwargs: Any ) -> List[Any]: auth_policy = _get_authentication_policy(credential) + # Merge any caller-supplied blocked redirect headers with the mandatory + # Event Grid credential headers, and remove the key from kwargs so it is + # not passed twice to SensitiveHeaderCleanupPolicy. + blocked_redirect_headers = [ + "Authorization", + "x-ms-authorization-auxiliary", + "aeg-sas-key", + "aeg-sas-token", + *kwargs.pop("blocked_redirect_headers", []), + ] sdk_moniker = "eventgrid/{}".format(VERSION) policies = [ RequestIdPolicy(**kwargs), @@ -131,6 +142,14 @@ def _policies( RedirectPolicy(**kwargs), RetryPolicy(**kwargs), auth_policy, + # Strip credential headers on cross-host redirects to avoid leaking them to + # a redirect target. `SensitiveHeaderCleanupPolicy` already covers + # `Authorization` and `x-ms-authorization-auxiliary` by default; the Event + # Grid SAS headers (`aeg-sas-key`/`aeg-sas-token`) are added here as well. + SensitiveHeaderCleanupPolicy( + blocked_redirect_headers=blocked_redirect_headers, + **kwargs, + ), CustomHookPolicy(**kwargs), NetworkTraceLoggingPolicy(**kwargs), DistributedTracingPolicy(**kwargs), diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/aio/_publisher_client_async.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/aio/_publisher_client_async.py index 3508dcca1ead..7f4ca0b215f7 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/aio/_publisher_client_async.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_legacy/aio/_publisher_client_async.py @@ -23,6 +23,7 @@ HttpLoggingPolicy, UserAgentPolicy, AsyncBearerTokenCredentialPolicy, + SensitiveHeaderCleanupPolicy, ) from azure.core.exceptions import ( ClientAuthenticationError, @@ -115,6 +116,16 @@ def _policies( credential: Union[AzureKeyCredential, AzureSasCredential, "AsyncTokenCredential"], **kwargs: Any ) -> List[Any]: auth_policy = _get_authentication_policy(credential, AsyncBearerTokenCredentialPolicy) + # Merge any caller-supplied blocked redirect headers with the mandatory + # Event Grid credential headers, and remove the key from kwargs so it is + # not passed twice to SensitiveHeaderCleanupPolicy. + blocked_redirect_headers = [ + "Authorization", + "x-ms-authorization-auxiliary", + "aeg-sas-key", + "aeg-sas-token", + *kwargs.pop("blocked_redirect_headers", []), + ] sdk_moniker = "eventgridpublisherclient/{}".format(VERSION) policies = [ RequestIdPolicy(**kwargs), @@ -125,6 +136,14 @@ def _policies( AsyncRedirectPolicy(**kwargs), AsyncRetryPolicy(**kwargs), auth_policy, + # Strip credential headers on cross-host redirects to avoid leaking them to + # a redirect target. `SensitiveHeaderCleanupPolicy` already covers + # `Authorization` and `x-ms-authorization-auxiliary` by default; the Event + # Grid SAS headers (`aeg-sas-key`/`aeg-sas-token`) are added here as well. + SensitiveHeaderCleanupPolicy( + blocked_redirect_headers=blocked_redirect_headers, + **kwargs, + ), CustomHookPolicy(**kwargs), NetworkTraceLoggingPolicy(**kwargs), DistributedTracingPolicy(**kwargs), diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_model_base.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_model_base.py index ff8dc9bc9eaa..c5e103af515e 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_model_base.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_model_base.py @@ -774,16 +774,16 @@ def _get_deserialize_callable_from_annotation( # pylint: disable=too-many-retur # is it optional? try: - if any(a for a in annotation.__args__ if a == type(None)): # pyright: ignore + if any(a for a in annotation.__args__ if a == type(None)): # pyright: ignore # pylint: disable=unidiomatic-typecheck if len(annotation.__args__) <= 2: # pyright: ignore if_obj_deserializer = _get_deserialize_callable_from_annotation( - next(a for a in annotation.__args__ if a != type(None)), module, rf # pyright: ignore + next(a for a in annotation.__args__ if a != type(None)), module, rf # pyright: ignore # pylint: disable=unidiomatic-typecheck ) return functools.partial(_deserialize_with_optional, if_obj_deserializer) # the type is Optional[Union[...]], we need to remove the None type from the Union annotation_copy = copy.copy(annotation) - annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a != type(None)] # pyright: ignore + annotation_copy.__args__ = [a for a in annotation_copy.__args__ if a != type(None)] # pyright: ignore # pylint: disable=unidiomatic-typecheck return _get_deserialize_callable_from_annotation(annotation_copy, module, rf) except AttributeError: pass diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_serialization.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_serialization.py index b24ab2885450..f51a7fcca715 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_serialization.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_serialization.py @@ -189,7 +189,7 @@ def deserialize_from_http_generics(cls, body_bytes: Optional[Union[AnyStr, IO]], class UTC(datetime.tzinfo): """Time Zone info for handling UTC""" - def utcoffset(self, dt): + def utcoffset(self, dt): # pylint: disable=unused-argument """UTF offset for UTC is 0. :param datetime.datetime dt: The datetime @@ -198,7 +198,7 @@ def utcoffset(self, dt): """ return datetime.timedelta(0) - def tzname(self, dt): + def tzname(self, dt): # pylint: disable=unused-argument """Timestamp representation. :param datetime.datetime dt: The datetime @@ -207,7 +207,7 @@ def tzname(self, dt): """ return "Z" - def dst(self, dt): + def dst(self, dt): # pylint: disable=unused-argument """No daylight saving for UTC. :param datetime.datetime dt: The datetime @@ -230,16 +230,16 @@ class _FixedOffset(datetime.tzinfo): # type: ignore def __init__(self, offset) -> None: self.__offset = offset - def utcoffset(self, dt): + def utcoffset(self, dt): # pylint: disable=unused-argument return self.__offset - def tzname(self, dt): + def tzname(self, dt): # pylint: disable=unused-argument return str(self.__offset.total_seconds() / 3600) def __repr__(self): return "".format(self.tzname(None)) - def dst(self, dt): + def dst(self, dt): # pylint: disable=unused-argument return datetime.timedelta(0) def __getinitargs__(self): diff --git a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_version.py b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_version.py index 215940119a6c..89d89e29c8ae 100644 --- a/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_version.py +++ b/sdk/eventgrid/azure-eventgrid/azure/eventgrid/_version.py @@ -6,4 +6,4 @@ # Changes may cause incorrect behavior and will be lost if the code is regenerated. # -------------------------------------------------------------------------- -VERSION = "4.22.0" +VERSION = "4.22.1" diff --git a/sdk/eventgrid/azure-eventgrid/dev_requirements.txt b/sdk/eventgrid/azure-eventgrid/dev_requirements.txt index db0eac072b34..ef0b72206b8f 100644 --- a/sdk/eventgrid/azure-eventgrid/dev_requirements.txt +++ b/sdk/eventgrid/azure-eventgrid/dev_requirements.txt @@ -4,5 +4,9 @@ azure-mgmt-resource==22.0.0 ../azure-mgmt-eventgrid azure-storage-queue -cloudevents<=2.0.0; python_version >= "3.7" +# Pin to cloudevents 1.x: 2.0.0 is a backwards-incompatible rewrite that removed the +# `cloudevents.http` module the CNCF tests/samples import. This is a test-only dependency +# (not in install_requires); the `<=2.0.0` pin was a latent issue that broke once 2.0.0 +# shipped. TODO: migrate the CNCF tests/samples to the cloudevents 2.x API and relax this pin. +cloudevents<2.0.0; python_version >= "3.7" aiohttp>=3.0 diff --git a/sdk/eventgrid/azure-eventgrid/setup.py b/sdk/eventgrid/azure-eventgrid/setup.py index b88a566aff72..9ce09970b80e 100644 --- a/sdk/eventgrid/azure-eventgrid/setup.py +++ b/sdk/eventgrid/azure-eventgrid/setup.py @@ -42,11 +42,11 @@ "Programming Language :: Python", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "License :: OSI Approved :: MIT License", ], zip_safe=False, @@ -63,8 +63,8 @@ }, install_requires=[ "isodate>=0.6.1", - "azure-core>=1.30.0", + "azure-core>=1.38.3", "typing-extensions>=4.6.0", ], - python_requires=">=3.8", + python_requires=">=3.10", ) diff --git a/sdk/eventgrid/azure-eventgrid/tests/test_eg_redirect.py b/sdk/eventgrid/azure-eventgrid/tests/test_eg_redirect.py new file mode 100644 index 000000000000..1ecc1a44b476 --- /dev/null +++ b/sdk/eventgrid/azure-eventgrid/tests/test_eg_redirect.py @@ -0,0 +1,244 @@ +# -------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See License.txt in the project root for +# license information. +# -------------------------------------------------------------------------- +"""Regression tests for the cross-host redirect credential-leak fix (MSRC 126697). + +The legacy ``EventGridPublisherClient`` must not re-send credential headers to a +different host when following an HTTP 3xx redirect. These tests drive the client +pipeline built by ``_policies`` through a mock transport that issues a cross-host +redirect and assert the credential header is stripped from the redirected request. +""" +import time + +import pytest +from requests import Response + +from azure.core.credentials import AzureKeyCredential, AzureSasCredential, AccessToken +from azure.core.pipeline import Pipeline, AsyncPipeline +from azure.core.pipeline.transport import HttpTransport, AsyncHttpTransport, HttpRequest + +from azure.eventgrid._legacy._publisher_client import EventGridPublisherClient +from azure.eventgrid._legacy.aio._publisher_client_async import ( + EventGridPublisherClient as EventGridPublisherClientAsync, +) + +ORIGINAL_URL = "https://topic.westus-1.eventgrid.azure.net/api/events" +REDIRECT_URL = "https://redirected.example.net/api/events" + + +class FakeTokenCredential(object): + def get_token(self, *scopes, **kwargs): + return AccessToken("fake-token", int(time.time()) + 3600) + + +class AsyncFakeTokenCredential(object): + async def get_token(self, *scopes, **kwargs): + return AccessToken("fake-token", int(time.time()) + 3600) + + +def _redirect_response(): + # 307/308 preserve the original method (POST) and are followed by azure-core's + # RedirectPolicy; a 301/302 would only be followed for GET/HEAD, so a POST publish + # is exposed to the cross-host leak specifically via a method-preserving redirect. + response = Response() + response.status_code = 307 + response.headers["location"] = REDIRECT_URL + return response + + +def _ok_response(): + response = Response() + response.status_code = 200 + return response + + +CREDENTIAL_CASES = [ + (AzureKeyCredential("my-secret-key"), "aeg-sas-key", "my-secret-key"), + (AzureSasCredential("my-sas-token"), "aeg-sas-token", "my-sas-token"), + (FakeTokenCredential(), "Authorization", "Bearer fake-token"), +] + +# The async client wraps a `TokenCredential` in `AsyncBearerTokenCredentialPolicy` +# which awaits `get_token`, so the async AAD case needs an async credential. +ASYNC_CREDENTIAL_CASES = [ + (AzureKeyCredential("my-secret-key"), "aeg-sas-key", "my-secret-key"), + (AzureSasCredential("my-sas-token"), "aeg-sas-token", "my-sas-token"), + (AsyncFakeTokenCredential(), "Authorization", "Bearer fake-token"), +] + + +@pytest.mark.parametrize("credential, cred_header, cred_value", CREDENTIAL_CASES) +def test_credentials_not_leaked_on_cross_host_redirect(credential, cred_header, cred_value): + class MockTransport(HttpTransport): + def __init__(self): + self.first = True + self.redirected_headers = None + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def close(self): + pass + + def open(self): + pass + + def send(self, request, **kwargs): + if self.first: + self.first = False + # The credential header is present on the original (in-host) request. + assert request.headers.get(cred_header) == cred_value + return _redirect_response() + # After following the cross-host redirect the credential must be gone. + self.redirected_headers = dict(request.headers) + return _ok_response() + + transport = MockTransport() + pipeline = Pipeline(transport=transport, policies=EventGridPublisherClient._policies(credential)) + pipeline.run(HttpRequest("POST", ORIGINAL_URL)) + + assert transport.redirected_headers is not None, "cross-host redirect was not followed" + assert not transport.redirected_headers.get(cred_header) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("credential, cred_header, cred_value", ASYNC_CREDENTIAL_CASES) +async def test_credentials_not_leaked_on_cross_host_redirect_async(credential, cred_header, cred_value): + class MockAsyncTransport(AsyncHttpTransport): + def __init__(self): + self.first = True + self.redirected_headers = None + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def close(self): + pass + + async def open(self): + pass + + async def send(self, request, **kwargs): + if self.first: + self.first = False + assert request.headers.get(cred_header) == cred_value + return _redirect_response() + self.redirected_headers = dict(request.headers) + return _ok_response() + + transport = MockAsyncTransport() + pipeline = AsyncPipeline( + transport=transport, policies=EventGridPublisherClientAsync._policies(credential) + ) + await pipeline.run(HttpRequest("POST", ORIGINAL_URL)) + + assert transport.redirected_headers is not None, "cross-host redirect was not followed" + assert not transport.redirected_headers.get(cred_header) + + +@pytest.mark.parametrize("credential, cred_header, cred_value", CREDENTIAL_CASES) +def test_credentials_not_leaked_on_redirect_then_retry(credential, cred_header, cred_value): + """A retry after a cross-host redirect (307 -> 503 -> 200) must not re-leak the credential. + + Requires azure-core >= 1.38.3, which persists the ``insecure_domain_change`` flag across + retries so the cleanup keeps stripping the credential re-added by the auth policy. + """ + + class MockTransport(HttpTransport): + def __init__(self): + self.calls = 0 + self.post_redirect_headers = [] + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def close(self): + pass + + def open(self): + pass + + def send(self, request, **kwargs): + self.calls += 1 + if self.calls == 1: + assert request.headers.get(cred_header) == cred_value + return _redirect_response() + # Every request to the redirected host - including the retry - must be clean. + self.post_redirect_headers.append(request.headers.get(cred_header)) + if self.calls == 2: + retryable = Response() + retryable.status_code = 503 + # ``Retry-After: 0`` forces a deterministic retry across all supported + # azure-core versions (independent of method-based retry heuristics) and + # avoids any real sleep in the test. + retryable.headers["Retry-After"] = "0" + return retryable + return _ok_response() + + transport = MockTransport() + policies = EventGridPublisherClient._policies(credential, retry_backoff_factor=0) + pipeline = Pipeline(transport=transport, policies=policies) + pipeline.run(HttpRequest("POST", ORIGINAL_URL)) + + assert transport.calls >= 3, "expected a redirect followed by a retry" + assert not any(transport.post_redirect_headers) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("credential, cred_header, cred_value", ASYNC_CREDENTIAL_CASES) +async def test_credentials_not_leaked_on_redirect_then_retry_async(credential, cred_header, cred_value): + """Async counterpart: a retry after a cross-host redirect (307 -> 503 -> 200) must not re-leak the credential. + + Requires azure-core >= 1.38.3, which persists the ``insecure_domain_change`` flag across + retries so the cleanup keeps stripping the credential re-added by the async auth policy. + """ + + class MockAsyncTransport(AsyncHttpTransport): + def __init__(self): + self.calls = 0 + self.post_redirect_headers = [] + + async def __aenter__(self): + return self + + async def __aexit__(self, *args): + pass + + async def close(self): + pass + + async def open(self): + pass + + async def send(self, request, **kwargs): + self.calls += 1 + if self.calls == 1: + assert request.headers.get(cred_header) == cred_value + return _redirect_response() + # Every request to the redirected host - including the retry - must be clean. + self.post_redirect_headers.append(request.headers.get(cred_header)) + if self.calls == 2: + retryable = Response() + retryable.status_code = 503 + retryable.headers["Retry-After"] = "0" + return retryable + return _ok_response() + + transport = MockAsyncTransport() + policies = EventGridPublisherClientAsync._policies(credential, retry_backoff_factor=0) + pipeline = AsyncPipeline(transport=transport, policies=policies) + await pipeline.run(HttpRequest("POST", ORIGINAL_URL)) + + assert transport.calls >= 3, "expected a redirect followed by a retry" + assert not any(transport.post_redirect_headers)