1package ldap
2
3import (
4 "context"
5 "errors"
6 "sort"
7 "strconv"
8 "strings"
9
10 "tug/internal/storage"
11)
12
13var attributeNames = []struct{ key, name string }{
14 {"objectclass", "objectClass"},
15 {"dc", "dc"},
16 {"o", "o"},
17 {"ou", "ou"},
18 {"uid", "uid"},
19 {"cn", "cn"},
20 {"sn", "sn"},
21 {"displayname", "displayName"},
22 {"uidnumber", "uidNumber"},
23 {"gidnumber", "gidNumber"},
24 {"homedirectory", "homeDirectory"},
25 {"memberof", "memberOf"},
26 {"description", "description"},
27 {"member", "member"},
28 {"memberuid", "memberUid"},
29 {"sshpublickey", "sshPublicKey"},
30 {"entryuuid", "entryUUID"},
31 {"namingcontexts", "namingContexts"},
32 {"supportedldapversion", "supportedLDAPVersion"},
33 {"supportedcontrol", "supportedControl"},
34}
35
36var operationalAttributes = map[string]bool{
37 "entryuuid": true,
38 "namingcontexts": true,
39 "supportedldapversion": true,
40 "supportedcontrol": true,
41}
42
43const pagedResultsOID = "1.2.840.113556.1.4.319"
44
45type directory struct {
46 store *storage.Store
47 base string
48 baseRDN rdn
49 primaryGroup string
50}
51
52type snapshot struct {
53 entries []*entry
54 byDN map[string]*entry
55}
56
57func newDirectory(store *storage.Store, baseDN, primaryGroup string) (*directory, error) {
58 rdns, err := parseDN(baseDN)
59 if err != nil || len(rdns) == 0 {
60 return nil, errDN
61 }
62 for _, r := range rdns {
63 if r.typ != "dc" && r.typ != "o" && r.typ != "ou" {
64 return nil, errDN
65 }
66 }
67 return &directory{store: store, base: joinDN(rdns), baseRDN: rdns[0], primaryGroup: primaryGroup}, nil
68}
69
70func (d *directory) userDN(username string) string {
71 return "uid=" + username + ",ou=people," + d.base
72}
73
74func (d *directory) groupDN(name string) string {
75 return "cn=" + name + ",ou=groups," + d.base
76}
77
78func (d *directory) peopleDN() string { return "ou=people," + d.base }
79
80func (d *directory) groupsDN() string { return "ou=groups," + d.base }
81
82func (d *directory) rootDSE() *entry {
83 return &entry{dn: "", attrs: map[string][]string{
84 "objectclass": {"top"},
85 "namingcontexts": {d.base},
86 "supportedldapversion": {"3"},
87 "supportedcontrol": {pagedResultsOID},
88 }}
89}
90
91func (d *directory) fixedEntries() []*entry {
92 baseClass := map[string]string{"dc": "domain", "o": "organization", "ou": "organizationalUnit"}[d.baseRDN.typ]
93 return []*entry{
94 {dn: d.base, attrs: map[string][]string{"objectclass": {"top", baseClass}, d.baseRDN.typ: {d.baseRDN.val}}},
95 {dn: d.peopleDN(), attrs: map[string][]string{"objectclass": {"top", "organizationalUnit"}, "ou": {"people"}}},
96 {dn: d.groupsDN(), attrs: map[string][]string{"objectclass": {"top", "organizationalUnit"}, "ou": {"groups"}}},
97 }
98}
99
100func surname(displayName, username string) string {
101 fields := strings.Fields(displayName)
102 if len(fields) < 2 {
103 return username
104 }
105 return fields[len(fields)-1]
106}
107
108func (d *directory) snapshot(ctx context.Context) (*snapshot, error) {
109 users, err := d.store.ListUsers(ctx)
110 if err != nil {
111 return nil, err
112 }
113 groups, err := d.store.ListGroups(ctx)
114 if err != nil {
115 return nil, err
116 }
117 keys, err := d.store.ListSSHKeys(ctx)
118 if err != nil {
119 return nil, err
120 }
121 keysOf := map[string][]string{}
122 for _, k := range keys {
123 keysOf[k.UserID] = append(keysOf[k.UserID], k.Authorized())
124 }
125 var primary *storage.Group
126 for i := range groups {
127 if groups[i].Name == d.primaryGroup {
128 primary = &groups[i]
129 }
130 }
131 publishable := map[string]storage.User{}
132 if primary != nil {
133 for _, u := range users {
134 if !u.Disabled {
135 publishable[u.ID] = u
136 }
137 }
138 }
139 members := map[string]map[string]storage.User{}
140 for _, g := range groups {
141 set := map[string]storage.User{}
142 if primary != nil && g.ID == primary.ID {
143 for id, u := range publishable {
144 set[id] = u
145 }
146 }
147 in, err := d.store.UsersInGroup(ctx, g.ID)
148 if err != nil {
149 return nil, err
150 }
151 for _, u := range in {
152 if p, ok := publishable[u.ID]; ok {
153 set[u.ID] = p
154 }
155 }
156 if len(set) > 0 {
157 members[g.ID] = set
158 }
159 }
160 memberOf := map[string][]string{}
161 var groupEntries []*entry
162 for _, g := range groups {
163 set, ok := members[g.ID]
164 if !ok {
165 continue
166 }
167 var names []string
168 for _, u := range set {
169 names = append(names, u.Username)
170 }
171 sort.Strings(names)
172 dns := make([]string, len(names))
173 for i, n := range names {
174 dns[i] = d.userDN(n)
175 }
176 for id := range set {
177 memberOf[id] = append(memberOf[id], d.groupDN(g.Name))
178 }
179 attrs := map[string][]string{
180 "objectclass": {"groupOfNames", "posixGroup"},
181 "cn": {g.Name},
182 "member": dns,
183 "memberuid": names,
184 "gidnumber": {strconv.Itoa(g.GIDNumber)},
185 "entryuuid": {g.ID},
186 }
187 if g.Description != "" {
188 attrs["description"] = []string{g.Description}
189 }
190 groupEntries = append(groupEntries, &entry{dn: d.groupDN(g.Name), attrs: attrs})
191 }
192 var userEntries []*entry
193 for _, u := range users {
194 if _, ok := publishable[u.ID]; !ok {
195 continue
196 }
197 cn := u.DisplayName
198 if cn == "" {
199 cn = u.Username
200 }
201 groupsOf := memberOf[u.ID]
202 sort.Strings(groupsOf)
203 attrs := map[string][]string{
204 "objectclass": {"inetOrgPerson", "organizationalPerson", "person", "posixAccount"},
205 "uid": {u.Username},
206 "cn": {cn},
207 "sn": {surname(u.DisplayName, u.Username)},
208 "uidnumber": {strconv.Itoa(u.UIDNumber)},
209 "gidnumber": {strconv.Itoa(primary.GIDNumber)},
210 "homedirectory": {u.HomeDirectory()},
211 "memberof": groupsOf,
212 "entryuuid": {u.ID},
213 }
214 if u.DisplayName != "" {
215 attrs["displayname"] = []string{u.DisplayName}
216 }
217 if k := keysOf[u.ID]; len(k) > 0 {
218 attrs["objectclass"] = append(attrs["objectclass"], "ldapPublicKey")
219 attrs["sshpublickey"] = k
220 }
221 userEntries = append(userEntries, &entry{dn: d.userDN(u.Username), attrs: attrs})
222 }
223 s := &snapshot{byDN: map[string]*entry{}}
224 s.entries = append(s.entries, d.fixedEntries()...)
225 s.entries = append(s.entries, userEntries...)
226 s.entries = append(s.entries, groupEntries...)
227 for _, e := range s.entries {
228 s.byDN[e.dn] = e
229 }
230 return s, nil
231}
232
233func (d *directory) publishable(ctx context.Context, username string) (storage.User, bool, error) {
234 u, err := d.store.UserByUsername(ctx, username)
235 switch {
236 case err == nil:
237 case storageAbsent(err):
238 return storage.User{}, false, nil
239 default:
240 return storage.User{}, false, err
241 }
242 if u.Disabled {
243 return storage.User{}, false, nil
244 }
245 if _, err := d.store.GroupByName(ctx, d.primaryGroup); err != nil {
246 if storageAbsent(err) {
247 return storage.User{}, false, nil
248 }
249 return storage.User{}, false, err
250 }
251 return u, true, nil
252}
253
254func storageAbsent(err error) bool {
255 return errors.Is(err, storage.ErrNotFound) || errors.Is(err, storage.ErrInvalid)
256}