summaryrefslogtreecommitdiff
path: root/ldap_cli.py
blob: abf3d4bf9c0635f8a76b19f66053d86a6f91638f (plain)
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}")