예제 #1
0
    def delete(self, request: Request, project: Project,
               codeowners: ProjectCodeOwners) -> Response:
        """
        Delete a CodeOwners
        """
        if not self.has_feature(request, project):
            raise PermissionDenied

        codeowners.delete()
        return Response(status=status.HTTP_204_NO_CONTENT)
예제 #2
0
    def get_owners(cls, project_id, data):
        """
        For a given project_id, and event data blob.
        We combine the schemas from IssueOwners and CodeOwners.

        If Everyone is returned, this means we implicitly are
        falling through our rules and everyone is responsible.

        If an empty list is returned, this means there are explicitly
        no owners.
        """
        from sentry.models import ProjectCodeOwners

        ownership = cls.get_ownership_cached(project_id)
        if not ownership:
            ownership = cls(project_id=project_id)

        codeowners = ProjectCodeOwners.get_codeowners_cached(project_id)
        ownership.schema = cls.get_combined_schema(ownership, codeowners)

        rules = cls._matching_ownership_rules(ownership, project_id, data)

        if not rules:
            return cls.Everyone if ownership.fallthrough else [], None

        owners = {o for rule in rules for o in rule.owners}
        owners_to_actors = resolve_actors(owners, project_id)
        ordered_actors = []
        for rule in rules:
            for o in rule.owners:
                if o in owners and owners_to_actors.get(o) is not None:
                    ordered_actors.append(owners_to_actors[o])
                    owners.remove(o)

        return ordered_actors, rules
예제 #3
0
    def serialize(self, obj, attrs, user):
        data = {
            "id": str(obj.id),
            "raw": obj.raw,
            "dateCreated": obj.date_added,
            "dateUpdated": obj.date_updated,
            "codeMappingId": str(obj.repository_project_path_config_id),
            "provider": "unknown",
        }

        data["provider"] = attrs.get("provider", "unknown")

        if "codeMapping" in self.expand:
            config = attrs.get("codeMapping", {})
            data["codeMapping"] = serialize(
                config,
                user=user,
                serializer=RepositoryProjectPathConfigSerializer())
        if "ownershipSyntax" in self.expand:
            data["ownershipSyntax"] = convert_schema_to_rules_text(obj.schema)

        if "errors" in self.expand:
            _, errors = ProjectCodeOwners.validate_codeowners_associations(
                obj.raw, obj.project)
            data["errors"] = errors

        return data
예제 #4
0
    def validate(self, attrs: Mapping[str, Any]) -> Mapping[str, Any]:
        # If it already exists, set default attrs with existing values
        if self.instance:
            attrs = {
                "raw": self.instance.raw,
                "code_mapping_id": self.instance.repository_project_path_config,
                **attrs,
            }

        if not attrs.get("raw", "").strip():
            return attrs

        # Ignore association errors and continue parsing CODEOWNERS for valid lines.
        # Allow users to incrementally fix association errors; for CODEOWNERS with many external mappings.
        associations, _ = ProjectCodeOwners.validate_codeowners_associations(
            attrs["raw"], self.context["project"]
        )

        issue_owner_rules = convert_codeowners_syntax(
            attrs["raw"], associations, attrs["code_mapping_id"]
        )

        # Convert IssueOwner syntax into schema syntax
        try:
            validated_data = create_schema_from_issue_owners(
                issue_owners=issue_owner_rules, project_id=self.context["project"].id
            )
            return {
                **attrs,
                "schema": validated_data,
            }
        except ValidationError as e:
            raise serializers.ValidationError(e)
