This commit is contained in:
Dimitris Papachristou 2021-12-28 17:50:41 +01:00 committed by GitHub
commit 8fe017239e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 349 additions and 19 deletions

54
docs/Autoprovision.md Normal file
View file

@ -0,0 +1,54 @@
Provisioning the roles and the associations of a user based on an attribute in his object, is a very useful practice for a variety of reasons, and can be implemented across multiple authentication providers for PDA. Below we demonstrate how to enable and configure Roles&Associations Provisioning during LDAP and OIDC authentication.
The allowed syntax for records inside the attribute of the user's object is:
```text.
if PDA-Role∈[Administrator, Operator]:
syntax:=prefix:"powerdns-admin":PDA-Role
else:
syntax:=prefix:"powerdns-admin":PDA-Role:<domain>:<account>
where prefix is given by an admin of PDA in the configurable field "ADVANCE:Urn prefix".
i.e. some valid urn values could be:
urn:yourNID:yourOrganization:powerdns-admin:Administrator
urn:yourNID:yourOrganization:powerdns-admin:User:example.com (supposing there is a domain in the local db called "example.com")
urn:yourNID:yourOrganization:powerdns-admin:User:example.com:examplenet (supposing there is an account in the local db called "examplenet")
urn:yourNID:yourOrganization:powerdns-admin:User::examplenet
```
Note: To use Roles&Associations Provisioning in it's fullest potential, the domains and the accounts provided in the entries must already exist, or else entries with no match in the local db will be skipped.
In order to keep users' privileges in-sync between the PDA's database and the LDAP or the OIDC, when no valid "powerdns-admin" values are found for the logged-in user, PDA will purge all privileges from the local database for this user. To avoid unintentional wipe outs of existing PDA privileges especially when admins enable this feature for the first time, the option "Purge Roles if empty" is also available. If toggled on, LDAP/OIDC entries that have no valid "powerdns-admin" records to their object's attribute, will lose all their associations with any domain or account, also reverting to a PDA-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 PDA-Role.
How to configure LDAP Roles Autoprovisioning:
1) Login as an admin to PowerDNS Admin.
2) Go to Settings --> Authentication.
3) Under Authentication, select LDAP.
4) Disable Group Security, if enabled.
5) Click the Radio Button for Roles Autoprovisioning.
6) Fill in the required info:
* Role Provisioning field - your_LDAP_Field.
* Urn prefix - your_URN_Prefix.
7) Enable Purge Roles If Empty, if you so wish, and click confirm when the prompt appears.
8) Click Save.
<a href="https://ibb.co/189yxmB"><img src="https://i.ibb.co/yW8vJQK/Screenshot-2021-09-13-at-13-39-33-Authentication-Settings-Power-DNS-Admin.png" alt="Screenshot-2021-09-13-at-13-39-33-Authentication-Settings-Power-DNS-Admin" border="0"></a>
How to configure OIDC Roles Autoprovisioning:
1) Login as an admin to PowerDNS Admin.
2) Go to Settings --> Authentication.
3) Under Authentication, select OpenID Connect OAuth.
4) Click the Radio Button for Roles Autoprovisioning.
5) If "Autoprovision Account Name property" and "Autoprovision Account Description property" fields are filled, you will be warned that both features can not be enabled at the same time. This means that if Roles Autoprovisioning is enabled, the other feature is automatically disabled and vice versa.
6) Fill in the required info:
* Role Provisioning field - your_OIDC_Field.
* Urn prefix - your_URN_Prefix.
7) Enable Purge Roles If Empty, if you so wish, and click confirm when the prompt appears.
8) Click Save.
<a href="https://imgbb.com/"><img src="https://i.ibb.co/SDkB2qw/Screenshot-2021-09-13-at-14-19-42-Authentication-Settings-Power-DNS-Admin.png" alt="Screenshot-2021-09-13-at-14-19-42-Authentication-Settings-Power-DNS-Admin" border="0"></a>

View file

@ -46,6 +46,10 @@ class Setting(db.Model):
'urn_value':'',
'autoprovisioning_attribute': '',
'purge': False,
'autoprovisioning_oidc': False,
'urn_value_oidc':'',
'autoprovisioning_attribute_oidc': '',
'purge_oidc': False,
'verify_user_email': False,
'ldap_enabled': False,
'ldap_type': 'ldap',

View file

