auth: Merge RemoteUserBackend into external_authentication_methods.

We register ZulipRemoteUserBackend as an external_authentication_method
to make it show up in the corresponding field in the /server_settings
endpoint.

This also allows rendering its login button together with
Google/Github/etc. leading to us being able to get rid of some of the
code that was handling it as a special case - the js code for plumbing
the "next" value and the special {% if only_sso %} block in login.html.
An additional consequence of the login.html change is that now the
backend will have it button rendered even if it isn't the only backend
enabled on the server.
This commit is contained in:
Mateusz Mandera 2019-12-10 00:42:12 +01:00
parent a842968090
commit 6dbd2b5fc3
5 changed files with 155 additions and 155 deletions

View File

@ -99,9 +99,6 @@ $(function () {
const email_formaction = $("#login_form").attr('action');
$("#login_form").attr('action', email_formaction + '/' + window.location.hash);
$(".social_login_form input[name='next']").attr('value', '/' + window.location.hash);
const sso_address = $("#sso-login").attr('href');
$("#sso-login").attr('href', sso_address + window.location.hash);
}
}

View File

@ -1045,7 +1045,6 @@ input.new-organization-button {
font-weight: 300;
}
.login-sso,
.login-social {
max-width: 100%;
min-width: 300px;

View File

@ -14,140 +14,127 @@ page can be easily identified in it's respective JavaScript file. -->
</div>
<div class="app-main login-page-container white-box inline-block">
{% if only_sso %}
{# SSO users don't have a password. #}
<div class="login-sso">
<a id="sso-login" href="/accounts/login/sso/?next={{ next }}" class="btn btn-large btn-primary">
{{ _('Log in with %(identity_provider)s', identity_provider="SSO") }}
</a>
</div>
{% else %}
{# Non-SSO users. #}
{% if realm_name %}
<div class="left-side">
<div class="org-header">
<img class="avatar" src="{{ realm_icon }}" alt="" />
<div class="info-box">
<div class="organization-name">{{ realm_name }}</div>
<div class="organization-path">{{ realm_uri }}</div>
</div>
</div>
<div class="description">
{{ realm_description|safe }}
{% if realm_name %}
<div class="left-side">
<div class="org-header">
<img class="avatar" src="{{ realm_icon }}" alt="" />
<div class="info-box">
<div class="organization-name">{{ realm_name }}</div>
<div class="organization-path">{{ realm_uri }}</div>
</div>
</div>
{% endif %}
<div class="right-side">
{% if no_auth_enabled %}
<div class="alert">
<p>No authentication backends are enabled on this
server yet, so it is impossible to login!</p>
<p>See the <a href="https://zulip.readthedocs.io/en/latest/production/install.html#step-3-configure-zulip">Zulip
authentication documentation</a> to learn how to configure authentication backends.</p>
</div>
{% else %}
{% if password_auth_enabled %}
<form name="login_form" id="login_form" method="post" class="login-form"
action="{{ url('django.contrib.auth.views.login') }}?next={{ next }}">
{% if two_factor_authentication_enabled %}
{{ wizard.management_form }}
{% endif %}
{{ csrf_input }}
<!-- .no-validation is for removing the red star in CSS -->
{% if not two_factor_authentication_enabled or wizard.steps.current == 'auth' %}
<div class="input-box no-validation">
<input id="id_username" type="{% if not require_email_format_usernames %}text{% else %}email{% endif %}"
name="username" class="{% if require_email_format_usernames %}email {% endif %}required"
{% if email %} value="{{ email }}" {% else %} value="" autofocus {% endif %}
maxlength="72" required />
<label for="id_username">
{% if not require_email_format_usernames and email_auth_enabled %}
{{ _('Email or username') }}
{% elif not require_email_format_usernames %}
{{ _('Username') }}
{% else %}
{{ _('Email') }}
{% endif %}
</label>
</div>
<div class="input-box no-validation">
<input id="id_password" name="password" class="required" type="password"
{% if email %} autofocus {% endif %}
required />
<label for="id_password" class="control-label">{{ _('Password') }}</label>
</div>
{% else %}
{% include "two_factor/_wizard_forms.html" %}
{% endif %}
{% if form.errors %}
<div class="alert alert-error">
{% for error in form.errors.values() %}
<div>{{ error | striptags }}</div>
{% endfor %}
</div>
{% endif %}
{% if already_registered %}
<div class="alert">
{{ _("You've already registered with this email address. Please log in below.") }}
</div>
{% endif %}
{% if is_deactivated %}
<div class="alert">
{{ deactivated_account_error }}
</div>
{% endif %}
{% if subdomain %}
<div class="alert">
{{ wrong_subdomain_error }}
</div>
{% endif %}
<button type="submit" name="button" class="full-width">
<img class="loader" src="/static/images/loader.svg" alt="" />
<span class="text">{{ _("Log in") }}</span>
</button>
</form>
{% if any_social_backend_enabled %}
<div class="or"><span>{{ _('OR') }}</span></div>
{% endif %}
{% endif %} <!-- if password_auth_enabled -->
{% for backend in external_authentication_methods %}
<div class="login-social">
<form class="social_login_form form-inline" action="{{ backend.login_url }}" method="get">
<input type="hidden" name="next" value="{{ next }}">
<button class="login-social-button" {% if backend.display_icon %} style="background-image:url({{ backend.display_icon }})" {% endif %}>
{{ _('Log in with %(identity_provider)s', identity_provider=backend.display_name) }}
</button>
</form>
</div>
{% endfor %}
<div class="actions">
{% if email_auth_enabled %}
<a class="forgot-password" href="/accounts/password/reset/">{{ _('Forgot your password?') }}</a>
{% endif %}
{% if not register_link_disabled %}
<a class="register-link float-right" href="/register/">{{ _('Sign up') }}</a>
{% endif %}
</div>
{% endif %}
<div class="description">
{{ realm_description|safe }}
</div>
</div>
{% endif %}
<div class="right-side">
{% if no_auth_enabled %}
<div class="alert">
<p>No authentication backends are enabled on this
server yet, so it is impossible to login!</p>
<p>See the <a href="https://zulip.readthedocs.io/en/latest/production/install.html#step-3-configure-zulip">Zulip
authentication documentation</a> to learn how to configure authentication backends.</p>
</div>
{% else %}
{% if password_auth_enabled %}
<form name="login_form" id="login_form" method="post" class="login-form"
action="{{ url('django.contrib.auth.views.login') }}?next={{ next }}">
{% if two_factor_authentication_enabled %}
{{ wizard.management_form }}
{% endif %}
{{ csrf_input }}
<!-- .no-validation is for removing the red star in CSS -->
{% if not two_factor_authentication_enabled or wizard.steps.current == 'auth' %}
<div class="input-box no-validation">
<input id="id_username" type="{% if not require_email_format_usernames %}text{% else %}email{% endif %}"
name="username" class="{% if require_email_format_usernames %}email {% endif %}required"
{% if email %} value="{{ email }}" {% else %} value="" autofocus {% endif %}
maxlength="72" required />
<label for="id_username">
{% if not require_email_format_usernames and email_auth_enabled %}
{{ _('Email or username') }}
{% elif not require_email_format_usernames %}
{{ _('Username') }}
{% else %}
{{ _('Email') }}
{% endif %}
</label>
</div>
<div class="input-box no-validation">
<input id="id_password" name="password" class="required" type="password"
{% if email %} autofocus {% endif %}
required />
<label for="id_password" class="control-label">{{ _('Password') }}</label>
</div>
{% else %}
{% include "two_factor/_wizard_forms.html" %}
{% endif %}
{% if form.errors %}
<div class="alert alert-error">
{% for error in form.errors.values() %}
<div>{{ error | striptags }}</div>
{% endfor %}
</div>
{% endif %}
{% if already_registered %}
<div class="alert">
{{ _("You've already registered with this email address. Please log in below.") }}
</div>
{% endif %}
{% if is_deactivated %}
<div class="alert">
{{ deactivated_account_error }}
</div>
{% endif %}
{% if subdomain %}
<div class="alert">
{{ wrong_subdomain_error }}
</div>
{% endif %}
<button type="submit" name="button" class="full-width">
<img class="loader" src="/static/images/loader.svg" alt="" />
<span class="text">{{ _("Log in") }}</span>
</button>
</form>
{% if any_social_backend_enabled %}
<div class="or"><span>{{ _('OR') }}</span></div>
{% endif %}
{% endif %} <!-- if password_auth_enabled -->
{% for backend in external_authentication_methods %}
<div class="login-social">
<form class="social_login_form form-inline" action="{{ backend.login_url }}" method="get">
<input type="hidden" name="next" value="{{ next }}">
<button class="login-social-button" {% if backend.display_icon %} style="background-image:url({{ backend.display_icon }})" {% endif %}>
{{ _('Log in with %(identity_provider)s', identity_provider=backend.display_name) }}
</button>
</form>
</div>
{% endfor %}
<div class="actions">
{% if email_auth_enabled %}
<a class="forgot-password" href="/accounts/password/reset/">{{ _('Forgot your password?') }}</a>
{% endif %}
{% if not register_link_disabled %}
<a class="register-link float-right" href="/register/">{{ _('Sign up') }}</a>
{% endif %}
</div>
{% endif %}
</div>
</div>
</div>
</div>

View File

@ -1924,6 +1924,7 @@ class ExternalMethodDictsTests(ZulipTestCase):
AUTHENTICATION_BACKENDS=('zproject.backends.EmailAuthBackend',
'zproject.backends.GitHubAuthBackend',
'zproject.backends.GoogleAuthBackend',
'zproject.backends.ZulipRemoteUserBackend',
'zproject.backends.SAMLAuthBackend',
'zproject.backends.AzureADAuthBackend')
):
@ -1933,7 +1934,7 @@ class ExternalMethodDictsTests(ZulipTestCase):
self.assertEqual(
[social_backend['name'] for social_backend in external_auth_methods[1:]],
[social_backend.name for social_backend in sorted(
[GitHubAuthBackend, AzureADAuthBackend, GoogleAuthBackend],
[ZulipRemoteUserBackend, GitHubAuthBackend, AzureADAuthBackend, GoogleAuthBackend],
key=lambda x: x.sort_order,
reverse=True
)]

View File

@ -244,25 +244,6 @@ class EmailAuthBackend(ZulipAuthMixin):
return user_profile
return None
class ZulipRemoteUserBackend(RemoteUserBackend):
"""Authentication backend that reads the Apache REMOTE_USER variable.
Used primarily in enterprise environments with an SSO solution
that has an Apache REMOTE_USER integration. For manual testing, see
https://zulip.readthedocs.io/en/latest/production/authentication-methods.html
See also remote_user_sso in zerver/views/auth.py.
"""
create_unknown_user = False
def authenticate(self, *, remote_user: str, realm: Realm,
return_data: Optional[Dict[str, Any]]=None) -> Optional[UserProfile]:
if not auth_enabled_helper(["RemoteUser"], realm):
return None
email = remote_user_to_email(remote_user)
return common_get_active_user(email, realm, return_data=return_data)
def is_valid_email(email: str) -> bool:
try:
validate_email(email)
@ -845,6 +826,42 @@ def external_auth_method(cls: Type[ExternalAuthMethod]) -> Type[ExternalAuthMeth
EXTERNAL_AUTH_METHODS.append(cls)
return cls
@external_auth_method
class ZulipRemoteUserBackend(RemoteUserBackend, ExternalAuthMethod):
"""Authentication backend that reads the Apache REMOTE_USER variable.
Used primarily in enterprise environments with an SSO solution
that has an Apache REMOTE_USER integration. For manual testing, see
https://zulip.readthedocs.io/en/latest/production/authentication-methods.html
See also remote_user_sso in zerver/views/auth.py.
"""
auth_backend_name = "RemoteUser"
name = "remoteuser"
display_icon = None
sort_order = 9000 # If configured, this backend should have its button near the top of the list.
create_unknown_user = False
def authenticate(self, *, remote_user: str, realm: Realm,
return_data: Optional[Dict[str, Any]]=None) -> Optional[UserProfile]:
if not auth_enabled_helper(["RemoteUser"], realm):
return None
email = remote_user_to_email(remote_user)
return common_get_active_user(email, realm, return_data=return_data)
@classmethod
def dict_representation(cls) -> List[ExternalAuthMethodDictT]:
return [dict(
name=cls.name,
display_name="SSO",
display_icon=cls.display_icon,
# The user goes to the same URL for both login and signup:
login_url=reverse('login-sso'),
signup_url=reverse('login-sso'),
)]
def redirect_deactivated_user_to_login() -> HttpResponseRedirect:
# Specifying the template name makes sure that the user is not redirected to dev_login in case of
# a deactivated account on a test server.
@ -1417,7 +1434,6 @@ AUTH_BACKEND_NAME_MAP = {
'Dev': DevAuthBackend,
'Email': EmailAuthBackend,
'LDAP': ZulipLDAPAuthBackend,
'RemoteUser': ZulipRemoteUserBackend,
} # type: Dict[str, Any]
for external_method in EXTERNAL_AUTH_METHODS: