Repository: zulip/zulip

Issue:
Problem with AUTH_LDAP_USER_FLAGS_BY_GROUP on Zulip >= 4.0 
Updating Zulip from 3.4 zu 4.2 broke my ldap sync, upon closer investigation I found the cause to be the extended set of roles:
because I was using

```
AUTH_LDAP_USER_FLAGS_BY_GROUP = {
        "is_realm_admin": "cn=someGroup,ou=filterGroups,dc=mydomain,dc=com",
}
```

I got the message

```
Exception: Ldap sync would have deactivated all owners of realm . This is most likely due to a misconfiguration of LDAP settings. Rolling back...
Use the --force option if the mass deactivation is intended.
```

Commenting out the above config block was not enough, I had to manually promote at least one user to OWNER for the sync to work again because there wasn't any owner.

Then I tried adjusting my above config snippet from "is_realm_admin" to "is_realm_owner" - that did not work and completely broke the ldap sync right at the start (`AttributeError: can't set attribute`).

How can I get back the feature of having an LDAP group whose members are automatically assigned the highest possible role?
The [relevant docs](https://zulip.readthedocs.io/en/stable/production/authentication-methods.html#other-fields) are rather spartanic.




Discussion:
Hello @zulip/server-authentication members, this issue was labeled with the "area: authentication" label, so you may want to check it out!

<!-- areaLabelAddition -->
Thanks for the report @debalance!  I can confirm the regression.

I think this needs a code change to add an `is_realm_owner` setter similar to this function:
```
    @is_realm_admin.setter                                                                           
    def is_realm_admin(self, value: bool) -> None:                                                   
        if value:                                                                                    
            self.role = UserProfile.ROLE_REALM_ADMINISTRATOR                                         
        elif self.role == UserProfile.ROLE_REALM_ADMINISTRATOR:                                      
            # We need to be careful to not accidentally change                                       
            # ROLE_GUEST to ROLE_MEMBER here.                                                        
            self.role = UserProfile.ROLE_MEMBER                                                      
```

i.e. this:
```
diff --git a/zerver/models.py b/zerver/models.py
index 46e73f6590..103e861461 100644
--- a/zerver/models.py
+++ b/zerver/models.py
@@ -1498,6 +1498,15 @@ class UserProfile(AbstractBaseUser, PermissionsMixin):
             # ROLE_GUEST to ROLE_MEMBER here.
             self.role = UserProfile.ROLE_MEMBER
 
+    @is_realm_owner.setter
+    def is_realm_owner(self, value: bool) -> None:
+        if value:
+            self.role = UserProfile.ROLE_REALM_OWNER
+        elif self.role == UserProfile.ROLE_REALM_OWNER:
+            # We need to be careful to not accidentally change
+            # ROLE_REALM_OWNER to ROLE_MEMBER here.
+            self.role = UserProfile.ROLE_MEMBER
+
     @property
     def has_billing_access(self) -> bool:
         return self.is_realm_owner or self.is_billing_admin
```

@debalance you're welcome to apply that patch to `zerver/models.py` in your installation and use `is_realm_owner`, though it's at present totally untested.

@mateuszmandera FYI; can you do a proper PR for this with tests and documentation, which should presumably cover `is_guest` as well?  And I guess we might want to do the similar work to make `is_realm_moderator` supported as well.
Thx @timabbott , I tested your patch - it seems to be incomplete (or I missed a required step after changing the file):

```
zulip@zulip:~/deployments/current/zerver$ /home/zulip/deployments/current/manage.py sync_ldap_user_data
Traceback (most recent call last):
  File "/home/zulip/deployments/current/manage.py", line 52, in <module>
    execute_from_command_line(sys.argv)
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 395, in execute
    django.setup()
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/apps/registry.py", line 114, in populate
    app_config.import_models()
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/apps/config.py", line 301, in import_models
    self.models_module = import_module(models_module_name)
  File "/usr/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/zulip/deployments/2021-06-02-22-13-46/confirmation/models.py", line 20, in <module>
    from zerver.models import EmailChangeStatus, MultiuseInvite, PreregistrationUser, Realm, UserProfile
  File "/home/zulip/deployments/2021-06-02-22-13-46/zerver/models.py", line 1092, in <module>
    class UserProfile(AbstractBaseUser, PermissionsMixin):
  File "/home/zulip/deployments/2021-06-02-22-13-46/zerver/models.py", line 1498, in UserProfile
    @is_realm_owner.setter
NameError: name 'is_realm_owner' is not defined
```

```
zulip@zulip:~/deployments/current$ /home/zulip/deployments/current/scripts/restart-server
Traceback (most recent call last):
  File "./manage.py", line 52, in <module>
    execute_from_command_line(sys.argv)
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 419, in execute_from_command_line
    utility.execute()
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/core/management/__init__.py", line 395, in execute
    django.setup()
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/__init__.py", line 24, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/apps/registry.py", line 114, in populate
    app_config.import_models()
  File "/home/zulip/deployments/2021-06-02-22-13-46/zulip-py3-venv/lib/python3.7/site-packages/django/apps/config.py", line 301, in import_models
    self.models_module = import_module(models_module_name)
  File "/usr/lib/python3.7/importlib/__init__.py", line 127, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 1006, in _gcd_import
  File "<frozen importlib._bootstrap>", line 983, in _find_and_load
  File "<frozen importlib._bootstrap>", line 967, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 677, in _load_unlocked
  File "<frozen importlib._bootstrap_external>", line 728, in exec_module
  File "<frozen importlib._bootstrap>", line 219, in _call_with_frames_removed
  File "/home/zulip/deployments/2021-06-02-22-13-46/confirmation/models.py", line 20, in <module>
    from zerver.models import EmailChangeStatus, MultiuseInvite, PreregistrationUser, Realm, UserProfile
  File "/home/zulip/deployments/2021-06-02-22-13-46/zerver/models.py", line 1092, in <module>
    class UserProfile(AbstractBaseUser, PermissionsMixin):
  File "/home/zulip/deployments/2021-06-02-22-13-46/zerver/models.py", line 1498, in UserProfile
    @is_realm_owner.setter
NameError: name 'is_realm_owner' is not defined
Traceback (most recent call last):
  File "/home/zulip/deployments/current/scripts/restart-server", line 51, in <module>
    ["./manage.py", "send_stats", "incr", "events.server_restart", str(int(time.time()))]
  File "/usr/lib/python3.7/subprocess.py", line 347, in check_call
    raise CalledProcessError(retcode, cmd)
subprocess.CalledProcessError: Command '['./manage.py', 'send_stats', 'incr', 'events.server_restart', '1622715924']' returned non-zero exit status 1.
```
I am not sure but I think the lines should be added after the code mentioned below to fix the `NameError` -
```python
@property
    def is_realm_owner(self) -> bool:
        return self.role == UserProfile.ROLE_REALM_OWNER
```
@debalance can you try this once?
@sahil839 You're right, now it's working!

Resolving patch:
diff --git a/zerver/models.py b/zerver/models.py
--- a/zerver/models.py
+++ b/zerver/models.py
@@ -1716,6 +1716,15 @@ def has_billing_access(self) -> bool:
     def is_realm_owner(self) -> bool:
         return self.role == UserProfile.ROLE_REALM_OWNER
 
+    @is_realm_owner.setter
+    def is_realm_owner(self, value: bool) -> None:
+        if value:
+            self.role = UserProfile.ROLE_REALM_OWNER
+        elif self.role == UserProfile.ROLE_REALM_OWNER:
+            # We need to be careful to not accidentally change
+            # ROLE_GUEST to ROLE_MEMBER here.
+            self.role = UserProfile.ROLE_MEMBER
+
     @property
     def is_guest(self) -> bool:
         return self.role == UserProfile.ROLE_GUEST
@@ -1733,6 +1742,15 @@ def is_guest(self, value: bool) -> None:
     def is_moderator(self) -> bool:
         return self.role == UserProfile.ROLE_MODERATOR
 
+    @is_moderator.setter
+    def is_moderator(self, value: bool) -> None:
+        if value:
+            self.role = UserProfile.ROLE_MODERATOR
+        elif self.role == UserProfile.ROLE_MODERATOR:
+            # We need to be careful to not accidentally change
+            # ROLE_GUEST to ROLE_MEMBER here.
+            self.role = UserProfile.ROLE_MEMBER
+
     @property
     def is_incoming_webhook(self) -> bool:
         return self.bot_type == UserProfile.INCOMING_WEBHOOK_BOT


Test patch:
diff --git a/zerver/tests/test_users.py b/zerver/tests/test_users.py
--- a/zerver/tests/test_users.py
+++ b/zerver/tests/test_users.py
@@ -95,6 +95,14 @@ def test_role_setters(self) -> None:
         self.assertEqual(user_profile.is_guest, False)
         self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_ADMINISTRATOR)
 
