Merge branch 'improve-saml-support'

This commit is contained in:
Khanh Ngo 2019-03-04 09:19:20 +07:00
commit ee2ed65ff9
5 changed files with 151 additions and 27 deletions

View file

@ -3,6 +3,7 @@ import json
import requests import requests
import hashlib import hashlib
import ipaddress import ipaddress
import os
from app import app from app import app
from distutils.version import StrictVersion from distutils.version import StrictVersion
@ -244,10 +245,12 @@ def init_saml_auth(req):
else: else:
settings['sp']['NameIDFormat'] = idp_data.get('sp', {}).get('NameIDFormat', 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified') 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'] settings['sp']['entityId'] = app.config['SAML_SP_ENTITY_ID']
cert = open(CERT_FILE, "r").readlines() if os.path.isfile(CERT_FILE):
key = open(KEY_FILE, "r").readlines() cert = open(CERT_FILE, "r").readlines()
settings['sp']['privateKey'] = "".join(key) settings['sp']['x509cert'] = "".join(cert)
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'] = {}
settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST' settings['sp']['assertionConsumerService']['binding'] = 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST'
settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized' settings['sp']['assertionConsumerService']['url'] = own_url+'/saml/authorized'
@ -273,7 +276,7 @@ def init_saml_auth(req):
settings['security']['nameIdEncrypted'] = False settings['security']['nameIdEncrypted'] = False
settings['security']['signMetadata'] = True settings['security']['signMetadata'] = True
settings['security']['wantAssertionsSigned'] = True settings['security']['wantAssertionsSigned'] = True
settings['security']['wantMessagesSigned'] = True settings['security']['wantMessagesSigned'] = app.config.get('SAML_WANT_MESSAGE_SIGNED', True)
settings['security']['wantNameIdEncrypted'] = False settings['security']['wantNameIdEncrypted'] = False
settings['contactPerson'] = {} settings['contactPerson'] = {}
settings['contactPerson']['support'] = {} settings['contactPerson']['support'] = {}

View file

