Repository: mathesar-foundation/mathesar

Issue:
We should be able to create new admin users, upgrade existing users to admin
## Description
* `is_superuser` is currently a readonly property in Users APIs.
* We should be able to set them while an admin is editing a user.
* Users (including super-users) should not be able to modify it's value for themselves.



Discussion:
@kgodey This needs to be on 'Launch blockers' since this is required by the UX for Users & Permissions.
I would like to work on this
Moved to launch blockers, @pavish. @aagman945 feel free to submit a PR, but since this is needed soon, we may reassign to a member of the core team if we don't see a PR for this in the next couple of days.

Resolving patch:
diff --git a/mathesar/api/ui/serializers/users.py b/mathesar/api/ui/serializers/users.py
--- a/mathesar/api/ui/serializers/users.py
+++ b/mathesar/api/ui/serializers/users.py
@@ -43,11 +43,23 @@ class Meta:
         ]
         extra_kwargs = {
             'password': {'write_only': True},
-            'is_superuser': {'read_only': True},
             'database_roles': {'read_only': True},
             'schema_roles': {'read_only': True}
         }
 
+    def get_fields(self):
+        fields = super().get_fields()
+        request = self.context.get("request", None)
+        if not hasattr(request, 'parser_context'):
+            return fields
+        kwargs = request.parser_context.get('kwargs')
+        if kwargs:
+            user_pk = kwargs.get('pk')
+            if user_pk:
+                if request.user.id == int(user_pk) or not request.user.is_superuser:
+                    fields["is_superuser"].read_only = True
+        return fields
+
     def create(self, validated_data):
         password = validated_data.pop('password')
         user = User(**validated_data)


Test patch:
diff --git a/mathesar/tests/api/test_user_api.py b/mathesar/tests/api/test_user_api.py
--- a/mathesar/tests/api/test_user_api.py
+++ b/mathesar/tests/api/test_user_api.py
@@ -156,6 +156,59 @@ def test_user_create_no_superuser(client_bob):
     assert response.json()[0]['code'] == 4004
 
 
+def test_superuser_create_superuser(client):
+    data = {
+        'username': 'alice_admin',
+        'email': 'alice_admin@example.com',
+        'password': 'password',
+        'short_name': 'Alice',
+        'full_name': 'Alice Jones',
+        'is_superuser': True
+    }
+    response = client.post('/api/ui/v0/users/', data)
+    response_data = response.json()
+
+    assert response.status_code == 201
+    assert 'id' in response_data
+    assert response_data['username'] == data['username']
+    assert response_data['email'] == data['email']
+    assert 'password' not in response_data
+    assert response_data['short_name'] == data['short_name']
+    assert response_data['full_name'] == data['full_name']
+    assert response_data['is_superuser'] is True
+    created_user = User.objects.get(id=response_data['id'])
+    assert created_user.password_change_needed is True
+    # clean up
+    created_user.delete()
+
+
+def test_superuser_patch_different_user_admin_privileges(client, user_alice):
+    data = {'is_superuser': True}
+    response = client.patch(f'/api/ui/v0/users/{user_alice.id}/', data)
+    response_data = response.json()
+
+    assert response.status_code == 200
+    assert response_data['is_superuser'] is True
+
+
+def test_superuser_patch_self_admin_privileges(client, admin_user):
+    data = {'is_superuser': False}
+    response = client.patch(f'/api/ui/v0/users/{admin_user.id}/', data)
+    response_data = response.json()
+
+    assert response.status_code == 200
+    assert response_data['is_superuser'] is True
+
+
+def test_user_patch_self_admin_privileges(client_bob, user_bob):
+    data = {'is_superuser': True}
+    response = client_bob.patch(f'/api/ui/v0/users/{user_bob.id}/', data)
+    response_data = response.json()
+
+    assert response.status_code == 200
+    assert response_data['is_superuser'] is False
+
+
 def test_user_delete(client, user_bob):
     # Ensure we can access the user via API
     initial_response = client.get(f'/api/ui/v0/users/{user_bob.id}/')