microsoft / PowerPlatformConnectors

This is a repository for Microsoft Power Automate, Power Apps, and Azure Logic Apps connectors
https://aka.ms/connectors
MIT License
995 stars 1.27k forks source link

[BUG] Api Key not being sent when deleting webhook #3678

Open BladeMF opened 1 month ago

BladeMF commented 1 month ago

Type of Connector

Custom Connector

Name of Connector

Everhour

Describe the bug

I have create a custom connector for Everhour. It works fine, but they don't include the Location header in their response to the request creating the webhook. So I added custom code to my connector adding the header:

public class Script : ScriptBase
{
public override async Task<HttpResponseMessage> ExecuteAsync()
    {
        HttpResponseMessage response = await this.Context.SendAsync(this.Context.Request, this.CancellationToken).ConfigureAwait(continueOnCapturedContext: false);
        if (response.IsSuccessStatusCode)
        {
            if (this.Context.OperationId == "Trigger")
            {
                return await this.AddLocationHeader(response).ConfigureAwait(false);
            }
        }
        return response;
    }
    private async Task<HttpResponseMessage> AddLocationHeader(HttpResponseMessage response)
    {
        var responseString = await response.Content.ReadAsStringAsync().ConfigureAwait(continueOnCapturedContext: false);
        var result = JObject.Parse(responseString);
        response.Headers.Add("Location", "https://private-anon-d7b81b023f-everhour.apiary-mock.com/hooks/" + result["id"]);
        return response;
    }
}

