Commit 5a85c704 by Max Rothman
parents
Copyright (C) 2007 Simon Willison
Copyright (C) 2008-2010 Canonical Ltd.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
include Makefile
include MANIFEST.in
include LICENSE.txt
include README.txt
include TODO.txt
recursive-include django_openid_auth/templates *.html
recursive-include example_consumer *.py
check:
PYTHONPATH=$(shell pwd) python manage.py test --verbosity=2 django_openid_auth
run-example-consumer:
PYTHONPATH=$(shell pwd) python manage.py syncdb --migrate
PYTHONPATH=$(shell pwd) python manage.py runserver
.PHONY: check run-example-consumer
= Django OpenID Authentication Support =
This package provides integration between Django's authentication
system and OpenID authentication. It also includes support for using
a fixed OpenID server endpoint, which can be useful when implementing
single signon systems.
== Basic Installation ==
0. Install the Jan Rain Python OpenID library. It can be found at:
http://openidenabled.com/python-openid/
It can also be found in most Linux distributions packaged as
"python-openid". You will need version 2.2.0 or later.
1. If you are using Django 1.6, configure your project to use the
pickle based session serializer:
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
2. Add 'django_openid_auth' to INSTALLED_APPS for your application.
At a minimum, you'll need the following in there:
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django_openid_auth',
)
3. Add 'django_auth_openid.auth.OpenIDBackend' to
AUTHENTICATION_BACKENDS. This should be in addition to the
default ModelBackend:
AUTHENTICATION_BACKENDS = (
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
4. To create users automatically when a new OpenID is used, add the
following to the settings:
OPENID_CREATE_USERS = True
5. To have user details updated from OpenID Simple Registration or
Attribute Exchange extension data each time they log in, add the
following:
OPENID_UPDATE_DETAILS_FROM_SREG = True
6. Hook up the login URLs to your application's urlconf with
something like:
urlpatterns = patterns('',
...
(r'^openid/', include('django_openid_auth.urls')),
...
)
7. Configure the LOGIN_URL and LOGIN_REDIRECT_URL appropriately for
your site:
LOGIN_URL = '/openid/login/'
LOGIN_REDIRECT_URL = '/'
This will allow pages that use the standard @login_required
decorator to use the OpenID login page.
8. Rerun "python manage.py syncdb" to add the UserOpenID table to
your database.
== Configuring Single Sign-On ==
If you only want to accept identities from a single OpenID server and
that server implemnts OpenID 2.0 identifier select mode, add the
following setting to your app:
OPENID_SSO_SERVER_URL = 'server-endpoint-url'
With this setting enabled, the user will not be prompted to enter
their identity URL, and instead an OpenID authentication request will
be started with the given server URL.
As an example, to use Launchpad accounts for SSO, you'd use:
OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
== Launchpad Teams Support ==
This library supports the Launchpad Teams OpenID extension. Using
this feature, it is possible to map Launchpad team memberships to
Django group memberships. It can be configured with:
OPENID_SSO_SERVER_URL = 'https://login.launchpad.net/'
OPENID_LAUNCHPAD_TEAMS_MAPPING = {
'launchpad-team-1': 'django-group-1',
'launchpad-team-2': 'django-group-2',
}
When a user logs in, they will be added or removed from the relevant
teams listed in the mapping.
If you have already django-groups and want to map these groups automatically, you can use the OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO variable in your settings.py file.
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO = True
If you use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO, the variable OPENID_LAUNCHPAD_TEAMS_MAPPING will be ignored.
If you want to exclude some groups from the auto mapping, use OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST. This variable has only an effect if OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO is True.
OPENID_LAUNCHPAD_TEAMS_MAPPING_AUTO_BLACKLIST = ['django-group1', 'django-group2']
If you want to restrict login to a subset of teams, so that only members of
those teams can login, you can use the OPENID_LAUNCHPAD_TEAMS_REQUIRED variable
in your settings.py file.
OPENID_LAUNCHPAD_TEAMS_REQUIRED = ['launchpad-team-1', 'launchpad-team-2']
Some accounts can be whitelisted from this required team restriction. This is
specifically useful for doing testing. In order to whitelist an account from
the required teams restriction you can use the OPENID_EMAIL_WHITELIST_REGEXP_LIST setting.
As an example, the following value
OPENID_EMAIL_WHITELIST_REGEXP_LIST = ['foo(\+[^@]*)?@foo.com']
would whitelist users with the following emails (and other matching the regular expression)
from being in a required team:
foo@foo.com
foo+bar@foo.com
== External redirect domains ==
By default, redirecting back to an external URL after auth is forbidden. To permit redirection to external URLs on a separate domain, define ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS in your settings.py file as a list of permitted domains:
ALLOWED_EXTERNAL_OPENID_REDIRECT_DOMAINS = ['example.com', 'example.org']
and redirects to external URLs on those domains will additionally be permitted.
== Use as /admin (django.admin.contrib) login ==
If you require openid authentication into the admin application, add the following setting:
OPENID_USE_AS_ADMIN_LOGIN = True
It is worth noting that a user needs to be be marked as a "staff user" to be able to access the admin interface. A new openid user will not normally be a "staff user".
The easiest way to resolve this is to use traditional authentication (OPENID_USE_AS_ADMIN_LOGIN = False) to sign in as your first user with a password and authorise your
openid user to be staff.
== Change Django usernames if the nickname changes on the provider ==
If you want your Django username to change when a user updates the nickname on their provider, add the following setting:
OPENID_FOLLOW_RENAMES = True
If the new nickname is available as a Django username, the user is renamed.
Otherwise the user will be renamed to nickname+i for an incrememnting value of i until no conflict occurs.
If the user has already been renamed to nickname+1 due to a conflict, and the nickname is still not available, the user will keep their existing username.
== Require a valid nickname ==
If you must have a valid, unique nickname in order to create a user accont, add the following setting:
OPENID_STRICT_USERNAMES = True
This will cause an OpenID login attempt to fail if the provider does not return a 'nickname' (username) for the user, or if the nickname conflicts with an existing user with a different openid identiy url.
Without this setting, logins without a nickname will be given the username 'openiduser', and upon conflicts with existing username, an incrementing number will be appended to the username until it is unique.
== Require Physical Multi-Factor Authentication ==
If your users should use a physical multi-factor authentication method, such as RSA tokens or YubiKey, add the following setting:
OPENID_PHYSICAL_MULTIFACTOR_REQUIRED = True
If the user's OpenID provider supports the PAPE extension and provides the Physical Multifactor authentication policy, this will
cause the OpenID login to fail if the user does not provide valid physical authentication to the provider.
== Override Login Failure Handling ==
You can optionally provide your own handler for login failures by adding the following setting:
OPENID_RENDER_FAILURE = failure_handler_function
Where failure_handler_function is a function reference that will take the following parameters:
def failure_handler_function(request, message, status=None, template_name=None, exception=None)
This function must return a Django.http.HttpResponse instance.
== Use the user's email for suggested usernames ==
You can optionally strip out non-alphanumeric characters from the user's email
to generate a preferred username, if the server doesn't provide nick
information, by setting the following setting:
OPENID_USE_EMAIL_FOR_USERNAME = True
Otherwise, and by default, if the server omits nick information and a user is
created it'll receive a username 'openiduser' + a number.
Consider also the OPENID_STRICT_USERNAMES setting (see ``Require a valid nickname``)
== Specify Valid Account Verification Schemes ==
When using OpenID Attribute Exchange, the attribute URI
http://ns.login.ubuntu.com/2013/validation/account is included in the request.
OpenID Providers that support this extension can reply with a token
representing what measures they have taken to validate the e-mail address
included in the response. To change the list of schemes acceptable for your
purposes you can change the setting:
OPENID_VALID_VERIFICATION_SCHEMES = {
None: (),
'http://example.com/': ('token_via_email',),
}
The element with the None key specifies a list of verification schemes that
will be accepted as trusted from OpenID Providers that we haven't explicitly
configured. These are, almost by definition, untrusted, so it is strongly
recommended that this list remain empty. Verified accounts will be granted the
django_openid_auth.account_verified permission, which can be checked using
user.has_perm() and the perms RequestContext attribute in the normal way.
N.B. Users of the South migration framework will need to provide a data
migration to create the permission when upgrading django-openid-auth, due to a
known issue in South. See http://south.aeracode.org/ticket/211 for details.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2008-2013 Canonical Ltd.
# Copyright (C) 2010 Dave Walker
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from urllib import urlencode
from urlparse import parse_qsl, urlparse
from django.conf import settings
from django.contrib import admin
from django.http import HttpResponseRedirect
from django_openid_auth import views
from django_openid_auth.models import Nonce, Association, UserOpenID
from django_openid_auth.store import DjangoOpenIDStore
class NonceAdmin(admin.ModelAdmin):
list_display = ('server_url', 'timestamp')
actions = ['cleanup_nonces']
def cleanup_nonces(self, request, queryset):
store = DjangoOpenIDStore()
count = store.cleanupNonces()
self.message_user(request, "%d expired nonces removed" % count)
cleanup_nonces.short_description = "Clean up expired nonces"
admin.site.register(Nonce, NonceAdmin)
class AssociationAdmin(admin.ModelAdmin):
list_display = ('server_url', 'assoc_type')
list_filter = ('assoc_type',)
search_fields = ('server_url',)
actions = ['cleanup_associations']
def cleanup_associations(self, request, queryset):
store = DjangoOpenIDStore()
count = store.cleanupAssociations()
self.message_user(request, "%d expired associations removed" % count)
cleanup_associations.short_description = "Clean up expired associations"
admin.site.register(Association, AssociationAdmin)
class UserOpenIDAdmin(admin.ModelAdmin):
raw_id_fields = ('user',)
list_display = ('user', 'claimed_id')
search_fields = ('claimed_id',)
admin.site.register(UserOpenID, UserOpenIDAdmin)
# store a reference to the original admin login
original_admin_login = admin.sites.AdminSite.login
def _openid_login(instance, request, error_message='', extra_context=None):
# Support for allowing openid authentication for /admin
# (django.contrib.admin)
if not getattr(settings, 'OPENID_USE_AS_ADMIN_LOGIN', False):
return original_admin_login(
instance, request, extra_context=extra_context)
if not request.user.is_authenticated():
# Redirect to openid login path,
_, _, path, _, query, _ = urlparse(request.get_full_path())
qs = dict(parse_qsl(query))
qs.setdefault('next', path)
return HttpResponseRedirect(
settings.LOGIN_URL + "?" + urlencode(qs))
if not request.user.is_staff:
return views.default_render_failure(
request, "User %s does not have admin/staff access."
% request.user.username)
# No error message was supplied
assert error_message, "Unknown Error: %s" % error_message
# Overide the standard admin login form.
admin.sites.AdminSite.login = _openid_login
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Exception classes thrown by OpenID Authentication and Validation."""
from __future__ import unicode_literals
class DjangoOpenIDException(Exception):
pass
class RequiredAttributeNotReturned(DjangoOpenIDException):
pass
class IdentityAlreadyClaimed(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = (
"Another user already exists for your selected OpenID")
else:
self.message = message
class DuplicateUsernameViolation(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = "Your desired username is already being used."
else:
self.message = message
class MissingUsernameViolation(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = "No nickname given for your account."
else:
self.message = message
class MissingPhysicalMultiFactor(DjangoOpenIDException):
def __init__(self, message=None):
if message is None:
self.message = (
"Login requires physical multi-factor authentication.")
else:
self.message = message
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from django import forms
from django.contrib.auth.admin import UserAdmin
from django.contrib.auth.forms import UserChangeForm
from django.contrib.auth.models import Group
from django.utils.translation import ugettext as _
from django.conf import settings
from openid.yadis import xri
def teams_new_unicode(self):
"""
Replacement for Group.__unicode__()
Calls original method to chain results
"""
name = self.unicode_before_teams()
teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
group_teams = [t for t in teams_mapping if teams_mapping[t] == self.name]
if len(group_teams) > 0:
return "%s -> %s" % (name, ", ".join(group_teams))
else:
return name
Group.unicode_before_teams = Group.__unicode__
Group.__unicode__ = teams_new_unicode
class UserChangeFormWithTeamRestriction(UserChangeForm):
"""
Extends UserChangeForm to add teams awareness to the user admin form
"""
def clean_groups(self):
data = self.cleaned_data['groups']
teams_mapping = getattr(settings, 'OPENID_LAUNCHPAD_TEAMS_MAPPING', {})
known_teams = teams_mapping.values()
user_groups = self.instance.groups.all()
for group in data:
if group.name in known_teams and group not in user_groups:
raise forms.ValidationError(
"The group %s is mapped to an external team. "
"You cannot assign it manually." % group.name)
return data
UserAdmin.form = UserChangeFormWithTeamRestriction
class OpenIDLoginForm(forms.Form):
openid_identifier = forms.CharField(
max_length=255,
widget=forms.TextInput(attrs={'class': 'required openid'}))
def clean_openid_identifier(self):
if 'openid_identifier' in self.cleaned_data:
openid_identifier = self.cleaned_data['openid_identifier']
if (xri.identifierScheme(openid_identifier) == 'XRI' and
getattr(settings, 'OPENID_DISALLOW_INAMES', False)):
raise forms.ValidationError(_('i-names are not supported'))
return self.cleaned_data['openid_identifier']
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from django.core.management.base import NoArgsCommand
from django_openid_auth.store import DjangoOpenIDStore
class Command(NoArgsCommand):
help = 'Clean up stale OpenID associations and nonces'
def handle_noargs(self, **options):
store = DjangoOpenIDStore()
store.cleanup()
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Association',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('server_url', models.TextField(max_length=2047)),
('handle', models.CharField(max_length=255)),
('secret', models.TextField(max_length=255)),
('issued', models.IntegerField()),
('lifetime', models.IntegerField()),
('assoc_type', models.TextField(max_length=64)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Nonce',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('server_url', models.CharField(max_length=2047)),
('timestamp', models.IntegerField()),
('salt', models.CharField(max_length=40)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='UserOpenID',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('claimed_id', models.TextField(unique=True, max_length=2047)),
('display_id', models.TextField(max_length=2047)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
],
options={
'permissions': (('account_verified', 'The OpenID has been verified'),),
},
bases=(models.Model,),
),
]
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from django.db import models
from django.contrib.auth.models import Permission, User
class Nonce(models.Model):
server_url = models.CharField(max_length=2047)
timestamp = models.IntegerField()
salt = models.CharField(max_length=40)
def __unicode__(self):
return u"Nonce: %s, %s" % (self.server_url, self.salt)
class Association(models.Model):
server_url = models.TextField(max_length=2047)
handle = models.CharField(max_length=255)
secret = models.TextField(max_length=255) # Stored base64 encoded
issued = models.IntegerField()
lifetime = models.IntegerField()
assoc_type = models.TextField(max_length=64)
def __unicode__(self):
return u"Association: %s, %s" % (self.server_url, self.handle)
class UserOpenID(models.Model):
user = models.ForeignKey(User)
claimed_id = models.TextField(max_length=2047, unique=True)
display_id = models.TextField(max_length=2047)
class Meta:
permissions = (
('account_verified', 'The OpenID has been verified'),
)
def delete(self, using=None):
permission = Permission.objects.get(codename='account_verified')
self.user.user_permissions.remove(permission)
super(UserOpenID, self).delete(using)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
import django.dispatch
openid_login_complete = django.dispatch.Signal(providing_args=[
'request', 'openid_response'])
# -*- 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 'Nonce'
db.create_table(u'django_openid_auth_nonce', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('server_url', self.gf('django.db.models.fields.CharField')(max_length=2047)),
('timestamp', self.gf('django.db.models.fields.IntegerField')()),
('salt', self.gf('django.db.models.fields.CharField')(max_length=40)),
))
db.send_create_signal(u'django_openid_auth', ['Nonce'])
# Adding model 'Association'
db.create_table(u'django_openid_auth_association', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('server_url', self.gf('django.db.models.fields.TextField')(max_length=2047)),
('handle', self.gf('django.db.models.fields.CharField')(max_length=255)),
('secret', self.gf('django.db.models.fields.TextField')(max_length=255)),
('issued', self.gf('django.db.models.fields.IntegerField')()),
('lifetime', self.gf('django.db.models.fields.IntegerField')()),
('assoc_type', self.gf('django.db.models.fields.TextField')(max_length=64)),
))
db.send_create_signal(u'django_openid_auth', ['Association'])
# Adding model 'UserOpenID'
db.create_table(u'django_openid_auth_useropenid', (
(u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
('claimed_id', self.gf('django.db.models.fields.TextField')(unique=True, max_length=2047)),
('display_id', self.gf('django.db.models.fields.TextField')(max_length=2047)),
))
db.send_create_signal(u'django_openid_auth', ['UserOpenID'])
def backwards(self, orm):
# Deleting model 'Nonce'
db.delete_table(u'django_openid_auth_nonce')
# Deleting model 'Association'
db.delete_table(u'django_openid_auth_association')
# Deleting model 'UserOpenID'
db.delete_table(u'django_openid_auth_useropenid')
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'django_openid_auth.association': {
'Meta': {'object_name': 'Association'},
'assoc_type': ('django.db.models.fields.TextField', [], {'max_length': '64'}),
'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'issued': ('django.db.models.fields.IntegerField', [], {}),
'lifetime': ('django.db.models.fields.IntegerField', [], {}),
'secret': ('django.db.models.fields.TextField', [], {'max_length': '255'}),
'server_url': ('django.db.models.fields.TextField', [], {'max_length': '2047'})
},
u'django_openid_auth.nonce': {
'Meta': {'object_name': 'Nonce'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'salt': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047'}),
'timestamp': ('django.db.models.fields.IntegerField', [], {})
},
u'django_openid_auth.useropenid': {
'Meta': {'object_name': 'UserOpenID'},
'claimed_id': ('django.db.models.fields.TextField', [], {'unique': 'True', 'max_length': '2047'}),
'display_id': ('django.db.models.fields.TextField', [], {'max_length': '2047'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
}
}
complete_apps = ['django_openid_auth']
\ No newline at end of file
# -*- coding: utf-8 -*-
from south.utils import datetime_utils as datetime
from south.db import db
from south.v2 import DataMigration
from django.db import connection, models, transaction
class Migration(DataMigration):
def add_account_verified_permission(self, orm):
ct, _ = orm['contenttypes.ContentType'].objects.get_or_create(
model='useropenid', app_label='django_openid_auth',
defaults=dict(name='user open id'))
perm, _ = orm['auth.permission'].objects.get_or_create(
content_type=ct, codename='account_verified',
defaults=dict(name=u'The OpenID account has been verified'))
def forwards(self, orm):
"Write your forwards methods here."
if getattr(connection.features,
'autocommits_when_autocommit_is_off', False):
# likely sqlite3 with django 1.6 and above
with transaction.autocommit():
self.add_account_verified_permission(orm)
else:
self.add_account_verified_permission(orm)
def backwards(self, orm):
"Write your backwards methods here."
models = {
u'auth.group': {
'Meta': {'object_name': 'Group'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
},
u'auth.permission': {
'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
},
u'auth.user': {
'Meta': {'object_name': 'User'},
'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
},
u'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
u'django_openid_auth.association': {
'Meta': {'object_name': 'Association'},
'assoc_type': ('django.db.models.fields.TextField', [], {'max_length': '64'}),
'handle': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'issued': ('django.db.models.fields.IntegerField', [], {}),
'lifetime': ('django.db.models.fields.IntegerField', [], {}),
'secret': ('django.db.models.fields.TextField', [], {'max_length': '255'}),
'server_url': ('django.db.models.fields.TextField', [], {'max_length': '2047'})
},
u'django_openid_auth.nonce': {
'Meta': {'object_name': 'Nonce'},
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'salt': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'server_url': ('django.db.models.fields.CharField', [], {'max_length': '2047'}),
'timestamp': ('django.db.models.fields.IntegerField', [], {})
},
u'django_openid_auth.useropenid': {
'Meta': {'object_name': 'UserOpenID'},
'claimed_id': ('django.db.models.fields.TextField', [], {'unique': 'True', 'max_length': '2047'}),
'display_id': ('django.db.models.fields.TextField', [], {'max_length': '2047'}),
u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']"})
}
}
complete_apps = ['contenttypes', 'auth', 'django_openid_auth']
symmetrical = True
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
import base64
import time
from openid.association import Association as OIDAssociation
from openid.store.interface import OpenIDStore
from openid.store.nonce import SKEW
from django_openid_auth.models import Association, Nonce
class DjangoOpenIDStore(OpenIDStore):
def __init__(self):
super(DjangoOpenIDStore, self).__init__()
self.max_nonce_age = 6 * 60 * 60 # Six hours
def storeAssociation(self, server_url, association):
try:
assoc = Association.objects.get(
server_url=server_url, handle=association.handle)
except Association.DoesNotExist:
assoc = Association(
server_url=server_url,
handle=association.handle,
secret=base64.encodestring(association.secret),
issued=association.issued,
lifetime=association.lifetime,
assoc_type=association.assoc_type)
else:
assoc.secret = base64.encodestring(association.secret)
assoc.issued = association.issued
assoc.lifetime = association.lifetime
assoc.assoc_type = association.assoc_type
assoc.save()
def getAssociation(self, server_url, handle=None):
assocs = []
if handle is not None:
assocs = Association.objects.filter(
server_url=server_url, handle=handle)
else:
assocs = Association.objects.filter(server_url=server_url)
associations = []
expired = []
for assoc in assocs:
association = OIDAssociation(
assoc.handle, base64.decodestring(assoc.secret), assoc.issued,
assoc.lifetime, assoc.assoc_type
)
if association.getExpiresIn() == 0:
expired.append(assoc)
else:
associations.append((association.issued, association))
for assoc in expired:
assoc.delete()
if not associations:
return None
associations.sort()
return associations[-1][1]
def removeAssociation(self, server_url, handle):
assocs = list(Association.objects.filter(
server_url=server_url, handle=handle))
assocs_exist = len(assocs) > 0
for assoc in assocs:
assoc.delete()
return assocs_exist
def useNonce(self, server_url, timestamp, salt):
if abs(timestamp - time.time()) > SKEW:
return False
try:
ononce = Nonce.objects.get(
server_url__exact=server_url,
timestamp__exact=timestamp,
salt__exact=salt)
except Nonce.DoesNotExist:
ononce = Nonce(
server_url=server_url,
timestamp=timestamp,
salt=salt)
ononce.save()
return True
return False
def cleanupNonces(self, _now=None):
if _now is None:
_now = int(time.time())
expired = Nonce.objects.filter(timestamp__lt=_now - SKEW)
count = expired.count()
if count:
expired.delete()
return count
def cleanupAssociations(self):
now = int(time.time())
expired = Association.objects.extra(
where=['issued + lifetime < %d' % now])
count = expired.count()
if count:
expired.delete()
return count
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>OpenID failed</title>
</head>
<body>
<h1>OpenID failed</h1>
<p>{{ message|escape }}</p>
</body>
</html>
{% load i18n %}
{% load url from future %}
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>Sign in with your OpenID</title>
<style type="text/css">
input.openid {
background: url({% url 'openid-logo' %}) no-repeat;
background-position: 0 50%;
padding-left: 16px;
}
</style>
</head>
<body>
<h1>Sign in with your OpenID</h1>
{% if form.errors %}
<p class="errors">{% trans "Please correct errors below:" %}<br />
{% if form.openid_identifier.errors %}
<span class="error">{{ form.openid_identifier.errors|join:", " }}</span>
{% endif %}
{% if form.next.errors %}
<span class="error">{{ form.next.errors|join:", " }}</span>
{% endif %}
</p>
{% endif %}
<form name="fopenid" action="{{ action }}" method="post">
{% csrf_token %}
<fieldset>
<legend>{% trans "Sign In Using Your OpenID" %}</legend>
<div class="form-row">
<label for="id_openid_identifier">{% trans "OpenID:" %}</label><br />
{{ form.openid_identifier }}
</div>
<div class="submit-row ">
<input name="bsignin" type="submit" value="{% trans "Sign in" %}">
</div>
{% if next %}
<input type="hidden" name="next" value="{{ next }}" />
{% endif %}
</fieldset>
</form>
</body>
</html>
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from .test_views import * # flake8: noqa
from .test_settings import *
from .test_store import *
from .test_auth import *
from .test_admin import *
from __future__ import unicode_literals
from django.test.utils import override_settings
override_session_serializer = override_settings(
SESSION_SERIALIZER='django.contrib.sessions.serializers.PickleSerializer')
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# 'AS IS' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""Tests for the django_openid_auth Admin login form replacement."""
from __future__ import unicode_literals
from django.conf import settings
from django.contrib.auth.models import User
from django.test import TestCase
from django.test.utils import override_settings
@override_settings(OPENID_USE_AS_ADMIN_LOGIN=True)
class SiteAdminTests(TestCase):
"""
TestCase for accessing /admin/ when the django_openid_auth form replacement
is in use.
"""
def test_admin_site_with_openid_login_authenticated_non_staff(self):
"""
If the request has an authenticated user, who is not flagged as a
staff member, then they get a failure response.
"""
User.objects.create_user(
username='testing', email='testing@example.com', password='test')
assert self.client.login(username='testing', password='test')
response = self.client.get('/admin/', follow=True)
self.assertContains(
response,
'User testing does not have admin/staff access.', status_code=403)
def test_admin_site_with_openid_login_non_authenticated_user(self):
"""
Unauthenticated users accessing the admin page should be directed to
the OpenID login url.
"""
response = self.client.get('/admin/', follow=True)
self.assertRedirects(
response,
getattr(settings, 'LOGIN_URL', '/openid/login') +
'?next=%2Fadmin%2F')
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from django.contrib.auth.models import User
from django.test import TestCase
from django_openid_auth.models import (
Permission,
UserOpenID,
)
class UserOpenIDModelTestCase(TestCase):
def test_create_useropenid(self):
user = User.objects.create_user('someuser', 'someuser@example.com',
password=None)
user_openid, created = UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
self.assertEqual('someuser', user_openid.user.username)
self.assertEqual(
user_openid.claimed_id, 'http://example.com/existing_identity')
self.assertEqual(
user_openid.display_id, 'http://example.com/existing_identity')
self.assertFalse(
User.objects.get(username='someuser').has_perm(
'django_openid_auth.account_verified'))
def test_delete_verified_useropenid(self):
user = User.objects.create_user('someuser', 'someuser@example.com',
password=None)
user_openid, created = UserOpenID.objects.get_or_create(
user=user,
claimed_id='http://example.com/existing_identity',
display_id='http://example.com/existing_identity')
permission = Permission.objects.get(codename='account_verified')
user.user_permissions.add(permission)
self.assertTrue(
User.objects.get(username='someuser').has_perm(
'django_openid_auth.account_verified'))
user_openid.delete()
self.assertFalse(
User.objects.get(username='someuser').has_perm(
'django_openid_auth.account_verified'))
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from unittest import skipIf
from django import VERSION
from django.conf import settings
from django.test import TestCase
class SessionSerializerTest(TestCase):
"""Django 1.6 changed the default session serializer to use JSON
instead of pickle for security reasons[0]. Unfortunately the
openid module on which we rely stores objects which are not JSON
serializable[1], so until this is fixed upstream (or we decide to
create a wrapper serializer) we are recommending Django 1.6 users
to fallback to the PickleSerializer.
[0] https://bit.ly/1myzetd
[1] https://github.com/openid/python-openid/issues/17
"""
@skipIf(VERSION < (1, 5), "Django 1.4 does not provide SESSION_SERIALIZER")
def test_using_pickle_session_serializer(self):
serializer = getattr(settings, 'SESSION_SERIALIZER', '')
self.assertEqual(
serializer, 'django.contrib.sessions.serializers.PickleSerializer')
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
import time
from django.test import TestCase
from openid.association import Association as OIDAssociation
from openid.store.nonce import SKEW
from django_openid_auth.models import Association, Nonce
from django_openid_auth.store import DjangoOpenIDStore
class OpenIDStoreTests(TestCase):
def setUp(self):
super(OpenIDStoreTests, self).setUp()
self.store = DjangoOpenIDStore()
def test_storeAssociation(self):
assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1')
self.store.storeAssociation('server-url', assoc)
dbassoc = Association.objects.get(
server_url='server-url', handle='handle')
self.assertEquals(dbassoc.server_url, 'server-url')
self.assertEquals(dbassoc.handle, 'handle')
self.assertEquals(dbassoc.secret, 'secret'.encode('base-64'))
self.assertEquals(dbassoc.issued, 42)
self.assertEquals(dbassoc.lifetime, 600)
self.assertEquals(dbassoc.assoc_type, 'HMAC-SHA1')
def test_storeAssociation_update_existing(self):
assoc = OIDAssociation('handle', 'secret', 42, 600, 'HMAC-SHA1')
self.store.storeAssociation('server-url', assoc)
# Now update the association with new information.
assoc = OIDAssociation('handle', 'secret2', 420, 900, 'HMAC-SHA256')
self.store.storeAssociation('server-url', assoc)
dbassoc = Association.objects.get(
server_url='server-url', handle='handle')
self.assertEqual(dbassoc.secret, 'secret2'.encode('base-64'))
self.assertEqual(dbassoc.issued, 420)
self.assertEqual(dbassoc.lifetime, 900)
self.assertEqual(dbassoc.assoc_type, 'HMAC-SHA256')
def test_getAssociation(self):
timestamp = int(time.time())
self.store.storeAssociation(
'server-url', OIDAssociation('handle', 'secret', timestamp, 600,
'HMAC-SHA1'))
assoc = self.store.getAssociation('server-url', 'handle')
self.assertTrue(isinstance(assoc, OIDAssociation))
self.assertEquals(assoc.handle, 'handle')
self.assertEquals(assoc.secret, 'secret')
self.assertEquals(assoc.issued, timestamp)
self.assertEquals(assoc.lifetime, 600)
self.assertEquals(assoc.assoc_type, 'HMAC-SHA1')
def test_getAssociation_unknown(self):
assoc = self.store.getAssociation('server-url', 'unknown')
self.assertEquals(assoc, None)
def test_getAssociation_expired(self):
lifetime = 600
timestamp = int(time.time()) - 2 * lifetime
self.store.storeAssociation(
'server-url', OIDAssociation('handle', 'secret', timestamp,
lifetime, 'HMAC-SHA1'))
# The association is not returned, and is removed from the database.
assoc = self.store.getAssociation('server-url', 'handle')
self.assertEquals(assoc, None)
self.assertRaises(Association.DoesNotExist, Association.objects.get,
server_url='server-url', handle='handle')
def test_getAssociation_no_handle(self):
timestamp = int(time.time())
self.store.storeAssociation(
'server-url', OIDAssociation('handle1', 'secret', timestamp + 1,
600, 'HMAC-SHA1'))
self.store.storeAssociation(
'server-url', OIDAssociation('handle2', 'secret', timestamp,
600, 'HMAC-SHA1'))
# The newest handle is returned.
assoc = self.store.getAssociation('server-url', None)
self.assertNotEquals(assoc, None)
self.assertEquals(assoc.handle, 'handle1')
self.assertEquals(assoc.issued, timestamp + 1)
def test_removeAssociation(self):
timestamp = int(time.time())
self.store.storeAssociation(
'server-url', OIDAssociation('handle', 'secret', timestamp, 600,
'HMAC-SHA1'))
self.assertEquals(
self.store.removeAssociation('server-url', 'handle'), True)
self.assertEquals(
self.store.getAssociation('server-url', 'handle'), None)
def test_removeAssociation_unknown(self):
self.assertEquals(
self.store.removeAssociation('server-url', 'unknown'), False)
def test_useNonce(self):
timestamp = time.time()
# The nonce can only be used once.
self.assertEqual(
self.store.useNonce('server-url', timestamp, 'salt'), True)
self.assertEqual(
self.store.useNonce('server-url', timestamp, 'salt'), False)
self.assertEqual(
self.store.useNonce('server-url', timestamp, 'salt'), False)
def test_useNonce_expired(self):
timestamp = time.time() - 2 * SKEW
self.assertEqual(
self.store.useNonce('server-url', timestamp, 'salt'), False)
def test_useNonce_future(self):
timestamp = time.time() + 2 * SKEW
self.assertEqual(
self.store.useNonce('server-url', timestamp, 'salt'), False)
def test_cleanupNonces(self):
timestamp = time.time()
self.assertEqual(
self.store.useNonce('server1', timestamp, 'salt1'), True)
self.assertEqual(
self.store.useNonce('server2', timestamp, 'salt2'), True)
self.assertEqual(
self.store.useNonce('server3', timestamp, 'salt3'), True)
self.assertEqual(Nonce.objects.count(), 3)
self.assertEqual(
self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 3)
self.assertEqual(Nonce.objects.count(), 0)
# The nonces have now been cleared:
self.assertEqual(
self.store.useNonce('server1', timestamp, 'salt1'), True)
self.assertEqual(
self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 1)
self.assertEqual(
self.store.cleanupNonces(_now=timestamp + 2 * SKEW), 0)
def test_cleanupAssociations(self):
timestamp = int(time.time()) - 100
self.store.storeAssociation(
'server-url', OIDAssociation('handle1', 'secret', timestamp,
50, 'HMAC-SHA1'))
self.store.storeAssociation(
'server-url', OIDAssociation('handle2', 'secret', timestamp,
200, 'HMAC-SHA1'))
self.assertEquals(self.store.cleanupAssociations(), 1)
# The second (non-expired) association is left behind.
self.assertNotEqual(self.store.getAssociation('server-url', 'handle2'),
None)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from django.conf.urls import patterns, include
from django.http import HttpResponse
def get_user(request):
return HttpResponse(request.user.username)
urlpatterns = patterns(
'',
(r'^getuser/$', get_user),
(r'^openid/', include('django_openid_auth.urls')),
)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from __future__ import unicode_literals
from django.conf.urls import patterns, url
urlpatterns = patterns(
'django_openid_auth.views',
url(r'^login/$', 'login_begin', name='openid-login'),
url(r'^complete/$', 'login_complete', name='openid-complete'),
url(r'^logo.gif$', 'logo', name='openid-logo'),
)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""
Django settings for example_consumer project.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.7/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
import django
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '34958734985734985734985798437'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = True
ALLOWED_HOSTS = []
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
)
# Application definition
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.admin',
'django_openid_auth',
)
if django.VERSION < (1, 7):
INSTALLED_APPS += ('south',)
ROOT_URLCONF = 'example_consumer.urls'
WSGI_APPLICATION = 'example_consumer.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.7/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Internationalization
# https://docs.djangoproject.com/en/1.7/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.7/howto/static-files/
STATIC_URL = '/static/'
# the library python-openid does not support a json session serializer
# <openid.yadis.manager.YadisServiceManager> is not JSON serializable
# https://github.com/openid/python-openid/issues/17
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
AUTHENTICATION_BACKENDS = (
'django_openid_auth.auth.OpenIDBackend',
'django.contrib.auth.backends.ModelBackend',
)
# Should users be created when new OpenIDs are used to log in?
OPENID_CREATE_USERS = True
# When logging in again, should we overwrite user details based on
# data received via Simple Registration?
OPENID_UPDATE_DETAILS_FROM_SREG = True
# Map of OpenID Provider base URLs to recognised account verification schemes
# returned in response to a http://ns.login.ubuntu.com/2013/validation/account
# request. Use None as the key in place of a URL to specify verification
# schemes that will be trusted from unknown OpenID Providers (not recommended).
OPENID_VALID_VERIFICATION_SCHEMES = {
None: (),
}
# If set, always use this as the identity URL rather than asking the
# user. This only makes sense if it is a server URL.
OPENID_SSO_SERVER_URL = 'https://login.ubuntu.com/'
# Tell django.contrib.auth to use the OpenID signin URLs.
LOGIN_URL = '/openid/login/'
LOGIN_REDIRECT_URL = '/'
# Should django_auth_openid be used to sign into the admin interface?
OPENID_USE_AS_ADMIN_LOGIN = False
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from django.conf.urls import patterns, include, url
from django.contrib import admin
import views
admin.autodiscover()
urlpatterns = patterns(
'',
url(r'^$', views.index),
url(r'^openid/', include('django_openid_auth.urls')),
url(r'^logout/$', 'django.contrib.auth.views.logout'),
url(r'^private/$', views.require_authentication),
url(r'^admin/', include(admin.site.urls)),
)
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2007 Simon Willison
# Copyright (C) 2008-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.utils.html import escape
def index(request):
s = ['<p>']
if request.user.is_authenticated():
s.append('You are signed in as <strong>%s</strong> (%s)' % (
escape(request.user.username),
escape(request.user.get_full_name())))
s.append(' | <a href="/logout">Sign out</a>')
else:
s.append('<a href="/openid/login">Sign in with OpenID</a>')
s.append('</p>')
s.append('<p><a href="/private">This requires authentication</a></p>')
return HttpResponse('\n'.join(s))
def next_works(request):
return HttpResponse('?next= bit works. <a href="/">Home</a>')
@login_required
def require_authentication(request):
return HttpResponse('This page requires authentication')
"""
WSGI config for demo project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.7/howto/deployment/wsgi/
"""
import os
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "demo.settings")
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example_consumer.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
This diff is collapsed. Click to expand it.
================
OpenID in Django
================
The ``django_openidconsumer`` package contains all of the code needed to set up
your Django application as an OpenID consumer. You can use it to allow OpenID
users to sign in to your site without having to create a new username and
password.
Overview
========
The OpenID consumer system consists of:
* Views for you to hook in to your application.
* Database models implementing the persistence layer of an OpenID consumer.
* Middleware that makes ``request.openid`` and ``request.openids``
properties available to your application views.
Dependencies
============
``django_openidconsumer`` uses the `python-openid library`_, which must be
installed separately somewhere on the Python path. You should install the 1.2.0
"combo" package which includes the ``yadis`` and ``urljr`` libraries.
The package also depends on the availability of Django's `session support`_.
.. _python-openid library: http://www.openidenabled.com/openid/libraries/python/
.. _session support: http://www.djangoproject.com/documentation/sessions/
Installation
============
Having ensured that both the ``python-openid`` library and the ``django_openidconsumer`` package are available on your Python path, you can
add OpenID consumer support to an application by doing the following:
1. Put ``django_openidconsumer`` in your ``INSTALLED_APPS`` setting.
2. Run the command ``manage.py syncdb`` to create the necessary tables.
3. Add ``django_openidconsumer.middleware.OpenIDMiddleware`` to your list
of ``MIDDLEWARE_CLASSES``, somewhere after the Session middleware.
4. Add the following views to your urlconf::
(r'^openid/$', 'django_openidconsumer.views.begin'),
(r'^openid/complete/$', 'django_openidconsumer.views.complete'),
(r'^openid/signout/$', 'django_openidconsumer.views.signout'),
You will then be able to browse to ``example.com/openid/`` and sign in using
an OpenID.
Using the OpenID middleware
===========================
With the Middleware installed, your views will have access to the user's OpenID
as the ``request.openid`` property. This will be ``None`` if the user has not
yet authenticated; otherwise it will be a ``django_openidconsumer.util.OpenID``
instance.
If you want the user's OpenID as a string, call the ``str()`` builtin on the
OpenID instance::
def example_view(request):
if request.openid:
return HttpResponse("OpenID is %s" % escape(str(request.openid)))
else:
return HttpResponse("No OpenID")
Users can sign in with more than one OpenID. This is supported by the
``request.openids`` property, which is a list of ``OpenID`` objects in the order
in which they were authenticated. ``request.openid`` merely returns the last
item in this list.
Using simple registration
=========================
Simple registration (or `sreg`_) is an extension to the OpenID specification
that allows you to request extra details about a user from their OpenID
provider. It is frequently used to pre-populate registration forms with
information such as the user's name, e-mail address or date of birth.
.. _sreg: http://openid.net/specs/openid-simple-registration-extension-1_0.html
Be aware that not all OpenID providers support sreg, and there is no guarantee
that the information you have requested will be returned. Simple registration
should be used as a convenience for your users rather than as a required step in
your authentication process.
Available simple registration fields are ``nickname``, ``email``, ``fullname``,
``dob``, ``gender``, ``postcode``, ``country``, ``language`` and ``timezone``.
Full details are available in the `spec`_.
.. _spec: http://openid.net/specs/openid-simple-registration-extension-1_0.html
To request this information, pass the fields that you wish to retrieve as an
additional ``sreg`` argument to the ``django_openidconsumer.views.begin`` view::
(r'^openid/$', 'django_openidconsumer.views.begin', {
'sreg': 'email,nickname'
}),
Any simple registration fields that are returned will be available in a
dictionary as the ``sreg`` property of the OpenID object::
def example_sreg(request):
if request.openid and request.openid.sreg.has_key('email'):
return HttpResponse("Your e-mail address is: %s" % escape(
request.openid.sreg['email']
))
else:
return HttpResponse("No e-mail address")
Customisation
=============
``django_openidconsumer`` uses two templates:
``openid_signin.html``
The form presented to the user when they sign in.
``openid_failure.html``
The template used to display an error message when something goes wrong.
You can over-ride the default templates by creating templates of the same name
and placing them somewhere on your template path. You can find the example
templates in the ``django_openidconsumer/templates`` directory.
The OpenID specification strongly recommends that any OpenID registration form
has a ``name`` attribute of ``openid_url`` to aid browser autocompletion, and
displays the `OpenID logo`_ inline in the form field using the following CSS::
input.openid {
background: url(/path/to/login-bg.gif) no-repeat;
background-position: 0 50%;
padding-left: 16px;
}
.. _OpenID logo: http://openid.net/login-bg.gif
By default, the package expects the ``django_openidconsumer.views.complete``
view to be located at ``/openid/complete/``. This is the view that the OpenID
provider will redirect the user to after they have authenticated. If you want to
put it somewhere else you can either pass an extra ``redirect_to`` argument to
``django_openidconsumer.views.begin`` or add an ``OPENID_REDIRECT_TO`` setting
to ``settings.py``.
You can pass a ``?next=`` query string argument containing a relative URL to
the ``begin`` view to control where the user will be redirected to having
returned to your site. You can also set the default redirection location
using the ``OPENID_REDIRECT_NEXT`` setting; if you do set set a default the user
will be redirected to your homepage.
i-names
=======
`i-names`_ are part of the OpenID 2.0 specification, which is currently being
developed. They are supported by the python-openid library, and hence are also
supported by ``django_openidconsumer``. You can tell if an OpenID is an i-name
by checking the ``request.openid.is_iname`` property.
.. _i-names: http://www.inames.net/
If you wish to disable i-name support, you can do so by adding the following to
your ``settings.py``::
OPENID_DISALLOW_INAMES = True
#!/usr/bin/env python
# django-openid-auth - OpenID integration for django.contrib.auth
#
# Copyright (C) 2009-2013 Canonical Ltd.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# * Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
"""OpenID integration for django.contrib.auth
A library that can be used to add OpenID support to Django applications.
The library integrates with Django's built in authentication system, so
most applications require minimal changes to support OpenID llogin. The
library also includes the following features:
* Basic user details are transferred from the OpenID server via the
Simple Registration extension or Attribute Exchange extension.
* can be configured to use a fixed OpenID server URL, for use in SSO.
* supports the launchpad.net teams extension to get team membership
info.
"""
from distutils.core import setup
description, long_description = __doc__.split('\n\n', 1)
VERSION = '0.6'
setup(
name='django-openid-auth',
version=VERSION,
author='Canonical Ltd',
author_email='noreply@canonical.com',
description=description,
long_description=long_description,
license='BSD',
platforms=['any'],
url='https://launchpad.net/django-openid-auth',
download_url=('http://launchpad.net/django-openid-auth/trunk/%s/+download'
'/django-openid-auth-%s.tar.gz' % (VERSION, VERSION)),
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Web Environment',
'Framework :: Django',
'Intended Audience :: Developers',
'License :: OSI Approved :: BSD License',
'Operating System :: OS Independent',
'Programming Language :: Python',
'Topic :: Software Development :: Libraries :: Python Modules'
],
packages=[
'django_openid_auth',
'django_openid_auth.management',
'django_openid_auth.management.commands',
'django_openid_auth.tests',
],
package_data={
'django_openid_auth': ['templates/openid/*.html'],
},
provides=['django_openid_auth'],
requires=['django (>=1.4)', 'openid (>=2.2.0)', 'south'],
)
[tox]
envlist =
py2.7-django1.4, py2.7-django1.5, py2.7-django1.6, py2.7-django1.7, py2.7-django1.8
[testenv]
commands = python manage.py test django_openid_auth
deps=
mock
python-openid
[testenv:py2.7-django1.4]
basepython = python2.7
deps =
django >= 1.4, < 1.5
{[testenv]deps}
south==1.0
[testenv:py2.7-django1.5]
basepython = python2.7
deps =
django >= 1.5, < 1.6
{[testenv]deps}
south==1.0
[testenv:py2.7-django1.6]
basepython = python2.7
deps =
django >= 1.6, < 1.7
{[testenv]deps}
south==1.0
[testenv:py2.7-django1.7]
basepython = python2.7
deps =
django >= 1.7, < 1.8
{[testenv]deps}
[testenv:py2.7-django1.8]
basepython = python2.7
deps =
django >= 1.8, < 1.9
{[testenv]deps}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment