removing superfluous dependency, updating readthedocs build process, plugin doc cleanup

pull/276/head
Andrew Mirsky 2025-08-11 17:38:50 -04:00
rodzic 8e47ede192
commit bfd2d86395
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: A98E67635CDF2C39
17 zmienionych plików z 188 dodań i 62 usunięć

Wyświetl plik

@ -1,15 +1,22 @@
version: 2
version: 2
build:
os: "ubuntu-24.04"
tools:
python: "3.13"
jobs:
pre_install:
- pip install --upgrade pip
- pip install uv
- uv pip install --group dev --group docs
- uv run pytest
build:
os: "ubuntu-24.04"
tools:
python: "3.13"
apt_packages:
- libldap2-dev
- libsasl2-dev
jobs:
pre_install:
- pip install --upgrade pip
- pip install uv
- uv venv
- uv pip install --group dev --group docs ".[contrib]"
- uv run pytest --mock-docker=true
build:
html:
- uv run python -m mkdocs build --clean --site-dir $READTHEDOCS_OUTPUT/html --config-file mkdocs.rtd.yml
mkdocs:
configuration: mkdocs.rtd.yml
mkdocs:
configuration: mkdocs.rtd.yml

Wyświetl plik

@ -27,7 +27,7 @@ from amqtt.session import Session
logger = logging.getLogger(__name__)
class CertificateAuthPlugin(BaseAuthPlugin):
class UserAuthCertPlugin(BaseAuthPlugin):
"""Used a *signed* x509 certificate's `Subject AlternativeName` or `SAN` to verify client authentication.
Often used for IoT devices, this method provides the most secure form of identification. A root

Wyświetl plik

@ -9,7 +9,7 @@ For ease of use, the [`user_mgr` command-line utility](auth_db.md/#user_mgr) to
list clients. And the [`topic_mgr` command-line utility](auth_db.md/#topic_mgr) to add client access to
subscribe, publish and receive messages on topics.
## Authentication Configuration
# Authentication Configuration
::: amqtt.contrib.auth_db.UserAuthDBPlugin.Config
options:
@ -17,7 +17,7 @@ subscribe, publish and receive messages on topics.
extra:
class_style: "simple"
## Authorization Configuration
# Authorization Configuration
::: amqtt.contrib.auth_db.TopicAuthDBPlugin.Config
options:

Wyświetl plik

@ -65,9 +65,32 @@ async def main():
asyncio.run(main())
```
::: amqtt.contrib.cert.CertificateAuthPlugin
## Background
### Root & Certificate Credentials
Often used for IoT devices, this method provides the most secure form of identification. A root
certificate, often referenced as a CA certificate -- either issued by a known authority (such as LetsEncrypt)
or a self-signed certificate) is used to sign a private key and certificate for the server. Each device/client
also gets a unique private key and certificate signed by the same CA certificate; also included in the device
certificate is a 'SAN' or SubjectAlternativeName which is the device's unique identifier.
Since both server and device certificates are signed by the same CA certificate, the client can
verify the server's authenticity; and the server can verify the client's authenticity. And since
the device's certificate contains a x509 SAN, the server (with this plugin) can identify the device securely.
!!! note "URI and Client ID configuration"
`uri_domain` configuration must be set to the same uri used to generate the device credentials
when a device is connecting with private key and certificate, the `client_id` must
match the device id used to generate the device credentials.
Available ore three scripts to help with the key generation and certificate signing: `ca_creds`, `server_creds`
and `device_creds`.
!!! note "Configuring broker & client for using Self-signed root CA"
If using self-signed root credentials, the `cafile` configuration for both broker and client need to be
configured with `cafile` set to the `ca.crt`.
## Root & Certificate Credentials
The process for generating a server's private key and certificate is only done once. If you have a private key & certificate --
such as one from verifying your webserver's domain with LetsEncrypt -- that you want to use, pass them to the `server_creds` cli.
@ -99,7 +122,7 @@ flowchart LR
ssi --> skc
```
### Device credentials
## Device credentials
For each device, create a device id to generate a device-specific private key and certificate using the `device_creds` cli.
Use the same CA as was used for the server (above) so the client & server recognize each other.
@ -127,9 +150,9 @@ flowchart LR
con["country, org<br/>common name<br/>& device id"] --> ccsr
```
### Configuration
## Configuration
::: amqtt.contrib.cert.CertificateAuthPlugin.Config
::: amqtt.contrib.cert.UserAuthCertPlugin.Config
options:
show_source: false
heading_level: 4
@ -137,7 +160,7 @@ flowchart LR
class_style: "simple"
### Key and Certificate Generation
## Key and Certificate Generation
::: mkdocs-typer2
:module: amqtt.scripts.ca_creds

Wyświetl plik

@ -5,22 +5,21 @@ These are fully supported plugins but require additional dependencies to be inst
`$ pip install '.[contrib]'`
- Relational Database Auth<br/>
_includes manager script to add, remove and create db entries_
- [DB Client Authentication](auth_db.md)<br/>
Authenticate a client's connection to broker based on entries in a relational db (mysql, postgres, maria, sqlite).<br/>
`amqtt.contrib.auth_db.AuthUserDBPlugin`
- [DB Client Authorization](auth_db.md)<br/>
Determine a client's access to topics.<br/>
`amqtt.contrib.auth_db.AuthTopicDBPlugin`
- [Relational Database Auth](auth_db.md)<br/>
Grant or deny access to clients based on entries in a relational db (mysql, postgres, maria, sqlite). _Includes
manager script to add, remove and create db entries_<br/>
- `amqtt.contrib.auth_db.UserAuthDBPlugin`
- `amqtt.contrib.auth_db.TopicAuthDBPlugin`
- [HTTP Auth](http.md)<br/>
Determine client authentication and authorization based on response from a separate HTTP server.<br/>
`amqtt.contrib.http.HttpAuthTopicPlugin`
Determine client authentication and/or authorization based on response from a separate HTTP server.<br/>
- `amqtt.contrib.http.UserAuthHttpPlugin`
- `amqtt.contrib.http.TopicAuthHttpPlugin`
- [LDAP Auth](ldap.md)<br/>
Authenticate a user with an LDAP directory server.<br/>
`amqtt.contrib.ldap.LDAPAuthPlugin`
- `amqtt.contrib.ldap.UserAuthLdapPlugin`
- `amqtt.contrib.ldap.TopicAuthLdapPlugin`
- [Shadows](shadows.md)<br/>
Device shadows provide a persistent, cloud-based representation of the state of a device,
@ -31,6 +30,16 @@ These are fully supported plugins but require additional dependencies to be inst
- [Certificate Auth](cert.md)<br/>
Using client-specific certificates, signed by a common authority (even if self-signed) provides
a highly secure way of authenticating mqtt clients. Often used with IoT devices where a unique
certificate can be initialized on initial provisioning. Includes command line utilities to generate
root, broker and device certificates with the correct X509 attributes to enable authenticity.<br/>
`amqtt.contrib.cert.CertificateAuthPlugin.Config`
certificate can be initialized on initial provisioning. _Includes command line utilities to generate
root, broker and device certificates with the correct X509
attributes to enable authenticity._<br/>
`amqtt.contrib.cert.UserAuthCertPlugin`
- [JWT Auth](jwt.md)<br/>
Plugin to determine user authentication and topic authorization based on claims in a JWT.
- `amqtt.contrib.jwt.UserAuthJwtPlugin` (client authentication)
- `amqtt.contrib.jwt.TopicAuthJwtPlugin` (topic authorization)
- [Session Persistence](session.md)<br/>
Plugin to store session information and retained topic messages in the event that the broker terminates abnormally.<br/>
`amqtt.contrib.persistence.SessionDBPlugin`

Wyświetl plik

@ -8,7 +8,7 @@ that respond with information about client authentication and/or topic-level aut
Configuration of these plugins is identical (except for the uri name) so that they can be used independently, if desired.
## User Auth
# User Auth
See the [Request and Response Modes](#request-response-modes) section below for details on `params_mode` and `response_mode`.
@ -49,7 +49,7 @@ See the [Request and Response Modes](#request-response-modes) section below for
extra:
class_style: "simple"
## Topic ACL
# Topic ACL
See the [Request and Response Modes](#request-response-modes) section below for details on `params_mode` and `response_mode`.

Wyświetl plik

@ -0,0 +1,50 @@
# Authentication & Authorization from JWT
- `amqtt.contrib.jwt.UserAuthJwtPlugin` (client authentication)
- `amqtt.contrib.jwt.TopicAuthJwtPlugin` (topic authorization)
Plugin to determine user authentication and topic authorization based on claims in a JWT.
# User Authentication
For auth, the JWT should include a key as specified in the configuration as `user_clam`:
```python
from datetime import datetime, UTC, timedelta
claims = {
"username": "example_user",
"exp": datetime.now(UTC) + timedelta(hours=1),
}
```
::: amqtt.contrib.jwt.UserAuthJwtPlugin.Config
options:
show_source: false
heading_level: 4
extra:
class_style: "simple"
# Topic Authorization
For authorizing a client for certain topics, the token should also include claims for publish, subscribe and receive;
keys based on how `publish_claim`, `subscribe_claim` and `receive_claim` are specified in the plugin's configuration.
```python
from datetime import datetime, UTC, timedelta
claims = {
"username": "example_user",
"exp": datetime.now(UTC) + timedelta(hours=1),
"publish_acl": ['my/topic/#', 'my/+/other'],
"subscribe_acl": ['my/+/other'],
"receive_acl": ['#']
}
```
::: amqtt.contrib.jwt.TopicAuthJwtPlugin.Config
options:
show_source: false
heading_level: 4
extra:
class_style: "simple"

Wyświetl plik

@ -8,7 +8,7 @@ for client authentication and/or topic-level authorization.
Authenticate a user with an LDAP directory server.
## User Auth
# User Auth
::: amqtt.contrib.ldap.UserAuthLdapPlugin.Config
options:
@ -16,7 +16,7 @@ Authenticate a user with an LDAP directory server.
extra:
class_style: "simple"
## Topic Auth (ACL)
# Topic Auth (ACL)
::: amqtt.contrib.ldap.TopicAuthLdapPlugin.Config
options:

Wyświetl plik

@ -1,10 +1,10 @@
# Session Persistence
`amqtt.plugins.persistence.SessionDBPlugin`
`amqtt.contrib.persistence.SessionDBPlugin`
Plugin to store session information and retained topic messages in the event that the broker terminates abnormally.
::: amqtt.plugins.persistence.SessionDBPlugin.Config
::: amqtt.contrib.persistence.SessionDBPlugin.Config
options:
show_source: false
heading_level: 4

Wyświetl plik

@ -171,7 +171,7 @@ state (last row in table).
| `{ "levels": [1, 10, 4]}` | `{"levels": [1, 4, 10]}` | `{"levels": [1, 4, 10]}` |
| `{ "brightness": 100, "mode": "eco" }` | `{ "brightness": 80 }` | `{ "brightness": 80, "mode": null }` |
### Configuration
## Configuration
::: amqtt.contrib.shadows.ShadowPlugin.Config
options:

Wyświetl plik

@ -1,13 +1,9 @@
import ast
import json
import pprint
from typing import Any
import griffe
from _griffe.agents.inspector import Inspector
from _griffe.agents.nodes.runtime import ObjectNode
from _griffe.agents.visitor import Visitor
from _griffe.models import Attribute
from griffe import Inspector, ObjectNode, Visitor, Attribute
from amqtt.contexts import default_listeners, default_broker_plugins, default_client_plugins
from amqtt.contrib.auth_db.plugin import default_hash_scheme

Wyświetl plik

@ -50,6 +50,8 @@ nav:
- LDAP Auth: plugins/ldap.md
- Shadows: plugins/shadows.md
- Certificate Auth: plugins/cert.md
- JWT Auth: plugins/jwt.md
- Session persistence: plugins/session.md
- Reference:
- Containerization: docker.md
- Support: support.md

Wyświetl plik

@ -33,7 +33,6 @@ dependencies = [
"passlib==1.7.4", # https://pypi.org/project/passlib
"PyYAML==6.0.2", # https://pypi.org/project/PyYAML
"typer==0.15.4",
"aiohttp>=3.12.7",
"dacite>=1.9.2",
"psutil>=7.0.0"
]
@ -67,6 +66,7 @@ dev = [
]
docs = [
"griffe>=1.11.1",
"markdown-callouts>=0.4",
"markdown-exec>=1.8",
"mkdocs>=1.6",

Wyświetl plik

@ -155,3 +155,12 @@ def ca_file_fixture():
for file in temp_dir.iterdir():
file.unlink()
temp_dir.rmdir()
def pytest_addoption(parser):
parser.addoption(
"--mock-docker",
action="store",
default="false",
help="for environments where docker isn't available, mock calls which require docker",
)

Wyświetl plik

@ -13,7 +13,7 @@ import pytest
from amqtt.broker import BrokerContext, Broker
from amqtt.client import MQTTClient
from amqtt.contrib.cert import CertificateAuthPlugin
from amqtt.contrib.cert import UserAuthCertPlugin
from amqtt.errors import ConnectError
from amqtt.scripts.server_creds import server_creds as get_server_creds
from amqtt.scripts.device_creds import device_creds as get_device_creds
@ -99,9 +99,9 @@ async def test_cert_plugin(ssl_object_mock, uri_domain, client_id, expected_resu
}
bc = BrokerContext(broker=Broker(config=empty_cfg))
bc.config = CertificateAuthPlugin.Config(uri_domain=uri_domain)
bc.config = UserAuthCertPlugin.Config(uri_domain=uri_domain)
cert_auth_plugin = CertificateAuthPlugin(bc)
cert_auth_plugin = UserAuthCertPlugin(bc)
s = Session()
s.client_id = client_id
@ -128,7 +128,7 @@ async def test_client_broker_cert_authentication(ca_creds, server_creds, device_
},
'plugins': {
'amqtt.plugins.logging_amqtt.PacketLoggerPlugin':{},
'amqtt.contrib.cert.CertificateAuthPlugin': {'uri_domain': 'test.amqtt.io'},
'amqtt.contrib.cert.UserAuthCertPlugin': {'uri_domain': 'test.amqtt.io'},
}
}
@ -187,7 +187,7 @@ async def test_client_broker_wrong_certs(ca_creds, server_creds, device_creds):
},
'plugins': {
'amqtt.plugins.logging_amqtt.PacketLoggerPlugin':{},
'amqtt.contrib.cert.CertificateAuthPlugin': {'uri_domain': 'test.amqtt.io'},
'amqtt.contrib.cert.UserAuthCertPlugin': {'uri_domain': 'test.amqtt.io'},
}
}

Wyświetl plik

@ -1,17 +1,17 @@
import asyncio
import ldap
import time
from pathlib import Path
from unittest.mock import patch, MagicMock
import pytest
from amqtt.broker import BrokerContext, Broker
from amqtt.client import MQTTClient
from amqtt.contexts import BrokerConfig, ListenerConfig, ClientConfig, Action
from amqtt.contrib.auth_db.user_mgr_cli import user_app
from amqtt.contrib.ldap import UserAuthLdapPlugin, TopicAuthLdapPlugin
from amqtt.errors import ConnectError
from amqtt.session import Session
from tests.test_cli import broker
# Pin the project name to avoid creating multiple stacks
@ -29,7 +29,7 @@ def docker_compose_file(pytestconfig):
return Path(pytestconfig.rootdir) / "tests/fixtures/ldap" / "docker-compose.yml"
@pytest.fixture(scope="session")
def ldap_service(docker_ip, docker_services):
def ldap_service_docker(docker_ip, docker_services):
"""Ensure that HTTP service is up and responsive."""
# `port_for` takes a container port and returns the corresponding host port
@ -38,6 +38,36 @@ def ldap_service(docker_ip, docker_services):
time.sleep(2)
return url
@pytest.fixture(scope="session")
def ldap_service_mock():
"""Ensure that HTTP service is up and responsive."""
# `port_for` takes a container port and returns the corresponding host port
url = "ldap://localhost:0"
mock_ldap_object = MagicMock()
def mock_simple_s(*args, **kwargs):
return [ ('dn', {'uid':'jdoe', 'publishACL':[b'my/topic/one', b'my/topic/two']}), ]
def mock_simple_bind_s(*args):
p = args[1]
if p in ("adminpassword", "johndoepassword"):
return
raise ldap.INVALID_CREDENTIALS
mock_ldap_object.search_s.side_effect = mock_simple_s
mock_ldap_object.simple_bind_s.side_effect = mock_simple_bind_s
with patch("ldap.initialize", return_value=mock_ldap_object):
yield url
@pytest.fixture(scope="session")
def ldap_service(request):
mode = request.config.getoption("--mock-docker")
if mode:
return request.getfixturevalue("ldap_service_mock")
return request.getfixturevalue("ldap_service_docker")
@pytest.mark.asyncio
async def test_ldap_user_plugin(ldap_service):
ctx = BrokerContext(Broker())

10
uv.lock
Wyświetl plik

@ -132,7 +132,6 @@ name = "amqtt"
version = "0.11.3"
source = { editable = "." }
dependencies = [
{ name = "aiohttp" },
{ name = "dacite" },
{ name = "passlib" },
{ name = "psutil" },
@ -188,6 +187,7 @@ dev = [
{ name = "types-setuptools" },
]
docs = [
{ name = "griffe" },
{ name = "markdown-callouts" },
{ name = "markdown-exec" },
{ name = "mkdocs" },
@ -207,7 +207,6 @@ docs = [
[package.metadata]
requires-dist = [
{ name = "aiohttp", specifier = ">=3.12.7" },
{ name = "aiohttp", marker = "extra == 'contrib'", specifier = ">=3.12.13" },
{ name = "aiosqlite", marker = "extra == 'contrib'", specifier = ">=0.21.0" },
{ name = "argon2-cffi", marker = "extra == 'contrib'", specifier = ">=25.1.0" },
@ -258,6 +257,7 @@ dev = [
{ name = "types-setuptools", specifier = ">=78.1.0.20250329" },
]
docs = [
{ name = "griffe", specifier = ">=1.11.1" },
{ name = "markdown-callouts", specifier = ">=0.4" },
{ name = "markdown-exec", specifier = ">=1.8" },
{ name = "mkdocs", specifier = ">=1.6" },
@ -936,14 +936,14 @@ wheels = [
[[package]]
name = "griffe"
version = "1.7.3"
version = "1.11.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a9/3e/5aa9a61f7c3c47b0b52a1d930302992229d191bf4bc76447b324b731510a/griffe-1.7.3.tar.gz", hash = "sha256:52ee893c6a3a968b639ace8015bec9d36594961e156e23315c8e8e51401fa50b", size = 395137, upload-time = "2025-04-23T11:29:09.147Z" }
sdist = { url = "https://files.pythonhosted.org/packages/18/0f/9cbd56eb047de77a4b93d8d4674e70cd19a1ff64d7410651b514a1ed93d5/griffe-1.11.1.tar.gz", hash = "sha256:d54ffad1ec4da9658901eb5521e9cddcdb7a496604f67d8ae71077f03f549b7e", size = 410996, upload-time = "2025-08-11T11:38:35.528Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/c6/5c20af38c2a57c15d87f7f38bee77d63c1d2a3689f74fefaf35915dd12b2/griffe-1.7.3-py3-none-any.whl", hash = "sha256:c6b3ee30c2f0f17f30bcdef5068d6ab7a2a4f1b8bf1a3e74b56fffd21e1c5f75", size = 129303, upload-time = "2025-04-23T11:29:07.145Z" },
{ url = "https://files.pythonhosted.org/packages/e6/a3/451ffd422ce143758a39c0290aaa7c9727ecc2bcc19debd7a8f3c6075ce9/griffe-1.11.1-py3-none-any.whl", hash = "sha256:5799cf7c513e4b928cfc6107ee6c4bc4a92e001f07022d97fd8dee2f612b6064", size = 138745, upload-time = "2025-08-11T11:38:33.964Z" },
]
[[package]]