Merge branch 'fix-saml'

This commit is contained in:
Khanh Ngo 2018-08-07 09:09:34 +07:00
commit 47d5858fc6
4 changed files with 135 additions and 14 deletions

View file

@ -19,7 +19,7 @@ if app.config['SAML_ENABLED']:
from onelogin.saml2.idp_metadata_parser import OneLogin_Saml2_IdPMetadataParser
idp_timestamp = datetime(1970, 1, 1)
idp_data = None
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'])
idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None))
if idp_data is None:
print('SAML: IDP Metadata initial load failed')
exit(-1)
@ -37,7 +37,7 @@ def get_idp_data():
def retreive_idp_data():
global idp_data, idp_timestamp
new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'])
new_idp_data = OneLogin_Saml2_IdPMetadataParser.parse_remote(app.config['SAML_METADATA_URL'], entity_id=app.config.get('SAML_IDP_ENTITY_ID', None))
if new_idp_data is not None:
idp_data = new_idp_data
idp_timestamp = datetime.now()
@ -205,7 +205,7 @@ def email_to_gravatar_url(email="", size=100):
def prepare_flask_request(request):
# If server is behind proxys or balancers use the HTTP_X_FORWARDED fields
url_data = urlparse.urlparse(request.url)
url_data = urlparse(request.url)
return {
'https': 'on' if request.scheme == 'https' else 'off',
'http_host': request.host,
@ -229,7 +229,10 @@ def init_saml_auth(req):
metadata = get_idp_data()
settings = {}
settings['sp'] = {}
settings['sp']['NameIDFormat'] = idp_data['sp']['NameIDFormat']
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']
cert = open(CERT_FILE, "r").readlines()
key = open(KEY_FILE, "r").readlines()
@ -275,4 +278,4 @@ def init_saml_auth(req):
settings['organization']['en-US']['name'] = 'PowerDNS-Admin'
settings['organization']['en-US']['url'] = own_url
auth = OneLogin_Saml2_Auth(req, settings)
return auth
return auth

View file

@ -608,6 +608,32 @@ class Account(db.Model):
users.append(User(id=uid).get_user_info_by_id().username)
self.grant_privileges(users)
def add_user(self, user):
"""
Add a single user to Account by User
"""
try:
au = AccountUser(self.id, user.id)
db.session.add(au)
db.session.commit()
return True
except:
db.session.rollback()
logging.error('Cannot add user privielges on account {0}'.format(self.name))
return False
def remove_user(self, user):
"""
Remove a single user from Account by User
"""
try:
AccountUser.query.filter(AccountUser.user_id == user.id).filter(AccountUser.account_id == self.id).delete()
db.session.commit()
return True
except:
db.session.rollback()
logging.error('Cannot revoke user privielges on account {0}'.format(self.name))
return False
class Role(db.Model):

View file

@ -17,7 +17,7 @@ from flask_login import login_user, logout_user, current_user, login_required
from werkzeug import secure_filename
from werkzeug.security import gen_salt
from .models import User, Account, Domain, Record, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord
from .models import User, Account, Domain, Record, Role, Server, History, Anonymous, Setting, DomainSetting, DomainTemplate, DomainTemplateRecord
from app import app, login_manager, github, google
from app.lib import utils
from app.decorators import admin_role_required, can_access_domain, can_configure_dnssec
@ -227,20 +227,65 @@ def saml_authorized():
self_url = self_url+req['script_name']
if 'RelayState' in request.form and self_url != request.form['RelayState']:
return redirect(auth.redirect_to(request.form['RelayState']))
user = User.query.filter_by(username=session['samlNameId'].lower()).first()
if app.config.get('SAML_ATTRIBUTE_USERNAME', False):
username = session['samlUserdata'][app.config['SAML_ATTRIBUTE_USERNAME']][0].lower()
else:
username = session['samlNameId'].lower()
user = User.query.filter_by(username=username).first()
if not user:
# create user
user = User(username=session['samlNameId'],
user = User(username=username,
plain_text_password = None,
email=session['samlNameId'])
user.create_local_user()
session['user_id'] = user.id
if session['samlUserdata'].has_key("email"):
user.email = session['samlUserdata']["email"][0].lower()
if session['samlUserdata'].has_key("givenname"):
user.firstname = session['samlUserdata']["givenname"][0]
if session['samlUserdata'].has_key("surname"):
user.lastname = session['samlUserdata']["surname"][0]
email_attribute_name = app.config.get('SAML_ATTRIBUTE_EMAIL', 'email')
givenname_attribute_name = app.config.get('SAML_ATTRIBUTE_GIVENNAME', 'givenname')
surname_attribute_name = app.config.get('SAML_ATTRIBUTE_SURNAME', 'surname')
account_attribute_name = app.config.get('SAML_ATTRIBUTE_ACCOUNT', None)
admin_attribute_name = app.config.get('SAML_ATTRIBUTE_ADMIN', None)
if email_attribute_name in session['samlUserdata']:
user.email = session['samlUserdata'][email_attribute_name][0].lower()
if givenname_attribute_name in session['samlUserdata']:
user.firstname = session['samlUserdata'][givenname_attribute_name][0]
if surname_attribute_name in session['samlUserdata']:
user.lastname = session['samlUserdata'][surname_attribute_name][0]
if admin_attribute_name:
user_accounts = set(user.get_account())
saml_accounts = []
for account_name in session['samlUserdata'].get(account_attribute_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()
saml_accounts.append(account)
saml_accounts = set(saml_accounts)
for account in saml_accounts - user_accounts:
account.add_user(user)
history = History(msg='Adding {0} to account {1}'.format(user.username, account.name), created_by='SAML Assertion')
history.add()
for account in user_accounts - saml_accounts:
account.remove_user(user)
history = History(msg='Removing {0} from account {1}'.format(user.username, account.name), created_by='SAML Assertion')
history.add()
if admin_attribute_name:
if 'true' in session['samlUserdata'].get(admin_attribute_name, []):
admin_role = Role.query.filter_by(name='Administrator').first().id
if user.role_id != admin_role:
user.role_id = admin_role
history = History(msg='Promoting {0} to administrator'.format(user.username), created_by='SAML Assertion')
history.add()
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.add()
user.plain_text_password = None
user.update_profile()
session['external_auth'] = True

View file

@ -97,6 +97,53 @@ SAML_PATH = os.path.join(os.path.dirname(__file__), 'saml')
SAML_METADATA_URL = 'https://<hostname>/FederationMetadata/2007-06/FederationMetadata.xml'
#Cache Lifetime in Seconds
SAML_METADATA_CACHE_LIFETIME = 1
## EntityID of the IdP to use. Only needed if more than one IdP is
## in the SAML_METADATA_URL
### Default: First (only) IdP in the SAML_METADATA_URL
### Example: https://idp.example.edu/idp
#SAML_IDP_ENTITY_ID = 'https://idp.example.edu/idp'
## NameID format to request
### Default: The SAML NameID Format in the metadata if present,
### otherwise urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
### Example: urn:oid:0.9.2342.19200300.100.1.1
#SAML_NAMEID_FORMAT = 'urn:oid:0.9.2342.19200300.100.1.1'
## Attribute to use for Email address
### Default: email
### Example: urn:oid:0.9.2342.19200300.100.1.3
#SAML_ATTRIBUTE_EMAIL = 'urn:oid:0.9.2342.19200300.100.1.3'
## Attribute to use for Given name
### Default: givenname
### Example: urn:oid:2.5.4.42
#SAML_ATTRIBUTE_GIVENNAME = 'urn:oid:2.5.4.42'
## Attribute to use for Surname
### Default: surname
### Example: urn:oid:2.5.4.4
#SAML_ATTRIBUTE_SURNAME = 'urn:oid:2.5.4.4'
## Attribute to use for username
### Default: Use NameID instead
### Example: urn:oid:0.9.2342.19200300.100.1.1
#SAML_ATTRIBUTE_USERNAME = 'urn:oid:0.9.2342.19200300.100.1.1'
## Attribute to get admin status from
### Default: Don't control admin with SAML attribute
### Example: https://example.edu/pdns-admin
### If set, look for the value 'true' to set a user as an administrator
### If not included in assertion, or set to something other than 'true',
### the user is set as a non-administrator user.
#SAML_ATTRIBUTE_ADMIN = 'https://example.edu/pdns-admin'
## Attribute to get account names from
### Default: Don't control accounts with SAML attribute
### If set, the user will be added and removed from accounts to match
### what's in the login assertion. Accounts that don't exist will
### be created and the user added to them.
SAML_ATTRIBUTE_ACCOUNT = 'https://example.edu/pdns-account'
SAML_SP_ENTITY_ID = 'http://<SAML SP Entity ID>'
SAML_SP_CONTACT_NAME = '<contact name>'
SAML_SP_CONTACT_MAIL = '<contact mail>'