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'
BIND_ADDRESS = '0.0.0.0'
PORT = 9191
OFFLINE_MODE = False
### DATABASE CONFIG
SQLA_DB_USER = 'pda'

View File

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

View File

@ -15,4 +15,3 @@ services:
- GUNICORN_TIMEOUT=60
- GUNICORN_WORKERS=2
- 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_master_name'] = utils.display_master_name
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[
'display_setting_state'] = utils.display_setting_state
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
@ -93,9 +91,4 @@ def create_app(config=None):
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

View File

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

View File

@ -2,14 +2,12 @@ import logging
import re
import json
import requests
import hashlib
import ipaddress
import idna
from collections.abc import Iterable
from distutils.version import StrictVersion
from urllib.parse import urlparse
from datetime import datetime, timedelta
def auth_from_url(url):
@ -186,17 +184,6 @@ def pdns_api_extended_uri(version):
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):
if value == 1:
return "ON"

View File

@ -193,7 +193,8 @@ class Setting(db.Model):
'otp_force': False,
'max_history_records': 1000,
'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):

View File

@ -1259,20 +1259,41 @@ def history_table(): # ajax call data
@login_required
@operator_role_required
def setting_basic():
if request.method == 'GET':
settings = [
'maintenance', 'fullscreen_layout', 'record_helper',
'login_ldap_first', 'default_record_table_size',
'default_domain_table_size', 'auto_ptr', 'record_quick_edit',
'pretty_ipv6_ptr', 'dnssec_admins_only',
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
'session_timeout', 'warn_session_timeout', 'ttl_options',
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force',
'deny_domain_override', 'enforce_api_ttl', 'account_name_extra_chars'
]
settings = [
'account_name_extra_chars',
'allow_user_create_domain',
'allow_user_remove_domain',
'allow_user_view_history',
'auto_ptr',
'bg_domain_updates',
'custom_css',
'default_domain_table_size',
'default_record_table_size',
'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'])

View File

@ -1,5 +1,10 @@
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 ..models.user import User, Anonymous
@ -96,4 +101,55 @@ def qrcode():
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'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;
}
.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 {
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>
{% for statistic in statistics %}
<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
class="fa fa-search"></i></a></td>
<td>{{ statistic['name'] }}</td>
@ -70,7 +70,7 @@
<tbody>
{% for config in configs %}
<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
class="fa fa-search"></i></a></td>
<td>{{ config['name'] }}</td>

View File

@ -8,13 +8,8 @@
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}">
<!-- 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/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 -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Tell Safari to not recognise telephone numbers -->
@ -25,20 +20,10 @@
{% if SETTING.get('custom_css') %}
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
{% 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 %}
</head>
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
{% if OFFLINE_MODE %}
{% 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 %}
{% set user_image_url = url_for('user.image', username=current_user.username) %}
<div class="wrapper">
{% block pageheader %}
<header class="main-header">
@ -62,14 +47,14 @@
<!-- User Account: style can be found in dropdown.less -->
<li class="dropdown user user-menu">
<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">
{{ current_user.firstname }}
</span>
</a>
<ul class="dropdown-menu">
<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>
{{ current_user.firstname }} {{ current_user.lastname }}
<small>{{ current_user.role.name }}</small>
@ -100,7 +85,7 @@
{% if current_user.id is defined %}
<div class="user-panel">
<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 class="pull-left info">
<p>{{ current_user.firstname }} {{ current_user.lastname }}</p>

View File

@ -15,12 +15,6 @@
{% if SETTING.get('custom_css') %}
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
{% 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>
<body class="hold-transition login-page">

View File

@ -11,13 +11,6 @@
{% assets "css_login" -%}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{%- 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>
<body class="hold-transition register-page">