Skip to content

Commit

Permalink
refactor: change env vars to use pydantic settings
Browse files Browse the repository at this point in the history
  • Loading branch information
lchen-2101 committed Oct 6, 2023
1 parent b129ad5 commit 318d19e
Show file tree
Hide file tree
Showing 11 changed files with 235 additions and 121 deletions.
217 changes: 167 additions & 50 deletions poetry.lock

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ packages = [{ include = "regtech-user-fi-management" }]

[tool.poetry.dependencies]
python = "^3.11"
fastapi = "^0.97.0"
fastapi = "^0.103.1"
uvicorn = "^0.22.0"
python-dotenv = "^1.0.0"
python-keycloak = "^3.0.0"
Expand All @@ -18,6 +18,7 @@ python-jose = "^3.3.0"
requests = "^2.31.0"
asyncpg = "^0.27.0"
alembic = "^1.12.0"
pydantic-settings = "^2.0.3"

[tool.poetry.group.dev.dependencies]
ruff = "^0.0.278"
Expand Down
4 changes: 3 additions & 1 deletion src/.env.local
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,6 @@ INST_DB_PWD=fi
INST_DB_HOST=localhost:5432
INST_DB_SCHEMA=public
INST_CONN=postgresql+asyncpg://${INST_DB_USER}:${INST_DB_PWD}@${INST_DB_HOST}/${INST_DB_NAME}
JWT_OPTS=verify_at_hash:False,verify_aud:False,verify_iss:False
JWT_OPTS_VERIFY_AT_HASH="false"
JWT_OPTS_VERIFY_AUD="false"
JWT_OPTS_VERIFY_ISS="false"
3 changes: 1 addition & 2 deletions src/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ INST_DB_USER=
INST_DB_PWD=
INST_DB_HOST=
INST_DB_SCHEMA=
INST_CONN=postgresql+asyncpg://${INST_DB_USER}:${INST_DB_PWD}@${INST_DB_HOST}/${INST_DB_NAME}
JWT_OPTS=
INST_CONN=postgresql+asyncpg://${INST_DB_USER}:${INST_DB_PWD}@${INST_DB_HOST}/${INST_DB_NAME}
41 changes: 41 additions & 0 deletions src/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import os
from pydantic import TypeAdapter

from pydantic_settings import BaseSettings, SettingsConfigDict

JWT_OPTS_PREFIX = "jwt_opts_"

env_files_to_load = [".env"]
if os.getenv("ENV", "LOCAL") == "LOCAL":
env_files_to_load.append(".env.local")


class Settings(BaseSettings):
inst_conn: str = ""
inst_db_schema: str = "public"
auth_client: str = ""
auth_url: str = ""
token_url: str = ""
certs_url: str = ""
kc_url: str = ""
kc_realm: str = ""
kc_admin_client_id: str = ""
kc_admin_client_secret: str = ""
kc_realm_url: str = ""

def __init__(self, **data):
super().__init__(**data)
jwt_opts_adapter = TypeAdapter(int | bool)
self.jwt_opts = {
key.replace(JWT_OPTS_PREFIX, ""): jwt_opts_adapter.validate_python(value)
for (key, value) in self.model_extra.items()
if key.startswith(JWT_OPTS_PREFIX)
}

model_config = SettingsConfigDict(env_file=env_files_to_load, extra="allow")


try:
settings = Settings()
except Exception as e:
raise SystemExit(f"failed to set up settings [{e}]")
7 changes: 4 additions & 3 deletions src/entities/engine/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@
async_scoped_session,
)
from asyncio import current_task
from config import settings

DB_URL = os.getenv("INST_CONN")
DB_SCHEMA = os.getenv("INST_DB_SCHEMA", "public")
engine = create_async_engine(DB_URL, echo=True).execution_options(schema_translate_map={None: DB_SCHEMA})
engine = create_async_engine(settings.inst_conn, echo=True).execution_options(
schema_translate_map={None: settings.inst_db_schema}
)
SessionLocal = async_scoped_session(async_sessionmaker(engine, expire_on_commit=False), current_task)


Expand Down
8 changes: 4 additions & 4 deletions src/entities/models/dto.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class FinancialInsitutionDomainDto(FinancialInsitutionDomainBase):
lei: str

