Skip to content

Commit e999e91

Browse files
committed
Very basic tests, but all passing
1 parent 7ea8106 commit e999e91

File tree

7 files changed

+242
-37
lines changed

7 files changed

+242
-37
lines changed

Diff for: README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ Environment variables:
7171
- It being compromised compromises the security of the API
7272
- `FASTAPI_SIMPLE_SECURITY_HIDE_DOCS`: Whether or not to hide the API key related endpoints from the documentation
7373
- `FASTAPI_SIMPLE_SECURITY_DB_LOCATION`: Location of the local sqlite database file
74-
- /app/sqlite.db by default
75-
- When running the app inside Docker, use a bind mount for persistence.
74+
- `sqlite.db` in the running directory by default
75+
- When running the app inside Docker, use a bind mount for persistence
7676
- `FAST_API_SIMPLE_SECURITY_AUTOMATIC_EXPIRATION`: Duration, in days, until an API key is deemed expired
7777
- 15 days by default
7878

Diff for: app/main.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,16 @@
55
app = FastAPI()
66

77

8+
@app.get("/unsecured")
9+
async def unsecured_endpoint():
10+
return {"message": "This is an unsecured endpoint"}
11+
12+
813
@app.get("/secure", dependencies=[Depends(fastapi_simple_security.api_key_security)])
914
async def secure_endpoint():
1015
return {"message": "This is a secure endpoint"}
1116

1217

13-
app.include_router(fastapi_simple_security.api_key_router, prefix="/auth", tags=["_auth"])
18+
app.include_router(
19+
fastapi_simple_security.api_key_router, prefix="/auth", tags=["_auth"]
20+
)

Diff for: fastapi_simple_security/_sqlite_access.py

+45-16
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ def __init__(self):
1111
try:
1212
self.db_location = os.environ["FASTAPI_SIMPLE_SECURITY_DB_LOCATION"]
1313
except KeyError:
14-
self.db_location = "app/sqlite.db"
14+
self.db_location = "sqlite.db"
1515

1616
try:
17-
self.expiration_limit = int(os.environ["FAST_API_SIMPLE_SECURITY_AUTOMATIC_EXPIRATION"])
17+
self.expiration_limit = int(
18+
os.environ["FAST_API_SIMPLE_SECURITY_AUTOMATIC_EXPIRATION"]
19+
)
1820
except KeyError:
1921
self.expiration_limit = 15
2022

@@ -51,7 +53,9 @@ def create_key(self, never_expire) -> str:
5153
api_key,
5254
1,
5355
1 if never_expire else 0,
54-
(datetime.utcnow() + timedelta(days=self.expiration_limit)).isoformat(timespec="seconds"),
56+
(
57+
datetime.utcnow() + timedelta(days=self.expiration_limit)
58+
).isoformat(timespec="seconds"),
5559
None,
5660
0,
5761
),
@@ -83,34 +87,47 @@ def renew_key(self, api_key: str, new_expiration_date: str) -> Optional[str]:
8387

8488
# Previously revoked key. Issue a text warning and reactivate it.
8589
if response[0] == 0:
86-
response_lines.append("This API key was revoked and has been reactivated.")
90+
response_lines.append(
91+
"This API key was revoked and has been reactivated."
92+
)
8793
# Expired key. Issue a text warning and reactivate it.
88-
if (not response[3]) and (datetime.fromisoformat(response[2]) < datetime.utcnow()):
94+
if (not response[3]) and (
95+
datetime.fromisoformat(response[2]) < datetime.utcnow()
96+
):
8997
response_lines.append("This API key was expired and is now renewed.")
9098

9199
if not new_expiration_date:
92-
parsed_expiration_date = (datetime.utcnow() + timedelta(days=self.expiration_limit)).isoformat(
93-
timespec="seconds"
94-
)
100+
parsed_expiration_date = (
101+
datetime.utcnow() + timedelta(days=self.expiration_limit)
102+
).isoformat(timespec="seconds")
95103
else:
96104
try:
97105
# We parse and re-write to the right timespec
98-
parsed_expiration_date = datetime.fromisoformat(new_expiration_date).isoformat(timespec="seconds")
106+
parsed_expiration_date = datetime.fromisoformat(
107+
new_expiration_date
108+
).isoformat(timespec="seconds")
99109
except ValueError:
100-
return "The expiration date could not be parsed. Please use ISO 8601."
110+
return (
111+
"The expiration date could not be parsed. Please use ISO 8601."
112+
)
101113

102114
c.execute(
103115
"""
104116
UPDATE fastapi_simple_security
105117
SET expiration_date = ?, is_active = 1
106118
WHERE api_key = ?
107119
""",
108-
(parsed_expiration_date, api_key,),
120+
(
121+
parsed_expiration_date,
122+
api_key,
123+
),
109124
)
110125

111126
connection.commit()
112127

113-
response_lines.append(f"The new expiration date for the API key is {parsed_expiration_date}")
128+
response_lines.append(
129+
f"The new expiration date for the API key is {parsed_expiration_date}"
130+
)
114131

115132
return " ".join(response_lines)
116133

@@ -162,15 +179,24 @@ def check_key(self, api_key: str) -> bool:
162179
# Inactive
163180
or response[0] != 1
164181
# Expired key
165-
or ((not response[3]) and (datetime.fromisoformat(response[2]) < datetime.utcnow()))
182+
or (
183+
(not response[3])
184+
and (datetime.fromisoformat(response[2]) < datetime.utcnow())
185+
)
166186
):
167187
# The key is not valid
168188
return False
169189
else:
170190
# The key is valid
171191

172192
# We run the logging in a separate thread as writing takes some time
173-
threading.Thread(target=self._update_usage, args=(api_key, response[1],)).start()
193+
threading.Thread(
194+
target=self._update_usage,
195+
args=(
196+
api_key,
197+
response[1],
198+
),
199+
).start()
174200

175201
# We return directly
176202
return True
@@ -186,7 +212,11 @@ def _update_usage(self, api_key: str, usage_count: int):
186212
SET total_queries = ?, latest_query_date = ?
187213
WHERE api_key = ?
188214
""",
189-
(usage_count + 1, datetime.utcnow().isoformat(timespec="seconds"), api_key),
215+
(
216+
usage_count + 1,
217+
datetime.utcnow().isoformat(timespec="seconds"),
218+
api_key,
219+
),
190220
)
191221

192222
connection.commit()
@@ -201,7 +231,6 @@ def get_usage_stats(self) -> List[Tuple[str, int, str, str, int]]:
201231
with sqlite3.connect(self.db_location) as connection:
202232
c = connection.cursor()
203233

204-
# TODO Add filtering somehow
205234
c.execute(
206235
"""
207236
SELECT api_key, is_active, never_expire, expiration_date, latest_query_date, total_queries

Diff for: poetry.lock

+131-15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)