removes authtoken

This commit is contained in:
adamscmRH
2018-02-27 13:23:45 -05:00
parent 6d7f60ea61
commit 40d563626e
6 changed files with 29 additions and 185 deletions

View File

@@ -17,7 +17,6 @@ from oauth2_provider.settings import oauth2_settings
# Django # Django
from django.conf import settings from django.conf import settings
from django.contrib.auth import authenticate
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError from django.core.exceptions import ObjectDoesNotExist, ValidationError as DjangoValidationError
@@ -4664,26 +4663,6 @@ class ActivityStreamSerializer(BaseSerializer):
return summary_fields return summary_fields
class AuthTokenSerializer(serializers.Serializer):
username = serializers.CharField()
password = serializers.CharField()
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
if username and password:
user = authenticate(username=username, password=password)
if user:
attrs['user'] = user
return attrs
else:
raise serializers.ValidationError(_('Unable to login with provided credentials.'))
else:
raise serializers.ValidationError(_('Must include "username" and "password".'))
class FactVersionSerializer(BaseFactSerializer): class FactVersionSerializer(BaseFactSerializer):
class Meta: class Meta:

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.7 on 2018-02-27 17:58
from __future__ import unicode_literals
import awx.main.fields
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('main', '0024_v330_add_oauth_activity_stream_registrar'),
]
operations = [
migrations.RemoveField(
model_name='authtoken',
name='user',
),
migrations.DeleteModel(
name='AuthToken',
),
]

View File

@@ -1,21 +1,15 @@
# Copyright (c) 2015 Ansible, Inc. # Copyright (c) 2015 Ansible, Inc.
# All Rights Reserved. # All Rights Reserved.
# Python
import datetime
import hashlib
import hmac
import uuid
# Django # Django
from django.conf import settings from django.conf import settings
from django.db import models, connection from django.db import models
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.contrib.sessions.models import Session from django.contrib.sessions.models import Session
from django.utils.timezone import now as tz_now from django.utils.timezone import now as tz_now
from django.utils.translation import ugettext_lazy as _
import six
# AWX # AWX
from awx.api.versioning import reverse from awx.api.versioning import reverse
@@ -27,7 +21,7 @@ from awx.main.models.rbac import (
) )
from awx.main.models.mixins import ResourceMixin, CustomVirtualEnvMixin from awx.main.models.mixins import ResourceMixin, CustomVirtualEnvMixin
__all__ = ['Organization', 'Team', 'Profile', 'AuthToken', 'UserSessionMembership'] __all__ = ['Organization', 'Team', 'Profile', 'UserSessionMembership']
class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVirtualEnvMixin): class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVirtualEnvMixin):
@@ -135,141 +129,6 @@ class Profile(CreatedModifiedModel):
) )
"""
Since expiration and session expiration is event driven a token could be
invalidated for both reasons. Further, we only support a single reason for a
session token being invalid. For this case, mark the token as expired.
Note: Again, because the value of reason is event based. The reason may not be
set (i.e. may equal '') even though a session is expired or a limit is reached.
"""
class AuthToken(BaseModel):
'''
Custom authentication tokens per user with expiration and request-specific
data.
'''
REASON_CHOICES = [
('', _('Token not invalidated')),
('timeout_reached', _('Token is expired')),
('limit_reached', _('The maximum number of allowed sessions for this user has been exceeded.')),
# invalid_token is not a used data-base value, but is returned by the
# api when a token is not found
('invalid_token', _('Invalid token')),
]
class Meta:
app_label = 'main'
key = models.CharField(max_length=40, primary_key=True)
user = prevent_search(models.ForeignKey('auth.User',
related_name='auth_tokens', on_delete=models.CASCADE))
created = models.DateTimeField(auto_now_add=True)
modified = models.DateTimeField(auto_now=True)
expires = models.DateTimeField(default=tz_now)
request_hash = prevent_search(models.CharField(max_length=40, blank=True,
default=''))
reason = models.CharField(
max_length=1024,
blank=True,
default='',
help_text=_('Reason the auth token was invalidated.')
)
@staticmethod
def reason_long(reason):
for x in AuthToken.REASON_CHOICES:
if x[0] == reason:
return six.text_type(x[1])
return None
@classmethod
def get_request_hash(cls, request):
h = hashlib.sha1()
h.update(settings.SECRET_KEY)
for header in settings.REMOTE_HOST_HEADERS:
value = request.META.get(header, '').split(',')[0].strip()
if value:
h.update(value)
break
h.update(request.META.get('HTTP_USER_AGENT', ''))
return h.hexdigest()
def save(self, *args, **kwargs):
if not self.pk:
self.refresh(save=False)
if not self.key:
self.key = self.generate_key()
return super(AuthToken, self).save(*args, **kwargs)
def refresh(self, now=None, save=True):
if not now:
now = tz_now()
if not self.pk or not self.is_expired(now=now):
self.expires = now + datetime.timedelta(seconds=settings.AUTH_TOKEN_EXPIRATION)
if save:
connection.on_commit(lambda: self.save(update_fields=['expires']))
def invalidate(self, reason='timeout_reached', save=True):
if not AuthToken.reason_long(reason):
raise ValueError(_('Invalid reason specified'))
self.reason = reason
if save:
self.save()
return reason
@staticmethod
def get_tokens_over_limit(user, now=None):
if now is None:
now = tz_now()
invalid_tokens = AuthToken.objects.none()
if settings.AUTH_TOKEN_PER_USER != -1:
invalid_tokens = AuthToken.objects.filter(
user=user,
expires__gt=now,
reason='',
).order_by('-created')[settings.AUTH_TOKEN_PER_USER:]
return invalid_tokens
def generate_key(self):
unique = uuid.uuid4()
return hmac.new(unique.bytes, digestmod=hashlib.sha1).hexdigest()
def is_expired(self, now=None):
if not now:
now = tz_now()
return bool(self.expires < now)
@property
def invalidated(self):
return bool(self.reason != '')
"""
Token is valid if it's in the set of unexpired tokens.
The unexpired token set is:
* tokens not expired
* limited to number of tokens per-user
* sorted by created on date
"""
def in_valid_tokens(self, now=None):
if not now:
now = tz_now()
valid_n_tokens_qs = self.user.auth_tokens.filter(
expires__gt=now,
reason='',
).order_by('-created')
if settings.AUTH_TOKEN_PER_USER != -1:
valid_n_tokens_qs = valid_n_tokens_qs[0:settings.AUTH_TOKEN_PER_USER]
valid_n_tokens = valid_n_tokens_qs.values_list('key', flat=True)
return bool(self.key in valid_n_tokens)
def __unicode__(self):
return self.key
class UserSessionMembership(BaseModel): class UserSessionMembership(BaseModel):
''' '''
A lookup table for session membership given user. A lookup table for session membership given user.