@ -293,27 +293,42 @@ def saml_authorized():
email_attribute_name = app.config.get('SAML_ATTRIBUTE_EMAIL', 'email') email_attribute_name = app.config.get('SAML_ATTRIBUTE_EMAIL', 'email')
givenname_attribute_name = app.config.get('SAML_ATTRIBUTE_GIVENNAME', 'givenname') givenname_attribute_name = app.config.get('SAML_ATTRIBUTE_GIVENNAME', 'givenname')
surname_attribute_name = app.config.get('SAML_ATTRIBUTE_SURNAME', 'surname') surname_attribute_name = app.config.get('SAML_ATTRIBUTE_SURNAME', 'surname')
name_attribute_name = app.config.get('SAML_ATTRIBUTE_NAME', None)
account_attribute_name = app.config.get('SAML_ATTRIBUTE_ACCOUNT', None) account_attribute_name = app.config.get('SAML_ATTRIBUTE_ACCOUNT', None)
admin_attribute_name = app.config.get('SAML_ATTRIBUTE_ADMIN', None) admin_attribute_name = app.config.get('SAML_ATTRIBUTE_ADMIN', None)
group_attribute_name = app.config.get('SAML_ATTRIBUTE_GROUP', None)
admin_group_name = app.config.get('SAML_GROUP_ADMIN_NAME', None)
group_to_account_mapping = create_group_to_account_mapping()
if email_attribute_name in session['samlUserdata']: if email_attribute_name in session['samlUserdata']:
user.email = session['samlUserdata'][email_attribute_name][0].lower() user.email = session['samlUserdata'][email_attribute_name][0].lower()
if givenname_attribute_name in session['samlUserdata']: if givenname_attribute_name in session['samlUserdata']:
user.firstname = session['samlUserdata'][givenname_attribute_name][0] user.firstname = session['samlUserdata'][givenname_attribute_name][0]
if surname_attribute_name in session['samlUserdata']: if surname_attribute_name in session['samlUserdata']:
user.lastname = session['samlUserdata'][surname_attribute_name][0] user.lastname = session['samlUserdata'][surname_attribute_name][0]
if admin_attribute_name: if name_attribute_name in session['samlUserdata']:
name = session['samlUserdata'][name_attribute_name][0].split(' ')
user.firstname = name[0]
user.lastname = ' '.join(name[1:])
if group_attribute_name:
user_groups = session['samlUserdata'].get(group_attribute_name, [])
else:
user_groups = []
if admin_attribute_name or group_attribute_name:
user_accounts = set(user.get_account()) user_accounts = set(user.get_account())
saml_accounts = [] saml_accounts = []
for group_mapping in group_to_account_mapping:
mapping = group_mapping.split('=')
group = mapping[0]
account_name = mapping[1]
if group in user_groups:
account = handle_account(account_name)
saml_accounts.append(account)
for account_name in session['samlUserdata'].get(account_attribute_name, []): for account_name in session['samlUserdata'].get(account_attribute_name, []):
clean_name = ''.join(c for c in account_name.lower() if c in "abcdefghijklmnopqrstuvwxyz0123456789") account = handle_account(account_name)
if len(clean_name) > Account.name.type.length:
logging.error("Account name {0} too long. Truncated.".format(clean_name))
account = Account.query.filter_by(name=clean_name).first()
if not account:
account = Account(name=clean_name.lower(), description='', contact='', mail='')
account.create_account()
history = History(msg='Account {0} created'.format(account.name), created_by='SAML Assertion')
history.add()
saml_accounts.append(account) saml_accounts.append(account)
saml_accounts = set(saml_accounts) saml_accounts = set(saml_accounts)
for account in saml_accounts - user_accounts: for account in saml_accounts - user_accounts:
@ -324,17 +339,13 @@ def saml_authorized():
account.remove_user(user) account.remove_user(user)
history = History(msg='Removing {0} from account {1}'.format(user.username, account.name), created_by='SAML Assertion') history = History(msg='Removing {0} from account {1}'.format(user.username, account.name), created_by='SAML Assertion')
history.add() history.add()
if admin_attribute_name: if admin_attribute_name and 'true' in session['samlUserdata'].get(admin_attribute_name, []):
if 'true' in session['samlUserdata'].get(admin_attribute_name, []): uplift_to_admin(user)
admin_role = Role.query.filter_by(name='Administrator').first().id elif admin_group_name in user_groups:
if user.role_id != admin_role: uplift_to_admin(user)
user.role_id = admin_role elif admin_attribute_name or group_attribute_name:
history = History(msg='Promoting {0} to administrator'.format(user.username), created_by='SAML Assertion') if user.role.name != 'User':
history.add() user.role_id = Role.query.filter_by(name='User').first().id
else:
user_role = Role.query.filter_by(name='User').first().id
if user.role_id != user_role:
user.role_id = user_role
history = History(msg='Demoting {0} to user'.format(user.username), created_by='SAML Assertion') history = History(msg='Demoting {0} to user'.format(user.username), created_by='SAML Assertion')
history.add() history.add()
user.plain_text_password = None user.plain_text_password = None
@ -343,7 +354,36 @@ def saml_authorized():
login_user(user, remember=False) login_user(user, remember=False)
return redirect(url_for('index')) return redirect(url_for('index'))
else: else:
return render_template('errors/SAML.html', errors=errors) return render_template('errors/SAML.html', errors=errors)
def create_group_to_account_mapping():
group_to_account_mapping_string = app.config.get('SAML_GROUP_TO_ACCOUNT_MAPPING', None)
if group_to_account_mapping_string and len(group_to_account_mapping_string.strip()) > 0:
group_to_account_mapping = group_to_account_mapping_string.split(',')
else:
group_to_account_mapping = []
return group_to_account_mapping
def handle_account(account_name):
clean_name = ''.join(c for c in account_name.lower() if c in "abcdefghijklmnopqrstuvwxyz0123456789")
if len(clean_name) > Account.name.type.length:
logging.error("Account name {0} too long. Truncated.".format(clean_name))
account = Account.query.filter_by(name=clean_name).first()
if not account:
account = Account(name=clean_name.lower(), description='', contact='', mail='')
account.create_account()
history = History(msg='Account {0} created'.format(account.name), created_by='SAML Assertion')
history.add()
return account
def uplift_to_admin(user):
if user.role.name != 'Administrator':
user.role_id = Role.query.filter_by(name='Administrator').first().id
history = History(msg='Promoting {0} to administrator'.format(user.username), created_by='SAML Assertion')
history.add()
@login_manager.unauthorized_handler @login_manager.unauthorized_handler

