oua-auth 0.3.0__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.
- oua_auth/__init__.py +57 -0
- oua_auth/admin.py +231 -0
- oua_auth/apps.py +66 -0
- oua_auth/authentication.py +1015 -0
- oua_auth/backend.py +177 -0
- oua_auth/logging_init.py +58 -0
- oua_auth/logging_utils.py +481 -0
- oua_auth/management/__init__.py +1 -0
- oua_auth/management/commands/__init__.py +1 -0
- oua_auth/management/commands/clean_expired_tokens.py +42 -0
- oua_auth/middleware.py +700 -0
- oua_auth/migrations/0001_initial.py +46 -0
- oua_auth/migrations/0002_suspicious_activity_and_user_security.py +109 -0
- oua_auth/migrations/__init__.py +1 -0
- oua_auth/models.py +233 -0
- oua_auth/security_middleware.py +96 -0
- oua_auth/token_blacklist.py +78 -0
- oua_auth-0.3.0.dist-info/METADATA +533 -0
- oua_auth-0.3.0.dist-info/RECORD +45 -0
- oua_auth-0.3.0.dist-info/WHEEL +5 -0
- oua_auth-0.3.0.dist-info/licenses/LICENSE +21 -0
- oua_auth-0.3.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/conftest.py +296 -0
- tests/settings.py +82 -0
- tests/test_admin.py +343 -0
- tests/test_admin_integration.py +195 -0
- tests/test_api_integration.py +412 -0
- tests/test_apps.py +110 -0
- tests/test_authentication.py +539 -0
- tests/test_authentication_admin.py +192 -0
- tests/test_authentication_extended.py +1029 -0
- tests/test_backend.py +290 -0
- tests/test_command_clean_expired_tokens.py +129 -0
- tests/test_integration.py +393 -0
- tests/test_logging.py +184 -0
- tests/test_logging_init.py +115 -0
- tests/test_logging_utils.py +241 -0
- tests/test_middleware.py +510 -0
- tests/test_middleware_integration.py +213 -0
- tests/test_security_middleware.py +137 -0
- tests/test_token_blacklist.py +341 -0
- tests/test_token_blacklist_memory.py +80 -0
- tests/test_user_security_profile.py +119 -0
- tests/urls.py +37 -0
oua_auth/__init__.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Organization Unified Access (OUA) - Django Authentication.
|
|
3
|
+
|
|
4
|
+
This package provides authentication integration with OUA SSO server for Django projects.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.3.0"
|
|
8
|
+
|
|
9
|
+
# Import and initialize logging first to ensure it's ready early
|
|
10
|
+
try:
|
|
11
|
+
from .logging_init import initialize_logging
|
|
12
|
+
|
|
13
|
+
initialize_logging()
|
|
14
|
+
except (ImportError, ModuleNotFoundError):
|
|
15
|
+
import logging
|
|
16
|
+
|
|
17
|
+
logging.basicConfig(level=logging.INFO)
|
|
18
|
+
logging.warning("OUA Auth logging initialization failed, using basic configuration")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Define a lazy loading mechanism for imports
|
|
22
|
+
class LazyLoader:
|
|
23
|
+
"""Lazily load modules only when accessed."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, import_path):
|
|
26
|
+
self.import_path = import_path
|
|
27
|
+
self._module = None
|
|
28
|
+
|
|
29
|
+
def __getattr__(self, name):
|
|
30
|
+
if self._module is None:
|
|
31
|
+
import importlib
|
|
32
|
+
|
|
33
|
+
self._module = importlib.import_module(self.import_path)
|
|
34
|
+
return getattr(self._module, name)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# Define all modules with a clean API through proxy objects
|
|
38
|
+
authentication = LazyLoader(".authentication")
|
|
39
|
+
middleware = LazyLoader(".middleware")
|
|
40
|
+
backend = LazyLoader(".backend")
|
|
41
|
+
security_middleware = LazyLoader(".security_middleware")
|
|
42
|
+
logging_utils = LazyLoader(".logging_utils")
|
|
43
|
+
models = LazyLoader(".models")
|
|
44
|
+
|
|
45
|
+
# Define __all__ to expose the intended public API
|
|
46
|
+
__all__ = [
|
|
47
|
+
"__version__",
|
|
48
|
+
"authentication",
|
|
49
|
+
"middleware",
|
|
50
|
+
"backend",
|
|
51
|
+
"security_middleware",
|
|
52
|
+
"logging_utils",
|
|
53
|
+
"models",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Note: Token blacklist initialization should be moved to the app's ready() method
|
|
57
|
+
# rather than initialized here during module import
|
oua_auth/admin.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Admin configuration for the oua_auth app."""
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
from django.utils import timezone
|
|
5
|
+
from django.utils.html import format_html
|
|
6
|
+
from django.shortcuts import render, redirect
|
|
7
|
+
from django.urls import path
|
|
8
|
+
from django.contrib import messages
|
|
9
|
+
from .models import BlacklistedToken, SuspiciousActivity, UserSecurityProfile
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@admin.register(BlacklistedToken)
|
|
13
|
+
class BlacklistedTokenAdmin(admin.ModelAdmin):
|
|
14
|
+
"""Admin interface for BlacklistedToken."""
|
|
15
|
+
|
|
16
|
+
list_display = (
|
|
17
|
+
"token_hash_truncated",
|
|
18
|
+
"blacklisted_by",
|
|
19
|
+
"created_at",
|
|
20
|
+
"expires_at",
|
|
21
|
+
"is_expired",
|
|
22
|
+
)
|
|
23
|
+
list_filter = ("created_at", "expires_at")
|
|
24
|
+
search_fields = ("token_hash", "blacklisted_by", "reason")
|
|
25
|
+
readonly_fields = ("token_hash", "created_at")
|
|
26
|
+
date_hierarchy = "created_at"
|
|
27
|
+
ordering = ("-created_at",)
|
|
28
|
+
actions = ["delete_expired_tokens", "extend_expiration"]
|
|
29
|
+
|
|
30
|
+
def token_hash_truncated(self, obj):
|
|
31
|
+
"""Display truncated token hash for better readability."""
|
|
32
|
+
return obj.token_hash[:10] + "..."
|
|
33
|
+
|
|
34
|
+
token_hash_truncated.short_description = "Token Hash"
|
|
35
|
+
|
|
36
|
+
def is_expired(self, obj):
|
|
37
|
+
"""Display if the token is expired with color coding."""
|
|
38
|
+
is_expired = obj.expires_at < timezone.now()
|
|
39
|
+
return format_html(
|
|
40
|
+
'<span style="color: {};">{}</span>',
|
|
41
|
+
"red" if is_expired else "green",
|
|
42
|
+
"Expired" if is_expired else "Active",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
is_expired.short_description = "Status"
|
|
46
|
+
|
|
47
|
+
def get_urls(self):
|
|
48
|
+
"""Add custom URLs for admin actions."""
|
|
49
|
+
urls = super().get_urls()
|
|
50
|
+
custom_urls = [
|
|
51
|
+
path(
|
|
52
|
+
"clean_expired/",
|
|
53
|
+
self.admin_site.admin_view(self.clean_expired_view),
|
|
54
|
+
name="oua_auth_blacklistedtoken_clean_expired",
|
|
55
|
+
),
|
|
56
|
+
]
|
|
57
|
+
return custom_urls + urls
|
|
58
|
+
|
|
59
|
+
def clean_expired_view(self, request):
|
|
60
|
+
"""View for cleaning expired tokens."""
|
|
61
|
+
if request.method == "POST":
|
|
62
|
+
count = BlacklistedToken.clean_expired_tokens()
|
|
63
|
+
self.message_user(
|
|
64
|
+
request, f"{count} expired tokens were removed.", messages.SUCCESS
|
|
65
|
+
)
|
|
66
|
+
return redirect("..")
|
|
67
|
+
|
|
68
|
+
return render(
|
|
69
|
+
request,
|
|
70
|
+
"admin/clean_expired_confirm.html",
|
|
71
|
+
{
|
|
72
|
+
"title": "Clean Expired Tokens",
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def delete_expired_tokens(self, request, queryset):
|
|
77
|
+
"""Action to delete expired tokens."""
|
|
78
|
+
count = BlacklistedToken.clean_expired_tokens()
|
|
79
|
+
self.message_user(
|
|
80
|
+
request, f"{count} expired tokens were removed.", messages.SUCCESS
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
delete_expired_tokens.short_description = "Delete all expired tokens"
|
|
84
|
+
|
|
85
|
+
def extend_expiration(self, request, queryset):
|
|
86
|
+
"""Extend the expiration of selected tokens by 24 hours."""
|
|
87
|
+
count = 0
|
|
88
|
+
for token in queryset:
|
|
89
|
+
token.expires_at = token.expires_at + timezone.timedelta(days=1)
|
|
90
|
+
token.save()
|
|
91
|
+
count += 1
|
|
92
|
+
self.message_user(
|
|
93
|
+
request,
|
|
94
|
+
f"Extended expiration for {count} tokens by 24 hours.",
|
|
95
|
+
messages.SUCCESS,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
extend_expiration.short_description = "Extend expiration by 24 hours"
|
|
99
|
+
|
|
100
|
+
def has_add_permission(self, request):
|
|
101
|
+
"""
|
|
102
|
+
Disable direct token creation through admin as tokens should only
|
|
103
|
+
be blacklisted through the API.
|
|
104
|
+
"""
|
|
105
|
+
return False
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class SuspiciousActivityAdmin(admin.ModelAdmin):
|
|
109
|
+
"""Admin configuration for SuspiciousActivity model."""
|
|
110
|
+
|
|
111
|
+
list_display = (
|
|
112
|
+
"user_identifier",
|
|
113
|
+
"activity_type",
|
|
114
|
+
"ip_address",
|
|
115
|
+
"timestamp",
|
|
116
|
+
"details_truncated",
|
|
117
|
+
)
|
|
118
|
+
list_filter = ("activity_type", "timestamp")
|
|
119
|
+
search_fields = ("user_identifier", "ip_address", "activity_type", "details")
|
|
120
|
+
readonly_fields = ("timestamp",)
|
|
121
|
+
actions = ["cleanup_old_activities"]
|
|
122
|
+
|
|
123
|
+
def details_truncated(self, obj):
|
|
124
|
+
"""Display truncated details for better readability."""
|
|
125
|
+
if not obj.details:
|
|
126
|
+
return "—"
|
|
127
|
+
details = obj.details
|
|
128
|
+
if len(details) > 50:
|
|
129
|
+
return details[:47] + "..."
|
|
130
|
+
return details
|
|
131
|
+
|
|
132
|
+
details_truncated.short_description = "Details"
|
|
133
|
+
|
|
134
|
+
def cleanup_old_activities(self, request, queryset):
|
|
135
|
+
"""Action to clean up old activities."""
|
|
136
|
+
count = SuspiciousActivity.cleanup_old_activities(days=30)
|
|
137
|
+
self.message_user(
|
|
138
|
+
request,
|
|
139
|
+
f"{count} old activities were removed (older than 30 days).",
|
|
140
|
+
messages.SUCCESS,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
cleanup_old_activities.short_description = "Delete activities older than 30 days"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class UserSecurityProfileAdmin(admin.ModelAdmin):
|
|
147
|
+
"""Admin configuration for UserSecurityProfile model."""
|
|
148
|
+
|
|
149
|
+
list_display = (
|
|
150
|
+
"user_email",
|
|
151
|
+
"is_locked",
|
|
152
|
+
"locked_until",
|
|
153
|
+
"failed_login_attempts",
|
|
154
|
+
"last_failed_login",
|
|
155
|
+
)
|
|
156
|
+
list_filter = ("is_locked", "last_failed_login")
|
|
157
|
+
search_fields = ("user__email", "user__username", "lock_reason")
|
|
158
|
+
actions = ["unlock_accounts", "reset_failed_attempts"]
|
|
159
|
+
readonly_fields = ("last_failed_login", "last_login_ip")
|
|
160
|
+
|
|
161
|
+
def user_email(self, obj):
|
|
162
|
+
"""Display user email for better identification."""
|
|
163
|
+
return obj.user.email
|
|
164
|
+
|
|
165
|
+
user_email.short_description = "User"
|
|
166
|
+
|
|
167
|
+
def unlock_accounts(self, request, queryset):
|
|
168
|
+
"""Action to unlock selected accounts."""
|
|
169
|
+
count = 0
|
|
170
|
+
for profile in queryset:
|
|
171
|
+
if profile.is_locked:
|
|
172
|
+
profile.unlock_account()
|
|
173
|
+
count += 1
|
|
174
|
+
self.message_user(request, f"Unlocked {count} accounts.", messages.SUCCESS)
|
|
175
|
+
|
|
176
|
+
unlock_accounts.short_description = "Unlock selected accounts"
|
|
177
|
+
|
|
178
|
+
def reset_failed_attempts(self, request, queryset):
|
|
179
|
+
"""Action to reset failed login attempts."""
|
|
180
|
+
count = 0
|
|
181
|
+
for profile in queryset:
|
|
182
|
+
if profile.failed_login_attempts > 0:
|
|
183
|
+
profile.failed_login_attempts = 0
|
|
184
|
+
profile.save(update_fields=["failed_login_attempts"])
|
|
185
|
+
count += 1
|
|
186
|
+
self.message_user(
|
|
187
|
+
request, f"Reset failed attempts for {count} accounts.", messages.SUCCESS
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
reset_failed_attempts.short_description = "Reset failed login attempts"
|
|
191
|
+
|
|
192
|
+
def get_urls(self):
|
|
193
|
+
"""Add custom URLs for admin actions."""
|
|
194
|
+
urls = super().get_urls()
|
|
195
|
+
custom_urls = [
|
|
196
|
+
path(
|
|
197
|
+
"create_missing_profiles/",
|
|
198
|
+
self.admin_site.admin_view(self.create_profiles_view),
|
|
199
|
+
name="oua_auth_usersecurityprofile_create_missing",
|
|
200
|
+
),
|
|
201
|
+
]
|
|
202
|
+
return custom_urls + urls
|
|
203
|
+
|
|
204
|
+
def create_profiles_view(self, request):
|
|
205
|
+
"""View for creating missing security profiles."""
|
|
206
|
+
if request.method == "POST":
|
|
207
|
+
count = UserSecurityProfile.auto_create_profiles()
|
|
208
|
+
self.message_user(
|
|
209
|
+
request, f"Created {count} new security profiles.", messages.SUCCESS
|
|
210
|
+
)
|
|
211
|
+
return redirect("..")
|
|
212
|
+
|
|
213
|
+
return render(
|
|
214
|
+
request,
|
|
215
|
+
"admin/create_profiles_confirm.html",
|
|
216
|
+
{
|
|
217
|
+
"title": "Create Missing Security Profiles",
|
|
218
|
+
},
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# Register models with custom admin interfaces
|
|
223
|
+
admin.site.register(SuspiciousActivity, SuspiciousActivityAdmin)
|
|
224
|
+
|
|
225
|
+
# Only register UserSecurityProfile if it's defined (it may not be defined during migrations)
|
|
226
|
+
try:
|
|
227
|
+
from .models import UserSecurityProfile
|
|
228
|
+
|
|
229
|
+
admin.site.register(UserSecurityProfile, UserSecurityProfileAdmin)
|
|
230
|
+
except (ImportError, admin.sites.AlreadyRegistered):
|
|
231
|
+
pass
|
oua_auth/apps.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Django application configuration for OUA Auth.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from django.apps import AppConfig
|
|
6
|
+
import logging
|
|
7
|
+
from django.db.models.signals import post_migrate
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def perform_post_migration_tasks(sender, **kwargs):
|
|
13
|
+
"""
|
|
14
|
+
Perform tasks that require database access after migrations complete.
|
|
15
|
+
|
|
16
|
+
This function is connected to the post_migrate signal to ensure
|
|
17
|
+
database operations happen only after the database is fully set up.
|
|
18
|
+
"""
|
|
19
|
+
# Initialize token blacklist
|
|
20
|
+
try:
|
|
21
|
+
from .token_blacklist import initialize_token_blacklist
|
|
22
|
+
|
|
23
|
+
initialize_token_blacklist()
|
|
24
|
+
logger.info("Token blacklist initialized successfully")
|
|
25
|
+
except Exception as e:
|
|
26
|
+
logger.warning(f"Failed to initialize token blacklist: {e}")
|
|
27
|
+
|
|
28
|
+
# Auto-create security profiles for users without them
|
|
29
|
+
try:
|
|
30
|
+
from .models import UserSecurityProfile
|
|
31
|
+
|
|
32
|
+
if hasattr(UserSecurityProfile, "auto_create_profiles"):
|
|
33
|
+
count = UserSecurityProfile.auto_create_profiles()
|
|
34
|
+
if count > 0:
|
|
35
|
+
logger.info(f"Created {count} missing user security profiles")
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.warning(f"Failed to create user security profiles: {e}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class OUAAppConfig(AppConfig):
|
|
41
|
+
"""
|
|
42
|
+
OUA Auth application configuration.
|
|
43
|
+
|
|
44
|
+
This configures the OUA authentication app and initializes
|
|
45
|
+
logging and other required services.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
name = "oua_auth"
|
|
49
|
+
verbose_name = "Organization Unified Access Authentication"
|
|
50
|
+
|
|
51
|
+
def ready(self):
|
|
52
|
+
"""
|
|
53
|
+
Initialize the application when Django is ready.
|
|
54
|
+
|
|
55
|
+
This method is called when the Django application registry is fully populated.
|
|
56
|
+
"""
|
|
57
|
+
# Initialize logging (this doesn't require database access)
|
|
58
|
+
from .logging_init import initialize_logging
|
|
59
|
+
|
|
60
|
+
initialize_logging()
|
|
61
|
+
|
|
62
|
+
# Connect the post_migrate signal handler for database operations
|
|
63
|
+
# This ensures database operations happen only after migrations
|
|
64
|
+
post_migrate.connect(perform_post_migration_tasks, sender=self)
|
|
65
|
+
|
|
66
|
+
logger.info("OUA Auth application initialized")
|