View File

@@ -205,8 +205,6 @@ class BaseTestMixin(MockCommonlySlowTestMixin):
user = User.objects.create_superuser(username, "%s@example.com", password) user = User.objects.create_superuser(username, "%s@example.com", password)
else: else:
user = User.objects.create_user(username, "%s@example.com", password) user = User.objects.create_user(username, "%s@example.com", password)
# New user should have no auth tokens by default.
self.assertFalse(user.auth_tokens.count())
self._user_passwords[user.username] = password self._user_passwords[user.username] = password
return user return user

View File

@@ -4,7 +4,7 @@ import pytest
from rest_framework.exceptions import PermissionDenied, ParseError from rest_framework.exceptions import PermissionDenied, ParseError
from awx.api.filters import FieldLookupBackend from awx.api.filters import FieldLookupBackend
from awx.main.models import (AdHocCommand, AuthToken, CustomInventoryScript, from awx.main.models import (AdHocCommand, CustomInventoryScript,
Credential, Job, JobTemplate, SystemJob, Credential, Job, JobTemplate, SystemJob,
UnifiedJob, User, WorkflowJob, UnifiedJob, User, WorkflowJob,
WorkflowJobTemplate, WorkflowJobOptions, WorkflowJobTemplate, WorkflowJobOptions,
@@ -54,9 +54,7 @@ def test_filter_on_password_field(password_field, lookup_suffix):
@pytest.mark.parametrize('model, query', [ @pytest.mark.parametrize('model, query', [
(AuthToken, 'request_hash__icontains'),
(User, 'password__icontains'), (User, 'password__icontains'),
(User, 'auth_tokens__key__icontains'),
(User, 'settings__value__icontains'), (User, 'settings__value__icontains'),
(UnifiedJob, 'job_args__icontains'), (UnifiedJob, 'job_args__icontains'),
(UnifiedJob, 'job_env__icontains'), (UnifiedJob, 'job_env__icontains'),

View File

@@ -34,21 +34,6 @@
* Just before the release of 2.0 a bug was discovered where clicking logout and then immediately clicking login without providing a username and password would successfully log * Just before the release of 2.0 a bug was discovered where clicking logout and then immediately clicking login without providing a username and password would successfully log
* the user back into the app. Implementing the above approach fixed this, forcing a new username/password to be entered each time the login dialog appears. * the user back into the app. Implementing the above approach fixed this, forcing a new username/password to be entered each time the login dialog appears.
* *
* #Login Workflow
*
* When the the login button is clicked, the following occurs:
*
* - Call Authorization.retrieveToken(username, password) - sends a POST request to /api/v2/authtoken to get a new token value.
* - Call Authorization.setToken(token, expires) to store the token and exipration time in a session cookie.
* - Start the expiration timer by calling the init() method of [js/shared/Timer.js](/static/docs/api/shared.function:Timer)
* - Get user informaton by calling Authorization.getUser() - sends a GET request to /api/v2/me
* - Store user information in the session cookie by calling Authorization.setUser().
* - Get the license by calling ConfigService.getConfig() - sends a GET request to /api/vi/config
* - Stores the license object in memory by calling CheckLicense.test(). This adds the version and a tested flag to the license object. The tested flag is initially set to false. Additionally, the pendoService and FeaturesService are called to initiate the other startup services
*
* Note that there is a session timer kept on the server side as well as the client side. Each time an API request is made, app.js calls
* Timer.isExpired(). This verifies the UI does not think the session is expired, and if not, moves the expiration time into the future. The number of
* seconds between API calls before a session is considered expired is set in config.js as session_timeout.
* *
* @Usage * @Usage
* This is usage information. * This is usage information.