예제 #5
0
    def get_autoassign_owners(cls, project_id, data, limit=2):
        """
        Get the auto-assign owner for a project if there are any.

        We combine the schemas from IssueOwners and CodeOwners.

        Returns a tuple of (auto_assignment_enabled, list_of_owners, assigned_by_codeowners: boolean).
        """
        from sentry.models import ProjectCodeOwners

        with metrics.timer("projectownership.get_autoassign_owners"):
            ownership = cls.get_ownership_cached(project_id)
            codeowners = ProjectCodeOwners.get_codeowners_cached(project_id)
            assigned_by_codeowners = False
            if not (ownership or codeowners):
                return False, [], assigned_by_codeowners

            if not ownership:
                ownership = cls(project_id=project_id)

            ownership_rules = cls._matching_ownership_rules(
                ownership, project_id, data)
            codeowners_rules = (cls._matching_ownership_rules(
                codeowners, project_id, data) if codeowners else [])

            if not (codeowners_rules or ownership_rules):
                return ownership.auto_assignment, [], assigned_by_codeowners

            ownership_actors = cls._find_actors(project_id, ownership_rules,
                                                limit)
            codeowners_actors = cls._find_actors(project_id, codeowners_rules,
                                                 limit)

            # Can happen if the ownership rule references a user/team that no longer
            # is assigned to the project or has been removed from the org.
            if not (ownership_actors or codeowners_actors):
                return ownership.auto_assignment, [], assigned_by_codeowners

            # Ownership rules take precedence over codeowner rules.
            actors = [*ownership_actors, *codeowners_actors][:limit]

            # Only the first item in the list is used for assignment, the rest are just used to suggest suspect owners.
            # So if ownership_actors is empty, it will be assigned by codeowners_actors
            if len(ownership_actors) == 0:
                assigned_by_codeowners = True

            from sentry.models import ActorTuple

            return (
                ownership.auto_assignment,
                ActorTuple.resolve_many(actors),
                assigned_by_codeowners,
            )
예제 #6
0
    def get_autoassign_owners(cls, project_id, data, limit=2):
        """
        Get the auto-assign owner for a project if there are any.

        We combine the schemas from IssueOwners and CodeOwners.

        Returns a tuple of (auto_assignment_enabled, list_of_owners).
        """
        from sentry.models import ProjectCodeOwners

        with metrics.timer("projectownership.get_autoassign_owners"):
            ownership = cls.get_ownership_cached(project_id)
            codeowners = ProjectCodeOwners.get_codeowners_cached(project_id)

            if not (ownership or codeowners):
                return False, []

            if not ownership:
                ownership = cls(project_id=project_id)

            ownership.schema = cls.get_combined_schema(ownership, codeowners)

            rules = cls._matching_ownership_rules(ownership, project_id, data)
            if not rules:
                return ownership.auto_assignment, []

            # We want the last matching rule to take the most precedence.
            owners = [owner for rule in rules for owner in rule.owners]
            owners.reverse()
            actors = {
                key: val
                for key, val in resolve_actors({owner
                                                for owner in owners},
                                               project_id).items() if val
            }
            actors = [actors[owner] for owner in owners
                      if owner in actors][:limit]

            # Can happen if the ownership rule references a user/team that no longer
            # is assigned to the project or has been removed from the org.
            if not actors:
                return ownership.auto_assignment, []

            from sentry.models import ActorTuple

            return ownership.auto_assignment, ActorTuple.resolve_many(actors)
예제 #7
0
    def validate(self, attrs: Mapping[str, Any]) -> Mapping[str, Any]:
        # If it already exists, set default attrs with existing values
        if self.instance:
            attrs = {
                "raw": self.instance.raw,
                "code_mapping_id":
                self.instance.repository_project_path_config,
                **attrs,
            }

        if not attrs.get("raw", "").strip():
            return attrs

        # We want to limit `raw` to a reasonable length, so that people don't end up with values
        # that are several megabytes large. To not break this functionality for existing customers
        # we temporarily allow rows that already exceed this limit to still be updated.
        # We do something similar with ProjectOwnership at the API level.
        existing_raw = self.instance.raw if self.instance else ""
        if len(attrs["raw"]) > MAX_RAW_LENGTH and len(
                existing_raw) <= MAX_RAW_LENGTH:
            raise serializers.ValidationError({
                "raw":
                f"Raw needs to be <= {MAX_RAW_LENGTH} characters in length"
            })

        # Ignore association errors and continue parsing CODEOWNERS for valid lines.
        # Allow users to incrementally fix association errors; for CODEOWNERS with many external mappings.
        associations, _ = ProjectCodeOwners.validate_codeowners_associations(
            attrs["raw"], self.context["project"])

        issue_owner_rules = convert_codeowners_syntax(attrs["raw"],
                                                      associations,
                                                      attrs["code_mapping_id"])

        # Convert IssueOwner syntax into schema syntax
        try:
            validated_data = create_schema_from_issue_owners(
                issue_owners=issue_owner_rules,
                project_id=self.context["project"].id)
            return {
                **attrs,
                "schema": validated_data,
            }
        except ValidationError as e:
            raise serializers.ValidationError(e)
