Skip to content

Commit

Permalink
Add REST API endpoint to download ABOUT files and SPDX document #60
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez committed Apr 23, 2024
1 parent 98eac00 commit 1380858
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 53 deletions.
70 changes: 70 additions & 0 deletions dje/outputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# DejaCode is a trademark of nexB Inc.
# SPDX-License-Identifier: AGPL-3.0-only
# See https://github.com/nexB/dejacode for support or download.
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

import re

from dejacode import __version__ as dejacode_version
from dejacode_toolkit import spdx


def safe_filename(filename):
"""Convert the provided `filename` to a safe filename."""
return re.sub("[^A-Za-z0-9.-]+", "_", filename).lower()


def get_spdx_extracted_licenses(spdx_packages):
"""
Return all the licenses to be included in the SPDX extracted_licenses.
Those include the `LicenseRef-` licenses, ie: licenses not available in the
SPDX list.
In the case of Product relationships, ProductComponent and ProductPackage,
the set of licenses of the related object, Component or Package, is used
as the licenses of the relationship is always a subset of the ones of the
related object.
This ensures that we have all the license required for a valid SPDX document.
"""
from product_portfolio.models import ProductRelationshipMixin

all_licenses = set()
for entry in spdx_packages:
if isinstance(entry, ProductRelationshipMixin):
all_licenses.update(entry.related_component_or_package.licenses.all())
else:
all_licenses.update(entry.licenses.all())

return [
license.as_spdx() for license in all_licenses if license.spdx_id.startswith("LicenseRef")
]


def get_spdx_document(instance, user):
spdx_packages = instance.get_spdx_packages()

creation_info = spdx.CreationInfo(
person_name=f"{user.first_name} {user.last_name}",
person_email=user.email,
organization_name=user.dataspace.name,
tool=f"DejaCode-{dejacode_version}",
)

document = spdx.Document(
name=f"dejacode_{instance.dataspace.name}_{instance._meta.model_name}_{instance}",
namespace=f"https://dejacode.com/spdxdocs/{instance.uuid}",
creation_info=creation_info,
packages=[package.as_spdx() for package in spdx_packages],
extracted_licenses=get_spdx_extracted_licenses(spdx_packages),
)

return document


def get_spdx_filename(spdx_document):
document_name = spdx_document.as_dict()["name"]
filename = f"{document_name}.spdx.json"
return safe_filename(filename)
55 changes: 3 additions & 52 deletions dje/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,10 +76,10 @@

from component_catalog.license_expression_dje import get_license_objects
from dejacode import __version__ as dejacode_version
from dejacode_toolkit import spdx
from dejacode_toolkit.purldb import PurlDB
from dejacode_toolkit.scancodeio import ScanCodeIO
from dejacode_toolkit.vulnerablecode import VulnerableCode
from dje import outputs
from dje.copier import COPY_DEFAULT_EXCLUDE
from dje.copier import SKIP
from dje.copier import get_object_in
Expand Down Expand Up @@ -2316,47 +2316,19 @@ def get_context_data(self, **kwargs):
return context


def get_spdx_extracted_licenses(spdx_packages):
"""
Return all the licenses to be included in the SPDX extracted_licenses.
Those include the `LicenseRef-` licenses, ie: licenses not available in the
SPDX list.
In the case of Product relationships, ProductComponent and ProductPackage,
the set of licenses of the related object, Component or Package, is used
as the licenses of the relationship is always a subset of the ones of the
related object.
This ensures that we have all the license required for a valid SPDX document.
"""
from product_portfolio.models import ProductRelationshipMixin

all_licenses = set()
for entry in spdx_packages:
if isinstance(entry, ProductRelationshipMixin):
all_licenses.update(entry.related_component_or_package.licenses.all())
else:
all_licenses.update(entry.licenses.all())

return [
license.as_spdx() for license in all_licenses if license.spdx_id.startswith("LicenseRef")
]


