Raw
  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}