ldap-cli 0.2.0__py3-none-any.whl
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.
- .kiro/specs/ldap-cli/design.md +386 -0
- .kiro/specs/ldap-cli/requirements.md +123 -0
- .kiro/specs/ldap-cli/tasks.md +246 -0
- CHANGELOG.md +47 -0
- README.md +145 -0
- docs/reference.md +198 -0
- ldap_cli-0.2.0.dist-info/METADATA +176 -0
- ldap_cli-0.2.0.dist-info/RECORD +18 -0
- ldap_cli-0.2.0.dist-info/WHEEL +4 -0
- ldap_cli-0.2.0.dist-info/entry_points.txt +2 -0
- ldap_cli-0.2.0.dist-info/licenses/LICENSE +21 -0
- ldapc/__init__.py +7 -0
- ldapc/_version.py +23 -0
- ldapc/cli.py +490 -0
- ldapc/config.py +184 -0
- ldapc/exceptions.py +37 -0
- ldapc/formatter.py +282 -0
- ldapc/ldap_client.py +506 -0
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
# Design Document: ldap-cli
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`ldapc` is a Python CLI tool for querying LDAP directories. It provides user and group search capabilities with persistent configuration and secure credential storage via the OS keychain.
|
|
6
|
+
|
|
7
|
+
The tool follows the same project conventions as `cftcli` and `identity-provider`: a single package with module-per-command structure, `pyproject.toml` for build configuration, hatchling as the build backend, and pytest for testing.
|
|
8
|
+
|
|
9
|
+
### Key Design Decisions
|
|
10
|
+
|
|
11
|
+
1. **`ldap3` over `python-ldap`**: The `ldap3` library is a pure-Python RFC 4510 conforming LDAP client. Unlike `python-ldap`, it requires no C compiler or OpenLDAP development headers, making installation trivial across platforms.
|
|
12
|
+
|
|
13
|
+
2. **`keyring` for credential storage**: The `keyring` library provides a unified API across macOS Keychain, Windows Credential Manager, and Linux Secret Service (via D-Bus). This avoids platform-specific code.
|
|
14
|
+
|
|
15
|
+
3. **`argparse` for CLI parsing**: Standard library, no extra dependency. Matches the pattern used in `cftcli`.
|
|
16
|
+
|
|
17
|
+
4. **`pyyaml` for configuration**: Already used across the workspace projects. Lightweight and well-understood.
|
|
18
|
+
|
|
19
|
+
5. **Dataclass-based configuration**: Following the pattern in `identity-provider`, configuration is modeled as a dataclass for type safety and easy serialization.
|
|
20
|
+
|
|
21
|
+
## Architecture
|
|
22
|
+
|
|
23
|
+
```mermaid
|
|
24
|
+
graph TD
|
|
25
|
+
A[ldapc CLI Entry Point] --> B{Argument Parser}
|
|
26
|
+
B -->|--configure| C[Configure Command]
|
|
27
|
+
B -->|--user| D[User Search Command]
|
|
28
|
+
B -->|--group| E[Group Search Command]
|
|
29
|
+
B -->|--version| F[Version Display]
|
|
30
|
+
B -->|--help / no args| G[Help Display]
|
|
31
|
+
|
|
32
|
+
C --> H[Config Writer]
|
|
33
|
+
C --> I[Keyring Store]
|
|
34
|
+
|
|
35
|
+
D --> J[Config Loader]
|
|
36
|
+
D --> K[LDAP Client]
|
|
37
|
+
D --> L[Result Formatter]
|
|
38
|
+
|
|
39
|
+
E --> J
|
|
40
|
+
E --> K
|
|
41
|
+
E --> L
|
|
42
|
+
|
|
43
|
+
J --> M[~/.ldapc/config.yaml]
|
|
44
|
+
I --> N[OS Keychain]
|
|
45
|
+
K --> O[LDAP Server]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Layer Separation
|
|
49
|
+
|
|
50
|
+
| Layer | Responsibility | Modules |
|
|
51
|
+
|-------|---------------|---------|
|
|
52
|
+
| CLI | Argument parsing, entry point, exit codes | `__init__.py`, `cli.py` |
|
|
53
|
+
| Config | Read/write YAML config, keyring access | `config.py` |
|
|
54
|
+
| LDAP | Connection management, search queries | `ldap_client.py` |
|
|
55
|
+
| Formatting | Output rendering (text, JSON) | `formatter.py` |
|
|
56
|
+
|
|
57
|
+
## Components and Interfaces
|
|
58
|
+
|
|
59
|
+
### CLI Module (`ldapc/cli.py`)
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
def main() -> None:
|
|
63
|
+
"""Entry point registered as console script `ldapc`."""
|
|
64
|
+
...
|
|
65
|
+
|
|
66
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
67
|
+
"""Construct the argument parser with all flags."""
|
|
68
|
+
...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Config Module (`ldapc/config.py`)
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
@dataclass
|
|
75
|
+
class LdapcConfig:
|
|
76
|
+
"""Configuration object for ldapc."""
|
|
77
|
+
host: str
|
|
78
|
+
username: str
|
|
79
|
+
base_dn: str = ""
|
|
80
|
+
|
|
81
|
+
def load_config(config_path: Path | None = None) -> LdapcConfig:
|
|
82
|
+
"""Load configuration from ~/.ldapc/config.yaml.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ConfigError: If the file is missing or contains invalid YAML.
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
89
|
+
def save_config(config: LdapcConfig, config_path: Path | None = None) -> None:
|
|
90
|
+
"""Serialize config to ~/.ldapc/config.yaml with 0600 permissions."""
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def config_to_yaml(config: LdapcConfig) -> str:
|
|
94
|
+
"""Serialize a config object to a YAML string."""
|
|
95
|
+
...
|
|
96
|
+
|
|
97
|
+
def yaml_to_config(yaml_str: str) -> LdapcConfig:
|
|
98
|
+
"""Parse a YAML string into a config object.
|
|
99
|
+
|
|
100
|
+
Raises:
|
|
101
|
+
ConfigError: If the YAML is invalid or missing required fields.
|
|
102
|
+
"""
|
|
103
|
+
...
|
|
104
|
+
|
|
105
|
+
def store_password(username: str, password: str) -> None:
|
|
106
|
+
"""Store password in OS keychain under service 'ldapc'."""
|
|
107
|
+
...
|
|
108
|
+
|
|
109
|
+
def retrieve_password(username: str) -> str:
|
|
110
|
+
"""Retrieve password from OS keychain.
|
|
111
|
+
|
|
112
|
+
Raises:
|
|
113
|
+
ConfigError: If no password is stored for the account.
|
|
114
|
+
"""
|
|
115
|
+
...
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### LDAP Client Module (`ldapc/ldap_client.py`)
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
@dataclass
|
|
122
|
+
class LdapEntry:
|
|
123
|
+
"""A single LDAP directory entry."""
|
|
124
|
+
dn: str
|
|
125
|
+
attributes: dict[str, list[str]]
|
|
126
|
+
|
|
127
|
+
class LdapClient:
|
|
128
|
+
"""Manages LDAP connections and searches."""
|
|
129
|
+
|
|
130
|
+
def __init__(self, host: str, username: str, password: str, timeout: int = 10) -> None:
|
|
131
|
+
...
|
|
132
|
+
|
|
133
|
+
def search_users(self, search_term: str, base_dn: str) -> list[LdapEntry]:
|
|
134
|
+
"""Search for user entries matching cn or uid."""
|
|
135
|
+
...
|
|
136
|
+
|
|
137
|
+
def search_groups(self, search_term: str, base_dn: str) -> list[LdapEntry]:
|
|
138
|
+
"""Search for group entries matching cn."""
|
|
139
|
+
...
|
|
140
|
+
|
|
141
|
+
def close(self) -> None:
|
|
142
|
+
"""Unbind and close the LDAP connection."""
|
|
143
|
+
...
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### Formatter Module (`ldapc/formatter.py`)
|
|
147
|
+
|
|
148
|
+
```python
|
|
149
|
+
def format_user_entries(entries: list[LdapEntry]) -> str:
|
|
150
|
+
"""Format user entries as aligned key-value text."""
|
|
151
|
+
...
|
|
152
|
+
|
|
153
|
+
def format_group_entries(entries: list[LdapEntry]) -> str:
|
|
154
|
+
"""Format group entries as aligned key-value text."""
|
|
155
|
+
...
|
|
156
|
+
|
|
157
|
+
def format_entries_json(entries: list[LdapEntry]) -> str:
|
|
158
|
+
"""Format entries as a JSON string."""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def build_user_filter(search_term: str) -> str:
|
|
162
|
+
"""Construct an LDAP filter for user search (cn and uid)."""
|
|
163
|
+
...
|
|
164
|
+
|
|
165
|
+
def build_group_filter(search_term: str) -> str:
|
|
166
|
+
"""Construct an LDAP filter for group search (cn)."""
|
|
167
|
+
...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Exceptions (`ldapc/exceptions.py`)
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
class LdapcError(Exception):
|
|
174
|
+
"""Base exception for ldapc."""
|
|
175
|
+
...
|
|
176
|
+
|
|
177
|
+
class ConfigError(LdapcError):
|
|
178
|
+
"""Configuration-related errors (exit code 2)."""
|
|
179
|
+
...
|
|
180
|
+
|
|
181
|
+
class ConnectionError(LdapcError):
|
|
182
|
+
"""LDAP connection errors (exit code 1)."""
|
|
183
|
+
...
|
|
184
|
+
|
|
185
|
+
class AuthenticationError(LdapcError):
|
|
186
|
+
"""LDAP bind/authentication errors (exit code 1)."""
|
|
187
|
+
...
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## Data Models
|
|
191
|
+
|
|
192
|
+
### Configuration File (`~/.ldapc/config.yaml`)
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
host: ldaps://ldap.example.com:636
|
|
196
|
+
username: cn=admin,dc=example,dc=com
|
|
197
|
+
base_dn: dc=example,dc=com
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### LdapcConfig Dataclass
|
|
201
|
+
|
|
202
|
+
| Field | Type | Required | Description |
|
|
203
|
+
|-------|------|----------|-------------|
|
|
204
|
+
| `host` | `str` | Yes | LDAP server URL (ldaps:// or ldap://) |
|
|
205
|
+
| `username` | `str` | Yes | Bind DN for authentication |
|
|
206
|
+
| `base_dn` | `str` | No | Base DN for searches (defaults to derived from host) |
|
|
207
|
+
|
|
208
|
+
### LdapEntry Dataclass
|
|
209
|
+
|
|
210
|
+
| Field | Type | Description |
|
|
211
|
+
|-------|------|-------------|
|
|
212
|
+
| `dn` | `str` | Distinguished name of the entry |
|
|
213
|
+
| `attributes` | `dict[str, list[str]]` | Attribute name → list of values |
|
|
214
|
+
|
|
215
|
+
### Exit Codes
|
|
216
|
+
|
|
217
|
+
| Code | Meaning |
|
|
218
|
+
|------|---------|
|
|
219
|
+
| 0 | Success (including no results) |
|
|
220
|
+
| 1 | Connection or authentication error |
|
|
221
|
+
| 2 | Configuration error or invalid arguments |
|
|
222
|
+
|
|
223
|
+
## Correctness Properties
|
|
224
|
+
|
|
225
|
+
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
|
|
226
|
+
|
|
227
|
+
### Property 1: Configuration round-trip
|
|
228
|
+
|
|
229
|
+
*For any* valid `LdapcConfig` object containing arbitrary host and username strings, serializing it to YAML via `config_to_yaml` and then parsing it back via `yaml_to_config` SHALL produce an equivalent configuration object.
|
|
230
|
+
|
|
231
|
+
**Validates: Requirements 2.4, 3.1, 3.3, 3.4**
|
|
232
|
+
|
|
233
|
+
### Property 2: Invalid YAML produces a descriptive error
|
|
234
|
+
|
|
235
|
+
*For any* string that is not valid YAML (or valid YAML missing required fields), calling `yaml_to_config` SHALL raise a `ConfigError` with a non-empty message describing the failure.
|
|
236
|
+
|
|
237
|
+
**Validates: Requirements 3.2**
|
|
238
|
+
|
|
239
|
+
### Property 3: Password is never written to disk
|
|
240
|
+
|
|
241
|
+
*For any* valid configuration containing a password, after running the configuration save flow, the password string SHALL NOT appear in the contents of `~/.ldapc/config.yaml` or any other file in `~/.ldapc/`.
|
|
242
|
+
|
|
243
|
+
**Validates: Requirements 4.4**
|
|
244
|
+
|
|
245
|
+
### Property 4: User search filter construction
|
|
246
|
+
|
|
247
|
+
*For any* non-empty search term string, `build_user_filter(search_term)` SHALL produce a valid LDAP filter that matches against both `cn` and `uid` attributes, and the search term SHALL appear in the resulting filter string.
|
|
248
|
+
|
|
249
|
+
**Validates: Requirements 5.1**
|
|
250
|
+
|
|
251
|
+
### Property 5: Group search filter construction
|
|
252
|
+
|
|
253
|
+
*For any* non-empty search term string, `build_group_filter(search_term)` SHALL produce a valid LDAP filter that matches against the `cn` attribute, and the search term SHALL appear in the resulting filter string.
|
|
254
|
+
|
|
255
|
+
**Validates: Requirements 6.1**
|
|
256
|
+
|
|
257
|
+
### Property 6: Entry formatting contains all required fields
|
|
258
|
+
|
|
259
|
+
*For any* `LdapEntry` with a non-empty `dn` and attributes containing `cn`, `mail`, and `memberOf` (for users) or `cn`, `description`, and `member` (for groups), the formatted output string SHALL contain the entry's DN and all specified attribute values.
|
|
260
|
+
|
|
261
|
+
**Validates: Requirements 5.2, 6.2, 7.1**
|
|
262
|
+
|
|
263
|
+
### Property 7: JSON output is valid and round-trips
|
|
264
|
+
|
|
265
|
+
*For any* list of `LdapEntry` objects, `format_entries_json(entries)` SHALL produce a string that is valid JSON, and deserializing that JSON SHALL yield data equivalent to the original entries' DN and attributes.
|
|
266
|
+
|
|
267
|
+
**Validates: Requirements 7.3**
|
|
268
|
+
|
|
269
|
+
## Error Handling
|
|
270
|
+
|
|
271
|
+
| Error Condition | User-Facing Message | Exit Code | Stream |
|
|
272
|
+
|----------------|--------------------:|:---------:|--------|
|
|
273
|
+
| Config file missing | `Configuration not found. Run 'ldapc --configure' to set up.` | 2 | stderr |
|
|
274
|
+
| Config file invalid YAML | `Invalid configuration file: {details}` | 2 | stderr |
|
|
275
|
+
| Keychain password missing | `No stored password. Run 'ldapc --configure' to set up.` | 2 | stderr |
|
|
276
|
+
| Invalid arguments | argparse default error | 2 | stderr |
|
|
277
|
+
| LDAP server unreachable | `Connection failed: unable to reach {host}` | 1 | stderr |
|
|
278
|
+
| LDAP connection timeout | `Connection timed out after 10 seconds: {host}` | 1 | stderr |
|
|
279
|
+
| TLS certificate error | `TLS certificate verification failed for {host}` | 1 | stderr |
|
|
280
|
+
| Authentication failure | `Authentication failed: invalid credentials for {username}` | 1 | stderr |
|
|
281
|
+
| No results found | `No results found for '{search_term}'` | 0 | stdout |
|
|
282
|
+
|
|
283
|
+
### Error Handling Strategy
|
|
284
|
+
|
|
285
|
+
- All exceptions inherit from `LdapcError` for consistent catching at the CLI layer.
|
|
286
|
+
- The CLI `main()` function wraps command execution in a try/except that maps exception types to exit codes.
|
|
287
|
+
- Error messages go to stderr; query results go to stdout.
|
|
288
|
+
- LDAP connections are always closed in a `finally` block to prevent resource leaks.
|
|
289
|
+
|
|
290
|
+
## Testing Strategy
|
|
291
|
+
|
|
292
|
+
### Test Framework and Tools
|
|
293
|
+
|
|
294
|
+
- **pytest** for test execution
|
|
295
|
+
- **hypothesis** for property-based testing
|
|
296
|
+
- **unittest.mock** for mocking LDAP connections and keyring
|
|
297
|
+
- **pytest-cov** for coverage reporting
|
|
298
|
+
|
|
299
|
+
### Property-Based Tests (hypothesis)
|
|
300
|
+
|
|
301
|
+
Each correctness property maps to a single hypothesis test with minimum 100 iterations:
|
|
302
|
+
|
|
303
|
+
| Property | Test File | Strategy |
|
|
304
|
+
|----------|-----------|----------|
|
|
305
|
+
| 1: Config round-trip | `tests/test_config_properties.py` | Generate random `LdapcConfig` objects via `@st.composite` |
|
|
306
|
+
| 2: Invalid YAML error | `tests/test_config_properties.py` | Generate random non-YAML strings |
|
|
307
|
+
| 3: Password not on disk | `tests/test_config_properties.py` | Generate random passwords, run save, grep files |
|
|
308
|
+
| 4: User filter construction | `tests/test_search_properties.py` | Generate random search term strings |
|
|
309
|
+
| 5: Group filter construction | `tests/test_search_properties.py` | Generate random search term strings |
|
|
310
|
+
| 6: Entry formatting | `tests/test_formatter_properties.py` | Generate random `LdapEntry` objects |
|
|
311
|
+
| 7: JSON round-trip | `tests/test_formatter_properties.py` | Generate random lists of `LdapEntry` objects |
|
|
312
|
+
|
|
313
|
+
Each test is tagged with: `# Feature: ldap-cli, Property {N}: {title}`
|
|
314
|
+
|
|
315
|
+
Configuration: `@settings(max_examples=100)`
|
|
316
|
+
|
|
317
|
+
### Unit Tests (example-based)
|
|
318
|
+
|
|
319
|
+
| Area | Test File | Coverage |
|
|
320
|
+
|------|-----------|----------|
|
|
321
|
+
| CLI argument parsing | `tests/test_cli.py` | --help, --version, no args, invalid args |
|
|
322
|
+
| Configure flow | `tests/test_configure.py` | Prompts, directory creation, permissions |
|
|
323
|
+
| Error scenarios | `tests/test_errors.py` | Connection failures, auth failures, timeouts |
|
|
324
|
+
| Exit codes | `tests/test_exit_codes.py` | All exit code scenarios |
|
|
325
|
+
| Output streams | `tests/test_output.py` | stderr for errors, stdout for results |
|
|
326
|
+
|
|
327
|
+
### Integration Tests
|
|
328
|
+
|
|
329
|
+
| Area | Test File | Coverage |
|
|
330
|
+
|------|-----------|----------|
|
|
331
|
+
| Keyring integration | `tests/test_keyring_integration.py` | Store/retrieve with mocked keyring backend |
|
|
332
|
+
| LDAP connection lifecycle | `tests/test_ldap_integration.py` | Connect, search, unbind with mocked server |
|
|
333
|
+
|
|
334
|
+
### Project Structure
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
ldapc/
|
|
338
|
+
├── pyproject.toml
|
|
339
|
+
├── CHANGELOG.md
|
|
340
|
+
├── README.md
|
|
341
|
+
├── LICENSE
|
|
342
|
+
├── docs/
|
|
343
|
+
│ └── README.md
|
|
344
|
+
├── ldapc/
|
|
345
|
+
│ ├── __init__.py
|
|
346
|
+
│ ├── _version.py
|
|
347
|
+
│ ├── cli.py
|
|
348
|
+
│ ├── config.py
|
|
349
|
+
│ ├── ldap_client.py
|
|
350
|
+
│ ├── formatter.py
|
|
351
|
+
│ └── exceptions.py
|
|
352
|
+
└── tests/
|
|
353
|
+
├── __init__.py
|
|
354
|
+
├── conftest.py
|
|
355
|
+
├── test_cli.py
|
|
356
|
+
├── test_configure.py
|
|
357
|
+
├── test_config_properties.py
|
|
358
|
+
├── test_search_properties.py
|
|
359
|
+
├── test_formatter_properties.py
|
|
360
|
+
├── test_errors.py
|
|
361
|
+
├── test_exit_codes.py
|
|
362
|
+
├── test_output.py
|
|
363
|
+
├── test_keyring_integration.py
|
|
364
|
+
└── test_ldap_integration.py
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Dependencies
|
|
368
|
+
|
|
369
|
+
```toml
|
|
370
|
+
[project]
|
|
371
|
+
dependencies = [
|
|
372
|
+
"ldap3>=2.9,<3.0",
|
|
373
|
+
"keyring>=25.0,<26.0",
|
|
374
|
+
"pyyaml>=6.0,<7.0",
|
|
375
|
+
]
|
|
376
|
+
|
|
377
|
+
[project.optional-dependencies]
|
|
378
|
+
dev = [
|
|
379
|
+
"pytest>=8.0,<9.0",
|
|
380
|
+
"pytest-cov>=5.0,<6.0",
|
|
381
|
+
"hypothesis>=6.100,<7.0",
|
|
382
|
+
"ruff>=0.4,<1.0",
|
|
383
|
+
"mypy>=1.10,<2.0",
|
|
384
|
+
"bandit>=1.7,<2.0",
|
|
385
|
+
]
|
|
386
|
+
```
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Requirements Document
|
|
2
|
+
|
|
3
|
+
## Introduction
|
|
4
|
+
|
|
5
|
+
A Python CLI tool (`ldapc`) for querying LDAP directories. The tool provides a simple interface to search for users and groups, with persistent configuration stored locally and secure credential management via the OS keychain.
|
|
6
|
+
|
|
7
|
+
## Glossary
|
|
8
|
+
|
|
9
|
+
- **CLI**: The `ldapc` command-line interface application
|
|
10
|
+
- **Configuration_Store**: The YAML configuration file located at `~/.ldapc/config.yaml`
|
|
11
|
+
- **Keychain**: The operating system's native credential storage (macOS Keychain, Windows Credential Manager, or Linux Secret Service)
|
|
12
|
+
- **LDAP_Server**: The remote LDAP directory server the CLI connects to
|
|
13
|
+
- **Config_Parser**: The component that reads and writes `~/.ldapc/config.yaml`
|
|
14
|
+
- **Config_Printer**: The component that serializes configuration objects back to YAML format
|
|
15
|
+
- **Search_Result**: A structured object containing attributes returned from an LDAP query
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
### Requirement 1: CLI Entry Point
|
|
20
|
+
|
|
21
|
+
**User Story:** As a developer, I want to invoke the LDAP CLI by typing `ldapc`, so that I can quickly query LDAP without remembering long commands.
|
|
22
|
+
|
|
23
|
+
#### Acceptance Criteria
|
|
24
|
+
|
|
25
|
+
1. THE CLI SHALL be invocable as `ldapc` from the shell after installation
|
|
26
|
+
2. WHEN invoked without arguments, THE CLI SHALL display a help message showing available options and usage examples
|
|
27
|
+
3. WHEN invoked with `--version`, THE CLI SHALL display the current version string
|
|
28
|
+
4. WHEN invoked with `--help`, THE CLI SHALL display the full help text including all supported flags
|
|
29
|
+
|
|
30
|
+
### Requirement 2: Interactive Configuration
|
|
31
|
+
|
|
32
|
+
**User Story:** As a developer, I want to configure the LDAP host and credentials interactively, so that I do not have to provide them on every invocation.
|
|
33
|
+
|
|
34
|
+
#### Acceptance Criteria
|
|
35
|
+
|
|
36
|
+
1. WHEN invoked with `--configure`, THE CLI SHALL prompt the user for the LDAP host URL
|
|
37
|
+
2. WHEN invoked with `--configure`, THE CLI SHALL prompt the user for the bind username
|
|
38
|
+
3. WHEN invoked with `--configure`, THE CLI SHALL prompt the user for the bind password
|
|
39
|
+
4. WHEN the user completes the configuration prompts, THE Configuration_Store SHALL persist the host and username to `~/.ldapc/config.yaml`
|
|
40
|
+
5. WHEN the user completes the configuration prompts, THE CLI SHALL store the password in the Keychain
|
|
41
|
+
6. IF the `~/.ldapc/` directory does not exist, THEN THE CLI SHALL create it before writing the configuration file
|
|
42
|
+
7. THE Configuration_Store SHALL set file permissions on `~/.ldapc/config.yaml` to owner-read-write only (0600)
|
|
43
|
+
|
|
44
|
+
### Requirement 3: Configuration File Parsing
|
|
45
|
+
|
|
46
|
+
**User Story:** As a developer, I want the CLI to reliably read and write its configuration file, so that my settings persist across sessions.
|
|
47
|
+
|
|
48
|
+
#### Acceptance Criteria
|
|
49
|
+
|
|
50
|
+
1. WHEN a valid `~/.ldapc/config.yaml` file exists, THE Config_Parser SHALL parse it into a configuration object containing host and username
|
|
51
|
+
2. IF `~/.ldapc/config.yaml` contains invalid YAML, THEN THE Config_Parser SHALL return a descriptive error message indicating the parse failure
|
|
52
|
+
3. THE Config_Printer SHALL format configuration objects back into valid YAML files
|
|
53
|
+
4. FOR ALL valid configuration objects, parsing then printing then parsing SHALL produce an equivalent object (round-trip property)
|
|
54
|
+
5. IF `~/.ldapc/config.yaml` does not exist, THEN THE CLI SHALL display an error instructing the user to run `ldapc --configure`
|
|
55
|
+
|
|
56
|
+
### Requirement 4: Secure Credential Storage
|
|
57
|
+
|
|
58
|
+
**User Story:** As a developer, I want my LDAP password stored securely in the OS keychain, so that it is not exposed in plain-text files.
|
|
59
|
+
|
|
60
|
+
#### Acceptance Criteria
|
|
61
|
+
|
|
62
|
+
1. WHEN storing credentials, THE CLI SHALL use the OS Keychain with service name `ldapc` and the configured username as the account identifier
|
|
63
|
+
2. WHEN retrieving credentials for an LDAP connection, THE CLI SHALL read the password from the Keychain
|
|
64
|
+
3. IF the Keychain does not contain a stored password for the configured account, THEN THE CLI SHALL display an error instructing the user to run `ldapc --configure`
|
|
65
|
+
4. THE CLI SHALL NOT write the password to any file on disk
|
|
66
|
+
|
|
67
|
+
### Requirement 5: Query by User
|
|
68
|
+
|
|
69
|
+
**User Story:** As a developer, I want to search LDAP for a user by name, so that I can quickly look up user attributes.
|
|
70
|
+
|
|
71
|
+
#### Acceptance Criteria
|
|
72
|
+
|
|
73
|
+
1. WHEN invoked with `--user {searchuser}`, THE CLI SHALL query the LDAP_Server for entries matching the search term against common name and uid attributes
|
|
74
|
+
2. WHEN the LDAP_Server returns matching user entries, THE CLI SHALL display each entry's distinguished name, common name, email, and group memberships in a readable format
|
|
75
|
+
3. WHEN the LDAP_Server returns no matching user entries, THE CLI SHALL display a message indicating no results were found for the search term
|
|
76
|
+
4. IF the LDAP_Server is unreachable, THEN THE CLI SHALL display a connection error message including the configured host
|
|
77
|
+
5. IF the LDAP bind credentials are rejected, THEN THE CLI SHALL display an authentication error message
|
|
78
|
+
|
|
79
|
+
### Requirement 6: Query by Group
|
|
80
|
+
|
|
81
|
+
**User Story:** As a developer, I want to search LDAP for a group by name, so that I can see group membership details.
|
|
82
|
+
|
|
83
|
+
#### Acceptance Criteria
|
|
84
|
+
|
|
85
|
+
1. WHEN invoked with `--group {searchgroup}`, THE CLI SHALL query the LDAP_Server for group entries matching the search term against common name
|
|
86
|
+
2. WHEN the LDAP_Server returns matching group entries, THE CLI SHALL display each group's distinguished name, common name, description, and member list in a readable format
|
|
87
|
+
3. WHEN the LDAP_Server returns no matching group entries, THE CLI SHALL display a message indicating no results were found for the search term
|
|
88
|
+
4. IF the LDAP_Server is unreachable, THEN THE CLI SHALL display a connection error message including the configured host
|
|
89
|
+
5. IF the LDAP bind credentials are rejected, THEN THE CLI SHALL display an authentication error message
|
|
90
|
+
|
|
91
|
+
### Requirement 7: Output Formatting
|
|
92
|
+
|
|
93
|
+
**User Story:** As a developer, I want search results displayed in a clear, readable format, so that I can quickly find the information I need.
|
|
94
|
+
|
|
95
|
+
#### Acceptance Criteria
|
|
96
|
+
|
|
97
|
+
1. THE CLI SHALL display search results using aligned key-value formatting for single results
|
|
98
|
+
2. WHEN multiple results are returned, THE CLI SHALL visually separate each entry with a delimiter
|
|
99
|
+
3. WHEN the `--json` flag is provided, THE CLI SHALL output results as valid JSON to stdout
|
|
100
|
+
4. THE CLI SHALL write error messages to stderr, keeping stdout reserved for query results
|
|
101
|
+
|
|
102
|
+
### Requirement 8: Connection Management
|
|
103
|
+
|
|
104
|
+
**User Story:** As a developer, I want the CLI to handle LDAP connections reliably, so that queries complete without leaving stale connections.
|
|
105
|
+
|
|
106
|
+
#### Acceptance Criteria
|
|
107
|
+
|
|
108
|
+
1. WHEN a query is initiated, THE CLI SHALL establish a TLS-secured connection to the LDAP_Server
|
|
109
|
+
2. WHEN a query completes, THE CLI SHALL unbind and close the LDAP connection
|
|
110
|
+
3. IF the connection times out after 10 seconds, THEN THE CLI SHALL display a timeout error and exit with a non-zero status code
|
|
111
|
+
4. IF a TLS certificate verification fails, THEN THE CLI SHALL display a certificate error message
|
|
112
|
+
|
|
113
|
+
### Requirement 9: Exit Codes
|
|
114
|
+
|
|
115
|
+
**User Story:** As a developer, I want consistent exit codes, so that I can use `ldapc` in scripts and automation.
|
|
116
|
+
|
|
117
|
+
#### Acceptance Criteria
|
|
118
|
+
|
|
119
|
+
1. WHEN a query completes successfully, THE CLI SHALL exit with code 0
|
|
120
|
+
2. WHEN a query returns no results, THE CLI SHALL exit with code 0
|
|
121
|
+
3. IF a connection or authentication error occurs, THEN THE CLI SHALL exit with code 1
|
|
122
|
+
4. IF a configuration error occurs, THEN THE CLI SHALL exit with code 2
|
|
123
|
+
5. IF an invalid argument is provided, THEN THE CLI SHALL exit with code 2
|