package ldap import ( "context" "errors" "sort" "strconv" "strings" "tug/internal/storage" ) var attributeNames = []struct{ key, name string }{ {"objectclass", "objectClass"}, {"dc", "dc"}, {"o", "o"}, {"ou", "ou"}, {"uid", "uid"}, {"cn", "cn"}, {"sn", "sn"}, {"displayname", "displayName"}, {"uidnumber", "uidNumber"}, {"gidnumber", "gidNumber"}, {"homedirectory", "homeDirectory"}, {"memberof", "memberOf"}, {"description", "description"}, {"member", "member"}, {"memberuid", "memberUid"}, {"sshpublickey", "sshPublicKey"}, {"entryuuid", "entryUUID"}, {"namingcontexts", "namingContexts"}, {"supportedldapversion", "supportedLDAPVersion"}, {"supportedcontrol", "supportedControl"}, } var operationalAttributes = map[string]bool{ "entryuuid": true, "namingcontexts": true, "supportedldapversion": true, "supportedcontrol": true, } const pagedResultsOID = "1.2.840.113556.1.4.319" type directory struct { store *storage.Store base string baseRDN rdn primaryGroup string } type snapshot struct { entries []*entry byDN map[string]*entry } func newDirectory(store *storage.Store, baseDN, primaryGroup string) (*directory, error) { rdns, err := parseDN(baseDN) if err != nil || len(rdns) == 0 { return nil, errDN } for _, r := range rdns { if r.typ != "dc" && r.typ != "o" && r.typ != "ou" { return nil, errDN } } return &directory{store: store, base: joinDN(rdns), baseRDN: rdns[0], primaryGroup: primaryGroup}, nil } func (d *directory) userDN(username string) string { return "uid=" + username + ",ou=people," + d.base } func (d *directory) groupDN(name string) string { return "cn=" + name + ",ou=groups," + d.base } func (d *directory) peopleDN() string { return "ou=people," + d.base } func (d *directory) groupsDN() string { return "ou=groups," + d.base } func (d *directory) rootDSE() *entry { return &entry{dn: "", attrs: map[string][]string{ "objectclass": {"top"}, "namingcontexts": {d.base}, "supportedldapversion": {"3"}, "supportedcontrol": {pagedResultsOID}, }} } func (d *directory) fixedEntries() []*entry { baseClass := map[string]string{"dc": "domain", "o": "organization", "ou": "organizationalUnit"}[d.baseRDN.typ] return []*entry{ {dn: d.base, attrs: map[string][]string{"objectclass": {"top", baseClass}, d.baseRDN.typ: {d.baseRDN.val}}}, {dn: d.peopleDN(), attrs: map[string][]string{"objectclass": {"top", "organizationalUnit"}, "ou": {"people"}}}, {dn: d.groupsDN(), attrs: map[string][]string{"objectclass": {"top", "organizationalUnit"}, "ou": {"groups"}}}, } } func surname(displayName, username string) string { fields := strings.Fields(displayName) if len(fields) < 2 { return username } return fields[len(fields)-1] } func (d *directory) snapshot(ctx context.Context) (*snapshot, error) { users, err := d.store.ListUsers(ctx) if err != nil { return nil, err } groups, err := d.store.ListGroups(ctx) if err != nil { return nil, err } keys, err := d.store.ListSSHKeys(ctx) if err != nil { return nil, err } keysOf := map[string][]string{} for _, k := range keys { keysOf[k.UserID] = append(keysOf[k.UserID], k.Authorized()) } var primary *storage.Group for i := range groups { if groups[i].Name == d.primaryGroup { primary = &groups[i] } } publishable := map[string]storage.User{} if primary != nil { for _, u := range users { if !u.Disabled { publishable[u.ID] = u } } } members := map[string]map[string]storage.User{} for _, g := range groups { set := map[string]storage.User{} if primary != nil && g.ID == primary.ID { for id, u := range publishable { set[id] = u } } in, err := d.store.UsersInGroup(ctx, g.ID) if err != nil { return nil, err } for _, u := range in { if p, ok := publishable[u.ID]; ok { set[u.ID] = p } } if len(set) > 0 { members[g.ID] = set } } memberOf := map[string][]string{} var groupEntries []*entry for _, g := range groups { set, ok := members[g.ID] if !ok { continue } var names []string for _, u := range set { names = append(names, u.Username) } sort.Strings(names) dns := make([]string, len(names)) for i, n := range names { dns[i] = d.userDN(n) } for id := range set { memberOf[id] = append(memberOf[id], d.groupDN(g.Name)) } attrs := map[string][]string{ "objectclass": {"groupOfNames", "posixGroup"}, "cn": {g.Name}, "member": dns, "memberuid": names, "gidnumber": {strconv.Itoa(g.GIDNumber)}, "entryuuid": {g.ID}, } if g.Description != "" { attrs["description"] = []string{g.Description} } groupEntries = append(groupEntries, &entry{dn: d.groupDN(g.Name), attrs: attrs}) } var userEntries []*entry for _, u := range users { if _, ok := publishable[u.ID]; !ok { continue } cn := u.DisplayName if cn == "" { cn = u.Username } groupsOf := memberOf[u.ID] sort.Strings(groupsOf) attrs := map[string][]string{ "objectclass": {"inetOrgPerson", "organizationalPerson", "person", "posixAccount"}, "uid": {u.Username}, "cn": {cn}, "sn": {surname(u.DisplayName, u.Username)}, "uidnumber": {strconv.Itoa(u.UIDNumber)}, "gidnumber": {strconv.Itoa(primary.GIDNumber)}, "homedirectory": {u.HomeDirectory()}, "memberof": groupsOf, "entryuuid": {u.ID}, } if u.DisplayName != "" { attrs["displayname"] = []string{u.DisplayName} } if k := keysOf[u.ID]; len(k) > 0 { attrs["objectclass"] = append(attrs["objectclass"], "ldapPublicKey") attrs["sshpublickey"] = k } userEntries = append(userEntries, &entry{dn: d.userDN(u.Username), attrs: attrs}) } s := &snapshot{byDN: map[string]*entry{}} s.entries = append(s.entries, d.fixedEntries()...) s.entries = append(s.entries, userEntries...) s.entries = append(s.entries, groupEntries...) for _, e := range s.entries { s.byDN[e.dn] = e } return s, nil } func (d *directory) publishable(ctx context.Context, username string) (storage.User, bool, error) { u, err := d.store.UserByUsername(ctx, username) switch { case err == nil: case storageAbsent(err): return storage.User{}, false, nil default: return storage.User{}, false, err } if u.Disabled { return storage.User{}, false, nil } if _, err := d.store.GroupByName(ctx, d.primaryGroup); err != nil { if storageAbsent(err) { return storage.User{}, false, nil } return storage.User{}, false, err } return u, true, nil } func storageAbsent(err error) bool { return errors.Is(err, storage.ErrNotFound) || errors.Is(err, storage.ErrInvalid) }