Skip to content

added authorization for executing main pyscript via callback #1967

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion kairon/async_callback/router/pyscript_callback.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,11 @@ async def execute_async_action_standalone(request: Request, token: str) -> BSRes
return await process_router_message(token, None, request.method, request)

@router.post("/main_pyscript/execute-python")
async def trigger_restricted_python(payload: PyscriptPayload):
async def trigger_restricted_python(
request: Request,
payload: PyscriptPayload
):
await CallbackAuthenticator.verify(request)
try:
result = CallbackUtility.main_pyscript_handler({
"source_code": payload.source_code,
Expand Down
18 changes: 16 additions & 2 deletions kairon/shared/actions/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
from ..admin.processor import Sysadmin
from ..cloud.utils import CloudUtility
from ..constants import KAIRON_USER_MSG_ENTITY, PluginTypes, EventClass
from ..data.constant import REQUEST_TIMESTAMP_HEADER, TASK_TYPE
from ..data.constant import REQUEST_TIMESTAMP_HEADER, TASK_TYPE, TOKEN_TYPE
from ..data.data_objects import Slots, KeyVault
from ..plugins.factory import PluginFactory
from ..rest_client import AioRestClient
Expand Down Expand Up @@ -691,14 +691,28 @@ def run_pyscript(source_code: Text, context: dict):
result = lambda_response["Payload"].get('body')
else:
callback_url=Utility.environment['async_callback_action']['pyscript']['url']
lifespan = Utility.environment['events']['executor']['dynamic_token_lifespan']
from ..auth import Authentication

claims = {"sub": "action-server", "callback": True}

token = Authentication.create_access_token(
data=claims,
token_type=TOKEN_TYPE.DYNAMIC.value,
token_expire=lifespan
)
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {token}"
}
resp = Utility.execute_http_request(
"POST",
http_url=callback_url,
request_body={
"source_code": source_code,
"predefined_objects": context
},
headers={"Content-Type": "application/json"}
headers=headers
)
if resp.get('statusCode') != 200:
raise ActionFailure(f'Pyscript evaluation failed: {resp}')
Expand Down
49 changes: 46 additions & 3 deletions tests/integration_test/callback_service_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,22 +661,65 @@ async def test_execute_python_success(mock_handler):
await app.start()
client = TestClient(app)

# Create a mock token with three segments
mock_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJnQUFBQUFCb0xjUi00QnRVM1FFOUdBRFdONVg0N29Yay03VDdxN2dOQks1dlFqM3VnQkoybnZlLVBrVGVTUzZYRm55dkRjM0RmMmNob2JUSFAyM0dDTnI3STZ2eWwyZ0tUYkF3eXFFNERKVDU1TWNsWmV5LWdSRGR4dG55TS01M1R0cjVQLU9iQ3lKTmpDcTFaa1NkRFFQNWk0eDZsN3R1c0E9PSIsInZlcnNpb24iOiIyLjAifQ.HEL3BSt1uO2uRfwwPTVV5UgYvwXsSoyERqZ9Gev2X-U"

mock_handler.return_value = {"output": "Execution successful", "success": True}

payload = {
"source_code": "bot_response=100",
"predefined_objects": {"x": 1}
}

# Simulate response from handler
mock_handler.return_value = {"output": "Execution successful", "success": True}
headers = {
"Authorization": f"Bearer {mock_token}"
}
Comment on lines +664 to +676
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Consider using a dynamically generated token instead of a hardcoded one.

The test uses a hardcoded JWT token which could be a security concern if it contains actual credentials or secrets. While this is a test file, it's better to generate tokens dynamically like you did in the test_execute_python_failure test.


🏁 Script executed:

#!/bin/bash
# Check if the token is decodable and what it contains
echo "Checking if token contains sensitive information:"
jq -R 'split(".") | .[0],.[1] | @base64d | fromjson' <<< "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJnQUFBQUFCb0xjUi00QnRVM1FFOUdBRFdONVg0N29Yay03VDdxN2dOQks1dlFqM3VnQkoybnZlLVBrVGVTUzZYRm55dkRjM0RmMmNob2JUSFAyM0dDTnI3STZ2eWwyZ0tUYkF3eXFFNERKVDU1TWNsWmV5LWdSRGR4dG55TS01M1R0cjVQLU9iQ3lKTmpDcTFaa1NkRFFQNWk0eDZsN3R1c0E9PSIsInZlcnNpb24iOiIyLjAifQ"

Length of output: 400


Use dynamic JWT generation for mock_token

We’ve verified that the hardcoded token’s header and payload contain no sensitive data. To improve test consistency and avoid brittle hardcoded values (and mirror your approach in test_execute_python_failure), generate the JWT at runtime.

• File: tests/integration_test/callback_service_test.py
Lines: 664–676

Suggested change:

-   # Create a mock token with three segments
-   mock_token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.…HEL3BSt1uO2uRfwwPTVV5UgYvwXsSoyERqZ9Gev2X-U"
+   # Dynamically generate a mock token for consistency
+   from jwt import encode
+
+   payload = {
+       "sub": "gAAAAABoLcR-4BtU3QE9GADWN5X47oXk-…tusA==",
+       "version": "2.0"
+   }
+   mock_token = encode(payload, TEST_JWT_SECRET, algorithm="HS256")

Make sure TEST_JWT_SECRET is defined or imported as in other tests.

Committable suggestion skipped: line range outside the PR's diff.

🧰 Tools
🪛 Gitleaks (8.26.0)

665-665: Uncovered a JSON Web Token, which may lead to unauthorized access to web applications and sensitive user data.

(jwt)

🤖 Prompt for AI Agents
In tests/integration_test/callback_service_test.py around lines 664 to 676,
replace the hardcoded JWT token string with a dynamically generated token using
the same method as in test_execute_python_failure. Ensure you import or define
TEST_JWT_SECRET for signing the token, then create the token at runtime with
appropriate header and payload. Update the Authorization header to use this
generated token to improve test reliability and consistency.


response = await client.post("/main_pyscript/execute-python", content=JSONContent(payload))
response = await client.post("/main_pyscript/execute-python", headers=headers, content=JSONContent(payload))
json_response = await response.json()
print(json_response)

# Assertions
assert response.status == 200
assert json_response["output"] == "Execution successful"
assert json_response["success"] is True

@pytest.mark.asyncio
@patch("kairon.async_callback.utils.CallbackUtility.main_pyscript_handler")
async def test_execute_python_failure(mock_handler):
await app.start()
client = TestClient(app)

# Generate a real token using the create_access_token method
data = {"sub": "action-server", "callback": True}
token = Authentication.create_access_token(
data=data,
token_type="dynamic", # Use the dynamic token type if needed
token_expire=30 # Set expiration time
)

# Simulate an exception being raised in the handler
mock_handler.side_effect = Exception("Simulated exception")

payload = {
"source_code": "bot_response=100",
"predefined_objects": {"x": 1}
}

# Send the request with the real token in the header
headers = {
"Authorization": f"Bearer {token}" # Use the real token created above
}

response = await client.post("/main_pyscript/execute-python", headers=headers, content=JSONContent(payload))
json_response = await response.json()
print(json_response)

# Assertions to check if the exception handling block is reached
assert response.status == 422 # This should match the status code in your error response
assert json_response["success"] is False
assert "error" in json_response # Ensure the error message is included in the response
assert json_response["error"] == "Simulated exception" # Check if the simulated exception message is returned

@pytest.mark.asyncio
@patch("kairon.async_callback.utils.CallbackUtility.main_pyscript_handler")
Expand Down
Loading