View file

@ -71,6 +71,12 @@ SAML_METADATA_CACHE_LIFETIME = 1
### Example: urn:oid:2.5.4.4 ### Example: urn:oid:2.5.4.4
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4' #SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
## Split into Given name and Surname
## Useful if your IDP only gives a display name
### Default: none
### Example: http://schemas.microsoft.com/identity/claims/displayname
#SAML_ATTRIBUTE_NAME = 'http://schemas.microsoft.com/identity/claims/displayname'
## Attribute to use for username ## Attribute to use for username
### Default: Use NameID instead ### Default: Use NameID instead
### Example: urn:oid:0.9.2342.19200300.100.1.1 ### Example: urn:oid:0.9.2342.19200300.100.1.1
@ -84,6 +90,22 @@ SAML_METADATA_CACHE_LIFETIME = 1
### the user is set as a non-administrator user. ### the user is set as a non-administrator user.
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin' #SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
## Attribute to get group from
### Default: Don't use groups from SAML attribute
### Example: https://example.edu/pdns-admin-group
#SAML_ATTRIBUTE_GROUP = 'https://example.edu/pdns-admin'
## Group namem to get admin status from
### Default: Don't control admin with SAML group
### Example: https://example.edu/pdns-admin
#SAML_GROUP_ADMIN_NAME = 'powerdns-admin'
## Attribute to get group to account mappings from
### Default: None
### If set, the user will be added and removed from accounts to match
### what's in the login assertion if they are in the required group
#SAML_GROUP_TO_ACCOUNT_MAPPING = 'dev-admins=dev,prod-admins=prod'
## Attribute to get account names from ## Attribute to get account names from
### Default: Don't control accounts with SAML attribute ### Default: Don't control accounts with SAML attribute
### If set, the user will be added and removed from accounts to match ### If set, the user will be added and removed from accounts to match
@ -97,6 +119,11 @@ SAML_SP_CONTACT_MAIL = '<contact mail>'
#Configures if SAML tokens should be encrypted. #Configures if SAML tokens should be encrypted.
#If enabled a new app certificate will be generated on restart #If enabled a new app certificate will be generated on restart
SAML_SIGN_REQUEST = False SAML_SIGN_REQUEST = False
# Configures if you want to request the IDP to sign the message
# Default is True
#SAML_WANT_MESSAGE_SIGNED = True
#Use SAML standard logout mechanism retrieved from idp metadata #Use SAML standard logout mechanism retrieved from idp metadata
#If configured false don't care about SAML session on logout. #If configured false don't care about SAML session on logout.
#Logout from PowerDNS-Admin only and keep SAML session authenticated. #Logout from PowerDNS-Admin only and keep SAML session authenticated.

View file

