kopia lustrzana https://github.com/Yakifo/amqtt
removing superfluous dependency, updating readthedocs build process, plugin doc cleanup
rodzic
8e47ede192
commit
bfd2d86395
|
@ -1,15 +1,22 @@
|
||||||
version: 2
|
version: 2
|
||||||
|
|
||||||
build:
|
build:
|
||||||
os: "ubuntu-24.04"
|
os: "ubuntu-24.04"
|
||||||
tools:
|
tools:
|
||||||
python: "3.13"
|
python: "3.13"
|
||||||
|
apt_packages:
|
||||||
|
- libldap2-dev
|
||||||
|
- libsasl2-dev
|
||||||
jobs:
|
jobs:
|
||||||
pre_install:
|
pre_install:
|
||||||
- pip install --upgrade pip
|
- pip install --upgrade pip
|
||||||
- pip install uv
|
- pip install uv
|
||||||
- uv pip install --group dev --group docs
|
- uv venv
|
||||||
- uv run pytest
|
- 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:
|
mkdocs:
|
||||||
configuration: mkdocs.rtd.yml
|
configuration: mkdocs.rtd.yml
|
||||||
|
|
|
@ -27,7 +27,7 @@ from amqtt.session import Session
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class CertificateAuthPlugin(BaseAuthPlugin):
|
class UserAuthCertPlugin(BaseAuthPlugin):
|
||||||
"""Used a *signed* x509 certificate's `Subject AlternativeName` or `SAN` to verify client authentication.
|
"""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
|
Often used for IoT devices, this method provides the most secure form of identification. A root
|
||||||
|
|
|
@ -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
|
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.
|
subscribe, publish and receive messages on topics.
|
||||||
|
|
||||||
## Authentication Configuration
|
# Authentication Configuration
|
||||||
|
|
||||||
::: amqtt.contrib.auth_db.UserAuthDBPlugin.Config
|
::: amqtt.contrib.auth_db.UserAuthDBPlugin.Config
|
||||||
options:
|
options:
|
||||||
|
@ -17,7 +17,7 @@ subscribe, publish and receive messages on topics.
|
||||||
extra:
|
extra:
|
||||||
class_style: "simple"
|
class_style: "simple"
|
||||||
|
|
||||||
## Authorization Configuration
|
# Authorization Configuration
|
||||||
|
|
||||||
::: amqtt.contrib.auth_db.TopicAuthDBPlugin.Config
|
::: amqtt.contrib.auth_db.TopicAuthDBPlugin.Config
|
||||||
options:
|
options:
|
||||||
|
|
|
@ -65,9 +65,32 @@ async def main():
|
||||||
asyncio.run(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 --
|
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.
|
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
|
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.
|
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.
|
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
|
con["country, org<br/>common name<br/>& device id"] --> ccsr
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration
|
## Configuration
|
||||||
|
|
||||||
::: amqtt.contrib.cert.CertificateAuthPlugin.Config
|
::: amqtt.contrib.cert.UserAuthCertPlugin.Config
|
||||||
options:
|
options:
|
||||||
show_source: false
|
show_source: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
@ -137,7 +160,7 @@ flowchart LR
|
||||||
class_style: "simple"
|
class_style: "simple"
|
||||||
|
|
||||||
|
|
||||||
### Key and Certificate Generation
|
## Key and Certificate Generation
|
||||||
|
|
||||||
::: mkdocs-typer2
|
::: mkdocs-typer2
|
||||||
:module: amqtt.scripts.ca_creds
|
:module: amqtt.scripts.ca_creds
|
||||||
|
|
|
@ -5,22 +5,21 @@ These are fully supported plugins but require additional dependencies to be inst
|
||||||
`$ pip install '.[contrib]'`
|
`$ pip install '.[contrib]'`
|
||||||
|
|
||||||
|
|
||||||
- Relational Database Auth<br/>
|
- [Relational Database Auth](auth_db.md)<br/>
|
||||||
_includes manager script to add, remove and create db entries_
|
Grant or deny access to clients based on entries in a relational db (mysql, postgres, maria, sqlite). _Includes
|
||||||
- [DB Client Authentication](auth_db.md)<br/>
|
manager script to add, remove and create db entries_<br/>
|
||||||
Authenticate a client's connection to broker based on entries in a relational db (mysql, postgres, maria, sqlite).<br/>
|
- `amqtt.contrib.auth_db.UserAuthDBPlugin`
|
||||||
`amqtt.contrib.auth_db.AuthUserDBPlugin`
|
- `amqtt.contrib.auth_db.TopicAuthDBPlugin`
|
||||||
- [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/>
|
- [HTTP Auth](http.md)<br/>
|
||||||
Determine client authentication and authorization based on response from a separate HTTP server.<br/>
|
Determine client authentication and/or authorization based on response from a separate HTTP server.<br/>
|
||||||
`amqtt.contrib.http.HttpAuthTopicPlugin`
|
- `amqtt.contrib.http.UserAuthHttpPlugin`
|
||||||
|
- `amqtt.contrib.http.TopicAuthHttpPlugin`
|
||||||
|
|
||||||
- [LDAP Auth](ldap.md)<br/>
|
- [LDAP Auth](ldap.md)<br/>
|
||||||
Authenticate a user with an LDAP directory server.<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/>
|
- [Shadows](shadows.md)<br/>
|
||||||
Device shadows provide a persistent, cloud-based representation of the state of a device,
|
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/>
|
- [Certificate Auth](cert.md)<br/>
|
||||||
Using client-specific certificates, signed by a common authority (even if self-signed) provides
|
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
|
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
|
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/>
|
root, broker and device certificates with the correct X509
|
||||||
`amqtt.contrib.cert.CertificateAuthPlugin.Config`
|
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`
|
||||||
|
|
|
@ -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.
|
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`.
|
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:
|
extra:
|
||||||
class_style: "simple"
|
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`.
|
See the [Request and Response Modes](#request-response-modes) section below for details on `params_mode` and `response_mode`.
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
@ -8,7 +8,7 @@ for client authentication and/or topic-level authorization.
|
||||||
|
|
||||||
Authenticate a user with an LDAP directory server.
|
Authenticate a user with an LDAP directory server.
|
||||||
|
|
||||||
## User Auth
|
# User Auth
|
||||||
|
|
||||||
::: amqtt.contrib.ldap.UserAuthLdapPlugin.Config
|
::: amqtt.contrib.ldap.UserAuthLdapPlugin.Config
|
||||||
options:
|
options:
|
||||||
|
@ -16,7 +16,7 @@ Authenticate a user with an LDAP directory server.
|
||||||
extra:
|
extra:
|
||||||
class_style: "simple"
|
class_style: "simple"
|
||||||
|
|
||||||
## Topic Auth (ACL)
|
# Topic Auth (ACL)
|
||||||
|
|
||||||
::: amqtt.contrib.ldap.TopicAuthLdapPlugin.Config
|
::: amqtt.contrib.ldap.TopicAuthLdapPlugin.Config
|
||||||
options:
|
options:
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
# Session Persistence
|
# 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.
|
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:
|
options:
|
||||||
show_source: false
|
show_source: false
|
||||||
heading_level: 4
|
heading_level: 4
|
||||||
|
|
|
@ -171,7 +171,7 @@ state (last row in table).
|
||||||
| `{ "levels": [1, 10, 4]}` | `{"levels": [1, 4, 10]}` | `{"levels": [1, 4, 10]}` |
|
| `{ "levels": [1, 10, 4]}` | `{"levels": [1, 4, 10]}` | `{"levels": [1, 4, 10]}` |
|
||||||
| `{ "brightness": 100, "mode": "eco" }` | `{ "brightness": 80 }` | `{ "brightness": 80, "mode": null }` |
|
| `{ "brightness": 100, "mode": "eco" }` | `{ "brightness": 80 }` | `{ "brightness": 80, "mode": null }` |
|
||||||
|
|
||||||
### Configuration
|
## Configuration
|
||||||
|
|
||||||
::: amqtt.contrib.shadows.ShadowPlugin.Config
|
::: amqtt.contrib.shadows.ShadowPlugin.Config
|
||||||
options:
|
options:
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import ast
|
import ast
|
||||||
import json
|
|
||||||
import pprint
|
import pprint
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import griffe
|
import griffe
|
||||||
from _griffe.agents.inspector import Inspector
|
from griffe import Inspector, ObjectNode, Visitor, Attribute
|
||||||
from _griffe.agents.nodes.runtime import ObjectNode
|
|
||||||
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.contexts import default_listeners, default_broker_plugins, default_client_plugins
|
||||||
from amqtt.contrib.auth_db.plugin import default_hash_scheme
|
from amqtt.contrib.auth_db.plugin import default_hash_scheme
|
||||||
|
|
|
@ -50,6 +50,8 @@ nav:
|
||||||
- LDAP Auth: plugins/ldap.md
|
- LDAP Auth: plugins/ldap.md
|
||||||
- Shadows: plugins/shadows.md
|
- Shadows: plugins/shadows.md
|
||||||
- Certificate Auth: plugins/cert.md
|
- Certificate Auth: plugins/cert.md
|
||||||
|
- JWT Auth: plugins/jwt.md
|
||||||
|
- Session persistence: plugins/session.md
|
||||||
- Reference:
|
- Reference:
|
||||||
- Containerization: docker.md
|
- Containerization: docker.md
|
||||||
- Support: support.md
|
- Support: support.md
|
||||||
|
|
|
@ -33,7 +33,6 @@ dependencies = [
|
||||||
"passlib==1.7.4", # https://pypi.org/project/passlib
|
"passlib==1.7.4", # https://pypi.org/project/passlib
|
||||||
"PyYAML==6.0.2", # https://pypi.org/project/PyYAML
|
"PyYAML==6.0.2", # https://pypi.org/project/PyYAML
|
||||||
"typer==0.15.4",
|
"typer==0.15.4",
|
||||||
"aiohttp>=3.12.7",
|
|
||||||
"dacite>=1.9.2",
|
"dacite>=1.9.2",
|
||||||
"psutil>=7.0.0"
|
"psutil>=7.0.0"
|
||||||
]
|
]
|
||||||
|
@ -67,6 +66,7 @@ dev = [
|
||||||
]
|
]
|
||||||
|
|
||||||
docs = [
|
docs = [
|
||||||
|
"griffe>=1.11.1",
|
||||||
"markdown-callouts>=0.4",
|
"markdown-callouts>=0.4",
|
||||||
"markdown-exec>=1.8",
|
"markdown-exec>=1.8",
|
||||||
"mkdocs>=1.6",
|
"mkdocs>=1.6",
|
||||||
|
|
|
@ -155,3 +155,12 @@ def ca_file_fixture():
|
||||||
for file in temp_dir.iterdir():
|
for file in temp_dir.iterdir():
|
||||||
file.unlink()
|
file.unlink()
|
||||||
temp_dir.rmdir()
|
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",
|
||||||
|
)
|
|
@ -13,7 +13,7 @@ import pytest
|
||||||
|
|
||||||
from amqtt.broker import BrokerContext, Broker
|
from amqtt.broker import BrokerContext, Broker
|
||||||
from amqtt.client import MQTTClient
|
from amqtt.client import MQTTClient
|
||||||
from amqtt.contrib.cert import CertificateAuthPlugin
|
from amqtt.contrib.cert import UserAuthCertPlugin
|
||||||
from amqtt.errors import ConnectError
|
from amqtt.errors import ConnectError
|
||||||
from amqtt.scripts.server_creds import server_creds as get_server_creds
|
from amqtt.scripts.server_creds import server_creds as get_server_creds
|
||||||
from amqtt.scripts.device_creds import device_creds as get_device_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 = 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 = Session()
|
||||||
s.client_id = client_id
|
s.client_id = client_id
|
||||||
|
@ -128,7 +128,7 @@ async def test_client_broker_cert_authentication(ca_creds, server_creds, device_
|
||||||
},
|
},
|
||||||
'plugins': {
|
'plugins': {
|
||||||
'amqtt.plugins.logging_amqtt.PacketLoggerPlugin':{},
|
'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': {
|
'plugins': {
|
||||||
'amqtt.plugins.logging_amqtt.PacketLoggerPlugin':{},
|
'amqtt.plugins.logging_amqtt.PacketLoggerPlugin':{},
|
||||||
'amqtt.contrib.cert.CertificateAuthPlugin': {'uri_domain': 'test.amqtt.io'},
|
'amqtt.contrib.cert.UserAuthCertPlugin': {'uri_domain': 'test.amqtt.io'},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import ldap
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from unittest.mock import patch, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from amqtt.broker import BrokerContext, Broker
|
from amqtt.broker import BrokerContext, Broker
|
||||||
from amqtt.client import MQTTClient
|
from amqtt.client import MQTTClient
|
||||||
from amqtt.contexts import BrokerConfig, ListenerConfig, ClientConfig, Action
|
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.contrib.ldap import UserAuthLdapPlugin, TopicAuthLdapPlugin
|
||||||
from amqtt.errors import ConnectError
|
from amqtt.errors import ConnectError
|
||||||
from amqtt.session import Session
|
from amqtt.session import Session
|
||||||
from tests.test_cli import broker
|
|
||||||
|
|
||||||
|
|
||||||
# Pin the project name to avoid creating multiple stacks
|
# 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"
|
return Path(pytestconfig.rootdir) / "tests/fixtures/ldap" / "docker-compose.yml"
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
@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."""
|
"""Ensure that HTTP service is up and responsive."""
|
||||||
|
|
||||||
# `port_for` takes a container port and returns the corresponding host port
|
# `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)
|
time.sleep(2)
|
||||||
return url
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_ldap_user_plugin(ldap_service):
|
async def test_ldap_user_plugin(ldap_service):
|
||||||
ctx = BrokerContext(Broker())
|
ctx = BrokerContext(Broker())
|
||||||
|
|
10
uv.lock
10
uv.lock
|
@ -132,7 +132,6 @@ name = "amqtt"
|
||||||
version = "0.11.3"
|
version = "0.11.3"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiohttp" },
|
|
||||||
{ name = "dacite" },
|
{ name = "dacite" },
|
||||||
{ name = "passlib" },
|
{ name = "passlib" },
|
||||||
{ name = "psutil" },
|
{ name = "psutil" },
|
||||||
|
@ -188,6 +187,7 @@ dev = [
|
||||||
{ name = "types-setuptools" },
|
{ name = "types-setuptools" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
|
{ name = "griffe" },
|
||||||
{ name = "markdown-callouts" },
|
{ name = "markdown-callouts" },
|
||||||
{ name = "markdown-exec" },
|
{ name = "markdown-exec" },
|
||||||
{ name = "mkdocs" },
|
{ name = "mkdocs" },
|
||||||
|
@ -207,7 +207,6 @@ docs = [
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiohttp", specifier = ">=3.12.7" },
|
|
||||||
{ name = "aiohttp", marker = "extra == 'contrib'", specifier = ">=3.12.13" },
|
{ name = "aiohttp", marker = "extra == 'contrib'", specifier = ">=3.12.13" },
|
||||||
{ name = "aiosqlite", marker = "extra == 'contrib'", specifier = ">=0.21.0" },
|
{ name = "aiosqlite", marker = "extra == 'contrib'", specifier = ">=0.21.0" },
|
||||||
{ name = "argon2-cffi", marker = "extra == 'contrib'", specifier = ">=25.1.0" },
|
{ name = "argon2-cffi", marker = "extra == 'contrib'", specifier = ">=25.1.0" },
|
||||||
|
@ -258,6 +257,7 @@ dev = [
|
||||||
{ name = "types-setuptools", specifier = ">=78.1.0.20250329" },
|
{ name = "types-setuptools", specifier = ">=78.1.0.20250329" },
|
||||||
]
|
]
|
||||||
docs = [
|
docs = [
|
||||||
|
{ name = "griffe", specifier = ">=1.11.1" },
|
||||||
{ name = "markdown-callouts", specifier = ">=0.4" },
|
{ name = "markdown-callouts", specifier = ">=0.4" },
|
||||||
{ name = "markdown-exec", specifier = ">=1.8" },
|
{ name = "markdown-exec", specifier = ">=1.8" },
|
||||||
{ name = "mkdocs", specifier = ">=1.6" },
|
{ name = "mkdocs", specifier = ">=1.6" },
|
||||||
|
@ -936,14 +936,14 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "griffe"
|
name = "griffe"
|
||||||
version = "1.7.3"
|
version = "1.11.1"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "colorama" },
|
{ 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 = [
|
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]]
|
[[package]]
|
||||||
|
|
Ładowanie…
Reference in New Issue