diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..379d39cc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,43 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + max-parallel: 5 + matrix: + python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: + ${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/tox.ini') }} + restore-keys: | + ${{ matrix.python-version }}-v1- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox tox-gh-actions + + - name: Tox tests + run: | + tox -v diff --git a/.isort.cfg b/.isort.cfg index 54a79a7f..310512e9 100644 --- a/.isort.cfg +++ b/.isort.cfg @@ -4,4 +4,3 @@ default_section = THIRDPARTY include_trailing_comma = true known_first_party = knox multi_line_output = 5 -not_skip = __init__.py diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 47771ed4..00000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -language: python -install: - - pip install tox - - pip install flake8 -matrix: - include: - - python: "2.7" - env: TOX_ENVS=py27-django111 - - python: "3.4" - env: TOX_ENVS=py34-django111,py34-django20 - - python: "3.5" - env: TOX_ENVS=py35-django111,py35-django20,py35-django20,py35-django21 - - python: "3.6" - env: TOX_ENVS=py36-django111,py36-django20,py36-django21 - - python: "3.6" - dist: xenial - sudo: true - env: TOX_ENVS=py36-django22 - - python: "3.7" - env: TOX_ENVS=py37-django20,py37-django21,py37-django22 - dist: xenial - sudo: true -before_script: - - flake8 . -script: - - tox -e $TOX_ENVS -deploy: - provider: pypi - user: james.mcmahon - password: testpasswordfortravis - on: - tags: true - repo: James1345/django-rest-knox - only: - - master - distributions: "sdist bdist_wheel" -git: - depth: false diff --git a/CHANGELOG.md b/CHANGELOG.md index 91e14522..96a31be7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,12 @@ +## 4.2.0 +- compatibility with Python up to 3.10 and Django up to 3.2 +- integration with github CI instead of travis +- Migration: "salt" field of model "AuthToken" is removed + ## 4.1.0 - Expiry format now defaults to whatever is used Django REST framework -- The behavior can be overriden via EXPIRY_DATETIME_FORMAT setting +- The behavior can be overridden via EXPIRY_DATETIME_FORMAT setting - Fully customizable expiry format via format_expiry_datetime - Fully customizable response payload via get_post_response_data @@ -71,13 +76,13 @@ Our release cycle was broken since 3.1.5, hence you can not find the previous re - Extend docs regarding usage of Token Authentication as single authentication method. ## 3.1.4 -- Fix compability with django-rest-swagger (bad inheritance) +- Fix compatibility with django-rest-swagger (bad inheritance) ## 3.1.3 - Avoid 500 error response for invalid-length token requests ## 3.1.2 -- restore compability with Python <2.7.7 +- restore compatibility with Python <2.7.7 ## 3.1.1 - use hmac.compare_digest instead of == for comparing hashes for more security diff --git a/README.md b/README.md index cbc29c98..203db1f1 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ django-rest-knox ================ -[![image](https://travis-ci.org/James1345/django-rest-knox.svg?branch=develop)](https://travis-ci.org/James1345/django-rest-knox) +[![image](https://github.com/James1345/django-rest-knox/workflows/Test/badge.svg?branch=develop)](https://github.com/James1345/django-rest-knox/actions) Authentication Module for django rest auth Knox provides easy to use authentication for [Django REST -Framework](http://www.django-rest-framework.org/) The aim is to allow +Framework](https://www.django-rest-framework.org/) The aim is to allow for common patterns in applications that are REST based, with little extra effort; and to ensure that connections remain secure. @@ -30,7 +30,7 @@ default implementation: an attacker unrestricted access to an account with a token if the database were compromised. - Knox tokens are only stored in an encrypted form. Even if the + Knox tokens are only stored in a secure hash form (like a password). Even if the database were somehow stolen, an attacker would not be able to log in with the stolen credentials. @@ -39,13 +39,13 @@ default implementation: the app settings (default is 10 hours.) More information can be found in the -[Documentation](http://james1345.github.io/django-rest-knox/) +[Documentation](https://james1345.github.io/django-rest-knox/) # Run the tests locally If you need to debug a test locally and if you have [docker](https://www.docker.com/) installed: -simply run the ``./docker-run-test.sh`` script and it will run the test suite in every Python / +simply run the ``./docker-run-tests.sh`` script and it will run the test suite in every Python / Django versions. You could also simply run regular ``tox`` in the root folder as well, but that would make testing the matrix of diff --git a/docs/auth.md b/docs/auth.md index 5872373a..63828ed9 100644 --- a/docs/auth.md +++ b/docs/auth.md @@ -4,7 +4,7 @@ Knox provides one class to handle authentication. ## TokenAuthentication -This works using [DRF's authentication system](http://www.django-rest-framework.org/api-guide/authentication/). +This works using [DRF's authentication system](https://www.django-rest-framework.org/api-guide/authentication/). Knox tokens should be generated using the provided views. Any `APIView` or `ViewSet` can be accessed using these tokens by adding `TokenAuthentication` @@ -65,9 +65,9 @@ from knox import views as knox_views from yourapp.api.views import LoginView urlpatterns = [ - url(r'login/', LoginView.as_view(), name='knox_login'), - url(r'logout/', knox_views.LogoutView.as_view(), name='knox_logout'), - url(r'logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'), + path(r'login/', LoginView.as_view(), name='knox_login'), + path(r'logout/', knox_views.LogoutView.as_view(), name='knox_logout'), + path(r'logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'), ] ``` @@ -101,8 +101,8 @@ from knox import views as knox_views from yourapp.api.views import LoginView urlpatterns = [ - url(r'login/', LoginView.as_view(), name='knox_login'), - url(r'logout/', knox_views.LogoutView.as_view(), name='knox_logout'), - url(r'logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'), + path(r'login/', LoginView.as_view(), name='knox_login'), + path(r'logout/', knox_views.LogoutView.as_view(), name='knox_logout'), + path(r'logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'), ] ``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md index 8a498e9f..1619797b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,5 +1,5 @@ # Django-Rest-Knox -Knox provides easy to use authentication for [Django REST Framework](http://www.django-rest-framework.org/) +Knox provides easy to use authentication for [Django REST Framework](https://www.django-rest-framework.org/) The aim is to allow for common patterns in applications that are REST based, with little extra effort; and to ensure that connections remain secure. diff --git a/docs/installation.md b/docs/installation.md index ad7da044..43945b44 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -6,7 +6,7 @@ Knox depends on `cryptography` to provide bindings to `OpenSSL` for token genera This requires the OpenSSL build libraries to be available. ### Windows -Cryptography is a statically linked build, no extra steps are needed +Cryptography is a statically linked build, no extra steps are needed. ### Linux `cryptography` should build very easily on Linux provided you have a C compiler, @@ -45,7 +45,7 @@ INSTALLED_APPS = ( ) ``` -- Make knox's TokenAuthentication your default authentification class +- Make knox's TokenAuthentication your default authentication class for django-rest-framework: ```python diff --git a/docs/settings.md b/docs/settings.md index a863a9ef..e44e322b 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -35,7 +35,7 @@ for production use. ### Tests SHA-512 and Whirlpool are secure, however, they are slow. This should not be a -problem for your users, but when testing it may be noticable (as test cases tend +problem for your users, but when testing it may be noticeable (as test cases tend to use many more requests much more quickly than real users). In testing scenarios it is acceptable to use `MD5` hashing.(`cryptography.hazmat.primitives.hashes.MD5`) @@ -63,7 +63,7 @@ By default this option is disabled and set to `None` -- thus no limit. ## USER_SERIALIZER This is the reference to the class used to serialize the `User` objects when -succesfully returning from `LoginView`. The default is `knox.serializers.UserSerializer` +successfully returning from `LoginView`. The default is `knox.serializers.UserSerializer` ## AUTO_REFRESH This defines if the token expiry time is extended by TOKEN_TTL each time the token @@ -93,13 +93,7 @@ be raised if there is an attempt to change them. from knox.settings import CONSTANTS print(CONSTANTS.DIGEST_LENGTH) #=> 128 -print(CONSTANTS.SALT_LENGTH) #=> 16 ``` ## DIGEST_LENGTH This is the length of the digest that will be stored in the database for each token. - -## SALT_LENGTH -This is the length of the [salt][salt] that will be stored in the database for each token. - -[salt]: https://en.wikipedia.org/wiki/Salt_(cryptography) diff --git a/docs/urls.md b/docs/urls.md index 95177124..628f8747 100644 --- a/docs/urls.md +++ b/docs/urls.md @@ -6,14 +6,14 @@ This can easily be included in your url config: ```python urlpatterns = [ #...snip... - url(r'api/auth/', include('knox.urls')) + path(r'api/auth/', include('knox.urls')) #...snip... ] ``` **Note** It is important to use the string syntax and not try to import `knox.urls`, as the reference to the `User` model will cause the app to fail at import time. -The views would then acessible as: +The views would then accessible as: - `/api/auth/login` -> `LoginView` - `/api/auth/logout` -> `LogoutView` diff --git a/docs/views.md b/docs/views.md index 509c58df..c8ed762b 100644 --- a/docs/views.md +++ b/docs/views.md @@ -20,7 +20,7 @@ helper methods: - `get_token_limit_per_user(self)`, to change the number of tokens available for a user - `get_user_serializer_class(self)`, to change the class used for serializing the user - `get_expiry_datetime_format(self)`, to change the datetime format used for expiry -- `format_expiry_datetime(self, expiry)`, to format the expiry `datetime` object at your convinience +- `format_expiry_datetime(self, expiry)`, to format the expiry `datetime` object at your convenience Finally, if none of these helper methods are sufficient, you can also override `get_post_response_data` to return a fully customized payload. diff --git a/knox/admin.py b/knox/admin.py index e65d7059..a045f7a7 100644 --- a/knox/admin.py +++ b/knox/admin.py @@ -5,6 +5,6 @@ @admin.register(models.AuthToken) class AuthTokenAdmin(admin.ModelAdmin): - list_display = ('digest', 'user', 'created',) + list_display = ('digest', 'user', 'created', 'expiry',) fields = () raw_id_fields = ('user',) diff --git a/knox/auth.py b/knox/auth.py index ffb5a287..c013c4dd 100644 --- a/knox/auth.py +++ b/knox/auth.py @@ -6,9 +6,8 @@ def compare_digest(a, b): import binascii -from django.contrib.auth import get_user_model from django.utils import timezone -from django.utils.translation import ugettext_lazy as _ +from django.utils.translation import gettext_lazy as _ from rest_framework import exceptions from rest_framework.authentication import ( BaseAuthentication, get_authorization_header, @@ -19,8 +18,6 @@ def compare_digest(a, b): from knox.settings import CONSTANTS, knox_settings from knox.signals import token_expired -User = get_user_model() - class TokenAuthentication(BaseAuthentication): ''' @@ -40,7 +37,10 @@ def authenticate(self, request): auth = get_authorization_header(request).split() prefix = knox_settings.AUTH_HEADER_PREFIX.encode() - if not auth or auth[0].lower() != prefix.lower(): + if not auth: + return None + if auth[0].lower() != prefix.lower(): + # Authorization header is possibly for another backend return None if len(auth) == 1: msg = _('Invalid token header. No credentials provided.') @@ -55,7 +55,7 @@ def authenticate(self, request): def authenticate_credentials(self, token): ''' - Due to the random nature of hashing a salted value, this must inspect + Due to the random nature of hashing a value, this must inspect each auth_token individually to find the correct one. Tokens that have expired will be deleted and skipped @@ -68,7 +68,7 @@ def authenticate_credentials(self, token): continue try: - digest = hash_token(token, auth_token.salt) + digest = hash_token(token) except (TypeError, binascii.Error): raise exceptions.AuthenticationFailed(msg) if compare_digest(digest, auth_token.digest): diff --git a/knox/crypto.py b/knox/crypto.py index 438c6b61..dba4f754 100644 --- a/knox/crypto.py +++ b/knox/crypto.py @@ -4,7 +4,7 @@ from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes -from knox.settings import CONSTANTS, knox_settings +from knox.settings import knox_settings sha = knox_settings.SECURE_HASH_ALGORITHM @@ -15,20 +15,14 @@ def create_token_string(): ).decode() -def create_salt_string(): - return binascii.hexlify( - generate_bytes(int(CONSTANTS.SALT_LENGTH / 2))).decode() - - -def hash_token(token, salt): +def hash_token(token): ''' - Calculates the hash of a token and salt. + Calculates the hash of a token. input is unhexlified - token and salt must contain an even number of hex digits or - a binascii.Error exception will be raised + token must contain an even number of hex digits or a binascii.Error + exception will be raised ''' digest = hashes.Hash(sha(), backend=default_backend()) digest.update(binascii.unhexlify(token)) - digest.update(binascii.unhexlify(salt)) return binascii.hexlify(digest.finalize()).decode() diff --git a/knox/migrations/0008_remove_authtoken_salt.py b/knox/migrations/0008_remove_authtoken_salt.py new file mode 100644 index 00000000..ba97bc96 --- /dev/null +++ b/knox/migrations/0008_remove_authtoken_salt.py @@ -0,0 +1,17 @@ +# Generated by Django 2.2.5 on 2019-09-16 12:10 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('knox', '0007_auto_20190111_0542'), + ] + + operations = [ + migrations.RemoveField( + model_name='authtoken', + name='salt', + ), + ] diff --git a/knox/models.py b/knox/models.py index 39910ec4..4dbee1ae 100644 --- a/knox/models.py +++ b/knox/models.py @@ -11,15 +11,14 @@ class AuthTokenManager(models.Manager): def create(self, user, expiry=knox_settings.TOKEN_TTL): token = crypto.create_token_string() - salt = crypto.create_salt_string() - digest = crypto.hash_token(token, salt) + digest = crypto.hash_token(token) if expiry is not None: expiry = timezone.now() + expiry instance = super(AuthTokenManager, self).create( token_key=token[:CONSTANTS.TOKEN_KEY_LENGTH], digest=digest, - salt=salt, user=user, expiry=expiry) + user=user, expiry=expiry) return instance, token @@ -31,8 +30,6 @@ class AuthToken(models.Model): max_length=CONSTANTS.DIGEST_LENGTH, primary_key=True) token_key = models.CharField( max_length=CONSTANTS.TOKEN_KEY_LENGTH, db_index=True) - salt = models.CharField( - max_length=CONSTANTS.SALT_LENGTH, unique=True) user = models.ForeignKey(User, null=False, blank=False, related_name='auth_token_set', on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) diff --git a/knox/settings.py b/knox/settings.py index 179522d7..5bb14483 100644 --- a/knox/settings.py +++ b/knox/settings.py @@ -42,7 +42,6 @@ class CONSTANTS: ''' TOKEN_KEY_LENGTH = 8 DIGEST_LENGTH = 128 - SALT_LENGTH = 16 def __setattr__(self, *args, **kwargs): raise Exception(''' diff --git a/knox/signals.py b/knox/signals.py index ad8a7538..3c644fc6 100644 --- a/knox/signals.py +++ b/knox/signals.py @@ -1,3 +1,3 @@ import django.dispatch -token_expired = django.dispatch.Signal(providing_args=["username", "source"]) +token_expired = django.dispatch.Signal() diff --git a/knox/urls.py b/knox/urls.py index d3506c3f..e3ac58c8 100644 --- a/knox/urls.py +++ b/knox/urls.py @@ -1,9 +1,9 @@ -from django.conf.urls import url +from django.urls import path from knox import views urlpatterns = [ - url(r'login/', views.LoginView.as_view(), name='knox_login'), - url(r'logout/', views.LogoutView.as_view(), name='knox_logout'), - url(r'logoutall/', views.LogoutAllView.as_view(), name='knox_logoutall'), + path(r'login/', views.LoginView.as_view(), name='knox_login'), + path(r'logout/', views.LogoutView.as_view(), name='knox_logout'), + path(r'logoutall/', views.LogoutAllView.as_view(), name='knox_logoutall'), ] diff --git a/knox_project/settings.py b/knox_project/settings.py index f0378fcd..85dcc48d 100644 --- a/knox_project/settings.py +++ b/knox_project/settings.py @@ -10,7 +10,6 @@ 'django.contrib.sessions', 'rest_framework', 'knox', - 'django_nose', ) MIDDLEWARE_CLASSES = ( @@ -18,7 +17,6 @@ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.middleware.security.SecurityMiddleware', ) @@ -55,5 +53,3 @@ USE_TZ = True STATIC_URL = '/static/' - -TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' diff --git a/knox_project/urls.py b/knox_project/urls.py index 3fb1a8ed..fa9ccfc8 100644 --- a/knox_project/urls.py +++ b/knox_project/urls.py @@ -1,10 +1,4 @@ -try: - # For django >= 2.0 - from django.urls import include, re_path -except ImportError: - # For django < 2.0 - from django.conf.urls import include, url - re_path = url +from django.urls import include, re_path from .views import RootView diff --git a/setup.py b/setup.py index 5e6ae73a..30d63bdb 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ # Versions should comply with PEP440. For a discussion on single-sourcing # the version across setup.py and the project code, see # https://packaging.python.org/en/latest/single_source_version.html - version='4.1.0', + version='4.2.0', description='Authentication for django rest framework', long_description=long_description, long_description_content_type='text/markdown', @@ -46,13 +46,11 @@ # Pick your license as you wish (should match "license" above) 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', ], # What does your project relate to? @@ -67,7 +65,12 @@ # your project is installed. For an analysis of "install_requires" vs pip's # requirements files see: # https://packaging.python.org/en/latest/requirements.html - install_requires=['django', 'djangorestframework', 'cryptography'], + python_requires='>=3.6', + install_requires=[ + 'django>=3.2', + 'djangorestframework', + 'cryptography', + ], # List additional groups of dependencies here (e.g. development # dependencies). You can install these using the following syntax, diff --git a/tests/tests.py b/tests/tests.py index ffe6f2d8..914ce204 100644 --- a/tests/tests.py +++ b/tests/tests.py @@ -3,10 +3,12 @@ from django.contrib.auth import get_user_model from django.test import override_settings -from django.utils.six.moves import reload_module +from django.urls import reverse from freezegun import freeze_time +from rest_framework.exceptions import AuthenticationFailed from rest_framework.serializers import DateTimeField from rest_framework.test import APIRequestFactory, APITestCase as TestCase +from six.moves import reload_module from knox import auth, views from knox.auth import TokenAuthentication @@ -15,13 +17,6 @@ from knox.settings import CONSTANTS, knox_settings from knox.signals import token_expired -try: - # For django >= 2.0 - from django.urls import reverse -except ImportError: - # For django < 2.0 - from django.conf.urls import reverse - User = get_user_model() root_url = reverse('api-root') @@ -170,7 +165,7 @@ def test_logout_all_deletes_only_targets_keys(self): def test_expired_tokens_login_fails(self): self.assertEqual(AuthToken.objects.count(), 0) instance, token = AuthToken.objects.create( - user=self.user, expiry=timedelta(seconds=0)) + user=self.user, expiry=timedelta(seconds=-1)) self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) response = self.client.post(root_url, {}, format='json') self.assertEqual(response.status_code, 401) @@ -179,9 +174,9 @@ def test_expired_tokens_login_fails(self): def test_expired_tokens_deleted(self): self.assertEqual(AuthToken.objects.count(), 0) for _ in range(10): - # 0 TTL gives an expired token + # -1 TTL gives an expired token instance, token = AuthToken.objects.create( - user=self.user, expiry=timedelta(seconds=0)) + user=self.user, expiry=timedelta(seconds=-1)) self.assertEqual(AuthToken.objects.count(), 10) # Attempting a single logout should delete all tokens @@ -202,6 +197,34 @@ def test_update_token_key(self): auth_token.token_key, ) + def test_authorization_header_empty(self): + rf = APIRequestFactory() + request = rf.get('/') + request.META = {'HTTP_AUTHORIZATION': ''} + self.assertEqual(TokenAuthentication().authenticate(request), None) + + def test_authorization_header_prefix_only(self): + rf = APIRequestFactory() + request = rf.get('/') + request.META = {'HTTP_AUTHORIZATION': 'Token'} + with self.assertRaises(AuthenticationFailed) as err: + (self.user, auth_token) = TokenAuthentication().authenticate(request) + self.assertIn( + 'Invalid token header. No credentials provided.', + str(err.exception), + ) + + def test_authorization_header_spaces_in_token_string(self): + rf = APIRequestFactory() + request = rf.get('/') + request.META = {'HTTP_AUTHORIZATION': 'Token wordone wordtwo'} + with self.assertRaises(AuthenticationFailed) as err: + (self.user, auth_token) = TokenAuthentication().authenticate(request) + self.assertIn( + 'Invalid token header. Token string should not contain spaces.', + str(err.exception), + ) + def test_invalid_token_length_returns_401_code(self): invalid_token = "1" * (CONSTANTS.TOKEN_KEY_LENGTH - 1) self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % invalid_token)) @@ -252,7 +275,7 @@ def test_token_expiry_is_extended_with_auto_refresh_activated(self): response = self.client.get(root_url, {}, format='json') self.assertEqual(response.status_code, 401) - def test_token_expiry_is_not_extended_with_auto_refresh_deativated(self): + def test_token_expiry_is_not_extended_with_auto_refresh_deactivated(self): self.assertEqual(knox_settings.AUTO_REFRESH, False) self.assertEqual(knox_settings.TOKEN_TTL, timedelta(hours=10)) @@ -295,7 +318,7 @@ def handler(sender, username, **kwargs): token_expired.connect(handler) - instance, token = AuthToken.objects.create(user=self.user, expiry=timedelta(0)) + instance, token = AuthToken.objects.create(user=self.user, expiry=timedelta(seconds=-1)) self.client.credentials(HTTP_AUTHORIZATION=('Token %s' % token)) self.client.post(root_url, {}, format='json') @@ -323,7 +346,7 @@ def test_does_not_exceed_on_expired_keys(self): reload_module(views) for _ in range(9): AuthToken.objects.create(user=self.user) - AuthToken.objects.create(user=self.user, expiry=timedelta(seconds=0)) + AuthToken.objects.create(user=self.user, expiry=timedelta(seconds=-1)) # now 10 keys, but only 9 valid so request should succeed. url = reverse('knox_login') self.client.credentials( diff --git a/tox.ini b/tox.ini index f80ab08d..c6610d86 100644 --- a/tox.ini +++ b/tox.ini @@ -1,11 +1,9 @@ [tox] envlist = - isort + isort, flake8, - py{27,34,35,36}-django111, - py{34,35,36}-django20, - py{35,36,37}-django21, - py{35,36,37}-django22, + py{36,37,38,39,310}-django32, + py{38,39,310}-django40, [testenv:flake8] deps = flake8 @@ -15,7 +13,7 @@ commands = flake8 knox [testenv:isort] deps = isort changedir = {toxinidir} -commands = isort --recursive --check-only --diff \ +commands = isort --check-only --diff \ knox \ knox_project/views.py \ setup.py \ @@ -29,13 +27,10 @@ setenv = DJANGO_SETTINGS_MODULE = knox_project.settings PIP_INDEX_URL = https://pypi.python.org/simple/ deps = - django111: Django>=1.11,<2.0 - django20: Django>=2.0,<2.1 - django21: Django>=2.1,<2.2 - django22: Django>=2.2,<2.3 - django-nose + django32: Django>=3.2,<3.3 + django40: Django>=4.0,<4.1 markdown<3.0 - isort + isort>=5.0 djangorestframework freezegun mkdocs @@ -44,3 +39,11 @@ deps = setuptools twine wheel + +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39, isort, flake8 + 3.10: py310