PK>HpOhuseraudit/admin.pyfrom django.core.urlresolvers import reverse from django.contrib.auth import get_user_model from 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 class LoginAttemptAdmin(admin.ModelAdmin): model = m.LoginAttempt list_display = ('username', 'count', 'timestamp', 'activate') list_display_links = None def activate(self, obj): UserModel = get_user_model() try: user = UserModel._default_manager.get_by_natural_key(obj.username) if user.is_active: return "Active" activation_url = reverse("reactivate_user", args=[user.id,]) return "Activate" % activation_url except UserModel.DoesNotExist: return "N/A" activate.short_description = "Status" activate.allow_tags = True admin.site.register(m.LoginLog, LogAdmin) admin.site.register(m.FailedLoginLog, LogAdmin) admin.site.register(m.LoginAttempt, LoginAttemptAdmin)PK,4HqZ&&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 from .signals import password_has_expired, account_has_expired logger = logging.getLogger("django.security") __all__ = ["AccountExpiryBackend"] @receiver(pre_save, sender=settings.AUTH_USER_MODEL) def user_pre_save(sender, instance=None, raw=False, **kwargs): user = instance 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_new_user = user.pk is None if is_new_user or raw: return if attrs.date_changed: update_date_changed(user, attrs.date_changed) # User has been re-activated. Ensure the last_login is set to None so # that the user isn't inactivated on next login by the AccountExpiryBackend current_user = sender.objects.get(pk=user.pk) if not current_user.is_active and user.is_active: user.last_login = None 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,4HLA<useraudit/signals.pyfrom django.dispatch import Signal password_has_expired = Signal(providing_args=["user"]) account_has_expired = Signal(providing_args=["user"]) login_failure_limit_reached = Signal(providing_args=["user"]) PKN2Huseraudit/__init__.pyPK](HA~ ~ useraudit/models.pyfrom django.db import models from django.contrib.auth.signals import user_logged_in import datetime class LoginAttempt(models.Model): username = models.CharField(max_length=255, null=True, blank=True) count = models.PositiveIntegerField(null=True, blank=True, default=0) timestamp = models.DateTimeField(auto_now_add=True) class LoginAttemptLogger(object): def reset(self, username): defaults = { 'count': 0, 'timestamp': datetime.datetime.now() } LoginAttempt.objects.update_or_create(username=username, defaults=defaults) def increment(self, username): obj, created = LoginAttempt.objects.get_or_create(username=username) obj.count += 1 obj.timestamp = datetime.datetime.now() obj.save() 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() login_attempt_logger = LoginAttemptLogger() def login_callback(sender, user, request, **kwargs): login_logger.log_login(user.get_username(), request) login_attempt_logger.reset(user.get_username()) # 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>Hkuseraudit/views.pyfrom django.http import HttpResponse, HttpResponseNotFound, HttpResponseRedirect from django.core.urlresolvers import reverse from django.contrib.auth import get_user_model from .models import LoginAttemptLogger from . import middleware login_attempt_logger = LoginAttemptLogger() def test_request_available(request): thread_request = middleware.get_request() if thread_request == request: return HttpResponse('OK') return HttpResponseNotFound() def reactivate_user(request, user_id): user = _get_user(user_id) user.is_active = True user.save() login_attempt_logger.reset(user.username) return HttpResponseRedirect(reverse("admin:useraudit_loginattempt_changelist")) def _get_user(user_id): UserModel = get_user_model() try: return UserModel.objects.get(id=user_id) except UserModel.DoesNotExist: logger.warning("User model for user_id %d not found" % user_id) return None PKB9H<9useraudit/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,4HR useraudit/backend.pyimport logging from django.contrib.auth import get_user_model from django.conf import settings from django.core.exceptions import PermissionDenied from django.dispatch import receiver, Signal from django.db.models.signals import pre_save from .signals import login_failure_limit_reached from .models import LoginLogger, LoginAttempt from .models import LoginAttemptLogger from .middleware import get_request logger = logging.getLogger("django.security") @receiver(pre_save, sender=settings.AUTH_USER_MODEL) def user_pre_save(sender, instance=None, raw=False, **kwargs): user = instance is_new_user = user.pk is None if is_new_user or raw: return # User has been re-activated. Ensure the failed login counter is set to 0 so # that the user isn't inactivated on next login by the AuthFailedLoggerBackend current_user = sender.objects.get(pk=user.pk) if not current_user.is_active and user.is_active: LoginAttemptLogger().reset(user.username) class AuthFailedLoggerBackend(object): def __init__(self): self.login_logger = LoginLogger() self.login_failure_limit = getattr(settings, 'LOGIN_FAILURE_LIMIT', None) or 0 self.login_attempt_logger = LoginAttemptLogger() def authenticate(self, **credentials): UserModel = get_user_model() self.username = credentials.get(get_user_model().USERNAME_FIELD) self.login_logger.log_failed_login(self.username, get_request()) self.login_attempt_logger.increment(self.username) self.block_user_if_needed() return None def block_user_if_needed(self): if not self.is_login_failure_limit_enabled(): return logger.debug("Login failure limit is enabled") if self.is_attempts_exceeded(): self._deactivate_user() user = self._get_user() login_failure_limit_reached.send(sender=user.__class__, user=user) logger.info("Login Prevented for user '%s'! Maximum failed logins %d reached!", self.username, self.login_failure_limit) raise PermissionDenied("Username '%s' has been blocked" % self.username) def is_login_failure_limit_enabled(self): return self.login_failure_limit > 0 def is_attempts_exceeded(self,): count = self._get_count() if count and count >= self.login_failure_limit: return True return False def _get_count(self): try: obj = LoginAttempt.objects.get(username=self.username) return obj.count except LoginAttempt.DoesNotExist: return None def _get_user(self): UserModel = get_user_model() try: return UserModel._default_manager.get_by_natural_key(self.username) except UserModel.DoesNotExist: logger.warning("User model for username %s not found" % self.username) return None def _deactivate_user(self): user = self._get_user() if user: user.is_active = False user.save(update_fields=["is_active"]) logger.warning("Username '%s' has been blocked" % self.username) return True return False PK>HuRuseraudit/urls.pyfrom django.conf.urls import patterns, include, url from .views import reactivate_user app_name = "useraudit" urlpatterns = [ url(r'/reactivate/(?P\d+)[/]?$', reactivate_user, name="reactivate_user"), ] PKN2HwYuseraudit/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), ] PKN2H useraudit/migrations/__init__.pyPK>HW/useraudit/migrations/0003_auto_20160406_1434.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('useraudit', '0002_loginattempt'), ] operations = [ migrations.AlterField( model_name='loginattempt', name='count', field=models.PositiveIntegerField(default=0, null=True, blank=True), ), ] PK>Hx)useraudit/migrations/0002_loginattempt.py# -*- coding: utf-8 -*- from __future__ import unicode_literals from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ ('useraudit', '0001_initial'), ] operations = [ migrations.CreateModel( name='LoginAttempt', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('username', models.CharField(max_length=255, null=True, blank=True)), ('count', models.PositiveIntegerField(null=True, blank=True)), ('timestamp', models.DateTimeField(auto_now_add=True)), ], ), ] PKN2Ho~$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'] PKN2H&useraudit/south_migrations/__init__.pyPK@9HZYL *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 6Hv8ss0django_useraudit-1.2.0.dist-info/DESCRIPTION.rstDjango user audit utilities like logging user log in, disabling access when password expires or user is inactive PK 6Hck++.django_useraudit-1.2.0.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.2.0"}PK 6H0fs .django_useraudit-1.2.0.dist-info/top_level.txtuseraudit PKf9H2)django_useraudit-1.2.0.dist-info/zip-safe PK 6Hndnn&django_useraudit-1.2.0.dist-info/WHEELWheel-Version: 1.0 Generator: bdist_wheel (0.26.0) Root-Is-Purelib: true Tag: py2-none-any Tag: py3-none-any PK 6Hݳ)django_useraudit-1.2.0.dist-info/METADATAMetadata-Version: 2.0 Name: django-useraudit Version: 1.2.0 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 6Hq'django_useraudit-1.2.0.dist-info/RECORDdjango_useraudit-1.2.0.dist-info/DESCRIPTION.rst,sha256=He8W0TWrWewUFxiF3ASX7u4lwYfQL2XVjY2tUlsJzjs,115 django_useraudit-1.2.0.dist-info/METADATA,sha256=d1lAoW4tXzohMkbOsN9TftiysaijuC2m20glv-ybPtc,794 django_useraudit-1.2.0.dist-info/RECORD,, django_useraudit-1.2.0.dist-info/WHEEL,sha256=GrqQvamwgBV4nLoJe0vhYRSWzWsx7xjlt74FT0SWYfE,110 django_useraudit-1.2.0.dist-info/metadata.json,sha256=ZyGkmDUJ0n2njkRqomWJX7U8EYfaxAKMcdi1elL_AU8,811 django_useraudit-1.2.0.dist-info/top_level.txt,sha256=NPzAggL9F0Qv-j_icqAoGplxt0MorJIv9oECTPaV-fA,10 django_useraudit-1.2.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1 useraudit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 useraudit/admin.py,sha256=AOnvlBB3nJIPcXImoqPKY2gfhvA1RFtys3dCTpgfR30,1222 useraudit/backend.py,sha256=iQG1SRP7-tGmH0zhXg84VV6ZmwfCBEMkRdb0KkZ-rUc,3234 useraudit/middleware.py,sha256=zJ3_bdqWHHuq7kIXbnuospui4tiJqyNcnCaOP9r3S0c,245 useraudit/models.py,sha256=kG6nksdE9zqGrFkdIc-Qa0Im24gqKDfKhKK2ge6vAI0,3198 useraudit/password_expiry.py,sha256=m_ANOTXEyzGdYPjjCkehzdQeSiavDcPLxQlbv3pRdSU,9948 useraudit/signals.py,sha256=7ZXP2gDQT2xF1r67x_9BcNOBFCWbI-YkPBeQ9PM2PL8,207 useraudit/test_settings.py,sha256=MT1DeZ6BVk4ZLoqNkHCI32N0BU5COziUfJ_3O1VBa6o,5588 useraudit/test_urls.py,sha256=tCWcRjy3ivnI9SPllHFT_W6A6KRdUA7uZd9bGm-i1sA,269 useraudit/urls.py,sha256=_QntMl-L_rshlB9d5ucdXeTeIq243mSNiPC8sOF5VGI,218 useraudit/views.py,sha256=v6GJ132tC9AqISwdRq8qz_kbQMMH9uCB_ezALxc_jP0,956 useraudit/migrations/0001_initial.py,sha256=jX-CGOT-qBgb2GirBOCexZrmFDIW79Qm_vLxVe5gxjA,1779 useraudit/migrations/0002_loginattempt.py,sha256=EaF1oVtAvWLtopH0oQt31pyzfCBVTWoULEqeq1skKOg,705 useraudit/migrations/0003_auto_20160406_1434.py,sha256=-HLJem4y2cIMC2zD2a0ekR37B0tp3_YqgprKaUEJipA,433 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 PK>HpOhuseraudit/admin.pyPK,4HqZ&&useraudit/password_expiry.pyPK,4HLA< ,useraudit/signals.pyPKN2H -useraudit/__init__.pyPK](HA~ ~ @-useraudit/models.pyPK>Hk9useraudit/views.pyPKB9H<9=useraudit/test_settings.pyPK,4HR Suseraudit/backend.pyPK>HuR`useraudit/urls.pyPKN2HwYauseraudit/middleware.pyPKփ5H:z  buseraudit/test_urls.pyPKN2H /duseraudit/migrations/__init__.pyPK>HW/mduseraudit/migrations/0003_auto_20160406_1434.pyPK>Hx)kfuseraudit/migrations/0002_loginattempt.pyPKN2Ho~$siuseraudit/migrations/0001_initial.pyPK@9Hx 辑 ipuseraudit/south_migrations/0002_auto__add_field_loginlog_forwarded_by__add_field_failedloginlog_forwar.pyPKN2H&{useraudit/south_migrations/__init__.pyPK@9HZYL *|useraudit/south_migrations/0001_initial.pyPK 6Hv8ss0django_useraudit-1.2.0.dist-info/DESCRIPTION.rstPK 6Hck++.django_useraudit-1.2.0.dist-info/metadata.jsonPK 6H0fs .)django_useraudit-1.2.0.dist-info/top_level.txtPKf9H2)django_useraudit-1.2.0.dist-info/zip-safePK 6Hndnn&Ǎdjango_useraudit-1.2.0.dist-info/WHEELPK 6Hݳ)ydjango_useraudit-1.2.0.dist-info/METADATAPK 6Hq'ڑdjango_useraudit-1.2.0.dist-info/RECORDPK