Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 75 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@

SDK zostało zaprojektowane w oparciu o oficjalne biblioteki referencyjne KSeF dla ekosystemów **Java** oraz **C#/.NET**, z naciskiem na zachowanie spójności pojęć oraz przepływów (workflow).

## 🔄 Kompatybilność API KSeF

Aktualna kompatybilność: **KSeF API `v2.1.1`** ([api-changelog.md](https://github.com/CIRFMF/ksef-docs/blob/2.1.1/api-changelog.md)).

## ✅ Funkcjonalności

- Klienci API (`KsefClient`, `AsyncKsefClient`) mapujący wywołania na endpointy KSeF.
Expand Down Expand Up @@ -104,6 +108,7 @@ tokens = AuthCoordinator(client.auth).authenticate_with_xades_key_pair(
context_identifier_type="nip",
context_identifier_value="5265877635",
subject_identifier_type="certificateSubject",
enforce_xades_compliance=False, # ustaw True, aby dodać X-KSeF-Feature: enforce-xades-compliance
).tokens
```

Expand Down Expand Up @@ -168,7 +173,76 @@ Uruchomienie testów z kontrolą pokrycia:
pytest --cov=ksef_client --cov-report=term-missing --cov-fail-under=100
```

Testy E2E (marker `e2e`) są wyłączone w standardowym przebiegu i wymagają osobnej konfiguracji środowiska oraz danych dostępowych.
Testy E2E (marker `e2e`) są wyłączone w standardowym przebiegu i wymagają osobnej konfiguracji
środowiska oraz danych dostępowych.

Scenariusz E2E obejmuje:
- logowanie tokenem KSeF,
- logowanie certyfikatem (XAdES),
- wystawienie faktury,
- pobranie UPO,
- listowanie faktur,
- pobranie ostatniej faktury.

Plik testów E2E:
- `tests/test_e2e_token_flows.py`

Dostępne testy:
- `test_e2e_test_environment_full_flow_token`
- `test_e2e_test_environment_full_flow_xades`
- `test_e2e_demo_environment_full_flow_token`
- `test_e2e_demo_environment_full_flow_xades`

Lokalne uruchomienie (token, TEST):

```bash
KSEF_E2E=1 \
KSEF_TEST_TOKEN=... \
KSEF_TEST_CONTEXT_TYPE=nip \
KSEF_TEST_CONTEXT_VALUE=... \
pytest tests/test_e2e_token_flows.py::test_e2e_test_environment_full_flow_token
```

Lokalne uruchomienie (XAdES, TEST):

```bash
KSEF_E2E=1 \
KSEF_TEST_CONTEXT_TYPE=nip \
KSEF_TEST_CONTEXT_VALUE=... \
KSEF_TEST_XADES_CERT_PEM="$(cat cert.pem)" \
KSEF_TEST_XADES_PRIVATE_KEY_PEM="$(cat key.pem)" \
pytest tests/test_e2e_token_flows.py::test_e2e_test_environment_full_flow_xades
```

W GitHub Actions testy E2E uruchamia workflow:
- `.github/workflows/python-e2e.yml`

Workflow uruchamia się:
- na `push` (dowolny branch),
- na `pull_request` do `main`,
- ręcznie przez `workflow_dispatch`.

Repozytoryjne sekrety do ustawienia:
- `KSEF_TEST_TOKEN`, `KSEF_TEST_CONTEXT_TYPE`, `KSEF_TEST_CONTEXT_VALUE` (token TEST)
- `KSEF_DEMO_TOKEN`, `KSEF_DEMO_CONTEXT_TYPE`, `KSEF_DEMO_CONTEXT_VALUE` (token DEMO)
- `KSEF_TEST_XADES_CERT_PEM` albo `KSEF_TEST_XADES_CERT_PEM_B64` (XAdES TEST)
- `KSEF_TEST_XADES_PRIVATE_KEY_PEM` albo `KSEF_TEST_XADES_PRIVATE_KEY_PEM_B64` (XAdES TEST)
- `KSEF_TEST_XADES_SUBJECT_IDENTIFIER_TYPE` opcjonalnie, domyślnie `certificateSubject`
- `KSEF_DEMO_XADES_CERT_PEM` albo `KSEF_DEMO_XADES_CERT_PEM_B64` (XAdES DEMO)
- `KSEF_DEMO_XADES_PRIVATE_KEY_PEM` albo `KSEF_DEMO_XADES_PRIVATE_KEY_PEM_B64` (XAdES DEMO)
- `KSEF_DEMO_XADES_SUBJECT_IDENTIFIER_TYPE` opcjonalnie, domyślnie `certificateSubject`

Przygotowanie sekretów PEM w wariancie Base64 (jedna linia):

```bash
base64 < cert.pem | tr -d '\n'
base64 < key.pem | tr -d '\n'
```

Anonimizacja w CI:
- dane uwierzytelniające są pobierane wyłącznie z `GitHub Secrets`,
- wartości sekretów są maskowane w logach (`::add-mask::`),
- testy nie logują tokenów, certyfikatów ani identyfikatorów kontekstu.

## 🤝 Kontrybucja

Expand Down
2 changes: 2 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ Dokumentacja opisuje **publiczne API** biblioteki `ksef-client-python` (import:

Opis kontraktu API (OpenAPI) oraz dokumenty procesowe i ograniczenia systemu znajdują się w `ksef-docs/`.

Kompatybilność SDK: **KSeF API `v2.1.1`**.

## Wymagania

- Python `>= 3.10`
Expand Down
3 changes: 2 additions & 1 deletion docs/api/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,13 @@ Zwraca JSON z polami m.in.:

Challenge jest ważny **10 minut**. W przypadku dłuższego procesu podpisu (np. HSM) należy pobrać nowe wyzwanie.

## `submit_xades_auth_request(signed_xml, verify_certificate_chain=None)`
## `submit_xades_auth_request(signed_xml, verify_certificate_chain=None, enforce_xades_compliance=False)`

Endpoint: `POST /auth/xades-signature` (bez `accessToken`).

- `signed_xml` – podpisany XML `AuthTokenRequest`
- `verify_certificate_chain` – opcjonalna weryfikacja łańcucha certyfikatów (przydatne w TE)
- `enforce_xades_compliance` – gdy `True`, dodaje nagłówek `X-KSeF-Feature: enforce-xades-compliance` (przydatne do wcześniejszej walidacji reguł XAdES na DEMO/PROD)

Zwraca (202) obiekt inicjujący auth, m.in. `referenceNumber` i `authenticationToken`.

Expand Down
14 changes: 14 additions & 0 deletions docs/api/testdata.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,20 @@ Endpoint: `POST /testdata/attachment/revoke`

Wyłącza obsługę załączników w konfiguracji testowej.

## Blokada uwierzytelnienia kontekstu

### `block_context_authentication(request_payload, access_token=None)`

Endpoint: `POST /testdata/context/block`

Blokuje możliwość uwierzytelniania dla wskazanego kontekstu (auth kończy się kodem `480`).

### `unblock_context_authentication(request_payload, access_token=None)`

Endpoint: `POST /testdata/context/unblock`

Odblokowuje możliwość uwierzytelniania dla wskazanego kontekstu.

## Limity i rate limits (test)

### `change_session_limits(request_payload, access_token)`
Expand Down
1 change: 1 addition & 0 deletions docs/services/workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Najczęściej używane parametry:
- `context_identifier_value`: np. NIP
- `subject_identifier_type`: np. `"certificateSubject"` (lub `"certificateFingerprint"`)
- `verify_certificate_chain`: przydatne w TE
- `enforce_xades_compliance`: gdy `True`, ustawia nagłówek `X-KSeF-Feature: enforce-xades-compliance`
- `poll_interval_seconds`, `max_attempts`: parametry pollingu

### `AuthCoordinator.authenticate_with_ksef_token(...) -> AuthResult`
Expand Down
3 changes: 3 additions & 0 deletions docs/workflows/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,15 @@ result = AuthCoordinator(client.auth).authenticate_with_xades_key_pair(
context_identifier_value="5265877635",
subject_identifier_type="certificateSubject",
verify_certificate_chain=None,
enforce_xades_compliance=False,
max_attempts=90,
poll_interval_seconds=2.0,
)
access_token = result.tokens.access_token.token
```

`enforce_xades_compliance=True` wymusza dodanie nagłówka `X-KSeF-Feature: enforce-xades-compliance` podczas `POST /auth/xades-signature`.

W przypadku posiadania certyfikatu i klucza jako osobnych plików dostępne jest również wczytanie przez `XadesKeyPair.from_pem_files(...)`.

Challenge jest ważny ok. 10 minut. W przypadku dłuższego procesu podpisu (np. HSM/podpis kwalifikowany) wymagane jest pobranie nowego wyzwania.
Expand Down
9 changes: 9 additions & 0 deletions src/ksef_client/clients/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

from .base import AsyncBaseApiClient, BaseApiClient

_KSEF_FEATURE_HEADER = "X-KSeF-Feature"
_ENFORCE_XADES_COMPLIANCE_FEATURE = "enforce-xades-compliance"


class AuthClient(BaseApiClient):
def get_active_sessions(
Expand Down Expand Up @@ -51,6 +54,7 @@ def submit_xades_auth_request(
signed_xml: str,
*,
verify_certificate_chain: bool | None = None,
enforce_xades_compliance: bool = False,
) -> Any:
params = {}
if verify_certificate_chain is not None:
Expand All @@ -59,6 +63,8 @@ def submit_xades_auth_request(
"Content-Type": "application/xml",
"Accept": "application/json",
}
if enforce_xades_compliance:
headers[_KSEF_FEATURE_HEADER] = _ENFORCE_XADES_COMPLIANCE_FEATURE
response_bytes = self._request_bytes(
"POST",
"/auth/xades-signature",
Expand Down Expand Up @@ -152,6 +158,7 @@ async def submit_xades_auth_request(
signed_xml: str,
*,
verify_certificate_chain: bool | None = None,
enforce_xades_compliance: bool = False,
) -> Any:
params = {}
if verify_certificate_chain is not None:
Expand All @@ -160,6 +167,8 @@ async def submit_xades_auth_request(
"Content-Type": "application/xml",
"Accept": "application/json",
}
if enforce_xades_compliance:
headers[_KSEF_FEATURE_HEADER] = _ENFORCE_XADES_COMPLIANCE_FEATURE
response_bytes = await self._request_bytes(
"POST",
"/auth/xades-signature",
Expand Down
44 changes: 44 additions & 0 deletions src/ksef_client/clients/testdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,28 @@ def disable_attachment(
expected_status={200, 204},
)

def block_context_authentication(
self, request_payload: dict[str, Any], *, access_token: str | None = None
) -> None:
self._request_json(
"POST",
"/testdata/context/block",
json=request_payload,
access_token=access_token,
expected_status={200},
)

def unblock_context_authentication(
self, request_payload: dict[str, Any], *, access_token: str | None = None
) -> None:
self._request_json(
"POST",
"/testdata/context/unblock",
json=request_payload,
access_token=access_token,
expected_status={200},
)

def change_session_limits(self, request_payload: dict[str, Any], *, access_token: str) -> Any:
return self._request_json(
"POST",
Expand Down Expand Up @@ -240,6 +262,28 @@ async def disable_attachment(
expected_status={200, 204},
)

async def block_context_authentication(
self, request_payload: dict[str, Any], *, access_token: str | None = None
) -> None:
await self._request_json(
"POST",
"/testdata/context/block",
json=request_payload,
access_token=access_token,
expected_status={200},
)

async def unblock_context_authentication(
self, request_payload: dict[str, Any], *, access_token: str | None = None
) -> None:
await self._request_json(
"POST",
"/testdata/context/unblock",
json=request_payload,
access_token=access_token,
expected_status={200},
)

async def change_session_limits(
self, request_payload: dict[str, Any], *, access_token: str
) -> Any:
Expand Down
33 changes: 33 additions & 0 deletions src/ksef_client/openapi_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,12 @@ class AuthenticationMethod(Enum):
PERSONALSIGNATURE = "PersonalSignature"
PEPPOLSIGNATURE = "PeppolSignature"

class AuthenticationMethodCategory(Enum):
XADESSIGNATURE = "XadesSignature"
NATIONALNODE = "NationalNode"
TOKEN = "Token"
OTHER = "Other"

class AuthenticationTokenStatus(Enum):
PENDING = "Pending"
ACTIVE = "Active"
Expand Down Expand Up @@ -617,6 +623,12 @@ class SubunitPermissionsSubunitIdentifierType(Enum):
INTERNALID = "InternalId"
NIP = "Nip"

class TestDataAuthenticationContextIdentifierType(Enum):
NIP = "Nip"
INTERNALID = "InternalId"
NIPVATUE = "NipVatUe"
PEPPOLID = "PeppolId"

class TestDataAuthorizedIdentifierType(Enum):
NIP = "Nip"
PESEL = "Pesel"
Expand Down Expand Up @@ -737,6 +749,7 @@ class AuthenticationInitResponse(OpenApiModel):
@dataclass(frozen=True)
class AuthenticationListItem(OpenApiModel):
authenticationMethod: AuthenticationMethod
authenticationMethodInfo: AuthenticationMethodInfo
referenceNumber: ReferenceNumber
startDate: str
status: StatusInfo
Expand All @@ -750,9 +763,16 @@ class AuthenticationListResponse(OpenApiModel):
items: list[AuthenticationListItem]
continuationToken: Optional[str] = None

@dataclass(frozen=True)
class AuthenticationMethodInfo(OpenApiModel):
category: AuthenticationMethodCategory
code: str
displayName: str

@dataclass(frozen=True)
class AuthenticationOperationStatusResponse(OpenApiModel):
authenticationMethod: AuthenticationMethod
authenticationMethodInfo: AuthenticationMethodInfo
startDate: str
status: StatusInfo
isTokenRedeemed: Optional[bool] = None
Expand Down Expand Up @@ -796,6 +816,10 @@ class BatchSessionEffectiveContextLimits(OpenApiModel):
maxInvoiceWithAttachmentSizeInMB: int
maxInvoices: int

@dataclass(frozen=True)
class BlockContextAuthenticationRequest(OpenApiModel):
contextIdentifier: Optional[TestDataAuthenticationContextIdentifier] = None

@dataclass(frozen=True)
class CertificateEffectiveSubjectLimits(OpenApiModel):
maxCertificates: Optional[int] = None
Expand Down Expand Up @@ -1768,6 +1792,11 @@ class SubunitPermissionsSubunitIdentifier(OpenApiModel):
type: SubunitPermissionsSubunitIdentifierType
value: str

@dataclass(frozen=True)
class TestDataAuthenticationContextIdentifier(OpenApiModel):
type: TestDataAuthenticationContextIdentifierType
value: str

@dataclass(frozen=True)
class TestDataAuthorizedIdentifier(OpenApiModel):
type: TestDataAuthorizedIdentifierType
Expand Down Expand Up @@ -1825,6 +1854,10 @@ class TokenStatusResponse(OpenApiModel):
class TooManyRequestsResponse(OpenApiModel):
status: dict[str, Any]

@dataclass(frozen=True)
class UnblockContextAuthenticationRequest(OpenApiModel):
contextIdentifier: Optional[TestDataAuthenticationContextIdentifier] = None

@dataclass(frozen=True)
class UpoPageResponse(OpenApiModel):
downloadUrl: str
Expand Down
Loading
Loading