Skip to content

Commit

Permalink
implement basic auth for boot endpoint and configuration via environm…
Browse files Browse the repository at this point in the history
…ent variables (#1)
  • Loading branch information
lenkan committed Dec 5, 2024
1 parent f59fb4b commit b353222
Show file tree
Hide file tree
Showing 7 changed files with 218 additions and 64 deletions.
15 changes: 7 additions & 8 deletions .github/workflows/python-app-ci.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions


name: GitHub Actions for KERIA
on:
push:
branches:
- 'main'
- 'development'
- "main"
- "development"
pull_request:
workflow_dispatch:

Expand All @@ -17,7 +16,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ macos-latest, ubuntu-latest ]
os: [macos-13, ubuntu-latest]

steps:
- uses: actions/checkout@v3
Expand All @@ -28,8 +27,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest hio
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install flake8 pytest
pip install -r requirements.txt -r requirements-freeze.txt
- name: Lint changes
run: |
# stop the build if there are Python syntax errors or undefined names
Expand All @@ -51,8 +50,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install pytest pytest-cov hio
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install pytest pytest-cov
pip install -r requirements.txt -r requirements-freeze.txt
- name: Run core KERIA tests
run: |
pytest --cov=./ --cov-report=xml
Expand Down
44 changes: 38 additions & 6 deletions images/keria.dockerfile
Original file line number Diff line number Diff line change
@@ -1,10 +1,42 @@
FROM weboftrust/keri:1.1.17
ARG IMAGE_PYTHON_VERSION=3.10
FROM python:${IMAGE_PYTHON_VERSION}-alpine AS builder
ARG IMAGE_PYTHON_VERSION

WORKDIR /usr/local/var
RUN apk --no-cache add \
bash \
alpine-sdk \
libffi-dev \
libsodium \
libsodium-dev

RUN mkdir keria
COPY . /usr/local/var/keria
SHELL ["/bin/bash", "-c"]

WORKDIR /usr/local/var/keria
RUN curl https://sh.rustup.rs -sSf | bash -s -- -y

RUN pip install -r requirements.txt
WORKDIR /keria

RUN pip install --upgrade pip && python -m venv venv
ENV PATH="/keria/venv/bin/:~/.cargo/bin:${PATH}"

COPY requirements.txt requirements-freeze.txt setup.py ./
RUN mkdir /keria/src && pip install -r requirements.txt -r requirements-freeze.txt

FROM python:${IMAGE_PYTHON_VERSION}-alpine AS runner
ARG IMAGE_PYTHON_VERSION

RUN apk --no-cache add \
bash \
alpine-sdk \
libsodium-dev

WORKDIR /keria

COPY --from=builder /keria /keria

RUN mkdir -p /usr/local/var/keri

COPY src/ src/

ENV PATH="/keria/venv/bin/:${PATH}"
ENTRYPOINT [ "keria" ]
CMD [ "start" ]
32 changes: 32 additions & 0 deletions requirements-freeze.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apispec==6.6.1
attrs==23.2.0
blake3==0.4.1
cbor2==5.6.4
cffi==1.16.0
cryptography==42.0.8
dataclasses-json==0.6.7
falcon==3.1.3
hio==0.6.10
hjson==3.1.0
http_sfv==0.9.9
jsonschema==4.22.0
jsonschema-specifications==2023.12.1
keri==1.1.17
lmdb==1.5.1
marshmallow==3.21.3
mnemonic==0.21
msgpack==1.0.8
multicommand==1.0.0
multidict==6.0.5
mypy-extensions==1.0.0
ordered-set==4.1.0
packaging==24.1
prettytable==3.10.0
pycparser==2.22
pysodium==0.7.17
PyYAML==6.0.1
referencing==0.35.1
rpds-py==0.18.1
typing-inspect==0.9.0
typing_extensions==4.12.2
wcwidth==0.2.13
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,4 @@
# enables pip install -r requirements.txt to work with setup.py dependencies
# pull the dependencies from setup.py for keri from pip index
--index-url https://pypi.org/simple/ # pypi base pip index or local pip index

