Merge branch 'master' into record_adjustment

This commit is contained in:
Khanh Ngo 2019-12-16 15:54:38 +07:00 committed by GitHub
commit f5b88ebc73
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 186 additions and 116 deletions

View file

@ -3,14 +3,11 @@ import json
import requests import requests
import hashlib import hashlib
import ipaddress import ipaddress
import os
from distutils.version import StrictVersion from distutils.version import StrictVersion
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime, timedelta from datetime import datetime, timedelta
from .certutil import KEY_FILE, CERT_FILE
def auth_from_url(url): def auth_from_url(url):
auth = None auth = None
@ -184,101 +181,6 @@ def email_to_gravatar_url(email="", size=100):
return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size) 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): def display_setting_state(value):
if value == 1: if value == 1:
return "ON" return "ON"

View file

@ -28,11 +28,13 @@ from ..services.google import google_oauth
from ..services.github import github_oauth from ..services.github import github_oauth
from ..services.azure import azure_oauth from ..services.azure import azure_oauth
from ..services.oidc import oidc_oauth from ..services.oidc import oidc_oauth
from ..services.saml import SAML
google = None google = None
github = None github = None
azure = None azure = None
oidc = None oidc = None
saml = None
index_bp = Blueprint('index', index_bp = Blueprint('index',
__name__, __name__,
@ -46,10 +48,12 @@ def register_modules():
global github global github
global azure global azure
global oidc global oidc
global saml
google = google_oauth() google = google_oauth()
github = github_oauth() github = github_oauth()
azure = azure_oauth() azure = azure_oauth()
oidc = oidc_oauth() oidc = oidc_oauth()
saml = SAML()
@index_bp.before_request @index_bp.before_request
@ -351,8 +355,8 @@ def logout():
'SAML_ENABLED' 'SAML_ENABLED'
) and 'samlSessionIndex' in session and current_app.config.get( ) and 'samlSessionIndex' in session and current_app.config.get(
'SAML_LOGOUT'): 'SAML_LOGOUT'):
req = utils.prepare_flask_request(request) req = saml.prepare_flask_request(request)
auth = utils.init_saml_auth(req) auth = saml.init_saml_auth(req)
if current_app.config.get('SAML_LOGOUT_URL'): if current_app.config.get('SAML_LOGOUT_URL'):
return redirect( return redirect(
auth.logout( auth.logout(
@ -579,10 +583,10 @@ def dyndns_update():
def saml_login(): def saml_login():
if not current_app.config.get('SAML_ENABLED'): if not current_app.config.get('SAML_ENABLED'):
abort(400) abort(400)
req = utils.prepare_flask_request(request) req = saml.prepare_flask_request(request)
auth = utils.init_saml_auth(req) auth = saml.init_saml_auth(req)
redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for( redirect_url = OneLogin_Saml2_Utils.get_self_url(req) + url_for(
'saml_authorized') 'index.saml_authorized')
return redirect(auth.login(return_to=redirect_url)) return redirect(auth.login(return_to=redirect_url))
@ -592,8 +596,8 @@ def saml_metadata():
current_app.logger.error("SAML authentication is disabled.") current_app.logger.error("SAML authentication is disabled.")
abort(400) abort(400)
req = utils.prepare_flask_request(request) req = saml.prepare_flask_request(request)
auth = utils.init_saml_auth(req) auth = saml.init_saml_auth(req)
settings = auth.get_settings() settings = auth.get_settings()
metadata = settings.get_sp_metadata() metadata = settings.get_sp_metadata()
errors = settings.validate_metadata(metadata) errors = settings.validate_metadata(metadata)
@ -612,8 +616,8 @@ def saml_authorized():
if not current_app.config.get('SAML_ENABLED'): if not current_app.config.get('SAML_ENABLED'):
current_app.logger.error("SAML authentication is disabled.") current_app.logger.error("SAML authentication is disabled.")
abort(400) abort(400)
req = utils.prepare_flask_request(request) req = saml.prepare_flask_request(request)
auth = utils.init_saml_auth(req) auth = saml.init_saml_auth(req)
auth.process_response() auth.process_response()
errors = auth.get_errors() errors = auth.get_errors()
if len(errors) == 0: if len(errors) == 0:
@ -719,7 +723,7 @@ def saml_authorized():
session['authentication_type'] = 'SAML' session['authentication_type'] = 'SAML'
login_user(user, remember=False) login_user(user, remember=False)
signin_history(user.username, 'SAML', True) signin_history(user.username, 'SAML', True)
return redirect(url_for('index')) return redirect(url_for('index.login'))
else: else:
return render_template('errors/SAML.html', errors=errors) return render_template('errors/SAML.html', errors=errors)
@ -765,8 +769,8 @@ def uplift_to_admin(user):
@index_bp.route('/saml/sls') @index_bp.route('/saml/sls')
def saml_logout(): def saml_logout():
req = utils.prepare_flask_request(request) req = saml.prepare_flask_request(request)
auth = utils.init_saml_auth(req) auth = saml.init_saml_auth(req)
url = auth.process_slo() url = auth.process_slo()
errors = auth.get_errors() errors = auth.get_errors()
if len(errors) == 0: if len(errors) == 0:

View file

@ -0,0 +1,163 @@
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'],
entity_id=current_app.config.get('SAML_IDP_ENTITY_ID',
None),
required_sso_binding=current_app.
config['SAML_IDP_SSO_BINDING'])
else:
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 self.idp_data is None:
current_app.logger.info(
'SAML: IDP Metadata initial load failed')
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()
return self.idp_data
def retrieve_idp_data(self):
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 = 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:
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:
current_app.logger.info(
"SAML: IDP Metadata could not be retrieved")
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 {
'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(self, req):
own_url = ''
if req['https'] == 'on':
own_url = 'https://'
else:
own_url = 'http://'
own_url += req['http_host']
metadata = self.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'] = 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']
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'] = True
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_MAIL']
settings['contactPerson']['technical'][
'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 = self.OneLogin_Saml2_Auth(req, settings)
return auth

View file

@ -1,5 +1,6 @@
.length-break { .length-break {
word-break: break-all !important; word-break: break-all !important;
width: 70% !important;
} }
table td { table td {

View file

@ -138,7 +138,7 @@
"columnDefs": [ "columnDefs": [
{ {
type: 'natural', type: 'natural',
targets: [0, 5] targets: [0, 4]
}, },
{ {
// hidden column so that we can add new records on top // hidden column so that we can add new records on top
@ -148,7 +148,7 @@
}, },
{ {
className: "length-break", className: "length-break",
targets: [ 5 ] targets: [ 4, 5 ]
} }
], ],
"orderFixed": [[8, 'asc']] "orderFixed": [[8, 'asc']]

View file

@ -62,10 +62,10 @@
<td> <td>
{{ record.ttl }} {{ record.ttl }}
</td> </td>
<td class="length-break"> <td>
{{ record.data }} {{ record.data }}
</td> </td>
<td class="length-break"> <td>
{{ record.comment }} {{ record.comment }}
</td> </td>
<td width="6%"> <td width="6%">
@ -123,7 +123,7 @@
"columnDefs": [ "columnDefs": [
{ {
type: 'natural', type: 'natural',
targets: [0, 5] targets: [0, 4]
}, },
{ {
// hidden column so that we can add new records on top // hidden column so that we can add new records on top
@ -133,7 +133,7 @@
}, },
{ {
className: "length-break", className: "length-break",
targets: [ 5 ] targets: [ 4, 5 ]
} }
], ],
"orderFixed": [[8, 'asc']] "orderFixed": [[8, 'asc']]