Skip to content

Commit 66468f6

Browse files
authored
Merge pull request #201 from Helene/basic_auth
Add HTTP "Basic Authentiction" support
2 parents ebb5181 + fb07bda commit 66468f6

File tree

7 files changed

+171
-49
lines changed

7 files changed

+171
-49
lines changed

Dockerfile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ ARG HTTPPROTOCOL=http
2828
ENV PROTOCOL=$HTTPPROTOCOL
2929
RUN echo "the HTTP/S protocol is set to $PROTOCOL"
3030

31+
ARG HTTPBASICAUTH=True
32+
ENV BASICAUTH=$HTTPBASICAUTH
33+
RUN echo "the HTTP/S basic authentication is set to $BASICAUTH"
34+
35+
ARG AUTHUSER=None
36+
ENV BASICAUTHUSER=$AUTHUSER
37+
38+
ARG AUTHPASSW=NotSet
39+
ENV BASICAUTHPASSW=$AUTHPASSW
40+
3141
ARG HTTPPORT=None
3242
ENV PORT=$HTTPPORT
3343
RUN echo "the OpentTSDB API HTTP/S port is set to $PORT"
@@ -129,7 +139,7 @@ RUN chown -R $UID:$GID /opt/IBM/bridge && \
129139
# Switch user
130140
USER $GID
131141

132-
CMD ["sh", "-c", "python3 zimonGrafanaIntf.py -c 10 -s $SERVER -r $PROTOCOL -p $PORT -e $PROMETHEUS -P $SERVERPORT -t $TLSKEYPATH -l $LOGPATH --tlsKeyFile $TLSKEYFILE --tlsCertFile $TLSCERTFILE --apiKeyName $APIKEYNAME --apiKeyValue $APIKEYVALUE"]
142+
CMD ["sh", "-c", "python3 zimonGrafanaIntf.py -c 10 -s $SERVER -r $PROTOCOL -b $BASICAUTH -u $BASICAUTHUSER -a BASICAUTHPASSW -p $PORT -e $PROMETHEUS -P $SERVERPORT -t $TLSKEYPATH -l $LOGPATH -k $TLSKEYFILE -m $TLSCERTFILE -n $APIKEYNAME -v $APIKEYVALUE"]
133143

134144
EXPOSE 4242 8443 9250
135145

