support etcd credentials in etcd plugin (#2442)

* support etcd credentials in etcd plugin

fixes #2441

* try to fix cleanup of authentication
This commit is contained in:
Christophe de Carvalho 2019-02-01 16:30:53 +01:00 committed by Miek Gieben
parent b455f86824
commit d878eeebbb
6 changed files with 156 additions and 14 deletions

View file

@ -31,7 +31,8 @@ etcd [ZONES...] {
fallthrough [ZONES...] fallthrough [ZONES...]
path PATH path PATH
endpoint ENDPOINT... endpoint ENDPOINT...
upstream credentials USERNAME PASSWORD
upstream [ADDRESS...]
tls CERT KEY CACERT tls CERT KEY CACERT
} }
~~~ ~~~
@ -42,8 +43,12 @@ etcd [ZONES...] {
queries for those zones will be subject to fallthrough. queries for those zones will be subject to fallthrough.
* **PATH** the path inside etcd. Defaults to "/skydns". * **PATH** the path inside etcd. Defaults to "/skydns".
* **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2379". * **ENDPOINT** the etcd endpoints. Defaults to "http://localhost:2379".
* `upstream` resolve names found in etcd (think CNAMEs) If you want CoreDNS to act as a proxy for clients, * `credentials` is used to set the **USERNAME** and **PASSWORD** for accessing the etcd cluster.
you'll need to add the forward plugin. CoreDNS will resolve CNAMEs against itself. * `upstream` upstream resolvers to be used resolve external names found in etcd (think CNAMEs)
pointing to external names. If you want CoreDNS to act as a proxy for clients, you'll need to add
the proxy plugin. If no **ADDRESS** is given, CoreDNS will resolve CNAMEs against itself.
**ADDRESS** can be an IP address, and IP:port or a string pointing to a file that is structured
as /etc/resolv.conf.
* `tls` followed by: * `tls` followed by:
* no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed * no arguments, if the server certificate is signed by a system-installed CA and no client cert is needed
@ -205,4 +210,4 @@ If you query the zone name for `TXT` now, you will get the following response:
~~~ sh ~~~ sh
% dig +short skydns.local TXT @localhost % dig +short skydns.local TXT @localhost
"this is a random text message." "this is a random text message."
~~~ ~~~

View file

@ -289,7 +289,7 @@ func newEtcdPlugin() *Etcd {
endpoints := []string{"http://localhost:2379"} endpoints := []string{"http://localhost:2379"}
tlsc, _ := tls.NewTLSConfigFromArgs() tlsc, _ := tls.NewTLSConfigFromArgs()
client, _ := newEtcdClient(endpoints, tlsc) client, _ := newEtcdClient(endpoints, tlsc, "", "")
return &Etcd{ return &Etcd{
Upstream: upstream.New(), Upstream: upstream.New(),

View file

@ -48,6 +48,8 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) {
tlsConfig *tls.Config tlsConfig *tls.Config
err error err error
endpoints = []string{defaultEndpoint} endpoints = []string{defaultEndpoint}
username string
password string
) )
for c.Next() { for c.Next() {
etc.Zones = c.RemainingArgs() etc.Zones = c.RemainingArgs()
@ -89,6 +91,15 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) {
if err != nil { if err != nil {
return &Etcd{}, err return &Etcd{}, err
} }
case "credentials":
args := c.RemainingArgs()
if len(args) == 0 {
return &Etcd{}, c.ArgErr()
}
if len(args) != 2 {
return &Etcd{}, c.Errf("credentials requires 2 arguments, username and password")
}
username, password = args[0], args[1]
default: default:
if c.Val() != "}" { if c.Val() != "}" {
return &Etcd{}, c.Errf("unknown property '%s'", c.Val()) return &Etcd{}, c.Errf("unknown property '%s'", c.Val())
@ -101,7 +112,7 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) {
} }
} }
client, err := newEtcdClient(endpoints, tlsConfig) client, err := newEtcdClient(endpoints, tlsConfig, username, password)
if err != nil { if err != nil {
return &Etcd{}, err return &Etcd{}, err
} }
@ -113,11 +124,15 @@ func etcdParse(c *caddy.Controller) (*Etcd, error) {
return &Etcd{}, nil return &Etcd{}, nil
} }
func newEtcdClient(endpoints []string, cc *tls.Config) (*etcdcv3.Client, error) { func newEtcdClient(endpoints []string, cc *tls.Config, username, password string) (*etcdcv3.Client, error) {
etcdCfg := etcdcv3.Config{ etcdCfg := etcdcv3.Config{
Endpoints: endpoints, Endpoints: endpoints,
TLS: cc, TLS: cc,
} }
if username != "" && password != "" {
etcdCfg.Username = username
etcdCfg.Password = password
}
cli, err := etcdcv3.New(etcdCfg) cli, err := etcdcv3.New(etcdCfg)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -14,43 +14,69 @@ func TestSetupEtcd(t *testing.T) {
expectedPath string expectedPath string
expectedEndpoint []string expectedEndpoint []string
expectedErrContent string // substring from the expected error. Empty for positive cases. expectedErrContent string // substring from the expected error. Empty for positive cases.
username string
password string
}{ }{
// positive // positive
{ {
`etcd`, false, "skydns", []string{"http://localhost:2379"}, "", `etcd`, false, "skydns", []string{"http://localhost:2379"}, "", "", "",
}, },
{ {
`etcd { `etcd {
endpoint http://localhost:2379 http://localhost:3379 http://localhost:4379 endpoint http://localhost:2379 http://localhost:3379 http://localhost:4379
}`, false, "skydns", []string{"http://localhost:2379", "http://localhost:3379", "http://localhost:4379"}, "", }`, false, "skydns", []string{"http://localhost:2379", "http://localhost:3379", "http://localhost:4379"}, "", "", "",
}, },
{ {
`etcd skydns.local { `etcd skydns.local {
endpoint localhost:300 endpoint localhost:300
} }
`, false, "skydns", []string{"localhost:300"}, "", `, false, "skydns", []string{"localhost:300"}, "", "", "",
}, },
//test for upstream //test for upstream
{ {
`etcd { `etcd {
endpoint localhost:300 endpoint localhost:300
upstream 8.8.8.8:53 8.8.4.4:53 upstream 8.8.8.8:53 8.8.4.4:53
}`, false, "skydns", []string{"localhost:300"}, "", }`, false, "skydns", []string{"localhost:300"}, "", "", "",
}, },
//test for optional upstream address //test for optional upstream address
{ {
`etcd { `etcd {
endpoint localhost:300 endpoint localhost:300
upstream upstream
}`, false, "skydns", []string{"localhost:300"}, "", }`, false, "skydns", []string{"localhost:300"}, "", "", "",
}, },
// negative // negative
{ {
`etcd { `etcd {
endpoints localhost:300 endpoints localhost:300
} }
`, true, "", []string{""}, "unknown property 'endpoints'", `, true, "", []string{""}, "unknown property 'endpoints'", "", "",
},
// with valid credentials
{
`etcd {
endpoint http://localhost:2379
credentials username password
}
`, false, "skydns", []string{"http://localhost:2379"}, "", "username", "password",
},
// with credentials, missing password
{
`etcd {
endpoint http://localhost:2379
credentials username
}
`, true, "skydns", []string{"http://localhost:2379"}, "credentials requires 2 arguments", "username", "",
},
// with credentials, missing username and password
{
`etcd {
endpoint http://localhost:2379
credentials
}
`, true, "skydns", []string{"http://localhost:2379"}, "Wrong argument count", "", "",
}, },
} }
@ -69,7 +95,7 @@ func TestSetupEtcd(t *testing.T) {
} }
if !strings.Contains(err.Error(), test.expectedErrContent) { if !strings.Contains(err.Error(), test.expectedErrContent) {
t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err, test.input) t.Errorf("Test %d: Expected error to contain: %v, found error: %v, input: %s", i, test.expectedErrContent, err.Error(), test.input)
continue continue
} }
} }
@ -87,5 +113,19 @@ func TestSetupEtcd(t *testing.T) {
} }
} }
} }
if !test.shouldErr {
if test.username != "" {
if etcd.Client.Username != test.username {
t.Errorf("Etcd username not correctly set for input %s. Excpeted: '%+v', actual: '%+v'", test.input, test.username, etcd.Client.Username)
}
}
if test.password != "" {
if etcd.Client.Password != test.password {
t.Errorf("Etcd password not correctly set for input %s. Excpeted: '%+v', actual: '%+v'", test.input, test.password, etcd.Client.Password)
}
}
}
} }
} }

