# SPDX-FileCopyrightText: 2023-2024 Repository Service for TUF Contributors
# SPDX-FileCopyrightText: 2022-2023 VMware Inc
#
# SPDX-License-Identifier: MIT
from enum import Enum
from typing import Any, Dict, List, Literal, Optional
from pydantic import BaseModel, ConfigDict, Field, model_validator
from repository_service_tuf_api import settings_repository
[docs]
class Roles(Enum):
ROOT = "root"
TARGETS = "targets"
SNAPSHOT = "snapshot"
TIMESTAMP = "timestamp"
BINS = "bins"
[docs]
@staticmethod
def is_role(input: Any) -> bool:
if not isinstance(input, str):
return False
return any(input == role.value for role in Roles)
[docs]
@staticmethod
def all_str() -> str:
return "root, targets, snapshot, timestamp and bins"
[docs]
@staticmethod
def values() -> List[str]:
return Literal["root", "targets", "snapshot", "timestamp", "bins"]
[docs]
@staticmethod
def online_roles_values() -> List[str]:
online_roles: List[str] = ["snapshot", "timestamp"]
if settings_repository.get_fresh("TARGETS_ONLINE_KEY", True):
online_roles.append("targets")
delegated_roles: List[str] = settings_repository.get_fresh(
"DELEGATED_ROLES_NAMES"
)
# All delegated roles names should start with "bins" if we are using
# hash bin delegation and none of the delegated roles should start with
# "bins" if we are using custom target delegation.
bins_used = True if delegated_roles[0].startswith("bins") else False
if bins_used:
online_roles.append(Roles.BINS.value)
else:
online_roles.extend(delegated_roles)
return online_roles
[docs]
class BaseErrorResponse(BaseModel):
error: str = Field(description="Error message")
details: Dict[str, str] | None = Field(
description="Error details", default=None
)
code: int | None = Field(
description="Error code if available", default=None
)
[docs]
class TUFSignedDelegationsRoles(BaseModel):
name: str
terminating: bool
keyids: List[str]
threshold: int
paths: List[str] | None = None
path_hash_prefixes: List[str] | None = None
x_rstuf_expire_policy: int = Field(
alias="x-rstuf-expire-policy",
description="Expire Policy for the role",
default=None,
)
# Note: No validation is required for paths as these patterns are only used
# to distribute artifacts. No files are created based on them.
paths: List[str] = Field(min_length=1)
[docs]
@model_validator(mode="before")
@classmethod
def validate_path_patterns(cls, values: Dict[str, Any]):
path_patterns = values.get("paths")
if any(len(pattern) < 1 for pattern in path_patterns):
raise ValueError("No empty strings are allowed as path patterns")
return values
[docs]
class TUFSignedDelegationsSuccinctRoles(BaseModel):
bit_length: int = Field(gt=0, lt=15)
name_prefix: str
keyids: List[str]
threshold: int
[docs]
class TUFKeys(BaseModel):
keytype: str
scheme: str
keyval: Dict[Literal["public", "issuer", "identity"], str]
name: str | None = Field(
description="Use x-rstuf-key-name instead. Key Name",
default=None,
)
x_rstuf_key_name: str | None = Field(
alias="x-rstuf-key-name", description="Key Name", default=None
)
x_rstuf_online_key_uri: Optional[str] = Field(
alias="x-rstuf-online-key-uri",
description="Online Key URI",
default=None,
)
[docs]
class TUFSignedDelegations(BaseModel):
keys: Dict[str, TUFKeys]
roles: List[TUFSignedDelegationsRoles] | None
succinct_roles: TUFSignedDelegationsSuccinctRoles | None
[docs]
class TUFSignedRoles(BaseModel):
keyids: List[str]
threshold: int
[docs]
class TUFSigned(BaseModel):
model_config = ConfigDict(
extra="allow",
)
type: str = Field(alias="_type")
version: int
spec_version: str
expires: str
keys: Dict[str, TUFKeys] | None = None
consistent_snapshot: bool | None = None
roles: Dict[Roles.values(), TUFSignedRoles] | None = None
meta: Dict[str, TUFSignedMetaFile] | None = None
targets: Dict[str, str] | None = None
delegations: TUFSignedDelegations | None = None
# Custom Validator for the extra fields (TUF unrecognized_fields)
[docs]
@model_validator(mode="before")
@classmethod
def validate_unrecognized_fields(
cls, values: Dict[str, Any]
) -> Dict[str, Any]:
all_required_field_names = {
v.alias or f for f, v in cls.model_fields.items()
}
for field_name in values:
if field_name not in all_required_field_names:
if (
not field_name.startswith("x")
or len(field_name.split("-")) < 3
):
raise ValueError(
f"Invalid: `{field_name}` field name, "
"unrecognized_field must use format x-<vendor>-<name>"
)
return values
[docs]
class TUFSignatures(BaseModel):
keyid: str
sig: str
bundle: Dict[str, Any] | None = None
[docs]
class TUFDelegations(BaseModel):
keys: Dict[str, TUFKeys]
roles: List[TUFSignedDelegationsRoles]