Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .bumpversion.cfg
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[bumpversion]
current_version = 1.9.1
current_version = 1.9.2
commit = False
tag = False

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand Down
2 changes: 2 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ repos:
hooks:
- id: bandit
args: ["--skip=B101"]
additional_dependencies:
- pbr
- repo: https://github.com/Lucas-C/pre-commit-hooks-markup
rev: v1.0.1
hooks:
Expand Down
200 changes: 156 additions & 44 deletions poetry.lock

Large diffs are not rendered by default.

9 changes: 3 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
[tool.poetry]
name = "libpvarki"
version = "1.9.1"
version = "1.9.2"
description = "Common helpers like standard logging init"
authors = ["Eero af Heurlin <eero.afheurlin@iki.fi>"]
homepage = "https://github.com/pvarki/python-libpvarki/"
repository = "https://github.com/pvarki/python-libpvarki/"
license = "MIT"
readme = "README.rst"


[tool.black]
line-length = 120
target-version = ['py38']
Expand Down Expand Up @@ -56,19 +55,17 @@ omit = ["tests/*"]
branch = true

[tool.poetry.dependencies]
python = "^3.8"
python = "^3.9"
ecs-logging = "^2.0"
fastapi = ">0.89,<1.0" # caret behaviour on 0.x is to lock to 0.x.*
# FIXME: Migrate to v2, see https://docs.pydantic.dev/2.3/migration/#basesettings-has-moved-to-pydantic-settings
pydantic= ">=1.10,<2.0"
cryptography = ">=41.0"
libadvian = "^1.4"
aiohttp = ">=3.10.2,<4.0"
aiodns = "^3.0"
brotli = "^1.0"
cchardet = { version="^2.1", python="<=3.10"}
# FIXME: Once everything using this is migrared to cryptography drop the dep
pyopenssl = ">=23.2"
libadvian = { git = "https://gitlab.com/advian-oss/python-libadvian.git", rev = "1002a851ba6284551132dbef075c2fa0e1ba110d" } # pragma: allowlist secret


[tool.poetry.group.dev.dependencies]
Expand Down
2 changes: 1 addition & 1 deletion src/libpvarki/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
""" Common helpers like standard logging init """
__version__ = "1.9.1" # NOTE Use `bump2version --config-file patch` to bump versions correctly
__version__ = "1.9.2" # NOTE Use `bump2version --config-file patch` to bump versions correctly
228 changes: 228 additions & 0 deletions src/libpvarki/auditlogging/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
# libpvarki.auditlogging

`libpvarki` module for providing structured audit logging compliant with organizational requirements.

## Structure

```
auditlogging/
├── README.md
├── src/
│ └── libpvarki/
│ └── auditlogging/
│ ├── __init__.py # Public API, AUDIT level setup
│ ├── context.py # ContextVars for async-safe request context
│ ├── middleware.py # FastAPI AuditMiddleware
│ ├── helpers.py # audit_log() and convenience functions
│ ├── propagation.py # Service-to-service header propagation
│ └── py.typed # PEP 561 marker
└── tests/
└── test_auditlogging.py
```

## Installation

Copy the `auditlogging/` directory into `libpvarki/src/libpvarki/`:

```bash
cp -r src/libpvarki/auditlogging /path/to/python-libpvarki/src/libpvarki/
cp tests/test_auditlogging.py /path/to/python-libpvarki/tests/
```

### Prerequisites

Requires libadvian with MR #15 for native AUDIT level. Until merged:

```toml
# pyproject.toml
[tool.poetry.dependencies]
libadvian = { git = "https://gitlab.com/advian-oss/python-libadvian.git", branch = "log_levels" }
```

The module includes a fallback that adds AUDIT level if libadvian doesn't have it yet.

## Integration with Existing Stack

```
libadvian.logging ← MR #15 adds AUDIT level
libpvarki.logging ← ECS formatting via ecs-logging
libpvarki.auditlogging ← THIS MODULE
rmapi / takrmapi / ocsprest / products
```

## Quick Start

### 1. Initialize in FastAPI app

```python
from fastapi import FastAPI
from libpvarki.auditlogging import init_audit, AuditMiddleware
import logging

app = FastAPI()
app.add_middleware(AuditMiddleware)

@app.on_event("startup")
async def startup():
init_audit(logging.INFO)
```

### 2. Log audit events

