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.
Files changed (45) hide show
  1. oua_auth/__init__.py +57 -0
  2. oua_auth/admin.py +231 -0
  3. oua_auth/apps.py +66 -0
  4. oua_auth/authentication.py +1015 -0
  5. oua_auth/backend.py +177 -0
  6. oua_auth/logging_init.py +58 -0
  7. oua_auth/logging_utils.py +481 -0
  8. oua_auth/management/__init__.py +1 -0
  9. oua_auth/management/commands/__init__.py +1 -0
  10. oua_auth/management/commands/clean_expired_tokens.py +42 -0
  11. oua_auth/middleware.py +700 -0
  12. oua_auth/migrations/0001_initial.py +46 -0
  13. oua_auth/migrations/0002_suspicious_activity_and_user_security.py +109 -0
  14. oua_auth/migrations/__init__.py +1 -0
  15. oua_auth/models.py +233 -0
  16. oua_auth/security_middleware.py +96 -0
  17. oua_auth/token_blacklist.py +78 -0
  18. oua_auth-0.3.0.dist-info/METADATA +533 -0
  19. oua_auth-0.3.0.dist-info/RECORD +45 -0
  20. oua_auth-0.3.0.dist-info/WHEEL +5 -0
  21. oua_auth-0.3.0.dist-info/licenses/LICENSE +21 -0
  22. oua_auth-0.3.0.dist-info/top_level.txt +2 -0
  23. tests/__init__.py +1 -0
  24. tests/conftest.py +296 -0
  25. tests/settings.py +82 -0
  26. tests/test_admin.py +343 -0
  27. tests/test_admin_integration.py +195 -0
  28. tests/test_api_integration.py +412 -0
  29. tests/test_apps.py +110 -0
  30. tests/test_authentication.py +539 -0
  31. tests/test_authentication_admin.py +192 -0
  32. tests/test_authentication_extended.py +1029 -0
  33. tests/test_backend.py +290 -0
  34. tests/test_command_clean_expired_tokens.py +129 -0
  35. tests/test_integration.py +393 -0
  36. tests/test_logging.py +184 -0
  37. tests/test_logging_init.py +115 -0
  38. tests/test_logging_utils.py +241 -0
  39. tests/test_middleware.py +510 -0
  40. tests/test_middleware_integration.py +213 -0
  41. tests/test_security_middleware.py +137 -0
  42. tests/test_token_blacklist.py +341 -0
  43. tests/test_token_blacklist_memory.py +80 -0
  44. tests/test_user_security_profile.py +119 -0
  45. 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")