diff --git a/circle/circle/settings/base.py b/circle/circle/settings/base.py index 5a33316..ccc7704 100644 --- a/circle/circle/settings/base.py +++ b/circle/circle/settings/base.py @@ -339,7 +339,6 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': 'django.contrib.auth.backends.ModelBackend', 'djangosaml2.backends.Saml2Backend', ) - LOGIN_URL = '/saml2/login/' remote_metadata = join(SITE_ROOT, 'remote_metadata.xml') if not isfile(remote_metadata): @@ -388,6 +387,8 @@ if get_env_variable('DJANGO_SAML', 'FALSE') == 'TRUE': 'DJANGO_SAML_GROUP_OWNER_ATTRIBUTES', '').split(',') SAML_CREATE_UNKNOWN_USER = True - if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) != False: + if get_env_variable('DJANGO_SAML_ORG_ID_ATTRIBUTE', False) is not False: SAML_ORG_ID_ATTRIBUTE = get_env_variable( 'DJANGO_SAML_ORG_ID_ATTRIBUTE') + +LOGIN_REDIRECT_URL = "/" diff --git a/circle/circle/urls.py b/circle/circle/urls.py index 6b712b1..80b8cad 100644 --- a/circle/circle/urls.py +++ b/circle/circle/urls.py @@ -6,6 +6,8 @@ from django.shortcuts import redirect from django.core.urlresolvers import reverse from circle.settings.base import get_env_variable +from dashboard.views import circle_login +from dashboard.forms import CirclePasswordResetForm, CircleSetPasswordForm admin.autodiscover() @@ -23,6 +25,19 @@ urlpatterns = patterns( url(r'^admin/', include(admin.site.urls)), url(r'^network/', include('network.urls')), url(r'^dashboard/', include('dashboard.urls')), + + url((r'^accounts/reset/(?P<uidb36>[0-9A-Za-z]{1,13})-' + '(?P<token>[0-9A-Za-z]{1,13}-[0-9A-Za-z]{1,20})/$'), + 'django.contrib.auth.views.password_reset_confirm', + {'set_password_form': CircleSetPasswordForm}, + name='accounts.password_reset_confirm' + ), + url(r'^accounts/password/reset/$', ("django.contrib.auth.views." + "password_reset"), + {'password_reset_form': CirclePasswordResetForm}, + name="accounts.password-reset", + ), + url(r'^accounts/login/?$', circle_login, name="accounts.login"), url(r'^accounts/', include('django.contrib.auth.urls')), ) diff --git a/circle/dashboard/forms.py b/circle/dashboard/forms.py index 20ab951..5357b8b 100644 --- a/circle/dashboard/forms.py +++ b/circle/dashboard/forms.py @@ -2,6 +2,9 @@ from datetime import timedelta import uuid from django.contrib.auth.models import User +from django.contrib.auth.forms import ( + AuthenticationForm, PasswordResetForm, SetPasswordForm, +) from crispy_forms.helper import FormHelper from crispy_forms.layout import ( @@ -762,6 +765,93 @@ class DiskAddForm(forms.Form): return helper +class CircleAuthenticationForm(AuthenticationForm): + # fields: username, password + + @property + def helper(self): + helper = FormHelper() + helper.form_show_labels = False + helper.layout = Layout( + AnyTag( + "div", + AnyTag( + "span", + AnyTag( + "i", + css_class="icon-user", + ), + css_class="input-group-addon", + ), + Field("username", placeholder=_("Username"), + css_class="form-control"), + css_class="input-group", + ), + AnyTag( + "div", + AnyTag( + "span", + AnyTag( + "i", + css_class="icon-lock", + ), + css_class="input-group-addon", + ), + Field("password", placeholder=_("Password"), + css_class="form-control"), + css_class="input-group", + ), + ) + helper.add_input(Submit("submit", _("Sign in"), + css_class="btn btn-success")) + return helper + + +class CirclePasswordResetForm(PasswordResetForm): + # fields: email + + @property + def helper(self): + helper = FormHelper() + helper.form_show_labels = False + helper.layout = Layout( + AnyTag( + "div", + AnyTag( + "span", + AnyTag( + "i", + css_class="icon-envelope", + ), + css_class="input-group-addon", + ), + Field("email", placeholder=_("Email address"), + css_class="form-control"), + Div( + AnyTag( + "button", + HTML(_("Reset password")), + css_class="btn btn-success", + ), + css_class="input-group-btn", + ), + css_class="input-group", + ), + ) + return helper + + +class CircleSetPasswordForm(SetPasswordForm): + + @property + def helper(self): + helper = FormHelper() + helper.add_input(Submit("submit", _("Change password"), + css_class="btn btn-success change-password", + css_id="submit-password-button")) + return helper + + class LinkButton(BaseInput): """ diff --git a/circle/dashboard/views.py b/circle/dashboard/views.py index e21ffc1..5bf296c 100644 --- a/circle/dashboard/views.py +++ b/circle/dashboard/views.py @@ -5,7 +5,9 @@ import re from datetime import datetime import requests +from django.conf import settings from django.contrib.auth.models import User, Group +from django.contrib.auth.views import login from django.contrib.messages import warning from django.core.exceptions import ( PermissionDenied, SuspiciousOperation, @@ -29,7 +31,7 @@ from braces.views import LoginRequiredMixin, SuperuserRequiredMixin from .forms import ( VmCustomizeForm, TemplateForm, LeaseForm, NodeForm, HostForm, - DiskAddForm, + DiskAddForm, CircleAuthenticationForm, ) from .tables import (VmListTable, NodeListTable, NodeVmListTable, TemplateListTable, LeaseListTable, GroupListTable,) @@ -1748,3 +1750,12 @@ class VmMigrateView(SuperuserRequiredMixin, TemplateView): messages.error(self.request, _("You didn't select a node!")) return redirect("%s#activity" % vm.get_absolute_url()) + + +def circle_login(request): + authentication_form = CircleAuthenticationForm + extra_context = { + 'saml2': hasattr(settings, "SAML_CONFIG") + } + return login(request, authentication_form=authentication_form, + extra_context=extra_context) diff --git a/circle/templates/registration/base.html b/circle/templates/registration/base.html new file mode 100644 index 0000000..02ec3ac --- /dev/null +++ b/circle/templates/registration/base.html @@ -0,0 +1,88 @@ +{% load i18n %} +{% load staticfiles %} +{% get_current_language as LANGUAGE_CODE %} +<html> +<head> + <meta charset="utf-8"> + <title>{% block title %}{% endblock %} | CIRCLE</title> + <meta name="description" content=""> + <meta name="author" content=""> + + <link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.0.0/css/bootstrap.min.css"> + <link href="//netdna.bootstrapcdn.com/font-awesome/3.2.1/css/font-awesome.css" rel="stylesheet"> +<style type="text/css"> + html, body { + background-color: #eee; + } + body { + margin-top: 40px; + } + .container { + width: 600px; + } + + ul, li { + list-style: none; + margin: 0; + padding: 0; + } + + .container > .content { + background-color: #fff; + padding: 20px; + -webkit-border-radius: 10px 10px 10px 10px; + -moz-border-radius: 10px 10px 10px 10px; + border-radius: 10px 10px 10px 10px; + -webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15); + -moz-box-shadow: 0 1px 2px rgba(0,0,0,.15); + box-shadow: 0 1px 2px rgba(0,0,0,.15); + } + + .login-form-errors .alert { + margin-right: 30px; + margin-left: 30px; + } + + .login-form { + margin-top: 40px; + padding: 0 10px; + } + + .login-form form { + padding: 0 20px; + } + + .input-group { + margin-bottom: 10px; + } + + .input-group-addon { + width: 38px; + } + + .form-group label { + margin-top: 20px; + } + + #submit-password-button { + margin-top: 15px; + } + + /* fix for crispy-forms' html */ + .form-group { + margin-bottom: 0px; + } + + .help-block { + display: none; + } + </style> +</head> +<body> + <div class="container"> + <div class="content"> + {% block content %}{% endblock %} + </div> + </div> <!-- /container --> +</body> +</html> diff --git a/circle/templates/registration/login.html b/circle/templates/registration/login.html index 21ad59c..c0a35a7 100644 --- a/circle/templates/registration/login.html +++ b/circle/templates/registration/login.html @@ -1,11 +1,35 @@ -{% extends "base.html" %} +{% extends "registration/base.html" %} {% load i18n %} -{% load staticfiles %} +{% load crispy_forms_tags %} {% get_current_language as LANGUAGE_CODE %} + +{% block title %}{% trans "Login" %}{% endblock %} + {% block content %} -<form action="" method="POST"> - {% csrf_token %} - {{ form }} - <input type="submit" value="LOGIN" /> -</form> +<div class="row"> + {% if form.password.errors or form.username.errors %} + <div class="login-form-errors"> + {% include "display-form-errors.html" %} + </div> + {% endif %} + <div class="col-sm-{% if saml2 %}6{% else %}12{% endif %}"> + <div class="login-form"> + <form action="" method="POST"> + {% csrf_token %} + {% crispy form %} + </form> + </div> + </div> + {% if saml2 %} + <div class="col-sm-6"> + <h4 style="padding-top: 0; margin-top: 0;">{% trans "Login with SSO" %}</h4> + <a href="{% url "saml2_login" %}">{% trans "Click here!" %}</a> + </div> + {% endif %} +</div> +<div class="row"> + <div class="col-sm-12"> + <a class="pull-right" href="{% url "accounts.password-reset" %}">{% trans "Forgot your password?" %}</a> + </div> +</div> {% endblock %} diff --git a/circle/templates/registration/password_reset_complete.html b/circle/templates/registration/password_reset_complete.html new file mode 100644 index 0000000..42ec7a8 --- /dev/null +++ b/circle/templates/registration/password_reset_complete.html @@ -0,0 +1,20 @@ +{% extends "registration/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% get_current_language as LANGUAGE_CODE %} + +{% block title %}{% trans "Password reset complete" %}{% endblock %} + +{% block content %} +<div class="row"> + <div class="login-form-errors"> + {% include "display-form-errors.html" %} + </div> + <div class="col-sm-12"> + <div class="alert alert-success"> + {% trans "Password change successful!" %} + <a href="{% url "accounts.login" %}">{% trans "Click here to login" %}</a> + </div> + </div> +</div> +{% endblock %} diff --git a/circle/templates/registration/password_reset_confirm.html b/circle/templates/registration/password_reset_confirm.html new file mode 100644 index 0000000..d2733ca --- /dev/null +++ b/circle/templates/registration/password_reset_confirm.html @@ -0,0 +1,28 @@ +{% extends "registration/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% get_current_language as LANGUAGE_CODE %} + +{% block title %}{% trans "Password reset confirm" %}{% endblock %} + +{% block content %} +<div class="row"> + <div class="login-form-errors"> + {% include "display-form-errors.html" %} + </div> + <div class="col-sm-12"> + <div style="margin: 0 0 25px 0;"> + {% blocktrans %}Please enter your new password twice so we can verify you typed it in correctly!{% endblocktrans %} + </div> + + {% if form %} + {% crispy form %} + {% else %} + <div class="alert alert-warning"> + {% url "accounts.password-reset" as url %} + {% blocktrans with url=url %}This token is expired, please <a href="{{ url }}">request</a> a new password reset link again!{% endblocktrans %} + </div> + {% endif %} + </div> +</div> +{% endblock %} diff --git a/circle/templates/registration/password_reset_done.html b/circle/templates/registration/password_reset_done.html new file mode 100644 index 0000000..c395764 --- /dev/null +++ b/circle/templates/registration/password_reset_done.html @@ -0,0 +1,18 @@ +{% extends "registration/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% get_current_language as LANGUAGE_CODE %} + +{% block title %}{% trans "Password reset done" %}{% endblock %} + +{% block content %} +<div class="row"> + <div class="login-form-errors"> + {% include "display-form-errors.html" %} + </div> + <div class="col-sm-12"> + <div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div> + {% trans "We have sent you an email about your next steps!" %} + </div> +</div> +{% endblock %} diff --git a/circle/templates/registration/password_reset_form.html b/circle/templates/registration/password_reset_form.html new file mode 100644 index 0000000..16b4ff7 --- /dev/null +++ b/circle/templates/registration/password_reset_form.html @@ -0,0 +1,22 @@ +{% extends "registration/base.html" %} +{% load i18n %} +{% load crispy_forms_tags %} +{% get_current_language as LANGUAGE_CODE %} + +{% block title %}{% trans "Password reset" %}{% endblock %} + +{% block content %} +<div class="row"> + <div class="login-form-errors"> + {% include "display-form-errors.html" %} + </div> + <div class="col-sm-12"> + <div class="pull-right"><a href="{% url "accounts.login" %}">{% trans "Back to login" %}</a></div> + <h4 style="margin: 0 0 25px 0;">{% blocktrans %}Enter your email address to reset your password!{% endblocktrans %}</h4> + <form action="" method="POST"> + {% csrf_token %} + {% crispy form %} + </form> + </div> +</div> +{% endblock %}