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 }}
|
tags: powerdnsadmin/pda-legacy:${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Docker Image Release Tagging
|
- name: Docker Image Release Tagging
|
||||||
uses: docker/build-push-action@v2
|
uses: docker/build-push-action@v4
|
||||||
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
with:
|
with:
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
context: ./
|
context: ./
|
||||||
file: ./docker/Dockerfile
|
file: ./docker/Dockerfile
|
||||||
push: true
|
push: true
|
||||||
|
3
.github/workflows/mega-linter.yml
vendored
3
.github/workflows/mega-linter.yml
vendored
@ -12,6 +12,9 @@ on:
|
|||||||
- "main"
|
- "main"
|
||||||
- "master"
|
- "master"
|
||||||
- "dependabot/**"
|
- "dependabot/**"
|
||||||
|
- "feature/**"
|
||||||
|
- "issues/**"
|
||||||
|
- "release/**"
|
||||||
|
|
||||||
env: # Comment env block if you do not want to apply fixes
|
env: # Comment env block if you do not want to apply fixes
|
||||||
# Apply linter fixes configuration
|
# 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
|
1. Update the configuration
|
||||||
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`.
|
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`.
|
||||||
Other environment variables are mentioned in
|
Other environment variables are mentioned in
|
||||||
the [legal_envvars](https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/master/configs/docker_config.py#L5-L46).
|
the [AppSettings.defaults](https://github.com/PowerDNS-Admin/PowerDNS-Admin/blob/master/powerdnsadmin/lib/settings.py) dictionary.
|
||||||
To use the Docker secrets feature it is possible to append `_FILE` to the environment variables and point to a file
|
To use a Docker-style secrets convention, one may append `_FILE` to the environment variables with a path to a file
|
||||||
with the values stored in it.
|
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
|
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)
|
string (https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY)
|
||||||
|
|
||||||
2. Start docker container
|
2. Start docker container
|
||||||
|
@ -7,7 +7,7 @@ SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
|||||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
||||||
BIND_ADDRESS = '0.0.0.0'
|
BIND_ADDRESS = '0.0.0.0'
|
||||||
PORT = 9191
|
PORT = 9191
|
||||||
SERVER_EXTERNAL_SSL = None
|
SERVER_EXTERNAL_SSL = os.getenv('SERVER_EXTERNAL_SSL', None)
|
||||||
|
|
||||||
### DATABASE CONFIG
|
### DATABASE CONFIG
|
||||||
SQLA_DB_USER = 'pda'
|
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
|
PORT = 80
|
||||||
SERVER_EXTERNAL_SSL = True
|
SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
|
||||||
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
|
EXPOSE 80/tcp
|
||||||
USER ${USER}
|
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"]
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
CMD ["gunicorn","powerdnsadmin:create_app()"]
|
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
|
- Setting up a zone
|
||||||
- Adding a record
|
- Adding a record
|
||||||
- <whatever else>
|
|
||||||
|
|
||||||
## Feature usage
|
## Feature usage
|
||||||
|
|
||||||
|
@ -4,11 +4,11 @@ from flask import Flask
|
|||||||
from flask_mail import Mail
|
from flask_mail import Mail
|
||||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||||
from flask_session import Session
|
from flask_session import Session
|
||||||
|
|
||||||
from .lib import utils
|
from .lib import utils
|
||||||
|
|
||||||
|
|
||||||
def create_app(config=None):
|
def create_app(config=None):
|
||||||
|
from powerdnsadmin.lib.settings import AppSettings
|
||||||
from . import models, routes, services
|
from . import models, routes, services
|
||||||
from .assets import assets
|
from .assets import assets
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -50,6 +50,9 @@ def create_app(config=None):
|
|||||||
elif config.endswith('.py'):
|
elif config.endswith('.py'):
|
||||||
app.config.from_pyfile(config)
|
app.config.from_pyfile(config)
|
||||||
|
|
||||||
|
# Load any settings defined with environment variables
|
||||||
|
AppSettings.load_environment(app)
|
||||||
|
|
||||||
# HSTS
|
# HSTS
|
||||||
if app.config.get('HSTS_ENABLED'):
|
if app.config.get('HSTS_ENABLED'):
|
||||||
from flask_sslify import SSLify
|
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_datetime_local'] = utils.format_datetime
|
||||||
app.jinja_env.filters['format_zone_type'] = utils.format_zone_type
|
app.jinja_env.filters['format_zone_type'] = utils.format_zone_type
|
||||||
|
|
||||||
# Register context proccessors
|
# Register context processors
|
||||||
from .models.setting import Setting
|
from .models.setting import Setting
|
||||||
|
|
||||||
@app.context_processor
|
@app.context_processor
|
||||||
|
@ -13,6 +13,7 @@ def admin_role_required(f):
|
|||||||
"""
|
"""
|
||||||
Grant access if user is in Administrator role
|
Grant access if user is in Administrator role
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name != 'Administrator':
|
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
|
Grant access if user is in Operator role or higher
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
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
|
Grant access if user is in Operator role or higher, or Users can view history
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name not in [
|
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 Account, or
|
||||||
- user is in granted Domain
|
- user is in granted Domain
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
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
|
- user is in Operator role or higher, or
|
||||||
- dnssec_admins_only is off
|
- dnssec_admins_only is off
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name not in [
|
if current_user.role.name not in [
|
||||||
'Administrator', 'Operator'
|
'Administrator', 'Operator'
|
||||||
] and Setting().get('dnssec_admins_only'):
|
] and Setting().get('dnssec_admins_only'):
|
||||||
abort(403)
|
abort(403)
|
||||||
|
|
||||||
@ -94,16 +99,18 @@ def can_configure_dnssec(f):
|
|||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
def can_remove_domain(f):
|
def can_remove_domain(f):
|
||||||
"""
|
"""
|
||||||
Grant access if:
|
Grant access if:
|
||||||
- user is in Operator role or higher, or
|
- user is in Operator role or higher, or
|
||||||
- allow_user_remove_domain is on
|
- allow_user_remove_domain is on
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name not in [
|
if current_user.role.name not in [
|
||||||
'Administrator', 'Operator'
|
'Administrator', 'Operator'
|
||||||
] and not Setting().get('allow_user_remove_domain'):
|
] and not Setting().get('allow_user_remove_domain'):
|
||||||
abort(403)
|
abort(403)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
@ -111,17 +118,17 @@ def can_remove_domain(f):
|
|||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def can_create_domain(f):
|
def can_create_domain(f):
|
||||||
"""
|
"""
|
||||||
Grant access if:
|
Grant access if:
|
||||||
- user is in Operator role or higher, or
|
- user is in Operator role or higher, or
|
||||||
- allow_user_create_domain is on
|
- allow_user_create_domain is on
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name not in [
|
if current_user.role.name not in [
|
||||||
'Administrator', 'Operator'
|
'Administrator', 'Operator'
|
||||||
] and not Setting().get('allow_user_create_domain'):
|
] and not Setting().get('allow_user_create_domain'):
|
||||||
abort(403)
|
abort(403)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
@ -144,11 +151,12 @@ def api_basic_auth(f):
|
|||||||
|
|
||||||
# Remove "Basic " from the header value
|
# Remove "Basic " from the header value
|
||||||
auth_header = auth_header[6:]
|
auth_header = auth_header[6:]
|
||||||
|
auth_components = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
auth_header = str(base64.b64decode(auth_header), 'utf-8')
|
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
|
# NK: We use auth_components here as we don't know if we'll have a colon,
|
||||||
# username, the rest of the string would be the password.
|
# 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)
|
auth_components = auth_header.split(':', maxsplit=1)
|
||||||
except (binascii.Error, UnicodeDecodeError) as e:
|
except (binascii.Error, UnicodeDecodeError) as e:
|
||||||
current_app.logger.error(
|
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
|
If request body contains one or more of specified keys, call
|
||||||
:param callback
|
:param callback
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
check_current_http_method = not http_methods or request.method in http_methods
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
if (check_current_http_method and
|
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)
|
callback(*args, **kwargs)
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@ -246,16 +257,18 @@ def api_role_can(action, roles=None, allow_self=False):
|
|||||||
except:
|
except:
|
||||||
username = None
|
username = None
|
||||||
if (
|
if (
|
||||||
(current_user.role.name in roles) or
|
(current_user.role.name in roles) or
|
||||||
(allow_self and user_id and current_user.id == user_id) or
|
(allow_self and user_id and current_user.id == user_id) or
|
||||||
(allow_self and username and current_user.username == username)
|
(allow_self and username and current_user.username == username)
|
||||||
):
|
):
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
msg = (
|
msg = (
|
||||||
"User {} with role {} does not have enough privileges to {}"
|
"User {} with role {} does not have enough privileges to {}"
|
||||||
).format(current_user.username, current_user.role.name, action)
|
).format(current_user.username, current_user.role.name, action)
|
||||||
raise NotEnoughPrivileges(message=msg)
|
raise NotEnoughPrivileges(message=msg)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@ -265,15 +278,16 @@ def api_can_create_domain(f):
|
|||||||
- user is in Operator role or higher, or
|
- user is in Operator role or higher, or
|
||||||
- allow_user_create_domain is on
|
- allow_user_create_domain is on
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if current_user.role.name not in [
|
if current_user.role.name not in [
|
||||||
'Administrator', 'Operator'
|
'Administrator', 'Operator'
|
||||||
] and not Setting().get('allow_user_create_domain'):
|
] and not Setting().get('allow_user_create_domain'):
|
||||||
msg = "User {0} does not have enough privileges to create zone"
|
msg = "User {0} does not have enough privileges to create zone"
|
||||||
current_app.logger.error(msg.format(current_user.username))
|
current_app.logger.error(msg.format(current_user.username))
|
||||||
raise NotEnoughPrivileges()
|
raise NotEnoughPrivileges()
|
||||||
|
|
||||||
if Setting().get('deny_domain_override'):
|
if Setting().get('deny_domain_override'):
|
||||||
req = request.get_json(force=True)
|
req = request.get_json(force=True)
|
||||||
domain = Domain()
|
domain = Domain()
|
||||||
@ -294,10 +308,11 @@ def apikey_can_create_domain(f):
|
|||||||
- deny_domain_override is off or
|
- deny_domain_override is off or
|
||||||
- override_domain is true (from request)
|
- override_domain is true (from request)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if g.apikey.role.name not in [
|
if g.apikey.role.name not in [
|
||||||
'Administrator', 'Operator'
|
'Administrator', 'Operator'
|
||||||
] and not Setting().get('allow_user_create_domain'):
|
] and not Setting().get('allow_user_create_domain'):
|
||||||
msg = "ApiKey #{0} does not have enough privileges to create zone"
|
msg = "ApiKey #{0} does not have enough privileges to create zone"
|
||||||
current_app.logger.error(msg.format(g.apikey.id))
|
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
|
- user is in Operator role or higher, or
|
||||||
- allow_user_remove_domain is on
|
- allow_user_remove_domain is on
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
check_current_http_method = not http_methods or request.method in http_methods
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
|
||||||
if (check_current_http_method and
|
if (check_current_http_method and
|
||||||
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||||
not Setting().get('allow_user_remove_domain')
|
not Setting().get('allow_user_remove_domain')
|
||||||
):
|
):
|
||||||
msg = "ApiKey #{0} does not have enough privileges to remove zone"
|
msg = "ApiKey #{0} does not have enough privileges to remove zone"
|
||||||
current_app.logger.error(msg.format(g.apikey.id))
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
raise NotEnoughPrivileges()
|
raise NotEnoughPrivileges()
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
@ -341,6 +359,7 @@ def apikey_is_admin(f):
|
|||||||
"""
|
"""
|
||||||
Grant access if user is in Administrator role
|
Grant access if user is in Administrator role
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if g.apikey.role.name != 'Administrator':
|
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 Operator role or higher, or
|
||||||
- user has explicitly been granted access to domain
|
- user has explicitly been granted access to domain
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
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
|
- user is in Operator role or higher, or
|
||||||
- dnssec_admins_only is off
|
- dnssec_admins_only is off
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def decorator(f=None):
|
def decorator(f=None):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
check_current_http_method = not http_methods or request.method in http_methods
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
|
||||||
if (check_current_http_method and
|
if (check_current_http_method and
|
||||||
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||||
Setting().get('dnssec_admins_only')
|
Setting().get('dnssec_admins_only')
|
||||||
):
|
):
|
||||||
msg = "ApiKey #{0} does not have enough privileges to configure dnssec"
|
msg = "ApiKey #{0} does not have enough privileges to configure dnssec"
|
||||||
current_app.logger.error(msg.format(g.apikey.id))
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
raise DomainAccessForbidden(message=msg)
|
raise DomainAccessForbidden(message=msg)
|
||||||
return f(*args, **kwargs) if f else None
|
return f(*args, **kwargs) if f else None
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
return decorator
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def allowed_record_types(f):
|
def allowed_record_types(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
@ -423,6 +447,7 @@ def allowed_record_types(f):
|
|||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
def allowed_record_ttl(f):
|
def allowed_record_ttl(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
@ -431,12 +456,12 @@ def allowed_record_ttl(f):
|
|||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
if g.apikey.role.name in ['Administrator', 'Operator']:
|
if g.apikey.role.name in ['Administrator', 'Operator']:
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
allowed_ttls = Setting().get_ttl_options()
|
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()
|
content = request.get_json()
|
||||||
try:
|
try:
|
||||||
for record in content['rrsets']:
|
for record in content['rrsets']:
|
||||||
@ -497,6 +522,7 @@ def dyndns_login_required(f):
|
|||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
def apikey_or_basic_auth(f):
|
def apikey_or_basic_auth(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
@ -505,4 +531,5 @@ def apikey_or_basic_auth(f):
|
|||||||
return apikey_auth(f)(*args, **kwargs)
|
return apikey_auth(f)(*args, **kwargs)
|
||||||
else:
|
else:
|
||||||
return api_basic_auth(f)(*args, **kwargs)
|
return api_basic_auth(f)(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
@ -1,44 +1,32 @@
|
|||||||
import os
|
import os
|
||||||
import urllib.parse
|
|
||||||
basedir = os.path.abspath(os.path.dirname(__file__))
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
### BASIC APP CONFIG
|
|
||||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
|
||||||
SECRET_KEY = 'e951e5a1f4b94151b360f47edf596dd2'
|
|
||||||
BIND_ADDRESS = '0.0.0.0'
|
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_ENABLE = True
|
||||||
CAPTCHA_LENGTH = 6
|
|
||||||
CAPTCHA_WIDTH = 160
|
|
||||||
CAPTCHA_HEIGHT = 60
|
CAPTCHA_HEIGHT = 60
|
||||||
|
CAPTCHA_LENGTH = 6
|
||||||
CAPTCHA_SESSION_KEY = 'captcha_image'
|
CAPTCHA_SESSION_KEY = 'captcha_image'
|
||||||
|
CAPTCHA_WIDTH = 160
|
||||||
### DATABASE CONFIG
|
CSRF_COOKIE_HTTPONLY = True
|
||||||
SQLA_DB_USER = 'pda'
|
HSTS_ENABLED = False
|
||||||
SQLA_DB_PASSWORD = 'changeme'
|
PORT = 9191
|
||||||
SQLA_DB_HOST = '127.0.0.1'
|
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||||
SQLA_DB_NAME = 'pda'
|
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
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
# SQLA_DB_USER = 'pda'
|
||||||
### DATABASE - MySQL
|
# SQLA_DB_PASSWORD = 'changeme'
|
||||||
|
# SQLA_DB_HOST = '127.0.0.1'
|
||||||
|
# SQLA_DB_NAME = 'pda'
|
||||||
# SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
|
# SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
|
||||||
# urllib.parse.quote_plus(SQLA_DB_USER),
|
# urllib.parse.quote_plus(SQLA_DB_USER),
|
||||||
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
|
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
|
||||||
# SQLA_DB_HOST,
|
# SQLA_DB_HOST,
|
||||||
# SQLA_DB_NAME
|
# 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
|
Update records from Master DNS server
|
||||||
"""
|
"""
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {'X-API-Key': self.PDNS_API_KEY}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
@ -650,7 +652,7 @@ class Domain(db.Model):
|
|||||||
r = utils.fetch_json(urljoin(
|
r = utils.fetch_json(urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
'/servers/localhost/zones/{0}/axfr-retrieve'.format(
|
'/servers/localhost/zones/{0}/axfr-retrieve'.format(
|
||||||
domain.name)),
|
urllib.parse.quote_plus(domain.name))),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(
|
timeout=int(
|
||||||
Setting().get('pdns_api_timeout')),
|
Setting().get('pdns_api_timeout')),
|
||||||
@ -673,6 +675,8 @@ class Domain(db.Model):
|
|||||||
"""
|
"""
|
||||||
Get zone DNSSEC information
|
Get zone DNSSEC information
|
||||||
"""
|
"""
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {'X-API-Key': self.PDNS_API_KEY}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
@ -681,7 +685,7 @@ class Domain(db.Model):
|
|||||||
urljoin(
|
urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||||
domain.name)),
|
urllib.parse.quote_plus(domain.name))),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='GET',
|
method='GET',
|
||||||
@ -709,6 +713,8 @@ class Domain(db.Model):
|
|||||||
"""
|
"""
|
||||||
Enable zone DNSSEC
|
Enable zone DNSSEC
|
||||||
"""
|
"""
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
||||||
@ -718,7 +724,9 @@ class Domain(db.Model):
|
|||||||
jdata = utils.fetch_json(
|
jdata = utils.fetch_json(
|
||||||
urljoin(
|
urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
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,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='PUT',
|
method='PUT',
|
||||||
@ -738,7 +746,8 @@ class Domain(db.Model):
|
|||||||
urljoin(
|
urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||||
domain.name)),
|
urllib.parse.quote_plus(domain.name)
|
||||||
|
)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='POST',
|
method='POST',
|
||||||
@ -775,6 +784,8 @@ class Domain(db.Model):
|
|||||||
"""
|
"""
|
||||||
Remove keys DNSSEC
|
Remove keys DNSSEC
|
||||||
"""
|
"""
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
||||||
@ -784,7 +795,7 @@ class Domain(db.Model):
|
|||||||
urljoin(
|
urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
|
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
|
||||||
domain.name, key_id)),
|
urllib.parse.quote_plus(domain.name), key_id)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='DELETE',
|
method='DELETE',
|
||||||
|
@ -251,6 +251,7 @@ class Record(object):
|
|||||||
Returns:
|
Returns:
|
||||||
new_rrsets(list): List of rrsets to be added
|
new_rrsets(list): List of rrsets to be added
|
||||||
del_rrsets(list): List of rrsets to be deleted
|
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
|
# Create submitted rrsets from submitted records
|
||||||
submitted_rrsets = self.build_rrsets(domain_name, 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
|
# PDNS API always return the comments with modified_at
|
||||||
# info, we have to remove it to be able to do the dict
|
# info, we have to remove it to be able to do the dict
|
||||||
# comparison between current and submitted rrsets
|
# comparison between current and submitted rrsets
|
||||||
|
zone_has_comments = False
|
||||||
for r in current_rrsets:
|
for r in current_rrsets:
|
||||||
for comment in r['comments']:
|
for comment in r['comments']:
|
||||||
if 'modified_at' in comment:
|
if 'modified_at' in comment:
|
||||||
|
zone_has_comments = True
|
||||||
del comment['modified_at']
|
del comment['modified_at']
|
||||||
|
|
||||||
# List of rrsets to be added
|
# 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("new_rrsets: \n{}".format(utils.pretty_json(new_rrsets)))
|
||||||
current_app.logger.debug("del_rrsets: \n{}".format(utils.pretty_json(del_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):
|
def apply_rrsets(self, domain_name, rrsets):
|
||||||
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
headers = {'X-API-Key': self.PDNS_API_KEY, 'Content-Type': 'application/json'}
|
||||||
@ -304,7 +307,7 @@ class Record(object):
|
|||||||
return jdata
|
return jdata
|
||||||
|
|
||||||
@staticmethod
|
@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."""
|
"""Turn the given changes into a single api payload."""
|
||||||
|
|
||||||
def replace_for_api(rrset):
|
def replace_for_api(rrset):
|
||||||
@ -312,9 +315,13 @@ class Record(object):
|
|||||||
if not rrset or rrset.get('changetype', None) != 'REPLACE':
|
if not rrset or rrset.get('changetype', None) != 'REPLACE':
|
||||||
return rrset
|
return rrset
|
||||||
replace_copy = dict(rrset)
|
replace_copy = dict(rrset)
|
||||||
# For compatibility with some backends: Remove comments from rrset if all are blank
|
has_nonempty_comments = any(bool(c.get('content', None)) for c in replace_copy.get('comments', []))
|
||||||
if not any((bool(c.get('content', None)) for c in replace_copy.get('comments', []))):
|
if not has_nonempty_comments:
|
||||||
replace_copy.pop('comments', None)
|
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
|
return replace_copy
|
||||||
|
|
||||||
def rrset_in(needle, haystack):
|
def rrset_in(needle, haystack):
|
||||||
@ -351,11 +358,11 @@ class Record(object):
|
|||||||
"submitted_records: {}".format(submitted_records))
|
"submitted_records: {}".format(submitted_records))
|
||||||
|
|
||||||
# Get the list of rrsets to be added and deleted
|
# 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.
|
# 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
|
# 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)}")
|
current_app.logger.debug(f"api payload: \n{utils.pretty_json(api_payload)}")
|
||||||
|
|
||||||
# Submit the changes to PDNS API
|
# 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,450 +1,19 @@
|
|||||||
import sys
|
import sys
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
import pytimeparse
|
import pytimeparse
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from distutils.util import strtobool
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from .base import db
|
from .base import db
|
||||||
|
from powerdnsadmin.lib.settings import AppSettings
|
||||||
|
|
||||||
|
|
||||||
class Setting(db.Model):
|
class Setting(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
name = db.Column(db.String(64), unique=True, index=True)
|
name = db.Column(db.String(64), unique=True, index=True)
|
||||||
value = db.Column(db.Text())
|
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
|
ZONE_TYPE_FORWARD = 'forward'
|
||||||
'local_db_enabled': True,
|
ZONE_TYPE_REVERSE = 'reverse'
|
||||||
'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',
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, value=None):
|
def __init__(self, id=None, name=None, value=None):
|
||||||
self.id = id
|
self.id = id
|
||||||
@ -457,44 +26,12 @@ class Setting(db.Model):
|
|||||||
self.name = name
|
self.name = name
|
||||||
self.value = value
|
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):
|
def set_maintenance(self, mode):
|
||||||
maintenance = Setting.query.filter(
|
maintenance = Setting.query.filter(
|
||||||
Setting.name == 'maintenance').first()
|
Setting.name == 'maintenance').first()
|
||||||
|
|
||||||
if maintenance is None:
|
if maintenance is None:
|
||||||
value = self.defaults['maintenance']
|
value = AppSettings.defaults['maintenance']
|
||||||
maintenance = Setting(name='maintenance', value=str(value))
|
maintenance = Setting(name='maintenance', value=str(value))
|
||||||
db.session.add(maintenance)
|
db.session.add(maintenance)
|
||||||
|
|
||||||
@ -516,7 +53,7 @@ class Setting(db.Model):
|
|||||||
current_setting = Setting.query.filter(Setting.name == setting).first()
|
current_setting = Setting.query.filter(Setting.name == setting).first()
|
||||||
|
|
||||||
if current_setting is None:
|
if current_setting is None:
|
||||||
value = self.defaults[setting]
|
value = AppSettings.defaults[setting]
|
||||||
current_setting = Setting(name=setting, value=str(value))
|
current_setting = Setting(name=setting, value=str(value))
|
||||||
db.session.add(current_setting)
|
db.session.add(current_setting)
|
||||||
|
|
||||||
@ -535,27 +72,30 @@ class Setting(db.Model):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def set(self, setting, value):
|
def set(self, setting, value):
|
||||||
|
import json
|
||||||
current_setting = Setting.query.filter(Setting.name == setting).first()
|
current_setting = Setting.query.filter(Setting.name == setting).first()
|
||||||
|
|
||||||
if current_setting is None:
|
if current_setting is None:
|
||||||
current_setting = Setting(name=setting, value=None)
|
current_setting = Setting(name=setting, value=None)
|
||||||
db.session.add(current_setting)
|
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:
|
try:
|
||||||
current_setting.value = value
|
current_setting.value = value
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error('Cannot edit setting {0}. DETAIL: {1}'.format(
|
current_app.logger.error('Cannot edit setting {0}. DETAIL: {1}'.format(setting, e))
|
||||||
setting, e))
|
|
||||||
current_app.logger.debug(traceback.format_exec())
|
current_app.logger.debug(traceback.format_exec())
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get(self, setting):
|
def get(self, setting):
|
||||||
if setting in self.defaults:
|
if setting in AppSettings.defaults:
|
||||||
|
|
||||||
if setting.upper() in current_app.config:
|
if setting.upper() in current_app.config:
|
||||||
result = current_app.config[setting.upper()]
|
result = current_app.config[setting.upper()]
|
||||||
@ -566,51 +106,45 @@ class Setting(db.Model):
|
|||||||
if hasattr(result, 'value'):
|
if hasattr(result, 'value'):
|
||||||
result = result.value
|
result = result.value
|
||||||
|
|
||||||
return self.convert_type(setting, result)
|
return AppSettings.convert_type(setting, result)
|
||||||
else:
|
else:
|
||||||
return self.defaults[setting]
|
return AppSettings.defaults[setting]
|
||||||
else:
|
else:
|
||||||
current_app.logger.error('Unknown setting queried: {0}'.format(setting))
|
current_app.logger.error('Unknown setting queried: {0}'.format(setting))
|
||||||
|
|
||||||
def get_group(self, group):
|
def get_group(self, group):
|
||||||
if not isinstance(group, list):
|
if not isinstance(group, list):
|
||||||
group = self.groups[group]
|
group = AppSettings.groups[group]
|
||||||
|
|
||||||
result = {}
|
result = {}
|
||||||
records = self.query.all()
|
|
||||||
|
|
||||||
for record in records:
|
for var_name, default_value in AppSettings.defaults.items():
|
||||||
if record.name in group:
|
if var_name in group:
|
||||||
result[record.name] = self.convert_type(record.name, record.value)
|
result[var_name] = self.get(var_name)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_records_allow_to_edit(self):
|
def get_records_allow_to_edit(self):
|
||||||
return list(
|
return list(
|
||||||
set(self.get_forward_records_allow_to_edit() +
|
set(self.get_supported_record_types(self.ZONE_TYPE_FORWARD) +
|
||||||
self.get_reverse_records_allow_to_edit()))
|
self.get_supported_record_types(self.ZONE_TYPE_REVERSE)))
|
||||||
|
|
||||||
def get_forward_records_allow_to_edit(self):
|
def get_supported_record_types(self, zone_type):
|
||||||
records = self.get('forward_records_allow_edit')
|
setting_value = []
|
||||||
f_records = literal_eval(records) if isinstance(records,
|
|
||||||
str) else records
|
if zone_type == self.ZONE_TYPE_FORWARD:
|
||||||
r_name = [r for r in f_records if f_records[r]]
|
setting_value = self.get('forward_records_allow_edit')
|
||||||
# Sort alphabetically if python version is smaller than 3.6
|
elif zone_type == self.ZONE_TYPE_REVERSE:
|
||||||
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
|
setting_value = self.get('reverse_records_allow_edit')
|
||||||
and sys.version_info[1] < 6):
|
|
||||||
r_name.sort()
|
records = literal_eval(setting_value) if isinstance(setting_value, str) else setting_value
|
||||||
return r_name
|
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
|
# Sort alphabetically if python version is smaller than 3.6
|
||||||
if sys.version_info[0] < 3 or (sys.version_info[0] == 3
|
if sys.version_info[0] < 3 or (sys.version_info[0] == 3 and sys.version_info[1] < 6):
|
||||||
and sys.version_info[1] < 6):
|
types.sort()
|
||||||
r_name.sort()
|
|
||||||
return r_name
|
return types
|
||||||
|
|
||||||
def get_ttl_options(self):
|
def get_ttl_options(self):
|
||||||
return [(pytimeparse.parse(ttl), ttl)
|
return [(pytimeparse.parse(ttl), ttl)
|
||||||
|
@ -133,6 +133,16 @@ class User(db.Model):
|
|||||||
conn.protocol_version = ldap.VERSION3
|
conn.protocol_version = ldap.VERSION3
|
||||||
return conn
|
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):
|
def ldap_search(self, searchFilter, baseDN, retrieveAttributes=None):
|
||||||
searchScope = ldap.SCOPE_SUBTREE
|
searchScope = ldap.SCOPE_SUBTREE
|
||||||
|
|
||||||
@ -280,7 +290,7 @@ class User(db.Model):
|
|||||||
Operator=LDAP_OPERATOR_GROUP,
|
Operator=LDAP_OPERATOR_GROUP,
|
||||||
User=LDAP_USER_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 = ""
|
sf_groups = ""
|
||||||
|
|
||||||
for group in ldap_group_security_roles.values():
|
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
|
Create local user witch stores username / password in the DB
|
||||||
"""
|
"""
|
||||||
# check if username existed
|
# 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:
|
if user:
|
||||||
return {'status': False, 'msg': 'Username is already in use'}
|
return {'status': False, 'msg': 'Username is already in use'}
|
||||||
|
|
||||||
# check if email existed
|
# 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:
|
if user:
|
||||||
return {'status': False, 'msg': 'Email address is already in use'}
|
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:
|
for entry in histories:
|
||||||
changes = []
|
changes = []
|
||||||
|
|
||||||
|
if entry.detail is None:
|
||||||
|
continue
|
||||||
|
|
||||||
if "add_rrsets" in entry.detail:
|
if "add_rrsets" in entry.detail:
|
||||||
details = json.loads(entry.detail)
|
details = json.loads(entry.detail)
|
||||||
if not details['add_rrsets'] and not details['del_rrsets']:
|
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
|
# filter only the records with the specific record_name, record_type
|
||||||
if record_name != None and record_type != None:
|
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['add_rrsets'] = list(
|
||||||
details['del_rrsets'] = list(filter_rr_list_by_name_and_type(details['del_rrsets'], record_name, record_type))
|
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']:
|
if not details['add_rrsets'] and not details['del_rrsets']:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# same record name and type RR are being deleted and created in same entry.
|
# 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:
|
for del_add_change in del_add_changes:
|
||||||
changes.append(HistoryRecordEntry(
|
changes.append(HistoryRecordEntry(
|
||||||
entry,
|
entry,
|
||||||
@ -155,8 +161,8 @@ def extract_changelogs_from_history(histories, record_name=None, record_type=Non
|
|||||||
# sort changes by the record name
|
# sort changes by the record name
|
||||||
if changes:
|
if changes:
|
||||||
changes.sort(key=lambda change:
|
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)
|
out_changes.extend(changes)
|
||||||
return out_changes
|
return out_changes
|
||||||
|
|
||||||
@ -1149,10 +1155,10 @@ def history_table(): # ajax call data
|
|||||||
.outerjoin(Account, Domain.account_id == Account.id) \
|
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||||
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||||
.filter(db.or_(
|
.filter(db.or_(
|
||||||
DomainUser.user_id == current_user.id,
|
DomainUser.user_id == current_user.id,
|
||||||
AccountUser.user_id == current_user.id
|
AccountUser.user_id == current_user.id
|
||||||
)) \
|
)) \
|
||||||
.subquery()
|
.subquery()
|
||||||
base_query = base_query.filter(History.domain_id.in_(allowed_domain_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 \
|
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()) \
|
).order_by(History.created_on.desc()) \
|
||||||
.limit(lim).all()
|
.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(
|
histories = base_query.filter(
|
||||||
db.and_(
|
db.and_(
|
||||||
@ -1296,7 +1303,8 @@ def history_table(): # ajax call data
|
|||||||
temp.append(h)
|
temp.append(h)
|
||||||
break
|
break
|
||||||
histories = temp
|
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(
|
histories = base_query.filter(
|
||||||
db.and_(
|
db.and_(
|
||||||
History.created_on <= max_date if max_date != None else True,
|
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()
|
.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(
|
histories = base_query.filter(
|
||||||
db.and_(
|
db.and_(
|
||||||
History.created_on <= max_date if max_date != None else True,
|
History.created_on <= max_date if max_date != None else True,
|
||||||
@ -1396,7 +1405,7 @@ def setting_basic_edit(setting):
|
|||||||
new_value = jdata['value']
|
new_value = jdata['value']
|
||||||
result = Setting().set(setting, new_value)
|
result = Setting().set(setting, new_value)
|
||||||
|
|
||||||
if (result):
|
if result:
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify({
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
@ -1460,52 +1469,29 @@ def setting_pdns():
|
|||||||
@login_required
|
@login_required
|
||||||
@operator_role_required
|
@operator_role_required
|
||||||
def setting_records():
|
def setting_records():
|
||||||
|
from powerdnsadmin.lib.settings import AppSettings
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
_fr = Setting().get('forward_records_allow_edit')
|
forward_records = Setting().get('forward_records_allow_edit')
|
||||||
_rr = Setting().get('reverse_records_allow_edit')
|
reverse_records = 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
|
|
||||||
|
|
||||||
return render_template('admin_setting_records.html',
|
return render_template('admin_setting_records.html',
|
||||||
f_records=f_records,
|
f_records=forward_records,
|
||||||
r_records=r_records)
|
r_records=reverse_records)
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
fr = {}
|
fr = {}
|
||||||
rr = {}
|
rr = {}
|
||||||
records = Setting().defaults['forward_records_allow_edit']
|
records = AppSettings.defaults['forward_records_allow_edit']
|
||||||
for r in records:
|
for r in records:
|
||||||
fr[r] = True if request.form.get('fr_{0}'.format(
|
fr[r] = True if request.form.get('fr_{0}'.format(
|
||||||
r.lower())) else False
|
r.lower())) else False
|
||||||
rr[r] = True if request.form.get('rr_{0}'.format(
|
rr[r] = True if request.form.get('rr_{0}'.format(
|
||||||
r.lower())) else False
|
r.lower())) else False
|
||||||
|
|
||||||
Setting().set('forward_records_allow_edit', str(fr))
|
Setting().set('forward_records_allow_edit', json.dumps(fr))
|
||||||
Setting().set('reverse_records_allow_edit', str(rr))
|
Setting().set('reverse_records_allow_edit', json.dumps(rr))
|
||||||
|
|
||||||
return redirect(url_for('admin.setting_records'))
|
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'])
|
@admin_bp.route('/setting/authentication', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@admin_role_required
|
@admin_role_required
|
||||||
@ -1517,6 +1503,7 @@ def setting_authentication():
|
|||||||
@login_required
|
@login_required
|
||||||
@admin_role_required
|
@admin_role_required
|
||||||
def setting_authentication_api():
|
def setting_authentication_api():
|
||||||
|
from powerdnsadmin.lib.settings import AppSettings
|
||||||
result = {'status': 1, 'messages': [], 'data': {}}
|
result = {'status': 1, 'messages': [], 'data': {}}
|
||||||
|
|
||||||
if request.form.get('commit') == '1':
|
if request.form.get('commit') == '1':
|
||||||
@ -1524,7 +1511,7 @@ def setting_authentication_api():
|
|||||||
data = json.loads(request.form.get('data'))
|
data = json.loads(request.form.get('data'))
|
||||||
|
|
||||||
for key, value in data.items():
|
for key, value in data.items():
|
||||||
if key in model.groups['authentication']:
|
if key in AppSettings.groups['authentication']:
|
||||||
model.set(key, value)
|
model.set(key, value)
|
||||||
|
|
||||||
result['data'] = Setting().get_group('authentication')
|
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)))
|
current_app.logger.debug("Fetched rrsets: \n{}".format(pretty_json(rrsets)))
|
||||||
|
|
||||||
# API server might be down, misconfigured
|
# API server might be down, misconfigured
|
||||||
if not rrsets and domain.type != 'slave':
|
if not rrsets and str(domain.type).lower() != 'slave':
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
quick_edit = Setting().get('record_quick_edit')
|
quick_edit = Setting().get('record_quick_edit')
|
||||||
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
||||||
forward_records_allow_to_edit = Setting(
|
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(
|
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()
|
ttl_options = Setting().get_ttl_options()
|
||||||
records = []
|
records = []
|
||||||
|
|
||||||
|
@ -258,7 +258,7 @@ def login():
|
|||||||
|
|
||||||
result = user.create_local_user()
|
result = user.create_local_user()
|
||||||
if not result['status']:
|
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)
|
session.pop('azure_token', None)
|
||||||
# note: a redirect to login results in an endless loop, so render the login page instead
|
# note: a redirect to login results in an endless loop, so render the login page instead
|
||||||
return render_template('login.html',
|
return render_template('login.html',
|
||||||
|
@ -37,6 +37,11 @@ def before_request():
|
|||||||
minutes=int(Setting().get('session_timeout')))
|
minutes=int(Setting().get('session_timeout')))
|
||||||
session.modified = True
|
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'])
|
@user_bp.route('/profile', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@ -24,9 +24,10 @@ def azure_oauth():
|
|||||||
'fetch_token': fetch_azure_token,
|
'fetch_token': fetch_azure_token,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto_configure = Setting().get('azure_oauth_auto_configure')
|
||||||
server_metadata_url = Setting().get('azure_oauth_metadata_url')
|
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
|
authlib_params['server_metadata_url'] = server_metadata_url
|
||||||
else:
|
else:
|
||||||
authlib_params['access_token_url'] = Setting().get('azure_oauth_token_url')
|
authlib_params['access_token_url'] = Setting().get('azure_oauth_token_url')
|
||||||
|
@ -26,9 +26,10 @@ def github_oauth():
|
|||||||
'update_token': update_token
|
'update_token': update_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto_configure = Setting().get('github_oauth_auto_configure')
|
||||||
server_metadata_url = Setting().get('github_oauth_metadata_url')
|
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
|
authlib_params['server_metadata_url'] = server_metadata_url
|
||||||
else:
|
else:
|
||||||
authlib_params['access_token_url'] = Setting().get('github_oauth_token_url')
|
authlib_params['access_token_url'] = Setting().get('github_oauth_token_url')
|
||||||
|
@ -25,9 +25,10 @@ def google_oauth():
|
|||||||
'update_token': update_token
|
'update_token': update_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto_configure = Setting().get('google_oauth_auto_configure')
|
||||||
server_metadata_url = Setting().get('google_oauth_metadata_url')
|
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
|
authlib_params['server_metadata_url'] = server_metadata_url
|
||||||
else:
|
else:
|
||||||
authlib_params['access_token_url'] = Setting().get('google_token_url')
|
authlib_params['access_token_url'] = Setting().get('google_token_url')
|
||||||
|
@ -25,9 +25,10 @@ def oidc_oauth():
|
|||||||
'update_token': update_token
|
'update_token': update_token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto_configure = Setting().get('oidc_oauth_auto_configure')
|
||||||
server_metadata_url = Setting().get('oidc_oauth_metadata_url')
|
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
|
authlib_params['server_metadata_url'] = server_metadata_url
|
||||||
else:
|
else:
|
||||||
authlib_params['access_token_url'] = Setting().get('oidc_oauth_token_url')
|
authlib_params['access_token_url'] = Setting().get('oidc_oauth_token_url')
|
||||||
|
@ -4,8 +4,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
src: local('Roboto Mono Light'), local('RobotoMono-Light'),
|
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('../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.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
||||||
/* roboto-mono-regular - latin */
|
/* roboto-mono-regular - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -13,8 +13,8 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
src: local('Roboto Mono'), local('RobotoMono-Regular'),
|
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('../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.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
||||||
/* roboto-mono-700 - latin */
|
/* roboto-mono-700 - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
@ -22,6 +22,6 @@
|
|||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
src: local('Roboto Mono Bold'), local('RobotoMono-Bold'),
|
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('../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.woff') format('woff'); /* Chrome 6+, Firefox 3.6+, IE 9+, Safari 5.1+ */
|
||||||
}
|
}
|
@ -3,89 +3,89 @@
|
|||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 300;
|
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'),
|
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('../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('../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('../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('../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.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
/* source-sans-pro-300italic - latin */
|
/* source-sans-pro-300italic - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 300;
|
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'),
|
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('../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('../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('../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('../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.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
/* source-sans-pro-regular - latin */
|
/* source-sans-pro-regular - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 400;
|
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'),
|
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('../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('../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('../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('../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.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
/* source-sans-pro-italic - latin */
|
/* source-sans-pro-italic - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 400;
|
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'),
|
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('../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('../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('../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('../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.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
/* source-sans-pro-600 - latin */
|
/* source-sans-pro-600 - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 600;
|
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'),
|
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('../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('../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('../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('../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.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
/* source-sans-pro-600italic - latin */
|
/* source-sans-pro-600italic - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
font-weight: 600;
|
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'),
|
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('../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('../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('../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('../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.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
||||||
/* source-sans-pro-700 - latin */
|
/* source-sans-pro-700 - latin */
|
||||||
@font-face {
|
@font-face {
|
||||||
font-family: 'Source Sans Pro';
|
font-family: 'Source Sans Pro';
|
||||||
font-style: normal;
|
font-style: normal;
|
||||||
font-weight: 700;
|
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'),
|
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('../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('../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('../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('../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.svg#SourceSansPro') format('svg'); /* Legacy iOS */
|
||||||
}
|
}
|
@ -30,14 +30,14 @@ function applyChanges(data, url, showResult, refreshPage) {
|
|||||||
function applyRecordChanges(data, domain) {
|
function applyRecordChanges(data, domain) {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
type : "POST",
|
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
|
data : JSON.stringify(data),// now data come in this function
|
||||||
contentType : "application/json; charset=utf-8",
|
contentType : "application/json; charset=utf-8",
|
||||||
crossDomain : true,
|
crossDomain : true,
|
||||||
dataType : "json",
|
dataType : "json",
|
||||||
success : function(data, status, jqXHR) {
|
success : function(data, status, jqXHR) {
|
||||||
// update Apply button value
|
// 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']);
|
$(".button_apply_changes").val(data['serial']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -133,7 +133,7 @@
|
|||||||
</option>
|
</option>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% 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 }}">
|
value="{{ domain.name }}">
|
||||||
{{ domain.name }}
|
{{ domain.name }}
|
||||||
</option>
|
</option>
|
||||||
|
@ -31,14 +31,14 @@
|
|||||||
<!-- Sidebar toggle button-->
|
<!-- Sidebar toggle button-->
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item">
|
<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>
|
<i class="fa-solid fa-bars"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
<li class="nav-item">
|
<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>
|
<i class="fa-solid fa-expand-arrows-alt"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@ -154,7 +154,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="{{ 'nav-item active' if active_page == 'admin_settings' else 'nav-item' }}">
|
<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>
|
<i class="nav-icon fa-solid fa-cog"></i>
|
||||||
<p>
|
<p>
|
||||||
Settings
|
Settings
|
||||||
@ -228,7 +228,7 @@
|
|||||||
<footer class="main-footer">
|
<footer class="main-footer">
|
||||||
<strong><a href="https://github.com/PowerDNS-Admin/PowerDNS-Admin">PowerDNS-Admin</a></strong> - A PowerDNS web
|
<strong><a href="https://github.com/PowerDNS-Admin/PowerDNS-Admin">PowerDNS-Admin</a></strong> - A PowerDNS web
|
||||||
interface with advanced features.
|
interface with advanced features.
|
||||||
<span class="float-right">Version 0.4.1</span>
|
<span class="float-right">Version 0.4.2</span>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<!-- ./wrapper -->
|
<!-- ./wrapper -->
|
||||||
@ -418,4 +418,4 @@
|
|||||||
{% block modals %}
|
{% block modals %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
@ -181,17 +181,17 @@
|
|||||||
{% if current_user.role.name in ['Administrator', 'Operator'] or not SETTING.get('dnssec_admins_only') %}
|
{% if current_user.role.name in ['Administrator', 'Operator'] or not SETTING.get('dnssec_admins_only') %}
|
||||||
$(document.body).on("click", ".button_dnssec", function () {
|
$(document.body).on("click", ".button_dnssec", function () {
|
||||||
var domain = $(this).prop('id');
|
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 () {
|
$(document.body).on("click", ".button_dnssec_enable", function () {
|
||||||
var domain = $(this).prop('id');
|
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 () {
|
$(document.body).on("click", ".button_dnssec_disable", function () {
|
||||||
var domain = $(this).prop('id');
|
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 %}
|
{% endif %}
|
||||||
</script>
|
</script>
|
||||||
|
@ -22,6 +22,15 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="content">
|
<section class="content">
|
||||||
<div class="container-fluid">
|
<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="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -77,11 +86,7 @@
|
|||||||
<th>Data</th>
|
<th>Data</th>
|
||||||
{% if domain.type != 'Slave' %}
|
{% if domain.type != 'Slave' %}
|
||||||
<th>Comment</th>
|
<th>Comment</th>
|
||||||
<th>Edit</th>
|
<th>Actions</th>
|
||||||
<th>Delete</th>
|
|
||||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
|
||||||
<th>Changelog</th>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<th>Invisible Sorting Column</th>
|
<th>Invisible Sorting Column</th>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -99,7 +104,7 @@
|
|||||||
<td>{{ record.comment }}</td>
|
<td>{{ record.comment }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if record.is_allowed_edit() %}
|
{% 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>
|
<i class="fa-solid fa-edit"></i>
|
||||||
</button>
|
</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -107,23 +112,19 @@
|
|||||||
<i class="fa-solid fa-exclamation-circle"></i>
|
<i class="fa-solid fa-exclamation-circle"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
{% if record.is_allowed_delete() %}
|
{% 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>
|
<i class="fa-solid fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
||||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
<button type="button" title="Changelog"
|
||||||
<td>
|
|
||||||
<button type="button"
|
|
||||||
onclick="show_record_changelog('{{ record.name }}','{{ record.type }}',event)"
|
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>
|
<i class="fa-solid fa-history" aria-hidden="true"></i>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
{% endif %}
|
||||||
{% endif %}
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- hidden column that we can sort on -->
|
<!-- hidden column that we can sort on -->
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
@ -146,6 +147,7 @@
|
|||||||
table#tbl_records thead th:nth-child(2),
|
table#tbl_records thead th:nth-child(2),
|
||||||
table#tbl_records thead th:nth-child(3),
|
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(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 { text-align: center; }
|
||||||
table#tbl_records tbody td:nth-child(0n+5),
|
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; }
|
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
|
// regardless of whatever sorting is done. See orderFixed
|
||||||
visible: false,
|
visible: false,
|
||||||
{% if domain.type != 'Slave' %}
|
{% if domain.type != 'Slave' %}
|
||||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
targets: [7]
|
||||||
targets: [9]
|
|
||||||
{% else %}
|
|
||||||
targets: [8]
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
targets: [5]
|
targets: [5]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -216,11 +214,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
{% if domain.type != 'Slave' %}
|
{% if domain.type != 'Slave' %}
|
||||||
{% if current_user.role.name in ['Administrator', 'Operator'] or allow_user_view_history %}
|
"orderFixed": [[7, 'asc']]
|
||||||
"orderFixed": [[9, 'asc']]
|
|
||||||
{% else %}
|
|
||||||
"orderFixed": [[8, 'asc']]
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
"orderFixed": [[5, 'asc']]
|
"orderFixed": [[5, 'asc']]
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -251,6 +245,7 @@
|
|||||||
|
|
||||||
$("#button_delete_confirm").unbind().one('click', function (e) {
|
$("#button_delete_confirm").unbind().one('click', function (e) {
|
||||||
table.row(nRow).remove().draw();
|
table.row(nRow).remove().draw();
|
||||||
|
detectUnsavedChanges(table);
|
||||||
modal.modal('hide');
|
modal.modal('hide');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -355,6 +350,7 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
var table = $("#tbl_records").DataTable();
|
var table = $("#tbl_records").DataTable();
|
||||||
saveRow(table, nEditing);
|
saveRow(table, nEditing);
|
||||||
|
detectUnsavedChanges(table);
|
||||||
nEditing = null;
|
nEditing = null;
|
||||||
nNew = false;
|
nNew = false;
|
||||||
});
|
});
|
||||||
@ -368,6 +364,94 @@
|
|||||||
}, $SCRIPT_ROOT + '/domain/' + domain + '/update', true);
|
}, $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') %}
|
{% if SETTING.get('record_helper') %}
|
||||||
//handle wacky record types
|
//handle wacky record types
|
||||||
$(document.body).on("focus", "#current_edit_record_data", function (e) {
|
$(document.body).on("focus", "#current_edit_record_data", function (e) {
|
||||||
@ -597,10 +681,6 @@
|
|||||||
});
|
});
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
window.onload = function () {
|
|
||||||
document.getElementById("loading-spinner").style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
@ -677,4 +757,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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 %}
|
{% endblock %}
|
||||||
|
@ -40,6 +40,7 @@
|
|||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="text" class="form-control" placeholder="Username" name="username"
|
<input type="text" class="form-control" placeholder="Username" name="username"
|
||||||
data-error="Please input your username" required
|
data-error="Please input your username" required
|
||||||
|
autofocus
|
||||||
{% if username %}value="{{ username }}" {% endif %}>
|
{% if username %}value="{{ username }}" {% endif %}>
|
||||||
<span class="help-block with-errors"></span>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -7,17 +7,17 @@ Flask-SQLAlchemy==2.5.1
|
|||||||
Flask-SSLify==0.1.5
|
Flask-SSLify==0.1.5
|
||||||
Flask-SeaSurf==1.1.1
|
Flask-SeaSurf==1.1.1
|
||||||
Flask-Session==0.4.0
|
Flask-Session==0.4.0
|
||||||
Flask==2.1.3
|
Flask==2.2.5
|
||||||
Jinja2==3.1.3
|
Jinja2==3.1.3
|
||||||
PyYAML==5.4
|
PyYAML==6.0.1
|
||||||
SQLAlchemy==1.3.24
|
SQLAlchemy==1.4.51
|
||||||
#alembic==1.9.0
|
#alembic==1.9.0
|
||||||
bcrypt==4.0.1
|
bcrypt==4.1.2
|
||||||
bravado-core==5.17.1
|
bravado-core==5.17.1
|
||||||
certifi==2022.12.7
|
certifi==2023.11.17
|
||||||
cffi==1.15.1
|
cffi==1.15.1
|
||||||
configobj==5.0.8
|
configobj==5.0.8
|
||||||
cryptography==39.0.2 # fixes CVE-2023-0286, CVE-2023-23931
|
cryptography==42.0.2
|
||||||
cssmin==0.2.0
|
cssmin==0.2.0
|
||||||
dnspython>=2.3.0
|
dnspython>=2.3.0
|
||||||
flask_session_captcha==1.3.0
|
flask_session_captcha==1.3.0
|
||||||
@ -26,23 +26,23 @@ itsdangerous==2.1.2
|
|||||||
jsonschema[format]>=2.5.1,<4.0.0 # until https://github.com/Yelp/bravado-core/pull/385
|
jsonschema[format]>=2.5.1,<4.0.0 # until https://github.com/Yelp/bravado-core/pull/385
|
||||||
lima==0.5
|
lima==0.5
|
||||||
--use-feature=no-binary-enable-wheel-cache lxml==4.9.0
|
--use-feature=no-binary-enable-wheel-cache lxml==4.9.0
|
||||||
mysqlclient==2.0.1
|
mysqlclient==2.2.1
|
||||||
passlib==1.7.4
|
passlib==1.7.4
|
||||||
#pyOpenSSL==22.1.0
|
#pyOpenSSL==22.1.0
|
||||||
pyasn1==0.4.8
|
pyasn1==0.4.8
|
||||||
pyotp==2.8.0
|
pyotp==2.8.0
|
||||||
pytest==7.2.1
|
pytest==7.4.4
|
||||||
python-ldap==3.4.3
|
python-ldap==3.4.3
|
||||||
python3-saml==1.15.0
|
python3-saml==1.15.0
|
||||||
pytimeparse==1.1.8
|
pytimeparse==1.1.8
|
||||||
pytz==2022.7.1
|
pytz==2022.7.1
|
||||||
qrcode==7.3.1
|
qrcode==7.3.1
|
||||||
requests==2.28.2
|
requests==2.31.0
|
||||||
rjsmin==1.2.1
|
rjsmin==1.2.1
|
||||||
webcolors==1.12
|
webcolors==1.12
|
||||||
werkzeug==2.1.2
|
werkzeug==2.3.8
|
||||||
zipp==3.11.0
|
zipp==3.11.0
|
||||||
rcssmin==1.1.1
|
rcssmin==1.1.1
|
||||||
zxcvbn==4.4.28
|
zxcvbn==4.4.28
|
||||||
psycopg2==2.9.5
|
psycopg2==2.9.5
|
||||||
setuptools==65.5.1 # fixes CVE-2022-40897
|
setuptools==65.5.1 # fixes CVE-2022-40897
|
||||||
|
Loading…
Reference in New Issue
Block a user