From 3457d9214a84a504a53eb6f77830adba4bafa4e4 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Fri, 31 Aug 2018 11:57:06 +0700 Subject: [PATCH 01/13] Adding Operator role --- app/decorators.py | 34 +++++++- app/models.py | 68 +++++++-------- app/static/custom/js/custom.js | 7 +- app/templates/admin_manageuser.html | 40 ++++----- .../admin_setting_authentication.html | 8 ++ app/templates/base.html | 24 +++--- app/templates/dashboard.html | 32 +++---- app/templates/dashboard_domain.html | 4 +- app/views.py | 86 +++++++++++-------- .../4a666113c7bb_add_operator_role.py | 58 +++++++++++++ 10 files changed, 228 insertions(+), 133 deletions(-) create mode 100644 migrations/versions/4a666113c7bb_add_operator_role.py diff --git a/app/decorators.py b/app/decorators.py index 673eea6..c563e00 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -6,6 +6,9 @@ from app.models import Role, Setting def admin_role_required(f): + """ + Grant access if user is in Administrator role + """ @wraps(f) def decorated_function(*args, **kwargs): if g.user.role.name != 'Administrator': @@ -14,10 +17,28 @@ def admin_role_required(f): return decorated_function -def can_access_domain(f): +def operator_role_required(f): + """ + Grant access if user is in Operator role or higher + """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name != 'Administrator': + if g.user.role.name not in ['Administrator', 'Operator']: + return redirect(url_for('error', code=401)) + return f(*args, **kwargs) + return decorated_function + + +def can_access_domain(f): + """ + Grant access if: + - user is in Operator role or higher, or + - user is in granted Account, or + - user is in granted Domain + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if g.user.role.name not in ['Administrator', 'Operator']: domain_name = kwargs.get('domain_name') user_domain = [d.name for d in g.user.get_domain()] @@ -29,10 +50,15 @@ def can_access_domain(f): def can_configure_dnssec(f): + """ + Grant access if: + - user is in Operator role or higher, or + - dnssec_admins_only is off + """ @wraps(f) def decorated_function(*args, **kwargs): - if g.user.role.name != 'Administrator' and Setting().get('dnssec_admins_only'): - return redirect(url_for('error', code=401)) + if g.user.role.name not in ['Administrator', 'Operator'] and Setting().get('dnssec_admins_only'): + return redirect(url_for('error', code=401)) return f(*args, **kwargs) return decorated_function diff --git a/app/models.py b/app/models.py index 5eec5ab..2117ea1 100644 --- a/app/models.py +++ b/app/models.py @@ -150,7 +150,7 @@ class User(db.Model): logging.error(e) logging.debug('baseDN: {0}'.format(baseDN)) logging.debug(traceback.format_exc()) - raise + def ldap_auth(self, ldap_username, password): try: @@ -165,6 +165,8 @@ class User(db.Model): """ Validate user credential """ + role_name = 'User' + if method == 'LOCAL': user_info = User.query.filter(User.username == self.username).first() @@ -179,12 +181,12 @@ class User(db.Model): return False if method == 'LDAP': - isadmin = False LDAP_TYPE = Setting().get('ldap_type') LDAP_BASE_DN = Setting().get('ldap_base_dn') LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic') LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username') LDAP_ADMIN_GROUP = Setting().get('ldap_admin_group') + LDAP_OPERATOR_GROUP = Setting().get('ldap_operator_group') LDAP_USER_GROUP = Setting().get('ldap_user_group') LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled') @@ -206,24 +208,30 @@ class User(db.Model): try: if LDAP_TYPE == 'ldap': if (self.ldap_search(searchFilter, LDAP_ADMIN_GROUP)): - isadmin = True + role_name = 'Administrator' logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP)) + elif (self.ldap_search(searchFilter, LDAP_OPERATOR_GROUP)): + role_name = 'Operator' + logging.info('User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'.format(self.username, LDAP_OPERATOR_GROUP)) elif (self.ldap_search(searchFilter, LDAP_USER_GROUP)): logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP)) else: - logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_USER_GROUP)) + logging.error('User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_OPERATOR_GROUP, LDAP_USER_GROUP)) return False elif LDAP_TYPE == 'ad': user_ldap_groups = [g.decode("utf-8") for g in ldap_result[0][0][1]['memberOf']] logging.debug('user_ldap_groups: {0}'.format(user_ldap_groups)) if (LDAP_ADMIN_GROUP in user_ldap_groups): - isadmin = True + role_name = 'Administrator' logging.info('User {0} is part of the "{1}" group that allows admin access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP)) + elif (LDAP_OPERATOR_GROUP in user_ldap_groups): + role_name = 'Operator' + logging.info('User {0} is part of the "{1}" group that allows operator access to PowerDNS-Admin'.format(self.username, LDAP_OPERATOR_GROUP)) elif (LDAP_USER_GROUP in user_ldap_groups): logging.info('User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin'.format(self.username, LDAP_USER_GROUP)) else: - logging.error('User {0} is not part of the "{1}" or "{2}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_USER_GROUP)) + logging.error('User {0} is not part of the "{1}", "{2}" or "{3}" groups that allow access to PowerDNS-Admin'.format(self.username, LDAP_ADMIN_GROUP, LDAP_OPERATOR_GROUP, LDAP_USER_GROUP)) return False else: logging.error('Invalid LDAP type') @@ -261,21 +269,17 @@ class User(db.Model): logging.debug(traceback.format_exc()) # first register user will be in Administrator role - self.role_id = Role.query.filter_by(name='User').first().id if User.query.count() == 0: self.role_id = Role.query.filter_by(name='Administrator').first().id - - # user will be in Administrator role if part of LDAP Admin group - if LDAP_GROUP_SECURITY_ENABLED: - if isadmin == True: - self.role_id = Role.query.filter_by(name='Administrator').first().id + else: + self.role_id = Role.query.filter_by(name=role_name).first().id self.create_user() logging.info('Created user "{0}" in the DB'.format(self.username)) - # user already exists in database, set their admin status based on group membership (if enabled) + # user already exists in database, set their role based on group membership (if enabled) if LDAP_GROUP_SECURITY_ENABLED: - self.set_admin(isadmin) + self.set_role(role_name) return True else: @@ -453,28 +457,15 @@ class User(db.Model): return False return False - def set_admin(self, is_admin): - """ - Set role for a user: - is_admin == True => Administrator - is_admin == False => User - """ - user_role_name = 'Administrator' if is_admin else 'User' - role = Role.query.filter(Role.name==user_role_name).first() - - try: - if role: - user = User.query.filter(User.username==self.username).first() - user.role_id = role.id - db.session.commit() - return True - else: - return False - except: - db.session.roleback() - logging.error('Cannot change user role in DB') - logging.debug(traceback.format_exc()) - return False + def set_role(self, role_name): + role = Role.query.filter(Role.name==role_name).first() + if role: + user = User.query.filter(User.username==self.username).first() + user.role_id = role.id + db.session.commit() + return {'status': True, 'msg': 'Set user role successfully'} + else: + return {'status': False, 'msg': 'Role does not exist'} class Account(db.Model): @@ -1825,8 +1816,9 @@ class Setting(db.Model): 'ldap_filter_basic': '', 'ldap_filter_username': '', 'ldap_sg_enabled': False, - 'ldap_admin_group': False, - 'ldap_user_group': False, + 'ldap_admin_group': '', + 'ldap_operator_group': '', + 'ldap_user_group': '', 'github_oauth_enabled': False, 'github_oauth_key': '', 'github_oauth_secret': '', diff --git a/app/static/custom/js/custom.js b/app/static/custom/js/custom.js index 6442bb2..e079130 100644 --- a/app/static/custom/js/custom.js +++ b/app/static/custom/js/custom.js @@ -22,9 +22,14 @@ function applyChanges(data, url, showResult, refreshPage) { }, error : function(jqXHR, status) { + // console.log(jqXHR); + // var modal = $("#modal_error"); + // modal.find('.modal-body p').text(jqXHR["responseText"]); + // modal.modal('show'); console.log(jqXHR); var modal = $("#modal_error"); - modal.find('.modal-body p').text(jqXHR["responseText"]); + var responseJson = jQuery.parseJSON(jqXHR.responseText); + modal.find('.modal-body p').text(responseJson['msg']); modal.modal('show'); } }); diff --git a/app/templates/admin_manageuser.html b/app/templates/admin_manageuser.html index 653738e..a3aa85a 100644 --- a/app/templates/admin_manageuser.html +++ b/app/templates/admin_manageuser.html @@ -36,7 +36,7 @@ First Name Last Name Email - Admin + Role Privileges Action @@ -49,18 +49,22 @@ {{ user.lastname }} {{ user.email }} - + - - - @@ -93,14 +97,6 @@ "pageLength": 10 }); - // avoid losing icheck box style when database refreshed - $('#tbl_users').on('draw.dt', function () { - $('.admin_toggle').iCheck({ - handle: 'checkbox', - checkboxClass: 'icheckbox_square-blue' - }); - }); - // handle revocation of privileges $(document.body).on('click', '.button_revoke', function() { var modal = $("#modal_revoke"); @@ -129,24 +125,18 @@ }); - // initialize pretty checkboxes - $('.admin_toggle').iCheck({ - checkboxClass : 'icheckbox_square-blue', - increaseArea : '20%' // optional - }); - - // handle checkbox toggling - $(document.body).on('ifToggled', '.admin_toggle', function() { - var is_admin = $(this).prop('checked'); + // handle user role changing + $('.user_role').on('change', function() { + var role_name = this.value; var username = $(this).prop('id'); postdata = { - 'action' : 'set_admin', + 'action' : 'update_user_role', 'data' : { 'username' : username, - 'is_admin' : is_admin + 'role_name' : role_name } }; - applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser'); + applyChanges(postdata, $SCRIPT_ROOT + '/admin/manageuser', showResult=true); }); {% endblock %} diff --git a/app/templates/admin_setting_authentication.html b/app/templates/admin_setting_authentication.html index e929065..e522178 100644 --- a/app/templates/admin_setting_authentication.html +++ b/app/templates/admin_setting_authentication.html @@ -133,6 +133,11 @@ +
+ + + +
@@ -197,6 +202,9 @@
  • Admin group - Your LDAP admin group.
  • +
  • + Operator group - Your LDAP operator group. +
  • User group - Your LDAP user group.
  • diff --git a/app/templates/base.html b/app/templates/base.html index 98845ea..0b1e220 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -108,26 +108,26 @@
  • Dashboard
  • - {% if current_user.role.name == 'Administrator' %} + {% if current_user.role.name in ['Administrator', 'Operator'] %}
  • New Domain
  • ADMINISTRATION
  • - Admin Console -
  • -
  • - Domain Templates -
  • -
  • - Users -
  • -
  • - Accounts + PDNS
  • History
  • +
  • + Domain Templates +
  • +
  • + Accounts +
  • +
  • + Users +
  • Settings @@ -138,8 +138,10 @@
  • {% endif %} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index b1d3400..871ff26 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -19,7 +19,7 @@ {% block content %}
    - {% if current_user.role.name == 'Administrator' %} + {% if current_user.role.name in ['Administrator', 'Operator'] %}
    @@ -69,7 +69,7 @@
    - +

    {{ uptime|display_second_to_time }}

    @@ -102,17 +102,17 @@ {% for history in histories %} - - {{ history.created_by }} - {{ history.msg }} - {{ history.created_on }} - - - - - {% endfor %} + + {{ history.created_by }} + {{ history.msg }} + {{ history.created_on }} + + + + + {% endfor %}
    @@ -136,7 +136,7 @@ Serial Master Account - Action + Action @@ -182,7 +182,7 @@ "ordering" : true, "columnDefs": [ { "orderable": false, "targets": [-1] } - {% if current_user.role.name != 'Administrator' %},{ "visible": false, "targets": [-2] }{% endif %} + {% if current_user.role.name not in ['Administrator', 'Operator'] %},{ "visible": false, "targets": [-2] }{% endif %} ], "processing" : true, "serverSide" : true, @@ -236,7 +236,7 @@ modal.modal('show'); }); - {% if current_user.role.name == 'Administrator' or not SETTING.get('dnssec_admins_only') %} + {% if current_user.role.name in ['Administrator', 'Operator'] or not SETTING.get('dnssec_admins_only') %} $(document.body).on("click", ".button_dnssec", function() { var domain = $(this).prop('id'); getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec', domain); diff --git a/app/templates/dashboard_domain.html b/app/templates/dashboard_domain.html index 6d42edb..8f1c1fe 100644 --- a/app/templates/dashboard_domain.html +++ b/app/templates/dashboard_domain.html @@ -23,13 +23,13 @@ {% endmacro %} {% macro account(domain) %} - {% if current_user.role.name =='Administrator' %} + {% if current_user.role.name in ['Administrator', 'Operator'] %} {% if domain.account_description != "" %}{{ domain.account.description }} {% endif %}[{{ domain.account.name }}] {% endif %} {% endmacro %} {% macro actions(domain) %} - {% if current_user.role.name =='Administrator' %} + {% if current_user.role.name in ['Administrator', 'Operator'] %} -

    - Error! -

    -
    - x -
      - {%- for msg in errors %} -
    • {{ msg }}
    • {% endfor -%} -
    -
    -
    -
    +
    +
    + +

    + Error! +

    +
    + x +
      + {%- for msg in errors %} +
    • {{ msg }}
    • {% endfor -%} +
    +
    +
    +
    {% endif %} {% endwith %}
    @@ -45,11 +45,11 @@

    Templates

    @@ -75,15 +75,15 @@ {% endfor %} diff --git a/app/templates/user_profile.html b/app/templates/user_profile.html index 6b9e3ba..90e9706 100644 --- a/app/templates/user_profile.html +++ b/app/templates/user_profile.html @@ -161,8 +161,7 @@ // handle checkbox toggling $('.otp_toggle').on('ifToggled', function(event) { var enable_otp = $(this).prop('checked'); - var username = $(this).prop('id'); - postdata = { + var postdata = { 'action' : 'enable_otp', 'data' : { 'enable_otp' : enable_otp From 40cb835b0e8871beb0f95e72c14e4675a2f83db0 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Fri, 31 Aug 2018 21:58:11 +0700 Subject: [PATCH 04/13] Add .lgtm.yml --- .lgtm.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .lgtm.yml diff --git a/.lgtm.yml b/.lgtm.yml new file mode 100644 index 0000000..a8c5b04 --- /dev/null +++ b/.lgtm.yml @@ -0,0 +1,10 @@ +extraction: + python: + python_setup: + version: 3 + index: + exclude: + - .git + - upload + - flask + - node_modules From 38d1d85a186ff1ebe217d7ff3447f6e61a9c7d09 Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Fri, 31 Aug 2018 22:30:08 +0700 Subject: [PATCH 05/13] Fixing string format --- app/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models.py b/app/models.py index bef9fc9..816e226 100644 --- a/app/models.py +++ b/app/models.py @@ -774,7 +774,7 @@ class Domain(db.Model): domain = Domain.query.filter(Domain.name==name).first() return domain.id except Exception as e: - logging.error('Domain does not exist. ERROR: {1}'.format(e)) + logging.error('Domain does not exist. ERROR: {0}'.format(e)) return None def update(self): From 3481af149b8a56e21bcfaec1c139f0adfaa0d89c Mon Sep 17 00:00:00 2001 From: Khanh Ngo Date: Sat, 1 Sep 2018 17:53:05 +0700 Subject: [PATCH 06/13] Add option to allow user to create domain --- app/decorators.py | 15 +++++++++++++++ app/models.py | 1 + app/templates/admin_setting_basic.html | 2 +- app/templates/base.html | 4 +++- app/views.py | 10 ++++++++-- .../versions/4a666113c7bb_add_operator_role.py | 8 +++++--- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/app/decorators.py b/app/decorators.py index f0e990c..a1cfacf 100644 --- a/app/decorators.py +++ b/app/decorators.py @@ -61,3 +61,18 @@ def can_configure_dnssec(f): return f(*args, **kwargs) return decorated_function + + +def can_create_domain(f): + """ + Grant access if: + - user is in Operator role or higher, or + - allow_user_create_domain is on + """ + @wraps(f) + def decorated_function(*args, **kwargs): + if g.user.role.name not in ['Administrator', 'Operator'] and not Setting().get('allow_user_create_domain'): + return redirect(url_for('error', code=401)) + + return f(*args, **kwargs) + return decorated_function diff --git a/app/models.py b/app/models.py index 816e226..82fabf9 100644 --- a/app/models.py +++ b/app/models.py @@ -1799,6 +1799,7 @@ class Setting(db.Model): 'allow_quick_edit': True, 'pretty_ipv6_ptr': False, 'dnssec_admins_only': False, + 'allow_user_create_domain': False, 'bg_domain_updates': False, 'site_name': 'PowerDNS-Admin', 'pdns_api_url': '', diff --git a/app/templates/admin_setting_basic.html b/app/templates/admin_setting_basic.html index b289f75..d5bdb84 100644 --- a/app/templates/admin_setting_basic.html +++ b/app/templates/admin_setting_basic.html @@ -69,7 +69,7 @@
    - - - - - + + + + +