Skip to content

Commit

Permalink
Add forgot username support
Browse files Browse the repository at this point in the history
Closes-Bug: #1298734

Change-Id: I800b95a5c7617a9493f83059fd97240d3d1968cb
  • Loading branch information
sir-sigurd committed Apr 23, 2014
1 parent 1e2bee6 commit 880c123
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 3 deletions.
39 changes: 37 additions & 2 deletions wildcard/api/keystone.py
Expand Up @@ -23,6 +23,7 @@
import urlparse

from django.conf import settings # noqa
from django.contrib.auth import authenticate
from django.contrib.auth import logout # noqa
from django.utils.translation import ugettext_lazy as _ # noqa

Expand Down Expand Up @@ -140,6 +141,32 @@ def keystoneclient(request, admin=False):
The client is cached so that subsequent API calls during the same
request/response cycle don't have to be re-authenticated.
"""
api_version = VERSIONS.get_active_version()
if not request:
endpoint = settings.OPENSTACK_KEYSTONE_URL
LOG.debug(
"Creating a new keystoneserviceclient connection to %s." % endpoint
)
user = authenticate(
username=settings.WILDCARD_ADMIN_USER,
password=settings.WILDCARD_ADMIN_PASSWORD,
auth_url=endpoint,
)
catalog = user.service_catalog
service = base.get_service_from_catalog(catalog, 'identity')
endpoint = base.get_url_for_service(
service,
user.services_region,
endpoint_type='adminURL',
)
return api_version['client'].Client(
token=user.token.id,
endpoint=endpoint,
insecure=getattr(settings, 'OPENSTACK_SSL_NO_VERIFY', False),
cacert=getattr(settings, 'OPENSTACK_SSL_CACERT', None),
debug=settings.DEBUG,
)

user = request.user
if admin:
if not user.is_superuser:
Expand All @@ -150,8 +177,6 @@ def keystoneclient(request, admin=False):
'OPENSTACK_ENDPOINT_TYPE',
'internalURL')

api_version = VERSIONS.get_active_version()

# Take care of client connection caching/fetching a new client.
# Admin vs. non-admin clients are cached separately for token matching.
cache_attr = "_keystoneclient_admin" if admin \
Expand Down Expand Up @@ -404,6 +429,16 @@ def user_update_tenant(request, user, project, admin=True):
return manager.update(user, project=project)


def user_find(request, admin=False, **kwargs):
manager = keystoneclient(request, admin=admin).users
user = None
try:
user = VERSIONS.upgrade_v2_user(manager.find(**kwargs))
except keystone_exceptions.NotFound:
pass
return user


def group_create(request, domain_id, name, description=None):
manager = keystoneclient(request, admin=True).groups
return manager.create(domain=domain_id,
Expand Down
54 changes: 54 additions & 0 deletions wildcard/forms.py
@@ -0,0 +1,54 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4

# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from django.conf import settings
from django.core.mail import send_mail
from django import forms
from django.utils.translation import ugettext_lazy as _

from horizon.forms import SelfHandlingForm
from horizon import messages

from wildcard.api import keystone


class ForgotUsernameForm(SelfHandlingForm):

email = forms.CharField()

user = None

def clean(self):
cleaned_data = super(ForgotUsernameForm, self).clean()
email = cleaned_data.get('email')
if email:
self.user = keystone.user_find(None, email=email)
if not self.user:
raise forms.ValidationError(
_("there is no user with such email")
)
return cleaned_data

def handle(self, request, data):
send_mail(
_('username reminder'),
_('your username is %s') % self.user.name,
settings.DEFAULT_FROM_EMAIL,
[self.user.email],
)
messages.success(
request,
_('your username was sent by email')
)
return True
13 changes: 13 additions & 0 deletions wildcard/local/local_settings.py.example
Expand Up @@ -29,6 +29,19 @@ OPENSTACK_HOST = "127.0.0.1"
OPENSTACK_KEYSTONE_URL = "http://%s:5000/v2.0" % OPENSTACK_HOST
OPENSTACK_KEYSTONE_DEFAULT_ROLE = "_member_"

# Keystone account username
WILDCARD_ADMIN_USER = "admin"
# Keystone account password
WILDCARD_ADMIN_PASSWORD = "admin"

# In real world we want to use smtp email backend and we should to set some variables for it.
#EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
#EMAIL_HOST = 'localhost'
#EMAIL_PORT = '25'
#EMAIL_HOST_USER = ''
#EMAIL_HOST_PASSWORD = ''
#EMAIL_USE_TLS = False

CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache'
Expand Down
6 changes: 6 additions & 0 deletions wildcard/settings.py
Expand Up @@ -158,6 +158,12 @@
import string
KICKSTAND_RANDOM_PASSWORD_CHARS = string.ascii_letters + string.digits

# we should force usage of v2.0 keystoneclient, since we specify
# v2.0 keystone endpoint
OPENSTACK_API_VERSIONS = {
'identity': 2.0,
}

try:
from local.local_settings import * # noqa
except ImportError:
Expand Down
21 changes: 21 additions & 0 deletions wildcard/templates/_forgot-username.html
@@ -0,0 +1,21 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}

{% block form_id %}forgot_username_form{% endblock %}
{% block form_action %}{% url 'forgot-username' %}{% endblock %}

{% block modal-header %}{% trans "Forgot Username" %}{% endblock %}

{% block modal-body %}
<div class="left">
<fieldset>
{% include "horizon/common/_form_fields.html" %}
</fieldset>
</div>
{% endblock %}

{% block modal-footer %}
<input class="btn btn-primary pull-right" type="submit" value="{% trans "Remind Username" %}" />
<a href="{% url 'login' %}" class="btn secondary cancel close">{% trans "Cancel" %}</a>
{% endblock %}
34 changes: 34 additions & 0 deletions wildcard/templates/auth/_login.html
@@ -0,0 +1,34 @@
{% extends "horizon/common/_modal_form.html" %}
{% load i18n %}
{% load url from future %}

{% block modal-header %}{% trans "Log In" %}{% endblock %}
{% block modal_class %}login {% if hide %}modal hide{% endif %}{% endblock %}

{% block form_action %}{% url 'login' %}{% endblock %}
{% block autocomplete %}{{ HORIZON_CONFIG.password_autocomplete }}{% endblock %}

{% block modal-body %}
<fieldset>
{% if request.user.is_authenticated and 'next' in request.GET %}
<div class="control-group clearfix error">
<span class="help-inline"><p>{% trans "You do not have permission to access the resource:" %}</p>
<p><b>{{ request.GET.next }}</b></p>
<p>{% url 'horizon:user_home' as home_url %}{% blocktrans %}Login as different user or go back to <a href="{{ home_url }}"> home page</a>{% endblocktrans %}</p>
</span>
</div>
{% endif %}
{% if request.COOKIES.logout_reason %}
<div class="control-group clearfix error">
<span class="help-inline"><p>{{ request.COOKIES.logout_reason }}</p></span>
</div>
{% endif %}
{% if next %}<input type="hidden" name="{{ redirect_field_name }}" value="{{ next }}" />{% endif %}
{% include "horizon/common/_form_fields.html" %}
</fieldset>
{% endblock %}

{% block modal-footer %}
<button type="submit" class="btn btn-primary pull-right">{% trans "Sign In" %}</button>
<a href="{% url 'forgot-username' %}" class="ajax-modal">{% trans "Forgot Username?" %}</a>
{% endblock %}
18 changes: 18 additions & 0 deletions wildcard/templates/forgot-username.html
@@ -0,0 +1,18 @@
{% load branding %}
{% load i18n %}

<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
<title>{% trans "Forgot Username" %} - {% site_branding %}</title>
{% include "_stylesheets.html" %}
</head>
<body>
<div id="container">
<div id='main_content'>
{% include '_forgot-username.html' %}
</div>
</div>
</body>
</html>
23 changes: 23 additions & 0 deletions wildcard/templates/splash.html
@@ -0,0 +1,23 @@
{% load branding %}
{% load i18n %}

<!DOCTYPE html>
<html>
<head>
<meta content='text/html; charset=utf-8' http-equiv='Content-Type' />
<title>{% trans "Login" %} - {% site_branding %}</title>
{% include "_stylesheets.html" %}
{% include "horizon/_conf.html" %}
{% include "horizon/client_side/_script_loader.html" %}
</head>
<body id="splash" ng-app='horizonApp'>
<div id="container">
<div id='main_content'>
{% include "horizon/_messages.html" %}
{% include 'auth/_login.html' %}
</div>
</div>
{% include "horizon/_scripts.html" %}
<div id="modal_wrapper" />
</body>
</html>
61 changes: 61 additions & 0 deletions wildcard/tests.py
@@ -0,0 +1,61 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from django.core.urlresolvers import reverse

from wildcard import api
from wildcard.test import helpers as test


SPLASH_URL = reverse('splash')
FORGOT_USERNAME_URL = reverse('forgot-username')


class ForgotUsernameTests(test.TestCase):

@test.create_stubs({api.keystone: ('user_find',)})
def test_existent_email(self):
user = self.users.get(id='1')
api.keystone.user_find(
None,
email=user.email,
).AndReturn(user)
self.mox.ReplayAll()

formData = {
'method': 'ForgotUsernameForm',
'email': user.email,
}
res = self.client.post(FORGOT_USERNAME_URL, formData)

self.assertNoFormErrors(res)
self.assertRedirectsNoFollow(res, SPLASH_URL)
self.assertMessageCount(success=1)

@test.create_stubs({api.keystone: ('user_find',)})
def test_nonexistent_email(self):
user = self.users.get(id='1')
api.keystone.user_find(
None,
email=user.email,
).AndReturn(None)
self.mox.ReplayAll()

formData = {
'method': 'ForgotUsernameForm',
'email': user.email,
}
res = self.client.post(FORGOT_USERNAME_URL, formData)

self.assertFormErrors(res, count=0)
23 changes: 22 additions & 1 deletion wildcard/urls.py
@@ -1,10 +1,31 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import horizon

from django.conf.urls import patterns, include, url

from wildcard.views import ForgotUsername

urlpatterns = patterns(
'',
url(r'^$', 'wildcard.views.splash', name='splash'),
url(r'^auth/', include('openstack_auth.urls')),
url(r'', include(horizon.urls))
url(r'', include(horizon.urls)),
url(
r'^forgot-username$',
ForgotUsername.as_view(),
name='forgot-username',
),
)
11 changes: 11 additions & 0 deletions wildcard/views.py
Expand Up @@ -14,13 +14,17 @@
# License for the specific language governing permissions and limitations
# under the License.

from django.core.urlresolvers import reverse_lazy
from django import shortcuts
from django.views.decorators import vary

import horizon
from horizon.forms import ModalFormView

from openstack_auth import views

from wildcard.forms import ForgotUsernameForm


def get_user_home(user):
if user.is_superuser:
Expand All @@ -36,3 +40,10 @@ def splash(request):
request.session.clear()
request.session.set_test_cookie()
return shortcuts.render(request, 'splash.html', {'form': form})


class ForgotUsername(ModalFormView):

form_class = ForgotUsernameForm
template_name = 'forgot-username.html'
success_url = reverse_lazy('splash')

0 comments on commit 880c123

Please sign in to comment.