Skip to content

Commit c9a1a54

Browse files
dcramermattrobenolt
authored andcommitted
Merge pull request #2540 from getsentry/access-v-membership
Vary access and membership for teams
1 parent 0570193 commit c9a1a54

File tree

7 files changed

+103
-34
lines changed

7 files changed

+103
-34
lines changed

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

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import itertools
44

55
from collections import defaultdict
6-
from django.conf import settings
76

87
from sentry.app import env
98
from sentry.api.serializers import Serializer, register, serialize
@@ -17,19 +16,7 @@
1716
class TeamSerializer(Serializer):
1817
def get_attrs(self, item_list, user):
1918
request = env.request
20-
if (request.is_superuser() and request.user == user) or settings.SENTRY_PUBLIC:
21-
inactive_memberships = frozenset(
22-
OrganizationMemberTeam.objects.filter(
23-
team__in=item_list,
24-
organizationmember__user=user,
25-
is_active=False,
26-
).values_list('team', flat=True)
27-
)
28-
memberships = frozenset([
29-
t.id for t in item_list
30-
if t.id not in inactive_memberships
31-
])
32-
elif user.is_authenticated():
19+
if user.is_authenticated():
3320
memberships = frozenset(
3421
OrganizationMemberTeam.objects.filter(
3522
organizationmember__user=user,
@@ -50,11 +37,22 @@ def get_attrs(self, item_list, user):
5037
else:
5138
access_requests = frozenset()
5239

40+
is_superuser = request.is_superuser() and request.user == user
5341
result = {}
5442
for team in item_list:
43+
is_member = team.id in memberships
44+
if is_member:
45+
has_access = True
46+
elif is_superuser:
47+
has_access = True
48+
elif team.organization.flags.allow_joinleave:
49+
has_access = True
50+
else:
51+
has_access = False
5552
result[team] = {
5653
'pending_request': team.id in access_requests,
57-
'is_member': team.id in memberships,
54+
'is_member': is_member,
55+
'has_access': has_access,
5856
}
5957
return result
6058

@@ -65,6 +63,7 @@ def serialize(self, obj, attrs, user):
6563
'name': obj.name,
6664
'dateCreated': obj.date_added,
6765
'isMember': attrs['is_member'],
66+
'hasAccess': attrs['has_access'],
6867
'isPending': attrs['pending_request'],
6968
}
7069

src/sentry/auth/access.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
__all__ = ['from_user', 'from_member', 'DEFAULT']
44

5+
import warnings
6+
57
from django.conf import settings
68

79
from sentry.models import AuthIdentity, AuthProvider, OrganizationMember
@@ -10,7 +12,10 @@
1012
class BaseAccess(object):
1113
is_active = False
1214
sso_is_valid = False
15+
# teams with valid access
1316
teams = ()
17+
# teams with valid membership
18+
memberships = ()
1419
scopes = frozenset()
1520

1621
def has_scope(self, scope):
@@ -19,12 +24,22 @@ def has_scope(self, scope):
1924
return scope in self.scopes
2025

2126
def has_team(self, team):
27+
warnings.warn('has_team() is deprecated in favor of has_team_access',
28+
DeprecationWarning)
29+
return self.has_team_access(team)
30+
31+
def has_team_access(self, team):
2232
if not self.is_active:
2333
return False
2434
return team in self.teams
2535

36+
def has_team_membership(self, team):
37+
if not self.is_active:
38+
return False
39+
return team in self.memberships
40+
2641
def has_team_scope(self, team, scope):
27-
return self.has_team(team) and self.has_scope(scope)
42+
return self.has_team_access(team) and self.has_scope(scope)
2843

2944
def to_django_context(self):
3045
return {
@@ -37,8 +52,9 @@ class Access(BaseAccess):
3752
# TODO(dcramer): this is still a little gross, and ideally backend access
3853
# would be based on the same scopes as API access so theres clarity in
3954
# what things mean
40-
def __init__(self, scopes, is_active, teams, sso_is_valid):
55+
def __init__(self, scopes, is_active, teams, memberships, sso_is_valid):
4156
self.teams = teams
57+
self.memberships = memberships
4258
self.scopes = scopes
4359

4460
self.is_active = is_active
@@ -50,10 +66,12 @@ def from_request(request, organization):
5066
return DEFAULT
5167

5268
if request.is_superuser():
69+
team_list = list(organization.team_set.all())
5370
return Access(
5471
scopes=settings.SENTRY_SCOPES,
5572
is_active=True,
56-
teams=organization.team_set.all(),
73+
teams=team_list,
74+
memberships=team_list,
5775
sso_is_valid=True,
5876
)
5977
return from_user(request.user, organization)
@@ -74,6 +92,9 @@ def from_user(user, organization):
7492
except OrganizationMember.DoesNotExist:
7593
return DEFAULT
7694

95+
# ensure cached relation
96+
om.organization = organization
97+
7798
return from_member(om)
7899

79100

@@ -100,11 +121,18 @@ def from_member(member):
100121
else:
101122
sso_is_valid = auth_identity.is_valid(member)
102123

124+
team_memberships = member.get_teams()
125+
if member.organization.flags.allow_joinleave:
126+
team_access = list(member.organization.team_set.all())
127+
else:
128+
team_access = team_memberships
129+
103130
return Access(
104131
is_active=True,
105132
sso_is_valid=sso_is_valid,
106133
scopes=member.get_scopes(),
107-
teams=member.get_teams(),
134+
memberships=team_memberships,
135+
teams=team_access,
108136
)
109137

110138

@@ -121,6 +149,10 @@ def is_active(self):
121149
def teams(self):
122150
return ()
123151

152+
@property
153+
def memberships(self):
154+
return ()
155+
124156
@property
125157
def scopes(self):
126158
return frozenset()

src/sentry/static/sentry/app/views/projectDetails.jsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,9 @@ const ProjectDetails = React.createClass({
9191
return;
9292
}
9393
let [activeTeam, activeProject] = this.identifyProject();
94-
let isMember = activeTeam && activeTeam.isMember;
94+
let hasAccess = activeTeam && activeTeam.hasAccess;
9595

96-
if (activeProject && isMember) {
96+
if (activeProject && hasAccess) {
9797
// TODO(dcramer): move member list to organization level
9898
this.api.request(this.getMemberListEndpoint(), {
9999
success: (data) => {
@@ -108,7 +108,7 @@ const ProjectDetails = React.createClass({
108108
error: false,
109109
errorType: null
110110
});
111-
} else if (isMember === false) {
111+
} else if (activeTeam && activeTeam.isMember) {
112112
this.setState({
113113
project: activeProject,
114114
team: activeTeam,

src/sentry/testutils/cases.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,10 @@ class PermissionTestCase(TestCase):
276276
def setUp(self):
277277
super(PermissionTestCase, self).setUp()
278278
self.owner = self.create_user(is_superuser=False)
279-
self.organization = self.create_organization(owner=self.owner)
279+
self.organization = self.create_organization(
280+
owner=self.owner,
281+
flags=0, # disable default allow_joinleave access
282+
)
280283
self.team = self.create_team(organization=self.organization)
281284

282285
def assert_can_access(self, user, path, method='GET'):

tests/sentry/api/endpoints/test_organization_access_request_details.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,14 @@ def test_team_admin_can_approve(self):
112112

113113
assert resp.status_code == 204
114114

115-
def test_teamless_admin_cannot_approve(self):
115+
def test_teamless_admin_cannot_approve_with_closed_membership(self):
116116
self.login_as(user=self.user)
117117

118-
organization = self.create_organization(name='foo', owner=self.user)
118+
organization = self.create_organization(
119+
name='foo',
120+
owner=self.user,
121+
flags=0, # kill allow_joinleave
122+
)
119123
user = self.create_user('[email protected]')
120124
member = self.create_member(
121125
organization=organization,

tests/sentry/auth/test_access.py

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from mock import Mock
55

66
from sentry.auth import access
7-
from sentry.models import AuthProvider
7+
from sentry.models import AuthProvider, Organization
88
from sentry.testutils import TestCase
99

1010

@@ -18,7 +18,8 @@ def test_no_access(self):
1818
assert not result.is_active
1919
assert result.sso_is_valid
2020
assert not result.scopes
21-
assert not result.has_team(team)
21+
assert not result.has_team_access(team)
22+
assert not result.has_team_membership(team)
2223

2324
def test_owner_all_teams(self):
2425
user = self.create_user()
@@ -33,11 +34,15 @@ def test_owner_all_teams(self):
3334
assert result.is_active
3435
assert result.sso_is_valid
3536
assert result.scopes == member.get_scopes()
36-
assert result.has_team(team)
37+
assert result.has_team_access(team)
38+
assert result.has_team_membership(team)
3739

38-
def test_member_no_teams(self):
40+
def test_member_no_teams_closed_membership(self):
3941
user = self.create_user()
40-
organization = self.create_organization(owner=self.user)
42+
organization = self.create_organization(
43+
owner=self.user,
44+
flags=0, # disable default allow_joinleave
45+
)
4146
member = self.create_member(
4247
organization=organization, user=user,
4348
role='member',
@@ -48,7 +53,27 @@ def test_member_no_teams(self):
4853
assert result.is_active
4954
assert result.sso_is_valid
5055
assert result.scopes == member.get_scopes()
51-
assert not result.has_team(team)
56+
assert not result.has_team_access(team)
57+
assert not result.has_team_membership(team)
58+
59+
def test_member_no_teams_open_membership(self):
60+
user = self.create_user()
61+
organization = self.create_organization(
62+
owner=self.user,
63+
flags=Organization.flags.allow_joinleave,
64+
)
65+
member = self.create_member(
66+
organization=organization, user=user,
67+
role='member', teams=(),
68+
)
69+
team = self.create_team(organization=organization)
70+
71+
result = access.from_user(user, organization)
72+
assert result.is_active
73+
assert result.sso_is_valid
74+
assert result.scopes == member.get_scopes()
75+
assert result.has_team_access(team)
76+
assert not result.has_team_membership(team)
5277

5378
def test_team_restricted_org_member_access(self):
5479
user = self.create_user()
@@ -64,7 +89,8 @@ def test_team_restricted_org_member_access(self):
6489
assert result.is_active
6590
assert result.sso_is_valid
6691
assert result.scopes == member.get_scopes()
67-
assert result.has_team(team)
92+
assert result.has_team_access(team)
93+
assert result.has_team_membership(team)
6894

6995
def test_unlinked_sso(self):
7096
user = self.create_user()
@@ -106,4 +132,5 @@ def test_no_access(self):
106132
assert not result.is_active
107133
assert result.sso_is_valid
108134
assert not result.scopes
109-
assert not result.has_team(Mock())
135+
assert not result.has_team_access(Mock())
136+
assert not result.has_team_membership(Mock())

tests/sentry/web/frontend/test_organization_members.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ def test_renders_with_context(self):
5858
]
5959

6060
def test_shows_access_requests_for_team_admin(self):
61-
organization = self.create_organization(name='foo', owner=self.user)
61+
organization = self.create_organization(
62+
name='foo',
63+
owner=self.user,
64+
flags=0, # kill default allow_joinleave
65+
)
6266
team_1 = self.create_team(name='foo', organization=organization)
6367
team_2 = self.create_team(name='bar', organization=organization)
6468

0 commit comments

Comments
 (0)