mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-12-04 19:15:30 +00:00
Updating master
branch to the latest release of 0.4.2
(#1743)
This commit is contained in:
commit
d255cb3d16
3
.github/workflows/build-and-publish.yml
vendored
3
.github/workflows/build-and-publish.yml
vendored
@ -70,9 +70,10 @@ jobs:
|
||||
tags: powerdnsadmin/pda-legacy:${{ github.ref_name }}
|
||||
|
||||
- name: Docker Image Release Tagging
|
||||
uses: docker/build-push-action@v2
|
||||
uses: docker/build-push-action@v4
|
||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: ./
|
||||
file: ./docker/Dockerfile
|
||||
push: true
|
||||
|
3
.github/workflows/mega-linter.yml
vendored
3
.github/workflows/mega-linter.yml
vendored
@ -12,6 +12,9 @@ on:
|
||||
- "main"
|
||||
- "master"
|
||||
- "dependabot/**"
|
||||
- "feature/**"
|
||||
- "issues/**"
|
||||
- "release/**"
|
||||
|
||||
env: # Comment env block if you do not want to apply fixes
|
||||
# Apply linter fixes configuration
|
||||
|
@ -57,10 +57,10 @@ This creates a volume named `pda-data` to persist the default SQLite database wi
|
||||
1. Update the configuration
|
||||
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`.
|
||||
Other environment variables are mentioned in
|
||||
the [legal_envvars](https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/master/configs/docker_config.py#L5-L46).
|
||||
To use the Docker secrets feature it is possible to append `_FILE` to the environment variables and point to a file
|
||||
with the values stored in it.
|
||||
Make sure to set the environment variable `SECRET_KEY` to a long random
|
||||
the [AppSettings.defaults](https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/master/powerdnsadmin/lib/settings.py) dictionary.
|
||||
To use a Docker-style secrets convention, one may append `_FILE` to the environment variables with a path to a file
|
||||
containing the intended value of the variable (e.g. `SQLALCHEMY_DATABASE_URI_FILE=/run/secrets/db_uri`).
|
||||
Make sure to set the environment variable `SECRET_KEY` to a long, random
|
||||
string (https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY)
|
||||
|
||||
2. Start docker container
|
||||
|
@ -7,7 +7,7 @@ SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||
BIND_ADDRESS = '0.0.0.0'
|
||||
PORT = 9191
|
||||
SERVER_EXTERNAL_SSL = None
|
||||
SERVER_EXTERNAL_SSL = os.getenv('SERVER_EXTERNAL_SSL', None)
|
||||
|
||||
### DATABASE CONFIG
|
||||
SQLA_DB_USER = 'pda'
|
||||
|
@ -1,151 +1,2 @@
|
||||
# import everything from environment variables
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
|
||||
# Defaults for Docker image
|
||||
BIND_ADDRESS = '0.0.0.0'
|
||||
PORT = 80
|
||||
SERVER_EXTERNAL_SSL = True
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
SESSION_TYPE = 'sqlalchemy'
|
||||
|
||||
legal_envvars = (
|
||||
'SECRET_KEY',
|
||||
'OIDC_OAUTH_ENABLED',
|
||||
'OIDC_OAUTH_KEY',
|
||||
'OIDC_OAUTH_SECRET',
|
||||
'OIDC_OAUTH_API_URL',
|
||||
'OIDC_OAUTH_TOKEN_URL',
|
||||
'OIDC_OAUTH_AUTHORIZE_URL',
|
||||
'OIDC_OAUTH_METADATA_URL',
|
||||
'OIDC_OAUTH_LOGOUT_URL',
|
||||
'OIDC_OAUTH_SCOPE',
|
||||
'OIDC_OAUTH_USERNAME',
|
||||
'OIDC_OAUTH_FIRSTNAME',
|
||||
'OIDC_OAUTH_LAST_NAME',
|
||||
'OIDC_OAUTH_EMAIL',
|
||||
'BIND_ADDRESS',
|
||||
'PORT',
|
||||
'SERVER_EXTERNAL_SSL',
|
||||
'LOG_LEVEL',
|
||||
'SALT',
|
||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||
'SQLALCHEMY_DATABASE_URI',
|
||||
'SQLALCHEMY_ENGINE_OPTIONS',
|
||||
'MAIL_SERVER',
|
||||
'MAIL_PORT',
|
||||
'MAIL_DEBUG',
|
||||
'MAIL_USE_TLS',
|
||||
'MAIL_USE_SSL',
|
||||
'MAIL_USERNAME',
|
||||
'MAIL_PASSWORD',
|
||||
'MAIL_DEFAULT_SENDER',
|
||||
'SAML_ENABLED',
|
||||
'SAML_DEBUG',
|
||||
'SAML_PATH',
|
||||
'SAML_METADATA_URL',
|
||||
'SAML_METADATA_CACHE_LIFETIME',
|
||||
'SAML_IDP_SSO_BINDING',
|
||||
'SAML_IDP_ENTITY_ID',
|
||||
'SAML_NAMEID_FORMAT',
|
||||
'SAML_ATTRIBUTE_EMAIL',
|
||||
'SAML_ATTRIBUTE_GIVENNAME',
|
||||
'SAML_ATTRIBUTE_SURNAME',
|
||||
'SAML_ATTRIBUTE_NAME',
|
||||
'SAML_ATTRIBUTE_USERNAME',
|
||||
'SAML_ATTRIBUTE_ADMIN',
|
||||
'SAML_ATTRIBUTE_GROUP',
|
||||
'SAML_GROUP_ADMIN_NAME',
|
||||
'SAML_GROUP_TO_ACCOUNT_MAPPING',
|
||||
'SAML_ATTRIBUTE_ACCOUNT',
|
||||
'SAML_SP_ENTITY_ID',
|
||||
'SAML_SP_CONTACT_NAME',
|
||||
'SAML_SP_CONTACT_MAIL',
|
||||
'SAML_SIGN_REQUEST',
|
||||
'SAML_WANT_MESSAGE_SIGNED',
|
||||
'SAML_LOGOUT',
|
||||
'SAML_LOGOUT_URL',
|
||||
'SAML_ASSERTION_ENCRYPTED',
|
||||
'REMOTE_USER_LOGOUT_URL',
|
||||
'REMOTE_USER_COOKIES',
|
||||
'SIGNUP_ENABLED',
|
||||
'LOCAL_DB_ENABLED',
|
||||
'LDAP_ENABLED',
|
||||
'SAML_CERT',
|
||||
'SAML_KEY',
|
||||
'SESSION_TYPE',
|
||||
'SESSION_COOKIE_SECURE',
|
||||
'CSRF_COOKIE_SECURE',
|
||||
'CAPTCHA_ENABLE',
|
||||
)
|
||||
|
||||
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
|
||||
|
||||
legal_envvars_bool = (
|
||||
'SQLALCHEMY_TRACK_MODIFICATIONS',
|
||||
'HSTS_ENABLED',
|
||||
'MAIL_DEBUG',
|
||||
'MAIL_USE_TLS',
|
||||
'MAIL_USE_SSL',
|
||||
'OIDC_OAUTH_ENABLED',
|
||||
'SAML_ENABLED',
|
||||
'SAML_DEBUG',
|
||||
'SAML_SIGN_REQUEST',
|
||||
'SAML_WANT_MESSAGE_SIGNED',
|
||||
'SAML_LOGOUT',
|
||||
'SAML_ASSERTION_ENCRYPTED',
|
||||
'REMOTE_USER_ENABLED',
|
||||
'SIGNUP_ENABLED',
|
||||
'LOCAL_DB_ENABLED',
|
||||
'LDAP_ENABLED',
|
||||
'SESSION_COOKIE_SECURE',
|
||||
'CSRF_COOKIE_SECURE',
|
||||
'CAPTCHA_ENABLE',
|
||||
'SERVER_EXTERNAL_SSL',
|
||||
)
|
||||
|
||||
legal_envvars_dict = (
|
||||
'SQLALCHEMY_ENGINE_OPTIONS',
|
||||
)
|
||||
|
||||
def str2bool(v):
|
||||
return v.lower() in ("true", "yes", "1")
|
||||
|
||||
|
||||
def dictfromstr(v, ret):
|
||||
try:
|
||||
return json.loads(ret)
|
||||
except Exception as e:
|
||||
print('Cannot parse json {} for variable {}'.format(ret, v))
|
||||
print(e)
|
||||
raise ValueError
|
||||
|
||||
|
||||
for v in legal_envvars:
|
||||
|
||||
ret = None
|
||||
# _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:
|
||||
raise AttributeError(
|
||||
"Both {} and {} are set but are exclusive.".format(
|
||||
v, v + '_FILE'))
|
||||
with open(os.environ[v + '_FILE']) as f:
|
||||
ret = f.read()
|
||||
f.close()
|
||||
|
||||
elif v in os.environ:
|
||||
ret = os.environ[v]
|
||||
|
||||
if ret is not None:
|
||||
if v in legal_envvars_bool:
|
||||
ret = str2bool(ret)
|
||||
if v in legal_envvars_int:
|
||||
ret = int(ret)
|
||||
if v in legal_envvars_dict:
|
||||
ret = dictfromstr(v, ret)
|
||||
sys.modules[__name__].__dict__[v] = ret
|
||||
|
@ -92,6 +92,6 @@ RUN chown ${USER}:${USER} ./configs /app && \
|
||||
|
||||
EXPOSE 80/tcp
|
||||
USER ${USER}
|
||||
HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1/"]
|
||||
HEALTHCHECK --interval=5s --timeout=5s --start-period=20s --retries=5 CMD wget --output-document=- --quiet --tries=1 http://127.0.0.1${SCRIPT_NAME:-/}
|
||||
ENTRYPOINT ["entrypoint.sh"]
|
||||
CMD ["gunicorn","powerdnsadmin:create_app()"]
|
||||
|
100
docs/announcements/project-update-2022-12-09.md
Normal file
100
docs/announcements/project-update-2022-12-09.md
Normal file
@ -0,0 +1,100 @@
|
||||
# PDA Project Update
|
||||
|
||||
## Introduction
|
||||
|
||||
Hello PDA community members,
|
||||
|
||||
My name is Matt Scott, and I am the owner of [Azorian Solutions](https://azorian.solutions), a consultancy for the
|
||||
Internet Service Provider (ISP) industry. I'm pleased to announce that I have taken ownership of the PDA project and
|
||||
will be taking over the lead maintainer role, effective immediately.
|
||||
|
||||
Please always remember and thank both [Khanh Ngo](https://github.com/ngoduykhanh) and
|
||||
[Jérôme Becot](https://github.com/jbe-dw) for their efforts in keeping this project alive thus far. Without the effort
|
||||
of Khanh creating the PDA project and community, and the efforts of Jérôme for holding up the lead maintainer role after
|
||||
Khanh had to step down, this project would not still be alive today.
|
||||
|
||||
With that being said, please read through all the following announcements as they are important if you're an active PDA
|
||||
user or community member. I intend to make many great enhancements to the project, but it could be a bumpy road ahead.
|
||||
|
||||
### Project Maintenance
|
||||
|
||||
As it stands today, contributions to the project are at a low. At this point, there is a rather large backlog of issues
|
||||
and feature requests in contrast to the current maintenance capacities. This is not to say you should lose hope though!
|
||||
As part of this project transition, some additional contribution interest has been generated and I expect to attract
|
||||
more with the changes I'm planning to make. In the near future, I may by-pass some usual maintenance processes in order
|
||||
to expedite some changes to the project that have been outstanding for some time.
|
||||
|
||||
This is to say however that unless the project attracts a healthy new contribution base, issues may continue to pile up
|
||||
as maintenance capacity is rather limited. This is further complicated by the fact that the current code base is harder
|
||||
to follow naturally since it largely lacks uniformity and standards. This lack of uniformity has lead to a difficult
|
||||
situation that makes implementing certain changes less effective. This status quo is not uncommon with projects born how
|
||||
PDA was born, so it's unfortunate but not unexpected.
|
||||
|
||||
### Change of Direction
|
||||
|
||||
In order to reorganize the project and get it on a track to a future that allows it to contend with other commercial
|
||||
quality products, I had to make many considerations to the proficiencies of two unique paths forward to achieve this
|
||||
goal. One path forward is seemingly obvious, continue maintaining the current code base while overhauling it to shift it
|
||||
towards the envisioned goal. The other path is a fresh solution design with a complete rebuild.
|
||||
|
||||
The answer to the aforementioned decision might seem obvious to those of you who typically favor the "don't reinvent the
|
||||
wheel" mentality. I'm unclear of the details surrounding the original use-case that drove the development of this
|
||||
project, but I don't believe it was on-par with some use-cases we see today which include operators handling many tens
|
||||
of thousands of zones and/or records. There are many changes that have been (sometimes) haphazardly implemented which
|
||||
has lead to the previously mentioned lack of uniformity among other issues. To put it simply, I'm not sure if the
|
||||
project ever had a grand vision per se but instead was mostly reactionary to community requests.
|
||||
|
||||
I believe that the current project has served the community fairly well from what I can tell. I know the product has
|
||||
certainly helped me in my professional efforts with many environments. I also believe that it's time to pivot so that
|
||||
the project can realize it's true potential, considering the existing user base. For this reason, I am beginning the
|
||||
planning phase of a project overhaul. This effort will involve a complete re-engineering of the project's contribution
|
||||
standards and requirements, technology stack, and project structure.
|
||||
|
||||
This was not an easy decision to come to but one must appreciate that there aren't as many people that can get very
|
||||
excited about working on the current project code base. The current project has many barriers to entry which I intend to
|
||||
drastically impact with future changes. The reality is that it's easier to gain contribution participation with a new
|
||||
build effort as it offers an opportunity to own a part of the project with impactful contributions.
|
||||
|
||||
### Project Enhancements
|
||||
|
||||
Since this is the beginning of a rebirth of the project so to speak, I want to implement a new operational tactic that
|
||||
will hopefully drive contributions through incentive. Many of us understand that any project, needs a leader to stay on
|
||||
track and organized. If everything were a democratic process, it would take too long and suffer unnecessary challenges.
|
||||
With that being said, I do believe that there is plenty of opportunity through-out various development phases of the
|
||||
project to allow for a democratic process where the community contributors and members can participate in the
|
||||
decision-making.
|
||||
|
||||
The plan to achieve the aforementioned democratic goal is to centralize communications and define some basic structured
|
||||
processes. To do this, more effective methods of communication have been implemented to allow those interested in
|
||||
contributing to easily participate in fluid, open communication. This has already been proving to be quite effective for
|
||||
exchanging ideas and visions while addressing the issue with contributors living in vastly different time zones. This is
|
||||
effectively a private chat hosted by the PDA project using Mattermost (a Slack-like alternative).
|
||||
|
||||
Even if you aren't in a position to directly contribute work to the project, you can still contribute by participating
|
||||
in these very important and early discussions that will impact the solution engineering. If the PDA project is an
|
||||
important tool in your organization, I encourage you to join the conversation and contribute where applicable your
|
||||
use-cases. Having more insight on the community use-cases will only benefit the future of this project.
|
||||
|
||||
If you're interested in joining the conversation, please email me at
|
||||
[admin@powerdnsadmin.org](mailto:admin@powerdnsadmin.org) for an invitation.
|
||||
|
||||
### Re-branding
|
||||
|
||||
As part of this project transition, I will also be changing the naming scheme in order to support the future development
|
||||
efforts toward a newly engineered solution. The current PDA project will ultimately become known as the "PDA Legacy"
|
||||
application. This change will help facilitate the long-term solution to take the branding position of the existing
|
||||
solution. Another effort I will be making is to get an app landing page online at the project's new domain:
|
||||
[powerdnsadmin.org](https://powerdnsadmin.org). This will act as one more point of online exposure for the project which
|
||||
will hopefully lend itself well to attracting additional community members.
|
||||
|
||||
### Contribution Requirements
|
||||
|
||||
Another big change that will be made with the new project, will be well-defined contribution requirements. I realize
|
||||
these requirements can be demotivating for some, but they are a necessary evil to ensure the project actually achieves
|
||||
its goals effectively. It's important to always remember that strict requirements are to everyone's benefit as they push
|
||||
for order where chaos is quite destructive.
|
||||
|
||||
### Closing
|
||||
|
||||
I hope these announcements garner more participation in the PDA community. The project definitely needs more help to
|
||||
achieve any goal at this point, so your participation is valued!
|
109
docs/announcements/project-update-2023-11-25.md
Normal file
109
docs/announcements/project-update-2023-11-25.md
Normal file
@ -0,0 +1,109 @@
|
||||
# PDA Project Update
|
||||
|
||||
## Introduction
|
||||
|
||||
Hello PDA community members,
|
||||
|
||||
I know it has been quite awhile since the last formal announcement like this. Things have been quite busy and difficult
|
||||
for me both professional and personally. While I try hard to never make my problems someone else's problems, I do
|
||||
believe it's important to be transparent with the community. I'm not going to go into details, but I will say that I
|
||||
have been dealing with some mental health issues that have been quite challenging. I'm not one to give up though,
|
||||
so I'm pushing through and trying to get back on track.
|
||||
|
||||
With that being said, let's jump into the announcements.
|
||||
|
||||
### Project Maintenance
|
||||
|
||||
Granted I haven't been nearly as active on the project as I would like to be, I have been keeping an eye on things and
|
||||
trying to keep up with the maintenance. I know there are a lot of issues and feature requests that have been piling up,
|
||||
and I'm sorry for that. Even if I had been more active in recent months, it would have not changed the true root cause
|
||||
of the issue.
|
||||
|
||||
This project was started out of a need for an individual's own use-case. I don't believe it was never intended to be a
|
||||
commercial quality product nor a community project. It did however gain traction quickly and the community grew. This
|
||||
is a great thing, but it also comes with some challenges. The biggest challenge is that the project was never designed
|
||||
to be a community project. This means that the project lacks many of the things that are required to effectively manage
|
||||
a community project. This is not to say that the project is doomed, but many of the fast-paced changes combined with
|
||||
the lack of standards has lead to a difficult situation that makes implementing certain changes incredibly unproductive
|
||||
and quite often, entirely counter-productive.
|
||||
|
||||
After many years of accepting contributions from those who are not professional developers, the project has become quite
|
||||
difficult to maintain. This is not to say that I don't appreciate the contributions, but it's important to understand
|
||||
that the state of the code-base for the project is not in a good place. This is not uncommon with projects born how PDA
|
||||
was born, so it's unfortunate but not unexpected.
|
||||
|
||||
As of today, there are so many dependencies and a large amount of very poorly implemented features that it's difficult
|
||||
to make any changes without breaking many other pieces. This is further complicated by the fact that the current code
|
||||
base is harder to follow naturally since it largely lacks uniformity and standards. This lack of uniformity has lead to
|
||||
a situation where automated regression testing is not possible. This is a very important aspect of any project that
|
||||
expects to be able to make changes without breaking things. This is also a very important aspect of any project that
|
||||
expects to be able to accept contributions from the community with minimum management resources.
|
||||
|
||||
The hard reality is that the majority of stakeholders in the project are not professional developers. This naturally
|
||||
means the amount of people that can offer quality contributions is very limited. This problem is further aggravated by
|
||||
the poor quality feature implementation which is very hard to follow, even for seasoned developers like myself. So many
|
||||
seemingly small issues that have been reported, have lead to finding that the resolution is not as simple as it seems.
|
||||
|
||||
### New Direction
|
||||
|
||||
As I previously stated in my last formal announcement, we would be working towards a total replacement of the project.
|
||||
Unfortunately, this is not a simple task, and it's not something that can be done quickly. Furthermore, with
|
||||
increasingly limited capacity in our own lives to work on this, we are essentially drowning in a sea of technical debt
|
||||
created by the past decisions of the project to accept all contributions. We have essentially reached a point where
|
||||
far too much time and resources are being wasted just to attempt to meet the current demand of requests on the current
|
||||
edition of PDA. This is a tragedy because the efforts that are invested into the current edition, really aren't
|
||||
creating true progress for the project, but instead merely delaying the inevitable.
|
||||
|
||||
As I have stated before to many community members, one aspect of taking over management of this project to ultimately
|
||||
save it and keep it alive, would involve making hard decisions that many will not agree with. It's unfortunate that
|
||||
many of those who are less than supportive of these decisions, often lack the appropriate experience to understand the
|
||||
importance of these decisions. I'm not saying that I'm always right, but I am saying that it's not hard to see where
|
||||
this is headed without some drastic changes.
|
||||
|
||||
With all of that being said, it's time for me to make some hard decisions. I have decided that the best course of
|
||||
action is to stop accepting contributions to the current edition of PDA. At this point, due to the aforementioned
|
||||
issues that lead to breaking the application with seemingly simple changes, it's just not worth the effort to try to
|
||||
keep up with the current edition. This is not to say that I'm giving up on the project, but instead I'm going to
|
||||
re-focus my efforts on the new edition of PDA. This is the only way to ensure that the project will survive and
|
||||
hopefully thrive in the future.
|
||||
|
||||
I will not abandon the current set of updates that were planned for the next release of `0.4.2` however. I have
|
||||
re-scheduled that release to be out by the end of the year. This will be the last release of the current edition of
|
||||
PDA. The consensus from some users is that the current edition is stable enough to be used in production environments.
|
||||
I don't necessarily agree with that, but I do believe that it's stable enough to be used in production
|
||||
environments with the understanding that it's not a commercial quality product.
|
||||
|
||||
### Future Contributions
|
||||
|
||||
For those of you wondering about contributions to the new edition of PDA, the answer for now is simple. I won't be
|
||||
accepting any contributions to the new edition until I can achieve a stable release that delivers the core features of
|
||||
the current edition. This is not to say that I won't be accepting any contributions at all, but instead that I will be
|
||||
very selective about what contributions I accept. I believe this is the only way to ensure that a solid foundation not
|
||||
only takes shape, but remains solid.
|
||||
|
||||
It is well understood that many developers have their own ways of doing things, but it's important to understand
|
||||
that this project is not a personal project. This project is a community project and therefore must be treated as such.
|
||||
This means that the project must be engineered in a way that allows for the community to participate in the development
|
||||
process. This is not possible if the project is not engineered in a way that is easy to follow and understand.
|
||||
|
||||
### Project Enhancements
|
||||
|
||||
It should be understood that one of the greatest benefits of this pivot is that it will allow for a more structured
|
||||
development process. As a result of that, the project could potentially see a future where it adopts a whole new set of
|
||||
features that weren't previously imagined. One prime example of this could be integration with registrar APIs. This
|
||||
could make easy work of tasks such as DNSSEC key rotation, which is currently a very manual process.
|
||||
|
||||
I am still working on final project requirements for additional phases of the new PDA edition, but these additions
|
||||
won't receive any attention until the core features are implemented. I will be sure to make announcements as these
|
||||
requirements are finalized. It is my intention to follow a request for proposal (RFP) process for these additional
|
||||
features. This will allow the community to participate in the decision-making process for future expansion of the
|
||||
project.
|
||||
|
||||
### Closing
|
||||
|
||||
I hope that by the time you have reached this point in the announcement, that I have elicited new hope for the
|
||||
long-term future of the project. I know that many of you have been waiting for a long time for some of the features that have been
|
||||
requested. I know that many of you have been waiting for a long time for some of the issues to be resolved, for
|
||||
requested features to be implemented, and for the project to be more stable. It's unfortunate that it has taken this
|
||||
long to get to this point, but this is the nature of life itself. I hope that you can understand that this is the only
|
||||
reasonable gamble that the project survives and thrives in the future.
|
@ -40,7 +40,6 @@
|
||||
|
||||
- Setting up a zone
|
||||
- Adding a record
|
||||
- <whatever else>
|
||||
|
||||
## Feature usage
|
||||
|
||||
|
@ -4,11 +4,11 @@ from flask import Flask
|
||||
from flask_mail import Mail
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
from flask_session import Session
|
||||
|
||||
from .lib import utils
|
||||
|
||||
|
||||
def create_app(config=None):
|
||||
from powerdnsadmin.lib.settings import AppSettings
|
||||
from . import models, routes, services
|
||||
from .assets import assets
|
||||
app = Flask(__name__)
|
||||
@ -50,6 +50,9 @@ def create_app(config=None):
|
||||
elif config.endswith('.py'):
|
||||
app.config.from_pyfile(config)
|
||||
|
||||
# Load any settings defined with environment variables
|
||||
AppSettings.load_environment(app)
|
||||
|
||||
# HSTS
|
||||
if app.config.get('HSTS_ENABLED'):
|
||||
from flask_sslify import SSLify
|
||||
@ -84,7 +87,7 @@ def create_app(config=None):
|
||||
app.jinja_env.filters['format_datetime_local'] = utils.format_datetime
|
||||
app.jinja_env.filters['format_zone_type'] = utils.format_zone_type
|
||||
|
||||
# Register context proccessors
|
||||
# Register context processors
|
||||
from .models.setting import Setting
|
||||
|
||||
@app.context_processor
|
||||
|
@ -13,6 +13,7 @@ def admin_role_required(f):
|
||||
"""
|
||||
Grant access if user is in Administrator role
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name != 'Administrator':
|
||||
@ -26,6 +27,7 @@ def operator_role_required(f):
|
||||
"""
|
||||
Grant access if user is in Operator role or higher
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
@ -39,6 +41,7 @@ def history_access_required(f):
|
||||
"""
|
||||
Grant access if user is in Operator role or higher, or Users can view history
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name not in [
|
||||
@ -57,6 +60,7 @@ def can_access_domain(f):
|
||||
- user is in granted Account, or
|
||||
- user is in granted Domain
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
@ -83,10 +87,11 @@ def can_configure_dnssec(f):
|
||||
- user is in Operator role or higher, or
|
||||
- dnssec_admins_only is off
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name not in [
|
||||
'Administrator', 'Operator'
|
||||
'Administrator', 'Operator'
|
||||
] and Setting().get('dnssec_admins_only'):
|
||||
abort(403)
|
||||
|
||||
@ -94,16 +99,18 @@ def can_configure_dnssec(f):
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def can_remove_domain(f):
|
||||
"""
|
||||
Grant access if:
|
||||
- user is in Operator role or higher, or
|
||||
- allow_user_remove_domain is on
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name not in [
|
||||
'Administrator', 'Operator'
|
||||
'Administrator', 'Operator'
|
||||
] and not Setting().get('allow_user_remove_domain'):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
@ -111,17 +118,17 @@ def can_remove_domain(f):
|
||||
return decorated_function
|
||||
|
||||
|
||||
|
||||
def can_create_domain(f):
|
||||
"""
|
||||
Grant access if:
|
||||
- user is in Operator role or higher, or
|
||||
- allow_user_create_domain is on
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name not in [
|
||||
'Administrator', 'Operator'
|
||||
'Administrator', 'Operator'
|
||||
] and not Setting().get('allow_user_create_domain'):
|
||||
abort(403)
|
||||
return f(*args, **kwargs)
|
||||
@ -144,11 +151,12 @@ def api_basic_auth(f):
|
||||
|
||||
# Remove "Basic " from the header value
|
||||
auth_header = auth_header[6:]
|
||||
auth_components = []
|
||||
|
||||
try:
|
||||
auth_header = str(base64.b64decode(auth_header), 'utf-8')
|
||||
# NK: We use auth_components here as we don't know if we'll have a :, we split it maximum 1 times to grab the
|
||||
# username, the rest of the string would be the password.
|
||||
# NK: We use auth_components here as we don't know if we'll have a colon,
|
||||
# we split it maximum 1 times to grab the username, the rest of the string would be the password.
|
||||
auth_components = auth_header.split(':', maxsplit=1)
|
||||
except (binascii.Error, UnicodeDecodeError) as e:
|
||||
current_app.logger.error(
|
||||
@ -211,16 +219,19 @@ def callback_if_request_body_contains_key(callback, http_methods=[], keys=[]):
|
||||
If request body contains one or more of specified keys, call
|
||||
:param callback
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
check_current_http_method = not http_methods or request.method in http_methods
|
||||
if (check_current_http_method and
|
||||
set(request.get_json(force=True).keys()).intersection(set(keys))
|
||||
set(request.get_json(force=True).keys()).intersection(set(keys))
|
||||
):
|
||||
callback(*args, **kwargs)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@ -246,16 +257,18 @@ def api_role_can(action, roles=None, allow_self=False):
|
||||
except:
|
||||
username = None
|
||||
if (
|
||||
(current_user.role.name in roles) or
|
||||
(allow_self and user_id and current_user.id == user_id) or
|
||||
(allow_self and username and current_user.username == username)
|
||||
(current_user.role.name in roles) or
|
||||
(allow_self and user_id and current_user.id == user_id) or
|
||||
(allow_self and username and current_user.username == username)
|
||||
):
|
||||
return f(*args, **kwargs)
|
||||
msg = (
|
||||
"User {} with role {} does not have enough privileges to {}"
|
||||
).format(current_user.username, current_user.role.name, action)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@ -265,10 +278,11 @@ def api_can_create_domain(f):
|
||||
- user is in Operator role or higher, or
|
||||
- allow_user_create_domain is on
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if current_user.role.name not in [
|
||||
'Administrator', 'Operator'
|
||||
'Administrator', 'Operator'
|
||||
] and not Setting().get('allow_user_create_domain'):
|
||||
msg = "User {0} does not have enough privileges to create zone"
|
||||
current_app.logger.error(msg.format(current_user.username))
|
||||
@ -294,10 +308,11 @@ def apikey_can_create_domain(f):
|
||||
- deny_domain_override is off or
|
||||
- override_domain is true (from request)
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.apikey.role.name not in [
|
||||
'Administrator', 'Operator'
|
||||
'Administrator', 'Operator'
|
||||
] and not Setting().get('allow_user_create_domain'):
|
||||
msg = "ApiKey #{0} does not have enough privileges to create zone"
|
||||
current_app.logger.error(msg.format(g.apikey.id))
|
||||
@ -320,20 +335,23 @@ def apikey_can_remove_domain(http_methods=[]):
|
||||
- user is in Operator role or higher, or
|
||||
- allow_user_remove_domain is on
|
||||
"""
|
||||
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
check_current_http_method = not http_methods or request.method in http_methods
|
||||
|
||||
if (check_current_http_method and
|
||||
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||
not Setting().get('allow_user_remove_domain')
|
||||
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||
not Setting().get('allow_user_remove_domain')
|
||||
):
|
||||
msg = "ApiKey #{0} does not have enough privileges to remove zone"
|
||||
current_app.logger.error(msg.format(g.apikey.id))
|
||||
raise NotEnoughPrivileges()
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@ -341,6 +359,7 @@ def apikey_is_admin(f):
|
||||
"""
|
||||
Grant access if user is in Administrator role
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.apikey.role.name != 'Administrator':
|
||||
@ -358,6 +377,7 @@ def apikey_can_access_domain(f):
|
||||
- user has Operator role or higher, or
|
||||
- user has explicitly been granted access to domain
|
||||
"""
|
||||
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
||||
@ -382,22 +402,26 @@ def apikey_can_configure_dnssec(http_methods=[]):
|
||||
- user is in Operator role or higher, or
|
||||
- dnssec_admins_only is off
|
||||
"""
|
||||
|
||||
def decorator(f=None):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
check_current_http_method = not http_methods or request.method in http_methods
|
||||
|
||||
if (check_current_http_method and
|
||||
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||
Setting().get('dnssec_admins_only')
|
||||
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||
Setting().get('dnssec_admins_only')
|
||||
):
|
||||
msg = "ApiKey #{0} does not have enough privileges to configure dnssec"
|
||||
current_app.logger.error(msg.format(g.apikey.id))
|
||||
raise DomainAccessForbidden(message=msg)
|
||||
return f(*args, **kwargs) if f else None
|
||||
|
||||
return decorated_function
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def allowed_record_types(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
@ -423,6 +447,7 @@ def allowed_record_types(f):
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def allowed_record_ttl(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
@ -436,7 +461,7 @@ def allowed_record_ttl(f):
|
||||
return f(*args, **kwargs)
|
||||
|
||||
allowed_ttls = Setting().get_ttl_options()
|
||||
allowed_numeric_ttls = [ ttl[0] for ttl in allowed_ttls ]
|
||||
allowed_numeric_ttls = [ttl[0] for ttl in allowed_ttls]
|
||||
content = request.get_json()
|
||||
try:
|
||||
for record in content['rrsets']:
|
||||
@ -497,6 +522,7 @@ def dyndns_login_required(f):
|
||||
|
||||
return decorated_function
|
||||
|
||||
|
||||
def apikey_or_basic_auth(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
@ -505,4 +531,5 @@ def apikey_or_basic_auth(f):
|
||||
return apikey_auth(f)(*args, **kwargs)
|
||||
else:
|
||||
return api_basic_auth(f)(*args, **kwargs)
|
||||
|
||||
return decorated_function
|
||||
|
@ -1,44 +1,32 @@
|
||||
import os
|
||||
import urllib.parse
|
||||
|
||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
### BASIC APP CONFIG
|
||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||
BIND_ADDRESS = '0.0.0.0'
|
||||
PORT = 9191
|
||||
HSTS_ENABLED = False
|
||||
SERVER_EXTERNAL_SSL = True
|
||||
|
||||
SESSION_TYPE = 'sqlalchemy'
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
|
||||
#CAPTCHA Config
|
||||
CAPTCHA_ENABLE = True
|
||||
CAPTCHA_LENGTH = 6
|
||||
CAPTCHA_WIDTH = 160
|
||||
CAPTCHA_HEIGHT = 60
|
||||
CAPTCHA_LENGTH = 6
|
||||
CAPTCHA_SESSION_KEY = 'captcha_image'
|
||||
|
||||
### DATABASE CONFIG
|
||||
SQLA_DB_USER = 'pda'
|
||||
SQLA_DB_PASSWORD = 'changeme'
|
||||
SQLA_DB_HOST = '127.0.0.1'
|
||||
SQLA_DB_NAME = 'pda'
|
||||
CAPTCHA_WIDTH = 160
|
||||
CSRF_COOKIE_HTTPONLY = True
|
||||
HSTS_ENABLED = False
|
||||
PORT = 9191
|
||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||
SAML_ASSERTION_ENCRYPTED = True
|
||||
SAML_ENABLED = False
|
||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||
SERVER_EXTERNAL_SSL = os.getenv('SERVER_EXTERNAL_SSL', True)
|
||||
SESSION_COOKIE_SAMESITE = 'Lax'
|
||||
SESSION_TYPE = 'sqlalchemy'
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
### DATABASE - MySQL
|
||||
# SQLA_DB_USER = 'pda'
|
||||
# SQLA_DB_PASSWORD = 'changeme'
|
||||
# SQLA_DB_HOST = '127.0.0.1'
|
||||
# SQLA_DB_NAME = 'pda'
|
||||
# SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
|
||||
# urllib.parse.quote_plus(SQLA_DB_USER),
|
||||
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
|
||||
# SQLA_DB_HOST,
|
||||
# SQLA_DB_NAME
|
||||
# )
|
||||
|
||||
### DATABASE - SQLite
|
||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||
|
||||
# SAML Authnetication
|
||||
SAML_ENABLED = False
|
||||
SAML_ASSERTION_ENCRYPTED = True
|
||||
|
638
powerdnsadmin/lib/settings.py
Normal file
638
powerdnsadmin/lib/settings.py
Normal file
@ -0,0 +1,638 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
basedir = os.path.abspath(Path(os.path.dirname(__file__)).parent)
|
||||
|
||||
class AppSettings(object):
|
||||
|
||||
defaults = {
|
||||
# Flask Settings
|
||||
'bind_address': '0.0.0.0',
|
||||
'csrf_cookie_secure': False,
|
||||
'log_level': 'WARNING',
|
||||
'port': 9191,
|
||||
'salt': '$2b$12$yLUMTIfl21FKJQpTkRQXCu',
|
||||
'secret_key': 'e951e5a1f4b94151b360f47edf596dd2',
|
||||
'session_cookie_secure': False,
|
||||
'session_type': 'sqlalchemy',
|
||||
'sqlalchemy_track_modifications': True,
|
||||
'sqlalchemy_database_uri': 'sqlite:///' + os.path.join(basedir, 'pdns.db'),
|
||||
'sqlalchemy_engine_options': {},
|
||||
|
||||
# General Settings
|
||||
'captcha_enable': True,
|
||||
'captcha_height': 60,
|
||||
'captcha_length': 6,
|
||||
'captcha_session_key': 'captcha_image',
|
||||
'captcha_width': 160,
|
||||
'mail_server': 'localhost',
|
||||
'mail_port': 25,
|
||||
'mail_debug': False,
|
||||
'mail_use_ssl': False,
|
||||
'mail_use_tls': False,
|
||||
'mail_username': '',
|
||||
'mail_password': '',
|
||||
'mail_default_sender': '',
|
||||
'remote_user_enabled': False,
|
||||
'remote_user_cookies': [],
|
||||
'remote_user_logout_url': '',
|
||||
'hsts_enabled': False,
|
||||
'server_external_ssl': True,
|
||||
'maintenance': False,
|
||||
'fullscreen_layout': True,
|
||||
'record_helper': True,
|
||||
'login_ldap_first': True,
|
||||
'default_record_table_size': 15,
|
||||
'default_domain_table_size': 10,
|
||||
'auto_ptr': False,
|
||||
'record_quick_edit': True,
|
||||
'pretty_ipv6_ptr': False,
|
||||
'dnssec_admins_only': False,
|
||||
'allow_user_create_domain': False,
|
||||
'allow_user_remove_domain': False,
|
||||
'allow_user_view_history': False,
|
||||
'custom_history_header': '',
|
||||
'delete_sso_accounts': False,
|
||||
'bg_domain_updates': False,
|
||||
'enable_api_rr_history': True,
|
||||
'preserve_history': False,
|
||||
'site_name': 'PowerDNS-Admin',
|
||||
'site_url': 'http://localhost:9191',
|
||||
'session_timeout': 10,
|
||||
'warn_session_timeout': True,
|
||||
'pdns_api_url': '',
|
||||
'pdns_api_key': '',
|
||||
'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,
|
||||
'pdns_admin_log_level': 'WARNING',
|
||||
|
||||
# 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 OAuth 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_auto_configure': True,
|
||||
'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 OAuth 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_auto_configure': False,
|
||||
'github_oauth_metadata_url': '',
|
||||
'github_oauth_token_url': 'https://github.com/login/oauth/access_token',
|
||||
'github_oauth_authorize_url': 'https://github.com/login/oauth/authorize',
|
||||
|
||||
# Azure OAuth 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_auto_configure': True,
|
||||
'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': 'displayName',
|
||||
'azure_group_accounts_name_re': '',
|
||||
'azure_group_accounts_description': 'description',
|
||||
'azure_group_accounts_description_re': '',
|
||||
|
||||
# OIDC OAuth Settings
|
||||
'oidc_oauth_enabled': False,
|
||||
'oidc_oauth_key': '',
|
||||
'oidc_oauth_secret': '',
|
||||
'oidc_oauth_scope': 'email',
|
||||
'oidc_oauth_api_url': '',
|
||||
'oidc_oauth_auto_configure': True,
|
||||
'oidc_oauth_metadata_url': '',
|
||||
'oidc_oauth_token_url': '',
|
||||
'oidc_oauth_authorize_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_account_name_property': '',
|
||||
'oidc_oauth_account_description_property': '',
|
||||
|
||||
# SAML Authentication Settings
|
||||
'saml_enabled': False,
|
||||
'saml_debug': False,
|
||||
'saml_path': os.path.join(basedir, 'saml'),
|
||||
'saml_metadata_url': None,
|
||||
'saml_metadata_cache_lifetime': 1,
|
||||
'saml_idp_sso_binding': 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect',
|
||||
'saml_idp_entity_id': None,
|
||||
'saml_nameid_format': None,
|
||||
'saml_attribute_account': None,
|
||||
'saml_attribute_email': 'email',
|
||||
'saml_attribute_givenname': 'givenname',
|
||||
'saml_attribute_surname': 'surname',
|
||||
'saml_attribute_name': None,
|
||||
'saml_attribute_username': None,
|
||||
'saml_attribute_admin': None,
|
||||
'saml_attribute_group': None,
|
||||
'saml_group_admin_name': None,
|
||||
'saml_group_operator_name': None,
|
||||
'saml_group_to_account_mapping': None,
|
||||
'saml_sp_entity_id': None,
|
||||
'saml_sp_contact_name': None,
|
||||
'saml_sp_contact_mail': None,
|
||||
'saml_sign_request': False,
|
||||
'saml_want_message_signed': True,
|
||||
'saml_logout': True,
|
||||
'saml_logout_url': None,
|
||||
'saml_assertion_encrypted': True,
|
||||
'saml_cert': None,
|
||||
'saml_key': None,
|
||||
|
||||
# Zone Record Settings
|
||||
'forward_records_allow_edit': {
|
||||
'A': True,
|
||||
'AAAA': True,
|
||||
'AFSDB': False,
|
||||
'ALIAS': False,
|
||||
'CAA': True,
|
||||
'CERT': False,
|
||||
'CDNSKEY': False,
|
||||
'CDS': False,
|
||||
'CNAME': True,
|
||||
'DNSKEY': False,
|
||||
'DNAME': False,
|
||||
'DS': False,
|
||||
'HINFO': False,
|
||||
'KEY': False,
|
||||
'LOC': True,
|
||||
'LUA': False,
|
||||
'MX': True,
|
||||
'NAPTR': False,
|
||||
'NS': True,
|
||||
'NSEC': False,
|
||||
'NSEC3': False,
|
||||
'NSEC3PARAM': False,
|
||||
'OPENPGPKEY': False,
|
||||
'PTR': True,
|
||||
'RP': False,
|
||||
'RRSIG': False,
|
||||
'SOA': False,
|
||||
'SPF': True,
|
||||
'SSHFP': False,
|
||||
'SRV': True,
|
||||
'TKEY': False,
|
||||
'TSIG': False,
|
||||
'TLSA': False,
|
||||
'SMIMEA': False,
|
||||
'TXT': True,
|
||||
'URI': False
|
||||
},
|
||||
'reverse_records_allow_edit': {
|
||||
'A': False,
|
||||
'AAAA': False,
|
||||
'AFSDB': False,
|
||||
'ALIAS': False,
|
||||
'CAA': False,
|
||||
'CERT': False,
|
||||
'CDNSKEY': False,
|
||||
'CDS': False,
|
||||
'CNAME': False,
|
||||
'DNSKEY': False,
|
||||
'DNAME': False,
|
||||
'DS': False,
|
||||
'HINFO': False,
|
||||
'KEY': False,
|
||||
'LOC': True,
|
||||
'LUA': False,
|
||||
'MX': False,
|
||||
'NAPTR': False,
|
||||
'NS': True,
|
||||
'NSEC': False,
|
||||
'NSEC3': False,
|
||||
'NSEC3PARAM': False,
|
||||
'OPENPGPKEY': False,
|
||||
'PTR': True,
|
||||
'RP': False,
|
||||
'RRSIG': False,
|
||||
'SOA': False,
|
||||
'SPF': False,
|
||||
'SSHFP': False,
|
||||
'SRV': False,
|
||||
'TKEY': False,
|
||||
'TSIG': False,
|
||||
'TLSA': False,
|
||||
'SMIMEA': False,
|
||||
'TXT': True,
|
||||
'URI': False
|
||||
},
|
||||
}
|
||||
|
||||
types = {
|
||||
# Flask Settings
|
||||
'bind_address': str,
|
||||
'csrf_cookie_secure': bool,
|
||||
'log_level': str,
|
||||
'port': int,
|
||||
'salt': str,
|
||||
'secret_key': str,
|
||||
'session_cookie_secure': bool,
|
||||
'session_type': str,
|
||||
'sqlalchemy_track_modifications': bool,
|
||||
'sqlalchemy_database_uri': str,
|
||||
'sqlalchemy_engine_options': dict,
|
||||
|
||||
# General Settings
|
||||
'captcha_enable': bool,
|
||||
'captcha_height': int,
|
||||
'captcha_length': int,
|
||||
'captcha_session_key': str,
|
||||
'captcha_width': int,
|
||||
'mail_server': str,
|
||||
'mail_port': int,
|
||||
'mail_debug': bool,
|
||||
'mail_use_ssl': bool,
|
||||
'mail_use_tls': bool,
|
||||
'mail_username': str,
|
||||
'mail_password': str,
|
||||
'mail_default_sender': str,
|
||||
'hsts_enabled': bool,
|
||||
'remote_user_enabled': bool,
|
||||
'remote_user_cookies': list,
|
||||
'remote_user_logout_url': str,
|
||||
'maintenance': bool,
|
||||
'fullscreen_layout': bool,
|
||||
'record_helper': bool,
|
||||
'login_ldap_first': bool,
|
||||
'default_record_table_size': int,
|
||||
'default_domain_table_size': int,
|
||||
'auto_ptr': bool,
|
||||
'record_quick_edit': bool,
|
||||
'pretty_ipv6_ptr': bool,
|
||||
'dnssec_admins_only': bool,
|
||||
'allow_user_create_domain': bool,
|
||||
'allow_user_remove_domain': bool,
|
||||
'allow_user_view_history': bool,
|
||||
'custom_history_header': str,
|
||||
'delete_sso_accounts': bool,
|
||||
'bg_domain_updates': bool,
|
||||
'enable_api_rr_history': bool,
|
||||
'preserve_history': bool,
|
||||
'site_name': str,
|
||||
'site_url': str,
|
||||
'session_timeout': int,
|
||||
'warn_session_timeout': bool,
|
||||
'pdns_api_url': str,
|
||||
'pdns_api_key': str,
|
||||
'pdns_api_timeout': int,
|
||||
'pdns_version': str,
|
||||
'verify_ssl_connections': bool,
|
||||
'verify_user_email': bool,
|
||||
'enforce_api_ttl': bool,
|
||||
'ttl_options': str,
|
||||
'otp_field_enabled': bool,
|
||||
'custom_css': str,
|
||||
'otp_force': bool,
|
||||
'max_history_records': int,
|
||||
'deny_domain_override': bool,
|
||||
'account_name_extra_chars': bool,
|
||||
'gravatar_enabled': bool,
|
||||
'pdns_admin_log_level': str,
|
||||
'forward_records_allow_edit': dict,
|
||||
'reverse_records_allow_edit': dict,
|
||||
|
||||
# Local Authentication Settings
|
||||
'local_db_enabled': bool,
|
||||
'signup_enabled': bool,
|
||||
'pwd_enforce_characters': bool,
|
||||
'pwd_min_len': int,
|
||||
'pwd_min_lowercase': int,
|
||||
'pwd_min_uppercase': int,
|
||||
'pwd_min_digits': int,
|
||||
'pwd_min_special': int,
|
||||
'pwd_enforce_complexity': bool,
|
||||
'pwd_min_complexity': int,
|
||||
|
||||
# LDAP Authentication Settings
|
||||
'ldap_enabled': bool,
|
||||
'ldap_type': str,
|
||||
'ldap_uri': str,
|
||||
'ldap_base_dn': str,
|
||||
'ldap_admin_username': str,
|
||||
'ldap_admin_password': str,
|
||||
'ldap_domain': str,
|
||||
'ldap_filter_basic': str,
|
||||
'ldap_filter_username': str,
|
||||
'ldap_filter_group': str,
|
||||
'ldap_filter_groupname': str,
|
||||
'ldap_sg_enabled': bool,
|
||||
'ldap_admin_group': str,
|
||||
'ldap_operator_group': str,
|
||||
'ldap_user_group': str,
|
||||
'autoprovisioning': bool,
|
||||
'autoprovisioning_attribute': str,
|
||||
'urn_value': str,
|
||||
'purge': bool,
|
||||
|
||||
# Google OAuth Settings
|
||||
'google_oauth_enabled': bool,
|
||||
'google_oauth_client_id': str,
|
||||
'google_oauth_client_secret': str,
|
||||
'google_oauth_scope': str,
|
||||
'google_base_url': str,
|
||||
'google_oauth_auto_configure': bool,
|
||||
'google_oauth_metadata_url': str,
|
||||
'google_token_url': str,
|
||||
'google_authorize_url': str,
|
||||
|
||||
# GitHub OAuth Settings
|
||||
'github_oauth_enabled': bool,
|
||||
'github_oauth_key': str,
|
||||
'github_oauth_secret': str,
|
||||
'github_oauth_scope': str,
|
||||
'github_oauth_api_url': str,
|
||||
'github_oauth_auto_configure': bool,
|
||||
'github_oauth_metadata_url': str,
|
||||
'github_oauth_token_url': str,
|
||||
'github_oauth_authorize_url': str,
|
||||
|
||||
# Azure OAuth Settings
|
||||
'azure_oauth_enabled': bool,
|
||||
'azure_oauth_key': str,
|
||||
'azure_oauth_secret': str,
|
||||
'azure_oauth_scope': str,
|
||||
'azure_oauth_api_url': str,
|
||||
'azure_oauth_auto_configure': bool,
|
||||
'azure_oauth_metadata_url': str,
|
||||
'azure_oauth_token_url': str,
|
||||
'azure_oauth_authorize_url': str,
|
||||
'azure_sg_enabled': bool,
|
||||
'azure_admin_group': str,
|
||||
'azure_operator_group': str,
|
||||
'azure_user_group': str,
|
||||
'azure_group_accounts_enabled': bool,
|
||||
'azure_group_accounts_name': str,
|
||||
'azure_group_accounts_name_re': str,
|
||||
'azure_group_accounts_description': str,
|
||||
'azure_group_accounts_description_re': str,
|
||||
|
||||
# OIDC OAuth Settings
|
||||
'oidc_oauth_enabled': bool,
|
||||
'oidc_oauth_key': str,
|
||||
'oidc_oauth_secret': str,
|
||||
'oidc_oauth_scope': str,
|
||||
'oidc_oauth_api_url': str,
|
||||
'oidc_oauth_auto_configure': bool,
|
||||
'oidc_oauth_metadata_url': str,
|
||||
'oidc_oauth_token_url': str,
|
||||
'oidc_oauth_authorize_url': str,
|
||||
'oidc_oauth_logout_url': str,
|
||||
'oidc_oauth_username': str,
|
||||
'oidc_oauth_email': str,
|
||||
'oidc_oauth_firstname': str,
|
||||
'oidc_oauth_last_name': str,
|
||||
'oidc_oauth_account_name_property': str,
|
||||
'oidc_oauth_account_description_property': str,
|
||||
|
||||
# SAML Authentication Settings
|
||||
'saml_enabled': bool,
|
||||
'saml_debug': bool,
|
||||
'saml_path': str,
|
||||
'saml_metadata_url': str,
|
||||
'saml_metadata_cache_lifetime': int,
|
||||
'saml_idp_sso_binding': str,
|
||||
'saml_idp_entity_id': str,
|
||||
'saml_nameid_format': str,
|
||||
'saml_attribute_account': str,
|
||||
'saml_attribute_email': str,
|
||||
'saml_attribute_givenname': str,
|
||||
'saml_attribute_surname': str,
|
||||
'saml_attribute_name': str,
|
||||
'saml_attribute_username': str,
|
||||
'saml_attribute_admin': str,
|
||||
'saml_attribute_group': str,
|
||||
'saml_group_admin_name': str,
|
||||
'saml_group_operator_name': str,
|
||||
'saml_group_to_account_mapping': str,
|
||||
'saml_sp_entity_id': str,
|
||||
'saml_sp_contact_name': str,
|
||||
'saml_sp_contact_mail': str,
|
||||
'saml_sign_request': bool,
|
||||
'saml_want_message_signed': bool,
|
||||
'saml_logout': bool,
|
||||
'saml_logout_url': str,
|
||||
'saml_assertion_encrypted': bool,
|
||||
'saml_cert': str,
|
||||
'saml_key': str,
|
||||
}
|
||||
|
||||
groups = {
|
||||
'authentication': [
|
||||
# Local Authentication Settings
|
||||
'local_db_enabled',
|
||||
'signup_enabled',
|
||||
'pwd_enforce_characters',
|
||||
'pwd_min_len',
|
||||
'pwd_min_lowercase',
|
||||
'pwd_min_uppercase',
|
||||
'pwd_min_digits',
|
||||
'pwd_min_special',
|
||||
'pwd_enforce_complexity',
|
||||
'pwd_min_complexity',
|
||||
|
||||
# LDAP Authentication Settings
|
||||
'ldap_enabled',
|
||||
'ldap_type',
|
||||
'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',
|
||||
'ldap_admin_group',
|
||||
'ldap_operator_group',
|
||||
'ldap_user_group',
|
||||
'autoprovisioning',
|
||||
'autoprovisioning_attribute',
|
||||
'urn_value',
|
||||
'purge',
|
||||
|
||||
# Google OAuth Settings
|
||||
'google_oauth_enabled',
|
||||
'google_oauth_client_id',
|
||||
'google_oauth_client_secret',
|
||||
'google_oauth_scope',
|
||||
'google_base_url',
|
||||
'google_oauth_auto_configure',
|
||||
'google_oauth_metadata_url',
|
||||
'google_token_url',
|
||||
'google_authorize_url',
|
||||
|
||||
# GitHub OAuth Settings
|
||||
'github_oauth_enabled',
|
||||
'github_oauth_key',
|
||||
'github_oauth_secret',
|
||||
'github_oauth_scope',
|
||||
'github_oauth_api_url',
|
||||
'github_oauth_auto_configure',
|
||||
'github_oauth_metadata_url',
|
||||
'github_oauth_token_url',
|
||||
'github_oauth_authorize_url',
|
||||
|
||||
# Azure OAuth Settings
|
||||
'azure_oauth_enabled',
|
||||
'azure_oauth_key',
|
||||
'azure_oauth_secret',
|
||||
'azure_oauth_scope',
|
||||
'azure_oauth_api_url',
|
||||
'azure_oauth_auto_configure',
|
||||
'azure_oauth_metadata_url',
|
||||
'azure_oauth_token_url',
|
||||
'azure_oauth_authorize_url',
|
||||
'azure_sg_enabled',
|
||||
'azure_admin_group',
|
||||
'azure_operator_group',
|
||||
'azure_user_group',
|
||||
'azure_group_accounts_enabled',
|
||||
'azure_group_accounts_name',
|
||||
'azure_group_accounts_name_re',
|
||||
'azure_group_accounts_description',
|
||||
'azure_group_accounts_description_re',
|
||||
|
||||
# OIDC OAuth Settings
|
||||
'oidc_oauth_enabled',
|
||||
'oidc_oauth_key',
|
||||
'oidc_oauth_secret',
|
||||
'oidc_oauth_scope',
|
||||
'oidc_oauth_api_url',
|
||||
'oidc_oauth_auto_configure',
|
||||
'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',
|
||||
]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def convert_type(name, value):
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
if name in AppSettings.types:
|
||||
var_type = AppSettings.types[name]
|
||||
|
||||
# Handle boolean values
|
||||
if var_type == bool and isinstance(value, str):
|
||||
if value.lower() in ['True', 'true', '1'] or value is True:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Handle float values
|
||||
if var_type == float:
|
||||
return float(value)
|
||||
|
||||
# Handle integer values
|
||||
if var_type == int:
|
||||
return int(value)
|
||||
|
||||
if (var_type == dict or var_type == list) and isinstance(value, str) and len(value) > 0:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except JSONDecodeError as e:
|
||||
# Provide backwards compatibility for legacy non-JSON format
|
||||
value = value.replace("'", '"').replace('True', 'true').replace('False', 'false')
|
||||
try:
|
||||
return json.loads(value)
|
||||
except JSONDecodeError as e:
|
||||
raise ValueError('Cannot parse json {} for variable {}'.format(value, name))
|
||||
|
||||
if var_type == str:
|
||||
return str(value)
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def load_environment(app):
|
||||
""" Load app settings from environment variables when defined. """
|
||||
import os
|
||||
|
||||
for var_name, default_value in AppSettings.defaults.items():
|
||||
env_name = var_name.upper()
|
||||
current_value = None
|
||||
|
||||
if env_name + '_FILE' in os.environ:
|
||||
if env_name in os.environ:
|
||||
raise AttributeError(
|
||||
"Both {} and {} are set but are exclusive.".format(
|
||||
env_name, env_name + '_FILE'))
|
||||
with open(os.environ[env_name + '_FILE']) as f:
|
||||
current_value = f.read()
|
||||
f.close()
|
||||
|
||||
elif env_name in os.environ:
|
||||
current_value = os.environ[env_name]
|
||||
|
||||
if current_value is not None:
|
||||
app.config[env_name] = AppSettings.convert_type(var_name, current_value)
|
@ -643,6 +643,8 @@ class Domain(db.Model):
|
||||
"""
|
||||
Update records from Master DNS server
|
||||
"""
|
||||
import urllib.parse
|
||||
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||
@ -650,7 +652,7 @@ class Domain(db.Model):
|
||||
r = utils.fetch_json(urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/axfr-retrieve'.format(
|
||||
domain.name)),
|
||||
urllib.parse.quote_plus(domain.name))),
|
||||
headers=headers,
|
||||
timeout=int(
|
||||
Setting().get('pdns_api_timeout')),
|
||||
@ -673,6 +675,8 @@ class Domain(db.Model):
|
||||
"""
|
||||
Get zone DNSSEC information
|
||||
"""
|
||||
import urllib.parse
|
||||
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||
@ -681,7 +685,7 @@ class Domain(db.Model):
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||
domain.name)),
|
||||
urllib.parse.quote_plus(domain.name))),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='GET',
|
||||
@ -709,6 +713,8 @@ class Domain(db.Model):
|
||||
"""
|
||||
Enable zone DNSSEC
|
||||
"""
|
||||
import urllib.parse
|
||||
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
||||
@ -718,7 +724,9 @@ class Domain(db.Model):
|
||||
jdata = utils.fetch_json(
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}'.format(domain.name)),
|
||||
'/servers/localhost/zones/{0}'.format(
|
||||
urllib.parse.quote_plus(domain.name)
|
||||
)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='PUT',
|
||||
@ -738,7 +746,8 @@ class Domain(db.Model):
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||
domain.name)),
|
||||
urllib.parse.quote_plus(domain.name)
|
||||
)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='POST',
|
||||
@ -775,6 +784,8 @@ class Domain(db.Model):
|
||||
"""
|
||||
Remove keys DNSSEC
|
||||
"""
|
||||
import urllib.parse
|
||||
|
||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||
if domain:
|
||||
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
||||
@ -784,7 +795,7 @@ class Domain(db.Model):
|
||||
urljoin(
|
||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
|
||||
domain.name, key_id)),
|
||||
urllib.parse.quote_plus(domain.name), key_id)),
|
||||
headers=headers,
|
||||
timeout=int(Setting().get('pdns_api_timeout')),
|
||||
method='DELETE',
|
||||
|
@ -251,6 +251,7 @@ class Record(object):
|
||||
Returns:
|
||||
new_rrsets(list): List of rrsets to be added
|
||||
del_rrsets(list): List of rrsets to be deleted
|
||||
zone_has_comments(bool): True if the zone currently contains persistent comments
|
||||
"""
|
||||
# Create submitted rrsets from submitted records
|
||||
submitted_rrsets = self.build_rrsets(domain_name, submitted_records)
|
||||
@ -266,9 +267,11 @@ class Record(object):
|
||||
# PDNS API always return the comments with modified_at
|
||||
# info, we have to remove it to be able to do the dict
|
||||
# comparison between current and submitted rrsets
|
||||
zone_has_comments = False
|
||||
for r in current_rrsets:
|
||||
for comment in r['comments']:
|
||||
if 'modified_at' in comment:
|
||||
zone_has_comments = True
|
||||
del comment['modified_at']
|
||||
|
||||
# List of rrsets to be added
|
||||
@ -290,7 +293,7 @@ class Record(object):
|
||||
current_app.logger.debug("new_rrsets: \n{}".format(utils.pretty_json(new_rrsets)))
|
||||
current_app.logger.debug("del_rrsets: \n{}".format(utils.pretty_json(del_rrsets)))
|
||||
|
||||
return new_rrsets, del_rrsets
|
||||
return new_rrsets, del_rrsets, zone_has_comments
|
||||
|
||||
def apply_rrsets(self, domain_name, rrsets):
|
||||
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
||||
@ -304,7 +307,7 @@ class Record(object):
|
||||
return jdata
|
||||
|
||||
@staticmethod
|
||||
def to_api_payload(new_rrsets, del_rrsets):
|
||||
def to_api_payload(new_rrsets, del_rrsets, comments_supported):
|
||||
"""Turn the given changes into a single api payload."""
|
||||
|
||||
def replace_for_api(rrset):
|
||||
@ -312,9 +315,13 @@ class Record(object):
|
||||
if not rrset or rrset.get('changetype', None) != 'REPLACE':
|
||||
return rrset
|
||||
replace_copy = dict(rrset)
|
||||
# For compatibility with some backends: Remove comments from rrset if all are blank
|
||||
if not any((bool(c.get('content', None)) for c in replace_copy.get('comments', []))):
|
||||
replace_copy.pop('comments', None)
|
||||
has_nonempty_comments = any(bool(c.get('content', None)) for c in replace_copy.get('comments', []))
|
||||
if not has_nonempty_comments:
|
||||
if comments_supported:
|
||||
replace_copy['comments'] = []
|
||||
else:
|
||||
# For backends that don't support comments: Remove the attribute altogether
|
||||
replace_copy.pop('comments', None)
|
||||
return replace_copy
|
||||
|
||||
def rrset_in(needle, haystack):
|
||||
@ -351,11 +358,11 @@ class Record(object):
|
||||
"submitted_records: {}".format(submitted_records))
|
||||
|
||||
# Get the list of rrsets to be added and deleted
|
||||
new_rrsets, del_rrsets = self.compare(domain_name, submitted_records)
|
||||
new_rrsets, del_rrsets, zone_has_comments = self.compare(domain_name, submitted_records)
|
||||
|
||||
# The history logic still needs *all* the deletes with full data to display a useful diff.
|
||||
# So create a "minified" copy for the api call, and return the original data back up
|
||||
api_payload = self.to_api_payload(new_rrsets['rrsets'], del_rrsets['rrsets'])
|
||||
api_payload = self.to_api_payload(new_rrsets['rrsets'], del_rrsets['rrsets'], zone_has_comments)
|
||||
current_app.logger.debug(f"api payload: \n{utils.pretty_json(api_payload)}")
|
||||
|
||||
# Submit the changes to PDNS API
|
||||
|
39
powerdnsadmin/models/sessions.py
Normal file
39
powerdnsadmin/models/sessions.py
Normal file
@ -0,0 +1,39 @@
|
||||
from flask import current_app, session
|
||||
from flask_login import current_user
|
||||
from .base import db
|
||||
|
||||
|
||||
class Sessions(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
session_id = db.Column(db.String(255), index=True, unique=True)
|
||||
data = db.Column(db.BLOB)
|
||||
expiry = db.Column(db.DateTime)
|
||||
|
||||
def __init__(self,
|
||||
id=None,
|
||||
session_id=None,
|
||||
data=None,
|
||||
expiry=None):
|
||||
self.id = id
|
||||
self.session_id = session_id
|
||||
self.data = data
|
||||
self.expiry = expiry
|
||||
|
||||
def __repr__(self):
|
||||
return '<Sessions {0}>'.format(self.id)
|
||||
|
||||
@staticmethod
|
||||
def clean_up_expired_sessions():
|
||||
"""Clean up expired sessions in the database"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import or_
|
||||
from sqlalchemy.exc import SQLAlchemyError
|
||||
|
||||
try:
|
||||
db.session.query(Sessions).filter(or_(Sessions.expiry < datetime.now(), Sessions.expiry is None)).delete()
|
||||
db.session.commit()
|
||||
except SQLAlchemyError as e:
|
||||
db.session.rollback()
|
||||
current_app.logger.error(e)
|
||||
return False
|
||||
return True
|
@ -1,12 +1,10 @@
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
import pytimeparse
|
||||
from ast import literal_eval
|
||||
from distutils.util import strtobool
|
||||
from flask import current_app
|
||||
|
||||
from .base import db
|
||||
from powerdnsadmin.lib.settings import AppSettings
|
||||
|
||||
|
||||
class Setting(db.Model):
|
||||
@ -14,437 +12,8 @@ class Setting(db.Model):
|
||||
name = db.Column(db.String(64), unique=True, index=True)
|
||||
value = db.Column(db.Text())
|
||||
|
||||
types = {
|
||||
'maintenance': bool,
|
||||
'fullscreen_layout': bool,
|
||||
'record_helper': bool,
|
||||
'login_ldap_first': bool,
|
||||
'default_record_table_size': int,
|
||||
'default_domain_table_size': int,
|
||||
'auto_ptr': bool,
|
||||
'record_quick_edit': bool,
|
||||
'pretty_ipv6_ptr': bool,
|
||||
'dnssec_admins_only': bool,
|
||||
'allow_user_create_domain': bool,
|
||||
'allow_user_remove_domain': bool,
|
||||
'allow_user_view_history': bool,
|
||||
'custom_history_header': str,
|
||||
'delete_sso_accounts': bool,
|
||||
'bg_domain_updates': bool,
|
||||
'enable_api_rr_history': bool,
|
||||
'preserve_history': bool,
|
||||
'site_name': str,
|
||||
'site_url': str,
|
||||
'session_timeout': int,
|
||||
'warn_session_timeout': bool,
|
||||
'pdns_api_url': str,
|
||||
'pdns_api_key': str,
|
||||
'pdns_api_timeout': int,
|
||||
'pdns_version': str,
|
||||
'verify_ssl_connections': bool,
|
||||
'verify_user_email': bool,
|
||||
'enforce_api_ttl': bool,
|
||||
'ttl_options': str,
|
||||
'otp_field_enabled': bool,
|
||||
'custom_css': str,
|
||||
'otp_force': bool,
|
||||
'max_history_records': int,
|
||||
'deny_domain_override': bool,
|
||||
'account_name_extra_chars': bool,
|
||||
'gravatar_enabled': bool,
|
||||
'forward_records_allow_edit': dict,
|
||||
'reverse_records_allow_edit': dict,
|
||||
'local_db_enabled': bool,
|
||||
'signup_enabled': bool,
|
||||
'pwd_enforce_characters': bool,
|
||||
'pwd_min_len': int,
|
||||
'pwd_min_lowercase': int,
|
||||
'pwd_min_uppercase': int,
|
||||
'pwd_min_digits': int,
|
||||
'pwd_min_special': int,
|
||||
'pwd_enforce_complexity': bool,
|
||||
'pwd_min_complexity': int,
|
||||
'ldap_enabled': bool,
|
||||
'ldap_type': str,
|
||||
'ldap_uri': str,
|
||||
'ldap_base_dn': str,
|
||||
'ldap_admin_username': str,
|
||||
'ldap_admin_password': str,
|
||||
'ldap_domain': str,
|
||||
'ldap_filter_basic': str,
|
||||
'ldap_filter_username': str,
|
||||
'ldap_filter_group': str,
|
||||
'ldap_filter_groupname': str,
|
||||
'ldap_sg_enabled': bool,
|
||||
'ldap_admin_group': str,
|
||||
'ldap_operator_group': str,
|
||||
'ldap_user_group': str,
|
||||
'autoprovisioning': bool,
|
||||
'autoprovisioning_attribute': str,
|
||||
'urn_value': str,
|
||||
'purge': bool,
|
||||
'google_oauth_enabled': bool,
|
||||
'google_oauth_client_id': str,
|
||||
'google_oauth_client_secret': str,
|
||||
'google_oauth_scope': str,
|
||||
'google_base_url': str,
|
||||
'google_oauth_auto_configure': bool,
|
||||
'google_oauth_metadata_url': str,
|
||||
'google_token_url': str,
|
||||
'google_authorize_url': str,
|
||||
'github_oauth_enabled': bool,
|
||||
'github_oauth_key': str,
|
||||
'github_oauth_secret': str,
|
||||
'github_oauth_scope': str,
|
||||
'github_oauth_api_url': str,
|
||||
'github_oauth_auto_configure': bool,
|
||||
'github_oauth_metadata_url': str,
|
||||
'github_oauth_token_url': str,
|
||||
'github_oauth_authorize_url': str,
|
||||
'azure_oauth_enabled': bool,
|
||||
'azure_oauth_key': str,
|
||||
'azure_oauth_secret': str,
|
||||
'azure_oauth_scope': str,
|
||||
'azure_oauth_api_url': str,
|
||||
'azure_oauth_auto_configure': bool,
|
||||
'azure_oauth_metadata_url': str,
|
||||
'azure_oauth_token_url': str,
|
||||
'azure_oauth_authorize_url': str,
|
||||
'azure_sg_enabled': bool,
|
||||
'azure_admin_group': str,
|
||||
'azure_operator_group': str,
|
||||
'azure_user_group': str,
|
||||
'azure_group_accounts_enabled': bool,
|
||||
'azure_group_accounts_name': str,
|
||||
'azure_group_accounts_name_re': str,
|
||||
'azure_group_accounts_description': str,
|
||||
'azure_group_accounts_description_re': str,
|
||||
'oidc_oauth_enabled': bool,
|
||||
'oidc_oauth_key': str,
|
||||
'oidc_oauth_secret': str,
|
||||
'oidc_oauth_scope': str,
|
||||
'oidc_oauth_api_url': str,
|
||||
'oidc_oauth_auto_configure': bool,
|
||||
'oidc_oauth_metadata_url': str,
|
||||
'oidc_oauth_token_url': str,
|
||||
'oidc_oauth_authorize_url': str,
|
||||
'oidc_oauth_logout_url': str,
|
||||
'oidc_oauth_username': str,
|
||||
'oidc_oauth_email': str,
|
||||
'oidc_oauth_firstname': str,
|
||||
'oidc_oauth_last_name': str,
|
||||
'oidc_oauth_account_name_property': str,
|
||||
'oidc_oauth_account_description_property': str,
|
||||
}
|
||||
|
||||
defaults = {
|
||||
# General Settings
|
||||
'maintenance': False,
|
||||
'fullscreen_layout': True,
|
||||
'record_helper': True,
|
||||
'login_ldap_first': True,
|
||||
'default_record_table_size': 15,
|
||||
'default_domain_table_size': 10,
|
||||
'auto_ptr': False,
|
||||
'record_quick_edit': True,
|
||||
'pretty_ipv6_ptr': False,
|
||||
'dnssec_admins_only': False,
|
||||
'allow_user_create_domain': False,
|
||||
'allow_user_remove_domain': False,
|
||||
'allow_user_view_history': False,
|
||||
'custom_history_header': '',
|
||||
'delete_sso_accounts': False,
|
||||
'bg_domain_updates': False,
|
||||
'enable_api_rr_history': True,
|
||||
'preserve_history': False,
|
||||
'site_name': 'PowerDNS-Admin',
|
||||
'site_url': 'http://localhost:9191',
|
||||
'session_timeout': 10,
|
||||
'warn_session_timeout': True,
|
||||
'pdns_api_url': '',
|
||||
'pdns_api_key': '',
|
||||
'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,
|
||||
'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': 'openid email profile',
|
||||
'google_base_url': 'https://www.googleapis.com/oauth2/v3/',
|
||||
'google_oauth_auto_configure': True,
|
||||
'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_auto_configure': False,
|
||||
'github_oauth_metadata_url': '',
|
||||
'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_auto_configure': True,
|
||||
'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': 'displayName',
|
||||
'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_auto_configure': True,
|
||||
'oidc_oauth_metadata_url': '',
|
||||
'oidc_oauth_token_url': '',
|
||||
'oidc_oauth_authorize_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_account_name_property': '',
|
||||
'oidc_oauth_account_description_property': '',
|
||||
|
||||
# Zone Record Settings
|
||||
'forward_records_allow_edit': {
|
||||
'A': True,
|
||||
'AAAA': True,
|
||||
'AFSDB': False,
|
||||
'ALIAS': False,
|
||||
'CAA': True,
|
||||
'CERT': False,
|
||||
'CDNSKEY': False,
|
||||
'CDS': False,
|
||||
'CNAME': True,
|
||||
'DNSKEY': False,
|
||||
'DNAME': False,
|
||||
'DS': False,
|
||||
'HINFO': False,
|
||||
'KEY': False,
|
||||
'LOC': True,
|
||||
'LUA': False,
|
||||
'MX': True,
|
||||
'NAPTR': False,
|
||||
'NS': True,
|
||||
'NSEC': False,
|
||||
'NSEC3': False,
|
||||
'NSEC3PARAM': False,
|
||||
'OPENPGPKEY': False,
|
||||
'PTR': True,
|
||||
'RP': False,
|
||||
'RRSIG': False,
|
||||
'SOA': False,
|
||||
'SPF': True,
|
||||
'SSHFP': False,
|
||||
'SRV': True,
|
||||
'TKEY': False,
|
||||
'TSIG': False,
|
||||
'TLSA': False,
|
||||
'SMIMEA': False,
|
||||
'TXT': True,
|
||||
'URI': False
|
||||
},
|
||||
'reverse_records_allow_edit': {
|
||||
'A': False,
|
||||
'AAAA': False,
|
||||
'AFSDB': False,
|
||||
'ALIAS': False,
|
||||
'CAA': False,
|
||||
'CERT': False,
|
||||
'CDNSKEY': False,
|
||||
'CDS': False,
|
||||
'CNAME': False,
|
||||
'DNSKEY': False,
|
||||
'DNAME': False,
|
||||
'DS': False,
|
||||
'HINFO': False,
|
||||
'KEY': False,
|
||||
'LOC': True,
|
||||
'LUA': False,
|
||||
'MX': False,
|
||||
'NAPTR': False,
|
||||
'NS': True,
|
||||
'NSEC': False,
|
||||
'NSEC3': False,
|
||||
'NSEC3PARAM': False,
|
||||
'OPENPGPKEY': False,
|
||||
'PTR': True,
|
||||
'RP': False,
|
||||
'RRSIG': False,
|
||||
'SOA': False,
|
||||
'SPF': False,
|
||||
'SSHFP': False,
|
||||
'SRV': False,
|
||||
'TKEY': False,
|
||||
'TSIG': False,
|
||||
'TLSA': False,
|
||||
'SMIMEA': False,
|
||||
'TXT': True,
|
||||
'URI': False
|
||||
},
|
||||
}
|
||||
|
||||
groups = {
|
||||
'authentication': [
|
||||
# Local Authentication Settings
|
||||
'local_db_enabled',
|
||||
'signup_enabled',
|
||||
'pwd_enforce_characters',
|
||||
'pwd_min_len',
|
||||
'pwd_min_lowercase',
|
||||
'pwd_min_uppercase',
|
||||
'pwd_min_digits',
|
||||
'pwd_min_special',
|
||||
'pwd_enforce_complexity',
|
||||
'pwd_min_complexity',
|
||||
|
||||
# LDAP Authentication Settings
|
||||
'ldap_enabled',
|
||||
'ldap_type',
|
||||
'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',
|
||||
'ldap_admin_group',
|
||||
'ldap_operator_group',
|
||||
'ldap_user_group',
|
||||
'autoprovisioning',
|
||||
'autoprovisioning_attribute',
|
||||
'urn_value',
|
||||
'purge',
|
||||
|
||||
# Google OAuth2 Settings
|
||||
'google_oauth_enabled',
|
||||
'google_oauth_client_id',
|
||||
'google_oauth_client_secret',
|
||||
'google_oauth_scope',
|
||||
'google_base_url',
|
||||
'google_oauth_auto_configure',
|
||||
'google_oauth_metadata_url',
|
||||
'google_token_url',
|
||||
'google_authorize_url',
|
||||
|
||||
# GitHub OAuth2 Settings
|
||||
'github_oauth_enabled',
|
||||
'github_oauth_key',
|
||||
'github_oauth_secret',
|
||||
'github_oauth_scope',
|
||||
'github_oauth_api_url',
|
||||
'github_oauth_auto_configure',
|
||||
'github_oauth_metadata_url',
|
||||
'github_oauth_token_url',
|
||||
'github_oauth_authorize_url',
|
||||
|
||||
# Azure OAuth2 Settings
|
||||
'azure_oauth_enabled',
|
||||
'azure_oauth_key',
|
||||
'azure_oauth_secret',
|
||||
'azure_oauth_scope',
|
||||
'azure_oauth_api_url',
|
||||
'azure_oauth_auto_configure',
|
||||
'azure_oauth_metadata_url',
|
||||
'azure_oauth_token_url',
|
||||
'azure_oauth_authorize_url',
|
||||
'azure_sg_enabled',
|
||||
'azure_admin_group',
|
||||
'azure_operator_group',
|
||||
'azure_user_group',
|
||||
'azure_group_accounts_enabled',
|
||||
'azure_group_accounts_name',
|
||||
'azure_group_accounts_name_re',
|
||||
'azure_group_accounts_description',
|
||||
'azure_group_accounts_description_re',
|
||||
|
||||
# OIDC OAuth2 Settings
|
||||
'oidc_oauth_enabled',
|
||||
'oidc_oauth_key',
|
||||
'oidc_oauth_secret',
|
||||
'oidc_oauth_scope',
|
||||
'oidc_oauth_api_url',
|
||||
'oidc_oauth_auto_configure',
|
||||
'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',
|
||||
]
|
||||
}
|
||||
ZONE_TYPE_FORWARD = 'forward'
|
||||
ZONE_TYPE_REVERSE = 'reverse'
|
||||
|
||||
def __init__(self, id=None, name=None, value=None):
|
||||
self.id = id
|
||||
@ -457,44 +26,12 @@ class Setting(db.Model):
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def convert_type(self, name, value):
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
if name in self.types:
|
||||
var_type = self.types[name]
|
||||
|
||||
# Handle boolean values
|
||||
if var_type == bool:
|
||||
if value == 'True' or value == 'true' or value == '1' or value is True:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
# Handle float values
|
||||
if var_type == float:
|
||||
return float(value)
|
||||
|
||||
# Handle integer values
|
||||
if var_type == int:
|
||||
return int(value)
|
||||
|
||||
if (var_type == dict or var_type == list) and isinstance(value, str) and len(value) > 0:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except JSONDecodeError as e:
|
||||
pass
|
||||
|
||||
if var_type == str:
|
||||
return str(value)
|
||||
|
||||
return value
|
||||
|
||||
def set_maintenance(self, mode):
|
||||
maintenance = Setting.query.filter(
|
||||
Setting.name == 'maintenance').first()
|
||||
|
||||
if maintenance is None:
|
||||
value = self.defaults['maintenance']
|
||||
value = AppSettings.defaults['maintenance']
|
||||
maintenance = Setting(name='maintenance', value=str(value))
|
||||
db.session.add(maintenance)
|
||||
|
||||
@ -516,7 +53,7 @@ class Setting(db.Model):
|
||||
current_setting = Setting.query.filter(Setting.name == setting).first()
|
||||
|
||||
if current_setting is None:
|
||||
value = self.defaults[setting]
|
||||
value = AppSettings.defaults[setting]
|
||||
current_setting = Setting(name=setting, value=str(value))
|
||||
db.session.add(current_setting)
|
||||
|
||||
@ -535,27 +72,30 @@ class Setting(db.Model):
|
||||
return False
|
||||
|
||||
def set(self, setting, value):
|
||||
import json
|
||||
current_setting = Setting.query.filter(Setting.name == setting).first()
|
||||
|
||||
if current_setting is None:
|
||||
current_setting = Setting(name=setting, value=None)
|
||||
db.session.add(current_setting)
|
||||
|
||||
value = str(self.convert_type(setting, value))
|
||||
value = AppSettings.convert_type(setting, value)
|
||||
|
||||
if isinstance(value, dict) or isinstance(value, list):
|
||||
value = json.dumps(value)
|
||||
|
||||
try:
|
||||
current_setting.value = value
|
||||
db.session.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
current_app.logger.error('Cannot edit setting {0}. DETAIL: {1}'.format(
|
||||
setting, e))
|
||||
current_app.logger.error('Cannot edit setting {0}. DETAIL: {1}'.format(setting, e))
|
||||
current_app.logger.debug(traceback.format_exec())
|
||||
db.session.rollback()
|
||||
return False
|
||||
|
||||
def get(self, setting):
|
||||
if setting in self.defaults:
|
||||
if setting in AppSettings.defaults:
|
||||
|
||||
if setting.upper() in current_app.config:
|
||||
result = current_app.config[setting.upper()]
|
||||
@ -566,51 +106,45 @@ class Setting(db.Model):
|
||||
if hasattr(result, 'value'):
|
||||
result = result.value
|
||||
|
||||
return self.convert_type(setting, result)
|
||||
return AppSettings.convert_type(setting, result)
|
||||
else:
|
||||
return self.defaults[setting]
|
||||
return AppSettings.defaults[setting]
|
||||
else:
|
||||
current_app.logger.error('Unknown setting queried: {0}'.format(setting))
|
||||
|
||||
def get_group(self, group):
|
||||
if not isinstance(group, list):
|
||||
group = self.groups[group]
|
||||
group = AppSettings.groups[group]
|
||||
|
||||
result = {}
|
||||
records = self.query.all()
|
||||
|
||||
for record in records:
|
||||
if record.name in group:
|
||||
result[record.name] = self.convert_type(record.name, record.value)
|
||||
for var_name, default_value in AppSettings.defaults.items():
|
||||
if var_name in group:
|
||||
result[var_name] = self.get(var_name)
|
||||
|
||||
return result
|
||||
|
||||
def get_records_allow_to_edit(self):
|
||||
return list(
|
||||
set(self.get_forward_records_allow_to_edit() +
|
||||
self.get_reverse_records_allow_to_edit()))
|
||||
set(self.get_supported_record_types(self.ZONE_TYPE_FORWARD) +
|
||||
self.get_supported_record_types(self.ZONE_TYPE_REVERSE)))
|
||||
|
||||
def get_forward_records_allow_to_edit(self):
|
||||
records = self.get('forward_records_allow_edit')
|
||||
f_records = literal_eval(records) if isinstance(records,
|
||||
str) else records
|
||||
r_name = [r for r in f_records if f_records[r]]
|
||||
# Sort alphabetically if python version is smaller than 3.6
|
||||
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
|
||||
and sys.version_info[1] < 6):
|
||||
r_name.sort()
|
||||
return r_name
|
||||
def get_supported_record_types(self, zone_type):
|
||||
setting_value = []
|
||||
|
||||
if zone_type == self.ZONE_TYPE_FORWARD:
|
||||
setting_value = self.get('forward_records_allow_edit')
|
||||
elif zone_type == self.ZONE_TYPE_REVERSE:
|
||||
setting_value = self.get('reverse_records_allow_edit')
|
||||
|
||||
records = literal_eval(setting_value) if isinstance(setting_value, str) else setting_value
|
||||
types = [r for r in records if records[r]]
|
||||
|
||||
def get_reverse_records_allow_to_edit(self):
|
||||
records = self.get('reverse_records_allow_edit')
|
||||
r_records = literal_eval(records) if isinstance(records,
|
||||
str) else records
|
||||
r_name = [r for r in r_records if r_records[r]]
|
||||
# Sort alphabetically if python version is smaller than 3.6
|
||||
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
|
||||
and sys.version_info[1] < 6):
|
||||
r_name.sort()
|
||||
return r_name
|
||||
if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 6):
|
||||
types.sort()
|
||||
|
||||
return types
|
||||
|
||||
def get_ttl_options(self):
|
||||
return [(pytimeparse.parse(ttl), ttl)
|
||||
|
@ -133,6 +133,16 @@ class User(db.Model):
|
||||
conn.protocol_version = ldap.VERSION3
|
||||
return conn
|
||||
|
||||
def escape_filter_chars(self, filter_str):
|
||||
"""
|
||||
Escape chars for ldap search
|
||||
"""
|
||||
escape_chars = ['\\', '*', '(', ')', '\x00']
|
||||
replace_chars = ['\\5c', '\\2a', '\\28', '\\29', '\\00']
|
||||
for escape_char in escape_chars:
|
||||
filter_str = filter_str.replace(escape_char, replace_chars[escape_chars.index(escape_char)])
|
||||
return filter_str
|
||||
|
||||
def ldap_search(self, searchFilter, baseDN, retrieveAttributes=None):
|
||||
searchScope = ldap.SCOPE_SUBTREE
|
||||
|
||||
@ -280,7 +290,7 @@ class User(db.Model):
|
||||
Operator=LDAP_OPERATOR_GROUP,
|
||||
User=LDAP_USER_GROUP,
|
||||
)
|
||||
user_dn = ldap_result[0][0][0]
|
||||
user_dn = self.escape_filter_chars(ldap_result[0][0][0])
|
||||
sf_groups = ""
|
||||
|
||||
for group in ldap_group_security_roles.values():
|
||||
@ -408,12 +418,12 @@ class User(db.Model):
|
||||
Create local user witch stores username / password in the DB
|
||||
"""
|
||||
# check if username existed
|
||||
user = User.query.filter(User.username == self.username).first()
|
||||
user = User.query.filter(str(User.username).lower() == self.username.lower()).first()
|
||||
if user:
|
||||
return {'status': False, 'msg': 'Username is already in use'}
|
||||
|
||||
# check if email existed
|
||||
user = User.query.filter(User.email == self.email).first()
|
||||
user = User.query.filter(str(User.email).lower() == self.email.lower()).first()
|
||||
if user:
|
||||
return {'status': False, 'msg': 'Email address is already in use'}
|
||||
|
||||
|
@ -119,6 +119,9 @@ def extract_changelogs_from_history(histories, record_name=None, record_type=Non
|
||||
for entry in histories:
|
||||
changes = []
|
||||
|
||||
if entry.detail is None:
|
||||
continue
|
||||
|
||||
if "add_rrsets" in entry.detail:
|
||||
details = json.loads(entry.detail)
|
||||
if not details['add_rrsets'] and not details['del_rrsets']:
|
||||
@ -128,14 +131,17 @@ def extract_changelogs_from_history(histories, record_name=None, record_type=Non
|
||||
|
||||
# filter only the records with the specific record_name, record_type
|
||||
if record_name != None and record_type != None:
|
||||
details['add_rrsets'] = list(filter_rr_list_by_name_and_type(details['add_rrsets'], record_name, record_type))
|
||||
details['del_rrsets'] = list(filter_rr_list_by_name_and_type(details['del_rrsets'], record_name, record_type))
|
||||
details['add_rrsets'] = list(
|
||||
filter_rr_list_by_name_and_type(details['add_rrsets'], record_name, record_type))
|
||||
details['del_rrsets'] = list(
|
||||
filter_rr_list_by_name_and_type(details['del_rrsets'], record_name, record_type))
|
||||
|
||||
if not details['add_rrsets'] and not details['del_rrsets']:
|
||||
continue
|
||||
|
||||
# same record name and type RR are being deleted and created in same entry.
|
||||
del_add_changes = set([(r['name'], r['type']) for r in details['add_rrsets']]).intersection([(r['name'], r['type']) for r in details['del_rrsets']])
|
||||
del_add_changes = set([(r['name'], r['type']) for r in details['add_rrsets']]).intersection(
|
||||
[(r['name'], r['type']) for r in details['del_rrsets']])
|
||||
for del_add_change in del_add_changes:
|
||||
changes.append(HistoryRecordEntry(
|
||||
entry,
|
||||
@ -155,8 +161,8 @@ def extract_changelogs_from_history(histories, record_name=None, record_type=Non
|
||||
# sort changes by the record name
|
||||
if changes:
|
||||
changes.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']
|
||||
)
|
||||
out_changes.extend(changes)
|
||||
return out_changes
|
||||
|
||||
@ -1149,10 +1155,10 @@ def history_table(): # ajax call data
|
||||
.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
|
||||
)) \
|
||||
.subquery()
|
||||
DomainUser.user_id == current_user.id,
|
||||
AccountUser.user_id == current_user.id
|
||||
)) \
|
||||
.subquery()
|
||||
base_query = base_query.filter(History.domain_id.in_(allowed_domain_id_subquery))
|
||||
|
||||
domain_name = request.args.get('domain_name_filter') if request.args.get('domain_name_filter') != None \
|
||||
@ -1271,7 +1277,8 @@ def history_table(): # ajax call data
|
||||
)
|
||||
).order_by(History.created_on.desc()) \
|
||||
.limit(lim).all()
|
||||
elif user_name != None and current_user.role.name in ['Administrator', 'Operator']: # only admins can see the user login-logouts
|
||||
elif user_name != None and current_user.role.name in ['Administrator',
|
||||
'Operator']: # only admins can see the user login-logouts
|
||||
|
||||
histories = base_query.filter(
|
||||
db.and_(
|
||||
@ -1296,7 +1303,8 @@ def history_table(): # ajax call data
|
||||
temp.append(h)
|
||||
break
|
||||
histories = temp
|
||||
elif (changed_by != None or max_date != None) and current_user.role.name in ['Administrator', 'Operator']: # select changed by and date filters only
|
||||
elif (changed_by != None or max_date != None) and current_user.role.name in ['Administrator',
|
||||
'Operator']: # select changed by and date filters only
|
||||
histories = base_query.filter(
|
||||
db.and_(
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
@ -1305,7 +1313,8 @@ def history_table(): # ajax call data
|
||||
)
|
||||
) \
|
||||
.order_by(History.created_on.desc()).limit(lim).all()
|
||||
elif (changed_by != None or max_date != None): # special filtering for user because one user does not have access to log-ins logs
|
||||
elif (
|
||||
changed_by != None or max_date != None): # special filtering for user because one user does not have access to log-ins logs
|
||||
histories = base_query.filter(
|
||||
db.and_(
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
@ -1396,7 +1405,7 @@ def setting_basic_edit(setting):
|
||||
new_value = jdata['value']
|
||||
result = Setting().set(setting, new_value)
|
||||
|
||||
if (result):
|
||||
if result:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
@ -1460,52 +1469,29 @@ def setting_pdns():
|
||||
@login_required
|
||||
@operator_role_required
|
||||
def setting_records():
|
||||
from powerdnsadmin.lib.settings import AppSettings
|
||||
if request.method == 'GET':
|
||||
_fr = Setting().get('forward_records_allow_edit')
|
||||
_rr = Setting().get('reverse_records_allow_edit')
|
||||
f_records = literal_eval(_fr) if isinstance(_fr, str) else _fr
|
||||
r_records = literal_eval(_rr) if isinstance(_rr, str) else _rr
|
||||
|
||||
forward_records = Setting().get('forward_records_allow_edit')
|
||||
reverse_records = Setting().get('reverse_records_allow_edit')
|
||||
return render_template('admin_setting_records.html',
|
||||
f_records=f_records,
|
||||
r_records=r_records)
|
||||
f_records=forward_records,
|
||||
r_records=reverse_records)
|
||||
elif request.method == 'POST':
|
||||
fr = {}
|
||||
rr = {}
|
||||
records = Setting().defaults['forward_records_allow_edit']
|
||||
records = AppSettings.defaults['forward_records_allow_edit']
|
||||
for r in records:
|
||||
fr[r] = True if request.form.get('fr_{0}'.format(
|
||||
r.lower())) else False
|
||||
rr[r] = True if request.form.get('rr_{0}'.format(
|
||||
r.lower())) else False
|
||||
|
||||
Setting().set('forward_records_allow_edit', str(fr))
|
||||
Setting().set('reverse_records_allow_edit', str(rr))
|
||||
Setting().set('forward_records_allow_edit', json.dumps(fr))
|
||||
Setting().set('reverse_records_allow_edit', json.dumps(rr))
|
||||
|
||||
return redirect(url_for('admin.setting_records'))
|
||||
|
||||
|
||||
def has_an_auth_method(local_db_enabled=None,
|
||||
ldap_enabled=None,
|
||||
google_oauth_enabled=None,
|
||||
github_oauth_enabled=None,
|
||||
oidc_oauth_enabled=None,
|
||||
azure_oauth_enabled=None):
|
||||
if local_db_enabled is None:
|
||||
local_db_enabled = Setting().get('local_db_enabled')
|
||||
if ldap_enabled is None:
|
||||
ldap_enabled = Setting().get('ldap_enabled')
|
||||
if google_oauth_enabled is None:
|
||||
google_oauth_enabled = Setting().get('google_oauth_enabled')
|
||||
if github_oauth_enabled is None:
|
||||
github_oauth_enabled = Setting().get('github_oauth_enabled')
|
||||
if oidc_oauth_enabled is 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
|
||||
|
||||
|
||||
@admin_bp.route('/setting/authentication', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@admin_role_required
|
||||
@ -1517,6 +1503,7 @@ def setting_authentication():
|
||||
@login_required
|
||||
@admin_role_required
|
||||
def setting_authentication_api():
|
||||
from powerdnsadmin.lib.settings import AppSettings
|
||||
result = {'status': 1, 'messages': [], 'data': {}}
|
||||
|
||||
if request.form.get('commit') == '1':
|
||||
@ -1524,7 +1511,7 @@ def setting_authentication_api():
|
||||
data = json.loads(request.form.get('data'))
|
||||
|
||||
for key, value in data.items():
|
||||
if key in model.groups['authentication']:
|
||||
if key in AppSettings.groups['authentication']:
|
||||
model.set(key, value)
|
||||
|
||||
result['data'] = Setting().get_group('authentication')
|
||||
|
@ -66,15 +66,15 @@ def domain(domain_name):
|
||||
current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets)))
|
||||
|
||||
# API server might be down, misconfigured
|
||||
if not rrsets and domain.type != 'slave':
|
||||
if not rrsets and str(domain.type).lower() != 'slave':
|
||||
abort(500)
|
||||
|
||||
quick_edit = Setting().get('record_quick_edit')
|
||||
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
||||
forward_records_allow_to_edit = Setting(
|
||||
).get_forward_records_allow_to_edit()
|
||||
).get_supported_record_types(Setting().ZONE_TYPE_FORWARD)
|
||||
reverse_records_allow_to_edit = Setting(
|
||||
).get_reverse_records_allow_to_edit()
|
||||
).get_supported_record_types(Setting().ZONE_TYPE_REVERSE)
|
||||
ttl_options = Setting().get_ttl_options()
|
||||
records = []
|
||||
|
||||
|
@ -258,7 +258,7 @@ def login():
|
||||
|
||||
result = user.create_local_user()
|
||||
if not result['status']:
|
||||
current_app.logger.warning('Unable to create ' + azure_username)
|
||||
current_app.logger.warning('Unable to create ' + azure_username + ' Reasoning: ' + result['msg'])
|
||||
session.pop('azure_token', None)
|
||||
# note: a redirect to login results in an endless loop, so render the login page instead
|
||||
return render_template('login.html',
|
||||
|
@ -37,6 +37,11 @@ def before_request():
|
||||
minutes=int(Setting().get('session_timeout')))
|
||||
session.modified = True
|
||||
|
||||
# Clean up expired sessions in the database
|
||||
if Setting().get('session_type') == 'sqlalchemy':
|
||||
from ..models.sessions import Sessions
|
||||
Sessions().clean_up_expired_sessions()
|
||||
|
||||
|
||||
@user_bp.route('/profile', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
|
@ -24,9 +24,10 @@ def azure_oauth():
|
||||
'fetch_token': fetch_azure_token,
|
||||
}
|
||||
|
||||
auto_configure = Setting().get('azure_oauth_auto_configure')
|
||||
server_metadata_url = Setting().get('azure_oauth_metadata_url')
|
||||
|
||||
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
|
||||
if auto_configure and 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')
|
||||
|
@ -26,9 +26,10 @@ def github_oauth():
|
||||
'update_token': update_token
|
||||
}
|
||||
|
||||
auto_configure = Setting().get('github_oauth_auto_configure')
|
||||
server_metadata_url = Setting().get('github_oauth_metadata_url')
|
||||
|
||||
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
|
||||
if auto_configure and 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')
|
||||
|
@ -25,9 +25,10 @@ def google_oauth():
|
||||
'update_token': update_token
|
||||
}
|
||||
|
||||
auto_configure = Setting().get('google_oauth_auto_configure')
|
||||
server_metadata_url = Setting().get('google_oauth_metadata_url')
|
||||
|
||||
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
|
||||
if auto_configure and 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')
|
||||
|
@ -25,9 +25,10 @@ def oidc_oauth():
|
||||
'update_token': update_token
|
||||
}
|
||||
|
||||
auto_configure = Setting().get('oidc_oauth_auto_configure')
|
||||
server_metadata_url = Setting().get('oidc_oauth_metadata_url')
|
||||
|
||||
if isinstance(server_metadata_url, str) and len(server_metadata_url.strip()) > 0:
|
||||
if auto_configure and 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')
|
||||
|
@ -4,8 +4,8 @@
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: local('Roboto Mono Light'), local('RobotoMono-Light'),
|
||||
url('/static/assets/fonts/roboto-mono-v7-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/static/assets/fonts/roboto-mono-v7-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
url('../fonts/roboto-mono-v7-latin-300.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/roboto-mono-v7-latin-300.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
/* roboto-mono-regular - latin */
|
||||
@font-face {
|
||||
@ -13,8 +13,8 @@
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Roboto Mono'), local('RobotoMono-Regular'),
|
||||
url('/static/assets/fonts/roboto-mono-v7-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/static/assets/fonts/roboto-mono-v7-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
url('../fonts/roboto-mono-v7-latin-regular.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/roboto-mono-v7-latin-regular.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
||||
/* roboto-mono-700 - latin */
|
||||
@font-face {
|
||||
@ -22,6 +22,6 @@
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Roboto Mono Bold'), local('RobotoMono-Bold'),
|
||||
url('/static/assets/fonts/roboto-mono-v7-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('/static/assets/fonts/roboto-mono-v7-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
url('../fonts/roboto-mono-v7-latin-700.woff2') format('woff2'), /* Chrome 26+, Opera 23+, Firefox 39+ */
|
||||
url('../fonts/roboto-mono-v7-latin-700.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||
}
|
@ -3,89 +3,89 @@
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
src: url('/static/assets/fonts/source-sans-pro-v13-latin-300.eot'); /* IE9 Compat Modes */
|
||||
src: url('../fonts/source-sans-pro-v13-latin-300.eot'); /* IE9 Compat Modes */
|
||||
src: local('Source Sans Pro Light'), local('SourceSansPro-Light'),
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300.woff') format('woff'), /* Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-300.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/source-sans-pro-v13-latin-300.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-300.woff') format('woff'), /* Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-300.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-300.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-300italic - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 300;
|
||||
src: url('/static/assets/fonts/source-sans-pro-v13-latin-300italic.eot'); /* IE9 Compat Modes */
|
||||
src: url('../fonts/source-sans-pro-v13-latin-300italic.eot'); /* IE9 Compat Modes */
|
||||
src: local('Source Sans Pro Light Italic'), local('SourceSansPro-LightItalic'),
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-300italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-300italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/source-sans-pro-v13-latin-300italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-300italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-300italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-300italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-regular - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url('/static/assets/fonts/source-sans-pro-v13-latin-regular.eot'); /* IE9 Compat Modes */
|
||||
src: url('../fonts/source-sans-pro-v13-latin-regular.eot'); /* IE9 Compat Modes */
|
||||
src: local('Source Sans Pro Regular'), local('SourceSansPro-Regular'),
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-regular.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/source-sans-pro-v13-latin-regular.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-regular.woff') format('woff'), /* Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-regular.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-regular.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-italic - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url('/static/assets/fonts/source-sans-pro-v13-latin-italic.eot'); /* IE9 Compat Modes */
|
||||
src: url('../fonts/source-sans-pro-v13-latin-italic.eot'); /* IE9 Compat Modes */
|
||||
src: local('Source Sans Pro Italic'), local('SourceSansPro-Italic'),
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/source-sans-pro-v13-latin-italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-600 - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('/static/assets/fonts/source-sans-pro-v13-latin-600.eot'); /* IE9 Compat Modes */
|
||||
src: url('../fonts/source-sans-pro-v13-latin-600.eot'); /* IE9 Compat Modes */
|
||||
src: local('Source Sans Pro SemiBold'), local('SourceSansPro-SemiBold'),
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600.woff') format('woff'), /* Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-600.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/source-sans-pro-v13-latin-600.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-600.woff') format('woff'), /* Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-600.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-600.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-600italic - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url('/static/assets/fonts/source-sans-pro-v13-latin-600italic.eot'); /* IE9 Compat Modes */
|
||||
src: url('../fonts/source-sans-pro-v13-latin-600italic.eot'); /* IE9 Compat Modes */
|
||||
src: local('Source Sans Pro SemiBold Italic'), local('SourceSansPro-SemiBoldItalic'),
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-600italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-600italic.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/source-sans-pro-v13-latin-600italic.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-600italic.woff') format('woff'), /* Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-600italic.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-600italic.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
||||
/* source-sans-pro-700 - latin */
|
||||
@font-face {
|
||||
font-family: 'Source Sans Pro';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: url('/static/assets/fonts/source-sans-pro-v13-latin-700.eot'); /* IE9 Compat Modes */
|
||||
src: url('../fonts/source-sans-pro-v13-latin-700.eot'); /* IE9 Compat Modes */
|
||||
src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'),
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-700.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-700.woff') format('woff'), /* Modern Browsers */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('/static/assets/fonts/source-sans-pro-v13-latin-700.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-700.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */
|
||||
url('../fonts/source-sans-pro-v13-latin-700.woff2') format('woff2'), /* Super Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-700.woff') format('woff'), /* Modern Browsers */
|
||||
url('../fonts/source-sans-pro-v13-latin-700.ttf') format('truetype'), /* Safari, Android, iOS */
|
||||
url('../fonts/source-sans-pro-v13-latin-700.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||
}
|
@ -30,14 +30,14 @@ function applyChanges(data, url, showResult, refreshPage) {
|
||||
function applyRecordChanges(data, domain) {
|
||||
$.ajax({
|
||||
type : "POST",
|
||||
url : $SCRIPT_ROOT + '/domain/' + domain + '/apply',
|
||||
url : $SCRIPT_ROOT + '/domain/' + encodeURIComponent(domain) + '/apply',
|
||||
data : JSON.stringify(data),// now data come in this function
|
||||
contentType : "application/json; charset=utf-8",
|
||||
crossDomain : true,
|
||||
dataType : "json",
|
||||
success : function(data, status, jqXHR) {
|
||||
// update Apply button value
|
||||
$.getJSON($SCRIPT_ROOT + '/domain/' + domain + '/info', function(data) {
|
||||
$.getJSON($SCRIPT_ROOT + '/domain/' + encodeURIComponent(domain) + '/info', function(data) {
|
||||
$(".button_apply_changes").val(data['serial']);
|
||||
});
|
||||
|
||||
|
@ -133,7 +133,7 @@
|
||||
</option>
|
||||
{% endwith %}
|
||||
{% else %}
|
||||
<option {% if account.id == domain.account_id %}selected{% endif %}
|
||||
<option {% if account.id and account.id == domain.account_id %}selected{% endif %}
|
||||
value="{{ domain.name }}">
|
||||
{{ domain.name }}
|
||||
</option>
|
||||
|
@ -31,14 +31,14 @@
|
||||
<!-- Sidebar toggle button-->
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="pushmenu" href="#" role="button">
|
||||
<a class="nav-link no-prompt" data-widget="pushmenu" href="#" role="button">
|
||||
<i class="fa-solid fa-bars"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" data-widget="fullscreen" href="#" role="button">
|
||||
<a class="nav-link no-prompt" data-widget="fullscreen" href="#" role="button">
|
||||
<i class="fa-solid fa-expand-arrows-alt"></i>
|
||||
</a>
|
||||
</li>
|
||||
@ -154,7 +154,7 @@
|
||||
</a>
|
||||
</li>
|
||||
<li class="{{ 'nav-item active' if active_page == 'admin_settings' else 'nav-item' }}">
|
||||
<a href="#" class="nav-link">
|
||||
<a href="#" class="nav-link no-prompt">
|
||||
<i class="nav-icon fa-solid fa-cog"></i>
|
||||
<p>
|
||||
Settings
|
||||
@ -228,7 +228,7 @@
|
||||
<footer class="main-footer">
|
||||
<strong><a href="https://github.com/PowerDNS-Admin/PowerDNS-Admin">PowerDNS-Admin</a></strong> - A PowerDNS web
|
||||
interface with advanced features.
|
||||
<span class="float-right">Version 0.4.1</span>
|
||||
<span class="float-right">Version 0.4.2</span>
|
||||
</footer>
|
||||
</div>
|
||||
<!-- ./wrapper -->
|
||||
|
@ -181,17 +181,17 @@
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or not SETTING.get('dnssec_admins_only') %}
|
||||
$(document.body).on("click", ".button_dnssec", function () {
|
||||
var domain = $(this).prop('id');
|
||||
getdnssec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec', domain);
|
||||
getdnssec($SCRIPT_ROOT + '/domain/' + encodeURIComponent(domain) + '/dnssec', domain);
|
||||
});
|
||||
|
||||
$(document.body).on("click", ".button_dnssec_enable", function () {
|
||||
var domain = $(this).prop('id');
|
||||
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/enable', '{{ csrf_token() }}');
|
||||
enable_dns_sec($SCRIPT_ROOT + '/domain/' + encodeURIComponent(domain) + '/dnssec/enable', '{{ csrf_token() }}');
|
||||
});
|
||||
|
||||
$(document.body).on("click", ".button_dnssec_disable", function () {
|
||||
var domain = $(this).prop('id');
|
||||
enable_dns_sec($SCRIPT_ROOT + '/domain/' + domain + '/dnssec/disable', '{{ csrf_token() }}');
|
||||
enable_dns_sec($SCRIPT_ROOT + '/domain/' + encodeURIComponent(domain) + '/dnssec/disable', '{{ csrf_token() }}');
|
||||
});
|
||||
{% endif %}
|
||||
</script>
|
||||
|
@ -22,6 +22,15 @@
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="container-fluid">
|
||||
<div class="card" id="unsaved-changes-card" style="display: none; position: sticky; top: 0; z-index: 999;">
|
||||
<div class="card-header" style="background-color: yellow;">
|
||||
<h3 class="card-title" style="color: red;">
|
||||
Warning: Unsaved Changes
|
||||
</h3>
|
||||
</div>
|
||||
<div class="card-body" style="font-size: 1rem; color: black; background-color: yellow;">
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
@ -77,11 +86,7 @@
|
||||
<th>Data</th>
|
||||
{% if domain.type != 'Slave' %}
|
||||
<th>Comment</th>
|
||||
<th>Edit</th>
|
||||
<th>Delete</th>
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
||||
<th>Changelog</th>
|
||||
{% endif %}
|
||||
<th>Actions</th>
|
||||
{% else %}
|
||||
<th>Invisible Sorting Column</th>
|
||||
{% endif %}
|
||||
@ -99,7 +104,7 @@
|
||||
<td>{{ record.comment }}</td>
|
||||
<td>
|
||||
{% if record.is_allowed_edit() %}
|
||||
<button type="button" class="btn btn-sm btn-warning button_edit">
|
||||
<button type="button" title="Edit" class="btn btn-sm btn-warning button_edit">
|
||||
<i class="fa-solid fa-edit"></i>
|
||||
</button>
|
||||
{% else %}
|
||||
@ -107,23 +112,19 @@
|
||||
<i class="fa-solid fa-exclamation-circle"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if record.is_allowed_delete() %}
|
||||
<button type="button" class="btn btn-sm btn-danger button_delete">
|
||||
<button type="button" title="Delete" class="btn btn-sm btn-danger button_delete">
|
||||
<i class="fa-solid fa-trash"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
||||
<td>
|
||||
<button type="button"
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
||||
<button type="button" title="Changelog"
|
||||
onclick="show_record_changelog('{{ record.name }}','{{ record.type }}',event)"
|
||||
class="btn btn-primary">
|
||||
class="btn btn-primary btn-sm">
|
||||
<i class="fa-solid fa-history" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
<!-- hidden column that we can sort on -->
|
||||
<td>1</td>
|
||||
@ -146,6 +147,7 @@
|
||||
table#tbl_records thead th:nth-child(2),
|
||||
table#tbl_records thead th:nth-child(3),
|
||||
table#tbl_records thead th:nth-child(4) { width: 100px; }
|
||||
table#tbl_records thead th:nth-child(7) { width: 80px; }
|
||||
table#tbl_records tbody td { text-align: center; }
|
||||
table#tbl_records tbody td:nth-child(0n+5),
|
||||
table#tbl_records tbody td:nth-child(0n+6) { text-align: left; word-break: break-all; }
|
||||
@ -194,11 +196,7 @@
|
||||
// regardless of whatever sorting is done. See orderFixed
|
||||
visible: false,
|
||||
{% if domain.type != 'Slave' %}
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
||||
targets: [9]
|
||||
{% else %}
|
||||
targets: [8]
|
||||
{% endif %}
|
||||
targets: [7]
|
||||
{% else %}
|
||||
targets: [5]
|
||||
{% endif %}
|
||||
@ -216,11 +214,7 @@
|
||||
}
|
||||
],
|
||||
{% if domain.type != 'Slave' %}
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
||||
"orderFixed": [[9, 'asc']]
|
||||
{% else %}
|
||||
"orderFixed": [[8, 'asc']]
|
||||
{% endif %}
|
||||
"orderFixed": [[7, 'asc']]
|
||||
{% else %}
|
||||
"orderFixed": [[5, 'asc']]
|
||||
{% endif %}
|
||||
@ -251,6 +245,7 @@
|
||||
|
||||
$("#button_delete_confirm").unbind().one('click', function (e) {
|
||||
table.row(nRow).remove().draw();
|
||||
detectUnsavedChanges(table);
|
||||
modal.modal('hide');
|
||||
});
|
||||
|
||||
@ -355,6 +350,7 @@
|
||||
e.stopPropagation();
|
||||
var table = $("#tbl_records").DataTable();
|
||||
saveRow(table, nEditing);
|
||||
detectUnsavedChanges(table);
|
||||
nEditing = null;
|
||||
nNew = false;
|
||||
});
|
||||
@ -368,6 +364,94 @@
|
||||
}, $SCRIPT_ROOT + '/domain/' + domain + '/update', true);
|
||||
});
|
||||
|
||||
var unsavedChanges = false;
|
||||
|
||||
function detectUnsavedChanges(table) {
|
||||
// Reset unsavedChanges to false at the start of the function
|
||||
unsavedChanges = false;
|
||||
var index = 0;
|
||||
var origcount = {{ records|length }};
|
||||
var count = table.page.info().recordsTotal;
|
||||
var changes = {}; // Dictionary to store changes
|
||||
|
||||
if (count != origcount) {
|
||||
unsavedChanges = true; //a record was either added or deleted.
|
||||
} else {
|
||||
{% for record in records %}
|
||||
var origrecordname = '{{ (record.name,domain.name) | display_record_name }}';
|
||||
var origrecordtype = '{{ record.type }}';
|
||||
var origrecordstatus = '{{ record.status }}';
|
||||
var origrecordttl = '{{ record.ttl }}';
|
||||
var origrecorddata = '{{ record.data }}';
|
||||
origrecorddata = origrecorddata.replace(/"/g, '\"');
|
||||
var origrecordcomment = '{{ record.comment }}';
|
||||
if (!table.row(index) || typeof table.row(index) == 'undefined') {
|
||||
unsavedChanges = true; //sanity check otherwise below code throws error if row at that index doesn't exist.
|
||||
} else {
|
||||
var editrecordname = table.row(index).data()[0];
|
||||
var editrecordtype = table.row(index).data()[1];
|
||||
var editrecordstatus = table.row(index).data()[2];
|
||||
var editrecordttl = table.row(index).data()[3];
|
||||
var editrecorddata = table.row(index).data()[4];
|
||||
var editrecordcomment = table.row(index).data()[5];
|
||||
if (origrecordname != editrecordname || origrecordtype != editrecordtype || origrecordstatus != editrecordstatus || origrecordttl != editrecordttl || origrecorddata != editrecorddata || origrecordcomment != editrecordcomment) {
|
||||
unsavedChanges = true;
|
||||
}
|
||||
}
|
||||
index++;
|
||||
{% endfor %}
|
||||
}
|
||||
unsavedChangesWarning(unsavedChanges);
|
||||
|
||||
// Get the modal and the navigation links
|
||||
var modal = document.getElementById('WarnLeave');
|
||||
var navLinks = document.querySelectorAll('.nav-link');
|
||||
|
||||
// Listen for clicks on navigation links
|
||||
navLinks.forEach(function(link) {
|
||||
if (!link.classList.contains('no-prompt')) {
|
||||
link.addEventListener('click', function(event) {
|
||||
if (unsavedChanges) {
|
||||
event.preventDefault(); // Prevent navigation
|
||||
modal.style.display = "block"; // Show the modal
|
||||
// Get the buttons
|
||||
var stayButton = document.getElementById('stay');
|
||||
var leaveButton = document.getElementById('leave');
|
||||
|
||||
// When the user clicks on "Stay", close the modal
|
||||
stayButton.onclick = function() {
|
||||
modal.style.display = "none";
|
||||
}
|
||||
|
||||
// When the user clicks on "Leave", navigate away
|
||||
leaveButton.onclick = function() {
|
||||
unsavedChanges = false; // No unsaved changes anymore
|
||||
location.href = link.href; // Navigate to the clicked link
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function unsavedChangesWarning(unsavedChanges) {
|
||||
var card = document.getElementById("unsaved-changes-card");
|
||||
var cardBody = card.querySelector(".card-body");
|
||||
|
||||
if(unsavedChanges){
|
||||
var message = 'There are unsaved changes for the zone. Please click Save Changes to save all of your changes.';
|
||||
card.style.display = 'block'; // to show the card
|
||||
cardBody.innerHTML = message;
|
||||
return false;
|
||||
} else {
|
||||
card.style.display = 'none'; // to hide the card
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
{% if SETTING.get('record_helper') %}
|
||||
//handle wacky record types
|
||||
$(document.body).on("focus", "#current_edit_record_data", function (e) {
|
||||
@ -597,10 +681,6 @@
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
window.onload = function () {
|
||||
document.getElementById("loading-spinner").style.display = "none";
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@ -677,4 +757,20 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal" tabindex="-1" id="WarnLeave">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Unsaved Changes</h5>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>You have unsaved changes. Are you sure you want to navigate away?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" id="stay">Stay</button>
|
||||
<button type="button" class="btn btn-secondary" id="leave">Leave</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -40,6 +40,7 @@
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" placeholder="Username" name="username"
|
||||
data-error="Please input your username" required
|
||||
autofocus
|
||||
{% if username %}value="{{ username }}" {% endif %}>
|
||||
<span class="help-block with-errors"></span>
|
||||
</div>
|
||||
|
@ -7,17 +7,17 @@ Flask-SQLAlchemy==2.5.1
|
||||
Flask-SSLify==0.1.5
|
||||
Flask-SeaSurf==1.1.1
|
||||
Flask-Session==0.4.0
|
||||
Flask==2.1.3
|
||||
Flask==2.2.5
|
||||
Jinja2==3.1.3
|
||||
PyYAML==5.4
|
||||
SQLAlchemy==1.3.24
|
||||
PyYAML==6.0.1
|
||||
SQLAlchemy==1.4.51
|
||||
#alembic==1.9.0
|
||||
bcrypt==4.0.1
|
||||
bcrypt==4.1.2
|
||||
bravado-core==5.17.1
|
||||
certifi==2022.12.7
|
||||
certifi==2023.11.17
|
||||
cffi==1.15.1
|
||||
configobj==5.0.8
|
||||
cryptography==39.0.2 # fixes CVE-2023-0286, CVE-2023-23931
|
||||
cryptography==42.0.2
|
||||
cssmin==0.2.0
|
||||
dnspython>=2.3.0
|
||||
flask_session_captcha==1.3.0
|
||||
@ -26,21 +26,21 @@ itsdangerous==2.1.2
|
||||
jsonschema[format]>=2.5.1,<4.0.0 # until https://github.com/Yelp/bravado-core/pull/385
|
||||
lima==0.5
|
||||
--use-feature=no-binary-enable-wheel-cache lxml==4.9.0
|
||||
mysqlclient==2.0.1
|
||||
mysqlclient==2.2.1
|
||||
passlib==1.7.4
|
||||
#pyOpenSSL==22.1.0
|
||||
pyasn1==0.4.8
|
||||
pyotp==2.8.0
|
||||
pytest==7.2.1
|
||||
pytest==7.4.4
|
||||
python-ldap==3.4.3
|
||||
python3-saml==1.15.0
|
||||
pytimeparse==1.1.8
|
||||
pytz==2022.7.1
|
||||
qrcode==7.3.1
|
||||
requests==2.28.2
|
||||
requests==2.31.0
|
||||
rjsmin==1.2.1
|
||||
webcolors==1.12
|
||||
werkzeug==2.1.2
|
||||
werkzeug==2.3.8
|
||||
zipp==3.11.0
|
||||
rcssmin==1.1.1
|
||||
zxcvbn==4.4.28
|
||||
|
Loading…
Reference in New Issue
Block a user