--editable . # install as editable
57 changes: 45 additions & 12 deletions src/keria/app/agenting.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
keria.app.agenting module
"""
from base64 import b64decode
import json
import os
from dataclasses import asdict
Expand Down Expand Up @@ -48,11 +49,13 @@
logger = ogler.getLogger()


def setup(name, bran, adminPort, bootPort, base='', httpPort=None, configFile=None, configDir=None,
keypath=None, certpath=None, cafilepath=None):
def setup(name: str, bran: str | None, adminPort: int, bootPort: int, base='', httpPort=None, configFile=None, configDir=None,
keypath=None, certpath=None, cafilepath=None, curls: list[str] | None = None, username: str | None = None,
password: str | None = None, cors: bool = False):
""" Set up an ahab in Signify mode """

agency = Agency(name=name, base=base, bran=bran, configFile=configFile, configDir=configDir)
agency = Agency(name=name, base=base, bran=bran, configFile=configFile, configDir=configDir, curls=curls)

bootApp = falcon.App(middleware=falcon.CORSMiddleware(
allow_origins='*', allow_credentials='*',
expose_headers=['cesr-attachment', 'cesr-date', 'content-type', 'signature', 'signature-input',
Expand All @@ -62,7 +65,7 @@ def setup(name, bran, adminPort, bootPort, base='', httpPort=None, configFile=No
if not bootServer.reopen():
raise RuntimeError(f"cannot create boot http server on port {bootPort}")
bootServerDoer = http.ServerDoer(server=bootServer)
bootEnd = BootEnd(agency)
bootEnd = BootEnd(agency, username=username, password=password)
bootApp.add_route("/boot", bootEnd)
bootApp.add_route("/health", HealthEnd())

Expand All @@ -73,7 +76,7 @@ def setup(name, bran, adminPort, bootPort, base='', httpPort=None, configFile=No
allow_origins='*', allow_credentials='*',
expose_headers=['cesr-attachment', 'cesr-date', 'content-type', 'signature', 'signature-input',
'signify-resource', 'signify-timestamp']))
if os.getenv("KERI_AGENT_CORS", "false").lower() in ("true", "1"):
if cors:
app.add_middleware(middleware=httping.HandleCORS())
app.add_middleware(authing.SignatureValidationComponent(agency=agency, authn=authn, allowed=["/agent"]))
app.req_options.media_handlers.update(media.Handlers())
Expand Down Expand Up @@ -152,11 +155,12 @@ class Agency(doing.DoDoer):
"""

def __init__(self, name, bran, base="", configFile=None, configDir=None, adb=None, temp=False):
def __init__(self, name, bran, base="", configFile=None, configDir=None, adb=None, temp=False, curls:list[str]|None=None):
self.name = name
self.base = base
self.bran = bran
self.temp = temp
self.curls = curls
self.configFile = configFile
self.configDir = configDir
self.cf = None
Expand All @@ -180,12 +184,16 @@ def create(self, caid):
reopen=True)

cf = None
habName = f"agent-{caid}"
if self.cf is not None: # Load config file if creating database
data = dict(self.cf.get())
if self.curls is not None:
data[habName] = { "dt": nowIso8601(), "curls": self.curls }

if "keria" in data:
curls = data["keria"]
data[f"agent-{caid}"] = curls
data[habName] = data["keria"]
del data["keria"]


