diff --git a/.gitignore b/.gitignore index f1f64bf6f..705298a0d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,8 @@ __pycache__/ *.swp *.zip *~ -build/bdist* -dist/ -nagstamon -nagstamon.conf/ -nagstamon.egg-info/ \ No newline at end of file +/build/bdist* +/dist/ +/nagstamon.conf/ +/nagstamon.egg-info/ +/certifi/ diff --git a/Nagstamon/Helpers.py b/Nagstamon/Helpers.py index 408ce4a9b..bdb429871 100644 --- a/Nagstamon/Helpers.py +++ b/Nagstamon/Helpers.py @@ -429,15 +429,15 @@ def compare_status_information(item): return(item.lower()) -def webbrowser_open(url): +def webbrowser_open(url, new=0): """ decide if default or custom browser is used for various tasks used by almost all """ if conf.use_default_browser: - webbrowser.open(url) + webbrowser.open(url, new=new) else: - webbrowser.get('{0} %s &'.format(conf.custom_browser)).open(url) + webbrowser.get('{0} %s &'.format(conf.custom_browser)).open(url, new=new) def get_distro(): diff --git a/Nagstamon/QUI/__init__.py b/Nagstamon/QUI/__init__.py index 66611ed1a..d430a2fe1 100644 --- a/Nagstamon/QUI/__init__.py +++ b/Nagstamon/QUI/__init__.py @@ -5815,6 +5815,11 @@ def __init__(self, dialog): self.window.label_idp_ecp_endpoint, self.window.input_lineedit_idp_ecp_endpoint] + self.AUTHENTICATION_OAUTH2_WEBFLOW_WIDGETS = [ + self.window.label_openid_configuration_endpoint, + self.window.input_lineedit_idp_ecp_endpoint + ] + # fill default order fields combobox with monitor server types self.window.input_combobox_type.addItems(sorted(SERVER_TYPES.keys(), key=str.lower)) # default to Nagios as it is the mostly used monitor server @@ -5828,14 +5833,14 @@ def __init__(self, dialog): self.window.button_choose_custom_cert_ca_file.clicked.connect(self.choose_custom_cert_ca_file) # fill authentication combobox - self.window.input_combobox_authentication.addItems(['Basic', 'Digest', 'Kerberos', 'Bearer']) + self.window.input_combobox_authentication.addItems(['Basic', 'Digest', 'Kerberos', 'Bearer', 'OAuth2 Web Flow']) if ECP_AVAILABLE is True: self.window.input_combobox_authentication.addItems(['ECP']) # detect change of server type which leads to certain options shown or hidden self.window.input_combobox_type.activated.connect(self.toggle_type) - # when authentication is changed to Kerberos then disable username/password as the are now useless + # when authentication is changed to Kerberos then disable username/password as they are now useless self.window.input_combobox_authentication.activated.connect(self.toggle_authentication) # reset Checkmk views @@ -5874,15 +5879,29 @@ def toggle_authentication(self): for widget in self.AUTHENTICATION_ECP_WIDGETS: widget.hide() + if self.window.input_combobox_authentication.currentText() == 'OAuth2 Web Flow': + for widget in self.AUTHENTICATION_OAUTH2_WEBFLOW_WIDGETS: + widget.show() + else: + for widget in self.AUTHENTICATION_OAUTH2_WEBFLOW_WIDGETS: + widget.hide() + # change credential input for bearer auth if self.window.input_combobox_authentication.currentText() == 'Bearer': for widget in self.AUTHENTICATION_BEARER_WIDGETS: widget.hide() - self.window.label_password.setText('Token') else: for widget in self.AUTHENTICATION_BEARER_WIDGETS: widget.show() - self.window.label_password.setText('Password') + + if self.window.input_combobox_authentication.currentText() == 'Bearer': + self.window.label_password.setText('Token:') + elif self.window.input_combobox_authentication.currentText() == 'OAuth2 Web Flow': + self.window.label_username.setText('Client ID:') + self.window.label_password.setText('Client Secret:') + else: + self.window.label_username.setText('Username:') + self.window.label_password.setText('Password:') # after hiding authentication widgets dialog might shrink self.window.adjustSize() diff --git a/Nagstamon/Servers/Generic.py b/Nagstamon/Servers/Generic.py index 0aca42590..e4441895e 100644 --- a/Nagstamon/Servers/Generic.py +++ b/Nagstamon/Servers/Generic.py @@ -32,6 +32,8 @@ from bs4 import BeautifulSoup import requests +from Nagstamon.Servers.Oauth2WebFlow import get_oauth2_session + # check ECP authentication support availability try: from requests_ecp import HTTPECPAuth @@ -299,7 +301,12 @@ def init_HTTP(self): self.session = None return False elif self.session is None: - self.session = self.create_session() + try: + self.session = self.create_session() + except Exception as e: + print("Error while creating session: {}".format(e)) + self.session = None + return False return True def create_session(self): @@ -309,7 +316,6 @@ def create_session(self): different between various server types """ session = requests.Session() - session.headers['User-Agent'] = self.USER_AGENT # support for different authentication types if self.authentication == 'basic': @@ -323,6 +329,15 @@ def create_session(self): session.auth = HTTPSKerberos() elif self.authentication == 'bearer': session.auth = BearerAuth(self.password) + elif self.authentication == 'oauth2 web flow': + session = get_oauth2_session( + client_id=self.username, + client_secret=self.password, + openid_config_url=self.idp_ecp_endpoint, + port=9000) + session.headers['X-OAuth2'] = "1" + + session.headers['User-Agent'] = self.USER_AGENT # default to check TLS validity if self.ignore_cert: @@ -1535,6 +1550,7 @@ def FetchURL(self, url, giveback='obj', cgi_data=None, no_auth=False, multipart= del temporary_session except Exception: + self.reset_HTTP() # reset session to refresh auth and cookies if conf.debug_mode: self.Error(sys.exc_info()) result, error = self.Error(sys.exc_info()) diff --git a/Nagstamon/Servers/IcingaDBWebNotifications.py b/Nagstamon/Servers/IcingaDBWebNotifications.py index 3d1c356df..86659da30 100644 --- a/Nagstamon/Servers/IcingaDBWebNotifications.py +++ b/Nagstamon/Servers/IcingaDBWebNotifications.py @@ -17,41 +17,16 @@ # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA -# Initial implementation by Marcus Mönnig -# -# This Server class connects against IcingaWeb2. The monitor URL in the setup should be -# something like http://icinga2/icingaweb2 -# -# Status/TODOs: -# -# * The IcingaWeb2 API is not implemented yet, so currently this implementation uses -# two HTTP requests per action. The first fetches the HTML, then the form data is extracted and -# then a second HTTP POST request is made which actually executed the action. -# Once IcingaWeb2 has an API, it's probably the better choice. - - -from Nagstamon.Servers.Generic import GenericServer -import urllib.parse +# Same as IcingaDBWeb but uses the notification endpoint to allow fine granular recipient management for alerts import sys import json import datetime -import socket from bs4 import BeautifulSoup from Nagstamon.Objects import (GenericHost, GenericService, Result) -from Nagstamon.Config import (conf, - AppInfo) -from Nagstamon.Helpers import webbrowser_open -from Nagstamon.Servers.IcingaDBWeb import IcingaDBWebServer - - -def strfdelta(tdelta, fmt): - d = {'days': tdelta.days} - d['hours'], rem = divmod(tdelta.seconds, 3600) - d['minutes'], d['seconds'] = divmod(rem, 60) - return fmt.format(**d) +from Nagstamon.Servers.IcingaDBWeb import IcingaDBWebServer, strfdelta class IcingaDBWebNotificationsServer(IcingaDBWebServer): @@ -137,7 +112,7 @@ def _update_new_host_content(self) -> Result: self.new_hosts[host_name].status = self.STATES_MAPPING['hosts'][status_numeric] self.new_hosts[host_name].last_check = datetime.datetime.fromtimestamp(int(float(notification['host']['state']['last_update']))) self.new_hosts[host_name].attempt = "{}/{}".format(notification['host']['state']['check_attempt'],notification['host']['max_check_attempts']) - self.new_hosts[host_name].status_information = BeautifulSoup(notification['host']['state']['output'].replace('\n', ' ').strip(), 'html.parser').text + self.new_hosts[host_name].status_information = BeautifulSoup(notification['text'].replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].passiveonly = not int(notification['host'].get('active_checks_enabled') or '0') self.new_hosts[host_name].notifications_disabled = not int(notification['host'].get('notifications_enabled') or '0') self.new_hosts[host_name].flapping = bool(int(notification['host']['state']['is_flapping'] or 0)) @@ -209,7 +184,7 @@ def _update_new_host_content(self) -> Result: self.new_hosts[host_name].services[service_name].status = self.STATES_MAPPING['services'][status_numeric] self.new_hosts[host_name].services[service_name].last_check = datetime.datetime.fromtimestamp(int(float(notification['service']['state']['last_update']))) - self.new_hosts[host_name].services[service_name].status_information = BeautifulSoup(notification['service']['state']['output'].replace('\n', ' ').strip(), 'html.parser').text + self.new_hosts[host_name].services[service_name].status_information = BeautifulSoup(notification['text'].replace('\n', ' ').strip(), 'html.parser').text self.new_hosts[host_name].services[service_name].passiveonly = not int(notification['service'].get('active_checks_enabled') or '0') self.new_hosts[host_name].services[service_name].notifications_disabled = not int(notification['service'].get('notifications_enabled') or '0') self.new_hosts[host_name].services[service_name].flapping = bool(int(notification['service']['state']['is_flapping'] or 0)) @@ -243,4 +218,4 @@ def _update_new_host_content(self) -> Result: self.new_hosts[host_name].services[service_name].duration = 'n/a' # return success - return Result() \ No newline at end of file + return Result() diff --git a/Nagstamon/Servers/Oauth2WebFlow.py b/Nagstamon/Servers/Oauth2WebFlow.py new file mode 100644 index 000000000..d5a215c90 --- /dev/null +++ b/Nagstamon/Servers/Oauth2WebFlow.py @@ -0,0 +1,87 @@ +"""Oauth2 Authorization Code Grant/Web Application Flow/Standard Flow with local webserver.""" +import requests +import http.server +import socketserver +from urllib.parse import urlparse, parse_qs +from requests_oauthlib import OAuth2Session +import logging + +from Nagstamon.Helpers import webbrowser_open + +log = logging.getLogger(__name__) + + +class LocalOauthServer(http.server.BaseHTTPRequestHandler): + + """Local Oauth server to receive authorization code.""" + + def __init__(self, request, client_address, server): + super().__init__(request, client_address, server) + if not hasattr(server, "code"): + server.code = None + + def do_GET(self): + url_parse = urlparse(self.path) + params = parse_qs(url_parse.query) + if "code" in params: + self.server.code = params["code"][0] + print(params["code"][0]) + self.protocol_version = 'HTTP/1.1' + self.send_response(200, 'OK') + self.send_header('Content-type', 'text/html') + self.end_headers() + self.wfile.write(bytes( + "