PKB@9HJ³\ŽŽuseraudit/admin.pyfrom django.contrib import admin from useraudit import models as m class LogAdmin(admin.ModelAdmin): model = m.Log search_fields = ['username'] list_filter = ['timestamp'] list_display = ('username', 'ip_address', 'forwarded_by', 'user_agent', 'timestamp') list_display_links = None admin.site.register(m.LoginLog, LogAdmin) admin.site.register(m.FailedLoginLog, LogAdmin) PKH›9Hë‹o›&&useraudit/password_expiry.py""" Django password and account expiry. This will prevent users from logging in unless they have changed their password within a configurable password expiry period. Expired users can reset their password using the normal registration forms. It will disable unused accounts. If users haven't logged in for a certain time period, their account will be disabled next time a login is attemped. Requirement for account expiry: whichever user model is used should implement AbstractBaseUser (standard Django user model does of course). How to use: 1. Add "useraudit" to the list of INSTALLED_APPS. 2. Put expiry backend *first* in the list of auth backends:: AUTHENTICATION_BACKENDS = ( 'useraudit.password_expiry.AccountExpiryBackend', # ... the rest ... ) 3. Use either Option A or Option B depending on your app. - A: Django custom auth model - B: "Profile" model related OneToOne with stock django User Option A: Use a django custom auth model for your users and add a field for password expiry:: # settings.py AUTH_USER_MODEL = "myapp.MyUser" AUTH_USER_MODEL_PASSWORD_CHANGE_DATE_ATTR = "password_change_date" # models.py from django.contrib.auth.models import AbstractUser class MyUser(AbstractUser): password_change_date = models.DateTimeField( auto_now_add=True, null=True, ) Option B: Use your existing profile model or create a new one, and add a field for password expiry:: # settings.py AUTH_USER_MODEL_PASSWORD_CHANGE_DATE_ATTR = "myprofile.password_change_date" # models.py from django.db import models from django.contrib.auth.models import User class MyProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) password_change_date = models.DateTimeField( auto_now_add=True, null=True, ) 4. Configure the settings relevant to password expiry:: # How long a user's password is good for. None or 0 means no expiration. PASSWORD_EXPIRY_DAYS = 180 # How long before expiry will the frontend start bothering the user PASSWORD_EXPIRY_WARNING_DAYS = 30 # # Disable the user's account if they haven't logged in for this time # ACCOUNT_EXPIRY_DAYS = 100 5. Add log handlers for "django.security" if they aren't already there. 6. Inspect all non-standard login views and make sure they are checking for User.is_active. 7. Add code to your frontend to nag the user if their password is due to expire. Otherwise one day they will be unable to login and they won't know why. todo: add an automatic process for e-mailing users before password expiry 8. In your deployment scripts, include a daily cronjob to run the disable_inactive_users management command. This will let users know if their account has been disabled. It requires the Sites framework to be enabled, and for the user model to have an "email" attribute. """ from collections import namedtuple from functools import reduce from datetime import timedelta from django.conf import settings from django.contrib.auth import get_user_model from django.core.exceptions import PermissionDenied from django.db.models.signals import pre_save from django.dispatch import receiver, Signal from django.utils import timezone import logging from .backend import AuthFailedLoggerBackend logger = logging.getLogger("django.security") __all__ = ["AccountExpiryBackend", "password_has_expired", "account_has_expired"] password_has_expired = Signal(providing_args=["user"]) account_has_expired = Signal(providing_args=["user"]) @receiver(pre_save, sender=settings.AUTH_USER_MODEL) def set_password_changed(sender, instance=None, raw=False, **kwargs): attrs = ExpirySettings.get() # We're saving the password change date only for existing users # Users just created should be taken care of by auto_now_add. # This way we can assume that a User profile object already exists # for the user. This is essential, because password change detection # can happen only in pre_save, in post_save it is too late. is_existing_user = instance.pk is not None if not raw and is_existing_user and attrs.date_changed: update_date_changed(instance, attrs.date_changed) def update_date_changed(user, date_changed_attr): def did_password_change(user): current_user = get_user_model().objects.get(pk=user.pk) return current_user.password != user.password def save_profile_password_change_date(user, date): parts = date_changed_attr.split('.') attr_name = parts[-1] profile = reduce(lambda obj, attr: getattr(obj, attr), parts[:-1], user) setattr(profile, attr_name, date) profile.save() def set_password_change_date(user, date): setattr(user, date_changed_attr, date) if did_password_change(user): now = timezone.now() if '.' in date_changed_attr: save_profile_password_change_date(user, now) else: set_password_change_date(user, now) def is_password_expired(user): earliest = ExpirySettings.get().earliest_possible_password_change if earliest: change_date = get_password_change_date(user) return change_date and change_date < earliest return False def get_password_change_date(user): attr = ExpirySettings.get().date_changed if attr: val = user if isinstance(attr, str): for part in attr.split("."): if hasattr(val, part): val = getattr(val, part) else: logger.warning("User model does not have a %s attribute" % attr) return None return val else: logger.warning("Password change attr in settings is not a string") return None def get_user_last_login(user): if hasattr(user, "last_login"): return user.last_login else: logger.warning("User model doesn't have last_login field. ACCOUNT_EXPIRY_DAYS setting will have no effect.") return None def is_account_expired(user): earliest = ExpirySettings.get().earliest_possible_login if earliest: last_login = get_user_last_login(user) return last_login and last_login < earliest return False class ExpirySettings(namedtuple("ExpirySettings", ["num_days", "num_warning_days", "date_changed", "password", "account_expiry"])): @classmethod def get(cls): expiry = getattr(settings, "PASSWORD_EXPIRY_DAYS", None) or 0 warning = getattr(settings, "PASSWORD_EXPIRY_WARNING_DAYS", None) or 0 date_changed = getattr(settings, "AUTH_USER_MODEL_PASSWORD_CHANGE_DATE_ATTR", None) or None password = getattr(settings, "AUTH_USER_MODEL_PASSWORD_ATTR", None) or "password" account_expiry = getattr(settings, "ACCOUNT_EXPIRY_DAYS", None) or 0 return cls(expiry, warning, date_changed, password, account_expiry) @property def earliest_possible_login(self): if self.account_expiry > 0: return timezone.now() - timedelta(days=self.account_expiry) return None @property def earliest_possible_password_change(self): if self.num_days > 0: return timezone.now() - timedelta(days=self.num_days) return None class AccountExpiryBackend(object): """ This backend doesn't authenticate, it just prevents authentication of a user whose account password has expired. """ def authenticate(self, username=None, password=None, **kwargs): user = self._lookup_user(username, password, **kwargs) if user: # Prevent authentication of inactive users (if the user # model supports it). Django only checks is_active at the # login view level. if hasattr(user, "is_active") and not user.is_active: self._prevent_login(username, "Account is not active") if is_password_expired(user): password_has_expired.send(sender=user.__class__, user=user) self._prevent_login(username, "Password has expired") if is_account_expired(user): logger.info("Disabling stale user account: %s" % user) user.is_active = False user.save() account_has_expired.send(sender=user.__class__, user=user) self._prevent_login(username, "Account has expired") # pass on to next handler return None def _prevent_login(self, username, msg="User login prevented"): def is_failed_login_logger_configured(): auth_backends = getattr(settings, 'AUTHENTICATION_BACKENDS', []) return 'useraudit.backend.AuthFailedLoggerBackend' in auth_backends logger.info("Login Prevented for user '%s'! %s", username, msg) if is_failed_login_logger_configured(): AuthFailedLoggerBackend().authenticate(username=username) raise PermissionDenied(msg) def _lookup_user(self, username=None, password=None, **kwargs): # This is the same procedure as in # django.contrib.auth.backends.ModelBackend, except without # the timing attack mitigation, because it doesn't take long # to check for expiry. UserModel = get_user_model() if username is None: username = kwargs.get(UserModel.USERNAME_FIELD) try: return UserModel._default_manager.get_by_natural_key(username) except UserModel.DoesNotExist: return None PKùN2Huseraudit/__init__.pyPKÖƒ5HÕR6,U U useraudit/models.pyfrom django.db import models from django.contrib.auth.signals import user_logged_in class Log(models.Model): class Meta: abstract = True ordering = ['-timestamp'] username = models.CharField(max_length=255, null=True, blank=True) ip_address = models.CharField(max_length=40, null=True, blank=True, verbose_name = "IP") forwarded_by = models.CharField(max_length=1000, null=True, blank=True) user_agent = models.CharField(max_length=255, null=True, blank=True) timestamp = models.DateTimeField(auto_now_add=True) class FailedLoginLog(Log): pass class LoginLog(Log): pass class LoginLogger(object): def log_failed_login(self, username, request): fields = self.extract_log_info(username, request) log = FailedLoginLog.objects.create(**fields) def log_login(self, username, request): fields = self.extract_log_info(username, request) log = LoginLog.objects.create(**fields) def extract_log_info(self, username, request): if request: ip_address, proxies = self.extract_ip_address(request) user_agent = request.META.get('HTTP_USER_AGENT') else: ip_address = None proxies = None user_agent = None return { 'username': username, 'ip_address': ip_address, 'user_agent': user_agent, 'forwarded_by': ",".join(proxies or []) } def extract_ip_address(self, request): client_ip = request.META.get('REMOTE_ADDR') proxies = None forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') if forwarded_for is not None: closest_proxy = client_ip forwarded_for_ips = [ip.strip() for ip in forwarded_for.split(',')] client_ip = forwarded_for_ips.pop(0) forwarded_for_ips.reverse() proxies = [closest_proxy] + forwarded_for_ips return (client_ip, proxies) login_logger = LoginLogger() def login_callback(sender, user, request, **kwargs): login_logger.log_login(user.get_username(), request) # User logged in Django signal user_logged_in.connect(login_callback) # Import password expiry module so that the signal is registered. # The password expiry feature won't be active unless the necessary # settings are present. from . import password_expiry # noqa PKÖƒ5H­!²useraudit/views.pyfrom django.http import HttpResponse, HttpResponseNotFound from . import middleware def test_request_available(request): thread_request = middleware.get_request() if thread_request == request: return HttpResponse('OK') return HttpResponseNotFound() PK‚B9H¶<Æ9ÔÔuseraudit/test_settings.py# Django test settings for django useraudit project. from os import path DEBUG = True TEMPLATE_DEBUG = DEBUG ADMINS = ( # ('Your Name', 'your_email@example.com'), ) MANAGERS = ADMINS DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'mysql', 'sqlite3' or 'oracle'. 'NAME': 'db.sqlite3', # Or path to database file if using sqlite3. 'USER': '', # Not used with sqlite3. 'PASSWORD': '', # Not used with sqlite3. 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 'PORT': '', # Set to empty string for default. Not used with sqlite3. } } # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # although not all choices may be available on all operating systems. # In a Windows environment this must be set to your system time zone. TIME_ZONE = 'America/Chicago' # Language code for this installation. All choices can be found here: # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' SITE_ID = 1 # If you set this to False, Django will make some optimizations so as not # to load the internationalization machinery. USE_I18N = True # If you set this to False, Django will not format dates, numbers and # calendars according to the current locale. USE_L10N = True # If you set this to False, Django will not use timezone-aware datetimes. USE_TZ = False # Absolute filesystem path to the directory that will hold user-uploaded files. # Example: "/home/media/media.lawrence.com/media/" MEDIA_ROOT = '' # URL that handles the media served from MEDIA_ROOT. Make sure to use a # trailing slash. # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" MEDIA_URL = '' # Absolute path to the directory static files should be collected to. # Don't put anything in this directory yourself; store your static files # in apps' "static/" subdirectories and in STATICFILES_DIRS. # Example: "/home/media/media.lawrence.com/static/" STATIC_ROOT = '' # URL prefix for static files. # Example: "http://media.lawrence.com/static/" STATIC_URL = '/static/' # Additional locations of static files STATICFILES_DIRS = ( # Put strings here, like "/home/html/static" or "C:/www/django/static". # Always use forward slashes, even on Windows. # Don't forget to use absolute paths, not relative paths. ) # List of finder classes that know how to find static files in # various locations. STATICFILES_FINDERS = ( 'django.contrib.staticfiles.finders.FileSystemFinder', 'django.contrib.staticfiles.finders.AppDirectoriesFinder', # 'django.contrib.staticfiles.finders.DefaultStorageFinder', ) # Make this unique, and don't share it with anybody. SECRET_KEY = '7h(an4%q1ycpr_%18p2tmc#_@-qrp9nn8=m_w+f0(!+kjb!!ok' TEMPLATES = [{ "BACKEND": "django.template.backends.django.DjangoTemplates", "APP_DIRS": True, "OPTIONS": { "context_processors": [ "django.contrib.auth.context_processors.auth", "django.template.context_processors.debug", "django.template.context_processors.i18n", "django.template.context_processors.media", "django.template.context_processors.static", "django.template.context_processors.tz", "django.contrib.messages.context_processors.messages", ], }, }] MIDDLEWARE_CLASSES = ( 'useraudit.middleware.RequestToThreadLocalMiddleware', 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', # Uncomment the next line for simple clickjacking protection: # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) AUTHENTICATION_BACKENDS = ( 'django.contrib.auth.backends.ModelBackend', 'useraudit.backend.AuthFailedLoggerBackend' ) ROOT_URLCONF = 'useraudit.test_urls' # Python dotted path to the WSGI application used by Django's runserver. #WSGI_APPLICATION = 'django_useraudit.wsgi.application' INSTALLED_APPS = ( 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.sites', 'django.contrib.messages', 'django.contrib.staticfiles', # Uncomment the next line to enable the admin: 'django.contrib.admin', # Uncomment the next line to enable admin documentation: # 'django.contrib.admindocs', 'useraudit' ) # A sample logging configuration. The only tangible logging # performed by this configuration is to send an email to # the site admins on every HTTP 500 error when DEBUG=False. # See http://docs.djangoproject.com/en/dev/topics/logging for # more details on how to customize your logging configuration. LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'filters': { 'require_debug_false': { '()': 'django.utils.log.RequireDebugFalse' } }, 'handlers': { 'mail_admins': { 'level': 'ERROR', 'filters': ['require_debug_false'], 'class': 'django.utils.log.AdminEmailHandler' } }, 'loggers': { 'django.request': { 'handlers': ['mail_admins'], 'level': 'ERROR', 'propagate': True, }, } } PK­’9HYBlö**useraudit/backend.pyfrom django.contrib.auth import get_user_model from .models import LoginLogger from .middleware import get_request class AuthFailedLoggerBackend(object): supports_inactive_user = False def __init__(self): self.login_logger = LoginLogger() def authenticate(self, **credentials): UserModel = get_user_model() username = credentials.get(UserModel.USERNAME_FIELD, None) username = username or credentials.get('username') self.login_logger.log_failed_login(username, get_request()) return None PKk@9Ht׸kYYuseraudit/urls.pyfrom django.conf.urls import patterns, include, url # Uncomment the next two lines to enable the admin: # from django.contrib import admin # admin.autodiscover() urlpatterns = patterns('useraudit.views', # Examples: # url(r'^$', 'django_useraudit.views.home', name='home'), # url(r'^django_useraudit/', include('django_useraudit.foo.urls')), # Uncomment the admin/doc line below to enable admin documentation: # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), # Uncomment the next line to enable the admin: # url(r'^admin/', include(admin.site.urls)), ) PKùN2HwYõõuseraudit/middleware.pyimport threading thread_data = threading.local() def get_request(): return getattr(thread_data, 'request', None) class RequestToThreadLocalMiddleware(object): def process_request(self, request): thread_data.request = request PKÖƒ5H•:z“  useraudit/test_urls.pyfrom django.conf.urls import include, url from django.contrib import admin from .views import test_request_available admin.autodiscover() urlpatterns = [ url(r'^admin/', include(admin.site.urls)), url(r'test_request_available[/]?$', test_request_available), ] PKùN2H useraudit/migrations/__init__.pyPKùN2H–o~óó$useraudit/migrations/0001_initial.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ ] operations = [ migrations.CreateModel( name='FailedLoginLog', fields=[ ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), ('username', models.CharField(blank=True, null=True, max_length=255)), ('ip_address', models.CharField(blank=True, null=True, max_length=40, verbose_name='IP')), ('forwarded_by', models.CharField(blank=True, null=True, max_length=1000)), ('user_agent', models.CharField(blank=True, null=True, max_length=255)), ('timestamp', models.DateTimeField(auto_now_add=True)), ], options={ 'abstract': False, 'ordering': ['-timestamp'], }, ), migrations.CreateModel( name='LoginLog', fields=[ ('id', models.AutoField(serialize=False, primary_key=True, auto_created=True, verbose_name='ID')), ('username', models.CharField(blank=True, null=True, max_length=255)), ('ip_address', models.CharField(blank=True, null=True, max_length=40, verbose_name='IP')), ('forwarded_by', models.CharField(blank=True, null=True, max_length=1000)), ('user_agent', models.CharField(blank=True, null=True, max_length=255)), ('timestamp', models.DateTimeField(auto_now_add=True)), ], options={ 'abstract': False, 'ordering': ['-timestamp'], }, ), ] PK­@9Hx 辑 ‘ iuseraudit/south_migrations/0002_auto__add_field_loginlog_forwarded_by__add_field_failedloginlog_forwar.py# -*- coding: utf-8 -*- import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models class Migration(SchemaMigration): def forwards(self, orm): # Adding field 'LoginLog.forwarded_by' db.add_column('useraudit_loginlog', 'forwarded_by', self.gf('django.db.models.fields.CharField')(max_length=1000, null=True, blank=True), keep_default=False) # Adding field 'FailedLoginLog.forwarded_by' db.add_column('useraudit_failedloginlog', 'forwarded_by', self.gf('django.db.models.fields.CharField')(max_length=1000, null=True, blank=True), keep_default=False) def backwards(self, orm): # Deleting field 'LoginLog.forwarded_by' db.delete_column('useraudit_loginlog', 'forwarded_by') # Deleting field 'FailedLoginLog.forwarded_by' db.delete_column('useraudit_failedloginlog', 'forwarded_by') models = { 'useraudit.failedloginlog': { 'Meta': {'ordering': "['-timestamp']", 'object_name': 'FailedLoginLog'}, 'forwarded_by': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}) }, 'useraudit.loginlog': { 'Meta': {'ordering': "['-timestamp']", 'object_name': 'LoginLog'}, 'forwarded_by': ('django.db.models.fields.CharField', [], {'max_length': '1000', 'null': 'True', 'blank': 'True'}), 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}) } } complete_apps = ['useraudit'] PKùN2H&useraudit/south_migrations/__init__.pyPK@9HZY±L¥ ¥ *useraudit/south_migrations/0001_initial.py# -*- coding: utf-8 -*- import datetime from south.db import db from south.v2 import SchemaMigration from django.db import models class Migration(SchemaMigration): def forwards(self, orm): # Adding model 'FailedLoginLog' db.create_table('useraudit_failedloginlog', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('username', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), ('ip_address', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)), ('user_agent', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), )) db.send_create_signal('useraudit', ['FailedLoginLog']) # Adding model 'LoginLog' db.create_table('useraudit_loginlog', ( ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), ('username', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), ('ip_address', self.gf('django.db.models.fields.CharField')(max_length=40, null=True, blank=True)), ('user_agent', self.gf('django.db.models.fields.CharField')(max_length=255, null=True, blank=True)), ('timestamp', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), )) db.send_create_signal('useraudit', ['LoginLog']) def backwards(self, orm): # Deleting model 'FailedLoginLog' db.delete_table('useraudit_failedloginlog') # Deleting model 'LoginLog' db.delete_table('useraudit_loginlog') models = { 'useraudit.failedloginlog': { 'Meta': {'ordering': "['-timestamp']", 'object_name': 'FailedLoginLog'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}) }, 'useraudit.loginlog': { 'Meta': {'ordering': "['-timestamp']", 'object_name': 'LoginLog'}, 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), 'ip_address': ('django.db.models.fields.CharField', [], {'max_length': '40', 'null': 'True', 'blank': 'True'}), 'timestamp': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}), 'username': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True', 'blank': 'True'}) } } complete_apps = ['useraudit'] PKý9HvÌ8ss0django_useraudit-1.0.1.dist-info/DESCRIPTION.rstDjango user audit utilities like logging user log in, disabling access when password expires or user is inactive PKý9H4ài»++.django_useraudit-1.0.1.dist-info/metadata.json{"classifiers": ["Framework :: Django", "Intended Audience :: Developers", "Intended Audience :: System Administrators", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", "Topic :: Software Development"], "download_url": "https://github.com/muccg/django-useraudit/releases", "extensions": {"python.details": {"contacts": [{"email": "devops@ccg.murdoch.edu.au", "name": "CCG, Murdoch University", "role": "author"}], "document_names": {"description": "DESCRIPTION.rst"}, "project_urls": {"Home": "https://github.com/muccg/django-useraudit"}}}, "generator": "bdist_wheel (0.26.0)", "metadata_version": "2.0", "name": "django-useraudit", "summary": "Django user audit utilities like logging user log in, disabling access when password expires or user is inactive", "version": "1.0.1"}PKý9H0Àfs .django_useraudit-1.0.1.dist-info/top_level.txtuseraudit PKf‹9H“×2)django_useraudit-1.0.1.dist-info/zip-safe PKý9HŒ''\\&django_useraudit-1.0.1.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any PKý9HCÉF)django_useraudit-1.0.1.dist-info/METADATAMetadata-Version: 2.0 Name: django-useraudit Version: 1.0.1 Summary: Django user audit utilities like logging user log in, disabling access when password expires or user is inactive Home-page: https://github.com/muccg/django-useraudit Author: CCG, Murdoch University Author-email: devops@ccg.murdoch.edu.au License: UNKNOWN Download-URL: https://github.com/muccg/django-useraudit/releases Platform: UNKNOWN Classifier: Framework :: Django Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Operating System :: OS Independent Classifier: Topic :: Software Development Django user audit utilities like logging user log in, disabling access when password expires or user is inactive PKý9Hå€W©©'django_useraudit-1.0.1.dist-info/RECORDdjango_useraudit-1.0.1.dist-info/DESCRIPTION.rst,sha256=He8W0TWrWewUFxiF3ASX7u4lwYfQL2XVjY2tUlsJzjs,115 django_useraudit-1.0.1.dist-info/METADATA,sha256=f7frI2qzPDv75wRPlnidOcBFxttksQLQO-pfU-7J6UQ,794 django_useraudit-1.0.1.dist-info/RECORD,, django_useraudit-1.0.1.dist-info/WHEEL,sha256=JTb7YztR8fkPg6aSjc571Q4eiVHCwmUDlX8PhuuqIIE,92 django_useraudit-1.0.1.dist-info/metadata.json,sha256=B-laotHpGEhxb3vdSx8061u4IzeQzGuai9_rpsvy5FM,811 django_useraudit-1.0.1.dist-info/top_level.txt,sha256=NPzAggL9F0Qv-j_icqAoGplxt0MorJIv9oECTPaV-fA,10 django_useraudit-1.0.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 useraudit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 useraudit/admin.py,sha256=RtJO_5h_w1beMS6TOD5lVNKc8-I5ZSgki08J3fMvn1M,398 useraudit/backend.py,sha256=Pk_eWt3iBA0cCmcF13ySmM3-5kB7_TxPquPjZqXSTIs,554 useraudit/middleware.py,sha256=zJ3_bdqWHHuq7kIXbnuospui4tiJqyNcnCaOP9r3S0c,245 useraudit/models.py,sha256=LKhXMW2ouVpEFScrHGtmMWFx6P5FT9pxvRcym1n_tCI,2389 useraudit/password_expiry.py,sha256=JS2Fs4dIFSa4-9I_Nt-bbGfhcr1pw_TZ22Hqv7jGFyE,9759 useraudit/test_settings.py,sha256=MT1DeZ6BVk4ZLoqNkHCI32N0BU5COziUfJ_3O1VBa6o,5588 useraudit/test_urls.py,sha256=tCWcRjy3ivnI9SPllHFT_W6A6KRdUA7uZd9bGm-i1sA,269 useraudit/urls.py,sha256=8f0VO27SVYeEouJg4-YxMF-bWwKl56ekCwFzhsu80XA,601 useraudit/views.py,sha256=D1a9JGd4-DEU4hmY_a3TDDzOISKUxWqMLQhz4Sa2zF0,271 useraudit/migrations/0001_initial.py,sha256=jX-CGOT-qBgb2GirBOCexZrmFDIW79Qm_vLxVe5gxjA,1779 useraudit/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 useraudit/south_migrations/0001_initial.py,sha256=kyBn6MwqIp6hMZLldvqR75405SomqfvCMtY1qkJlinQ,3237 useraudit/south_migrations/0002_auto__add_field_loginlog_forwarded_by__add_field_failedloginlog_forwar.py,sha256=pToWUatEGZEFfq6QrdDMS2r2CfZgAF3F5o7JiwkAWrM,2705 useraudit/south_migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 PKB@9HJ³\ŽŽuseraudit/admin.pyPKH›9Hë‹o›&&¾useraudit/password_expiry.pyPKùN2H(useraudit/__init__.pyPKÖƒ5HÕR6,U U J(useraudit/models.pyPKÖƒ5H­!²Ð1useraudit/views.pyPK‚B9H¶<Æ9ÔÔ3useraudit/test_settings.pyPK­’9HYBlö**Iuseraudit/backend.pyPKk@9Ht׸kYYwKuseraudit/urls.pyPKùN2HwYõõÿMuseraudit/middleware.pyPKÖƒ5H•:z“  )Ouseraudit/test_urls.pyPKùN2H jPuseraudit/migrations/__init__.pyPKùN2H–o~óó$¨Puseraudit/migrations/0001_initial.pyPK­@9Hx 辑 ‘ iÝWuseraudit/south_migrations/0002_auto__add_field_loginlog_forwarded_by__add_field_failedloginlog_forwar.pyPKùN2H&õbuseraudit/south_migrations/__init__.pyPK@9HZY±L¥ ¥ *9cuseraudit/south_migrations/0001_initial.pyPKý9HvÌ8ss0&pdjango_useraudit-1.0.1.dist-info/DESCRIPTION.rstPKý9H4ài»++.çpdjango_useraudit-1.0.1.dist-info/metadata.jsonPKý9H0Àfs .^tdjango_useraudit-1.0.1.dist-info/top_level.txtPKf‹9H“×2)´tdjango_useraudit-1.0.1.dist-info/zip-safePKý9HŒ''\\&ütdjango_useraudit-1.0.1.dist-info/WHEELPKý9HCÉF)œudjango_useraudit-1.0.1.dist-info/METADATAPKý9Hå€W©©'ýxdjango_useraudit-1.0.1.dist-info/RECORDPKðë€