From ab4495dc466ccf177d29fd68edcdf7fb07abbb3b Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sat, 8 Apr 2023 17:05:27 -0400 Subject: [PATCH 01/18] Completed the implementation of the `SERVER_EXTERNAL_SSL` environment setting into the app config files. Completed the implementation of the aforementioned environment setting into the OAuth workflows. Documented the aforementioned setting in the Environment-variables.md wiki document. --- configs/docker_config.py | 18 +- .../configuration/Environment-variables.md | 117 +++---- powerdnsadmin/default_config.py | 1 + powerdnsadmin/routes/index.py | 313 ++++++++++-------- powerdnsadmin/services/azure.py | 10 +- powerdnsadmin/services/github.py | 11 +- powerdnsadmin/services/google.py | 12 +- powerdnsadmin/services/oidc.py | 13 +- 8 files changed, 270 insertions(+), 225 deletions(-) diff --git a/configs/docker_config.py b/configs/docker_config.py index f934548..652777a 100644 --- a/configs/docker_config.py +++ b/configs/docker_config.py @@ -1,3 +1,8 @@ +# import everything from environment variables +import os +import sys +import json + # Defaults for Docker image BIND_ADDRESS = '0.0.0.0' PORT = 80 @@ -23,6 +28,7 @@ legal_envvars = ( 'OIDC_OAUTH_EMAIL', 'BIND_ADDRESS', 'PORT', + 'SERVER_EXTERNAL_SSL', 'LOG_LEVEL', 'SALT', 'SQLALCHEMY_TRACK_MODIFICATIONS', @@ -97,21 +103,18 @@ legal_envvars_bool = ( 'SESSION_COOKIE_SECURE', 'CSRF_COOKIE_SECURE', 'CAPTCHA_ENABLE', + 'SERVER_EXTERNAL_SSL', ) legal_envvars_dict = ( 'SQLALCHEMY_ENGINE_OPTIONS', ) -# import everything from environment variables -import os -import sys -import json - def str2bool(v): return v.lower() in ("true", "yes", "1") -def dictfromstr(v,ret): + +def dictfromstr(v, ret): try: return json.loads(ret) except Exception as e: @@ -119,10 +122,11 @@ def dictfromstr(v,ret): print(e) raise ValueError + for v in legal_envvars: ret = None - # _FILE suffix will allow to read value from file, usefull for Docker's + # _FILE suffix will allow to read value from file, useful for Docker containers. # secrets feature if v + '_FILE' in os.environ: if v in os.environ: diff --git a/docs/wiki/configuration/Environment-variables.md b/docs/wiki/configuration/Environment-variables.md index d49f60f..7f42727 100644 --- a/docs/wiki/configuration/Environment-variables.md +++ b/docs/wiki/configuration/Environment-variables.md @@ -1,64 +1,65 @@ # Supported environment variables -| Variable | Description | Required | Default value | -| ---------| ----------- | -------- | ------------- | -| BIND_ADDRESS | -| CSRF_COOKIE_SECURE | -| SESSION_TYPE | null|filesystem|sqlalchemy | | filesystem | -| LDAP_ENABLED | -| LOCAL_DB_ENABLED | -| LOG_LEVEL | -| MAIL_DEBUG | -| MAIL_DEFAULT_SENDER | -| MAIL_PASSWORD | -| MAIL_PORT | -| MAIL_SERVER | -| MAIL_USERNAME | -| MAIL_USE_SSL | -| MAIL_USE_TLS | -| OFFLINE_MODE | -| OIDC_OAUTH_API_URL | | | | -| OIDC_OAUTH_AUTHORIZE_URL | -| OIDC_OAUTH_TOKEN_URL | | | | -| OIDC_OAUTH_METADATA_URL | | | | -| PORT | -| REMOTE_USER_COOKIES | -| REMOTE_USER_LOGOUT_URL | -| SALT | -| SAML_ASSERTION_ENCRYPTED | -| SAML_ATTRIBUTE_ACCOUNT | -| SAML_ATTRIBUTE_ADMIN | -| SAML_ATTRIBUTE_EMAIL | -| SAML_ATTRIBUTE_GIVENNAME | -| SAML_ATTRIBUTE_GROUP | -| SAML_ATTRIBUTE_NAME | -| SAML_ATTRIBUTE_SURNAME | -| SAML_ATTRIBUTE_USERNAME | -| SAML_CERT | -| SAML_DEBUG | -| SAML_ENABLED | -| SAML_GROUP_ADMIN_NAME | -| SAML_GROUP_TO_ACCOUNT_MAPPING | -| SAML_IDP_SSO_BINDING | -| SAML_IDP_ENTITY_ID | -| SAML_KEY | -| SAML_LOGOUT | -| SAML_LOGOUT_URL | -| SAML_METADATA_CACHE_LIFETIME | -| SAML_METADATA_URL | -| SAML_NAMEID_FORMAT | -| SAML_PATH | -| SAML_SIGN_REQUEST | -| SAML_SP_CONTACT_MAIL | -| SAML_SP_CONTACT_NAME | -| SAML_SP_ENTITY_ID | -| SAML_WANT_MESSAGE_SIGNED | -| SECRET_KEY | Flask secret key [^1] | Y | no default | -| SESSION_COOKIE_SECURE | -| SIGNUP_ENABLED | -| SQLALCHEMY_DATABASE_URI | SQL Alchemy URI to connect to database | N | no default | +| Variable | Description | Required | Default value | +|--------------------------------|--------------------------------------------------------------------------|------------|---------------| +| BIND_ADDRESS | +| CSRF_COOKIE_SECURE | +| SESSION_TYPE | null | filesystem | sqlalchemy | | filesystem | +| LDAP_ENABLED | +| LOCAL_DB_ENABLED | +| LOG_LEVEL | +| MAIL_DEBUG | +| MAIL_DEFAULT_SENDER | +| MAIL_PASSWORD | +| MAIL_PORT | +| MAIL_SERVER | +| MAIL_USERNAME | +| MAIL_USE_SSL | +| MAIL_USE_TLS | +| OFFLINE_MODE | +| OIDC_OAUTH_API_URL | | | | +| OIDC_OAUTH_AUTHORIZE_URL | +| OIDC_OAUTH_TOKEN_URL | | | | +| OIDC_OAUTH_METADATA_URL | | | | +| PORT | +| SERVER_EXTERNAL_SSL | Forceful override of URL schema detection when using the url_for method. | False | None | +| REMOTE_USER_COOKIES | +| REMOTE_USER_LOGOUT_URL | +| SALT | +| SAML_ASSERTION_ENCRYPTED | +| SAML_ATTRIBUTE_ACCOUNT | +| SAML_ATTRIBUTE_ADMIN | +| SAML_ATTRIBUTE_EMAIL | +| SAML_ATTRIBUTE_GIVENNAME | +| SAML_ATTRIBUTE_GROUP | +| SAML_ATTRIBUTE_NAME | +| SAML_ATTRIBUTE_SURNAME | +| SAML_ATTRIBUTE_USERNAME | +| SAML_CERT | +| SAML_DEBUG | +| SAML_ENABLED | +| SAML_GROUP_ADMIN_NAME | +| SAML_GROUP_TO_ACCOUNT_MAPPING | +| SAML_IDP_SSO_BINDING | +| SAML_IDP_ENTITY_ID | +| SAML_KEY | +| SAML_LOGOUT | +| SAML_LOGOUT_URL | +| SAML_METADATA_CACHE_LIFETIME | +| SAML_METADATA_URL | +| SAML_NAMEID_FORMAT | +| SAML_PATH | +| SAML_SIGN_REQUEST | +| SAML_SP_CONTACT_MAIL | +| SAML_SP_CONTACT_NAME | +| SAML_SP_ENTITY_ID | +| SAML_WANT_MESSAGE_SIGNED | +| SECRET_KEY | Flask secret key [^1] | Y | no default | +| SESSION_COOKIE_SECURE | +| SIGNUP_ENABLED | +| SQLALCHEMY_DATABASE_URI | SQL Alchemy URI to connect to database | N | no default | | SQLALCHEMY_TRACK_MODIFICATIONS | -| SQLALCHEMY_ENGINE_OPTIONS | json string. e.g. '{"pool_recycle":600,"echo":1}' [^2] | +| SQLALCHEMY_ENGINE_OPTIONS | json string. e.g. '{"pool_recycle":600,"echo":1}' [^2] | [^1]: Flask secret key (see https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY for how to generate) [^2]: See Flask-SQLAlchemy Documentation for all engine options. diff --git a/powerdnsadmin/default_config.py b/powerdnsadmin/default_config.py index 55d28ef..3ae1b13 100644 --- a/powerdnsadmin/default_config.py +++ b/powerdnsadmin/default_config.py @@ -8,6 +8,7 @@ SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2' BIND_ADDRESS = '0.0.0.0' PORT = 9191 HSTS_ENABLED = False +SERVER_EXTERNAL_SSL = None SESSION_TYPE = 'sqlalchemy' SESSION_COOKIE_SAMESITE = 'Lax' diff --git a/powerdnsadmin/routes/index.py b/powerdnsadmin/routes/index.py index 19fd277..4351a54 100644 --- a/powerdnsadmin/routes/index.py +++ b/powerdnsadmin/routes/index.py @@ -45,6 +45,7 @@ index_bp = Blueprint('index', template_folder='templates', url_prefix='/') + @index_bp.before_app_first_request def register_modules(): global google @@ -68,7 +69,7 @@ def before_request(): # Check site is in maintenance mode maintenance = Setting().get('maintenance') if maintenance and current_user.is_authenticated and current_user.role.name not in [ - 'Administrator', 'Operator' + 'Administrator', 'Operator' ]: return render_template('maintenance.html') @@ -98,7 +99,11 @@ def google_login(): ) abort(400) else: - redirect_uri = url_for('google_authorized', _external=True) + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + redirect_uri = url_for('google_authorized', **params) return google.authorize_redirect(redirect_uri) @@ -110,7 +115,11 @@ def github_login(): ) abort(400) else: - redirect_uri = url_for('github_authorized', _external=True) + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + redirect_uri = url_for('github_authorized', **params) return github.authorize_redirect(redirect_uri) @@ -122,9 +131,11 @@ def azure_login(): ) abort(400) else: - redirect_uri = url_for('azure_authorized', - _external=True, - _scheme='https') + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + redirect_uri = url_for('azure_authorized', **params) return azure.authorize_redirect(redirect_uri) @@ -136,7 +147,11 @@ def oidc_login(): ) abort(400) else: - redirect_uri = url_for('oidc_authorized', _external=True) + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + redirect_uri = url_for('oidc_authorized', **params) return oidc.authorize_redirect(redirect_uri) @@ -198,7 +213,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) + current_app.logger.info('Azure login returned: ' + azure_info) me = json.loads(azure_info) azure_info = azure.post('me/getMemberGroups', @@ -250,30 +265,30 @@ def login(): if Setting().get('azure_sg_enabled'): if Setting().get('azure_admin_group') in mygroups: current_app.logger.info('Setting role for user ' + - azure_username + - ' to Administrator due to group membership') + azure_username + + ' to Administrator due to group membership') user.set_role("Administrator") else: if Setting().get('azure_operator_group') in mygroups: current_app.logger.info('Setting role for user ' + - azure_username + - ' to Operator due to group membership') + azure_username + + ' to Operator due to group membership') user.set_role("Operator") else: if Setting().get('azure_user_group') in mygroups: current_app.logger.info('Setting role for user ' + - azure_username + - ' to User due to group membership') + azure_username + + ' to User due to group membership') user.set_role("User") else: current_app.logger.warning('User ' + - azure_username + - ' has no relevant group memberships') + azure_username + + ' has no relevant group memberships') session.pop('azure_token', None) return render_template('login.html', - saml_enabled=SAML_ENABLED, - error=('User ' + azure_username + - ' is not in any authorised groups.')) + saml_enabled=SAML_ENABLED, + error=('User ' + azure_username + + ' is not in any authorised groups.')) # Handle account/group creation, if enabled if Setting().get('azure_group_accounts_enabled') and mygroups: @@ -394,20 +409,21 @@ def login(): session.pop('oidc_token', None) return redirect(url_for('index.login')) - #This checks if the account_name_property and account_description property were included in settings. - if Setting().get('oidc_oauth_account_name_property') and Setting().get('oidc_oauth_account_description_property'): + # This checks if the account_name_property and account_description property were included in settings. + if Setting().get('oidc_oauth_account_name_property') and Setting().get( + 'oidc_oauth_account_description_property'): - #Gets the name_property and description_property. + # Gets the name_property and description_property. name_prop = Setting().get('oidc_oauth_account_name_property') 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 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] - #Run on all groups the user is in by the index num. + # Run on all groups the user is in by the index num. for i in range(len(accounts_name_prop)): description = '' if i < len(accounts_desc_prop): @@ -417,7 +433,7 @@ def login(): account_to_add.append(account) user_accounts = user.get_accounts() - # Add accounts + # Add accounts for account in account_to_add: if account not in user_accounts: account.add_user(user) @@ -426,7 +442,7 @@ def login(): if Setting().get('delete_sso_accounts'): for account in user_accounts: if account not in account_to_add: - account.remove_user(user) + account.remove_user(user) session['user_id'] = user.id session['authentication_type'] = 'OAuth' @@ -490,34 +506,36 @@ def login(): saml_enabled=SAML_ENABLED, error='Token required') - if Setting().get('autoprovisioning') and auth_method!='LOCAL': - urn_value=Setting().get('urn_value') - Entitlements=user.read_entitlements(Setting().get('autoprovisioning_attribute')) - if len(Entitlements)==0 and Setting().get('purge'): + if Setting().get('autoprovisioning') and auth_method != 'LOCAL': + urn_value = Setting().get('urn_value') + Entitlements = user.read_entitlements(Setting().get('autoprovisioning_attribute')) + if len(Entitlements) == 0 and Setting().get('purge'): user.set_role("User") user.revoke_privilege(True) - elif len(Entitlements)!=0: + elif len(Entitlements) != 0: if checkForPDAEntries(Entitlements, urn_value): user.updateUser(Entitlements) else: - current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix') + current_app.logger.warning( + 'Not a single powerdns-admin record was found, possibly a typo in the prefix') if Setting().get('purge'): user.set_role("User") user.revoke_privilege(True) - current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' ) + current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.') return authenticate_user(user, auth_method, remember_me) + def checkForPDAEntries(Entitlements, urn_value): """ Run through every record located in the ldap attribute given and determine if there are any valid powerdns-admin records """ - urnArguments=[x.lower() for x in urn_value.split(':')] + urnArguments = [x.lower() for x in urn_value.split(':')] for Entitlement in Entitlements: - entArguments=Entitlement.split(':powerdns-admin') - entArguments=[x.lower() for x in entArguments[0].split(':')] - if (entArguments==urnArguments): + entArguments = Entitlement.split(':powerdns-admin') + entArguments = [x.lower() for x in entArguments[0].split(':')] + if (entArguments == urnArguments): return True return False @@ -553,14 +571,15 @@ def signin_history(username, authenticator, success): # Write history History(msg='User {} authentication {}'.format(username, str_success), - detail = json.dumps({ - 'username': username, - 'authenticator': authenticator, - 'ip_address': request_ip, - 'success': 1 if success else 0 - }), + detail=json.dumps({ + 'username': username, + 'authenticator': authenticator, + 'ip_address': request_ip, + 'success': 1 if success else 0 + }), created_by='System').add() + # Get a list of Azure security groups the user is a member of def get_azure_groups(uri): azure_info = azure.get(uri).text @@ -576,30 +595,33 @@ def get_azure_groups(uri): mygroups = [] return mygroups + # Handle user login, write history and, if set, handle showing the register_otp QR code. # if Setting for OTP on first login is enabled, and OTP field is also enabled, # but user isn't using it yet, enable OTP, get QR code and display it, logging the user out. def authenticate_user(user, authenticator, remember=False): login_user(user, remember=remember) signin_history(user.username, authenticator, True) - if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret and session['authentication_type'] not in ['OAuth']: + if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret \ + and session['authentication_type'] not in ['OAuth']: user.update_profile(enable_otp=True) user_id = current_user.id prepare_welcome_user(user_id) return redirect(url_for('index.welcome')) return redirect(url_for('index.login')) + # Prepare user to enter /welcome screen, otherwise they won't have permission to do so def prepare_welcome_user(user_id): logout_user() session['welcome_user_id'] = user_id + @index_bp.route('/logout') def logout(): if current_app.config.get( 'SAML_ENABLED' - ) and 'samlSessionIndex' in session and current_app.config.get( - 'SAML_LOGOUT'): + ) and 'samlSessionIndex' in session and current_app.config.get('SAML_LOGOUT'): req = saml.prepare_flask_request(request) auth = saml.init_saml_auth(req) if current_app.config.get('SAML_LOGOUT_URL'): @@ -651,13 +673,12 @@ def logout(): def password_policy_check(user, password): - def check_policy(chars, user_password, setting): - lenreq = int(Setting().get(setting)) + setting_as_int = 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)) + return (setting_as_int, len(user_password) - len(test_string)) def matches_policy(item, policy_fails): return "*" if item in policy_fails else "" @@ -704,12 +725,14 @@ def password_policy_check(user, password): (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}") + 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}") + 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: @@ -728,7 +751,8 @@ def password_policy_check(user, password): 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.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."} @@ -738,77 +762,78 @@ def password_policy_check(user, password): @index_bp.route('/register', methods=['GET', 'POST']) def register(): - CAPTCHA_ENABLE = current_app.config.get('CAPTCHA_ENABLE') - if Setting().get('signup_enabled'): - if current_user.is_authenticated: - return redirect(url_for('index.index')) - if request.method == 'GET': - return render_template('register.html', captcha_enable=CAPTCHA_ENABLE) - elif request.method == 'POST': - username = request.form.get('username', '').strip() - password = request.form.get('password', '') - firstname = request.form.get('firstname', '').strip() - lastname = request.form.get('lastname', '').strip() - email = request.form.get('email', '').strip() - rpassword = request.form.get('rpassword', '') + CAPTCHA_ENABLE = current_app.config.get('CAPTCHA_ENABLE') + if Setting().get('signup_enabled'): + if current_user.is_authenticated: + return redirect(url_for('index.index')) + if request.method == 'GET': + return render_template('register.html', captcha_enable=CAPTCHA_ENABLE) + elif request.method == 'POST': + username = request.form.get('username', '').strip() + password = request.form.get('password', '') + firstname = request.form.get('firstname', '').strip() + lastname = request.form.get('lastname', '').strip() + email = request.form.get('email', '').strip() + rpassword = request.form.get('rpassword', '') - is_valid_email = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$') + is_valid_email = re.compile(r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$') - error_messages = {} - if not firstname: - error_messages['firstname'] = 'First Name is required' - if not lastname: - error_messages['lastname'] = 'Last Name is required' - if not username: - error_messages['username'] = 'Username is required' - if not password: - error_messages['password'] = 'Password is required' - if not rpassword: - error_messages['rpassword'] = 'Password confirmation is required' - if not email: - error_messages['email'] = 'Email is required' - if not is_valid_email.match(email): - error_messages['email'] = 'Invalid email address' - if password != rpassword: - error_messages['password'] = 'Password confirmation does not match' - error_messages['rpassword'] = 'Password confirmation does not match' + error_messages = {} + if not firstname: + error_messages['firstname'] = 'First Name is required' + if not lastname: + error_messages['lastname'] = 'Last Name is required' + if not username: + error_messages['username'] = 'Username is required' + if not password: + error_messages['password'] = 'Password is required' + if not rpassword: + error_messages['rpassword'] = 'Password confirmation is required' + if not email: + error_messages['email'] = 'Email is required' + if not is_valid_email.match(email): + error_messages['email'] = 'Invalid email address' + if password != rpassword: + error_messages['password'] = 'Password confirmation does not match' + error_messages['rpassword'] = 'Password confirmation does not match' - if not captcha.validate(): - return render_template( - 'register.html', error='Invalid CAPTCHA answer', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE) + if not captcha.validate(): + return render_template( + 'register.html', error='Invalid CAPTCHA answer', error_messages=error_messages, + captcha_enable=CAPTCHA_ENABLE) - if error_messages: - return render_template('register.html', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE) + if error_messages: + return render_template('register.html', error_messages=error_messages, captcha_enable=CAPTCHA_ENABLE) - user = User(username=username, - plain_text_password=password, - firstname=firstname, - lastname=lastname, - email=email - ) + user = User(username=username, + plain_text_password=password, + firstname=firstname, + lastname=lastname, + 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) + (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']: - if Setting().get('verify_user_email'): - send_account_verification(email) - if Setting().get('otp_force') and Setting().get('otp_field_enabled'): - user.update_profile(enable_otp=True) - prepare_welcome_user(user.id) - return redirect(url_for('index.welcome')) - else: - return redirect(url_for('index.login')) + try: + result = user.create_local_user() + if result and result['status']: + if Setting().get('verify_user_email'): + send_account_verification(email) + if Setting().get('otp_force') and Setting().get('otp_field_enabled'): + user.update_profile(enable_otp=True) + prepare_welcome_user(user.id) + return redirect(url_for('index.welcome')) + else: + return redirect(url_for('index.login')) + else: + return render_template('register.html', + error=result['msg'], captcha_enable=CAPTCHA_ENABLE) + except Exception as e: + return render_template('register.html', error=e, captcha_enable=CAPTCHA_ENABLE) else: - return render_template('register.html', - error=result['msg'], captcha_enable=CAPTCHA_ENABLE) - except Exception as e: - return render_template('register.html', error=e, captcha_enable=CAPTCHA_ENABLE) - else: - return render_template('errors/404.html'), 404 + return render_template('errors/404.html'), 404 # Show welcome page on first login if otp_force is enabled @@ -827,12 +852,15 @@ def welcome(): if otp_token and otp_token.isdigit(): good_token = user.verify_totp(otp_token) if not good_token: - return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Invalid token") + return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, + error="Invalid token") else: - return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Token required") + return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, + error="Token required") session.pop('welcome_user_id') return redirect(url_for('index.index')) + @index_bp.route('/confirm/', methods=['GET']) def confirm_email(token): email = confirm_token(token) @@ -919,10 +947,10 @@ def dyndns_update(): .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 - )).all() + db.or_( + DomainUser.user_id == current_user.id, + AccountUser.user_id == current_user.id + )).all() except Exception as e: current_app.logger.error('DynDNS Error: {0}'.format(e)) current_app.logger.debug(traceback.format_exc()) @@ -982,13 +1010,13 @@ def dyndns_update(): if result['status'] == 'ok': history = History( msg='DynDNS update: updated {} successfully'.format(hostname), - detail = json.dumps({ - 'domain': domain.name, - 'record': hostname, - 'type': rtype, - 'old_value': oldip, - 'new_value': str(ip) - }), + detail=json.dumps({ + 'domain': domain.name, + 'record': hostname, + 'type': rtype, + 'old_value': oldip, + 'new_value': str(ip) + }), created_by=current_user.username, domain_id=domain.id) history.add() @@ -999,7 +1027,7 @@ def dyndns_update(): elif r.is_allowed_edit(): ondemand_creation = DomainSetting.query.filter( DomainSetting.domain == domain).filter( - DomainSetting.setting == 'create_via_dyndns').first() + DomainSetting.setting == 'create_via_dyndns').first() if (ondemand_creation is not None) and (strtobool( ondemand_creation.value) == True): @@ -1024,11 +1052,11 @@ def dyndns_update(): msg= 'DynDNS update: created record {0} in zone {1} successfully' .format(hostname, domain.name, str(ip)), - detail = json.dumps({ - 'domain': domain.name, - 'record': hostname, - 'value': str(ip) - }), + detail=json.dumps({ + 'domain': domain.name, + 'record': hostname, + 'value': str(ip) + }), created_by=current_user.username, domain_id=domain.id) history.add() @@ -1088,7 +1116,7 @@ def saml_authorized(): req = saml.prepare_flask_request(request) auth = saml.init_saml_auth(req) auth.process_response() - current_app.logger.debug( auth.get_attributes() ) + current_app.logger.debug(auth.get_attributes()) errors = auth.get_errors() if len(errors) == 0: session['samlUserdata'] = auth.get_attributes() @@ -1097,7 +1125,7 @@ def saml_authorized(): self_url = OneLogin_Saml2_Utils.get_self_url(req) self_url = self_url + req['script_name'] if 'RelayState' in request.form and self_url != request.form[ - 'RelayState']: + 'RelayState']: return redirect(auth.redirect_to(request.form['RelayState'])) if current_app.config.get('SAML_ATTRIBUTE_USERNAME', False): username = session['samlUserdata'][ @@ -1129,7 +1157,7 @@ def saml_authorized(): admin_group_name = current_app.config.get('SAML_GROUP_ADMIN_NAME', None) operator_group_name = current_app.config.get('SAML_GROUP_OPERATOR_NAME', - None) + None) group_to_account_mapping = create_group_to_account_mapping() if email_attribute_name in session['samlUserdata']: @@ -1170,13 +1198,13 @@ def saml_authorized(): account.add_user(user) history = History(msg='Adding {0} to account {1}'.format( user.username, account.name), - created_by='SAML Assertion') + created_by='SAML Assertion') history.add() for account in user_accounts - saml_accounts: account.remove_user(user) history = History(msg='Removing {0} from account {1}'.format( user.username, account.name), - created_by='SAML Assertion') + created_by='SAML Assertion') history.add() if admin_attribute_name and 'true' in session['samlUserdata'].get( admin_attribute_name, []): @@ -1190,7 +1218,7 @@ def saml_authorized(): user.role_id = Role.query.filter_by(name='User').first().id history = History(msg='Demoting {0} to user'.format( user.username), - created_by='SAML Assertion') + created_by='SAML Assertion') history.add() user.plain_text_password = None user.update_profile() @@ -1234,15 +1262,16 @@ def uplift_to_admin(user): user.role_id = Role.query.filter_by(name='Administrator').first().id history = History(msg='Promoting {0} to administrator'.format( user.username), - created_by='SAML Assertion') + created_by='SAML Assertion') history.add() + def uplift_to_operator(user): if user.role.name != 'Operator': user.role_id = Role.query.filter_by(name='Operator').first().id history = History(msg='Promoting {0} to operator'.format( user.username), - created_by='SAML Assertion') + created_by='SAML Assertion') history.add() diff --git a/powerdnsadmin/services/azure.py b/powerdnsadmin/services/azure.py index 65f3bf3..2976c98 100644 --- a/powerdnsadmin/services/azure.py +++ b/powerdnsadmin/services/azure.py @@ -38,14 +38,16 @@ def azure_oauth(): @current_app.route('/azure/authorized') def azure_authorized(): - session['azure_oauthredir'] = url_for('.azure_authorized', - _external=True, - _scheme='https') + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + session['azure_oauthredir'] = url_for('.azure_authorized', **params) token = azure.authorize_access_token() if token is None: return 'Access denied: reason=%s error=%s' % ( request.args['error'], request.args['error_description']) session['azure_token'] = (token) - return redirect(url_for('index.login', _external=True, _scheme='https')) + return redirect(url_for('index.login', **params)) return azure diff --git a/powerdnsadmin/services/github.py b/powerdnsadmin/services/github.py index ff4a20f..73671fb 100644 --- a/powerdnsadmin/services/github.py +++ b/powerdnsadmin/services/github.py @@ -40,13 +40,16 @@ def github_oauth(): @current_app.route('/github/authorized') def github_authorized(): - session['github_oauthredir'] = url_for('.github_authorized', - _external=True) + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + session['github_oauthredir'] = url_for('.github_authorized', **params) token = github.authorize_access_token() if token is None: return 'Access denied: reason=%s error=%s' % ( request.args['error'], request.args['error_description']) - session['github_token'] = (token) - return redirect(url_for('index.login')) + session['github_token'] = token + return redirect(url_for('index.login', **params)) return github diff --git a/powerdnsadmin/services/google.py b/powerdnsadmin/services/google.py index 5604819..88a0d12 100644 --- a/powerdnsadmin/services/google.py +++ b/powerdnsadmin/services/google.py @@ -39,16 +39,18 @@ def google_oauth(): @current_app.route('/google/authorized') def google_authorized(): - session['google_oauthredir'] = url_for( - '.google_authorized', _external=True) + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + session['google_oauthredir'] = url_for('.google_authorized', **params) token = google.authorize_access_token() if token is None: return 'Access denied: reason=%s error=%s' % ( request.args['error_reason'], request.args['error_description'] ) - session['google_token'] = (token) - return redirect(url_for('index.login')) + session['google_token'] = token + return redirect(url_for('index.login', **params)) return google - diff --git a/powerdnsadmin/services/oidc.py b/powerdnsadmin/services/oidc.py index 7b0cd46..3304b6a 100644 --- a/powerdnsadmin/services/oidc.py +++ b/powerdnsadmin/services/oidc.py @@ -39,13 +39,16 @@ def oidc_oauth(): @current_app.route('/oidc/authorized') def oidc_authorized(): - session['oidc_oauthredir'] = url_for('.oidc_authorized', - _external=True) + use_ssl = current_app.config.get('SERVER_EXTERNAL_SSL') + params = {'_external': True} + if isinstance(use_ssl, bool): + params['_scheme'] = 'https' if use_ssl else 'http' + session['oidc_oauthredir'] = url_for('.oidc_authorized', **params) token = oidc.authorize_access_token() if token is None: return 'Access denied: reason=%s error=%s' % ( request.args['error'], request.args['error_description']) - session['oidc_token'] = (token) - return redirect(url_for('index.login')) + session['oidc_token'] = token + return redirect(url_for('index.login', **params)) - return oidc \ No newline at end of file + return oidc From ee9012fa2418f88429ba51d6c3fa3ea9724b7c60 Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sat, 8 Apr 2023 17:14:55 -0400 Subject: [PATCH 02/18] Completed OAuth change to make the use of the metadata URL setting exclusive to the authorization and token URL settings. If the former is defined, it will be used in preference to the latter. --- powerdnsadmin/services/azure.py | 5 +++-- powerdnsadmin/services/github.py | 5 +++-- powerdnsadmin/services/google.py | 5 +++-- powerdnsadmin/services/oidc.py | 5 +++-- 4 files changed, 12 insertions(+), 8 deletions(-) diff --git a/powerdnsadmin/services/azure.py b/powerdnsadmin/services/azure.py index 2976c98..901cc45 100644 --- a/powerdnsadmin/services/azure.py +++ b/powerdnsadmin/services/azure.py @@ -20,8 +20,6 @@ def azure_oauth(): 'client_secret': Setting().get('azure_oauth_secret'), 'api_base_url': Setting().get('azure_oauth_api_url'), 'request_token_url': None, - 'access_token_url': Setting().get('azure_oauth_token_url'), - 'authorize_url': Setting().get('azure_oauth_authorize_url'), 'client_kwargs': {'scope': Setting().get('azure_oauth_scope')}, 'fetch_token': fetch_azure_token, } @@ -30,6 +28,9 @@ def azure_oauth(): if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0: authlib_params['server_metadata_url'] = server_metadata_url + else: + authlib_params['access_token_url'] = Setting().get('azure_oauth_token_url') + authlib_params['authorize_url'] = Setting().get('azure_oauth_authorize_url') azure = authlib_oauth_client.register( 'azure', diff --git a/powerdnsadmin/services/github.py b/powerdnsadmin/services/github.py index 73671fb..f322e8c 100644 --- a/powerdnsadmin/services/github.py +++ b/powerdnsadmin/services/github.py @@ -21,8 +21,6 @@ def github_oauth(): 'request_token_params': {'scope': Setting().get('github_oauth_scope')}, 'api_base_url': Setting().get('github_oauth_api_url'), 'request_token_url': None, - 'access_token_url': Setting().get('github_oauth_token_url'), - 'authorize_url': Setting().get('github_oauth_authorize_url'), 'client_kwargs': {'scope': Setting().get('github_oauth_scope')}, 'fetch_token': fetch_github_token, 'update_token': update_token @@ -32,6 +30,9 @@ def github_oauth(): if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0: authlib_params['server_metadata_url'] = server_metadata_url + else: + authlib_params['access_token_url'] = Setting().get('github_oauth_token_url') + authlib_params['authorize_url'] = Setting().get('github_oauth_authorize_url') github = authlib_oauth_client.register( 'github', diff --git a/powerdnsadmin/services/google.py b/powerdnsadmin/services/google.py index 88a0d12..011c120 100644 --- a/powerdnsadmin/services/google.py +++ b/powerdnsadmin/services/google.py @@ -20,8 +20,6 @@ def google_oauth(): 'client_secret': Setting().get('google_oauth_client_secret'), 'api_base_url': Setting().get('google_base_url'), 'request_token_url': None, - 'access_token_url': Setting().get('google_token_url'), - 'authorize_url': Setting().get('google_authorize_url'), 'client_kwargs': {'scope': Setting().get('google_oauth_scope')}, 'fetch_token': fetch_google_token, 'update_token': update_token @@ -31,6 +29,9 @@ def google_oauth(): if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0: authlib_params['server_metadata_url'] = server_metadata_url + else: + authlib_params['access_token_url'] = Setting().get('google_token_url') + authlib_params['authorize_url'] = Setting().get('google_authorize_url') google = authlib_oauth_client.register( 'google', diff --git a/powerdnsadmin/services/oidc.py b/powerdnsadmin/services/oidc.py index 3304b6a..25c73f0 100644 --- a/powerdnsadmin/services/oidc.py +++ b/powerdnsadmin/services/oidc.py @@ -20,8 +20,6 @@ def oidc_oauth(): 'client_secret': Setting().get('oidc_oauth_secret'), 'api_base_url': Setting().get('oidc_oauth_api_url'), 'request_token_url': None, - 'access_token_url': Setting().get('oidc_oauth_token_url'), - 'authorize_url': Setting().get('oidc_oauth_authorize_url'), 'client_kwargs': {'scope': Setting().get('oidc_oauth_scope')}, 'fetch_token': fetch_oidc_token, 'update_token': update_token @@ -31,6 +29,9 @@ def oidc_oauth(): if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0: authlib_params['server_metadata_url'] = server_metadata_url + else: + authlib_params['access_token_url'] = Setting().get('oidc_oauth_token_url') + authlib_params['authorize_url'] = Setting().get('oidc_oauth_authorize_url') oidc = authlib_oauth_client.register( 'oidc', From a46ab760fd3d958730aa33555910616bb3f4748c Mon Sep 17 00:00:00 2001 From: Matt Scott Date: Sat, 8 Apr 2023 17:40:30 -0400 Subject: [PATCH 03/18] Reorganized universal OAuth fields' order to a uniform standard. Also updated the `Client ID` field prompts to a uniform standard. --- .../admin_setting_authentication.html | 84 +++++++++---------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/powerdnsadmin/templates/admin_setting_authentication.html b/powerdnsadmin/templates/admin_setting_authentication.html index cbe6800..ca8baa5 100644 --- a/powerdnsadmin/templates/admin_setting_authentication.html +++ b/powerdnsadmin/templates/admin_setting_authentication.html @@ -749,22 +749,41 @@
+ 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 04/18] 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 05/18] 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 06/18] 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 }}