ldap-manager 1.0.12__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.
- ldap_manager-1.0.12/LICENSE +8 -0
- ldap_manager-1.0.12/PKG-INFO +24 -0
- ldap_manager-1.0.12/README.md +282 -0
- ldap_manager-1.0.12/ldap_manager/__init__.py +3 -0
- ldap_manager-1.0.12/ldap_manager/audit.py +145 -0
- ldap_manager-1.0.12/ldap_manager/backup.py +300 -0
- ldap_manager-1.0.12/ldap_manager/batch.py +247 -0
- ldap_manager-1.0.12/ldap_manager/cli.py +1987 -0
- ldap_manager-1.0.12/ldap_manager/config.py +146 -0
- ldap_manager-1.0.12/ldap_manager/connection.py +68 -0
- ldap_manager-1.0.12/ldap_manager/groups.py +235 -0
- ldap_manager-1.0.12/ldap_manager/ldif_ops.py +240 -0
- ldap_manager-1.0.12/ldap_manager/passwords.py +93 -0
- ldap_manager-1.0.12/ldap_manager/ppolicy.py +304 -0
- ldap_manager-1.0.12/ldap_manager/server.py +276 -0
- ldap_manager-1.0.12/ldap_manager/sshkeys.py +137 -0
- ldap_manager-1.0.12/ldap_manager/tree.py +217 -0
- ldap_manager-1.0.12/ldap_manager/users.py +496 -0
- ldap_manager-1.0.12/ldap_manager.egg-info/PKG-INFO +24 -0
- ldap_manager-1.0.12/ldap_manager.egg-info/SOURCES.txt +37 -0
- ldap_manager-1.0.12/ldap_manager.egg-info/dependency_links.txt +1 -0
- ldap_manager-1.0.12/ldap_manager.egg-info/entry_points.txt +2 -0
- ldap_manager-1.0.12/ldap_manager.egg-info/requires.txt +13 -0
- ldap_manager-1.0.12/ldap_manager.egg-info/top_level.txt +1 -0
- ldap_manager-1.0.12/pyproject.toml +124 -0
- ldap_manager-1.0.12/setup.cfg +4 -0
- ldap_manager-1.0.12/tests/test_audit.py +88 -0
- ldap_manager-1.0.12/tests/test_backup.py +145 -0
- ldap_manager-1.0.12/tests/test_batch.py +144 -0
- ldap_manager-1.0.12/tests/test_cli.py +178 -0
- ldap_manager-1.0.12/tests/test_config.py +67 -0
- ldap_manager-1.0.12/tests/test_groups.py +151 -0
- ldap_manager-1.0.12/tests/test_ldif_ops.py +160 -0
- ldap_manager-1.0.12/tests/test_passwords.py +71 -0
- ldap_manager-1.0.12/tests/test_ppolicy.py +181 -0
- ldap_manager-1.0.12/tests/test_server.py +129 -0
- ldap_manager-1.0.12/tests/test_sshkeys.py +143 -0
- ldap_manager-1.0.12/tests/test_tree.py +116 -0
- ldap_manager-1.0.12/tests/test_users.py +290 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Copyright 2026 Israel Hen
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
8
|
+
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ldap-manager
|
|
3
|
+
Version: 1.0.12
|
|
4
|
+
Summary: CLI power tools for sysadmins who run OpenLDAP and refuse to touch a web UI.
|
|
5
|
+
Author-email: Israel Hen <israelhen153@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/israelhen153/ldap-manager
|
|
7
|
+
Project-URL: Repository, https://github.com/israelhen153/ldap-manager
|
|
8
|
+
Project-URL: Issues, https://github.com/israelhen153/ldap-manager/issues
|
|
9
|
+
Project-URL: Documentation, https://github.com/israelhen153/ldap-manager#readme
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Requires-Dist: python-ldap>=3.4
|
|
13
|
+
Requires-Dist: click>=8.1
|
|
14
|
+
Requires-Dist: pyyaml>=6.0
|
|
15
|
+
Requires-Dist: passlib>=1.7
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
Requires-Dist: pytest-mock; extra == "dev"
|
|
19
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
20
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
21
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
22
|
+
Requires-Dist: types-PyYAML; extra == "dev"
|
|
23
|
+
Requires-Dist: bandit>=1.7; extra == "dev"
|
|
24
|
+
Dynamic: license-file
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# ldap-manager
|
|
2
|
+
|
|
3
|
+
# Tests:
|
|
4
|
+
[](https://github.com/israelhen153/ldap-manager/actions/workflows/makefile.yml)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
**CLI power tools for sysadmins who run OpenLDAP and refuse to touch a web UI.**
|
|
8
|
+
|
|
9
|
+
Manage users, groups, backups, SSH keys, password policies, and server operations — all from one command. Replace your pile of bash scripts and raw `ldapmodify` commands with something that actually has `--dry-run`.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install ldap-manager
|
|
13
|
+
ldap-manager user list --enabled --json
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Why?
|
|
19
|
+
|
|
20
|
+
If you manage OpenLDAP, your options are:
|
|
21
|
+
|
|
22
|
+
| Tool | Problem |
|
|
23
|
+
|------|---------|
|
|
24
|
+
| `ldapmodify` / `ldapsearch` | LDIF by hand for every operation |
|
|
25
|
+
| phpLDAPadmin | Abandoned, PHP, browser-only |
|
|
26
|
+
| LDAP Account Manager | Java, $$$, overkill for CLI admins |
|
|
27
|
+
| Your 15 bash scripts | Fragile, no error handling, no dry-run |
|
|
28
|
+
|
|
29
|
+
**ldap-manager** is the missing CLI. One tool, tab completion, JSON output, dry-run on destructive operations, and actual tests.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Features
|
|
34
|
+
|
|
35
|
+
| Category | What you get |
|
|
36
|
+
|----------|-------------|
|
|
37
|
+
| **Users** | Create, update, delete, enable/disable, search with filters, dump as JSON |
|
|
38
|
+
| **Groups** | Create, delete, add/remove members, list, supports posixGroup and groupOfNames |
|
|
39
|
+
| **Backup & Restore** | Full `slapcat`/`slapadd` dumps with gzip, metadata, and config backup |
|
|
40
|
+
| **Batch Operations** | Bulk create/update/delete from JSON/CSV/TSV with `--dry-run` |
|
|
41
|
+
| **Password Reset** | Reset all users at once, CSV output with new passwords |
|
|
42
|
+
| **SSH Keys** | Add, remove, list `ldapPublicKey` attributes per user |
|
|
43
|
+
| **Server Ops** | Status, start/stop/restart, reindex with `--auto` |
|
|
44
|
+
| **Password Policy** | View policy config, check user expiry/lockout status |
|
|
45
|
+
| **LDIF Export/Import** | Standards-compliant RFC 2849 export and import with dry-run |
|
|
46
|
+
| **Tree Management** | List/create/delete OUs, visualize your DIT |
|
|
47
|
+
| **Audit Logging** | JSON-lines audit trail of all operations |
|
|
48
|
+
| **JSON Output** | `--json` flag on 12+ commands for scripting and pipelines |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Quickstart
|
|
53
|
+
|
|
54
|
+
### Install
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
# Rocky/RHEL 8+
|
|
58
|
+
dnf install epel-release -y
|
|
59
|
+
dnf install python3-ldap python3-pyyaml python3-click python3-passlib \
|
|
60
|
+
openldap-clients openldap-servers -y
|
|
61
|
+
|
|
62
|
+
pip install ldap-manager
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Configure
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
cp config.example.yaml /etc/ldap-manager/config.yaml
|
|
69
|
+
vim /etc/ldap-manager/config.yaml
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Or use environment variables:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
export LDAP_URI="ldap://localhost:389"
|
|
76
|
+
export LDAP_BIND_DN="cn=admin,dc=example,dc=com"
|
|
77
|
+
export LDAP_BIND_PASSWORD="secret"
|
|
78
|
+
export LDAP_BASE_DN="dc=example,dc=com"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Go
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
ldap-manager user list
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
### Users
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ldap-manager user list --enabled --json
|
|
95
|
+
ldap-manager user get jdoe
|
|
96
|
+
ldap-manager user create jdoe --cn "John Doe" --mail john@example.com
|
|
97
|
+
ldap-manager user update jdoe --set mail=new@example.com --set loginShell=/bin/zsh
|
|
98
|
+
ldap-manager user delete jdoe --yes
|
|
99
|
+
ldap-manager user disable jdoe
|
|
100
|
+
ldap-manager user enable jdoe
|
|
101
|
+
ldap-manager user passwd jdoe
|
|
102
|
+
|
|
103
|
+
# Search with filters
|
|
104
|
+
ldap-manager user search --uid "j*"
|
|
105
|
+
ldap-manager user search --mail "*@engineering.com" --enabled
|
|
106
|
+
ldap-manager user search --filter "(description=contractor*)" --json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Groups
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
ldap-manager group list
|
|
113
|
+
ldap-manager group create devops --gid 5000
|
|
114
|
+
ldap-manager group add-member devops jdoe
|
|
115
|
+
ldap-manager group remove-member devops jdoe
|
|
116
|
+
ldap-manager group members devops --json
|
|
117
|
+
ldap-manager group delete old_team --yes
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Backup & Restore
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
ldap-manager backup dump --tag pre-migration
|
|
124
|
+
ldap-manager backup list
|
|
125
|
+
ldap-manager backup restore /var/backups/ldap/ldap_backup_20240101_120000
|
|
126
|
+
ldap-manager backup restore /path/to/backup --with-config --yes
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Batch Operations
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Bulk create from CSV/JSON
|
|
133
|
+
ldap-manager batch create users.csv --dry-run
|
|
134
|
+
ldap-manager batch create users.json --yes
|
|
135
|
+
|
|
136
|
+
# Bulk delete
|
|
137
|
+
ldap-manager batch delete terminations.csv --dry-run
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### Bulk Password Reset
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
ldap-manager passwd-all --dry-run
|
|
144
|
+
ldap-manager passwd-all --output /secure/passwords.csv --length 24
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### SSH Keys
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
ldap-manager user ssh-key-list jdoe
|
|
151
|
+
ldap-manager user ssh-key-add jdoe ~/.ssh/id_ed25519.pub
|
|
152
|
+
ldap-manager user ssh-key-remove jdoe 1
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Server Operations
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
ldap-manager server status
|
|
159
|
+
ldap-manager server start
|
|
160
|
+
ldap-manager server stop
|
|
161
|
+
ldap-manager server restart
|
|
162
|
+
ldap-manager server reindex --auto # stops slapd, reindexes, restarts
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Password Policy
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
ldap-manager ppolicy status jdoe # expiry, lockout, grace logins
|
|
169
|
+
ldap-manager ppolicy policy # view current policy config
|
|
170
|
+
ldap-manager ppolicy check-all # find expired/locked accounts
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### LDIF Export & Import
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
ldap-manager user export --format ldif --scope all -o backup.ldif
|
|
177
|
+
ldap-manager user export --format json --enabled -o active_users.json
|
|
178
|
+
ldap-manager import users.ldif --dry-run
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Tree Management
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
ldap-manager tree show # visualize DIT
|
|
185
|
+
ldap-manager tree list-ous
|
|
186
|
+
ldap-manager tree create-ou "ou=Contractors,dc=example,dc=com"
|
|
187
|
+
ldap-manager tree delete-ou "ou=OldDept,dc=example,dc=com" --recursive
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### Audit Log
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
ldap-manager audit log --since 2024-01-01
|
|
194
|
+
ldap-manager audit log --action create --target jdoe
|
|
195
|
+
ldap-manager audit status
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Global Options
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
ldap-manager -c /path/to/config.yaml user list # custom config
|
|
202
|
+
ldap-manager -v user list # verbose logging
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## Configuration
|
|
208
|
+
|
|
209
|
+
Config is loaded from (in order, later overrides earlier):
|
|
210
|
+
|
|
211
|
+
1. `/etc/ldap-manager/config.yaml` (system)
|
|
212
|
+
2. `~/.ldap-manager.yaml` (user)
|
|
213
|
+
3. `--config` flag (explicit)
|
|
214
|
+
4. Environment variables (highest priority)
|
|
215
|
+
|
|
216
|
+
See `config.example.yaml` for all options.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Development
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
git clone https://github.com/YOURUSERNAME/ldap-manager.git
|
|
224
|
+
cd ldap-manager
|
|
225
|
+
make install # creates venv, installs deps
|
|
226
|
+
make ci # lint + typecheck + security + tests
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Tests use mocked LDAP connections — no live server needed.
|
|
230
|
+
|
|
231
|
+
```
|
|
232
|
+
make lint # ruff check + format
|
|
233
|
+
make typecheck # mypy
|
|
234
|
+
make security # bandit
|
|
235
|
+
make test # pytest with coverage
|
|
236
|
+
make ci # all of the above
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Project Structure
|
|
242
|
+
|
|
243
|
+
```
|
|
244
|
+
ldap_manager/
|
|
245
|
+
├── __init__.py # Package metadata
|
|
246
|
+
├── cli.py # Click CLI — 40+ commands
|
|
247
|
+
├── config.py # YAML + env config loading
|
|
248
|
+
├── connection.py # LDAP connection context manager
|
|
249
|
+
├── users.py # User CRUD, enable/disable, search
|
|
250
|
+
├── groups.py # Group management (posixGroup + groupOfNames)
|
|
251
|
+
├── passwords.py # Bulk password generation + reset
|
|
252
|
+
├── backup.py # slapcat/slapadd dump + restore
|
|
253
|
+
├── batch.py # Bulk operations from CSV/JSON/TSV
|
|
254
|
+
├── server.py # Server status, start/stop, reindex
|
|
255
|
+
├── sshkeys.py # SSH public key management
|
|
256
|
+
├── ppolicy.py # Password policy status + checks
|
|
257
|
+
├── ldif_ops.py # RFC 2849 LDIF export/import
|
|
258
|
+
├── tree.py # OU/DIT management + visualization
|
|
259
|
+
└── audit.py # JSON-lines audit logging
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Design Decisions
|
|
265
|
+
|
|
266
|
+
- **`slapcat`/`slapadd` for backup** — protocol-level export (`ldapsearch`) is lossy. It misses `cn=config`, overlays, ACLs, and operational attributes. `slapcat` captures everything.
|
|
267
|
+
- **`loginShell` for enable/disable** — simpler and more portable than `shadowExpire` or `nsAccountLock`. No overlay needed.
|
|
268
|
+
- **SSHA passwords** — most universally supported LDAP hash. Change `hash_scheme` in config if your server has `pw-argon2`.
|
|
269
|
+
- **Dry-run on destructive ops** — batch, import, delete, and password reset all support `--dry-run`.
|
|
270
|
+
- **JSON output everywhere** — `--json` on 12+ commands for piping to `jq`, scripts, and monitoring.
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Requirements
|
|
275
|
+
|
|
276
|
+
- Python 3.10+
|
|
277
|
+
- OpenLDAP client libraries (`libldap2-dev` / `openldap-devel`)
|
|
278
|
+
- OpenLDAP server tools on the LDAP host (for backup/restore and server ops)
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"""Structured audit logging for LDAP operations.
|
|
2
|
+
|
|
3
|
+
Writes a JSON-lines audit log of every modification operation.
|
|
4
|
+
Each line is a self-contained JSON object with timestamp, action,
|
|
5
|
+
target, operator, and details.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
# Default audit log location
|
|
20
|
+
DEFAULT_AUDIT_LOG = "/var/log/ldap-manager/audit.jsonl"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class AuditLogger:
|
|
24
|
+
"""Append-only JSON-lines audit logger."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, log_path: str | Path | None = None) -> None:
|
|
27
|
+
if log_path is None:
|
|
28
|
+
log_path = os.environ.get("LDAP_MANAGER_AUDIT_LOG", DEFAULT_AUDIT_LOG)
|
|
29
|
+
self._path = Path(log_path)
|
|
30
|
+
self._enabled = True
|
|
31
|
+
|
|
32
|
+
# Try to create the log directory and file
|
|
33
|
+
try:
|
|
34
|
+
self._path.parent.mkdir(parents=True, exist_ok=True)
|
|
35
|
+
# Touch the file to verify we can write
|
|
36
|
+
if not self._path.exists():
|
|
37
|
+
self._path.touch(mode=0o640)
|
|
38
|
+
except PermissionError:
|
|
39
|
+
log.warning(
|
|
40
|
+
"Cannot write audit log to %s — audit logging disabled. "
|
|
41
|
+
"Create the directory or set LDAP_MANAGER_AUDIT_LOG env var.",
|
|
42
|
+
self._path,
|
|
43
|
+
)
|
|
44
|
+
self._enabled = False
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def enabled(self) -> bool:
|
|
48
|
+
return self._enabled
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def path(self) -> Path:
|
|
52
|
+
return self._path
|
|
53
|
+
|
|
54
|
+
def log(
|
|
55
|
+
self,
|
|
56
|
+
action: str,
|
|
57
|
+
target: str,
|
|
58
|
+
*,
|
|
59
|
+
operator: str = "",
|
|
60
|
+
details: dict[str, Any] | None = None,
|
|
61
|
+
success: bool = True,
|
|
62
|
+
error: str = "",
|
|
63
|
+
) -> None:
|
|
64
|
+
"""Write an audit log entry.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
action: Operation performed (e.g. "user.create", "group.add_member")
|
|
68
|
+
target: DN or identifier of the target (e.g. "uid=jdoe,ou=People,...")
|
|
69
|
+
operator: Who performed the action (bind DN, defaults to config bind_dn)
|
|
70
|
+
details: Additional structured data about the operation
|
|
71
|
+
success: Whether the operation succeeded
|
|
72
|
+
error: Error message if failed
|
|
73
|
+
"""
|
|
74
|
+
if not self._enabled:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
entry = {
|
|
78
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
|
79
|
+
"action": action,
|
|
80
|
+
"target": target,
|
|
81
|
+
"operator": operator,
|
|
82
|
+
"success": success,
|
|
83
|
+
"hostname": os.uname().nodename,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if details:
|
|
87
|
+
entry["details"] = details
|
|
88
|
+
if error:
|
|
89
|
+
entry["error"] = error
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
line = json.dumps(entry, ensure_ascii=False, separators=(",", ":"))
|
|
93
|
+
with open(self._path, "a", encoding="utf-8") as f:
|
|
94
|
+
f.write(line + "\n")
|
|
95
|
+
except (OSError, PermissionError) as exc:
|
|
96
|
+
log.warning("Failed to write audit log: %s", exc)
|
|
97
|
+
|
|
98
|
+
def query(
|
|
99
|
+
self,
|
|
100
|
+
*,
|
|
101
|
+
action: str | None = None,
|
|
102
|
+
target: str | None = None,
|
|
103
|
+
since: str | None = None,
|
|
104
|
+
limit: int = 50,
|
|
105
|
+
) -> list[dict[str, Any]]:
|
|
106
|
+
"""Query the audit log.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
action: Filter by action prefix (e.g. "user" matches "user.create")
|
|
110
|
+
target: Filter by target substring
|
|
111
|
+
since: ISO timestamp — only entries after this time
|
|
112
|
+
limit: Max entries to return (newest first)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
List of audit log entries, newest first.
|
|
116
|
+
"""
|
|
117
|
+
if not self._path.is_file():
|
|
118
|
+
return []
|
|
119
|
+
|
|
120
|
+
entries: list[dict[str, Any]] = []
|
|
121
|
+
try:
|
|
122
|
+
with open(self._path, encoding="utf-8") as f:
|
|
123
|
+
for line in f:
|
|
124
|
+
line = line.strip()
|
|
125
|
+
if not line:
|
|
126
|
+
continue
|
|
127
|
+
try:
|
|
128
|
+
entry = json.loads(line)
|
|
129
|
+
except json.JSONDecodeError:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
if action and not entry.get("action", "").startswith(action):
|
|
133
|
+
continue
|
|
134
|
+
if target and target not in entry.get("target", ""):
|
|
135
|
+
continue
|
|
136
|
+
if since and entry.get("timestamp", "") < since:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
entries.append(entry)
|
|
140
|
+
except OSError:
|
|
141
|
+
return []
|
|
142
|
+
|
|
143
|
+
# Newest first, limited
|
|
144
|
+
entries.reverse()
|
|
145
|
+
return entries[:limit]
|