Skip to content

Commit 679803a

Browse files
committed
Merge branch 'master' into feat/apm-v2
* master: ref(admin): Convert user edit page to react (#14074) ref: Remove unused Group.get_oldest_event and legacy events behavior (#14038) ref(api): Update DELETE users/ to support hard deleting (#14068) test(OrganizationDiscoverSavedQueryDetailTest): Stabilize put test (#14077) meta(readme): Sentry logo should link to sentry.io (#14076) ref: Remove duplicate column (#14073) App platform/update permissions token auth (#14046) feat: Support issue IDs as canonical parameters ref: Change to new traceparent header for Python SDK (#14070) feat: Use option to force-disable transaction events (#14056) feat(apm): Register option to force-disable transaction events (#14055) Feat/mark sentry app installed put route (#14060) ref: Remove unused Group.event_set property (#14036) fix: Filter out groups that are pending deletion/merge from `by_qualified_short_id` (SEN-849) fix(ui): Fix resolve/ignore actions for accounts without multi… (#14058) Fix: Remove extra $.param introduced in GH-14051 (#14061) feat: Use Snuba for Group.from_event_id (#14034) fix(ui) Display implicit default sort and default to descending (#14042) fix(github) Fix 404s not being handled in repository search (#14030) fix: Pass an empty array to $.param instead of an empty string when options.query is falsey (#14051) # Conflicts: # src/sentry/utils/sdk.py
2 parents cc319c9 + a031b32 commit 679803a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

49 files changed

+2308
-482
lines changed

README.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
<p align="center">
44
<p align="center">
5-
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" alt="Sentry" height="72"
5+
<a href="https://sentry.io/?utm_source=github&utm_medium=logo" target="_blank">
6+
<img src="https://sentry-brand.storage.googleapis.com/sentry-logo-black.png" alt="Sentry" height="72">
7+
</a>
68
</p>
79
<p align="center">
810
Users and logs provide clues. Sentry provides answers.
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from __future__ import absolute_import
2+
3+
from sentry import analytics
4+
5+
6+
class SentryAppInstallationUpdatedEvent(analytics.Event):
7+
type = 'sentry_app_installation.updated'
8+
9+
attributes = (
10+
analytics.Attribute('sentry_app_installation_id'),
11+
analytics.Attribute('sentry_app_id'),
12+
analytics.Attribute('organization_id'),
13+
)
14+
15+
16+
analytics.register(SentryAppInstallationUpdatedEvent)

src/sentry/api/bases/sentryapps.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,16 @@ def wrapped(self, *args, **kwargs):
4545

4646
class SentryAppsPermission(SentryPermission):
4747
scope_map = {
48-
'GET': (), # Public endpoint.
48+
# GET is ideally a public endpoint but for now we are allowing for
49+
# anyone who has member permissions or above.
50+
'GET': ('event:read',
51+
'event:write',
52+
'event:admin',
53+
'project:releases',
54+
'project:read',
55+
'org:read',
56+
'member:read',
57+
'team:read',),
4958
'POST': ('org:read', 'org:integrations', 'org:write', 'org:admin'),
5059
}
5160

@@ -118,12 +127,25 @@ class SentryAppPermission(SentryPermission):
118127
}
119128

120129
published_scope_map = {
121-
'GET': (), # Public endpoint.
130+
# GET is ideally a public endpoint but for now we are allowing for
131+
# anyone who has member permissions or above.
132+
'GET': ('event:read',
133+
'event:write',
134+
'event:admin',
135+
'project:releases',
136+
'project:read',
137+
'org:read',
138+
'member:read',
139+
'team:read',),
122140
'PUT': ('org:write', 'org:admin'),
123141
'POST': ('org:write', 'org:admin'),
124142
'DELETE': ('org:admin'),
125143
}
126144

145+
@property
146+
def scope_map(self):
147+
return self.published_scope_map
148+
127149
def has_object_permission(self, request, view, sentry_app):
128150
if not hasattr(request, 'user') or not request.user:
129151
return False
@@ -227,6 +249,12 @@ class SentryAppInstallationPermission(SentryPermission):
227249
'POST': ('org:integrations', 'event:write', 'event:admin'),
228250
}
229251

