launchpad-reviewers team mailing list archive
-
launchpad-reviewers team
-
Mailing list archive
-
Message #32912
[Merge] ~enriqueesanchz/launchpad:add-import-export-endpoints into launchpad:master
Enrique Sánchez has proposed merging ~enriqueesanchz/launchpad:add-import-export-endpoints into launchpad:master.
Commit message:
Add VulnerabilitySet import export endpoints
Requested reviews:
Launchpad code reviewers (launchpad-reviewers)
For more details, see:
https://code.launchpad.net/~enriqueesanchz/launchpad/+git/launchpad/+merge/491584
--
Your team Launchpad code reviewers is requested to review the proposed merge of ~enriqueesanchz/launchpad:add-import-export-endpoints into launchpad:master.
diff --git a/lib/lp/app/browser/launchpad.py b/lib/lp/app/browser/launchpad.py
index 748ae9f..a5558df 100644
--- a/lib/lp/app/browser/launchpad.py
+++ b/lib/lp/app/browser/launchpad.py
@@ -66,6 +66,7 @@ from lp.blueprints.interfaces.specification import ISpecificationSet
from lp.blueprints.interfaces.sprint import ISprintSet
from lp.bugs.interfaces.bug import IBugSet
from lp.bugs.interfaces.malone import IMaloneApplication
+from lp.bugs.interfaces.vulnerability import IVulnerabilitySet
from lp.buildmaster.interfaces.builder import IBuilderSet
from lp.buildmaster.interfaces.processor import IProcessorSet
from lp.charms.interfaces.charmbase import ICharmBaseSet
@@ -920,6 +921,7 @@ class LaunchpadRootNavigation(Navigation):
"testopenid": ITestOpenIDApplication,
"questions": IQuestionSet,
"temporary-blobs": ITemporaryStorageManager,
+ "vulnerabilities": IVulnerabilitySet,
"+feature-rules": IFeatureRules,
# These three have been renamed, and no redirects done, as the old
# urls now point to the product pages.
diff --git a/lib/lp/bugs/browser/configure.zcml b/lib/lp/bugs/browser/configure.zcml
index e710a4f..2e5e0cc 100644
--- a/lib/lp/bugs/browser/configure.zcml
+++ b/lib/lp/bugs/browser/configure.zcml
@@ -864,6 +864,10 @@
for="lp.bugs.interfaces.cve.ICveSet"
path_expression="string:cve"
parent_utility="lp.bugs.interfaces.malone.IMaloneApplication"/>
+ <lp:url
+ for="lp.bugs.interfaces.vulnerability.IVulnerabilitySet"
+ path_expression="string:vulnerabilities"
+ parent_utility="lp.services.webapp.interfaces.ILaunchpadRoot"/>
<lp:navigation
module="lp.bugs.browser.cve"
classes="
diff --git a/lib/lp/bugs/interfaces/vulnerability.py b/lib/lp/bugs/interfaces/vulnerability.py
index 90a9829..bda5c1a 100644
--- a/lib/lp/bugs/interfaces/vulnerability.py
+++ b/lib/lp/bugs/interfaces/vulnerability.py
@@ -12,17 +12,29 @@ __all__ = [
]
from lazr.enum import DBEnumeratedType, DBItem
-from lazr.restful.declarations import exported, exported_as_webservice_entry
+from lazr.restful.declarations import (
+ REQUEST_USER,
+ call_with,
+ collection_default_content,
+ export_write_operation,
+ exported,
+ exported_as_webservice_collection,
+ exported_as_webservice_entry,
+ operation_for_version,
+ operation_parameters,
+)
from lazr.restful.fields import CollectionField, Reference
+from lazr.restful.interface import copy_field
from zope.interface import Interface
from zope.schema import Bool, Choice, Datetime, Int, List, TextLine
from lp import _
from lp.app.enums import InformationType
from lp.app.interfaces.informationtype import IInformationType
-from lp.bugs.enums import VulnerabilityStatus
+from lp.bugs.enums import VulnerabilityHandlerEnum, VulnerabilityStatus
from lp.bugs.interfaces.bugtask import BugTaskImportance
from lp.bugs.interfaces.cve import ICve
+from lp.code.interfaces.gitrepository import IGitRepository
from lp.registry.interfaces.distribution import IDistribution
from lp.registry.interfaces.person import IPerson
@@ -329,7 +341,7 @@ class IVulnerabilityEdit(Interface):
# XXX lgp171188 2022-04-20 bug=760849: "beta" is a lie to get WADL
# generation working. Individual attributes must set their version to
# "devel".
-@exported_as_webservice_entry(as_of="beta")
+@exported_as_webservice_entry(plural_name="vulnerabilities", as_of="beta")
class IVulnerability(
IVulnerabilityView,
IVulnerabilityEditableAttributes,
@@ -339,6 +351,7 @@ class IVulnerability(
"""A vulnerability."""
+@exported_as_webservice_collection(IVulnerability)
class IVulnerabilitySet(Interface):
"""The set of all vulnerabilities."""
@@ -381,6 +394,94 @@ class IVulnerabilitySet(Interface):
def findByIds(vulnerability_ids, visible_by_user=None):
"""Returns the vulnerabilities with the given IDs."""
+ @operation_parameters(
+ handler=Choice(
+ title=_("The handler to import or export vulnerability data"),
+ readonly=True,
+ required=True,
+ vocabulary=VulnerabilityHandlerEnum,
+ ),
+ git_repository=Reference(schema=IGitRepository),
+ git_ref=TextLine(
+ title=_("The git reference to import from."),
+ required=True,
+ ),
+ git_paths=List(
+ title=_("The list of paths to import from."),
+ value_type=TextLine(),
+ required=True,
+ ),
+ information_type=copy_field(
+ IVulnerabilityEditableAttributes["information_type"]
+ ),
+ import_since_commit_sha1=TextLine(
+ title=_(
+ "Commit sha1 from which to do the import. All files added or "
+ "modified since this commit will be imported."
+ ),
+ required=False,
+ ),
+ )
+ @call_with(requester=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def importData(
+ requester,
+ handler,
+ git_repository,
+ git_ref,
+ git_paths,
+ information_type,
+ import_since_commit_sha1,
+ ):
+ """Trigger a vulnerability data import
+
+ :param handler: The handler that will perform the import.
+ :param git_repository: The git repository to import from.
+ :param git_ref: The git reference to import from.
+ :param git_paths: The paths in the git repo to import from.
+ :param information_type: The information type of the vulnerability.
+ If the git repo is private, the information type must be PRIVATE.
+ :param import_since_commit_sha1: The commit SHA1 to import from.
+ If null, import all data.
+ """
+
+ @operation_parameters(
+ handler=Choice(
+ title=_("The handler to import or export vulnerability data"),
+ readonly=True,
+ required=True,
+ vocabulary=VulnerabilityHandlerEnum,
+ ),
+ sources=List(
+ title=_(
+ "List of objects to get data from. If null, import all data."
+ ),
+ required=False,
+ ),
+ )
+ @call_with(requester=REQUEST_USER)
+ @export_write_operation()
+ @operation_for_version("devel")
+ def exportData(
+ requester,
+ handler,
+ sources,
+ ):
+ """Trigger a vulnerability data export
+
+ :param handler: The handler that will perform the export.
+ :param sources: List of objects to get data from. If null, import all
+ data.
+ """
+
+ @collection_default_content()
+ def empty_list():
+ """Return an empty collection of vulnerabilities.
+
+ This only exists to keep lazr.restful happy.
+ """
+
class IVulnerabilityActivity(Interface):
"""`IVulnerabilityActivity` attributes that require launchpad.View."""
diff --git a/lib/lp/bugs/interfaces/webservice.py b/lib/lp/bugs/interfaces/webservice.py
index 3524273..e69d8e8 100644
--- a/lib/lp/bugs/interfaces/webservice.py
+++ b/lib/lp/bugs/interfaces/webservice.py
@@ -35,6 +35,7 @@ __all__ = [
"IllegalRelatedBugTasksParams",
"IllegalTarget",
"IVulnerability",
+ "IVulnerabilitySet",
"NominationError",
"NominationSeriesObsoleteError",
"UserCannotEditBugTaskAssignee",
@@ -81,7 +82,7 @@ from lp.bugs.interfaces.structuralsubscription import (
IStructuralSubscription,
IStructuralSubscriptionTarget,
)
-from lp.bugs.interfaces.vulnerability import IVulnerability
+from lp.bugs.interfaces.vulnerability import IVulnerability, IVulnerabilitySet
from lp.code.interfaces.branchmergeproposal import IBranchMergeProposal
from lp.registry.interfaces.distributionsourcepackage import (
IDistributionSourcePackage,
diff --git a/lib/lp/bugs/model/tests/test_vulnerability.py b/lib/lp/bugs/model/tests/test_vulnerability.py
index 52d6a13..67b9597 100644
--- a/lib/lp/bugs/model/tests/test_vulnerability.py
+++ b/lib/lp/bugs/model/tests/test_vulnerability.py
@@ -6,6 +6,7 @@ from fixtures import MockPatch
from storm.store import Store
from testtools.matchers import MatchesStructure
from zope.component import getUtility
+from zope.security.interfaces import Unauthorized
from zope.security.proxy import removeSecurityProxy
from lp.app.enums import InformationType
@@ -15,7 +16,7 @@ from lp.app.errors import (
)
from lp.app.interfaces.launchpad import ILaunchpadCelebrities
from lp.app.interfaces.services import IService
-from lp.bugs.enums import VulnerabilityStatus
+from lp.bugs.enums import VulnerabilityHandlerEnum, VulnerabilityStatus
from lp.bugs.interfaces.buglink import IBugLinkTarget
from lp.bugs.interfaces.bugtask import BugTaskImportance
from lp.bugs.interfaces.vulnerability import (
@@ -23,6 +24,12 @@ from lp.bugs.interfaces.vulnerability import (
IVulnerabilitySet,
VulnerabilityChange,
)
+from lp.bugs.interfaces.vulnerabilityjob import IImportVulnerabilityJobSource
+from lp.bugs.model.importvulnerabilityjob import ImportVulnerabilityJob
+from lp.bugs.model.vulnerability import (
+ VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG,
+ UnauthorizedVulnerabilityHandler,
+)
from lp.bugs.model.vulnerabilitysubscription import VulnerabilitySubscription
from lp.registry.enums import BugSharingPolicy, TeamMembershipPolicy
from lp.services.webapp.authorization import check_permission
@@ -30,10 +37,14 @@ from lp.testing import (
TestCaseWithFactory,
admin_logged_in,
anonymous_logged_in,
+ api_url,
+ feature_flags,
person_logged_in,
+ set_feature_flag,
verifyObject,
)
from lp.testing.layers import DatabaseFunctionalLayer
+from lp.testing.pages import webservice_for_person
def grant_access_to_non_public_vulnerability(vulnerability, person):
@@ -639,3 +650,385 @@ class TestVulnerabilitySet(TestCaseWithFactory):
visible_by_user=person,
),
)
+
+
+class TestVulnerabilitySetImportData(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super().setUp()
+ self.requester = self.factory.makePerson()
+ self.handler = VulnerabilityHandlerEnum.SOSS
+ self.git_ref = "ref/heads/main"
+ self.git_paths = ["cves"]
+ self.information_type = InformationType.PROPRIETARY
+ self.team = self.factory.makeTeam(members=[self.requester])
+
+ def test_importData(self):
+ """Test that we can create a ImportVulnerabilityJob using importData
+ method.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ self.factory.makeDistribution(name="soss", owner=self.requester)
+
+ repo = self.factory.makeGitRepository(
+ owner=self.team, information_type=InformationType.USERDATA
+ )
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ with person_logged_in(self.requester):
+ getUtility(IVulnerabilitySet).importData(
+ self.requester,
+ self.handler,
+ repo,
+ self.git_ref,
+ self.git_paths,
+ self.information_type,
+ import_since_commit_sha1=None,
+ )
+
+ job = getUtility(IImportVulnerabilityJobSource).get(self.handler)
+ naked_job = removeSecurityProxy(job)
+ self.assertIsInstance(naked_job, ImportVulnerabilityJob)
+ self.assertEqual(naked_job.git_repository, repo.git_https_url)
+ self.assertEqual(naked_job.git_ref, self.git_ref)
+ self.assertEqual(naked_job.git_paths, self.git_paths)
+ self.assertEqual(
+ naked_job.information_type, self.information_type.value
+ )
+ self.assertEqual(naked_job.import_since_commit_sha1, None)
+
+ def test_importData_feature_flag_disabled(self):
+ """Test that if the feature flag is disabled it raises the
+ NotImplementedError exception."""
+
+ # All parameters None, feature flag is the first check
+ self.assertRaisesWithContent(
+ NotImplementedError,
+ "Vulnerability import API is currently disabled",
+ getUtility(IVulnerabilitySet).importData,
+ *(None,) * 7,
+ )
+
+ def test_importData_git_repo_unauthorized(self):
+ """Test that a user cannot create a ImportVulnerabilityJob for a git
+ repository that is not visible for that user.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ self.factory.makeDistribution(name="soss", owner=self.requester)
+
+ repo = self.factory.makeGitRepository(
+ information_type=InformationType.PRIVATESECURITY
+ )
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ self.assertRaises(
+ Unauthorized,
+ getUtility(IVulnerabilitySet).importData,
+ self.requester,
+ self.handler,
+ repo,
+ self.git_ref,
+ self.git_paths,
+ self.information_type,
+ import_since_commit_sha1=None,
+ )
+
+ def test_importData_handler_unauthorized(self):
+ """Test that we cannot create a ImportVulnerabilityJob if the user is
+ not authorized to use the handler.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ self.factory.makeDistribution(name="soss")
+
+ repo = self.factory.makeGitRepository(
+ owner=self.team, information_type=InformationType.USERDATA
+ )
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ self.assertRaisesWithContent(
+ UnauthorizedVulnerabilityHandler,
+ f"You don't have enough rights to use {self.handler}",
+ getUtility(IVulnerabilitySet).importData,
+ self.requester,
+ self.handler,
+ repo,
+ self.git_ref,
+ self.git_paths,
+ self.information_type,
+ import_since_commit_sha1=None,
+ )
+
+ def test_importData_information_type_private(self):
+ """Test that we cannot create a ImportVulnerabilityJob when
+ information_type is public but the git repository is private.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ self.factory.makeDistribution(name="soss", owner=self.requester)
+
+ repo = self.factory.makeGitRepository(
+ owner=self.team, information_type=InformationType.USERDATA
+ )
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ information_type = InformationType.PUBLIC
+ with person_logged_in(self.requester):
+ self.assertRaisesWithContent(
+ ValueError,
+ "information_type must be PRIVATE when importing from a "
+ "private git repository",
+ getUtility(IVulnerabilitySet).importData,
+ self.requester,
+ self.handler,
+ repo,
+ self.git_ref,
+ self.git_paths,
+ information_type,
+ import_since_commit_sha1=None,
+ )
+
+ def test_importData_wrong_git_ref(self):
+ """Test that we cannot create a ImportVulnerabilityJob when git_ref is
+ not in the git repository.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ self.factory.makeDistribution(name="soss", owner=self.requester)
+
+ repo = self.factory.makeGitRepository(
+ owner=self.team, information_type=InformationType.USERDATA
+ )
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ with person_logged_in(self.requester):
+ self.assertRaisesWithContent(
+ ValueError,
+ "wrong-git-ref does not exist in the specified git repository",
+ getUtility(IVulnerabilitySet).importData,
+ self.requester,
+ self.handler,
+ repo,
+ "wrong-git-ref",
+ self.git_paths,
+ self.information_type,
+ import_since_commit_sha1=None,
+ )
+
+ def test_importData_wrong_import_since_commit_sha1(self):
+ """Test that we cannot create a ImportVulnerabilityJob when
+ import_since_commit_sha1 is not in git_ref.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ self.factory.makeDistribution(name="soss", owner=self.requester)
+
+ repo = self.factory.makeGitRepository(
+ owner=self.team, information_type=InformationType.USERDATA
+ )
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ import_since_commit_sha1 = "1" * 40
+ with person_logged_in(self.requester):
+ self.assertRaisesWithContent(
+ ValueError,
+ f"{import_since_commit_sha1} does not exist in "
+ f"{self.git_ref}",
+ getUtility(IVulnerabilitySet).importData,
+ self.requester,
+ self.handler,
+ repo,
+ self.git_ref,
+ self.git_paths,
+ self.information_type,
+ import_since_commit_sha1,
+ )
+
+
+class TestVulnerabilitySetWebService(TestCaseWithFactory):
+ layer = DatabaseFunctionalLayer
+
+ def setUp(self):
+ super().setUp()
+ self.vulnerability_set = getUtility(IVulnerabilitySet)
+ self.vulnerability_set_url = api_url(self.vulnerability_set)
+ self.requester = self.factory.makePerson()
+ self.handler = VulnerabilityHandlerEnum.SOSS
+ self.factory.makeDistribution(name="soss", owner=self.requester)
+ self.team = self.factory.makeTeam(members=[self.requester])
+ self.git_paths = ["cves"]
+ self.information_type = InformationType.PROPRIETARY
+ self.git_ref = "ref/heads/main"
+
+ def test_importData_webservice_required_arguments_missing(self):
+ """Test that importData webservice requires missing arguments."""
+ webservice = webservice_for_person(
+ self.requester,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ self.vulnerability_set_url,
+ "importData",
+ )
+ self.assertEqual(400, response.status)
+ self.assertEqual(
+ {
+ "git_paths: Required input is missing.",
+ "git_ref: Required input is missing.",
+ "git_repository: Required input is missing.",
+ "handler: Required input is missing.",
+ },
+ set(response.body.decode().split("\n")),
+ )
+
+ def test_importData_webservice(self):
+ """Test that we can create a ImportVulnerabilityJob using the
+ webservice.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ repo = self.factory.makeGitRepository(owner=self.team)
+ repo_url = api_url(repo)
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ webservice = webservice_for_person(
+ self.requester,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ self.vulnerability_set_url,
+ "importData",
+ handler=self.handler.title,
+ git_repository=repo_url,
+ git_ref=self.git_ref,
+ git_paths=self.git_paths,
+ information_type=self.information_type.title,
+ import_since_commit_sha1=None,
+ )
+ self.assertEqual(200, response.status)
+
+ def test_importData_webservice_feature_flag_disabled(self):
+ """Test that we cannot create a ImportVulnerabilityJob using the
+ webservice when the vulnerability import API is disabled.
+ """
+ repo = self.factory.makeGitRepository(owner=self.team)
+ repo_url = api_url(repo)
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ webservice = webservice_for_person(
+ self.requester,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ self.vulnerability_set_url,
+ "importData",
+ handler=self.handler.title,
+ git_repository=repo_url,
+ git_ref=self.git_ref,
+ git_paths=self.git_paths,
+ information_type=self.information_type.title,
+ import_since_commit_sha1=None,
+ )
+ self.assertEqual(500, response.status)
+ self.assertEqual(
+ "Vulnerability import API is currently disabled",
+ response.body.decode().split("\n")[0],
+ )
+
+ def test_importData_webservice_git_repo_unauthorized(self):
+ """Test that we cannot create a ImportVulnerabilityJob using the
+ webservice when the git repository is not visible by the user.
+ """
+ self.useContext(feature_flags())
+ set_feature_flag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG, "true")
+
+ owner = self.factory.makePerson()
+ repo = self.factory.makeGitRepository(
+ owner=owner,
+ information_type=InformationType.PRIVATESECURITY,
+ )
+ with person_logged_in(owner):
+ repo_url = api_url(repo)
+
+ self.factory.makeGitRefs(
+ repository=repo,
+ paths=[self.git_ref],
+ )
+
+ webservice = webservice_for_person(
+ self.requester,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ self.vulnerability_set_url,
+ "importData",
+ handler=self.handler.title,
+ git_repository=repo_url,
+ git_ref=self.git_ref,
+ git_paths=self.git_paths,
+ information_type=self.information_type.title,
+ import_since_commit_sha1=None,
+ )
+
+ # 401 Unauthorized
+ self.assertEqual(401, response.status)
+
+ def test_exportData_webservice_required_arguments_missing(self):
+ """Test that exportData webservice requires missing arguments."""
+ webservice = webservice_for_person(
+ self.requester,
+ default_api_version="devel",
+ )
+ response = webservice.named_post(
+ self.vulnerability_set_url,
+ "exportData",
+ )
+ self.assertEqual(400, response.status)
+ self.assertEqual(
+ {
+ "handler: Required input is missing.",
+ },
+ set(response.body.decode().split("\n")),
+ )
diff --git a/lib/lp/bugs/model/vulnerability.py b/lib/lp/bugs/model/vulnerability.py
index c33cc0f..4d4ba07 100644
--- a/lib/lp/bugs/model/vulnerability.py
+++ b/lib/lp/bugs/model/vulnerability.py
@@ -39,6 +39,7 @@ from lp.bugs.interfaces.vulnerability import (
IVulnerabilitySet,
VulnerabilityChange,
)
+from lp.bugs.interfaces.vulnerabilityjob import IImportVulnerabilityJobSource
from lp.bugs.model.bug import Bug
from lp.bugs.model.buglinktarget import BugLinkTargetMixin
from lp.bugs.model.vulnerabilitysubscription import VulnerabilitySubscription
@@ -56,8 +57,13 @@ from lp.services.database.enumcol import DBEnum
from lp.services.database.interfaces import IStore
from lp.services.database.stormbase import StormBase
from lp.services.database.stormexpr import Array, ArrayAgg, ArrayIntersects
+from lp.services.features import getFeatureFlag
from lp.services.xref.interfaces import IXRefSet
+VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG = (
+ "bugs.vulnerability_import_export.enabled"
+)
+
@implementer(IVulnerability, IBugLinkTarget)
class Vulnerability(StormBase, BugLinkTargetMixin, InformationTypeMixin):
@@ -333,6 +339,10 @@ class Vulnerability(StormBase, BugLinkTargetMixin, InformationTypeMixin):
@implementer(IVulnerabilitySet)
class VulnerabilitySet:
+ def __init__(self):
+ """See `IVulnerabilitySet`."""
+ self.title = "The Vulnerabilities collection"
+
def new(
self,
distribution,
@@ -382,6 +392,96 @@ class VulnerabilitySet:
clauses.append(get_vulnerability_privacy_filter(visible_by_user))
return IStore(Vulnerability).find(Vulnerability, *clauses)
+ def importData(
+ self,
+ requester,
+ handler,
+ git_repository,
+ git_ref,
+ git_paths,
+ information_type,
+ import_since_commit_sha1,
+ ):
+ """See `IVulnerabilitySet`."""
+
+ if not getFeatureFlag(VULNERABILITY_IMPORT_ENABLED_FEATURE_FLAG):
+ raise NotImplementedError(
+ "Vulnerability import API is currently disabled"
+ )
+
+ # Launchpad's object permissions automatically check requester's
+ # permissions to git repo
+
+ # Check requester's permissions to handler
+ from lp.bugs.enums import VulnerabilityHandlerEnum
+ from lp.bugs.scripts.soss.sossimport import SOSSImporter
+
+ if handler == VulnerabilityHandlerEnum.SOSS:
+ importer = SOSSImporter()
+ else:
+ raise Exception("Handler not found")
+
+ if not importer.checkUserPermissions(requester):
+ raise UnauthorizedVulnerabilityHandler(
+ f"You don't have enough rights to use {handler}"
+ )
+
+ # If repository is private, information_type must be PRIVATE
+ if (
+ git_repository.private
+ and information_type in PUBLIC_INFORMATION_TYPES
+ ):
+ # TODO check what's usually the validation error codes we use
+ raise ValueError(
+ "information_type must be PRIVATE when importing from a "
+ "private git repository"
+ )
+
+ repo_ref = git_repository.getRefByPath(git_ref)
+ if repo_ref is None:
+ raise ValueError(
+ f"{git_ref} does not exist in the specified git repository"
+ )
+
+ # Check import_since_commit_sha1 exists in git_ref
+ if import_since_commit_sha1 and not git_repository.checkCommitInRef(
+ import_since_commit_sha1, git_ref
+ ):
+ raise ValueError(
+ f"{import_since_commit_sha1} does not exist in {git_ref}"
+ )
+
+ # Trigger the import job after validations pass
+ job_source = getUtility(IImportVulnerabilityJobSource)
+ job_source.create(
+ handler,
+ git_repository.git_https_url,
+ git_ref,
+ git_paths,
+ information_type.value,
+ import_since_commit_sha1,
+ )
+
+ return None
+
+ def exportData(
+ self,
+ requester,
+ handler,
+ sources,
+ ):
+ """See `IVulnerabilitySet`."""
+
+ raise NotImplementedError(
+ "Vulnerability export API is currently disabled"
+ )
+
+ return None
+
+ def empty_list(self):
+ """See `IVulnerabilitySet`."""
+ return []
+
@implementer(IVulnerabilityActivity)
class VulnerabilityActivity(StormBase):
@@ -506,3 +606,7 @@ def get_vulnerability_privacy_filter(user):
policy_grant_query,
)
]
+
+
+class UnauthorizedVulnerabilityHandler(Exception):
+ """The user is not allowed to use the VulnerabilityHandler."""
diff --git a/lib/lp/bugs/scripts/soss/sossimport.py b/lib/lp/bugs/scripts/soss/sossimport.py
index c96ec23..773caf4 100644
--- a/lib/lp/bugs/scripts/soss/sossimport.py
+++ b/lib/lp/bugs/scripts/soss/sossimport.py
@@ -30,10 +30,12 @@ from lp.bugs.scripts.soss.models import SOSSRecord
from lp.registry.interfaces.distribution import IDistributionSet
from lp.registry.interfaces.externalpackage import ExternalPackageType
from lp.registry.interfaces.person import IPersonSet
+from lp.registry.interfaces.role import IPersonRoles
from lp.registry.interfaces.sourcepackagename import ISourcePackageNameSet
from lp.registry.model.distribution import Distribution
from lp.registry.model.externalpackage import ExternalPackage
from lp.registry.model.person import Person
+from lp.registry.security import SecurityAdminDistribution
from lp.testing import person_logged_in
__all__ = [
@@ -476,3 +478,8 @@ class SOSSImporter:
packagetype = first_item[0]
package = first_item[1][0]
return packagetype, package
+
+ def checkUserPermissions(self, user):
+ return SecurityAdminDistribution(self.soss).checkAuthenticated(
+ IPersonRoles(user)
+ )
diff --git a/lib/lp/bugs/scripts/soss/tests/test_sossimport.py b/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
index 1c692c1..d577e6a 100644
--- a/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
+++ b/lib/lp/bugs/scripts/soss/tests/test_sossimport.py
@@ -28,8 +28,11 @@ class TestSOSSImporter(TestCaseWithFactory):
self.soss_record = SOSSRecord.from_yaml(file)
self.cve = self.factory.makeCVE(sequence="2025-1979")
+ self.owner = self.factory.makePerson()
self.soss = self.factory.makeDistribution(
- name="soss", displayname="SOSS"
+ name="soss",
+ displayname="SOSS",
+ owner=self.owner,
)
transaction.commit()
@@ -438,3 +441,10 @@ class TestSOSSImporter(TestCaseWithFactory):
self.soss_record, f"CVE-{self.cve.sequence}"
)
self.assertEqual(valid, False)
+
+ def test_checkUserPermissions(self):
+ soss_importer = SOSSImporter()
+
+ user = self.factory.makePerson()
+ self.assertEqual(soss_importer.checkUserPermissions(user), False)
+ self.assertEqual(soss_importer.checkUserPermissions(self.owner), True)
diff --git a/lib/lp/code/interfaces/gitrepository.py b/lib/lp/code/interfaces/gitrepository.py
index e40be5d..f8bd045 100644
--- a/lib/lp/code/interfaces/gitrepository.py
+++ b/lib/lp/code/interfaces/gitrepository.py
@@ -770,6 +770,9 @@ class IGitRepositoryView(IHasRecipes, IAccessTokenTarget):
"yet been scanned."
)
+ def checkCommitInRef(paths):
+ """Check if a commit exists in a git ref."""
+
def updateMergeCommitIDs(paths):
"""Update commit SHA1s of merge proposals for this repository.
diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
index 483f986..465b876 100644
--- a/lib/lp/code/model/gitrepository.py
+++ b/lib/lp/code/model/gitrepository.py
@@ -1424,6 +1424,16 @@ class GitRepository(
)
return not jobs.is_empty()
+ def checkCommitInRef(self, commit, ref):
+ """See `IGitRepository`."""
+ store = Store.of(self)
+ return not store.find(
+ GitRef.commit_sha1,
+ GitRef.repository_id == self.id,
+ GitRef.commit_sha1 == commit,
+ GitRef.path == ref,
+ ).is_empty()
+
def updateMergeCommitIDs(self, paths):
"""See `IGitRepository`."""
store = Store.of(self)
diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
index 4e48291..62f50be 100644
--- a/lib/lp/code/model/tests/test_gitrepository.py
+++ b/lib/lp/code/model/tests/test_gitrepository.py
@@ -2560,6 +2560,44 @@ class TestGitRepositoryRefs(TestCaseWithFactory):
hosting_fixture.getRefs.extract_kwargs(),
)
+ master_sha1 = hashlib.sha1(b"refs/heads/master").hexdigest()
+ author = self.factory.makePerson()
+ with person_logged_in(author):
+ author_email = author.preferredemail.email
+ author_date = datetime(2015, 1, 1, tzinfo=timezone.utc)
+ committer_date = datetime(2015, 1, 2, tzinfo=timezone.utc)
+ self.useFixture(
+ GitHostingFixture(
+ commits=[
+ {
+ "sha1": master_sha1,
+ "message": "tip of master",
+ "author": {
+ "name": author.displayname,
+ "email": author_email,
+ "time": int(seconds_since_epoch(author_date)),
+ },
+ "committer": {
+ "name": "New Person",
+ "email": "new-person@xxxxxxxxxxx",
+ "time": int(seconds_since_epoch(committer_date)),
+ },
+ "parents": [],
+ "tree": hashlib.sha1(b"").hexdigest(),
+ }
+ ]
+ )
+ )
+
+ def test_checkCommitInRef(self):
+ repository = self.factory.makeGitRepository()
+ paths = ["refs/heads/master"]
+ ref = self.factory.makeGitRefs(repository=repository, paths=paths)
+ result = repository.checkCommitInRef(
+ ref[0].commit_sha1, "refs/heads/master"
+ )
+ self.assertEqual(result, True)
+
def test_fetchRefCommits(self):
# fetchRefCommits fetches detailed tip commit metadata for the
# requested refs.
diff --git a/lib/lp/registry/security.py b/lib/lp/registry/security.py
index 03692fe..2d8a2b6 100644
--- a/lib/lp/registry/security.py
+++ b/lib/lp/registry/security.py
@@ -6,6 +6,7 @@
__all__ = [
"EditByOwnersOrAdmins",
"PublicOrPrivateTeamsExistence",
+ "SecurityAdminDistribution",
]
from storm.expr import Select, Union
diff --git a/lib/lp/services/features/flags.py b/lib/lp/services/features/flags.py
index d904851..c4472d8 100644
--- a/lib/lp/services/features/flags.py
+++ b/lib/lp/services/features/flags.py
@@ -330,6 +330,15 @@ flag_info = sorted(
"",
"",
),
+ (
+ "bugs.vulnerability_import_export.enabled",
+ "boolean",
+ "If true, users with the right permissions can trigger "
+ "vulnerability data imports and exports via API",
+ "",
+ "",
+ "",
+ ),
]
)
Follow ups