From ba2d0133846f3dfda260246f81a817cc297c1358 Mon Sep 17 00:00:00 2001 From: jamshale Date: Tue, 14 Jan 2025 10:59:55 -0800 Subject: [PATCH] Format and add linting rules/github action Signed-off-by: jamshale --- .github/workflows/pr-linting.yml | 21 +++++++++ server/app/__init__.py | 4 +- server/app/dependencies.py | 8 +++- server/app/models/di_proof.py | 14 +++++- server/app/models/did_document.py | 48 ++++++++++++++----- server/app/models/did_log.py | 28 ++++++++++-- server/app/models/web_schemas.py | 19 ++++++-- server/app/plugins/askar.py | 76 ++++++++++++++++++------------- server/app/plugins/didwebvh.py | 23 ++++++---- server/app/routers/identifiers.py | 46 ++++++++----------- server/app/utilities.py | 34 ++++++++------ server/config.py | 20 ++++---- server/main.py | 3 ++ server/poetry.lock | 42 ++++++++--------- server/pyproject.toml | 29 +++++++++++- server/tests/fixtures.py | 7 +-- server/tests/signer.py | 6 ++- server/tests/test_core.py | 44 ++++++++---------- 18 files changed, 303 insertions(+), 169 deletions(-) create mode 100644 .github/workflows/pr-linting.yml diff --git a/.github/workflows/pr-linting.yml b/.github/workflows/pr-linting.yml new file mode 100644 index 0000000..06056d0 --- /dev/null +++ b/.github/workflows/pr-linting.yml @@ -0,0 +1,21 @@ +name: Ruff Code Formatter and Linting Check + +on: + pull_request: + branches: [ main ] + +jobs: + lint: + name: lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Ruff Format and Lint Check + uses: astral-sh/ruff-action@v3 + with: + src: "./server" + version: "0.9.1" + args: "format --check" \ No newline at end of file diff --git a/server/app/__init__.py b/server/app/__init__.py index 81c3c00..17ec3cd 100644 --- a/server/app/__init__.py +++ b/server/app/__init__.py @@ -1,5 +1,6 @@ -from fastapi import FastAPI, APIRouter +from fastapi import APIRouter, FastAPI from fastapi.responses import JSONResponse + from app.routers import identifiers from config import settings @@ -10,6 +11,7 @@ @api_router.get("/server/status", tags=["Server"], include_in_schema=False) async def server_status(): + """Server status endpoint.""" return JSONResponse(status_code=200, content={"status": "ok"}) diff --git a/server/app/dependencies.py b/server/app/dependencies.py index 0fc811a..179f71b 100644 --- a/server/app/dependencies.py +++ b/server/app/dependencies.py @@ -1,13 +1,17 @@ +"""This module contains dependencies used by the FastAPI application.""" + from fastapi import HTTPException -from config import settings + from app.plugins import AskarStorage async def identifier_available(did: str): + """Check if a DID identifier is available.""" if await AskarStorage().fetch("didDocument", did): raise HTTPException(status_code=409, detail="Identifier unavailable.") async def did_document_exists(did: str): + """Check if a DID document exists.""" if not await AskarStorage().fetch("didDocument", did): - raise HTTPException(status_code=404, detail="Ressource not found.") + raise HTTPException(status_code=404, detail="Resource not found.") diff --git a/server/app/models/di_proof.py b/server/app/models/di_proof.py index 58f6b3a..f665a64 100644 --- a/server/app/models/di_proof.py +++ b/server/app/models/di_proof.py @@ -1,13 +1,21 @@ -from typing import Dict, Any +"""This module defines the DataIntegrityProof model used for data integrity proofs.""" + +from typing import Any, Dict + from pydantic import BaseModel, Field, field_validator class BaseModel(BaseModel): + """Base model for all models in the application.""" + def model_dump(self, **kwargs) -> Dict[str, Any]: + """Dump the model to a dictionary.""" return super().model_dump(by_alias=True, exclude_none=True, **kwargs) class DataIntegrityProof(BaseModel): + """DataIntegrityProof model.""" + type: str = Field("DataIntegrityProof") cryptosuite: str = Field("eddsa-jcs-2022") proofValue: str = Field() @@ -35,22 +43,26 @@ class DataIntegrityProof(BaseModel): @field_validator("type") @classmethod def validate_type(cls, value): + """Validate the type field.""" assert value == "DataIntegrityProof" return value @field_validator("cryptosuite") @classmethod def validate_cryptosuite(cls, value): + """Validate the cryptosuite field.""" assert value in ["eddsa-jcs-2022"] return value @field_validator("proofPurpose") @classmethod def validate_proof_purpose(cls, value): + """Validate the proofPurpose field.""" assert value in ["assertionMethod", "authentication"] return value @field_validator("expires") @classmethod def validate_expires(cls, value): + """Validate the expires field.""" return value diff --git a/server/app/models/did_document.py b/server/app/models/did_document.py index 59c9fc3..53f65c1 100644 --- a/server/app/models/did_document.py +++ b/server/app/models/did_document.py @@ -1,25 +1,30 @@ -from typing import Union, List, Dict, Any -from pydantic import BaseModel, Field, field_validator -from .di_proof import DataIntegrityProof -from multiformats import multibase -from config import settings +"""DID Document model.""" + import re +from typing import Any, Dict, List, Union + import validators +from multiformats import multibase +from pydantic import BaseModel, Field, field_validator +from .di_proof import DataIntegrityProof DID_WEB_REGEX = re.compile("did:web:((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)") -DID_WEB_ID_REGEX = re.compile( - "did:web:((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)#([a-z0-9._%-]+)" -) +DID_WEB_ID_REGEX = re.compile("did:web:((?:[a-zA-Z0-9._%-]*:)*[a-zA-Z0-9._%-]+)#([a-z0-9._%-]+)") class BaseModel(BaseModel): + """Base model for all models in the application.""" + def model_dump(self, **kwargs) -> Dict[str, Any]: + """Dump the model to a dictionary.""" return super().model_dump(by_alias=True, exclude_none=True, **kwargs) class VerificationMethod(BaseModel): + """VerificationMethod model.""" + id: str = Field() type: Union[str, List[str]] = Field() controller: str = Field() @@ -27,12 +32,14 @@ class VerificationMethod(BaseModel): @field_validator("id") @classmethod def verification_method_id_validator(cls, value): + """Validate the id field.""" assert value.startswith("did:") return value @field_validator("type") @classmethod def verification_method_type_validator(cls, value): + """Validate the type field.""" assert value in [ "Multikey", "JsonWebKey", @@ -42,40 +49,51 @@ def verification_method_type_validator(cls, value): @field_validator("controller") @classmethod def verification_method_controller_validator(cls, value): + """Validate the controller field.""" assert value.startswith("did:") return value class JsonWebKey(BaseModel): + """JsonWebKey model.""" + kty: str = Field("OKP") crv: str = Field("Ed25519") x: str = Field() class VerificationMethodJwk(VerificationMethod): + """VerificationMethodJwk model.""" + publicKeyJwk: JsonWebKey = Field() @field_validator("publicKeyJwk") @classmethod def verification_method_public_key_validator(cls, value): + """Validate the public key field.""" # TODO decode b64 return value class VerificationMethodMultikey(VerificationMethod): + """VerificationMethodMultikey model.""" + publicKeyMultibase: str = Field() @field_validator("publicKeyMultibase") @classmethod def verification_method_public_key_validator(cls, value): + """Validate the public key field.""" try: multibase.decode(value) - except: + except Exception: assert False, f"Unable to decode public key multibase value {value}" return value class Service(BaseModel): + """Service model.""" + id: str = Field() type: Union[str, List[str]] = Field() serviceEndpoint: str = Field() @@ -83,17 +101,21 @@ class Service(BaseModel): @field_validator("id") @classmethod def service_id_validator(cls, value): + """Validate the id field.""" assert value.startswith("did:") return value @field_validator("serviceEndpoint") @classmethod def service_endpoint_validator(cls, value): + """Validate the service endpoint field.""" assert validators.url(value), f"Invalid service endpoint {value}." return value class DidDocument(BaseModel): + """DID Document model.""" + context: Union[str, List[str]] = Field( ["https://www.w3.org/ns/did/v1"], alias="@context", @@ -103,9 +125,7 @@ class DidDocument(BaseModel): description: str = Field(None) controller: str = Field(None) alsoKnownAs: List[str] = Field(None) - verificationMethod: List[ - Union[VerificationMethodMultikey, VerificationMethodJwk] - ] = Field(None) + verificationMethod: List[Union[VerificationMethodMultikey, VerificationMethodJwk]] = Field(None) authentication: List[Union[str, VerificationMethod]] = Field(None) assertionMethod: List[Union[str, VerificationMethod]] = Field(None) keyAgreement: List[Union[str, VerificationMethod]] = Field(None) @@ -127,15 +147,19 @@ class DidDocument(BaseModel): @field_validator("context") @classmethod def context_validator(cls, value): + """Validate the context field.""" assert value[0] == "https://www.w3.org/ns/did/v1", "Invalid context." return value @field_validator("id") @classmethod def id_validator(cls, value): + """Validate the id field.""" assert value.startswith("did:") return value class SecuredDidDocument(DidDocument): + """Secured DID Document model.""" + proof: Union[DataIntegrityProof, List[DataIntegrityProof]] = Field() diff --git a/server/app/models/did_log.py b/server/app/models/did_log.py index 51dbce8..b4a37a8 100644 --- a/server/app/models/did_log.py +++ b/server/app/models/did_log.py @@ -1,32 +1,48 @@ -from typing import Union, List, Dict, Any +"""DID Log models.""" + +from typing import Any, Dict, List, Union + from pydantic import BaseModel, Field -from .did_document import DidDocument -from .di_proof import DataIntegrityProof + from config import settings +from .di_proof import DataIntegrityProof +from .did_document import DidDocument + class BaseModel(BaseModel): + """Base model for all models in the application.""" + def model_dump(self, **kwargs) -> Dict[str, Any]: + """Dump the model to a dictionary.""" return super().model_dump(by_alias=True, exclude_none=True, **kwargs) class Witness(BaseModel): + """Witness model.""" + id: str = Field(None) weight: int = Field(None) class WitnessParam(BaseModel): + """WitnessParam model.""" + threshold: int = Field(None) selfWeight: int = Field(None) witnesses: List[Witness] = Field(None) class WitnessSignature(BaseModel): + """WitnessSignature model.""" + versionId: str = Field(None) proof: List[DataIntegrityProof] = Field() class InitialLogParameters(BaseModel): + """InitialLogParameters model.""" + method: str = Field(f"did:webvh:{settings.WEBVH_VERSION}") scid: str = Field() updateKeys: List[str] = Field() @@ -37,6 +53,8 @@ class InitialLogParameters(BaseModel): class LogParameters(BaseModel): + """LogParameters model.""" + prerotation: bool = Field(None) portable: bool = Field(None) updateKeys: List[str] = Field(None) @@ -64,6 +82,8 @@ class LogParameters(BaseModel): class InitialLogEntry(BaseModel): + """InitialLogEntry model.""" + versionId: str = Field() versionTime: str = Field() parameters: LogParameters = Field() @@ -72,6 +92,8 @@ class InitialLogEntry(BaseModel): class LogEntry(BaseModel): + """LogEntry model.""" + versionId: str = Field() versionTime: str = Field() parameters: LogParameters = Field() diff --git a/server/app/models/web_schemas.py b/server/app/models/web_schemas.py index 3f3a45f..5a3d8a0 100644 --- a/server/app/models/web_schemas.py +++ b/server/app/models/web_schemas.py @@ -1,24 +1,37 @@ -from typing import Dict, Any, List +"""Pydantic models for the web schemas.""" + +from typing import Any, Dict, List + from pydantic import BaseModel, Field -from .did_document import SecuredDidDocument -from .did_log import InitialLogEntry, LogEntry, WitnessSignature + from .di_proof import DataIntegrityProof +from .did_document import SecuredDidDocument +from .did_log import InitialLogEntry, LogEntry class BaseModel(BaseModel): + """Base model for all models in the application.""" + def model_dump(self, **kwargs) -> Dict[str, Any]: + """Dump the model to a dictionary.""" return super().model_dump(by_alias=True, exclude_none=True, **kwargs) class RegisterDID(BaseModel): + """RegisterDID model.""" + didDocument: SecuredDidDocument = Field() class RegisterInitialLogEntry(BaseModel): + """RegisterInitialLogEntry model.""" + logEntry: InitialLogEntry = Field() class UpdateLogEntry(BaseModel): + """UpdateLogEntry model.""" + logEntry: LogEntry = Field() witnessProof: List[DataIntegrityProof] = Field(None) diff --git a/server/app/plugins/askar.py b/server/app/plugins/askar.py index 3e3a420..d1a6ce1 100644 --- a/server/app/plugins/askar.py +++ b/server/app/plugins/askar.py @@ -1,66 +1,78 @@ -import json -from fastapi import HTTPException -from aries_askar import Store, Key -from aries_askar.bindings import LocalKeyHandle -from config import settings +"""Askar plugin for storing and verifying data.""" + import hashlib +import json import uuid -from multiformats import multibase -from datetime import datetime, timezone, timedelta +from datetime import datetime, timedelta, timezone from hashlib import sha256 + import canonicaljson +from aries_askar import Key, Store +from aries_askar.bindings import LocalKeyHandle +from fastapi import HTTPException +from multiformats import multibase + +from config import settings class AskarStorage: + """Askar storage plugin.""" + def __init__(self): + """Initialize the Askar storage plugin.""" self.db = settings.ASKAR_DB - self.key = Store.generate_raw_key( - hashlib.md5(settings.DOMAIN.encode()).hexdigest() - ) + self.key = Store.generate_raw_key(hashlib.md5(settings.DOMAIN.encode()).hexdigest()) async def provision(self, recreate=False): + """Provision the Askar storage.""" await Store.provision(self.db, "raw", self.key, recreate=recreate) async def open(self): + """Open the Askar storage.""" return await Store.open(self.db, "raw", self.key) async def fetch(self, category, data_key): + """Fetch data from the store.""" store = await self.open() try: async with store.session() as session: data = await session.fetch(category, data_key) return json.loads(data.value) - except: + except Exception: return None async def store(self, category, data_key, data): + """Store data in the store.""" store = await self.open() try: async with store.session() as session: await session.insert(category, data_key, json.dumps(data)) - except: + except Exception: raise HTTPException(status_code=404, detail="Couldn't store record.") async def update(self, category, data_key, data): + """Update data in the store.""" store = await self.open() try: async with store.session() as session: await session.replace(category, data_key, json.dumps(data)) - except: + except Exception: raise HTTPException(status_code=404, detail="Couldn't update record.") class AskarVerifier: + """Askar verifier plugin.""" + def __init__(self): + """Initialize the Askar verifier plugin.""" self.type = "DataIntegrityProof" self.cryptosuite = "eddsa-jcs-2022" self.purpose = "assertionMethod" def create_proof_config(self, did): + """Create a proof configuration.""" expires = str( - (datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat( - "T", "seconds" - ) + (datetime.now(timezone.utc) + timedelta(minutes=10)).isoformat("T", "seconds") ) return { "type": self.type, @@ -72,36 +84,38 @@ def create_proof_config(self, did): } def create_challenge(self, value): + """Create a challenge.""" return str(uuid.uuid5(uuid.NAMESPACE_DNS, settings.SECRET_KEY + value)) def validate_challenge(self, proof, did): + """Validate the challenge.""" try: if proof.get("domain"): assert proof["domain"] == settings.DOMAIN, "Domain mismatch." if proof.get("challenge"): - assert proof["challenge"] == self.create_challenge( - did + proof["expires"] - ), "Challenge mismatch." + assert proof["challenge"] == self.create_challenge(did + proof["expires"]), ( + "Challenge mismatch." + ) except AssertionError as msg: raise HTTPException(status_code=400, detail=str(msg)) def validate_proof(self, proof): + """Validate the proof.""" try: if proof.get("expires"): - assert datetime.fromisoformat(proof["expires"]) > datetime.now( - timezone.utc - ), "Proof expired." + assert datetime.fromisoformat(proof["expires"]) > datetime.now(timezone.utc), ( + "Proof expired." + ) assert proof["type"] == self.type, f"Expected {self.type} proof type." - assert ( - proof["cryptosuite"] == self.cryptosuite - ), f"Expected {self.cryptosuite} proof cryptosuite." - assert ( - proof["proofPurpose"] == self.purpose - ), f"Expected {self.purpose} proof purpose." + assert proof["cryptosuite"] == self.cryptosuite, ( + f"Expected {self.cryptosuite} proof cryptosuite." + ) + assert proof["proofPurpose"] == self.purpose, f"Expected {self.purpose} proof purpose." except AssertionError as msg: raise HTTPException(status_code=400, detail=str(msg)) def verify_proof(self, document, proof): + """Verify the proof.""" self.validate_proof(proof) multikey = proof["verificationMethod"].split("#")[-1] @@ -119,9 +133,7 @@ def verify_proof(self, document, proof): ) try: if not key.verify_signature(message=hash_data, signature=signature): - raise HTTPException( - status_code=400, detail="Signature was forged or corrupt." - ) + raise HTTPException(status_code=400, detail="Signature was forged or corrupt.") return True - except: + except Exception: raise HTTPException(status_code=400, detail="Error verifying proof.") diff --git a/server/app/plugins/didwebvh.py b/server/app/plugins/didwebvh.py index 0d2810a..c126271 100644 --- a/server/app/plugins/didwebvh.py +++ b/server/app/plugins/didwebvh.py @@ -1,13 +1,20 @@ -from config import settings +"""DID Web Verifiable History (DID WebVH) plugin.""" + +import json from datetime import datetime -from app.models.did_log import LogParameters, InitialLogEntry + import canonicaljson -import json -from multiformats import multihash, multibase +from multiformats import multibase, multihash + +from app.models.did_log import InitialLogEntry, LogParameters +from config import settings class DidWebVH: + """DID Web Verifiable History (DID WebVH) plugin.""" + def __init__(self): + """Initialize the DID WebVH plugin.""" self.prefix = settings.DID_WEBVH_PREFIX self.method_version = f"{self.prefix}0.4" self.did_string_base = self.prefix + r"{SCID}:" + settings.DOMAIN @@ -20,9 +27,7 @@ def _init_parameters(self, update_key, next_key=None, ttl=100): return parameters def _init_state(self, did_doc): - return json.loads( - json.dumps(did_doc).replace("did:web:", self.prefix + r"{SCID}:") - ) + return json.loads(json.dumps(did_doc).replace("did:web:", self.prefix + r"{SCID}:")) def _generate_scid(self, log_entry): # https://identity.foundation/trustdidweb/#generate-scid @@ -39,10 +44,12 @@ def _generate_entry_hash(self, log_entry): return encoded def create_initial_did_doc(self, did_string): + """Create an initial DID document.""" did_doc = {"@context": [], "id": did_string} - return log_entry + return did_doc def create(self, did_doc, update_key): + """Create a new DID WebVH log.""" # https://identity.foundation/trustdidweb/#create-register log_entry = InitialLogEntry( versionId=r"{SCID}", diff --git a/server/app/routers/identifiers.py b/server/app/routers/identifiers.py index 8f260fc..b7653a7 100644 --- a/server/app/routers/identifiers.py +++ b/server/app/routers/identifiers.py @@ -1,11 +1,15 @@ +"""Identifier endpoints for DIDWeb and DIDWebVH.""" + +import json + +from fastapi import APIRouter, HTTPException, Response +from fastapi.responses import JSONResponse + from app.dependencies import identifier_available -from app.models.web_schemas import RegisterDID, RegisterInitialLogEntry, UpdateLogEntry from app.models.did_document import DidDocument +from app.models.web_schemas import RegisterDID, RegisterInitialLogEntry, UpdateLogEntry from app.plugins import AskarStorage, AskarVerifier, DidWebVH from config import settings -from fastapi import APIRouter, HTTPException, Response -from fastapi.responses import JSONResponse -import json router = APIRouter(tags=["Identifiers"]) @@ -16,6 +20,7 @@ async def request_did( namespace: str = None, identifier: str = None, ): + """Request a DID document and proof options for a given namespace and identifier.""" if namespace and identifier: client_id = f"{namespace}:{identifier}" did = f"{settings.DID_WEB_BASE}:{client_id}" @@ -28,15 +33,14 @@ async def request_did( }, ) - raise HTTPException( - status_code=400, detail="Missing namespace or identifier query." - ) + raise HTTPException(status_code=400, detail="Missing namespace or identifier query.") @router.post("/") async def register_did( request_body: RegisterDID, ): + """Register a DID document and proof set.""" did_document = request_body.model_dump()["didDocument"] did = did_document["id"] @@ -84,15 +88,13 @@ async def register_did( await AskarStorage().store("authorizedKey", did, authorized_key) return JSONResponse(status_code=201, content={}) - initial_log_entry = DidWebVH().create(did_document, authorized_key) - return JSONResponse(status_code=201, content={"logEntry": initial_log_entry}) - raise HTTPException(status_code=400, detail="Bad Request, something went wrong.") # DIDWebVH @router.get("/{namespace}/{identifier}") async def get_log_state(namespace: str, identifier: str): + """Get the current state of the log for a given namespace and identifier.""" client_id = f"{namespace}:{identifier}" log_entry = await AskarStorage().fetch("logEntries", client_id) if not log_entry: @@ -110,6 +112,7 @@ async def create_didwebvh( identifier: str, request_body: RegisterInitialLogEntry, ): + """Create a new log entry for a given namespace and identifier.""" client_id = f"{namespace}:{identifier}" log_entry = request_body.model_dump()["logEntry"] did = f"{settings.DID_WEB_BASE}:{namespace}:{identifier}" @@ -118,9 +121,7 @@ async def create_didwebvh( proof = log_entry.pop("proof", None) proof = proof if isinstance(proof, list) else [proof] if len(proof) != 1: - raise HTTPException( - status_code=400, detail="Expecting singular proof from controller." - ) + raise HTTPException(status_code=400, detail="Expecting singular proof from controller.") # Verify proofs proof = proof[0] @@ -144,10 +145,7 @@ async def create_didwebvh( @router.get("/{namespace}/{identifier}/did.json", include_in_schema=False) async def read_did(namespace: str, identifier: str): - """ - https://identity.foundation/didwebvh/next/#read-resolve - """ - client_id = f"{namespace}:{identifier}" + """See https://identity.foundation/didwebvh/next/#read-resolve.""" did = f"{settings.DID_WEB_BASE}:{namespace}:{identifier}" did_doc = await AskarStorage().fetch("didDocument", did) if did_doc: @@ -157,9 +155,7 @@ async def read_did(namespace: str, identifier: str): @router.get("/{namespace}/{identifier}/did.jsonl", include_in_schema=False) async def read_did_log(namespace: str, identifier: str): - """ - https://identity.foundation/didwebvh/next/#read-resolve - """ + """See https://identity.foundation/didwebvh/next/#read-resolve.""" client_id = f"{namespace}:{identifier}" log_entries = await AskarStorage().fetch("logEntries", client_id) if log_entries: @@ -170,17 +166,11 @@ async def read_did_log(namespace: str, identifier: str): @router.put("/{namespace}/{identifier}") async def update_did(namespace: str, identifier: str, request_body: UpdateLogEntry): - """ - https://identity.foundation/didwebvh/next/#update-rotate - """ - client_id = f"{namespace}:{identifier}" + """See https://identity.foundation/didwebvh/next/#update-rotate.""" raise HTTPException(status_code=501, detail="Not Implemented") @router.delete("/{namespace}/{identifier}") async def deactivate_did(namespace: str, identifier: str): - """ - https://identity.foundation/didwebvh/next/#deactivate-revoke - """ - client_id = f"{namespace}:{identifier}" + """See https://identity.foundation/didwebvh/next/#deactivate-revoke.""" raise HTTPException(status_code=501, detail="Not Implemented") diff --git a/server/app/utilities.py b/server/app/utilities.py index 1453f56..4c0fbb5 100644 --- a/server/app/utilities.py +++ b/server/app/utilities.py @@ -1,36 +1,43 @@ +"""Utility functions for the DID Web server.""" + +from fastapi import HTTPException + from app.models.did_document import DidDocument +from app.plugins import AskarStorage, AskarVerifier from config import settings -from app.plugins import AskarVerifier, AskarStorage -from fastapi import HTTPException def to_did_web(namespace: str, identifier: str): + """Convert namespace and identifier to a DID Web identifier.""" return f"{settings.DID_WEB_BASE}:{namespace}:{identifier}" async def location_available(did: str): + """Check if a location is available.""" if await AskarStorage().fetch("didDocument", did): raise HTTPException(status_code=409, detail="Identifier unavailable.") async def did_document_exists(did: str): + """Check if a DID document exists.""" if not await AskarStorage().fetch("didDocument", did): raise HTTPException(status_code=404, detail="Ressource not found.") async def valid_did_registration(did_document): + """Validate a DID document registration.""" did_document proofs = did_document.pop("proof") try: # assert ( # did_document["id"] == f"{settings.DID_WEB_BASE}:{namespace}:{identifier}" # ), "Id mismatch between DID Document and requested endpoint." - assert ( - len(did_document["verificationMethod"]) >= 1 - ), "DID Documentmust contain at least 1 verificationMethod." - assert ( - isinstance(proofs, list) and len(proofs) == 2 - ), "Insuficient proofs, must contain a client and an endorser proof." + assert len(did_document["verificationMethod"]) >= 1, ( + "DID Document must contain at least 1 verificationMethod." + ) + assert isinstance(proofs, list) and len(proofs) == 2, ( + "Insufficient proofs, must contain a client and an endorser proof." + ) except AssertionError as msg: raise HTTPException(status_code=400, detail=str(msg)) @@ -46,15 +53,18 @@ async def valid_did_registration(did_document): async def identifier_available(identifier: str): + """Check if an identifier is available.""" if await AskarStorage().fetch("didDocument", identifier): raise HTTPException(status_code=409, detail="Identifier unavailable.") def derive_did(namespace, identifier): + """Derive a DID from a namespace and identifier.""" return f"{settings.DID_WEB_BASE}:{namespace}:{identifier}" def create_did_doc(did, multikey, kid="key-01"): + """Create a DID document.""" return DidDocument( id=did, verificationMethod=[ @@ -72,17 +82,15 @@ def create_did_doc(did, multikey, kid="key-01"): def find_key(did_doc, kid): + """Find a key in a DID document.""" return next( - ( - vm["publicKeyMultibase"] - for vm in did_doc["verificationMethod"] - if vm["id"] == kid - ), + (vm["publicKeyMultibase"] for vm in did_doc["verificationMethod"] if vm["id"] == kid), None, ) def find_proof(proof_set, kid): + """Find a proof in a proof set.""" return next( (proof for proof in proof_set if proof["verificationMethod"] == kid), None, diff --git a/server/config.py b/server/config.py index 98f47ae..c3b021f 100644 --- a/server/config.py +++ b/server/config.py @@ -1,13 +1,18 @@ -from pydantic_settings import BaseSettings +"""App configuration.""" + +import logging import os + from dotenv import load_dotenv -import logging +from pydantic_settings import BaseSettings basedir = os.path.abspath(os.path.dirname(__file__)) load_dotenv(os.path.join(basedir, ".env")) class Settings(BaseSettings): + """App settings.""" + PROJECT_TITLE: str = "DID WebVH Server" PROJECT_VERSION: str = "v0" @@ -26,15 +31,8 @@ class Settings(BaseSettings): POSTGRES_SERVER_PORT: str = os.getenv("POSTGRES_SERVER_PORT", "") ASKAR_DB: str = "sqlite://app.db" - if ( - POSTGRES_USER - and POSTGRES_PASSWORD - and POSTGRES_SERVER_NAME - and POSTGRES_SERVER_PORT - ): - logging.info( - f"Using postgres storage: {POSTGRES_SERVER_NAME}:{POSTGRES_SERVER_PORT}" - ) + if POSTGRES_USER and POSTGRES_PASSWORD and POSTGRES_SERVER_NAME and POSTGRES_SERVER_PORT: + logging.info(f"Using postgres storage: {POSTGRES_SERVER_NAME}:{POSTGRES_SERVER_PORT}") ASKAR_DB: str = f"postgres://{POSTGRES_USER}:{POSTGRES_PASSWORD}@{POSTGRES_SERVER_NAME}:{POSTGRES_SERVER_PORT}/didwebvh-server" else: logging.info("Using SQLite database") diff --git a/server/main.py b/server/main.py index 07a8e12..b6f5e08 100644 --- a/server/main.py +++ b/server/main.py @@ -1,6 +1,9 @@ +"""Main entry point for the server.""" + import asyncio import uvicorn + from app.plugins import AskarStorage if __name__ == "__main__": diff --git a/server/poetry.lock b/server/poetry.lock index d8d2860..af3aeb2 100644 --- a/server/poetry.lock +++ b/server/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[package]] name = "annotated-types" @@ -474,29 +474,29 @@ cli = ["click (>=5.0)"] [[package]] name = "ruff" -version = "0.6.8" +version = "0.9.1" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.8-py3-none-linux_armv6l.whl", hash = "sha256:77944bca110ff0a43b768f05a529fecd0706aac7bcce36d7f1eeb4cbfca5f0f2"}, - {file = "ruff-0.6.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:27b87e1801e786cd6ede4ada3faa5e254ce774de835e6723fd94551464c56b8c"}, - {file = "ruff-0.6.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:cd48f945da2a6334f1793d7f701725a76ba93bf3d73c36f6b21fb04d5338dcf5"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:677e03c00f37c66cea033274295a983c7c546edea5043d0c798833adf4cf4c6f"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f1476236b3eacfacfc0f66aa9e6cd39f2a624cb73ea99189556015f27c0bdeb"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f5a2f17c7d32991169195d52a04c95b256378bbf0de8cb98478351eb70d526f"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5fd0d4b7b1457c49e435ee1e437900ced9b35cb8dc5178921dfb7d98d65a08d0"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8034b19b993e9601f2ddf2c517451e17a6ab5cdb1c13fdff50c1442a7171d87"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6cfb227b932ba8ef6e56c9f875d987973cd5e35bc5d05f5abf045af78ad8e098"}, - {file = "ruff-0.6.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef0411eccfc3909269fed47c61ffebdcb84a04504bafa6b6df9b85c27e813b0"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:007dee844738c3d2e6c24ab5bc7d43c99ba3e1943bd2d95d598582e9c1b27750"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ce60058d3cdd8490e5e5471ef086b3f1e90ab872b548814e35930e21d848c9ce"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1085c455d1b3fdb8021ad534379c60353b81ba079712bce7a900e834859182fa"}, - {file = "ruff-0.6.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:70edf6a93b19481affd287d696d9e311388d808671bc209fb8907b46a8c3af44"}, - {file = "ruff-0.6.8-py3-none-win32.whl", hash = "sha256:792213f7be25316f9b46b854df80a77e0da87ec66691e8f012f887b4a671ab5a"}, - {file = "ruff-0.6.8-py3-none-win_amd64.whl", hash = "sha256:ec0517dc0f37cad14a5319ba7bba6e7e339d03fbf967a6d69b0907d61be7a263"}, - {file = "ruff-0.6.8-py3-none-win_arm64.whl", hash = "sha256:8d3bb2e3fbb9875172119021a13eed38849e762499e3cfde9588e4b4d70968dc"}, - {file = "ruff-0.6.8.tar.gz", hash = "sha256:a5bf44b1aa0adaf6d9d20f86162b34f7c593bfedabc51239953e446aefc8ce18"}, + {file = "ruff-0.9.1-py3-none-linux_armv6l.whl", hash = "sha256:84330dda7abcc270e6055551aca93fdde1b0685fc4fd358f26410f9349cf1743"}, + {file = "ruff-0.9.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3cae39ba5d137054b0e5b472aee3b78a7c884e61591b100aeb544bcd1fc38d4f"}, + {file = "ruff-0.9.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:50c647ff96f4ba288db0ad87048257753733763b409b2faf2ea78b45c8bb7fcb"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0c8b149e9c7353cace7d698e1656ffcf1e36e50f8ea3b5d5f7f87ff9986a7ca"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:beb3298604540c884d8b282fe7625651378e1986c25df51dec5b2f60cafc31ce"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:39d0174ccc45c439093971cc06ed3ac4dc545f5e8bdacf9f067adf879544d969"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:69572926c0f0c9912288915214ca9b2809525ea263603370b9e00bed2ba56dbd"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:937267afce0c9170d6d29f01fcd1f4378172dec6760a9f4dface48cdabf9610a"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:186c2313de946f2c22bdf5954b8dd083e124bcfb685732cfb0beae0c47233d9b"}, + {file = "ruff-0.9.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f94942a3bb767675d9a051867c036655fe9f6c8a491539156a6f7e6b5f31831"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:728d791b769cc28c05f12c280f99e8896932e9833fef1dd8756a6af2261fd1ab"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2f312c86fb40c5c02b44a29a750ee3b21002bd813b5233facdaf63a51d9a85e1"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:ae017c3a29bee341ba584f3823f805abbe5fe9cd97f87ed07ecbf533c4c88366"}, + {file = "ruff-0.9.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5dc40a378a0e21b4cfe2b8a0f1812a6572fc7b230ef12cd9fac9161aa91d807f"}, + {file = "ruff-0.9.1-py3-none-win32.whl", hash = "sha256:46ebf5cc106cf7e7378ca3c28ce4293b61b449cd121b98699be727d40b79ba72"}, + {file = "ruff-0.9.1-py3-none-win_amd64.whl", hash = "sha256:342a824b46ddbcdddd3abfbb332fa7fcaac5488bf18073e841236aadf4ad5c19"}, + {file = "ruff-0.9.1-py3-none-win_arm64.whl", hash = "sha256:1cd76c7f9c679e6e8f2af8f778367dca82b95009bc7b1a85a47f1521ae524fa7"}, + {file = "ruff-0.9.1.tar.gz", hash = "sha256:fd2b25ecaf907d6458fa842675382c8597b3c746a2dde6717fe3415425df0c17"}, ] [[package]] @@ -587,4 +587,4 @@ crypto-eth-addresses = ["eth-hash[pycryptodome] (>=0.7.0)"] [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "bb8d5f74c5792a6970821d3bfeee53e986a1d067605c8f4d6d7d460d33e263de" +content-hash = "59a7662c2d72029b39424de960ba88d2d69b2f8b5a4a8acbbdd1501e87427678" diff --git a/server/pyproject.toml b/server/pyproject.toml index 6eb1fe1..5378815 100644 --- a/server/pyproject.toml +++ b/server/pyproject.toml @@ -38,7 +38,32 @@ jsonlines = "^4.0.0" pytest-asyncio = "^0.25.2" [tool.poetry.group.dev.dependencies] -ruff = "^0.6.8" +ruff = "^0.9.1" + +[tool.ruff] +lint.select = ["B006", "C", "D", "E", "F"] +line-length = 100 + +lint.ignore = [ + # Google Python Doc Style + "D203", + "D204", + "D213", + "D215", + "D400", + "D401", + "D404", + "D406", + "D407", + "D408", + "D409", + "D413", + "D202", # Allow blank line after docstring + "D104", # Don't require docstring in public package +] + +[tool.ruff.lint.per-file-ignores] +"**/{tests}/*" = ["B006", "D", "E501", "F841"] [build-system] requires = ["poetry-core"] @@ -46,4 +71,4 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] asyncio_mode = "auto" -asyncio_default_fixture_loop_scope = "function" \ No newline at end of file +asyncio_default_fixture_loop_scope = "function" diff --git a/server/tests/fixtures.py b/server/tests/fixtures.py index b3b4135..58b8410 100644 --- a/server/tests/fixtures.py +++ b/server/tests/fixtures.py @@ -1,6 +1,5 @@ +from app.models.did_document import DidDocument from config import settings -from app.models.did_document import DidDocument, SecuredDidDocument -from app.models.di_proof import DataIntegrityProof TEST_SEED = "ixUwS8A2SYzmPiGor7t08wgg1ifNABrB" TEST_AUTHORISED_KEY = "z6Mkixacx8HJ5nRBJvJKNdv83v1ejZBpz3HvRCfa2JaKbQJV" @@ -15,6 +14,4 @@ "proofPurpose": "assertionMethod", } -TEST_DID_DOCUMENT = DidDocument( - context=["https://www.w3.org/ns/did/v1"], id=TEST_DID -).model_dump() +TEST_DID_DOCUMENT = DidDocument(context=["https://www.w3.org/ns/did/v1"], id=TEST_DID).model_dump() diff --git a/server/tests/signer.py b/server/tests/signer.py index 7959842..3e43e5c 100644 --- a/server/tests/signer.py +++ b/server/tests/signer.py @@ -1,9 +1,11 @@ from hashlib import sha256 + import canonicaljson -from multiformats import multibase from aries_askar import Key, KeyAlg from aries_askar.bindings import LocalKeyHandle -from tests.fixtures import TEST_SEED, TEST_PROOF_OPTIONS +from multiformats import multibase + +from tests.fixtures import TEST_PROOF_OPTIONS, TEST_SEED def sign(document, options=TEST_PROOF_OPTIONS): diff --git a/server/tests/test_core.py b/server/tests/test_core.py index dcf695d..9357fe3 100644 --- a/server/tests/test_core.py +++ b/server/tests/test_core.py @@ -1,20 +1,22 @@ -from app.routers.identifiers import request_did, read_did, read_did_log, create_didwebvh -from app.plugins import AskarStorage, AskarVerifier, DidWebVH -from app.models.web_schemas import RegisterInitialLogEntry -from app.models.did_log import LogEntry +import asyncio +import json from datetime import datetime, timezone + +import pytest + +from app.models.did_log import LogEntry +from app.models.web_schemas import RegisterInitialLogEntry +from app.plugins import AskarStorage, AskarVerifier, DidWebVH +from app.routers.identifiers import create_didwebvh, read_did, read_did_log, request_did from tests.fixtures import ( - TEST_DOMAIN, - TEST_DID_NAMESPACE, - TEST_DID_IDENTIFIER, + TEST_AUTHORISED_KEY, TEST_DID, TEST_DID_DOCUMENT, - TEST_AUTHORISED_KEY, + TEST_DID_IDENTIFIER, + TEST_DID_NAMESPACE, + TEST_DOMAIN, TEST_PROOF_OPTIONS, ) -import json -import pytest -import asyncio from tests.signer import sign askar = AskarStorage() @@ -49,19 +51,13 @@ async def test_request_did(): did_request = json.loads(did_request.body.decode()) assert did_request.get("didDocument").get("id") == TEST_DID assert did_request.get("proofOptions").get("type") == TEST_PROOF_OPTIONS["type"] - assert ( - did_request.get("proofOptions").get("cryptosuite") - == TEST_PROOF_OPTIONS["cryptosuite"] - ) - assert ( - did_request.get("proofOptions").get("proofPurpose") - == TEST_PROOF_OPTIONS["proofPurpose"] - ) + assert did_request.get("proofOptions").get("cryptosuite") == TEST_PROOF_OPTIONS["cryptosuite"] + assert did_request.get("proofOptions").get("proofPurpose") == TEST_PROOF_OPTIONS["proofPurpose"] assert did_request.get("proofOptions").get("domain") == TEST_DOMAIN assert did_request.get("proofOptions").get("challenge") - assert datetime.fromisoformat( - did_request.get("proofOptions").get("expires") - ) > datetime.now(timezone.utc) + assert datetime.fromisoformat(did_request.get("proofOptions").get("expires")) > datetime.now( + timezone.utc + ) @pytest.mark.asyncio @@ -105,9 +101,7 @@ async def test_register_log_entry(): signed_log_entry = sign(log_entry) signed_log_entry["proof"] = [signed_log_entry["proof"]] log_request = RegisterInitialLogEntry.model_validate({"logEntry": signed_log_entry}) - response = await create_didwebvh( - TEST_DID_NAMESPACE, TEST_DID_IDENTIFIER, log_request - ) + response = await create_didwebvh(TEST_DID_NAMESPACE, TEST_DID_IDENTIFIER, log_request) log_entry = response.body.decode() LogEntry.model_validate(json.loads(log_entry))