mirror of
https://gitea.publichub.eu/oscar.krause/fastapi-dls.git
synced 2025-11-26 07:06:13 +00:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
699dbf6fac | ||
|
|
317699ff58 | ||
|
|
55446f7d9c | ||
|
|
88c78efcd9 | ||
|
|
fb3ac4291f | ||
|
|
15f14cac11 | ||
|
|
018d7c34fc | ||
|
|
1aee423120 | ||
|
|
a6b2f2a942 | ||
|
|
e33024db86 | ||
|
|
4ad15f0849 | ||
|
|
7bad0359af | ||
|
|
59a7c9f15a | ||
|
|
bc6d692f0a | ||
|
|
63c37c6334 | ||
|
|
fa2c06972e | ||
|
|
e4e6387b2a | ||
|
|
f2be9dca8d | ||
|
|
52dd425583 | ||
|
|
286399d79a | ||
|
|
4ab1a2ed22 | ||
|
|
459c0e21af | ||
|
|
98ef64211b | ||
|
|
0b4bb65546 | ||
|
|
47624f5019 | ||
|
|
2b9d7821c0 | ||
|
|
45f5108717 |
@@ -1,10 +0,0 @@
|
|||||||
# https://packages.ubuntu.com
|
|
||||||
fastapi==0.91.0
|
|
||||||
uvicorn[standard]==0.15.0
|
|
||||||
python-jose[pycryptodome]==3.3.0
|
|
||||||
pycryptodome==3.11.0
|
|
||||||
python-dateutil==2.8.2
|
|
||||||
sqlalchemy==1.4.46
|
|
||||||
markdown==3.4.3
|
|
||||||
python-dotenv==0.21.0
|
|
||||||
jinja2==3.1.2
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
# https://packages.ubuntu.com
|
|
||||||
fastapi==0.101.0
|
|
||||||
uvicorn[standard]==0.23.2
|
|
||||||
python-jose[pycryptodome]==3.3.0
|
|
||||||
pycryptodome==3.11.0
|
|
||||||
python-dateutil==2.8.2
|
|
||||||
sqlalchemy==1.4.47
|
|
||||||
markdown==3.4.4
|
|
||||||
python-dotenv==1.0.0
|
|
||||||
jinja2==3.1.2
|
|
||||||
10
.DEBIAN/requirements-ubuntu-24.10.txt
Normal file
10
.DEBIAN/requirements-ubuntu-24.10.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# https://packages.ubuntu.com
|
||||||
|
fastapi==0.110.3
|
||||||
|
uvicorn[standard]==0.30.3
|
||||||
|
python-jose[pycryptodome]==3.3.0
|
||||||
|
pycryptodome==3.20.0
|
||||||
|
python-dateutil==2.9.0
|
||||||
|
sqlalchemy==2.0.32
|
||||||
|
markdown==3.6
|
||||||
|
python-dotenv==1.0.1
|
||||||
|
jinja2==3.1.3
|
||||||
@@ -48,6 +48,7 @@ package() {
|
|||||||
install -Dm755 "$srcdir/$pkgname/app/main.py" "$pkgdir/opt/$pkgname/main.py"
|
install -Dm755 "$srcdir/$pkgname/app/main.py" "$pkgdir/opt/$pkgname/main.py"
|
||||||
install -Dm755 "$srcdir/$pkgname/app/orm.py" "$pkgdir/opt/$pkgname/orm.py"
|
install -Dm755 "$srcdir/$pkgname/app/orm.py" "$pkgdir/opt/$pkgname/orm.py"
|
||||||
install -Dm755 "$srcdir/$pkgname/app/util.py" "$pkgdir/opt/$pkgname/util.py"
|
install -Dm755 "$srcdir/$pkgname/app/util.py" "$pkgdir/opt/$pkgname/util.py"
|
||||||
|
install -Dm755 "$srcdir/$pkgname/app/middleware.py" "$pkgdir/opt/$pkgname/middleware.py"
|
||||||
install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname"
|
install -Dm644 "$srcdir/$pkgname.default" "$pkgdir/etc/default/$pkgname"
|
||||||
install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
|
install -Dm644 "$srcdir/$pkgname.service" "$pkgdir/usr/lib/systemd/system/$pkgname.service"
|
||||||
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
install -Dm644 "$srcdir/$pkgname.tmpfiles" "$pkgdir/usr/lib/tmpfiles.d/$pkgname.conf"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ build:docker:
|
|||||||
changes:
|
changes:
|
||||||
- app/**/*
|
- app/**/*
|
||||||
- Dockerfile
|
- Dockerfile
|
||||||
|
- requirements.txt
|
||||||
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
- if: $CI_PIPELINE_SOURCE == 'merge_request_event'
|
||||||
tags: [ docker ]
|
tags: [ docker ]
|
||||||
before_script:
|
before_script:
|
||||||
@@ -141,14 +142,19 @@ test:
|
|||||||
DATABASE: sqlite:///../app/db.sqlite
|
DATABASE: sqlite:///../app/db.sqlite
|
||||||
parallel:
|
parallel:
|
||||||
matrix:
|
matrix:
|
||||||
- IMAGE: [ 'python:3.11-slim-bookworm', 'python:3.12-slim-bullseye' ]
|
- IMAGE: [ 'python:3.12-slim-bookworm' ]
|
||||||
REQUIREMENTS:
|
REQUIREMENTS: [ 'requirements.txt' ]
|
||||||
- requirements.txt
|
- IMAGE: [ 'debian:bookworm' ] # EOL: June 06, 2026
|
||||||
- .DEBIAN/requirements-bookworm-12.txt
|
REQUIREMENTS: [ '.DEBIAN/requirements-bookworm-12.txt' ]
|
||||||
- .DEBIAN/requirements-ubuntu-23.10.txt
|
- IMAGE: [ 'ubuntu:24.04' ] # EOL: April 2036
|
||||||
- .DEBIAN/requirements-ubuntu-24.04.txt
|
REQUIREMENTS: [ '.DEBIAN/requirements-ubuntu-24.04.txt' ]
|
||||||
|
- IMAGE: [ 'ubuntu:24.10' ]
|
||||||
|
REQUIREMENTS: [ '.DEBIAN/requirements-ubuntu-24.10.txt' ]
|
||||||
before_script:
|
before_script:
|
||||||
- apt-get update && apt-get install -y python3-dev gcc
|
- apt-get update && apt-get install -y python3-dev python3-pip python3-venv gcc
|
||||||
|
- python3 -m venv venv
|
||||||
|
- source venv/bin/activate
|
||||||
|
- pip install --upgrade pip
|
||||||
- pip install -r $REQUIREMENTS
|
- pip install -r $REQUIREMENTS
|
||||||
- pip install pytest httpx
|
- pip install pytest httpx
|
||||||
- mkdir -p app/cert
|
- mkdir -p app/cert
|
||||||
@@ -162,7 +168,7 @@ test:
|
|||||||
dotenv: version.env
|
dotenv: version.env
|
||||||
junit: ['**/report.xml']
|
junit: ['**/report.xml']
|
||||||
|
|
||||||
.test:linux:
|
.test:apt:
|
||||||
stage: test
|
stage: test
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
||||||
@@ -201,15 +207,15 @@ test:
|
|||||||
- apt-get purge -qq -y fastapi-dls
|
- apt-get purge -qq -y fastapi-dls
|
||||||
- apt-get autoremove -qq -y && apt-get clean -qq
|
- apt-get autoremove -qq -y && apt-get clean -qq
|
||||||
|
|
||||||
test:debian:
|
test:apt:debian:
|
||||||
extends: .test:linux
|
extends: .test:apt
|
||||||
image: debian:bookworm-slim
|
image: debian:bookworm-slim
|
||||||
|
|
||||||
test:ubuntu:
|
test:apt:ubuntu:
|
||||||
extends: .test:linux
|
extends: .test:apt
|
||||||
image: ubuntu:24.04
|
image: ubuntu:24.04
|
||||||
|
|
||||||
test:archlinux:
|
test:pacman:archlinux:
|
||||||
image: archlinux:base
|
image: archlinux:base
|
||||||
rules:
|
rules:
|
||||||
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
- if: $CI_COMMIT_BRANCH && $CI_COMMIT_BRANCH != $CI_DEFAULT_BRANCH
|
||||||
@@ -250,7 +256,7 @@ semgrep-sast:
|
|||||||
|
|
||||||
test_coverage:
|
test_coverage:
|
||||||
# extends: test
|
# extends: test
|
||||||
image: python:3.11-slim-bookworm
|
image: python:3.12-slim-bookworm
|
||||||
allow_failure: true
|
allow_failure: true
|
||||||
stage: test
|
stage: test
|
||||||
rules:
|
rules:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ RUN apk update \
|
|||||||
&& apk add --no-cache --virtual build-deps gcc g++ python3-dev musl-dev pkgconfig \
|
&& apk add --no-cache --virtual build-deps gcc g++ python3-dev musl-dev pkgconfig \
|
||||||
&& apk add --no-cache curl postgresql postgresql-dev mariadb-dev sqlite-dev \
|
&& apk add --no-cache curl postgresql postgresql-dev mariadb-dev sqlite-dev \
|
||||||
&& pip install --no-cache-dir --upgrade uvicorn \
|
&& pip install --no-cache-dir --upgrade uvicorn \
|
||||||
&& pip install --no-cache-dir psycopg2==2.9.9 mysqlclient==2.2.4 pysqlite3==0.5.2 \
|
&& pip install --no-cache-dir psycopg2==2.9.10 mysqlclient==2.2.6 pysqlite3==0.5.4 \
|
||||||
&& pip install --no-cache-dir -r /tmp/requirements.txt \
|
&& pip install --no-cache-dir -r /tmp/requirements.txt \
|
||||||
&& apk del build-deps
|
&& apk del build-deps
|
||||||
|
|
||||||
|
|||||||
13
README.md
13
README.md
@@ -330,11 +330,11 @@ Packages are available here:
|
|||||||
|
|
||||||
Successful tested with:
|
Successful tested with:
|
||||||
|
|
||||||
- Debian 12 (Bookworm) (EOL: tba.)
|
- **Debian 12 (Bookworm)** (EOL: June 06, 2026)
|
||||||
- Ubuntu 22.10 (Kinetic Kudu) (EOL: July 20, 2023)
|
- *Ubuntu 22.10 (Kinetic Kudu)* (EOL: July 20, 2023)
|
||||||
- Ubuntu 23.04 (Lunar Lobster) (EOL: January 2024)
|
- *Ubuntu 23.04 (Lunar Lobster)* (EOL: January 2024)
|
||||||
- Ubuntu 23.10 (Mantic Minotaur) (EOL: July 2024)
|
- *Ubuntu 23.10 (Mantic Minotaur)* (EOL: July 2024)
|
||||||
- Ubuntu 24.04 (Noble Numbat) (EOL: April 2036)
|
- **Ubuntu 24.04 (Noble Numbat)** (EOL: April 2036)
|
||||||
|
|
||||||
Not working with:
|
Not working with:
|
||||||
|
|
||||||
@@ -411,7 +411,7 @@ After first success you have to replace `--issue` with `--renew`.
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
| Variable | Default | Usage |
|
| Variable | Default | Usage |
|
||||||
|------------------------|----------------------------------------|------------------------------------------------------------------------------------------------------|
|
|--------------------------|----------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------|
|
||||||
| `DEBUG` | `false` | Toggles `fastapi` debug mode |
|
| `DEBUG` | `false` | Toggles `fastapi` debug mode |
|
||||||
| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable |
|
| `DLS_URL` | `localhost` | Used in client-token to tell guest driver where dls instance is reachable |
|
||||||
| `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable |
|
| `DLS_PORT` | `443` | Used in client-token to tell guest driver where dls instance is reachable |
|
||||||
@@ -425,6 +425,7 @@ After first success you have to replace `--issue` with `--renew`.
|
|||||||
| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid |
|
| `ALLOTMENT_REF` | `20000000-0000-0000-0000-000000000001` | Allotment identification uuid |
|
||||||
| `INSTANCE_KEY_RSA` | `<app-dir>/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs \*3 |
|
| `INSTANCE_KEY_RSA` | `<app-dir>/cert/instance.private.pem` | Site-wide private RSA key for singing JWTs \*3 |
|
||||||
| `INSTANCE_KEY_PUB` | `<app-dir>/cert/instance.public.pem` | Site-wide public key \*3 |
|
| `INSTANCE_KEY_PUB` | `<app-dir>/cert/instance.public.pem` | Site-wide public key \*3 |
|
||||||
|
| `SUPPORT_MALFORMED_JSON` | `false` | Support parsing for mal formatted "mac_address_list" ([Issue](https://git.collinwebdesigns.de/oscar.krause/fastapi-dls/-/issues/1)) |
|
||||||
|
|
||||||
\*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license
|
\*1 For example, if the lease period is one day and the renewal period is 20%, the client attempts to renew its license
|
||||||
every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the
|
every 4.8 hours. If network connectivity is lost, the loss of connectivity is detected during license renewal and the
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ app.add_middleware(
|
|||||||
allow_methods=['*'],
|
allow_methods=['*'],
|
||||||
allow_headers=['*'],
|
allow_headers=['*'],
|
||||||
)
|
)
|
||||||
|
if bool(env('SUPPORT_MALFORMED_JSON', False)):
|
||||||
|
from middleware import PatchMalformedJsonMiddleware
|
||||||
|
|
||||||
|
logger.info(f'Enabled "PatchMalformedJsonMiddleware"!')
|
||||||
|
app.add_middleware(PatchMalformedJsonMiddleware, enabled=True)
|
||||||
|
|
||||||
|
|
||||||
# Helper
|
# Helper
|
||||||
|
|||||||
43
app/middleware.py
Normal file
43
app/middleware.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||||||
|
from starlette.requests import Request
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class PatchMalformedJsonMiddleware(BaseHTTPMiddleware):
|
||||||
|
# see oscar.krause/fastapi-dls#1
|
||||||
|
|
||||||
|
REGEX = '(\"mac_address_list\"\:\s?\[)([\w\d])'
|
||||||
|
|
||||||
|
def __init__(self, app, enabled: bool):
|
||||||
|
super().__init__(app)
|
||||||
|
self.enabled = enabled
|
||||||
|
|
||||||
|
async def dispatch(self, request: Request, call_next):
|
||||||
|
body = await request.body()
|
||||||
|
content_type = request.headers.get('Content-Type')
|
||||||
|
|
||||||
|
if self.enabled and content_type == 'application/json':
|
||||||
|
body = body.decode()
|
||||||
|
try:
|
||||||
|
json.loads(body)
|
||||||
|
except json.decoder.JSONDecodeError:
|
||||||
|
logger.warning(f'Malformed json received! Try to fix it, "PatchMalformedJsonMiddleware" is enabled.')
|
||||||
|
s = PatchMalformedJsonMiddleware.fix_json(body)
|
||||||
|
logger.debug(f'Fixed JSON: "{s}"')
|
||||||
|
s = json.loads(s) # ensure json is now valid
|
||||||
|
# set new body
|
||||||
|
request._body = json.dumps(s).encode('utf-8')
|
||||||
|
|
||||||
|
response = await call_next(request)
|
||||||
|
return response
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def fix_json(s: str) -> str:
|
||||||
|
s = s.replace('\t', '')
|
||||||
|
s = s.replace('\n', '')
|
||||||
|
return re.sub(PatchMalformedJsonMiddleware.REGEX, r'\1"\2', s)
|
||||||
22
app/orm.py
22
app/orm.py
@@ -1,4 +1,4 @@
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect, text
|
from sqlalchemy import Column, VARCHAR, CHAR, ForeignKey, DATETIME, update, and_, inspect, text
|
||||||
@@ -66,7 +66,17 @@ class Origin(Base):
|
|||||||
if origin_refs is None:
|
if origin_refs is None:
|
||||||
deletions = session.query(Origin).delete()
|
deletions = session.query(Origin).delete()
|
||||||
else:
|
else:
|
||||||
deletions = session.query(Origin).filter(Origin.origin_ref in origin_refs).delete()
|
deletions = session.query(Origin).filter(Origin.origin_ref.in_(origin_refs)).delete()
|
||||||
|
session.commit()
|
||||||
|
session.close()
|
||||||
|
return deletions
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_expired(engine: Engine) -> int:
|
||||||
|
session = sessionmaker(bind=engine)()
|
||||||
|
origins = session.query(Origin).join(Lease, Origin.origin_ref == Lease.origin_ref, isouter=True).filter(Lease.lease_ref.is_(None)).all()
|
||||||
|
origin_refs = [origin.origin_ref for origin in origins]
|
||||||
|
deletions = session.query(Origin).filter(Origin.origin_ref.in_(origin_refs)).delete()
|
||||||
session.commit()
|
session.commit()
|
||||||
session.close()
|
session.close()
|
||||||
return deletions
|
return deletions
|
||||||
@@ -94,10 +104,10 @@ class Lease(Base):
|
|||||||
'lease_ref': self.lease_ref,
|
'lease_ref': self.lease_ref,
|
||||||
'origin_ref': self.origin_ref,
|
'origin_ref': self.origin_ref,
|
||||||
# 'scope_ref': self.scope_ref,
|
# 'scope_ref': self.scope_ref,
|
||||||
'lease_created': self.lease_created.isoformat(),
|
'lease_created': self.lease_created.replace(tzinfo=timezone.utc).isoformat(),
|
||||||
'lease_expires': self.lease_expires.isoformat(),
|
'lease_expires': self.lease_expires.replace(tzinfo=timezone.utc).isoformat(),
|
||||||
'lease_updated': self.lease_updated.isoformat(),
|
'lease_updated': self.lease_updated.replace(tzinfo=timezone.utc).isoformat(),
|
||||||
'lease_renewal': lease_renewal.isoformat(),
|
'lease_renewal': lease_renewal.replace(tzinfo=timezone.utc).isoformat(),
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
fastapi==0.115.3
|
fastapi==0.115.5
|
||||||
uvicorn[standard]==0.32.0
|
uvicorn[standard]==0.32.0
|
||||||
python-jose==3.3.0
|
python-jose==3.3.0
|
||||||
pycryptodome==3.21.0
|
pycryptodome==3.21.0
|
||||||
|
|||||||
14
test/main.py
14
test/main.py
@@ -1,7 +1,8 @@
|
|||||||
|
import sys
|
||||||
from base64 import b64encode as b64enc
|
from base64 import b64encode as b64enc
|
||||||
from hashlib import sha256
|
|
||||||
from calendar import timegm
|
from calendar import timegm
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from hashlib import sha256
|
||||||
from os.path import dirname, join
|
from os.path import dirname, join
|
||||||
from uuid import uuid4, UUID
|
from uuid import uuid4, UUID
|
||||||
|
|
||||||
@@ -9,7 +10,6 @@ from dateutil.relativedelta import relativedelta
|
|||||||
from jose import jwt, jwk
|
from jose import jwt, jwk
|
||||||
from jose.constants import ALGORITHMS
|
from jose.constants import ALGORITHMS
|
||||||
from starlette.testclient import TestClient
|
from starlette.testclient import TestClient
|
||||||
import sys
|
|
||||||
|
|
||||||
# add relative path to use packages as they were in the app/ dir
|
# add relative path to use packages as they were in the app/ dir
|
||||||
sys.path.append('../')
|
sys.path.append('../')
|
||||||
@@ -18,6 +18,7 @@ sys.path.append('../app')
|
|||||||
from app import main
|
from app import main
|
||||||
from app.util import load_key
|
from app.util import load_key
|
||||||
|
|
||||||
|
# main.app.add_middleware(PatchMalformedJsonMiddleware, enabled=True)
|
||||||
client = TestClient(main.app)
|
client = TestClient(main.app)
|
||||||
|
|
||||||
ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld'
|
ORIGIN_REF, ALLOTMENT_REF, SECRET = str(uuid4()), '20000000-0000-0000-0000-000000000001', 'HelloWorld'
|
||||||
@@ -106,6 +107,15 @@ def test_auth_v1_origin():
|
|||||||
assert response.json().get('origin_ref') == ORIGIN_REF
|
assert response.json().get('origin_ref') == ORIGIN_REF
|
||||||
|
|
||||||
|
|
||||||
|
def test_auth_v1_origin_malformed_json(): # see oscar.krause/fastapi-dls#1
|
||||||
|
from middleware import PatchMalformedJsonMiddleware
|
||||||
|
|
||||||
|
# test regex (temporary, until this section is merged into main.py
|
||||||
|
s = '{"environment": {"fingerprint": {"mac_address_list": [ff:ff:ff:ff:ff:ff"]}}'
|
||||||
|
replaced = PatchMalformedJsonMiddleware.fix_json(s)
|
||||||
|
assert replaced == '{"environment": {"fingerprint": {"mac_address_list": ["ff:ff:ff:ff:ff:ff"]}}'
|
||||||
|
|
||||||
|
|
||||||
def auth_v1_origin_update():
|
def auth_v1_origin_update():
|
||||||
payload = {
|
payload = {
|
||||||
"registration_pending": False,
|
"registration_pending": False,
|
||||||
|
|||||||
Reference in New Issue
Block a user