So now the connector runs that URL when the flow is turned off. The problem is that no authentication headers are sent. The delete action is defined in the connector, but the deletion seems to ignore the definition (you can see I even added a random header just to make sure it is sent, but it isn't):

  /hooks/{hook_id}:
    delete:
      responses:
        '204':
          description: Success
          schema: {}
      summary: Delete a Webhook
      description: Deletes a webhook given an id
      operationId: DeleteWebhook
      x-ms-visibility: internal
      parameters:
        - $ref: '#/parameters/Content-type'
        - name: hook_id
          in: path
          required: true
          type: integer
        - name: X-My-Header
          in: header
          type: string
          default: My value

I'd appreciate any help here. I know what the headers are not sent, because their API offers a mock address that displays the calls. The delete call looks like this:

host: private-anon-d7b81b023f-everhour.apiary-mock.com
x-real-ip: 2.18.26.80
content-length: 0
x-ms-trigger-type: openapiconnectionwebhook
accept-language: en-us
x-ms-workflow-id: a342b9ba4f754909a4e476fd7d3094bb
x-ms-workflow-version: 08584735468993414396
x-ms-workflow-name: d666f7fd-f1f0-4d3a-a471-00077a8a487b
x-ms-workflow-system-id: /locations/uksouth/scaleunits/prod-14/workflows/a342b9ba4f754909a4e476fd7d3094bb
x-ms-workflow-run-id: 08584735468990753370210395964cu00
x-ms-workflow-operation-name: trigger
x-ms-execution-location: uksouth
x-ms-workflow-subscription-id: b1b36c61-d929-4258-8ace-6d31523e9820
x-ms-workflow-resourcegroup-name: 21c205cc335d4b0fb444ff527de57ab6-1c51a2c5741d41fe8e6f5ae93dc0bc90
x-ms-tracking-id: 72a6d878-283d-4ef3-b782-56b74e71f6f5
x-ms-correlation-id: 72a6d878-283d-4ef3-b782-56b74e71f6f5
x-ms-client-request-id: 72a6d878-283d-4ef3-b782-56b74e71f6f5
user-agent: azure-logic-apps/1.0 (workflow a342b9ba4f754909a4e476fd7d3094bb; version 08584735468993414396) microsoft-flow/1.0
x-ms-activity-vector: 00.01.in.0b.in.1b.in.03.in.1p
x-akamai-config-log-detail: true
accept-encoding: gzip
akamai-origin-hop: 1
via: 1.1 akamai.net(ghost) (akamaighost)
pragma: no-cache
cache-control: no-cache, max-age=0
akamai-grn: 0.501a1202.1728059986.171cd967
opc-request-id: gen_cd32fd0b-b143-4a21-8153-7cb859ec7f26

The operation "Trigger" definition is:

  /hooks:
    x-ms-notification-content:
      description: Default response
      schema: {}
    post:
      responses:
        '201':
          description: Default
      summary: Triggers
      operationId: Trigger
      x-ms-trigger: single
      parameters:
        - $ref: '#/parameters/Content-type'
        - name: body
          in: body
          required: true
          schema:
            type: object
            properties:
              targetUrl:
                type: string
                description: targetUrl
                x-ms-notification-url: true
                x-ms-visibility: internal
                title: ''
              events:
                type: array
                items:
                  type: string
                description: events
              project:
                type: string
                description: project
            required:
              - targetUrl
      description: Any trigger

Is this a security bug?

No, this is not a security bug

What is the severity of this bug?

Severity 2 - One or more important connector features are down

To Reproduce

Turn off the flow using the connector.

Expected behavior

The authentication header is sent with the delete requests.

Environment summary

Web.

Additional context

No additional context.

troystaylor commented 1 month ago

For your delete action, what is 'X-My-Header'? You have not included what the Security setting is for your connector - is it set like this?: image

BladeMF commented 1 month ago

X-My-Header is a test to see if that definition was being used when calling the delete URL. It's not sent. Here is the security:

securityDefinitions:
  API Key:
    type: apiKey
    in: header
    name: X-Api-Key
security:
  - API Key: []
BladeMF commented 1 month ago

I can paste the whole connector if needed.

troystaylor commented 1 month ago

Can you tell me how you are getting that Location URL? I've been able to create the trigger: image And create a flow with the When An HTTP Request Is Received trigger, parse the header, but my HTTP action back to https://api.everhour.com/hooks times out: image

BladeMF commented 1 month ago

Oh sorry, this is a specific URL generated by the Apiary inspector:

image

Go to the inspector and it will give you the URL. Otherwise use https://api.everhour.com. Does that make sense?

troystaylor commented 1 month ago

I've sent an email to Everhour - if we get the respond to URL figured out, then this should work and I don't think you need to add the location.

BladeMF commented 1 month ago

What do you mean the respond url? It is always DELETE http://api.everhour.com/hooks/<hook-id>. Or am I misunderstanding something?

troystaylor commented 1 month ago

This the part that I can't get to work, which would be a POST back to them: image

BladeMF commented 1 month ago

I actually don't do this and it is working nonetheless. I assumed it was some sort of standard and you took care of it. I register hooks alright, I just can't get them to unregister. Here is the whole connector:

{
  "swagger": "2.0",
  "info": {
    "title": "Everhour",
    "description": "",
    "version": "1.0"
  },
  "host": "api.everhour.com",
  "basePath": "/",
  "schemes": [
    "https"
  ],
  "consumes": [],
  "produces": [],
  "paths": {
    "/projects": {
      "get": {
        "responses": {
          "default": {
            "description": "default",
            "schema": {
              "type": "array",
              "items": {
                "$ref": "#/definitions/Project"
              }
            }
          }
        },
        "summary": "Get projects",
        "description": "Gets the project list",
        "operationId": "GetProjects",
        "parameters": [
          {
            "$ref": "#/parameters/Content-type"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "type": "integer"
          },
          {
            "name": "query",
            "in": "query",
            "required": false,
            "type": "string"
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "type": "string",
            "enum": [
              "as",
              "ev",
              "b3",
              "b2",
              "pv",
              "gh",
              "in",
              "tr",
              "jr"
            ]
          }
        ]
      }
    },
    "/hooks": {
      "x-ms-notification-content": {
        "description": "Default response",
        "schema": {}
      },
      "post": {
        "responses": {
          "201": {
            "description": "Default"
          }
        },
        "summary": "Triggers",
        "operationId": "Trigger",
        "x-ms-trigger": "single",
        "parameters": [
          {
            "$ref": "#/parameters/Content-type"
          },
          {
            "name": "body",
            "in": "body",
            "required": true,
            "schema": {
              "type": "object",
              "properties": {
                "targetUrl": {
                  "type": "string",
                  "description": "targetUrl",
                  "x-ms-notification-url": true,
                  "x-ms-visibility": "internal",
                  "title": ""
                },
                "events": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  },
                  "description": "events"
                },
                "project": {
                  "type": "string",
                  "description": "project"
                }
              },
              "required": [
                "targetUrl"
              ]
            }
          }
        ],
        "description": "Any trigger"
      }
    },
    "/hooks/{hook_id}": {
      "delete": {
        "responses": {
          "204": {
            "description": "Success",
            "schema": {}
          }
        },
        "summary": "Delete a Webhook",
        "description": "Deletes a webhook given an id",
        "operationId": "DeleteWebhook",
        "x-ms-visibility": "internal",
        "parameters": [
          {
            "$ref": "#/parameters/Content-type"
          },
          {
            "name": "hook_id",
            "in": "path",
            "required": true,
            "type": "integer"
          },
          {
            "name": "X-My-Header",
            "in": "header",
            "type": "string",
            "default": "My value"
          }
        ]
      },
      "get": {
        "responses": {
          "default": {
            "description": "default",
            "schema": {
              "$ref": "#/definitions/Hook"
            }
          }
        },
        "summary": "Get webhook",
        "description": "Gets a webhook by id",
        "operationId": "GetWebhook",
        "parameters": [
          {
            "$ref": "#/parameters/Content-type"
          },
          {
            "name": "hook_id",
            "in": "path",
            "required": true,
            "type": "string"
          }
        ]
      }
    }
  },
  "definitions": {
    "Hook": {
      "type": "object",
      "properties": {
        "id": {
          "type": "integer",
          "format": "int32",
          "description": "id"
        },
        "targetUrl": {
          "type": "string",
          "description": "targetUrl"
        },
        "events": {
          "type": "array",
          "items": {
            "type": "string"
          },
          "description": "events"
        },
        "project": {
          "type": "string",
          "description": "project"
        },
        "isActive": {
          "type": "boolean",
          "description": "isActive"
        },
        "createdAt": {
          "type": "string",
          "description": "createdAt"
        },
        "lastUsedAt": {
          "type": "string",
          "description": "lastUsedAt"
        }
      }
    },
    "Project": {
      "type": "object",
      "properties": {
        "id": {
          "type": "string",
          "description": "id"
        },
        "name": {
          "type": "string",
          "description": "name"
        },
        "workspaceId": {
          "type": "string",
          "description": "workspaceId"
        },
        "workspaceName": {
          "type": "string",
          "description": "workspaceName"
        },
        "client": {
          "type": "integer",
          "format": "int32",
          "description": "client"
        },
        "type": {
          "type": "string",
          "description": "type"
        },
        "favorite": {
          "type": "boolean",
          "description": "favorite"
        },
        "users": {
          "type": "array",
          "items": {
            "type": "integer",
            "format": "int32"
          },
          "description": "users"
        },
        "billing": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "type"
            },
            "fee": {
              "type": "integer",
              "format": "int32",
              "description": "fee"
            }
          },
          "description": "billing"
        },
        "rate": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "type"
            },
            "rate": {
              "type": "integer",
              "format": "int32",
              "description": "rate"
            },
            "userRateOverrides": {
              "type": "object",
              "properties": {},
              "description": "userRateOverrides"
            },
            "userCostOverrides": {
              "type": "object",
              "properties": {},
              "description": "userCostOverrides"
            }
          },
          "description": "rate"
        },
        "budget": {
          "type": "object",
          "properties": {
            "type": {
              "type": "string",
              "description": "type"
            },
            "budget": {
              "type": "integer",
              "format": "int32",
              "description": "budget"
            },
            "progress": {
              "type": "integer",
              "format": "int32",
              "description": "progress"
            },
            "timeProgress": {
              "type": "integer",
              "format": "int32",
              "description": "timeProgress"
            },
            "expenseProgress": {
              "type": "integer",
              "format": "int32",
              "description": "expenseProgress"
            },
            "period": {
              "type": "string",
              "description": "period"
            },
            "appliedFrom": {
              "type": "string",
              "description": "appliedFrom"
            },
            "disallowOverbudget": {
              "type": "boolean",
              "description": "disallowOverbudget"
            },
            "excludeUnbillableTime": {
              "type": "boolean",
              "description": "excludeUnbillableTime"
            },
            "excludeExpenses": {
              "type": "boolean",
              "description": "excludeExpenses"
            },
            "showToUsers": {
              "type": "boolean",
              "description": "showToUsers"
            },
            "threshold": {
              "type": "integer",
              "format": "int32",
              "description": "threshold"
            }
          },
          "description": "budget"
        }
      }
    }
  },
  "parameters": {
    "Content-type": {
      "name": "Content-type",
      "in": "header",
      "type": "string",
      "default": "application/json"
    }
  },
  "responses": {},
  "securityDefinitions": {
    "API Key": {
      "type": "apiKey",
      "in": "header",
      "name": "X-Api-Key"
    }
  },
  "security": [
    {
      "API Key": []
    }
  ],
  "tags": []
}
troystaylor commented 1 month ago

I’m not sure why the custom code is needed then. If the location is a static URL, you could return it with a policy.

BladeMF commented 1 month ago

It's not static. I get the id from the response of the hook registration.

BladeMF commented 1 month ago

you could return it with a policy.

I am not sure how to do that. Can you explain what you mean?

BladeMF commented 1 month ago

In fact, I am new to connectors and while I beginning to get it, policies are still a blur to me :-) I will dig in the documentation, but it will certainly help if you can give an example of what you mean. I am switching to paconn now so I can use pagination, so that's my first bump with the policies.

BladeMF commented 1 month ago

I think I understood it. Like:

            {
                "templateId": "setheader",
                "title": "Set Location for webhook response",
                "parameters": {
                    "x-ms-apimTemplateParameter.name": "Location",
                    "x-ms-apimTemplateParameter.value": "https://api.everhour.com/hooks/{@body().id}",
                    "x-ms-apimTemplate-policySection": "Response"
                }
            },

Am I correct?