diff options
author | Julien Pivotto <roidelapluie@inuits.eu> | 2020-05-01 14:26:51 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-05-01 14:26:51 +0200 |
commit | 202ecf9c9d1d1960cc9cac24838d13e9cff5edca (patch) | |
tree | 114dd7799aad5b6d8241bcf4756565ed16aa7f59 /https | |
parent | b42819b69d4d6f81c9b13ab6c7cefcb7aa6a395a (diff) | |
download | prometheus_node_collector-202ecf9c9d1d1960cc9cac24838d13e9cff5edca.tar.bz2 prometheus_node_collector-202ecf9c9d1d1960cc9cac24838d13e9cff5edca.tar.xz prometheus_node_collector-202ecf9c9d1d1960cc9cac24838d13e9cff5edca.zip |
Add basic authentication (#1683)
* Add basic authentication
Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
Diffstat (limited to 'https')
-rw-r--r-- | https/README.md | 23 | ||||
-rw-r--r-- | https/testdata/tls_config_auth_user_list_invalid.bad.yml | 5 | ||||
-rw-r--r-- | https/testdata/tls_config_junk_key.yml | 2 | ||||
-rw-r--r-- | https/testdata/tls_config_noAuth_certPath_keyPath_empty.bad.yml | 3 | ||||
-rw-r--r-- | https/testdata/tls_config_users.good.yml | 8 | ||||
-rw-r--r-- | https/testdata/tls_config_users_noTLS.good.yml | 5 | ||||
-rw-r--r-- | https/tls_config.go | 110 | ||||
-rw-r--r-- | https/tls_config_test.go | 117 | ||||
-rw-r--r-- | https/users.go | 73 |
9 files changed, 297 insertions, 49 deletions
diff --git a/https/README.md b/https/README.md index e8e4504..70b321e 100644 --- a/https/README.md +++ b/https/README.md | |||
@@ -25,4 +25,27 @@ tls_config: | |||
25 | 25 | ||
26 | # CA certificate for client certificate authentication to the server | 26 | # CA certificate for client certificate authentication to the server |
27 | [ client_ca_file: <filename> ] | 27 | [ client_ca_file: <filename> ] |
28 | |||
29 | # List of usernames and hashed passwords that have full access to the web | ||
30 | # server via basic authentication. If empty, no basic authentication is | ||
31 | # required. Passwords are hashed with bcrypt. | ||
32 | basic_auth_users: | ||
33 | [ <username>: <password> ... ] | ||
28 | ``` | 34 | ``` |
35 | |||
36 | ## About bcrypt | ||
37 | |||
38 | There are several tools out there to generate bcrypt passwords, e.g. | ||
39 | [htpasswd](https://httpd.apache.org/docs/2.4/programs/htpasswd.html): | ||
40 | |||
41 | `htpasswd -nBC 10 "" | tr -d ':\n` | ||
42 | |||
43 | That command will prompt you for a password and output the hashed password, | ||
44 | which will look something like: | ||
45 | `$2y$10$X0h1gDsPszWURQaxFh.zoubFi6DXncSjhoQNJgRrnGs7EsimhC7zG` | ||
46 | |||
47 | The cost (10 in the example) influences the time it takes for computing the | ||
48 | hash. A higher cost will en up slowing down the authentication process. | ||
49 | Depending on the machine, a cost of 10 will take about ~70ms where a cost of | ||
50 | 18 can take up to a few seconds. That hash will be computed on every | ||
51 | password-protected request. | ||
diff --git a/https/testdata/tls_config_auth_user_list_invalid.bad.yml b/https/testdata/tls_config_auth_user_list_invalid.bad.yml new file mode 100644 index 0000000..90c1d95 --- /dev/null +++ b/https/testdata/tls_config_auth_user_list_invalid.bad.yml | |||
@@ -0,0 +1,5 @@ | |||
1 | tls_config : | ||
2 | cert_file : "testdata/server.crt" | ||
3 | key_file : "testdata/server.key" | ||
4 | basic_auth_users: | ||
5 | john: doe | ||
diff --git a/https/testdata/tls_config_junk_key.yml b/https/testdata/tls_config_junk_key.yml new file mode 100644 index 0000000..acb2cc3 --- /dev/null +++ b/https/testdata/tls_config_junk_key.yml | |||
@@ -0,0 +1,2 @@ | |||
1 | tls_config : | ||
2 | cert_filse: "testdata/server.crt" | ||
diff --git a/https/testdata/tls_config_noAuth_certPath_keyPath_empty.bad.yml b/https/testdata/tls_config_noAuth_certPath_keyPath_empty.bad.yml index 2ed9195..1511b5a 100644 --- a/https/testdata/tls_config_noAuth_certPath_keyPath_empty.bad.yml +++ b/https/testdata/tls_config_noAuth_certPath_keyPath_empty.bad.yml | |||
@@ -1,3 +1,4 @@ | |||
1 | tls_config : | 1 | tls_config : |
2 | cert_file : "" | 2 | cert_file : "" |
3 | key_file : "" \ No newline at end of file | 3 | key_file : "" |
4 | client_auth_type: "x" | ||
diff --git a/https/testdata/tls_config_users.good.yml b/https/testdata/tls_config_users.good.yml new file mode 100644 index 0000000..278177d --- /dev/null +++ b/https/testdata/tls_config_users.good.yml | |||
@@ -0,0 +1,8 @@ | |||
1 | tls_config : | ||
2 | cert_file : "testdata/server.crt" | ||
3 | key_file : "testdata/server.key" | ||
4 | basic_auth_users: | ||
5 | alice: $2y$12$1DpfPeqF9HzHJt.EWswy1exHluGfbhnn3yXhR7Xes6m3WJqFg0Wby | ||
6 | bob: $2y$18$4VeFDzXIoPHKnKTU3O3GH.N.vZu06CVqczYZ8WvfzrddFU6tGqjR. | ||
7 | carol: $2y$10$qRTBuFoULoYNA7AQ/F3ck.trZBPyjV64.oA4ZsSBCIWvXuvQlQTuu | ||
8 | dave: $2y$10$2UXri9cIDdgeKjBo4Rlpx.U3ZLDV8X1IxKmsfOvhcM5oXQt/mLmXq | ||
diff --git a/https/testdata/tls_config_users_noTLS.good.yml b/https/testdata/tls_config_users_noTLS.good.yml new file mode 100644 index 0000000..d3a7987 --- /dev/null +++ b/https/testdata/tls_config_users_noTLS.good.yml | |||
@@ -0,0 +1,5 @@ | |||
1 | basic_auth_users: | ||
2 | alice: $2y$12$1DpfPeqF9HzHJt.EWswy1exHluGfbhnn3yXhR7Xes6m3WJqFg0Wby | ||
3 | bob: $2y$18$4VeFDzXIoPHKnKTU3O3GH.N.vZu06CVqczYZ8WvfzrddFU6tGqjR. | ||
4 | carol: $2y$10$qRTBuFoULoYNA7AQ/F3ck.trZBPyjV64.oA4ZsSBCIWvXuvQlQTuu | ||
5 | dave: $2y$10$2UXri9cIDdgeKjBo4Rlpx.U3ZLDV8X1IxKmsfOvhcM5oXQt/mLmXq | ||
diff --git a/https/tls_config.go b/https/tls_config.go index 4b29862..44e57e9 100644 --- a/https/tls_config.go +++ b/https/tls_config.go | |||
@@ -20,12 +20,20 @@ import ( | |||
20 | "io/ioutil" | 20 | "io/ioutil" |
21 | "net/http" | 21 | "net/http" |
22 | 22 | ||
23 | "github.com/go-kit/kit/log" | ||
24 | "github.com/go-kit/kit/log/level" | ||
23 | "github.com/pkg/errors" | 25 | "github.com/pkg/errors" |
26 | config_util "github.com/prometheus/common/config" | ||
24 | "gopkg.in/yaml.v2" | 27 | "gopkg.in/yaml.v2" |
25 | ) | 28 | ) |
26 | 29 | ||
30 | var ( | ||
31 | errNoTLSConfig = errors.New("TLS config is not present") | ||
32 | ) | ||
33 | |||
27 | type Config struct { | 34 | type Config struct { |
28 | TLSConfig TLSStruct `yaml:"tls_config"` | 35 | TLSConfig TLSStruct `yaml:"tls_config"` |
36 | Users map[string]config_util.Secret `yaml:"basic_auth_users"` | ||
29 | } | 37 | } |
30 | 38 | ||
31 | type TLSStruct struct { | 39 | type TLSStruct struct { |
@@ -35,13 +43,18 @@ type TLSStruct struct { | |||
35 | ClientCAs string `yaml:"client_ca_file"` | 43 | ClientCAs string `yaml:"client_ca_file"` |
36 | } | 44 | } |
37 | 45 | ||
38 | func getTLSConfig(configPath string) (*tls.Config, error) { | 46 | func getConfig(configPath string) (*Config, error) { |
39 | content, err := ioutil.ReadFile(configPath) | 47 | content, err := ioutil.ReadFile(configPath) |
40 | if err != nil { | 48 | if err != nil { |
41 | return nil, err | 49 | return nil, err |
42 | } | 50 | } |
43 | c := &Config{} | 51 | c := &Config{} |
44 | err = yaml.Unmarshal(content, c) | 52 | err = yaml.UnmarshalStrict(content, c) |
53 | return c, err | ||
54 | } | ||
55 | |||
56 | func getTLSConfig(configPath string) (*tls.Config, error) { | ||
57 | c, err := getConfig(configPath) | ||
45 | if err != nil { | 58 | if err != nil { |
46 | return nil, err | 59 | return nil, err |
47 | } | 60 | } |
@@ -50,14 +63,18 @@ func getTLSConfig(configPath string) (*tls.Config, error) { | |||
50 | 63 | ||
51 | // ConfigToTLSConfig generates the golang tls.Config from the TLSStruct config. | 64 | // ConfigToTLSConfig generates the golang tls.Config from the TLSStruct config. |
52 | func ConfigToTLSConfig(c *TLSStruct) (*tls.Config, error) { | 65 | func ConfigToTLSConfig(c *TLSStruct) (*tls.Config, error) { |
53 | cfg := &tls.Config{ | 66 | if c.TLSCertPath == "" && c.TLSKeyPath == "" && c.ClientAuth == "" && c.ClientCAs == "" { |
54 | MinVersion: tls.VersionTLS12, | 67 | return nil, errNoTLSConfig |
55 | } | 68 | } |
56 | if len(c.TLSCertPath) == 0 { | 69 | |
57 | return nil, errors.New("missing TLSCertPath") | 70 | if c.TLSCertPath == "" { |
71 | return nil, errors.New("missing cert_file") | ||
58 | } | 72 | } |
59 | if len(c.TLSKeyPath) == 0 { | 73 | if c.TLSKeyPath == "" { |
60 | return nil, errors.New("missing TLSKeyPath") | 74 | return nil, errors.New("missing key_file") |
75 | } | ||
76 | cfg := &tls.Config{ | ||
77 | MinVersion: tls.VersionTLS12, | ||
61 | } | 78 | } |
62 | loadCert := func() (*tls.Certificate, error) { | 79 | loadCert := func() (*tls.Certificate, error) { |
63 | cert, err := tls.LoadX509KeyPair(c.TLSCertPath, c.TLSKeyPath) | 80 | cert, err := tls.LoadX509KeyPair(c.TLSCertPath, c.TLSKeyPath) |
@@ -74,7 +91,7 @@ func ConfigToTLSConfig(c *TLSStruct) (*tls.Config, error) { | |||
74 | return loadCert() | 91 | return loadCert() |
75 | } | 92 | } |
76 | 93 | ||
77 | if len(c.ClientCAs) > 0 { | 94 | if c.ClientCAs != "" { |
78 | clientCAPool := x509.NewCertPool() | 95 | clientCAPool := x509.NewCertPool() |
79 | clientCAFile, err := ioutil.ReadFile(c.ClientCAs) | 96 | clientCAFile, err := ioutil.ReadFile(c.ClientCAs) |
80 | if err != nil { | 97 | if err != nil { |
@@ -83,40 +100,67 @@ func ConfigToTLSConfig(c *TLSStruct) (*tls.Config, error) { | |||
83 | clientCAPool.AppendCertsFromPEM(clientCAFile) | 100 | clientCAPool.AppendCertsFromPEM(clientCAFile) |
84 | cfg.ClientCAs = clientCAPool | 101 | cfg.ClientCAs = clientCAPool |
85 | } | 102 | } |
86 | if len(c.ClientAuth) > 0 { | 103 | |
87 | switch s := (c.ClientAuth); s { | 104 | switch c.ClientAuth { |
88 | case "NoClientCert": | 105 | case "RequestClientCert": |
89 | cfg.ClientAuth = tls.NoClientCert | 106 | cfg.ClientAuth = tls.RequestClientCert |
90 | case "RequestClientCert": | 107 | case "RequireClientCert": |
91 | cfg.ClientAuth = tls.RequestClientCert | 108 | cfg.ClientAuth = tls.RequireAnyClientCert |
92 | case "RequireClientCert": | 109 | case "VerifyClientCertIfGiven": |
93 | cfg.ClientAuth = tls.RequireAnyClientCert | 110 | cfg.ClientAuth = tls.VerifyClientCertIfGiven |
94 | case "VerifyClientCertIfGiven": | 111 | case "RequireAndVerifyClientCert": |
95 | cfg.ClientAuth = tls.VerifyClientCertIfGiven | 112 | cfg.ClientAuth = tls.RequireAndVerifyClientCert |
96 | case "RequireAndVerifyClientCert": | 113 | case "", "NoClientCert": |
97 | cfg.ClientAuth = tls.RequireAndVerifyClientCert | 114 | cfg.ClientAuth = tls.NoClientCert |
98 | case "": | 115 | default: |
99 | cfg.ClientAuth = tls.NoClientCert | 116 | return nil, errors.New("Invalid ClientAuth: " + c.ClientAuth) |
100 | default: | ||
101 | return nil, errors.New("Invalid ClientAuth: " + s) | ||
102 | } | ||
103 | } | 117 | } |
104 | if len(c.ClientCAs) > 0 && cfg.ClientAuth == tls.NoClientCert { | 118 | |
119 | if c.ClientCAs != "" && cfg.ClientAuth == tls.NoClientCert { | ||
105 | return nil, errors.New("Client CA's have been configured without a Client Auth Policy") | 120 | return nil, errors.New("Client CA's have been configured without a Client Auth Policy") |
106 | } | 121 | } |
122 | |||
107 | return cfg, nil | 123 | return cfg, nil |
108 | } | 124 | } |
109 | 125 | ||
110 | // Listen starts the server on the given address. If tlsConfigPath isn't empty the server connection will be started using TLS. | 126 | // Listen starts the server on the given address. If tlsConfigPath isn't empty the server connection will be started using TLS. |
111 | func Listen(server *http.Server, tlsConfigPath string) error { | 127 | func Listen(server *http.Server, tlsConfigPath string, logger log.Logger) error { |
112 | if (tlsConfigPath) == "" { | 128 | if tlsConfigPath == "" { |
129 | level.Info(logger).Log("msg", "TLS is disabled and it cannot be enabled on the fly.") | ||
113 | return server.ListenAndServe() | 130 | return server.ListenAndServe() |
114 | } | 131 | } |
115 | var err error | 132 | |
116 | server.TLSConfig, err = getTLSConfig(tlsConfigPath) | 133 | if err := validateUsers(tlsConfigPath); err != nil { |
117 | if err != nil { | ||
118 | return err | 134 | return err |
119 | } | 135 | } |
136 | |||
137 | // Setup basic authentication. | ||
138 | var handler http.Handler = http.DefaultServeMux | ||
139 | if server.Handler != nil { | ||
140 | handler = server.Handler | ||
141 | } | ||
142 | server.Handler = &userAuthRoundtrip{ | ||
143 | tlsConfigPath: tlsConfigPath, | ||
144 | logger: logger, | ||
145 | handler: handler, | ||
146 | } | ||
147 | |||
148 | config, err := getTLSConfig(tlsConfigPath) | ||
149 | switch err { | ||
150 | case nil: | ||
151 | // Valid TLS config. | ||
152 | level.Info(logger).Log("msg", "TLS is enabled and it cannot be disabled on the fly.") | ||
153 | case errNoTLSConfig: | ||
154 | // No TLS config, back to plain HTTP. | ||
155 | level.Info(logger).Log("msg", "TLS is disabled and it cannot be enabled on the fly.") | ||
156 | return server.ListenAndServe() | ||
157 | default: | ||
158 | // Invalid TLS config. | ||
159 | return err | ||
160 | } | ||
161 | |||
162 | server.TLSConfig = config | ||
163 | |||
120 | // Set the GetConfigForClient method of the HTTPS server so that the config | 164 | // Set the GetConfigForClient method of the HTTPS server so that the config |
121 | // and certs are reloaded on new connections. | 165 | // and certs are reloaded on new connections. |
122 | server.TLSConfig.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) { | 166 | server.TLSConfig.GetConfigForClient = func(*tls.ClientHelloInfo) (*tls.Config, error) { |
diff --git a/https/tls_config_test.go b/https/tls_config_test.go index 4b1b4e0..07f412a 100644 --- a/https/tls_config_test.go +++ b/https/tls_config_test.go | |||
@@ -28,7 +28,8 @@ import ( | |||
28 | ) | 28 | ) |
29 | 29 | ||
30 | var ( | 30 | var ( |
31 | port = getPort() | 31 | port = getPort() |
32 | testlogger = &testLogger{} | ||
32 | 33 | ||
33 | ErrorMap = map[string]*regexp.Regexp{ | 34 | ErrorMap = map[string]*regexp.Regexp{ |
34 | "HTTP Response to HTTPS": regexp.MustCompile(`server gave HTTP response to HTTPS client`), | 35 | "HTTP Response to HTTPS": regexp.MustCompile(`server gave HTTP response to HTTPS client`), |
@@ -38,12 +39,21 @@ var ( | |||
38 | "Invalid ClientAuth": regexp.MustCompile(`invalid ClientAuth`), | 39 | "Invalid ClientAuth": regexp.MustCompile(`invalid ClientAuth`), |
39 | "TLS handshake": regexp.MustCompile(`tls`), | 40 | "TLS handshake": regexp.MustCompile(`tls`), |
40 | "HTTP Request to HTTPS server": regexp.MustCompile(`HTTP`), | 41 | "HTTP Request to HTTPS server": regexp.MustCompile(`HTTP`), |
41 | "Invalid CertPath": regexp.MustCompile(`missing TLSCertPath`), | 42 | "Invalid CertPath": regexp.MustCompile(`missing cert_file`), |
42 | "Invalid KeyPath": regexp.MustCompile(`missing TLSKeyPath`), | 43 | "Invalid KeyPath": regexp.MustCompile(`missing key_file`), |
43 | "ClientCA set without policy": regexp.MustCompile(`Client CA's have been configured without a Client Auth Policy`), | 44 | "ClientCA set without policy": regexp.MustCompile(`Client CA's have been configured without a Client Auth Policy`), |
45 | "Bad password": regexp.MustCompile(`hashedSecret too short to be a bcrypted password`), | ||
46 | "Unauthorized": regexp.MustCompile(`Unauthorized`), | ||
47 | "Forbidden": regexp.MustCompile(`Forbidden`), | ||
44 | } | 48 | } |
45 | ) | 49 | ) |
46 | 50 | ||
51 | type testLogger struct{} | ||
52 | |||
53 | func (t *testLogger) Log(keyvals ...interface{}) error { | ||
54 | return nil | ||
55 | } | ||
56 | |||
47 | func getPort() string { | 57 | func getPort() string { |
48 | listener, err := net.Listen("tcp", ":0") | 58 | listener, err := net.Listen("tcp", ":0") |
49 | if err != nil { | 59 | if err != nil { |
@@ -61,6 +71,8 @@ type TestInputs struct { | |||
61 | YAMLConfigPath string | 71 | YAMLConfigPath string |
62 | ExpectedError *regexp.Regexp | 72 | ExpectedError *regexp.Regexp |
63 | UseTLSClient bool | 73 | UseTLSClient bool |
74 | Username string | ||
75 | Password string | ||
64 | } | 76 | } |
65 | 77 | ||
66 | func TestYAMLFiles(t *testing.T) { | 78 | func TestYAMLFiles(t *testing.T) { |
@@ -73,7 +85,7 @@ func TestYAMLFiles(t *testing.T) { | |||
73 | { | 85 | { |
74 | Name: `empty config yml`, | 86 | Name: `empty config yml`, |
75 | YAMLConfigPath: "testdata/tls_config_empty.yml", | 87 | YAMLConfigPath: "testdata/tls_config_empty.yml", |
76 | ExpectedError: ErrorMap["Invalid CertPath"], | 88 | ExpectedError: nil, |
77 | }, | 89 | }, |
78 | { | 90 | { |
79 | Name: `invalid config yml (invalid structure)`, | 91 | Name: `invalid config yml (invalid structure)`, |
@@ -81,6 +93,11 @@ func TestYAMLFiles(t *testing.T) { | |||
81 | ExpectedError: ErrorMap["YAML error"], | 93 | ExpectedError: ErrorMap["YAML error"], |
82 | }, | 94 | }, |
83 | { | 95 | { |
96 | Name: `invalid config yml (invalid key)`, | ||
97 | YAMLConfigPath: "testdata/tls_config_junk_key.yml", | ||
98 | ExpectedError: ErrorMap["YAML error"], | ||
99 | }, | ||
100 | { | ||
84 | Name: `invalid config yml (cert path empty)`, | 101 | Name: `invalid config yml (cert path empty)`, |
85 | YAMLConfigPath: "testdata/tls_config_noAuth_certPath_empty.bad.yml", | 102 | YAMLConfigPath: "testdata/tls_config_noAuth_certPath_empty.bad.yml", |
86 | ExpectedError: ErrorMap["Invalid CertPath"], | 103 | ExpectedError: ErrorMap["Invalid CertPath"], |
@@ -120,6 +137,11 @@ func TestYAMLFiles(t *testing.T) { | |||
120 | YAMLConfigPath: "testdata/tls_config_auth_clientCAs_invalid.bad.yml", | 137 | YAMLConfigPath: "testdata/tls_config_auth_clientCAs_invalid.bad.yml", |
121 | ExpectedError: ErrorMap["No such file"], | 138 | ExpectedError: ErrorMap["No such file"], |
122 | }, | 139 | }, |
140 | { | ||
141 | Name: `invalid config yml (invalid user list)`, | ||
142 | YAMLConfigPath: "testdata/tls_config_auth_user_list_invalid.bad.yml", | ||
143 | ExpectedError: ErrorMap["Bad password"], | ||
144 | }, | ||
123 | } | 145 | } |
124 | for _, testInputs := range testTables { | 146 | for _, testInputs := range testTables { |
125 | t.Run(testInputs.Name, testInputs.Test) | 147 | t.Run(testInputs.Name, testInputs.Test) |
@@ -189,7 +211,7 @@ func TestConfigReloading(t *testing.T) { | |||
189 | recordConnectionError(errors.New("Panic starting server")) | 211 | recordConnectionError(errors.New("Panic starting server")) |
190 | } | 212 | } |
191 | }() | 213 | }() |
192 | err := Listen(server, badYAMLPath) | 214 | err := Listen(server, badYAMLPath, testlogger) |
193 | recordConnectionError(err) | 215 | recordConnectionError(err) |
194 | }() | 216 | }() |
195 | 217 | ||
@@ -266,21 +288,28 @@ func (test *TestInputs) Test(t *testing.T) { | |||
266 | recordConnectionError(errors.New("Panic starting server")) | 288 | recordConnectionError(errors.New("Panic starting server")) |
267 | } | 289 | } |
268 | }() | 290 | }() |
269 | err := Listen(server, test.YAMLConfigPath) | 291 | err := Listen(server, test.YAMLConfigPath, testlogger) |
270 | recordConnectionError(err) | 292 | recordConnectionError(err) |
271 | }() | 293 | }() |
272 | 294 | ||
273 | var ClientConnection func() (*http.Response, error) | 295 | ClientConnection := func() (*http.Response, error) { |
274 | if test.UseTLSClient { | 296 | var client *http.Client |
275 | ClientConnection = func() (*http.Response, error) { | 297 | var proto string |
276 | client := getTLSClient() | 298 | if test.UseTLSClient { |
277 | return client.Get("https://localhost" + port) | 299 | client = getTLSClient() |
300 | proto = "https" | ||
301 | } else { | ||
302 | client = http.DefaultClient | ||
303 | proto = "http" | ||
278 | } | 304 | } |
279 | } else { | 305 | req, err := http.NewRequest("GET", proto+"://localhost"+port, nil) |
280 | ClientConnection = func() (*http.Response, error) { | 306 | if err != nil { |
281 | client := http.DefaultClient | 307 | t.Error(err) |
282 | return client.Get("http://localhost" + port) | ||
283 | } | 308 | } |
309 | if test.Username != "" { | ||
310 | req.SetBasicAuth(test.Username, test.Password) | ||
311 | } | ||
312 | return client.Do(req) | ||
284 | } | 313 | } |
285 | go func() { | 314 | go func() { |
286 | time.Sleep(250 * time.Millisecond) | 315 | time.Sleep(250 * time.Millisecond) |
@@ -360,3 +389,61 @@ func swapFileContents(file1, file2 string) error { | |||
360 | } | 389 | } |
361 | return nil | 390 | return nil |
362 | } | 391 | } |
392 | |||
393 | func TestUsers(t *testing.T) { | ||
394 | testTables := []*TestInputs{ | ||
395 | { | ||
396 | Name: `without basic auth`, | ||
397 | YAMLConfigPath: "testdata/tls_config_users_noTLS.good.yml", | ||
398 | ExpectedError: ErrorMap["Unauthorized"], | ||
399 | }, | ||
400 | { | ||
401 | Name: `with correct basic auth`, | ||
402 | YAMLConfigPath: "testdata/tls_config_users_noTLS.good.yml", | ||
403 | Username: "dave", | ||
404 | Password: "dave123", | ||
405 | ExpectedError: nil, | ||
406 | }, | ||
407 | { | ||
408 | Name: `without basic auth and TLS`, | ||
409 | YAMLConfigPath: "testdata/tls_config_users.good.yml", | ||
410 | UseTLSClient: true, | ||
411 | ExpectedError: ErrorMap["Unauthorized"], | ||
412 | }, | ||
413 | { | ||
414 | Name: `with correct basic auth and TLS`, | ||
415 | YAMLConfigPath: "testdata/tls_config_users.good.yml", | ||
416 | UseTLSClient: true, | ||
417 | Username: "dave", | ||
418 | Password: "dave123", | ||
419 | ExpectedError: nil, | ||
420 | }, | ||
421 | { | ||
422 | Name: `with another correct basic auth and TLS`, | ||
423 | YAMLConfigPath: "testdata/tls_config_users.good.yml", | ||
424 | UseTLSClient: true, | ||
425 | Username: "carol", | ||
426 | Password: "carol123", | ||
427 | ExpectedError: nil, | ||
428 | }, | ||
429 | { | ||
430 | Name: `with bad password and TLS`, | ||
431 | YAMLConfigPath: "testdata/tls_config_users.good.yml", | ||
432 | UseTLSClient: true, | ||
433 | Username: "dave", | ||
434 | Password: "bad", | ||
435 | ExpectedError: ErrorMap["Forbidden"], | ||
436 | }, | ||
437 | { | ||
438 | Name: `with bad username and TLS`, | ||
439 | YAMLConfigPath: "testdata/tls_config_users.good.yml", | ||
440 | UseTLSClient: true, | ||
441 | Username: "nonexistent", | ||
442 | Password: "nonexistent", | ||
443 | ExpectedError: ErrorMap["Forbidden"], | ||
444 | }, | ||
445 | } | ||
446 | for _, testInputs := range testTables { | ||
447 | t.Run(testInputs.Name, testInputs.Test) | ||
448 | } | ||
449 | } | ||
diff --git a/https/users.go b/https/users.go new file mode 100644 index 0000000..170c87b --- /dev/null +++ b/https/users.go | |||
@@ -0,0 +1,73 @@ | |||
1 | // Copyright 2020 The Prometheus Authors | ||
2 | // Licensed under the Apache License, Version 2.0 (the "License"); | ||
3 | // you may not use this file except in compliance with the License. | ||
4 | // You may obtain a copy of the License at | ||
5 | // | ||
6 | // http://www.apache.org/licenses/LICENSE-2.0 | ||
7 | // | ||
8 | // Unless required by applicable law or agreed to in writing, software | ||
9 | // distributed under the License is distributed on an "AS IS" BASIS, | ||
10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
11 | // See the License for the specific language governing permissions and | ||
12 | // limitations under the License. | ||
13 | |||
14 | package https | ||
15 | |||
16 | import ( | ||
17 | "net/http" | ||
18 | |||
19 | "github.com/go-kit/kit/log" | ||
20 | "golang.org/x/crypto/bcrypt" | ||
21 | ) | ||
22 | |||
23 | func validateUsers(configPath string) error { | ||
24 | c, err := getConfig(configPath) | ||
25 | if err != nil { | ||
26 | return err | ||
27 | } | ||
28 | |||
29 | for _, p := range c.Users { | ||
30 | _, err = bcrypt.Cost([]byte(p)) | ||
31 | if err != nil { | ||
32 | return err | ||
33 | } | ||
34 | } | ||
35 | |||
36 | return nil | ||
37 | } | ||
38 | |||
39 | type userAuthRoundtrip struct { | ||
40 | tlsConfigPath string | ||
41 | handler http.Handler | ||
42 | logger log.Logger | ||
43 | } | ||
44 | |||
45 | func (u *userAuthRoundtrip) ServeHTTP(w http.ResponseWriter, r *http.Request) { | ||
46 | c, err := getConfig(u.tlsConfigPath) | ||
47 | if err != nil { | ||
48 | u.logger.Log("msg", "Unable to parse configuration", "err", err) | ||
49 | http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) | ||
50 | return | ||
51 | } | ||
52 | |||
53 | if len(c.Users) == 0 { | ||
54 | u.handler.ServeHTTP(w, r) | ||
55 | return | ||
56 | } | ||
57 | |||
58 | user, pass, ok := r.BasicAuth() | ||
59 | if !ok { | ||
60 | w.Header().Set("WWW-Authenticate", "Basic") | ||
61 | http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) | ||
62 | return | ||
63 | } | ||
64 | |||
65 | if hashedPassword, ok := c.Users[user]; ok { | ||
66 | if err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(pass)); err == nil { | ||
67 | u.handler.ServeHTTP(w, r) | ||
68 | return | ||
69 | } | ||
70 | } | ||
71 | |||
72 | http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) | ||
73 | } | ||