@ -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
@ -669,19 +669,19 @@ 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)
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):
"""
@ -712,12 +720,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:
@ -742,7 +749,7 @@ def getCorrectEntitlements(Entitlements):
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)
@ -752,7 +759,7 @@ def getCorrectEntitlements(Entitlements):
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
@ -797,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):

View file

@ -1616,6 +1616,25 @@ def setting_authentication():
request.form.get('oidc_oauth_account_name_property'))
Setting().set('oidc_oauth_account_description_property',
request.form.get('oidc_oauth_account_description_property'))
Setting().set('autoprovisioning_oidc', True
if request.form.get('autoprovisioning_oidc') == 'ON' else False)
Setting().set('autoprovisioning_attribute_oidc',
request.form.get('autoprovisioning_attribute_oidc'))
if request.form.get('autoprovisioning_oidc')=='ON':
if validateURN(request.form.get('urn_value_oidc')):
Setting().set('urn_value_oidc',
request.form.get('urn_value_oidc'))
else:
return render_template('admin_setting_authentication.html',
error="Invalid urn")
else:
Setting().set('urn_value_oidc',
request.form.get('urn_value_oidc'))
Setting().set('purge_oidc', True
if request.form.get('purge_oidc') == 'ON' else False)
result = {
'status': True,
'msg':

View file

@ -392,7 +392,7 @@ def login():
return redirect(url_for('index.login'))
#This checks if the account_name_property and account_description property were included in settings.
if Setting().get('oidc_oauth_account_name_property') and Setting().get('oidc_oauth_account_description_property'):
if Setting().get('oidc_oauth_account_name_property') and Setting().get('oidc_oauth_account_description_property') and not Setting().get('autoprovisioning_oidc'):
#Gets the name_property and description_property.
name_prop = Setting().get('oidc_oauth_account_name_property')
@ -425,6 +425,24 @@ def login():
if account not in account_to_add:
account.remove_user(user)
if Setting().get('autoprovisioning_oidc'):
urn_value=Setting().get('urn_value_oidc')
key=Setting().get('autoprovisioning_attribute_oidc')
if key in me:
Entitlements=[me[key]] if type(me[key]) is not list else me[key]
if len(Entitlements)==0 and Setting().get('purge_oidc'):
user.set_role("User")
user.revoke_privilege(True)
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('purge_oidc'):
user.set_role("User")
user.revoke_privilege(True)
current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' )
session['user_id'] = user.id
session['authentication_type'] = 'OAuth'
return authenticate_user(user, 'OIDC OAuth')
@ -496,7 +514,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'):

View file

@ -310,19 +310,33 @@
</ul>
</dd>
<dt>ADVANCE</dt>
<dd> Provision PDA user privileges based on LDAP Object Attributes. Alternative to Group Security Role Management.
<dd> Provision PDA user privileges based on LDAP Object Attributes. This feature and GROUP SECURITY are mutually exclusive.
<ul>
<li>
Roles Autoprovisioning - If toggled on, the PDA Role and the associations of users found in the local db, will be instantly updated from the LDAP server every time they log in.
</li>
<li>
Roles provisioning field - The attribute in the ldap server populated by the urn values where PDA will look for a new Role and/or new associations to domains/accounts.
Roles provisioning field - The attribute in the LDAP server populated by the urn values where PDA will look for a new Role and/or new associations to domains/accounts. The allowed syntax for records inside this attribute <b> in your LDAP server </b> is: <br>
<i> if PDA-Role∈[Administrator, Operator]:
<ul>
<li>
syntax:=prefix:"powerdns-admin":PDA-Role
</li>
</ul>
else: <br>
<ul>
<li>
syntax:=prefix:"powerdns-admin":PDA-Role:&ltdomain&gt:&ltaccount&gt
</li>
</ul>
where prefix is given in the field "Urn prefix".
</i>
</li>
<li>
Urn prefix - The prefix used before the static keyword "powerdns-admin" for your entitlements in the ldap server. Must comply with RFC no.8141.
Urn prefix - The prefix used before the static keyword "powerdns-admin" for your entitlements in the LDAP server. Must comply with RFC no.8141.
</li>
<li>
Purge Roles If Empty - If toggled on, ldap entries 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.<br> If toggled off, in the same scenario they get to keep their existing associations and their current Role.
Purge Roles If Empty - If toggled on, LDAP entries 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.<br> If toggled off, in the same scenario they get to keep their existing associations and their current Role.
</li>
</ul>
@ -665,6 +679,43 @@
<input type="text" class="form-control" name="oidc_oauth_account_description_property" id="oidc_oauth_account_description_property" placeholder="e.g. account_description" data-error="Please input property containing account_description" value="{{ SETTING.get('oidc_oauth_account_description_property') }}">
<span class="help-block with-errors"></span>
</div>
<legend></legend>
<div class="form-group">
<label>Roles Autoprovisioning</label>
<div class="radio">
<label>
<input type="radio" name="autoprovisioning_oidc" id="autoprovisioning_oidc_off" value="OFF" {% if not SETTING.get('autoprovisioning_oidc') %}checked{% endif %}> OFF
</label>
&nbsp;&nbsp;&nbsp;
<label>
<input type="radio" name="autoprovisioning_oidc" id="autoprovisioning_oidc_on" value="ON"
{% if SETTING.get('autoprovisioning_oidc') %}checked{% endif %}> ON
</div>
</div>
<div class="form-group">
<label for="autoprovisioning_attribute_oidc">Roles provisioning field</label>
<input type="text" class="form-control" name="autoprovisioning_attribute_oidc" id="autoprovisioning_attribute_oidc" placeholder="e.g. eduPersonEntitlement" data-error=" Please input field responsible for autoprovisioning" value="{{ SETTING.get('autoprovisioning_attribute_oidc') }}">
<span class="help-block with-errors"></span>
</div>
<div class="form-group {% if error %}has-error{% endif %}">
<label for="urn_value_oidc">Urn prefix</label>
<input type="text" class="form-control" name="urn_value_oidc" id="urn_value_oidc" placeholder="e.g. urn:mace:<yourOrganization>" data-error="Please fill this field" value="{{ SETTING.get('urn_value_oidc') }}">
{% if error %}
<span class="help-block with-errors">Please input the correct prefix for your urn value</span>
{% endif %}
</div>
<div class="form-group">
<label>Purge Roles If Empty</label>
<div class="radio">
<label>
<input type="radio" name="purge_oidc" id="purge_oidc_off" value="OFF" {% if not SETTING.get('purge_oidc') %}checked{% endif %}> OFF
</label>
&nbsp;&nbsp;&nbsp;
<label>
<input type="radio" name="purge_oidc" id="purge_oidc_on" value="ON" {% if SETTING.get('purge_oidc') %}checked{% endif %}> ON
</div>
</div>
</fieldset>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
@ -673,7 +724,41 @@
</div>
<div class="col-md-8">
<legend>Help</legend>
<p>Fill in all the fields in the left form.</p>
<dl class="dl-horizontal">
<p>Fill in all the fields in the left form.</p>
<dt>ADVANCE</dt>
<dd> Provision PDA user privileges based on OIDC Object Attributes. This feature and "Autoprovision Account Name/Description property" are mutually exclusive.
<ul>
<li>
Roles Autoprovisioning - If toggled on, the PDA Role and the associations of users found in the local db, will be instantly updated from the OIDC server every time they log in.
</li>
<li>
Roles provisioning field - The attribute in the OIDC server populated by the urn values where PDA will look for a new Role and/or new associations to domains/accounts. The allowed syntax for records inside this attribute <b> in your OIDC server </b> is: <br>
<i> if PDA-Role∈[Administrator, Operator]:
<ul>
<li>
syntax:=prefix:"powerdns-admin":PDA-Role
</li>
</ul>
else: <br>
<ul>
<li>
syntax:=prefix:"powerdns-admin":PDA-Role:&ltdomain&gt:&ltaccount&gt
</li>
</ul>
where prefix is given in the field "Urn prefix".
</i>
</li>
<li>
Urn prefix - The prefix used before the static keyword "powerdns-admin" for your entitlements in the OIDC server. Must comply with RFC no.8141.
</li>
<li>
Purge Roles If Empty - If toggled on, OIDC entries 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.<br> If toggled off, in the same scenario they get to keep their existing associations and their current Role.
</li>
</ul>
</dd>
</dl>
</div>
</div>
</div>
@ -1022,6 +1107,14 @@
$('#oidc_oauth_firstname').prop('required', true);
$('#oidc_oauth_last_name').prop('required', true);
$('#oidc_oauth_email').prop('required', true);
if ($('#autoprovisioning_oidc').is(":checked")) {
$('#autoprovisioning_attribute_oidc').prop('required', true);
$('#urn_value_oidc').prop('required', true);
$('#oidc_oauth_account_name_property').prop('readonly', true);
$('#oidc_oauth_account_description_property').prop('readonly', true);
}
} else {
$('#oidc_oauth_key').prop('required', false);
$('#oidc_oauth_secret').prop('required', false);
@ -1033,10 +1126,86 @@
$('#oidc_oauth_firstname').prop('required', false);
$('#oidc_oauth_last_name').prop('required', false);
$('#oidc_oauth_email').prop('required', false);
if ($('#autoprovisioning_oidc').is(":checked")) {
$('#autoprovisioning_attribute_oidc').prop('required', false);
$('#urn_value_oidc').prop('required', true);
$('#oidc_oauth_account_name_property').prop('readonly', true);
$('#oidc_oauth_account_description_property').prop('readonly', true);
}
}
});
var handler= function(event, variable){
if( variable.is('[readonly]') ){
variable.click( function () {
var modal = $("#modal_warning");
var info = "Roles Autoprovisioning is enabled, to use this feature disable it first." ;
modal.find('.modal-body p').text(info);
modal.find('#button_warning_confirm').click(function () {
modal.modal('hide');
})
modal.find('#warning_X').click(function () {
modal.modal('hide');
})
modal.modal('show');
});
}
}
$("input[name='autoprovisioning_oidc']" ).change(function(){
if ($('#autoprovisioning_oidc_on').is(":checked") && $('#oidc_oauth_enabled').is(":checked")) {
$('#autoprovisioning_attribute_oidc').prop('required', true);
$('#urn_value_oidc').prop('required', true);
$('#purge_oidc').prop('required', true);
}
else{
$('#autoprovisioning_attribute_oidc').prop('required', false);
$('#urn_value_oidc').prop('required', false);
$('#purge_oidc').prop('required', false);
$('#oidc_oauth_account_name_property').prop('readonly', false);
$('#oidc_oauth_account_description_property').prop('readonly', false);
$('#oidc_oauth_account_name_property').mouseover( function () {
$(this).css("cursor", "text")
});
$('#oidc_oauth_account_description_property').mouseover( function () {
$(this).css("cursor", "text")
});
$('#oidc_oauth_account_name_property').off("click");
$('#oidc_oauth_account_description_property').off("click");
}
if ($('#autoprovisioning_oidc_on').is(":checked")){
document.getElementById('autoprovisioning_oidc_on').checked=false;
document.getElementById('autoprovisioning_oidc_off').checked=true;
var modal= $("#modal_confirm_oidc");
var info = "Are you sure you want to do this? By enabling Roles Autoprovisioning you are disabling the feature above" ;
modal.find('.modal-body p').text(info);
modal.find('#button_confirm').click(function () {
document.getElementById('autoprovisioning_oidc_on').checked=true;
document.getElementById('autoprovisioning_oidc_off').checked=false;
$('#oidc_oauth_account_name_property').prop('readonly', true);
$('#oidc_oauth_account_description_property').prop('readonly', true);
$('#oidc_oauth_account_name_property').mouseover( function () {
$(this).css("cursor", "not-allowed")
});
$('#oidc_oauth_account_description_property').mouseover( function () {
$(this).css("cursor", "not-allowed")
});
modal.modal('hide');
})
modal.find('#button_cancel').click(function () {
modal.modal('hide');
})
modal.find('#X').click(function () {
modal.modal('hide');
})
modal.modal('show');
}
});
// init validation requirement at first time page load
{% if SETTING.get('oidc_oauth_enabled') %}
{% if SETTING.get('oidc_oauth_enabled') %}
$('#oidc_oauth_key').prop('required', true);
$('#oidc_oauth_secret').prop('required', true);
$('#oidc_oauth_scope').prop('required', true);
@ -1047,7 +1216,44 @@
$('#oidc_oauth_firstname').prop('required', true);
$('#oidc_oauth_last_name').prop('required', true);
$('#oidc_oauth_email').prop('required', true);
if ($('#autoprovisioning_oidc_on').is(":checked")) {
$('#autoprovisioning_attribute_oidc').prop('required', true);
$('#urn_value_oidc').prop('required', true);
$('#oidc_oauth_account_name_property').prop('readonly', true);
$('#oidc_oauth_account_description_property').prop('readonly', true);
$('#oidc_oauth_account_name_property').mouseover( function () {
$(this).css("cursor", "not-allowed")
});
$('#oidc_oauth_account_description_property').mouseover( function () {
$(this).css("cursor", "not-allowed")
});
$('#oidc_oauth_account_name_property').on("click", handler(event, $('#oidc_oauth_account_name_property')));
$('#oidc_oauth_account_description_property').on("click", handler(event, $('#oidc_oauth_account_description_property')));
}
{% endif %}
$("input[name='purge_oidc']" ).change(function(){
if ($("#purge_oidc_on").is(":checked")){
document.getElementById('purge_oidc_on').checked=false;
document.getElementById('purge_oidc_off').checked=true;
var modal = $("#modal_confirm_oidc");
var info = "Are you sure you want to do this? Users will lose their associated domains unless they already have their autoprovisioning field prepopulated." ;
modal.find('.modal-body p').text(info);
modal.find('#button_confirm').click(function () {
document.getElementById('purge_oidc_on').checked=true;
document.getElementById('purge_oidc_off').checked=false;
modal.modal('hide');
})
modal.find('#button_cancel').click(function () {
modal.modal('hide');
})
modal.find('#X').click(function () {
modal.modal('hide');
})
modal.modal('show');
}
});
//END: OIDC Tab JS
</script>
@ -1077,6 +1283,28 @@
</div>
<div class="modal fade modal-warning" id="modal_confirm_oidc" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="X" >
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Confirmation</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-left" id="button_cancel" name="purge_oidc" value="OFF" data-dismiss="modal" >Cancel</button>
<button type="button" class="btn btn-flat btn-success" id="button_confirm">Confirm</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
<div class="modal fade modal-warning" id="modal_warning" data-keyboard="false" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">