From b933599075bfa0378938d71c67df4c83aeb68506 Mon Sep 17 00:00:00 2001 From: David Arnold Date: Wed, 3 Jun 2020 12:47:21 -0500 Subject: [PATCH] WIP: Implement Setup --- ldap.go | 27 +++++- setup.go | 272 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 284 insertions(+), 15 deletions(-) diff --git a/ldap.go b/ldap.go index 5cb1b39..b1c6ba6 100644 --- a/ldap.go +++ b/ldap.go @@ -21,7 +21,7 @@ import ( "github.com/coredns/coredns/plugin/pkg/fall" "github.com/miekg/dns" - "gopkg.in/ldap.v2" + "gopkg.in/ldap.v3" ) // Ldap is an ldap plugin to serve zone entries from a ldap backend. @@ -30,15 +30,38 @@ type Ldap struct { Fall fall.F Zones []string Client *ldap.Client + clientConfig } +// New returns an initialized Ldap with defaults. +func New(zones []string) *Ldap { + k := new(Ldap) + k.Zones = zones + return k +} + + var ( errNoItems = errors.New("no items found") errNsNotExposed = errors.New("namespace is not exposed") errInvalidRequest = errors.New("invalid query name") ) +func (l *Ldap) InitClient() (err error) { + l, err := Dial("tcp", fmt.Sprintf("%s:%d", "ldap.example.com", 389)) + if err != nil { + log.Fatal(err) + } + defer l.Close() + + // Reconnect with TLS + err = l.StartTLS(&tls.Config{InsecureSkipVerify: true}) + if err != nil { + log.Fatal(err) + } +} + // Services implements the ServiceBackend interface. func (l *Ldap) Services(ctx context.Context, state request.Request, exact bool, opt plugin.Options) (services []msg.Service, err error) { services, err = l.Records(ctx, state, exact) @@ -77,4 +100,4 @@ func (l *Ldap) Records(ctx context.Context, state request.Request, exact bool) ( } segments := strings.Split(msg.Path(name, l.PathPrefix), "/") return l.loopNodes(r.Kvs, segments, star, state.QType()) -} \ No newline at end of file +} diff --git a/setup.go b/setup.go index d1455d0..734cbe3 100644 --- a/setup.go +++ b/setup.go @@ -1,40 +1,286 @@ package ldap import ( + "sync" + "context" "github.com/coredns/coredns/core/dnsserver" "github.com/coredns/coredns/plugin" "github.com/coredns/coredns/plugin/metrics" + "github.com/coredns/coredns/plugin/pkg/dnsutil" + clog "github.com/coredns/coredns/plugin/pkg/log" + "github.com/coredns/coredns/plugin/pkg/upstream" "github.com/caddyserver/caddy" ) + +const pluginName = "ldap" + +// Define log to be a logger with the plugin name in it. +var log = clog.NewWithPlugin(pluginName) + // init registers this plugin. -func init() { plugin.Register("ldap", setup) } +func init() { plugin.Register(pluginName, setup) } // setup is the function that gets called when the config parser see the token "ldap". Setup is responsible // for parsing any extra options the ldap plugin may have. The first token this function sees is "ldap". func setup(c *caddy.Controller) error { - c.Next() // Ignore "ldap" and give us the next token. - if c.NextArg() { - // If there was another token, return an error, because we don't have any configuration. - // Any errors returned from this setup function should be wrapped with plugin.Error, so we - // can present a slightly nicer error message to the user. - return plugin.Error("ldap", c.ArgErr()) + + // parse corefile config + l, err := ldapParse(c) + if err != nil { + return plugin.Error(pluginName, err) } - // Add a startup function that will -- after all plugins have been loaded -- check if the - // prometheus plugin has been used - if so we will export metrics. We can only register - // this metric once, hence the "once.Do". + err = l.InitLdapCache(context.Background()) + if err != nil { + return plugin.Error(pluginName, err) + } + + l.RegisterLdapCache(c) + + // add prometheus metrics on startup c.OnStartup(func() error { - once.Do(func() { metrics.MustRegister(c, requestCount) }) + // add plugin-global metric once + once.Do(func() { + metrics.MustRegister(c, requestCount) + }) return nil }) // Add the Plugin to CoreDNS, so Servers can use it in their plugin chain. dnsserver.GetConfig(c).AddPlugin(func(next plugin.Handler) plugin.Handler { - return Ldap{Next: next} + l.Next = next + return l }) - // All OK, return a nil error. return nil } + +var once sync.Once + + +// RegisterLdapCache registers LdapCache start and stop functions with Caddy +func (l *Ldap) RegisterLdapCache(c *caddy.Controller) { + c.OnStartup(func() error { + go l.APIConn.Run() + + timeout := time.After(5 * time.Second) + ticker := time.NewTicker(100 * time.Millisecond) + for { + select { + case <-ticker.C: + if k.APIConn.HasSynced() { + return nil + } + case <-timeout: + return nil + } + } + }) + + c.OnShutdown(func() error { + return l.APIConn.Stop() + }) +} + + +func ldapParse(c *caddy.Controller) (*Ldap, error) { + var ( + ldap *Ldap + err error + ) + + i := 0 + for c.Next() { + if i > 0 { + return nil, plugin.ErrOnce + } + i++ + + l, err = ParseStanza(c) + if err != nil { + return ldap, err + } + } + return ldap, nil +} + + +// ParseStanza parses a ldap stanza +func ParseStanza(c *caddy.Controller) (*Ldap, error) { + + ldap := New([]string{""}) + ldap.autoPathSearch = searchFromResolvConf() + + opts := dnsControlOpts{ + initEndpointsCache: true, + ignoreEmptyService: false, + } + ldap.opts = opts + + zones := c.RemainingArgs() + + if len(zones) != 0 { + ldap.Zones = zones + for i := 0; i < len(ldap.Zones); i++ { + ldap.Zones[i] = plugin.Host(ldap.Zones[i]).Normalize() + } + } else { + ldap.Zones = make([]string, len(c.ServerBlockKeys)) + for i := 0; i < len(c.ServerBlockKeys); i++ { + ldap.Zones[i] = plugin.Host(c.ServerBlockKeys[i]).Normalize() + } + } + + ldap.primaryZoneIndex = -1 + for i, z := range ldap.Zones { + if dnsutil.IsReverse(z) > 0 { + continue + } + ldap.primaryZoneIndex = i + break + } + + if ldap.primaryZoneIndex == -1 { + return nil, errors.New("non-reverse zone name must be used") + } + + ldap.Upstream = upstream.New() + + for c.NextBlock() { + switch c.Val() { + // RFC 4516 URL + case "endpoint_pod_names": + args := c.RemainingArgs() + if len(args) > 0 { + return nil, c.ArgErr() + } + ldap.endpointNameMode = true + continue + case "pods": + args := c.RemainingArgs() + if len(args) == 1 { + switch args[0] { + case podModeDisabled, podModeInsecure, podModeVerified: + ldap.podMode = args[0] + default: + return nil, fmt.Errorf("wrong value for pods: %s, must be one of: disabled, verified, insecure", args[0]) + } + continue + } + return nil, c.ArgErr() + case "namespaces": + args := c.RemainingArgs() + if len(args) > 0 { + for _, a := range args { + ldap.Namespaces[a] = struct{}{} + } + continue + } + return nil, c.ArgErr() + case "endpoint": + args := c.RemainingArgs() + if len(args) > 0 { + // Multiple endpoints are deprecated but still could be specified, + // only the first one be used, though + ldap.APIServerList = args + if len(args) > 1 { + log.Warningf("Multiple endpoints have been deprecated, only the first specified endpoint '%s' is used", args[0]) + } + continue + } + return nil, c.ArgErr() + case "tls": // cert key cacertfile + args := c.RemainingArgs() + if len(args) == 3 { + ldap.APIClientCert, ldap.APIClientKey, ldap.APICertAuth = args[0], args[1], args[2] + continue + } + return nil, c.ArgErr() + case "labels": + args := c.RemainingArgs() + if len(args) > 0 { + labelSelectorString := strings.Join(args, " ") + ls, err := meta.ParseToLabelSelector(labelSelectorString) + if err != nil { + return nil, fmt.Errorf("unable to parse label selector value: '%v': %v", labelSelectorString, err) + } + ldap.opts.labelSelector = ls + continue + } + return nil, c.ArgErr() + case "namespace_labels": + args := c.RemainingArgs() + if len(args) > 0 { + namespaceLabelSelectorString := strings.Join(args, " ") + nls, err := meta.ParseToLabelSelector(namespaceLabelSelectorString) + if err != nil { + return nil, fmt.Errorf("unable to parse namespace_label selector value: '%v': %v", namespaceLabelSelectorString, err) + } + ldap.opts.namespaceLabelSelector = nls + continue + } + return nil, c.ArgErr() + case "fallthrough": + ldap.Fall.SetZonesFromArgs(c.RemainingArgs()) + case "ttl": + args := c.RemainingArgs() + if len(args) == 0 { + return nil, c.ArgErr() + } + t, err := strconv.Atoi(args[0]) + if err != nil { + return nil, err + } + if t < 0 || t > 3600 { + return nil, c.Errf("ttl must be in range [0, 3600]: %d", t) + } + ldap.ttl = uint32(t) + case "transfer": + tos, froms, err := parse.Transfer(c, false) + if err != nil { + return nil, err + } + if len(froms) != 0 { + return nil, c.Errf("transfer from is not supported with this plugin") + } + ldap.TransferTo = tos + case "noendpoints": + if len(c.RemainingArgs()) != 0 { + return nil, c.ArgErr() + } + ldap.opts.initEndpointsCache = false + case "ignore": + args := c.RemainingArgs() + if len(args) > 0 { + ignore := args[0] + if ignore == "empty_service" { + ldap.opts.ignoreEmptyService = true + continue + } else { + return nil, fmt.Errorf("unable to parse ignore value: '%v'", ignore) + } + } + case "kubeconfig": + args := c.RemainingArgs() + if len(args) == 2 { + config := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: args[0]}, + &clientcmd.ConfigOverrides{CurrentContext: args[1]}, + ) + ldap.ClientConfig = config + continue + } + return nil, c.ArgErr() + default: + return nil, c.Errf("unknown property '%s'", c.Val()) + } + } + + if len(ldap.Namespaces) != 0 && ldap.opts.namespaceLabelSelector != nil { + return nil, c.Errf("namespaces and namespace_labels cannot both be set") + } + + return ldap, nil +} \ No newline at end of file