class UserNotificationDetailsSerializer(serializers.Serializer): deployNotifications = EmptyIntegerField( required=False, min_value=2, max_value=4, allow_null=True ) personalActivityNotifications = serializers.BooleanField(required=False) selfAssignOnResolve = serializers.BooleanField(required=False) subscribeByDefault = serializers.BooleanField(required=False) workflowNotifications = EmptyIntegerField( required=False, min_value=0, max_value=2, allow_null=True )
class CheckInSerializer(serializers.Serializer): status = serializers.ChoiceField(choices=( ("ok", CheckInStatus.OK), ("error", CheckInStatus.ERROR), ("in_progress", CheckInStatus.IN_PROGRESS), )) duration = EmptyIntegerField(required=False, allow_null=True)
class RepositorySerializer(serializers.Serializer): status = serializers.ChoiceField(choices=( # XXX(dcramer): these are aliased, and we prefer 'active' over 'visible' ("visible", "visible"), ("active", "active"), )) integrationId = EmptyIntegerField(required=False, allow_null=True)
class CronJobValidator(serializers.Serializer): schedule_type = serializers.ChoiceField( choices=zip(SCHEDULE_TYPES.keys(), SCHEDULE_TYPES.keys())) schedule = ObjectField() checkin_margin = EmptyIntegerField(required=False, default=None) max_runtime = EmptyIntegerField(required=False, default=None) def validate_schedule_type(self, value): if value: value = SCHEDULE_TYPES[value] return value def validate(self, attrs): if "schedule_type" in attrs: schedule_type = attrs["schedule_type"] else: schedule_type = self.instance["schedule_type"] value = attrs.get("schedule") if not value: return attrs if schedule_type == ScheduleType.INTERVAL: # type: [int count, str unit name] if not isinstance(value, list): raise ValidationError("Invalid value for schedule_type") if not isinstance(value[0], int): raise ValidationError( "Invalid value for schedule unit count (index 0)") if value[1] not in INTERVAL_NAMES: raise ValidationError( "Invalid value for schedule unit name (index 1)") elif schedule_type == ScheduleType.CRONTAB: # type: str schedule if not isinstance(value, six.string_types): raise ValidationError("Invalid value for schedule_type") value = value.strip() if value.startswith("@"): try: value = NONSTANDARD_CRONTAB_SCHEDULES[value] except KeyError: raise ValidationError("Schedule was not parseable") if not croniter.is_valid(value): raise ValidationError("Schedule was not parseable") attrs["schedule"] = value return attrs
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 OrganizationSerializer(serializers.Serializer): name = serializers.CharField(max_length=64) slug = serializers.RegexField(r'^[a-z0-9_\-]+$', max_length=50) accountRateLimit = EmptyIntegerField( min_value=0, max_value=1000000, required=False, allow_null=True, ) projectRateLimit = EmptyIntegerField( min_value=50, max_value=100, required=False, allow_null=True, ) avatar = AvatarField(required=False, allow_null=True) avatarType = serializers.ChoiceField( choices=( ('upload', 'upload'), ('letter_avatar', 'letter_avatar'), ), required=False, allow_null=True, ) openMembership = serializers.BooleanField(required=False) allowSharedIssues = serializers.BooleanField(required=False) enhancedPrivacy = serializers.BooleanField(required=False) 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.BooleanField(required=False) attachmentsRole = serializers.CharField(required=True) scrubIPAddresses = serializers.BooleanField(required=False) scrapeJavaScript = serializers.BooleanField(required=False) isEarlyAdopter = serializers.BooleanField(required=False) require2FA = serializers.BooleanField(required=False) trustedRelays = ListField(child=serializers.CharField(), required=False) @memoize def _has_legacy_rate_limits(self): org = self.context['organization'] return OrganizationOption.objects.filter( organization=org, key__in=LEGACY_RATE_LIMIT_OPTIONS, ).exists() def _has_sso_enabled(self): org = self.context['organization'] return AuthProvider.objects.filter(organization=org).exists() def validate_slug(self, value): # Historically, the only check just made sure there was more than 1 # character for the slug, but since then, there are many slugs that # fit within this new imposed limit. We're not fixing existing, but # just preventing new bad values. if len(value) < 3: raise serializers.ValidationError( 'This slug "%s" is too short. Minimum of 3 characters.' % (value, )) if value in RESERVED_ORGANIZATION_SLUGS: raise serializers.ValidationError( 'This slug "%s" is reserved and not allowed.' % (value, )) qs = Organization.objects.filter( slug=value, ).exclude(id=self.context['organization'].id) if qs.exists(): raise serializers.ValidationError( 'The slug "%s" is already in use.' % (value, )) return value def validate_sensitiveFields(self, value): if value and not all(value): raise serializers.ValidationError('Empty values are not allowed.') return value def validate_safeFields(self, value): if value and not all(value): raise serializers.ValidationError('Empty values are not allowed.') return value def validate_attachmentsRole(self, value): try: roles.get(value) except KeyError: raise serializers.ValidationError('Invalid role') return value def validate_require2FA(self, value): user = self.context['user'] has_2fa = Authenticator.objects.user_has_2fa(user) if value and not has_2fa: raise serializers.ValidationError(ERR_NO_2FA) if value and self._has_sso_enabled(): raise serializers.ValidationError(ERR_SSO_ENABLED) return value def validate_trustedRelays(self, value): from sentry import features organization = self.context['organization'] request = self.context["request"] has_relays = features.has('organizations:relay', organization, actor=request.user) if not has_relays: raise serializers.ValidationError( 'Organization does not have the relay feature enabled') return value def validate_accountRateLimit(self, value): if not self._has_legacy_rate_limits: raise serializers.ValidationError( 'The accountRateLimit option cannot be configured for this organization' ) return value def validate_projectRateLimit(self, value): if not self._has_legacy_rate_limits: raise serializers.ValidationError( 'The accountRateLimit option cannot be configured for this organization' ) return value def validate(self, attrs): attrs = super(OrganizationSerializer, self).validate(attrs) if attrs.get('avatarType') == 'upload': has_existing_file = OrganizationAvatar.objects.filter( organization=self.context['organization'], file__isnull=False, ).exists() if not has_existing_file and not attrs.get('avatar'): raise serializers.ValidationError({ 'avatarType': 'Cannot set avatarType to upload without avatar', }) return attrs def save(self): org = self.context['organization'] changed_data = {} for key, option, type_, default_value in ORG_OPTIONS: if key not in self.initial_data: continue try: option_inst = OrganizationOption.objects.get(organization=org, key=option) except OrganizationOption.DoesNotExist: OrganizationOption.objects.set_value( organization=org, key=option, value=type_(self.initial_data[key]), ) if self.initial_data[key] != default_value: changed_data[key] = u'to {}'.format(self.initial_data[key]) else: option_inst.value = self.initial_data[key] # check if ORG_OPTIONS changed if option_inst.has_changed('value'): old_val = option_inst.old_value('value') changed_data[key] = u'from {} to {}'.format( old_val, option_inst.value) option_inst.save() if 'openMembership' in self.initial_data: org.flags.allow_joinleave = self.initial_data['openMembership'] if 'allowSharedIssues' in self.initial_data: org.flags.disable_shared_issues = not self.initial_data[ 'allowSharedIssues'] if 'enhancedPrivacy' in self.initial_data: org.flags.enhanced_privacy = self.initial_data['enhancedPrivacy'] if 'isEarlyAdopter' in self.initial_data: org.flags.early_adopter = self.initial_data['isEarlyAdopter'] if 'require2FA' in self.initial_data: org.flags.require_2fa = self.initial_data['require2FA'] if 'name' in self.initial_data: org.name = self.initial_data['name'] if 'slug' in self.initial_data: org.slug = self.initial_data['slug'] org_tracked_field = { 'name': org.name, 'slug': org.slug, 'default_role': org.default_role, 'flag_field': { 'allow_joinleave': org.flags.allow_joinleave.is_set, 'enhanced_privacy': org.flags.enhanced_privacy.is_set, 'disable_shared_issues': org.flags.disable_shared_issues.is_set, 'early_adopter': org.flags.early_adopter.is_set, 'require_2fa': org.flags.require_2fa.is_set, } } # check if fields changed for f, v in six.iteritems(org_tracked_field): if f is not 'flag_field': if org.has_changed(f): old_val = org.old_value(f) changed_data[f] = u'from {} to {}'.format(old_val, v) else: # check if flag fields changed for f, v in six.iteritems(org_tracked_field['flag_field']): if org.flag_has_changed(f): changed_data[f] = u'to {}'.format(v) org.save() if 'avatar' in self.initial_data or 'avatarType' in self.initial_data: OrganizationAvatar.save_avatar( relation={'organization': org}, type=self.initial_data.get('avatarType', 'upload'), avatar=self.initial_data.get('avatar'), filename=u'{}.png'.format(org.slug), ) if 'require2FA' in self.initial_data and self.initial_data[ 'require2FA'] is True: org.handle_2fa_required(self.context['request']) return org, changed_data
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")
class OrganizationSerializer(serializers.Serializer): name = serializers.CharField(max_length=64) slug = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50) accountRateLimit = EmptyIntegerField(min_value=0, max_value=1000000, required=False, allow_null=True) projectRateLimit = EmptyIntegerField(min_value=50, max_value=100, required=False, allow_null=True) avatar = AvatarField(required=False, allow_null=True) avatarType = serializers.ChoiceField( choices=(("upload", "upload"), ("letter_avatar", "letter_avatar")), required=False, allow_null=True, ) openMembership = serializers.BooleanField(required=False) allowSharedIssues = serializers.BooleanField(required=False) enhancedPrivacy = serializers.BooleanField(required=False) 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) attachmentsRole = serializers.CharField(required=True) debugFilesRole = serializers.CharField(required=True) eventsMemberAdmin = serializers.BooleanField(required=False) alertsMemberWrite = serializers.BooleanField(required=False) scrubIPAddresses = serializers.BooleanField(required=False) scrapeJavaScript = serializers.BooleanField(required=False) isEarlyAdopter = serializers.BooleanField(required=False) require2FA = serializers.BooleanField(required=False) requireEmailVerification = serializers.BooleanField(required=False) trustedRelays = ListField(child=TrustedRelaySerializer(), required=False) allowJoinRequests = serializers.BooleanField(required=False) relayPiiConfig = serializers.CharField(required=False, allow_blank=True, allow_null=True) apdexThreshold = serializers.IntegerField(min_value=1, required=False) @memoize def _has_legacy_rate_limits(self): org = self.context["organization"] return OrganizationOption.objects.filter( organization=org, key__in=LEGACY_RATE_LIMIT_OPTIONS).exists() def _has_sso_enabled(self): org = self.context["organization"] return AuthProvider.objects.filter(organization=org).exists() def validate_slug(self, value): # Historically, the only check just made sure there was more than 1 # character for the slug, but since then, there are many slugs that # fit within this new imposed limit. We're not fixing existing, but # just preventing new bad values. if len(value) < 3: raise serializers.ValidationError( f'This slug "{value}" is too short. Minimum of 3 characters.') if value in RESERVED_ORGANIZATION_SLUGS: raise serializers.ValidationError( f'This slug "{value}" is reserved and not allowed.') qs = Organization.objects.filter(slug=value).exclude( id=self.context["organization"].id) if qs.exists(): raise serializers.ValidationError( f'The slug "{value}" is already in use.') return value def validate_relayPiiConfig(self, value): organization = self.context["organization"] return validate_pii_config_update(organization, value) def validate_sensitiveFields(self, value): if value and not all(value): raise serializers.ValidationError("Empty values are not allowed.") return value def validate_safeFields(self, value): if value and not all(value): raise serializers.ValidationError("Empty values are not allowed.") return value def validate_attachmentsRole(self, value): try: roles.get(value) except KeyError: raise serializers.ValidationError("Invalid role") return value def validate_debugFilesRole(self, value): try: roles.get(value) except KeyError: raise serializers.ValidationError("Invalid role") return value def validate_require2FA(self, value): user = self.context["user"] has_2fa = Authenticator.objects.user_has_2fa(user) if value and not has_2fa: raise serializers.ValidationError(ERR_NO_2FA) if value and self._has_sso_enabled(): raise serializers.ValidationError(ERR_SSO_ENABLED) return value def validate_requireEmailVerification(self, value): user = self.context["user"] has_verified = UserEmail.get_primary_email(user).is_verified if value and not has_verified: raise serializers.ValidationError(ERR_EMAIL_VERIFICATION) return value def validate_trustedRelays(self, value): from sentry import features organization = self.context["organization"] request = self.context["request"] has_relays = features.has("organizations:relay", organization, actor=request.user) if not has_relays: raise serializers.ValidationError( "Organization does not have the relay feature enabled") # make sure we don't have multiple instances of one public key public_keys = set() if value is not None: for key_info in value: key = key_info.get("public_key") if key in public_keys: raise serializers.ValidationError( f"Duplicated key in Trusted Relays: '{key}'") public_keys.add(key) return value def validate_accountRateLimit(self, value): if not self._has_legacy_rate_limits: raise serializers.ValidationError( "The accountRateLimit option cannot be configured for this organization" ) return value def validate_projectRateLimit(self, value): if not self._has_legacy_rate_limits: raise serializers.ValidationError( "The accountRateLimit option cannot be configured for this organization" ) return value def validate(self, attrs): attrs = super().validate(attrs) if attrs.get("avatarType") == "upload": has_existing_file = OrganizationAvatar.objects.filter( organization=self.context["organization"], file__isnull=False).exists() if not has_existing_file and not attrs.get("avatar"): raise serializers.ValidationError({ "avatarType": "Cannot set avatarType to upload without avatar" }) return attrs def save_trusted_relays(self, incoming, changed_data, organization): timestamp_now = datetime.utcnow().replace(tzinfo=UTC).isoformat() option_key = "sentry:trusted-relays" try: # get what we already have existing = OrganizationOption.objects.get( organization=organization, key=option_key) key_dict = {val.get("public_key"): val for val in existing.value} original_number_of_keys = len(existing.value) except OrganizationOption.DoesNotExist: key_dict = {} # we don't have anything set original_number_of_keys = 0 existing = None modified = False for option in incoming: public_key = option.get("public_key") existing_info = key_dict.get(public_key, {}) option["created"] = existing_info.get("created", timestamp_now) option["last_modified"] = existing_info.get("last_modified") # check if we modified the current public_key info and update last_modified if we did if (not existing_info or existing_info.get("name") != option.get("name") or existing_info.get("description") != option.get("description")): option["last_modified"] = timestamp_now modified = True # check to see if the only modifications were some deletions (which are not captured in the loop above) if len(incoming) != original_number_of_keys: modified = True if modified: # we have some modifications create a log message if existing is not None: # generate an update log message changed_data[ "trustedRelays"] = f"from {existing} to {incoming}" existing.value = incoming existing.save() else: # first time we set trusted relays, generate a create log message changed_data["trustedRelays"] = f"to {incoming}" OrganizationOption.objects.set_value(organization=organization, key=option_key, value=incoming) return incoming def save(self): from sentry import features org = self.context["organization"] changed_data = {} if not hasattr(org, "__data"): update_tracked_data(org) for key, option, type_, default_value in ORG_OPTIONS: if key not in self.initial_data: continue try: option_inst = OrganizationOption.objects.get(organization=org, key=option) update_tracked_data(option_inst) except OrganizationOption.DoesNotExist: OrganizationOption.objects.set_value( organization=org, key=option, value=type_(self.initial_data[key])) if self.initial_data[key] != default_value: changed_data[key] = f"to {self.initial_data[key]}" else: option_inst.value = self.initial_data[key] # check if ORG_OPTIONS changed if has_changed(option_inst, "value"): old_val = old_value(option_inst, "value") changed_data[ key] = f"from {old_val} to {option_inst.value}" option_inst.save() trusted_realy_info = self.validated_data.get("trustedRelays") if trusted_realy_info is not None: self.save_trusted_relays(trusted_realy_info, changed_data, org) if "openMembership" in self.initial_data: org.flags.allow_joinleave = self.initial_data["openMembership"] if "allowSharedIssues" in self.initial_data: org.flags.disable_shared_issues = not self.initial_data[ "allowSharedIssues"] if "enhancedPrivacy" in self.initial_data: org.flags.enhanced_privacy = self.initial_data["enhancedPrivacy"] if "isEarlyAdopter" in self.initial_data: org.flags.early_adopter = self.initial_data["isEarlyAdopter"] if "require2FA" in self.initial_data: org.flags.require_2fa = self.initial_data["require2FA"] if (features.has("organizations:required-email-verification", org) and "requireEmailVerification" in self.initial_data): org.flags.require_email_verification = self.initial_data[ "requireEmailVerification"] if "name" in self.initial_data: org.name = self.initial_data["name"] if "slug" in self.initial_data: org.slug = self.initial_data["slug"] org_tracked_field = { "name": org.name, "slug": org.slug, "default_role": org.default_role, "flag_field": { "allow_joinleave": org.flags.allow_joinleave.is_set, "enhanced_privacy": org.flags.enhanced_privacy.is_set, "disable_shared_issues": org.flags.disable_shared_issues.is_set, "early_adopter": org.flags.early_adopter.is_set, "require_2fa": org.flags.require_2fa.is_set, }, } # check if fields changed for f, v in org_tracked_field.items(): if f != "flag_field": if has_changed(org, f): old_val = old_value(org, f) changed_data[f] = f"from {old_val} to {v}" else: # check if flag fields changed for f, v in org_tracked_field["flag_field"].items(): if flag_has_changed(org, f): changed_data[f] = f"to {v}" org.save() if "avatar" in self.initial_data or "avatarType" in self.initial_data: OrganizationAvatar.save_avatar( relation={"organization": org}, type=self.initial_data.get("avatarType", "upload"), avatar=self.initial_data.get("avatar"), filename=f"{org.slug}.png", ) if self.initial_data.get("require2FA") is True: org.handle_2fa_required(self.context["request"]) if (features.has("organizations:required-email-verification", org) and self.initial_data.get("requireEmailVerification") is True): org.handle_email_verification_required(self.context["request"]) return org, changed_data
class DiscoverQuerySerializer(serializers.Serializer): projects = ListField(child=serializers.IntegerField(), required=True, allow_null=False) start = serializers.CharField(required=False, allow_null=True) end = serializers.CharField(required=False, allow_null=True) range = serializers.CharField(required=False, allow_null=True) statsPeriod = serializers.CharField(required=False, allow_null=True) statsPeriodStart = serializers.CharField(required=False, allow_null=True) statsPeriodEnd = serializers.CharField(required=False, allow_null=True) fields = ListField(child=serializers.CharField(), required=False, default=[]) conditionFields = ListField(child=ListField(), required=False, allow_null=True) limit = EmptyIntegerField(min_value=0, max_value=10000, required=False, allow_null=True) rollup = EmptyIntegerField(required=False, allow_null=True) orderby = serializers.CharField(required=False, default="", allow_blank=True) conditions = ListField(child=ListField(), required=False, allow_null=True) aggregations = ListField(child=ListField(), required=False, default=[]) groupby = ListField(child=serializers.CharField(), required=False, allow_null=True) turbo = serializers.BooleanField(required=False) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) data = kwargs["data"] fields = data.get("fields") or [] match = next( (self.get_array_field(field).group(1) for field in fields if self.get_array_field(field) is not None), None, ) self.arrayjoin = match if match else None def validate(self, data): data["arrayjoin"] = self.arrayjoin # prevent conflicting date ranges from being supplied date_fields = ["start", "statsPeriod", "range", "statsPeriodStart"] date_fields_provided = len( [data.get(f) for f in date_fields if data.get(f) is not None]) if date_fields_provided == 0: raise serializers.ValidationError("You must specify a date filter") elif date_fields_provided > 1: raise serializers.ValidationError( "Conflicting date filters supplied") if not data.get("fields") and not data.get("aggregations"): raise serializers.ValidationError( "Specify at least one field or aggregation") try: start, end = get_date_range_from_params( { "start": data.get("start"), "end": data.get("end"), "statsPeriod": data.get("statsPeriod") or data.get("range"), "statsPeriodStart": data.get("statsPeriodStart"), "statsPeriodEnd": data.get("statsPeriodEnd"), }, optional=True, ) except InvalidParams as e: raise serializers.ValidationError(str(e)) if start is None or end is None: raise serializers.ValidationError( "Either start and end dates or range is required") data["start"] = start data["end"] = end return data def validate_conditions(self, value): # Handle error (exception_stacks), stack(exception_frames) return [self.get_condition(condition) for condition in value] def validate_aggregations(self, value): valid_functions = {"count()", "uniq", "avg", "sum"} requested_functions = {agg[0] for agg in value} if not requested_functions.issubset(valid_functions): invalid_functions = ", ".join(requested_functions - valid_functions) raise serializers.ValidationError( f"Invalid aggregate function - {invalid_functions}") return value def get_array_field(self, field): pattern = r"^(error|stack)\..+" term = re.search(pattern, field) if term and SENTRY_SNUBA_MAP.get(field): return term return None def get_condition(self, condition): array_field = self.get_array_field(condition[0]) has_equality_operator = condition[1] in ("=", "!=") # Cast boolean values to 1 / 0 if isinstance(condition[2], bool): condition[2] = int(condition[2]) # Strip double quotes on strings if isinstance(condition[2], str): match = re.search(r'^"(.*)"$', condition[2]) if match: condition[2] = match.group(1) # Apply has function to any array field if it's = / != and not part of arrayjoin if array_field and has_equality_operator and (array_field.group(1) != self.arrayjoin): value = condition[2] if isinstance(value, str): value = f"'{value}'" bool_value = 1 if condition[1] == "=" else 0 return [["has", [array_field.group(0), value]], "=", bool_value] return condition
class RateLimitSerializer(serializers.Serializer): count = EmptyIntegerField(min_value=0, required=False, allow_null=True) window = EmptyIntegerField(min_value=0, max_value=60 * 60 * 24, required=False, allow_null=True)
class OrganizationSerializer(serializers.Serializer): name = serializers.CharField(max_length=64) slug = serializers.RegexField(r"^[a-z0-9_\-]+$", max_length=50) accountRateLimit = EmptyIntegerField( min_value=0, max_value=1000000, required=False, allow_null=True ) projectRateLimit = EmptyIntegerField( min_value=50, max_value=100, required=False, allow_null=True ) avatar = AvatarField(required=False, allow_null=True) avatarType = serializers.ChoiceField( choices=(("upload", "upload"), ("letter_avatar", "letter_avatar")), required=False, allow_null=True, ) openMembership = serializers.BooleanField(required=False) allowSharedIssues = serializers.BooleanField(required=False) enhancedPrivacy = serializers.BooleanField(required=False) 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) attachmentsRole = serializers.CharField(required=True) scrubIPAddresses = serializers.BooleanField(required=False) scrapeJavaScript = serializers.BooleanField(required=False) isEarlyAdopter = serializers.BooleanField(required=False) require2FA = serializers.BooleanField(required=False) trustedRelays = ListField(child=serializers.CharField(), required=False) allowJoinRequests = serializers.BooleanField(required=False) @memoize def _has_legacy_rate_limits(self): org = self.context["organization"] return OrganizationOption.objects.filter( organization=org, key__in=LEGACY_RATE_LIMIT_OPTIONS ).exists() def _has_sso_enabled(self): org = self.context["organization"] return AuthProvider.objects.filter(organization=org).exists() def validate_slug(self, value): # Historically, the only check just made sure there was more than 1 # character for the slug, but since then, there are many slugs that # fit within this new imposed limit. We're not fixing existing, but # just preventing new bad values. if len(value) < 3: raise serializers.ValidationError( 'This slug "%s" is too short. Minimum of 3 characters.' % (value,) ) if value in RESERVED_ORGANIZATION_SLUGS: raise serializers.ValidationError( 'This slug "%s" is reserved and not allowed.' % (value,) ) qs = Organization.objects.filter(slug=value).exclude(id=self.context["organization"].id) if qs.exists(): raise serializers.ValidationError('The slug "%s" is already in use.' % (value,)) return value def validate_sensitiveFields(self, value): if value and not all(value): raise serializers.ValidationError("Empty values are not allowed.") return value def validate_safeFields(self, value): if value and not all(value): raise serializers.ValidationError("Empty values are not allowed.") return value def validate_attachmentsRole(self, value): try: roles.get(value) except KeyError: raise serializers.ValidationError("Invalid role") return value def validate_require2FA(self, value): user = self.context["user"] has_2fa = Authenticator.objects.user_has_2fa(user) if value and not has_2fa: raise serializers.ValidationError(ERR_NO_2FA) if value and self._has_sso_enabled(): raise serializers.ValidationError(ERR_SSO_ENABLED) return value def validate_trustedRelays(self, value): from sentry import features organization = self.context["organization"] request = self.context["request"] has_relays = features.has("organizations:relay", organization, actor=request.user) if not has_relays: raise serializers.ValidationError( "Organization does not have the relay feature enabled" ) return value def validate_accountRateLimit(self, value): if not self._has_legacy_rate_limits: raise serializers.ValidationError( "The accountRateLimit option cannot be configured for this organization" ) return value def validate_projectRateLimit(self, value): if not self._has_legacy_rate_limits: raise serializers.ValidationError( "The accountRateLimit option cannot be configured for this organization" ) return value def validate(self, attrs): attrs = super(OrganizationSerializer, self).validate(attrs) if attrs.get("avatarType") == "upload": has_existing_file = OrganizationAvatar.objects.filter( organization=self.context["organization"], file__isnull=False ).exists() if not has_existing_file and not attrs.get("avatar"): raise serializers.ValidationError( {"avatarType": "Cannot set avatarType to upload without avatar"} ) return attrs def save(self): org = self.context["organization"] changed_data = {} for key, option, type_, default_value in ORG_OPTIONS: if key not in self.initial_data: continue try: option_inst = OrganizationOption.objects.get(organization=org, key=option) except OrganizationOption.DoesNotExist: OrganizationOption.objects.set_value( organization=org, key=option, value=type_(self.initial_data[key]) ) if self.initial_data[key] != default_value: changed_data[key] = u"to {}".format(self.initial_data[key]) else: option_inst.value = self.initial_data[key] # check if ORG_OPTIONS changed if option_inst.has_changed("value"): old_val = option_inst.old_value("value") changed_data[key] = u"from {} to {}".format(old_val, option_inst.value) option_inst.save() if "openMembership" in self.initial_data: org.flags.allow_joinleave = self.initial_data["openMembership"] if "allowSharedIssues" in self.initial_data: org.flags.disable_shared_issues = not self.initial_data["allowSharedIssues"] if "enhancedPrivacy" in self.initial_data: org.flags.enhanced_privacy = self.initial_data["enhancedPrivacy"] if "isEarlyAdopter" in self.initial_data: org.flags.early_adopter = self.initial_data["isEarlyAdopter"] if "require2FA" in self.initial_data: org.flags.require_2fa = self.initial_data["require2FA"] if "name" in self.initial_data: org.name = self.initial_data["name"] if "slug" in self.initial_data: org.slug = self.initial_data["slug"] org_tracked_field = { "name": org.name, "slug": org.slug, "default_role": org.default_role, "flag_field": { "allow_joinleave": org.flags.allow_joinleave.is_set, "enhanced_privacy": org.flags.enhanced_privacy.is_set, "disable_shared_issues": org.flags.disable_shared_issues.is_set, "early_adopter": org.flags.early_adopter.is_set, "require_2fa": org.flags.require_2fa.is_set, }, } # check if fields changed for f, v in six.iteritems(org_tracked_field): if f != "flag_field": if org.has_changed(f): old_val = org.old_value(f) changed_data[f] = u"from {} to {}".format(old_val, v) else: # check if flag fields changed for f, v in six.iteritems(org_tracked_field["flag_field"]): if org.flag_has_changed(f): changed_data[f] = u"to {}".format(v) org.save() if "avatar" in self.initial_data or "avatarType" in self.initial_data: OrganizationAvatar.save_avatar( relation={"organization": org}, type=self.initial_data.get("avatarType", "upload"), avatar=self.initial_data.get("avatar"), filename=u"{}.png".format(org.slug), ) if "require2FA" in self.initial_data and self.initial_data["require2FA"] is True: org.handle_2fa_required(self.context["request"]) return org, changed_data
class DiscoverQuerySerializer(serializers.Serializer): projects = ListField( child=serializers.IntegerField(), required=True, allow_null=False, ) start = serializers.CharField(required=False, allow_null=True) end = serializers.CharField(required=False, allow_null=True) range = serializers.CharField(required=False, allow_null=True) statsPeriod = serializers.CharField(required=False, allow_null=True) statsPeriodStart = serializers.CharField(required=False, allow_null=True) statsPeriodEnd = serializers.CharField(required=False, allow_null=True) fields = ListField( child=serializers.CharField(), required=False, default=[], ) conditionFields = ListField( child=ListField(), required=False, allow_null=True, ) limit = EmptyIntegerField(min_value=0, max_value=10000, required=False, allow_null=True) rollup = EmptyIntegerField(required=False, allow_null=True) orderby = serializers.CharField(required=False, default="", allow_blank=True) conditions = ListField( child=ListField(), required=False, allow_null=True, ) aggregations = ListField( child=ListField(), required=False, default=[] ) groupby = ListField( child=serializers.CharField(), required=False, allow_null=True, ) turbo = serializers.BooleanField(required=False) def __init__(self, *args, **kwargs): super(DiscoverQuerySerializer, self).__init__(*args, **kwargs) data = kwargs['data'] fields = data.get('fields') or [] match = next( ( self.get_array_field(field).group(1) for field in fields if self.get_array_field(field) is not None ), None ) self.arrayjoin = match if match else None def validate(self, data): data['arrayjoin'] = self.arrayjoin # prevent conflicting date ranges from being supplied date_fields = ['start', 'statsPeriod', 'range', 'statsPeriodStart'] date_fields_provided = len([data.get(f) for f in date_fields if data.get(f) is not None]) if date_fields_provided == 0: raise serializers.ValidationError('You must specify a date filter') elif date_fields_provided > 1: raise serializers.ValidationError('Conflicting date filters supplied') if not data.get('fields') and not data.get('aggregations'): raise serializers.ValidationError('Specify at least one field or aggregation') try: start, end = get_date_range_from_params({ 'start': data.get('start'), 'end': data.get('end'), 'statsPeriod': data.get('statsPeriod') or data.get('range'), 'statsPeriodStart': data.get('statsPeriodStart'), 'statsPeriodEnd': data.get('statsPeriodEnd'), }, optional=True) except InvalidParams as exc: raise serializers.ValidationError(exc.message) if start is None or end is None: raise serializers.ValidationError('Either start and end dates or range is required') data['start'] = start data['end'] = end return data def validate_conditions(self, value): # Handle error (exception_stacks), stack(exception_frames) return [self.get_condition(condition) for condition in value] def validate_aggregations(self, value): valid_functions = set(['count()', 'uniq', 'avg']) requested_functions = set(agg[0] for agg in value) if not requested_functions.issubset(valid_functions): invalid_functions = ', '.join((requested_functions - valid_functions)) raise serializers.ValidationError( u'Invalid aggregate function - {}'.format(invalid_functions) ) return value def get_array_field(self, field): pattern = r"^(error|stack)\..+" term = re.search(pattern, field) if term and SENTRY_SNUBA_MAP.get(field): return term return None def get_condition(self, condition): array_field = self.get_array_field(condition[0]) has_equality_operator = condition[1] in ('=', '!=') # Cast boolean values to 1 / 0 if isinstance(condition[2], bool): condition[2] = int(condition[2]) # Strip double quotes on strings if isinstance(condition[2], six.string_types): match = re.search(r'^"(.*)"$', condition[2]) if match: condition[2] = match.group(1) # Apply has function to any array field if it's = / != and not part of arrayjoin if array_field and has_equality_operator and (array_field.group(1) != self.arrayjoin): value = condition[2] if (isinstance(value, six.string_types)): value = u"'{}'".format(value) bool_value = 1 if condition[1] == '=' else 0 return [['has', [array_field.group(0), value]], '=', bool_value] return condition