the37lab-authlib 0.1.1756367559__py3-none-any.whl → 0.1.1756371198__py3-none-any.whl
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/auth.py +148 -28
- {the37lab_authlib-0.1.1756367559.dist-info → the37lab_authlib-0.1.1756371198.dist-info}/METADATA +1 -1
- {the37lab_authlib-0.1.1756367559.dist-info → the37lab_authlib-0.1.1756371198.dist-info}/RECORD +5 -5
- {the37lab_authlib-0.1.1756367559.dist-info → the37lab_authlib-0.1.1756371198.dist-info}/WHEEL +0 -0
- {the37lab_authlib-0.1.1756367559.dist-info → the37lab_authlib-0.1.1756371198.dist-info}/top_level.txt +0 -0
the37lab_authlib/auth.py
CHANGED
|
@@ -12,13 +12,21 @@ import logging
|
|
|
12
12
|
import os
|
|
13
13
|
from functools import wraps
|
|
14
14
|
from isodate import parse_duration
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
15
17
|
|
|
16
18
|
logging.basicConfig(level=logging.DEBUG)
|
|
17
19
|
logger = logging.getLogger(__name__)
|
|
18
20
|
|
|
19
21
|
class AuthManager:
|
|
20
|
-
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):
|
|
21
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()
|
|
22
30
|
if environment_prefix:
|
|
23
31
|
prefix = environment_prefix.upper() + '_'
|
|
24
32
|
db_dsn = os.getenv(f'{prefix}DATABASE_URL')
|
|
@@ -65,6 +73,9 @@ class AuthManager:
|
|
|
65
73
|
|
|
66
74
|
if app:
|
|
67
75
|
self.init_app(app)
|
|
76
|
+
|
|
77
|
+
# Start the background update thread
|
|
78
|
+
self._start_update_thread()
|
|
68
79
|
|
|
69
80
|
def _extract_token_from_header(self):
|
|
70
81
|
auth = request.authorization
|
|
@@ -96,17 +107,34 @@ class AuthManager:
|
|
|
96
107
|
}
|
|
97
108
|
try:
|
|
98
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
|
|
99
122
|
with self.db.get_cursor() as cur:
|
|
100
123
|
# First get the API token record
|
|
101
124
|
cur.execute("""
|
|
102
|
-
SELECT t.*, u
|
|
125
|
+
SELECT t.*, u.*, r.name as role_name FROM api_tokens t
|
|
103
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
|
|
104
129
|
WHERE t.id = %s
|
|
105
130
|
""", (parsed['id'],))
|
|
106
|
-
|
|
107
|
-
if not
|
|
131
|
+
results = cur.fetchall()
|
|
132
|
+
if not results:
|
|
108
133
|
raise AuthError('Invalid API token')
|
|
109
134
|
|
|
135
|
+
# Get the first row for token/user data (all rows will have same token/user data)
|
|
136
|
+
result = results[0]
|
|
137
|
+
|
|
110
138
|
# Verify the nonce
|
|
111
139
|
if not bcrypt.checkpw(parsed['nonce'].encode('utf-8'), result['token'].encode('utf-8')):
|
|
112
140
|
raise AuthError('Invalid API token')
|
|
@@ -115,29 +143,28 @@ class AuthManager:
|
|
|
115
143
|
if result['expires_at'] and result['expires_at'] < datetime.utcnow():
|
|
116
144
|
raise AuthError('API token has expired')
|
|
117
145
|
|
|
118
|
-
#
|
|
119
|
-
|
|
120
|
-
UPDATE api_tokens
|
|
121
|
-
SET last_used_at = %s
|
|
122
|
-
WHERE id = %s
|
|
123
|
-
""", (datetime.utcnow(), parsed['id']))
|
|
146
|
+
# Schedule last used timestamp update (asynchronous with 10s delay)
|
|
147
|
+
self._schedule_last_used_update(parsed['id'])
|
|
124
148
|
|
|
125
|
-
#
|
|
126
|
-
|
|
127
|
-
SELECT r.name FROM roles r
|
|
128
|
-
JOIN user_roles ur ON ur.role_id = r.id
|
|
129
|
-
WHERE ur.user_id = %s
|
|
130
|
-
""", (result['user_id'],))
|
|
131
|
-
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]
|
|
132
151
|
|
|
133
152
|
# Construct user object
|
|
134
|
-
|
|
153
|
+
user_data = {
|
|
135
154
|
'id': result['user_id'],
|
|
136
155
|
'username': result['username'],
|
|
137
156
|
'email': result['email'],
|
|
138
157
|
'real_name': result['real_name'],
|
|
139
158
|
'roles': roles
|
|
140
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
|
|
141
168
|
except ValueError:
|
|
142
169
|
raise AuthError('Invalid token format')
|
|
143
170
|
|
|
@@ -441,21 +468,42 @@ class AuthManager:
|
|
|
441
468
|
logger.debug(f"Token payload: {payload}")
|
|
442
469
|
user_id = int(payload['sub']) # Convert string ID back to integer
|
|
443
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
|
|
444
482
|
with self.db.get_cursor() as cur:
|
|
445
|
-
cur.execute("SELECT * FROM users WHERE id = %s", (user_id,))
|
|
446
|
-
user = cur.fetchone()
|
|
447
|
-
if not user:
|
|
448
|
-
logger.error(f"User not found for ID: {user_id}")
|
|
449
|
-
raise AuthError('User not found', 404)
|
|
450
|
-
# Fetch roles
|
|
451
483
|
cur.execute("""
|
|
452
|
-
SELECT r.name FROM
|
|
453
|
-
JOIN user_roles ur ON ur.
|
|
454
|
-
|
|
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
|
|
455
488
|
""", (user_id,))
|
|
456
|
-
|
|
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]
|
|
457
499
|
user['roles'] = roles
|
|
458
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
|
+
|
|
459
507
|
return user
|
|
460
508
|
except jwt.InvalidTokenError as e:
|
|
461
509
|
logger.error(f"Invalid token error: {str(e)}")
|
|
@@ -464,6 +512,78 @@ class AuthManager:
|
|
|
464
512
|
logger.error(f"Unexpected error during token validation: {str(e)}")
|
|
465
513
|
raise AuthError(str(e), 500)
|
|
466
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
|
+
|
|
467
587
|
def get_current_user(self):
|
|
468
588
|
return self._authenticate_request()
|
|
469
589
|
|
{the37lab_authlib-0.1.1756367559.dist-info → the37lab_authlib-0.1.1756371198.dist-info}/RECORD
RENAMED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
the37lab_authlib/__init__.py,sha256=cFVTWL-0YIMqwOMVy1P8mOt_bQODJp-L9bfp2QQ8CTo,132
|
|
2
|
-
the37lab_authlib/auth.py,sha256=
|
|
2
|
+
the37lab_authlib/auth.py,sha256=x5PlHnjIw4OSIn7dOYdPw-_Ef4306Y0rJeOaVN8efXY,28985
|
|
3
3
|
the37lab_authlib/db.py,sha256=cmnmykKvq6V5e-D0HGiRN4DjFBOGB-SL1HpFjR5uyCw,3162
|
|
4
4
|
the37lab_authlib/decorators.py,sha256=L-gJUUwDUT2JXTptQ6XEey1LkI5RprbqzEfArWI7F8Y,1305
|
|
5
5
|
the37lab_authlib/exceptions.py,sha256=mdplK5sKNtagPAzSGq5NGsrQ4r-k03DKJBKx6myWwZc,317
|
|
6
6
|
the37lab_authlib/models.py,sha256=-PlvQlHGIsSdrH0H9Cdh_vTPlltGV8G1Z1mmGQvAg9Y,3422
|
|
7
|
-
the37lab_authlib-0.1.
|
|
8
|
-
the37lab_authlib-0.1.
|
|
9
|
-
the37lab_authlib-0.1.
|
|
10
|
-
the37lab_authlib-0.1.
|
|
7
|
+
the37lab_authlib-0.1.1756371198.dist-info/METADATA,sha256=ODFfvXVF6rycV7_gsST1K8s5_sASh5K93Qdmrn5ZRVY,8319
|
|
8
|
+
the37lab_authlib-0.1.1756371198.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
the37lab_authlib-0.1.1756371198.dist-info/top_level.txt,sha256=6Jmxw4UeLrhfJXgRKbXWY4OhxRSaMs0dKKhNCGWWSwc,17
|
|
10
|
+
the37lab_authlib-0.1.1756371198.dist-info/RECORD,,
|
{the37lab_authlib-0.1.1756367559.dist-info → the37lab_authlib-0.1.1756371198.dist-info}/WHEEL
RENAMED
|
File without changes
|
|
File without changes
|