Skip to content
347 changes: 336 additions & 11 deletions cyclonedx/model/license.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"""

from collections.abc import Iterable
from datetime import datetime
from enum import Enum
from json import loads as json_loads
from typing import TYPE_CHECKING, Any, Optional, Union
Expand All @@ -38,6 +39,7 @@
from ..schema.schema import SchemaVersion1Dot5, SchemaVersion1Dot6, SchemaVersion1Dot7
from . import AttachedText, Property, XsUri
from .bom_ref import BomRef
from .contact import OrganizationalContact, OrganizationalEntity


@serializable.serializable_enum
Expand Down Expand Up @@ -68,6 +70,319 @@ class LicenseAcknowledgement(str, Enum):
"""


@serializable.serializable_enum
class LicenseType(str, Enum):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add tests for completeness to tests/test_enums.py

"""
This is our internal representation of the `licenseTypeEnumeration` ENUM type
within the CycloneDX standard.

.. note::
Introduced in CycloneDX v1.5

.. note::
See the CycloneDX Schema:
https://cyclonedx.org/docs/1.7/json/#metadata_tools_oneOf_i0_components_items_licenses_items_oneOf_i0_license_licensing_licenseTypes
"""

Comment thread
lmco-shuynh marked this conversation as resolved.
ACADEMIC = 'academic'
APPLIANCE = 'appliance'
CLIENT_ACCESS = 'client-access'
CONCURRENT_USER = 'concurrent-user'
CORE_POINTS = 'core-points'
CUSTOM_METRIC = 'custom-metric'
DEVICE = 'device'
EVALUATION = 'evaluation'
NAMED_USER = 'named-user'
NODE_LOCKED = 'node-locked'
OEM = 'oem'
PERPETUAL = 'perpetual'
PROCESSOR_POINTS = 'processor-points'
SUBSCRIPTION = 'subscription'
USER = 'user'
OTHER = 'other'


@serializable.serializable_class(ignore_unknown_during_deserialization=True)
class LicenseEntity:
"""
This is our internal representation of the licensor/licensee/purchaser type
within the CycloneDX standard.

Exactly one of ``organization`` or ``individual`` MUST be provided.

.. note::
Introduced in CycloneDX v1.5

.. note::
See the CycloneDX Schema definition:
https://cyclonedx.org/docs/1.7/json/#metadata_tools_oneOf_i0_components_items_licenses_items_oneOf_i0_license_licensing_licensor
"""

def __init__(
self, *,
organization: Optional[OrganizationalEntity] = None,
individual: Optional[OrganizationalContact] = None,
) -> None:
if not organization and not individual:
raise MutuallyExclusivePropertiesException(
'Either `organization` or `individual` MUST be supplied'
)
if organization and individual:
raise MutuallyExclusivePropertiesException(
'Only one of `organization` or `individual` MUST be supplied - not both'
)
self._organization = organization
self._individual = individual

@property
@serializable.xml_sequence(1)
def organization(self) -> Optional[OrganizationalEntity]:
"""
The organization.

Returns:
`OrganizationalEntity` or `None`
"""
return self._organization

@organization.setter
def organization(self, organization: Optional[OrganizationalEntity]) -> None:
self._organization = organization
if organization is not None:
self._individual = None

@property
@serializable.xml_sequence(2)
def individual(self) -> Optional[OrganizationalContact]:
"""
The individual.

Returns:
`OrganizationalContact` or `None`
"""
return self._individual

@individual.setter
def individual(self, individual: Optional[OrganizationalContact]) -> None:
self._individual = individual
if individual is not None:
self._organization = None

def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
self._organization, self._individual,
))

def __eq__(self, other: object) -> bool:
if isinstance(other, LicenseEntity):
return self.__comparable_tuple() == other.__comparable_tuple()
return False

def __lt__(self, other: Any) -> bool:
if isinstance(other, LicenseEntity):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented

def __hash__(self) -> int:
return hash(self.__comparable_tuple())

