Skip to content
3 changes: 3 additions & 0 deletions .flake8
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[flake8]
max-line-length = 150
ignore = E401,F401,W503
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# local configs
env.*.local
.vscode

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,38 @@ http_trigger.send_response(data=payload, headers=headers, status_code=status_cod
To connect to a postgres resource, use the following snippet:
```
import psycopg2
from wayscript.ingegrations import sql
from wayscript.integrations import sql

kwargs = sql.get_psycopg2_connection_kwargs(_id)
connection = psycopg2.connect(**kwargs)
```

## Secrets

### Create/Update Secret

To create a new secret, or update an existing one:
```
from wayscript import secret_manager

my_secret_value = "an application key or other private information"
secret_manager.set_secret('my_secret_key', my_secret_value)

```

To test an existing secret, and update if the secret is no longer valid (expired authorization token):
```
import os
from wayscript import secret_manager

# Retrieve existing key from secret
auth_key = os.getenv('AUTH_KEY_MAY_EXPIRE')

# Test connection to service using auth_key
if not authorized:
# Get new auth_key from service
auth_key = 'New Key From Service Request'
secret_manager.set_secret('AUTH_KEY_MAY_EXPIRE', auth_key)

# Continue flow as normal
```
6 changes: 6 additions & 0 deletions src/wayscript/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
WayScript Errors
"""


class MissingCredentialsError(Exception):
"""Error thrown when a workspace integration does not have requisite credentials"""
pass


class UnauthorizedUserError(Exception):
"""Error thrown when a user does not have the necessary authorization for the operation"""
pass
2 changes: 1 addition & 1 deletion src/wayscript/integrations/slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def get_client_for_workspace_integration(_id: str) -> WebClient:
access_token = credentials.get("access_token")
except json.decoder.JSONDecodeError:
access_token = None

if not access_token:
raise MissingCredentialsError(f"No credentials found for {_id}")
else:
Expand Down
15 changes: 9 additions & 6 deletions src/wayscript/integrations/sql.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@


PSYCOPG2_CONNECTION_KWARG_MAP = {
"dbname": "database_name",
"user": "database_user",
"password": "database_password",
"host": "database_host",
"port": "database_port",
"dbname": "database_name",
"user": "database_user",
"password": "database_password",
"host": "database_host",
"port": "database_port",
}


Expand All @@ -35,7 +35,7 @@
def get_connection_kwargs(_id: str, credentials_mapping: dict) -> dict:
"""
Return connection kwargs

If you want to instantiate your own client, use this method.
"""
wayscript_client = utils.WayScriptClient()
Expand All @@ -57,14 +57,17 @@ def get_connection_kwargs(_id: str, credentials_mapping: dict) -> dict:

return kwargs


def get_psycopg2_connection_kwargs(_id: str) -> dict:
"""Get connection kwargs for psycopg2"""
return get_connection_kwargs(_id, credentials_mapping=PSYCOPG2_CONNECTION_KWARG_MAP)


def get_mssql_connection_kwargs(_id: str) -> dict:
"""Get connection kwargs for SQL Server connection via mssql driver"""
return get_connection_kwargs(_id, credentials_mapping=MSSQL_CONNECTION_KWARG_MAP)


def get_mysql_connection_kwargs(_id: str) -> dict:
"""Return connection kwargs for MySQL connection via mysql driver"""
return get_connection_kwargs(_id, credentials_mapping=MYSQL_CONNECTION_KWARG_MAP)
13 changes: 13 additions & 0 deletions src/wayscript/secret_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from . import utils, context, errors


def set_secret(secret_key: str, secret_val: str) -> None:
process_details = context.get_process()
lair_id = process_details["lair_id"]
client = utils.WayScriptClient()
response = client.set_lair_secret(lair_id, secret_key, secret_val)

# Handle unique error state
if response.status_code == 403:
raise errors.UnauthorizedUserError
response.raise_for_status()
9 changes: 5 additions & 4 deletions src/wayscript/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,24 @@
import os

AUTH_ROUTE = "auth"
PROCESSES_ROUTE = "processes"
FILES_ROUTE = "files"
LAIRS_ROUTE = "lairs"
PROCESSES_ROUTE = "processes"
WEBHOOKS = "webhooks"
WORKSPACES_ROUTE = "workspaces"
WORKSPACE_INTEGRATIONS_ROUTE = "workspace-integrations"



ROUTES = {
"auth": {"refresh": f"{AUTH_ROUTE}/refresh"},
"files": {"set_secret": f'{FILES_ROUTE}/lairs/$id/secrets'},
"lairs": {"detail": f"{LAIRS_ROUTE}/$id"},
"processes": { "detail_expanded": f"{PROCESSES_ROUTE}/$id/detail"},
"processes": {"detail_expanded": f"{PROCESSES_ROUTE}/$id/detail"},
"webhooks": {"http_trigger_response": f"{WEBHOOKS}/http-trigger/response/$id"},
"workspaces": {
"detail": f"{WORKSPACES_ROUTE}/$id",
"user_application_key_detail": f"{WORKSPACES_ROUTE}/$id/users/self",
},
},
"workspace-integrations": {"detail": f"{WORKSPACE_INTEGRATIONS_ROUTE}/$id"},
}

Expand Down
2 changes: 1 addition & 1 deletion src/wayscript/triggers/http_trigger.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from wayscript import utils


def send_response(data: dict=None, headers: dict=None, status_code: int=None):
def send_response(data: dict = None, headers: dict = None, status_code: int = None):
"""Send response to http trigger endpoint"""
assert data, "data kwarg is required"
assert headers, "headers kwarg is required"
Expand Down
24 changes: 20 additions & 4 deletions src/wayscript/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ def get_process_execution_user_token():
token = os.environ.get("WAYSCRIPT_EXECUTION_USER_TOKEN")
return token


def set_process_execution_user_token(value: str):
os.environ["WAYSCRIPT_EXECUTION_USER_TOKEN"] = value
return value


def get_process_id():
"""Return uuid of current container execution"""
process_id = os.environ["WS_PROCESS_ID"]
Expand All @@ -26,6 +28,7 @@ def get_refresh_token():
refresh_token = os.environ["WAYSCRIPT_EXECUTION_USER_REFRESH_TOKEN"]
return refresh_token


def get_application_key():
application_key = os.environ["WAYSCRIPT_EXECUTION_USER_APPLICATION_KEY"]
return application_key
Expand All @@ -51,11 +54,11 @@ class WayScriptClient:
def __init__(self, *args, **kwargs):
"""Init a wayscript client"""
self.session = requests.Session()
access_token = get_process_execution_user_token()
access_token = get_process_execution_user_token()
self.session.headers["authorization"] = f"Bearer {access_token}"
self.session.headers["content-type"] = "application/json"
def _get_url(self, subpath: str, route: str, template_args: dict=None):

def _get_url(self, subpath: str, route: str, template_args: dict = None):
"""Generate an url"""
subpath_template_str = settings.ROUTES[subpath][route]
subpath_template = string.Template(subpath_template_str)
Expand Down Expand Up @@ -104,7 +107,7 @@ def get_workspace_detail(self, _id: str):
return response

@retry_on_401_wrapper
def post_webhook_http_trigger_response(self, _id: str, payload: dict=None):
def post_webhook_http_trigger_response(self, _id: str, payload: dict = None):
"""
Post an http trigger response

Expand All @@ -122,3 +125,16 @@ def get_user_detail_by_application_key(self, application_key: str, workspace_id:
url = self._get_url(subpath="workspaces", route="user_application_key_detail", template_args={"id": workspace_id})
response = self.session.get(url, headers={"authorization": f"Bearer {application_key}"})
return response

def set_lair_secret(self, _id: str, secret_key: str, secret_val: str):
"""
Create a new secret or update an existing secret

_id: lair id
secret_key: key to update secret for
secret_val: value to set secret to (will be encrypted)
"""
payload = {"key": secret_key, "value": secret_val}
url = self._get_url(subpath="files", route="set_secret", template_args={"id": _id})
response = self.session.post(url, json=payload)
return response
11 changes: 6 additions & 5 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

@pytest.fixture(autouse=True)
def stub_environment():

stubbed_environment_variables = [
"WAYSCRIPT_EXECUTION_USER_TOKEN",
"WS_PROCESS_ID",
Expand All @@ -15,10 +14,11 @@ def stub_environment():
for var in stubbed_environment_variables:
os.environ[var] = "TEST_SETTING"


LAIR_ID = "61429be0-25a0-4030-a824-0e34163e441e"
WORKSPACE_ID = "fbcb455f-0d49-4fb7-b6d4-44e005a3ca42"
WORKSPACE_INTEGRATION_ID = "9b3e827e-f113-41fc-a14e-4ba53afa1707"


@pytest.fixture
def patch_client_get_url(monkeypatch):
Expand Down Expand Up @@ -57,8 +57,8 @@ def processes_detail_expanded_response():
"service_id": "42621a34-cbab-48f3-a3d2-400d83868caf",
"status": None,
"trigger_id": "081f3cfe-b9ba-4e6c-9f9a-d20206d3a3ee"
}
}
}
return data