예제 #8
0
    def get_owners(
        cls, project_id: int, data: Mapping[str, Any]
    ) -> Tuple[Union["Everyone", Sequence["ActorTuple"]],
               Optional[Sequence[Rule]]]:
        """
        For a given project_id, and event data blob.
        We combine the schemas from IssueOwners and CodeOwners.

        If there are no matching rules, check ProjectOwnership.fallthrough:
            If ProjectOwnership.fallthrough is enabled, return Everyone (all project members)
             - we implicitly are falling through our rules and everyone is responsible.
            If ProjectOwnership.fallthrough is disabled, return an empty list
             - there are explicitly no owners

        If there are matching rules, return the ordered actors.
            The order is determined by iterating through rules sequentially, evaluating
            CODEOWNERS (if present), followed by Ownership Rules
        """
        from sentry.models import ProjectCodeOwners

        ownership = cls.get_ownership_cached(project_id)
        if not ownership:
            ownership = cls(project_id=project_id)

        codeowners = ProjectCodeOwners.get_codeowners_cached(project_id)
        ownership.schema = cls.get_combined_schema(ownership, codeowners)

        rules = cls._matching_ownership_rules(ownership, project_id, data)

        if not rules:
            return cls.Everyone if ownership.fallthrough else [], None

        owners = {o for rule in rules for o in rule.owners}
        owners_to_actors = resolve_actors(owners, project_id)
        ordered_actors = []
        for rule in rules:
            for o in rule.owners:
                if o in owners and owners_to_actors.get(o) is not None:
                    ordered_actors.append(owners_to_actors[o])
                    owners.remove(o)

        return ordered_actors, rules
예제 #9
0
    def tearDown(self):
        cache.delete(ProjectCodeOwners.get_cache_key(self.project.id))

        super().tearDown()
예제 #10
0
    def test_merge_codeowners(self):
        self.code_mapping_2 = self.create_code_mapping(
            project=self.project,
            organization_integration=self.oi,
            stack_root="stack/root/",
        )

        code_owners_1_rule = Rule(
            Matcher("codeowners", "docs/*"),
            [Owner("user", self.user.email),
             Owner("team", self.team.slug)],
        )
        code_owners_2_rule = Rule(
            Matcher("codeowners", "stack/root/docs/*"),
            [Owner("user", self.user.email),
             Owner("team", self.team.slug)],
        )

        self.code_owners = self.create_codeowners(
            self.project,
            self.code_mapping,
            raw=self.data["raw"],
            schema=dump_schema([code_owners_1_rule]),
        )

        self.code_owners_2 = self.create_codeowners(
            self.project,
            self.code_mapping_2,
            raw=self.data["raw"],
            schema=dump_schema([code_owners_2_rule]),
        )

        code_owners = ProjectCodeOwners.objects.filter(project=self.project)
        merged = ProjectCodeOwners.merge_code_owners_list(
            code_owners_list=code_owners)

        assert merged.schema == {
            "$version":
            1,
            "rules": [
                {
                    "matcher": {
                        "type": "codeowners",
                        "pattern": "docs/*"
                    },
                    "owners": [
                        {
                            "type": "user",
                            "identifier": "admin@localhost"
                        },
                        {
                            "type": "team",
                            "identifier": "tiger-team"
                        },
                    ],
                },
                {
                    "matcher": {
                        "type": "codeowners",
                        "pattern": "stack/root/docs/*"
                    },
                    "owners": [
                        {
                            "type": "user",
                            "identifier": "admin@localhost"
                        },
                        {
                            "type": "team",
                            "identifier": "tiger-team"
                        },
                    ],
                },
            ],
        }