Skip to content

Commit 3cfc6fd

Browse files
committed
add worker skeleton fastapi setup
1 parent f7a80b3 commit 3cfc6fd

File tree

10 files changed

+435
-62
lines changed

10 files changed

+435
-62
lines changed

src/worker/.env.example

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# The following are required in all environments
2+
ENV="LOCAL"
3+
# Generate the following using `openssl rand -hex 32`
4+
SECRET_KEY = ""
5+
ALGORITHM = "HS256"
6+
ACCESS_TOKEN_EXPIRE_MINUTES = 120
7+
8+
# The username and password used to authenticate to this worker to trigger SFTP calls.
9+
# These are SECRET.
10+
USERNAME="tester-user"
11+
PASSWORD="tester-pw"
12+
13+
# GCP related keys. Retrieved for the service account with write access to GCS here: https://stackoverflow.com/questions/46287267/how-can-i-get-the-file-service-account-json-for-google-translate-api
14+
GCP_SERVICE_ACCOUNT_KEY_PATH=""
15+
16+
# SFTP related env vars
17+
SFTP_HOST=""
18+
SFTP_PORT=""
19+
SFTP_USER=""
20+
SFTP_PASSWORD=""
21+
22+
# API key to access the SST backend.
23+
BACKEND_API_KEY=""

src/worker/README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# REST API for SST peripheral jobs/actions.
2+
3+
There is no user database. This job has no access to the databases used by the SST.
4+
5+
### Prerequisites
6+
7+
In order to work with and test GCS related functionality, you'll need to setup default credentials:
8+
https://cloud.google.com/docs/authentication/set-up-adc-local-dev-environment#local-user-cred
9+
10+
You will also need to add the permission Storage Writer or Storage Admin to your Datakind Account in GCP to allow for local interaction with the storage buckets.
11+
12+
### For local testing:
13+
14+
Enter into the root directory of the repo.
15+
16+
17+
1. Copy the `.env.example` file to `.env`
18+
1. Run `export ENV_FILE_PATH='/full/path/to/.env'`
19+
1. `python3 -m venv .venv`
20+
1. `source .venv/bin/activate`
21+
1. `pip install uv`
22+
1. `uv sync --all-extras --dev`
23+
1. `coverage run -m pytest -v -s ./src/webapp/`
24+
25+
For all of the following, be in the repo root folder (`student-success-tool/`).
26+
27+
Spin up the app locally:
28+
29+
1. `fastapi dev src/webapp/main.py`
30+
1. Go to `http://127.0.0.1:8000/docs`
31+
1. Hit the `Authorize` button on the top right and enter the tester credentials:
32+
33+
* username: `tester-user`
34+
* password: `tester-pw`
35+
36+
Before committing, make sure to run:
37+
38+
1. `black src/webapp/.`
39+
1. Test using `coverage run -m pytest -v -s ./src/webapp/*.py`
40+
1. Test using `coverage run -m pytest -v -s ./src/webapp/routers/*.py`

src/worker/authn.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import jwt
2+
3+
from fastapi.security import (
4+
OAuth2PasswordBearer,
5+
OAuth2PasswordRequestForm,
6+
APIKeyHeader,
7+
APIKeyQuery,
8+
)
9+
from pydantic import BaseModel
10+
from datetime import timedelta, datetime, timezone
11+
from .config import env_vars
12+
from typing import Annotated
13+
from fastapi import Depends, HTTPException, status
14+
15+
16+
oauth2_scheme = OAuth2PasswordBearer(
17+
tokenUrl="token",
18+
)
19+
20+
21+
class Token(BaseModel):
22+
access_token: str
23+
token_type: str
24+
25+
26+
class TokenData(BaseModel):
27+
username: str | None = None
28+
29+
30+
def check_creds(username: str, password: str):
31+
if username == env_vars["USERNAME"] and password == env_vars["PASSWORD"]:
32+
return True
33+
raise HTTPException(
34+
status_code=status.HTTP_401_UNAUTHORIZED,
35+
detail="Creds for worker job not correct",
36+
)
37+
38+
39+
def create_access_token(data: dict, expires_delta: timedelta | None = None):
40+
to_encode = data.copy()
41+
if expires_delta:
42+
expire = datetime.now(timezone.utc) + expires_delta
43+
else:
44+
expire = datetime.now(timezone.utc) + timedelta(
45+
minutes=env_vars["ACCESS_TOKEN_EXPIRE_MINUTES"]
46+
)
47+
to_encode.update({"exp": expire})
48+
encoded_jwt = jwt.encode(
49+
to_encode, env_vars["SECRET_KEY"], algorithm=env_vars["ALGORITHM"]
50+
)
51+
return encoded_jwt
52+
53+
54+
async def get_current_username(
55+
token: Annotated[str, Depends(oauth2_scheme)],
56+
) -> str:
57+
credentials_exception = HTTPException(
58+
status_code=status.HTTP_401_UNAUTHORIZED,
59+
detail="Could not validate credentials",
60+
headers={"WWW-Authenticate": "Bearer"},
61+
)
62+
username = ""
63+
try:
64+
payload = jwt.decode(
65+
token, env_vars["SECRET_KEY"], algorithms=env_vars["ALGORITHM"]
66+
)
67+
username = payload.get("sub")
68+
if username is None:
69+
raise credentials_exception
70+
token_data = TokenData(username=username)
71+
except InvalidTokenError:
72+
raise credentials_exception
73+
if token_data.username != env_vars["USERNAME"]:
74+
raise credentials_exception
75+
return username

