Skip to content

Commit

Permalink
Merge pull request #53 from bcgov/feature/innkeeper-subapp
Browse files Browse the repository at this point in the history
Secure innkeeper. Make innkeeper sub-application
  • Loading branch information
ianco authored Jan 31, 2022
2 parents 1ac2e8d + cbde520 commit 58f2ba2
Show file tree
Hide file tree
Showing 15 changed files with 122 additions and 100 deletions.
2 changes: 2 additions & 0 deletions charts/traction/templates/traction_api_deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ spec:
initialDelaySeconds: 60
periodSeconds: 30
env:
- name: TRACTION_API_ADMIN_USER
value: {{ .Values.traction_api.api.adminuser }}
- name: TRACTION_API_ADMIN_KEY
valueFrom:
secretKeyRef:
Expand Down
2 changes: 2 additions & 0 deletions charts/traction/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,8 @@ traction_api:
db:
admin: tractionadminuser
user: tractionuser
api:
adminuser: innkeeper

serviceAccount:
# -- Specifies whether a service account should be created
Expand Down
1 change: 1 addition & 0 deletions scripts/.env-example
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ TRACTION_PSQL_USER=tractionuser
TRACTION_PSQL_USER_PWD=tractionPass


TRACTION_API_ADMIN_USER=innkeeper
TRACTION_API_ADMIN_KEY=change-me

# ------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions scripts/docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ services:
- TRACTION_DB_ADMIN_PWD=${TRACTION_PSQL_ADMIN_PWD}
- TRACTION_DB_USER=${TRACTION_PSQL_USER}
- TRACTION_DB_USER_PWD=${TRACTION_PSQL_USER_PWD}
- TRACTION_API_ADMIN_USER=${TRACTION_API_ADMIN_USER}
- TRACTION_API_ADMIN_KEY=${TRACTION_API_ADMIN_KEY}
- ACAPY_ADMIN_URL=${ACAPY_ADMIN_URL}
- ACAPY_ADMIN_URL_API_KEY=${ACAPY_ADMIN_URL_API_KEY}
Expand Down
12 changes: 10 additions & 2 deletions services/traction/api/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,12 @@ class GlobalConfig(BaseSettings):
TITLE: str = "Traction"
DESCRIPTION: str = "A digital wallet solution for organizations"

TENANT_TITLE: str = "Traction"
TENANT_DESCRIPTION: str = "A digital wallet solution for organizations"
# sub-app titles/descriptions
TENANT_TITLE: str = "Traction Tenant"
TENANT_DESCRIPTION: str = "Endpoints for Tenants of Traction"

INNKEEPER_TITLE: str = "Traction Innkeeper"
INNKEEPER_DESCRIPTION: str = "Endpoints for Innkeeper of Traction"

ENVIRONMENT: EnvironmentEnum
DEBUG: bool = False
Expand Down Expand Up @@ -54,7 +58,11 @@ class GlobalConfig(BaseSettings):
"ACAPY_ADMIN_URL_API_KEY", "change-me"
)

TRACTION_API_ADMIN_USER: str = os.environ.get(
"TRACTION_API_ADMIN_USER", "innkeeper"
)
TRACTION_API_ADMIN_KEY: str = os.environ.get("TRACTION_API_ADMIN_KEY", "change-me")

TRACTION_WEBHOOK_URL: str = os.environ.get(
"TRACTION_WEBHOOK_URL", "http://traction-api:5000/webhook"
)
Expand Down
22 changes: 22 additions & 0 deletions services/traction/api/endpoints/dependencies/jwt_security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from datetime import datetime, timedelta

from jose import jwt
from pydantic import BaseModel

from api.core.config import settings


class AccessToken(BaseModel):
access_token: str
token_type: str


def create_access_token(data: dict):
expires_delta = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode = data.copy()
expire = datetime.utcnow() + expires_delta
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
)
return AccessToken(access_token=encoded_jwt, token_type="bearer")
Original file line number Diff line number Diff line change
@@ -1,9 +1,4 @@
from datetime import datetime, timedelta
from typing import Optional

