From d90a20f8daa92d0eec6321cc4d5d8f6887cbf96a Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Fri, 13 Dec 2019 21:22:58 +0700 Subject: [PATCH 1/7] Fix #602 domain table with long records --- powerdnsadmin/static/custom/css/custom.css | 1 + powerdnsadmin/templates/domain.html | 4 ++-- powerdnsadmin/templates/template_edit.html | 8 ++++---- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/powerdnsadmin/static/custom/css/custom.css b/powerdnsadmin/static/custom/css/custom.css index 7b8e281..474d77a 100644 --- a/powerdnsadmin/static/custom/css/custom.css +++ b/powerdnsadmin/static/custom/css/custom.css @@ -1,5 +1,6 @@ .length-break { word-break: break-all !important; + width: 70% !important; } table td { diff --git a/powerdnsadmin/templates/domain.html b/powerdnsadmin/templates/domain.html index 2146bb3..90846de 100644 --- a/powerdnsadmin/templates/domain.html +++ b/powerdnsadmin/templates/domain.html @@ -138,7 +138,7 @@ "columnDefs": [ { type: 'natural', - targets: [0, 5] + targets: [0, 4] }, { // hidden column so that we can add new records on top @@ -148,7 +148,7 @@ }, { className: "length-break", - targets: [ 5 ] + targets: [ 4, 5 ] } ], "orderFixed": [[8, 'asc']] diff --git a/powerdnsadmin/templates/template_edit.html b/powerdnsadmin/templates/template_edit.html index 8c22ca9..497d82c 100644 --- a/powerdnsadmin/templates/template_edit.html +++ b/powerdnsadmin/templates/template_edit.html @@ -62,10 +62,10 @@ {{ record.ttl }} - + {{ record.data }} - + {{ record.comment }} @@ -123,7 +123,7 @@ "columnDefs": [ { type: 'natural', - targets: [0, 5] + targets: [0, 4] }, { // hidden column so that we can add new records on top @@ -133,7 +133,7 @@ }, { className: "length-break", - targets: [ 5 ] + targets: [ 4, 5 ] } ], "orderFixed": [[8, 'asc']] From c0594b2c0b5076a8bf49a7dd6f50962e5dc17b07 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Fri, 13 Dec 2019 21:55:11 +0700 Subject: [PATCH 2/7] Fix SAML --- powerdnsadmin/lib/utils.py | 98 +-------------------- powerdnsadmin/routes/index.py | 24 ++--- powerdnsadmin/services/saml.py | 156 +++++++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 107 deletions(-) create mode 100644 powerdnsadmin/services/saml.py diff --git a/powerdnsadmin/lib/utils.py b/powerdnsadmin/lib/utils.py index 821489f..9c34574 100644 --- a/powerdnsadmin/lib/utils.py +++ b/powerdnsadmin/lib/utils.py @@ -1,11 +1,10 @@ +import os import re import json import requests import hashlib import ipaddress -import os -# from app import app from distutils.version import StrictVersion from urllib.parse import urlparse from datetime import datetime, timedelta @@ -185,101 +184,6 @@ def email_to_gravatar_url(email="", size=100): return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size) -def prepare_flask_request(request): - # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields - url_data = urlparse(request.url) - return { - 'https': 'on' if request.scheme == 'https' else 'off', - 'http_host': request.host, - 'server_port': url_data.port, - 'script_name': request.path, - 'get_data': request.args.copy(), - 'post_data': request.form.copy(), - # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 - 'lowercase_urlencoding': True, - 'query_string': request.query_string - } - - -def init_saml_auth(req): - own_url = '' - if req['https'] == 'on': - own_url = 'https://' - else: - own_url = 'http://' - own_url += req['http_host'] - metadata = get_idp_data() - settings = {} - settings['sp'] = {} - if 'SAML_NAMEID_FORMAT' in app.config: - settings['sp']['NameIDFormat'] = app.config['SAML_NAMEID_FORMAT'] - else: - settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get( - 'NameIDFormat', - 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified') - settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID'] - if os.path.isfile(CERT_FILE): - cert = open(CERT_FILE, "r").readlines() - settings['sp']['x509cert'] = "".join(cert) - if os.path.isfile(KEY_FILE): - key = open(KEY_FILE, "r").readlines() - settings['sp']['privateKey'] = "".join(key) - settings['sp']['assertionConsumerService'] = {} - settings['sp']['assertionConsumerService'][ - 'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' - settings['sp']['assertionConsumerService'][ - 'url'] = own_url + '/saml/authorized' - settings['sp']['attributeConsumingService'] = {} - settings['sp']['singleLogoutService'] = {} - settings['sp']['singleLogoutService'][ - 'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' - settings['sp']['singleLogoutService']['url'] = own_url + '/saml/sls' - settings['idp'] = metadata['idp'] - settings['strict'] = True - settings['debug'] = app.config['SAML_DEBUG'] - settings['security'] = {} - settings['security'][ - 'digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' - settings['security']['metadataCacheDuration'] = None - settings['security']['metadataValidUntil'] = None - settings['security']['requestedAuthnContext'] = True - settings['security'][ - 'signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' - settings['security']['wantAssertionsEncrypted'] = False - settings['security']['wantAttributeStatement'] = True - settings['security']['wantNameId'] = True - settings['security']['authnRequestsSigned'] = app.config[ - 'SAML_SIGN_REQUEST'] - settings['security']['logoutRequestSigned'] = app.config[ - 'SAML_SIGN_REQUEST'] - settings['security']['logoutResponseSigned'] = app.config[ - 'SAML_SIGN_REQUEST'] - settings['security']['nameIdEncrypted'] = False - settings['security']['signMetadata'] = True - settings['security']['wantAssertionsSigned'] = True - settings['security']['wantMessagesSigned'] = app.config.get( - 'SAML_WANT_MESSAGE_SIGNED', True) - settings['security']['wantNameIdEncrypted'] = False - settings['contactPerson'] = {} - settings['contactPerson']['support'] = {} - settings['contactPerson']['support']['emailAddress'] = app.config[ - 'SAML_SP_CONTACT_NAME'] - settings['contactPerson']['support']['givenName'] = app.config[ - 'SAML_SP_CONTACT_MAIL'] - settings['contactPerson']['technical'] = {} - settings['contactPerson']['technical']['emailAddress'] = app.config[ - 'SAML_SP_CONTACT_NAME'] - settings['contactPerson']['technical']['givenName'] = app.config[ - 'SAML_SP_CONTACT_MAIL'] - settings['organization'] = {} - settings['organization']['en-US'] = {} - settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin' - settings['organization']['en-US']['name'] = 'PowerDNS-Admin' - settings['organization']['en-US']['url'] = own_url - auth = OneLogin_Saml2_Auth(req, settings) - return auth - - def display_setting_state(value): if value == 1: return "ON" diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index e244933..c4a3f3e 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -27,11 +27,13 @@ from ..services.google import google_oauth from ..services.github import github_oauth from ..services.azure import azure_oauth from ..services.oidc import oidc_oauth +from ..services.saml import SAML google = None github = None azure = None oidc = None +saml = None index_bp = Blueprint('index', __name__, @@ -45,10 +47,12 @@ def register_modules(): global github global azure global oidc + global saml google = google_oauth() github = github_oauth() azure = azure_oauth() oidc = oidc_oauth() + saml = SAML() @index_bp.before_request @@ -311,8 +315,8 @@ def logout(): 'SAML_ENABLED' ) and 'samlSessionIndex' in session and current_app.config.get( 'SAML_LOGOUT'): - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) + req = saml.prepare_flask_request(request) + auth = saml.init_saml_auth(req) if current_app.config.get('SAML_LOGOUT_URL'): return redirect( auth.logout( @@ -520,8 +524,8 @@ def dyndns_update(): def saml_login(): if not current_app.config.get('SAML_ENABLED'): abort(400) - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) + req = saml.prepare_flask_request(request) + auth = saml.init_saml_auth(req) redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for( 'saml_authorized') return redirect(auth.login(return_to=redirect_url)) @@ -533,8 +537,8 @@ def saml_metadata(): current_app.logger.error("SAML authentication is disabled.") abort(400) - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) + req = saml.prepare_flask_request(request) + auth = saml.init_saml_auth(req) settings = auth.get_settings() metadata = settings.get_sp_metadata() errors = settings.validate_metadata(metadata) @@ -553,8 +557,8 @@ def saml_authorized(): if not current_app.config.get('SAML_ENABLED'): current_app.logger.error("SAML authentication is disabled.") abort(400) - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) + req = saml.prepare_flask_request(request) + auth = saml.init_saml_auth(req) auth.process_response() errors = auth.get_errors() if len(errors) == 0: @@ -705,8 +709,8 @@ def uplift_to_admin(user): @index_bp.route('/saml/sls') def saml_logout(): - req = utils.prepare_flask_request(request) - auth = utils.init_saml_auth(req) + req = saml.prepare_flask_request(request) + auth = saml.init_saml_auth(req) url = auth.process_slo() errors = auth.get_errors() if len(errors) == 0: diff --git a/powerdnsadmin/services/saml.py b/powerdnsadmin/services/saml.py new file mode 100644 index 0000000..00b3652 --- /dev/null +++ b/powerdnsadmin/services/saml.py @@ -0,0 +1,156 @@ +from datetime import datetime, timedelta +from threading import Thread +from flask import current_app + +from ..lib.certutil import KEY_FILE, CERT_FILE + + +class SAML(object): + def __init__(self): + if current_app.config['SAML_ENABLED']: + from onelogin.saml2.auth import OneLogin_Saml2_Auth + from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser + idp_timestamp = datetime(1970, 1, 1) + idp_data = None + if 'SAML_IDP_ENTITY_ID' in current_app.config: + idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + current_app.config['SAML_METADATA_URL'], + entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', + None), + required_sso_binding=current_app. + config['SAML_IDP_SSO_BINDING']) + else: + idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + current_app.config['SAML_METADATA_URL'], + entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', + None)) + if idp_data is None: + current_app.logger.info( + 'SAML: IDP Metadata initial load failed') + exit(-1) + idp_timestamp = datetime.now() + + def get_idp_data(): + global idp_data, idp_timestamp + lifetime = timedelta( + minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) + if idp_timestamp + lifetime < datetime.now(): + background_thread = Thread(target=retrieve_idp_data) + background_thread.start() + return idp_data + + def retrieve_idp_data(): + global idp_data, idp_timestamp + if 'SAML_IDP_SSO_BINDING' in current_app.config: + new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + current_app.config['SAML_METADATA_URL'], + entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None), + required_sso_binding=current_app.config['SAML_IDP_SSO_BINDING'] + ) + else: + new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + current_app.config['SAML_METADATA_URL'], + entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None)) + if new_idp_data is not None: + idp_data = new_idp_data + idp_timestamp = datetime.now() + current_app.logger.info( + "SAML: IDP Metadata successfully retrieved from: " + + current_app.config['SAML_METADATA_URL']) + else: + current_app.logger.info( + "SAML: IDP Metadata could not be retrieved") + + def prepare_flask_request(request): + # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields + url_data = urlparse(request.url) + return { + 'https': 'on' if request.scheme == 'https' else 'off', + 'http_host': request.host, + 'server_port': url_data.port, + 'script_name': request.path, + 'get_data': request.args.copy(), + 'post_data': request.form.copy(), + # Uncomment if using ADFS as IdP, https://github.com/onelogin/python-saml/pull/144 + 'lowercase_urlencoding': True, + 'query_string': request.query_string + } + + def init_saml_auth(req): + own_url = '' + if req['https'] == 'on': + own_url = 'https://' + else: + own_url = 'http://' + own_url += req['http_host'] + metadata = get_idp_data() + settings = {} + settings['sp'] = {} + if 'SAML_NAMEID_FORMAT' in current_app.config: + settings['sp']['NameIDFormat'] = current_app.config[ + 'SAML_NAMEID_FORMAT'] + else: + settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get( + 'NameIDFormat', + 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified') + settings['sp']['entityId'] = current_app.config['SAML_SP_ENTITY_ID'] + if os.path.isfile(CERT_FILE): + cert = open(CERT_FILE, "r").readlines() + settings['sp']['x509cert'] = "".join(cert) + if os.path.isfile(KEY_FILE): + key = open(KEY_FILE, "r").readlines() + settings['sp']['privateKey'] = "".join(key) + settings['sp']['assertionConsumerService'] = {} + settings['sp']['assertionConsumerService'][ + 'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' + settings['sp']['assertionConsumerService'][ + 'url'] = own_url + '/saml/authorized' + settings['sp']['attributeConsumingService'] = {} + settings['sp']['singleLogoutService'] = {} + settings['sp']['singleLogoutService'][ + 'binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + settings['sp']['singleLogoutService']['url'] = own_url + '/saml/sls' + settings['idp'] = metadata['idp'] + settings['strict'] = True + settings['debug'] = current_app.config['SAML_DEBUG'] + settings['security'] = {} + settings['security'][ + 'digestAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + settings['security']['metadataCacheDuration'] = None + settings['security']['metadataValidUntil'] = None + settings['security']['requestedAuthnContext'] = True + settings['security'][ + 'signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' + settings['security']['wantAssertionsEncrypted'] = False + settings['security']['wantAttributeStatement'] = True + settings['security']['wantNameId'] = True + settings['security']['authnRequestsSigned'] = current_app.config[ + 'SAML_SIGN_REQUEST'] + settings['security']['logoutRequestSigned'] = current_app.config[ + 'SAML_SIGN_REQUEST'] + settings['security']['logoutResponseSigned'] = current_app.config[ + 'SAML_SIGN_REQUEST'] + settings['security']['nameIdEncrypted'] = False + settings['security']['signMetadata'] = True + settings['security']['wantAssertionsSigned'] = True + settings['security']['wantMessagesSigned'] = current_app.config.get( + 'SAML_WANT_MESSAGE_SIGNED', True) + settings['security']['wantNameIdEncrypted'] = False + settings['contactPerson'] = {} + settings['contactPerson']['support'] = {} + settings['contactPerson']['support'][ + 'emailAddress'] = current_app.config['SAML_SP_CONTACT_NAME'] + settings['contactPerson']['support']['givenName'] = current_app.config[ + 'SAML_SP_CONTACT_MAIL'] + settings['contactPerson']['technical'] = {} + settings['contactPerson']['technical'][ + 'emailAddress'] = current_app.config['SAML_SP_CONTACT_NAME'] + settings['contactPerson']['technical'][ + 'givenName'] = current_app.config['SAML_SP_CONTACT_MAIL'] + settings['organization'] = {} + settings['organization']['en-US'] = {} + settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin' + settings['organization']['en-US']['name'] = 'PowerDNS-Admin' + settings['organization']['en-US']['url'] = own_url + auth = OneLogin_Saml2_Auth(req, settings) + return auth \ No newline at end of file From ad6b04bd785d2c78ecc9963e21c7f0dcd12ab7e7 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Sat, 14 Dec 2019 08:31:23 +0700 Subject: [PATCH 3/7] LGTM fixes --- powerdnsadmin/lib/utils.py | 3 --- powerdnsadmin/services/saml.py | 33 +++++++++++++++------------------ 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/powerdnsadmin/lib/utils.py b/powerdnsadmin/lib/utils.py index 9c34574..af8dd9b 100644 --- a/powerdnsadmin/lib/utils.py +++ b/powerdnsadmin/lib/utils.py @@ -1,4 +1,3 @@ -import os import re import json import requests @@ -9,8 +8,6 @@ from distutils.version import StrictVersion from urllib.parse import urlparse from datetime import datetime, timedelta -from .certutil import KEY_FILE, CERT_FILE - def auth_from_url(url): auth = None diff --git a/powerdnsadmin/services/saml.py b/powerdnsadmin/services/saml.py index 00b3652..0d9276c 100644 --- a/powerdnsadmin/services/saml.py +++ b/powerdnsadmin/services/saml.py @@ -10,37 +10,34 @@ class SAML(object): if current_app.config['SAML_ENABLED']: from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser - idp_timestamp = datetime(1970, 1, 1) - idp_data = None + self.idp_timestamp = datetime.now() + self.idp_data = None if 'SAML_IDP_ENTITY_ID' in current_app.config: - idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=current_app. config['SAML_IDP_SSO_BINDING']) else: - idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None)) - if idp_data is None: + if self.idp_data is None: current_app.logger.info( 'SAML: IDP Metadata initial load failed') exit(-1) - idp_timestamp = datetime.now() - def get_idp_data(): - global idp_data, idp_timestamp + def get_idp_data(self): lifetime = timedelta( minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) - if idp_timestamp + lifetime < datetime.now(): - background_thread = Thread(target=retrieve_idp_data) + if self.idp_timestamp + lifetime < datetime.now(): + background_thread = Thread(target=self.retrieve_idp_data) background_thread.start() - return idp_data + return self.idp_data - def retrieve_idp_data(): - global idp_data, idp_timestamp + def retrieve_idp_data(self): if 'SAML_IDP_SSO_BINDING' in current_app.config: new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], @@ -52,8 +49,8 @@ class SAML(object): current_app.config['SAML_METADATA_URL'], entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None)) if new_idp_data is not None: - idp_data = new_idp_data - idp_timestamp = datetime.now() + self.idp_data = new_idp_data + self.idp_timestamp = datetime.now() current_app.logger.info( "SAML: IDP Metadata successfully retrieved from: " + current_app.config['SAML_METADATA_URL']) @@ -61,7 +58,7 @@ class SAML(object): current_app.logger.info( "SAML: IDP Metadata could not be retrieved") - def prepare_flask_request(request): + def prepare_flask_request(self, request): # If server is behind proxys or balancers use the HTTP_X_FORWARDED fields url_data = urlparse(request.url) return { @@ -76,14 +73,14 @@ class SAML(object): 'query_string': request.query_string } - def init_saml_auth(req): + def init_saml_auth(self, req): own_url = '' if req['https'] == 'on': own_url = 'https://' else: own_url = 'http://' own_url += req['http_host'] - metadata = get_idp_data() + metadata = self.get_idp_data() settings = {} settings['sp'] = {} if 'SAML_NAMEID_FORMAT' in current_app.config: From cd3535dcd25416e4590687df21f057c288a34aef Mon Sep 17 00:00:00 2001 From: Neven1986 Date: Sat, 14 Dec 2019 18:59:59 +0100 Subject: [PATCH 4/7] - Variable references inside SAML class were fixed - Function signatures inside SAML class were fixed - Redirect URL for /saml/login path was modified (saml_authorized -> index.saml_authorized) Current status is that SAML metadata can be generated under /saml/metadata and communication to SAML iDP is working Problems remaining: - SAML Response doesn't contain any attributes (There is no AttributeStatement on the Response). It can be that problem is on iDP side - Background thread in retrieve_idp_data() cannot be spawned, this part is currently commented out, old code needs to be revisited --- powerdnsadmin/routes/index.py | 2 +- powerdnsadmin/services/saml.py | 46 +++++++++++++++++++++------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index c4a3f3e..d10b690 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -527,7 +527,7 @@ def saml_login(): req = saml.prepare_flask_request(request) auth = saml.init_saml_auth(req) redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for( - 'saml_authorized') + 'index.saml_authorized') return redirect(auth.login(return_to=redirect_url)) diff --git a/powerdnsadmin/services/saml.py b/powerdnsadmin/services/saml.py index 0d9276c..9825b21 100644 --- a/powerdnsadmin/services/saml.py +++ b/powerdnsadmin/services/saml.py @@ -1,17 +1,22 @@ from datetime import datetime, timedelta from threading import Thread from flask import current_app +import os from ..lib.certutil import KEY_FILE, CERT_FILE - +from ..lib.utils import urlparse class SAML(object): def __init__(self): if current_app.config['SAML_ENABLED']: from onelogin.saml2.auth import OneLogin_Saml2_Auth from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser + self.idp_timestamp = datetime.now() + self.OneLogin_Saml2_Auth = OneLogin_Saml2_Auth + self.OneLogin_Saml2_IdPMetadataParser = OneLogin_Saml2_IdPMetadataParser self.idp_data = None + if 'SAML_IDP_ENTITY_ID' in current_app.config: self.idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], @@ -30,31 +35,38 @@ class SAML(object): exit(-1) def get_idp_data(self): - lifetime = timedelta( - minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) - if self.idp_timestamp + lifetime < datetime.now(): - background_thread = Thread(target=self.retrieve_idp_data) - background_thread.start() + +### Currently commented out while throwing exception, +### will take a look soon +# lifetime = timedelta( +# minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) +# if self.idp_timestamp + lifetime < datetime.now(): +# background_thread = Thread(target=self.retrieve_idp_data) +# background_thread.start() + + + self.retrieve_idp_data() return self.idp_data def retrieve_idp_data(self): - if 'SAML_IDP_SSO_BINDING' in current_app.config: - new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + + if 'SAML_IDP_SSO_BINDING' in current_app.config: + new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=current_app.config['SAML_IDP_SSO_BINDING'] ) - else: - new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote( + else: + new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None)) - if new_idp_data is not None: + if new_idp_data is not None: self.idp_data = new_idp_data self.idp_timestamp = datetime.now() current_app.logger.info( "SAML: IDP Metadata successfully retrieved from: " + current_app.config['SAML_METADATA_URL']) - else: + else: current_app.logger.info( "SAML: IDP Metadata could not be retrieved") @@ -87,7 +99,7 @@ class SAML(object): settings['sp']['NameIDFormat'] = current_app.config[ 'SAML_NAMEID_FORMAT'] else: - settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get( + settings['sp']['NameIDFormat'] = self.idp_data.get('sp', {}).get( 'NameIDFormat', 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified') settings['sp']['entityId'] = current_app.config['SAML_SP_ENTITY_ID'] @@ -118,7 +130,7 @@ class SAML(object): settings['security']['requestedAuthnContext'] = True settings['security'][ 'signatureAlgorithm'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256' - settings['security']['wantAssertionsEncrypted'] = False + settings['security']['wantAssertionsEncrypted'] = True settings['security']['wantAttributeStatement'] = True settings['security']['wantNameId'] = True settings['security']['authnRequestsSigned'] = current_app.config[ @@ -141,13 +153,13 @@ class SAML(object): 'SAML_SP_CONTACT_MAIL'] settings['contactPerson']['technical'] = {} settings['contactPerson']['technical'][ - 'emailAddress'] = current_app.config['SAML_SP_CONTACT_NAME'] + 'emailAddress'] = current_app.config['SAML_SP_CONTACT_MAIL'] settings['contactPerson']['technical'][ - 'givenName'] = current_app.config['SAML_SP_CONTACT_MAIL'] + 'givenName'] = current_app.config['SAML_SP_CONTACT_NAME'] settings['organization'] = {} settings['organization']['en-US'] = {} settings['organization']['en-US']['displayname'] = 'PowerDNS-Admin' settings['organization']['en-US']['name'] = 'PowerDNS-Admin' settings['organization']['en-US']['url'] = own_url - auth = OneLogin_Saml2_Auth(req, settings) + auth = self.OneLogin_Saml2_Auth(req, settings) return auth \ No newline at end of file From 894756ad961f4d1c81077aadcd7b4118acb1681d Mon Sep 17 00:00:00 2001 From: Neven1986 Date: Sat, 14 Dec 2019 21:45:51 +0100 Subject: [PATCH 5/7] Invocation of separate threa in get_idp_data() problem resolved --- powerdnsadmin/services/saml.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/powerdnsadmin/services/saml.py b/powerdnsadmin/services/saml.py index 9825b21..ac5ad70 100644 --- a/powerdnsadmin/services/saml.py +++ b/powerdnsadmin/services/saml.py @@ -34,20 +34,18 @@ class SAML(object): 'SAML: IDP Metadata initial load failed') exit(-1) + def get_idp_data(self): -### Currently commented out while throwing exception, -### will take a look soon -# lifetime = timedelta( -# minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) -# if self.idp_timestamp + lifetime < datetime.now(): -# background_thread = Thread(target=self.retrieve_idp_data) -# background_thread.start() + lifetime = timedelta(minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) + if self.idp_timestamp + lifetime < datetime.now(): + background_thread = Thread(target=self.retrieve_idp_data()) + background_thread.start() - self.retrieve_idp_data() return self.idp_data + def retrieve_idp_data(self): if 'SAML_IDP_SSO_BINDING' in current_app.config: @@ -162,4 +160,4 @@ class SAML(object): settings['organization']['en-US']['name'] = 'PowerDNS-Admin' settings['organization']['en-US']['url'] = own_url auth = self.OneLogin_Saml2_Auth(req, settings) - return auth \ No newline at end of file + return auth From 37f24f9fde73232d88c90a8f7af49c45d5078b23 Mon Sep 17 00:00:00 2001 From: Neven1986 Date: Sun, 15 Dec 2019 01:15:30 +0100 Subject: [PATCH 6/7] Correction of redirect URL after successful SAML login From my perspective, if agreed, this change can be merged, because the basic SAM auth. functionality is now present and was tested with "samlidp.io" iDP. However, there are further improvements which I would like to integrate, but as a separate features in separate pull requests --- powerdnsadmin/routes/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index d10b690..097e92c 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -663,7 +663,7 @@ def saml_authorized(): user.update_profile() session['authentication_type'] = 'SAML' login_user(user, remember=False) - return redirect(url_for('index')) + return redirect(url_for('index.login')) else: return render_template('errors/SAML.html', errors=errors) From de581e9e1d92d0e92f5547c1a3b91b875d2e02a3 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Sun, 15 Dec 2019 09:40:05 +0700 Subject: [PATCH 7/7] Yapf code formatting --- powerdnsadmin/services/saml.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/powerdnsadmin/services/saml.py b/powerdnsadmin/services/saml.py index ac5ad70..c131826 100644 --- a/powerdnsadmin/services/saml.py +++ b/powerdnsadmin/services/saml.py @@ -6,6 +6,7 @@ import os from ..lib.certutil import KEY_FILE, CERT_FILE from ..lib.utils import urlparse + class SAML(object): def __init__(self): if current_app.config['SAML_ENABLED']: @@ -34,10 +35,10 @@ class SAML(object): 'SAML: IDP Metadata initial load failed') exit(-1) - def get_idp_data(self): - lifetime = timedelta(minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) + lifetime = timedelta( + minutes=current_app.config['SAML_METADATA_CACHE_LIFETIME']) if self.idp_timestamp + lifetime < datetime.now(): background_thread = Thread(target=self.retrieve_idp_data()) @@ -45,26 +46,25 @@ class SAML(object): return self.idp_data - def retrieve_idp_data(self): - if 'SAML_IDP_SSO_BINDING' in current_app.config: + if 'SAML_IDP_SSO_BINDING' in current_app.config: new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None), required_sso_binding=current_app.config['SAML_IDP_SSO_BINDING'] ) - else: + else: new_idp_data = self.OneLogin_Saml2_IdPMetadataParser.parse_remote( current_app.config['SAML_METADATA_URL'], entity_id=current_app.config.get('SAML_IDP_ENTITY_ID', None)) - if new_idp_data is not None: + if new_idp_data is not None: self.idp_data = new_idp_data self.idp_timestamp = datetime.now() current_app.logger.info( "SAML: IDP Metadata successfully retrieved from: " + current_app.config['SAML_METADATA_URL']) - else: + else: current_app.logger.info( "SAML: IDP Metadata could not be retrieved")