class ProjectAdminSerializer(ProjectMemberSerializer): name = serializers.CharField(max_length=200) slug = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50) team = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50) digestsMinDelay = serializers.IntegerField(min_value=60, max_value=3600) digestsMaxDelay = serializers.IntegerField(min_value=60, max_value=3600) subjectPrefix = serializers.CharField(max_length=200, allow_blank=True) subjectTemplate = serializers.CharField(max_length=200) securityToken = serializers.RegexField( r"^[-a-zA-Z0-9+/=\s]+$", max_length=255, allow_blank=True ) securityTokenHeader = serializers.RegexField( r"^[a-zA-Z0-9_\-]+$", max_length=20, allow_blank=True ) verifySSL = serializers.BooleanField(required=False) defaultEnvironment = serializers.CharField(required=False, allow_null=True, allow_blank=True) dataScrubber = serializers.BooleanField(required=False) dataScrubberDefaults = serializers.BooleanField(required=False) sensitiveFields = ListField(child=serializers.CharField(), required=False) safeFields = ListField(child=serializers.CharField(), required=False) storeCrashReports = serializers.IntegerField( min_value=-1, max_value=STORE_CRASH_REPORTS_MAX, required=False, allow_null=True ) relayPiiConfig = serializers.CharField(required=False, allow_blank=True, allow_null=True) builtinSymbolSources = ListField(child=serializers.CharField(), required=False) symbolSources = serializers.CharField(required=False, allow_blank=True, allow_null=True) scrubIPAddresses = serializers.BooleanField(required=False) groupingConfig = serializers.CharField(required=False, allow_blank=True, allow_null=True) groupingEnhancements = serializers.CharField(required=False, allow_blank=True, allow_null=True) fingerprintingRules = serializers.CharField(required=False, allow_blank=True, allow_null=True) secondaryGroupingConfig = serializers.CharField( required=False, allow_blank=True, allow_null=True ) secondaryGroupingExpiry = serializers.IntegerField(min_value=1, required=False, allow_null=True) scrapeJavaScript = serializers.BooleanField(required=False) allowedDomains = EmptyListField(child=OriginField(allow_blank=True), required=False) resolveAge = EmptyIntegerField(required=False, allow_null=True) platform = serializers.CharField(required=False, allow_null=True, allow_blank=True) copy_from_project = serializers.IntegerField(required=False) dynamicSampling = DynamicSamplingSerializer(required=False) def validate(self, data): max_delay = ( data["digestsMaxDelay"] if "digestsMaxDelay" in data else self.context["project"].get_option("digests:mail:maximum_delay") ) min_delay = ( data["digestsMinDelay"] if "digestsMinDelay" in data else self.context["project"].get_option("digests:mail:minimum_delay") ) if min_delay is not None and max_delay and max_delay is not None and min_delay > max_delay: raise serializers.ValidationError( {"digestsMinDelay": "The minimum delay on digests must be lower than the maximum."} ) return data def validate_allowedDomains(self, value): value = filter(bool, value) if len(value) == 0: raise serializers.ValidationError( "Empty value will block all requests, use * to accept from all domains" ) return value def validate_slug(self, slug): if slug in RESERVED_PROJECT_SLUGS: raise serializers.ValidationError(f'The slug "{slug}" is reserved and not allowed.') project = self.context["project"] other = ( Project.objects.filter(slug=slug, organization=project.organization) .exclude(id=project.id) .first() ) if other is not None: raise serializers.ValidationError( "Another project (%s) is already using that slug" % other.name ) return slug def validate_relayPiiConfig(self, value): organization = self.context["project"].organization return validate_pii_config_update(organization, value) def validate_builtinSymbolSources(self, value): if not value: return value from sentry import features organization = self.context["project"].organization request = self.context["request"] has_sources = features.has("organizations:symbol-sources", organization, actor=request.user) if not has_sources: raise serializers.ValidationError("Organization is not allowed to set symbol sources") return value def validate_symbolSources(self, sources_json): if not sources_json: return sources_json from sentry import features organization = self.context["project"].organization request = self.context["request"] try: # We should really only grab and parse if there are sources in sources_json whose # secrets are set to {"hidden-secret":true} orig_sources = parse_sources( self.context["project"].get_option("sentry:symbol_sources") ) sources = parse_backfill_sources(sources_json.strip(), orig_sources) except InvalidSourcesError as e: raise serializers.ValidationError(str(e)) sources_json = json.dumps(sources) if sources else "" # If no sources are added or modified, we're either only deleting sources or doing nothing. # This is always allowed. added_or_modified_sources = [s for s in sources if s not in orig_sources] if not added_or_modified_sources: return sources_json # Adding sources is only allowed if custom symbol sources are enabled. has_sources = features.has( "organizations:custom-symbol-sources", organization, actor=request.user ) if not has_sources: raise serializers.ValidationError( "Organization is not allowed to set custom symbol sources" ) has_multiple_appconnect = features.has( "organizations:app-store-connect-multiple", organization, actor=request.user ) appconnect_sources = [s for s in sources if s.get("type") == "appStoreConnect"] if not has_multiple_appconnect and len(appconnect_sources) > 1: raise serializers.ValidationError( "Only one Apple App Store Connect application is allowed in this project" ) return sources_json def validate_groupingEnhancements(self, value): if not value: return value try: Enhancements.from_config_string(value) except InvalidEnhancerConfig as e: raise serializers.ValidationError(str(e)) return value def validate_secondaryGroupingExpiry(self, value): if not isinstance(value, (int, float)) or math.isnan(value): raise serializers.ValidationError( f"Grouping expiry must be a numerical value, a UNIX timestamp with second resolution, found {type(value)}" ) now = time.time() if value < now: raise serializers.ValidationError( "Grouping expiry must be sometime within the next 90 days and not in the past. Perhaps you specified the timestamp not in seconds?" ) max_expiry_date = now + (91 * 24 * 3600) if value > max_expiry_date: value = max_expiry_date return value def validate_fingerprintingRules(self, value): if not value: return value try: FingerprintingRules.from_config_string(value) except InvalidFingerprintingConfig as e: raise serializers.ValidationError(str(e)) return value def validate_copy_from_project(self, other_project_id): try: other_project = Project.objects.filter( id=other_project_id, organization_id=self.context["project"].organization_id ).prefetch_related("teams")[0] except IndexError: raise serializers.ValidationError("Project to copy settings from not found.") request = self.context["request"] if not request.access.has_project_access(other_project): raise serializers.ValidationError( "Project settings cannot be copied from a project you do not have access to." ) for project_team in other_project.projectteam_set.all(): if not request.access.has_team_scope(project_team.team, "team:write"): raise serializers.ValidationError( "Project settings cannot be copied from a project with a team you do not have write access to." ) return other_project_id def validate_platform(self, value): if Project.is_valid_platform(value): return value raise serializers.ValidationError("Invalid platform")
class ProjectAdminSerializer(ProjectMemberSerializer): name = serializers.CharField(max_length=200) slug = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50) team = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50) digestsMinDelay = serializers.IntegerField(min_value=60, max_value=3600) digestsMaxDelay = serializers.IntegerField(min_value=60, max_value=3600) subjectPrefix = serializers.CharField(max_length=200, allow_blank=True) subjectTemplate = serializers.CharField(max_length=200) securityToken = serializers.RegexField(r"^[-a-zA-Z0-9+/=\s]+$", max_length=255, allow_blank=True) securityTokenHeader = serializers.RegexField(r"^[a-zA-Z0-9_\-]+$", max_length=20, allow_blank=True) verifySSL = serializers.BooleanField(required=False) defaultEnvironment = serializers.CharField(required=False, allow_null=True, allow_blank=True) dataScrubber = serializers.BooleanField(required=False) dataScrubberDefaults = serializers.BooleanField(required=False) sensitiveFields = ListField(child=serializers.CharField(), required=False) safeFields = ListField(child=serializers.CharField(), required=False) storeCrashReports = serializers.IntegerField(min_value=-1, max_value=20, required=False, allow_null=True) relayPiiConfig = serializers.CharField(required=False, allow_blank=True, allow_null=True) builtinSymbolSources = ListField(child=serializers.CharField(), required=False) symbolSources = serializers.CharField(required=False, allow_blank=True, allow_null=True) scrubIPAddresses = serializers.BooleanField(required=False) groupingConfig = serializers.CharField(required=False, allow_blank=True, allow_null=True) groupingEnhancements = serializers.CharField(required=False, allow_blank=True, allow_null=True) groupingEnhancementsBase = serializers.CharField(required=False, allow_blank=True, allow_null=True) fingerprintingRules = serializers.CharField(required=False, allow_blank=True, allow_null=True) scrapeJavaScript = serializers.BooleanField(required=False) allowedDomains = EmptyListField(child=OriginField(allow_blank=True), required=False) resolveAge = EmptyIntegerField(required=False, allow_null=True) platform = serializers.CharField(required=False, allow_null=True, allow_blank=True) copy_from_project = serializers.IntegerField(required=False) dynamicSampling = DynamicSamplingSerializer(required=False) def validate(self, data): max_delay = ( data["digestsMaxDelay"] if "digestsMaxDelay" in data else self.context["project"].get_option("digests:mail:maximum_delay")) min_delay = ( data["digestsMinDelay"] if "digestsMinDelay" in data else self.context["project"].get_option("digests:mail:minimum_delay")) if min_delay is not None and max_delay and max_delay is not None and min_delay > max_delay: raise serializers.ValidationError({ "digestsMinDelay": "The minimum delay on digests must be lower than the maximum." }) return data def validate_allowedDomains(self, value): value = filter(bool, value) if len(value) == 0: raise serializers.ValidationError( "Empty value will block all requests, use * to accept from all domains" ) return value def validate_slug(self, slug): if slug in RESERVED_PROJECT_SLUGS: raise serializers.ValidationError( f'The slug "{slug}" is reserved and not allowed.') project = self.context["project"] other = (Project.objects.filter( slug=slug, organization=project.organization).exclude(id=project.id).first()) if other is not None: raise serializers.ValidationError( "Another project (%s) is already using that slug" % other.name) return slug def validate_relayPiiConfig(self, value): organization = self.context["project"].organization return validate_pii_config_update(organization, value) def validate_builtinSymbolSources(self, value): if not value: return value from sentry import features organization = self.context["project"].organization request = self.context["request"] has_sources = features.has("organizations:symbol-sources", organization, actor=request.user) if not has_sources: raise serializers.ValidationError( "Organization is not allowed to set symbol sources") return value def validate_symbolSources(self, sources_json): if not sources_json: return sources_json from sentry import features organization = self.context["project"].organization request = self.context["request"] has_sources = features.has("organizations:custom-symbol-sources", organization, actor=request.user) if not has_sources: raise serializers.ValidationError( "Organization is not allowed to set custom symbol sources") try: sources = parse_sources(sources_json.strip()) sources_json = json.dumps(sources) if sources else "" except InvalidSourcesError as e: raise serializers.ValidationError(str(e)) return sources_json def validate_groupingEnhancements(self, value): if not value: return value try: Enhancements.from_config_string(value) except InvalidEnhancerConfig as e: raise serializers.ValidationError(str(e)) return value def validate_fingerprintingRules(self, value): if not value: return value try: FingerprintingRules.from_config_string(value) except InvalidFingerprintingConfig as e: raise serializers.ValidationError(str(e)) return value def validate_copy_from_project(self, other_project_id): try: other_project = Project.objects.filter( id=other_project_id, organization_id=self.context["project"].organization_id ).prefetch_related("teams")[0] except IndexError: raise serializers.ValidationError( "Project to copy settings from not found.") request = self.context["request"] if not request.access.has_project_access(other_project): raise serializers.ValidationError( "Project settings cannot be copied from a project you do not have access to." ) for project_team in other_project.projectteam_set.all(): if not request.access.has_team_scope(project_team.team, "team:write"): raise serializers.ValidationError( "Project settings cannot be copied from a project with a team you do not have write access to." ) return other_project_id def validate_platform(self, value): if Project.is_valid_platform(value): return value raise serializers.ValidationError("Invalid platform")