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_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:symbol-sources', organization, actor=request.user) if not has_sources: raise serializers.ValidationError( 'Organization is not allowed to set symbol sources') try: sources = parse_sources(sources_json.strip()) sources_json = json.dumps(sources) if sources else '' except InvalidSourcesError as e: raise serializers.ValidationError(e.message) return sources_json
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_symbolSources(self, attrs, source): sources_json = attrs[source] if not sources_json: return attrs 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' ) try: sources = parse_sources(sources_json.strip()) attrs[source] = json.dumps(sources) if sources else '' except InvalidSourcesError as e: raise serializers.ValidationError(e.message) return attrs
def put(self, request: Request, project) -> Response: """ Update a Project ```````````````` Update various attributes and configurable settings for the given project. Only supplied values are updated. :pparam string organization_slug: the slug of the organization the project belongs to. :pparam string project_slug: the slug of the project to update. :param string name: the new name for the project. :param string slug: the new slug for the project. :param string team: the slug of new team for the project. Note, will be deprecated soon when multiple teams can have access to a project. :param string platform: the new platform for the project. :param boolean isBookmarked: in case this API call is invoked with a user context this allows changing of the bookmark flag. :param int digestsMinDelay: :param int digestsMaxDelay: :auth: required """ has_project_write = (request.auth and request.auth.has_scope("project:write")) or ( request.access and request.access.has_scope("project:write") ) changed_proj_settings = {} if has_project_write: serializer_cls = ProjectAdminSerializer else: serializer_cls = ProjectMemberSerializer serializer = serializer_cls( data=request.data, partial=True, context={"project": project, "request": request} ) if not serializer.is_valid(): return Response(serializer.errors, status=400) result = serializer.validated_data allow_dynamic_sampling = features.has( "organizations:filters-and-sampling", project.organization, actor=request.user ) allow_dynamic_sampling_error_rules = features.has( "organizations:filters-and-sampling-error-rules", project.organization, actor=request.user, ) if not allow_dynamic_sampling and result.get("dynamicSampling"): # trying to set dynamic sampling with feature disabled return Response( {"detail": ["You do not have permission to set dynamic sampling."]}, status=403, ) if not has_project_write: # options isn't part of the serializer, but should not be editable by members for key in chain(ProjectAdminSerializer().fields.keys(), ["options"]): if request.data.get(key) and not result.get(key): return Response( {"detail": ["You do not have permission to perform this action."]}, status=403, ) changed = False old_slug = None if result.get("slug"): old_slug = project.slug project.slug = result["slug"] changed = True changed_proj_settings["new_slug"] = project.slug changed_proj_settings["old_slug"] = old_slug if result.get("name"): project.name = result["name"] changed = True changed_proj_settings["new_project"] = project.name old_team_id = None new_team = None if result.get("team"): return Response( {"detail": ["Editing a team via this endpoint has been deprecated."]}, status=400 ) if result.get("platform"): project.platform = result["platform"] changed = True if changed: project.save() if old_team_id is not None: ProjectTeam.objects.filter(project=project, team_id=old_team_id).update( team=new_team ) if old_slug: ProjectRedirect.record(project, old_slug) if result.get("isBookmarked"): try: with transaction.atomic(): ProjectBookmark.objects.create(project_id=project.id, user=request.user) except IntegrityError: pass elif result.get("isBookmarked") is False: ProjectBookmark.objects.filter(project_id=project.id, user=request.user).delete() if result.get("digestsMinDelay"): project.update_option("digests:mail:minimum_delay", result["digestsMinDelay"]) if result.get("digestsMaxDelay"): project.update_option("digests:mail:maximum_delay", result["digestsMaxDelay"]) if result.get("subjectPrefix") is not None: if project.update_option("mail:subject_prefix", result["subjectPrefix"]): changed_proj_settings["mail:subject_prefix"] = result["subjectPrefix"] if result.get("subjectTemplate"): project.update_option("mail:subject_template", result["subjectTemplate"]) if result.get("scrubIPAddresses") is not None: if project.update_option("sentry:scrub_ip_address", result["scrubIPAddresses"]): changed_proj_settings["sentry:scrub_ip_address"] = result["scrubIPAddresses"] if result.get("groupingConfig") is not None: if project.update_option("sentry:grouping_config", result["groupingConfig"]): changed_proj_settings["sentry:grouping_config"] = result["groupingConfig"] if result.get("groupingEnhancements") is not None: if project.update_option( "sentry:grouping_enhancements", result["groupingEnhancements"] ): changed_proj_settings["sentry:grouping_enhancements"] = result[ "groupingEnhancements" ] if result.get("fingerprintingRules") is not None: if project.update_option("sentry:fingerprinting_rules", result["fingerprintingRules"]): changed_proj_settings["sentry:fingerprinting_rules"] = result["fingerprintingRules"] if result.get("secondaryGroupingConfig") is not None: if project.update_option( "sentry:secondary_grouping_config", result["secondaryGroupingConfig"] ): changed_proj_settings["sentry:secondary_grouping_config"] = result[ "secondaryGroupingConfig" ] if result.get("secondaryGroupingExpiry") is not None: if project.update_option( "sentry:secondary_grouping_expiry", result["secondaryGroupingExpiry"] ): changed_proj_settings["sentry:secondary_grouping_expiry"] = result[ "secondaryGroupingExpiry" ] if result.get("securityToken") is not None: if project.update_option("sentry:token", result["securityToken"]): changed_proj_settings["sentry:token"] = result["securityToken"] if result.get("securityTokenHeader") is not None: if project.update_option("sentry:token_header", result["securityTokenHeader"]): changed_proj_settings["sentry:token_header"] = result["securityTokenHeader"] if result.get("verifySSL") is not None: if project.update_option("sentry:verify_ssl", result["verifySSL"]): changed_proj_settings["sentry:verify_ssl"] = result["verifySSL"] if result.get("dataScrubber") is not None: if project.update_option("sentry:scrub_data", result["dataScrubber"]): changed_proj_settings["sentry:scrub_data"] = result["dataScrubber"] if result.get("dataScrubberDefaults") is not None: if project.update_option("sentry:scrub_defaults", result["dataScrubberDefaults"]): changed_proj_settings["sentry:scrub_defaults"] = result["dataScrubberDefaults"] if result.get("sensitiveFields") is not None: if project.update_option("sentry:sensitive_fields", result["sensitiveFields"]): changed_proj_settings["sentry:sensitive_fields"] = result["sensitiveFields"] if result.get("safeFields") is not None: if project.update_option("sentry:safe_fields", result["safeFields"]): changed_proj_settings["sentry:safe_fields"] = result["safeFields"] if "storeCrashReports" in result is not None: if project.get_option("sentry:store_crash_reports") != result["storeCrashReports"]: changed_proj_settings["sentry:store_crash_reports"] = result["storeCrashReports"] if result["storeCrashReports"] is None: project.delete_option("sentry:store_crash_reports") else: project.update_option("sentry:store_crash_reports", result["storeCrashReports"]) if result.get("relayPiiConfig") is not None: if project.update_option("sentry:relay_pii_config", result["relayPiiConfig"]): changed_proj_settings["sentry:relay_pii_config"] = ( result["relayPiiConfig"].strip() or None ) if result.get("builtinSymbolSources") is not None: if project.update_option( "sentry:builtin_symbol_sources", result["builtinSymbolSources"] ): changed_proj_settings["sentry:builtin_symbol_sources"] = result[ "builtinSymbolSources" ] if result.get("symbolSources") is not None: if project.update_option("sentry:symbol_sources", result["symbolSources"]): # Redact secrets so they don't get logged directly to the Audit Log sources_json = result["symbolSources"] or None try: sources = parse_sources(sources_json) except Exception: sources = [] redacted_sources = redact_source_secrets(sources) changed_proj_settings["sentry:symbol_sources"] = redacted_sources if "defaultEnvironment" in result: if result["defaultEnvironment"] is None: project.delete_option("sentry:default_environment") else: project.update_option("sentry:default_environment", result["defaultEnvironment"]) # resolveAge can be None if "resolveAge" in result: if project.update_option( "sentry:resolve_age", 0 if result.get("resolveAge") is None else int(result["resolveAge"]), ): changed_proj_settings["sentry:resolve_age"] = result["resolveAge"] if result.get("scrapeJavaScript") is not None: if project.update_option("sentry:scrape_javascript", result["scrapeJavaScript"]): changed_proj_settings["sentry:scrape_javascript"] = result["scrapeJavaScript"] if result.get("allowedDomains"): if project.update_option("sentry:origins", result["allowedDomains"]): changed_proj_settings["sentry:origins"] = result["allowedDomains"] if "isSubscribed" in result: NotificationSetting.objects.update_settings( ExternalProviders.EMAIL, NotificationSettingTypes.ISSUE_ALERTS, get_option_value_from_boolean(result.get("isSubscribed")), user=request.user, project=project, ) if "dynamicSampling" in result: raw_dynamic_sampling = result["dynamicSampling"] if ( not allow_dynamic_sampling_error_rules and self._dynamic_sampling_contains_error_rule(raw_dynamic_sampling) ): return Response( { "detail": [ "Dynamic Sampling only accepts rules of type transaction or trace" ] }, status=400, ) fixed_rules = self._fix_rule_ids(project, raw_dynamic_sampling) project.update_option("sentry:dynamic_sampling", fixed_rules) # TODO(dcramer): rewrite options to use standard API config if has_project_write: options = request.data.get("options", {}) if "sentry:origins" in options: project.update_option( "sentry:origins", clean_newline_inputs(options["sentry:origins"]) ) if "sentry:resolve_age" in options: project.update_option("sentry:resolve_age", int(options["sentry:resolve_age"])) if "sentry:scrub_data" in options: project.update_option("sentry:scrub_data", bool(options["sentry:scrub_data"])) if "sentry:scrub_defaults" in options: project.update_option( "sentry:scrub_defaults", bool(options["sentry:scrub_defaults"]) ) if "sentry:safe_fields" in options: project.update_option( "sentry:safe_fields", [s.strip().lower() for s in options["sentry:safe_fields"]] ) if "sentry:store_crash_reports" in options: project.update_option( "sentry:store_crash_reports", convert_crashreport_count( options["sentry:store_crash_reports"], allow_none=True ), ) if "sentry:relay_pii_config" in options: project.update_option( "sentry:relay_pii_config", options["sentry:relay_pii_config"].strip() or None ) if "sentry:sensitive_fields" in options: project.update_option( "sentry:sensitive_fields", [s.strip().lower() for s in options["sentry:sensitive_fields"]], ) if "sentry:scrub_ip_address" in options: project.update_option( "sentry:scrub_ip_address", bool(options["sentry:scrub_ip_address"]) ) if "sentry:grouping_config" in options: project.update_option("sentry:grouping_config", options["sentry:grouping_config"]) if "sentry:fingerprinting_rules" in options: project.update_option( "sentry:fingerprinting_rules", options["sentry:fingerprinting_rules"] ) if "mail:subject_prefix" in options: project.update_option("mail:subject_prefix", options["mail:subject_prefix"]) if "sentry:default_environment" in options: project.update_option( "sentry:default_environment", options["sentry:default_environment"] ) if "sentry:csp_ignored_sources_defaults" in options: project.update_option( "sentry:csp_ignored_sources_defaults", bool(options["sentry:csp_ignored_sources_defaults"]), ) if "sentry:csp_ignored_sources" in options: project.update_option( "sentry:csp_ignored_sources", clean_newline_inputs(options["sentry:csp_ignored_sources"]), ) if "sentry:blacklisted_ips" in options: project.update_option( "sentry:blacklisted_ips", clean_newline_inputs(options["sentry:blacklisted_ips"]), ) if "feedback:branding" in options: project.update_option( "feedback:branding", "1" if options["feedback:branding"] else "0" ) if "sentry:reprocessing_active" in options: project.update_option( "sentry:reprocessing_active", bool(options["sentry:reprocessing_active"]) ) if "filters:blacklisted_ips" in options: project.update_option( "sentry:blacklisted_ips", clean_newline_inputs(options["filters:blacklisted_ips"]), ) if f"filters:{FilterTypes.RELEASES}" in options: if features.has("projects:custom-inbound-filters", project, actor=request.user): project.update_option( f"sentry:{FilterTypes.RELEASES}", clean_newline_inputs(options[f"filters:{FilterTypes.RELEASES}"]), ) else: return Response( {"detail": ["You do not have that feature enabled"]}, status=400 ) if f"filters:{FilterTypes.ERROR_MESSAGES}" in options: if features.has("projects:custom-inbound-filters", project, actor=request.user): project.update_option( f"sentry:{FilterTypes.ERROR_MESSAGES}", clean_newline_inputs( options[f"filters:{FilterTypes.ERROR_MESSAGES}"], case_insensitive=False, ), ) else: return Response( {"detail": ["You do not have that feature enabled"]}, status=400 ) if "copy_from_project" in result: if not project.copy_settings_from(result["copy_from_project"]): return Response({"detail": ["Copy project settings failed."]}, status=409) self.create_audit_entry( request=request, organization=project.organization, target_object=project.id, event=AuditLogEntryEvent.PROJECT_EDIT, data=changed_proj_settings, ) data = serialize(project, request.user, DetailedProjectSerializer()) return Response(data)
def serialize(self, obj, attrs, user): from sentry.plugins.base import plugins def get_value_with_default(key): value = attrs["options"].get(key) if value is not None: return value return projectoptions.get_well_known_default( key, epoch=attrs["options"].get("sentry:option-epoch") ) data = super().serialize(obj, attrs, user) data.update( { "latestRelease": attrs["latest_release"], "options": { "sentry:csp_ignored_sources_defaults": bool( attrs["options"].get("sentry:csp_ignored_sources_defaults", True) ), "sentry:csp_ignored_sources": "\n".join( attrs["options"].get("sentry:csp_ignored_sources", []) or [] ), "sentry:reprocessing_active": bool( attrs["options"].get("sentry:reprocessing_active", False) ), "filters:blacklisted_ips": "\n".join( attrs["options"].get("sentry:blacklisted_ips", []) ), f"filters:{FilterTypes.RELEASES}": "\n".join( attrs["options"].get(f"sentry:{FilterTypes.RELEASES}", []) ), f"filters:{FilterTypes.ERROR_MESSAGES}": "\n".join( attrs["options"].get(f"sentry:{FilterTypes.ERROR_MESSAGES}", []) ), "feedback:branding": attrs["options"].get("feedback:branding", "1") == "1", }, "digestsMinDelay": attrs["options"].get( "digests:mail:minimum_delay", digests.minimum_delay ), "digestsMaxDelay": attrs["options"].get( "digests:mail:maximum_delay", digests.maximum_delay ), "subjectPrefix": attrs["options"].get( "mail:subject_prefix", options.get("mail.subject-prefix") ), "allowedDomains": attrs["options"].get("sentry:origins", ["*"]), "resolveAge": int(attrs["options"].get("sentry:resolve_age", 0)), "dataScrubber": bool(attrs["options"].get("sentry:scrub_data", True)), "dataScrubberDefaults": bool(attrs["options"].get("sentry:scrub_defaults", True)), "safeFields": attrs["options"].get("sentry:safe_fields", []), "storeCrashReports": convert_crashreport_count( attrs["options"].get("sentry:store_crash_reports"), allow_none=True ), "sensitiveFields": attrs["options"].get("sentry:sensitive_fields", []), "subjectTemplate": attrs["options"].get("mail:subject_template") or DEFAULT_SUBJECT_TEMPLATE.template, "securityToken": attrs["options"].get("sentry:token") or obj.get_security_token(), "securityTokenHeader": attrs["options"].get("sentry:token_header"), "verifySSL": bool(attrs["options"].get("sentry:verify_ssl", False)), "scrubIPAddresses": bool(attrs["options"].get("sentry:scrub_ip_address", False)), "scrapeJavaScript": bool(attrs["options"].get("sentry:scrape_javascript", True)), "groupingConfig": get_value_with_default("sentry:grouping_config"), "groupingEnhancements": get_value_with_default("sentry:grouping_enhancements"), "groupingEnhancementsBase": get_value_with_default( "sentry:grouping_enhancements_base" ), "secondaryGroupingExpiry": get_value_with_default( "sentry:secondary_grouping_expiry" ), "secondaryGroupingConfig": get_value_with_default( "sentry:secondary_grouping_config" ), "fingerprintingRules": get_value_with_default("sentry:fingerprinting_rules"), "organization": attrs["org"], "plugins": serialize( [ plugin for plugin in plugins.configurable_for_project(obj, version=None) if plugin.has_project_conf() ], user, PluginSerializer(obj), ), "platforms": attrs["platforms"], "processingIssues": attrs["processing_issues"], "defaultEnvironment": attrs["options"].get("sentry:default_environment"), "relayPiiConfig": attrs["options"].get("sentry:relay_pii_config"), "builtinSymbolSources": get_value_with_default("sentry:builtin_symbol_sources"), "dynamicSampling": get_value_with_default("sentry:dynamic_sampling"), "eventProcessing": { "symbolicationDegraded": False, }, } ) custom_symbol_sources_json = attrs["options"].get("sentry:symbol_sources") try: sources = parse_sources(custom_symbol_sources_json, False) except Exception: # In theory sources stored on the project should be valid. If they are invalid, we don't # want to abort serialization just for sources, so just return an empty list instead of # returning sources with their secrets included. serialized_sources = "[]" else: redacted_sources = redact_source_secrets(sources) serialized_sources = json.dumps(redacted_sources) data.update( { "symbolSources": serialized_sources, } ) return data