252+
def has_permission(self, request, *args, **kwargs):
253+
# To let the app mark the installation as installed, we don't care about permissions
254+
if request.user.is_sentry_app and request.method == 'PUT':
255+
return True
256+
return super(SentryAppInstallationPermission, self).has_permission(request, *args, **kwargs)
257+
230258
def has_object_permission(self, request, view, installation):
231259
if not hasattr(request, 'user') or not request.user:
232260
return False
@@ -236,6 +264,10 @@ def has_object_permission(self, request, view, installation):
236264
if is_active_superuser(request):
237265
return True
238266

267+
# if user is an app, make sure it's for that same app
268+
if request.user.is_sentry_app:
269+
return request.user == installation.sentry_app.proxy_user
270+
239271
if installation.organization not in request.user.get_orgs():
240272
raise Http404
241273

src/sentry/api/endpoints/sentry_app_installation_details.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55
from sentry.api.bases import SentryAppInstallationBaseEndpoint
66
from sentry.api.serializers import serialize
7-
from sentry.mediators.sentry_app_installations import Destroyer
7+
from sentry.mediators.sentry_app_installations import Destroyer, Updater
8+
from sentry.api.serializers.rest_framework import SentryAppInstallationSerializer
89

910

1011
class SentryAppInstallationDetailsEndpoint(SentryAppInstallationBaseEndpoint):
@@ -19,3 +20,22 @@ def delete(self, request, installation):
1920
request=request,
2021
)
2122
return Response(status=204)
23+
24+
def put(self, request, installation):
25+
serializer = SentryAppInstallationSerializer(
26+
installation,
27+
data=request.data,
28+
partial=True,
29+
)
30+
31+
if serializer.is_valid():
32+
result = serializer.validated_data
33+
34+
updated_installation = Updater.run(
35+
user=request.user,
36+
sentry_app_installation=installation,
37+
status=result.get('status'),
38+
)
39+
40+
return Response(serialize(updated_installation, request.user))
41+
return Response(serializer.errors, status=400)

src/sentry/api/endpoints/user_details.py

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -86,19 +86,22 @@ def validate(self, attrs):
8686
return super(UserSerializer, self).validate(attrs)
8787

8888

89-
class AdminUserSerializer(BaseUserSerializer):
89+
class SuperuserUserSerializer(BaseUserSerializer):
9090
isActive = serializers.BooleanField(source='is_active')
91+
isStaff = serializers.BooleanField(source='is_staff')
92+
isSuperuser = serializers.BooleanField(source='is_superuser')
9193

9294
class Meta:
9395
model = User
9496
# no idea wtf is up with django rest framework, but we need is_active
9597
# and isActive
96-
fields = ('name', 'username', 'isActive')
98+
fields = ('name', 'username', 'isActive', 'isStaff', 'isSuperuser')
9799
# write_only_fields = ('password',)
98100

99101

100-
class OrganizationsSerializer(serializers.Serializer):
102+
class DeleteUserSerializer(serializers.Serializer):
101103
organizations = ListField(child=serializers.CharField(required=False), required=True)
104+
hardDelete = serializers.BooleanField(required=False)
102105

103106

104107
class UserDetailsEndpoint(UserEndpoint):
@@ -130,12 +133,13 @@ def put(self, request, user):
130133
"""
131134

132135
if is_active_superuser(request):
133-
serializer_cls = AdminUserSerializer
136+
serializer_cls = SuperuserUserSerializer
134137
else:
135138
serializer_cls = UserSerializer
136139
serializer = serializer_cls(user, data=request.data, partial=True)
137140

138-
serializer_options = UserOptionsSerializer(data=request.data.get('options', {}), partial=True)
141+
serializer_options = UserOptionsSerializer(
142+
data=request.data.get('options', {}), partial=True)
139143

140144
# This serializer should NOT include privileged fields e.g. password
141145
if not serializer.is_valid() or not serializer_options.is_valid():
@@ -170,11 +174,12 @@ def delete(self, request, user):
170174
171175
Also removes organizations if they are an owner
172176
:pparam string user_id: user id
177+
:param boolean hard_delete: Completely remove the user from the database (requires super user)
173178
:param list organizations: List of organization ids to remove
174179
:auth required:
175180
"""
176181

