├── .editorconfig ├── .gitignore ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── README.rst ├── dodo.py ├── encrypted_id ├── __init__.py └── models.py ├── requirements.dev.txt ├── requirements1.10.txt ├── requirements1.11.txt ├── requirements1.8.txt ├── requirements1.9.txt ├── setup.cfg ├── setup.py ├── tests ├── manage.py ├── pytest.ini ├── tapp │ ├── __init__.py │ ├── admin.py │ ├── migrations │ │ ├── 0001_initial.py │ │ ├── 0002_auto_20170611_2026.py │ │ └── __init__.py │ ├── models.py │ ├── tests.py │ └── views.py ├── tekey │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── test_decode.py ├── test_ekey.py ├── test_encode.py ├── test_model.py └── test_view.py └── tox.ini /.editorconfig: -------------------------------------------------------------------------------- 1 | # Override for Makefile 2 | [{Makefile, makefile, GNUmakefile}] 3 | indent_style = tab 4 | indent_size = 4 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .doit.db 9 | tests/db.sqlite3 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | .hypothesis/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | 57 | # Sphinx documentation 58 | docs/_build/ 59 | 60 | # PyBuilder 61 | target/ 62 | 63 | # Ipython Notebook 64 | .ipynb_checkpoints 65 | 66 | # Editors 67 | .idea/ 68 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | - repo: git://github.com/pre-commit/pre-commit-hooks 2 | sha: v0.4.2 3 | hooks: 4 | - id: trailing-whitespace 5 | - id: flake8 6 | args: [--max-line-length=80] 7 | 8 | 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - 3.5 5 | 6 | os: 7 | - linux 8 | 9 | 10 | before_install: pip install --upgrade tox pip virtualenv wheel setuptools --quiet 11 | before_script: pyclean --verbose . 12 | script: tox 13 | 14 | notifications: 15 | email: 16 | recipients: upadhyay@gmail.com 17 | template: 18 | - "%{repository}@%{branch}: %{message}(%{build_url})" 19 | - "Build: %{build_number},SHA: %{commit},Committer: %{author},Diff: %{compare_url}" 20 | on_success: always 21 | on_failure: always 22 | 23 | 24 | cache: apt 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Amit Upadhyay 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | django-encrypted-id 2 | =================== 3 | 4 | **Note**: Encrypted IDs generated by version 0.3.0 onwards will be different 5 | from those generated by version 0.2.0. But versions 0.3.x will decrypt the IDs 6 | generated by the version 0.2.0. 7 | 8 | **Note**: Version 0.2.0 is a breaking change from versions 9 | `0.1.x `_. 10 | If you've been using *ekey* in permalinks, then it is recommended for you to 11 | not upgrade to 0.2.x. 12 | 13 | ---- 14 | 15 | Consider this example model: 16 | 17 | .. code-block:: python 18 | 19 | from django.db import models 20 | 21 | from encrypted_id.models import EncryptedIDModel 22 | 23 | 24 | class Foo(EncryptedIDModel): 25 | text = models.TextField() 26 | 27 | 28 | By inheriting from ``EncryptedIDModel``, you get .ekey as a property on your 29 | model instances. This is how they will look like: 30 | 31 | .. code-block:: python 32 | 33 | In [1]: from tapp.models import Foo 34 | 35 | In [2]: f = Foo.objects.create(text="asd") 36 | 37 | In [3]: f.id 38 | Out[3]: 1 39 | 40 | In [4]: f.ekey 41 | Out[4]: 'bxuZXwM4NdgGauVWR-ueUA' 42 | You can do reverse lookup: 43 | 44 | In [5]: from encrypted_id import decode 45 | 46 | In [6]: decode(f.ekey) 47 | Out[6]: 1 48 | 49 | If you can not inherit from the helper base class, no problem, you can just use 50 | the ``ekey()`` function from ``encrypted_id`` package: 51 | 52 | .. code-block:: python 53 | 54 | In [7]: from encrypted_id import ekey 55 | 56 | In [8]: from django.contrib.auth.models import User 57 | 58 | In [9]: ekey(User.objects.get(pk=1)) 59 | Out[9]: 'bxuZXwM4NdgGauVWR-ueUA' 60 | 61 | 62 | To do the reverse lookup, you have two helpers available. First is provided by 63 | ``EncryptedIDManager``, which is used by default if you inherit from 64 | ``EncryptedIDModel``, and have not overwritten the ``.objects``: 65 | 66 | .. code-block:: python 67 | 68 | In [10]: Foo.objects.get_by_ekey(f.ekey) 69 | Out[10]: 70 | 71 | 72 | But sometimes you will prefer the form: 73 | 74 | .. code-block:: python 75 | 76 | In [11]: Foo.objects.get_by_ekey_or_404(f.ekey) 77 | Out[11]: 78 | 79 | 80 | Which works the same, but instead of raising ``DoesNotExist``, it raises 81 | ``Http404``, so it can be used in views. 82 | 83 | You your manager is not inheriting from ``EncryptedIDManager``, you can use: 84 | 85 | .. code-block:: python 86 | 87 | In [12]: e = ekey(User.objects.first()) 88 | 89 | In [13]: e 90 | Out[13]: 'bxuZXwM4NdgGauVWR-ueUA' 91 | 92 | In [14]: get_object_or_404(User, e) 93 | Out[14]: 94 | 95 | 96 | ``encrypted_id.get_object_or_404``, as well as 97 | ``EncryptedIDManager.get_by_ekey`` and 98 | ``EncryptedIDManager.get_by_ekey_or_404`` take extra keyword argument, that can 99 | be used to filter if you want. 100 | 101 | If you are curious, the regex used to match the generated ids is: 102 | 103 | .. code-block:: python 104 | 105 | "[0-9a-zA-Z-_]+" 106 | 107 | 108 | If you are using `smarturls `_, you can use URL 109 | pattern like: 110 | 111 | .. code-block:: python 112 | 113 | "//" 114 | 115 | 116 | I recommend this usage of encrypted-id over UUID, as UUIDs have significant 117 | issues that should be considered (tldr: they take more space on disk and RAM, 118 | and have inferior indexing than integer ids), and if your goal is simply to 119 | make URLs non guessable, encrypted id is a superior approach. 120 | 121 | If you are curious about the encryption used: I am using ``AES``, from 122 | ``pycryptodomex`` library, and am using ``SECRET_KEY`` for password 123 | (``SECRET_KEY[:32]``) and ``IV`` (first 16 characters of hash of ``SECRET_KEY`` 124 | and a *sub_key*), in the ``AES.CBC`` mode. The *sub_key* is taken from the 125 | model's ``Meta`` attribute ``ek_key``, or simply ``db_table`` if ``ek_key`` is 126 | not set. 127 | 128 | In general it is recommended not to have static ``IV``, but ``CBC`` offsets 129 | some of the problems with having static IV. What is the the issue with static 130 | IV you ask: if plain text "abc" and "abe" are encrypted, the first two bytes 131 | would be same. Now this does not present a serious problem for us, as the 132 | plain text that I am encrypting uses ``CRC32`` in the beginning of payload, so 133 | even if you have ids, 1, 11, an attacker can not say they both start with same 134 | first character. 135 | 136 | The library also supports the scenario that you have to cycle ``SECRET_KEY`` 137 | due to some reason, so URLs encrypted with older ``SECRET_KEY`` can still be 138 | decoded after you have changed it (as long as you store old versions in 139 | ``SECRET_KEYS`` setting). In order to decrypt the library tries each secret 140 | key, and compares the ``CRC32`` of data to know for sure (as sure as things get 141 | in such things), that we have decrypted properly. 142 | 143 | Do feel free to raise an issue here, if you face any issues, I would be happy 144 | to help. The library supports both python 2.7 and 3.5, as well as it all 145 | versions of django that django team supports. 146 | 147 | -------------------------------------------------------------------------------- /dodo.py: -------------------------------------------------------------------------------- 1 | def task_aliases(): 2 | aliases = """ 3 | tests0: tox -e py27-django15 4 | tests: tox 5 | release: python setup.py bdist_wheel sdist --formats=bztar,zip upload 6 | cleanup: rm -rf .tox 7 | readme: restview README.rst 8 | """ 9 | for alias in aliases.split("\n"): 10 | alias = alias.strip() 11 | 12 | if not alias: 13 | continue 14 | 15 | name, command = alias.split(":", 1) 16 | 17 | yield { 18 | "basename": name, 19 | "actions": [command], 20 | 'verbosity': 2, 21 | } 22 | -------------------------------------------------------------------------------- /encrypted_id/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import absolute_import 3 | from __future__ import division 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | try: 8 | basestring 9 | except NameError: 10 | basestring = str 11 | 12 | from Cryptodome.Cipher import AES 13 | 14 | import base64 15 | import binascii 16 | import hashlib 17 | import struct 18 | 19 | from django.conf import settings 20 | from django.db.models import Model 21 | from django.http import Http404 22 | from django.shortcuts import get_object_or_404 as go4 23 | 24 | 25 | __version__ = "0.3.3" 26 | __license__ = "BSD" 27 | __author__ = "Amit Upadhyay" 28 | __email__ = "upadhyay@gmail.com" 29 | __url__ = "http://amitu.com/encrypted-id/" 30 | __source__ = "https://github.com/amitu/django-encrypted-id" 31 | __docformat__ = "html" 32 | 33 | 34 | class EncryptedIDDecodeError(Exception): 35 | def __init__(self, msg="Failed to decrypt, invalid input."): 36 | super(EncryptedIDDecodeError, self).__init__(msg) 37 | 38 | 39 | def encode(the_id, sub_key): 40 | assert 0 <= the_id < 2 ** 64 41 | 42 | version = 1 43 | 44 | crc = binascii.crc32(str(the_id).encode('utf-8')) & 0xffffffff 45 | 46 | message = struct.pack(b"=1.8", "pycryptodomex", 56 | ], 57 | 58 | 59 | packages=["encrypted_id"], 60 | zip_safe=True, 61 | 62 | 63 | keywords=['Django', 'Web'], 64 | 65 | 66 | classifiers=[ 67 | 68 | 'Development Status :: 5 - Production/Stable', 69 | 70 | 'Intended Audience :: Developers', 71 | 'Intended Audience :: End Users/Desktop', 72 | 'Intended Audience :: System Administrators', 73 | 'Intended Audience :: Information Technology', 74 | 'Intended Audience :: Other Audience', 75 | 76 | 'Natural Language :: English', 77 | 78 | 'Operating System :: OS Independent', 79 | 'Operating System :: POSIX :: Linux', 80 | 'Operating System :: Microsoft :: Windows', 81 | 'Operating System :: MacOS :: MacOS X', 82 | 83 | 'Programming Language :: Python', 84 | 'Programming Language :: Python :: 2.7', 85 | 'Programming Language :: Python :: 3.5', 86 | 87 | 'Programming Language :: Python :: Implementation :: CPython', 88 | 89 | 'Topic :: Software Development', 90 | 91 | ], 92 | ) 93 | -------------------------------------------------------------------------------- /tests/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tekey.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pep8maxlinelength = 120 -------------------------------------------------------------------------------- /tests/tapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amitu/django-encrypted-id/c7c085d822d3378f847efd9beeb1c1eca1a572fb/tests/tapp/__init__.py -------------------------------------------------------------------------------- /tests/tapp/admin.py: -------------------------------------------------------------------------------- 1 | # from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /tests/tapp/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ] 11 | 12 | operations = [ 13 | migrations.CreateModel( 14 | name='Foo', 15 | fields=[ 16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 17 | ('text', models.TextField()), 18 | ], 19 | options={ 20 | 'abstract': False, 21 | }, 22 | ), 23 | migrations.CreateModel( 24 | name='Foo2', 25 | fields=[ 26 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), 27 | ('text', models.TextField()), 28 | ], 29 | options={ 30 | 'abstract': False, 31 | }, 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /tests/tapp/migrations/0002_auto_20170611_2026.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.11.2 on 2017-06-11 20:26 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('tapp', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Bar', 17 | fields=[ 18 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 19 | ('text', models.TextField()), 20 | ], 21 | options={ 22 | 'abstract': False, 23 | }, 24 | ), 25 | migrations.AlterModelTable( 26 | name='foo', 27 | table='xxx', 28 | ), 29 | migrations.AlterModelTable( 30 | name='foo2', 31 | table='yyy', 32 | ), 33 | ] 34 | -------------------------------------------------------------------------------- /tests/tapp/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amitu/django-encrypted-id/c7c085d822d3378f847efd9beeb1c1eca1a572fb/tests/tapp/migrations/__init__.py -------------------------------------------------------------------------------- /tests/tapp/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from encrypted_id.models import EncryptedIDModel, EncryptedIDManager 4 | 5 | 6 | class Foo(EncryptedIDModel): 7 | text = models.TextField() 8 | 9 | class Meta: 10 | db_table = 'xxx' 11 | 12 | 13 | class Foo2Manager(EncryptedIDManager): 14 | pass 15 | 16 | 17 | class Foo2(EncryptedIDModel): 18 | text = models.TextField() 19 | 20 | class Meta: 21 | db_table = 'yyy' 22 | ek_key = 'xxx' 23 | 24 | 25 | class Bar(EncryptedIDModel): 26 | text = models.TextField() 27 | -------------------------------------------------------------------------------- /tests/tapp/tests.py: -------------------------------------------------------------------------------- 1 | # from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /tests/tapp/views.py: -------------------------------------------------------------------------------- 1 | from django.views.generic import DetailView 2 | 3 | from tapp.models import Foo 4 | 5 | 6 | class FooView(DetailView): 7 | model = Foo 8 | slug_field = 'ekey' 9 | template_name = 'admin/base.html' 10 | -------------------------------------------------------------------------------- /tests/tekey/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amitu/django-encrypted-id/c7c085d822d3378f847efd9beeb1c1eca1a572fb/tests/tekey/__init__.py -------------------------------------------------------------------------------- /tests/tekey/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for tekey project. 3 | 4 | Generated by 'django-admin startproject' using Django 1.8.7. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/1.8/ref/settings/ 11 | """ 12 | 13 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 14 | import os 15 | 16 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = '3qmq&fh=^fl-*a9e82uj$kyj8=0)2s9@n4j^@y6qa=p#^fzcco' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = [] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = ( 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | 41 | 'tapp' 42 | ) 43 | 44 | MIDDLEWARE_CLASSES = ( 45 | 'django.contrib.sessions.middleware.SessionMiddleware', 46 | 'django.middleware.common.CommonMiddleware', 47 | 'django.middleware.csrf.CsrfViewMiddleware', 48 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 49 | 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 50 | 'django.contrib.messages.middleware.MessageMiddleware', 51 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 52 | 'django.middleware.security.SecurityMiddleware', 53 | ) 54 | 55 | ROOT_URLCONF = 'tekey.urls' 56 | 57 | TEMPLATES = [ 58 | { 59 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 60 | 'DIRS': [], 61 | 'APP_DIRS': True, 62 | 'OPTIONS': { 63 | 'context_processors': [ 64 | 'django.template.context_processors.debug', 65 | 'django.template.context_processors.request', 66 | 'django.contrib.auth.context_processors.auth', 67 | 'django.contrib.messages.context_processors.messages', 68 | ], 69 | }, 70 | }, 71 | ] 72 | 73 | WSGI_APPLICATION = 'tekey.wsgi.application' 74 | 75 | 76 | # Database 77 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 78 | 79 | DATABASES = { 80 | 'default': { 81 | 'ENGINE': 'django.db.backends.sqlite3', 82 | 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), 83 | } 84 | } 85 | 86 | 87 | # Internationalization 88 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 89 | 90 | LANGUAGE_CODE = 'en-us' 91 | 92 | TIME_ZONE = 'UTC' 93 | 94 | USE_I18N = True 95 | 96 | USE_L10N = True 97 | 98 | USE_TZ = True 99 | 100 | 101 | # Static files (CSS, JavaScript, Images) 102 | # https://docs.djangoproject.com/en/1.8/howto/static-files/ 103 | 104 | STATIC_URL = '/static/' 105 | -------------------------------------------------------------------------------- /tests/tekey/urls.py: -------------------------------------------------------------------------------- 1 | """tekey URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Add an import: from blog import urls as blog_urls 14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls)) 15 | """ 16 | from django.conf.urls import include, url 17 | from django.contrib import admin 18 | 19 | from tapp.views import FooView 20 | 21 | urlpatterns = [ 22 | url(r'^admin/', include(admin.site.urls)), 23 | url(r'^foo/(?P[0-9a-zA-Z-_]+.{0,2})/$', FooView.as_view(), 24 | name='foo'), 25 | # surl('/foo//', FooView.as_view(), name='foo'), 26 | ] 27 | -------------------------------------------------------------------------------- /tests/tekey/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for tekey project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tekey.settings") 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /tests/test_decode.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from encrypted_id import EncryptedIDDecodeError, decode, encode 4 | 5 | 6 | def test_decode(): 7 | with pytest.raises(EncryptedIDDecodeError): 8 | decode("", "") # strucr.error 9 | with pytest.raises(EncryptedIDDecodeError): 10 | decode("1", "") # binascii.Error 11 | with pytest.raises(EncryptedIDDecodeError): 12 | decode(encode(0, "")[:-1] + 'Z', "") # crc error 13 | -------------------------------------------------------------------------------- /tests/test_ekey.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | import pytest 8 | from django.http import Http404 9 | 10 | from encrypted_id import ekey, get_object_or_404 11 | from tapp.models import Foo 12 | 13 | 14 | def test_ekey(db): 15 | assert db is db 16 | 17 | foo = Foo.objects.create(text="asd") 18 | assert ekey(foo) == foo.ekey 19 | assert foo == get_object_or_404(Foo, foo.ekey) 20 | 21 | 22 | def test_allow_none_ekey(db): 23 | assert db is db 24 | 25 | with pytest.raises(Http404): 26 | get_object_or_404(Foo, None) 27 | 28 | with pytest.raises(Foo.DoesNotExist): 29 | Foo.objects.get(ekey=None) 30 | -------------------------------------------------------------------------------- /tests/test_encode.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from encrypted_id import encode, decode 4 | 5 | 6 | def test_encode(): 7 | sub_key = uuid.uuid4().hex 8 | assert decode(encode(10, sub_key), sub_key) == 10 9 | -------------------------------------------------------------------------------- /tests/test_model.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | from tapp.models import Bar, Foo, Foo2 8 | from django.shortcuts import get_list_or_404, get_object_or_404 9 | from django.http import Http404 10 | import pytest 11 | 12 | 13 | def test_model(db): 14 | assert db is db 15 | 16 | foo = Foo.objects.create(text="hello") 17 | assert foo.ekey 18 | assert foo == Foo.objects.get_by_ekey(foo.ekey) 19 | assert foo == Foo.objects.get_by_ekey_or_404(foo.ekey) 20 | assert foo == Foo.objects.get(ekey=foo.ekey) 21 | assert foo == Foo.objects.filter(ekey=foo.ekey).get() 22 | 23 | foo = Foo2.objects.create(text="hello") 24 | assert foo.ekey 25 | assert foo == Foo2.objects.get_by_ekey(foo.ekey) 26 | assert foo == Foo2.objects.get_by_ekey_or_404(foo.ekey) 27 | assert foo == Foo2.objects.get(ekey=foo.ekey) 28 | assert foo == Foo2.objects.filter(ekey=foo.ekey).get() 29 | 30 | with pytest.raises(Http404): 31 | Foo.objects.get_by_ekey_or_404("123123") 32 | 33 | with pytest.raises(Http404): 34 | get_list_or_404(Foo, ekey="123123") 35 | 36 | with pytest.raises(Http404): 37 | get_object_or_404(Foo, ekey="123123") 38 | 39 | 40 | def test_sub_key(db): 41 | assert db is db 42 | 43 | foo = Foo.objects.create(text='hello') 44 | 45 | try: 46 | foo2 = Foo2.objects.get(pk=foo.pk) 47 | except Foo2.DoesNotExist: 48 | foo2 = Foo2.objects.create(pk=foo.pk, text='hello') 49 | 50 | try: 51 | bar = Bar.objects.get(pk=foo.pk) 52 | except Bar.DoesNotExist: 53 | bar = Bar.objects.create(pk=foo.pk, text='hello') 54 | 55 | assert foo.pk == foo2.pk == bar.pk 56 | assert foo.ekey == foo2.ekey 57 | assert foo.ekey != bar.ekey 58 | -------------------------------------------------------------------------------- /tests/test_view.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import division 3 | from __future__ import absolute_import 4 | from __future__ import print_function 5 | from __future__ import unicode_literals 6 | 7 | from django.core.urlresolvers import reverse 8 | from django.test import Client 9 | 10 | from tapp.models import Foo 11 | 12 | 13 | def test_view(db): 14 | assert db is db 15 | 16 | client = Client() 17 | 18 | foo = Foo.objects.create(text="hello") 19 | url = reverse("foo", args=(foo.ekey,)) 20 | response = client.get(url) 21 | response_object = response.context["object"] 22 | assert response_object == foo 23 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | downloadcache = {toxworkdir}/cache/ 3 | envlist = 4 | py27-django18, 5 | py27-django19, 6 | py27-django110, 7 | py27-django111, 8 | py35-django18, 9 | py35-django19, 10 | py35-django110, 11 | py35-django111 12 | 13 | 14 | [testenv] 15 | changedir = tests 16 | commands = 17 | py.test --ds tekey.settings --pep8 18 | 19 | [django18] 20 | deps = -rrequirements1.8.txt 21 | 22 | [django19] 23 | deps = -rrequirements1.9.txt 24 | 25 | [django110] 26 | deps = -rrequirements1.10.txt 27 | 28 | [django111] 29 | deps = -rrequirements1.11.txt 30 | 31 | 32 | [testenv:py27-django18] 33 | basepython = python2.7 34 | deps = {[django18]deps} 35 | 36 | [testenv:py27-django19] 37 | basepython = python2.7 38 | deps = {[django19]deps} 39 | 40 | [testenv:py27-django110] 41 | basepython = python2.7 42 | deps = {[django110]deps} 43 | 44 | [testenv:py27-django111] 45 | basepython = python2.7 46 | deps = {[django111]deps} 47 | 48 | [testenv:py35-django18] 49 | basepython = python3.5 50 | deps = {[django18]deps} 51 | 52 | [testenv:py35-django19] 53 | basepython = python3.5 54 | deps = {[django19]deps} 55 | 56 | [testenv:py35-django110] 57 | basepython = python3.5 58 | deps = {[django110]deps} 59 | 60 | [testenv:py35-django111] 61 | basepython = python3.5 62 | deps = {[django111]deps} 63 | --------------------------------------------------------------------------------