We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add audit logging for isSuperuser/isStaff changes


--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `src/sentry/api/endpoints/user_details.py`
Content:
```
1 import logging
2 from datetime import datetime
3 
4 import pytz
5 from django.conf import settings
6 from django.contrib.auth import logout
7 from django.utils.translation import ugettext_lazy as _
8 from rest_framework import serializers, status
9 from rest_framework.response import Response
10 
11 from sentry import roles
12 from sentry.api import client
13 from sentry.api.bases.user import UserEndpoint
14 from sentry.api.decorators import sudo_required
15 from sentry.api.serializers import serialize
16 from sentry.api.serializers.models.user import DetailedUserSerializer
17 from sentry.api.serializers.rest_framework import ListField
18 from sentry.auth.superuser import is_active_superuser
19 from sentry.constants import LANGUAGES
20 from sentry.models import Organization, OrganizationMember, OrganizationStatus, User, UserOption
21 
22 delete_logger = logging.getLogger("sentry.deletions.api")
23 
24 
25 def _get_timezone_choices():
26     results = []
27     for tz in pytz.all_timezones:
28         now = datetime.now(pytz.timezone(tz))
29         offset = now.strftime("%z")
30         results.append((int(offset), tz, f"(UTC{offset}) {tz}"))
31     results.sort()
32 
33     for i in range(len(results)):
34         results[i] = results[i][1:]
35     return results
36 
37 
38 TIMEZONE_CHOICES = _get_timezone_choices()
39 
40 
41 class UserOptionsSerializer(serializers.Serializer):
42     language = serializers.ChoiceField(choices=LANGUAGES, required=False)
43     stacktraceOrder = serializers.ChoiceField(
44         choices=(
45             ("-1", _("Default (let Sentry decide)")),
46             ("1", _("Most recent call last")),
47             ("2", _("Most recent call first")),
48         ),
49         required=False,
50     )
51     timezone = serializers.ChoiceField(choices=TIMEZONE_CHOICES, required=False)
52     clock24Hours = serializers.BooleanField(required=False)
53     theme = serializers.ChoiceField(
54         choices=(
55             ("light", _("Light")),
56             ("dark", _("Dark")),
57             ("system", _("Default to system")),
58         ),
59         required=False,
60     )
61 
62 
63 class BaseUserSerializer(serializers.ModelSerializer):
64     def validate_username(self, value):
65         if User.objects.filter(username__iexact=value).exclude(id=self.instance.id).exists():
66             raise serializers.ValidationError("That username is already in use.")
67         return value
68 
69     def validate(self, attrs):
70         attrs = super().validate(attrs)
71 
72         if self.instance.email == self.instance.username:
73             if attrs.get("username", self.instance.email) != self.instance.email:
74                 # ... this probably needs to handle newsletters and such?
75                 attrs.setdefault("email", attrs["username"])
76 
77         return attrs
78 
79     def update(self, instance, validated_data):
80         if "isActive" not in validated_data:
81             validated_data["isActive"] = instance.is_active
82         return super().update(instance, validated_data)
83 
84 
85 class UserSerializer(BaseUserSerializer):
86     class Meta:
87         model = User
88         fields = ("name", "username")
89 
90     def validate(self, attrs):
91         for field in settings.SENTRY_MANAGED_USER_FIELDS:
92             attrs.pop(field, None)
93 
94         return super().validate(attrs)
95 
96 
97 class PrivilegedUserSerializer(BaseUserSerializer):
98     isActive = serializers.BooleanField(source="is_active")
99     isStaff = serializers.BooleanField(source="is_staff")
100     isSuperuser = serializers.BooleanField(source="is_superuser")
101 
102     class Meta:
103         model = User
104         # no idea wtf is up with django rest framework, but we need is_active
105         # and isActive
106         fields = ("name", "username", "isActive", "isStaff", "isSuperuser")
107 
108 
109 class DeleteUserSerializer(serializers.Serializer):
110     organizations = ListField(child=serializers.CharField(required=False), required=True)
111     hardDelete = serializers.BooleanField(required=False)
112 
113 
114 class UserDetailsEndpoint(UserEndpoint):
115     def get(self, request, user):
116         """
117         Retrieve User Details
118         `````````````````````
119 
120         Return details for an account's details and options such as: full name, timezone, 24hr times, language,
121         stacktrace_order.
122 
123         :auth: required
124         """
125         return Response(serialize(user, request.user, DetailedUserSerializer()))
126 
127     def put(self, request, user):
128         """
129         Update Account Appearance options
130         `````````````````````````````````
131 
132         Update account appearance options. Only supplied values are updated.
133 
134         :pparam string user_id: user id
135         :param string language: language preference
136         :param string stacktrace_order: One of -1 (default), 1 (most recent call last), 2 (most recent call first).
137         :param string timezone: timezone option
138         :param clock_24_hours boolean: use 24 hour clock
139         :param string theme: UI theme, either "light", "dark", or "system"
140         :auth: required
141         """
142 
143         if is_active_superuser(request) and request.access.has_permission("users.admin"):
144             serializer_cls = PrivilegedUserSerializer
145         else:
146             serializer_cls = UserSerializer
147         serializer = serializer_cls(user, data=request.data, partial=True)
148 
149         serializer_options = UserOptionsSerializer(
150             data=request.data.get("options", {}), partial=True
151         )
152 
153         # This serializer should NOT include privileged fields e.g. password
154         if not serializer.is_valid() or not serializer_options.is_valid():
155             return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
156 
157         # map API keys to keys in model
158         key_map = {
159             "theme": "theme",
160             "language": "language",
161             "timezone": "timezone",
162             "stacktraceOrder": "stacktrace_order",
163             "clock24Hours": "clock_24_hours",
164         }
165 
166         options_result = serializer_options.validated_data
167 
168         for key in key_map:
169             if key in options_result:
170                 UserOption.objects.set_value(
171                     user=user, key=key_map.get(key, key), value=options_result.get(key)
172                 )
173 
174         user = serializer.save()
175 
176         return Response(serialize(user, request.user, DetailedUserSerializer()))
177 
178     @sudo_required
179     def delete(self, request, user):
180         """
181         Delete User Account
182 
183         Also removes organizations if they are an owner
184         :pparam string user_id: user id
185         :param boolean hard_delete: Completely remove the user from the database (requires super user)
186         :param list organizations: List of organization ids to remove
187         :auth required:
188         """
189 
190         serializer = DeleteUserSerializer(data=request.data)
191 
192         if not serializer.is_valid():
193             return Response(status=status.HTTP_400_BAD_REQUEST)
194 
195         # from `frontend/remove_account.py`
196         org_list = Organization.objects.filter(
197             member_set__role__in=[x.id for x in roles.with_scope("org:admin")],
198             member_set__user=user,
199             status=OrganizationStatus.VISIBLE,
200         )
201 
202         org_results = []
203         for org in org_list:
204             org_results.append({"organization": org, "single_owner": org.has_single_owner()})
205 
206         avail_org_slugs = {o["organization"].slug for o in org_results}
207         orgs_to_remove = set(serializer.validated_data.get("organizations")).intersection(
208             avail_org_slugs
209         )
210 
211         for result in org_results:
212             if result["single_owner"]:
213                 orgs_to_remove.add(result["organization"].slug)
214 
215         for org_slug in orgs_to_remove:
216             client.delete(path=f"/organizations/{org_slug}/", request=request, is_sudo=True)
217 
218         remaining_org_ids = [
219             o.id for o in org_list if o.slug in avail_org_slugs.difference(orgs_to_remove)
220         ]
221 
222         if remaining_org_ids:
223             OrganizationMember.objects.filter(
224                 organization__in=remaining_org_ids, user=user
225             ).delete()
226 
227         logging_data = {
228             "actor_id": request.user.id,
229             "ip_address": request.META["REMOTE_ADDR"],
230             "user_id": user.id,
231         }
232 
233         hard_delete = serializer.validated_data.get("hardDelete", False)
234 
235         # Only active superusers can hard delete accounts
236         if hard_delete and not is_active_superuser(request):
237             return Response(
238                 {"detail": "Only superusers may hard delete a user account"},
239                 status=status.HTTP_403_FORBIDDEN,
240             )
241 
242         is_current_user = request.user.id == user.id
243 
244         if hard_delete:
245             user.delete()
246             delete_logger.info("user.removed", extra=logging_data)
247         else:
248             User.objects.filter(id=user.id).update(is_active=False)
249             delete_logger.info("user.deactivate", extra=logging_data)
250 
251         # if the user deleted their own account log them out
252         if is_current_user:
253             logout(request)
254 
255         return Response(status=status.HTTP_204_NO_CONTENT)
256
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/src/sentry/api/endpoints/user_details.py b/src/sentry/api/endpoints/user_details.py
--- a/src/sentry/api/endpoints/user_details.py
+++ b/src/sentry/api/endpoints/user_details.py
@@ -4,6 +4,7 @@
 import pytz
 from django.conf import settings
 from django.contrib.auth import logout
+from django.db import transaction
 from django.utils.translation import ugettext_lazy as _
 from rest_framework import serializers, status
 from rest_framework.response import Response
@@ -19,6 +20,7 @@
 from sentry.constants import LANGUAGES
 from sentry.models import Organization, OrganizationMember, OrganizationStatus, User, UserOption
 
+audit_logger = logging.getLogger("sentry.audit.user")
 delete_logger = logging.getLogger("sentry.deletions.api")
 
 
@@ -171,7 +173,18 @@
                     user=user, key=key_map.get(key, key), value=options_result.get(key)
                 )
 
-        user = serializer.save()
+        with transaction.atomic():
+            user = serializer.save()
+
+            if any(k in request.data for k in ("isStaff", "isSuperuser", "isActive")):
+                audit_logger.info(
+                    "user.edit",
+                    extra={
+                        "user_id": user.id,
+                        "actor_id": request.user.id,
+                        "form_data": request.data,
+                    },
+                )
 
         return Response(serialize(user, request.user, DetailedUserSerializer()))

{"golden_diff": "diff --git a/src/sentry/api/endpoints/user_details.py b/src/sentry/api/endpoints/user_details.py\n--- a/src/sentry/api/endpoints/user_details.py\n+++ b/src/sentry/api/endpoints/user_details.py\n@@ -4,6 +4,7 @@\n import pytz\n from django.conf import settings\n from django.contrib.auth import logout\n+from django.db import transaction\n from django.utils.translation import ugettext_lazy as _\n from rest_framework import serializers, status\n from rest_framework.response import Response\n@@ -19,6 +20,7 @@\n from sentry.constants import LANGUAGES\n from sentry.models import Organization, OrganizationMember, OrganizationStatus, User, UserOption\n \n+audit_logger = logging.getLogger(\"sentry.audit.user\")\n delete_logger = logging.getLogger(\"sentry.deletions.api\")\n \n \n@@ -171,7 +173,18 @@\n                     user=user, key=key_map.get(key, key), value=options_result.get(key)\n                 )\n \n-        user = serializer.save()\n+        with transaction.atomic():\n+            user = serializer.save()\n+\n+            if any(k in request.data for k in (\"isStaff\", \"isSuperuser\", \"isActive\")):\n+                audit_logger.info(\n+                    \"user.edit\",\n+                    extra={\n+                        \"user_id\": user.id,\n+                        \"actor_id\": request.user.id,\n+                        \"form_data\": request.data,\n+                    },\n+                )\n \n         return Response(serialize(user, request.user, DetailedUserSerializer()))\n", "issue": "Add audit logging for isSuperuser/isStaff changes\n\n", "before_files": [{"content": "import logging\nfrom datetime import datetime\n\nimport pytz\nfrom django.conf import settings\nfrom django.contrib.auth import logout\nfrom django.utils.translation import ugettext_lazy as _\nfrom rest_framework import serializers, status\nfrom rest_framework.response import Response\n\nfrom sentry import roles\nfrom sentry.api import client\nfrom sentry.api.bases.user import UserEndpoint\nfrom sentry.api.decorators import sudo_required\nfrom sentry.api.serializers import serialize\nfrom sentry.api.serializers.models.user import DetailedUserSerializer\nfrom sentry.api.serializers.rest_framework import ListField\nfrom sentry.auth.superuser import is_active_superuser\nfrom sentry.constants import LANGUAGES\nfrom sentry.models import Organization, OrganizationMember, OrganizationStatus, User, UserOption\n\ndelete_logger = logging.getLogger(\"sentry.deletions.api\")\n\n\ndef _get_timezone_choices():\n    results = []\n    for tz in pytz.all_timezones:\n        now = datetime.now(pytz.timezone(tz))\n        offset = now.strftime(\"%z\")\n        results.append((int(offset), tz, f\"(UTC{offset}) {tz}\"))\n    results.sort()\n\n    for i in range(len(results)):\n        results[i] = results[i][1:]\n    return results\n\n\nTIMEZONE_CHOICES = _get_timezone_choices()\n\n\nclass UserOptionsSerializer(serializers.Serializer):\n    language = serializers.ChoiceField(choices=LANGUAGES, required=False)\n    stacktraceOrder = serializers.ChoiceField(\n        choices=(\n            (\"-1\", _(\"Default (let Sentry decide)\")),\n            (\"1\", _(\"Most recent call last\")),\n            (\"2\", _(\"Most recent call first\")),\n        ),\n        required=False,\n    )\n    timezone = serializers.ChoiceField(choices=TIMEZONE_CHOICES, required=False)\n    clock24Hours = serializers.BooleanField(required=False)\n    theme = serializers.ChoiceField(\n        choices=(\n            (\"light\", _(\"Light\")),\n            (\"dark\", _(\"Dark\")),\n            (\"system\", _(\"Default to system\")),\n        ),\n        required=False,\n    )\n\n\nclass BaseUserSerializer(serializers.ModelSerializer):\n    def validate_username(self, value):\n        if User.objects.filter(username__iexact=value).exclude(id=self.instance.id).exists():\n            raise serializers.ValidationError(\"That username is already in use.\")\n        return value\n\n    def validate(self, attrs):\n        attrs = super().validate(attrs)\n\n        if self.instance.email == self.instance.username:\n            if attrs.get(\"username\", self.instance.email) != self.instance.email:\n                # ... this probably needs to handle newsletters and such?\n                attrs.setdefault(\"email\", attrs[\"username\"])\n\n        return attrs\n\n    def update(self, instance, validated_data):\n        if \"isActive\" not in validated_data:\n            validated_data[\"isActive\"] = instance.is_active\n        return super().update(instance, validated_data)\n\n\nclass UserSerializer(BaseUserSerializer):\n    class Meta:\n        model = User\n        fields = (\"name\", \"username\")\n\n    def validate(self, attrs):\n        for field in settings.SENTRY_MANAGED_USER_FIELDS:\n            attrs.pop(field, None)\n\n        return super().validate(attrs)\n\n\nclass PrivilegedUserSerializer(BaseUserSerializer):\n    isActive = serializers.BooleanField(source=\"is_active\")\n    isStaff = serializers.BooleanField(source=\"is_staff\")\n    isSuperuser = serializers.BooleanField(source=\"is_superuser\")\n\n    class Meta:\n        model = User\n        # no idea wtf is up with django rest framework, but we need is_active\n        # and isActive\n        fields = (\"name\", \"username\", \"isActive\", \"isStaff\", \"isSuperuser\")\n\n\nclass DeleteUserSerializer(serializers.Serializer):\n    organizations = ListField(child=serializers.CharField(required=False), required=True)\n    hardDelete = serializers.BooleanField(required=False)\n\n\nclass UserDetailsEndpoint(UserEndpoint):\n    def get(self, request, user):\n        \"\"\"\n        Retrieve User Details\n        `````````````````````\n\n        Return details for an account's details and options such as: full name, timezone, 24hr times, language,\n        stacktrace_order.\n\n        :auth: required\n        \"\"\"\n        return Response(serialize(user, request.user, DetailedUserSerializer()))\n\n    def put(self, request, user):\n        \"\"\"\n        Update Account Appearance options\n        `````````````````````````````````\n\n        Update account appearance options. Only supplied values are updated.\n\n        :pparam string user_id: user id\n        :param string language: language preference\n        :param string stacktrace_order: One of -1 (default), 1 (most recent call last), 2 (most recent call first).\n        :param string timezone: timezone option\n        :param clock_24_hours boolean: use 24 hour clock\n        :param string theme: UI theme, either \"light\", \"dark\", or \"system\"\n        :auth: required\n        \"\"\"\n\n        if is_active_superuser(request) and request.access.has_permission(\"users.admin\"):\n            serializer_cls = PrivilegedUserSerializer\n        else:\n            serializer_cls = UserSerializer\n        serializer = serializer_cls(user, data=request.data, partial=True)\n\n        serializer_options = UserOptionsSerializer(\n            data=request.data.get(\"options\", {}), partial=True\n        )\n\n        # This serializer should NOT include privileged fields e.g. password\n        if not serializer.is_valid() or not serializer_options.is_valid():\n            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)\n\n        # map API keys to keys in model\n        key_map = {\n            \"theme\": \"theme\",\n            \"language\": \"language\",\n            \"timezone\": \"timezone\",\n            \"stacktraceOrder\": \"stacktrace_order\",\n            \"clock24Hours\": \"clock_24_hours\",\n        }\n\n        options_result = serializer_options.validated_data\n\n        for key in key_map:\n            if key in options_result:\n                UserOption.objects.set_value(\n                    user=user, key=key_map.get(key, key), value=options_result.get(key)\n                )\n\n        user = serializer.save()\n\n        return Response(serialize(user, request.user, DetailedUserSerializer()))\n\n    @sudo_required\n    def delete(self, request, user):\n        \"\"\"\n        Delete User Account\n\n        Also removes organizations if they are an owner\n        :pparam string user_id: user id\n        :param boolean hard_delete: Completely remove the user from the database (requires super user)\n        :param list organizations: List of organization ids to remove\n        :auth required:\n        \"\"\"\n\n        serializer = DeleteUserSerializer(data=request.data)\n\n        if not serializer.is_valid():\n            return Response(status=status.HTTP_400_BAD_REQUEST)\n\n        # from `frontend/remove_account.py`\n        org_list = Organization.objects.filter(\n            member_set__role__in=[x.id for x in roles.with_scope(\"org:admin\")],\n            member_set__user=user,\n            status=OrganizationStatus.VISIBLE,\n        )\n\n        org_results = []\n        for org in org_list:\n            org_results.append({\"organization\": org, \"single_owner\": org.has_single_owner()})\n\n        avail_org_slugs = {o[\"organization\"].slug for o in org_results}\n        orgs_to_remove = set(serializer.validated_data.get(\"organizations\")).intersection(\n            avail_org_slugs\n        )\n\n        for result in org_results:\n            if result[\"single_owner\"]:\n                orgs_to_remove.add(result[\"organization\"].slug)\n\n        for org_slug in orgs_to_remove:\n            client.delete(path=f\"/organizations/{org_slug}/\", request=request, is_sudo=True)\n\n        remaining_org_ids = [\n            o.id for o in org_list if o.slug in avail_org_slugs.difference(orgs_to_remove)\n        ]\n\n        if remaining_org_ids:\n            OrganizationMember.objects.filter(\n                organization__in=remaining_org_ids, user=user\n            ).delete()\n\n        logging_data = {\n            \"actor_id\": request.user.id,\n            \"ip_address\": request.META[\"REMOTE_ADDR\"],\n            \"user_id\": user.id,\n        }\n\n        hard_delete = serializer.validated_data.get(\"hardDelete\", False)\n\n        # Only active superusers can hard delete accounts\n        if hard_delete and not is_active_superuser(request):\n            return Response(\n                {\"detail\": \"Only superusers may hard delete a user account\"},\n                status=status.HTTP_403_FORBIDDEN,\n            )\n\n        is_current_user = request.user.id == user.id\n\n        if hard_delete:\n            user.delete()\n            delete_logger.info(\"user.removed\", extra=logging_data)\n        else:\n            User.objects.filter(id=user.id).update(is_active=False)\n            delete_logger.info(\"user.deactivate\", extra=logging_data)\n\n        # if the user deleted their own account log them out\n        if is_current_user:\n            logout(request)\n\n        return Response(status=status.HTTP_204_NO_CONTENT)\n", "path": "src/sentry/api/endpoints/user_details.py"}], "after_files": [{"content": "import logging\nfrom datetime import datetime\n\nimport pytz\nfrom django.conf import settings\nfrom django.contrib.auth import logout\nfrom django.db import transaction\nfrom django.utils.translation import ugettext_lazy as _\nfrom rest_framework import serializers, status\nfrom rest_framework.response import Response\n\nfrom sentry import roles\nfrom sentry.api import client\nfrom sentry.api.bases.user import UserEndpoint\nfrom sentry.api.decorators import sudo_required\nfrom sentry.api.serializers import serialize\nfrom sentry.api.serializers.models.user import DetailedUserSerializer\nfrom sentry.api.serializers.rest_framework import ListField\nfrom sentry.auth.superuser import is_active_superuser\nfrom sentry.constants import LANGUAGES\nfrom sentry.models import Organization, OrganizationMember, OrganizationStatus, User, UserOption\n\naudit_logger = logging.getLogger(\"sentry.audit.user\")\ndelete_logger = logging.getLogger(\"sentry.deletions.api\")\n\n\ndef _get_timezone_choices():\n    results = []\n    for tz in pytz.all_timezones:\n        now = datetime.now(pytz.timezone(tz))\n        offset = now.strftime(\"%z\")\n        results.append((int(offset), tz, f\"(UTC{offset}) {tz}\"))\n    results.sort()\n\n    for i in range(len(results)):\n        results[i] = results[i][1:]\n    return results\n\n\nTIMEZONE_CHOICES = _get_timezone_choices()\n\n\nclass UserOptionsSerializer(serializers.Serializer):\n    language = serializers.ChoiceField(choices=LANGUAGES, required=False)\n    stacktraceOrder = serializers.ChoiceField(\n        choices=(\n            (\"-1\", _(\"Default (let Sentry decide)\")),\n            (\"1\", _(\"Most recent call last\")),\n            (\"2\", _(\"Most recent call first\")),\n        ),\n        required=False,\n    )\n    timezone = serializers.ChoiceField(choices=TIMEZONE_CHOICES, required=False)\n    clock24Hours = serializers.BooleanField(required=False)\n    theme = serializers.ChoiceField(\n        choices=(\n            (\"light\", _(\"Light\")),\n            (\"dark\", _(\"Dark\")),\n            (\"system\", _(\"Default to system\")),\n        ),\n        required=False,\n    )\n\n\nclass BaseUserSerializer(serializers.ModelSerializer):\n    def validate_username(self, value):\n        if User.objects.filter(username__iexact=value).exclude(id=self.instance.id).exists():\n            raise serializers.ValidationError(\"That username is already in use.\")\n        return value\n\n    def validate(self, attrs):\n        attrs = super().validate(attrs)\n\n        if self.instance.email == self.instance.username:\n            if attrs.get(\"username\", self.instance.email) != self.instance.email:\n                # ... this probably needs to handle newsletters and such?\n                attrs.setdefault(\"email\", attrs[\"username\"])\n\n        return attrs\n\n    def update(self, instance, validated_data):\n        if \"isActive\" not in validated_data:\n            validated_data[\"isActive\"] = instance.is_active\n        return super().update(instance, validated_data)\n\n\nclass UserSerializer(BaseUserSerializer):\n    class Meta:\n        model = User\n        fields = (\"name\", \"username\")\n\n    def validate(self, attrs):\n        for field in settings.SENTRY_MANAGED_USER_FIELDS:\n            attrs.pop(field, None)\n\n        return super().validate(attrs)\n\n\nclass PrivilegedUserSerializer(BaseUserSerializer):\n    isActive = serializers.BooleanField(source=\"is_active\")\n    isStaff = serializers.BooleanField(source=\"is_staff\")\n    isSuperuser = serializers.BooleanField(source=\"is_superuser\")\n\n    class Meta:\n        model = User\n        # no idea wtf is up with django rest framework, but we need is_active\n        # and isActive\n        fields = (\"name\", \"username\", \"isActive\", \"isStaff\", \"isSuperuser\")\n\n\nclass DeleteUserSerializer(serializers.Serializer):\n    organizations = ListField(child=serializers.CharField(required=False), required=True)\n    hardDelete = serializers.BooleanField(required=False)\n\n\nclass UserDetailsEndpoint(UserEndpoint):\n    def get(self, request, user):\n        \"\"\"\n        Retrieve User Details\n        `````````````````````\n\n        Return details for an account's details and options such as: full name, timezone, 24hr times, language,\n        stacktrace_order.\n\n        :auth: required\n        \"\"\"\n        return Response(serialize(user, request.user, DetailedUserSerializer()))\n\n    def put(self, request, user):\n        \"\"\"\n        Update Account Appearance options\n        `````````````````````````````````\n\n        Update account appearance options. Only supplied values are updated.\n\n        :pparam string user_id: user id\n        :param string language: language preference\n        :param string stacktrace_order: One of -1 (default), 1 (most recent call last), 2 (most recent call first).\n        :param string timezone: timezone option\n        :param clock_24_hours boolean: use 24 hour clock\n        :param string theme: UI theme, either \"light\", \"dark\", or \"system\"\n        :auth: required\n        \"\"\"\n\n        if is_active_superuser(request) and request.access.has_permission(\"users.admin\"):\n            serializer_cls = PrivilegedUserSerializer\n        else:\n            serializer_cls = UserSerializer\n        serializer = serializer_cls(user, data=request.data, partial=True)\n\n        serializer_options = UserOptionsSerializer(\n            data=request.data.get(\"options\", {}), partial=True\n        )\n\n        # This serializer should NOT include privileged fields e.g. password\n        if not serializer.is_valid() or not serializer_options.is_valid():\n            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)\n\n        # map API keys to keys in model\n        key_map = {\n            \"theme\": \"theme\",\n            \"language\": \"language\",\n            \"timezone\": \"timezone\",\n            \"stacktraceOrder\": \"stacktrace_order\",\n            \"clock24Hours\": \"clock_24_hours\",\n        }\n\n        options_result = serializer_options.validated_data\n\n        for key in key_map:\n            if key in options_result:\n                UserOption.objects.set_value(\n                    user=user, key=key_map.get(key, key), value=options_result.get(key)\n                )\n\n        with transaction.atomic():\n            user = serializer.save()\n\n            if any(k in request.data for k in (\"isStaff\", \"isSuperuser\", \"isActive\")):\n                audit_logger.info(\n                    \"user.edit\",\n                    extra={\n                        \"user_id\": user.id,\n                        \"actor_id\": request.user.id,\n                        \"form_data\": request.data,\n                    },\n                )\n\n        return Response(serialize(user, request.user, DetailedUserSerializer()))\n\n    @sudo_required\n    def delete(self, request, user):\n        \"\"\"\n        Delete User Account\n\n        Also removes organizations if they are an owner\n        :pparam string user_id: user id\n        :param boolean hard_delete: Completely remove the user from the database (requires super user)\n        :param list organizations: List of organization ids to remove\n        :auth required:\n        \"\"\"\n\n        serializer = DeleteUserSerializer(data=request.data)\n\n        if not serializer.is_valid():\n            return Response(status=status.HTTP_400_BAD_REQUEST)\n\n        # from `frontend/remove_account.py`\n        org_list = Organization.objects.filter(\n            member_set__role__in=[x.id for x in roles.with_scope(\"org:admin\")],\n            member_set__user=user,\n            status=OrganizationStatus.VISIBLE,\n        )\n\n        org_results = []\n        for org in org_list:\n            org_results.append({\"organization\": org, \"single_owner\": org.has_single_owner()})\n\n        avail_org_slugs = {o[\"organization\"].slug for o in org_results}\n        orgs_to_remove = set(serializer.validated_data.get(\"organizations\")).intersection(\n            avail_org_slugs\n        )\n\n        for result in org_results:\n            if result[\"single_owner\"]:\n                orgs_to_remove.add(result[\"organization\"].slug)\n\n        for org_slug in orgs_to_remove:\n            client.delete(path=f\"/organizations/{org_slug}/\", request=request, is_sudo=True)\n\n        remaining_org_ids = [\n            o.id for o in org_list if o.slug in avail_org_slugs.difference(orgs_to_remove)\n        ]\n\n        if remaining_org_ids:\n            OrganizationMember.objects.filter(\n                organization__in=remaining_org_ids, user=user\n            ).delete()\n\n        logging_data = {\n            \"actor_id\": request.user.id,\n            \"ip_address\": request.META[\"REMOTE_ADDR\"],\n            \"user_id\": user.id,\n        }\n\n        hard_delete = serializer.validated_data.get(\"hardDelete\", False)\n\n        # Only active superusers can hard delete accounts\n        if hard_delete and not is_active_superuser(request):\n            return Response(\n                {\"detail\": \"Only superusers may hard delete a user account\"},\n                status=status.HTTP_403_FORBIDDEN,\n            )\n\n        is_current_user = request.user.id == user.id\n\n        if hard_delete:\n            user.delete()\n            delete_logger.info(\"user.removed\", extra=logging_data)\n        else:\n            User.objects.filter(id=user.id).update(is_active=False)\n            delete_logger.info(\"user.deactivate\", extra=logging_data)\n\n        # if the user deleted their own account log them out\n        if is_current_user:\n            logout(request)\n\n        return Response(status=status.HTTP_204_NO_CONTENT)\n", "path": "src/sentry/api/endpoints/user_details.py"}]}