1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
#!/usr/bin/env python3
import argparse
import ldap
import n.ldap
class AmbiguousPathError(Exception):
pass
class EntryNotFoundError(Exception):
pass
def _dwim_find_object(ad, base_dn, tail):
"""
Helper for dwim_find_object() that makes directory lookups to guess the
correct naming attribute given its value.
When tail is 'foo=bar', directly use 'foo' as the attribute name, at first
trying to directly build a DN, then making a onelevel search.
Otherwise try searching for onelevel children with 'cn' then 'ou'
attributes in that order.
"""
if "=" in tail:
k, v = tail.split("=", 1)
attrs = [k]
try:
dn = n.ldap.build_dn({k: v}, base_dn)
_ = ad.read(dn, ["1.1"])
return [dn]
except ldap.NO_SUCH_OBJECT:
# Carry on to the search stage, so that we could find entries where
# this k=v is used as a non-naming attribute (e.g. for entries that
# have multiple 'cn' values).
pass
except ldap.INVALID_DN_SYNTAX:
# This also happens if the DN has an unknown attribute.
raise
else:
v = tail
attrs = ["cn", "ou"]
for k in attrs:
print(f"Trying search of {k}={v!r} under {base_dn!r}")
res = ad.search(base_dn,
n.ldap.SCOPE_ONELEVEL,
"(%s=%s)", (k, v),
["1.1"])
if res:
return [dn for dn, _ in res]
raise EntryNotFoundError("No match found for %r" % path)
def dwim_find_object(ad, path):
"""
Resolve attribute-less paths such as /Hosts/Foreign/foo into actual entry
DNs together with a search scope.
A suffix of '//' means subtree scope, '/' means onelevel scope, otherwise
base scope. For example, '/Hosts//' means the entire /Hosts subtree.
A prefix of '//' disables implicit root at the directory's base DN.
All 'head' path components are implicitly ou= unless specified otherwise.
The 'tail' path component is looked up in the directory as 'cn=', then as 'ou='.
"""
# Special case for referencing the base DN itself
if path == "//":
return n.ldap.Directory.base, n.ldap.SCOPE_SUBTREE
elif path == "/":
return n.ldap.Directory.base, n.ldap.SCOPE_ONELEVEL
elif path in {"", "."}:
return n.ldap.Directory.base, n.ldap.SCOPE_BASE
# Trailing slash suffix determines scope
if path.endswith("//"):
scope = n.ldap.SCOPE_SUBTREE
elif path.endswith("/"):
scope = n.ldap.SCOPE_ONELEVEL
else:
scope = n.ldap.SCOPE_BASE
path = path.rstrip("/")
# Double slash prefix specifies unprefixed paths (e.g. /cn=config)
if path.startswith("//"):
root = ""
else:
root = n.ldap.Directory.base
path = path.lstrip("/")
*head, tail = path.split("/")
print("head", repr(head), "tail", repr(tail))
base = [root]
for h in head:
if "=" in h:
k, v = h.split("=", 1)
else:
k, v = "ou", h
base.append({k: v})
base.reverse()
base_dn = n.ldap.build_dn(*base)
print("base", repr(base_dn))
entry_dns = _dwim_find_object(ad, base_dn, tail)
if len(entry_dns) > 1:
raise AmbiguousPathError(f"{len(entry_dns)} results found for {tail!r}")
return entry_dns[0], scope
parser = argparse.ArgumentParser()
parser.add_argument("command")
parser.add_argument("rest", nargs="*")
args = parser.parse_args()
ad = n.ldap.Directory()
if args.command == "lookup":
path = args.rest[0]
base_dn, scope = dwim_find_object(ad, path)
print(f"Got base {base_dn!r} scope {scope!r}")
else:
exit(f"error: Unknown command {args.command!r}")
|