From 209c89be100e8641821ffcc03bb8dac1b1f5edff Mon Sep 17 00:00:00 2001 From: Strifel Date: Sat, 15 Aug 2020 18:33:16 +0200 Subject: [PATCH] ldap: Add option to limit user access to certain realms. This adds an option for restricting a ldap user to only be allowed to login into certain realms. This is done by configuring an attribute mapping of "org_membership" to an ldap attribute that will contain the list of subdomains the ldap user is allowed to access. This is analogous to how it's done in SAML. Co-authored-by: Mateusz Mandera --- docs/production/authentication-methods.md | 28 ++++++++++++++++++++++ zerver/tests/test_auth_backends.py | 29 +++++++++++++++++++++++ zproject/backends.py | 11 +++++++++ zproject/prod_settings_template.py | 3 +++ 4 files changed, 71 insertions(+) diff --git a/docs/production/authentication-methods.md b/docs/production/authentication-methods.md index 91a9b412b1..0b37022f6c 100644 --- a/docs/production/authentication-methods.md +++ b/docs/production/authentication-methods.md @@ -284,6 +284,34 @@ details. [upstream-ldap-groups]: https://django-auth-ldap.readthedocs.io/en/latest/groups.html#limiting-access +### Restricting LDAP user access to specific organizations + +If you're hosting multiple Zulip organizations, you can restrict which +users have access to which organizations. +This is done by setting `org_membership` in `AUTH_LDAP_USER_ATTR_MAP` to the name of +the LDAP attribute which will contain a list of subdomains that the +user should be allowed to access. + +For the root subdomain, `www` in the list will work, or any other of +`settings.ROOT_SUBDOMAIN_ALIASES`. + +For example, with `org_membership` set to `department`, a user with +the following attributes will have access to the root and `engineering` subdomains: +``` +... +department: engineering +department: www +... +``` + +```eval_rst +.. warning:: + Restricting access using this mechanism only affects authentication via LDAP, + and won't prevent users from accessing the organization using any other + authentication backends that are enabled for the organization. +``` + + ### Troubleshooting Most issues with LDAP authentication are caused by misconfigurations of diff --git a/zerver/tests/test_auth_backends.py b/zerver/tests/test_auth_backends.py index 79bdf7dab9..f6571d823f 100644 --- a/zerver/tests/test_auth_backends.py +++ b/zerver/tests/test_auth_backends.py @@ -3782,6 +3782,35 @@ class FetchAPIKeyTest(ZulipTestCase): ) self.assert_json_success(result) + @override_settings( + AUTHENTICATION_BACKENDS=("zproject.backends.ZulipLDAPAuthBackend",), + AUTH_LDAP_USER_ATTR_MAP={"full_name": "cn", "org_membership": "department"}, + ) + def test_ldap_auth_email_auth_organization_restriction(self) -> None: + self.init_default_ldap_database() + # We do test two combinations here: + # The first user has no (department) attribute set + # The second user has one set, but to a different value + result = self.client_post( + "/api/v1/fetch_api_key", + dict(username=self.example_email("hamlet"), password=self.ldap_password("hamlet")), + ) + self.assert_json_error(result, "Your username or password is incorrect.", 403) + + self.change_ldap_user_attr("hamlet", "department", "testWrongRealm") + result = self.client_post( + "/api/v1/fetch_api_key", + dict(username=self.example_email("hamlet"), password=self.ldap_password("hamlet")), + ) + self.assert_json_error(result, "Your username or password is incorrect.", 403) + + self.change_ldap_user_attr("hamlet", "department", "zulip") + result = self.client_post( + "/api/v1/fetch_api_key", + dict(username=self.example_email("hamlet"), password=self.ldap_password("hamlet")), + ) + self.assert_json_success(result) + def test_inactive_user(self) -> None: do_deactivate_user(self.user_profile) result = self.client_post( diff --git a/zproject/backends.py b/zproject/backends.py index 734df3a0ee..19524e265d 100644 --- a/zproject/backends.py +++ b/zproject/backends.py @@ -631,6 +631,14 @@ class ZulipLDAPAuthBackendBase(ZulipAuthMixin, LDAPBackend): ldap_disabled = bool(int(account_control_value) & LDAP_USER_ACCOUNT_CONTROL_DISABLED_MASK) return ldap_disabled + def is_account_realm_access_forbidden(self, ldap_user: _LDAPUser, realm: Realm) -> bool: + if "org_membership" not in settings.AUTH_LDAP_USER_ATTR_MAP: + return False + + org_membership_attr = settings.AUTH_LDAP_USER_ATTR_MAP["org_membership"] + allowed_orgs: List[str] = ldap_user.attrs.get(org_membership_attr, []) + return not is_subdomain_in_allowed_subdomains_list(realm.subdomain, allowed_orgs) + @classmethod def get_mapped_name(cls, ldap_user: _LDAPUser) -> str: """Constructs the user's Zulip full_name from the LDAP data""" @@ -767,6 +775,9 @@ class ZulipLDAPAuthBackend(ZulipLDAPAuthBackendBase): username = self.user_email_from_ldapuser(username, ldap_user) + if self.is_account_realm_access_forbidden(ldap_user, self._realm): + raise ZulipLDAPException("User not allowed to access realm") + if "userAccountControl" in settings.AUTH_LDAP_USER_ATTR_MAP: # nocoverage ldap_disabled = self.is_account_control_disabled_user(ldap_user) if ldap_disabled: diff --git a/zproject/prod_settings_template.py b/zproject/prod_settings_template.py index bfd34d6cc1..76237766bf 100644 --- a/zproject/prod_settings_template.py +++ b/zproject/prod_settings_template.py @@ -236,6 +236,9 @@ AUTH_LDAP_USER_ATTR_MAP = { ## who are disabled in LDAP/Active Directory (and reactivate users who are not). ## See docs for usage details and precise semantics. # "userAccountControl": "userAccountControl", + ## Restrict access to organizations using an LDAP attribute. + ## See https://zulip.readthedocs.io/en/latest/production/authentication-methods.html#restricting-ldap-user-access-to-specific-organizations + # "org_membership": "department", } ## Whether to automatically deactivate users not found in LDAP. If LDAP