Skip to content

Commit 8761f03

Browse files
fix(alerts): Make clear that superuser may edit comments (#18455)
1 parent cd37111 commit 8761f03

File tree

3 files changed

+91
-22
lines changed

3 files changed

+91
-22
lines changed

src/sentry/api/endpoints/organization_incident_comment_details.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,15 @@ def convert_args(self, request, activity_id, *args, **kwargs):
2828
args, kwargs = super(CommentDetailsEndpoint, self).convert_args(request, *args, **kwargs)
2929

3030
try:
31+
# Superusers may mutate any comment
32+
user_filter = {} if request.user.is_superuser else {"user": request.user}
33+
3134
kwargs["activity"] = IncidentActivity.objects.get(
3235
id=activity_id,
33-
user=request.user,
3436
incident=kwargs["incident"],
3537
# Only allow modifying comments
3638
type=IncidentActivityType.COMMENT.value,
39+
**user_filter
3740
)
3841
except IncidentActivity.DoesNotExist:
3942
raise ResourceDoesNotExist

src/sentry/static/sentry/app/components/activity/note/header.tsx

Lines changed: 32 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import ConfigStore from 'app/stores/configStore';
77
import LinkWithConfirmation from 'app/components/links/linkWithConfirmation';
88
import {User} from 'app/types';
99
import {Theme} from 'app/utils/theme';
10+
import Tooltip from 'app/components/tooltip';
1011

1112
import EditorTools from './editorTools';
1213

@@ -17,28 +18,38 @@ type Props = {
1718
onDelete: () => void;
1819
};
1920

20-
function canEdit(editingUser: User) {
21-
const user = ConfigStore.get('user');
22-
return user && (user.isSuperuser || user.id === editingUser.id);
23-
}
21+
const NoteHeader = ({authorName, user, onEdit, onDelete}: Props) => {
22+
const activeUser = ConfigStore.get('user');
23+
const canEdit = activeUser && (activeUser.isSuperuser || user.id === activeUser.id);
2424

25-
const NoteHeader = ({authorName, user, onEdit, onDelete}: Props) => (
26-
<div>
27-
<ActivityAuthor>{authorName}</ActivityAuthor>
28-
{canEdit(user) && (
29-
<EditorTools>
30-
<Edit onClick={onEdit}>{t('Edit')}</Edit>
31-
<LinkWithConfirmation
32-
title={t('Remove')}
33-
message={t('Are you sure you wish to delete this comment?')}
34-
onConfirm={onDelete}
35-
>
36-
<Remove>{t('Remove')}</Remove>
37-
</LinkWithConfirmation>
38-
</EditorTools>
39-
)}
40-
</div>
41-
);
25+
return (
26+
<div>
27+
<ActivityAuthor>{authorName}</ActivityAuthor>
28+
{canEdit && (
29+
<EditorTools>
30+
<Tooltip
31+
title={t('You can edit this comment due to your superuser status')}
32+
disabled={!activeUser.isSuperuser}
33+
>
34+
<Edit onClick={onEdit}>{t('Edit')}</Edit>
35+
</Tooltip>
36+
<Tooltip
37+
title={t('You can delete this comment due to your superuser status')}
38+
disabled={!activeUser.isSuperuser}
39+
>
40+
<LinkWithConfirmation
41+
title={t('Remove')}
42+
message={t('Are you sure you wish to delete this comment?')}
43+
onConfirm={onDelete}
44+
>
45+
<Remove>{t('Remove')}</Remove>
46+
</LinkWithConfirmation>
47+
</Tooltip>
48+
</EditorTools>
49+
)}
50+
</div>
51+
);
52+
};
4253

4354
const getActionStyle = (p: {theme: Theme}) => `
4455
padding: 0 7px;

tests/sentry/api/endpoints/test_organization_incident_comment_details.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ def setUp(self):
1919
self.incident, user=self.user, type=IncidentActivityType.DETECTED.value
2020
)
2121

22+
user2 = self.create_user()
23+
self.user2_activity = self.create_incident_comment(
24+
incident=self.incident, user=user2, comment="hello from another user"
25+
)
26+
2227
@fixture
2328
def organization(self):
2429
return self.create_organization()
@@ -76,6 +81,34 @@ def test_simple(self):
7681
assert activity.user == self.user
7782
assert activity.comment == comment
7883

84+
def test_cannot_edit_others_comment(self):
85+
with self.feature("organizations:incidents"):
86+
self.get_valid_response(
87+
self.organization.slug,
88+
self.incident.identifier,
89+
self.user2_activity.id,
90+
comment="edited comment",
91+
status_code=404,
92+
)
93+
94+
def test_superuser_can_edit(self):
95+
self.user.is_superuser = True
96+
self.user.save()
97+
98+
edited_comment = "this comment has been edited"
99+
100+
with self.feature("organizations:incidents"):
101+
self.get_valid_response(
102+
self.organization.slug,
103+
self.incident.identifier,
104+
self.user2_activity.id,
105+
comment=edited_comment,
106+
status_code=200,
107+
)
108+
activity = IncidentActivity.objects.get(id=self.user2_activity.id)
109+
assert activity.user != self.user
110+
assert activity.comment == edited_comment
111+
79112

80113
class OrganizationIncidentCommentDeleteEndpointTest(BaseIncidentCommentDetailsTest, APITestCase):
81114
method = "delete"
@@ -86,3 +119,25 @@ def test_simple(self):
86119
self.organization.slug, self.incident.identifier, self.activity.id, status_code=204
87120
)
88121
assert not IncidentActivity.objects.filter(id=self.activity.id).exists()
122+
123+
def test_cannot_delete_others_comments(self):
124+
with self.feature("organizations:incidents"):
125+
self.get_valid_response(
126+
self.organization.slug,
127+
self.incident.identifier,
128+
self.user2_activity.id,
129+
status_code=404,
130+
)
131+
132+
def test_superuser_can_delete(self):
133+
self.user.is_superuser = True
134+
self.user.save()
135+
136+
with self.feature("organizations:incidents"):
137+
self.get_valid_response(
138+
self.organization.slug,
139+
self.incident.identifier,
140+
self.user2_activity.id,
141+
status_code=204,
142+
)
143+
assert not IncidentActivity.objects.filter(id=self.user2_activity.id).exists()

0 commit comments

Comments
 (0)