-
Notifications
You must be signed in to change notification settings - Fork 0
fix(api): surface subscription deletion errors to users #11
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: cursor_only-issues-20260113-cursor_completion_base_fixapi_surface_subscription_deletion_errors_to_users_pr181
Are you sure you want to change the base?
Changes from all commits
b61fd8f
9e990c5
1ddaece
16aa925
b6b7ff0
630b9d0
8c8c79b
bf431fb
8295134
8a7d997
fa2f27b
5489175
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -853,7 +853,7 @@ def rebuild_trigger_subscription( | |
| """ | ||
| Create a subscription builder for rebuilding an existing subscription. | ||
|
|
||
| This method creates a builder pre-filled with data from the rebuild request, | ||
| This method rebuild the subscription by call DELETE and CREATE API of the third party provider(e.g. GitHub) | ||
| keeping the same subscription_id and endpoint_id so the webhook URL remains unchanged. | ||
|
|
||
| :param tenant_id: Tenant ID | ||
|
|
@@ -868,111 +868,44 @@ def rebuild_trigger_subscription( | |
| if not provider_controller: | ||
| raise ValueError(f"Provider {provider_id} not found") | ||
|
|
||
| # Use distributed lock to prevent race conditions on the same subscription | ||
| lock_key = f"trigger_subscription_rebuild_lock:{tenant_id}_{subscription_id}" | ||
| with redis_client.lock(lock_key, timeout=20): | ||
| with Session(db.engine, expire_on_commit=False) as session: | ||
| try: | ||
| # Get subscription within the transaction | ||
| subscription: TriggerSubscription | None = ( | ||
| session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first() | ||
| ) | ||
| if not subscription: | ||
| raise ValueError(f"Subscription {subscription_id} not found") | ||
|
|
||
| credential_type = CredentialType.of(subscription.credential_type) | ||
| if credential_type not in [CredentialType.OAUTH2, CredentialType.API_KEY]: | ||
| raise ValueError("Credential type not supported for rebuild") | ||
|
|
||
| # Decrypt existing credentials for merging | ||
| credential_encrypter, _ = create_trigger_provider_encrypter_for_subscription( | ||
| tenant_id=tenant_id, | ||
| controller=provider_controller, | ||
| subscription=subscription, | ||
| ) | ||
| decrypted_credentials = dict(credential_encrypter.decrypt(subscription.credentials)) | ||
|
|
||
| # Merge credentials: if caller passed HIDDEN_VALUE, retain existing decrypted value | ||
| merged_credentials: dict[str, Any] = { | ||
| key: value if value != HIDDEN_VALUE else decrypted_credentials.get(key, UNKNOWN_VALUE) | ||
| for key, value in credentials.items() | ||
| } | ||
|
|
||
| user_id = subscription.user_id | ||
|
|
||
| # TODO: Trying to invoke update api of the plugin trigger provider | ||
|
|
||
| # FALLBACK: If the update api is not implemented, | ||
| # delete the previous subscription and create a new one | ||
|
|
||
| # Unsubscribe the previous subscription (external call, but we'll handle errors) | ||
| try: | ||
| TriggerManager.unsubscribe_trigger( | ||
| tenant_id=tenant_id, | ||
| user_id=user_id, | ||
| provider_id=provider_id, | ||
| subscription=subscription.to_entity(), | ||
| credentials=decrypted_credentials, | ||
| credential_type=credential_type, | ||
| ) | ||
| except Exception as e: | ||
| logger.exception("Error unsubscribing trigger during rebuild", exc_info=e) | ||
| # Continue anyway - the subscription might already be deleted externally | ||
|
|
||
| # Create a new subscription with the same subscription_id and endpoint_id (external call) | ||
| new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger( | ||
| tenant_id=tenant_id, | ||
| user_id=user_id, | ||
| provider_id=provider_id, | ||
| endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id), | ||
| parameters=parameters, | ||
| credentials=merged_credentials, | ||
| credential_type=credential_type, | ||
| ) | ||
|
|
||
| # Update the subscription in the same transaction | ||
| # Inline update logic to reuse the same session | ||
| if name is not None and name != subscription.name: | ||
| existing = ( | ||
| session.query(TriggerSubscription) | ||
| .filter_by(tenant_id=tenant_id, provider_id=str(provider_id), name=name) | ||
| .first() | ||
| ) | ||
| if existing and existing.id != subscription.id: | ||
| raise ValueError(f"Subscription name '{name}' already exists for this provider") | ||
| subscription.name = name | ||
|
|
||
| # Update parameters | ||
| subscription.parameters = dict(parameters) | ||
|
|
||
| # Update credentials with merged (and encrypted) values | ||
| subscription.credentials = dict(credential_encrypter.encrypt(merged_credentials)) | ||
|
|
||
| # Update properties | ||
| if new_subscription.properties: | ||
| properties_encrypter, _ = create_provider_encrypter( | ||
| tenant_id=tenant_id, | ||
| config=provider_controller.get_properties_schema(), | ||
| cache=NoOpProviderCredentialCache(), | ||
| ) | ||
| subscription.properties = dict(properties_encrypter.encrypt(dict(new_subscription.properties))) | ||
|
|
||
| # Update expiration timestamp | ||
| if new_subscription.expires_at is not None: | ||
| subscription.expires_at = new_subscription.expires_at | ||
| subscription = TriggerProviderService.get_subscription_by_id( | ||
| tenant_id=tenant_id, | ||
| subscription_id=subscription_id, | ||
| ) | ||
| if not subscription: | ||
| raise ValueError(f"Subscription {subscription_id} not found") | ||
|
|
||
| # Commit the transaction | ||
| session.commit() | ||
| credential_type = CredentialType.of(subscription.credential_type) | ||
| if credential_type not in {CredentialType.OAUTH2, CredentialType.API_KEY}: | ||
| raise ValueError(f"Credential type {credential_type} not supported for auto creation") | ||
|
|
||
| # Clear subscription cache | ||
| delete_cache_for_subscription( | ||
| tenant_id=tenant_id, | ||
| provider_id=subscription.provider_id, | ||
| subscription_id=subscription.id, | ||
| ) | ||
| # Delete the previous subscription | ||
| user_id = subscription.user_id | ||
| TriggerManager.unsubscribe_trigger( | ||
| tenant_id=tenant_id, | ||
| user_id=user_id, | ||
| provider_id=provider_id, | ||
| subscription=subscription.to_entity(), | ||
| credentials=subscription.credentials, | ||
| credential_type=credential_type, | ||
| ) | ||
|
|
||
| except Exception as e: | ||
| # Rollback on any error | ||
| session.rollback() | ||
| logger.exception("Failed to rebuild trigger subscription", exc_info=e) | ||
| raise | ||
| # Create a new subscription with the same subscription_id and endpoint_id | ||
| new_subscription: TriggerSubscriptionEntity = TriggerManager.subscribe_trigger( | ||
| tenant_id=tenant_id, | ||
| user_id=user_id, | ||
| provider_id=provider_id, | ||
| endpoint=generate_plugin_trigger_endpoint_url(subscription.endpoint_id), | ||
| parameters=parameters, | ||
| credentials=credentials, | ||
| credential_type=credential_type, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. HIDDEN_VALUE credentials sent to third-party APIsHigh Severity The refactored Additional Locations (1) |
||
| ) | ||
| TriggerProviderService.update_trigger_subscription( | ||
| tenant_id=tenant_id, | ||
| subscription_id=subscription.id, | ||
| name=name, | ||
| parameters=parameters, | ||
| credentials=credentials, | ||
| properties=new_subscription.properties, | ||
| expires_at=new_subscription.expires_at, | ||
| ) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Concurrent rebuilds cause race conditions without distributed lockMedium Severity The refactored |
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UNAUTHORIZED subscriptions cannot update properties anymore
Medium Severity
The
manually_createdvariable is computed but never used. The old code had a separate branch forCredentialType.UNAUTHORIZEDsubscriptions that allowed updating name and properties without rebuilding. The new code only handles the "rename only" case. When an UNAUTHORIZED subscription tries to update properties (with or without name), it falls through torebuild_trigger_subscription, which rejects UNAUTHORIZED credential types with an error. This breaks property updates for manually-created subscriptions.