Merge branch 'feature/privacy-first'

This commit is contained in:
Matt Scott 2023-01-24 05:32:38 -05:00
commit 948973ac83
16 changed files with 100 additions and 84 deletions

View File

@ -7,7 +7,6 @@ SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2' SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0' BIND_ADDRESS = '0.0.0.0'
PORT = 9191 PORT = 9191
OFFLINE_MODE = False
### DATABASE CONFIG ### DATABASE CONFIG
SQLA_DB_USER = 'pda' SQLA_DB_USER = 'pda'

View File

@ -50,7 +50,6 @@ legal_envvars = (
'SAML_LOGOUT', 'SAML_LOGOUT',
'SAML_LOGOUT_URL', 'SAML_LOGOUT_URL',
'SAML_ASSERTION_ENCRYPTED', 'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_LOGOUT_URL', 'REMOTE_USER_LOGOUT_URL',
'REMOTE_USER_COOKIES', 'REMOTE_USER_COOKIES',
'SIGNUP_ENABLED', 'SIGNUP_ENABLED',
@ -77,7 +76,6 @@ legal_envvars_bool = (
'SAML_WANT_MESSAGE_SIGNED', 'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT', 'SAML_LOGOUT',
'SAML_ASSERTION_ENCRYPTED', 'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_ENABLED', 'REMOTE_USER_ENABLED',
'SIGNUP_ENABLED', 'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED', 'LOCAL_DB_ENABLED',

View File

@ -15,4 +15,3 @@ services:
- GUNICORN_TIMEOUT=60 - GUNICORN_TIMEOUT=60
- GUNICORN_WORKERS=2 - GUNICORN_WORKERS=2
- GUNICORN_LOGLEVEL=DEBUG - GUNICORN_LOGLEVEL=DEBUG
- OFFLINE_MODE=False # True for offline, False for external resources

View File

@ -74,8 +74,6 @@ def create_app(config=None):
app.jinja_env.filters['display_record_name'] = utils.display_record_name app.jinja_env.filters['display_record_name'] = utils.display_record_name
app.jinja_env.filters['display_master_name'] = utils.display_master_name app.jinja_env.filters['display_master_name'] = utils.display_master_name
app.jinja_env.filters['display_second_to_time'] = utils.display_time app.jinja_env.filters['display_second_to_time'] = utils.display_time
app.jinja_env.filters[
'email_to_gravatar_url'] = utils.email_to_gravatar_url
app.jinja_env.filters[ app.jinja_env.filters[
'display_setting_state'] = utils.display_setting_state 'display_setting_state'] = utils.display_setting_state
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
@ -93,9 +91,4 @@ def create_app(config=None):
setting = Setting() setting = Setting()
return dict(SETTING=setting) return dict(SETTING=setting)
@app.context_processor
def inject_mode():
setting = app.config.get('OFFLINE_MODE', False)
return dict(OFFLINE_MODE=setting)
return app return app

View File

@ -8,7 +8,6 @@ SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
BIND_ADDRESS = '0.0.0.0' BIND_ADDRESS = '0.0.0.0'
PORT = 9191 PORT = 9191
HSTS_ENABLED = False HSTS_ENABLED = False
OFFLINE_MODE = False
FILESYSTEM_SESSIONS_ENABLED = False FILESYSTEM_SESSIONS_ENABLED = False
SESSION_COOKIE_SAMESITE = 'Lax' SESSION_COOKIE_SAMESITE = 'Lax'
CSRF_COOKIE_HTTPONLY = True CSRF_COOKIE_HTTPONLY = True

View File

@ -2,14 +2,12 @@ import logging
import re import re
import json import json
import requests import requests
import hashlib
import ipaddress import ipaddress
import idna import idna
from collections.abc import Iterable from collections.abc import Iterable
from distutils.version import StrictVersion from distutils.version import StrictVersion
from urllib.parse import urlparse from urllib.parse import urlparse
from datetime import datetime, timedelta
def auth_from_url(url): def auth_from_url(url):
@ -186,17 +184,6 @@ def pdns_api_extended_uri(version):
return "" return ""
def email_to_gravatar_url(email="", size=100):
"""
AD doesn't necessarily have email
"""
if email is None:
email = ""
hash_string = hashlib.md5(email.encode('utf-8')).hexdigest()
return "https://s.gravatar.com/avatar/{0}?s={1}".format(hash_string, size)
def display_setting_state(value): def display_setting_state(value):
if value == 1: if value == 1:
return "ON" return "ON"

View File

@ -193,7 +193,8 @@ class Setting(db.Model):
'otp_force': False, 'otp_force': False,
'max_history_records': 1000, 'max_history_records': 1000,
'deny_domain_override': False, 'deny_domain_override': False,
'account_name_extra_chars': False 'account_name_extra_chars': False,
'gravatar_enabled': False,
} }
def __init__(self, id=None, name=None, value=None): def __init__(self, id=None, name=None, value=None):

