Skip to content

Commit 6dfc509

Browse files
committed
Basic auth for web UI
1 parent f4b4c6c commit 6dfc509

File tree

4 files changed

+114
-48
lines changed

4 files changed

+114
-48
lines changed

config.json.sample

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
// Web user interface options
1818
//"web-ui-enabled": false,
19+
//"web-ui-username": null,
20+
//"web-ui-password": null,
1921
//"web-ui-whitelist": ["127.0.0.1"],
2022

2123
// TLS/SSL cert (necessary for HTTPS and web socket server to work)
@@ -65,4 +67,4 @@
6567
]
6668
}
6769
]
68-
}
70+
}

gitautodeploy/cli/config.py

+2
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ def get_config_defaults():
3939

4040
# Web user interface options
4141
config['web-ui-enabled'] = False # Disabled by default until authentication is in place
42+
config['web-ui-username'] = None
43+
config['web-ui-password'] = None
4244
config['web-ui-whitelist'] = ['127.0.0.1']
4345
config['web-ui-require-https'] = True
4446

gitautodeploy/data/git-auto-deploy.conf.json

+2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
// Web user interface options
1818
//"web-ui-enabled": false,
19+
//"web-ui-username": null,
20+
//"web-ui-password": null,
1921
//"web-ui-whitelist": ["127.0.0.1"],
2022

2123
// TLS/SSL cert (necessary for HTTPS and web socket server to work)

gitautodeploy/httpserver.py

