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
ldapc/formatter.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Formatter module for ldapc.
|
|
2
|
+
|
|
3
|
+
Provides LDAP search filter construction and output formatting functions
|
|
4
|
+
for user and group entries. Supports aligned key-value text, JSON, and
|
|
5
|
+
YAML output formats.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import yaml
|
|
13
|
+
|
|
14
|
+
from ldapc.ldap_client import LdapEntry
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_user_filter(search_term: str) -> str:
|
|
18
|
+
"""Construct an LDAP filter for user search (cn and uid).
|
|
19
|
+
|
|
20
|
+
Builds an OR filter that matches the search term as a substring
|
|
21
|
+
against both the common name (cn) and user ID (uid) attributes.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
search_term: The term to search for in cn and uid fields.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
An LDAP filter string matching cn or uid.
|
|
28
|
+
|
|
29
|
+
Example:
|
|
30
|
+
>>> build_user_filter("john")
|
|
31
|
+
'(|(cn=*john*)(uid=*john*))'
|
|
32
|
+
"""
|
|
33
|
+
return f"(|(cn=*{search_term}*)(uid=*{search_term}*))"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_group_filter(search_term: str) -> str:
|
|
37
|
+
"""Construct an LDAP filter for group search (cn).
|
|
38
|
+
|
|
39
|
+
Builds a filter that matches the search term as a substring
|
|
40
|
+
against the common name (cn) attribute.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
search_term: The term to search for in the cn field.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
An LDAP filter string matching cn.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
>>> build_group_filter("admin")
|
|
50
|
+
'(cn=*admin*)'
|
|
51
|
+
"""
|
|
52
|
+
return f"(cn=*{search_term}*)"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def format_user_entries(entries: list[LdapEntry]) -> str:
|
|
56
|
+
"""Format user entries as aligned key-value text.
|
|
57
|
+
|
|
58
|
+
Displays each user entry's distinguished name, common name, email,
|
|
59
|
+
and group memberships. Multiple entries are separated by a delimiter
|
|
60
|
+
line. Keys are padded to a consistent width for alignment.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
entries: List of LdapEntry objects representing user entries.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
A formatted string with aligned key-value pairs.
|
|
67
|
+
"""
|
|
68
|
+
if not entries:
|
|
69
|
+
return ""
|
|
70
|
+
|
|
71
|
+
blocks: list[str] = []
|
|
72
|
+
for entry in entries:
|
|
73
|
+
lines = _format_user_block(entry)
|
|
74
|
+
blocks.append("\n".join(lines))
|
|
75
|
+
|
|
76
|
+
return "\n---\n".join(blocks)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def format_group_entries(entries: list[LdapEntry]) -> str:
|
|
80
|
+
"""Format group entries as aligned key-value text.
|
|
81
|
+
|
|
82
|
+
Displays each group entry's distinguished name, common name,
|
|
83
|
+
description, and member list. Multiple entries are separated by a
|
|
84
|
+
delimiter line. Keys are padded to a consistent width for alignment.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
entries: List of LdapEntry objects representing group entries.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A formatted string with aligned key-value pairs.
|
|
91
|
+
"""
|
|
92
|
+
if not entries:
|
|
93
|
+
return ""
|
|
94
|
+
|
|
95
|
+
blocks: list[str] = []
|
|
96
|
+
for entry in entries:
|
|
97
|
+
lines = _format_group_block(entry)
|
|
98
|
+
blocks.append("\n".join(lines))
|
|
99
|
+
|
|
100
|
+
return "\n---\n".join(blocks)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def format_entries_json(entries: list[LdapEntry]) -> str:
|
|
104
|
+
"""Format entries as a JSON string.
|
|
105
|
+
|
|
106
|
+
Serializes a list of LdapEntry objects into a JSON array where each
|
|
107
|
+
element has "dn" and "attributes" keys.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
entries: List of LdapEntry objects to serialize.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
A valid JSON string representing the entries.
|
|
114
|
+
"""
|
|
115
|
+
data = [
|
|
116
|
+
{"dn": entry.dn, "attributes": entry.attributes}
|
|
117
|
+
for entry in entries
|
|
118
|
+
]
|
|
119
|
+
return json.dumps(data, indent=2)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _format_user_block(entry: LdapEntry) -> list[str]:
|
|
123
|
+
"""Format a single user entry as aligned key-value lines.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
entry: An LdapEntry representing a user.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
A list of formatted lines.
|
|
130
|
+
"""
|
|
131
|
+
cn = _get_first_attr(entry, "cn")
|
|
132
|
+
mail = _get_first_attr(entry, "mail")
|
|
133
|
+
groups = entry.attributes.get("memberOf", [])
|
|
134
|
+
|
|
135
|
+
key_width = 10 # Enough for "memberOf" + padding
|
|
136
|
+
|
|
137
|
+
lines = [
|
|
138
|
+
f"{'DN':<{key_width}}: {entry.dn}",
|
|
139
|
+
f"{'CN':<{key_width}}: {cn}",
|
|
140
|
+
f"{'Email':<{key_width}}: {mail}",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
if groups:
|
|
144
|
+
lines.append(f"{'Groups':<{key_width}}: {groups[0]}")
|
|
145
|
+
for group in groups[1:]:
|
|
146
|
+
lines.append(f"{'':<{key_width}} {group}")
|
|
147
|
+
else:
|
|
148
|
+
lines.append(f"{'Groups':<{key_width}}: ")
|
|
149
|
+
|
|
150
|
+
return lines
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _format_group_block(entry: LdapEntry) -> list[str]:
|
|
154
|
+
"""Format a single group entry as aligned key-value lines.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
entry: An LdapEntry representing a group.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
A list of formatted lines.
|
|
161
|
+
"""
|
|
162
|
+
cn = _get_first_attr(entry, "cn")
|
|
163
|
+
description = _get_first_attr(entry, "description")
|
|
164
|
+
members = entry.attributes.get("member", [])
|
|
165
|
+
|
|
166
|
+
key_width = 12 # Enough for "Description" + padding
|
|
167
|
+
|
|
168
|
+
lines = [
|
|
169
|
+
f"{'DN':<{key_width}}: {entry.dn}",
|
|
170
|
+
f"{'CN':<{key_width}}: {cn}",
|
|
171
|
+
f"{'Description':<{key_width}}: {description}",
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
if members:
|
|
175
|
+
lines.append(f"{'Members':<{key_width}}: {members[0]}")
|
|
176
|
+
for member in members[1:]:
|
|
177
|
+
lines.append(f"{'':<{key_width}} {member}")
|
|
178
|
+
else:
|
|
179
|
+
lines.append(f"{'Members':<{key_width}}: ")
|
|
180
|
+
|
|
181
|
+
return lines
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _get_first_attr(entry: LdapEntry, attr_name: str) -> str:
|
|
185
|
+
"""Get the first value of an attribute, or empty string if missing.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
entry: The LdapEntry to read from.
|
|
189
|
+
attr_name: The attribute name to look up.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
The first value of the attribute, or an empty string.
|
|
193
|
+
"""
|
|
194
|
+
values = entry.attributes.get(attr_name, [])
|
|
195
|
+
return values[0] if values else ""
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _extract_cn(dn: str) -> str:
|
|
199
|
+
"""Extract the cn value from a DN string.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
dn: A distinguished name like 'cn=admins,ou=groups,dc=example,dc=com'.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
The cn value (e.g. 'admins'), or the full DN if no cn= found.
|
|
206
|
+
"""
|
|
207
|
+
for part in dn.split(","):
|
|
208
|
+
part = part.strip()
|
|
209
|
+
if part.lower().startswith("cn="):
|
|
210
|
+
return part[3:]
|
|
211
|
+
return dn
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def format_user_entries_yaml(entries: list[LdapEntry]) -> str:
|
|
215
|
+
"""Format user entries as YAML.
|
|
216
|
+
|
|
217
|
+
Each entry becomes a YAML document with cn, email, and groups
|
|
218
|
+
(groups shown as just their cn component).
|
|
219
|
+
|
|
220
|
+
Args:
|
|
221
|
+
entries: List of LdapEntry objects representing user entries.
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
A YAML-formatted string.
|
|
225
|
+
"""
|
|
226
|
+
if not entries:
|
|
227
|
+
return ""
|
|
228
|
+
|
|
229
|
+
docs: list[str] = []
|
|
230
|
+
for entry in entries:
|
|
231
|
+
cn = _get_first_attr(entry, "cn")
|
|
232
|
+
mail = _get_first_attr(entry, "mail")
|
|
233
|
+
groups = entry.attributes.get("memberOf", [])
|
|
234
|
+
|
|
235
|
+
data: dict = {"cn": cn}
|
|
236
|
+
if mail:
|
|
237
|
+
data["email"] = mail
|
|
238
|
+
if groups:
|
|
239
|
+
data["groups"] = [_extract_cn(g) for g in groups]
|
|
240
|
+
else:
|
|
241
|
+
data["groups"] = []
|
|
242
|
+
|
|
243
|
+
docs.append(yaml.dump(data, default_flow_style=False, sort_keys=False).rstrip())
|
|
244
|
+
|
|
245
|
+
return "---\n" + "\n---\n".join(docs)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def format_group_entries_yaml(entries: list[LdapEntry]) -> str:
|
|
249
|
+
"""Format group entries as YAML.
|
|
250
|
+
|
|
251
|
+
Each entry becomes a YAML document with cn, description, and members
|
|
252
|
+
(members shown as just their cn or uid component).
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
entries: List of LdapEntry objects representing group entries.
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
A YAML-formatted string.
|
|
259
|
+
"""
|
|
260
|
+
if not entries:
|
|
261
|
+
return ""
|
|
262
|
+
|
|
263
|
+
docs: list[str] = []
|
|
264
|
+
for entry in entries:
|
|
265
|
+
cn = _get_first_attr(entry, "cn")
|
|
266
|
+
description = _get_first_attr(entry, "description")
|
|
267
|
+
members = entry.attributes.get("member", [])
|
|
268
|
+
member_uids = entry.attributes.get("memberUid", [])
|
|
269
|
+
|
|
270
|
+
data: dict = {"cn": cn}
|
|
271
|
+
if description:
|
|
272
|
+
data["description"] = description
|
|
273
|
+
if members:
|
|
274
|
+
data["members"] = [_extract_cn(m) for m in members]
|
|
275
|
+
elif member_uids:
|
|
276
|
+
data["members"] = member_uids
|
|
277
|
+
else:
|
|
278
|
+
data["members"] = []
|
|
279
|
+
|
|
280
|
+
docs.append(yaml.dump(data, default_flow_style=False, sort_keys=False).rstrip())
|
|
281
|
+
|
|
282
|
+
return "---\n" + "\n---\n".join(docs)
|