Repository: permitio/opal
Issue: Bitbucket webhook secret is not matched correctly

**Describe the bug**
When sending a webhook from Bitbucket to the OPAL server with an secret configured then the the response on the request is an `401,  No secret was provided!`. This is unexpected as the configuration looks correct.

**To Reproduce**
1. run OPAL server with the following config:
OPAL_POLICY_REPO_WEBHOOK_SECRET=secret (in our case this is a UUID)
OPAL_POLICY_REPO_WEBHOOK_PARAMS={"secret_header_name":"x-hook-uuid","secret_type":"token","secret_parsing_regex":"(.*)","event_header_name":"X-Event-Key","push_event_value":"repo:push"}
OPAL_POLICY_REPO_URL=repo-url
2. send a webhook request to /webhook with the following headers:
--header 'X-Event-Key: repo:push' 
--header 'x-hook-uuid:  secret' 
3. response is 401 No secret was provided!

I have added some extra log statements in the code and I can see that it can find the header and does get the value from the header. Also in the step where it does the regex matching I see that it gets the secret correctly. It is then put on the first place of the matches array. But then this statement tries to get the second item of the array: `secret = matches[1] if len(matches) > 0 else None` which is empty. 

**Expected behavior**
I would expect that it would be able to match the secret in the webhook request with the secret that is configured in the server.

**Screenshots**
no screenshots

**OPAL version**
 - Version: 0.4.0


Changed files:
packages/opal-server/opal_server/policy/webhook/deps.py, packages/opal-server/opal_server/tests/policy_repo_webhook_test.py

diff --git a/packages/opal-server/opal_server/policy/webhook/deps.py b/packages/opal-server/opal_server/policy/webhook/deps.py
index ae3b3fa..99e8061 100644
--- a/packages/opal-server/opal_server/policy/webhook/deps.py
+++ b/packages/opal-server/opal_server/policy/webhook/deps.py
@@ -4,66 +4,81 @@ import re
 from typing import List, Optional
 
 from fastapi import Header, HTTPException, Request, status
-from opal_common.schemas.webhook import SecretTypeEnum
+from opal_common.schemas.webhook import GitWebhookRequestParams, SecretTypeEnum
 from opal_server.config import opal_server_config
 from pydantic import BaseModel
 
 
-async def validate_git_secret_or_throw(request: Request) -> bool:
-    """authenticates a request from a git service webhook system by checking
-    that the request contains a valid signature (i.e: via the secret stored on
-    github) or a valid token (as stored in Gitlab)."""
-    if opal_server_config.POLICY_REPO_WEBHOOK_SECRET is None:
-        # webhook can be configured without secret (not recommended but quite possible)
-        return True
+def validate_git_secret_or_throw_factory(
+    webhook_secret: Optional[str] = opal_server_config.POLICY_REPO_WEBHOOK_SECRET,
+    webhook_params: GitWebhookRequestParams = opal_server_config.POLICY_REPO_WEBHOOK_PARAMS,
+):
+    """Factory function to create secret validator dependency according to
+    config.
 
-    # get the secret the git service has sent us
-    secret = request.headers.get(
-        opal_server_config.POLICY_REPO_WEBHOOK_PARAMS.secret_header_name, ""
-    )
+    Returns: validate_git_secret_or_throw (async function)
 
-    # parse out the actual secret (Some services like Github add prefixes)
-    matches = re.findall(
-        opal_server_config.POLICY_REPO_WEBHOOK_PARAMS.secret_parsing_regex,
-        secret,
-    )
-    secret = matches[1] if len(matches) > 0 else None
+    Args:
+        webhook_secret (Optional[ str ], optional): The secret to validate. Defaults to opal_server_config.POLICY_REPO_WEBHOOK_SECRET.
+        webhook_params (GitWebhookRequestParams, optional):The webhook configuration - including how to parse the secret. Defaults to opal_server_config.POLICY_REPO_WEBHOOK_PARAMS.
+    """
 
-    # check we actually got something
-    if secret is None or len(secret) == 0:
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="No secret was provided!",
+    async def validate_git_secret_or_throw(request: Request) -> bool:
+        """authenticates a request from a git service webhook system by
+        checking that the request contains a valid signature (i.e: via the
+        secret stored on github) or a valid token (as stored in Gitlab)."""
+        if webhook_secret is None:
+            # webhook can be configured without secret (not recommended but quite possible)
+            return True
+
+        # get the secret the git service has sent us
+        incoming_secret = request.headers.get(webhook_params.secret_header_name, "")
+
+        # parse out the actual secret (Some services like Github add prefixes)
+        matches = re.findall(
+            webhook_params.secret_parsing_regex,
+            incoming_secret,
         )
+        incoming_secret = matches[0] if len(matches) > 0 else None
 
-    # Check secret as signature
-    if (
-        opal_server_config.POLICY_REPO_WEBHOOK_PARAMS.secret_type
-        == SecretTypeEnum.signature
-    ):
-        # calculate our signature on the post body
-        payload = await request.body()
-        our_signature = hmac.new(
-            opal_server_config.POLICY_REPO_WEBHOOK_SECRET.encode("utf-8"),
-            payload,
-            hashlib.sha256,
-        ).hexdigest()
-
-        # compare signatures on the post body
-        provided_signature = secret
-        if not hmac.compare_digest(our_signature, provided_signature):
+        # check we actually got something
+        if incoming_secret is None or len(incoming_secret) == 0:
             raise HTTPException(
                 status_code=status.HTTP_401_UNAUTHORIZED,
-                detail="signatures didn't match!",
+                detail="No secret was provided!",
             )