class Config:
orm_mode = True
from_attributes = True


class FinancialInstitutionBase(BaseModel):
Expand All @@ -26,7 +26,7 @@ class FinancialInstitutionDto(FinancialInstitutionBase):
lei: str

class Config:
orm_mode = True
from_attributes = True


class FinancialInstitutionWithDomainsDto(FinancialInstitutionDto):
Expand All @@ -37,13 +37,13 @@ class DeniedDomainDto(BaseModel):
domain: str

class Config:
orm_mode = True
from_attributes = True


class UserProfile(BaseModel):
first_name: str
last_name: str
leis: Optional[Set[str]]
leis: Set[str] = None

def to_keycloak_user(self):
return {"firstName": self.first_name, "lastName": self.last_name}
Expand Down
9 changes: 0 additions & 9 deletions src/env.py

This file was deleted.

6 changes: 3 additions & 3 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import os
import logging
import env # noqa: F401
from http import HTTPStatus
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import JSONResponse
Expand All @@ -12,6 +10,8 @@

from oauth2 import BearerTokenAuthBackend

from config import settings

log = logging.getLogger()

app = FastAPI()
Expand All @@ -32,7 +32,7 @@ async def general_exception_handler(request: Request, exception: Exception) -> J
)


oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl=os.getenv("AUTH_URL"), tokenUrl=os.getenv("TOKEN_URL"))
oauth2_scheme = OAuth2AuthorizationCodeBearer(authorizationUrl=settings.auth_url, tokenUrl=settings.token_url)

app.add_middleware(AuthenticationMiddleware, backend=BearerTokenAuthBackend(oauth2_scheme))
app.add_middleware(
Expand Down
43 changes: 10 additions & 33 deletions src/oauth2/oauth2_admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,19 @@

from keycloak import KeycloakAdmin, KeycloakOpenIDConnection, exceptions as kce

log = logging.getLogger(__name__)


def get_jwt_opts(opts_string: str) -> Dict[str, bool | int]:
"""
Parses out the opts_string into JWT options dictionary.
Args:
opts_string (str): comma separated key value pairs in the form of "key1:True,key2:False"
all options are boolean, with exception of 'leeway' being int
valid options can be found here:
https://github.com/mpdavis/python-jose/blob/4b0701b46a8d00988afcc5168c2b3a1fd60d15d8/jose/jwt.py#L81
from config import settings

Returns:
dict: dictionary of options supported by jwt, mentioned in link above
"""
jwt_opts = {}
if opts_string:
pairs = opts_string.split(",")
for pair in pairs:
[key, value] = pair.split(":", 1)
jwt_opts[key] = ast.literal_eval(value)
return jwt_opts


JWT_OPTS = get_jwt_opts(os.getenv("JWT_OPTS", ""))
log = logging.getLogger(__name__)


class OAuth2Admin:
def __init__(self) -> None:
self._keys = None
conn = KeycloakOpenIDConnection(
server_url=os.getenv("KC_URL"),
realm_name=os.getenv("KC_REALM"),
client_id=os.getenv("KC_ADMIN_CLIENT_ID"),
client_secret_key=os.getenv("KC_ADMIN_CLIENT_SECRET"),
server_url=settings.kc_url,
realm_name=settings.kc_realm,
client_id=settings.kc_admin_client_id,
client_secret_key=settings.kc_admin_client_secret,
)
self._admin = KeycloakAdmin(connection=conn)

Expand All @@ -54,16 +31,16 @@ def get_claims(self, token: str) -> Dict[str, str] | None:
return jose.jwt.decode(
token=token,
key=self._get_keys(),
issuer=os.getenv("KC_REALM_URL"),
audience=os.getenv("AUTH_CLIENT"),
options=JWT_OPTS,
issuer=settings.kc_realm_url,
audience=settings.auth_client,
options=settings.jwt_opts,
)
except jose.ExpiredSignatureError:
pass

def _get_keys(self) -> Dict[str, Any]:
if self._keys is None:
response = requests.get(os.getenv("CERTS_URL"))
response = requests.get(settings.certs_url)
self._keys = response.json()
return self._keys

Expand Down
15 changes: 0 additions & 15 deletions tests/oauth2/test_oauth2_admin.py

This file was deleted.

0 comments on commit 318d19e

Please sign in to comment.