View file

@ -0,0 +1,72 @@
// +build etcd
package test
import (
"context"
"testing"
)
// uses some stuff from etcd_tests.go
func TestEtcdCredentials(t *testing.T) {
corefile := `.:0 {
etcd skydns.test {
path /skydns
}
}`
ex, _, _, err := CoreDNSServerAndPorts(corefile)
if err != nil {
t.Fatalf("Could not get CoreDNS serving instance: %s", err)
}
defer ex.Stop()
etc := etcdPlugin()
username := "root"
password := "password"
key := "foo"
value := "bar"
var ctx = context.TODO()
if _, err := etc.Client.Put(ctx, key, value); err != nil {
t.Errorf("Failed to put dummy value un etcd: %v", err)
}
if _, err := etc.Client.RoleAdd(ctx, "root"); err != nil {
t.Errorf("Failed to create root role: %s", err)
}
if _, err := etc.Client.UserAdd(ctx, username, password); err != nil {
t.Errorf("Failed to create user: %s", err)
}
if _, err := etc.Client.UserGrantRole(ctx, username, "root"); err != nil {
t.Errorf("Failed to assign role to root user: %v", err)
}
if _, err := etc.Client.AuthEnable(ctx); err != nil {
t.Errorf("Failed to enable authentication: %s", err)
}
etc2 := etcdPluginWithCredentials(username, password)
defer func() {
if _, err := etc2.Client.AuthDisable(ctx); err != nil {
t.Errorf("Fail to disable authentication: %v", err)
}
}()
resp, err := etc2.Client.Get(ctx, key)
if err != nil {
t.Errorf("Fail to retrieve value from etcd: %v", err)
}
if len(resp.Kvs) != 1 {
t.Errorf("Too many response found: %+v", resp)
return
}
actual := resp.Kvs[0].Value
expected := "bar"
if string(resp.Kvs[0].Value) != expected {
t.Errorf("Value doesn't match, expected:%s actual:%s", actual, expected)
}
}

View file

@ -23,6 +23,16 @@ func etcdPlugin() *etcd.Etcd {
return &etcd.Etcd{Client: cli, PathPrefix: "/skydns"} return &etcd.Etcd{Client: cli, PathPrefix: "/skydns"}
} }
func etcdPluginWithCredentials(username, password string) *etcd.Etcd {
etcdCfg := etcdcv3.Config{
Endpoints: []string{"http://localhost:2379"},
Username: username,
Password: password,
}
cli, _ := etcdcv3.New(etcdCfg)
return &etcd.Etcd{Client: cli, PathPrefix: "/skydns"}
}
// This test starts two coredns servers (and needs etcd). Configure a stubzones in both (that will loop) and // This test starts two coredns servers (and needs etcd). Configure a stubzones in both (that will loop) and
// will then test if we detect this loop. // will then test if we detect this loop.
func TestEtcdStubLoop(t *testing.T) { func TestEtcdStubLoop(t *testing.T) {