diff --git a/docs/configuration/backends/consul.md b/docs/configuration/backends/consul.md index 734f83dc0..5b1c7b430 100644 --- a/docs/configuration/backends/consul.md +++ b/docs/configuration/backends/consul.md @@ -114,3 +114,4 @@ Additional settings can be defined using Consul Catalog tags: | `traefik.frontend.passHostHeader=true` | Forward client `Host` header to the backend. | | `traefik.frontend.priority=10` | Override default frontend priority | | `traefik.frontend.entryPoints=http,https` | Assign this frontend to entry points `http` and `https`. Overrides `defaultEntryPoints`. | +| `traefik.frontend.auth.basic=EXPR` | Sets basic authentication for that frontend in CSV format: `User:Hash,User:Hash` | diff --git a/integration/consul_catalog_test.go b/integration/consul_catalog_test.go index 16de26030..9fad9e974 100644 --- a/integration/consul_catalog_test.go +++ b/integration/consul_catalog_test.go @@ -202,3 +202,42 @@ func (s *ConsulCatalogSuite) TestExposedByDefaultTrueSimpleServiceMultipleNode(c err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) c.Assert(err, checker.IsNil) } + +func (s *ConsulCatalogSuite) TestBasicAuthSimpleService(c *check.C) { + cmd, output := s.cmdTraefik( + withConfigFile("fixtures/consul_catalog/simple.toml"), + "--consulCatalog", + "--consulCatalog.exposedByDefault=true", + "--consulCatalog.endpoint="+s.consulIP+":8500", + "--consulCatalog.domain=consul.localhost") + err := cmd.Start() + c.Assert(err, checker.IsNil) + defer cmd.Process.Kill() + + defer func() { + s.displayTraefikLog(c, output) + }() + + nginx := s.composeProject.Container(c, "nginx") + + err = s.registerService("test", nginx.NetworkSettings.IPAddress, 80, []string{ + "traefik.frontend.auth.basic=test:$2a$06$O5NksJPAcgrC9MuANkSoE.Xe9DSg7KcLLFYNr1Lj6hPcMmvgwxhme,test2:$2y$10$xP1SZ70QbZ4K2bTGKJOhpujkpcLxQcB3kEPF6XAV19IdcqsZTyDEe", + }) + c.Assert(err, checker.IsNil, check.Commentf("Error registering service")) + defer s.deregisterService("test", nginx.NetworkSettings.IPAddress) + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:8000/", nil) + c.Assert(err, checker.IsNil) + req.Host = "test.consul.localhost" + + err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusUnauthorized), try.HasBody()) + c.Assert(err, checker.IsNil) + + req.SetBasicAuth("test", "test") + err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) + + req.SetBasicAuth("test2", "test2") + err = try.Request(req, 5*time.Second, try.StatusCodeIs(http.StatusOK), try.HasBody()) + c.Assert(err, checker.IsNil) +} diff --git a/provider/consul/consul_catalog.go b/provider/consul/consul_catalog.go index b058ee7e5..86d87eeda 100644 --- a/provider/consul/consul_catalog.go +++ b/provider/consul/consul_catalog.go @@ -330,6 +330,14 @@ func (p *CatalogProvider) getAttribute(name string, tags []string, defaultValue return p.getTag(p.getPrefixedName(name), tags, defaultValue) } +func (p *CatalogProvider) getBasicAuth(tags []string) []string { + list := p.getAttribute("frontend.auth.basic", tags, "") + if list != "" { + return strings.Split(list, ",") + } + return []string{} +} + func (p *CatalogProvider) hasTag(name string, tags []string) bool { // Very-very unlikely that a Consul tag would ever start with '=!=' tag := p.getTag(name, tags, "=!=") @@ -377,6 +385,7 @@ func (p *CatalogProvider) buildConfig(catalog []catalogUpdate) *types.Configurat "getBackendName": p.getBackendName, "getBackendAddress": p.getBackendAddress, "getAttribute": p.getAttribute, + "getBasicAuth": p.getBasicAuth, "getTag": p.getTag, "hasTag": p.hasTag, "getEntryPoints": p.getEntryPoints, diff --git a/provider/consul/consul_catalog_test.go b/provider/consul/consul_catalog_test.go index 21eff493e..23f3f1422 100644 --- a/provider/consul/consul_catalog_test.go +++ b/provider/consul/consul_catalog_test.go @@ -348,6 +348,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) { "random.foo=bar", "traefik.backend.maxconn.amount=1000", "traefik.backend.maxconn.extractorfunc=client.ip", + "traefik.frontend.auth.basic=test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/,test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0", }, }, Nodes: []*api.ServiceEntry{ @@ -380,6 +381,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) { Rule: "Host:test.localhost", }, }, + BasicAuth: []string{"test:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/", "test2:$apr1$d9hr9HBB$4HxwgUir3HP4EsggP/QNo0"}, }, }, expectedBackends: map[string]*types.Backend{ @@ -411,6 +413,7 @@ func TestConsulCatalogBuildConfig(t *testing.T) { t.Fatalf("expected %#v, got %#v", c.expectedBackends, actualConfig.Backends) } if !reflect.DeepEqual(actualConfig.Frontends, c.expectedFrontends) { + t.Fatalf("expected %#v, got %#v", c.expectedFrontends["frontend-test"].BasicAuth, actualConfig.Frontends["frontend-test"].BasicAuth) t.Fatalf("expected %#v, got %#v", c.expectedFrontends, actualConfig.Frontends) } } @@ -853,3 +856,38 @@ func TestConsulCatalogFilterEnabled(t *testing.T) { }) } } + +func TestConsulCatalogGetBasicAuth(t *testing.T) { + cases := []struct { + desc string + tags []string + expected []string + }{ + { + desc: "label missing", + tags: []string{}, + expected: []string{}, + }, + { + desc: "label existing", + tags: []string{ + "traefik.frontend.auth.basic=user:password", + }, + expected: []string{"user:password"}, + }, + } + + for _, c := range cases { + c := c + t.Run(c.desc, func(t *testing.T) { + t.Parallel() + provider := &CatalogProvider{ + Prefix: "traefik", + } + actual := provider.getBasicAuth(c.tags) + if !reflect.DeepEqual(actual, c.expected) { + t.Errorf("actual %q, expected %q", actual, c.expected) + } + }) + } +} diff --git a/templates/consul_catalog.tmpl b/templates/consul_catalog.tmpl index 1787232f2..edcf9a7a1 100644 --- a/templates/consul_catalog.tmpl +++ b/templates/consul_catalog.tmpl @@ -40,6 +40,9 @@ "{{.}}", {{end}}] {{end}} + basicAuth = [{{range getBasicAuth .Attributes}} + "{{.}}", + {{end}}] [frontends."frontend-{{.ServiceName}}".routes."route-host-{{.ServiceName}}"] rule = "{{getFrontendRule .}}" {{end}}