View File

@ -1259,20 +1259,41 @@ def history_table(): # ajax call data
@login_required @login_required
@operator_role_required @operator_role_required
def setting_basic(): def setting_basic():
if request.method == 'GET': settings = [
settings = [ 'account_name_extra_chars',
'maintenance', 'fullscreen_layout', 'record_helper', 'allow_user_create_domain',
'login_ldap_first', 'default_record_table_size', 'allow_user_remove_domain',
'default_domain_table_size', 'auto_ptr', 'record_quick_edit', 'allow_user_view_history',
'pretty_ipv6_ptr', 'dnssec_admins_only', 'auto_ptr',
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name', 'bg_domain_updates',
'session_timeout', 'warn_session_timeout', 'ttl_options', 'custom_css',
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email', 'default_domain_table_size',
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force', 'default_record_table_size',
'deny_domain_override', 'enforce_api_ttl', 'account_name_extra_chars' 'delete_sso_accounts',
] 'deny_domain_override',
'dnssec_admins_only',
'enable_api_rr_history',
'enforce_api_ttl',
'fullscreen_layout',
'gravatar_enabled',
'login_ldap_first',
'maintenance',
'max_history_records',
'otp_field_enabled',
'otp_force',
'pdns_api_timeout',
'pretty_ipv6_ptr',
'record_helper',
'record_quick_edit',
'session_timeout',
'site_name',
'ttl_options',
'verify_ssl_connections',
'verify_user_email',
'warn_session_timeout',
]
return render_template('admin_setting_basic.html', settings=settings) return render_template('admin_setting_basic.html', settings=settings)
@admin_bp.route('/setting/basic/<path:setting>/edit', methods=['POST']) @admin_bp.route('/setting/basic/<path:setting>/edit', methods=['POST'])

View File

@ -1,5 +1,10 @@
import datetime import datetime
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app import hashlib
import imghdr
import mimetypes
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, \
current_app, after_this_request, abort
from flask_login import current_user, login_required, login_manager from flask_login import current_user, login_required, login_manager
from ..models.user import User, Anonymous from ..models.user import User, Anonymous
@ -96,4 +101,55 @@ def qrcode():
'Cache-Control': 'no-cache, no-store, must-revalidate', 'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache', 'Pragma': 'no-cache',
'Expires': '0' 'Expires': '0'
} }
@user_bp.route('/image', methods=['GET'])
@login_required
def image():
"""Returns the user profile image or avatar."""
@after_this_request
def add_cache_headers(response_):
"""When the response is ok, add cache headers."""
if 200 <= response_.status_code <= 399:
response_.cache_control.private = True
response_.cache_control.max_age = int(datetime.timedelta(days=1).total_seconds())
return response_
def return_image(content, content_type=None):
"""Return the given binary image content. Guess the type if not given."""
if not content_type:
guess = mimetypes.guess_type('example.' + imghdr.what(None, h=content))
if guess and guess[0]:
content_type = guess[0]
return content, 200, {'Content-Type': content_type}
# To prevent "cache poisoning", the username query parameter is required
if request.args.get('username', None) != current_user.username:
abort(400)
setting = Setting()
if session['authentication_type'] == 'LDAP':
search_filter = '(&({0}={1}){2})'.format(setting.get('ldap_filter_username'),
current_user.username,
setting.get('ldap_filter_basic'))
result = User().ldap_search(search_filter, setting.get('ldap_base_dn'))
if result and result[0] and result[0][0] and result[0][0][1]:
user_obj = result[0][0][1]
for key in ['jpegPhoto', 'thumbnailPhoto']:
if key in user_obj and user_obj[key] and user_obj[key][0]:
current_app.logger.debug(f'Return {key} from ldap as user image')
return return_image(user_obj[key][0])
email = current_user.email
if email and setting.get('gravatar_enabled'):
hash_ = hashlib.md5(email.encode('utf-8')).hexdigest()
url = f'https://s.gravatar.com/avatar/{hash_}?s=100'
current_app.logger.debug('Redirect user image request to gravatar')
return redirect(url, 307)
# Fallback to the local default image
return current_app.send_static_file('img/user_image.png')

View File

