From 0f8b8984a20e38ac3cb95a80674bacfdee2abe69 Mon Sep 17 00:00:00 2001 From: vmarkop Date: Wed, 8 Dec 2021 13:37:17 +0200 Subject: [PATCH 1/5] Added SAML Autoprovisioning --- powerdnsadmin/models/setting.py | 4 ++ powerdnsadmin/models/user.py | 7 +- powerdnsadmin/routes/index.py | 117 ++++++++++++++++++++------------ 3 files changed, 81 insertions(+), 47 deletions(-) diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py index a46cfb6..a713d76 100644 --- a/powerdnsadmin/models/setting.py +++ b/powerdnsadmin/models/setting.py @@ -110,6 +110,10 @@ class Setting(db.Model): 'oidc_oauth_email': 'email', 'oidc_oauth_account_name_property': '', 'oidc_oauth_account_description_property': '', + 'saml_autoprovisioning': False, + 'saml_urn_value': '', + 'saml_autoprovisioning_attribute': '', + 'saml_purge': False, 'forward_records_allow_edit': { 'A': True, 'AAAA': True, diff --git a/powerdnsadmin/models/user.py b/powerdnsadmin/models/user.py index f5e3556..9021e4b 100644 --- a/powerdnsadmin/models/user.py +++ b/powerdnsadmin/models/user.py @@ -659,11 +659,11 @@ class User(db.Model): current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e)) return entitlements - def updateUser(self, Entitlements): + def updateUser(self, Entitlements, urn_value): """ Update user associations based on ldap attribute """ - entitlements= getCorrectEntitlements(Entitlements) + entitlements= getCorrectEntitlements(Entitlements, urn_value) if len(entitlements)!=0: self.revoke_privilege(True) for entitlement in entitlements: @@ -702,12 +702,11 @@ class User(db.Model): if account!=None: account.add_user(user) -def getCorrectEntitlements(Entitlements): +def getCorrectEntitlements(Entitlements, urn_value): """ Gather a list of valid records from the ldap attribute given """ from ..models.role import Role - urn_value=Setting().get('urn_value') urnArgs=[x.lower() for x in urn_value.split(':')] entitlements=[] for Entitlement in Entitlements: diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index ccdcd6b..2fb3b4b 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -504,7 +504,7 @@ def login(): elif len(Entitlements)!=0: if checkForPDAEntries(Entitlements, urn_value): - user.updateUser(Entitlements) + user.updateUser(Entitlements, urn_value) else: current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix') if Setting().get('purge'): @@ -924,7 +924,6 @@ def saml_metadata(): resp = make_response(errors.join(', '), 500) return resp - @index_bp.route('/saml/authorized', methods=['GET', 'POST']) def saml_authorized(): errors = [] @@ -989,51 +988,77 @@ def saml_authorized(): 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_accounts()) - saml_accounts = [] - for group_mapping in group_to_account_mapping: - mapping = group_mapping.split('=') - group = mapping[0] - account_name = mapping[1] + if not Setting().get('saml_autoprovisioning'): + 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_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: + if group in user_groups: + account = handle_account(account_name) + saml_accounts.append(account) + + for account_name in session['samlUserdata'].get( + account_attribute_name, []): account = handle_account(account_name) 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 and 'true' in session['samlUserdata'].get( + admin_attribute_name, []): + uplift_to_admin(user) + elif admin_group_name in user_groups: + uplift_to_admin(user) + elif admin_attribute_name or group_attribute_name: + if user.role.name != 'User': + user.role_id = Role.query.filter_by(name='User').first().id + history = History(msg='Demoting {0} to user'.format( + user.username), + created_by='SAML Assertion') + history.add() + elif Setting().get('saml_autoprovisioning'): + urn_value = Setting().get('saml_urn_value') # urn_value for + key = Setting().get('saml_autoprovisioning_attribute') + Entitlements = read_saml_entitlements(urn_value, session['samlUserdata']) + if len(Entitlements)==0 and Setting().get('saml_purge'): + if user.role.name != 'User': + user.role_id = Role.query.filter_by(name='User').first().id + history = History(msg='Demoting {0} to user'.format( + user.username), + created_by='SAML Autoprovision') + history.add() + elif len(Entitlements)!=0: + if checkForPDAEntries(Entitlements, urn_value): + user.updateUser(Entitlements, urn_value) + else: + current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix') + if Setting().get('saml_purge'): + current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' ) + if user.role.name != 'User': + user.role_id = Role.query.filter_by(name='User').first().id + history = History(msg='Demoting {0} to user'.format( + user.username), + created_by='SAML Autoprovision') + history.add() - for account_name in session['samlUserdata'].get( - account_attribute_name, []): - account = handle_account(account_name) - 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 and 'true' in session['samlUserdata'].get( - admin_attribute_name, []): - uplift_to_admin(user) - elif admin_group_name in user_groups: - uplift_to_admin(user) - elif admin_attribute_name or group_attribute_name: - if user.role.name != 'User': - user.role_id = Role.query.filter_by(name='User').first().id - 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['authentication_type'] = 'SAML' @@ -1043,6 +1068,12 @@ def saml_authorized(): else: return render_template('errors/SAML.html', errors=errors) +def read_saml_entitlements(urn_value, saml_userdata): + Entitlements = [] + if urn_value in saml_userdata: + for k in saml_userdata[urn_value]: + Entitlements.append(k) + return Entitlements def create_group_to_account_mapping(): group_to_account_mapping_string = current_app.config.get( From 9f8ec56183820f612dfea119a5671777d6a17fcf Mon Sep 17 00:00:00 2001 From: kkmanos Date: Wed, 8 Dec 2021 14:38:30 +0200 Subject: [PATCH 2/5] added role autoprovisioning for saml --- powerdnsadmin/models/setting.py | 2 +- powerdnsadmin/routes/index.py | 20 +++++++++++--------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py index a713d76..d73e34b 100644 --- a/powerdnsadmin/models/setting.py +++ b/powerdnsadmin/models/setting.py @@ -111,7 +111,7 @@ class Setting(db.Model): 'oidc_oauth_account_name_property': '', 'oidc_oauth_account_description_property': '', 'saml_autoprovisioning': False, - 'saml_urn_value': '', + 'saml_urn_prefix': '', 'saml_autoprovisioning_attribute': '', 'saml_purge': False, 'forward_records_allow_edit': { diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 2fb3b4b..9586966 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -1035,9 +1035,9 @@ def saml_authorized(): created_by='SAML Assertion') history.add() elif Setting().get('saml_autoprovisioning'): - urn_value = Setting().get('saml_urn_value') # urn_value for - key = Setting().get('saml_autoprovisioning_attribute') - Entitlements = read_saml_entitlements(urn_value, session['samlUserdata']) + urn_prefix = Setting().get('saml_urn_prefix') + autoprovisioning_attribute = Setting().get('saml_autoprovisioning_attribute') + Entitlements = read_saml_entitlements(urn_prefix, autoprovisioning_attribute, session['samlUserdata']) if len(Entitlements)==0 and Setting().get('saml_purge'): if user.role.name != 'User': user.role_id = Role.query.filter_by(name='User').first().id @@ -1046,8 +1046,8 @@ def saml_authorized(): created_by='SAML Autoprovision') history.add() elif len(Entitlements)!=0: - if checkForPDAEntries(Entitlements, urn_value): - user.updateUser(Entitlements, urn_value) + if checkForPDAEntries(Entitlements, autoprovisioning_attribute): + user.updateUser(Entitlements, autoprovisioning_attribute) else: current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix') if Setting().get('saml_purge'): @@ -1068,11 +1068,13 @@ def saml_authorized(): else: return render_template('errors/SAML.html', errors=errors) -def read_saml_entitlements(urn_value, saml_userdata): +def read_saml_entitlements(urn_prefix, autoprovisioning_attribute, saml_userdata): Entitlements = [] - if urn_value in saml_userdata: - for k in saml_userdata[urn_value]: - Entitlements.append(k) + if autoprovisioning_attribute in saml_userdata: + for k in saml_userdata[autoprovisioning_attribute]: + pref = k.split(":powerdns-admin:")[0] + if pref == urn_prefix: + Entitlements.append(k) return Entitlements def create_group_to_account_mapping(): From 3fd10013ea9856f11d4605e44d82952fed7f2efa Mon Sep 17 00:00:00 2001 From: kkmanos Date: Wed, 8 Dec 2021 15:38:27 +0200 Subject: [PATCH 3/5] minimized code. test passed --- powerdnsadmin/routes/index.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 9586966..78e065d 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -1037,7 +1037,11 @@ def saml_authorized(): elif Setting().get('saml_autoprovisioning'): urn_prefix = Setting().get('saml_urn_prefix') autoprovisioning_attribute = Setting().get('saml_autoprovisioning_attribute') - Entitlements = read_saml_entitlements(urn_prefix, autoprovisioning_attribute, session['samlUserdata']) + Entitlements = [] + if autoprovisioning_attribute in session['samlUserdata']: + for k in session['samlUserdata'][autoprovisioning_attribute]: + Entitlements.append(k) + if len(Entitlements)==0 and Setting().get('saml_purge'): if user.role.name != 'User': user.role_id = Role.query.filter_by(name='User').first().id @@ -1046,8 +1050,8 @@ def saml_authorized(): created_by='SAML Autoprovision') history.add() elif len(Entitlements)!=0: - if checkForPDAEntries(Entitlements, autoprovisioning_attribute): - user.updateUser(Entitlements, autoprovisioning_attribute) + if checkForPDAEntries(Entitlements, urn_prefix): + user.updateUser(Entitlements, urn_prefix) else: current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix') if Setting().get('saml_purge'): @@ -1068,14 +1072,6 @@ def saml_authorized(): else: return render_template('errors/SAML.html', errors=errors) -def read_saml_entitlements(urn_prefix, autoprovisioning_attribute, saml_userdata): - Entitlements = [] - if autoprovisioning_attribute in saml_userdata: - for k in saml_userdata[autoprovisioning_attribute]: - pref = k.split(":powerdns-admin:")[0] - if pref == urn_prefix: - Entitlements.append(k) - return Entitlements def create_group_to_account_mapping(): group_to_account_mapping_string = current_app.config.get( From 9316c6629149441fdf2c24418586d78f67d72978 Mon Sep 17 00:00:00 2001 From: vmarkop Date: Wed, 15 Dec 2021 17:30:57 +0200 Subject: [PATCH 4/5] Added SAML Autoprovisioning settings in config --- configs/development.py | 31 +++++++++++++++++++++++++++++++ powerdnsadmin/routes/index.py | 4 +++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/configs/development.py b/configs/development.py index 2c2e63d..40fcc62 100644 --- a/configs/development.py +++ b/configs/development.py @@ -147,6 +147,37 @@ SAML_ENABLED = False # #SAML_LOGOUT_URL = 'https://google.com' # #SAML_ASSERTION_ENCRYPTED = True +# SAML_WANT_MESSAGE_SIGNED + +# SAML Autoprovisioning +# If toggled on, the PDA Role and the associations of users found in the local db +# will be directly updated from the SAML IDP every time they log in. +# NOTE: This feature and the assertion of "Admin / Account" attributes are mutually exclusive. +# If used, the values for Admin/Account given above will be ignored. +SAML_AUTOPROVISIONING = True +# The urn value of the attribute in the SAML Authn Response where PDA will look +# for a new Role and/or new associations to domains/accounts. +# Example: urn:oid:1.3.6.1.4.1.5923.1.1.1.7 +# The record syntax for this attribute inside the SAML Response must look like: +# prefix:powerdns-admin:PDA-Role, to provision an Administrator or Operator, or +# prefix:powerdns-admin:User::, provision a User +# who has access to one or more Domains and belongs to one or more Accounts. +# the "prefix" is given in the next attribute +SAML_AUTOPROVISIONING_ATTRIBUTE = 'urn:oid:1.3.6.1.4.1.5923.1.1.1.7' +# The prefix used before the static keyword "powerdns-admin" for your entitlements +# in the SAML Response. Must be a valid URN. +# Example: urn:mace:example.com +SAML_URN_PREFIX = 'urn:mace:example.com' +# If toggled on, SAML logins that have no valid "powerdns-admin" records +# to their autoprovisioning field, will lose all their associations +# with any domain or account, also reverting to a User in the process, +# despite their current role in the local db. +# If toggled off, in the same scenario they get to keep +# their existing associations and their current Role. +### CAUTION: Enabling this feature will revoke existing users' access to their +# associated domains unless they have their autoprovisioning field prepopulated. +SAML_PURGE = False + # Remote authentication settings diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 78e065d..205e319 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -1049,19 +1049,21 @@ def saml_authorized(): user.username), created_by='SAML Autoprovision') history.add() + user.revoke_privilege(True) elif len(Entitlements)!=0: if checkForPDAEntries(Entitlements, urn_prefix): user.updateUser(Entitlements, urn_prefix) else: current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix') if Setting().get('saml_purge'): - current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' ) + current_app.logger.warning('Procceding to revoke every privilege from ' + user.username + '.' ) if user.role.name != 'User': user.role_id = Role.query.filter_by(name='User').first().id history = History(msg='Demoting {0} to user'.format( user.username), created_by='SAML Autoprovision') history.add() + user.revoke_privilege(True) user.plain_text_password = None user.update_profile() From 5fc3412058f0bcc588260533923c523d324815cd Mon Sep 17 00:00:00 2001 From: KostasMparmparousis Date: Tue, 21 Dec 2021 15:38:06 +0200 Subject: [PATCH 5/5] SAML Provisioning update --- powerdnsadmin/models/user.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/powerdnsadmin/models/user.py b/powerdnsadmin/models/user.py index 99df8fc..65bc5b2 100644 --- a/powerdnsadmin/models/user.py +++ b/powerdnsadmin/models/user.py @@ -605,7 +605,7 @@ class User(db.Model): return False def set_role(self, role_name): - role = Role.query.filter(Role.name == role_name).first() + role = Role.query.filter(Role.name == role_name.capitalize()).first() if role: user = User.query.filter(User.username == self.username).first() user.role_id = role.id @@ -676,12 +676,12 @@ class User(db.Model): entitlements= getCorrectEntitlements(Entitlements, urn_value) if len(entitlements)!=0: self.revoke_privilege(True) + role="user" for entitlement in entitlements: arguments=entitlement.split(':') entArgs=arguments[arguments.index('powerdns-admin')+1:] - role= entArgs[0] - self.set_role(role) - if (role=="User") and len(entArgs)>1: + role= self.get_role(role,entArgs[0].lower()) + if (role=="user") and len(entArgs)>1: current_domains=getUserInfo(self.get_user_domains()) current_accounts=getUserInfo(self.get_accounts()) domain=entArgs[1] @@ -689,6 +689,14 @@ class User(db.Model): if len(entArgs)>2: account=entArgs[2] self.addMissingAccount(account, current_accounts) + self.set_role(role) + + def get_role(self, previousRole, newRole): + dict = { "user": 1, "operator" : 2, "administrator" : 3} + if (dict[newRole] > dict[previousRole]): + return newRole + else: + return previousRole def addMissingDomain(self, autoprovision_domain, current_domains): """ @@ -741,7 +749,7 @@ def getCorrectEntitlements(Entitlements, urn_value): continue entArgs=arguments[arguments.index('powerdns-admin')+1:] - role=entArgs[0] + role=entArgs[0].lower() roles= Role.query.all() role_names=get_role_names(roles) @@ -751,7 +759,7 @@ def getCorrectEntitlements(Entitlements, urn_value): continue if len(entArgs)>1: - if (role!="User"): + if (role!="user"): e="Too many arguments for Admin or Operator" current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e)) continue @@ -796,7 +804,7 @@ def get_role_names(roles): """ roles_list=[] for role in roles: - roles_list.append(role.name) + roles_list.append(role.name.lower()) return roles_list def getUserInfo(DomainsOrAccounts):