Skip to content

Commit dff34e4

Browse files
leedongweipriscilawebdev
authored andcommitted
feat(project-creation): Backend for disableMemberProjectCreation flag (#62294)
1 parent 294d3dd commit dff34e4

File tree

6 files changed

+69
-1
lines changed

6 files changed

+69
-1
lines changed

src/sentry/api/endpoints/organization_details.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,7 @@ class OrganizationSerializer(BaseOrganizationSerializer):
232232

233233
openMembership = serializers.BooleanField(required=False)
234234
allowSharedIssues = serializers.BooleanField(required=False)
235+
allowMemberProjectCreation = serializers.BooleanField(required=False)
235236
enhancedPrivacy = serializers.BooleanField(required=False)
236237
dataScrubber = serializers.BooleanField(required=False)
237238
dataScrubberDefaults = serializers.BooleanField(required=False)
@@ -476,6 +477,8 @@ def save(self):
476477
and "requireEmailVerification" in data
477478
):
478479
org.flags.require_email_verification = data["requireEmailVerification"]
480+
if "allowMemberProjectCreation" in data:
481+
org.flags.disable_member_project_creation = not data["allowMemberProjectCreation"]
479482
if "name" in data:
480483
org.name = data["name"]
481484
if "slug" in data:
@@ -492,6 +495,7 @@ def save(self):
492495
"early_adopter": org.flags.early_adopter.is_set,
493496
"require_2fa": org.flags.require_2fa.is_set,
494497
"codecov_access": org.flags.codecov_access.is_set,
498+
"disable_member_project_creation": org.flags.disable_member_project_creation.is_set,
495499
},
496500
}
497501

src/sentry/api/endpoints/organization_projects_experiment.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
CONFLICTING_TEAM_SLUG_ERROR = "A team with this slug already exists."
3030
MISSING_PERMISSION_ERROR_STRING = "You do not have permission to join a new team as a Team Admin."
31+
DISABLED_FEATURE_ERROR_STRING = "Your organization has disabled this feature for members."
3132

3233

3334
def _generate_suffix() -> str:
@@ -89,6 +90,10 @@ def post(self, request: Request, organization: Organization) -> Response:
8990

9091
if not features.has("organizations:team-roles", organization):
9192
raise ResourceDoesNotExist(detail=MISSING_PERMISSION_ERROR_STRING)
93+
if organization.flags.disable_member_project_creation and not request.access.has_scope(
94+
"org:write"
95+
):
96+
raise PermissionDenied(detail=DISABLED_FEATURE_ERROR_STRING)
9297

9398
# parse the email to retrieve the username before the "@"
9499
parsed_email = fetch_slugifed_email_username(request.user.email)

src/sentry/api/endpoints/team_projects.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,10 +159,19 @@ def post(self, request: Request, team) -> Response:
159159
"""
160160
Create a new project bound to a team.
161161
"""
162+
from sentry.api.endpoints.organization_projects_experiment import (
163+
DISABLED_FEATURE_ERROR_STRING,
164+
)
165+
162166
serializer = ProjectPostSerializer(data=request.data)
163167

164168
if not serializer.is_valid():
165169
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
170+
if (
171+
team.organization.flags.disable_member_project_creation
172+
and not request.access.has_scope("org:write")
173+
):
174+
return Response({"detail": DISABLED_FEATURE_ERROR_STRING}, status=403)
166175

167176
result = serializer.validated_data
168177
with transaction.atomic(router.db_for_write(Project)):

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ def serialize(
341341
and obj.flags.require_email_verification
342342
),
343343
"avatar": avatar,
344+
"allowMemberProjectCreation": not obj.flags.disable_member_project_creation,
344345
"links": {
345346
"organizationUrl": generate_organization_url(obj.slug),
346347
"regionUrl": generate_region_url(),

tests/sentry/api/endpoints/test_organization_projects_experiment.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from django.utils.text import slugify
77

88
from sentry.api.endpoints.organization_projects_experiment import (
9+
DISABLED_FEATURE_ERROR_STRING,
910
OrganizationProjectsExperimentEndpoint,
1011
fetch_slugifed_email_username,
1112
)
@@ -265,3 +266,25 @@ def get_callthrough(*args, **kwargs):
265266
"detail": "You must be a member of the organization to join a new team as a Team Admin",
266267
}
267268
assert Team.objects.count() == prior_team_count
269+
270+
@with_feature(["organizations:team-roles"])
271+
def test_disable_member_project_creation(self):
272+
test_org = self.create_organization(flags=256)
273+
274+
test_member = self.create_user(is_superuser=False)
275+
self.create_member(user=test_member, organization=test_org, role="member", teams=[])
276+
self.login_as(user=test_member)
277+
response = self.get_error_response(
278+
test_org.slug,
279+
name="foo",
280+
status_code=403,
281+
)
282+
assert response.data["detail"] == DISABLED_FEATURE_ERROR_STRING
283+
test_manager = self.create_user(is_superuser=False)
284+
self.create_member(user=test_manager, organization=test_org, role="manager", teams=[])
285+
self.login_as(user=test_manager)
286+
self.get_success_response(
287+
test_org.slug,
288+
name="foo",
289+
status_code=201,
290+
)

tests/sentry/api/endpoints/test_team_projects.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from unittest.mock import Mock
22

3+
from sentry.api.endpoints.organization_projects_experiment import DISABLED_FEATURE_ERROR_STRING
34
from sentry.ingest import inbound_filters
45
from sentry.models.project import Project
56
from sentry.models.rule import Rule
@@ -129,7 +130,7 @@ def test_default_rules(self):
129130
assert signal_handler.call_count == 0
130131
alert_rule_created.disconnect(signal_handler)
131132

132-
def test_without_default_rules(self):
133+
def test_without_default_rules_disable_member_project_creation(self):
133134
response = self.get_success_response(
134135
self.organization.slug,
135136
self.team.slug,
@@ -140,6 +141,31 @@ def test_without_default_rules(self):
140141
project = Project.objects.get(id=response.data["id"])
141142
assert not Rule.objects.filter(project=project).exists()
142143

144+
def test_disable_member_project_creation(self):
145+
test_org = self.create_organization(flags=256)
146+
test_team = self.create_team(organization=test_org)
147+
148+
test_member = self.create_user(is_superuser=False)
149+
self.create_member(user=test_member, organization=test_org, role="admin", teams=[test_team])
150+
self.login_as(user=test_member)
151+
response = self.get_error_response(
152+
test_org.slug,
153+
test_team.slug,
154+
**self.data,
155+
status_code=403,
156+
)
157+
assert response.data["detail"] == DISABLED_FEATURE_ERROR_STRING
158+
159+
test_manager = self.create_user(is_superuser=False)
160+
self.create_member(user=test_manager, organization=test_org, role="manager", teams=[])
161+
self.login_as(user=test_manager)
162+
self.get_success_response(
163+
test_org.slug,
164+
test_team.slug,
165+
**self.data,
166+
status_code=201,
167+
)
168+
143169
def test_default_inbound_filters(self):
144170
filters = ["browser-extensions", "legacy-browsers", "web-crawlers", "filtered-transaction"]
145171
python_response = self.get_success_response(

0 commit comments

Comments
 (0)