From 2e7b6a3774129e2228611cf598a44a9fe148f26d Mon Sep 17 00:00:00 2001 From: Erik Kalviainen Date: Thu, 11 Apr 2024 21:00:11 -0400 Subject: [PATCH] First pass on auth --- .github/workflows/meya.check-test.yaml | 39 ++++++++++++++++++++++++ .gitignore | 9 ++++++ .isort.cfg | 10 ++++++ .meyaignore | 1 + README.md | 39 ++---------------------- bot/default.yaml | 6 ++++ component/is_expired.py | 22 ++++++++++++++ component/sha256.py | 27 +++++++++++++++++ component/sha256_test.py | 24 +++++++++++++++ examples/generate_page_context.py | 24 +++++++++++++++ flow/catchall.yaml | 5 +++ flow/leak.yaml | 8 +++++ flow/open_page.yaml | 37 +++++++++++++++++++++++ flow/secret.yaml | 7 +++++ integration/db.yaml | 1 + integration/orb.yaml | 15 +++++++++ integration/sensitive_data.yaml | 1 + page.html | 42 ++++++++++++++++++++++++++ pyproject.toml | 15 +++++++++ pytest.ini | 5 +++ vault.yaml | 1 + 21 files changed, 301 insertions(+), 37 deletions(-) create mode 100644 .github/workflows/meya.check-test.yaml create mode 100644 .gitignore create mode 100644 .isort.cfg create mode 100644 .meyaignore create mode 100644 bot/default.yaml create mode 100644 component/is_expired.py create mode 100644 component/sha256.py create mode 100644 component/sha256_test.py create mode 100644 examples/generate_page_context.py create mode 100644 flow/catchall.yaml create mode 100644 flow/leak.yaml create mode 100644 flow/open_page.yaml create mode 100644 flow/secret.yaml create mode 100644 integration/db.yaml create mode 100644 integration/orb.yaml create mode 100644 integration/sensitive_data.yaml create mode 100644 page.html create mode 100644 pyproject.toml create mode 100644 pytest.ini create mode 100644 vault.yaml diff --git a/.github/workflows/meya.check-test.yaml b/.github/workflows/meya.check-test.yaml new file mode 100644 index 0000000..1772fc3 --- /dev/null +++ b/.github/workflows/meya.check-test.yaml @@ -0,0 +1,39 @@ +# This workflow will install Python and Meya dependencies, runs tests and code formatting check +name: Meya build + +on: + repository_dispatch: + push: + branches: + - '*' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.10 + uses: actions/setup-python@v1 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install --upgrade \ + --extra-index-url https://meya:${{ secrets.auth_token }}@grid.meya.ai/registry/pypi \ + "pygit2==1.1.1" \ + "meya-sdk>=2.0.0" \ + "meya-cli>=2.0.0" + - name: Authenticate using Meya auth token + run: | + meya auth add --grid-url https://grid.meya.ai --auth-token ${{ secrets.auth_token }} + - name: Connect to production app + run: | + meya connect --grid-url https://grid.meya.ai --app-id ${{ secrets.app_id }} + - name: Check code syntax and formatting + run: meya check + - name: Run tests + run: meya test + - name: Deploy + if: github.ref == 'refs/heads/master' + run: meya push --force --build-image diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5aeba56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.direnv +.idea/* +.meya +.pytest_cache +__pycache__ +node_modules +venv +vault.secret*.yaml +scratch/ diff --git a/.isort.cfg b/.isort.cfg new file mode 100644 index 0000000..4728e96 --- /dev/null +++ b/.isort.cfg @@ -0,0 +1,10 @@ +[settings] +force_single_line=True +include_trailing_comma=True +use_parentheses=True +multi_line_output=3 +line_width=79 +lines_between_types=1 +skip=.direnv,.idea,.git,.meya,.pytest_cache,dist,node_modules,venv,__pycache__ +not_skip=__init__.py +known_first_party=* diff --git a/.meyaignore b/.meyaignore new file mode 100644 index 0000000..bb0224d --- /dev/null +++ b/.meyaignore @@ -0,0 +1 @@ +.github/ diff --git a/README.md b/README.md index 5348ba7..99379ab 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,5 @@ ![Meya build](https://github.com/meya-ai/grid-template-hello-world/workflows/Meya%20build/badge.svg) -# hello-world - -Basic template BFML and Python code that runs on Meya. - -## Setup - -```shell script -brew install python@3 libgit2 -MEYA_AUTH_TOKEN=your_meya_auth_token -MEYA_APP_ID=app-your_app_id -# you can optionally setup a venv instead as well -python3 -m venv venv # optional -. venv/bin/activate # optional -pip3 install --upgrade \ - --extra-index-url https://meya:$MEYA_AUTH_TOKEN@grid.meya.ai/registry/pypi \ - "pygit2>=1.2.1" \ - "meya-sdk>=2.0.0" \ - "meya-cli>=2.0.0" -# auth (if needed) -meya auth add --auth-token $MEYA_AUTH_TOKEN -# connect to existing app -meya connect --app-id $MEYA_APP_ID -``` - -## Workflow -```shell script -meya check -meya format -meya test --watch -# to download secrets -meya vault download --file vault.secret.yaml -# if new secrets (after changing the yaml file) -meya vault upload --file vault.secret.yaml -meya push --watch -# for a full rebuild (useful for production deployments) -meya push --force --build-image -``` # auth-bot + +Basic template BFML and Python code that runs on Meya. \ No newline at end of file diff --git a/bot/default.yaml b/bot/default.yaml new file mode 100644 index 0000000..631e4de --- /dev/null +++ b/bot/default.yaml @@ -0,0 +1,6 @@ +type: meya.bot.element +name: Auth Bot +avatar: + image: https://i.postimg.cc/brYQscPR/key.png + crop: square +markdown: true diff --git a/component/is_expired.py b/component/is_expired.py new file mode 100644 index 0000000..29b223d --- /dev/null +++ b/component/is_expired.py @@ -0,0 +1,22 @@ +import time + +from dataclasses import dataclass +from meya.component.element import Component +from meya.element.field import element_field +from meya.entry import Entry +from typing import List + + +def is_timestamp_expired(timestamp): + current_timestamp = int(time.time()) + return timestamp < current_timestamp + + +@dataclass +class IsExpiredComponentElement(Component): + timestamp: int = element_field() + + async def start(self) -> List[Entry]: + return self.respond( + data=dict(result=is_timestamp_expired(self.timestamp)) + ) diff --git a/component/sha256.py b/component/sha256.py new file mode 100644 index 0000000..fde7d0a --- /dev/null +++ b/component/sha256.py @@ -0,0 +1,27 @@ +import hashlib +import hmac + +from dataclasses import dataclass +from meya.component.element import Component +from meya.element.field import element_field +from meya.entry import Entry +from typing import List + + +def compute_hmac_sha256_signature(string, secret): + hashed = hmac.new( + secret.encode("utf-8"), string.encode("utf-8"), hashlib.sha256 + ) + return hashed.digest().hex() + + +@dataclass +class Sha256ComponentElement(Component): + string: str = element_field() + secret: str = element_field() + + async def start(self) -> List[Entry]: + computed_signature = compute_hmac_sha256_signature( + self.string, self.secret + ) + return self.respond(data=dict(result=computed_signature)) diff --git a/component/sha256_test.py b/component/sha256_test.py new file mode 100644 index 0000000..5f0a3d6 --- /dev/null +++ b/component/sha256_test.py @@ -0,0 +1,24 @@ +import pytest + +from component.sha256 import compute_hmac_sha256_signature + +# check online with https://devglan.com/online-tools/hmac-sha256-online + + +@pytest.mark.parametrize( + ("string", "secret", "computed_hash"), + [ + ( + "erik@meya.ai", + "solar_eclipse", + "c99b34b1bf5f83f61f3d77f0bae4633a49ee857061634e0e67668edd45944911", + ), + ( + "erik@meya.ai1712881476", + "solar_eclipse", + "031d822ebc78950a475b0ba8f117b0aae44c6df5a7045e993324b9c46ae94a88", + ), + ], +) +def test_square(string: str, secret: str, computed_hash: str): + assert compute_hmac_sha256_signature(string, secret) == computed_hash diff --git a/examples/generate_page_context.py b/examples/generate_page_context.py new file mode 100644 index 0000000..c6e25f5 --- /dev/null +++ b/examples/generate_page_context.py @@ -0,0 +1,24 @@ +import time + +from component.sha256 import compute_hmac_sha256_signature + +# example +ttl = 5 * 60 # 5 minutes +current_timestamp = int(time.time()) + ttl +user_id = "erik@meya.ai" +secret = "solar_eclipse" +computed_signature = compute_hmac_sha256_signature( + f"{user_id}{current_timestamp}", secret +) +print(computed_signature) +print( + f""" + pageContext: {{ + expires: {current_timestamp}, + user_hash: "{computed_signature}" + }} +""" +) + +# confirm with https://www.freeformatter.com/hmac-generator.html#before-output +# assert computed_signature == "fbafc1e2deedcf00932e64f8112b82774b6c4fa1430ee2543337b1d6da135e19" diff --git a/flow/catchall.yaml b/flow/catchall.yaml new file mode 100644 index 0000000..fc5efdf --- /dev/null +++ b/flow/catchall.yaml @@ -0,0 +1,5 @@ +triggers: + - catchall + +steps: + - say: Sorry, I don't understand. diff --git a/flow/leak.yaml b/flow/leak.yaml new file mode 100644 index 0000000..ad419c8 --- /dev/null +++ b/flow/leak.yaml @@ -0,0 +1,8 @@ +triggers: + - keyword: leak + +steps: + - say: Your secret is... + - typing: on + - delay: 1 + - say: (@ user.secret ) diff --git a/flow/open_page.yaml b/flow/open_page.yaml new file mode 100644 index 0000000..7fbc93b --- /dev/null +++ b/flow/open_page.yaml @@ -0,0 +1,37 @@ +triggers: + - type: page_open + when: (@ not thread.open_page ) + +steps: + - thread_set: + open_page: true + + # Orb userId + - type: meya.user.component.try_reverse_lookup + integration: integration.orb + - flow_set: user_id + + # verify identity (sha256 is valid not expired) + - type: component.sha256 + string: (@ flow.user_id )(@ flow.context.expires ) + secret: (@ vault.verification_secret ) + - flow_set: user_hash + - type: component.is_expired + timestamp: (@ flow.context.expires ) + - flow_set: is_expired + + - if: (@ flow.user_hash == flow.context.user_hash and not flow.is_expired) + then: + jump: verified + else: + jump: not_verified + + - (verified) + - say: ✅ Welcome, (@ flow.user_id )! + - end + + - (not_verified) + - say: ⛔️You are denied. + - thread_set: + mode: blocked + - end diff --git a/flow/secret.yaml b/flow/secret.yaml new file mode 100644 index 0000000..e6dabd7 --- /dev/null +++ b/flow/secret.yaml @@ -0,0 +1,7 @@ +triggers: + - keyword: secret + +steps: + - ask: What's your secret? + - user_set: secret + - say: Ok, I'll remember that. Say "leak" to hear it. diff --git a/integration/db.yaml b/integration/db.yaml new file mode 100644 index 0000000..3ecf6f5 --- /dev/null +++ b/integration/db.yaml @@ -0,0 +1 @@ +type: meya.db.integration diff --git a/integration/orb.yaml b/integration/orb.yaml new file mode 100644 index 0000000..b92e7b3 --- /dev/null +++ b/integration/orb.yaml @@ -0,0 +1,15 @@ +type: meya.orb.integration +identity_verification: false +theme: + brand_color: '#004a95' +launcher: + type: message + icon: '' + text: 🤐 +composer: + character_limit: + length: 64 + error_text: You've gone too far 😡 + upload: + progress_text: Discombobulating ☢️ + error_text: File couldn't upload 😭 diff --git a/integration/sensitive_data.yaml b/integration/sensitive_data.yaml new file mode 100644 index 0000000..a27a7c9 --- /dev/null +++ b/integration/sensitive_data.yaml @@ -0,0 +1 @@ +type: meya.sensitive_data.integration diff --git a/page.html b/page.html new file mode 100644 index 0000000..388d900 --- /dev/null +++ b/page.html @@ -0,0 +1,42 @@ + + + + + test-bot-auth + + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..054c194 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,15 @@ +[tool.black] +exclude = ''' +/( + \.direnv + | \.git + | \.meya + | \.idea + | node_modules + | venv + | __pycache__ + | \.pytest_cache +)/ +''' +line-length = 79 +target-version = ['py37'] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..49cae2c --- /dev/null +++ b/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +python_files = *_test.py +norecursedirs = .direnv .git .idea .meya .pytest_cache node_modules venv __pycache__ +filterwarnings = ignore::DeprecationWarning:jinja2 +asyncio_mode = auto \ No newline at end of file diff --git a/vault.yaml b/vault.yaml new file mode 100644 index 0000000..009cf13 --- /dev/null +++ b/vault.yaml @@ -0,0 +1 @@ +verification_secret: solar_eclipse