src/worker/authn_test.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Test file for authn.py.
2+
"""
3+
4+
import pytest
5+
6+
from fastapi import HTTPException
7+
8+
PASSWORD_STR = "pass123"
9+
10+
11+
def test_password_functions():
12+
"""Run tests on various password functions."""
13+
assert True

src/worker/config.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
"""Helper dict to retrieve OS env variables. This list includes all environment variables needed.
2+
"""
3+
4+
import os
5+
from dotenv import load_dotenv
6+
7+
# defaults to unit test values.
8+
env_vars = {
9+
"ENV": "LOCAL",
10+
"SECRET_KEY": "",
11+
"ALGORITHM": "HS256",
12+
"ACCESS_TOKEN_EXPIRE_MINUTES": "120",
13+
"USERNAME": "tester-user",
14+
"PASSWORD": "tester-pw",
15+
"BACKEND_API_KEY": "",
16+
}
17+
18+
gcs_vars = {
19+
"GCP_SERVICE_ACCOUNT_KEY_PATH": "",
20+
}
21+
22+
sftp_vars = {
23+
"SFTP_HOST": "",
24+
"SFTP_PORT": "",
25+
"SFTP_USER": "",
26+
"SFTP_PASSWORD": "",
27+
}
28+
29+
30+
# Setup function to get environment variables. Should be called at startup time.
31+
def startup_env_vars():
32+
env_file = os.environ.get("ENV_FILE_PATH")
33+
if not env_file:
34+
raise ValueError(
35+
"Missing .env filepath variable. Required. Set ENV_FILE_PATH to full path of .env file."
36+
)
37+
load_dotenv(env_file)
38+
global env_vars
39+
for name in env_vars:
40+
env_var = os.environ.get(name)
41+
if not env_var:
42+
raise ValueError(
43+
"Missing " + name + " value missing. Required environment variable."
44+
)
45+
if name == "ENV" and env_var not in [
46+
"PROD",
47+
"STAGING",
48+
"DEV",
49+
"LOCAL",
50+
]:
51+
raise ValueError(
52+
"ENV environment variable not one of: PROD, STAGING, DEV, LOCAL."
53+
)
54+
if (
55+
name == "ACCESS_TOKEN_EXPIRE_MINUTES"
56+
or name == "ACCESS_TOKEN_EXPIRE_MINUTES"
57+
) and not env_var.isdigit():
58+
raise ValueError(
59+
"ACCESS_TOKEN_EXPIRE_MINUTES and ACCESS_TOKEN_EXPIRE_MINUTES environment variables must be an int."
60+
)
61+
env_vars[name] = env_var
62+
if env_vars["ENV"] != "LOCAL":
63+
global gcs_vars
64+
for name in gcs_vars:
65+
env_var = os.environ.get(name)
66+
if not env_var:
67+
raise ValueError(
68+
"Missing "
69+
+ name
70+
+ " value missing. Required GCP environment variable."
71+
)
72+
gcs_vars[name] = env_var
73+
global sftp_vars
74+
for name in sftp_vars:
75+
env_var = os.environ.get(name)
76+
if not env_var:
77+
raise ValueError(
78+
"Missing "
79+
+ name
80+
+ " value missing. Required SFTP environment variable."
81+
)
82+
sftp_vars[name] = env_var

src/worker/index.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
<title>SST Worker</title>
77
</head>
88
<body>
9-
<p>SST Worker</p>
9+
SST Worker: this worker provides endpoints for
10+
<ul>
11+
<li>Pulling from SFTP</li>
12+
</ul>
1013
</body>
1114
</html>

0 commit comments

Comments
 (0)