cf = configing.Configer(name=f"{caid}",
base="",
Expand All @@ -197,7 +205,7 @@ def create(self, caid):

# Create the Hab for the Agent with only 2 AIDs
agentHby = habbing.Habery(name=caid, base=self.base, bran=self.bran, ks=ks, cf=cf, temp=self.temp)
agentHab = agentHby.makeHab(f"agent-{caid}", ns="agent", transferable=True, delpre=caid)
agentHab = agentHby.makeHab(habName, ns="agent", transferable=True, delpre=caid)
agentRgy = Regery(hby=agentHby, name=agentHab.name, base=self.base, temp=self.temp)

agent = Agent(agentHby, agentRgy, agentHab,
Expand Down Expand Up @@ -784,17 +792,40 @@ def loadEnds(app):
class BootEnd:
""" Resource class for creating datastore in cloud ahab """

def __init__(self, agency):
def __init__(self, agency: Agency, username: str | None = None, password: str | None = None):
""" Provides endpoints for initializing and unlocking an agent
Parameters:
agency (Agency): Agency for managing agents
"""
self.authn = authing.Authenticater(agency=agency)
self.username = username
self.password = password
self.agency = agency

def authenticate(self, req: falcon.Request):
if self.username is None and self.password is None:
return

if req.auth is None:
raise falcon.HTTPUnauthorized(title="Unauthorized")

scheme, token = req.auth.split(' ')
if scheme != 'Basic':
raise falcon.HTTPUnauthorized(title="Unauthorized")

def on_post(self, req, rep):
try:
username, password = b64decode(token).decode('utf-8').split(':')

if username == self.username and password == self.password:
return

except Exception:
raise falcon.HTTPUnauthorized(title="Unauthorized")

raise falcon.HTTPUnauthorized(title="Unauthorized")

def on_post(self, req: falcon.Request, rep: falcon.Response):
""" Inception event POST endpoint
Give me a new Agent. Create Habery using ctrlPRE as database name, agentHab that anchors the caid and
Expand All @@ -806,6 +837,8 @@ def on_post(self, req, rep):
"""

self.authenticate(req)

body = req.get_media()
if "icp" not in body:
raise falcon.HTTPBadRequest(title="invalid inception",
Expand Down
61 changes: 25 additions & 36 deletions src/keria/app/cli/commands/start.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"""
import argparse
import logging
import os

from keri import __version__
from keri import help
Expand All @@ -25,15 +26,12 @@
parser.add_argument('-a', '--admin-http-port',
dest="admin",
action='store',
default=3901,
help="Admin port number the HTTP server listens on. Default is 3901.")
parser.add_argument('-H', '--http',
action='store',
default=3902,
help="Local port number the HTTP server listens on. Default is 3902.")
parser.add_argument('-B', '--boot',
action='store',
default=3903,
help="Boot port number the Boot HTTP server listens on. This port needs to be secured."
" Default is 3903.")
parser.add_argument('-n', '--name',
Expand Down Expand Up @@ -68,40 +66,31 @@ def launch(args):

logger = help.ogler.getLogger()

logger.info("******* Starting Agent for %s listening: admin/%s, http/%s "
".******", args.name, args.admin, args.http)
adminPort = int(args.admin or os.getenv("KERIA_ADMIN_PORT", "3901"))
httpPort = int(args.http or os.getenv("KERIA_HTTP_PORT", "3902"))
bootPort = int(args.boot or os.getenv("KERIA_BOOT_PORT", "3903"))

runAgent(name=args.name,
base=args.base,
bran=args.bran,
admin=int(args.admin),
http=int(args.http),
boot=int(args.boot),
configFile=args.configFile,
configDir=args.configDir,
keypath=args.keypath,
certpath=args.certpath,
cafilepath=args.cafilepath)

logger.info("******* Ended Agent for %s listening: admin/%s, http/%s"
".******", args.name, args.admin, args.http)


def runAgent(name="ahab", base="", bran="", admin=3901, http=3902, boot=3903, configFile=None,
configDir=None, keypath=None, certpath=None, cafilepath=None, expire=0.0):
"""
Setup and run a KERIA Agency
"""
logger.info("******* Starting Agent for %s listening: admin/%s, http/%s"
".******", args.name, adminPort, httpPort)

doers = []
doers.extend(agenting.setup(name=name, base=base, bran=bran,
adminPort=admin,
httpPort=http,
bootPort=boot,
configFile=configFile,
configDir=configDir,
keypath=keypath,
certpath=certpath,
cafilepath=cafilepath))
doers.extend(agenting.setup(name=args.name or "ahab",
base=args.base,
bran=args.bran or os.getenv("KERIA_PASSCODE") or "",
adminPort=adminPort,
httpPort=httpPort,
bootPort=bootPort,
configFile=args.configFile,
configDir=args.configDir,
keypath=args.keypath,
certpath=args.certpath,
cafilepath=args.cafilepath,
curls=os.getenv("KERIA_CURLS").split(";") if os.getenv("KERIA_CURLS") is not None else None,
username=os.getenv("KERIA_USERNAME"),
password=os.getenv("KERIA_PASSWORD"),
cors=os.getenv("KERI_AGENT_CORS", "false").lower() in ("true", "1")))

directing.runController(doers=doers, expire=0.0)

directing.runController(doers=doers, expire=expire)
logger.info("******* Ended Agent for %s listening: admin/%s, http/%s"
".******", args.name, adminPort, httpPort)
Loading

0 comments on commit b353222

Please sign in to comment.