def __repr__(self) -> str:
return f'<LicenseEntity organization={self._organization!r}, individual={self._individual!r}>'


@serializable.serializable_class(ignore_unknown_during_deserialization=True)
class Licensing:
"""
This is our internal representation of the `licensingType` complex type
within the CycloneDX standard.

Licensing details describing the licensor/licensee, license type, renewal
and expiration dates, and other important metadata.

.. note::
Introduced in CycloneDX v1.5

.. note::
See the CycloneDX Schema definition:
https://cyclonedx.org/docs/1.7/json/#metadata_tools_oneOf_i0_components_items_licenses_items_oneOf_i0_license_licensing
"""

def __init__(
self, *,
alt_ids: Optional[Iterable[str]] = None,
licensor: Optional[LicenseEntity] = None,
licensee: Optional[LicenseEntity] = None,
purchaser: Optional[LicenseEntity] = None,
purchase_order: Optional[str] = None,
license_types: Optional[Iterable[LicenseType]] = None,
last_renewal: Optional[datetime] = None,
expiration: Optional[datetime] = None,
) -> None:
self.alt_ids = alt_ids or []
self.licensor = licensor
self.licensee = licensee
self.purchaser = purchaser
self.purchase_order = purchase_order
self.license_types = license_types or []
self.last_renewal = last_renewal
self.expiration = expiration

@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'altId')
@serializable.xml_sequence(1)
def alt_ids(self) -> 'SortedSet[str]':
"""
License identifiers that may be used to manage licenses and their lifecycle.

Returns:
`SortedSet[str]`
"""
return self._alt_ids

@alt_ids.setter
def alt_ids(self, alt_ids: Iterable[str]) -> None:
self._alt_ids = SortedSet(alt_ids)

@property
@serializable.xml_sequence(2)
def licensor(self) -> Optional[LicenseEntity]:
"""
The individual or organization that grants a license to another individual or organization.

Returns:
`LicenseEntity` or `None`
"""
return self._licensor

@licensor.setter
def licensor(self, licensor: Optional[LicenseEntity]) -> None:
self._licensor = licensor

@property
@serializable.xml_sequence(3)
def licensee(self) -> Optional[LicenseEntity]:
"""
The individual or organization for which a license was granted to.

Returns:
`LicenseEntity` or `None`
"""
return self._licensee

@licensee.setter
def licensee(self, licensee: Optional[LicenseEntity]) -> None:
self._licensee = licensee

@property
@serializable.xml_sequence(4)
def purchaser(self) -> Optional[LicenseEntity]:
"""
The individual or organization that purchased the license.

Returns:
`LicenseEntity` or `None`
"""
return self._purchaser

@purchaser.setter
def purchaser(self, purchaser: Optional[LicenseEntity]) -> None:
self._purchaser = purchaser

@property
@serializable.xml_sequence(5)
def purchase_order(self) -> Optional[str]:
"""
The purchase order identifier the purchaser sent to a supplier or vendor to
authorize a purchase.

Returns:
`str` or `None`
"""
return self._purchase_order

@purchase_order.setter
def purchase_order(self, purchase_order: Optional[str]) -> None:
self._purchase_order = purchase_order

@property
@serializable.xml_array(serializable.XmlArraySerializationType.NESTED, 'licenseType')
@serializable.xml_sequence(6)
def license_types(self) -> 'SortedSet[LicenseType]':
"""
The type of license(s) that was granted to the licensee.

Returns:
`SortedSet[LicenseType]`
"""
return self._license_types

@license_types.setter
def license_types(self, license_types: Iterable[LicenseType]) -> None:
self._license_types = SortedSet(license_types)

@property
@serializable.type_mapping(serializable.helpers.XsdDateTime)
@serializable.xml_sequence(7)
def last_renewal(self) -> Optional[datetime]:
"""
The timestamp indicating when the license was last renewed. For new purchases, this is
often the purchase or acquisition date. For non-perpetual licenses or subscriptions, this
is the timestamp of when the license was last renewed.

Returns:
`datetime` or `None`
"""
return self._last_renewal

