diff --git a/docs/production/authentication-methods.md b/docs/production/authentication-methods.md index 79ad9de402..23c6755dbc 100644 --- a/docs/production/authentication-methods.md +++ b/docs/production/authentication-methods.md @@ -179,6 +179,18 @@ This feature works by checking for the `ACCOUNTDISABLE` flag on the [this handy resource](https://jackstromberg.com/2013/01/useraccountcontrol-attributeflag-values/) for details on the various `userAccountControl` flags. +#### Deactivating non-matching users + +Starting with Zulip 2.0, Zulip supports automatically deactivating +users if they are not found by the `AUTH_LDAP_USER_SEARCH` query +(either because the user is no longer in LDAP/Active Directory, or +because the user no longer matches the query). This feature is +enabled by default if LDAP is the only authentication backend +configured on the Zulip server. Otherwise, you can enable this +feature by setting `LDAP_DEACTIVATE_NON_MATCHING_USERS` to `True` in +`/etc/zulip/settings.py`. Nonmatching users will be fully deactivated +the next time your `manage.py sync_ldap_user_data` cron job runs. + #### Other fields Other fields you may want to sync from LDAP include: diff --git a/zerver/management/commands/sync_ldap_user_data.py b/zerver/management/commands/sync_ldap_user_data.py index 8c3e2872f3..c22fdc4211 100644 --- a/zerver/management/commands/sync_ldap_user_data.py +++ b/zerver/management/commands/sync_ldap_user_data.py @@ -28,6 +28,8 @@ def sync_ldap_user_data(user_profiles: List[UserProfile]) -> None: logger.info("Updated %s." % (u.email,)) else: logger.warning("Did not find %s in LDAP." % (u.email,)) + if settings.LDAP_DEACTIVATE_NON_MATCHING_USERS: + logger.info("Deactivated non-matching user: %s" % (u.email,)) except ZulipLDAPException as e: logger.error("Error attempting to update user %s:" % (u.email,)) logger.error(e) diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index d76cba2bd0..e83edf3385 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -2587,6 +2587,19 @@ class TestZulipLDAPUserPopulator(ZulipLDAPTestCase): hamlet = self.example_user('hamlet') self.assertEqual(hamlet.avatar_source, UserProfile.AVATAR_FROM_USER) + def test_deactivate_non_matching_users(self) -> None: + self.mock_ldap.directory = {} + + with self.settings(LDAP_APPEND_DOMAIN='zulip.com', + AUTH_LDAP_BIND_PASSWORD='', + AUTH_LDAP_USER_DN_TEMPLATE='uid=%(user)s,ou=users,dc=zulip,dc=com', + LDAP_DEACTIVATE_NON_MATCHING_USERS=True): + result = sync_user_from_ldap(self.example_user('hamlet')) + + self.assertFalse(result) + hamlet = self.example_user('hamlet') + self.assertFalse(hamlet.is_active) + class TestZulipAuthMixin(ZulipTestCase): def test_get_user(self) -> None: backend = ZulipAuthMixin() diff --git a/zproject/backends.py b/zproject/backends.py index e4ae972719..8791f2c5bc 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -443,7 +443,11 @@ class ZulipLDAPUserPopulator(ZulipLDAPAuthBackendBase): def sync_user_from_ldap(user_profile: UserProfile) -> bool: backend = ZulipLDAPUserPopulator() updated_user = backend.populate_user(backend.django_to_ldap_username(user_profile.email)) - return updated_user is not None + if not updated_user: + if settings.LDAP_DEACTIVATE_NON_MATCHING_USERS: + do_deactivate_user(user_profile) + return False + return True class DevAuthBackend(ZulipAuthMixin): # Allow logging in as any user without a password. diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index 909b3b924e..f74f71459a 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -478,6 +478,11 @@ AUTH_LDAP_USER_ATTR_MAP = { # "userAccountControl": "userAccountControl", } +# Whether to automatically deactivate users not found in LDAP. If LDAP +# is the only authentication method, then this setting defaults to +# True. If other authentication methods are enabled, it defaults to +# False. +#LDAP_DEACTIVATE_NON_MATCHING_USERS = True ################ # Miscellaneous settings. diff --git a/zproject/settings.py b/zproject/settings.py index badd7497f4..11a1ab83a9 100644 --- a/zproject/settings.py +++ b/zproject/settings.py @@ -1297,6 +1297,11 @@ ZULIP_IOS_APP_ID = 'org.zulip.Zulip' USING_APACHE_SSO = ('zproject.backends.ZulipRemoteUserBackend' in AUTHENTICATION_BACKENDS) +if 'LDAP_DEACTIVATE_NON_MATCHING_USERS' not in vars(): + LDAP_DEACTIVATE_NON_MATCHING_USERS = ( + len(AUTHENTICATION_BACKENDS) == 1 and (AUTHENTICATION_BACKENDS[0] == + "zproject.backends.ZulipLDAPAuthBackend")) + if len(AUTHENTICATION_BACKENDS) == 1 and (AUTHENTICATION_BACKENDS[0] == "zproject.backends.ZulipRemoteUserBackend"): HOME_NOT_LOGGED_IN = "/accounts/login/sso/"