UGOS 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ugos-1.0.0/LICENSE +16 -0
- ugos-1.0.0/PKG-INFO +139 -0
- ugos-1.0.0/README.md +113 -0
- ugos-1.0.0/pyproject.toml +47 -0
- ugos-1.0.0/src/ugos/__about__.py +6 -0
- ugos-1.0.0/src/ugos/__init__.py +92 -0
- ugos-1.0.0/src/ugos/_auth.py +293 -0
- ugos-1.0.0/src/ugos/_client_base.py +52 -0
- ugos-1.0.0/src/ugos/_client_calls.py +200 -0
- ugos-1.0.0/src/ugos/_client_helpers.py +217 -0
- ugos-1.0.0/src/ugos/_client_lifecycle.py +144 -0
- ugos-1.0.0/src/ugos/_client_resources.py +192 -0
- ugos-1.0.0/src/ugos/_client_transfer.py +501 -0
- ugos-1.0.0/src/ugos/_codec.py +528 -0
- ugos-1.0.0/src/ugos/_config.py +183 -0
- ugos-1.0.0/src/ugos/_data/__init__.py +1 -0
- ugos-1.0.0/src/ugos/_data/action_contracts.json +325 -0
- ugos-1.0.0/src/ugos/_data/catalog.json +1256 -0
- ugos-1.0.0/src/ugos/_dynamic.py +221 -0
- ugos-1.0.0/src/ugos/_encoding.py +159 -0
- ugos-1.0.0/src/ugos/_pagination.py +158 -0
- ugos-1.0.0/src/ugos/_redaction.py +158 -0
- ugos-1.0.0/src/ugos/_transport.py +446 -0
- ugos-1.0.0/src/ugos/catalog.py +184 -0
- ugos-1.0.0/src/ugos/client.py +113 -0
- ugos-1.0.0/src/ugos/exceptions.py +137 -0
- ugos-1.0.0/src/ugos/models/__init__.py +37 -0
- ugos-1.0.0/src/ugos/models/common.py +87 -0
- ugos-1.0.0/src/ugos/models/entities.py +124 -0
- ugos-1.0.0/src/ugos/models/users.py +43 -0
- ugos-1.0.0/src/ugos/py.typed +0 -0
- ugos-1.0.0/src/ugos/resources/__init__.py +55 -0
- ugos-1.0.0/src/ugos/resources/administrators.py +71 -0
- ugos-1.0.0/src/ugos/resources/application.py +107 -0
- ugos-1.0.0/src/ugos/resources/base.py +38 -0
- ugos-1.0.0/src/ugos/resources/certificates.py +62 -0
- ugos-1.0.0/src/ugos/resources/contentrules.py +98 -0
- ugos-1.0.0/src/ugos/resources/contenttypes.py +9 -0
- ugos-1.0.0/src/ugos/resources/fwrules.py +103 -0
- ugos-1.0.0/src/ugos/resources/generic.py +283 -0
- ugos-1.0.0/src/ugos/resources/generic_controllers.py +169 -0
- ugos-1.0.0/src/ugos/resources/groups.py +94 -0
- ugos-1.0.0/src/ugos/resources/icap.py +132 -0
- ugos-1.0.0/src/ugos/resources/idps.py +125 -0
- ugos-1.0.0/src/ugos/resources/interfaces.py +40 -0
- ugos-1.0.0/src/ugos/resources/l7.py +110 -0
- ugos-1.0.0/src/ugos/resources/licensing.py +39 -0
- ugos-1.0.0/src/ugos/resources/mailsecurity.py +101 -0
- ugos-1.0.0/src/ugos/resources/networks.py +82 -0
- ugos-1.0.0/src/ugos/resources/netzones.py +78 -0
- ugos-1.0.0/src/ugos/resources/nlist.py +72 -0
- ugos-1.0.0/src/ugos/resources/overridedomains.py +47 -0
- ugos-1.0.0/src/ugos/resources/overrides.py +109 -0
- ugos-1.0.0/src/ugos/resources/overrides_content.py +254 -0
- ugos-1.0.0/src/ugos/resources/overrides_content_a.py +187 -0
- ugos-1.0.0/src/ugos/resources/overrides_lists.py +294 -0
- ugos-1.0.0/src/ugos/resources/overrides_misc.py +342 -0
- ugos-1.0.0/src/ugos/resources/overrides_network.py +291 -0
- ugos-1.0.0/src/ugos/resources/pcap.py +64 -0
- ugos-1.0.0/src/ugos/resources/registry.py +55 -0
- ugos-1.0.0/src/ugos/resources/settings.py +72 -0
- ugos-1.0.0/src/ugos/resources/sysinfo.py +39 -0
- ugos-1.0.0/src/ugos/resources/twofa.py +62 -0
- ugos-1.0.0/src/ugos/resources/urlcategory.py +69 -0
- ugos-1.0.0/src/ugos/resources/urlfiltering.py +23 -0
- ugos-1.0.0/src/ugos/resources/urllists.py +9 -0
- ugos-1.0.0/src/ugos/resources/users.py +74 -0
ugos-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
MIT No Attribution
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
11
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
12
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
13
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
14
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
15
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
16
|
+
SOFTWARE.
|
ugos-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: UGOS
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Python client for the UserGate NGFW web-console API
|
|
5
|
+
Project-URL: Repository, https://codeberg.org/owldog/ugos
|
|
6
|
+
License-Expression: MIT-0
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: api,firewall,httpx,ngfw,ugos
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: System Administrators
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Topic :: System :: Networking :: Firewalls
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx<0.29,>=0.27
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# UGOS
|
|
28
|
+
|
|
29
|
+
Synchronous Python client for the UserGate NGFW web-console API.
|
|
30
|
+
|
|
31
|
+
- Python 3.10+
|
|
32
|
+
- `httpx` transport
|
|
33
|
+
- compatible with UserGate NGFW 7.x
|
|
34
|
+
- typed resources plus exact access to every catalog action
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add UGOS
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
From a local wheel:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
uv add ./dist/ugos-*.whl
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Quick start
|
|
49
|
+
|
|
50
|
+
### Environment variables
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
export UGOS_URL=https://ngfw.example:8001
|
|
54
|
+
export UGOS_USERNAME=Admin
|
|
55
|
+
export UGOS_PASSWORD=...
|
|
56
|
+
export UGOS_VERIFY=true
|
|
57
|
+
export UGOS_TIMEOUT=30
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from ugos import UGOSClient
|
|
62
|
+
|
|
63
|
+
with UGOSClient.from_env() as client:
|
|
64
|
+
client.login()
|
|
65
|
+
|
|
66
|
+
print(client.licensing.get_info())
|
|
67
|
+
print(client.users.list(limit=20).total)
|
|
68
|
+
|
|
69
|
+
for user in client.users.iter_all(page_size=100):
|
|
70
|
+
print(user["name"])
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`from_env()` reads:
|
|
74
|
+
|
|
75
|
+
| Parameter | Environment variable | Default |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `base_url` | `UGOS_URL` | required |
|
|
78
|
+
| `username` | `UGOS_USERNAME` | `None` |
|
|
79
|
+
| `password` | `UGOS_PASSWORD` | `None` |
|
|
80
|
+
| `verify` | `UGOS_CA_BUNDLE`, `UGOS_VERIFY` | `True` |
|
|
81
|
+
| `timeout` | `UGOS_TIMEOUT` | `30.0` |
|
|
82
|
+
| `allow_insecure_http` | `UGOS_ALLOW_INSECURE_HTTP` | `False` |
|
|
83
|
+
|
|
84
|
+
`UGOS_CA_BUNDLE` takes precedence over `UGOS_VERIFY` and accepts a path to a
|
|
85
|
+
CA bundle. `UGOS_VERIFY` accepts `true`, `false`, `1`, `0`, `yes`, `no`,
|
|
86
|
+
`on`, `off`, or a CA path.
|
|
87
|
+
|
|
88
|
+
### Explicit configuration
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
with UGOSClient(
|
|
92
|
+
base_url="https://ngfw.example:8001",
|
|
93
|
+
username="Admin",
|
|
94
|
+
password="...",
|
|
95
|
+
verify="/path/to/ca.pem",
|
|
96
|
+
timeout=30,
|
|
97
|
+
) as client:
|
|
98
|
+
client.login()
|
|
99
|
+
print(client.users.list(limit=20).total)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`from_env()` also accepts keyword overrides, which take precedence over
|
|
103
|
+
environment variables:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
with UGOSClient.from_env(password="...", verify=False) as client:
|
|
107
|
+
client.login()
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Generic and typed access
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
client.users.list(limit=50) # typed helper
|
|
114
|
+
client.users.actions.UsersList(limit=50) # exact action
|
|
115
|
+
client.call("users", "UsersList", limit=50) # raw call
|
|
116
|
+
client.dhcp.list() # generic resource
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Errors
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from ugos import NotFoundError, UGOSError
|
|
123
|
+
|
|
124
|
+
try:
|
|
125
|
+
client.users.get(999)
|
|
126
|
+
except NotFoundError as exc:
|
|
127
|
+
print(exc.code, exc.message)
|
|
128
|
+
except UGOSError as exc:
|
|
129
|
+
print(exc)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Compatibility
|
|
133
|
+
|
|
134
|
+
Compatible with UserGate NGFW 7.x. For builds with a different action set, pass
|
|
135
|
+
`catalog_check=False` or `strict=False`.
|
|
136
|
+
|
|
137
|
+
## License
|
|
138
|
+
|
|
139
|
+
MIT-0. See `LICENSE`.
|
ugos-1.0.0/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# UGOS
|
|
2
|
+
|
|
3
|
+
Synchronous Python client for the UserGate NGFW web-console API.
|
|
4
|
+
|
|
5
|
+
- Python 3.10+
|
|
6
|
+
- `httpx` transport
|
|
7
|
+
- compatible with UserGate NGFW 7.x
|
|
8
|
+
- typed resources plus exact access to every catalog action
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
uv add UGOS
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
From a local wheel:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
uv add ./dist/ugos-*.whl
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick start
|
|
23
|
+
|
|
24
|
+
### Environment variables
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
export UGOS_URL=https://ngfw.example:8001
|
|
28
|
+
export UGOS_USERNAME=Admin
|
|
29
|
+
export UGOS_PASSWORD=...
|
|
30
|
+
export UGOS_VERIFY=true
|
|
31
|
+
export UGOS_TIMEOUT=30
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from ugos import UGOSClient
|
|
36
|
+
|
|
37
|
+
with UGOSClient.from_env() as client:
|
|
38
|
+
client.login()
|
|
39
|
+
|
|
40
|
+
print(client.licensing.get_info())
|
|
41
|
+
print(client.users.list(limit=20).total)
|
|
42
|
+
|
|
43
|
+
for user in client.users.iter_all(page_size=100):
|
|
44
|
+
print(user["name"])
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`from_env()` reads:
|
|
48
|
+
|
|
49
|
+
| Parameter | Environment variable | Default |
|
|
50
|
+
|---|---|---|
|
|
51
|
+
| `base_url` | `UGOS_URL` | required |
|
|
52
|
+
| `username` | `UGOS_USERNAME` | `None` |
|
|
53
|
+
| `password` | `UGOS_PASSWORD` | `None` |
|
|
54
|
+
| `verify` | `UGOS_CA_BUNDLE`, `UGOS_VERIFY` | `True` |
|
|
55
|
+
| `timeout` | `UGOS_TIMEOUT` | `30.0` |
|
|
56
|
+
| `allow_insecure_http` | `UGOS_ALLOW_INSECURE_HTTP` | `False` |
|
|
57
|
+
|
|
58
|
+
`UGOS_CA_BUNDLE` takes precedence over `UGOS_VERIFY` and accepts a path to a
|
|
59
|
+
CA bundle. `UGOS_VERIFY` accepts `true`, `false`, `1`, `0`, `yes`, `no`,
|
|
60
|
+
`on`, `off`, or a CA path.
|
|
61
|
+
|
|
62
|
+
### Explicit configuration
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
with UGOSClient(
|
|
66
|
+
base_url="https://ngfw.example:8001",
|
|
67
|
+
username="Admin",
|
|
68
|
+
password="...",
|
|
69
|
+
verify="/path/to/ca.pem",
|
|
70
|
+
timeout=30,
|
|
71
|
+
) as client:
|
|
72
|
+
client.login()
|
|
73
|
+
print(client.users.list(limit=20).total)
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`from_env()` also accepts keyword overrides, which take precedence over
|
|
77
|
+
environment variables:
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
with UGOSClient.from_env(password="...", verify=False) as client:
|
|
81
|
+
client.login()
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Generic and typed access
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
client.users.list(limit=50) # typed helper
|
|
88
|
+
client.users.actions.UsersList(limit=50) # exact action
|
|
89
|
+
client.call("users", "UsersList", limit=50) # raw call
|
|
90
|
+
client.dhcp.list() # generic resource
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Errors
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from ugos import NotFoundError, UGOSError
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
client.users.get(999)
|
|
100
|
+
except NotFoundError as exc:
|
|
101
|
+
print(exc.code, exc.message)
|
|
102
|
+
except UGOSError as exc:
|
|
103
|
+
print(exc)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Compatibility
|
|
107
|
+
|
|
108
|
+
Compatible with UserGate NGFW 7.x. For builds with a different action set, pass
|
|
109
|
+
`catalog_check=False` or `strict=False`.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT-0. See `LICENSE`.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.27.0,<2"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "UGOS"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "Python client for the UserGate NGFW web-console API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT-0"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
keywords = ["ugos", "ngfw", "firewall", "api", "httpx"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Intended Audience :: System Administrators",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Typing :: Typed",
|
|
27
|
+
"Topic :: System :: Networking :: Firewalls",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
]
|
|
30
|
+
dependencies = ["httpx>=0.27,<0.29"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Repository = "https://codeberg.org/owldog/ugos"
|
|
34
|
+
|
|
35
|
+
[tool.hatch.version]
|
|
36
|
+
path = "src/ugos/__about__.py"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/ugos"]
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.targets.sdist]
|
|
42
|
+
include = [
|
|
43
|
+
"/src",
|
|
44
|
+
"/README.md",
|
|
45
|
+
"/LICENSE",
|
|
46
|
+
"/pyproject.toml",
|
|
47
|
+
]
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""Python client for the UGOS NGFW web-console API."""
|
|
2
|
+
|
|
3
|
+
from .__about__ import SUPPORTED_MAJOR_VERSION, SUPPORTED_NGFW, __version__
|
|
4
|
+
from .catalog import Catalog, load_catalog
|
|
5
|
+
from .client import UGOSClient
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
AccessDeniedError,
|
|
8
|
+
APIError,
|
|
9
|
+
AuthenticationError,
|
|
10
|
+
CatalogError,
|
|
11
|
+
CleanupError,
|
|
12
|
+
ConfigurationError,
|
|
13
|
+
ConflictError,
|
|
14
|
+
CSRFError,
|
|
15
|
+
DecodeError,
|
|
16
|
+
DownloadError,
|
|
17
|
+
DuplicateError,
|
|
18
|
+
HTTPStatusError,
|
|
19
|
+
NotFoundError,
|
|
20
|
+
ProtocolError,
|
|
21
|
+
ReadOnlyListError,
|
|
22
|
+
RequestTimeoutError,
|
|
23
|
+
ServerError,
|
|
24
|
+
SessionExpiredError,
|
|
25
|
+
TransportError,
|
|
26
|
+
UnknownActionError,
|
|
27
|
+
UnknownControllerError,
|
|
28
|
+
UGOSError,
|
|
29
|
+
ValidationError,
|
|
30
|
+
)
|
|
31
|
+
from .models.common import DownloadedFile, ListResult, SessionInfo
|
|
32
|
+
from .models.entities import (
|
|
33
|
+
Administrator,
|
|
34
|
+
Certificate,
|
|
35
|
+
FirewallRule,
|
|
36
|
+
Group,
|
|
37
|
+
Interface,
|
|
38
|
+
LicenseInfo,
|
|
39
|
+
NetworkList,
|
|
40
|
+
PcapFilter,
|
|
41
|
+
PcapRule,
|
|
42
|
+
Tag,
|
|
43
|
+
Zone,
|
|
44
|
+
)
|
|
45
|
+
from .resources.base import Resource
|
|
46
|
+
|
|
47
|
+
__all__ = [
|
|
48
|
+
"SUPPORTED_MAJOR_VERSION",
|
|
49
|
+
"SUPPORTED_NGFW",
|
|
50
|
+
"APIError",
|
|
51
|
+
"AccessDeniedError",
|
|
52
|
+
"Administrator",
|
|
53
|
+
"AuthenticationError",
|
|
54
|
+
"CSRFError",
|
|
55
|
+
"Catalog",
|
|
56
|
+
"CatalogError",
|
|
57
|
+
"Certificate",
|
|
58
|
+
"CleanupError",
|
|
59
|
+
"ConfigurationError",
|
|
60
|
+
"ConflictError",
|
|
61
|
+
"DecodeError",
|
|
62
|
+
"DownloadError",
|
|
63
|
+
"DownloadedFile",
|
|
64
|
+
"DuplicateError",
|
|
65
|
+
"FirewallRule",
|
|
66
|
+
"Group",
|
|
67
|
+
"HTTPStatusError",
|
|
68
|
+
"Interface",
|
|
69
|
+
"LicenseInfo",
|
|
70
|
+
"ListResult",
|
|
71
|
+
"NetworkList",
|
|
72
|
+
"NotFoundError",
|
|
73
|
+
"PcapFilter",
|
|
74
|
+
"PcapRule",
|
|
75
|
+
"ProtocolError",
|
|
76
|
+
"ReadOnlyListError",
|
|
77
|
+
"RequestTimeoutError",
|
|
78
|
+
"Resource",
|
|
79
|
+
"ServerError",
|
|
80
|
+
"SessionExpiredError",
|
|
81
|
+
"SessionInfo",
|
|
82
|
+
"Tag",
|
|
83
|
+
"TransportError",
|
|
84
|
+
"UnknownActionError",
|
|
85
|
+
"UnknownControllerError",
|
|
86
|
+
"UGOSClient",
|
|
87
|
+
"UGOSError",
|
|
88
|
+
"ValidationError",
|
|
89
|
+
"Zone",
|
|
90
|
+
"__version__",
|
|
91
|
+
"load_catalog",
|
|
92
|
+
]
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
"""Console authentication, CSRF handling and session lifecycle."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import re
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from typing import Any, cast
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from ._codec import is_error_payload, parse_response
|
|
13
|
+
from ._encoding import encode_params
|
|
14
|
+
from ._redaction import redact_mapping
|
|
15
|
+
from ._transport import FORM_HEADERS, HTML_HEADERS, HttpTransport
|
|
16
|
+
from .exceptions import (
|
|
17
|
+
AuthenticationError,
|
|
18
|
+
CSRFError,
|
|
19
|
+
DecodeError,
|
|
20
|
+
HTTPStatusError,
|
|
21
|
+
ProtocolError,
|
|
22
|
+
SessionExpiredError,
|
|
23
|
+
UGOSError,
|
|
24
|
+
)
|
|
25
|
+
from .models.common import SessionInfo
|
|
26
|
+
|
|
27
|
+
_CSRF_RE = re.compile(r"CSRF_TOKEN\s*=\s*['\"]([^'\"]+)['\"]")
|
|
28
|
+
_CSRF_INPUT_RE = re.compile(
|
|
29
|
+
r"name=['\"]csrfmiddlewaretoken['\"][^>]*value=['\"]([^'\"]+)['\"]",
|
|
30
|
+
re.IGNORECASE,
|
|
31
|
+
)
|
|
32
|
+
_CSRF_META_RE = re.compile(
|
|
33
|
+
r"name=['\"]csrf-token['\"][^>]*content=['\"]([^'\"]+)['\"]",
|
|
34
|
+
re.IGNORECASE,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class AuthManager:
|
|
39
|
+
"""Owns login/logout, cookies and the CSRF token."""
|
|
40
|
+
|
|
41
|
+
def __init__(
|
|
42
|
+
self,
|
|
43
|
+
transport: HttpTransport,
|
|
44
|
+
*,
|
|
45
|
+
username: str | None = None,
|
|
46
|
+
password: str | None = None,
|
|
47
|
+
login_path: str = "utm/login",
|
|
48
|
+
logout_path: str = "utm/CoreLogout",
|
|
49
|
+
auto_reauth: bool = True,
|
|
50
|
+
) -> None:
|
|
51
|
+
self._transport = transport
|
|
52
|
+
self._username = username
|
|
53
|
+
self._password = password
|
|
54
|
+
self._login_path = login_path
|
|
55
|
+
self._logout_path = logout_path
|
|
56
|
+
self._auto_reauth = auto_reauth
|
|
57
|
+
self._session: SessionInfo | None = None
|
|
58
|
+
self._csrf: str | None = None
|
|
59
|
+
self._generation = 0
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def session(self) -> SessionInfo | None:
|
|
63
|
+
return self._session
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def authenticated(self) -> bool:
|
|
67
|
+
return self._session is not None
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def csrf_token(self) -> str | None:
|
|
71
|
+
return self._csrf
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def generation(self) -> int:
|
|
75
|
+
return self._generation
|
|
76
|
+
|
|
77
|
+
def invalidate(self) -> None:
|
|
78
|
+
self._session = None
|
|
79
|
+
self._csrf = None
|
|
80
|
+
self._generation += 1
|
|
81
|
+
self._transport.clear_cookies(("_console_session", "csrf_token"))
|
|
82
|
+
|
|
83
|
+
@property
|
|
84
|
+
def auto_reauth(self) -> bool:
|
|
85
|
+
return self._auto_reauth
|
|
86
|
+
|
|
87
|
+
def ensure_csrf(self) -> str:
|
|
88
|
+
if self._csrf:
|
|
89
|
+
return self._csrf
|
|
90
|
+
token = self._transport.get_cookie("csrf_token")
|
|
91
|
+
if token:
|
|
92
|
+
self._csrf = token
|
|
93
|
+
return token
|
|
94
|
+
return self.refresh_csrf()
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _is_login_redirect(status_code: int, location: str) -> bool:
|
|
98
|
+
if status_code not in {301, 302, 303, 307, 308}:
|
|
99
|
+
return False
|
|
100
|
+
return "utm/login" in location or location.rstrip("/").endswith("/login")
|
|
101
|
+
|
|
102
|
+
def _raise_csrf_status(self, response: httpx.Response) -> None:
|
|
103
|
+
location = response.headers.get("location", "")
|
|
104
|
+
login_redirect = self._is_login_redirect(response.status_code, location)
|
|
105
|
+
if response.status_code == 401 or login_redirect:
|
|
106
|
+
raise SessionExpiredError(
|
|
107
|
+
"session expired while refreshing CSRF",
|
|
108
|
+
status_code=response.status_code,
|
|
109
|
+
)
|
|
110
|
+
if response.status_code >= 400:
|
|
111
|
+
raise CSRFError(
|
|
112
|
+
f"CSRF refresh failed with HTTP {response.status_code}",
|
|
113
|
+
status_code=response.status_code,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _extract_csrf_token(self, response: httpx.Response) -> str:
|
|
117
|
+
token = self._transport.get_cookie("csrf_token") or _extract_csrf(response.text)
|
|
118
|
+
if not token:
|
|
119
|
+
raise CSRFError("the login page did not contain a CSRF token")
|
|
120
|
+
return token
|
|
121
|
+
|
|
122
|
+
def refresh_csrf(self) -> str:
|
|
123
|
+
path = "/" if self.authenticated else self._login_path
|
|
124
|
+
try:
|
|
125
|
+
response = self._transport.request(
|
|
126
|
+
"GET",
|
|
127
|
+
path,
|
|
128
|
+
headers=HTML_HEADERS,
|
|
129
|
+
idempotent=True,
|
|
130
|
+
)
|
|
131
|
+
except UGOSError:
|
|
132
|
+
raise
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
raise CSRFError(f"failed to obtain a CSRF token: {exc}") from exc
|
|
135
|
+
self._raise_csrf_status(response)
|
|
136
|
+
self.note_response(response)
|
|
137
|
+
self._csrf = self._extract_csrf_token(response)
|
|
138
|
+
return self._csrf
|
|
139
|
+
|
|
140
|
+
def _reset_session(self) -> None:
|
|
141
|
+
self._transport.clear_cookies(("_console_session", "csrf_token"))
|
|
142
|
+
self._session = None
|
|
143
|
+
self._csrf = None
|
|
144
|
+
|
|
145
|
+
def _login_page_token(self) -> str:
|
|
146
|
+
page = self._transport.request(
|
|
147
|
+
"GET", self._login_path, headers=HTML_HEADERS, idempotent=True
|
|
148
|
+
)
|
|
149
|
+
self.note_response(page)
|
|
150
|
+
return self._extract_csrf_token(page)
|
|
151
|
+
|
|
152
|
+
def _post_login(self, token: str) -> httpx.Response:
|
|
153
|
+
body = encode_params(
|
|
154
|
+
{
|
|
155
|
+
"username": self._username,
|
|
156
|
+
"password": self._password,
|
|
157
|
+
"csrfmiddlewaretoken": token,
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
response = self._transport.request(
|
|
161
|
+
"POST",
|
|
162
|
+
self._login_path,
|
|
163
|
+
data=body,
|
|
164
|
+
headers=FORM_HEADERS,
|
|
165
|
+
idempotent=False,
|
|
166
|
+
follow_redirects=False,
|
|
167
|
+
)
|
|
168
|
+
self.note_response(response)
|
|
169
|
+
return response
|
|
170
|
+
|
|
171
|
+
def _parse_login_response(self, response: httpx.Response) -> Mapping[str, Any]:
|
|
172
|
+
try:
|
|
173
|
+
payload = parse_response(response, expect="json")
|
|
174
|
+
except DecodeError as exc:
|
|
175
|
+
raise AuthenticationError(
|
|
176
|
+
"login returned a non-JSON response",
|
|
177
|
+
status_code=response.status_code,
|
|
178
|
+
) from exc
|
|
179
|
+
except (HTTPStatusError, ProtocolError) as exc:
|
|
180
|
+
raise AuthenticationError(
|
|
181
|
+
f"login failed: {exc}",
|
|
182
|
+
status_code=getattr(exc, "status_code", response.status_code),
|
|
183
|
+
payload=getattr(exc, "payload", None),
|
|
184
|
+
) from exc
|
|
185
|
+
if not isinstance(payload, Mapping) or not payload.get("auth_token"):
|
|
186
|
+
raise AuthenticationError("login was rejected by the appliance")
|
|
187
|
+
return payload
|
|
188
|
+
|
|
189
|
+
def _build_session(self, payload: Mapping[str, Any], token: str) -> SessionInfo:
|
|
190
|
+
session_cookie = self._transport.get_cookie("_console_session")
|
|
191
|
+
if not session_cookie:
|
|
192
|
+
raise ProtocolError("login succeeded but _console_session cookie is absent")
|
|
193
|
+
self._csrf = self._transport.get_cookie("csrf_token") or token
|
|
194
|
+
self._session = SessionInfo(
|
|
195
|
+
username=cast(str, self._username),
|
|
196
|
+
admin_id=_as_int(payload.get("admin_id")),
|
|
197
|
+
product=_as_str(payload.get("product")),
|
|
198
|
+
version=_as_str(payload.get("version")),
|
|
199
|
+
auth_token=_as_bool(payload.get("auth_token")),
|
|
200
|
+
fingerprint=hashlib.sha256(session_cookie.encode()).hexdigest()[:12],
|
|
201
|
+
raw=redact_mapping(payload),
|
|
202
|
+
)
|
|
203
|
+
self._generation += 1
|
|
204
|
+
return self._session
|
|
205
|
+
|
|
206
|
+
def _has_reusable_session(self, force: bool) -> bool:
|
|
207
|
+
return self._session is not None and not force
|
|
208
|
+
|
|
209
|
+
def _credentials_missing(self) -> bool:
|
|
210
|
+
return not self._username or self._password is None
|
|
211
|
+
|
|
212
|
+
def _should_reset_session(self, force: bool) -> bool:
|
|
213
|
+
return force or self._session is not None
|
|
214
|
+
|
|
215
|
+
def login(self, *, force: bool = False) -> SessionInfo:
|
|
216
|
+
if self._has_reusable_session(force):
|
|
217
|
+
return self._session # type: ignore[return-value]
|
|
218
|
+
if self._credentials_missing():
|
|
219
|
+
raise AuthenticationError("username and password are required for login")
|
|
220
|
+
if self._should_reset_session(force):
|
|
221
|
+
self._reset_session()
|
|
222
|
+
token = self._login_page_token()
|
|
223
|
+
response = self._post_login(token)
|
|
224
|
+
return self._build_session(self._parse_login_response(response), token)
|
|
225
|
+
|
|
226
|
+
def _logout_request(self) -> httpx.Response:
|
|
227
|
+
token = self._csrf or self.ensure_csrf()
|
|
228
|
+
response = self._transport.request(
|
|
229
|
+
"POST",
|
|
230
|
+
self._logout_path,
|
|
231
|
+
data=encode_params({"csrfmiddlewaretoken": token}),
|
|
232
|
+
headers=FORM_HEADERS,
|
|
233
|
+
idempotent=False,
|
|
234
|
+
)
|
|
235
|
+
self.note_response(response)
|
|
236
|
+
return response
|
|
237
|
+
|
|
238
|
+
def _logout_result(self, response: httpx.Response) -> bool:
|
|
239
|
+
try:
|
|
240
|
+
payload = parse_response(response, expect="auto")
|
|
241
|
+
except SessionExpiredError:
|
|
242
|
+
return True
|
|
243
|
+
return not is_error_payload(payload)
|
|
244
|
+
|
|
245
|
+
def logout(self, *, best_effort: bool = True) -> bool:
|
|
246
|
+
if self._session is None:
|
|
247
|
+
self._generation += 1
|
|
248
|
+
self._transport.clear_cookies(("_console_session", "csrf_token"))
|
|
249
|
+
return True
|
|
250
|
+
ok = False
|
|
251
|
+
try:
|
|
252
|
+
ok = self._logout_result(self._logout_request())
|
|
253
|
+
except UGOSError:
|
|
254
|
+
if not best_effort:
|
|
255
|
+
raise
|
|
256
|
+
finally:
|
|
257
|
+
self._generation += 1
|
|
258
|
+
self._reset_session()
|
|
259
|
+
return ok
|
|
260
|
+
|
|
261
|
+
def note_response(self, response: httpx.Response) -> None:
|
|
262
|
+
token = self._transport.get_cookie("csrf_token")
|
|
263
|
+
if token:
|
|
264
|
+
self._csrf = token
|
|
265
|
+
# Session state is invalidated by the guarded re-auth path, not here:
|
|
266
|
+
# a stale response from another thread must not wipe a fresh session.
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _extract_csrf(html: str) -> str | None:
|
|
270
|
+
for pattern in (_CSRF_RE, _CSRF_INPUT_RE, _CSRF_META_RE):
|
|
271
|
+
match = pattern.search(html)
|
|
272
|
+
if match:
|
|
273
|
+
return match.group(1)
|
|
274
|
+
return None
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _as_int(value: Any) -> int | None:
|
|
278
|
+
try:
|
|
279
|
+
return int(value) if value is not None else None
|
|
280
|
+
except (TypeError, ValueError):
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _as_str(value: Any) -> str | None:
|
|
285
|
+
return str(value) if value is not None else None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _as_bool(value: Any) -> bool:
|
|
289
|
+
if isinstance(value, bool):
|
|
290
|
+
return value
|
|
291
|
+
if isinstance(value, str):
|
|
292
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
293
|
+
return bool(value)
|