the37lab-authlib 0.1.1751369506__tar.gz → 0.1.1756371198__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of the37lab-authlib might be problematic. Click here for more details.
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/PKG-INFO +16 -1
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/README.md +15 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/pyproject.toml +1 -1
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/auth.py +169 -30
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/db.py +21 -4
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib.egg-info/PKG-INFO +16 -1
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/setup.cfg +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/__init__.py +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/decorators.py +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/exceptions.py +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/models.py +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib.egg-info/SOURCES.txt +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib.egg-info/dependency_links.txt +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib.egg-info/requires.txt +0 -0
- {the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: the37lab_authlib
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1756371198
|
|
4
4
|
Summary: Python SDK for the Authlib
|
|
5
5
|
Author-email: the37lab <info@the37lab.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -42,6 +42,9 @@ A Python authentication library that provides JWT, OAuth2, and API token authent
|
|
|
42
42
|
- [API Token Override for Testing](#api-token-override-for-testing)
|
|
43
43
|
- [Usage](#usage)
|
|
44
44
|
- [Warning](#warning)
|
|
45
|
+
- [User Override for Testing](#user-override-for-testing)
|
|
46
|
+
- [Usage](#usage-1)
|
|
47
|
+
- [Warning](#warning-1)
|
|
45
48
|
|
|
46
49
|
## Installation
|
|
47
50
|
|
|
@@ -233,3 +236,15 @@ For testing purposes, you can bypass the database and provide a static mapping o
|
|
|
233
236
|
Replace `MYAPP` with your environment prefix.
|
|
234
237
|
|
|
235
238
|
**Warning:** This method is intended only for testing and development. Do not use this approach in production environments.
|
|
239
|
+
|
|
240
|
+
## User Override for Testing
|
|
241
|
+
|
|
242
|
+
For testing purposes, you can force all authentication to return a specific user by setting the `{PREFIX}USER_OVERRIDE` environment variable:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
export MYAPP_USER_OVERRIDE="testuser"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
If set, all requests will be authenticated as the specified user, regardless of any tokens or credentials provided. This cannot be combined with `api_tokens` or `db_dsn`.
|
|
249
|
+
|
|
250
|
+
**Warning:** This method is intended only for testing and development. Do not use this approach in production environments.
|
|
@@ -25,6 +25,9 @@ A Python authentication library that provides JWT, OAuth2, and API token authent
|
|
|
25
25
|
- [API Token Override for Testing](#api-token-override-for-testing)
|
|
26
26
|
- [Usage](#usage)
|
|
27
27
|
- [Warning](#warning)
|
|
28
|
+
- [User Override for Testing](#user-override-for-testing)
|
|
29
|
+
- [Usage](#usage-1)
|
|
30
|
+
- [Warning](#warning-1)
|
|
28
31
|
|
|
29
32
|
## Installation
|
|
30
33
|
|
|
@@ -216,3 +219,15 @@ For testing purposes, you can bypass the database and provide a static mapping o
|
|
|
216
219
|
Replace `MYAPP` with your environment prefix.
|
|
217
220
|
|
|
218
221
|
**Warning:** This method is intended only for testing and development. Do not use this approach in production environments.
|
|
222
|
+
|
|
223
|
+
## User Override for Testing
|
|
224
|
+
|
|
225
|
+
For testing purposes, you can force all authentication to return a specific user by setting the `{PREFIX}USER_OVERRIDE` environment variable:
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
export MYAPP_USER_OVERRIDE="testuser"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
If set, all requests will be authenticated as the specified user, regardless of any tokens or credentials provided. This cannot be combined with `api_tokens` or `db_dsn`.
|
|
232
|
+
|
|
233
|
+
**Warning:** This method is intended only for testing and development. Do not use this approach in production environments.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "the37lab_authlib"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.1756371198"
|
|
8
8
|
description = "Python SDK for the Authlib"
|
|
9
9
|
authors = [{name = "the37lab", email = "info@the37lab.com"}]
|
|
10
10
|
dependencies = ["flask", "psycopg2-binary", "pyjwt", "python-dotenv", "requests", "authlib", "bcrypt"]
|
{the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/auth.py
RENAMED
|
@@ -11,12 +11,22 @@ import bcrypt
|
|
|
11
11
|
import logging
|
|
12
12
|
import os
|
|
13
13
|
from functools import wraps
|
|
14
|
+
from isodate import parse_duration
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
14
17
|
|
|
15
18
|
logging.basicConfig(level=logging.DEBUG)
|
|
16
19
|
logger = logging.getLogger(__name__)
|
|
17
20
|
|
|
18
21
|
class AuthManager:
|
|
19
|
-
def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer', environment_prefix=None, api_tokens=None):
|
|
22
|
+
def __init__(self, app=None, db_dsn=None, jwt_secret=None, oauth_config=None, id_type='integer', environment_prefix=None, api_tokens=None, cache_ttl=10):
|
|
23
|
+
self.user_override = None
|
|
24
|
+
self._user_cache = {}
|
|
25
|
+
self._cache_ttl = cache_ttl or 10 # 10 seconds
|
|
26
|
+
self._last_used_updates = {} # Track pending updates
|
|
27
|
+
self._update_lock = threading.Lock()
|
|
28
|
+
self._update_thread = None
|
|
29
|
+
self._shutdown_event = threading.Event()
|
|
20
30
|
if environment_prefix:
|
|
21
31
|
prefix = environment_prefix.upper() + '_'
|
|
22
32
|
db_dsn = os.getenv(f'{prefix}DATABASE_URL')
|
|
@@ -36,6 +46,15 @@ class AuthManager:
|
|
|
36
46
|
if ':' in entry:
|
|
37
47
|
key, user = entry.split(':', 1)
|
|
38
48
|
api_tokens[key.strip()] = user.strip()
|
|
49
|
+
user_override_env = os.getenv(f'{prefix}USER_OVERRIDE')
|
|
50
|
+
if user_override_env:
|
|
51
|
+
self.user_override = user_override_env
|
|
52
|
+
else:
|
|
53
|
+
prefix = ''
|
|
54
|
+
|
|
55
|
+
self.expiry_time = parse_duration(os.getenv(f'{prefix}JWT_TOKEN_EXPIRY_TIME', 'PT1H'))
|
|
56
|
+
if self.user_override and (api_tokens or db_dsn):
|
|
57
|
+
raise ValueError('Cannot set user_override together with api_tokens or db_dsn')
|
|
39
58
|
if api_tokens and db_dsn:
|
|
40
59
|
raise ValueError('Cannot set both api_tokens and db_dsn')
|
|
41
60
|
self.api_tokens = api_tokens or None
|
|
@@ -54,6 +73,9 @@ class AuthManager:
|
|
|
54
73
|
|
|
55
74
|
if app:
|
|
56
75
|
self.init_app(app)
|
|
76
|
+
|
|
77
|
+
# Start the background update thread
|
|
78
|
+
self._start_update_thread()
|
|
57
79
|
|
|
58
80
|
def _extract_token_from_header(self):
|
|
59
81
|
auth = request.authorization
|
|
@@ -85,17 +107,34 @@ class AuthManager:
|
|
|
85
107
|
}
|
|
86
108
|
try:
|
|
87
109
|
parsed = ApiToken.parse_token(api_token)
|
|
110
|
+
|
|
111
|
+
# Check cache first
|
|
112
|
+
cache_key = f"api_token_{parsed['id']}"
|
|
113
|
+
current_time = datetime.utcnow()
|
|
114
|
+
|
|
115
|
+
if cache_key in self._user_cache:
|
|
116
|
+
cached_data, cache_time = self._user_cache[cache_key]
|
|
117
|
+
if (current_time - cache_time).total_seconds() < self._cache_ttl:
|
|
118
|
+
logger.debug(f"Returning cached API token data for ID: {parsed['id']}")
|
|
119
|
+
return cached_data.copy() # Return a copy to avoid modifying cache
|
|
120
|
+
|
|
121
|
+
# Cache miss or expired, fetch from database
|
|
88
122
|
with self.db.get_cursor() as cur:
|
|
89
123
|
# First get the API token record
|
|
90
124
|
cur.execute("""
|
|
91
|
-
SELECT t.*, u
|
|
125
|
+
SELECT t.*, u.*, r.name as role_name FROM api_tokens t
|
|
92
126
|
JOIN users u ON t.user_id = u.id
|
|
127
|
+
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
|
128
|
+
LEFT JOIN roles r ON ur.role_id = r.id
|
|
93
129
|
WHERE t.id = %s
|
|
94
130
|
""", (parsed['id'],))
|
|
95
|
-
|
|
96
|
-
if not
|
|
131
|
+
results = cur.fetchall()
|
|
132
|
+
if not results:
|
|
97
133
|
raise AuthError('Invalid API token')
|
|
98
134
|
|
|
135
|
+
# Get the first row for token/user data (all rows will have same token/user data)
|
|
136
|
+
result = results[0]
|
|
137
|
+
|
|
99
138
|
# Verify the nonce
|
|
100
139
|
if not bcrypt.checkpw(parsed['nonce'].encode('utf-8'), result['token'].encode('utf-8')):
|
|
101
140
|
raise AuthError('Invalid API token')
|
|
@@ -104,33 +143,40 @@ class AuthManager:
|
|
|
104
143
|
if result['expires_at'] and result['expires_at'] < datetime.utcnow():
|
|
105
144
|
raise AuthError('API token has expired')
|
|
106
145
|
|
|
107
|
-
#
|
|
108
|
-
|
|
109
|
-
UPDATE api_tokens
|
|
110
|
-
SET last_used_at = %s
|
|
111
|
-
WHERE id = %s
|
|
112
|
-
""", (datetime.utcnow(), parsed['id']))
|
|
146
|
+
# Schedule last used timestamp update (asynchronous with 10s delay)
|
|
147
|
+
self._schedule_last_used_update(parsed['id'])
|
|
113
148
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
SELECT r.name FROM roles r
|
|
117
|
-
JOIN user_roles ur ON ur.role_id = r.id
|
|
118
|
-
WHERE ur.user_id = %s
|
|
119
|
-
""", (result['user_id'],))
|
|
120
|
-
roles = [row['name'] for row in cur.fetchall()]
|
|
149
|
+
# Extract roles from results
|
|
150
|
+
roles = [row['role_name'] for row in results if row['role_name'] is not None]
|
|
121
151
|
|
|
122
152
|
# Construct user object
|
|
123
|
-
|
|
153
|
+
user_data = {
|
|
124
154
|
'id': result['user_id'],
|
|
125
155
|
'username': result['username'],
|
|
126
156
|
'email': result['email'],
|
|
127
157
|
'real_name': result['real_name'],
|
|
128
158
|
'roles': roles
|
|
129
159
|
}
|
|
160
|
+
|
|
161
|
+
# Cache the result
|
|
162
|
+
self._user_cache[cache_key] = (user_data.copy(), current_time)
|
|
163
|
+
|
|
164
|
+
# Clean up expired cache entries
|
|
165
|
+
self._cleanup_cache()
|
|
166
|
+
|
|
167
|
+
return user_data
|
|
130
168
|
except ValueError:
|
|
131
169
|
raise AuthError('Invalid token format')
|
|
132
170
|
|
|
133
171
|
def _authenticate_request(self):
|
|
172
|
+
if self.user_override:
|
|
173
|
+
return {
|
|
174
|
+
'id': self.user_override,
|
|
175
|
+
'username': self.user_override,
|
|
176
|
+
'email': '',
|
|
177
|
+
'real_name': self.user_override,
|
|
178
|
+
'roles': []
|
|
179
|
+
}
|
|
134
180
|
auth_header = request.headers.get('Authorization')
|
|
135
181
|
api_token = request.headers.get('X-API-Token')
|
|
136
182
|
|
|
@@ -422,21 +468,42 @@ class AuthManager:
|
|
|
422
468
|
logger.debug(f"Token payload: {payload}")
|
|
423
469
|
user_id = int(payload['sub']) # Convert string ID back to integer
|
|
424
470
|
|
|
471
|
+
# Check cache first
|
|
472
|
+
cache_key = f"user_{user_id}"
|
|
473
|
+
current_time = datetime.utcnow()
|
|
474
|
+
|
|
475
|
+
if cache_key in self._user_cache:
|
|
476
|
+
cached_data, cache_time = self._user_cache[cache_key]
|
|
477
|
+
if (current_time - cache_time).total_seconds() < self._cache_ttl:
|
|
478
|
+
logger.debug(f"Returning cached user data for ID: {user_id}")
|
|
479
|
+
return cached_data.copy() # Return a copy to avoid modifying cache
|
|
480
|
+
|
|
481
|
+
# Cache miss or expired, fetch from database
|
|
425
482
|
with self.db.get_cursor() as cur:
|
|
426
|
-
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
427
|
-
user = cur.fetchone()
|
|
428
|
-
if not user:
|
|
429
|
-
logger.error(f"User not found for ID: {user_id}")
|
|
430
|
-
raise AuthError('User not found', 404)
|
|
431
|
-
# Fetch roles
|
|
432
483
|
cur.execute("""
|
|
433
|
-
SELECT r.name FROM
|
|
434
|
-
JOIN user_roles ur ON ur.
|
|
435
|
-
|
|
484
|
+
SELECT u.*, r.name as role_name FROM users u
|
|
485
|
+
LEFT JOIN user_roles ur ON ur.user_id = u.id
|
|
486
|
+
LEFT JOIN roles r ON ur.role_id = r.id
|
|
487
|
+
WHERE u.id = %s
|
|
436
488
|
""", (user_id,))
|
|
437
|
-
|
|
489
|
+
results = cur.fetchall()
|
|
490
|
+
if not results:
|
|
491
|
+
logger.error(f"User not found for ID: {user_id}")
|
|
492
|
+
raise AuthError('User not found', 404)
|
|
493
|
+
|
|
494
|
+
# Get the first row for user data (all rows will have same user data)
|
|
495
|
+
user = results[0]
|
|
496
|
+
|
|
497
|
+
# Extract roles from results
|
|
498
|
+
roles = [row['role_name'] for row in results if row['role_name'] is not None]
|
|
438
499
|
user['roles'] = roles
|
|
439
500
|
|
|
501
|
+
# Cache the result
|
|
502
|
+
self._user_cache[cache_key] = (user.copy(), current_time)
|
|
503
|
+
|
|
504
|
+
# Clean up expired cache entries
|
|
505
|
+
self._cleanup_cache()
|
|
506
|
+
|
|
440
507
|
return user
|
|
441
508
|
except jwt.InvalidTokenError as e:
|
|
442
509
|
logger.error(f"Invalid token error: {str(e)}")
|
|
@@ -445,6 +512,78 @@ class AuthManager:
|
|
|
445
512
|
logger.error(f"Unexpected error during token validation: {str(e)}")
|
|
446
513
|
raise AuthError(str(e), 500)
|
|
447
514
|
|
|
515
|
+
def _cleanup_cache(self):
|
|
516
|
+
"""Remove expired cache entries."""
|
|
517
|
+
current_time = datetime.utcnow()
|
|
518
|
+
expired_keys = [
|
|
519
|
+
key for key, (_, cache_time) in self._user_cache.items()
|
|
520
|
+
if (current_time - cache_time).total_seconds() >= self._cache_ttl
|
|
521
|
+
]
|
|
522
|
+
for key in expired_keys:
|
|
523
|
+
del self._user_cache[key]
|
|
524
|
+
|
|
525
|
+
def _start_update_thread(self):
|
|
526
|
+
"""Start the background thread for processing last_used_at updates."""
|
|
527
|
+
if self._update_thread is None or not self._update_thread.is_alive():
|
|
528
|
+
self._update_thread = threading.Thread(target=self._update_worker, daemon=True)
|
|
529
|
+
self._update_thread.start()
|
|
530
|
+
logger.debug("Started background update thread")
|
|
531
|
+
|
|
532
|
+
def _schedule_last_used_update(self, token_id):
|
|
533
|
+
"""Schedule a last_used_at update for an API token with 10s delay."""
|
|
534
|
+
with self._update_lock:
|
|
535
|
+
self._last_used_updates[token_id] = time.time()
|
|
536
|
+
logger.debug(f"Scheduled last_used update for token {token_id}")
|
|
537
|
+
|
|
538
|
+
def _update_worker(self):
|
|
539
|
+
"""Background worker that processes last_used_at updates."""
|
|
540
|
+
while not self._shutdown_event.is_set():
|
|
541
|
+
try:
|
|
542
|
+
current_time = time.time()
|
|
543
|
+
tokens_to_update = []
|
|
544
|
+
|
|
545
|
+
# Collect tokens that need updating (older than 10 seconds)
|
|
546
|
+
with self._update_lock:
|
|
547
|
+
for token_id, schedule_time in list(self._last_used_updates.items()):
|
|
548
|
+
if current_time - schedule_time >= 10: # 10 second delay
|
|
549
|
+
tokens_to_update.append(token_id)
|
|
550
|
+
del self._last_used_updates[token_id]
|
|
551
|
+
|
|
552
|
+
# Perform batch update
|
|
553
|
+
if tokens_to_update:
|
|
554
|
+
self._perform_batch_update(tokens_to_update)
|
|
555
|
+
|
|
556
|
+
# Sleep for a short interval
|
|
557
|
+
time.sleep(10)
|
|
558
|
+
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.error(f"Error in update worker: {e}")
|
|
561
|
+
time.sleep(5) # Wait longer on error
|
|
562
|
+
|
|
563
|
+
def _perform_batch_update(self, token_ids):
|
|
564
|
+
"""Perform batch update of last_used_at for multiple tokens."""
|
|
565
|
+
try:
|
|
566
|
+
with self.db.get_cursor() as cur:
|
|
567
|
+
# Update all tokens in a single query
|
|
568
|
+
placeholders = ','.join(['%s'] * len(token_ids))
|
|
569
|
+
cur.execute(f"""
|
|
570
|
+
UPDATE api_tokens
|
|
571
|
+
SET last_used_at = %s
|
|
572
|
+
WHERE id IN ({placeholders})
|
|
573
|
+
""", [datetime.utcnow()] + token_ids)
|
|
574
|
+
|
|
575
|
+
logger.debug(f"Updated last_used_at for {len(token_ids)} tokens: {token_ids}")
|
|
576
|
+
|
|
577
|
+
except Exception as e:
|
|
578
|
+
logger.error(f"Error performing batch update: {e}")
|
|
579
|
+
|
|
580
|
+
def shutdown(self):
|
|
581
|
+
"""Shutdown the background update thread."""
|
|
582
|
+
self._shutdown_event.set()
|
|
583
|
+
if self._update_thread and self._update_thread.is_alive():
|
|
584
|
+
self._update_thread.join(timeout=5)
|
|
585
|
+
logger.debug("Background update thread shutdown complete")
|
|
586
|
+
|
|
448
587
|
def get_current_user(self):
|
|
449
588
|
return self._authenticate_request()
|
|
450
589
|
|
|
@@ -473,12 +612,12 @@ class AuthManager:
|
|
|
473
612
|
def _create_token(self, user):
|
|
474
613
|
payload = {
|
|
475
614
|
'sub': str(user['id']),
|
|
476
|
-
'exp': datetime.utcnow() +
|
|
615
|
+
'exp': datetime.utcnow() + self.expiry_time,
|
|
477
616
|
'iat': datetime.utcnow()
|
|
478
617
|
}
|
|
479
618
|
logger.debug(f"Creating token with payload: {payload}")
|
|
480
619
|
token = jwt.encode(payload, self.jwt_secret, algorithm='HS256')
|
|
481
|
-
logger.
|
|
620
|
+
logger.info(f"Created token: {token}")
|
|
482
621
|
return token
|
|
483
622
|
|
|
484
623
|
def _create_refresh_token(self, user):
|
{the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/db.py
RENAMED
|
@@ -1,15 +1,28 @@
|
|
|
1
1
|
import psycopg2
|
|
2
2
|
from psycopg2.extras import RealDictCursor
|
|
3
|
+
from psycopg2 import pool
|
|
3
4
|
from contextlib import contextmanager
|
|
4
5
|
from .models import UUIDGenerator, IntegerGenerator
|
|
5
6
|
|
|
6
7
|
class Database:
|
|
7
|
-
def __init__(self, dsn, id_type='uuid'):
|
|
8
|
+
def __init__(self, dsn, id_type='uuid', min_conn=1, max_conn=10):
|
|
8
9
|
self.dsn = dsn
|
|
9
10
|
self.id_generator = UUIDGenerator() if id_type == 'uuid' else IntegerGenerator()
|
|
10
11
|
self.id_type = id_type
|
|
12
|
+
self.min_conn = min_conn
|
|
13
|
+
self.max_conn = max_conn
|
|
14
|
+
self._pool = None
|
|
15
|
+
self._init_pool()
|
|
11
16
|
self._init_db()
|
|
12
17
|
|
|
18
|
+
def _init_pool(self):
|
|
19
|
+
self._pool = pool.ThreadedConnectionPool(
|
|
20
|
+
self.min_conn,
|
|
21
|
+
self.max_conn,
|
|
22
|
+
self.dsn,
|
|
23
|
+
cursor_factory=RealDictCursor
|
|
24
|
+
)
|
|
25
|
+
|
|
13
26
|
def _init_db(self):
|
|
14
27
|
with self.get_connection() as conn:
|
|
15
28
|
with conn.cursor() as cur:
|
|
@@ -54,7 +67,7 @@ class Database:
|
|
|
54
67
|
|
|
55
68
|
@contextmanager
|
|
56
69
|
def get_connection(self):
|
|
57
|
-
conn =
|
|
70
|
+
conn = self._pool.getconn()
|
|
58
71
|
try:
|
|
59
72
|
yield conn
|
|
60
73
|
conn.commit()
|
|
@@ -62,7 +75,7 @@ class Database:
|
|
|
62
75
|
conn.rollback()
|
|
63
76
|
raise
|
|
64
77
|
finally:
|
|
65
|
-
|
|
78
|
+
self._pool.putconn(conn)
|
|
66
79
|
|
|
67
80
|
@contextmanager
|
|
68
81
|
def get_cursor(self):
|
|
@@ -71,4 +84,8 @@ class Database:
|
|
|
71
84
|
yield cur
|
|
72
85
|
|
|
73
86
|
def get_id_generator(self):
|
|
74
|
-
return self.id_generator
|
|
87
|
+
return self.id_generator
|
|
88
|
+
|
|
89
|
+
def close(self):
|
|
90
|
+
if self._pool:
|
|
91
|
+
self._pool.closeall()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: the37lab_authlib
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1756371198
|
|
4
4
|
Summary: Python SDK for the Authlib
|
|
5
5
|
Author-email: the37lab <info@the37lab.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
@@ -42,6 +42,9 @@ A Python authentication library that provides JWT, OAuth2, and API token authent
|
|
|
42
42
|
- [API Token Override for Testing](#api-token-override-for-testing)
|
|
43
43
|
- [Usage](#usage)
|
|
44
44
|
- [Warning](#warning)
|
|
45
|
+
- [User Override for Testing](#user-override-for-testing)
|
|
46
|
+
- [Usage](#usage-1)
|
|
47
|
+
- [Warning](#warning-1)
|
|
45
48
|
|
|
46
49
|
## Installation
|
|
47
50
|
|
|
@@ -233,3 +236,15 @@ For testing purposes, you can bypass the database and provide a static mapping o
|
|
|
233
236
|
Replace `MYAPP` with your environment prefix.
|
|
234
237
|
|
|
235
238
|
**Warning:** This method is intended only for testing and development. Do not use this approach in production environments.
|
|
239
|
+
|
|
240
|
+
## User Override for Testing
|
|
241
|
+
|
|
242
|
+
For testing purposes, you can force all authentication to return a specific user by setting the `{PREFIX}USER_OVERRIDE` environment variable:
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
export MYAPP_USER_OVERRIDE="testuser"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
If set, all requests will be authenticated as the specified user, regardless of any tokens or credentials provided. This cannot be combined with `api_tokens` or `db_dsn`.
|
|
249
|
+
|
|
250
|
+
**Warning:** This method is intended only for testing and development. Do not use this approach in production environments.
|
|
File without changes
|
{the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{the37lab_authlib-0.1.1751369506 → the37lab_authlib-0.1.1756371198}/src/the37lab_authlib/models.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|