Merge pull request #281 from ajmirsky/plugin_docs

Expanded structure for plugin documentation
pull/284/head
Andrew Mirsky 2025-07-30 12:01:18 -04:00 zatwierdzone przez GitHub
commit cdcee7cbf8
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
16 zmienionych plików z 227 dodań i 150 usunięć

Wyświetl plik

@ -129,7 +129,9 @@ class Broker:
plugin_namespace: plugin namespace to use when loading plugin entry_points. defaults to `amqtt.broker.plugins`.
Raises:
BrokerError, ParserError, PluginError
BrokerError: problem with broker configuration
PluginImportError: if importing a plugin from configuration
PluginInitError: if initialization plugin fails
"""

Wyświetl plik

@ -86,7 +86,8 @@ class MQTTClient:
config: dictionary of configuration options (see [client configuration](client_config.md)).
Raises:
PluginError
PluginImportError: if importing a plugin from configuration
PluginInitError: if initialization plugin fails
"""
@ -145,7 +146,7 @@ class MQTTClient:
[CONNACK](http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/os/mqtt-v3.1.1-os.html#_Toc398718033)'s return code
Raises:
ClientError, ConnectError
ConnectError: could not connect to broker
"""
additional_headers = additional_headers if additional_headers is not None else {}
@ -380,6 +381,7 @@ class MQTTClient:
Raises:
asyncio.TimeoutError: if timeout occurs before a message is delivered
ClientError: if client is not connected
"""
if self._handler is None:

Wyświetl plik

@ -1,5 +1,5 @@
import logging
from dataclasses import dataclass, field, fields, replace
import logging
try:
from enum import Enum, StrEnum
@ -9,15 +9,14 @@ except ImportError:
class StrEnum(str, Enum): #type: ignore[no-redef]
pass
from pathlib import Path
from collections.abc import Iterator
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from dacite import Config as DaciteConfig, from_dict as dict_to_dataclass
from amqtt.mqtt.constants import QOS_0, QOS_2
if TYPE_CHECKING:
import asyncio
@ -155,9 +154,9 @@ class BrokerConfig(Dictable):
listeners: dict[Literal["default"] | str, ListenerConfig] = field(default_factory=default_listeners) # noqa: PYI051
"""Network of listeners used by the services. a 'default' named listener is required; if another listener
does not set a value, the 'default' settings are applied. See
[ListenerConfig](./#amqtt.contexts.ListenerConfig) for more information."""
[ListenerConfig](#amqtt.contexts.ListenerConfig) for more information."""
sys_interval: int | None = None
"""*Deprecated field to configure the `BrokerSysPlugin`. See [`BrokerSysPlugin`](../packaged_plugins.md/#sys-topics)
"""*Deprecated field to configure the `BrokerSysPlugin`. See [`BrokerSysPlugin`](#sys-topics)
for recommended configuration.*"""
timeout_disconnect_delay: int | None = 0
"""Client disconnect timeout without a keep-alive."""
@ -165,8 +164,8 @@ class BrokerConfig(Dictable):
"""Seconds for an inactive session to be retained."""
auth: dict[str, Any] | None = None
"""*Deprecated field used to config EntryPoint-loaded plugins. See
[`AnonymousAuthPlugin`](./#anonymous-auth-plugin) and
[`FileAuthPlugin`](/packaged_plugins/#password-file-auth-plugin) for recommended configuration.*"""
[`AnonymousAuthPlugin`](#anonymous-auth-plugin) and
[`FileAuthPlugin`](#password-file-auth-plugin) for recommended configuration.*"""
topic_check: dict[str, Any] | None = None
"""*Deprecated field used to config EntryPoint-loaded plugins. See
[`TopicTabooPlugin`](#taboo-topic-plugin) and
@ -328,7 +327,7 @@ class ClientConfig(Dictable):
"""Specify the topics and what flags should be set for messages published to them."""
broker: ConnectionConfig | None = field(default_factory=ConnectionConfig)
"""Configuration for connecting to the broker. See
[ConnectionConfig](./#amqtt.contexts.ConnectionConfig) for more information."""
[ConnectionConfig](#amqtt.contexts.ConnectionConfig) for more information."""
plugins: dict[str, Any] | list[dict[str, Any]] | None = field(default_factory=default_client_plugins)
"""The dictionary has a key of the dotted-module path of a class derived from `BasePlugin`; the value is
a dictionary of configuration options for that plugin. See [Plugins](http://localhost:8000/custom_plugins/)
@ -337,7 +336,7 @@ class ClientConfig(Dictable):
"""If establishing a secure connection, should the hostname of the certificate be verified."""
will: WillConfig | None = None
"""Message, topic and flags that should be sent to if the client disconnects. See
[WillConfig](./#amqtt.contexts.WillConfig)"""
[WillConfig](#amqtt.contexts.WillConfig)"""
def __post__init__(self) -> None:
"""Check config for errors and transform fields for easier use."""

Wyświetl plik

@ -56,14 +56,15 @@ class UserAuthDBPlugin(BaseAuthPlugin, BaseTopicPlugin):
connection: str
"""SQLAlchemy connection string for the asyncio version of the database connector:
- mysql+aiomysql://user:password@host:port/dbname
- postgresql+asyncpg://user:password@host:port/dbname
- sqlite+aiosqlite:///dbfilename.db
- `mysql+aiomysql://user:password@host:port/dbname`
- `postgresql+asyncpg://user:password@host:port/dbname`
- `sqlite+aiosqlite:///dbfilename.db`
"""
sync_schema: bool = False
"""Use SQLAlchemy to create / update the database schema."""
hash_schemes: list[str] = field(default_factory=default_hash_scheme)
"""list of hash schemes to use for passwords"""
class TopicAuthDBPlugin(BaseTopicPlugin):
@ -100,9 +101,10 @@ class TopicAuthDBPlugin(BaseTopicPlugin):
connection: str
"""SQLAlchemy connection string for the asyncio version of the database connector:
- mysql+aiomysql://user:password@host:port/dbname
- postgresql+asyncpg://user:password@host:port/dbname
- sqlite+aiosqlite:///dbfilename.db
- `mysql+aiomysql://user:password@host:port/dbname`
- `postgresql+asyncpg://user:password@host:port/dbname`
- `sqlite+aiosqlite:///dbfilename.db`
"""
sync_schema: bool = False
"""Use SQLAlchemy to create / update the database schema."""

Wyświetl plik

@ -48,7 +48,7 @@ HTTP_4xx_MIN = 400
HTTP_4xx_MAX = 499
class HttpAuthTopicPlugin(BaseAuthPlugin, BaseTopicPlugin):
class HttpAuthPlugin(BaseAuthPlugin, BaseTopicPlugin):
def __init__(self, context: BrokerContext) -> None:
super().__init__(context)
@ -137,44 +137,27 @@ class HttpAuthTopicPlugin(BaseAuthPlugin, BaseTopicPlugin):
@dataclass
class Config:
"""Configuration for the HTTP Auth & ACL Plugin.
Members:
- host *(str) hostname of the server for the auth & acl check
- port *(int) port of the server for the auth & acl check
- user_uri *(str) uri of the topic check (e.g. '/user')
- topic_uri *(str) uri of the topic check (e.g. '/acl')
- request_method *(RequestMethod) send the request as a GET, POST or PUT
- params_mode *(ParamsMode) send the request with json or form data
- response_mode *(ResponseMode) expected response from the auth/acl server. STATUS (code), JSON, or TEXT.
- user_agent *(str) the 'User-Agent' header sent along with the request
ParamsMode:
for user authentication, the http server will receive in json or form format the following:
- username *(str)*
- password *(str)*
- client_id *(str)*
for superuser validation, the http server will receive in json or form format the following:
- username *(str)*
for acl check, the http server will receive in json or form format the following:
- username *(str)*
- client_id *(str)*
- topic *(str)*
- acc *(int)* client can receive (1), can publish(2), can receive & publish (3) and can subscribe (4)
"""
"""Configuration for the HTTP Auth & ACL Plugin."""
host: str
"""hostname of the server for the auth & acl check"""
port: int
"""port of the server for the auth & acl check"""
user_uri: str
"""URI of the topic check (e.g. '/user')"""
topic_uri: str
"""URI of the topic check (e.g. '/acl')"""
request_method: RequestMethod = RequestMethod.GET
params_mode: ParamsMode = ParamsMode.JSON
response_mode: ResponseMode = ResponseMode.JSON
"""send the request as a GET, POST or PUT"""
params_mode: ParamsMode = ParamsMode.JSON # see docs/plugins/http.md for additional details
"""send the request with `JSON` or `FORM` data. *additional details below*"""
response_mode: ResponseMode = ResponseMode.JSON # see docs/plugins/http.md for additional details
"""expected response from the auth/acl server. `STATUS` (code), `JSON`, or `TEXT`. *additional details below*"""
with_tls: bool = False
"""http or https"""
user_agent: str = "amqtt"
"""the 'User-Agent' header sent along with the request"""
superuser_uri: str | None = None
"""URI to verify if the user is a superuser (e.g. '/superuser'), `None` if superuser is not supported"""
timeout: int = 5
"""duration, in seconds, to wait for the HTTP server to respond"""

Wyświetl plik

@ -1,81 +0,0 @@
# Contributed Plugins
Plugins that are not part of the core functionality of the aMQTT broker or client and require additional dependencies:
`$ pip install '.[contrib]'`
# Relational Database for Authentication and Authorization
- `amqtt.contrib.auth_db.AuthUserDBPlugin` (authentication) verify a client's ability to connect to broker
- `amqtt.contrib.auth_db.AuthTopicDBPlugine` (authorization) determine a client's access to topics
Relational database access is supported using SQLAlchemy so MySQL, MariaDB, Postgres and SQLite support is available.
For ease of use, the [`user_mgr` command-line utility](contrib_plugins.md/#user_mgr) to add, remove, update and
list clients. And the [`topic_mgr` command-line utility](contrib_plugins.md/#user_topic) to add client access to
subscribe, publish and receive messages on topics.
## Authentication Configuration
::: amqtt.contrib.auth_db.UserAuthDBPlugin.Config
## Authorization Configuration
::: amqtt.contrib.auth_db.TopicAuthDBPlugin.Config
## Command line for authentication
::: mkdocs-typer2
:module: amqtt.contrib.auth_db.user_mgr_cli
:name: user_mgr
## Command line for authorization
::: mkdocs-typer2
:module: amqtt.contrib.auth_db.topic_mgr_cli
:name: topic_mgr
# Authentication & Topic Access via external HTTP server
`amqtt.contrib.http.HttpAuthTopicPlugin`
If clients accessing the broker are managed by another application, implement API endpoints
that allows the broker to check if a client is authenticated and what topics that client
is authorized to access.
**Configuration**
- `host` *(str) hostname of the server for the auth & acl check
- `port` *(int) port of the server for the auth & acl check
- `user_uri` *(str) uri of the user auth check (e.g. '/user')
- `topic_uri` *(str) uri of the topic check (e.g. '/acl')
- `request_method` *(RequestMethod) send the request as a GET, POST or PUT
- `params_mode` *(ParamsMode) send the request with json or form data
- `response_mode` *(ResponseMode) expected response from the auth/acl server. STATUS (code), JSON, or TEXT.
- `user_agent` *(str) the 'User-Agent' header sent along with the request
Each endpoint (uri) will receive the information needed to determine authentication and authorization (in either
json or form data format, based on the `params_mode`)
For user authentication (`user_uri`), the http server will receive in json or form format the following:
- username *(str)*
- password *(str)*
- client_id *(str)*
For superuser validation (`superuser_uri`), the http server will receive in json or form format the following:
- username *(str)*
For acl check (`acl_uri`), the http server will receive in json or form format the following:
- username *(str)*
- client_id *(str)*
- topic *(str)*
- acc *(int)* client can receive (1), can publish(2), can receive & publish (3) and can subscribe (4)
The HTTP endpoints can respond in three different ways, depending on `response_mode`:
1. STATUS - allowing access should respond with a 2xx status code. rejection is 4xx.
if a 5xx is received, the plugin will not participate in the filtering operation and will defer to another topic filtering plugin to determine access
2. JSON - response should be `{'ok':true|false|null, 'error':'optional reason for false or null response'}`.
`true` allows access, `false` denies access and `null` the plugin will not participate in the filtering operation
3. TEXT - `ok` allows access, any other message denies access. non-participation not supported with this mode.

Wyświetl plik

@ -0,0 +1,39 @@
# Relational Database for Authentication and Authorization
- `amqtt.contrib.auth_db.AuthUserDBPlugin` (authentication) verify a client's ability to connect to broker
- `amqtt.contrib.auth_db.AuthTopicDBPlugin` (authorization) determine a client's access to topics
Relational database access is supported using SQLAlchemy so MySQL, MariaDB, Postgres and SQLite support is available.
For ease of use, the [`user_mgr` command-line utility](auth_db.md/#user_mgr) to add, remove, update and
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
::: amqtt.contrib.auth_db.UserAuthDBPlugin.Config
options:
heading_level: 4
extra:
class_style: "simple"
## Authorization Configuration
::: amqtt.contrib.auth_db.TopicAuthDBPlugin.Config
options:
heading_level: 4
extra:
class_style: "simple"
## CLI
::: mkdocs-typer2
:module: amqtt.contrib.auth_db.user_mgr_cli
:name: user_mgr
::: mkdocs-typer2
:module: amqtt.contrib.auth_db.topic_mgr_cli
:name: topic_mgr

Wyświetl plik

@ -0,0 +1,19 @@
# Contributed Plugins
These are fully supported plugins but require additional dependencies to be installed:
`$ 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`
- [HTTP Auth](http.md)<br/>
Determine client authentication and authorization based on response from a separate HTTP server.<br/>
`amqtt.contrib.http.HttpAuthTopicPlugin`

Wyświetl plik

@ -77,7 +77,9 @@ and then run via `amqtt -c myBroker.yaml`.
variables to configure its behavior.
::: amqtt.plugins.base.BasePlugin
options:
show_source: false
heading_level: 3
## Events
@ -85,16 +87,13 @@ and then run via `amqtt -c myBroker.yaml`.
All plugins are notified of events if the `BasePlugin` subclass implements one or more of these methods:
### Client and Broker
### Client
- `async def on_mqtt_packet_sent(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None`
- `async def on_mqtt_packet_received(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None`
### Client Only
none
### Broker Only
### Broker
- `async def on_broker_pre_start(self) -> None`
- `async def on_broker_post_start(self) -> None`
@ -111,6 +110,9 @@ none
- `async def on_broker_client_unsubscribed(self, *, client_id: str, topic: str) -> None`
- `async def on_broker_message_received(self, *, client_id: str, message: ApplicationMessage) -> None`
- `async def on_broker_message_broadcast(self, *, client_id: str, message: ApplicationMessage) -> None`
- `async def on_mqtt_packet_sent(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None`
- `async def on_mqtt_packet_received(self, *, packet: MQTTPacket[MQTTVariableHeader, MQTTPayload[MQTTVariableHeader], MQTTFixedHeader], session: Session | None = None) -> None`
## Authentication Plugins
@ -128,6 +130,9 @@ If there are multiple authentication plugins:
- `None` gets ignored from the determination
::: amqtt.plugins.base.BaseAuthPlugin
options:
show_source: false
heading_level: 3
## Topic Filter Plugins
@ -144,7 +149,9 @@ If there are multiple topic plugins:
- `None` will be ignored
::: amqtt.plugins.base.BaseTopicPlugin
options:
show_source: false
heading_level: 3
!!! note
A custom plugin class can subclass from both `BaseAuthPlugin` and `BaseTopicPlugin` as long it defines

Wyświetl plik

@ -0,0 +1,97 @@
# Authentication & Authorization via external HTTP server
`amqtt.contrib.http.HttpAuthPlugin`
If clients accessing the broker are managed by another application, it can implement API endpoints
that respond with information about client authenticated and topic-level authorization.
!!! info "browser-based mqtt over websockets"
One of the primary use cases for this plugin is to enable browser-based applications to communicate with mqtt
over websockets.
!!! warning
Care must be taken to make sure the mqtt password is secure (encrypted).
For more implementation information:
??? info "recipe for authentication"
Provide the client id and username when webpage is initially rendered or passed to the mqtt initialization from stored
cookies. If application is secure, the user's password will be hashed should be encrypted and cannot be used to
authenticate a client. Instead, the application should create an encrypted password (eg jwt) which the server
can then verify when the broker contacts the application.
??? example "mqtt in javascript"
Example initialization of mqtt in javascript:
import mqtt from 'mqtt';
const url = 'https://path.to.amqtt.broker';
const options = {
'myclientid',
connectTimeout: 30000,
username: 'myclientid',
password: '' // encrypted password
};
try {
const clientMqtt = await mqtt.connect(url, options);
See the 'Request and Response Modes' section below for details on `params_mode` and `response_mode`.
::: amqtt.contrib.http.HttpAuthPlugin.Config
options:
show_source: false
heading_level: 4
extra:
class_style: "simple"
[//]: # (manually creating the heading so it doesn't show in the sidebar ToC)
<h2>Params and Response Modes</h2>
Each URI endpoint will receive different information in order to determine authentication and authorization;
format will depend on `params_mode` configuration attribute (`json` or `form`).:
*For user authentication, the request will contain:*
- username *(str)*
- password *(str)*
- client_id *(str)*
*For superuser validation, the request will contain:*
- username *(str)*
*For acl check, the request will contain:*
- username *(str)*
- client_id *(str)*
- topic *(str)*
- acc *(int)* : client can receive (1), can publish(2), can receive & publish (3) and can subscribe (4)
All endpoints should respond with the following, dependent on `response_mode` configuration attribute:
*In `status` mode:
- status code: 2xx (granted) or 4xx(denied) or 5xx (noop)
!!! note "5xx response"
**noop** (no operation): plugin will not participate in the filtering operation and will defer to another
topic filtering plugin to determine access. if there is no other topic filtering plugin, access will be denied.
*In `json` mode:*
- status code: 2xx
- content-type: application/json
- response: {'ok': True } (granted), {'ok': False, 'error': 'optional error message' } (denied) or { 'error': 'optional error message' } (noop)
!!! note "excluded 'ok' key"
**noop** (no operation): plugin will not participate in the filtering operation and will defer to another
topic filtering plugin to determine access. if there is no other topic filtering plugin, access will be denied.
*In `text` mode:*
- status code: 2xx
- content-type: text/plain
- response: 'ok' or 'error'
!!! note "noop not supported"
in text mode, noop (no operation) is not supported

Wyświetl plik

@ -23,7 +23,7 @@ and configured for the broker:
--8<-- "pyproject.toml:included"
```
But the same 4 plugins were activated in the previous default config:
But the previous default config only caused 4 plugins to be active:
```yaml
--8<-- "samples/legacy.yaml"
@ -248,7 +248,7 @@ plugins:
`amqtt.plugins.logging_amqtt.PacketLoggerPlugin`
This plugin issues debug-level messages for [mqtt events](custom_plugins.md#client-and-broker): `on_mqtt_packet_sent`
This plugin issues debug-level messages for [mqtt events](custom_plugins.md#events): `on_mqtt_packet_sent`
and `on_mqtt_packet_received`.
```yaml

Wyświetl plik

@ -32,4 +32,5 @@ used by the included broker script: [broker configuration](broker_config.md)
::: amqtt.broker.Broker
[^1]: See [PyYAML](http://pyyaml.org/wiki/PyYAMLDocumentation) for loading YAML files as Python dict.

Wyświetl plik

@ -10,11 +10,13 @@ from _griffe.agents.visitor import Visitor
from _griffe.models import Attribute
from amqtt.contexts import default_listeners, default_broker_plugins, default_client_plugins
from amqtt.contrib.auth_db.plugin import default_hash_scheme
default_factory_map = {
'default_listeners': default_listeners(),
'default_broker_plugins': default_broker_plugins(),
'default_client_plugins': default_client_plugins()
'default_client_plugins': default_client_plugins(),
'default_hash_scheme': default_hash_scheme()
}
def get_qualified_name(node: ast.AST) -> str | None:

1
docs/templates/README vendored 100644
Wyświetl plik

@ -0,0 +1 @@
template overrides for mkdocs-materials

Wyświetl plik

@ -38,9 +38,12 @@ nav:
- Client: references/client.md
- Common: references/common.md
- Plugins:
- Packaged: packaged_plugins.md
- Custom: custom_plugins.md
- Contributed: contrib_plugins.md
- Packaged: plugins/packaged_plugins.md
- Custom: plugins/custom_plugins.md
- Contributed:
- plugins/contrib.md
- Database Auth: plugins/auth_db.md
- HTTP Auth: plugins/http.md
- Configuration:
- Broker: references/broker_config.md
- Client: references/client_config.md
@ -58,6 +61,7 @@ theme:
name: material
logo: assets/amqtt_bw.svg
features:
- toc.integrate
- announce.dismiss
- content.action.edit
- content.action.view
@ -118,6 +122,7 @@ markdown_extensions:
- pymdownx.tilde
- toc:
permalink: "¤"
toc_depth: 3
plugins:
- search
@ -135,7 +140,6 @@ plugins:
options:
# extra:
# template_log_display: true
annotation_path: "full"
docstring_options:
ignore_init_summary: true
docstring_section_style: list

Wyświetl plik

@ -8,7 +8,7 @@ from aiohttp.web import Response
from amqtt.broker import BrokerContext, Broker
from amqtt.contexts import Action
from amqtt.contrib.http import HttpAuthTopicPlugin, ParamsMode, ResponseMode, RequestMethod
from amqtt.contrib.http import HttpAuthPlugin, ParamsMode, ResponseMode, RequestMethod
from amqtt.session import Session
logger = logging.getLogger(__name__)
@ -135,7 +135,7 @@ async def test_request_acl_response(empty_broker, http_server, kind, url,
username, matcher, is_allowed):
context = BrokerContext(broker=empty_broker)
context.config = HttpAuthTopicPlugin.Config(
context.config = HttpAuthPlugin.Config(
host="127.0.0.1",
port=8080,
user_uri=url,
@ -144,7 +144,7 @@ async def test_request_acl_response(empty_broker, http_server, kind, url,
params_mode=params_mode,
response_mode=response_mode,
)
http_acl = HttpAuthTopicPlugin(context)
http_acl = HttpAuthPlugin(context)
logger.warning(f'kind is {kind}')
if kind == TestKind.ACL:
s = Session()