from fastapi.security import OAuth2PasswordBearer
from jose import jwt
from pydantic import BaseModel
from starlette_context import context
from starlette.middleware.base import (
BaseHTTPMiddleware,
Expand All @@ -16,19 +11,6 @@
from api.core.config import settings


class TenantToken(BaseModel):
access_token: str
token_type: str


class TenantTokenData(BaseModel):
wallet_id: Optional[str] = None
bearer_token: Optional[str] = None


oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")


class JWTTFetchingMiddleware(BaseHTTPMiddleware):
"""Middleware to inject tenant JWT into context."""

Expand Down Expand Up @@ -69,16 +51,3 @@ async def authenticate_tenant(username: str, password: str):
return tenant
except Exception:
return None


def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
to_encode = data.copy()
if expires_delta:
expire = datetime.utcnow() + expires_delta
else:
expire = datetime.utcnow() + timedelta(minutes=15)
to_encode.update({"exp": expire})
encoded_jwt = jwt.encode(
to_encode, settings.JWT_SECRET_KEY, algorithm=settings.JWT_ALGORITHM
)
return encoded_jwt
7 changes: 0 additions & 7 deletions services/traction/api/endpoints/routes/api.py

This file was deleted.

14 changes: 1 addition & 13 deletions services/traction/api/endpoints/routes/connections.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
from enum import Enum
from typing import Optional

from fastapi import APIRouter, Depends
from fastapi import APIRouter
from pydantic import BaseModel

from api import acapy_utils as au
from api.tenant_security import (
oauth2_scheme,
)


router = APIRouter()

Expand Down Expand Up @@ -80,8 +76,6 @@ async def get_connections(
connection_state: Optional[ConnectionStateType] = None,
their_did: Optional[str] = None,
their_role: Optional[ConnectionRoleType] = None,
# note we don't need the token here but we need to make sure it gets set
_token: str = Depends(oauth2_scheme),
):
params = {
"alias": alias,
Expand All @@ -99,8 +93,6 @@ async def get_connections(
@router.post("/create-invitation", response_model=Invitation)
async def create_invitation(
alias: str | None = None,
# note we don't need the token here but we need to make sure it gets set
_token: str = Depends(oauth2_scheme),
):
params = {"alias": alias}
invitation = await au.acapy_POST(
Expand All @@ -113,8 +105,6 @@ async def create_invitation(
async def receive_invitation(
payload: dict,
alias: str | None = None,
# note we don't need the token here but we need to make sure it gets set
_token: str = Depends(oauth2_scheme),
):
params = {"alias": alias}
connection = await au.acapy_POST(
Expand All @@ -128,8 +118,6 @@ async def send_message(
payload: BasicMessage,
connection_id: str | None = None,
alias: str | None = None,
# note we don't need the token here but we need to make sure it gets set
_token: str = Depends(oauth2_scheme),
):
if not connection_id:
lookup_params = {"alias": alias}
Expand Down
8 changes: 1 addition & 7 deletions services/traction/api/endpoints/routes/ledger.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,10 @@
from enum import Enum
from typing import Optional

from fastapi import APIRouter, Depends
from fastapi import APIRouter
from pydantic import BaseModel

from api import acapy_utils as au
from api.tenant_security import (
oauth2_scheme,
)


router = APIRouter()

Expand All @@ -27,8 +23,6 @@ class DIDEndpointType(str, Enum):
async def get_did_endpoint(
did: str,
endpoint_type: Optional[DIDEndpointType] = None,
# note we don't need the token here but we need to make sure it gets set
_token: str = Depends(oauth2_scheme),
):
params = {"did": did}
if endpoint_type:
Expand Down
21 changes: 0 additions & 21 deletions services/traction/api/endpoints/routes/tenants.py

This file was deleted.

58 changes: 58 additions & 0 deletions services/traction/api/innkeeper_main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from fastapi import APIRouter, Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from starlette.middleware import Middleware
from starlette_context import plugins
from starlette_context.middleware import RawContextMiddleware

from api.endpoints.routes.innkeeper import router as innkeeper_router
from api.endpoints.dependencies.jwt_security import AccessToken, create_access_token
from api.core.config import settings as s


middleware = [
Middleware(
RawContextMiddleware,
plugins=(plugins.RequestIdPlugin(), plugins.CorrelationIdPlugin()),
),
]

router = APIRouter()


def get_innkeeperapp() -> FastAPI:
application = FastAPI(
title=s.INNKEEPER_TITLE,
description=s.INNKEEPER_DESCRIPTION,
debug=s.DEBUG,
middleware=middleware,
)
# mount the token endpoint
application.include_router(router, prefix="")
# mount other endpoints, these will be secured by the above token endpoint
application.include_router(
innkeeper_router,
prefix=s.API_V1_STR,
dependencies=[Depends(OAuth2PasswordBearer(tokenUrl="token"))],
tags=["innkeeper"],
)
return application


@router.post("/token", response_model=AccessToken)
async def login_for_traction_api_admin(
form_data: OAuth2PasswordRequestForm = Depends(),
):
authenticated = await authenticate_innkeeper(form_data.username, form_data.password)
if not authenticated:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect Traction Api Admin User or Traction Api Admin Key",
headers={"WWW-Authenticate": "Bearer"},
)
return create_access_token(data={"sub": form_data.username})


async def authenticate_innkeeper(username: str, password: str):
if s.TRACTION_API_ADMIN_USER == username and s.TRACTION_API_ADMIN_KEY == password:
return True
return False
7 changes: 5 additions & 2 deletions services/traction/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
from starlette.responses import JSONResponse

from api.db.errors import DoesNotExist, AlreadyExists
from api.endpoints.routes.api import api_router
from api.endpoints.routes.webhooks import get_webhookapp
from api.core.config import settings
from api.innkeeper_main import get_innkeeperapp
from api.tenant_main import get_tenantapp


Expand All @@ -23,16 +23,19 @@ def get_application() -> FastAPI:
debug=settings.DEBUG,
middleware=None,
)
application.include_router(api_router, prefix=settings.API_V1_STR)
return application


app = get_application()
webhook_app = get_webhookapp()
app.mount("/webhook", webhook_app)

tenant_app = get_tenantapp()
app.mount("/tenant", tenant_app)

innkeeper_app = get_innkeeperapp()
app.mount("/innkeeper", innkeeper_app)


@app.exception_handler(DoesNotExist)
async def does_not_exist_exception_handler(request: Request, exc: DoesNotExist):
Expand Down
35 changes: 18 additions & 17 deletions services/traction/api/tenant_main.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
from datetime import timedelta

from fastapi import APIRouter, Depends, FastAPI, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from fastapi.security import OAuth2PasswordRequestForm, OAuth2PasswordBearer
from starlette.middleware import Middleware
from starlette_context import plugins
from starlette_context.middleware import RawContextMiddleware

from api.endpoints.routes.tenant_api import tenant_router
from api.tenant_security import (
TenantToken,
authenticate_tenant,
create_access_token,
from api.endpoints.dependencies.jwt_security import AccessToken, create_access_token
from api.core.config import settings
from api.endpoints.dependencies.tenant_security import (
JWTTFetchingMiddleware,
authenticate_tenant,
)
from api.core.config import settings


middleware = [
Middleware(
Expand All @@ -34,23 +30,28 @@ def get_tenantapp() -> FastAPI:
debug=settings.DEBUG,
middleware=middleware,
)
application.include_router(tenant_router, prefix=settings.API_V1_STR)
# mount the token endpoint
application.include_router(router, prefix="")
# mount other endpoints, these will be secured by the above token endpoint
application.include_router(
tenant_router,
prefix=settings.API_V1_STR,
dependencies=[Depends(OAuth2PasswordBearer(tokenUrl="token"))],
)
return application


@router.post("/token", response_model=TenantToken)
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends()):
@router.post("/token", response_model=AccessToken)
async def login_for_tenant_access_token(
form_data: OAuth2PasswordRequestForm = Depends(),
):
tenant = await authenticate_tenant(form_data.username, form_data.password)
if not tenant:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Incorrect wallet_id or wallet_key",
headers={"WWW-Authenticate": "Bearer"},
)
access_token_expires = timedelta(minutes=settings.JWT_ACCESS_TOKEN_EXPIRE_MINUTES)
access_token = create_access_token(
data={"sub": tenant["wallet_id"], "key": tenant["wallet_token"]},
expires_delta=access_token_expires,
return create_access_token(
data={"sub": tenant["wallet_id"], "key": tenant["wallet_token"]}
)
return {"access_token": access_token, "token_type": "bearer"}
1 change: 1 addition & 0 deletions services/traction/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ httptools==0.3.0
idna==3.3
itsdangerous==2.0.1
Jinja2==3.0.3
jose==1.0.0
Mako==1.1.6
MarkupSafe==2.0.1
passlib==1.7.4
Expand Down

0 comments on commit 58f2ba2

Please sign in to comment.