```python
import logging
from libpvarki.auditlogging import audit_log

LOGGER = logging.getLogger(__name__)

LOGGER.audit(
"Certificate issued for user",
extra=audit_log(
category="iam",
action="cert_issue",
outcome="success",
target_user="NORPPA11",
target_resource="DEADBEEF", # cert serial
)
)
```

### 3. Propagate context to downstream services

```python
from libpvarki.mtlshelp.session import get_session
from libpvarki.auditlogging import get_propagation_headers

session = await get_session(client_cert, client_key, ca_cert)
headers = get_propagation_headers() # Includes X-Initiator-* headers
await session.post(url, json=data, headers=headers)
```

## Request Flow

```
User (NORPPA11) ─mTLS─► nginx ───► rmapi ───► takrmapi
│ │ │
│ │ └── Sees X-Initiator-User: NORPPA11
│ └── Extracts from X-ClientCert-DN
└── Sets X-ClientCert-DN: CN=NORPPA11
```

## Header Conventions

### nginx → service (direct mTLS)

```
X-Request-ID: <trace-id>
X-Real-IP: <client-ip>
X-ClientCert-DN: CN=<callsign>,O=PVARKI,C=FI
X-ClientCert-Serial: <cert-serial>
```

### service → service (propagation)

```
X-Request-ID: <trace-id>
X-Initiator-User: <callsign>
X-Initiator-IP: <client-ip>
X-Initiator-Role: <role>
X-Initiator-Cert-Serial: <cert-serial>
```

## nginx Configuration

```nginx

Choose a reason for hiding this comment

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

Setting these in the actual web client end should not be possible as well, so they should be dropped by the nginx that is proxying the client requests.

# In your nginx server block
proxy_set_header X-Request-ID $request_id;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-ClientCert-DN $ssl_client_s_dn;
proxy_set_header X-ClientCert-Serial $ssl_client_serial;
```

## Event Categories

| Category | Use For |
|----------|---------|
| `authentication` | Login, logout, OTP exchange, JWT validation |
| `authorization` | Permission checks, access denied |
| `iam` | Enrollment, cert issuance, revocation |
| `configuration` | Settings changes, admin actions |
| `session` | JWT creation, refresh, expiry |
| `intrusion_detection` | Failed attempts, anomalies |

## Convenience Functions

```python
from libpvarki.auditlogging import (
audit_authentication, # category="authentication"
audit_iam, # category="iam"
audit_authorization, # category="authorization"
audit_configuration, # category="configuration"
audit_session, # category="session"
audit_anomaly, # category="intrusion_detection", outcome="failure"
)

# Examples
LOGGER.audit("Login successful", extra=audit_authentication("login", outcome="success"))
LOGGER.audit("Cert issued", extra=audit_iam("cert_issue", target_user="NORPPA11"))
LOGGER.audit("Brute force detected", extra=audit_anomaly("brute_force", error_message="5 failed attempts"))
```

## ECS Output Example

With `LOG_CONSOLE_FORMATTER=ecs` (default), output is ECS-compliant JSON:

```json
{
"@timestamp": "2025-12-21T00:00:00.000Z",
"ecs.version": "1.6.0",
"log.level": "AUDIT",
"log.logger": "rasenmaeher_api.routes.token",
"message": "OTP exchange successful for NORPPA11",
"event.category": "authentication",
"event.action": "otp_exchange",
"event.outcome": "success",
"source.ip": "203.0.113.50",
"source.user.name": "NORPPA11",
"tls.client.x509.serial_number": "DEADBEEF",
"user.target.name": "NORPPA11",
"trace.id": "abc-123-def-456",
"service.name": "rmapi",
"service.version": "1.6.4"
}
```

## Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `LOG_CONSOLE_FORMATTER` | `ecs` | `ecs` for JSON, `local` for human-readable |
| `SERVICE_NAME` | hostname | Service name in logs |
| `RELEASE_TAG` | `unknown` | Service version in logs |

## Testing

```bash
cd python-libpvarki
pytest tests/test_auditlogging.py -v
```

## Migration from init_logging

Replace `init_logging` with `init_audit` to enable AUDIT level:

```python
# Before
from libpvarki.logging import init_logging
init_logging(logging.INFO)

# After
from libpvarki.auditlogging import init_audit
init_audit(logging.INFO)
```

Or continue using `init_logging` - the AUDIT level is registered on module import.
Loading
Loading