@ -42,15 +42,6 @@ table td {
background-position: center; background-position: center;
} }
.navbar-nav>.user-menu>.dropdown-menu>li.user-header>img.img-circle.offline {
filter: brightness(0);
border-color: black;
}
.navbar-nav>.user-menu .user-image.offline {
filter: brightness(0);
}
.search-input { .search-input {
width: 100%; width: 100%;
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

@ -35,7 +35,7 @@
<tbody> <tbody>
{% for statistic in statistics %} {% for statistic in statistics %}
<tr class="odd gradeX"> <tr class="odd gradeX">
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ statistic['name'] }}" <td><a href="https://doc.powerdns.com/authoritative/search.html?q={{ statistic['name'] }}"
target="_blank" class="btn btn-flat btn-xs blue"><i target="_blank" class="btn btn-flat btn-xs blue"><i
class="fa fa-search"></i></a></td> class="fa fa-search"></i></a></td>
<td>{{ statistic['name'] }}</td> <td>{{ statistic['name'] }}</td>
@ -70,7 +70,7 @@
<tbody> <tbody>
{% for config in configs %} {% for config in configs %}
<tr class="odd gradeX"> <tr class="odd gradeX">
<td><a href="https://google.com/search?q=site:doc.powerdns.com+{{ config['name'] }}" <td><a href="https://doc.powerdns.com/authoritative/search.html?q={{ config['name'] }}"
target="_blank" class="btn btn-flat btn-xs blue"><i target="_blank" class="btn btn-flat btn-xs blue"><i
class="fa fa-search"></i></a></td> class="fa fa-search"></i></a></td>
<td>{{ config['name'] }}</td> <td>{{ config['name'] }}</td>

View File

@ -8,13 +8,8 @@
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %} {% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}">
<!-- Get Google Fonts we like --> <!-- Get Google Fonts we like -->
{% if OFFLINE_MODE %}
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/source_sans_pro.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='assets/css/source_sans_pro.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/roboto_mono.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='assets/css/roboto_mono.css') }}">
{% else %}
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Mono:400,300,700">
{% endif %}
<!-- Tell the browser to be responsive to screen width --> <!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport"> <meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Tell Safari to not recognise telephone numbers --> <!-- Tell Safari to not recognise telephone numbers -->
@ -25,20 +20,10 @@
{% if SETTING.get('custom_css') %} {% if SETTING.get('custom_css') %}
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}"> <link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
{% endif %} {% endif %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
{% endblock %} {% endblock %}
</head> </head>
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}"> <body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
{% if OFFLINE_MODE %} {% set user_image_url = url_for('user.image', username=current_user.username) %}
{% set gravatar_url = url_for('static', filename='img/gravatar.png') %}
{% elif current_user.email is defined %}
{% set gravatar_url = current_user.email|email_to_gravatar_url(size=80) %}
{% endif %}
<div class="wrapper"> <div class="wrapper">
{% block pageheader %} {% block pageheader %}
<header class="main-header"> <header class="main-header">
@ -62,14 +47,14 @@
<!-- User Account: style can be found in dropdown.less --> <!-- User Account: style can be found in dropdown.less -->
<li class="dropdown user user-menu"> <li class="dropdown user user-menu">
<a href="#" class="dropdown-toggle" data-toggle="dropdown"> <a href="#" class="dropdown-toggle" data-toggle="dropdown">
<img src="{{ gravatar_url }}" class="user-image {{ 'offline' if OFFLINE_MODE }}" alt="User Image"/> <img src="{{ user_image_url }}" class="user-image" alt="User Image"/>
<span class="hidden-xs"> <span class="hidden-xs">
{{ current_user.firstname }} {{ current_user.firstname }}
</span> </span>
</a> </a>
<ul class="dropdown-menu"> <ul class="dropdown-menu">
<li class="user-header"> <li class="user-header">
<img src="{{ gravatar_url }}" class="img-circle {{ 'offline' if OFFLINE_MODE }}" alt="User Image"/> <img src="{{ user_image_url }}" class="img-circle" alt="User Image"/>
<p> <p>
{{ current_user.firstname }} {{ current_user.lastname }} {{ current_user.firstname }} {{ current_user.lastname }}
<small>{{ current_user.role.name }}</small> <small>{{ current_user.role.name }}</small>
@ -100,7 +85,7 @@
{% if current_user.id is defined %} {% if current_user.id is defined %}
<div class="user-panel"> <div class="user-panel">
<div class="pull-left image"> <div class="pull-left image">
<img src="{{ gravatar_url }}" class="img-circle" alt="User Image"/> <img src="{{ user_image_url }}" class="img-circle" alt="User Image"/>
</div> </div>
<div class="pull-left info"> <div class="pull-left info">
<p>{{ current_user.firstname }} {{ current_user.lastname }}</p> <p>{{ current_user.firstname }} {{ current_user.lastname }}</p>

View File

@ -15,12 +15,6 @@
{% if SETTING.get('custom_css') %} {% if SETTING.get('custom_css') %}
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}"> <link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
{% endif %} {% endif %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head> </head>
<body class="hold-transition login-page"> <body class="hold-transition login-page">

View File

@ -11,13 +11,6 @@
{% assets "css_login" -%} {% assets "css_login" -%}
<link rel="stylesheet" href="{{ ASSET_URL }}"> <link rel="stylesheet" href="{{ ASSET_URL }}">
{%- endassets %} {%- endassets %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
</head> </head>
<body class="hold-transition register-page"> <body class="hold-transition register-page">