-    # Check secret as token
-    elif secret != opal_server_config.POLICY_REPO_WEBHOOK_SECRET.encode("utf-8"):
-        raise HTTPException(
-            status_code=status.HTTP_401_UNAUTHORIZED,
-            detail="secret-tokens didn't match!",
-        )
 
-    return True
+        # Check secret as signature
+        if webhook_params.secret_type == SecretTypeEnum.signature:
+            # calculate our signature on the post body
+            payload = await request.body()
+            our_signature = hmac.new(
+                webhook_secret.encode("utf-8"),
+                payload,
+                hashlib.sha256,
+            ).hexdigest()
+
+            # compare signatures on the post body
+            provided_signature = incoming_secret
+            if not hmac.compare_digest(our_signature, provided_signature):
+                raise HTTPException(
+                    status_code=status.HTTP_401_UNAUTHORIZED,
+                    detail="signatures didn't match!",
+                )
+        # Check secret as token
+        elif incoming_secret.encode("utf-8") != webhook_secret.encode("utf-8"):
+            raise HTTPException(
+                status_code=status.HTTP_401_UNAUTHORIZED,
+                detail="secret-tokens didn't match!",
+            )
+
+        return True
+
+    return validate_git_secret_or_throw
+
+
+# Init with defaults
+validate_git_secret_or_throw = validate_git_secret_or_throw_factory()
 
 
 class GitChanges(BaseModel):
diff --git a/packages/opal-server/opal_server/tests/policy_repo_webhook_test.py b/packages/opal-server/opal_server/tests/policy_repo_webhook_test.py
index c658a9f..2138a2c 100644
--- a/packages/opal-server/opal_server/tests/policy_repo_webhook_test.py
+++ b/packages/opal-server/opal_server/tests/policy_repo_webhook_test.py
@@ -14,7 +14,10 @@ from fastapi_websocket_pubsub import PubSubClient
 from flaky import flaky
 from opal_common.schemas.webhook import GitWebhookRequestParams
 from opal_server.policy.webhook.api import get_webhook_router, is_matching_webhook_url
-from opal_server.policy.webhook.deps import extracted_git_changes
+from opal_server.policy.webhook.deps import (
+    extracted_git_changes,
+    validate_git_secret_or_throw_factory,
+)
 
 # Add parent path to use local src as package for tests
 root_dir = os.path.abspath(
@@ -42,7 +45,7 @@ REPO_FULL_NAME = "permitio/opal"
 REPO_URL = f"https://github.com/{REPO_FULL_NAME}"
 
 # configure the server to work with a fake secret and our mock repository
-opal_server_config.POLICY_REPO_WEBHOOK_SECRET = "SECRET"
+SECRET = opal_server_config.POLICY_REPO_WEBHOOK_SECRET = "SECRET"
 opal_server_config.POLICY_REPO_URL = REPO_URL
 
 
@@ -59,13 +62,13 @@ GITHUB_WEBHOOK_BODY_SAMPLE = {
     },
 }
 SIGNATURE = hmac.new(
-    opal_server_config.POLICY_REPO_WEBHOOK_SECRET.encode("utf-8"),
+    SECRET.encode("utf-8"),
     json.dumps(GITHUB_WEBHOOK_BODY_SAMPLE).encode("utf-8"),
     hashlib.sha256,
 ).hexdigest()
 GITHUB_WEBHOOK_HEADERS = {
     "X-GitHub-Event": "push",
-    "X-Hub-Signature-256": SIGNATURE,
+    "X-Hub-Signature-256": f"sha256={SIGNATURE}",
 }
 
 #######
@@ -130,7 +133,7 @@ GITLAB_WEBHOOK_BODY_SAMPLE = {
 }
 GITLAB_WEBHOOK_HEADERS = {
     "X-Gitlab-Event": "Push Hook",
-    "X-Gitlab-Token": opal_server_config.POLICY_REPO_WEBHOOK_SECRET,
+    "X-Gitlab-Token": SECRET,
 }
 
 #######
@@ -191,7 +194,7 @@ AZURE_GIT_WEBHOOK_BODY_SAMPLE = {
     "createdDate": "2022-12-15T17:28:23.1937259Z",
 }
 AZURE_GIT_WEBHOOK_HEADERS = {
-    "x-api-key": opal_server_config.POLICY_REPO_WEBHOOK_SECRET,
+    "x-api-key": SECRET,
 }
 
 #######
@@ -208,7 +211,7 @@ BITBUCKET_WEBHOOK_BODY_SAMPLE = {
 
 BITBUCKET_WEBHOOK_HEADERS = {
     "x-event-key": "repo:push",
-    "x-hook-uuid": opal_server_config.POLICY_REPO_WEBHOOK_SECRET,
+    "x-hook-uuid": SECRET,
 }
 
 
@@ -227,8 +230,12 @@ def setup_server(event, webhook_config):
     async def publish(event):
         events.append(event)
 
+    validate_git_secret_or_throw = validate_git_secret_or_throw_factory(
+        SECRET, webhook_config
+    )
+
     webhook_router = get_webhook_router(
-        None,
+        [Depends(validate_git_secret_or_throw)],
         Depends(extracted_git_changes),
         PolicySourceTypes.Git,
         publish,
@@ -253,7 +260,11 @@ def setup_server(event, webhook_config):
 def github_mode_server():
     event = Event()
     # Run the server as a separate process
-    proc = Process(target=setup_server, args=(event, None), daemon=True)
+    proc = Process(
+        target=setup_server,
+        args=(event, opal_server_config.POLICY_REPO_WEBHOOK_PARAMS),
+        daemon=True,
+    )
     proc.start()
     yield event
     proc.kill()  # Cleanup after test