class ExportSPDXDocumentView(
LoginRequiredMixin,
DataspaceScopeMixin,
GetDataspacedObjectMixin,
BaseDetailView,
):
def get(self, request, *args, **kwargs):
instance = self.get_object()
spdx_document = self.get_spdx_document(instance, self.request.user)
document_name = spdx_document.as_dict()["name"]
filename = f"{document_name}.spdx.json"
spdx_document = outputs.get_spdx_document(self.get_object(), self.request.user)

if not spdx_document:
raise Http404

filename = outputs.get_spdx_filename(spdx_document)
response = FileResponse(
spdx_document.as_json(),
filename=filename,
Expand All @@ -2366,27 +2338,6 @@ def get(self, request, *args, **kwargs):

return response

@staticmethod
def get_spdx_document(instance, user):
spdx_packages = instance.get_spdx_packages()

creation_info = spdx.CreationInfo(
person_name=f"{user.first_name} {user.last_name}",
person_email=user.email,
organization_name=user.dataspace.name,
tool=f"DejaCode-{dejacode_version}",
)

document = spdx.Document(
name=f"dejacode_{instance.dataspace.name}_{instance._meta.model_name}_{instance}",
namespace=f"https://dejacode.com/spdxdocs/{instance.uuid}",
creation_info=creation_info,
packages=[package.as_spdx() for package in spdx_packages],
extracted_licenses=get_spdx_extracted_licenses(spdx_packages),
)

return document


class ExportCycloneDXBOMView(
LoginRequiredMixin,
Expand Down
33 changes: 32 additions & 1 deletion product_portfolio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#

from django.core.exceptions import ValidationError
from django.http import FileResponse
from django.http import Http404

import django_filters
from rest_framework import permissions
Expand All @@ -20,6 +22,7 @@
from component_catalog.api import PackageEmbeddedSerializer
from component_catalog.api import ValidateLicenseExpressionMixin
from component_catalog.license_expression_dje import clean_related_expression
from dje import outputs
from dje.api import CreateRetrieveUpdateListViewSet
from dje.api import DataspacedAPIFilterSet
from dje.api import DataspacedHyperlinkedRelatedField
Expand All @@ -33,6 +36,7 @@
from dje.filters import MultipleUUIDFilter
from dje.filters import NameVersionFilter
from dje.permissions import assign_all_object_permissions
from dje.views import SendAboutFilesMixin
from product_portfolio.filters import ComponentCompletenessAPIFilter
from product_portfolio.forms import ImportFromScanForm
from product_portfolio.forms import ImportManifestsForm
Expand Down Expand Up @@ -268,7 +272,7 @@ class PullProjectDataSerializer(serializers.Serializer):
)


class ProductViewSet(CreateRetrieveUpdateListViewSet):
class ProductViewSet(SendAboutFilesMixin, CreateRetrieveUpdateListViewSet):
queryset = Product.objects.none()
serializer_class = ProductSerializer
filterset_class = ProductFilterSet
Expand Down Expand Up @@ -400,6 +404,33 @@ def pull_scancodeio_project_data(self, request, *args, **kwargs):

return Response({"status": "Packages import from ScanCode.io in progress..."})

@action(detail=True)
def about_files(self, request, uuid):
instance = self.get_object()
about_files = instance.get_about_files()
filename = self.get_filename(instance)
return self.get_zipped_response(about_files, filename)

@action(detail=True)
def spdx_document(self, request, uuid):
spdx_document = outputs.get_spdx_document(
instance=self.get_object(),
user=self.request.user,
)

if not spdx_document:
raise Http404

filename = outputs.get_spdx_filename(spdx_document)
response = FileResponse(
spdx_document.as_json(),
filename=filename,
content_type="application/json",
)
response["Content-Disposition"] = f'attachment; filename="{filename}"'

return response


class BaseProductRelationSerializer(ValidateLicenseExpressionMixin, DataspacedSerializer):
product = NameVersionHyperlinkedRelatedField(
Expand Down

0 comments on commit 1380858

Please sign in to comment.