451 lines
14 KiB
Go
451 lines
14 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/rs/zerolog"
|
|
"go.opentelemetry.io/otel/trace"
|
|
|
|
"github.com/GeertJohan/yubigo"
|
|
"github.com/glauth/glauth/v2/internal/monitoring"
|
|
"github.com/glauth/glauth/v2/pkg/config"
|
|
"github.com/glauth/glauth/v2/pkg/stats"
|
|
"github.com/glauth/ldap"
|
|
)
|
|
|
|
type configHandler struct {
|
|
backend config.Backend
|
|
log *zerolog.Logger
|
|
cfg *config.Config
|
|
yubikeyAuth *yubigo.YubiAuth
|
|
ldohelper LDAPOpsHelper
|
|
attmatcher *regexp.Regexp
|
|
|
|
monitor monitoring.MonitorInterface
|
|
tracer trace.Tracer
|
|
}
|
|
|
|
// NewConfigHandler creates a new config backed handler
|
|
func NewConfigHandler(opts ...Option) Handler {
|
|
options := newOptions(opts...)
|
|
|
|
handler := configHandler{
|
|
backend: options.Backend,
|
|
log: options.Logger,
|
|
cfg: options.Config, // TODO only used to access Users and Groups, move that to dedicated options
|
|
yubikeyAuth: options.YubiAuth,
|
|
ldohelper: options.LDAPHelper,
|
|
attmatcher: configattributematcher,
|
|
monitor: options.Monitor,
|
|
tracer: options.Tracer,
|
|
}
|
|
return handler
|
|
}
|
|
|
|
func (h configHandler) GetBackend() config.Backend {
|
|
return h.backend
|
|
}
|
|
func (h configHandler) GetLog() *zerolog.Logger {
|
|
return h.log
|
|
}
|
|
func (h configHandler) GetCfg() *config.Config {
|
|
return h.cfg
|
|
}
|
|
func (h configHandler) GetYubikeyAuth() *yubigo.YubiAuth {
|
|
return h.yubikeyAuth
|
|
}
|
|
|
|
// Bind implements a bind request against the config file
|
|
func (h configHandler) Bind(bindDN, bindSimplePw string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
|
|
ctx, span := h.tracer.Start(context.Background(), "handler.configHandler.Bind")
|
|
defer span.End()
|
|
|
|
start := time.Now()
|
|
defer func() {
|
|
h.monitor.SetResponseTimeMetric(
|
|
map[string]string{"operation": "bind", "status": fmt.Sprintf("%v", result)},
|
|
time.Since(start).Seconds(),
|
|
)
|
|
}()
|
|
return h.ldohelper.Bind(ctx, h, bindDN, bindSimplePw, conn)
|
|
}
|
|
|
|
// Search implements a search request against the config file
|
|
func (h configHandler) Search(bindDN string, searchReq ldap.SearchRequest, conn net.Conn) (result ldap.ServerSearchResult, err error) {
|
|
ctx, span := h.tracer.Start(context.Background(), "handler.configHandler.Search")
|
|
defer span.End()
|
|
|
|
start := time.Now()
|
|
defer func() {
|
|
h.monitor.SetResponseTimeMetric(
|
|
map[string]string{"operation": "search", "status": fmt.Sprintf("%v", result.ResultCode)},
|
|
time.Since(start).Seconds(),
|
|
)
|
|
}()
|
|
return h.ldohelper.Search(ctx, h, bindDN, searchReq, conn)
|
|
}
|
|
|
|
// Add is not supported for a static config file
|
|
func (h configHandler) Add(boundDN string, req ldap.AddRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
|
|
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Add")
|
|
defer span.End()
|
|
|
|
start := time.Now()
|
|
defer func() {
|
|
h.monitor.SetResponseTimeMetric(
|
|
map[string]string{"operation": "add", "status": fmt.Sprintf("%v", result)},
|
|
time.Since(start).Seconds(),
|
|
)
|
|
}()
|
|
return ldap.LDAPResultInsufficientAccessRights, nil
|
|
}
|
|
|
|
// Modify is not supported for a static config file
|
|
func (h configHandler) Modify(boundDN string, req ldap.ModifyRequest, conn net.Conn) (result ldap.LDAPResultCode, err error) {
|
|
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Modify")
|
|
defer span.End()
|
|
|
|
start := time.Now()
|
|
defer func() {
|
|
h.monitor.SetResponseTimeMetric(
|
|
map[string]string{"operation": "modify", "status": fmt.Sprintf("%v", result)},
|
|
time.Since(start).Seconds(),
|
|
)
|
|
}()
|
|
return ldap.LDAPResultInsufficientAccessRights, nil
|
|
}
|
|
|
|
// Delete is not supported for a static config file
|
|
func (h configHandler) Delete(boundDN string, deleteDN string, conn net.Conn) (result ldap.LDAPResultCode, err error) {
|
|
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Delete")
|
|
defer span.End()
|
|
|
|
start := time.Now()
|
|
defer func() {
|
|
h.monitor.SetResponseTimeMetric(
|
|
map[string]string{"operation": "delete", "status": fmt.Sprintf("%v", result)},
|
|
time.Since(start).Seconds(),
|
|
)
|
|
}()
|
|
return ldap.LDAPResultInsufficientAccessRights, nil
|
|
}
|
|
|
|
func (h configHandler) FindUser(ctx context.Context, userName string, searchByUPN bool) (f bool, u config.User, err error) {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindUser")
|
|
defer span.End()
|
|
user := config.User{}
|
|
found := false
|
|
|
|
for _, u := range h.cfg.Users {
|
|
if searchByUPN {
|
|
if strings.EqualFold(u.Mail, userName) {
|
|
found = true
|
|
user = u
|
|
}
|
|
} else {
|
|
if strings.EqualFold(u.Name, userName) {
|
|
found = true
|
|
user = u
|
|
}
|
|
}
|
|
}
|
|
|
|
return found, user, nil
|
|
}
|
|
|
|
func (h configHandler) FindGroup(ctx context.Context, groupName string) (f bool, g config.Group, err error) {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindGroup")
|
|
defer span.End()
|
|
// TODO Does g get erased, and above does u get erased?
|
|
// TODO and what about f?
|
|
group := config.Group{}
|
|
found := false
|
|
for _, g := range h.cfg.Groups {
|
|
if strings.EqualFold(g.Name, groupName) {
|
|
found = true
|
|
group = g
|
|
}
|
|
}
|
|
return found, group, nil
|
|
}
|
|
|
|
func (h configHandler) FindPosixAccounts(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error) {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindPosixAccounts")
|
|
defer span.End()
|
|
|
|
entries := []*ldap.Entry{}
|
|
|
|
for _, u := range h.cfg.Users {
|
|
attrs := []*ldap.EntryAttribute{}
|
|
for _, nameAttr := range h.backend.NameFormatAsArray {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: nameAttr, Values: []string{u.Name}})
|
|
}
|
|
|
|
if len(u.GivenName) > 0 {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "givenName", Values: []string{u.GivenName}})
|
|
}
|
|
|
|
if len(u.SN) > 0 {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "sn", Values: []string{u.SN}})
|
|
}
|
|
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "ou", Values: []string{h.getGroupName(ctx, u.PrimaryGroup)}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "uidNumber", Values: []string{fmt.Sprintf("%d", u.UIDNumber)}})
|
|
|
|
if u.Disabled {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"inactive"}})
|
|
} else {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "accountStatus", Values: []string{"active"}})
|
|
}
|
|
|
|
if len(u.Mail) > 0 {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "mail", Values: []string{u.Mail}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "userPrincipalName", Values: []string{u.Mail}})
|
|
}
|
|
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixAccount", "shadowAccount"}})
|
|
|
|
if len(u.LoginShell) > 0 {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "loginShell", Values: []string{u.LoginShell}})
|
|
} else {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "loginShell", Values: []string{"/bin/bash"}})
|
|
}
|
|
|
|
if len(u.Homedir) > 0 {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "homeDirectory", Values: []string{u.Homedir}})
|
|
} else {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "homeDirectory", Values: []string{"/home/" + u.Name}})
|
|
}
|
|
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", u.Name)}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "gecos", Values: []string{fmt.Sprintf("%s", u.Name)}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", u.PrimaryGroup)}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberOf", Values: h.getGroupDNs(ctx, append(u.OtherGroups, u.PrimaryGroup))})
|
|
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowExpire", Values: []string{"-1"}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowFlag", Values: []string{"134538308"}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowInactive", Values: []string{"-1"}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowLastChange", Values: []string{"11000"}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowMax", Values: []string{"99999"}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowMin", Values: []string{"-1"}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "shadowWarning", Values: []string{"7"}})
|
|
|
|
if len(u.SSHKeys) > 0 {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: h.backend.SSHKeyAttr, Values: u.SSHKeys})
|
|
}
|
|
|
|
if len(u.CustomAttrs) > 0 {
|
|
for key, attr := range u.CustomAttrs {
|
|
switch typedattr := attr.(type) {
|
|
case []interface{}:
|
|
var values []string
|
|
for _, v := range typedattr {
|
|
switch typedvalue := v.(type) {
|
|
case string:
|
|
values = append(values, MaybeDecode(typedvalue))
|
|
default:
|
|
values = append(values, MaybeDecode(fmt.Sprintf("%v", typedvalue)))
|
|
}
|
|
}
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: key, Values: values})
|
|
default:
|
|
h.log.Warn().Str("key", key).Interface("value", attr).Msg("Unable to map custom attribute")
|
|
}
|
|
}
|
|
}
|
|
var dn string
|
|
if hierarchy == "" {
|
|
dn = fmt.Sprintf("%s=%s,%s=%s,%s", h.backend.NameFormatAsArray[0], u.Name, h.backend.GroupFormatAsArray[0], h.getGroupName(ctx, u.PrimaryGroup), h.backend.BaseDN)
|
|
} else {
|
|
dn = fmt.Sprintf("%s=%s,%s=%s,%s,%s", h.backend.NameFormatAsArray[0], u.Name, h.backend.GroupFormatAsArray[0], h.getGroupName(ctx, u.PrimaryGroup), hierarchy, h.backend.BaseDN)
|
|
}
|
|
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
func (h configHandler) FindPosixGroups(ctx context.Context, hierarchy string) (entrylist []*ldap.Entry, err error) {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.FindPosixGroups")
|
|
defer span.End()
|
|
|
|
asGroupOfUniqueNames := hierarchy == "ou=groups"
|
|
|
|
entries := []*ldap.Entry{}
|
|
|
|
for _, g := range h.cfg.Groups {
|
|
attrs := []*ldap.EntryAttribute{}
|
|
for _, groupAttr := range h.backend.GroupFormatAsArray {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: groupAttr, Values: []string{g.Name}})
|
|
}
|
|
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "description", Values: []string{fmt.Sprintf("%s", g.Name)}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "gidNumber", Values: []string{fmt.Sprintf("%d", g.GIDNumber)}})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "uniqueMember", Values: h.getGroupMemberDNs(ctx, g.GIDNumber)})
|
|
if asGroupOfUniqueNames {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"groupOfUniqueNames", "top"}})
|
|
} else {
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "memberUid", Values: h.getGroupMemberIDs(ctx, g.GIDNumber)})
|
|
attrs = append(attrs, &ldap.EntryAttribute{Name: "objectClass", Values: []string{"posixGroup", "top"}})
|
|
}
|
|
dn := fmt.Sprintf("%s=%s,%s,%s", h.backend.GroupFormatAsArray[0], g.Name, hierarchy, h.backend.BaseDN)
|
|
entries = append(entries, &ldap.Entry{DN: dn, Attributes: attrs})
|
|
}
|
|
|
|
return entries, nil
|
|
}
|
|
|
|
// Close does not actually close anything, because the config data is kept in memory
|
|
func (h configHandler) Close(boundDn string, conn net.Conn) error {
|
|
_, span := h.tracer.Start(context.Background(), "handler.configHandler.Close")
|
|
defer span.End()
|
|
|
|
stats.Frontend.Add("closes", 1)
|
|
return nil
|
|
}
|
|
|
|
func (h configHandler) getGroupMemberDNs(ctx context.Context, gid int) []string {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupMemberDNs")
|
|
defer span.End()
|
|
|
|
var insertOuUsers string
|
|
if h.cfg.Behaviors.LegacyVersion > 0 && h.cfg.Behaviors.LegacyVersion <= 20100 {
|
|
insertOuUsers = ""
|
|
} else {
|
|
insertOuUsers = ",ou=users"
|
|
}
|
|
members := make(map[string]bool)
|
|
for _, u := range h.cfg.Users {
|
|
if u.PrimaryGroup == gid {
|
|
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormatAsArray[0], u.Name, h.backend.GroupFormatAsArray[0], h.getGroupName(ctx, u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
|
|
members[dn] = true
|
|
} else {
|
|
for _, othergid := range u.OtherGroups {
|
|
if othergid == gid {
|
|
dn := fmt.Sprintf("%s=%s,%s=%s%s,%s", h.backend.NameFormatAsArray[0], u.Name, h.backend.GroupFormatAsArray[0], h.getGroupName(ctx, u.PrimaryGroup), insertOuUsers, h.backend.BaseDN)
|
|
members[dn] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, g := range h.cfg.Groups {
|
|
if gid == g.GIDNumber {
|
|
for _, includegroupid := range g.IncludeGroups {
|
|
if includegroupid != gid {
|
|
includegroupmembers := h.getGroupMemberDNs(ctx, includegroupid)
|
|
|
|
for _, includegroupmember := range includegroupmembers {
|
|
members[includegroupmember] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m := []string{}
|
|
for k := range members {
|
|
m = append(m, k)
|
|
}
|
|
|
|
sort.Strings(m)
|
|
|
|
return m
|
|
}
|
|
|
|
func (h configHandler) getGroupMemberIDs(ctx context.Context, gid int) []string {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupMemberIDs")
|
|
defer span.End()
|
|
|
|
members := make(map[string]bool)
|
|
for _, u := range h.cfg.Users {
|
|
if u.PrimaryGroup == gid {
|
|
members[u.Name] = true
|
|
} else {
|
|
for _, othergid := range u.OtherGroups {
|
|
if othergid == gid {
|
|
members[u.Name] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, g := range h.cfg.Groups {
|
|
if gid == g.GIDNumber {
|
|
for _, includegroupid := range g.IncludeGroups {
|
|
if includegroupid == gid {
|
|
h.log.Warn().Int("groupid", includegroupid).Msg("Ignoring myself as included group")
|
|
} else {
|
|
includegroupmemberids := h.getGroupMemberIDs(ctx, includegroupid)
|
|
|
|
for _, includegroupmemberid := range includegroupmemberids {
|
|
members[includegroupmemberid] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
m := []string{}
|
|
for k := range members {
|
|
m = append(m, k)
|
|
}
|
|
|
|
sort.Strings(m)
|
|
|
|
return m
|
|
}
|
|
|
|
// Converts an array of GUIDs into an array of DNs
|
|
func (h configHandler) getGroupDNs(ctx context.Context, gids []int) []string {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupDNs")
|
|
defer span.End()
|
|
|
|
groups := make(map[string]bool)
|
|
for _, gid := range gids {
|
|
for _, g := range h.cfg.Groups {
|
|
if g.GIDNumber == gid {
|
|
dn := fmt.Sprintf("%s=%s,ou=groups,%s", h.backend.GroupFormatAsArray[0], g.Name, h.backend.BaseDN)
|
|
groups[dn] = true
|
|
}
|
|
|
|
for _, includegroupid := range g.IncludeGroups {
|
|
if includegroupid == gid && g.GIDNumber != gid {
|
|
includegroupdns := h.getGroupDNs(ctx, []int{g.GIDNumber})
|
|
|
|
for _, includegroupdn := range includegroupdns {
|
|
groups[includegroupdn] = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
g := []string{}
|
|
for k := range groups {
|
|
g = append(g, k)
|
|
}
|
|
|
|
sort.Strings(g)
|
|
|
|
return g
|
|
}
|
|
|
|
func (h configHandler) getGroupName(ctx context.Context, gid int) string {
|
|
ctx, span := h.tracer.Start(ctx, "handler.configHandler.getGroupName")
|
|
defer span.End()
|
|
|
|
for _, g := range h.cfg.Groups {
|
|
if g.GIDNumber == gid {
|
|
return g.Name
|
|
}
|
|
}
|
|
return ""
|
|
}
|