@last_renewal.setter
def last_renewal(self, last_renewal: Optional[datetime]) -> None:
self._last_renewal = last_renewal

@property
@serializable.type_mapping(serializable.helpers.XsdDateTime)
@serializable.xml_sequence(8)
def expiration(self) -> Optional[datetime]:
"""
The timestamp indicating when the current license expires (if applicable).

Returns:
`datetime` or `None`
"""
return self._expiration

@expiration.setter
def expiration(self, expiration: Optional[datetime]) -> None:
self._expiration = expiration

def __comparable_tuple(self) -> _ComparableTuple:
return _ComparableTuple((
_ComparableTuple(self._alt_ids),
self._licensor, self._licensee, self._purchaser,
self._purchase_order,
_ComparableTuple(self._license_types),
self._last_renewal, self._expiration,
))

def __eq__(self, other: object) -> bool:
if isinstance(other, Licensing):
return self.__comparable_tuple() == other.__comparable_tuple()
return False

def __lt__(self, other: Any) -> bool:
if isinstance(other, Licensing):
return self.__comparable_tuple() < other.__comparable_tuple()
return NotImplemented

def __hash__(self) -> int:
return hash(self.__comparable_tuple())

def __repr__(self) -> str:
return f'<Licensing alt_ids={self._alt_ids!r}, licensor={self._licensor!r}, licensee={self._licensee!r}, ' \
f'purchaser={self._purchaser!r}, purchase_order={self._purchase_order!r}, ' \
f'license_types={self._license_types!r}, last_renewal={self._last_renewal!r}, ' \
f'expiration={self._expiration!r}>'


@serializable.serializable_class(
name='license',
ignore_unknown_during_deserialization=True
Expand All @@ -86,6 +401,7 @@ def __init__(
bom_ref: Optional[Union[str, BomRef]] = None,
id: Optional[str] = None, name: Optional[str] = None,
text: Optional[AttachedText] = None, url: Optional[XsUri] = None,
licensing: Optional[Licensing] = None,
acknowledgement: Optional[LicenseAcknowledgement] = None,
properties: Optional[Iterable[Property]] = None,
) -> None:
Expand All @@ -101,6 +417,7 @@ def __init__(
self._name = name if not id else None
self._text = text
self._url = url
self._licensing = licensing
self._acknowledgement = acknowledgement
self._properties = SortedSet(properties or [])

Expand Down Expand Up @@ -192,17 +509,24 @@ def url(self) -> Optional[XsUri]:
def url(self, url: Optional[XsUri]) -> None:
self._url = url

# @property
# ...
# @serializable.view(SchemaVersion1Dot5)
# @serializable.view(SchemaVersion1Dot6)
# @serializable.xml_sequence(5)
# def licensing(self) -> ...:
# ... # TODO since CDX1.5
#
# @licensing.setter
# def licensing(self, ...) -> None:
# ... # TODO since CDX1.5
@property
@serializable.view(SchemaVersion1Dot5)
@serializable.view(SchemaVersion1Dot6)
@serializable.view(SchemaVersion1Dot7)
@serializable.xml_sequence(5)
def licensing(self) -> Optional[Licensing]:
"""
Licensing details describing the licensor/licensee, license type, renewal and expiration
dates, and other important metadata.

Returns:
`Licensing` or `None`
"""
return self._licensing

@licensing.setter
def licensing(self, licensing: Optional[Licensing]) -> None:
self._licensing = licensing

@property
@serializable.view(SchemaVersion1Dot5)
Expand Down Expand Up @@ -256,6 +580,7 @@ def __comparable_tuple(self) -> _ComparableTuple:
self._id, self._name,
self._url,
self._text,
self._licensing,
self._bom_ref.value,
_ComparableTuple(self._properties),
))
Expand Down
Loading