+        user_profile.is_realm_owner = False
+        self.assertEqual(user_profile.is_realm_owner, False)
+        self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_ADMINISTRATOR)
+
+        user_profile.is_moderator = False
+        self.assertEqual(user_profile.is_moderator, False)
+        self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_ADMINISTRATOR)
+
         user_profile.is_realm_admin = False
         self.assertEqual(user_profile.is_realm_admin, False)
         self.assertEqual(user_profile.role, UserProfile.ROLE_MEMBER)
@@ -111,6 +119,22 @@ def test_role_setters(self) -> None:
         self.assertEqual(user_profile.is_guest, False)
         self.assertEqual(user_profile.role, UserProfile.ROLE_MEMBER)
 
+        user_profile.is_realm_owner = True
+        self.assertEqual(user_profile.is_realm_owner, True)
+        self.assertEqual(user_profile.role, UserProfile.ROLE_REALM_OWNER)
+
+        user_profile.is_realm_owner = False
+        self.assertEqual(user_profile.is_realm_owner, False)
+        self.assertEqual(user_profile.role, UserProfile.ROLE_MEMBER)
+
+        user_profile.is_moderator = True
+        self.assertEqual(user_profile.is_moderator, True)
+        self.assertEqual(user_profile.role, UserProfile.ROLE_MODERATOR)
+
+        user_profile.is_moderator = False
+        self.assertEqual(user_profile.is_moderator, False)
+        self.assertEqual(user_profile.role, UserProfile.ROLE_MEMBER)
+
     def test_get_admin_users(self) -> None:
         user_profile = self.example_user("hamlet")
         do_change_user_role(user_profile, UserProfile.ROLE_MEMBER, acting_user=None)