From 4304d5f8dbd37b85a4bd5c8e5e6a1af863bcd1b1 Mon Sep 17 00:00:00 2001
From: Dinesh
For more information, have a look at
diff --git a/templates/zerver/gitlab-error.md b/templates/zerver/gitlab-error.md
new file mode 100644
index 0000000000..612bfb1800
--- /dev/null
+++ b/templates/zerver/gitlab-error.md
@@ -0,0 +1,12 @@
+You are using the **GitLab auth backend**, but it is not properly
+configured. Please check the following:
+
+* You have added `{{ root_domain_uri }}/complete/gitlab/` as the callback
+URL in the OAuth application in GitLab. You can register OAuth apps at
+[GitLab Applications](https://gitlab.com/profile/applications).
+
+* You have set `{{ client_id_key_name }}` in `{{ settings_path }}` and
+`social_auth_gitlab_secret` in `{{ secrets_path }}` with the values
+from your OAuth application.
+
+* Navigate back to the login page and attempt the GitLab auth flow again.
diff --git a/zerver/migrations/0269_gitlab_auth.py b/zerver/migrations/0269_gitlab_auth.py
new file mode 100644
index 0000000000..d8b20bf82c
--- /dev/null
+++ b/zerver/migrations/0269_gitlab_auth.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.11.26 on 2020-01-29 17:30
+from __future__ import unicode_literals
+
+import bitfield.models
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('zerver', '0268_add_userpresence_realm_timestamp_index'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='realm',
+ name='authentication_methods',
+ field=bitfield.models.BitField(['Google', 'Email', 'GitHub', 'LDAP', 'Dev', 'RemoteUser', 'AzureAD', 'SAML', 'GitLab'], default=2147483647),
+ ),
+ ]
diff --git a/zerver/models.py b/zerver/models.py
index 247dc757c6..3a4cf67310 100644
--- a/zerver/models.py
+++ b/zerver/models.py
@@ -136,7 +136,7 @@ class Realm(models.Model):
INVITES_STANDARD_REALM_DAILY_MAX = 3000
MESSAGE_VISIBILITY_LIMITED = 10000
AUTHENTICATION_FLAGS = [u'Google', u'Email', u'GitHub', u'LDAP', u'Dev',
- u'RemoteUser', u'AzureAD', u'SAML']
+ u'RemoteUser', u'AzureAD', u'SAML', u'GitLab']
SUBDOMAIN_FOR_ROOT_DOMAIN = ''
# User-visible display name and description used on e.g. the organization homepage
diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py
index 976f3bae47..a85a8ebe38 100644
--- a/zerver/tests/test_auth_backends.py
+++ b/zerver/tests/test_auth_backends.py
@@ -58,9 +58,9 @@ from confirmation.models import Confirmation, create_confirmation_link
from zproject.backends import ZulipDummyBackend, EmailAuthBackend, \
GoogleAuthBackend, ZulipRemoteUserBackend, ZulipLDAPAuthBackend, \
- ZulipLDAPUserPopulator, DevAuthBackend, GitHubAuthBackend, ZulipAuthMixin, \
- dev_auth_enabled, password_auth_enabled, github_auth_enabled, google_auth_enabled, \
- require_email_format_usernames, AUTH_BACKEND_NAME_MAP, \
+ ZulipLDAPUserPopulator, DevAuthBackend, GitHubAuthBackend, GitLabAuthBackend, ZulipAuthMixin, \
+ dev_auth_enabled, password_auth_enabled, github_auth_enabled, gitlab_auth_enabled, \
+ google_auth_enabled, require_email_format_usernames, AUTH_BACKEND_NAME_MAP, \
ZulipLDAPConfigurationError, ZulipLDAPExceptionNoMatchingLDAPUser, ZulipLDAPExceptionOutsideDomain, \
ZulipLDAPException, query_ldap, sync_user_from_ldap, SocialAuthMixin, \
PopulateUserLDAPError, SAMLAuthBackend, saml_auth_enabled, email_belongs_to_ldap, \
@@ -1695,6 +1695,26 @@ class GitHubAuthBackendTest(SocialAuthBase):
mock_warning.assert_called_once_with("Social auth (GitHub) failed because user has no verified"
" emails associated with the account")
+class GitLabAuthBackendTest(SocialAuthBase):
+ __unittest_skip__ = False
+
+ BACKEND_CLASS = GitLabAuthBackend
+ CLIENT_KEY_SETTING = "SOCIAL_AUTH_GITLAB_KEY"
+ LOGIN_URL = "/accounts/login/social/gitlab"
+ SIGNUP_URL = "/accounts/register/social/gitlab"
+ AUTHORIZATION_URL = "https://gitlab.com/oauth/authorize"
+ ACCESS_TOKEN_URL = "https://gitlab.com/oauth/token"
+ USER_INFO_URL = "https://gitlab.com/api/v4/user"
+ AUTH_FINISH_URL = "/complete/gitlab/"
+ CONFIG_ERROR_URL = "/config-error/gitlab"
+
+ def test_gitlab_auth_enabled(self) -> None:
+ with self.settings(AUTHENTICATION_BACKENDS=('zproject.backends.GitLabAuthBackend',)):
+ self.assertTrue(gitlab_auth_enabled())
+
+ def get_account_data_dict(self, email: str, name: str) -> Dict[str, Any]:
+ return dict(email=email, name=name, email_verified=True)
+
class GoogleAuthBackendTest(SocialAuthBase):
__unittest_skip__ = False
diff --git a/zerver/tests/test_docs.py b/zerver/tests/test_docs.py
index 973aa7db91..60ec2b3f5b 100644
--- a/zerver/tests/test_docs.py
+++ b/zerver/tests/test_docs.py
@@ -413,6 +413,36 @@ class ConfigErrorTest(ZulipTestCase):
self.assert_not_in_success_response(["zproject/dev_settings.py"], result)
self.assert_not_in_success_response(["zproject/dev-secrets.conf"], result)
+ @override_settings(SOCIAL_AUTH_GITLAB_KEY=None)
+ def test_gitlab(self) -> None:
+ result = self.client_get("/accounts/login/social/gitlab")
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(result.url, '/config-error/gitlab')
+ result = self.client_get(result.url)
+ self.assert_in_success_response(["social_auth_gitlab_key"], result)
+ self.assert_in_success_response(["social_auth_gitlab_secret"], result)
+ self.assert_in_success_response(["zproject/dev-secrets.conf"], result)
+ self.assert_not_in_success_response(["SOCIAL_AUTH_GITLAB_KEY"], result)
+ self.assert_not_in_success_response(["zproject/dev_settings.py"], result)
+ self.assert_not_in_success_response(["/etc/zulip/settings.py"], result)
+ self.assert_not_in_success_response(["/etc/zulip/zulip-secrets.conf"], result)
+
+ @override_settings(SOCIAL_AUTH_GITLAB_KEY=None)
+ @override_settings(DEVELOPMENT=False)
+ def test_gitlab_production_error(self) -> None:
+ """Test the !DEVELOPMENT code path of config-error."""
+ result = self.client_get("/accounts/login/social/gitlab")
+ self.assertEqual(result.status_code, 302)
+ self.assertEqual(result.url, '/config-error/gitlab')
+ result = self.client_get(result.url)
+ self.assert_in_success_response(["SOCIAL_AUTH_GITLAB_KEY"], result)
+ self.assert_in_success_response(["/etc/zulip/settings.py"], result)
+ self.assert_in_success_response(["social_auth_gitlab_secret"], result)
+ self.assert_in_success_response(["/etc/zulip/zulip-secrets.conf"], result)
+ self.assert_not_in_success_response(["social_auth_gitlab_key"], result)
+ self.assert_not_in_success_response(["zproject/dev_settings.py"], result)
+ self.assert_not_in_success_response(["zproject/dev-secrets.conf"], result)
+
@override_settings(SOCIAL_AUTH_SAML_ENABLED_IDPS=None)
def test_saml_error(self) -> None:
result = self.client_get("/accounts/login/social/saml")
diff --git a/zerver/views/auth.py b/zerver/views/auth.py
index b48aeed4fe..81ce7c57fd 100644
--- a/zerver/views/auth.py
+++ b/zerver/views/auth.py
@@ -429,6 +429,9 @@ def start_social_login(request: HttpRequest, backend: str, extra_arg: Optional[s
if (backend == "google") and not (settings.SOCIAL_AUTH_GOOGLE_KEY and
settings.SOCIAL_AUTH_GOOGLE_SECRET):
return redirect_to_config_error("google")
+ if (backend == "gitlab") and not (settings.SOCIAL_AUTH_GITLAB_KEY and
+ settings.SOCIAL_AUTH_GITLAB_SECRET):
+ return redirect_to_config_error("gitlab")
# TODO: Add a similar block for AzureAD.
return oauth_redirect_to_root(request, backend_url, 'social', extra_url_params=extra_url_params)
diff --git a/zproject/backends.py b/zproject/backends.py
index ef5c53ac86..c50131a408 100644
--- a/zproject/backends.py
+++ b/zproject/backends.py
@@ -39,6 +39,7 @@ from onelogin.saml2.errors import OneLogin_Saml2_Error
from social_core.backends.github import GithubOAuth2, GithubOrganizationOAuth2, \
GithubTeamOAuth2
from social_core.backends.azuread import AzureADOAuth2
+from social_core.backends.gitlab import GitLabOAuth2
from social_core.backends.base import BaseAuth
from social_core.backends.google import GoogleOAuth2
from social_core.backends.saml import SAMLAuth
@@ -110,6 +111,9 @@ def google_auth_enabled(realm: Optional[Realm]=None) -> bool:
def github_auth_enabled(realm: Optional[Realm]=None) -> bool:
return auth_enabled_helper(['GitHub'], realm)
+def gitlab_auth_enabled(realm: Optional[Realm]=None) -> bool:
+ return auth_enabled_helper(['GitLab'], realm)
+
def saml_auth_enabled(realm: Optional[Realm]=None) -> bool:
return auth_enabled_helper(['SAML'], realm)
@@ -1324,6 +1328,21 @@ class AzureADAuthBackend(SocialAuthMixin, AzureADOAuth2):
auth_backend_name = "AzureAD"
display_icon = "/static/images/landing-page/logos/azuread-icon.png"
+@external_auth_method
+class GitLabAuthBackend(SocialAuthMixin, GitLabOAuth2):
+ sort_order = 75
+ name = "gitlab"
+ auth_backend_name = "GitLab"
+ display_icon = "/static/images/landing-page/logos/gitlab-icon.png"
+
+ # Note: GitLab as of early 2020 supports having multiple email
+ # addresses connected with a GitLab account, and we could access
+ # those emails, but its APIs don't indicate which of those email
+ # addresses were verified, so we cannot use them for
+ # authentication like we do for the GitHub integration. Instead,
+ # we just use the primary email address, which is always verified.
+ # (No code is required to do so, as that's the default behavior).
+
@external_auth_method
class GoogleAuthBackend(SocialAuthMixin, GoogleOAuth2):
sort_order = 150
diff --git a/zproject/default_settings.py b/zproject/default_settings.py
index fc133f957a..d33173e6a0 100644
--- a/zproject/default_settings.py
+++ b/zproject/default_settings.py
@@ -50,6 +50,7 @@ FAKE_LDAP_NUM_USERS = 8
SOCIAL_AUTH_GITHUB_KEY = get_secret('social_auth_github_key', development_only=True)
SOCIAL_AUTH_GITHUB_ORG_NAME = None # type: Optional[str]
SOCIAL_AUTH_GITHUB_TEAM_ID = None # type: Optional[str]
+SOCIAL_AUTH_GITLAB_KEY = get_secret('social_auth_gitlab_key')
SOCIAL_AUTH_SUBDOMAIN = None # type: Optional[str]
SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = get_secret('azure_oauth2_secret')
SOCIAL_AUTH_GOOGLE_KEY = get_secret('social_auth_google_key', development_only=True)
diff --git a/zproject/dev_settings.py b/zproject/dev_settings.py
index 4083a51ba0..73a075129b 100644
--- a/zproject/dev_settings.py
+++ b/zproject/dev_settings.py
@@ -47,6 +47,7 @@ AUTHENTICATION_BACKENDS = (
'zproject.backends.GoogleAuthBackend',
'zproject.backends.SAMLAuthBackend',
# 'zproject.backends.AzureADAuthBackend',
+ 'zproject.backends.GitLabAuthBackend',
)
EXTERNAL_URI_SCHEME = "http://"
diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py
index b88381a888..0ac046e14a 100644
--- a/zproject/prod_settings_template.py
+++ b/zproject/prod_settings_template.py
@@ -119,6 +119,7 @@ AUTHENTICATION_BACKENDS = (
'zproject.backends.EmailAuthBackend', # Email and password; just requires SMTP setup
# 'zproject.backends.GoogleAuthBackend', # Google auth, setup below
# 'zproject.backends.GitHubAuthBackend', # GitHub auth, setup below
+ # 'zproject.backends.GitLabAuthBackend', # GitLab auth, setup below
# 'zproject.backends.AzureADAuthBackend', # Microsoft Azure Active Directory auth, setup below
# 'zproject.backends.SAMLAuthBackend', # SAML, setup below
# 'zproject.backends.ZulipLDAPAuthBackend', # LDAP, setup below
@@ -148,6 +149,27 @@ AUTHENTICATION_BACKENDS = (
# client secret in zulip-secrets.conf as `social_auth_google_secret`.
#SOCIAL_AUTH_GOOGLE_KEY =