diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..458c50c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Release + +on: + push: + tags: + - "*" + +jobs: + release: + runs-on: ubuntu-18.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install build packages + run: python -m pip install -U build + - name: Build packages + run: python -m build + - name: Create Release + uses: softprops/action-gh-release@v1 + with: + files: | + dist/*.tar.gz + dist/*.whl + - name: Install system pkgs + run: sudo apt-get update && sudo apt-get install awscli + - name: Upload to S3 + env: + AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }} + AWS_PACKAGE_BUCKET: ${{ secrets.AWS_PACKAGE_BUCKET }} + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + run: for F in dist/*; do /usr/bin/aws s3 cp ${F} s3://${AWS_PACKAGE_BUCKET}/python/ ; done diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..f323107 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,22 @@ +name: Run Tests + +on: [push, pull_request] + +jobs: + test: + strategy: + matrix: + python: ['3.6', '3.8'] + platform: [ubuntu-18.04] + runs-on: ${{ matrix.platform }} + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + - name: Install test dependencies + run: sudo apt-get update && sudo apt-get install firefox-geckodriver && python -m pip install -U tox tox-gh-actions + - name: Test + run: python -m tox diff --git a/.gitignore b/.gitignore index 7bc6535..7079be9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,159 @@ -*.pyc -django_session_security.egg-info + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings .ropeproject -.idea -dist -docs/build -docs/source/_static/ -test_project/geckodriver.log -db.sqlite + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/session_security/middleware.py b/session_security/middleware.py index a8235fb..8ffc465 100644 --- a/session_security/middleware.py +++ b/session_security/middleware.py @@ -13,9 +13,10 @@ import django from django.contrib.auth import logout -try: # Django 2.0 + +try: # Django 2.0 from django.urls import reverse, resolve, Resolver404 -except: # Django < 2.0 +except ImportError: # Django < 2.0 from django.core.urlresolvers import reverse, resolve, Resolver404 try: @@ -35,7 +36,7 @@ class SessionSecurityMiddleware(MiddlewareMixin): """ def is_passive_request(self, request): - """ Should we skip activity update on this URL/View. """ + """Should we skip activity update on this URL/View.""" if request.path in PASSIVE_URLS: return True @@ -54,8 +55,8 @@ def get_expire_seconds(self, request): return EXPIRE_AFTER def process_request(self, request): - """ Update last activity time or logout. """ - + """Update last activity time or logout.""" + if django.VERSION < (1, 10): is_authenticated = request.user.is_authenticated() else: @@ -65,7 +66,7 @@ def process_request(self, request): return now = datetime.now() - if '_session_security' not in request.session: + if "_session_security" not in request.session: set_last_activity(request.session, now) return @@ -73,8 +74,10 @@ def process_request(self, request): expire_seconds = self.get_expire_seconds(request) if delta >= timedelta(seconds=expire_seconds): logout(request) - elif (request.path == reverse('session_security_ping') and - 'idleFor' in request.GET): + elif ( + request.path == reverse("session_security_ping") + and "idleFor" in request.GET + ): self.update_last_activity(request, now) elif not self.is_passive_request(request): set_last_activity(request.session, now) @@ -90,7 +93,7 @@ def update_last_activity(self, request, now): # Gracefully ignore non-integer values try: - client_idle_for = int(request.GET['idleFor']) + client_idle_for = int(request.GET["idleFor"]) except ValueError: return diff --git a/session_security/tests/project/urls.py b/session_security/tests/project/urls.py index 03a946e..675a4c6 100644 --- a/session_security/tests/project/urls.py +++ b/session_security/tests/project/urls.py @@ -1,6 +1,6 @@ import time -from django.conf.urls import include, url +from django.urls import include, re_path try: from django.conf.urls import patterns @@ -21,12 +21,12 @@ def get(self, request, *args, **kwargs): urlpatterns = [ - url(r'^$', generic.TemplateView.as_view(template_name='home.html')), - url(r'^sleep/$', login_required( + re_path(r'^$', generic.TemplateView.as_view(template_name='home.html')), + re_path(r'^sleep/$', login_required( SleepView.as_view(template_name='home.html')), name='sleep'), - url(r'^admin/', admin.site.urls), - url(r'session_security/', include('session_security.urls')), - url(r'^ignore/$', login_required( + re_path(r'^admin/', admin.site.urls), + re_path(r'session_security/', include('session_security.urls')), + re_path(r'^ignore/$', login_required( generic.TemplateView.as_view(template_name='home.html')), name='ignore'), ] diff --git a/session_security/tests/test_base.py b/session_security/tests/test_base.py index 42611b3..b783d81 100644 --- a/session_security/tests/test_base.py +++ b/session_security/tests/test_base.py @@ -6,7 +6,7 @@ from selenium.webdriver.common.keys import Keys from selenium.webdriver.common.action_chains import ActionChains -from selenium import webdriver; +from selenium import webdriver from selenium.webdriver.firefox.webdriver import WebDriver from selenium.webdriver import Remote from django.contrib.staticfiles.testing import StaticLiveServerTestCase @@ -17,7 +17,7 @@ from session_security.settings import WARN_AFTER, EXPIRE_AFTER -WAIT_TIME = 5 if not os.environ.get('CI', False) else 30 +WAIT_TIME = 5 if not os.environ.get("CI", False) else 30 class SettingsMixin(object): @@ -30,10 +30,11 @@ def setUp(self): super(SettingsMixin, self).setUp() -class BaseLiveServerTestCase(SettingsMixin, StaticLiveServerTestCase, - LiveServerTestCase): +class BaseLiveServerTestCase( + SettingsMixin, StaticLiveServerTestCase, LiveServerTestCase +): - fixtures = ['session_security_test_user'] + fixtures = ["session_security_test_user"] def setUp(self): SettingsMixin.setUp(self) @@ -42,18 +43,20 @@ def setUp(self): options = FirefoxOptions() options.add_argument("--headless") super(LiveServerTestCase, self).setUp() - self.sel= webdriver.Firefox(options=options) - self.sel.get('%s%s' % (self.live_server_url, '/admin/')) - self.sel.find_element_by_name('username').send_keys('test') - self.sel.find_element_by_name('password').send_keys('test') + self.sel = webdriver.Firefox(options=options) + self.sel.get("%s%s" % (self.live_server_url, "/admin/")) + self.sel.find_element_by_name("username").send_keys("test") + self.sel.find_element_by_name("password").send_keys("test") self.sel.find_element_by_xpath('//input[@value="Log in"]').click() self.sel.execute_script('window.open("/admin/", "other")') def press_space(self): body = self.sel.find_element_by_tag_name("body") body.send_keys(Keys.SPACE) + def tearDown(self): self.sel.quit() + @classmethod def tearDownClass(cls): - super(BaseLiveServerTestCase, cls).tearDownClass() \ No newline at end of file + super(BaseLiveServerTestCase, cls).tearDownClass() diff --git a/session_security/tests/test_script.py b/session_security/tests/test_script.py index bf52fe2..0e445cc 100644 --- a/session_security/tests/test_script.py +++ b/session_security/tests/test_script.py @@ -13,19 +13,20 @@ class ScriptTestCase(BaseLiveServerTestCase): - - def test_warning_shows_and_session_expires(self): start = datetime.datetime.now() for win in self.sel.window_handles: - self.sel.switch_to_window(win) + self.sel.switch_to.window(win) try: el = WebDriverWait(self.sel, self.max_warn_after).until( - expected_conditions.visibility_of_element_located((By.ID, "session_security_warning"))) - assert(el.is_displayed()) + expected_conditions.visibility_of_element_located( + (By.ID, "session_security_warning") + ) + ) + assert el.is_displayed() except: - assert(False) #max_warn_after did not display el. + assert False # max_warn_after did not display el. end = datetime.datetime.now() delta = end - start @@ -33,53 +34,63 @@ def test_warning_shows_and_session_expires(self): self.assertLessEqual(delta.seconds, self.max_warn_after) for win in self.sel.window_handles: - self.sel.switch_to_window(win) + self.sel.switch_to.window(win) try: el = WebDriverWait(self.sel, self.max_expire_after).until( - expected_conditions.visibility_of_element_located((By.ID, "id_password"))) - assert(el.is_displayed()) + expected_conditions.visibility_of_element_located( + (By.ID, "id_password") + ) + ) + assert el.is_displayed() delta = datetime.datetime.now() - start self.assertGreaterEqual(delta.seconds, self.min_expire_after) self.assertLessEqual(delta.seconds, self.max_expire_after) except: - assert(False) #Test fails if timeout expires + assert False # Test fails if timeout expires def test_activity_hides_warning(self): - time.sleep(6 * .7) + time.sleep(6 * 0.7) try: WebDriverWait(self.sel, self.max_warn_after).until( - expected_conditions.visibility_of_element_located((By.ID, "session_security_warning"))) + expected_conditions.visibility_of_element_located( + (By.ID, "session_security_warning") + ) + ) self.press_space() for win in self.sel.window_handles: - self.sel.switch_to_window(win) + self.sel.switch_to.window(win) try: el = WebDriverWait(self.sel, 20).until( - expected_conditions.invisibility_of_element_located((By.ID, "session_security_warning"))) + expected_conditions.invisibility_of_element_located( + (By.ID, "session_security_warning") + ) + ) - assert(not el.is_displayed()) + assert not el.is_displayed() except: - assert(False) #Test fails if element invisibilty times out + assert False # Test fails if element invisibilty times out except: - assert(False) #Test fails if element visility times out - - + assert False # Test fails if element visility times out def test_activity_prevents_warning(self): - time.sleep(self.min_warn_after * .7) + time.sleep(self.min_warn_after * 0.7) self.press_space() start = datetime.datetime.now() try: el = WebDriverWait(self.sel, self.max_warn_after).until( - expected_conditions.visibility_of_element_located((By.ID, "session_security_warning"))) - assert(el.is_displayed()) + expected_conditions.visibility_of_element_located( + (By.ID, "session_security_warning") + ) + ) + assert el.is_displayed() for win in self.sel.window_handles: - self.sel.switch_to_window(win) + self.sel.switch_to.window(win) delta = datetime.datetime.now() - start self.assertGreaterEqual(delta.seconds, self.min_warn_after) except: - assert(False) + assert False diff --git a/session_security/utils.py b/session_security/utils.py index a0dbccc..5c03159 100644 --- a/session_security/utils.py +++ b/session_security/utils.py @@ -36,4 +36,3 @@ def get_last_activity(session): return datetime.now() except TypeError: return datetime.now() - diff --git a/setup.py b/setup.py index c9e4cd0..18e5c01 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ def run(self): setup( name='django-session-security', - version='2.6.6.dev1', + version='3.0', description='Client and server side session timeout with warnings', author='∞', author_email='yourlabs@googlegroups.com', diff --git a/tox.ini b/tox.ini index 3b7fbdc..ba839cf 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] -envlist = py{27,35,36}-django{18,19,110,111} - py{35,36,37,38}-django{111,20} - py{36,37,38}-django{30,31,32} +envlist = py{36,37}-django{32}, py{38,39,310}-django{32,40} +skip_missing_interpreters = true + [testenv] usedevelop = true commands = @@ -10,21 +10,23 @@ deps = coverage unittest-data-provider selenium - django18: Django>=1.8,<1.9 - django19: Django>=1.9,<1.10 - django110: Django>=1.10,<1.11 - django111: Django>=1.11a1 - django20: Django >=2.0.0,<3.0.0 - django30: Django >=3.0,<3.1 - django31: Django >=3.1,<3.2 - django32: Django >=3.2 + django32: Django >=3.2,<4.0 + django40: Django >=4.0,<4.1 setenv = PIP_ALLOW_EXTERNAL=true DJANGO_SETTINGS_MODULE=session_security.tests.project.settings passenv = CI DISPLAY DBDIFF_* TEST_* TOX_* SAUCE_* [testenv:checkqa] -basepython = python2.7 -commands = pep8 --ignore E128 --exclude project session_security +basepython = python3.8 +commands = pycodestyle --max-line-length 88 --ignore E128,E722,W503 --exclude project session_security deps = - pep8 + pycodestyle + +[gh-actions] +python = + 3.6: py36 + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310