Skip to content

Commit

Permalink
feat: export license policy to JSON and YAML (#2356)
Browse files Browse the repository at this point in the history
* feat: export license policy to JSON and YAML

* fix: typo in unittest
  • Loading branch information
StefanFl authored Dec 17, 2024
1 parent f39a2e8 commit d25f2ea
Show file tree
Hide file tree
Showing 11 changed files with 473 additions and 15 deletions.
30 changes: 30 additions & 0 deletions backend/application/commons/services/export.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import json
from datetime import datetime
from typing import Any

import jsonpickle
from defusedcsv import csv
from django.db.models.query import QuerySet
from django.http import HttpResponse
Expand Down Expand Up @@ -98,3 +100,31 @@ def export_csv(
fields.append(value)

writer.writerow(fields)


def object_to_json(object_to_encode: Any) -> str:
jsonpickle.set_encoder_options("json", ensure_ascii=False)
json_string = jsonpickle.encode(object_to_encode, unpicklable=False)

json_dict = json.loads(json_string)
json_dict = _remove_empty_elements(json_dict)

return json.dumps(json_dict, indent=4, sort_keys=True, ensure_ascii=False)


def _remove_empty_elements(d: dict) -> dict:
"""recursively remove empty lists, empty dicts, or None elements from a dictionary"""

def empty(x):
return x is None or x == {} or x == []

if not isinstance(d, (dict, list)):
return d
if isinstance(d, list):
return [v for v in (_remove_empty_elements(v) for v in d) if not empty(v)]

return {
k: v
for k, v in ((k, _remove_empty_elements(v)) for k, v in d.items())
if not empty(v)
}
68 changes: 68 additions & 0 deletions backend/application/licenses/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Optional, Tuple

from django.db.models.query import QuerySet
from django.http import HttpResponse
from django_filters.rest_framework import DjangoFilterBackend
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.decorators import action
Expand All @@ -15,6 +16,7 @@

from application.access_control.services.authorization import user_has_permission_or_403
from application.access_control.services.roles_permissions import Permissions
from application.commons.services.global_request import get_current_user
from application.core.models import Branch, Product
from application.core.queries.branch import get_branch_by_id
from application.core.queries.product import get_product_by_id
Expand Down Expand Up @@ -99,6 +101,10 @@
get_license_policy_member,
get_license_policy_members,
)
from application.licenses.services.export_license_policy import (
export_license_policy_json,
export_license_policy_yaml,
)
from application.licenses.services.license_group import (
copy_license_group,
import_scancode_licensedb,
Expand Down Expand Up @@ -545,6 +551,68 @@ def apply_product(self, request):
status=HTTP_204_NO_CONTENT,
)

@extend_schema(
methods=["GET"],
responses={200: None},
)
@action(detail=True, methods=["get"])
def export_json(self, request, pk=None):
license_policy = self._get_license_policy(pk, False)
license_policy_export = export_license_policy_json(license_policy)

response = HttpResponse( # pylint: disable=http-response-with-content-type-json
content=license_policy_export,
content_type="application/json",
)
response["Content-Disposition"] = (
f"attachment; filename=license_policy_{pk}.json"
)

return response

@extend_schema(
methods=["GET"],
responses={200: None},
)
@action(detail=True, methods=["get"])
def export_yaml(self, request, pk=None):
license_policy = self._get_license_policy(pk, False)
license_policy_export = export_license_policy_yaml(license_policy)

response = HttpResponse(
content=license_policy_export,
content_type="application/yaml",
)
response["Content-Disposition"] = (
f"attachment; filename=license_policy_{pk}.yaml"
)

return response

def _get_license_policy(self, pk: int, manager: bool) -> License_Policy:
license_policy = get_license_policy(pk)
if license_policy is None:
raise NotFound("License policy not found")

if not manager and license_policy.is_public:
return license_policy

user = get_current_user()
if not user:
raise PermissionDenied("No user found")

if user.is_superuser:
return license_policy

license_policy_member = get_license_policy_member(license_policy, user)
if not license_policy.is_public and not license_policy_member:
raise NotFound("License policy not found")

if manager and license_policy_member and not license_policy_member.is_manager:
raise PermissionDenied("You are not a manager of this license policy")

return license_policy


class LicensePolicyItemViewSet(ModelViewSet):
serializer_class = LicensePolicyItemSerializer
Expand Down
83 changes: 83 additions & 0 deletions backend/application/licenses/services/export_license_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import json
from dataclasses import dataclass
from typing import Optional

import yaml

from application.commons.services.export import object_to_json
from application.licenses.models import License_Policy, License_Policy_Item
from application.licenses.services.license_policy import get_ignore_component_type_list


@dataclass
class License_Policy_Export_Item:
evaluation_result: str
spdx_license: Optional[str] = None
license_expression: Optional[str] = None
unknown_license: Optional[str] = None
license_group: Optional[str] = None


@dataclass
class License_Policy_Export_Ignore_Component_Type:
component_type: str


@dataclass
class License_Policy_Export:
name: str
description: str
items: list[License_Policy_Export_Item]
ignore_component_types: list[License_Policy_Export_Ignore_Component_Type]


def export_license_policy_yaml(license_policy: License_Policy) -> str:
return yaml.dump(json.loads(export_license_policy_json(license_policy)))


def export_license_policy_json(license_policy: License_Policy) -> str:
return object_to_json(_create_license_policy_export(license_policy))


def _create_license_policy_export(
license_policy: License_Policy,
) -> License_Policy_Export:
license_policy_eport = License_Policy_Export(
name=license_policy.name,
description=license_policy.description,
items=[],
ignore_component_types=get_ignore_component_type_list(
license_policy.ignore_component_types
),
)

license_policy_item: Optional[License_Policy_Item] = None
for license_policy_item in license_policy.license_policy_items.all():
if license_policy_item.license_group:
for spdx_license in license_policy_item.license_group.licenses.all():
license_policy_eport_item = License_Policy_Export_Item(
spdx_license=spdx_license.spdx_id,
license_group=license_policy_item.license_group.name,
evaluation_result=license_policy_item.evaluation_result,
)
license_policy_eport.items.append(license_policy_eport_item)
elif license_policy_item.license:
license_policy_eport_item = License_Policy_Export_Item(
spdx_license=license_policy_item.license.spdx_id,
evaluation_result=license_policy_item.evaluation_result,
)
license_policy_eport.items.append(license_policy_eport_item)
elif license_policy_item.license_expression:
license_policy_eport_item = License_Policy_Export_Item(
license_expression=license_policy_item.license_expression,
evaluation_result=license_policy_item.evaluation_result,
)
license_policy_eport.items.append(license_policy_eport_item)
elif license_policy_item.unknown_license:
license_policy_eport_item = License_Policy_Export_Item(
unknown_license=license_policy_item.unknown_license,
evaluation_result=license_policy_item.evaluation_result,
)
license_policy_eport.items.append(license_policy_eport_item)

return license_policy_eport
18 changes: 9 additions & 9 deletions backend/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@ requests = "2.32.3" # https://github.com/psf/requests
# ------------------------------------------------------------------------------
pymysql = "1.1.1" # https://github.com/PyMySQL/PyMySQL
psycopg = { version = "3.2.3", extras = ["binary"] } # https://github.com/psycopg/psycopg
# Excel and CSV
# Dataformats
# ------------------------------------------------------------------------------
defusedcsv = "2.0.0" # https://github.com/raphaelm/defusedcsv
openpyxl = "3.1.5" # https://foss.heptapod.net/openpyxl/openpyxl
# Package URL
jsonpickle = "4.0.1" # https://github.com/jsonpickle/jsonpickle
pyyaml = "6.0.2" # https://github.com/yaml/pyyaml
# Field specifications
# ------------------------------------------------------------------------------
packageurl-python = "0.16.0" # https://github.com/package-url/packageurl-python
cvss = "3.3" # https://github.com/RedHatProductSecurity/cvss
semver = "3.0.2" # https://github.com/python-semver/python-semver
license-expression = "30.4.0" # https://github.com/aboutcode-org/license-expression
# Task queue
# ------------------------------------------------------------------------------
huey = "2.5.2" # https://github.com/coleifer/huey
Expand All @@ -55,11 +60,7 @@ jira = "3.8.0" # https://github.com/pycontribs/jira
# ------------------------------------------------------------------------------
inflect = "7.4.0" # https://github.com/jaraco/inflect
validators = "0.34.0" # https://github.com/python-validators/validators
cvss = "3.3" # https://github.com/RedHatProductSecurity/cvss
jsonpickle = "4.0.1" # https://github.com/jsonpickle/jsonpickle
py-ocsf-models = "0.2.0" # https://github.com/prowler-cloud/py-ocsf-models
semver = "3.0.2" # https://github.com/python-semver/python-semver
license-expression = "30.4.0" # https://github.com/aboutcode-org/license-expression

# Development dependencies
# ------------------------------------------------------------------------------
Expand Down
2 changes: 2 additions & 0 deletions backend/unittests/access_control/api/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,8 @@ def test_authentication(self, mock_user):
self._check_authentication(["post"], "/api/license_policies/1/copy/")
self._check_authentication(["post"], "/api/license_policies/1/apply/")
self._check_authentication(["post"], "/api/license_policies/apply_product/")
self._check_authentication(["get"], "/api/license_policies/1/export_json/")
self._check_authentication(["get"], "/api/license_policies/1/export_yaml/")

self._check_authentication(["get", "post"], "/api/license_policy_items/")
self._check_authentication(
Expand Down
Loading

0 comments on commit d25f2ea

Please sign in to comment.