177-
serializer = OrganizationsSerializer(data=request.data)
182+
serializer = DeleteUserSerializer(data=request.data)
178183

179184
if not serializer.is_valid():
180185
return Response(status=status.HTTP_400_BAD_REQUEST)
@@ -194,20 +199,13 @@ def delete(self, request, user):
194199
})
195200

196201
avail_org_slugs = set([o['organization'].slug for o in org_results])
197-
orgs_to_remove = set(serializer.validated_data.get('organizations')).intersection(avail_org_slugs)
202+
orgs_to_remove = set(serializer.validated_data.get(
203+
'organizations')).intersection(avail_org_slugs)
198204

199205
for result in org_results:
200206
if result['single_owner']:
201207
orgs_to_remove.add(result['organization'].slug)
202208

203-
delete_logger.info(
204-
'user.deactivate',
205-
extra={
206-
'actor_id': request.user.id,
207-
'ip_address': request.META['REMOTE_ADDR'],
208-
}
209-
)
210-
211209
for org_slug in orgs_to_remove:
212210
client.delete(
213211
path=u'/organizations/{}/'.format(org_slug),
@@ -221,15 +219,33 @@ def delete(self, request, user):
221219
if remaining_org_ids:
222220
OrganizationMember.objects.filter(
223221
organization__in=remaining_org_ids,
224-
user=request.user,
222+
user=user,
225223
).delete()
226224

227-
User.objects.filter(
228-
id=request.user.id,
229-
).update(
230-
is_active=False,
231-
)
225+
logging_data = {
226+
'actor_id': request.user.id,
227+
'ip_address': request.META['REMOTE_ADDR'],
228+
}
229+
230+
hard_delete = serializer.validated_data.get('hardDelete', False)
231+
232+
# Only active superusers can hard delete accounts
233+
if hard_delete and not is_active_superuser(request):
234+
return Response(
235+
{'detail': 'Only superusers may hard delete a user account'},
236+
status=status.HTTP_403_FORBIDDEN)
237+
238+
is_current_user = request.user.id == user.id
239+
240+
if hard_delete:
241+
user.delete()
242+
delete_logger.info('user.removed', extra=logging_data)
243+
else:
244+
User.objects.filter(id=user.id).update(is_active=False)
245+
delete_logger.info('user.deactivate', extra=logging_data)
232246

233-
logout(request)
247+
# if the user deleted their own account log them out
248+
if is_current_user:
249+
logout(request)
234250

235251
return Response(status=status.HTTP_204_NO_CONTENT)

src/sentry/api/serializers/models/sentry_app_installation.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
from sentry.api.serializers import Serializer, register
55
from sentry.models import SentryAppInstallation
6+
from sentry.constants import SentryAppInstallationStatus
67

78

89
@register(SentryAppInstallation)
@@ -17,6 +18,7 @@ def serialize(self, install, attrs, user):
1718
'slug': install.organization.slug,
1819
},
1920
'uuid': install.uuid,
21+
'status': SentryAppInstallationStatus.as_str(install.status),
2022
}
2123

2224
if install.api_grant:

src/sentry/api/serializers/models/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def serialize(self, obj, attrs, user):
7878
'has2fa': attrs['has2fa'],
7979
'lastActive': obj.last_active,
8080
'isSuperuser': obj.is_superuser,
81+
'isStaff': obj.is_staff,
8182
}
8283

8384
if obj == user:
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from __future__ import absolute_import
2+
3+
4+
from rest_framework import serializers
5+
from rest_framework.serializers import Serializer, ValidationError
6+
from sentry.constants import SentryAppInstallationStatus
7+
8+
9+
class SentryAppInstallationSerializer(Serializer):
10+
status = serializers.CharField()
11+
12+
def validate_status(self, new_status):
13+
# can only set status to installed
14+
if new_status != SentryAppInstallationStatus.INSTALLED_STR:
15+
raise ValidationError(
16+
u"Invalid value '{}' for status. Valid values: '{}'".format(
17+
new_status, SentryAppInstallationStatus.INSTALLED_STR))
18+
19+
return new_status

0 commit comments

Comments
 (0)