From 4383c337d4085ed485372d83876d50f9bd50ef6c Mon Sep 17 00:00:00 2001 From: Melchior NOGUES Date: Wed, 27 Jul 2022 11:35:47 +0200 Subject: [PATCH 01/77] fix: ldap type ad search user group when nested groups --- powerdnsadmin/models/user.py | 92 ++++++++++++++++++++---------------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/powerdnsadmin/models/user.py b/powerdnsadmin/models/user.py index 2f8b87c..cb857c8 100644 --- a/powerdnsadmin/models/user.py +++ b/powerdnsadmin/models/user.py @@ -5,6 +5,7 @@ import bcrypt import pyotp import ldap import ldap.filter +from collections import OrderedDict from flask import current_app from flask_login import AnonymousUserMixin from sqlalchemy import orm @@ -282,53 +283,62 @@ class User(db.Model): LDAP_USER_GROUP)) return False elif LDAP_TYPE == 'ad': - ldap_admin_group_filter, ldap_operator_group, ldap_user_group = "", "", "" - if LDAP_ADMIN_GROUP: - ldap_admin_group_filter = "(memberOf:1.2.840.113556.1.4.1941:={0})".format(LDAP_ADMIN_GROUP) - if LDAP_OPERATOR_GROUP: - ldap_operator_group = "(memberOf:1.2.840.113556.1.4.1941:={0})".format(LDAP_OPERATOR_GROUP) - if LDAP_USER_GROUP: - ldap_user_group = "(memberOf:1.2.840.113556.1.4.1941:={0})".format(LDAP_USER_GROUP) - searchFilter = "(&({0}={1})(|{2}{3}{4}))".format(LDAP_FILTER_USERNAME, self.username, - ldap_admin_group_filter, - ldap_operator_group, ldap_user_group) - ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN) - user_ad_member_of = ldap_result[0][0][1].get( - 'memberOf') + ldap_group_security_roles = OrderedDict( + Administrator=LDAP_ADMIN_GROUP, + Operator=LDAP_OPERATOR_GROUP, + User=LDAP_USER_GROUP, + ) + user_dn = ldap_result[0][0][0] + sf_groups = "" - if not user_ad_member_of: + for group in ldap_group_security_roles.values(): + if not group: + continue + + sf_groups += f"(distinguishedName={group})" + + sf_member_user = f"(member:1.2.840.113556.1.4.1941:={user_dn})" + search_filter = f"(&(|{sf_groups}){sf_member_user})" + current_app.logger.debug(f"LDAP groupSearchFilter '{search_filter}'") + + ldap_user_groups = [ + group[0][0] + for group in self.ldap_search( + search_filter, + LDAP_BASE_DN + ) + ] + + if not ldap_user_groups: current_app.logger.error( - 'User {0} does not belong to any group while LDAP_GROUP_SECURITY_ENABLED is ON' - .format(self.username)) + f"User '{self.username}' " + "does not belong to any group " + "while LDAP_GROUP_SECURITY_ENABLED is ON" + ) return False - user_ad_member_of = [g.decode("utf-8") for g in user_ad_member_of] + current_app.logger.debug( + "LDAP User security groups " + f"for user '{self.username}': " + " ".join(ldap_user_groups) + ) - if (LDAP_ADMIN_GROUP in user_ad_member_of): - role_name = 'Administrator' + for role, ldap_group in ldap_group_security_roles.items(): + # Continue when groups is not defined or + # user is'nt member of LDAP group + if not ldap_group or not ldap_group in ldap_user_groups: + continue + + role_name = role current_app.logger.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_ad_member_of): - role_name = 'Operator' - current_app.logger.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_ad_member_of): - current_app.logger.info( - 'User {0} is part of the "{1}" group that allows user access to PowerDNS-Admin' - .format(self.username, - LDAP_USER_GROUP)) - else: - current_app.logger.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 + f"User '{self.username}' member of " + f"the '{ldap_group}' group that allows " + f"'{role}' access to to PowerDNS-Admin" + ) + + # Stop loop on first found + break + else: current_app.logger.error('Invalid LDAP type') return False From fd30e3ff497f6bd6984ef329e0e62592da085af8 Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sat, 11 Mar 2023 14:46:58 -0500 Subject: [PATCH 02/77] Added new JWKS URL setting for each OAuth provider and updated the associated authorization service to use the setting during the initialization of the authlib. --- powerdnsadmin/models/setting.py | 18 ++++--- powerdnsadmin/routes/admin.py | 8 +++ powerdnsadmin/services/azure.py | 1 + powerdnsadmin/services/github.py | 1 + powerdnsadmin/services/google.py | 1 + powerdnsadmin/services/oidc.py | 1 + .../admin_setting_authentication.html | 52 +++++++++++++++++-- 7 files changed, 71 insertions(+), 11 deletions(-) diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py index e820af9..331898a 100644 --- a/powerdnsadmin/models/setting.py +++ b/powerdnsadmin/models/setting.py @@ -28,7 +28,7 @@ class Setting(db.Model): 'allow_user_create_domain': False, 'allow_user_remove_domain': False, 'allow_user_view_history': False, - 'delete_sso_accounts': False, + 'delete_sso_accounts': False, 'bg_domain_updates': False, 'enable_api_rr_history': True, 'preserve_history': False, @@ -44,7 +44,7 @@ class Setting(db.Model): 'local_db_enabled': True, 'signup_enabled': True, 'autoprovisioning': False, - 'urn_value':'', + 'urn_value': '', 'autoprovisioning_attribute': '', 'purge': False, 'verify_user_email': False, @@ -69,15 +69,17 @@ class Setting(db.Model): 'github_oauth_scope': 'email', 'github_oauth_api_url': 'https://api.github.com/user', 'github_oauth_token_url': - 'https://github.com/login/oauth/access_token', + 'https://github.com/login/oauth/access_token', 'github_oauth_authorize_url': - 'https://github.com/login/oauth/authorize', + 'https://github.com/login/oauth/authorize', + 'github_oauth_jwks_url': '', 'google_oauth_enabled': False, 'google_oauth_client_id': '', 'google_oauth_client_secret': '', 'google_token_url': 'https://oauth2.googleapis.com/token', 'google_oauth_scope': 'openid email profile', 'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', + 'google_oauth_jwks_url': '', 'google_base_url': 'https://www.googleapis.com/oauth2/v3/', 'azure_oauth_enabled': False, 'azure_oauth_key': '', @@ -85,9 +87,10 @@ class Setting(db.Model): 'azure_oauth_scope': 'User.Read openid email profile', 'azure_oauth_api_url': 'https://graph.microsoft.com/v1.0/', 'azure_oauth_token_url': - 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/token', + 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/token', 'azure_oauth_authorize_url': - 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/authorize', + 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/authorize', + 'azure_oauth_jwks_url': '', 'azure_sg_enabled': False, 'azure_admin_group': '', 'azure_operator_group': '', @@ -104,6 +107,7 @@ class Setting(db.Model): 'oidc_oauth_api_url': '', 'oidc_oauth_token_url': '', 'oidc_oauth_authorize_url': '', + 'oidc_oauth_jwks_url': '', 'oidc_oauth_metadata_url': '', 'oidc_oauth_logout_url': '', 'oidc_oauth_username': 'preferred_username', @@ -284,7 +288,7 @@ class Setting(db.Model): result = self.query.filter(Setting.name == setting).first() if result is not None: - if hasattr(result,'value'): + if hasattr(result, 'value'): result = result.value return strtobool(result) if result in [ 'True', 'False' diff --git a/powerdnsadmin/routes/admin.py b/powerdnsadmin/routes/admin.py index 0d85d29..5aea1d2 100644 --- a/powerdnsadmin/routes/admin.py +++ b/powerdnsadmin/routes/admin.py @@ -1642,6 +1642,8 @@ def setting_authentication(): request.form.get('google_oauth_scope')) Setting().set('google_authorize_url', request.form.get('google_authorize_url')) + Setting().set('google_oauth_jwks_url', + request.form.get('google_oauth_jwks_url')) Setting().set('google_base_url', request.form.get('google_base_url')) result = { @@ -1673,6 +1675,8 @@ def setting_authentication(): request.form.get('github_oauth_token_url')) Setting().set('github_oauth_authorize_url', request.form.get('github_oauth_authorize_url')) + Setting().set('github_oauth_jwks_url', + request.form.get('github_oauth_jwks_url')) result = { 'status': True, 'msg': @@ -1702,6 +1706,8 @@ def setting_authentication(): request.form.get('azure_oauth_token_url')) Setting().set('azure_oauth_authorize_url', request.form.get('azure_oauth_authorize_url')) + Setting().set('azure_oauth_jwks_url', + request.form.get('azure_oauth_jwks_url')) Setting().set( 'azure_sg_enabled', True if request.form.get('azure_sg_enabled') == 'ON' else False) @@ -1753,6 +1759,8 @@ def setting_authentication(): request.form.get('oidc_oauth_token_url')) Setting().set('oidc_oauth_authorize_url', request.form.get('oidc_oauth_authorize_url')) + Setting().set('oidc_oauth_jwks_url', + request.form.get('oidc_oauth_jwks_url')) Setting().set('oidc_oauth_metadata_url', request.form.get('oidc_oauth_metadata_url')) Setting().set('oidc_oauth_logout_url', diff --git a/powerdnsadmin/services/azure.py b/powerdnsadmin/services/azure.py index 46fb1af..691b153 100644 --- a/powerdnsadmin/services/azure.py +++ b/powerdnsadmin/services/azure.py @@ -23,6 +23,7 @@ def azure_oauth(): request_token_url=None, access_token_url=Setting().get('azure_oauth_token_url'), authorize_url=Setting().get('azure_oauth_authorize_url'), + jwks_url=Setting().get('azure_oauth_jwks_url'), client_kwargs={'scope': Setting().get('azure_oauth_scope')}, fetch_token=fetch_azure_token, ) diff --git a/powerdnsadmin/services/github.py b/powerdnsadmin/services/github.py index cf615e8..8bcbe87 100644 --- a/powerdnsadmin/services/github.py +++ b/powerdnsadmin/services/github.py @@ -24,6 +24,7 @@ def github_oauth(): request_token_url=None, access_token_url=Setting().get('github_oauth_token_url'), authorize_url=Setting().get('github_oauth_authorize_url'), + jwks_url=Setting().get('github_oauth_jwks_url'), client_kwargs={'scope': Setting().get('github_oauth_scope')}, fetch_token=fetch_github_token, update_token=update_token) diff --git a/powerdnsadmin/services/google.py b/powerdnsadmin/services/google.py index 68775a2..0a62463 100644 --- a/powerdnsadmin/services/google.py +++ b/powerdnsadmin/services/google.py @@ -23,6 +23,7 @@ def google_oauth(): request_token_url=None, access_token_url=Setting().get('google_token_url'), authorize_url=Setting().get('google_authorize_url'), + jwks_url=Setting().get('google_oauth_jwks_url'), client_kwargs={'scope': Setting().get('google_oauth_scope')}, fetch_token=fetch_google_token, update_token=update_token) diff --git a/powerdnsadmin/services/oidc.py b/powerdnsadmin/services/oidc.py index b5da89e..432457f 100644 --- a/powerdnsadmin/services/oidc.py +++ b/powerdnsadmin/services/oidc.py @@ -23,6 +23,7 @@ def oidc_oauth(): request_token_url=None, access_token_url=Setting().get('oidc_oauth_token_url'), authorize_url=Setting().get('oidc_oauth_authorize_url'), + jwks_url=Setting().get('oidc_oauth_jwks_url'), server_metadata_url=Setting().get('oidc_oauth_metadata_url'), client_kwargs={'scope': Setting().get('oidc_oauth_scope')}, fetch_token=fetch_oidc_token, diff --git a/powerdnsadmin/templates/admin_setting_authentication.html b/powerdnsadmin/templates/admin_setting_authentication.html index 26d9a72..5750d89 100644 --- a/powerdnsadmin/templates/admin_setting_authentication.html +++ b/powerdnsadmin/templates/admin_setting_authentication.html @@ -663,6 +663,17 @@ value="{{ SETTING.get('google_authorize_url') }}"> +
+ + + +
+
+ + + +
@@ -915,6 +937,17 @@ value="{{ SETTING.get('azure_oauth_authorize_url') }}"> +
+ + + +
GROUP SECURITY @@ -1206,10 +1239,21 @@ name="oidc_oauth_authorize_url" id="oidc_oauth_authorize_url" placeholder="e.g. https://oidc.com/login/oauth/authorize" - data-error="Plesae input Authorize URL" + data-error="Please input Authorize URL" value="{{ SETTING.get('oidc_oauth_authorize_url') }}"> +
+ + + +
@@ -1217,7 +1261,7 @@ name="oidc_oauth_metadata_url" id="oidc_oauth_metadata_url" placeholder="e.g. https://oidc.com/login/oauth/.well-known/openid-configuration" - data-error="Plesae input Metadata URL" + data-error="Please input Metadata URL" value="{{ SETTING.get('oidc_oauth_metadata_url') }}">
@@ -1270,7 +1314,7 @@ From 369188e80e95a8896b6e61326ece8acc706fb1dc Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sat, 11 Mar 2023 14:50:02 -0500 Subject: [PATCH 03/77] Disabled MegaLinter workflow for all branches currently. --- .github/workflows/mega-linter.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/mega-linter.yml b/.github/workflows/mega-linter.yml index 4c005da..943d3a3 100644 --- a/.github/workflows/mega-linter.yml +++ b/.github/workflows/mega-linter.yml @@ -6,6 +6,7 @@ name: MegaLinter on: push: branches-ignore: + - "*" - "dev" - "main" - "master" From c61489adfc2c1d7cc8aab384b17846ded6347edc Mon Sep 17 00:00:00 2001 From: Stefan Ubbink Date: Sun, 12 Mar 2023 13:11:20 +0100 Subject: [PATCH 04/77] Improve things for using PostgreSQL --- configs/development.py | 10 ++++++++++ docker-test/Dockerfile | 1 + docker/Dockerfile | 1 + docs/wiki/database-setup/Setup-PostgreSQL.md | 13 ++++--------- ...ning-PowerDNS-Admin-on-Ubuntu-or-Debian.md | 19 +++++++++++++++---- requirements.txt | 1 + 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/configs/development.py b/configs/development.py index 6d1dd0d..e92ac72 100644 --- a/configs/development.py +++ b/configs/development.py @@ -27,6 +27,7 @@ CAPTCHA_SESSION_KEY = 'captcha_image' SESSION_TYPE = 'sqlalchemy' ### DATABASE - MySQL +## Don't forget to uncomment the import in the top #SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format( # urllib.parse.quote_plus(SQLA_DB_USER), # urllib.parse.quote_plus(SQLA_DB_PASSWORD), @@ -34,6 +35,15 @@ SESSION_TYPE = 'sqlalchemy' # SQLA_DB_NAME #) +### DATABASE - PostgreSQL +## Don't forget to uncomment the import in the top +#SQLALCHEMY_DATABASE_URI = 'postgres://{}:{}@{}/{}'.format( +# urllib.parse.quote_plus(SQLA_DB_USER), +# urllib.parse.quote_plus(SQLA_DB_PASSWORD), +# SQLA_DB_HOST, +# SQLA_DB_NAME +#) + ### DATABASE - SQLite SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db') diff --git a/docker-test/Dockerfile b/docker-test/Dockerfile index 17f934f..7191825 100644 --- a/docker-test/Dockerfile +++ b/docker-test/Dockerfile @@ -11,6 +11,7 @@ RUN apt-get update -y \ libffi-dev \ libldap2-dev \ libmariadb-dev-compat \ + libpq-dev \ libsasl2-dev \ libssl-dev \ libxml2-dev \ diff --git a/docker/Dockerfile b/docker/Dockerfile index b553998..55ccdfd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,6 +2,7 @@ FROM alpine:3.17 AS builder ARG BUILD_DEPENDENCIES="build-base \ libffi-dev \ + libpq-dev \ libxml2-dev \ mariadb-connector-c-dev \ openldap-dev \ diff --git a/docs/wiki/database-setup/Setup-PostgreSQL.md b/docs/wiki/database-setup/Setup-PostgreSQL.md index a6e3364..197aae5 100644 --- a/docs/wiki/database-setup/Setup-PostgreSQL.md +++ b/docs/wiki/database-setup/Setup-PostgreSQL.md @@ -15,10 +15,9 @@ The below will create a database called powerdnsadmindb and a user of powerdnsad ``` $ sudo su - postgres $ createuser powerdnsadmin -$ createdb powerdnsadmindb +$ createdb -E UTF8 -l en_US.UTF-8 -O powerdnsadmin -T template0 powerdnsadmindb 'The database for PowerDNS-Admin' $ psql -postgres=# alter user powerdnsadmin with encrypted password 'powerdnsadmin'; -postgres=# grant all privileges on database powerdnsadmindb to powerdnsadmin; +postgres=# ALTER ROLE powerdnsadmin WITH PASSWORD 'powerdnsadmin_password'; ``` Note: @@ -51,18 +50,14 @@ On debian based systems these files are located in: ## Install required packages: ### Red-hat based systems: +TODO: confirm this is correct ``` sudo yum install postgresql-libs ``` ### Debian based systems: ``` -apt install libpq-dev python-dev -``` - -### Install python packages: -``` -pip3 install psycopg2 +apt install python3-psycopg2 ``` ## Known Issues: diff --git a/docs/wiki/install/Running-PowerDNS-Admin-on-Ubuntu-or-Debian.md b/docs/wiki/install/Running-PowerDNS-Admin-on-Ubuntu-or-Debian.md index d3bc834..ad51c2b 100644 --- a/docs/wiki/install/Running-PowerDNS-Admin-on-Ubuntu-or-Debian.md +++ b/docs/wiki/install/Running-PowerDNS-Admin-on-Ubuntu-or-Debian.md @@ -7,19 +7,30 @@ First setup your database accordingly: ### Install required packages for building python libraries from requirements.txt file +For Debian 11 (bullseye) and above: ```bash -sudo apt install -y python3-dev git libsasl2-dev libldap2-dev libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev pkg-config apt-transport-https virtualenv build-essential curl +sudo apt install -y python3-dev git libsasl2-dev libldap2-dev python3-venv libmariadb-dev pkg-config build-essential curl libpq-dev +``` +Older systems might also need the following: +```bash +sudo apt install -y libssl-dev libxml2-dev libxslt1-dev libxmlsec1-dev libffi-dev apt-transport-https virtualenv ``` ### Install NodeJs ```bash -curl -sL https://deb.nodesource.com/setup_14.x | bash - -apt install -y nodejs +curl -sL https://deb.nodesource.com/setup_14.x | sudo bash - +sudo apt install -y nodejs ``` ### Install yarn to build asset files - +For Debian 11 (bullseye) and above: +```bash +curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | sudo tee /usr/share/keyrings/yarnkey.gpg >/dev/null +echo "deb [signed-by=/usr/share/keyrings/yarnkey.gpg] https://dl.yarnpkg.com/debian stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +sudo apt update && sudo apt install -y yarn +``` +For older Debian systems: ```bash sudo curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list diff --git a/requirements.txt b/requirements.txt index 9753bf0..839c004 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,3 +43,4 @@ webcolors==1.12 werkzeug==2.1.2 zipp==3.11.0 rcssmin==1.1.1 +psycopg2==2.9.5 From 1afe9b490866f8afb951acddecbc53b0fbdefb8e Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sun, 12 Mar 2023 09:13:54 -0400 Subject: [PATCH 05/77] Finished adding new OAuth Server Metadata URL setting to Google, GitHub, and Microsoft OAuth service configuration features. --- powerdnsadmin/models/setting.py | 3 + powerdnsadmin/routes/admin.py | 10 ++- powerdnsadmin/services/azure.py | 1 + powerdnsadmin/services/github.py | 1 + powerdnsadmin/services/google.py | 1 + .../admin_setting_authentication.html | 65 ++++++++++++------- 6 files changed, 57 insertions(+), 24 deletions(-) diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py index 331898a..bef6897 100644 --- a/powerdnsadmin/models/setting.py +++ b/powerdnsadmin/models/setting.py @@ -73,6 +73,7 @@ class Setting(db.Model): 'github_oauth_authorize_url': 'https://github.com/login/oauth/authorize', 'github_oauth_jwks_url': '', + 'github_oauth_metadata_url': '', 'google_oauth_enabled': False, 'google_oauth_client_id': '', 'google_oauth_client_secret': '', @@ -80,6 +81,7 @@ class Setting(db.Model): 'google_oauth_scope': 'openid email profile', 'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', 'google_oauth_jwks_url': '', + 'google_oauth_metadata_url': '', 'google_base_url': 'https://www.googleapis.com/oauth2/v3/', 'azure_oauth_enabled': False, 'azure_oauth_key': '', @@ -91,6 +93,7 @@ class Setting(db.Model): 'azure_oauth_authorize_url': 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/authorize', 'azure_oauth_jwks_url': '', + 'azure_oauth_metadata_url': '', 'azure_sg_enabled': False, 'azure_admin_group': '', 'azure_operator_group': '', diff --git a/powerdnsadmin/routes/admin.py b/powerdnsadmin/routes/admin.py index 5aea1d2..900e70c 100644 --- a/powerdnsadmin/routes/admin.py +++ b/powerdnsadmin/routes/admin.py @@ -1636,6 +1636,8 @@ def setting_authentication(): request.form.get('google_oauth_client_id')) Setting().set('google_oauth_client_secret', request.form.get('google_oauth_client_secret')) + Setting().set('google_oauth_metadata_url', + request.form.get('google_oauth_metadata_url')) Setting().set('google_token_url', request.form.get('google_token_url')) Setting().set('google_oauth_scope', @@ -1671,6 +1673,8 @@ def setting_authentication(): request.form.get('github_oauth_scope')) Setting().set('github_oauth_api_url', request.form.get('github_oauth_api_url')) + Setting().set('github_oauth_metadata_url', + request.form.get('github_oauth_metadata_url')) Setting().set('github_oauth_token_url', request.form.get('github_oauth_token_url')) Setting().set('github_oauth_authorize_url', @@ -1702,6 +1706,8 @@ def setting_authentication(): request.form.get('azure_oauth_scope')) Setting().set('azure_oauth_api_url', request.form.get('azure_oauth_api_url')) + Setting().set('azure_oauth_metadata_url', + request.form.get('azure_oauth_metadata_url')) Setting().set('azure_oauth_token_url', request.form.get('azure_oauth_token_url')) Setting().set('azure_oauth_authorize_url', @@ -1755,14 +1761,14 @@ def setting_authentication(): request.form.get('oidc_oauth_scope')) Setting().set('oidc_oauth_api_url', request.form.get('oidc_oauth_api_url')) + Setting().set('oidc_oauth_metadata_url', + request.form.get('oidc_oauth_metadata_url')) Setting().set('oidc_oauth_token_url', request.form.get('oidc_oauth_token_url')) Setting().set('oidc_oauth_authorize_url', request.form.get('oidc_oauth_authorize_url')) Setting().set('oidc_oauth_jwks_url', request.form.get('oidc_oauth_jwks_url')) - Setting().set('oidc_oauth_metadata_url', - request.form.get('oidc_oauth_metadata_url')) Setting().set('oidc_oauth_logout_url', request.form.get('oidc_oauth_logout_url')) Setting().set('oidc_oauth_username', diff --git a/powerdnsadmin/services/azure.py b/powerdnsadmin/services/azure.py index 691b153..c1fb626 100644 --- a/powerdnsadmin/services/azure.py +++ b/powerdnsadmin/services/azure.py @@ -24,6 +24,7 @@ def azure_oauth(): access_token_url=Setting().get('azure_oauth_token_url'), authorize_url=Setting().get('azure_oauth_authorize_url'), jwks_url=Setting().get('azure_oauth_jwks_url'), + server_metadata_url=Setting().get('azure_oauth_metadata_url'), client_kwargs={'scope': Setting().get('azure_oauth_scope')}, fetch_token=fetch_azure_token, ) diff --git a/powerdnsadmin/services/github.py b/powerdnsadmin/services/github.py index 8bcbe87..13c2f00 100644 --- a/powerdnsadmin/services/github.py +++ b/powerdnsadmin/services/github.py @@ -25,6 +25,7 @@ def github_oauth(): access_token_url=Setting().get('github_oauth_token_url'), authorize_url=Setting().get('github_oauth_authorize_url'), jwks_url=Setting().get('github_oauth_jwks_url'), + server_metadata_url=Setting().get('github_oauth_metadata_url'), client_kwargs={'scope': Setting().get('github_oauth_scope')}, fetch_token=fetch_github_token, update_token=update_token) diff --git a/powerdnsadmin/services/google.py b/powerdnsadmin/services/google.py index 0a62463..fc9af12 100644 --- a/powerdnsadmin/services/google.py +++ b/powerdnsadmin/services/google.py @@ -24,6 +24,7 @@ def google_oauth(): access_token_url=Setting().get('google_token_url'), authorize_url=Setting().get('google_authorize_url'), jwks_url=Setting().get('google_oauth_jwks_url'), + server_metadata_url=Setting().get('google_oauth_metadata_url'), client_kwargs={'scope': Setting().get('google_oauth_scope')}, fetch_token=fetch_google_token, update_token=update_token) diff --git a/powerdnsadmin/templates/admin_setting_authentication.html b/powerdnsadmin/templates/admin_setting_authentication.html index 5750d89..7675797 100644 --- a/powerdnsadmin/templates/admin_setting_authentication.html +++ b/powerdnsadmin/templates/admin_setting_authentication.html @@ -630,9 +630,16 @@ value="{{ SETTING.get('google_oauth_client_secret') }}"> -
-
- ADVANCE +
+ + + +
-
-
- ADVANCE
+
+ + + +
@@ -893,9 +907,6 @@ value="{{ SETTING.get('azure_oauth_secret') }}">
-
-
- ADVANCED
+
+ + + +
+
+ + + +
-
- - - -
@@ -1278,7 +1299,7 @@
- CLAIMS + Claims
- ADVANCE + Advanced
From ee68b18e278fb18b152bdac306c1f53d67ceeb09 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 12 Mar 2023 13:36:30 +0000 Subject: [PATCH 06/77] Added custom header in created_by segment option --- powerdnsadmin/models/setting.py | 3 ++- powerdnsadmin/routes/admin.py | 1 + powerdnsadmin/routes/api.py | 16 ++++++++++++---- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py index e820af9..6b46817 100644 --- a/powerdnsadmin/models/setting.py +++ b/powerdnsadmin/models/setting.py @@ -28,7 +28,8 @@ class Setting(db.Model): 'allow_user_create_domain': False, 'allow_user_remove_domain': False, 'allow_user_view_history': False, - 'delete_sso_accounts': False, + 'delete_sso_accounts': False, + 'custom_history_header': '', 'bg_domain_updates': False, 'enable_api_rr_history': True, 'preserve_history': False, diff --git a/powerdnsadmin/routes/admin.py b/powerdnsadmin/routes/admin.py index 0d85d29..759316f 100644 --- a/powerdnsadmin/routes/admin.py +++ b/powerdnsadmin/routes/admin.py @@ -1391,6 +1391,7 @@ def setting_basic(): 'default_domain_table_size', 'default_record_table_size', 'delete_sso_accounts', + 'custom_history_header', 'deny_domain_override', 'dnssec_admins_only', 'enable_api_rr_history', diff --git a/powerdnsadmin/routes/api.py b/powerdnsadmin/routes/api.py index 2c9a2cc..e764f91 100644 --- a/powerdnsadmin/routes/api.py +++ b/powerdnsadmin/routes/api.py @@ -48,6 +48,12 @@ user_detailed_schema = UserDetailedSchema() account_schema = AccountSchema(many=True) account_single_schema = AccountSchema() +def is_custom_header_api(): + custom_header_setting = Setting().get('custom_history_header') + if custom_header_setting != '' and custom_header_setting in request.headers: + return request.headers[custom_header_setting] + else: + return g.apikey.description def get_user_domains(): domains = db.session.query(Domain) \ @@ -1104,6 +1110,7 @@ def api_zone_forward(server_id, zone_id): domain = Domain() domain.update() status = resp.status_code + created_by_value=is_custom_header_api() if 200 <= status < 300: current_app.logger.debug("Request to powerdns API successful") if Setting().get('enable_api_rr_history'): @@ -1116,19 +1123,19 @@ def api_zone_forward(server_id, zone_id): 'add_rrsets': list(filter(lambda r: r['changetype'] == "REPLACE", data['rrsets'])), 'del_rrsets': list(filter(lambda r: r['changetype'] == "DELETE", data['rrsets'])) }), - created_by=g.apikey.description, + created_by=created_by_value, domain_id=Domain().get_id_by_name(zone_id.rstrip('.'))) history.add() elif request.method == 'DELETE': history = History(msg='Deleted zone {0}'.format(zone_id.rstrip('.')), detail='', - created_by=g.apikey.description, + created_by=created_by_value, domain_id=Domain().get_id_by_name(zone_id.rstrip('.'))) history.add() elif request.method != 'GET': history = History(msg='Updated zone {0}'.format(zone_id.rstrip('.')), detail='', - created_by=g.apikey.description, + created_by=created_by_value, domain_id=Domain().get_id_by_name(zone_id.rstrip('.'))) history.add() return resp.content, resp.status_code, resp.headers.items() @@ -1152,6 +1159,7 @@ def api_create_zone(server_id): if resp.status_code == 201: current_app.logger.debug("Request to powerdns API successful") + created_by_value=is_custom_header_api() data = request.get_json(force=True) if g.apikey.role.name not in ['Administrator', 'Operator']: @@ -1166,7 +1174,7 @@ def api_create_zone(server_id): history = History(msg='Add domain {0}'.format( data['name'].rstrip('.')), detail=json.dumps(data), - created_by=g.apikey.description, + created_by=created_by_value, domain_id=domain.get_id_by_name(data['name'].rstrip('.'))) history.add() From 84cfd165b4d3e34d15b024b9cbcc651b66aa8d8e Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sun, 12 Mar 2023 10:27:04 -0400 Subject: [PATCH 07/77] Re-arranged side navigation to include the "Global Search" feature regardless of user role as the global search feature is now accessible to all users. Also moved the "Activity" feature link higher in the menu to remove duplicate code from the navigation code base. --- powerdnsadmin/templates/base.html | 34 +++++++++++++------------------ 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/powerdnsadmin/templates/base.html b/powerdnsadmin/templates/base.html index 5a47bd2..408f4be 100644 --- a/powerdnsadmin/templates/base.html +++ b/powerdnsadmin/templates/base.html @@ -101,14 +101,22 @@ {% endif %} - {% if current_user.role.name in ['Administrator', 'Operator'] %} - -
  • - - -

    Global Search

    +
  • +
  • + + +

    Global Search

    +
    +
  • + {% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %} +
  • + + +

    Activity

  • + {% endif %} + {% if current_user.role.name in ['Administrator', 'Operator'] %}
  • @@ -121,12 +129,6 @@

    Server Configuration

  • -
  • - - -

    Activity

    -
    -
  • @@ -189,14 +191,6 @@ {% endif %}
  • - {% elif SETTING.get('allow_user_view_history') %} - -
  • - - -

    History

    -
    -
  • {% endif %} {% endif %} From 0ac7a5a4537cb32b6aa22f4ca110a7dd27427171 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 12 Mar 2023 15:00:32 +0000 Subject: [PATCH 08/77] Added some explanation about some of the 'basic' settings in the admin --- docs/basic_settings.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 docs/basic_settings.md diff --git a/docs/basic_settings.md b/docs/basic_settings.md new file mode 100644 index 0000000..6a47d6a --- /dev/null +++ b/docs/basic_settings.md @@ -0,0 +1,17 @@ +### PowerDNSAdmin basic settings + +PowerDNSAdmin has many features and settings available to be turned either off or on. +In this docs those settings will be explain. +To find the settings in the the dashboard go to settings>basic. + +allow_user_create_domain: This setting is used to allow users with the `user` role to create a domain, not possible by +default. + +allow_user_remove_domain: Same as `allow_user_create_domain` but for removing a domain. + +allow_user_view_history: Allow a user with the role `user` to view and access the history. + +custom_history_header: This is a string type variable, when inputting an header name, if exists in the request it will +be in the created_by column in the history, if empty or not mentioned will default to the api_key description. + +site_name: This will be the site name. From 695d7462958812ad3a3f411077988b2efae51bbe Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 12 Mar 2023 15:32:32 +0000 Subject: [PATCH 09/77] Changed basic_settings.md path --- docs/{ => wiki/configuration}/basic_settings.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/{ => wiki/configuration}/basic_settings.md (100%) diff --git a/docs/basic_settings.md b/docs/wiki/configuration/basic_settings.md similarity index 100% rename from docs/basic_settings.md rename to docs/wiki/configuration/basic_settings.md From 3e9e73fb3a50bf9fbf149924b8b03d9abeb80836 Mon Sep 17 00:00:00 2001 From: Stefan Ubbink Date: Sun, 12 Mar 2023 20:40:19 +0100 Subject: [PATCH 10/77] Change domain(s) to zone(s) in the templates --- .../templates/admin_edit_account.html | 14 +++++++------- powerdnsadmin/templates/admin_edit_key.html | 18 +++++++++--------- powerdnsadmin/templates/admin_edit_user.html | 4 ++-- .../templates/admin_global_search.html | 2 +- powerdnsadmin/templates/admin_history.html | 8 ++++---- .../templates/admin_manage_account.html | 2 +- powerdnsadmin/templates/admin_manage_keys.html | 2 +- powerdnsadmin/templates/admin_manage_user.html | 2 +- .../admin_setting_authentication.html | 4 ++-- powerdnsadmin/templates/dashboard.html | 2 +- powerdnsadmin/templates/domain_add.html | 6 +++--- powerdnsadmin/templates/domain_changelog.html | 6 +++--- powerdnsadmin/templates/domain_remove.html | 4 ++-- powerdnsadmin/templates/domain_setting.html | 18 +++++++++--------- 14 files changed, 46 insertions(+), 46 deletions(-) diff --git a/powerdnsadmin/templates/admin_edit_account.html b/powerdnsadmin/templates/admin_edit_account.html index 4894df8..ca08ab7 100644 --- a/powerdnsadmin/templates/admin_edit_account.html +++ b/powerdnsadmin/templates/admin_edit_account.html @@ -96,7 +96,7 @@
    -

    Users on the right have access to manage records in all domains +

    Users on the right have access to manage records in all zones associated with the account.

    Click on users to move between columns.

    @@ -113,12 +113,12 @@
    -

    Domains on the right are associated with the account. Red marked domain names are +

    Zones on the right are associated with the account. Red marked zone names are already associated with other accounts. - Moving already associated domains to this account will overwrite the previous + Moving already associated zones to this account will overwrite the previous associated account.

    -

    Hover over the red domain names to show the associated account. Click on domains to +

    Hover over the red zone names to show the associated account. Click on zones to move between columns.

    From 1cea4b7ce324590a4ae1c8f675f432cd389d114b Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Fri, 17 Mar 2023 03:44:08 +0000 Subject: [PATCH 21/77] feat(authentication): added password policy checker function --- powerdnsadmin/routes/index.py | 88 +++++++++++++++++++++++++++++++++++ requirements.txt | 1 + 2 files changed, 89 insertions(+) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 72305a6..79768c7 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -5,6 +5,8 @@ import traceback import datetime import ipaddress import base64 +import string +from zxcvbn import zxcvbn from distutils.util import strtobool from yaml import Loader, load from flask import Blueprint, render_template, make_response, url_for, current_app, g, session, request, redirect, abort @@ -649,6 +651,92 @@ def logout(): return redirect(redirect_uri) +def password_policy_check(user, password): + + def check_policy(chars, user_password, setting): + lenreq = int(Setting().get(setting)) + test_string = user_password + for c in chars: + test_string = test_string.replace(c, '') + return (lenreq, len(user_password) - len(test_string)) + + def matches_policy(item, policy_fails): + return "*" if item in policy_fails else "" + + policy = [] + policy_fails = {} + + # If either policy is enabled check basics first ... this is obvious! + if Setting().get('pwd_enforce_characters') or Setting().get('pwd_enforce_complexity'): + # Cannot contain username + if user.username in password: + policy_fails["username"] = True + policy.append(f"{matches_policy('username', policy_fails)}cannot contain username") + + # Cannot contain password + if user.firstname in password: + policy_fails["firstname"] = True + policy.append(f"{matches_policy('firstname', policy_fails)}cannot contain firstname") + + # Cannot contain lastname + if user.lastname in password: + policy_fails["lastname"] = True + policy.append(f"{matches_policy('lastname', policy_fails)}cannot contain lastname") + + # Cannot contain email + if user.email in password: + policy_fails["email"] = True + policy.append(f"{matches_policy('email', policy_fails)}cannot contain email") + + # Check if we're enforcing character requirements + if Setting().get('pwd_enforce_characters'): + # Length + pwd_min_len_setting = int(Setting().get('pwd_min_len')) + pwd_len = len(password) + if pwd_len < pwd_min_len_setting: + policy_fails["length"] = True + policy.append(f"{matches_policy('length', policy_fails)}length={pwd_len}/{pwd_min_len_setting}") + # Digits + (pwd_min_digits_setting, pwd_digits) = check_policy(string.digits, password, 'pwd_min_digits') + if pwd_digits < pwd_min_digits_setting: + policy_fails["digits"] = True + policy.append(f"{matches_policy('digits', policy_fails)}digits={pwd_digits}/{pwd_min_digits_setting}") + # Lowercase + (pwd_min_lowercase_setting, pwd_lowercase) = check_policy(string.digits, password, 'pwd_min_lowercase') + if pwd_lowercase < pwd_min_lowercase_setting: + policy_fails["lowercase"] = True + policy.append(f"{matches_policy('lowercase', policy_fails)}lowercase={pwd_lowercase}/{pwd_min_lowercase_setting}") + # Uppercase + (pwd_min_uppercase_setting, pwd_uppercase) = check_policy(string.digits, password, 'pwd_min_uppercase') + if pwd_uppercase < pwd_min_uppercase_setting: + policy_fails["uppercase"] = True + policy.append(f"{matches_policy('uppercase', policy_fails)}uppercase={pwd_uppercase}/{pwd_min_uppercase_setting}") + # Special + (pwd_min_special_setting, pwd_special) = check_policy(string.digits, password, 'pwd_min_special') + if pwd_special < pwd_min_special_setting: + policy_fails["special"] = True + policy.append(f"{matches_policy('special', policy_fails)}special={pwd_special}/{pwd_min_special_setting}") + + if Setting().get('pwd_enforce_complexity'): + # Complexity checking + zxcvbn_inputs = [] + for input in (user.firstname, user.lastname, user.username, user.email): + if len(input): + zxcvbn_inputs.append(input) + + result = zxcvbn(password, user_inputs=zxcvbn_inputs) + pwd_min_complexity_setting = int(Setting().get('pwd_min_complexity')) + pwd_complexity = result['guesses_log10'] + if pwd_complexity < pwd_min_complexity_setting: + policy_fails["complexity"] = True + policy.append(f"{matches_policy('complexity', policy_fails)}complexity={pwd_complexity:.0f}/{pwd_min_complexity_setting}") + + policy_str = {"password": f"Fails policy: {', '.join(policy)}. Items prefixed with '*' failed."} + + # NK: the first item in the tuple indicates a PASS, so, we check for any True's and negate that + return (not any(policy_fails.values()), policy_str) + + @index_bp.route('/register', methods=['GET', 'POST']) def register(): CAPTCHA_ENABLE = current_app.config.get('CAPTCHA_ENABLE') diff --git a/requirements.txt b/requirements.txt index 83d1011..b8256b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -43,3 +43,4 @@ webcolors==1.12 werkzeug==2.1.2 zipp==3.11.0 rcssmin==1.1.1 +zxcvbn==4.4.28 \ No newline at end of file From fc14e9189d9b26c383384e5bf62095fe114650db Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Fri, 17 Mar 2023 03:45:09 +0000 Subject: [PATCH 22/77] feat(authentication): check password policy during registration of new users --- powerdnsadmin/routes/index.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 79768c7..a21ad31 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -788,6 +788,10 @@ def register(): email=email ) + (password_policy_pass, password_policy) = password_policy_check(user, password) + if not password_policy_pass: + return render_template('register.html', error_messages=password_policy, captcha_enable=CAPTCHA_ENABLE) + try: result = user.create_local_user() if result and result['status']: From 64017195da56b5fcefce23e37367b83e2a404707 Mon Sep 17 00:00:00 2001 From: Nigel Kukard Date: Fri, 17 Mar 2023 03:45:37 +0000 Subject: [PATCH 23/77] feat(authentication): check password policy during user profile password change --- powerdnsadmin/routes/user.py | 13 +++++++++++++ powerdnsadmin/templates/user_profile.html | 19 ++++++++++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/powerdnsadmin/routes/user.py b/powerdnsadmin/routes/user.py index 65d7e08..adba502 100644 --- a/powerdnsadmin/routes/user.py +++ b/powerdnsadmin/routes/user.py @@ -9,6 +9,8 @@ from flask_login import current_user, login_required, login_manager from ..models.user import User, Anonymous from ..models.setting import Setting +from .index import password_policy_check + user_bp = Blueprint('user', __name__, @@ -79,12 +81,23 @@ def profile(): .format(current_user.username) }), 400) + (password_policy_pass, password_policy) = password_policy_check(current_user.get_user_info_by_username(), new_password) + if not password_policy_pass: + if request.data: + return make_response( + jsonify({ + 'status': 'error', + 'msg': password_policy['password'], + }), 400) + return render_template('user_profile.html', error_messages=password_policy) + user = User(username=current_user.username, plain_text_password=new_password, firstname=firstname, lastname=lastname, email=email, reload_info=False) + user.update_profile() return render_template('user_profile.html') diff --git a/powerdnsadmin/templates/user_profile.html b/powerdnsadmin/templates/user_profile.html index 3bb9971..ee161bb 100644 --- a/powerdnsadmin/templates/user_profile.html +++ b/powerdnsadmin/templates/user_profile.html @@ -34,13 +34,13 @@ -
    - - - -
    Group Security @@ -1413,17 +1380,6 @@ value="{{ SETTING.get('oidc_oauth_authorize_url') }}"> -
    - - - -
    From a2429ad9d6fe0b7e7c5dce8433b3afe722757806 Mon Sep 17 00:00:00 2001 From: Stefan Ubbink Date: Sun, 2 Apr 2023 20:46:32 +0200 Subject: [PATCH 55/77] Make it possible again to use a different Zone Type than 'native', fixes #1501 --- powerdnsadmin/routes/domain.py | 6 +++--- powerdnsadmin/templates/domain_add.html | 18 +++++++++--------- powerdnsadmin/templates/domain_setting.html | 16 ++++++++-------- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/powerdnsadmin/routes/domain.py b/powerdnsadmin/routes/domain.py index 593e91d..bcf91cc 100644 --- a/powerdnsadmin/routes/domain.py +++ b/powerdnsadmin/routes/domain.py @@ -66,7 +66,7 @@ def domain(domain_name): current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets))) # API server might be down, misconfigured - if not rrsets and domain.type != 'Slave': + if not rrsets and domain.type != 'slave': abort(500) quick_edit = Setting().get('record_quick_edit') @@ -206,7 +206,7 @@ def changelog(domain_name): current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets))) # API server might be down, misconfigured - if not rrsets and domain.type != 'Slave': + if not rrsets and domain.type != 'slave': abort(500) records_allow_to_edit = Setting().get_records_allow_to_edit() @@ -294,7 +294,7 @@ def record_changelog(domain_name, record_name, record_type): current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets))) # API server might be down, misconfigured - if not rrsets and domain.type != 'Slave': + if not rrsets and domain.type != 'slave': abort(500) # get all changelogs for this domain, in descening order diff --git a/powerdnsadmin/templates/domain_add.html b/powerdnsadmin/templates/domain_add.html index c568210..8be5817 100644 --- a/powerdnsadmin/templates/domain_add.html +++ b/powerdnsadmin/templates/domain_add.html @@ -76,11 +76,16 @@
    +
    -
    @@ -228,10 +228,10 @@ diff --git a/powerdnsadmin/templates/domain_setting.html b/powerdnsadmin/templates/domain_setting.html index bc9470e..f59f4a9 100644 --- a/powerdnsadmin/templates/domain_setting.html +++ b/powerdnsadmin/templates/domain_setting.html @@ -220,12 +220,12 @@
    -
    + Secret
    +
    + + + +
    +
    + + + +
    @@ -785,16 +804,6 @@ value="{{ SETTING.get('google_token_url') }}">
    -
    - - - -
    @@ -806,15 +815,6 @@ value="{{ SETTING.get('google_authorize_url') }}">
    -
    - - - -
    @@ -870,26 +870,26 @@ -
    - +
    + Secret
    @@ -1311,21 +1311,21 @@ OAuth
    - +
    - +
    From 9168dd99e074709e2d1cc7f6dd357f25c4aa9e8b Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sat, 8 Apr 2023 18:11:55 -0400 Subject: [PATCH 60/77] Updated the OAuth login handlers to utilize uniform user naming variables. Updated the GitHub login process to split the user's full name based on spaces so that first and last name are filled in on PDA profile. --- powerdnsadmin/routes/index.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 4351a54..706d0b9 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -189,17 +189,25 @@ def login(): if 'github_token' in session: me = json.loads(github.get('user').text) github_username = me['login'] - github_name = me['name'] + github_first_name = me['name'] + github_last_name = '' github_email = me['email'] + # If the user's full name from GitHub contains at least two words, use the first word as the first name and + # the rest as the last name. + github_name_parts = github_first_name.split(' ') + if len(github_name_parts) > 1: + github_first_name = github_name_parts[0] + github_last_name = ' '.join(github_name_parts[1:]) + user = User.query.filter_by(username=github_username).first() if user is None: user = User.query.filter_by(email=github_email).first() if not user: user = User(username=github_username, plain_text_password=None, - firstname=github_name, - lastname='', + firstname=github_first_name, + lastname=github_last_name, email=github_email) result = user.create_local_user() @@ -227,8 +235,8 @@ def login(): mygroups = [] azure_username = me["userPrincipalName"] - azure_givenname = me["givenName"] - azure_familyname = me["surname"] + azure_first_name = me["givenName"] + azure_last_name = me["surname"] if "mail" in me: azure_email = me["mail"] else: @@ -244,8 +252,8 @@ def login(): if not user: user = User(username=azure_username, plain_text_password=None, - firstname=azure_givenname, - lastname=azure_familyname, + firstname=azure_first_name, + lastname=azure_last_name, email=azure_email) result = user.create_local_user() @@ -386,21 +394,21 @@ def login(): if 'oidc_token' in session: me = json.loads(oidc.get('userinfo').text) oidc_username = me[Setting().get('oidc_oauth_username')] - oidc_givenname = me[Setting().get('oidc_oauth_firstname')] - oidc_familyname = me[Setting().get('oidc_oauth_last_name')] + oidc_first_name = me[Setting().get('oidc_oauth_firstname')] + oidc_last_name = me[Setting().get('oidc_oauth_last_name')] oidc_email = me[Setting().get('oidc_oauth_email')] user = User.query.filter_by(username=oidc_username).first() if not user: user = User(username=oidc_username, plain_text_password=None, - firstname=oidc_givenname, - lastname=oidc_familyname, + firstname=oidc_first_name, + lastname=oidc_last_name, email=oidc_email) result = user.create_local_user() else: - user.firstname = oidc_givenname - user.lastname = oidc_familyname + user.firstname = oidc_first_name + user.lastname = oidc_last_name user.email = oidc_email user.plain_text_password = None result = user.update_local_user() From ece96262124985143467534394c64a35b7b35cda Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sat, 8 Apr 2023 18:14:40 -0400 Subject: [PATCH 61/77] Updated the OAuth login handlers to utilize uniform user naming variables. Updated the GitHub login process to split the user's full name based on spaces so that first and last name are filled in on PDA profile. --- powerdnsadmin/routes/index.py | 59 ++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 706d0b9..2636cf1 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -164,18 +164,18 @@ def login(): if 'google_token' in session: user_data = json.loads(google.get('userinfo').text) - first_name = user_data['given_name'] - surname = user_data['family_name'] - email = user_data['email'] - user = User.query.filter_by(username=email).first() + google_first_name = user_data['given_name'] + google_last_name = user_data['family_name'] + google_email = user_data['email'] + user = User.query.filter_by(username=google_email).first() if user is None: - user = User.query.filter_by(email=email).first() + user = User.query.filter_by(email=google_email).first() if not user: - user = User(username=email, - firstname=first_name, - lastname=surname, + user = User(username=google_email, + firstname=google_first_name, + lastname=google_last_name, plain_text_password=None, - email=email) + email=google_email) result = user.create_local_user() if not result['status']: @@ -187,11 +187,11 @@ def login(): return authenticate_user(user, 'Google OAuth') if 'github_token' in session: - me = json.loads(github.get('user').text) - github_username = me['login'] - github_first_name = me['name'] + user_data = json.loads(github.get('user').text) + github_username = user_data['login'] + github_first_name = user_data['name'] github_last_name = '' - github_email = me['email'] + github_email = user_data['email'] # If the user's full name from GitHub contains at least two words, use the first word as the first name and # the rest as the last name. @@ -222,7 +222,7 @@ def login(): if 'azure_token' in session: azure_info = azure.get('me?$select=displayName,givenName,id,mail,surname,userPrincipalName').text current_app.logger.info('Azure login returned: ' + azure_info) - me = json.loads(azure_info) + user_data = json.loads(azure_info) azure_info = azure.post('me/getMemberGroups', json={'securityEnabledOnly': False}).text @@ -234,15 +234,15 @@ def login(): else: mygroups = [] - azure_username = me["userPrincipalName"] - azure_first_name = me["givenName"] - azure_last_name = me["surname"] - if "mail" in me: - azure_email = me["mail"] + azure_username = user_data["userPrincipalName"] + azure_first_name = user_data["givenName"] + azure_last_name = user_data["surname"] + if "mail" in user_data: + azure_email = user_data["mail"] else: azure_email = "" if not azure_email: - azure_email = me["userPrincipalName"] + azure_email = user_data["userPrincipalName"] # Handle foreign principals such as guest users azure_email = re.sub(r"#.*$", "", azure_email) @@ -392,11 +392,11 @@ def login(): return authenticate_user(user, 'Azure OAuth') if 'oidc_token' in session: - me = json.loads(oidc.get('userinfo').text) - oidc_username = me[Setting().get('oidc_oauth_username')] - oidc_first_name = me[Setting().get('oidc_oauth_firstname')] - oidc_last_name = me[Setting().get('oidc_oauth_last_name')] - oidc_email = me[Setting().get('oidc_oauth_email')] + user_data = json.loads(oidc.get('userinfo').text) + oidc_username = user_data[Setting().get('oidc_oauth_username')] + oidc_first_name = user_data[Setting().get('oidc_oauth_firstname')] + oidc_last_name = user_data[Setting().get('oidc_oauth_last_name')] + oidc_email = user_data[Setting().get('oidc_oauth_email')] user = User.query.filter_by(username=oidc_username).first() if not user: @@ -426,10 +426,11 @@ def login(): desc_prop = Setting().get('oidc_oauth_account_description_property') account_to_add = [] - # If the name_property and desc_property exist in me (A variable that contains all the userinfo from the IdP). - if name_prop in me and desc_prop in me: - accounts_name_prop = [me[name_prop]] if type(me[name_prop]) is not list else me[name_prop] - accounts_desc_prop = [me[desc_prop]] if type(me[desc_prop]) is not list else me[desc_prop] + # If the name_property and desc_property exist in me (A variable that contains all the userinfo from the + # IdP). + if name_prop in user_data and desc_prop in user_data: + accounts_name_prop = [user_data[name_prop]] if type(user_data[name_prop]) is not list else user_data[name_prop] + accounts_desc_prop = [user_data[desc_prop]] if type(user_data[desc_prop]) is not list else user_data[desc_prop] # Run on all groups the user is in by the index num. for i in range(len(accounts_name_prop)): From 737e104912af07d07bdf2daf7242fae94adcba45 Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sun, 9 Apr 2023 10:11:00 -0400 Subject: [PATCH 62/77] Added KnockoutJS NPM package. Re-formatted and re-organized settings model. Working on Knockout model integration into existing authentication settings editor view. --- package.json | 1 + powerdnsadmin/assets.py | 2 + powerdnsadmin/models/setting.py | 95 +++-- powerdnsadmin/routes/admin.py | 50 ++- .../js/app-authentication-settings-editor.js | 273 +++++++++++++ .../admin_setting_authentication.html | 381 ++++++++---------- yarn.lock | 7 +- 7 files changed, 541 insertions(+), 268 deletions(-) create mode 100644 powerdnsadmin/static/custom/js/app-authentication-settings-editor.js diff --git a/package.json b/package.json index 42f866d..3aaac1d 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "jquery-ui-dist": "^1.13.2", "jquery.quicksearch": "^2.4.0", "jtimeout": "^3.2.0", + "knockout": "^3.5.1", "multiselect": "^0.9.12" }, "resolutions": { diff --git a/powerdnsadmin/assets.py b/powerdnsadmin/assets.py index 5e17d7f..8f9192f 100644 --- a/powerdnsadmin/assets.py +++ b/powerdnsadmin/assets.py @@ -20,6 +20,7 @@ js_login = Bundle( 'node_modules/jquery/dist/jquery.js', 'node_modules/bootstrap/dist/js/bootstrap.js', 'node_modules/icheck/icheck.js', + 'node_modules/knockout/build/output/knockout-latest.js', 'custom/js/custom.js', filters=(ConcatFilter, 'rjsmin'), output='generated/login.js') @@ -55,6 +56,7 @@ js_main = Bundle( 'node_modules/datatables.net-plugins/sorting/natural.js', 'node_modules/jtimeout/src/jTimeout.js', 'node_modules/jquery.quicksearch/src/jquery.quicksearch.js', + 'node_modules/knockout/build/output/knockout-latest.js', 'custom/js/custom.js', 'node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js', filters=(ConcatFilter, 'rjsmin'), diff --git a/powerdnsadmin/models/setting.py b/powerdnsadmin/models/setting.py index dedaaab..1ef3166 100644 --- a/powerdnsadmin/models/setting.py +++ b/powerdnsadmin/models/setting.py @@ -15,6 +15,7 @@ class Setting(db.Model): value = db.Column(db.Text()) defaults = { + # General Settings 'maintenance': False, 'fullscreen_layout': True, 'record_helper': True, @@ -42,56 +43,79 @@ class Setting(db.Model): 'pdns_api_timeout': 30, 'pdns_version': '4.1.1', 'verify_ssl_connections': True, + 'verify_user_email': False, + 'enforce_api_ttl': False, + 'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours', + 'otp_field_enabled': True, + 'custom_css': '', + 'otp_force': False, + 'max_history_records': 1000, + 'deny_domain_override': False, + 'account_name_extra_chars': False, + 'gravatar_enabled': False, + + # Local Authentication Settings 'local_db_enabled': True, 'signup_enabled': True, - 'autoprovisioning': False, - 'urn_value': '', - 'autoprovisioning_attribute': '', - 'purge': False, - 'verify_user_email': False, + 'pwd_enforce_characters': False, + 'pwd_min_len': 10, + 'pwd_min_lowercase': 3, + 'pwd_min_uppercase': 2, + 'pwd_min_digits': 2, + 'pwd_min_special': 1, + 'pwd_enforce_complexity': False, + 'pwd_min_complexity': 11, + + # LDAP Authentication Settings 'ldap_enabled': False, 'ldap_type': 'ldap', 'ldap_uri': '', 'ldap_base_dn': '', 'ldap_admin_username': '', 'ldap_admin_password': '', + 'ldap_domain': '', 'ldap_filter_basic': '', - 'ldap_filter_group': '', 'ldap_filter_username': '', + 'ldap_filter_group': '', 'ldap_filter_groupname': '', 'ldap_sg_enabled': False, 'ldap_admin_group': '', 'ldap_operator_group': '', 'ldap_user_group': '', - 'ldap_domain': '', + 'autoprovisioning': False, + 'autoprovisioning_attribute': '', + 'urn_value': '', + 'purge': False, + + # Google OAuth2 Settings + 'google_oauth_enabled': False, + 'google_oauth_client_id': '', + 'google_oauth_client_secret': '', + 'google_oauth_scope': 'openid email profile', + 'google_base_url': 'https://www.googleapis.com/oauth2/v3/', + 'google_oauth_metadata_url': 'https://accounts.google.com/.well-known/openid-configuration', + 'google_token_url': 'https://oauth2.googleapis.com/token', + 'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', + + # GitHub OAuth2 Settings 'github_oauth_enabled': False, 'github_oauth_key': '', 'github_oauth_secret': '', 'github_oauth_scope': 'email', 'github_oauth_api_url': 'https://api.github.com/user', - 'github_oauth_token_url': - 'https://github.com/login/oauth/access_token', - 'github_oauth_authorize_url': - 'https://github.com/login/oauth/authorize', 'github_oauth_metadata_url': '', - 'google_oauth_enabled': False, - 'google_oauth_client_id': '', - 'google_oauth_client_secret': '', - 'google_token_url': 'https://oauth2.googleapis.com/token', - 'google_oauth_scope': 'openid email profile', - 'google_authorize_url': 'https://accounts.google.com/o/oauth2/v2/auth', - 'google_oauth_metadata_url': '', - 'google_base_url': 'https://www.googleapis.com/oauth2/v3/', + 'github_oauth_token_url': 'https://github.com/login/oauth/access_token', + 'github_oauth_authorize_url': 'https://github.com/login/oauth/authorize', + + # Azure OAuth2 Settings 'azure_oauth_enabled': False, 'azure_oauth_key': '', 'azure_oauth_secret': '', 'azure_oauth_scope': 'User.Read openid email profile', 'azure_oauth_api_url': 'https://graph.microsoft.com/v1.0/', - 'azure_oauth_token_url': - 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/token', - 'azure_oauth_authorize_url': - 'https://login.microsoftonline.com/[tenancy]/oauth2/v2.0/authorize', 'azure_oauth_metadata_url': '', + 'azure_oauth_token_url': '', + 'azure_oauth_authorize_url': '', 'azure_sg_enabled': False, 'azure_admin_group': '', 'azure_operator_group': '', @@ -101,22 +125,25 @@ class Setting(db.Model): 'azure_group_accounts_name_re': '', 'azure_group_accounts_description': 'description', 'azure_group_accounts_description_re': '', + + # OIDC OAuth2 Settings 'oidc_oauth_enabled': False, 'oidc_oauth_key': '', 'oidc_oauth_secret': '', 'oidc_oauth_scope': 'email', 'oidc_oauth_api_url': '', + 'oidc_oauth_metadata_url': '', 'oidc_oauth_token_url': '', 'oidc_oauth_authorize_url': '', - 'oidc_oauth_metadata_url': '', 'oidc_oauth_logout_url': '', 'oidc_oauth_username': 'preferred_username', + 'oidc_oauth_email': 'email', 'oidc_oauth_firstname': 'given_name', 'oidc_oauth_last_name': 'family_name', - 'oidc_oauth_email': 'email', 'oidc_oauth_account_name_property': '', 'oidc_oauth_account_description_property': '', - 'enforce_api_ttl': False, + + # Zone Record Settings 'forward_records_allow_edit': { 'A': True, 'AAAA': True, @@ -193,22 +220,6 @@ class Setting(db.Model): 'TXT': True, 'URI': False }, - 'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours', - 'otp_field_enabled': True, - 'custom_css': '', - 'otp_force': False, - 'max_history_records': 1000, - 'deny_domain_override': False, - 'account_name_extra_chars': False, - 'gravatar_enabled': False, - 'pwd_enforce_characters': False, - 'pwd_min_len': 10, - 'pwd_min_lowercase': 3, - 'pwd_min_uppercase': 2, - 'pwd_min_digits': 2, - 'pwd_min_special': 1, - 'pwd_enforce_complexity': False, - 'pwd_min_complexity': 11 } def __init__(self, id=None, name=None, value=None): diff --git a/powerdnsadmin/routes/admin.py b/powerdnsadmin/routes/admin.py index eedabdc..7ec669d 100644 --- a/powerdnsadmin/routes/admin.py +++ b/powerdnsadmin/routes/admin.py @@ -72,8 +72,8 @@ def get_record_changes(del_rrset, add_rrset): """For the given record, return the state dict.""" return { "disabled": record['disabled'], - "content": record['content'], - "comment": record.get('comment', ''), + "content": record['content'], + "comment": record.get('comment', ''), } add_records = get_records(add_rrset) @@ -149,8 +149,8 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n # Sort them by the record name if change_num in out_changes: out_changes[change_num].sort(key=lambda change: - change.del_rrset['name'] if change.del_rrset else change.add_rrset['name'] - ) + change.del_rrset['name'] if change.del_rrset else change.add_rrset['name'] + ) # only used for changelog per record if record_name != None and record_type != None: # then get only the records with the specific (record_name, record_type) tuple @@ -897,7 +897,8 @@ class DetailedHistory(): description=DetailedHistory.get_key_val(detail_dict, "description")) - elif any(msg in history.msg for msg in ['Change zone','Change domain']) and 'access control' in history.msg: # added or removed a user from a zone + elif any(msg in history.msg for msg in ['Change zone', + 'Change domain']) and 'access control' in history.msg: # added or removed a user from a zone users_with_access = DetailedHistory.get_key_val(detail_dict, "user_has_access") self.detailed_msg = render_template_string(""" @@ -942,7 +943,7 @@ class DetailedHistory(): linked_domains=DetailedHistory.get_key_val(detail_dict, "domains")) - elif any(msg in history.msg for msg in ['Update type for zone','Update type for domain']): + elif any(msg in history.msg for msg in ['Update type for zone', 'Update type for domain']): self.detailed_msg = render_template_string("""
    @@ -977,7 +978,8 @@ class DetailedHistory(): 'status'), history_msg=DetailedHistory.get_key_val(detail_dict, 'msg')) - elif any(msg in history.msg for msg in ['Update zone','Update domain']) and 'associate account' in history.msg: # When an account gets associated or dissociate with zones + elif any(msg in history.msg for msg in ['Update zone', + 'Update domain']) and 'associate account' in history.msg: # When an account gets associated or dissociate with zones self.detailed_msg = render_template_string('''
    Zone: {{ domain }}
    @@ -1231,8 +1233,10 @@ def history_table(): # ajax call data .filter( db.and_( db.or_( - History.msg.like("%domain " + domain_name) if domain_name != "*" else History.msg.like("%domain%"), - History.msg.like("%zone " + domain_name) if domain_name != "*" else History.msg.like("%zone%"), + History.msg.like("%domain " + domain_name) if domain_name != "*" else History.msg.like( + "%domain%"), + History.msg.like("%zone " + domain_name) if domain_name != "*" else History.msg.like( + "%zone%"), History.msg.like( "%domain " + domain_name + " access control") if domain_name != "*" else History.msg.like( "%domain%access control"), @@ -1540,7 +1544,8 @@ def has_an_auth_method(local_db_enabled=None, oidc_oauth_enabled = Setting().get('oidc_oauth_enabled') if azure_oauth_enabled is None: azure_oauth_enabled = Setting().get('azure_oauth_enabled') - return local_db_enabled or ldap_enabled or google_oauth_enabled or github_oauth_enabled or oidc_oauth_enabled or azure_oauth_enabled + return local_db_enabled or ldap_enabled or google_oauth_enabled or github_oauth_enabled or oidc_oauth_enabled \ + or azure_oauth_enabled @admin_bp.route('/setting/authentication', methods=['GET', 'POST']) @@ -1562,17 +1567,20 @@ def setting_authentication(): pwd_enforce_characters = True if request.form.get('pwd_enforce_characters') else False pwd_min_len = safe_cast(request.form.get('pwd_min_len', Setting().defaults["pwd_min_len"]), int, Setting().defaults["pwd_min_len"]) - pwd_min_lowercase = safe_cast(request.form.get('pwd_min_lowercase', Setting().defaults["pwd_min_lowercase"]), int, - Setting().defaults["pwd_min_lowercase"]) - pwd_min_uppercase = safe_cast(request.form.get('pwd_min_uppercase', Setting().defaults["pwd_min_uppercase"]), int, - Setting().defaults["pwd_min_uppercase"]) + pwd_min_lowercase = safe_cast( + request.form.get('pwd_min_lowercase', Setting().defaults["pwd_min_lowercase"]), int, + Setting().defaults["pwd_min_lowercase"]) + pwd_min_uppercase = safe_cast( + request.form.get('pwd_min_uppercase', Setting().defaults["pwd_min_uppercase"]), int, + Setting().defaults["pwd_min_uppercase"]) pwd_min_digits = safe_cast(request.form.get('pwd_min_digits', Setting().defaults["pwd_min_digits"]), int, Setting().defaults["pwd_min_digits"]) pwd_min_special = safe_cast(request.form.get('pwd_min_special', Setting().defaults["pwd_min_special"]), int, Setting().defaults["pwd_min_special"]) pwd_enforce_complexity = True if request.form.get('pwd_enforce_complexity') else False - pwd_min_complexity = safe_cast(request.form.get('pwd_min_complexity', Setting().defaults["pwd_min_complexity"]), int, + pwd_min_complexity = safe_cast(request.form.get('pwd_min_complexity', + Setting().defaults["pwd_min_complexity"]), int, Setting().defaults["pwd_min_complexity"]) if not has_an_auth_method(local_db_enabled=local_db_enabled): @@ -1585,14 +1593,12 @@ def setting_authentication(): else: Setting().set('local_db_enabled', local_db_enabled) Setting().set('signup_enabled', signup_enabled) - Setting().set('pwd_enforce_characters', pwd_enforce_characters) Setting().set('pwd_min_len', pwd_min_len) Setting().set('pwd_min_lowercase', pwd_min_lowercase) Setting().set('pwd_min_uppercase', pwd_min_uppercase) Setting().set('pwd_min_digits', pwd_min_digits) Setting().set('pwd_min_special', pwd_min_special) - Setting().set('pwd_enforce_complexity', pwd_enforce_complexity) Setting().set('pwd_min_complexity', pwd_min_complexity) @@ -2097,16 +2103,16 @@ def global_search(): results = server.global_search(object_type='all', query=query) # Filter results to domains to which the user has access permission - if current_user.role.name not in [ 'Administrator', 'Operator' ]: + if current_user.role.name not in ['Administrator', 'Operator']: allowed_domains = db.session.query(Domain) \ .outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \ .outerjoin(Account, Domain.account_id == Account.id) \ .outerjoin(AccountUser, Account.id == AccountUser.account_id) \ .filter( - db.or_( - DomainUser.user_id == current_user.id, - AccountUser.user_id == current_user.id - )) \ + db.or_( + DomainUser.user_id == current_user.id, + AccountUser.user_id == current_user.id + )) \ .with_entities(Domain.name) \ .all() allowed_domains = [value for value, in allowed_domains] diff --git a/powerdnsadmin/static/custom/js/app-authentication-settings-editor.js b/powerdnsadmin/static/custom/js/app-authentication-settings-editor.js new file mode 100644 index 0000000..104b3e9 --- /dev/null +++ b/powerdnsadmin/static/custom/js/app-authentication-settings-editor.js @@ -0,0 +1,273 @@ +let model; + +let AuthenticationSettingsModel = function (user_data, csrf_token, selector) { + let self = this; + + let defaults = { + tab_active: '', + tab_default: 'local', + + // Local Authentication Settings + local_db_enabled: true, + signup_enabled: true, + pwd_enforce_characters: false, + pwd_min_len: 10, + pwd_min_lowercase: 3, + pwd_min_uppercase: 2, + pwd_min_digits: 2, + pwd_min_special: 1, + pwd_enforce_complexity: false, + pwd_min_complexity: 11, + + // LDAP Authentication Settings + ldap_enabled: false, + ldap_type: 'ldap', + ldap_uri: '', + ldap_base_dn: '', + ldap_admin_username: '', + ldap_admin_password: '', + ldap_domain: '', + ldap_filter_basic: '', + ldap_filter_username: '', + ldap_filter_group: '', + ldap_filter_groupname: '', + ldap_sg_enabled: false, + ldap_admin_group: '', + ldap_operator_group: '', + ldap_user_group: '', + autoprovisioning: false, + autoprovisioning_attribute: '', + urn_value: '', + purge: false, + + // Google OAuth2 Settings + google_oauth_enabled: false, + google_oauth_client_id: '', + google_oauth_client_secret: '', + google_oauth_scope: '', + google_base_url: '', + google_oauth_auto_configure: false, + google_oauth_metadata_url: '', + google_token_url: '', + google_authorize_url: '', + + // GitHub OAuth2 Settings + github_oauth_enabled: false, + github_oauth_key: '', + github_oauth_secret: '', + github_oauth_scope: '', + github_oauth_api_url: '', + github_oauth_auto_configure: false, + github_oauth_metadata_url: '', + github_oauth_token_url: '', + github_oauth_authorize_url: '', + + // Azure AD OAuth2 Settings + azure_oauth_enabled: false, + azure_oauth_key: '', + azure_oauth_secret: '', + azure_oauth_scope: '', + azure_oauth_api_url: '', + azure_oauth_auto_configure: false, + azure_oauth_metadata_url: '', + azure_oauth_token_url: '', + azure_oauth_authorize_url: '', + azure_sg_enabled: false, + azure_admin_group: '', + azure_operator_group: '', + azure_user_group: '', + azure_group_accounts_enabled: false, + azure_group_accounts_name: '', + azure_group_accounts_name_re: '', + azure_group_accounts_description: '', + azure_group_accounts_description_re: '', + + // OIDC OAuth2 Settings + oidc_oauth_enabled: false, + oidc_oauth_key: '', + oidc_oauth_secret: '', + oidc_oauth_scope: '', + oidc_oauth_api_url: '', + oidc_oauth_auto_configure: false, + oidc_oauth_metadata_url: '', + oidc_oauth_token_url: '', + oidc_oauth_authorize_url: '', + oidc_oauth_logout_url: '', + oidc_oauth_username: '', + oidc_oauth_email: '', + oidc_oauth_firstname: '', + oidc_oauth_last_name: '', + oidc_oauth_account_name_property: '', + oidc_oauth_account_description_property: '', + } + + self.data = {}; + + self.setupObservables = function () { + self.tab_active = ko.observable(self.data.tab_active); + self.tab_default = ko.observable(self.data.tab_default); + + // Local Authentication Settings + self.local_db_enabled = ko.observable(self.data.local_db_enabled); + self.signup_enabled = ko.observable(self.data.signup_enabled); + self.pwd_enforce_characters = ko.observable(self.data.pwd_enforce_characters); + self.pwd_min_len = ko.observable(self.data.pwd_min_len); + self.pwd_min_lowercase = ko.observable(self.data.pwd_min_lowercase); + self.pwd_min_uppercase = ko.observable(self.data.pwd_min_uppercase); + self.pwd_min_digits = ko.observable(self.data.pwd_min_digits); + self.pwd_min_special = ko.observable(self.data.pwd_min_special); + self.pwd_enforce_complexity = ko.observable(self.data.pwd_enforce_complexity); + self.pwd_min_complexity = ko.observable(self.data.pwd_min_complexity); + + // LDAP Authentication Settings + self.ldap_enabled = ko.observable(self.data.ldap_enabled); + self.ldap_type = ko.observable(self.data.ldap_type); + self.ldap_uri = ko.observable(self.data.ldap_uri); + self.ldap_base_dn = ko.observable(self.data.ldap_base_dn); + self.ldap_admin_username = ko.observable(self.data.ldap_admin_username); + self.ldap_admin_password = ko.observable(self.data.ldap_admin_password); + self.ldap_domain = ko.observable(self.data.ldap_domain); + self.ldap_filter_basic = ko.observable(self.data.ldap_filter_basic); + self.ldap_filter_username = ko.observable(self.data.ldap_filter_username); + self.ldap_filter_group = ko.observable(self.data.ldap_filter_group); + self.ldap_filter_groupname = ko.observable(self.data.ldap_filter_groupname); + self.ldap_sg_enabled = ko.observable(self.data.ldap_sg_enabled); + self.ldap_admin_group = ko.observable(self.data.ldap_admin_group); + self.ldap_operator_group = ko.observable(self.data.ldap_operator_group); + self.ldap_user_group = ko.observable(self.data.ldap_user_group); + self.autoprovisioning = ko.observable(self.data.autoprovisioning); + self.autoprovisioning_attribute = ko.observable(self.data.autoprovisioning_attribute); + self.urn_value = ko.observable(self.data.urn_value); + self.purge = ko.observable(self.data.purge); + + // Google OAuth2 Settings + self.google_oauth_enabled = ko.observable(self.data.google_oauth_enabled); + self.google_oauth_client_id = ko.observable(self.data.google_oauth_client_id); + self.google_oauth_client_secret = ko.observable(self.data.google_oauth_client_secret); + self.google_oauth_scope = ko.observable(self.data.google_oauth_scope); + self.google_base_url = ko.observable(self.data.google_base_url); + self.google_oauth_auto_configure = ko.observable(self.data.google_oauth_auto_configure); + self.google_oauth_metadata_url = ko.observable(self.data.google_oauth_metadata_url); + self.google_token_url = ko.observable(self.data.google_token_url); + self.google_authorize_url = ko.observable(self.data.google_authorize_url); + + // GitHub OAuth2 Settings + self.github_oauth_enabled = ko.observable(self.data.github_oauth_enabled); + self.github_oauth_key = ko.observable(self.data.github_oauth_key); + self.github_oauth_secret = ko.observable(self.data.github_oauth_secret); + self.github_oauth_scope = ko.observable(self.data.github_oauth_scope); + self.github_oauth_api_url = ko.observable(self.data.github_oauth_api_url); + self.github_oauth_auto_configure = ko.observable(self.data.github_oauth_auto_configure); + self.github_oauth_metadata_url = ko.observable(self.data.github_oauth_metadata_url); + self.github_oauth_token_url = ko.observable(self.data.github_oauth_token_url); + self.github_oauth_authorize_url = ko.observable(self.data.github_oauth_authorize_url); + + // Azure AD OAuth2 Settings + self.azure_oauth_enabled = ko.observable(self.data.azure_oauth_enabled); + self.azure_oauth_key = ko.observable(self.data.azure_oauth_key); + self.azure_oauth_secret = ko.observable(self.data.azure_oauth_secret); + self.azure_oauth_scope = ko.observable(self.data.azure_oauth_scope); + self.azure_oauth_api_url = ko.observable(self.data.azure_oauth_api_url); + self.azure_oauth_auto_configure = ko.observable(self.data.azure_oauth_auto_configure); + self.azure_oauth_metadata_url = ko.observable(self.data.azure_oauth_metadata_url); + self.azure_oauth_token_url = ko.observable(self.data.azure_oauth_token_url); + self.azure_oauth_authorize_url = ko.observable(self.data.azure_oauth_authorize_url); + self.azure_sg_enabled = ko.observable(self.data.azure_sg_enabled); + self.azure_admin_group = ko.observable(self.data.azure_admin_group); + self.azure_operator_group = ko.observable(self.data.azure_operator_group); + self.azure_user_group = ko.observable(self.data.azure_user_group); + self.azure_group_accounts_enabled = ko.observable(self.data.azure_group_accounts_enabled); + self.azure_group_accounts_name = ko.observable(self.data.azure_group_accounts_name); + self.azure_group_accounts_name_re = ko.observable(self.data.azure_group_accounts_name_re); + self.azure_group_accounts_description = ko.observable(self.data.azure_group_accounts_description); + self.azure_group_accounts_description_re = ko.observable(self.data.azure_group_accounts_description_re); + + // OIDC OAuth2 Settings + self.oidc_oauth_enabled = ko.observable(self.data.oidc_oauth_enabled); + self.oidc_oauth_key = ko.observable(self.data.oidc_oauth_key); + self.oidc_oauth_secret = ko.observable(self.data.oidc_oauth_secret); + self.oidc_oauth_scope = ko.observable(self.data.oidc_oauth_scope); + self.oidc_oauth_api_url = ko.observable(self.data.oidc_oauth_api_url); + self.oidc_oauth_auto_configure = ko.observable(self.data.oidc_oauth_auto_configure); + self.oidc_oauth_metadata_url = ko.observable(self.data.oidc_oauth_metadata_url); + self.oidc_oauth_token_url = ko.observable(self.data.oidc_oauth_token_url); + self.oidc_oauth_authorize_url = ko.observable(self.data.oidc_oauth_authorize_url); + self.oidc_oauth_logout_url = ko.observable(self.data.oidc_oauth_logout_url); + self.oidc_oauth_username = ko.observable(self.data.oidc_oauth_username); + self.oidc_oauth_email = ko.observable(self.data.oidc_oauth_email); + self.oidc_oauth_firstname = ko.observable(self.data.oidc_oauth_firstname); + self.oidc_oauth_last_name = ko.observable(self.data.oidc_oauth_last_name); + self.oidc_oauth_account_name_property = ko.observable(self.data.oidc_oauth_account_name_property); + self.oidc_oauth_account_description_property = ko.observable(self.data.oidc_oauth_account_description_property); + } + + self.updateWithDefaults = function (instance) { + self.data = $.extend(defaults, instance) + } + + self.activateTab = function (tab) { + $('[role="tablist"] a.nav-link').blur(); + self.tab_active(tab); + window.location.hash = tab; + } + + self.activateDefaultTab = function () { + self.activateTab(self.tab_default()); + } + + self.initTabs = function() { + if (self.hasHash()) { + self.activateTab(self.getHash()); + } else { + self.activateDefaultTab(); + } + } + + self.getHash = function () { + return window.location.hash.substring(1); + } + + self.hasHash = function () { + return window.location.hash.length > 1; + } + + self.setupListeners = function () { + if ('onhashchange' in window) { + $(window).bind('hashchange', self.onHashChange); + } + } + + self.destroyListeners = function () { + if ('onhashchange' in window) { + $(window).unbind('hashchange', self.onHashChange); + } + } + + self.onTabClick = function (model, event) { + self.activateTab($(event.target).data('tab')); + return false; + } + + self.onHashChange = function (event) { + let hash = window.location.hash.trim(); + if (hash.length > 1) { + self.activateTab(hash.substring(1)); + } else { + self.activateDefaultTab(); + } + } + + self.updateWithDefaults(user_data); + self.setupObservables(); + + ko.applyBindings(self); + + self.initTabs(); + self.setupListeners(); +} + +$(function () { + // TODO: Load the data from the server and pass it to the model instantiation + loaded_data = {}; + model = new AuthenticationSettingsModel(loaded_data, CSRF_TOKEN, '#settings-editor'); +}) \ No newline at end of file diff --git a/powerdnsadmin/templates/admin_setting_authentication.html b/powerdnsadmin/templates/admin_setting_authentication.html index ca8baa5..73d305d 100644 --- a/powerdnsadmin/templates/admin_setting_authentication.html +++ b/powerdnsadmin/templates/admin_setting_authentication.html @@ -25,7 +25,7 @@
    -
    +

    Settings Editor

    @@ -43,31 +43,43 @@
    Associate: {{ history_assoc_account }}