Expand Down Expand Up @@ -116,6 +116,7 @@ def workspace_integrations_detail_response():
}
return data


@pytest.fixture
def workspace_integration_sql_credentials():
"""Data for sql credentials"""
Expand All @@ -133,12 +134,12 @@ def workspace_integration_sql_credentials():
def workspace_integrations_detail_response_sql(workspace_integration_sql_credentials):
"""
Data from GET /workspaces-integrations/<id>

For type=sql
"""
data = {
"id": WORKSPACE_INTEGRATION_ID,
"type": "sql",
"credentials": json.dumps(workspace_integration_sql_credentials),
}
return data
return data
2 changes: 1 addition & 1 deletion tests/integrations/test_slack.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from wayscript.integrations import slack


@responses.activate
def test_slack_get_client_for_workspace_integration(workspace_integrations_detail_response, patch_client_get_url):
"""Test that we can retrieve a slack client from an integration"""
Expand All @@ -13,4 +14,3 @@ def test_slack_get_client_for_workspace_integration(workspace_integrations_detai
_id = workspace_integrations_detail_response["id"]
client = slack.get_client_for_workspace_integration(_id)
assert isinstance(client, WebClient)

4 changes: 3 additions & 1 deletion tests/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def test_process_expanded_detail_methods(patch_client_get_url, processes_detail_
callable = getattr(ws_context, f"get_{response_key}")
assert callable() == processes_detail_expanded_response[response_key]


@responses.activate
def test_get_lair(patch_client_get_url, lairs_detail_response):
"""Test returning lair data"""
Expand All @@ -31,12 +32,13 @@ def test_get_workspace(patch_client_get_url, lairs_detail_response, workspaces_d
"""Test returning workspace data"""
responses.add(responses.GET, patch_client_get_url,
json=workspaces_detail_response, status=200)

monkeypatch.setattr(ws_context, "get_lair", lambda *args, **kwargs: lairs_detail_response)

workspace = ws_context.get_workspace()
assert workspace == workspaces_detail_response


@responses.activate
def test_get_user_by_application_key(patch_client_get_url, lairs_detail_response, user_detail_response, monkeypatch):
responses.add(responses.GET, patch_client_get_url,
Expand Down
22 changes: 22 additions & 0 deletions tests/test_secret_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import pytest
import responses

from wayscript import secret_manager
from wayscript.errors import UnauthorizedUserError


@responses.activate
def test_set_secret(patch_client_get_url):
"""Test set secret"""
responses.add(responses.POST, patch_client_get_url, json={}, status=200)

secret_manager.set_secret("test_key", "test_val")


@responses.activate
def test_set_secret_unauthorized_user(patch_client_get_url):
"""Test that set secret raises the appropriate error for unauthorized users"""
responses.add(responses.POST, patch_client_get_url, json={}, status=403)

with pytest.raises(UnauthorizedUserError):
secret_manager.set_secret("test_key", "test_val")
11 changes: 5 additions & 6 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,21 @@

from wayscript import settings, utils


@responses.activate
@pytest.mark.parametrize(
"method,_id,expected_subpath,http_method",
[
("get_lair_detail","334d88dc-f9bf-4d3e-a7a2-0dec1279e4d9", "lairs/334d88dc-f9bf-4d3e-a7a2-0dec1279e4d9", "GET"),
("get_process_detail_expanded","378daaa4-0fc3-48da-9f51-f745151dfc08", "processes/378daaa4-0fc3-48da-9f51-f745151dfc08/detail", "GET"),
("get_lair_detail", "334d88dc-f9bf-4d3e-a7a2-0dec1279e4d9", "lairs/334d88dc-f9bf-4d3e-a7a2-0dec1279e4d9", "GET"),
("get_process_detail_expanded", "378daaa4-0fc3-48da-9f51-f745151dfc08", "processes/378daaa4-0fc3-48da-9f51-f745151dfc08/detail", "GET"),
("post_webhook_http_trigger_response", "19cb0a0f-f48c-4191-84b5-ba7be8ceb1a0", "webhooks/http-trigger/response/19cb0a0f-f48c-4191-84b5-ba7be8ceb1a0", "POST"),
("get_workspace_detail", "19cb0a0f-f48c-4191-84b5-ba7be8ceb1a0", "workspaces/19cb0a0f-f48c-4191-84b5-ba7be8ceb1a0", "GET"),
("get_workspace_integration_detail","9b3e827e-f113-41fc-a14e-4ba53afa1707", "workspace-integrations/9b3e827e-f113-41fc-a14e-4ba53afa1707", "GET"),
("get_workspace_integration_detail", "9b3e827e-f113-41fc-a14e-4ba53afa1707", "workspace-integrations/9b3e827e-f113-41fc-a14e-4ba53afa1707", "GET"),
],
)
def test__get_url_generates_correct_endpoints(method, _id, expected_subpath, http_method):
"""Test that wayscript client's _get_url method generates the correct expected url subpath"""

expected_url = f"{settings.WAYSCRIPT_ORIGIN}/{expected_subpath}"
responses_method = getattr(responses, http_method)
responses.add(responses_method, expected_url,
Expand All @@ -28,5 +29,3 @@ def test__get_url_generates_correct_endpoints(method, _id, expected_subpath, htt

assert len(responses.calls) == 1
assert responses.calls[0].request.url == expected_url