+107-47
Original file line numberDiff line numberDiff line change
@@ -10,81 +10,75 @@ class WebhookRequestHandler(SimpleHTTPRequestHandler, object):
1010
HTTP requests."""
1111

1212
def __init__(self, *args, **kwargs):
13-
self._config = config
14-
self._event_store = event_store
15-
self._server_status = server_status
16-
self._is_https = is_https
17-
super(WebhookRequestHandler, self).__init__(*args, **kwargs)
13+
self._config = config
14+
self._event_store = event_store
15+
self._server_status = server_status
16+
self._is_https = is_https
17+
super(WebhookRequestHandler, self).__init__(*args, **kwargs)
1818

19-
def end_headers (self):
19+
def end_headers(self):
2020
self.send_header('Access-Control-Allow-Origin', '*')
2121
SimpleHTTPRequestHandler.end_headers(self)
2222

2323
def do_HEAD(self):
24-
import json
2524

26-
if not self._config['web-ui-enabled']:
27-
self.send_error(403, "Web UI is not enabled")
25+
# Web UI needs to be enabled
26+
if not self.validate_web_ui_enabled():
2827
return
2928

30-
if not self._is_https and self._config['web-ui-require-https']:
31-
32-
# Attempt to redirect the request to HTTPS
33-
server_status = self.get_server_status()
34-
if 'https-uri' in server_status:
35-
self.send_response(307)
36-
self.send_header('Location', '%s%s' % (server_status['https-uri'], self.path))
37-
self.end_headers()
38-
return
29+
# Web UI might require HTTPS
30+
if not self.validate_web_ui_https():
31+
return
3932

40-
self.send_error(403, "Web UI is only accessible through HTTPS")
33+
# Client needs to be whitelisted
34+
if not self.validate_web_ui_whitelist():
4135
return
4236

43-
if not self.client_address[0] in self._config['web-ui-whitelist']:
44-
self.send_error(403, "%s is not allowed access" % self.client_address[0])
37+
# Client needs to authenticate
38+
if not self.validate_web_ui_authentication():
4539
return
4640

4741
return SimpleHTTPRequestHandler.do_HEAD(self)
4842

4943
def do_GET(self):
50-
import json
5144

52-
if not self._config['web-ui-enabled']:
53-
self.send_error(403, "Web UI is not enabled")
45+
# Web UI needs to be enabled
46+
if not self.validate_web_ui_enabled():
5447
return
5548

56-
if not self._is_https and self._config['web-ui-require-https']:
57-
58-
# Attempt to redirect the request to HTTPS
59-
server_status = self.get_server_status()
60-
if 'https-uri' in server_status:
61-
self.send_response(307)
62-
self.send_header('Location', '%s%s' % (server_status['https-uri'], self.path))
63-
self.end_headers()
64-
return
49+
# Web UI might require HTTPS
50+
if not self.validate_web_ui_https():
51+
return
6552

66-
self.send_error(403, "Web UI is only accessible through HTTPS")
53+
# Client needs to be whitelisted
54+
if not self.validate_web_ui_whitelist():
6755
return
6856

69-
if not self.client_address[0] in self._config['web-ui-whitelist']:
70-
self.send_error(403, "%s is not allowed access" % self.client_address[0])
57+
# Client needs to authenticate
58+
if not self.validate_web_ui_authentication():
7159
return
7260

61+
# Handle API call
7362
if self.path == "/api/status":
74-
data = {
75-
'events': self._event_store.dict_repr(),
76-
}
77-
78-
data.update(self.get_server_status())
79-
80-
self.send_response(200, 'OK')
81-
self.send_header('Content-type', 'application/json')
82-
self.end_headers()
83-
self.wfile.write(json.dumps(data).encode('utf-8'))
63+
self.handle_status_api()
8464
return
8565

66+
# Serve static file
8667
return SimpleHTTPRequestHandler.do_GET(self)
8768

69+
def handle_status_api(self):
70+
import json
71+
data = {
72+
'events': self._event_store.dict_repr(),
73+
}
74+
75+
data.update(self.get_server_status())
76+
77+
self.send_response(200, 'OK')
78+
self.send_header('Content-type', 'application/json')
79+
self.end_headers()
80+
self.wfile.write(json.dumps(data).encode('utf-8'))
81+
8882
def do_POST(self):
8983
"""Invoked on incoming POST requests"""
9084
from threading import Timer
@@ -235,17 +229,83 @@ def save_test_case(self, test_case):
235229

236230
def get_server_status(self):
237231
"""Generate a copy of the server status object that contains the public IP or hostname."""
232+
238233
server_status = {}
239234
for item in self._server_status.items():
240235
key, value = item
241236
public_host = self.headers.get('host').split(':')[0]
237+
242238
if key == 'http-uri':
243239
server_status[key] = value.replace(self._config['http-host'], public_host)
240+
244241
if key == 'https-uri':
245242
server_status[key] = value.replace(self._config['https-host'], public_host)
243+
246244
if key == 'wss-uri':
247245
server_status[key] = value.replace(self._config['wss-host'], public_host)
246+
248247
return server_status
249248

250-
return WebhookRequestHandler
249+
def validate_web_ui_enabled(self):
250+
"""Verify that the Web UI is enabled"""
251+
252+
if self._config['web-ui-enabled']:
253+
return True
254+
255+
self.send_error(403, "Web UI is not enabled")
256+
return False
257+
258+
def validate_web_ui_https(self):
259+
"""Verify that the request is made over HTTPS"""
260+
261+
if self._is_https and self._config['web-ui-require-https']:
262+
return True
263+
264+
# Attempt to redirect the request to HTTPS
265+
server_status = self.get_server_status()
266+
if 'https-uri' in server_status:
267+
self.send_response(307)
268+
self.send_header('Location', '%s%s' % (server_status['https-uri'], self.path))
269+
self.end_headers()
270+
return False
251271

272+
self.send_error(403, "Web UI is only accessible through HTTPS")
273+
return False
274+
275+
def validate_web_ui_whitelist(self):
276+
"""Verify that the client address is whitelisted"""
277+
278+
# Allow all if whitelist is empty
279+
if len(self._config['web-ui-whitelist']) == 0:
280+
return True
281+
282+
# Verify that client IP is whitelisted
283+
if self.client_address[0] in self._config['web-ui-whitelist']:
284+
return True
285+
286+
self.send_error(403, "%s is not allowed access" % self.client_address[0])
287+
return False
288+
289+
def validate_web_ui_authentication(self):
290+
"""Authenticate the user"""
291+
import base64
292+
293+
# Verify that a username and password is specified in the config
294+
if self._config['web-ui-username'] is None or self._config['web-ui-password'] is None:
295+
self.send_error(403, "Authentication credentials missing in config")
296+
return False
297+
298+
# Verify that the provided username and password matches the ones in the config
299+
key = base64.b64encode("%s:%s" % (self._config['web-ui-username'], self._config['web-ui-password']))
300+
if self.headers.getheader('Authorization') == 'Basic ' + key:
301+
return True
302+
303+
# Let the client know that authentication is required
304+
self.send_response(401)
305+
self.send_header('WWW-Authenticate', 'Basic realm=\"GAD\"')
306+
self.send_header('Content-type', 'text/html')
307+
self.end_headers()
308+
self.wfile.write('Not authenticated')
309+
return False
310+
311+
return WebhookRequestHandler

0 commit comments

Comments
 (0)