Skip to content

Commit fdaef19

Browse files
authored
Merge pull request #42 from bugout-dev/ro-db-connection
Using ro db session for middlewares
2 parents 0107d4d + e38b2dc commit fdaef19

9 files changed

Lines changed: 117 additions & 39 deletions

File tree

brood/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
is_token_restricted_or_installation,
3636
get_current_user_or_installation,
3737
)
38-
from .external import yield_db_session_from_env
38+
from .db import yield_db_session_from_env
3939
from .version import BROOD_VERSION
4040
from .settings import (
4141
group_invite_link_from_env,

brood/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from . import data
1212
from . import exceptions
1313
from . import subscriptions
14-
from .external import SessionLocal
14+
from .db import SessionLocal
1515
from .models import (
1616
User,
1717
Group,

brood/db.py

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""
2+
Connections to external services
3+
"""
4+
from typing import Optional
5+
6+
from sqlalchemy import create_engine
7+
from sqlalchemy.orm.session import Session, sessionmaker
8+
9+
from .settings import (
10+
BROOD_DB_URI,
11+
BROOD_DB_URI_READ_ONLY,
12+
BROOD_POOL_SIZE,
13+
BROOD_DB_STATEMENT_TIMEOUT_MILLIS,
14+
)
15+
16+
17+
def create_brood_engine(url: Optional[str], pool_size: int, statement_timeout: int):
18+
# Pooling: https://docs.sqlalchemy.org/en/14/core/pooling.html#sqlalchemy.pool.QueuePool
19+
# Statement timeout: https://stackoverflow.com/a/44936982
20+
return create_engine(
21+
url=url,
22+
pool_size=pool_size,
23+
connect_args={"options": f"-c statement_timeout={statement_timeout}"},
24+
)
25+
26+
27+
engine = create_brood_engine(
28+
url=BROOD_DB_URI,
29+
pool_size=BROOD_POOL_SIZE,
30+
statement_timeout=BROOD_DB_STATEMENT_TIMEOUT_MILLIS,
31+
)
32+
SessionLocal = sessionmaker(bind=engine)
33+
34+
35+
def yield_db_session_from_env() -> Session:
36+
"""
37+
Creates an active database session using configuration from the environment and yields it as
38+
per FastAPI docs:
39+
https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-dependency
40+
41+
Behaves identically to db_session_from_env in all other respects.
42+
"""
43+
session = SessionLocal()
44+
try:
45+
yield session
46+
finally:
47+
session.close()
48+
49+
50+
# Read only
51+
RO_engine = create_brood_engine(
52+
url=BROOD_DB_URI_READ_ONLY,
53+
pool_size=BROOD_POOL_SIZE,
54+
statement_timeout=BROOD_DB_STATEMENT_TIMEOUT_MILLIS,
55+
)
56+
RO_SessionLocal = sessionmaker(bind=RO_engine)
57+
58+
59+
def yield_db_read_only_session() -> Session:
60+
"""
61+
Yields read only database connection (created using environment variables).
62+
As per FastAPI docs:
63+
https://fastapi.tiangolo.com/tutorial/sql-databases/#create-a-dependency
64+
"""
65+
session = RO_SessionLocal()
66+
try:
67+
yield session
68+
finally:
69+
session.close()

brood/external.py

Lines changed: 0 additions & 28 deletions
This file was deleted.

brood/middleware.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
from typing import Optional, Union
23
from uuid import UUID
34

@@ -11,9 +12,11 @@
1112
from . import actions
1213
from . import data
1314
from . import models
14-
from .external import yield_db_session_from_env
15+
from .db import yield_db_read_only_session
1516
from .settings import BOT_INSTALLATION_TOKEN, BOT_INSTALLATION_TOKEN_HEADER
1617

18+
logger = logging.getLogger(__name__)
19+
1720
# Login implementation follows:
1821
# https://fastapi.tiangolo.com/tutorial/security/simple-oauth2/
1922
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
@@ -22,20 +25,23 @@
2225

2326
async def get_current_user(
2427
token: UUID = Depends(oauth2_scheme),
25-
db_session=Depends(yield_db_session_from_env),
28+
db_session=Depends(yield_db_read_only_session),
2629
) -> models.User:
2730
try:
2831
token_object = actions.get_token(session=db_session, token=token)
2932
except actions.TokenNotFound:
3033
raise HTTPException(status_code=404, detail="Access token not found")
34+
except Exception:
35+
logger.error("Unhandled exception at get_current_user")
36+
raise HTTPException(status_code=500)
3137
if not token_object.active:
3238
raise HTTPException(status_code=403, detail="Token has expired")
3339
return token_object.user
3440

3541

3642
async def get_current_user_with_groups(
3743
token: UUID = Depends(oauth2_scheme),
38-
db_session=Depends(yield_db_session_from_env),
44+
db_session=Depends(yield_db_read_only_session),
3945
) -> data.UserWithGroupsResponse:
4046
try:
4147
token_active, user_extended = actions.get_current_user_with_groups(
@@ -44,6 +50,7 @@ async def get_current_user_with_groups(
4450
except actions.TokenNotFound:
4551
raise HTTPException(status_code=404, detail="Access token not found")
4652
except Exception:
53+
logger.error("Unhandled exception at get_current_user_with_groups")
4754
raise HTTPException(status_code=500)
4855
if not token_active:
4956
raise HTTPException(status_code=403, detail="Token has expired")
@@ -78,7 +85,7 @@ def autogenerated_user_token_check(request: Request) -> bool:
7885
async def get_current_user_or_installation(
7986
request: Request,
8087
token: UUID = Depends(oauth2_scheme_manual),
81-
db_session=Depends(yield_db_session_from_env),
88+
db_session=Depends(yield_db_read_only_session),
8289
) -> Union[models.User, bool]:
8390
"""
8491
Allow access if Bugout installation token provided, if not
@@ -97,7 +104,7 @@ async def get_current_user_or_installation(
97104
async def is_token_restricted_or_installation(
98105
request: Request,
99106
token: UUID = Depends(oauth2_scheme_manual),
100-
db_session=Depends(yield_db_session_from_env),
107+
db_session=Depends(yield_db_read_only_session),
101108
) -> bool:
102109
"""
103110
Allow access if Bugout installation provided.
@@ -114,10 +121,13 @@ async def is_token_restricted_or_installation(
114121

115122
async def is_token_restricted(
116123
token: UUID = Depends(oauth2_scheme),
117-
db_session=Depends(yield_db_session_from_env),
124+
db_session=Depends(yield_db_read_only_session),
118125
) -> bool:
119126
try:
120127
token_object = actions.get_token(session=db_session, token=token)
121128
except actions.TokenNotFound:
122129
raise HTTPException(status_code=404, detail="Access token not found")
130+
except Exception:
131+
logger.error("Unhandled exception at is_token_restricted")
132+
raise HTTPException(status_code=500)
123133
return token_object.restricted

brood/resources/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .version import BROOD_RESOURCES_VERSION
2222
from ..data import VersionResponse
2323
from .. import models as brood_models
24-
from ..external import yield_db_session_from_env
24+
from ..db import yield_db_session_from_env
2525
from ..middleware import get_current_user
2626
from ..settings import ORIGINS, DOCS_TARGET_PATH, BROOD_OPENAPI_LIST
2727

brood/resources/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import json
66

77
from .models import Resource
8-
from ..external import SessionLocal
8+
from ..db import SessionLocal
99

1010

1111
def resources_list_handler(args: argparse.Namespace) -> None:

brood/settings.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,34 @@
2626
)
2727
MOONSTREAM_APPLICATION_ID = os.environ.get("MOONSTREAM_APPLICATION_ID")
2828

29-
DB_URI = os.environ.get("BROOD_DB_URI")
29+
# Database
30+
BROOD_DB_URI = os.environ.get("BROOD_DB_URI")
31+
if BROOD_DB_URI is None:
32+
raise ValueError("BROOD_DB_URI environment variable not set")
33+
BROOD_DB_URI_READ_ONLY = os.environ.get("BROOD_DB_URI_READ_ONLY")
34+
if BROOD_DB_URI_READ_ONLY is None:
35+
raise ValueError("BROOD_DB_URI_READ_ONLY environment variable not set")
3036

37+
BROOD_POOL_SIZE_RAW = os.environ.get("BROOD_POOL_SIZE", 0)
38+
try:
39+
if BROOD_POOL_SIZE_RAW is not None:
40+
BROOD_POOL_SIZE = int(BROOD_POOL_SIZE_RAW)
41+
except:
42+
raise Exception(f"Could not parse BROOD_POOL_SIZE as int: {BROOD_POOL_SIZE_RAW}")
43+
44+
BROOD_DB_STATEMENT_TIMEOUT_MILLIS_RAW = os.environ.get(
45+
"BROOD_DB_STATEMENT_TIMEOUT_MILLIS"
46+
)
47+
BROOD_DB_STATEMENT_TIMEOUT_MILLIS = 10000
48+
try:
49+
if BROOD_DB_STATEMENT_TIMEOUT_MILLIS_RAW is not None:
50+
BROOD_DB_STATEMENT_TIMEOUT_MILLIS = int(BROOD_DB_STATEMENT_TIMEOUT_MILLIS_RAW)
51+
except:
52+
raise ValueError(
53+
f"BROOD_DB_STATEMENT_TIMEOUT_MILLIS must be an integer: {BROOD_DB_STATEMENT_TIMEOUT_MILLIS_RAW}"
54+
)
55+
56+
# Bots
3157
BOT_INSTALLATION_TOKEN = os.environ.get("BUGOUT_BOT_INSTALLATION_TOKEN")
3258
BOT_INSTALLATION_TOKEN_HEADER_RAW = os.environ.get(
3359
"BUGOUT_BOT_INSTALLATION_TOKEN_HEADER"

configs/sample.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Required environment variables
22
export BROOD_DB_URI="postgresql://<username>:<password>@<db_host>/<db_name>"
3+
export BROOD_DB_URI_READ_ONLY="postgresql://<username>:<password>@<db_host>/<db_name>"
34
export BROOD_CORS_ALLOWED_ORIGINS="http://localhost:3000,https://bugout.dev,https://www.bugout.dev"
45
export BUGOUT_WEB_URL="https://bugout.dev"
56
export BUGOUT_GROUP_FREE_SEATS=5

0 commit comments

Comments
 (0)