@ -62,6 +62,12 @@ SAML_METADATA_CACHE_LIFETIME = 1
### Example: urn:oid:2.5.4.4 ### Example: urn:oid:2.5.4.4
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4' #SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
## Split into Given name and Surname
## Useful if your IDP only gives a display name
### Default: none
### Example: http://schemas.microsoft.com/identity/claims/displayname
#SAML_ATTRIBUTE_NAME = 'http://schemas.microsoft.com/identity/claims/displayname'
## Attribute to use for username ## Attribute to use for username
### Default: Use NameID instead ### Default: Use NameID instead
### Example: urn:oid:0.9.2342.19200300.100.1.1 ### Example: urn:oid:0.9.2342.19200300.100.1.1
@ -75,6 +81,22 @@ SAML_METADATA_CACHE_LIFETIME = 1
### the user is set as a non-administrator user. ### the user is set as a non-administrator user.
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin' #SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
## Attribute to get group from
### Default: Don't use groups from SAML attribute
### Example: https://example.edu/pdns-admin-group
#SAML_ATTRIBUTE_GROUP = 'https://example.edu/pdns-admin'
## Group namem to get admin status from
### Default: Don't control admin with SAML group
### Example: https://example.edu/pdns-admin
#SAML_GROUP_ADMIN_NAME = 'powerdns-admin'
## Attribute to get group to account mappings from
### Default: None
### If set, the user will be added and removed from accounts to match
### what's in the login assertion if they are in the required group
#SAML_GROUP_TO_ACCOUNT_MAPPING = 'dev-admins=dev,prod-admins=prod'
## Attribute to get account names from ## Attribute to get account names from
### Default: Don't control accounts with SAML attribute ### Default: Don't control accounts with SAML attribute
### If set, the user will be added and removed from accounts to match ### If set, the user will be added and removed from accounts to match
@ -88,6 +110,11 @@ SAML_SP_CONTACT_MAIL = '<contact mail>'
#Configures if SAML tokens should be encrypted. #Configures if SAML tokens should be encrypted.
#If enabled a new app certificate will be generated on restart #If enabled a new app certificate will be generated on restart
SAML_SIGN_REQUEST = False SAML_SIGN_REQUEST = False
# Configures if you want to request the IDP to sign the message
# Default is True
#SAML_WANT_MESSAGE_SIGNED = True
#Use SAML standard logout mechanism retrieved from idp metadata #Use SAML standard logout mechanism retrieved from idp metadata
#If configured false don't care about SAML session on logout. #If configured false don't care about SAML session on logout.
#Logout from PowerDNS-Admin only and keep SAML session authenticated. #Logout from PowerDNS-Admin only and keep SAML session authenticated.

View file

@ -69,6 +69,12 @@ SAML_METADATA_CACHE_LIFETIME = 1
### Example: urn:oid:2.5.4.4 ### Example: urn:oid:2.5.4.4
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4' #SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
## Split into Given name and Surname
## Useful if your IDP only gives a display name
### Default: none
### Example: http://schemas.microsoft.com/identity/claims/displayname
#SAML_ATTRIBUTE_NAME = 'http://schemas.microsoft.com/identity/claims/displayname'
## Attribute to use for username ## Attribute to use for username
### Default: Use NameID instead ### Default: Use NameID instead
### Example: urn:oid:0.9.2342.19200300.100.1.1 ### Example: urn:oid:0.9.2342.19200300.100.1.1
@ -82,6 +88,22 @@ SAML_METADATA_CACHE_LIFETIME = 1
### the user is set as a non-administrator user. ### the user is set as a non-administrator user.
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin' #SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
## Attribute to get group from
### Default: Don't use groups from SAML attribute
### Example: https://example.edu/pdns-admin-group
#SAML_ATTRIBUTE_GROUP = 'https://example.edu/pdns-admin'
## Group namem to get admin status from
### Default: Don't control admin with SAML group
### Example: https://example.edu/pdns-admin
#SAML_GROUP_ADMIN_NAME = 'powerdns-admin'
## Attribute to get group to account mappings from
### Default: None
### If set, the user will be added and removed from accounts to match
### what's in the login assertion if they are in the required group
#SAML_GROUP_TO_ACCOUNT_MAPPING = 'dev-admins=dev,prod-admins=prod'
## Attribute to get account names from ## Attribute to get account names from
### Default: Don't control accounts with SAML attribute ### Default: Don't control accounts with SAML attribute
### If set, the user will be added and removed from accounts to match ### If set, the user will be added and removed from accounts to match
@ -95,6 +117,11 @@ SAML_SP_CONTACT_MAIL = '<contact mail>'
#Configures if SAML tokens should be encrypted. #Configures if SAML tokens should be encrypted.
#If enabled a new app certificate will be generated on restart #If enabled a new app certificate will be generated on restart
SAML_SIGN_REQUEST = False SAML_SIGN_REQUEST = False
# Configures if you want to request the IDP to sign the message
# Default is True
#SAML_WANT_MESSAGE_SIGNED = True
#Use SAML standard logout mechanism retrieved from idp metadata #Use SAML standard logout mechanism retrieved from idp metadata
#If configured false don't care about SAML session on logout. #If configured false don't care about SAML session on logout.
#Logout from PowerDNS-Admin only and keep SAML session authenticated. #Logout from PowerDNS-Admin only and keep SAML session authenticated.