source/confParser.py

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
'''
2222

2323
import argparse
24+
import base64
25+
import binascii
2426
import os
2527
from messages import MSG
2628
from metaclasses import Singleton
@@ -36,14 +38,10 @@ def checkFileExists(path, filename):
3638

3739

3840
def checkTLSsettings(args):
39-
if args.get('prometheus') and (not args.get('tlsKeyPath') or not
40-
args.get('tlsKeyFile') or not
41-
args.get('tlsCertFile')):
42-
return False, MSG['MissingSSLCert']
43-
elif args.get('protocol') == "https" and (not args.get('tlsKeyPath') or not
44-
args.get('tlsKeyFile') or not
45-
args.get('tlsCertFile')
46-
):
41+
if args.get('protocol') == "https" and (not args.get('tlsKeyPath') or not
42+
args.get('tlsKeyFile') or not
43+
args.get('tlsCertFile')
44+
):
4745
return False, MSG['MissingParm']
4846
elif args.get('protocol') == "https" and not os.path.exists(args.get('tlsKeyPath')):
4947
return False, MSG['KeyPathError']
@@ -56,6 +54,23 @@ def checkTLSsettings(args):
5654
return True, ''
5755

5856

57+
def checkBasicAuthsettings(args):
58+
if args.get('enabled') and (not args.get('username') or not
59+
args.get('password')
60+
):
61+
return False, MSG['MissingParm']
62+
elif args.get('enabled') and ("/" in str(args.get('password')) and not
63+
os.path.isfile(args.get('password'))
64+
):
65+
return False, MSG['FileNotFound'].format(args.get('password'))
66+
elif args.get('enabled') and "/" not in str(args.get('password')):
67+
try:
68+
base64.b64decode(args.get('password'), validate=True)
69+
except binascii.Error:
70+
return False, MSG['WrongFormat'].format("basic auth password")
71+
return True, ''
72+
73+
5974
def checkApplicationPort(args):
6075
if not args.get('port', None) and not args.get('prometheus', None):
6176
return False, MSG['MissingPortParam']
@@ -78,7 +93,6 @@ def checkCAsettings(args):
7893

7994
def getSettings(argv):
8095
settings = {}
81-
msg = ''
8296
defaults = ConfigManager().defaults
8397
args, msg = parse_cmd_args(argv)
8498
if args and defaults:
@@ -93,6 +107,10 @@ def getSettings(argv):
93107
return None, msg
94108
# check API key settings
95109
valid, msg = checkAPIsettings(settings)
110+
if not valid:
111+
return None, msg
112+
# check basic auth settings
113+
valid, msg = checkBasicAuthsettings(settings)
96114
if not valid:
97115
return None, msg
98116
# check TLS settings
@@ -186,7 +204,10 @@ class Password(argparse.Action):
186204

187205
def __call__(self, parser, namespace, values, option_string):
188206
if values is None:
189-
print('Enter your apiKey value')
207+
if self.dest == 'password':
208+
print('Enter your basic auth password')
209+
else:
210+
print('Enter your apiKey value')
190211
values = getpass.getpass()
191212

192213
setattr(namespace, self.dest, values)
@@ -213,6 +234,12 @@ def parse_cmd_args(argv):
213234
help='port number listening on OpenTSDB API HTTP(S) connections (Default from config.ini: 4242, if enabled)')
214235
parser.add_argument('-r', '--protocol', action="store", choices=["http", "https"], default=None,
215236
help='Connection protocol HTTP/HTTPS (Default from config.ini: "http")')
237+
parser.add_argument('-b', '--enabled', action="store", choices=["True", "False"], default=None,
238+
help='Controls if HTTP/S basic authentication should be enabled or not (Default from config.ini: "True")')
239+
parser.add_argument('-u', '--username', action="store", default=None,
240+
help='HTTP/S basic authentication user name(Default from config.ini: \'scale_admin\')')
241+
parser.add_argument('-a', '--password', action=Password, nargs='?', dest='password', default=None,
242+
help='Enter your HTTP/S basic authentication password:')
216243
parser.add_argument('-t', '--tlsKeyPath', action="store", default=None,
217244
help='Directory path of tls privkey.pem and cert.pem file location (Required only for HTTPS ports 8443/9250)')
218245
parser.add_argument('-k', '--tlsKeyFile', action="store", default=None,

source/config.ini

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
##################### OpenTSDB API Connection Defaults ########################
2-
[connection]
2+
[opentsdb_plugin]
33
# Port number the bridge listening on for Grafana data requests over
44
# OpentTSDB plugin
55
#
@@ -8,20 +8,43 @@
88
# (Default: 4242)
99
# port = 4242
1010

11-
# Protocol (http, https)
12-
protocol = http
13-
1411
##################### Prometheus Exporter API Connection Defaults #############
15-
# Port number the bridge listening on for Prometheus server https requests;
16-
# ssl cert and key configuration required
12+
[prometheues_exporter_plugin]
13+
# Port number the bridge listening on for Prometheus server requests;
1714
# prometheus = 9250
1815

1916
# Sensor counters deltas will be exported by default.
2017
# Set True if you want export original sensor counters.
2118
rawCounters = True
2219

20+
##################### API Protocol scheme #####################################
21+
[connection]
22+
# Protocol (http, https)
23+
protocol = http
24+
25+
##################### API Basic Authentication ################################
26+
[basic_auth]
27+
# Basic authentication is enabled by default.
28+
# It is recommended to leave Basic Authentication enabled, especially when
29+
# connecting via HTTP. The bridge will validate the username and password
30+
# from `Authorization` header of every incoming http request.
31+
enabled = True
32+
33+
# user name used for bridge api http/s basic authentication
34+
username = scale_admin
35+
36+
# Base64 encoded password string used for bridge api http/s basic authentication
37+
# Example string encoding via cli:
38+
# $ echo "MyVeryStrongPassw0rd!" |base64
39+
# TXlWZXJ5U3Ryb25nUGFzc3cwcmQhCg==
40+
#
41+
# password = TXlWZXJ5U3Ryb25nUGFzc3cwcmQhCg==
42+
#
43+
# alternatively you can store a Base64 encoded string in a file
44+
# and specify the file location as the basic authentication password, f.e:
45+
# password = /etc/bridge_auth/basic_scale-21
2346

24-
#################################### API SSL OAuth ############################
47+
##################### API SSL OAuth ############################################
2548
[tls]
2649
# Directory path of tls key and cert file location
2750
# tlsKeyPath = /etc/bridge_ssl/certs
@@ -34,7 +57,7 @@ rawCounters = True
3457

3558

3659

37-
#################################### GPFS Server ##############################
60+
##################### GPFS Server ###############################################
3861
[server]
3962
# The ip address to bind to, empty will bind to all interfaces
4063
server = localhost
@@ -63,12 +86,12 @@ apiKeyName = scale_grafana
6386
# caCertPath = "/etc/ssl/certs/service-ca.crt"
6487
caCertPath = False
6588

66-
#################################### GPFS Server data query settings ###########
89+
##################### GPFS Server data query settings ############################
6790
[query]
6891
# Use or not the historical data from disk (default: no)
6992
includeDiskData = no
7093

71-
#################################### Logging ###################################
94+
##################### Logging ####################################################
7295
[logging]
7396
# Directory where the bridge can store logs
7497
logPath = /var/log/ibm_bridge_for_grafana

source/messages.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,5 +74,7 @@
7474
'StopCustomThread': 'Stopped custom thread {}',
7575
'MetricInResults': 'Metric {} is in Collector.metrcs',
7676
'MetricNotInResults': 'Metric {} is not in Collector.metrcs',
77-
'ConnApplications': 'Registered applications: \n {}'
77+
'ConnApplications': 'Registered applications: \n {}',
78+
'WrongFormat': 'The {} specified in wrong format.',
79+
'AuthValidationError': 'Basic auth data not valid'
7880
}

source/zimonGrafanaIntf.py

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from cherrypy.lib.cpstats import StatsPage
4444

4545
ENDPOINTS = {}
46+
AUTH_DICT = {}
4647

4748

4849
def processFormJSON(entity):
@@ -95,9 +96,15 @@ def setup_cherrypy_logging(args):
9596

9697
def updateCherrypyConf(args):
9798

98-
globalConfig = {'global': {'error_page.default': format_default_error_page,
99-
'request.error_response': handle_error},
100-
}
99+
global_settings = {'error_page.default': format_default_error_page,
100+
'request.error_response': handle_error}
101+
if args.get('enabled', False):
102+
global_settings.update({'tools.auth_basic.on': True,
103+
'tools.auth_basic.realm': "localhost",
104+
'tools.auth_basic.checkpassword': check_basic_auth,
105+
'tools.auth_basic.accept_charset': "UTF-8"
106+
})
107+
globalConfig = {'global': global_settings}
101108

102109
cherrypy.config.update(globalConfig)
103110

@@ -107,6 +114,16 @@ def updateCherrypyConf(args):
107114
cherrypy.server.unsubscribe()
108115

109116

117+
def check_basic_auth(realm, username, password):
118+
if (username and password
119+
and username in AUTH_DICT
120+
and AUTH_DICT[username] == password):
121+
return True
122+
logger = getBridgeLogger()
123+
logger.details(MSG['AuthValidationError'])
124+
return False
125+
126+
110127
def bind_opentsdb_server(args):
111128
opentsdb_server = cherrypy._cpserver.Server()
112129
opentsdb_server.socket_port = args.get('port')
@@ -125,23 +142,24 @@ def bind_prometheus_server(args):
125142
prometheus_server = cherrypy._cpserver.Server()
126143
prometheus_server.socket_port = args.get('prometheus')
127144
prometheus_server._socket_host = '0.0.0.0'
128-
certPath = os.path.join(args.get('tlsKeyPath'), args.get('tlsCertFile'))
129-
keyPath = os.path.join(args.get('tlsKeyPath'), args.get('tlsKeyFile'))
130-
prometheus_server.ssl_module = 'builtin'
131-
prometheus_server.ssl_certificate = certPath
132-
prometheus_server.ssl_private_key = keyPath
145+
if args.get('protocol') == "https":
146+
certPath = os.path.join(args.get('tlsKeyPath'), args.get('tlsCertFile'))
147+
keyPath = os.path.join(args.get('tlsKeyPath'), args.get('tlsKeyFile'))
148+
prometheus_server.ssl_module = 'builtin'
149+
prometheus_server.ssl_certificate = certPath
150+
prometheus_server.ssl_private_key = keyPath
133151
prometheus_server.statistics = analytics.cherrypy_internal_stats
134152
prometheus_server.subscribe()
135153

136154

137-
def resolveAPIKeyValue(storedKey):
138-
keyValue = None
139-
if "/" in str(storedKey):
140-
with open(storedKey) as file:
141-
keyValue = file.read().rstrip()
142-
return keyValue
155+
def resolve_path_to_value(stored_key):
156+
path_value = None
157+
if "/" in str(stored_key):
158+
with open(stored_key) as file:
159+
path_value = file.read().rstrip()
160+
return path_value
143161
else:
144-
return storedKey
162+
return stored_key
145163

146164

147165
def refresh_metadata(refresh_all=False):
@@ -190,6 +208,9 @@ def main(argv):
190208

191209
registered_apps = []
192210

211+
if args.get('enabled', False):
212+
AUTH_DICT[args.get('username')] = resolve_path_to_value(args.get('password'))
213+
193214
# prepare the logger
194215
logger = configureLogging(args.get('logPath'), args.get('logFile', None),
195216
args.get('logLevel'))
@@ -210,7 +231,7 @@ def main(argv):
210231
mdHandler = MetadataHandler(logger=logger, server=args.get('server'),
211232
port=args.get('serverPort'),
212233
apiKeyName=args.get('apiKeyName'),
213-
apiKeyValue=resolveAPIKeyValue(args.get('apiKeyValue')),
234+
apiKeyValue=resolve_path_to_value(args.get('apiKeyValue')),
214235
caCertPath=args.get('caCertPath'),
215236
includeDiskData=args.get('includeDiskData'),
216237
sleepTime=args.get('retryDelay', None)

0 commit comments

Comments
 (0)