Compare commits
186 commits
Author | SHA1 | Date | |
---|---|---|---|
cd94b5c0ac | |||
0b2ad520b7 | |||
302e793665 | |||
328780e2d4 | |||
ca4c145a18 | |||
7808febad8 | |||
9ef0f2b8d6 | |||
94a923a965 | |||
0da9b2185e | |||
07f0d215a7 | |||
fc8367535b | |||
d2f35a4059 | |||
737e1fb93b | |||
f0008ce401 | |||
6f12b783a8 | |||
51a7f636b0 | |||
9f46188c7e | |||
caa48b7fe5 | |||
591055d4aa | |||
940551e99e | |||
f45ff2ce03 | |||
6c1dfd2408 | |||
701a442d12 | |||
a3b70a8f47 | |||
1332c8d29d | |||
b3f9b4a2b0 | |||
bfaf5655ae | |||
dd04a837bb | |||
5bb1a7ee29 | |||
c85a5dac24 | |||
3081036c2c | |||
c7b4aa3434 | |||
e7d5a3aba0 | |||
20b866a784 | |||
1662a812ba | |||
c49df09ac8 | |||
924537b468 | |||
4f8a547d47 | |||
ee9f568a8d | |||
d7ae34ed53 | |||
1c9ca60508 | |||
0e655c1357 | |||
ba2423d6f5 | |||
46e51f16cb | |||
b8ee91ab9a | |||
c246775ffe | |||
f96103db79 | |||
bf83662108 | |||
1f34dbf810 | |||
b7197948c1 | |||
ddf2d4788b | |||
1ec6b76f89 | |||
4ce1b71c57 | |||
79457bdc85 | |||
10dc2b0273 | |||
993e02b635 | |||
07c71fb0bf | |||
c4a9498898 | |||
6e04d0419b | |||
d6e64dce8e | |||
b069cea8d1 | |||
fd933f8dbc | |||
0505b934a1 | |||
083a023e57 | |||
054e0e6eba | |||
c13dd2d835 | |||
567f66fbde | |||
ff5270fbad | |||
92bad7b11c | |||
43a6e46e66 | |||
ee72fdf9c2 | |||
8f73512d2e | |||
700fa0d9ce | |||
00dc23f21b | |||
36fdb3733f | |||
ce60ca0b9d | |||
b197491a86 | |||
d23a57da50 | |||
4180882fb7 | |||
bbbcf271fe | |||
32983635c6 | |||
f3a98eb692 | |||
39cddd3b34 | |||
b66b37ecfd | |||
5f10f739ea | |||
98db953820 | |||
44c4531f02 | |||
86700f8fd7 | |||
46993e08c0 | |||
4c19f95928 | |||
3a4efebf95 | |||
7f86730909 | |||
8f6a800836 | |||
3cd98251b3 | |||
54b257768f | |||
718b41e3d1 | |||
dd0a5f6326 | |||
c3d438842f | |||
33e7ffb747 | |||
2c18e5c88f | |||
2917c47fd1 | |||
c6e0293177 | |||
942482b706 | |||
4d1db72699 | |||
680e4cf431 | |||
1604494f1d | |||
710cb75bfe | |||
70b1accaa0 | |||
7254a94497 | |||
3034630bc0 | |||
d72709e0f4 | |||
a1c1b35696 | |||
94eeae0cad | |||
75a30f14fb | |||
76562f8a46 | |||
6455189c32 | |||
7e6d5d2e4a | |||
372fdd7bd0 | |||
0dfced4968 | |||
33282ae4af | |||
078b0b2f4d | |||
55ad73d92e | |||
a679073928 | |||
b5fc9045f2 | |||
f3bcf1b834 | |||
b8ffb1dae9 | |||
b10a706e15 | |||
b12377796b | |||
58f3c241b4 | |||
9228128907 | |||
3167e50f65 | |||
810f773a5b | |||
cf62658e19 | |||
352e7d388a | |||
8735f1e273 | |||
74b89b1b7e | |||
70c2744f29 | |||
3c59ba6f84 | |||
b4d7f66e29 | |||
9632898b40 | |||
f9f966df75 | |||
27f5c89f70 | |||
7ef6f5db4e | |||
ab6480a4b4 | |||
0ef57b2f9f | |||
8377f08d3b | |||
22eabef06a | |||
e993422106 | |||
25db119d02 | |||
9946f72a85 | |||
5125c9764c | |||
0f9a5f8652 | |||
f3f91d56e2 | |||
1b4fe8935d | |||
4e63f8380b | |||
1f4580a27a | |||
5123d542e4 | |||
94da9198c0 | |||
a3fd856dd8 | |||
84ae753db2 | |||
5eb2edee2c | |||
4e39d5a461 | |||
cfc8567180 | |||
39db31b5ae | |||
eb730be8f9 | |||
831fbf3cb3 | |||
125883330e | |||
73c267848c | |||
5ac126f349 | |||
52298f8289 | |||
a598c52729 | |||
3476c8a9ec | |||
99f12df748 | |||
0ef132a7be | |||
fd0485d897 | |||
9f4b6ffcdb | |||
ab7e1eb71b | |||
d43c9a581f | |||
edb2a354d1 | |||
78245d339f | |||
f442fef3d6 | |||
9f562714f2 | |||
2044ce4737 | |||
33eff6313f | |||
417338d826 | |||
4d391ccb34 |
19
.github/stale.yml
vendored
Normal file
19
.github/stale.yml
vendored
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
# Number of days of inactivity before an issue becomes stale
|
||||||
|
daysUntilStale: 60
|
||||||
|
# Number of days of inactivity before a stale issue is closed
|
||||||
|
daysUntilClose: 7
|
||||||
|
# Issues with these labels will never be considered stale
|
||||||
|
exemptLabels:
|
||||||
|
- pinned
|
||||||
|
- security
|
||||||
|
- enhancement
|
||||||
|
- feature request
|
||||||
|
# Label to use when marking an issue as stale
|
||||||
|
staleLabel: wontfix
|
||||||
|
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||||
|
markComment: >
|
||||||
|
This issue has been automatically marked as stale because it has not had
|
||||||
|
recent activity. It will be closed if no further activity occurs. Thank you
|
||||||
|
for your contributions.
|
||||||
|
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||||
|
closeComment: true
|
56
.github/workflows/build-and-publish.yml
vendored
Normal file
56
.github/workflows/build-and-publish.yml
vendored
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- 'master'
|
||||||
|
tags:
|
||||||
|
- 'v*.*.*'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push-docker-image:
|
||||||
|
name: Build Docker image and push to repositories
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
ngoduykhanh/powerdns-admin
|
||||||
|
tags: |
|
||||||
|
type=ref,event=tag
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=semver,pattern={{major}}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
id: buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build latest image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
if: github.ref == 'refs/heads/master'
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ngoduykhanh/powerdns-admin:latest
|
||||||
|
|
||||||
|
- name: Build release image
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
if: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
with:
|
||||||
|
context: ./
|
||||||
|
file: ./docker/Dockerfile
|
||||||
|
push: true
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -39,3 +39,4 @@ powerdnsadmin/static/generated
|
||||||
.webassets-cache
|
.webassets-cache
|
||||||
.venv*
|
.venv*
|
||||||
.pytest_cache
|
.pytest_cache
|
||||||
|
.DS_Store
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
language: minimal
|
|
||||||
script:
|
|
||||||
- docker-compose -f docker-compose-test.yml up --exit-code-from powerdns-admin --abort-on-container-exit
|
|
||||||
services:
|
|
||||||
- docker
|
|
12
.whitesource
Normal file
12
.whitesource
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"scanSettings": {
|
||||||
|
"baseBranches": []
|
||||||
|
},
|
||||||
|
"checkRunSettings": {
|
||||||
|
"vulnerableCheckRunConclusionLevel": "failure",
|
||||||
|
"displayMode": "diff"
|
||||||
|
},
|
||||||
|
"issueSettings": {
|
||||||
|
"minSeverityLevel": "LOW"
|
||||||
|
}
|
||||||
|
}
|
10
README.md
10
README.md
|
@ -1,7 +1,6 @@
|
||||||
# PowerDNS-Admin
|
# PowerDNS-Admin
|
||||||
A PowerDNS web interface with advanced features.
|
A PowerDNS web interface with advanced features.
|
||||||
|
|
||||||
[![Build Status](https://travis-ci.org/ngoduykhanh/PowerDNS-Admin.svg?branch=master)](https://travis-ci.org/ngoduykhanh/PowerDNS-Admin)
|
|
||||||
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:python)
|
[![Language grade: Python](https://img.shields.io/lgtm/grade/python/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:python)
|
||||||
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:javascript)
|
[![Language grade: JavaScript](https://img.shields.io/lgtm/grade/javascript/g/ngoduykhanh/PowerDNS-Admin.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/ngoduykhanh/PowerDNS-Admin/context:javascript)
|
||||||
|
|
||||||
|
@ -18,6 +17,7 @@ A PowerDNS web interface with advanced features.
|
||||||
- DynDNS 2 protocol support
|
- DynDNS 2 protocol support
|
||||||
- Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!)
|
- Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!)
|
||||||
- Limited API for manipulating zones and records
|
- Limited API for manipulating zones and records
|
||||||
|
- Full IDN/Punycode support
|
||||||
|
|
||||||
## Running PowerDNS-Admin
|
## Running PowerDNS-Admin
|
||||||
There are several ways to run PowerDNS-Admin. The easiest way is to use Docker.
|
There are several ways to run PowerDNS-Admin. The easiest way is to use Docker.
|
||||||
|
@ -31,7 +31,8 @@ To get started as quickly as possible try option 1. If you want to make modifica
|
||||||
The easiest is to just run the latest Docker image from Docker Hub:
|
The easiest is to just run the latest Docker image from Docker Hub:
|
||||||
```
|
```
|
||||||
$ docker run -d \
|
$ docker run -d \
|
||||||
-v pda-data:/data
|
-e SECRET_KEY='a-very-secret-key' \
|
||||||
|
-v pda-data:/data \
|
||||||
-p 9191:80 \
|
-p 9191:80 \
|
||||||
ngoduykhanh/powerdns-admin:latest
|
ngoduykhanh/powerdns-admin:latest
|
||||||
```
|
```
|
||||||
|
@ -42,6 +43,7 @@ This creates a volume called `pda-data` to persist the SQLite database with the
|
||||||
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 the [legal_envvars](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/configs/docker_config.py#L5-L46).
|
Other environment variables are mentioned in the [legal_envvars](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/configs/docker_config.py#L5-L46).
|
||||||
To use the Docker secrets feature it is possible to append `_FILE` to the environment variables and point to a file with the values stored in it.
|
To use the Docker secrets feature it is possible to append `_FILE` to the environment variables and point to a file with the values stored in it.
|
||||||
|
Make sure to set the environment variable `SECRET_KEY` to a long random string (https://flask.palletsprojects.com/en/1.1.x/config/#SECRET_KEY)
|
||||||
|
|
||||||
2. Start docker container
|
2. Start docker container
|
||||||
```
|
```
|
||||||
|
@ -52,3 +54,7 @@ You can then access PowerDNS-Admin by pointing your browser to http://localhost:
|
||||||
|
|
||||||
## Screenshots
|
## Screenshots
|
||||||
![dashboard](https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png)
|
![dashboard](https://user-images.githubusercontent.com/6447444/44068603-0d2d81f6-9fa5-11e8-83af-14e2ad79e370.png)
|
||||||
|
|
||||||
|
## LICENSE
|
||||||
|
MIT. See [LICENSE](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/LICENSE)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
|
#import urllib.parse
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
### BASIC APP CONFIG
|
### BASIC APP CONFIG
|
||||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||||
|
@ -16,7 +17,12 @@ SQLA_DB_NAME = 'pda'
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
|
||||||
### DATABASE - MySQL
|
### DATABASE - MySQL
|
||||||
# SQLALCHEMY_DATABASE_URI = 'mysql://' + SQLA_DB_USER + ':' + SQLA_DB_PASSWORD + '@' + SQLA_DB_HOST + '/' + SQLA_DB_NAME
|
#SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
|
||||||
|
# urllib.parse.quote_plus(SQLA_DB_USER),
|
||||||
|
# urllib.parse.quote_plus(SQLA_DB_PASSWORD),
|
||||||
|
# SQLA_DB_HOST,
|
||||||
|
# SQLA_DB_NAME
|
||||||
|
#)
|
||||||
|
|
||||||
### DATABASE - SQLite
|
### DATABASE - SQLite
|
||||||
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||||
|
@ -130,7 +136,7 @@ SAML_ENABLED = False
|
||||||
# SAML_CERT_FILE = '/etc/pki/powerdns-admin/cert.crt'
|
# SAML_CERT_FILE = '/etc/pki/powerdns-admin/cert.crt'
|
||||||
# SAML_CERT_KEY = '/etc/pki/powerdns-admin/key.pem'
|
# SAML_CERT_KEY = '/etc/pki/powerdns-admin/key.pem'
|
||||||
|
|
||||||
# Cofigures if SAML tokens should be encrypted.
|
# Configures if SAML tokens should be encrypted.
|
||||||
# SAML_SIGN_REQUEST = False
|
# SAML_SIGN_REQUEST = False
|
||||||
# #Use SAML standard logout mechanism retreived from idp metadata
|
# #Use SAML standard logout mechanism retreived from idp metadata
|
||||||
# #If configured false don't care about SAML session on logout.
|
# #If configured false don't care about SAML session on logout.
|
||||||
|
@ -141,3 +147,19 @@ SAML_ENABLED = False
|
||||||
# #SAML_LOGOUT_URL = 'https://google.com'
|
# #SAML_LOGOUT_URL = 'https://google.com'
|
||||||
|
|
||||||
# #SAML_ASSERTION_ENCRYPTED = True
|
# #SAML_ASSERTION_ENCRYPTED = True
|
||||||
|
|
||||||
|
# Remote authentication settings
|
||||||
|
|
||||||
|
# Whether to enable remote user authentication or not
|
||||||
|
# Defaults to False
|
||||||
|
# REMOTE_USER_ENABLED=True
|
||||||
|
|
||||||
|
# If set, users will be redirected to this location on logout
|
||||||
|
# Ignore or set to None to avoid redirecting altogether
|
||||||
|
# Warning: if REMOTE_USER environment variable is still set after logging out and not cleared by
|
||||||
|
# some external module, not defining a custom logout URL might trigger a loop
|
||||||
|
# that will just log the user back in right after logging out
|
||||||
|
# REMOTE_USER_LOGOUT_URL=https://my.sso.com/cas/logout
|
||||||
|
|
||||||
|
# An optional list of remote authentication tied cookies to be removed upon logout
|
||||||
|
# REMOTE_USER_COOKIES=['MOD_AUTH_CAS', 'MOD_AUTH_CAS_S']
|
||||||
|
|
|
@ -5,6 +5,9 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
|
||||||
|
|
||||||
legal_envvars = (
|
legal_envvars = (
|
||||||
'SECRET_KEY',
|
'SECRET_KEY',
|
||||||
|
'OIDC_OAUTH_API_URL',
|
||||||
|
'OIDC_OAUTH_TOKEN_URL',
|
||||||
|
'OIDC_OAUTH_AUTHORIZE_URL',
|
||||||
'BIND_ADDRESS',
|
'BIND_ADDRESS',
|
||||||
'PORT',
|
'PORT',
|
||||||
'LOG_LEVEL',
|
'LOG_LEVEL',
|
||||||
|
@ -45,7 +48,15 @@ legal_envvars = (
|
||||||
'SAML_LOGOUT',
|
'SAML_LOGOUT',
|
||||||
'SAML_LOGOUT_URL',
|
'SAML_LOGOUT_URL',
|
||||||
'SAML_ASSERTION_ENCRYPTED',
|
'SAML_ASSERTION_ENCRYPTED',
|
||||||
'OFFLINE_MODE'
|
'OFFLINE_MODE',
|
||||||
|
'REMOTE_USER_LOGOUT_URL',
|
||||||
|
'REMOTE_USER_COOKIES',
|
||||||
|
'SIGNUP_ENABLED',
|
||||||
|
'LOCAL_DB_ENABLED',
|
||||||
|
'LDAP_ENABLED',
|
||||||
|
'SAML_CERT',
|
||||||
|
'SAML_KEY',
|
||||||
|
'FILESYSTEM_SESSIONS_ENABLED'
|
||||||
)
|
)
|
||||||
|
|
||||||
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
|
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
|
||||||
|
@ -62,7 +73,12 @@ legal_envvars_bool = (
|
||||||
'SAML_WANT_MESSAGE_SIGNED',
|
'SAML_WANT_MESSAGE_SIGNED',
|
||||||
'SAML_LOGOUT',
|
'SAML_LOGOUT',
|
||||||
'SAML_ASSERTION_ENCRYPTED',
|
'SAML_ASSERTION_ENCRYPTED',
|
||||||
'OFFLINE_MODE'
|
'OFFLINE_MODE',
|
||||||
|
'REMOTE_USER_ENABLED',
|
||||||
|
'SIGNUP_ENABLED',
|
||||||
|
'LOCAL_DB_ENABLED',
|
||||||
|
'LDAP_ENABLED',
|
||||||
|
'FILESYSTEM_SESSIONS_ENABLED'
|
||||||
)
|
)
|
||||||
|
|
||||||
# import everything from environment variables
|
# import everything from environment variables
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import os
|
import os
|
||||||
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
### BASIC APP CONFIG
|
### BASIC APP CONFIG
|
||||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
FROM alpine:3.11 AS builder
|
FROM alpine:3.13 AS builder
|
||||||
LABEL maintainer="k@ndk.name"
|
LABEL maintainer="k@ndk.name"
|
||||||
|
|
||||||
ARG BUILD_DEPENDENCIES="build-base \
|
ARG BUILD_DEPENDENCIES="build-base \
|
||||||
|
@ -6,10 +6,10 @@ ARG BUILD_DEPENDENCIES="build-base \
|
||||||
libxml2-dev \
|
libxml2-dev \
|
||||||
mariadb-connector-c-dev \
|
mariadb-connector-c-dev \
|
||||||
openldap-dev \
|
openldap-dev \
|
||||||
py3-pip \
|
|
||||||
python3-dev \
|
python3-dev \
|
||||||
xmlsec-dev \
|
xmlsec-dev \
|
||||||
yarn"
|
yarn \
|
||||||
|
cargo"
|
||||||
|
|
||||||
ENV LC_ALL=en_US.UTF-8 \
|
ENV LC_ALL=en_US.UTF-8 \
|
||||||
LANG=en_US.UTF-8 \
|
LANG=en_US.UTF-8 \
|
||||||
|
@ -17,8 +17,11 @@ ENV LC_ALL=en_US.UTF-8 \
|
||||||
FLASK_APP=/build/powerdnsadmin/__init__.py
|
FLASK_APP=/build/powerdnsadmin/__init__.py
|
||||||
|
|
||||||
# Get dependencies
|
# Get dependencies
|
||||||
|
# py3-pip should not belong to BUILD_DEPENDENCIES. Otherwise, when we remove
|
||||||
|
# them with "apk del" at the end of build stage, the python requests module
|
||||||
|
# will be removed as well - (Tested with alpine:3.12 and python 3.8.5).
|
||||||
RUN apk add --no-cache ${BUILD_DEPENDENCIES} && \
|
RUN apk add --no-cache ${BUILD_DEPENDENCIES} && \
|
||||||
ln -s /usr/bin/pip3 /usr/bin/pip
|
apk add --no-cache py3-pip
|
||||||
|
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
|
|
||||||
|
@ -62,34 +65,31 @@ RUN mkdir -p /app && \
|
||||||
mkdir -p /app/configs && \
|
mkdir -p /app/configs && \
|
||||||
cp -r /build/configs/docker_config.py /app/configs
|
cp -r /build/configs/docker_config.py /app/configs
|
||||||
|
|
||||||
# Cleanup
|
|
||||||
RUN pip install pip-autoremove && \
|
|
||||||
pip-autoremove cssmin -y && \
|
|
||||||
pip-autoremove jsmin -y && \
|
|
||||||
pip-autoremove pytest -y && \
|
|
||||||
pip uninstall -y pip-autoremove && \
|
|
||||||
apk del ${BUILD_DEPENDENCIES}
|
|
||||||
|
|
||||||
|
|
||||||
# Build image
|
# Build image
|
||||||
FROM alpine:3.11
|
FROM alpine:3.13
|
||||||
|
|
||||||
ENV FLASK_APP=/app/powerdnsadmin/__init__.py
|
ENV FLASK_APP=/app/powerdnsadmin/__init__.py \
|
||||||
|
USER=pda
|
||||||
|
|
||||||
RUN apk add --no-cache mariadb-connector-c postgresql-client py3-gunicorn py3-psycopg2 xmlsec tzdata && \
|
RUN apk add --no-cache mariadb-connector-c postgresql-client py3-gunicorn py3-psycopg2 xmlsec tzdata libcap && \
|
||||||
addgroup -S pda && \
|
addgroup -S ${USER} && \
|
||||||
adduser -S -D -G pda pda && \
|
adduser -S -D -G ${USER} ${USER} && \
|
||||||
mkdir /data && \
|
mkdir /data && \
|
||||||
chown pda:pda /data
|
chown ${USER}:${USER} /data && \
|
||||||
|
setcap cap_net_bind_service=+ep $(readlink -f /usr/bin/python3) && \
|
||||||
|
apk del libcap
|
||||||
|
|
||||||
COPY --from=builder /usr/bin/flask /usr/bin/
|
COPY --from=builder /usr/bin/flask /usr/bin/
|
||||||
COPY --from=builder /usr/lib/python3.8/site-packages /usr/lib/python3.8/site-packages/
|
COPY --from=builder /usr/lib/python3.8/site-packages /usr/lib/python3.8/site-packages/
|
||||||
COPY --from=builder --chown=pda:pda /app /app/
|
COPY --from=builder --chown=root:${USER} /app /app/
|
||||||
COPY ./docker/entrypoint.sh /usr/bin/
|
COPY ./docker/entrypoint.sh /usr/bin/
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
RUN chown ${USER}:${USER} ./configs /app && \
|
||||||
|
cat ./powerdnsadmin/default_config.py ./configs/docker_config.py > ./powerdnsadmin/docker_config.py
|
||||||
|
|
||||||
EXPOSE 80/tcp
|
EXPOSE 80/tcp
|
||||||
|
USER ${USER}
|
||||||
HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1/"]
|
HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1/"]
|
||||||
ENTRYPOINT ["entrypoint.sh"]
|
ENTRYPOINT ["entrypoint.sh"]
|
||||||
CMD ["gunicorn","powerdnsadmin:create_app()","--user","pda","--group","pda"]
|
CMD ["gunicorn","powerdnsadmin:create_app()"]
|
||||||
|
|
|
@ -2,18 +2,14 @@
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd /app
|
cd /app
|
||||||
|
|
||||||
GUNICORN_TIMEOUT="${GUINCORN_TIMEOUT:-120}"
|
GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-120}"
|
||||||
GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}"
|
GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}"
|
||||||
GUNICORN_LOGLEVEL="${GUNICORN_LOGLEVEL:-info}"
|
GUNICORN_LOGLEVEL="${GUNICORN_LOGLEVEL:-info}"
|
||||||
BIND_ADDRESS="${BIND_ADDRESS:-0.0.0.0:80}"
|
BIND_ADDRESS="${BIND_ADDRESS:-0.0.0.0:80}"
|
||||||
|
|
||||||
cat ./powerdnsadmin/default_config.py ./configs/docker_config.py > ./powerdnsadmin/docker_config.py
|
|
||||||
|
|
||||||
GUNICORN_ARGS="-t ${GUNICORN_TIMEOUT} --workers ${GUNICORN_WORKERS} --bind ${BIND_ADDRESS} --log-level ${GUNICORN_LOGLEVEL}"
|
GUNICORN_ARGS="-t ${GUNICORN_TIMEOUT} --workers ${GUNICORN_WORKERS} --bind ${BIND_ADDRESS} --log-level ${GUNICORN_LOGLEVEL}"
|
||||||
if [ "$1" == gunicorn ]; then
|
if [ "$1" == gunicorn ]; then
|
||||||
# run as user pda so that if a SQLite database is generated it is writeable
|
/bin/sh -c "flask db upgrade"
|
||||||
# by that user
|
|
||||||
su pda -s /bin/sh -c "flask db upgrade"
|
|
||||||
exec "$@" $GUNICORN_ARGS
|
exec "$@" $GUNICORN_ARGS
|
||||||
|
|
||||||
else
|
else
|
||||||
|
|
123
docs/API.md
123
docs/API.md
|
@ -1,105 +1,134 @@
|
||||||
### API Usage
|
### API Usage
|
||||||
|
|
||||||
|
#### Getting started with docker
|
||||||
|
|
||||||
1. Run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
|
1. Run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
|
||||||
2. Click to register user, type e.g. user: admin and password: admin
|
2. Click to register user, type e.g. user: admin and password: admin
|
||||||
3. Login to UI in settings enable allow domain creation for users, now you can create and manage domains with admin account and also ordinary users
|
3. Login to UI in settings enable allow domain creation for users, now you can create and manage domains with admin account and also ordinary users
|
||||||
4. Encode your user and password to base64, in our example we have user admin and password admin so in linux cmd line we type:
|
4. Click on the API Keys menu then click on teh "Add Key" button to add a new Administrator Key
|
||||||
|
5. Keep the base64 encoded apikey somewhere safe as it won't be available in clear anymore
|
||||||
|
|
||||||
```
|
|
||||||
|
#### Accessing the API
|
||||||
|
|
||||||
|
The PDA API consists of two distinct parts:
|
||||||
|
|
||||||
|
- The /powerdnsadmin endpoints manages PDA content (accounts, users, apikeys) and also allow domain creation/deletion
|
||||||
|
- The /server endpoints are proxying queries to the backend PowerDNS instance's API. PDA acts as a proxy managing several API Keys and permissions to the PowerDNS content.
|
||||||
|
|
||||||
|
The requests to the API needs two headers:
|
||||||
|
|
||||||
|
- The classic 'Content-Type: application/json' is required to all POST and PUT requests, though it's harmless to use it on each call
|
||||||
|
- The authentication header to provide either the login:password basic authentication or the Api Key authentication.
|
||||||
|
|
||||||
|
When you access the `/powerdnsadmin` endpoint, you must use the Basic Auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Encode your user and password to base64
|
||||||
$ echo -n 'admin:admin'|base64
|
$ echo -n 'admin:admin'|base64
|
||||||
YWRtaW46YWRtaW4=
|
YWRtaW46YWRtaW4=
|
||||||
|
# Use the ouput as your basic auth header
|
||||||
|
curl -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X <method> <url>
|
||||||
```
|
```
|
||||||
|
|
||||||
we use generated output in basic authentication, we authenticate as user,
|
When you access the `/server` endpoint, you must use the ApiKey
|
||||||
with basic authentication, we can create/delete/get zone and create/delete/get/update apikeys
|
|
||||||
|
|
||||||
creating domain:
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use the already base64 encoded key in your header
|
||||||
|
curl -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' -X <method> <url>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Finally, the `/sync_domains` endpoint accepts both basic and apikey authentication
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
Creating domain via `/powerdnsadmin`:
|
||||||
|
|
||||||
|
```bash
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
|
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
|
||||||
```
|
```
|
||||||
|
|
||||||
creating apikey which has Administrator role, apikey can have also User role, when creating such apikey you have to specify also domain for which apikey is valid:
|
Creating an apikey which has the Administrator role:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
|
# Create the key
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
|
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
|
||||||
```
|
```
|
||||||
|
Example response (don't forget to save the plain key from the output)
|
||||||
|
|
||||||
call above will return response like this:
|
```json
|
||||||
|
[
|
||||||
```
|
{
|
||||||
[{"description": "samekey", "domains": [], "role": {"name": "Administrator", "id": 1}, "id": 2, "plain_key": "aGCthP3KLAeyjZI"}]
|
"accounts": [],
|
||||||
|
"description": "masterkey",
|
||||||
|
"domains": [],
|
||||||
|
"role": {
|
||||||
|
"name": "Administrator",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"plain_key": "aGCthP3KLAeyjZI"
|
||||||
|
}
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
we take plain_key and base64 encode it, this is the only time we can get API key in plain text and save it somewhere:
|
We can use the apikey for all calls to PowerDNS (don't forget to specify Content-Type):
|
||||||
|
|
||||||
```
|
Getting powerdns configuration (Administrator Key is needed):
|
||||||
$ echo -n 'aGCthP3KLAeyjZI'|base64
|
|
||||||
YUdDdGhQM0tMQWV5alpJ
|
|
||||||
```
|
|
||||||
|
|
||||||
We can use apikey for all calls specified in our API specification (it tries to follow powerdns API 1:1, only tsigkeys endpoints are not yet implemented), don't forget to specify Content-Type!
|
```bash
|
||||||
|
|
||||||
getting powerdns configuration:
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
|
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
|
||||||
```
|
```
|
||||||
|
|
||||||
creating and updating records:
|
Creating and updating records:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
|
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
|
||||||
```
|
```
|
||||||
|
|
||||||
getting domain:
|
Getting a domain:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
list zone records:
|
List a zone's records:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
add new record:
|
Add a new record:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||||
```
|
```
|
||||||
|
|
||||||
update record:
|
Update a record:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||||
```
|
```
|
||||||
|
|
||||||
delete record:
|
Delete a record:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
|
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
### Generate ER diagram
|
### Generate ER diagram
|
||||||
|
|
||||||
```
|
With docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install build packages
|
||||||
apt-get install python-dev graphviz libgraphviz-dev pkg-config
|
apt-get install python-dev graphviz libgraphviz-dev pkg-config
|
||||||
```
|
# Get the required python libraries
|
||||||
|
|
||||||
```
|
|
||||||
pip install graphviz mysqlclient ERAlchemy
|
pip install graphviz mysqlclient ERAlchemy
|
||||||
```
|
# Start the docker container
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
# Set environment variables
|
||||||
|
|
||||||
```
|
|
||||||
source .env
|
source .env
|
||||||
```
|
# Generate the diagrams
|
||||||
|
|
||||||
```
|
|
||||||
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
|
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
|
||||||
```
|
```
|
||||||
|
|
|
@ -18,3 +18,82 @@ Now you can enable the OAuth in PowerDNS-Admin.
|
||||||
* Restart PowerDNS-Admin
|
* Restart PowerDNS-Admin
|
||||||
|
|
||||||
This should allow you to log in using OAuth.
|
This should allow you to log in using OAuth.
|
||||||
|
|
||||||
|
#### Keycloak
|
||||||
|
|
||||||
|
To link to Keycloak for authentication, you need to create a new client in the Keycloak Administration Console.
|
||||||
|
* Log in to the Keycloak Administration Console
|
||||||
|
* Go to Clients > Create
|
||||||
|
* Enter a Client ID (for example 'powerdns-admin') and click 'Save'
|
||||||
|
* Scroll down to 'Access Type' and choose 'Confidential'.
|
||||||
|
* Scroll down to 'Valid Redirect URIs' and enter 'https://<pdnsa address>/oidc/authorized'
|
||||||
|
* Click 'Save'
|
||||||
|
* Go to the 'Credentials' tab and copy the Client Secret
|
||||||
|
* Log in to PowerDNS-Admin and go to 'Settings > Authentication > OpenID Connect OAuth'
|
||||||
|
* Enter the following details:
|
||||||
|
* Client key -> Client ID
|
||||||
|
* Client secret > Client secret copied from keycloak
|
||||||
|
* Scope: `profile`
|
||||||
|
* API URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/
|
||||||
|
* Token URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/token
|
||||||
|
* Authorize URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/auth
|
||||||
|
* Logout URL: https://<keycloak url>/auth/realms/<realm>/protocol/openid-connect/logout
|
||||||
|
* Leave the rest default
|
||||||
|
* Save the changes and restart PowerDNS-Admin
|
||||||
|
* Use the new 'Sign in using OpenID Connect' button to log in.
|
||||||
|
|
||||||
|
#### OpenID Connect OAuth
|
||||||
|
To link to oidc service for authenticationregister your PowerDNS-Admin in the OIDC Provider. This requires your PowerDNS-Admin web interface to use an HTTPS URL.
|
||||||
|
|
||||||
|
Enable OpenID Connect OAuth option.
|
||||||
|
* Client key, The client ID
|
||||||
|
* Scope, The scope of the data.
|
||||||
|
* API URL, <oidc_provider_link>/auth (The ending can be different with each provider)
|
||||||
|
* Token URL, <oidc_provider_link>/token
|
||||||
|
* Authorize URL, <oidc_provider_link>/auth
|
||||||
|
* Logout URL, <oidc_provider_link>/logout
|
||||||
|
|
||||||
|
* Username, This will be the claim that will be used as the username. (Usually preferred_username)
|
||||||
|
* First Name, This will be the firstname of the user. (Usually given_name)
|
||||||
|
* Last Name, This will be the lastname of the user. (Usually family_name)
|
||||||
|
* Email, This will be the email of the user. (Usually email)
|
||||||
|
|
||||||
|
#### To create accounts on oidc login use the following properties:
|
||||||
|
* Autoprovision Account Name Property, This property will set the name of the created account.
|
||||||
|
This property can be a string or a list.
|
||||||
|
* Autoprovision Account Description Property, This property will set the description of the created account.
|
||||||
|
This property can be a string or a list.
|
||||||
|
|
||||||
|
If we get a variable named "groups" and "groups_description" from our IdP.
|
||||||
|
This variable contains groups that the user is a part of.
|
||||||
|
We will put the variable name "groups" in the "Name Property" and "groups_description" in the "Description Property".
|
||||||
|
This will result in the following account being created:
|
||||||
|
Input we get from the Idp:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
"preferred_username": "example_username",
|
||||||
|
"given_name": "example_firstame",
|
||||||
|
"family_name": "example_lastname",
|
||||||
|
"email": "example_email",
|
||||||
|
"groups": ["github", "gitlab"]
|
||||||
|
"groups_description": ["github.com", "gitlab.com"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The user properties will be:
|
||||||
|
```
|
||||||
|
Username: customer_username
|
||||||
|
First Name: customer_firstame
|
||||||
|
Last Name: customer_lastname
|
||||||
|
Email: customer_email
|
||||||
|
Role: User
|
||||||
|
```
|
||||||
|
|
||||||
|
The groups properties will be:
|
||||||
|
```
|
||||||
|
Name: github Description: github.com Members: example_username
|
||||||
|
Name: gitlab Description: gitlab.com Members: example_username
|
||||||
|
```
|
||||||
|
|
||||||
|
If the option "delete_sso_accounts" is turned on the user will only be apart of groups the IdP provided and removed from all other accoubnts.
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""add apikey account mapping table
|
||||||
|
|
||||||
|
Revision ID: 0967658d9c0d
|
||||||
|
Revises: 0d3d93f1c2e0
|
||||||
|
Create Date: 2021-11-13 22:28:46.133474
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0967658d9c0d'
|
||||||
|
down_revision = '0d3d93f1c2e0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('apikey_account',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('apikey_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('account_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['account_id'], ['account.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['apikey_id'], ['apikey.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_history_created_on'), ['created_on'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_history_created_on'))
|
||||||
|
|
||||||
|
op.drop_table('apikey_account')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -0,0 +1,34 @@
|
||||||
|
"""Add domain_id to history table
|
||||||
|
|
||||||
|
Revision ID: 0d3d93f1c2e0
|
||||||
|
Revises: 3f76448bb6de
|
||||||
|
Create Date: 2021-02-15 17:23:05.688241
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0d3d93f1c2e0'
|
||||||
|
down_revision = '3f76448bb6de'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('domain_id', sa.Integer(), nullable=True))
|
||||||
|
batch_op.create_foreign_key('fk_domain_id', 'domain', ['domain_id'], ['id'])
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||||
|
batch_op.drop_constraint('fk_domain_id', type_='foreignkey')
|
||||||
|
batch_op.drop_column('domain_id')
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
|
@ -2,6 +2,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"admin-lte": "2.4.9",
|
"admin-lte": "2.4.9",
|
||||||
"bootstrap": "^3.4.1",
|
"bootstrap": "^3.4.1",
|
||||||
|
"bootstrap-datepicker": "^1.8.0",
|
||||||
"bootstrap-validator": "^0.11.9",
|
"bootstrap-validator": "^0.11.9",
|
||||||
"datatables.net-plugins": "^1.10.19",
|
"datatables.net-plugins": "^1.10.19",
|
||||||
"icheck": "^1.0.2",
|
"icheck": "^1.0.2",
|
||||||
|
|
|
@ -4,6 +4,7 @@ from flask import Flask
|
||||||
from flask_seasurf import SeaSurf
|
from flask_seasurf import SeaSurf
|
||||||
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 .lib import utils
|
from .lib import utils
|
||||||
|
|
||||||
|
@ -45,6 +46,17 @@ def create_app(config=None):
|
||||||
csrf.exempt(routes.api.api_zone_subpath_forward)
|
csrf.exempt(routes.api.api_zone_subpath_forward)
|
||||||
csrf.exempt(routes.api.api_zone_forward)
|
csrf.exempt(routes.api.api_zone_forward)
|
||||||
csrf.exempt(routes.api.api_create_zone)
|
csrf.exempt(routes.api.api_create_zone)
|
||||||
|
csrf.exempt(routes.api.api_create_account)
|
||||||
|
csrf.exempt(routes.api.api_delete_account)
|
||||||
|
csrf.exempt(routes.api.api_update_account)
|
||||||
|
csrf.exempt(routes.api.api_create_user)
|
||||||
|
csrf.exempt(routes.api.api_delete_user)
|
||||||
|
csrf.exempt(routes.api.api_update_user)
|
||||||
|
csrf.exempt(routes.api.api_list_account_users)
|
||||||
|
csrf.exempt(routes.api.api_add_account_user)
|
||||||
|
csrf.exempt(routes.api.api_remove_account_user)
|
||||||
|
csrf.exempt(routes.api.api_zone_cryptokeys)
|
||||||
|
csrf.exempt(routes.api.api_zone_cryptokey)
|
||||||
|
|
||||||
# Load config from env variables if using docker
|
# Load config from env variables if using docker
|
||||||
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
|
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
|
||||||
|
@ -69,6 +81,12 @@ def create_app(config=None):
|
||||||
from flask_sslify import SSLify
|
from flask_sslify import SSLify
|
||||||
_sslify = SSLify(app) # lgtm [py/unused-local-variable]
|
_sslify = SSLify(app) # lgtm [py/unused-local-variable]
|
||||||
|
|
||||||
|
# Load Flask-Session
|
||||||
|
if app.config.get('FILESYSTEM_SESSIONS_ENABLED'):
|
||||||
|
app.config['SESSION_TYPE'] = 'filesystem'
|
||||||
|
sess = Session()
|
||||||
|
sess.init_app(app)
|
||||||
|
|
||||||
# SMTP
|
# SMTP
|
||||||
app.mail = Mail(app)
|
app.mail = Mail(app)
|
||||||
|
|
||||||
|
@ -86,6 +104,7 @@ def create_app(config=None):
|
||||||
'email_to_gravatar_url'] = utils.email_to_gravatar_url
|
'email_to_gravatar_url'] = utils.email_to_gravatar_url
|
||||||
app.jinja_env.filters[
|
app.jinja_env.filters[
|
||||||
'display_setting_state'] = utils.display_setting_state
|
'display_setting_state'] = utils.display_setting_state
|
||||||
|
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
|
||||||
|
|
||||||
# Register context proccessors
|
# Register context proccessors
|
||||||
from .models.setting import Setting
|
from .models.setting import Setting
|
||||||
|
|
|
@ -23,6 +23,7 @@ css_login = Bundle('node_modules/bootstrap/dist/css/bootstrap.css',
|
||||||
js_login = Bundle('node_modules/jquery/dist/jquery.js',
|
js_login = Bundle('node_modules/jquery/dist/jquery.js',
|
||||||
'node_modules/bootstrap/dist/js/bootstrap.js',
|
'node_modules/bootstrap/dist/js/bootstrap.js',
|
||||||
'node_modules/icheck/icheck.js',
|
'node_modules/icheck/icheck.js',
|
||||||
|
'custom/js/custom.js',
|
||||||
filters=(ConcatFilter, 'jsmin'),
|
filters=(ConcatFilter, 'jsmin'),
|
||||||
output='generated/login.js')
|
output='generated/login.js')
|
||||||
|
|
||||||
|
@ -39,6 +40,7 @@ css_main = Bundle(
|
||||||
'node_modules/admin-lte/dist/css/AdminLTE.css',
|
'node_modules/admin-lte/dist/css/AdminLTE.css',
|
||||||
'node_modules/admin-lte/dist/css/skins/_all-skins.css',
|
'node_modules/admin-lte/dist/css/skins/_all-skins.css',
|
||||||
'custom/css/custom.css',
|
'custom/css/custom.css',
|
||||||
|
'node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.css',
|
||||||
filters=('cssmin', 'cssrewrite'),
|
filters=('cssmin', 'cssrewrite'),
|
||||||
output='generated/main.css')
|
output='generated/main.css')
|
||||||
|
|
||||||
|
@ -58,6 +60,7 @@ js_main = Bundle('node_modules/jquery/dist/jquery.js',
|
||||||
'node_modules/jtimeout/src/jTimeout.js',
|
'node_modules/jtimeout/src/jTimeout.js',
|
||||||
'node_modules/jquery.quicksearch/src/jquery.quicksearch.js',
|
'node_modules/jquery.quicksearch/src/jquery.quicksearch.js',
|
||||||
'custom/js/custom.js',
|
'custom/js/custom.js',
|
||||||
|
'node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js',
|
||||||
filters=(ConcatFilter, 'jsmin'),
|
filters=(ConcatFilter, 'jsmin'),
|
||||||
output='generated/main.js')
|
output='generated/main.js')
|
||||||
|
|
||||||
|
|
|
@ -8,7 +8,6 @@ from .models import User, ApiKey, Setting, Domain, Setting
|
||||||
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges
|
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges
|
||||||
from .lib.errors import DomainAccessForbidden
|
from .lib.errors import DomainAccessForbidden
|
||||||
|
|
||||||
|
|
||||||
def admin_role_required(f):
|
def admin_role_required(f):
|
||||||
"""
|
"""
|
||||||
Grant access if user is in Administrator role
|
Grant access if user is in Administrator role
|
||||||
|
@ -35,6 +34,21 @@ def operator_role_required(f):
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def history_access_required(f):
|
||||||
|
"""
|
||||||
|
Grant access if user is in Operator role or higher, or Users can view history
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if current_user.role.name not in [
|
||||||
|
'Administrator', 'Operator'
|
||||||
|
] and not Setting().get('allow_user_view_history'):
|
||||||
|
abort(403)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
def can_access_domain(f):
|
def can_access_domain(f):
|
||||||
"""
|
"""
|
||||||
Grant access if:
|
Grant access if:
|
||||||
|
@ -79,6 +93,23 @@ def can_configure_dnssec(f):
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
def can_remove_domain(f):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in Operator role or higher, or
|
||||||
|
- allow_user_remove_domain is on
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if current_user.role.name not in [
|
||||||
|
'Administrator', 'Operator'
|
||||||
|
] and not Setting().get('allow_user_remove_domain'):
|
||||||
|
abort(403)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def can_create_domain(f):
|
def can_create_domain(f):
|
||||||
"""
|
"""
|
||||||
|
@ -161,6 +192,59 @@ def is_json(f):
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def callback_if_request_body_contains_key(callback, http_methods=[], keys=[]):
|
||||||
|
"""
|
||||||
|
If request body contains one or more of specified keys, call
|
||||||
|
:param callback
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
if (check_current_http_method and
|
||||||
|
set(request.get_json(force=True).keys()).intersection(set(keys))
|
||||||
|
):
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
def api_role_can(action, roles=None, allow_self=False):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in the permitted roles
|
||||||
|
- allow_self and kwargs['user_id'] = current_user.id
|
||||||
|
- allow_self and kwargs['username'] = current_user.username
|
||||||
|
"""
|
||||||
|
if roles is None:
|
||||||
|
roles = ['Administrator', 'Operator']
|
||||||
|
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
try:
|
||||||
|
user_id = int(kwargs.get('user_id'))
|
||||||
|
except:
|
||||||
|
user_id = None
|
||||||
|
try:
|
||||||
|
username = kwargs.get('username')
|
||||||
|
except:
|
||||||
|
username = None
|
||||||
|
if (
|
||||||
|
(current_user.role.name in roles) or
|
||||||
|
(allow_self and user_id and current_user.id == user_id) or
|
||||||
|
(allow_self and username and current_user.username == username)
|
||||||
|
):
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
msg = (
|
||||||
|
"User {} with role {} does not have enough privileges to {}"
|
||||||
|
).format(current_user.username, current_user.role.name, action)
|
||||||
|
raise NotEnoughPrivileges(message=msg)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def api_can_create_domain(f):
|
def api_can_create_domain(f):
|
||||||
"""
|
"""
|
||||||
Grant access if:
|
Grant access if:
|
||||||
|
@ -180,6 +264,48 @@ def api_can_create_domain(f):
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def apikey_can_create_domain(f):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in Operator role or higher, or
|
||||||
|
- allow_user_create_domain is on
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if g.apikey.role.name not in [
|
||||||
|
'Administrator', 'Operator'
|
||||||
|
] and not Setting().get('allow_user_create_domain'):
|
||||||
|
msg = "ApiKey #{0} does not have enough privileges to create domain"
|
||||||
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
|
raise NotEnoughPrivileges()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def apikey_can_remove_domain(http_methods=[]):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in Operator role or higher, or
|
||||||
|
- allow_user_remove_domain is on
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
|
||||||
|
if (check_current_http_method and
|
||||||
|
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||||
|
not Setting().get('allow_user_remove_domain')
|
||||||
|
):
|
||||||
|
msg = "ApiKey #{0} does not have enough privileges to remove domain"
|
||||||
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
|
raise NotEnoughPrivileges()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def apikey_is_admin(f):
|
def apikey_is_admin(f):
|
||||||
"""
|
"""
|
||||||
Grant access if user is in Administrator role
|
Grant access if user is in Administrator role
|
||||||
|
@ -196,21 +322,52 @@ def apikey_is_admin(f):
|
||||||
|
|
||||||
|
|
||||||
def apikey_can_access_domain(f):
|
def apikey_can_access_domain(f):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user has Operator role or higher, or
|
||||||
|
- user has explicitly been granted access to domain
|
||||||
|
"""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
apikey = g.apikey
|
|
||||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
||||||
domains = apikey.domains
|
zone_id = kwargs.get('zone_id').rstrip(".")
|
||||||
zone_id = kwargs.get('zone_id')
|
domain_names = [item.name for item in g.apikey.domains]
|
||||||
domain_names = [item.name for item in domains]
|
|
||||||
|
|
||||||
if zone_id not in domain_names:
|
accounts = g.apikey.accounts
|
||||||
|
accounts_domains = [domain.name for a in accounts for domain in a.domains]
|
||||||
|
|
||||||
|
allowed_domains = set(domain_names + accounts_domains)
|
||||||
|
|
||||||
|
if zone_id not in allowed_domains:
|
||||||
raise DomainAccessForbidden()
|
raise DomainAccessForbidden()
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def apikey_can_configure_dnssec(http_methods=[]):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in Operator role or higher, or
|
||||||
|
- dnssec_admins_only is off
|
||||||
|
"""
|
||||||
|
def decorator(f=None):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
|
||||||
|
if (check_current_http_method and
|
||||||
|
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||||
|
Setting().get('dnssec_admins_only')
|
||||||
|
):
|
||||||
|
msg = "ApiKey #{0} does not have enough privileges to configure dnssec"
|
||||||
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
|
raise DomainAccessForbidden(message=msg)
|
||||||
|
return f(*args, **kwargs) if f else None
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def apikey_auth(f):
|
def apikey_auth(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
|
@ -256,3 +413,13 @@ def dyndns_login_required(f):
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
def apikey_or_basic_auth(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
api_auth_header = request.headers.get('X-API-KEY')
|
||||||
|
if api_auth_header:
|
||||||
|
return apikey_auth(f)(*args, **kwargs)
|
||||||
|
else:
|
||||||
|
return api_basic_auth(f)(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import os
|
import os
|
||||||
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
|
import urllib.parse
|
||||||
|
basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
### BASIC APP CONFIG
|
### BASIC APP CONFIG
|
||||||
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
|
||||||
|
@ -8,6 +9,7 @@ BIND_ADDRESS = '0.0.0.0'
|
||||||
PORT = 9191
|
PORT = 9191
|
||||||
HSTS_ENABLED = False
|
HSTS_ENABLED = False
|
||||||
OFFLINE_MODE = False
|
OFFLINE_MODE = False
|
||||||
|
FILESYSTEM_SESSIONS_ENABLED = False
|
||||||
|
|
||||||
### DATABASE CONFIG
|
### DATABASE CONFIG
|
||||||
SQLA_DB_USER = 'pda'
|
SQLA_DB_USER = 'pda'
|
||||||
|
@ -16,10 +18,15 @@ SQLA_DB_HOST = '127.0.0.1'
|
||||||
SQLA_DB_NAME = 'pda'
|
SQLA_DB_NAME = 'pda'
|
||||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
|
||||||
### DATBASE - MySQL
|
### DATABASE - MySQL
|
||||||
SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME
|
SQLALCHEMY_DATABASE_URI = 'mysql://{}:{}@{}/{}'.format(
|
||||||
|
urllib.parse.quote_plus(SQLA_DB_USER),
|
||||||
|
urllib.parse.quote_plus(SQLA_DB_PASSWORD),
|
||||||
|
SQLA_DB_HOST,
|
||||||
|
SQLA_DB_NAME
|
||||||
|
)
|
||||||
|
|
||||||
### DATABSE - SQLite
|
### DATABASE - SQLite
|
||||||
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
|
||||||
|
|
||||||
# SAML Authnetication
|
# SAML Authnetication
|
||||||
|
|
|
@ -60,7 +60,8 @@ class ApiKeyNotUsable(StructuredException):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name=None,
|
name=None,
|
||||||
message="Api key must have domains or have administrative role"):
|
message=("Api key must have domains or accounts"
|
||||||
|
" or an administrative role")):
|
||||||
StructuredException.__init__(self)
|
StructuredException.__init__(self)
|
||||||
self.message = message
|
self.message = message
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -82,3 +83,91 @@ class RequestIsNotJSON(StructuredException):
|
||||||
StructuredException.__init__(self)
|
StructuredException.__init__(self)
|
||||||
self.message = message
|
self.message = message
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class AccountCreateFail(StructuredException):
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Creation of account failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class AccountCreateDuplicate(StructuredException):
|
||||||
|
status_code = 409
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Creation of account failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class AccountUpdateFail(StructuredException):
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Update of account failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class AccountDeleteFail(StructuredException):
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Delete of account failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class AccountNotExists(StructuredException):
|
||||||
|
status_code = 404
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Account does not exist"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreateFail(StructuredException):
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Creation of user failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class UserCreateDuplicate(StructuredException):
|
||||||
|
status_code = 409
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Creation of user failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
class UserUpdateFail(StructuredException):
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Update of user failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
class UserUpdateFailEmail(StructuredException):
|
||||||
|
status_code = 409
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Update of user failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class UserDeleteFail(StructuredException):
|
||||||
|
status_code = 500
|
||||||
|
|
||||||
|
def __init__(self, name=None, message="Delete of user failed"):
|
||||||
|
StructuredException.__init__(self)
|
||||||
|
self.message = message
|
||||||
|
self.name = name
|
||||||
|
|
|
@ -11,10 +11,21 @@ class RoleSchema(Schema):
|
||||||
name = fields.String()
|
name = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class AccountSummarySchema(Schema):
|
||||||
|
id = fields.Integer()
|
||||||
|
name = fields.String()
|
||||||
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
|
||||||
|
class ApiKeySummarySchema(Schema):
|
||||||
|
id = fields.Integer()
|
||||||
|
description = fields.String()
|
||||||
|
|
||||||
|
|
||||||
class ApiKeySchema(Schema):
|
class ApiKeySchema(Schema):
|
||||||
id = fields.Integer()
|
id = fields.Integer()
|
||||||
role = fields.Embed(schema=RoleSchema)
|
role = fields.Embed(schema=RoleSchema)
|
||||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||||
description = fields.String()
|
description = fields.String()
|
||||||
key = fields.String()
|
key = fields.String()
|
||||||
|
|
||||||
|
@ -23,5 +34,33 @@ class ApiPlainKeySchema(Schema):
|
||||||
id = fields.Integer()
|
id = fields.Integer()
|
||||||
role = fields.Embed(schema=RoleSchema)
|
role = fields.Embed(schema=RoleSchema)
|
||||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||||
description = fields.String()
|
description = fields.String()
|
||||||
plain_key = fields.String()
|
plain_key = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class UserSchema(Schema):
|
||||||
|
id = fields.Integer()
|
||||||
|
username = fields.String()
|
||||||
|
firstname = fields.String()
|
||||||
|
lastname = fields.String()
|
||||||
|
email = fields.String()
|
||||||
|
role = fields.Embed(schema=RoleSchema)
|
||||||
|
|
||||||
|
class UserDetailedSchema(Schema):
|
||||||
|
id = fields.Integer()
|
||||||
|
username = fields.String()
|
||||||
|
firstname = fields.String()
|
||||||
|
lastname = fields.String()
|
||||||
|
email = fields.String()
|
||||||
|
role = fields.Embed(schema=RoleSchema)
|
||||||
|
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||||
|
|
||||||
|
class AccountSchema(Schema):
|
||||||
|
id = fields.Integer()
|
||||||
|
name = fields.String()
|
||||||
|
description = fields.String()
|
||||||
|
contact = fields.String()
|
||||||
|
mail = fields.String()
|
||||||
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
apikeys = fields.Embed(schema=ApiKeySummarySchema, many=True)
|
||||||
|
|
|
@ -5,6 +5,7 @@ import requests
|
||||||
import hashlib
|
import hashlib
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
from distutils.version import StrictVersion
|
from distutils.version import StrictVersion
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
|
@ -103,6 +104,13 @@ def fetch_json(remote_url,
|
||||||
data = None
|
data = None
|
||||||
try:
|
try:
|
||||||
data = json.loads(r.content.decode('utf-8'))
|
data = json.loads(r.content.decode('utf-8'))
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# If the decoding fails, switch to slower but probably working .json()
|
||||||
|
try:
|
||||||
|
logging.warning("UTF-8 content.decode failed, switching to slower .json method")
|
||||||
|
data = r.json()
|
||||||
|
except Exception as e:
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
'Error while loading JSON data from {0}'.format(remote_url)) from e
|
'Error while loading JSON data from {0}'.format(remote_url)) from e
|
||||||
|
@ -212,6 +220,15 @@ def pretty_json(data):
|
||||||
return json.dumps(data, sort_keys=True, indent=4)
|
return json.dumps(data, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_list(l):
|
||||||
|
if not l:
|
||||||
|
l = []
|
||||||
|
elif not isinstance(l, Iterable) or isinstance(l, str):
|
||||||
|
l = [l]
|
||||||
|
|
||||||
|
yield from l
|
||||||
|
|
||||||
|
|
||||||
class customBoxes:
|
class customBoxes:
|
||||||
boxes = {
|
boxes = {
|
||||||
"reverse": (" ", " "),
|
"reverse": (" ", " "),
|
||||||
|
@ -219,3 +236,22 @@ class customBoxes:
|
||||||
"inaddrarpa": ("in-addr", "%.in-addr.arpa")
|
"inaddrarpa": ("in-addr", "%.in-addr.arpa")
|
||||||
}
|
}
|
||||||
order = ["reverse", "ip6arpa", "inaddrarpa"]
|
order = ["reverse", "ip6arpa", "inaddrarpa"]
|
||||||
|
|
||||||
|
def pretty_domain_name(value):
|
||||||
|
"""
|
||||||
|
Display domain name in original format.
|
||||||
|
If it is IDN domain (Punycode starts with xn--), do the
|
||||||
|
idna decoding.
|
||||||
|
Note that any part of the domain name can be individually punycoded
|
||||||
|
"""
|
||||||
|
if isinstance(value, str):
|
||||||
|
if value.startswith('xn--') \
|
||||||
|
or value.find('.xn--') != -1:
|
||||||
|
try:
|
||||||
|
return value.encode().decode('idna')
|
||||||
|
except:
|
||||||
|
raise Exception("Cannot decode IDN domain")
|
||||||
|
else:
|
||||||
|
return value
|
||||||
|
else:
|
||||||
|
raise Exception("Require the Punycode in string format")
|
||||||
|
|
|
@ -8,6 +8,7 @@ from .account_user import AccountUser
|
||||||
from .server import Server
|
from .server import Server
|
||||||
from .history import History
|
from .history import History
|
||||||
from .api_key import ApiKey
|
from .api_key import ApiKey
|
||||||
|
from .api_key_account import ApiKeyAccount
|
||||||
from .setting import Setting
|
from .setting import Setting
|
||||||
from .domain import Domain
|
from .domain import Domain
|
||||||
from .domain_setting import DomainSetting
|
from .domain_setting import DomainSetting
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
|
import traceback
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
|
from ..lib import utils
|
||||||
from .base import db
|
from .base import db
|
||||||
|
from .setting import Setting
|
||||||
from .user import User
|
from .user import User
|
||||||
from .account_user import AccountUser
|
from .account_user import AccountUser
|
||||||
|
|
||||||
|
@ -13,6 +17,9 @@ class Account(db.Model):
|
||||||
contact = db.Column(db.String(128))
|
contact = db.Column(db.String(128))
|
||||||
mail = db.Column(db.String(128))
|
mail = db.Column(db.String(128))
|
||||||
domains = db.relationship("Domain", back_populates="account")
|
domains = db.relationship("Domain", back_populates="account")
|
||||||
|
apikeys = db.relationship("ApiKey",
|
||||||
|
secondary="apikey_account",
|
||||||
|
back_populates="accounts")
|
||||||
|
|
||||||
def __init__(self, name=None, description=None, contact=None, mail=None):
|
def __init__(self, name=None, description=None, contact=None, mail=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -20,6 +27,12 @@ class Account(db.Model):
|
||||||
self.contact = contact
|
self.contact = contact
|
||||||
self.mail = mail
|
self.mail = mail
|
||||||
|
|
||||||
|
# PDNS configs
|
||||||
|
self.PDNS_STATS_URL = Setting().get('pdns_api_url')
|
||||||
|
self.PDNS_API_KEY = Setting().get('pdns_api_key')
|
||||||
|
self.PDNS_VERSION = Setting().get('pdns_version')
|
||||||
|
self.API_EXTENDED_URL = utils.pdns_api_extended_uri(self.PDNS_VERSION)
|
||||||
|
|
||||||
if self.name is not None:
|
if self.name is not None:
|
||||||
self.name = ''.join(c for c in self.name.lower()
|
self.name = ''.join(c for c in self.name.lower()
|
||||||
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||||
|
@ -88,7 +101,7 @@ class Account(db.Model):
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return {'status': True, 'msg': 'Account updated successfully'}
|
return {'status': True, 'msg': 'Account updated successfully'}
|
||||||
|
|
||||||
def delete_account(self):
|
def delete_account(self, commit=True):
|
||||||
"""
|
"""
|
||||||
Delete an account
|
Delete an account
|
||||||
"""
|
"""
|
||||||
|
@ -97,13 +110,14 @@ class Account(db.Model):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
Account.query.filter(Account.name == self.name).delete()
|
Account.query.filter(Account.name == self.name).delete()
|
||||||
db.session.commit()
|
if commit:
|
||||||
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
'Cannot delete account {0} from DB. DETAIL: {1}'.format(
|
'Cannot delete account {0} from DB. DETAIL: {1}'.format(
|
||||||
self.username, e))
|
self.name, e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_user(self):
|
def get_user(self):
|
||||||
|
@ -200,3 +214,59 @@ class Account(db.Model):
|
||||||
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
|
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
|
||||||
format(self.name, e))
|
format(self.name, e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def update(self):
|
||||||
|
"""
|
||||||
|
Fetch accounts from PowerDNS and syncs them into DB
|
||||||
|
"""
|
||||||
|
db_accounts = Account.query.all()
|
||||||
|
list_db_accounts = [d.name for d in db_accounts]
|
||||||
|
current_app.logger.info("Found {} accounts in PowerDNS-Admin".format(
|
||||||
|
len(list_db_accounts)))
|
||||||
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
|
try:
|
||||||
|
jdata = utils.fetch_json(
|
||||||
|
urljoin(self.PDNS_STATS_URL,
|
||||||
|
self.API_EXTENDED_URL + '/servers/localhost/zones'),
|
||||||
|
headers=headers,
|
||||||
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
|
verify=Setting().get('verify_ssl_connections'))
|
||||||
|
list_jaccount = set(d['account'] for d in jdata if d['account'])
|
||||||
|
current_app.logger.info("Found {} accounts in PowerDNS".format(
|
||||||
|
len(list_jaccount)))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Remove accounts that don't exist any more
|
||||||
|
should_removed_db_account = list(
|
||||||
|
set(list_db_accounts).difference(list_jaccount))
|
||||||
|
for account_name in should_removed_db_account:
|
||||||
|
account_id = self.get_id_by_name(account_name)
|
||||||
|
if not account_id:
|
||||||
|
continue
|
||||||
|
current_app.logger.info("Deleting account for {0}".format(account_name))
|
||||||
|
account = Account.query.get(account_id)
|
||||||
|
account.delete_account(commit=False)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error(
|
||||||
|
'Can not delete account from DB. DETAIL: {0}'.format(e))
|
||||||
|
current_app.logger.debug(traceback.format_exc())
|
||||||
|
|
||||||
|
for account_name in list_jaccount:
|
||||||
|
account_id = self.get_id_by_name(account_name)
|
||||||
|
if account_id:
|
||||||
|
continue
|
||||||
|
current_app.logger.info("Creating account for {0}".format(account_name))
|
||||||
|
account = Account(name=account_name)
|
||||||
|
db.session.add(account)
|
||||||
|
|
||||||
|
db.session.commit()
|
||||||
|
current_app.logger.info('Update accounts finished')
|
||||||
|
return {
|
||||||
|
'status': 'ok',
|
||||||
|
'msg': 'Account table has been updated successfully'
|
||||||
|
}
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(
|
||||||
|
'Cannot update account table. Error: {0}'.format(e))
|
||||||
|
return {'status': 'error', 'msg': 'Cannot update account table'}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import random
|
import secrets
|
||||||
import string
|
import string
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from .base import db, domain_apikey
|
from .base import db
|
||||||
from ..models.role import Role
|
from ..models.role import Role
|
||||||
from ..models.domain import Domain
|
from ..models.domain import Domain
|
||||||
|
from ..models.account import Account
|
||||||
|
|
||||||
class ApiKey(db.Model):
|
class ApiKey(db.Model):
|
||||||
__tablename__ = "apikey"
|
__tablename__ = "apikey"
|
||||||
|
@ -16,17 +16,21 @@ class ApiKey(db.Model):
|
||||||
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
||||||
role = db.relationship('Role', back_populates="apikeys", lazy=True)
|
role = db.relationship('Role', back_populates="apikeys", lazy=True)
|
||||||
domains = db.relationship("Domain",
|
domains = db.relationship("Domain",
|
||||||
secondary=domain_apikey,
|
secondary="domain_apikey",
|
||||||
back_populates="apikeys")
|
back_populates="apikeys")
|
||||||
|
accounts = db.relationship("Account",
|
||||||
|
secondary="apikey_account",
|
||||||
|
back_populates="apikeys")
|
||||||
|
|
||||||
def __init__(self, key=None, desc=None, role_name=None, domains=[]):
|
def __init__(self, key=None, desc=None, role_name=None, domains=[], accounts=[]):
|
||||||
self.id = None
|
self.id = None
|
||||||
self.description = desc
|
self.description = desc
|
||||||
self.role_name = role_name
|
self.role_name = role_name
|
||||||
self.domains[:] = domains
|
self.domains[:] = domains
|
||||||
|
self.accounts[:] = accounts
|
||||||
if not key:
|
if not key:
|
||||||
rand_key = ''.join(
|
rand_key = ''.join(
|
||||||
random.choice(string.ascii_letters + string.digits)
|
secrets.choice(string.ascii_letters + string.digits)
|
||||||
for _ in range(15))
|
for _ in range(15))
|
||||||
self.plain_key = rand_key
|
self.plain_key = rand_key
|
||||||
self.key = self.get_hashed_password(rand_key).decode('utf-8')
|
self.key = self.get_hashed_password(rand_key).decode('utf-8')
|
||||||
|
@ -54,7 +58,7 @@ class ApiKey(db.Model):
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def update(self, role_name=None, description=None, domains=None):
|
def update(self, role_name=None, description=None, domains=None, accounts=None):
|
||||||
try:
|
try:
|
||||||
if role_name:
|
if role_name:
|
||||||
role = Role.query.filter(Role.name == role_name).first()
|
role = Role.query.filter(Role.name == role_name).first()
|
||||||
|
@ -63,12 +67,18 @@ class ApiKey(db.Model):
|
||||||
if description:
|
if description:
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
if domains:
|
if domains is not None:
|
||||||
domain_object_list = Domain.query \
|
domain_object_list = Domain.query \
|
||||||
.filter(Domain.name.in_(domains)) \
|
.filter(Domain.name.in_(domains)) \
|
||||||
.all()
|
.all()
|
||||||
self.domains[:] = domain_object_list
|
self.domains[:] = domain_object_list
|
||||||
|
|
||||||
|
if accounts is not None:
|
||||||
|
account_object_list = Account.query \
|
||||||
|
.filter(Account.name.in_(accounts)) \
|
||||||
|
.all()
|
||||||
|
self.accounts[:] = account_object_list
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg_str = 'Update of apikey failed. Error: {0}'
|
msg_str = 'Update of apikey failed. Error: {0}'
|
||||||
|
@ -87,13 +97,22 @@ class ApiKey(db.Model):
|
||||||
else:
|
else:
|
||||||
pw = self.plain_text_password
|
pw = self.plain_text_password
|
||||||
|
|
||||||
|
# The salt value is currently re-used here intentionally because
|
||||||
|
# the implementation relies on just the API key's value itself
|
||||||
|
# for database lookup: ApiKey.is_validate() would have no way of
|
||||||
|
# discerning whether any given key is valid if bcrypt.gensalt()
|
||||||
|
# was used. As far as is known, this is fine as long as the
|
||||||
|
# value of new API keys is randomly generated in a
|
||||||
|
# cryptographically secure fashion, as this then makes
|
||||||
|
# expendable as an exception the otherwise vital protection of
|
||||||
|
# proper salting as provided by bcrypt.gensalt().
|
||||||
return bcrypt.hashpw(pw.encode('utf-8'),
|
return bcrypt.hashpw(pw.encode('utf-8'),
|
||||||
current_app.config.get('SALT').encode('utf-8'))
|
current_app.config.get('SALT').encode('utf-8'))
|
||||||
|
|
||||||
def check_password(self, hashed_password):
|
def check_password(self, hashed_password):
|
||||||
# Check hased password. Using bcrypt,
|
# Check hashed password. Using bcrypt,
|
||||||
# the salt is saved into the hash itself
|
# the salt is saved into the hash itself
|
||||||
if (self.plain_text_password):
|
if self.plain_text_password:
|
||||||
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
|
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
|
||||||
hashed_password.encode('utf-8'))
|
hashed_password.encode('utf-8'))
|
||||||
return False
|
return False
|
||||||
|
@ -112,3 +131,12 @@ class ApiKey(db.Model):
|
||||||
raise Exception("Unauthorized")
|
raise Exception("Unauthorized")
|
||||||
|
|
||||||
return apikey
|
return apikey
|
||||||
|
|
||||||
|
def associate_account(self, account):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def dissociate_account(self, account):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_accounts(self):
|
||||||
|
return True
|
||||||
|
|
20
powerdnsadmin/models/api_key_account.py
Normal file
20
powerdnsadmin/models/api_key_account.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from .base import db
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyAccount(db.Model):
|
||||||
|
__tablename__ = 'apikey_account'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
apikey_id = db.Column(db.Integer,
|
||||||
|
db.ForeignKey('apikey.id'),
|
||||||
|
nullable=False)
|
||||||
|
account_id = db.Column(db.Integer,
|
||||||
|
db.ForeignKey('account.id'),
|
||||||
|
nullable=False)
|
||||||
|
db.UniqueConstraint('apikey_id', 'account_id', name='uniq_apikey_account')
|
||||||
|
|
||||||
|
def __init__(self, apikey_id, account_id):
|
||||||
|
self.apikey_id = apikey_id
|
||||||
|
self.account_id = account_id
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<ApiKey_Account {0} {1}>'.format(self.apikey_id, self.account_id)
|
|
@ -74,23 +74,21 @@ class Domain(db.Model):
|
||||||
"""
|
"""
|
||||||
Get all domains which has in PowerDNS
|
Get all domains which has in PowerDNS
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(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(domain_name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(
|
timeout=int(
|
||||||
Setting().get('pdns_api_timeout')),
|
Setting().get('pdns_api_timeout')),
|
||||||
verify=Setting().get('verify_ssl_connections'))
|
verify=Setting().get('verify_ssl_connections'))
|
||||||
return jdata
|
return jdata
|
||||||
|
|
||||||
def get_domains(self):
|
def get_domains(self):
|
||||||
"""
|
"""
|
||||||
Get all domains which has in PowerDNS
|
Get all domains which has in PowerDNS
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
jdata = utils.fetch_json(
|
jdata = utils.fetch_json(
|
||||||
urljoin(self.PDNS_STATS_URL,
|
urljoin(self.PDNS_STATS_URL,
|
||||||
self.API_EXTENDED_URL + '/servers/localhost/zones'),
|
self.API_EXTENDED_URL + '/servers/localhost/zones'),
|
||||||
|
@ -118,10 +116,9 @@ class Domain(db.Model):
|
||||||
db_domain = Domain.query.all()
|
db_domain = Domain.query.all()
|
||||||
list_db_domain = [d.name for d in db_domain]
|
list_db_domain = [d.name for d in db_domain]
|
||||||
dict_db_domain = dict((x.name, x) for x in db_domain)
|
dict_db_domain = dict((x.name, x) for x in db_domain)
|
||||||
current_app.logger.info("Found {} entries in PowerDNS-Admin".format(
|
current_app.logger.info("Found {} domains in PowerDNS-Admin".format(
|
||||||
len(list_db_domain)))
|
len(list_db_domain)))
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(
|
jdata = utils.fetch_json(
|
||||||
urljoin(self.PDNS_STATS_URL,
|
urljoin(self.PDNS_STATS_URL,
|
||||||
|
@ -131,7 +128,7 @@ class Domain(db.Model):
|
||||||
verify=Setting().get('verify_ssl_connections'))
|
verify=Setting().get('verify_ssl_connections'))
|
||||||
list_jdomain = [d['name'].rstrip('.') for d in jdata]
|
list_jdomain = [d['name'].rstrip('.') for d in jdata]
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
"Found {} entries in PowerDNS server".format(len(list_jdomain)))
|
"Found {} zones in PowerDNS server".format(len(list_jdomain)))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# domains should remove from db since it doesn't exist in powerdns anymore
|
# domains should remove from db since it doesn't exist in powerdns anymore
|
||||||
|
@ -169,8 +166,8 @@ class Domain(db.Model):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
'Can not update domain table. Error: {0}'.format(e))
|
'Cannot update domain table. Error: {0}'.format(e))
|
||||||
return {'status': 'error', 'msg': 'Can not update domain table'}
|
return {'status': 'error', 'msg': 'Cannot update domain table'}
|
||||||
|
|
||||||
def update_pdns_admin_domain(self, domain, account_id, data, do_commit=True):
|
def update_pdns_admin_domain(self, domain, account_id, data, do_commit=True):
|
||||||
# existing domain, only update if something actually has changed
|
# existing domain, only update if something actually has changed
|
||||||
|
@ -211,8 +208,7 @@ class Domain(db.Model):
|
||||||
Add a domain to power dns
|
Add a domain to power dns
|
||||||
"""
|
"""
|
||||||
|
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
domain_name = domain_name + '.'
|
domain_name = domain_name + '.'
|
||||||
domain_ns = [ns + '.' for ns in domain_ns]
|
domain_ns = [ns + '.' for ns in domain_ns]
|
||||||
|
@ -262,15 +258,14 @@ class Domain(db.Model):
|
||||||
"""
|
"""
|
||||||
Read Domain from PowerDNS and add into PDNS-Admin
|
Read Domain from PowerDNS and add into PDNS-Admin
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
if not domain:
|
if not domain:
|
||||||
try:
|
try:
|
||||||
domain = utils.fetch_json(
|
domain = 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(
|
'/servers/localhost/zones/{0}'.format(
|
||||||
domain_dict['name'])),
|
domain_dict['name'])),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
verify=Setting().get('verify_ssl_connections'))
|
verify=Setting().get('verify_ssl_connections'))
|
||||||
|
@ -315,8 +310,8 @@ class Domain(db.Model):
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if not domain:
|
if not domain:
|
||||||
return {'status': 'error', 'msg': 'Domain does not exist.'}
|
return {'status': 'error', 'msg': 'Domain does not exist.'}
|
||||||
headers = {}
|
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
|
|
||||||
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
|
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
|
||||||
soa_edit_api = 'DEFAULT'
|
soa_edit_api = 'DEFAULT'
|
||||||
|
@ -329,13 +324,13 @@ class Domain(db.Model):
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(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(domain.name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(
|
timeout=int(
|
||||||
Setting().get('pdns_api_timeout')),
|
Setting().get('pdns_api_timeout')),
|
||||||
method='PUT',
|
method='PUT',
|
||||||
verify=Setting().get('verify_ssl_connections'),
|
verify=Setting().get('verify_ssl_connections'),
|
||||||
data=post_data)
|
data=post_data)
|
||||||
if 'error' in jdata.keys():
|
if 'error' in jdata.keys():
|
||||||
current_app.logger.error(jdata['error'])
|
current_app.logger.error(jdata['error'])
|
||||||
return {'status': 'error', 'msg': jdata['error']}
|
return {'status': 'error', 'msg': jdata['error']}
|
||||||
|
@ -365,21 +360,21 @@ class Domain(db.Model):
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if not domain:
|
if not domain:
|
||||||
return {'status': 'error', 'msg': 'Domain does not exist.'}
|
return {'status': 'error', 'msg': 'Domain does not exist.'}
|
||||||
headers = {}
|
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
|
|
||||||
post_data = {"kind": kind, "masters": masters}
|
post_data = {"kind": kind, "masters": masters}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(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(domain.name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(
|
timeout=int(
|
||||||
Setting().get('pdns_api_timeout')),
|
Setting().get('pdns_api_timeout')),
|
||||||
method='PUT',
|
method='PUT',
|
||||||
verify=Setting().get('verify_ssl_connections'),
|
verify=Setting().get('verify_ssl_connections'),
|
||||||
data=post_data)
|
data=post_data)
|
||||||
if 'error' in jdata.keys():
|
if 'error' in jdata.keys():
|
||||||
current_app.logger.error(jdata['error'])
|
current_app.logger.error(jdata['error'])
|
||||||
return {'status': 'error', 'msg': jdata['error']}
|
return {'status': 'error', 'msg': jdata['error']}
|
||||||
|
@ -410,27 +405,27 @@ class Domain(db.Model):
|
||||||
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
|
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
domain_auto_ptr = DomainSetting.query.filter(
|
domain_auto_ptr = DomainSetting.query.filter(
|
||||||
DomainSetting.domain == domain_obj).filter(
|
DomainSetting.domain == domain_obj).filter(
|
||||||
DomainSetting.setting == 'auto_ptr').first()
|
DomainSetting.setting == 'auto_ptr').first()
|
||||||
domain_auto_ptr = strtobool(
|
domain_auto_ptr = strtobool(
|
||||||
domain_auto_ptr.value) if domain_auto_ptr else False
|
domain_auto_ptr.value) if domain_auto_ptr else False
|
||||||
system_auto_ptr = Setting().get('auto_ptr')
|
system_auto_ptr = Setting().get('auto_ptr')
|
||||||
self.name = domain_name
|
self.name = domain_name
|
||||||
domain_id = self.get_id_by_name(domain_reverse_name)
|
domain_id = self.get_id_by_name(domain_reverse_name)
|
||||||
if None == domain_id and \
|
if domain_id is None and \
|
||||||
(
|
(
|
||||||
system_auto_ptr or
|
system_auto_ptr or
|
||||||
domain_auto_ptr
|
domain_auto_ptr
|
||||||
):
|
):
|
||||||
result = self.add(domain_reverse_name, 'Master', 'DEFAULT', '', '')
|
result = self.add(domain_reverse_name, 'Master', 'DEFAULT', [], [])
|
||||||
self.update()
|
self.update()
|
||||||
if result['status'] == 'ok':
|
if result['status'] == 'ok':
|
||||||
history = History(msg='Add reverse lookup domain {0}'.format(
|
history = History(msg='Add reverse lookup domain {0}'.format(
|
||||||
domain_reverse_name),
|
domain_reverse_name),
|
||||||
detail=str({
|
detail=str({
|
||||||
'domain_type': 'Master',
|
'domain_type': 'Master',
|
||||||
'domain_master_ips': ''
|
'domain_master_ips': ''
|
||||||
}),
|
}),
|
||||||
created_by='System')
|
created_by='System')
|
||||||
history.add()
|
history.add()
|
||||||
else:
|
else:
|
||||||
return {
|
return {
|
||||||
|
@ -443,9 +438,9 @@ class Domain(db.Model):
|
||||||
self.grant_privileges(domain_user_ids)
|
self.grant_privileges(domain_user_ids)
|
||||||
return {
|
return {
|
||||||
'status':
|
'status':
|
||||||
'ok',
|
'ok',
|
||||||
'msg':
|
'msg':
|
||||||
'New reverse lookup domain created with granted privileges'
|
'New reverse lookup domain created with granted privileges'
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
'status': 'ok',
|
'status': 'ok',
|
||||||
|
@ -497,16 +492,15 @@ class Domain(db.Model):
|
||||||
"""
|
"""
|
||||||
Delete a single domain name from powerdns
|
Delete a single domain name from powerdns
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
utils.fetch_json(urljoin(
|
utils.fetch_json(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(domain_name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='DELETE',
|
method='DELETE',
|
||||||
verify=Setting().get('verify_ssl_connections'))
|
verify=Setting().get('verify_ssl_connections'))
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
'Deleted domain successfully from PowerDNS: {0}'.format(
|
'Deleted domain successfully from PowerDNS: {0}'.format(
|
||||||
domain_name))
|
domain_name))
|
||||||
|
@ -525,6 +519,13 @@ class Domain(db.Model):
|
||||||
domain_setting.delete()
|
domain_setting.delete()
|
||||||
domain.apikeys[:] = []
|
domain.apikeys[:] = []
|
||||||
|
|
||||||
|
# Remove history for domain
|
||||||
|
domain_history = History.query.filter(
|
||||||
|
History.domain_id == domain.id
|
||||||
|
)
|
||||||
|
if domain_history:
|
||||||
|
domain_history.delete()
|
||||||
|
|
||||||
# then remove domain
|
# then remove domain
|
||||||
Domain.query.filter(Domain.name == domain_name).delete()
|
Domain.query.filter(Domain.name == domain_name).delete()
|
||||||
if do_commit:
|
if do_commit:
|
||||||
|
@ -540,8 +541,8 @@ class Domain(db.Model):
|
||||||
user_ids = []
|
user_ids = []
|
||||||
query = db.session.query(
|
query = db.session.query(
|
||||||
DomainUser, Domain).filter(User.id == DomainUser.user_id).filter(
|
DomainUser, Domain).filter(User.id == DomainUser.user_id).filter(
|
||||||
Domain.id == DomainUser.domain_id).filter(
|
Domain.id == DomainUser.domain_id).filter(
|
||||||
Domain.name == self.name).all()
|
Domain.name == self.name).all()
|
||||||
for q in query:
|
for q in query:
|
||||||
user_ids.append(q[0].user_id)
|
user_ids.append(q[0].user_id)
|
||||||
return user_ids
|
return user_ids
|
||||||
|
@ -566,7 +567,7 @@ class Domain(db.Model):
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
'Cannot revoke user privileges on domain {0}. DETAIL: {1}'.
|
'Cannot revoke user privileges on domain {0}. DETAIL: {1}'.
|
||||||
format(self.name, e))
|
format(self.name, e))
|
||||||
current_app.logger.debug(print(traceback.format_exc()))
|
current_app.logger.debug(print(traceback.format_exc()))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -578,36 +579,62 @@ class Domain(db.Model):
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
'Cannot grant user privileges to domain {0}. DETAIL: {1}'.
|
'Cannot grant user privileges to domain {0}. DETAIL: {1}'.
|
||||||
format(self.name, e))
|
format(self.name, e))
|
||||||
current_app.logger.debug(print(traceback.format_exc()))
|
current_app.logger.debug(print(traceback.format_exc()))
|
||||||
|
|
||||||
|
def revoke_privileges_by_id(self, user_id):
|
||||||
|
"""
|
||||||
|
Remove a single user from privilege list based on user_id
|
||||||
|
"""
|
||||||
|
new_uids = [u for u in self.get_user() if u != user_id]
|
||||||
|
users = []
|
||||||
|
for uid in new_uids:
|
||||||
|
users.append(User(id=uid).get_user_info_by_id().username)
|
||||||
|
|
||||||
|
self.grant_privileges(users)
|
||||||
|
|
||||||
|
def add_user(self, user):
|
||||||
|
"""
|
||||||
|
Add a single user to Domain by User
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
du = DomainUser(self.id, user.id)
|
||||||
|
db.session.add(du)
|
||||||
|
db.session.commit()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
db.session.rollback()
|
||||||
|
current_app.logger.error(
|
||||||
|
'Cannot add user privileges on domain {0}. DETAIL: {1}'.
|
||||||
|
format(self.name, e))
|
||||||
|
return False
|
||||||
|
|
||||||
def update_from_master(self, domain_name):
|
def update_from_master(self, domain_name):
|
||||||
"""
|
"""
|
||||||
Update records from Master DNS server
|
Update records from Master DNS server
|
||||||
"""
|
"""
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
try:
|
try:
|
||||||
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)),
|
domain.name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(
|
timeout=int(
|
||||||
Setting().get('pdns_api_timeout')),
|
Setting().get('pdns_api_timeout')),
|
||||||
method='PUT',
|
method='PUT',
|
||||||
verify=Setting().get('verify_ssl_connections'))
|
verify=Setting().get('verify_ssl_connections'))
|
||||||
return {'status': 'ok', 'msg': r.get('result')}
|
return {'status': 'ok', 'msg': r.get('result')}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
'Cannot update from master. DETAIL: {0}'.format(e))
|
'Cannot update from master. DETAIL: {0}'.format(e))
|
||||||
return {
|
return {
|
||||||
'status':
|
'status':
|
||||||
'error',
|
'error',
|
||||||
'msg':
|
'msg':
|
||||||
'There was something wrong, please contact administrator'
|
'There was something wrong, please contact administrator'
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {'status': 'error', 'msg': 'This domain does not exist'}
|
return {'status': 'error', 'msg': 'This domain does not exist'}
|
||||||
|
@ -618,14 +645,13 @@ class Domain(db.Model):
|
||||||
"""
|
"""
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
try:
|
try:
|
||||||
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}/cryptokeys'.format(
|
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||||
domain.name)),
|
domain.name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='GET',
|
method='GET',
|
||||||
|
@ -642,9 +668,9 @@ class Domain(db.Model):
|
||||||
'Cannot get domain dnssec. DETAIL: {0}'.format(e))
|
'Cannot get domain dnssec. DETAIL: {0}'.format(e))
|
||||||
return {
|
return {
|
||||||
'status':
|
'status':
|
||||||
'error',
|
'error',
|
||||||
'msg':
|
'msg':
|
||||||
'There was something wrong, please contact administrator'
|
'There was something wrong, please contact administrator'
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
return {'status': 'error', 'msg': 'This domain does not exist'}
|
return {'status': 'error', 'msg': 'This domain does not exist'}
|
||||||
|
@ -655,15 +681,14 @@ class Domain(db.Model):
|
||||||
"""
|
"""
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
try:
|
try:
|
||||||
# Enable API-RECTIFY for domain, BEFORE activating DNSSEC
|
# Enable API-RECTIFY for domain, BEFORE activating DNSSEC
|
||||||
post_data = {"api_rectify": True}
|
post_data = {"api_rectify": True}
|
||||||
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(domain.name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='PUT',
|
method='PUT',
|
||||||
|
@ -673,7 +698,7 @@ class Domain(db.Model):
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'msg':
|
'msg':
|
||||||
'API-RECTIFY could not be enabled for this domain',
|
'API-RECTIFY could not be enabled for this domain',
|
||||||
'jdata': jdata
|
'jdata': jdata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -682,8 +707,8 @@ 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}/cryptokeys'.format(
|
'/servers/localhost/zones/{0}/cryptokeys'.format(
|
||||||
domain.name)),
|
domain.name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='POST',
|
method='POST',
|
||||||
|
@ -692,12 +717,12 @@ class Domain(db.Model):
|
||||||
if 'error' in jdata:
|
if 'error' in jdata:
|
||||||
return {
|
return {
|
||||||
'status':
|
'status':
|
||||||
'error',
|
'error',
|
||||||
'msg':
|
'msg':
|
||||||
'Cannot enable DNSSEC for this domain. Error: {0}'.
|
'Cannot enable DNSSEC for this domain. Error: {0}'.
|
||||||
format(jdata['error']),
|
format(jdata['error']),
|
||||||
'jdata':
|
'jdata':
|
||||||
jdata
|
jdata
|
||||||
}
|
}
|
||||||
|
|
||||||
return {'status': 'ok'}
|
return {'status': 'ok'}
|
||||||
|
@ -708,9 +733,9 @@ class Domain(db.Model):
|
||||||
current_app.logger.debug(traceback.format_exc())
|
current_app.logger.debug(traceback.format_exc())
|
||||||
return {
|
return {
|
||||||
'status':
|
'status':
|
||||||
'error',
|
'error',
|
||||||
'msg':
|
'msg':
|
||||||
'There was something wrong, please contact administrator'
|
'There was something wrong, please contact administrator'
|
||||||
}
|
}
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -722,15 +747,14 @@ class Domain(db.Model):
|
||||||
"""
|
"""
|
||||||
domain = Domain.query.filter(Domain.name == domain_name).first()
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
if domain:
|
if domain:
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
try:
|
try:
|
||||||
# Deactivate DNSSEC
|
# Deactivate DNSSEC
|
||||||
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}/cryptokeys/{1}'.format(
|
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
|
||||||
domain.name, key_id)),
|
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',
|
||||||
|
@ -738,12 +762,12 @@ class Domain(db.Model):
|
||||||
if jdata != True:
|
if jdata != True:
|
||||||
return {
|
return {
|
||||||
'status':
|
'status':
|
||||||
'error',
|
'error',
|
||||||
'msg':
|
'msg':
|
||||||
'Cannot disable DNSSEC for this domain. Error: {0}'.
|
'Cannot disable DNSSEC for this domain. Error: {0}'.
|
||||||
format(jdata['error']),
|
format(jdata['error']),
|
||||||
'jdata':
|
'jdata':
|
||||||
jdata
|
jdata
|
||||||
}
|
}
|
||||||
|
|
||||||
# Disable API-RECTIFY for domain, AFTER deactivating DNSSEC
|
# Disable API-RECTIFY for domain, AFTER deactivating DNSSEC
|
||||||
|
@ -751,7 +775,7 @@ 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(domain.name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
timeout=int(Setting().get('pdns_api_timeout')),
|
||||||
method='PUT',
|
method='PUT',
|
||||||
|
@ -761,7 +785,7 @@ class Domain(db.Model):
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'msg':
|
'msg':
|
||||||
'API-RECTIFY could not be disabled for this domain',
|
'API-RECTIFY could not be disabled for this domain',
|
||||||
'jdata': jdata
|
'jdata': jdata
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -774,7 +798,7 @@ class Domain(db.Model):
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'msg':
|
'msg':
|
||||||
'There was something wrong, please contact administrator',
|
'There was something wrong, please contact administrator',
|
||||||
'domain': domain.name,
|
'domain': domain.name,
|
||||||
'id': key_id
|
'id': key_id
|
||||||
}
|
}
|
||||||
|
@ -797,8 +821,7 @@ class Domain(db.Model):
|
||||||
if not domain:
|
if not domain:
|
||||||
return {'status': False, 'msg': 'Domain does not exist'}
|
return {'status': False, 'msg': 'Domain does not exist'}
|
||||||
|
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
account_name = Account().get_name_by_id(account_id)
|
account_name = Account().get_name_by_id(account_id)
|
||||||
|
|
||||||
|
@ -807,13 +830,13 @@ class Domain(db.Model):
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(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(domain_name)),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
timeout=int(
|
timeout=int(
|
||||||
Setting().get('pdns_api_timeout')),
|
Setting().get('pdns_api_timeout')),
|
||||||
method='PUT',
|
method='PUT',
|
||||||
verify=Setting().get('verify_ssl_connections'),
|
verify=Setting().get('verify_ssl_connections'),
|
||||||
data=post_data)
|
data=post_data)
|
||||||
|
|
||||||
if 'error' in jdata.keys():
|
if 'error' in jdata.keys():
|
||||||
current_app.logger.error(jdata['error'])
|
current_app.logger.error(jdata['error'])
|
||||||
|
@ -852,7 +875,7 @@ class Domain(db.Model):
|
||||||
.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(
|
.filter(
|
||||||
db.or_(
|
db.or_(
|
||||||
DomainUser.user_id == user_id,
|
DomainUser.user_id == user_id,
|
||||||
AccountUser.user_id == user_id
|
AccountUser.user_id == user_id
|
||||||
)).filter(Domain.id == self.id).first()
|
)).filter(Domain.id == self.id).first()
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import traceback
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from .base import db
|
from .base import db
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import traceback
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
|
@ -6,17 +8,22 @@ from .base import db
|
||||||
|
|
||||||
class History(db.Model):
|
class History(db.Model):
|
||||||
id = db.Column(db.Integer, primary_key=True)
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
# format of msg field must not change. History traversing is done using part of the msg field
|
||||||
msg = db.Column(db.String(256))
|
msg = db.Column(db.String(256))
|
||||||
# detail = db.Column(db.Text().with_variant(db.Text(length=2**24-2), 'mysql'))
|
# detail = db.Column(db.Text().with_variant(db.Text(length=2**24-2), 'mysql'))
|
||||||
detail = db.Column(db.Text())
|
detail = db.Column(db.Text())
|
||||||
created_by = db.Column(db.String(128))
|
created_by = db.Column(db.String(128))
|
||||||
created_on = db.Column(db.DateTime, default=datetime.utcnow)
|
created_on = db.Column(db.DateTime, index=True, default=datetime.utcnow)
|
||||||
|
domain_id = db.Column(db.Integer,
|
||||||
|
db.ForeignKey('domain.id'),
|
||||||
|
nullable=True)
|
||||||
|
|
||||||
def __init__(self, id=None, msg=None, detail=None, created_by=None):
|
def __init__(self, id=None, msg=None, detail=None, created_by=None, domain_id=None):
|
||||||
self.id = id
|
self.id = id
|
||||||
self.msg = msg
|
self.msg = msg
|
||||||
self.detail = detail
|
self.detail = detail
|
||||||
self.created_by = created_by
|
self.created_by = created_by
|
||||||
|
self.domain_id = domain_id
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '<History {0}>'.format(self.msg)
|
return '<History {0}>'.format(self.msg)
|
||||||
|
@ -29,6 +36,7 @@ class History(db.Model):
|
||||||
h.msg = self.msg
|
h.msg = self.msg
|
||||||
h.detail = self.detail
|
h.detail = self.detail
|
||||||
h.created_by = self.created_by
|
h.created_by = self.created_by
|
||||||
|
h.domain_id = self.domain_id
|
||||||
db.session.add(h)
|
db.session.add(h)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,10 @@ from .domain import Domain
|
||||||
from .domain_setting import DomainSetting
|
from .domain_setting import DomainSetting
|
||||||
|
|
||||||
|
|
||||||
|
def by_record_content_pair(e):
|
||||||
|
return e[0]['content']
|
||||||
|
|
||||||
|
|
||||||
class Record(object):
|
class Record(object):
|
||||||
"""
|
"""
|
||||||
This is not a model, it's just an object
|
This is not a model, it's just an object
|
||||||
|
@ -44,8 +48,7 @@ class Record(object):
|
||||||
"""
|
"""
|
||||||
Query domain's rrsets via PDNS API
|
Query domain's rrsets via PDNS API
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
|
@ -60,7 +63,17 @@ class Record(object):
|
||||||
.format(e))
|
.format(e))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
return jdata['rrsets']
|
rrsets=[]
|
||||||
|
for r in jdata['rrsets']:
|
||||||
|
if len(r['records']) == 0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
while len(r['comments'])<len(r['records']):
|
||||||
|
r['comments'].append({"content": "", "account": ""})
|
||||||
|
r['records'], r['comments'] = (list(t) for t in zip(*sorted(zip(r['records'], r['comments']), key=by_record_content_pair)))
|
||||||
|
rrsets.append(r)
|
||||||
|
|
||||||
|
return rrsets
|
||||||
|
|
||||||
def add(self, domain_name, rrset):
|
def add(self, domain_name, rrset):
|
||||||
"""
|
"""
|
||||||
|
@ -86,8 +99,7 @@ class Record(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
# Continue if the record is ready to be added
|
# Continue if the record is ready to be added
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(urljoin(
|
||||||
|
@ -117,21 +129,26 @@ class Record(object):
|
||||||
"""
|
"""
|
||||||
Merge the rrsets that has same "name" and
|
Merge the rrsets that has same "name" and
|
||||||
"type".
|
"type".
|
||||||
Return: a new rrest which has multiple "records"
|
Return: a new rrset which has multiple "records"
|
||||||
and "comments"
|
and "comments"
|
||||||
"""
|
"""
|
||||||
if not rrsets:
|
if not rrsets:
|
||||||
raise Exception("Empty rrsets to merge")
|
raise Exception("Empty rrsets to merge")
|
||||||
elif len(rrsets) == 1:
|
elif len(rrsets) == 1:
|
||||||
# It is unique rrest already
|
# It is unique rrset already
|
||||||
return rrsets[0]
|
return rrsets[0]
|
||||||
else:
|
else:
|
||||||
# Merge rrsets into one
|
# Merge rrsets into one
|
||||||
rrest = rrsets[0]
|
rrset = rrsets[0]
|
||||||
for r in rrsets[1:]:
|
for r in rrsets[1:]:
|
||||||
rrest['records'] = rrest['records'] + r['records']
|
rrset['records'] = rrset['records'] + r['records']
|
||||||
rrest['comments'] = rrest['comments'] + r['comments']
|
rrset['comments'] = rrset['comments'] + r['comments']
|
||||||
return rrest
|
while len(rrset['comments']) < len(rrset['records']):
|
||||||
|
rrset['comments'].append({"content": "", "account": ""})
|
||||||
|
zipped_list = zip(rrset['records'], rrset['comments'])
|
||||||
|
tuples = zip(*sorted(zipped_list, key=by_record_content_pair))
|
||||||
|
rrset['records'], rrset['comments'] = [list(t) for t in tuples]
|
||||||
|
return rrset
|
||||||
|
|
||||||
def build_rrsets(self, domain_name, submitted_records):
|
def build_rrsets(self, domain_name, submitted_records):
|
||||||
"""
|
"""
|
||||||
|
@ -142,12 +159,23 @@ class Record(object):
|
||||||
submitted_records(list): List of records submitted from PDA datatable
|
submitted_records(list): List of records submitted from PDA datatable
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
transformed_rrsets(list): List of rrests converted from PDA datatable
|
transformed_rrsets(list): List of rrsets converted from PDA datatable
|
||||||
"""
|
"""
|
||||||
rrsets = []
|
rrsets = []
|
||||||
for record in submitted_records:
|
for record in submitted_records:
|
||||||
# Format the record name
|
# Format the record name
|
||||||
#
|
#
|
||||||
|
# Translate template placeholders into proper record data
|
||||||
|
record['record_data'] = record['record_data'].replace('[ZONE]', domain_name)
|
||||||
|
# Translate record name into punycode (IDN) as that's the only way
|
||||||
|
# to convey non-ascii records to the dns server
|
||||||
|
record['record_name'] = record['record_name'].encode('idna').decode()
|
||||||
|
#TODO: error handling
|
||||||
|
# If the record is an alias (CNAME), we will also make sure that
|
||||||
|
# the target domain is properly converted to punycode (IDN)
|
||||||
|
if record["record_type"] == 'CNAME':
|
||||||
|
record['record_data'] = record['record_data'].encode('idna').decode()
|
||||||
|
#TODO: error handling
|
||||||
# If it is ipv6 reverse zone and PRETTY_IPV6_PTR is enabled,
|
# If it is ipv6 reverse zone and PRETTY_IPV6_PTR is enabled,
|
||||||
# We convert ipv6 address back to reverse record format
|
# We convert ipv6 address back to reverse record format
|
||||||
# before submitting to PDNS API.
|
# before submitting to PDNS API.
|
||||||
|
@ -175,7 +203,7 @@ class Record(object):
|
||||||
] and record["record_data"].strip()[-1:] != '.':
|
] and record["record_data"].strip()[-1:] != '.':
|
||||||
record["record_data"] += '.'
|
record["record_data"] += '.'
|
||||||
|
|
||||||
record_conntent = {
|
record_content = {
|
||||||
"content": record["record_data"],
|
"content": record["record_data"],
|
||||||
"disabled":
|
"disabled":
|
||||||
False if record['record_status'] == 'Active' else True
|
False if record['record_status'] == 'Active' else True
|
||||||
|
@ -185,19 +213,22 @@ class Record(object):
|
||||||
record_comments = [{
|
record_comments = [{
|
||||||
"content": record["record_comment"],
|
"content": record["record_comment"],
|
||||||
"account": ""
|
"account": ""
|
||||||
}] if record.get("record_comment") else []
|
}] if record.get("record_comment") else [{
|
||||||
|
"content": "",
|
||||||
|
"account": ""
|
||||||
|
}]
|
||||||
|
|
||||||
# Add the formatted record to rrsets list
|
# Add the formatted record to rrsets list
|
||||||
rrsets.append({
|
rrsets.append({
|
||||||
"name": record_name,
|
"name": record_name,
|
||||||
"type": record["record_type"],
|
"type": record["record_type"],
|
||||||
"ttl": int(record["record_ttl"]),
|
"ttl": int(record["record_ttl"]),
|
||||||
"records": [record_conntent],
|
"records": [record_content],
|
||||||
"comments": record_comments
|
"comments": record_comments
|
||||||
})
|
})
|
||||||
|
|
||||||
# Group the records which has the same name and type.
|
# Group the records which has the same name and type.
|
||||||
# The rrest then has multiple records inside.
|
# The rrset then has multiple records inside.
|
||||||
transformed_rrsets = []
|
transformed_rrsets = []
|
||||||
|
|
||||||
# Sort the list before using groupby
|
# Sort the list before using groupby
|
||||||
|
@ -218,8 +249,8 @@ class Record(object):
|
||||||
submitted_records(list): List of records submitted from PDA datatable
|
submitted_records(list): List of records submitted from PDA datatable
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
new_rrsets(list): List of rrests to be added
|
new_rrsets(list): List of rrsets to be added
|
||||||
del_rrsets(list): List of rrests to be deleted
|
del_rrsets(list): List of rrsets to be deleted
|
||||||
"""
|
"""
|
||||||
# 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)
|
||||||
|
@ -237,7 +268,8 @@ class Record(object):
|
||||||
# comparison between current and submitted rrsets
|
# comparison between current and submitted rrsets
|
||||||
for r in current_rrsets:
|
for r in current_rrsets:
|
||||||
for comment in r['comments']:
|
for comment in r['comments']:
|
||||||
del comment['modified_at']
|
if 'modified_at' in comment:
|
||||||
|
del comment['modified_at']
|
||||||
|
|
||||||
# List of rrsets to be added
|
# List of rrsets to be added
|
||||||
new_rrsets = {"rrsets": []}
|
new_rrsets = {"rrsets": []}
|
||||||
|
@ -260,11 +292,22 @@ class Record(object):
|
||||||
|
|
||||||
return new_rrsets, del_rrsets
|
return new_rrsets, del_rrsets
|
||||||
|
|
||||||
|
def apply_rrsets(self, domain_name, rrsets):
|
||||||
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
|
jdata = utils.fetch_json(urljoin(
|
||||||
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
|
'/servers/localhost/zones/{0}'.format(domain_name)),
|
||||||
|
headers=headers,
|
||||||
|
method='PATCH',
|
||||||
|
verify=Setting().get('verify_ssl_connections'),
|
||||||
|
data=rrsets)
|
||||||
|
return jdata
|
||||||
|
|
||||||
def apply(self, domain_name, submitted_records):
|
def apply(self, domain_name, submitted_records):
|
||||||
"""
|
"""
|
||||||
Apply record changes to a domain. This function
|
Apply record changes to a domain. This function
|
||||||
will make 2 calls to the PDNS API to DELETE and
|
will make 2 calls to the PDNS API to DELETE and
|
||||||
REPLACE records (rrests)
|
REPLACE records (rrsets)
|
||||||
"""
|
"""
|
||||||
current_app.logger.debug(
|
current_app.logger.debug(
|
||||||
"submitted_records: {}".format(submitted_records))
|
"submitted_records: {}".format(submitted_records))
|
||||||
|
@ -272,47 +315,67 @@ class Record(object):
|
||||||
# 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 = self.compare(domain_name, submitted_records)
|
||||||
|
|
||||||
|
# Remove blank comments from rrsets for compatibility with some backends
|
||||||
|
def remove_blank_comments(rrset):
|
||||||
|
if not rrset['comments']:
|
||||||
|
del rrset['comments']
|
||||||
|
elif isinstance(rrset['comments'], list):
|
||||||
|
# Merge all non-blank comment values into a list
|
||||||
|
merged_comments = [
|
||||||
|
v
|
||||||
|
for c in rrset['comments']
|
||||||
|
for v in c.values()
|
||||||
|
if v
|
||||||
|
]
|
||||||
|
# Delete comment if all values are blank (len(merged_comments) == 0)
|
||||||
|
if not merged_comments:
|
||||||
|
del rrset['comments']
|
||||||
|
|
||||||
|
for r in new_rrsets['rrsets']:
|
||||||
|
remove_blank_comments(r)
|
||||||
|
|
||||||
|
for r in del_rrsets['rrsets']:
|
||||||
|
remove_blank_comments(r)
|
||||||
|
|
||||||
# Submit the changes to PDNS API
|
# Submit the changes to PDNS API
|
||||||
try:
|
try:
|
||||||
headers = {}
|
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
if del_rrsets["rrsets"]:
|
if del_rrsets["rrsets"]:
|
||||||
jdata1 = utils.fetch_json(urljoin(
|
result = self.apply_rrsets(domain_name, del_rrsets)
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
if 'error' in result.keys():
|
||||||
'/servers/localhost/zones/{0}'.format(domain_name)),
|
|
||||||
headers=headers,
|
|
||||||
method='PATCH',
|
|
||||||
verify=Setting().get('verify_ssl_connections'),
|
|
||||||
data=del_rrsets)
|
|
||||||
if 'error' in jdata1.keys():
|
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
'Cannot apply record changes with deleting rrsets step. PDNS error: {}'
|
'Cannot apply record changes with deleting rrsets step. PDNS error: {}'
|
||||||
.format(jdata1['error']))
|
.format(result['error']))
|
||||||
print(jdata1['error'])
|
|
||||||
return {
|
return {
|
||||||
'status': 'error',
|
'status': 'error',
|
||||||
'msg': jdata1['error'].replace("'", "")
|
'msg': result['error'].replace("'", "")
|
||||||
}
|
}
|
||||||
|
|
||||||
if new_rrsets["rrsets"]:
|
if new_rrsets["rrsets"]:
|
||||||
jdata2 = utils.fetch_json(
|
result = self.apply_rrsets(domain_name, new_rrsets)
|
||||||
urljoin(
|
if 'error' in result.keys():
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
|
||||||
'/servers/localhost/zones/{0}'.format(domain_name)),
|
|
||||||
headers=headers,
|
|
||||||
timeout=int(Setting().get('pdns_api_timeout')),
|
|
||||||
method='PATCH',
|
|
||||||
verify=Setting().get('verify_ssl_connections'),
|
|
||||||
data=new_rrsets)
|
|
||||||
if 'error' in jdata2.keys():
|
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
'Cannot apply record changes with adding rrsets step. PDNS error: {}'
|
'Cannot apply record changes with adding rrsets step. PDNS error: {}'
|
||||||
.format(jdata2['error']))
|
.format(result['error']))
|
||||||
return {
|
|
||||||
'status': 'error',
|
# rollback - re-add the removed record if the adding operation is failed.
|
||||||
'msg': jdata2['error'].replace("'", "")
|
if del_rrsets["rrsets"]:
|
||||||
}
|
rollback_rrests = del_rrsets
|
||||||
|
for r in del_rrsets["rrsets"]:
|
||||||
|
r['changetype'] = 'REPLACE'
|
||||||
|
rollback = self.apply_rrsets(domain_name, rollback_rrests)
|
||||||
|
if 'error' in rollback.keys():
|
||||||
|
return dict(status='error',
|
||||||
|
msg='Failed to apply changes. Cannot rollback previous failed operation: {}'
|
||||||
|
.format(rollback['error'].replace("'", "")))
|
||||||
|
else:
|
||||||
|
return dict(status='error',
|
||||||
|
msg='Failed to apply changes. Rolled back previous failed operation: {}'
|
||||||
|
.format(result['error'].replace("'", "")))
|
||||||
|
else:
|
||||||
|
return {
|
||||||
|
'status': 'error',
|
||||||
|
'msg': result['error'].replace("'", "")
|
||||||
|
}
|
||||||
|
|
||||||
self.auto_ptr(domain_name, new_rrsets, del_rrsets)
|
self.auto_ptr(domain_name, new_rrsets, del_rrsets)
|
||||||
self.update_db_serial(domain_name)
|
self.update_db_serial(domain_name)
|
||||||
|
@ -437,8 +500,7 @@ class Record(object):
|
||||||
"""
|
"""
|
||||||
Delete a record from domain
|
Delete a record from domain
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
data = {
|
data = {
|
||||||
"rrsets": [{
|
"rrsets": [{
|
||||||
"name": self.name.rstrip('.') + '.',
|
"name": self.name.rstrip('.') + '.',
|
||||||
|
@ -500,8 +562,7 @@ class Record(object):
|
||||||
"""
|
"""
|
||||||
Update single record
|
Update single record
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"rrsets": [{
|
"rrsets": [{
|
||||||
|
@ -542,8 +603,7 @@ class Record(object):
|
||||||
}
|
}
|
||||||
|
|
||||||
def update_db_serial(self, domain):
|
def update_db_serial(self, domain):
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(urljoin(
|
||||||
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
|
||||||
'/servers/localhost/zones/{0}'.format(domain)),
|
'/servers/localhost/zones/{0}'.format(domain)),
|
||||||
|
|
|
@ -24,8 +24,7 @@ class Server(object):
|
||||||
"""
|
"""
|
||||||
Get server config
|
Get server config
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(urljoin(
|
||||||
|
@ -46,8 +45,7 @@ class Server(object):
|
||||||
"""
|
"""
|
||||||
Get server statistics
|
Get server statistics
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(urljoin(
|
||||||
|
@ -68,8 +66,7 @@ class Server(object):
|
||||||
"""
|
"""
|
||||||
Search zone/record/comment directly from PDNS API
|
Search zone/record/comment directly from PDNS API
|
||||||
"""
|
"""
|
||||||
headers = {}
|
headers = {'X-API-Key': self.PDNS_API_KEY}
|
||||||
headers['X-API-Key'] = self.PDNS_API_KEY
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
jdata = utils.fetch_json(urljoin(
|
jdata = utils.fetch_json(urljoin(
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
import sys
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
import pytimeparse
|
import pytimeparse
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
|
@ -24,7 +26,11 @@ class Setting(db.Model):
|
||||||
'pretty_ipv6_ptr': False,
|
'pretty_ipv6_ptr': False,
|
||||||
'dnssec_admins_only': False,
|
'dnssec_admins_only': False,
|
||||||
'allow_user_create_domain': False,
|
'allow_user_create_domain': False,
|
||||||
|
'allow_user_remove_domain': False,
|
||||||
|
'allow_user_view_history': False,
|
||||||
|
'delete_sso_accounts': False,
|
||||||
'bg_domain_updates': False,
|
'bg_domain_updates': False,
|
||||||
|
'enable_api_rr_history': True,
|
||||||
'site_name': 'PowerDNS-Admin',
|
'site_name': 'PowerDNS-Admin',
|
||||||
'site_url': 'http://localhost:9191',
|
'site_url': 'http://localhost:9191',
|
||||||
'session_timeout': 10,
|
'session_timeout': 10,
|
||||||
|
@ -36,6 +42,10 @@ class Setting(db.Model):
|
||||||
'verify_ssl_connections': True,
|
'verify_ssl_connections': True,
|
||||||
'local_db_enabled': True,
|
'local_db_enabled': True,
|
||||||
'signup_enabled': True,
|
'signup_enabled': True,
|
||||||
|
'autoprovisioning': False,
|
||||||
|
'urn_value':'',
|
||||||
|
'autoprovisioning_attribute': '',
|
||||||
|
'purge': False,
|
||||||
'verify_user_email': False,
|
'verify_user_email': False,
|
||||||
'ldap_enabled': False,
|
'ldap_enabled': False,
|
||||||
'ldap_type': 'ldap',
|
'ldap_type': 'ldap',
|
||||||
|
@ -81,6 +91,11 @@ class Setting(db.Model):
|
||||||
'azure_admin_group': '',
|
'azure_admin_group': '',
|
||||||
'azure_operator_group': '',
|
'azure_operator_group': '',
|
||||||
'azure_user_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_enabled': False,
|
'oidc_oauth_enabled': False,
|
||||||
'oidc_oauth_key': '',
|
'oidc_oauth_key': '',
|
||||||
'oidc_oauth_secret': '',
|
'oidc_oauth_secret': '',
|
||||||
|
@ -88,6 +103,13 @@ class Setting(db.Model):
|
||||||
'oidc_oauth_api_url': '',
|
'oidc_oauth_api_url': '',
|
||||||
'oidc_oauth_token_url': '',
|
'oidc_oauth_token_url': '',
|
||||||
'oidc_oauth_authorize_url': '',
|
'oidc_oauth_authorize_url': '',
|
||||||
|
'oidc_oauth_logout_url': '',
|
||||||
|
'oidc_oauth_username': 'preferred_username',
|
||||||
|
'oidc_oauth_firstname': 'given_name',
|
||||||
|
'oidc_oauth_last_name': 'family_name',
|
||||||
|
'oidc_oauth_email': 'email',
|
||||||
|
'oidc_oauth_account_name_property': '',
|
||||||
|
'oidc_oauth_account_description_property': '',
|
||||||
'forward_records_allow_edit': {
|
'forward_records_allow_edit': {
|
||||||
'A': True,
|
'A': True,
|
||||||
'AAAA': True,
|
'AAAA': True,
|
||||||
|
@ -165,6 +187,10 @@ class Setting(db.Model):
|
||||||
'URI': False
|
'URI': False
|
||||||
},
|
},
|
||||||
'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours',
|
'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
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, value=None):
|
def __init__(self, id=None, name=None, value=None):
|
||||||
|
@ -245,11 +271,18 @@ class Setting(db.Model):
|
||||||
|
|
||||||
def get(self, setting):
|
def get(self, setting):
|
||||||
if setting in self.defaults:
|
if setting in self.defaults:
|
||||||
result = self.query.filter(Setting.name == setting).first()
|
|
||||||
|
if setting.upper() in current_app.config:
|
||||||
|
result = current_app.config[setting.upper()]
|
||||||
|
else:
|
||||||
|
result = self.query.filter(Setting.name == setting).first()
|
||||||
|
|
||||||
if result is not None:
|
if result is not None:
|
||||||
return strtobool(result.value) if result.value in [
|
if hasattr(result,'value'):
|
||||||
|
result = result.value
|
||||||
|
return strtobool(result) if result in [
|
||||||
'True', 'False'
|
'True', 'False'
|
||||||
] else result.value
|
] else result
|
||||||
else:
|
else:
|
||||||
return self.defaults[setting]
|
return self.defaults[setting]
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,17 +1,22 @@
|
||||||
import os
|
import os
|
||||||
import base64
|
import base64
|
||||||
import bcrypt
|
|
||||||
import traceback
|
import traceback
|
||||||
|
import bcrypt
|
||||||
import pyotp
|
import pyotp
|
||||||
import ldap
|
import ldap
|
||||||
import ldap.filter
|
import ldap.filter
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask_login import AnonymousUserMixin
|
from flask_login import AnonymousUserMixin
|
||||||
|
from sqlalchemy import orm
|
||||||
|
import qrcode as qrc
|
||||||
|
import qrcode.image.svg as qrc_svg
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from .base import db
|
from .base import db
|
||||||
from .role import Role
|
from .role import Role
|
||||||
from .setting import Setting
|
from .setting import Setting
|
||||||
from .domain_user import DomainUser
|
from .domain_user import DomainUser
|
||||||
|
from .account_user import AccountUser
|
||||||
|
|
||||||
|
|
||||||
class Anonymous(AnonymousUserMixin):
|
class Anonymous(AnonymousUserMixin):
|
||||||
|
@ -29,6 +34,7 @@ class User(db.Model):
|
||||||
otp_secret = db.Column(db.String(16))
|
otp_secret = db.Column(db.String(16))
|
||||||
confirmed = db.Column(db.SmallInteger, nullable=False, default=0)
|
confirmed = db.Column(db.SmallInteger, nullable=False, default=0)
|
||||||
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
||||||
|
accounts = None
|
||||||
|
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
id=None,
|
id=None,
|
||||||
|
@ -103,7 +109,7 @@ class User(db.Model):
|
||||||
return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
|
return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
|
||||||
|
|
||||||
def check_password(self, hashed_password):
|
def check_password(self, hashed_password):
|
||||||
# Check hased password. Using bcrypt, the salt is saved into the hash itself
|
# Check hashed password. Using bcrypt, the salt is saved into the hash itself
|
||||||
if (self.plain_text_password):
|
if (self.plain_text_password):
|
||||||
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
|
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
|
||||||
hashed_password.encode('utf-8'))
|
hashed_password.encode('utf-8'))
|
||||||
|
@ -128,9 +134,8 @@ class User(db.Model):
|
||||||
conn.protocol_version = ldap.VERSION3
|
conn.protocol_version = ldap.VERSION3
|
||||||
return conn
|
return conn
|
||||||
|
|
||||||
def ldap_search(self, searchFilter, baseDN):
|
def ldap_search(self, searchFilter, baseDN, retrieveAttributes=None):
|
||||||
searchScope = ldap.SCOPE_SUBTREE
|
searchScope = ldap.SCOPE_SUBTREE
|
||||||
retrieveAttributes = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = self.ldap_init_conn()
|
conn = self.ldap_init_conn()
|
||||||
|
@ -191,7 +196,7 @@ class User(db.Model):
|
||||||
current_app.logger.exception("Recursive AD Group search error")
|
current_app.logger.exception("Recursive AD Group search error")
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def is_validate(self, method, src_ip=''):
|
def is_validate(self, method, src_ip='', trust_user=False):
|
||||||
"""
|
"""
|
||||||
Validate user credential
|
Validate user credential
|
||||||
"""
|
"""
|
||||||
|
@ -202,8 +207,8 @@ class User(db.Model):
|
||||||
User.username == self.username).first()
|
User.username == self.username).first()
|
||||||
|
|
||||||
if user_info:
|
if user_info:
|
||||||
if user_info.password and self.check_password(
|
if trust_user or (user_info.password and self.check_password(
|
||||||
user_info.password):
|
user_info.password)):
|
||||||
current_app.logger.info(
|
current_app.logger.info(
|
||||||
'User "{0}" logged in successfully. Authentication request from {1}'
|
'User "{0}" logged in successfully. Authentication request from {1}'
|
||||||
.format(self.username, src_ip))
|
.format(self.username, src_ip))
|
||||||
|
@ -231,7 +236,7 @@ class User(db.Model):
|
||||||
LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled')
|
LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled')
|
||||||
|
|
||||||
# validate AD user password
|
# validate AD user password
|
||||||
if Setting().get('ldap_type') == 'ad':
|
if Setting().get('ldap_type') == 'ad' and not trust_user:
|
||||||
ldap_username = "{0}@{1}".format(self.username,
|
ldap_username = "{0}@{1}".format(self.username,
|
||||||
Setting().get('ldap_domain'))
|
Setting().get('ldap_domain'))
|
||||||
if not self.ldap_auth(ldap_username, self.password):
|
if not self.ldap_auth(ldap_username, self.password):
|
||||||
|
@ -258,7 +263,7 @@ class User(db.Model):
|
||||||
ldap_username = ldap.filter.escape_filter_chars(
|
ldap_username = ldap.filter.escape_filter_chars(
|
||||||
ldap_result[0][0][0])
|
ldap_result[0][0][0])
|
||||||
|
|
||||||
if Setting().get('ldap_type') != 'ad':
|
if Setting().get('ldap_type') != 'ad' and not trust_user:
|
||||||
# validate ldap user password
|
# validate ldap user password
|
||||||
if not self.ldap_auth(ldap_username, self.password):
|
if not self.ldap_auth(ldap_username, self.password):
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
|
@ -431,7 +436,8 @@ class User(db.Model):
|
||||||
return {'status': False, 'msg': 'Email address is already in use'}
|
return {'status': False, 'msg': 'Email address is already in use'}
|
||||||
|
|
||||||
# first register user will be in Administrator role
|
# first register user will be in Administrator role
|
||||||
self.role_id = Role.query.filter_by(name='User').first().id
|
if self.role_id is None:
|
||||||
|
self.role_id = Role.query.filter_by(name='User').first().id
|
||||||
if User.query.count() == 0:
|
if User.query.count() == 0:
|
||||||
self.role_id = Role.query.filter_by(
|
self.role_id = Role.query.filter_by(
|
||||||
name='Administrator').first().id
|
name='Administrator').first().id
|
||||||
|
@ -473,7 +479,7 @@ class User(db.Model):
|
||||||
user.email = self.email
|
user.email = self.email
|
||||||
|
|
||||||
# store new password hash (only if changed)
|
# store new password hash (only if changed)
|
||||||
if self.plain_text_password != "":
|
if self.plain_text_password:
|
||||||
user.password = self.get_hashed_password(
|
user.password = self.get_hashed_password(
|
||||||
self.plain_text_password).decode("utf-8")
|
self.plain_text_password).decode("utf-8")
|
||||||
|
|
||||||
|
@ -484,7 +490,6 @@ class User(db.Model):
|
||||||
"""
|
"""
|
||||||
Update user profile
|
Update user profile
|
||||||
"""
|
"""
|
||||||
|
|
||||||
user = User.query.filter(User.username == self.username).first()
|
user = User.query.filter(User.username == self.username).first()
|
||||||
if not user:
|
if not user:
|
||||||
return False
|
return False
|
||||||
|
@ -540,9 +545,26 @@ class User(db.Model):
|
||||||
Note: This doesn't include the permission granting from Account
|
Note: This doesn't include the permission granting from Account
|
||||||
which user belong to
|
which user belong to
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return self.get_domain_query().all()
|
return self.get_domain_query().all()
|
||||||
|
|
||||||
|
def get_user_domains(self):
|
||||||
|
from ..models.base import db
|
||||||
|
from .account import Account
|
||||||
|
from .domain import Domain
|
||||||
|
from .account_user import AccountUser
|
||||||
|
from .domain_user import DomainUser
|
||||||
|
|
||||||
|
domains = db.session.query(Domain) \
|
||||||
|
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||||
|
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||||
|
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||||
|
.filter(
|
||||||
|
db.or_(
|
||||||
|
DomainUser.user_id == self.id,
|
||||||
|
AccountUser.user_id == self.id
|
||||||
|
)).all()
|
||||||
|
return domains
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
Delete a user
|
Delete a user
|
||||||
|
@ -560,7 +582,7 @@ class User(db.Model):
|
||||||
self.username, e))
|
self.username, e))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def revoke_privilege(self):
|
def revoke_privilege(self, update_user=False):
|
||||||
"""
|
"""
|
||||||
Revoke all privileges from a user
|
Revoke all privileges from a user
|
||||||
"""
|
"""
|
||||||
|
@ -570,6 +592,8 @@ class User(db.Model):
|
||||||
user_id = user.id
|
user_id = user.id
|
||||||
try:
|
try:
|
||||||
DomainUser.query.filter(DomainUser.user_id == user_id).delete()
|
DomainUser.query.filter(DomainUser.user_id == user_id).delete()
|
||||||
|
if (update_user)==True:
|
||||||
|
AccountUser.query.filter(AccountUser.user_id == user_id).delete()
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -589,3 +613,195 @@ class User(db.Model):
|
||||||
return {'status': True, 'msg': 'Set user role successfully'}
|
return {'status': True, 'msg': 'Set user role successfully'}
|
||||||
else:
|
else:
|
||||||
return {'status': False, 'msg': 'Role does not exist'}
|
return {'status': False, 'msg': 'Role does not exist'}
|
||||||
|
|
||||||
|
@orm.reconstructor
|
||||||
|
def set_account(self):
|
||||||
|
self.accounts = self.get_accounts()
|
||||||
|
|
||||||
|
def get_accounts(self):
|
||||||
|
"""
|
||||||
|
Get accounts associated with this user
|
||||||
|
"""
|
||||||
|
from .account import Account
|
||||||
|
from .account_user import AccountUser
|
||||||
|
accounts = []
|
||||||
|
query = db.session\
|
||||||
|
.query(
|
||||||
|
AccountUser,
|
||||||
|
Account)\
|
||||||
|
.filter(self.id == AccountUser.user_id)\
|
||||||
|
.filter(Account.id == AccountUser.account_id)\
|
||||||
|
.order_by(Account.name)\
|
||||||
|
.all()
|
||||||
|
for q in query:
|
||||||
|
accounts.append(q[1])
|
||||||
|
return accounts
|
||||||
|
|
||||||
|
def get_qrcode_value(self):
|
||||||
|
img = qrc.make(self.get_totp_uri(),
|
||||||
|
image_factory=qrc_svg.SvgPathImage)
|
||||||
|
stream = BytesIO()
|
||||||
|
img.save(stream)
|
||||||
|
return stream.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def read_entitlements(self, key):
|
||||||
|
"""
|
||||||
|
Get entitlements from ldap server associated with this user
|
||||||
|
"""
|
||||||
|
LDAP_BASE_DN = Setting().get('ldap_base_dn')
|
||||||
|
LDAP_FILTER_USERNAME = Setting().get('ldap_filter_username')
|
||||||
|
LDAP_FILTER_BASIC = Setting().get('ldap_filter_basic')
|
||||||
|
searchFilter = "(&({0}={1}){2})".format(LDAP_FILTER_USERNAME,
|
||||||
|
self.username,
|
||||||
|
LDAP_FILTER_BASIC)
|
||||||
|
current_app.logger.debug('Ldap searchFilter {0}'.format(searchFilter))
|
||||||
|
ldap_result = self.ldap_search(searchFilter, LDAP_BASE_DN, [key])
|
||||||
|
current_app.logger.debug('Ldap search result: {0}'.format(ldap_result))
|
||||||
|
entitlements=[]
|
||||||
|
if ldap_result:
|
||||||
|
dict=ldap_result[0][0][1]
|
||||||
|
if len(dict)!=0:
|
||||||
|
for entitlement in dict[key]:
|
||||||
|
entitlements.append(entitlement.decode("utf-8"))
|
||||||
|
else:
|
||||||
|
e="Not found value in the autoprovisioning attribute field "
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
return entitlements
|
||||||
|
|
||||||
|
def updateUser(self, Entitlements):
|
||||||
|
"""
|
||||||
|
Update user associations based on ldap attribute
|
||||||
|
"""
|
||||||
|
entitlements= getCorrectEntitlements(Entitlements)
|
||||||
|
if len(entitlements)!=0:
|
||||||
|
self.revoke_privilege(True)
|
||||||
|
for entitlement in entitlements:
|
||||||
|
arguments=entitlement.split(':')
|
||||||
|
entArgs=arguments[arguments.index('powerdns-admin')+1:]
|
||||||
|
role= entArgs[0]
|
||||||
|
self.set_role(role)
|
||||||
|
if (role=="User") and len(entArgs)>1:
|
||||||
|
current_domains=getUserInfo(self.get_user_domains())
|
||||||
|
current_accounts=getUserInfo(self.get_accounts())
|
||||||
|
domain=entArgs[1]
|
||||||
|
self.addMissingDomain(domain, current_domains)
|
||||||
|
if len(entArgs)>2:
|
||||||
|
account=entArgs[2]
|
||||||
|
self.addMissingAccount(account, current_accounts)
|
||||||
|
|
||||||
|
def addMissingDomain(self, autoprovision_domain, current_domains):
|
||||||
|
"""
|
||||||
|
Add domain gathered by autoprovisioning to the current domains list of a user
|
||||||
|
"""
|
||||||
|
from ..models.domain import Domain
|
||||||
|
user = db.session.query(User).filter(User.username == self.username).first()
|
||||||
|
if autoprovision_domain not in current_domains:
|
||||||
|
domain= db.session.query(Domain).filter(Domain.name == autoprovision_domain).first()
|
||||||
|
if domain!=None:
|
||||||
|
domain.add_user(user)
|
||||||
|
|
||||||
|
def addMissingAccount(self, autoprovision_account, current_accounts):
|
||||||
|
"""
|
||||||
|
Add account gathered by autoprovisioning to the current accounts list of a user
|
||||||
|
"""
|
||||||
|
from ..models.account import Account
|
||||||
|
user = db.session.query(User).filter(User.username == self.username).first()
|
||||||
|
if autoprovision_account not in current_accounts:
|
||||||
|
account= db.session.query(Account).filter(Account.name == autoprovision_account).first()
|
||||||
|
if account!=None:
|
||||||
|
account.add_user(user)
|
||||||
|
|
||||||
|
def getCorrectEntitlements(Entitlements):
|
||||||
|
"""
|
||||||
|
Gather a list of valid records from the ldap attribute given
|
||||||
|
"""
|
||||||
|
from ..models.role import Role
|
||||||
|
urn_value=Setting().get('urn_value')
|
||||||
|
urnArgs=[x.lower() for x in urn_value.split(':')]
|
||||||
|
entitlements=[]
|
||||||
|
for Entitlement in Entitlements:
|
||||||
|
arguments=Entitlement.split(':')
|
||||||
|
|
||||||
|
if ('powerdns-admin' in arguments):
|
||||||
|
prefix=arguments[0:arguments.index('powerdns-admin')]
|
||||||
|
prefix=[x.lower() for x in prefix]
|
||||||
|
if (prefix!=urnArgs):
|
||||||
|
e= "Typo in first part of urn value"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
else:
|
||||||
|
e="Entry not a PowerDNS-Admin record"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(arguments)<=len(urnArgs)+1: #prefix:powerdns-admin
|
||||||
|
e="No value given after the prefix"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
entArgs=arguments[arguments.index('powerdns-admin')+1:]
|
||||||
|
role=entArgs[0]
|
||||||
|
roles= Role.query.all()
|
||||||
|
role_names=get_role_names(roles)
|
||||||
|
|
||||||
|
if role not in role_names:
|
||||||
|
e="Role given by entry not a role availabe in PowerDNS-Admin. Check for spelling errors"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(entArgs)>1:
|
||||||
|
if (role!="User"):
|
||||||
|
e="Too many arguments for Admin or Operator"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
if len(entArgs)<=3:
|
||||||
|
if entArgs[1] and not checkIfDomainExists(entArgs[1]):
|
||||||
|
continue
|
||||||
|
if len(entArgs)==3:
|
||||||
|
if entArgs[2] and not checkIfAccountExists(entArgs[2]):
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
e="Too many arguments"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
continue
|
||||||
|
|
||||||
|
entitlements.append(Entitlement)
|
||||||
|
|
||||||
|
return entitlements
|
||||||
|
|
||||||
|
|
||||||
|
def checkIfDomainExists(domainName):
|
||||||
|
from ..models.domain import Domain
|
||||||
|
domain= db.session.query(Domain).filter(Domain.name == domainName)
|
||||||
|
if len(domain.all())==0:
|
||||||
|
e= domainName + " is not found in the database"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def checkIfAccountExists(accountName):
|
||||||
|
from ..models.account import Account
|
||||||
|
account= db.session.query(Account).filter(Account.name == accountName)
|
||||||
|
if len(account.all())==0:
|
||||||
|
e= accountName + " is not found in the database"
|
||||||
|
current_app.logger.warning("Cannot apply autoprovisioning on user: {}".format(e))
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_role_names(roles):
|
||||||
|
"""
|
||||||
|
returns all the roles available in database in string format
|
||||||
|
"""
|
||||||
|
roles_list=[]
|
||||||
|
for role in roles:
|
||||||
|
roles_list.append(role.name)
|
||||||
|
return roles_list
|
||||||
|
|
||||||
|
def getUserInfo(DomainsOrAccounts):
|
||||||
|
current=[]
|
||||||
|
for DomainOrAccount in DomainsOrAccounts:
|
||||||
|
current.append(DomainOrAccount.name)
|
||||||
|
return current
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -28,6 +28,19 @@ def handle_internal_server_error(e):
|
||||||
return render_template('errors/500.html', code=500, message=e), 500
|
return render_template('errors/500.html', code=500, message=e), 500
|
||||||
|
|
||||||
|
|
||||||
|
def load_if_valid(user, method, src_ip, trust_user = False):
|
||||||
|
try:
|
||||||
|
auth = user.is_validate(method, src_ip, trust_user)
|
||||||
|
if auth == False:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
# login_user(user, remember=False)
|
||||||
|
return User.query.filter(User.id==user.id).first()
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.error('Error: {0}'.format(e))
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
@login_manager.user_loader
|
||||||
def load_user(id):
|
def load_user(id):
|
||||||
"""
|
"""
|
||||||
|
@ -37,29 +50,42 @@ def load_user(id):
|
||||||
|
|
||||||
|
|
||||||
@login_manager.request_loader
|
@login_manager.request_loader
|
||||||
def login_via_authorization_header(request):
|
def login_via_authorization_header_or_remote_user(request):
|
||||||
|
# Try to login using Basic Authentication
|
||||||
auth_header = request.headers.get('Authorization')
|
auth_header = request.headers.get('Authorization')
|
||||||
if auth_header:
|
if auth_header:
|
||||||
|
auth_method = request.args.get('auth_method', 'LOCAL')
|
||||||
|
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
|
||||||
auth_header = auth_header.replace('Basic ', '', 1)
|
auth_header = auth_header.replace('Basic ', '', 1)
|
||||||
try:
|
try:
|
||||||
auth_header = str(base64.b64decode(auth_header), 'utf-8')
|
auth_header = str(base64.b64decode(auth_header), 'utf-8')
|
||||||
username, password = auth_header.split(":")
|
username, password = auth_header.split(":")
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
user = User(username=username,
|
user = User(username=username,
|
||||||
password=password,
|
password=password,
|
||||||
plain_text_password=password)
|
plain_text_password=password)
|
||||||
try:
|
return load_if_valid(user, method=auth_method, src_ip=request.remote_addr)
|
||||||
auth_method = request.args.get('auth_method', 'LOCAL')
|
|
||||||
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
|
# Try login by checking a REMOTE_USER environment variable
|
||||||
auth = user.is_validate(method=auth_method,
|
remote_user = request.remote_user
|
||||||
src_ip=request.remote_addr)
|
if remote_user and current_app.config.get('REMOTE_USER_ENABLED'):
|
||||||
if auth == False:
|
session_remote_user = session.get('remote_user')
|
||||||
return None
|
|
||||||
else:
|
# If we already validated a remote user against an authorization method
|
||||||
# login_user(user, remember=False)
|
# a local user should have been created in the database, so we force a 'LOCAL' auth_method
|
||||||
return User.query.filter(User.id==user.id).first()
|
auth_method = 'LOCAL' if session_remote_user else current_app.config.get('REMOTE_AUTH_METHOD', 'LDAP')
|
||||||
except Exception as e:
|
current_app.logger.debug(
|
||||||
current_app.logger.error('Error: {0}'.format(e))
|
'REMOTE_USER environment variable found: attempting {0} authentication for username "{1}"'
|
||||||
return None
|
.format(auth_method, remote_user))
|
||||||
|
user = User(username=remote_user.strip())
|
||||||
|
valid_remote_user = load_if_valid(user, method=auth_method, src_ip=request.remote_addr, trust_user=True)
|
||||||
|
|
||||||
|
if valid_remote_user:
|
||||||
|
# If we were successful in authenticating a trusted remote user, store it in session
|
||||||
|
session['remote_user'] = valid_remote_user.username
|
||||||
|
|
||||||
|
return valid_remote_user
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -3,6 +3,7 @@ from flask import Blueprint, render_template, url_for, current_app, request, jso
|
||||||
from flask_login import login_required, current_user, login_manager
|
from flask_login import login_required, current_user, login_manager
|
||||||
from sqlalchemy import not_
|
from sqlalchemy import not_
|
||||||
|
|
||||||
|
from ..decorators import operator_role_required
|
||||||
from ..lib.utils import customBoxes
|
from ..lib.utils import customBoxes
|
||||||
from ..models.user import User, Anonymous
|
from ..models.user import User, Anonymous
|
||||||
from ..models.account import Account
|
from ..models.account import Account
|
||||||
|
@ -60,7 +61,7 @@ def domains_custom(boxId):
|
||||||
))
|
))
|
||||||
|
|
||||||
template = current_app.jinja_env.get_template("dashboard_domain.html")
|
template = current_app.jinja_env.get_template("dashboard_domain.html")
|
||||||
render = template.make_module(vars={"current_user": current_user})
|
render = template.make_module(vars={"current_user": current_user, "allow_user_view_history": Setting().get('allow_user_view_history')})
|
||||||
|
|
||||||
columns = [
|
columns = [
|
||||||
Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master,
|
Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master,
|
||||||
|
@ -150,11 +151,46 @@ def dashboard():
|
||||||
else:
|
else:
|
||||||
current_app.logger.info('Updating domains in background...')
|
current_app.logger.info('Updating domains in background...')
|
||||||
|
|
||||||
|
show_bg_domain_button = BG_DOMAIN_UPDATE
|
||||||
|
if BG_DOMAIN_UPDATE and current_user.role.name not in ['Administrator', 'Operator']:
|
||||||
|
show_bg_domain_button = False
|
||||||
|
|
||||||
# Stats for dashboard
|
# Stats for dashboard
|
||||||
domain_count = Domain.query.count()
|
domain_count = 0
|
||||||
|
history_number = 0
|
||||||
|
history = []
|
||||||
user_num = User.query.count()
|
user_num = User.query.count()
|
||||||
history_number = History.query.count()
|
if current_user.role.name in ['Administrator', 'Operator']:
|
||||||
history = History.query.order_by(History.created_on.desc()).limit(4)
|
domain_count = Domain.query.count()
|
||||||
|
history_number = History.query.count()
|
||||||
|
history = History.query.order_by(History.created_on.desc()).limit(4).all()
|
||||||
|
elif Setting().get('allow_user_view_history'):
|
||||||
|
history = db.session.query(History) \
|
||||||
|
.join(Domain, History.domain_id == Domain.id) \
|
||||||
|
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||||
|
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||||
|
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||||
|
.order_by(History.created_on.desc()) \
|
||||||
|
.filter(
|
||||||
|
db.or_(
|
||||||
|
DomainUser.user_id == current_user.id,
|
||||||
|
AccountUser.user_id == current_user.id
|
||||||
|
)).all()
|
||||||
|
history_number = len(history) # history.count()
|
||||||
|
history = history[:4]
|
||||||
|
domain_count = db.session.query(Domain) \
|
||||||
|
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||||
|
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||||
|
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||||
|
.filter(
|
||||||
|
db.or_(
|
||||||
|
DomainUser.user_id == current_user.id,
|
||||||
|
AccountUser.user_id == current_user.id
|
||||||
|
)).count()
|
||||||
|
|
||||||
|
from .admin import convert_histories, DetailedHistory
|
||||||
|
detailedHistories = convert_histories(history)
|
||||||
|
|
||||||
server = Server(server_id='localhost')
|
server = Server(server_id='localhost')
|
||||||
statistics = server.get_statistic()
|
statistics = server.get_statistic()
|
||||||
if statistics:
|
if statistics:
|
||||||
|
@ -171,13 +207,14 @@ def dashboard():
|
||||||
user_num=user_num,
|
user_num=user_num,
|
||||||
history_number=history_number,
|
history_number=history_number,
|
||||||
uptime=uptime,
|
uptime=uptime,
|
||||||
histories=history,
|
histories=detailedHistories,
|
||||||
show_bg_domain_button=BG_DOMAIN_UPDATE,
|
show_bg_domain_button=show_bg_domain_button,
|
||||||
pdns_version=Setting().get('pdns_version'))
|
pdns_version=Setting().get('pdns_version'))
|
||||||
|
|
||||||
|
|
||||||
@dashboard_bp.route('/domains-updater', methods=['GET', 'POST'])
|
@dashboard_bp.route('/domains-updater', methods=['GET', 'POST'])
|
||||||
@login_required
|
@login_required
|
||||||
|
@operator_role_required
|
||||||
def domains_updater():
|
def domains_updater():
|
||||||
current_app.logger.debug('Update domains in background')
|
current_app.logger.debug('Update domains in background')
|
||||||
d = Domain().update()
|
d = Domain().update()
|
||||||
|
|
|
@ -8,8 +8,9 @@ from distutils.version import StrictVersion
|
||||||
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, abort, jsonify, g, session
|
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, abort, jsonify, g, session
|
||||||
from flask_login import login_required, current_user, login_manager
|
from flask_login import login_required, current_user, login_manager
|
||||||
|
|
||||||
|
from ..lib.utils import pretty_domain_name
|
||||||
from ..lib.utils import pretty_json
|
from ..lib.utils import pretty_json
|
||||||
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec
|
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec, can_remove_domain
|
||||||
from ..models.user import User, Anonymous
|
from ..models.user import User, Anonymous
|
||||||
from ..models.account import Account
|
from ..models.account import Account
|
||||||
from ..models.setting import Setting
|
from ..models.setting import Setting
|
||||||
|
@ -20,7 +21,11 @@ from ..models.record_entry import RecordEntry
|
||||||
from ..models.domain_template import DomainTemplate
|
from ..models.domain_template import DomainTemplate
|
||||||
from ..models.domain_template_record import DomainTemplateRecord
|
from ..models.domain_template_record import DomainTemplateRecord
|
||||||
from ..models.domain_setting import DomainSetting
|
from ..models.domain_setting import DomainSetting
|
||||||
|
from ..models.base import db
|
||||||
|
from ..models.domain_user import DomainUser
|
||||||
|
from ..models.account_user import AccountUser
|
||||||
|
from .admin import extract_changelogs_from_a_history_entry
|
||||||
|
from ..decorators import history_access_required
|
||||||
domain_bp = Blueprint('domain',
|
domain_bp = Blueprint('domain',
|
||||||
__name__,
|
__name__,
|
||||||
template_folder='templates',
|
template_folder='templates',
|
||||||
|
@ -61,7 +66,7 @@ def domain(domain_name):
|
||||||
current_app.logger.debug("Fetched rrests: \n{}".format(pretty_json(rrsets)))
|
current_app.logger.debug("Fetched rrests: \n{}".format(pretty_json(rrsets)))
|
||||||
|
|
||||||
# API server might be down, misconfigured
|
# API server might be down, misconfigured
|
||||||
if not rrsets:
|
if not rrsets and domain.type != 'Slave':
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
quick_edit = Setting().get('record_quick_edit')
|
quick_edit = Setting().get('record_quick_edit')
|
||||||
|
@ -91,7 +96,7 @@ def domain(domain_name):
|
||||||
# If it is reverse zone and pretty_ipv6_ptr setting
|
# If it is reverse zone and pretty_ipv6_ptr setting
|
||||||
# is enabled, we reformat the name for ipv6 records.
|
# is enabled, we reformat the name for ipv6 records.
|
||||||
if Setting().get('pretty_ipv6_ptr') and r[
|
if Setting().get('pretty_ipv6_ptr') and r[
|
||||||
'type'] == 'PTR' and 'ip6.arpa' in r_name:
|
'type'] == 'PTR' and 'ip6.arpa' in r_name and '*' not in r_name:
|
||||||
r_name = dns.reversename.to_address(
|
r_name = dns.reversename.to_address(
|
||||||
dns.name.from_text(r_name))
|
dns.name.from_text(r_name))
|
||||||
|
|
||||||
|
@ -99,14 +104,17 @@ def domain(domain_name):
|
||||||
# PDA jinja2 template can understand.
|
# PDA jinja2 template can understand.
|
||||||
index = 0
|
index = 0
|
||||||
for record in r['records']:
|
for record in r['records']:
|
||||||
|
if (len(r['comments'])>index):
|
||||||
|
c=r['comments'][index]['content']
|
||||||
|
else:
|
||||||
|
c=''
|
||||||
record_entry = RecordEntry(
|
record_entry = RecordEntry(
|
||||||
name=r_name,
|
name=r_name,
|
||||||
type=r['type'],
|
type=r['type'],
|
||||||
status='Disabled' if record['disabled'] else 'Active',
|
status='Disabled' if record['disabled'] else 'Active',
|
||||||
ttl=r['ttl'],
|
ttl=r['ttl'],
|
||||||
data=record['content'],
|
data=record['content'],
|
||||||
comment=r['comments'][index]['content']
|
comment=c,
|
||||||
if r['comments'] else '',
|
|
||||||
is_allowed_edit=True)
|
is_allowed_edit=True)
|
||||||
index += 1
|
index += 1
|
||||||
records.append(record_entry)
|
records.append(record_entry)
|
||||||
|
@ -124,7 +132,217 @@ def domain(domain_name):
|
||||||
records=records,
|
records=records,
|
||||||
editable_records=editable_records,
|
editable_records=editable_records,
|
||||||
quick_edit=quick_edit,
|
quick_edit=quick_edit,
|
||||||
ttl_options=ttl_options)
|
ttl_options=ttl_options,
|
||||||
|
current_user=current_user)
|
||||||
|
|
||||||
|
|
||||||
|
@domain_bp.route('/remove', methods=['GET', 'POST'])
|
||||||
|
@login_required
|
||||||
|
@can_remove_domain
|
||||||
|
def remove():
|
||||||
|
# domains is a list of all the domains a User may access
|
||||||
|
# Admins may access all
|
||||||
|
# Regular users only if they are associated with the domain
|
||||||
|
if current_user.role.name in ['Administrator', 'Operator']:
|
||||||
|
domains = Domain.query.order_by(Domain.name).all()
|
||||||
|
else:
|
||||||
|
# Get query for domain to which the user has access permission.
|
||||||
|
# This includes direct domain permission AND permission through
|
||||||
|
# account membership
|
||||||
|
domains = db.session.query(Domain) \
|
||||||
|
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||||
|
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||||
|
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||||
|
.filter(
|
||||||
|
db.or_(
|
||||||
|
DomainUser.user_id == current_user.id,
|
||||||
|
AccountUser.user_id == current_user.id
|
||||||
|
)).order_by(Domain.name)
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
# TODO Change name from 'domainid' to something else, its confusing
|
||||||
|
domain_name = request.form['domainid']
|
||||||
|
|
||||||
|
# Get domain from Database, might be None
|
||||||
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
|
|
||||||
|
# Check if the domain is in domains before removal
|
||||||
|
if domain not in domains:
|
||||||
|
abort(403)
|
||||||
|
|
||||||
|
# Delete
|
||||||
|
d = Domain()
|
||||||
|
result = d.delete(domain_name)
|
||||||
|
|
||||||
|
if result['status'] == 'error':
|
||||||
|
abort(500)
|
||||||
|
|
||||||
|
history = History(msg='Delete domain {0}'.format(
|
||||||
|
pretty_domain_name(domain_name)),
|
||||||
|
created_by=current_user.username)
|
||||||
|
history.add()
|
||||||
|
|
||||||
|
return redirect(url_for('dashboard.dashboard'))
|
||||||
|
|
||||||
|
else:
|
||||||
|
# On GET return the domains we got earlier
|
||||||
|
return render_template('domain_remove.html',
|
||||||
|
domainss=domains)
|
||||||
|
|
||||||
|
@domain_bp.route('/<path:domain_name>/changelog', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
@can_access_domain
|
||||||
|
@history_access_required
|
||||||
|
def changelog(domain_name):
|
||||||
|
g.user = current_user
|
||||||
|
login_manager.anonymous_user = Anonymous
|
||||||
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
|
if not domain:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
# Query domain's rrsets from PowerDNS API
|
||||||
|
rrsets = Record().get_rrsets(domain.name)
|
||||||
|
current_app.logger.debug("Fetched rrests: \n{}".format(pretty_json(rrsets)))
|
||||||
|
|
||||||
|
# API server might be down, misconfigured
|
||||||
|
if not rrsets and domain.type != 'Slave':
|
||||||
|
abort(500)
|
||||||
|
|
||||||
|
records_allow_to_edit = Setting().get_records_allow_to_edit()
|
||||||
|
records = []
|
||||||
|
|
||||||
|
# get all changelogs for this domain, in descening order
|
||||||
|
if current_user.role.name in [ 'Administrator', 'Operator' ]:
|
||||||
|
histories = History.query.filter(History.domain_id == domain.id).order_by(History.created_on.desc()).all()
|
||||||
|
else:
|
||||||
|
# if the user isn't an administrator or operator,
|
||||||
|
# allow_user_view_history must be enabled to get here,
|
||||||
|
# so include history for the domains for the user
|
||||||
|
histories = db.session.query(History) \
|
||||||
|
.join(Domain, History.domain_id == Domain.id) \
|
||||||
|
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||||
|
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||||
|
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||||
|
.order_by(History.created_on.desc()) \
|
||||||
|
.filter(
|
||||||
|
db.and_(db.or_(
|
||||||
|
DomainUser.user_id == current_user.id,
|
||||||
|
AccountUser.user_id == current_user.id
|
||||||
|
),
|
||||||
|
History.domain_id == domain.id
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
if StrictVersion(Setting().get('pdns_version')) >= StrictVersion('4.0.0'):
|
||||||
|
for r in rrsets:
|
||||||
|
if r['type'] in records_allow_to_edit:
|
||||||
|
r_name = r['name'].rstrip('.')
|
||||||
|
|
||||||
|
# If it is reverse zone and pretty_ipv6_ptr setting
|
||||||
|
# is enabled, we reformat the name for ipv6 records.
|
||||||
|
if Setting().get('pretty_ipv6_ptr') and r[
|
||||||
|
'type'] == 'PTR' and 'ip6.arpa' in r_name and '*' not in r_name:
|
||||||
|
r_name = dns.reversename.to_address(
|
||||||
|
dns.name.from_text(r_name))
|
||||||
|
|
||||||
|
# Create the list of records in format that
|
||||||
|
# PDA jinja2 template can understand.
|
||||||
|
index = 0
|
||||||
|
for record in r['records']:
|
||||||
|
if (len(r['comments'])>index):
|
||||||
|
c=r['comments'][index]['content']
|
||||||
|
else:
|
||||||
|
c=''
|
||||||
|
record_entry = RecordEntry(
|
||||||
|
name=r_name,
|
||||||
|
type=r['type'],
|
||||||
|
status='Disabled' if record['disabled'] else 'Active',
|
||||||
|
ttl=r['ttl'],
|
||||||
|
data=record['content'],
|
||||||
|
comment=c,
|
||||||
|
is_allowed_edit=True)
|
||||||
|
index += 1
|
||||||
|
records.append(record_entry)
|
||||||
|
else:
|
||||||
|
# Unsupported version
|
||||||
|
abort(500)
|
||||||
|
|
||||||
|
changes_set = dict()
|
||||||
|
for i in range(len(histories)):
|
||||||
|
extract_changelogs_from_a_history_entry(changes_set, histories[i], i)
|
||||||
|
if i in changes_set and len(changes_set[i]) == 0: # if empty, then remove the key
|
||||||
|
changes_set.pop(i)
|
||||||
|
return render_template('domain_changelog.html', domain=domain, allHistoryChanges=changes_set)
|
||||||
|
|
||||||
|
"""
|
||||||
|
Returns a changelog for a specific pair of (record_name, record_type)
|
||||||
|
"""
|
||||||
|
@domain_bp.route('/<path:domain_name>/changelog/<path:record_name>-<path:record_type>', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
@can_access_domain
|
||||||
|
@history_access_required
|
||||||
|
def record_changelog(domain_name, record_name, record_type):
|
||||||
|
|
||||||
|
g.user = current_user
|
||||||
|
login_manager.anonymous_user = Anonymous
|
||||||
|
domain = Domain.query.filter(Domain.name == domain_name).first()
|
||||||
|
if not domain:
|
||||||
|
abort(404)
|
||||||
|
# Query domain's rrsets from PowerDNS API
|
||||||
|
rrsets = Record().get_rrsets(domain.name)
|
||||||
|
current_app.logger.debug("Fetched rrests: \n{}".format(pretty_json(rrsets)))
|
||||||
|
|
||||||
|
# API server might be down, misconfigured
|
||||||
|
if not rrsets and domain.type != 'Slave':
|
||||||
|
abort(500)
|
||||||
|
|
||||||
|
# get all changelogs for this domain, in descening order
|
||||||
|
if current_user.role.name in [ 'Administrator', 'Operator' ]:
|
||||||
|
histories = History.query.filter(History.domain_id == domain.id).order_by(History.created_on.desc()).all()
|
||||||
|
else:
|
||||||
|
# if the user isn't an administrator or operator,
|
||||||
|
# allow_user_view_history must be enabled to get here,
|
||||||
|
# so include history for the domains for the user
|
||||||
|
histories = db.session.query(History) \
|
||||||
|
.join(Domain, History.domain_id == Domain.id) \
|
||||||
|
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||||
|
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||||
|
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||||
|
.order_by(History.created_on.desc()) \
|
||||||
|
.filter(
|
||||||
|
db.and_(db.or_(
|
||||||
|
DomainUser.user_id == current_user.id,
|
||||||
|
AccountUser.user_id == current_user.id
|
||||||
|
),
|
||||||
|
History.domain_id == domain.id
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
changes_set_of_record = dict()
|
||||||
|
for i in range(len(histories)):
|
||||||
|
extract_changelogs_from_a_history_entry(changes_set_of_record, histories[i], i, record_name, record_type)
|
||||||
|
if i in changes_set_of_record and len(changes_set_of_record[i]) == 0: # if empty, then remove the key
|
||||||
|
changes_set_of_record.pop(i)
|
||||||
|
|
||||||
|
indexes_to_pop = []
|
||||||
|
for change_num in changes_set_of_record:
|
||||||
|
changes_i = changes_set_of_record[change_num]
|
||||||
|
for hre in changes_i: # for each history record entry in changes_i
|
||||||
|
if 'type' in hre.add_rrest and hre.add_rrest['name'] == record_name and hre.add_rrest['type'] == record_type:
|
||||||
|
continue
|
||||||
|
elif 'type' in hre.del_rrest and hre.del_rrest['name'] == record_name and hre.del_rrest['type'] == record_type:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
changes_set_of_record[change_num].remove(hre)
|
||||||
|
if change_num in changes_set_of_record and len(changes_set_of_record[change_num]) == 0: # if empty, then remove the key
|
||||||
|
indexes_to_pop.append(change_num)
|
||||||
|
|
||||||
|
for i in indexes_to_pop:
|
||||||
|
changes_set_of_record.pop(i)
|
||||||
|
|
||||||
|
return render_template('domain_changelog.html', domain=domain, allHistoryChanges=changes_set_of_record,
|
||||||
|
record_name = record_name, record_type = record_type)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@domain_bp.route('/add', methods=['GET', 'POST'])
|
@domain_bp.route('/add', methods=['GET', 'POST'])
|
||||||
|
@ -145,7 +363,30 @@ def add():
|
||||||
'errors/400.html',
|
'errors/400.html',
|
||||||
msg="Please enter a valid domain name"), 400
|
msg="Please enter a valid domain name"), 400
|
||||||
|
|
||||||
|
# If User creates the domain, check some additional stuff
|
||||||
|
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||||
|
# Get all the account_ids of the user
|
||||||
|
user_accounts_ids = current_user.get_accounts()
|
||||||
|
user_accounts_ids = [x.id for x in user_accounts_ids]
|
||||||
|
# User may not create domains without Account
|
||||||
|
if int(account_id) == 0 or int(account_id) not in user_accounts_ids:
|
||||||
|
return render_template(
|
||||||
|
'errors/400.html',
|
||||||
|
msg="Please use a valid Account"), 400
|
||||||
|
|
||||||
|
|
||||||
#TODO: Validate ip addresses input
|
#TODO: Validate ip addresses input
|
||||||
|
|
||||||
|
# Encode domain name into punycode (IDN)
|
||||||
|
try:
|
||||||
|
domain_name = domain_name.encode('idna').decode()
|
||||||
|
except:
|
||||||
|
current_app.logger.error("Cannot encode the domain name {}".format(domain_name))
|
||||||
|
current_app.logger.debug(traceback.format_exc())
|
||||||
|
return render_template(
|
||||||
|
'errors/400.html',
|
||||||
|
msg="Please enter a valid domain name"), 400
|
||||||
|
|
||||||
if domain_type == 'slave':
|
if domain_type == 'slave':
|
||||||
if request.form.getlist('domain_master_address'):
|
if request.form.getlist('domain_master_address'):
|
||||||
domain_master_string = request.form.getlist(
|
domain_master_string = request.form.getlist(
|
||||||
|
@ -165,13 +406,16 @@ def add():
|
||||||
domain_master_ips=domain_master_ips,
|
domain_master_ips=domain_master_ips,
|
||||||
account_name=account_name)
|
account_name=account_name)
|
||||||
if result['status'] == 'ok':
|
if result['status'] == 'ok':
|
||||||
history = History(msg='Add domain {0}'.format(domain_name),
|
domain_id = Domain().get_id_by_name(domain_name)
|
||||||
|
history = History(msg='Add domain {0}'.format(
|
||||||
|
pretty_domain_name(domain_name)),
|
||||||
detail=str({
|
detail=str({
|
||||||
'domain_type': domain_type,
|
'domain_type': domain_type,
|
||||||
'domain_master_ips': domain_master_ips,
|
'domain_master_ips': domain_master_ips,
|
||||||
'account_id': account_id
|
'account_id': account_id
|
||||||
}),
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=domain_id)
|
||||||
history.add()
|
history.add()
|
||||||
|
|
||||||
# grant user access to the domain
|
# grant user access to the domain
|
||||||
|
@ -212,7 +456,8 @@ def add():
|
||||||
"del_rrests":
|
"del_rrests":
|
||||||
result['data'][1]['rrsets']
|
result['data'][1]['rrsets']
|
||||||
})),
|
})),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=domain_id)
|
||||||
history.add()
|
history.add()
|
||||||
else:
|
else:
|
||||||
history = History(
|
history = History(
|
||||||
|
@ -231,13 +476,19 @@ def add():
|
||||||
current_app.logger.debug(traceback.format_exc())
|
current_app.logger.debug(traceback.format_exc())
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
|
# Get
|
||||||
else:
|
else:
|
||||||
accounts = Account.query.all()
|
# Admins and Operators can set to any account
|
||||||
|
if current_user.role.name in ['Administrator', 'Operator']:
|
||||||
|
accounts = Account.query.order_by(Account.name).all()
|
||||||
|
else:
|
||||||
|
accounts = current_user.get_accounts()
|
||||||
return render_template('domain_add.html',
|
return render_template('domain_add.html',
|
||||||
templates=templates,
|
templates=templates,
|
||||||
accounts=accounts)
|
accounts=accounts)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@domain_bp.route('/setting/<path:domain_name>/delete', methods=['POST'])
|
@domain_bp.route('/setting/<path:domain_name>/delete', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
@operator_role_required
|
@operator_role_required
|
||||||
|
@ -248,7 +499,8 @@ def delete(domain_name):
|
||||||
if result['status'] == 'error':
|
if result['status'] == 'error':
|
||||||
abort(500)
|
abort(500)
|
||||||
|
|
||||||
history = History(msg='Delete domain {0}'.format(domain_name),
|
history = History(msg='Delete domain {0}'.format(
|
||||||
|
pretty_domain_name(domain_name)),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
|
|
||||||
|
@ -264,7 +516,7 @@ def setting(domain_name):
|
||||||
if not domain:
|
if not domain:
|
||||||
abort(404)
|
abort(404)
|
||||||
users = User.query.all()
|
users = User.query.all()
|
||||||
accounts = Account.query.all()
|
accounts = Account.query.order_by(Account.name).all()
|
||||||
|
|
||||||
# get list of user ids to initialize selection data
|
# get list of user ids to initialize selection data
|
||||||
d = Domain(name=domain_name)
|
d = Domain(name=domain_name)
|
||||||
|
@ -291,9 +543,11 @@ def setting(domain_name):
|
||||||
d.grant_privileges(new_user_ids)
|
d.grant_privileges(new_user_ids)
|
||||||
|
|
||||||
history = History(
|
history = History(
|
||||||
msg='Change domain {0} access control'.format(domain_name),
|
msg='Change domain {0} access control'.format(
|
||||||
|
pretty_domain_name(domain_name)),
|
||||||
detail=str({'user_has_access': new_user_list}),
|
detail=str({'user_has_access': new_user_list}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=d.id)
|
||||||
history.add()
|
history.add()
|
||||||
|
|
||||||
return redirect(url_for('domain.setting', domain_name=domain_name))
|
return redirect(url_for('domain.setting', domain_name=domain_name))
|
||||||
|
@ -327,13 +581,15 @@ def change_type(domain_name):
|
||||||
kind=domain_type,
|
kind=domain_type,
|
||||||
masters=domain_master_ips)
|
masters=domain_master_ips)
|
||||||
if status['status'] == 'ok':
|
if status['status'] == 'ok':
|
||||||
history = History(msg='Update type for domain {0}'.format(domain_name),
|
history = History(msg='Update type for domain {0}'.format(
|
||||||
|
pretty_domain_name(domain_name)),
|
||||||
detail=str({
|
detail=str({
|
||||||
"domain": domain_name,
|
"domain": domain_name,
|
||||||
"type": domain_type,
|
"type": domain_type,
|
||||||
"masters": domain_master_ips
|
"masters": domain_master_ips
|
||||||
}),
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=Domain().get_id_by_name(domain_name))
|
||||||
history.add()
|
history.add()
|
||||||
return redirect(url_for('domain.setting', domain_name = domain_name))
|
return redirect(url_for('domain.setting', domain_name = domain_name))
|
||||||
else:
|
else:
|
||||||
|
@ -359,12 +615,14 @@ def change_soa_edit_api(domain_name):
|
||||||
soa_edit_api=new_setting)
|
soa_edit_api=new_setting)
|
||||||
if status['status'] == 'ok':
|
if status['status'] == 'ok':
|
||||||
history = History(
|
history = History(
|
||||||
msg='Update soa_edit_api for domain {0}'.format(domain_name),
|
msg='Update soa_edit_api for domain {0}'.format(
|
||||||
|
pretty_domain_name(domain_name)),
|
||||||
detail=str({
|
detail=str({
|
||||||
"domain": domain_name,
|
"domain": domain_name,
|
||||||
"soa_edit_api": new_setting
|
"soa_edit_api": new_setting
|
||||||
}),
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=d.get_id_by_name(domain_name))
|
||||||
history.add()
|
history.add()
|
||||||
return redirect(url_for('domain.setting', domain_name = domain_name))
|
return redirect(url_for('domain.setting', domain_name = domain_name))
|
||||||
else:
|
else:
|
||||||
|
@ -418,24 +676,35 @@ def record_apply(domain_name):
|
||||||
'status':
|
'status':
|
||||||
'error',
|
'error',
|
||||||
'msg':
|
'msg':
|
||||||
'Domain name {0} does not exist'.format(domain_name)
|
'Domain name {0} does not exist'.format(pretty_domain_name(domain_name))
|
||||||
}), 404)
|
}), 404)
|
||||||
|
|
||||||
r = Record()
|
r = Record()
|
||||||
result = r.apply(domain_name, submitted_record)
|
result = r.apply(domain_name, submitted_record)
|
||||||
if result['status'] == 'ok':
|
if result['status'] == 'ok':
|
||||||
history = History(
|
history = History(
|
||||||
msg='Apply record changes to domain {0}'.format(domain_name),
|
msg='Apply record changes to domain {0}'.format(pretty_domain_name(domain_name)),
|
||||||
detail=str(
|
detail=str(
|
||||||
json.dumps({
|
json.dumps({
|
||||||
"domain": domain_name,
|
"domain": domain_name,
|
||||||
"add_rrests": result['data'][0]['rrsets'],
|
"add_rrests": result['data'][0]['rrsets'],
|
||||||
"del_rrests": result['data'][1]['rrsets']
|
"del_rrests": result['data'][1]['rrsets']
|
||||||
})),
|
})),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=domain.id)
|
||||||
history.add()
|
history.add()
|
||||||
return make_response(jsonify(result), 200)
|
return make_response(jsonify(result), 200)
|
||||||
else:
|
else:
|
||||||
|
history = History(
|
||||||
|
msg='Failed to apply record changes to domain {0}'.format(
|
||||||
|
pretty_domain_name(domain_name)),
|
||||||
|
detail=str(
|
||||||
|
json.dumps({
|
||||||
|
"domain": domain_name,
|
||||||
|
"msg": result['msg'],
|
||||||
|
})),
|
||||||
|
created_by=current_user.username)
|
||||||
|
history.add()
|
||||||
return make_response(jsonify(result), 400)
|
return make_response(jsonify(result), 400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(
|
current_app.logger.error(
|
||||||
|
@ -554,8 +823,10 @@ def admin_setdomainsetting(domain_name):
|
||||||
if setting.set(new_value):
|
if setting.set(new_value):
|
||||||
history = History(
|
history = History(
|
||||||
msg='Setting {0} changed value to {1} for {2}'.
|
msg='Setting {0} changed value to {1} for {2}'.
|
||||||
format(new_setting, new_value, domain.name),
|
format(new_setting, new_value,
|
||||||
created_by=current_user.username)
|
pretty_domain_name(domain_name)),
|
||||||
|
created_by=current_user.username,
|
||||||
|
domain_id=domain.id)
|
||||||
history.add()
|
history.add()
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify({
|
||||||
|
@ -573,8 +844,9 @@ def admin_setdomainsetting(domain_name):
|
||||||
history = History(
|
history = History(
|
||||||
msg=
|
msg=
|
||||||
'New setting {0} with value {1} for {2} has been created'
|
'New setting {0} with value {1} for {2} has been created'
|
||||||
.format(new_setting, new_value, domain.name),
|
.format(new_setting, new_value, pretty_domain_name(domain_name)),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=domain.id)
|
||||||
history.add()
|
history.add()
|
||||||
return make_response(
|
return make_response(
|
||||||
jsonify({
|
jsonify({
|
||||||
|
|
|
@ -4,6 +4,7 @@ import json
|
||||||
import traceback
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import base64
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from yaml import Loader, load
|
from yaml import Loader, load
|
||||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||||
|
@ -43,7 +44,6 @@ index_bp = Blueprint('index',
|
||||||
template_folder='templates',
|
template_folder='templates',
|
||||||
url_prefix='/')
|
url_prefix='/')
|
||||||
|
|
||||||
|
|
||||||
@index_bp.before_app_first_request
|
@index_bp.before_app_first_request
|
||||||
def register_modules():
|
def register_modules():
|
||||||
global google
|
global google
|
||||||
|
@ -168,10 +168,8 @@ def login():
|
||||||
return redirect(url_for('index.login'))
|
return redirect(url_for('index.login'))
|
||||||
|
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
login_user(user, remember=False)
|
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
signin_history(user.username, 'Google OAuth', True)
|
return authenticate_user(user, 'Google OAuth')
|
||||||
return redirect(url_for('index.index'))
|
|
||||||
|
|
||||||
if 'github_token' in session:
|
if 'github_token' in session:
|
||||||
me = json.loads(github.get('user').text)
|
me = json.loads(github.get('user').text)
|
||||||
|
@ -196,9 +194,7 @@ def login():
|
||||||
|
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
login_user(user, remember=False)
|
return authenticate_user(user, 'Github OAuth')
|
||||||
signin_history(user.username, 'Github OAuth', True)
|
|
||||||
return redirect(url_for('index.index'))
|
|
||||||
|
|
||||||
if 'azure_token' in session:
|
if 'azure_token' in session:
|
||||||
azure_info = azure.get('me?$select=displayName,givenName,id,mail,surname,userPrincipalName').text
|
azure_info = azure.get('me?$select=displayName,givenName,id,mail,surname,userPrincipalName').text
|
||||||
|
@ -279,16 +275,102 @@ def login():
|
||||||
error=('User ' + azure_username +
|
error=('User ' + azure_username +
|
||||||
' is not in any authorised groups.'))
|
' is not in any authorised groups.'))
|
||||||
|
|
||||||
login_user(user, remember=False)
|
# Handle account/group creation, if enabled
|
||||||
signin_history(user.username, 'Azure OAuth', True)
|
if Setting().get('azure_group_accounts_enabled') and mygroups:
|
||||||
return redirect(url_for('index.index'))
|
current_app.logger.info('Azure group account sync enabled')
|
||||||
|
name_value = Setting().get('azure_group_accounts_name')
|
||||||
|
description_value = Setting().get('azure_group_accounts_description')
|
||||||
|
select_values = name_value
|
||||||
|
if description_value != '':
|
||||||
|
select_values += ',' + description_value
|
||||||
|
|
||||||
|
mygroups = get_azure_groups(
|
||||||
|
'me/memberOf/microsoft.graph.group?$count=false&$securityEnabled=true&$select={}'.format(select_values))
|
||||||
|
|
||||||
|
description_pattern = Setting().get('azure_group_accounts_description_re')
|
||||||
|
pattern = Setting().get('azure_group_accounts_name_re')
|
||||||
|
|
||||||
|
# Loop through users security groups
|
||||||
|
for azure_group in mygroups:
|
||||||
|
if name_value in azure_group:
|
||||||
|
group_name = azure_group[name_value]
|
||||||
|
group_description = ''
|
||||||
|
if description_value in azure_group:
|
||||||
|
group_description = azure_group[description_value]
|
||||||
|
|
||||||
|
# Do regex search if enabled for group description
|
||||||
|
if description_pattern != '':
|
||||||
|
current_app.logger.info('Matching group description {} against regex {}'.format(
|
||||||
|
group_description, description_pattern))
|
||||||
|
matches = re.match(
|
||||||
|
description_pattern, group_description)
|
||||||
|
if matches:
|
||||||
|
current_app.logger.info(
|
||||||
|
'Group {} matched regexp'.format(group_description))
|
||||||
|
group_description = matches.group(1)
|
||||||
|
else:
|
||||||
|
# Regexp didn't match, continue to next iteration
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Do regex search if enabled for group name
|
||||||
|
if pattern != '':
|
||||||
|
current_app.logger.info(
|
||||||
|
'Matching group name {} against regex {}'.format(group_name, pattern))
|
||||||
|
matches = re.match(pattern, group_name)
|
||||||
|
if matches:
|
||||||
|
current_app.logger.info(
|
||||||
|
'Group {} matched regexp'.format(group_name))
|
||||||
|
group_name = matches.group(1)
|
||||||
|
else:
|
||||||
|
# Regexp didn't match, continue to next iteration
|
||||||
|
continue
|
||||||
|
|
||||||
|
account = Account()
|
||||||
|
account_id = account.get_id_by_name(account_name=group_name)
|
||||||
|
|
||||||
|
if account_id:
|
||||||
|
account = Account.query.get(account_id)
|
||||||
|
# check if user has permissions
|
||||||
|
account_users = account.get_user()
|
||||||
|
current_app.logger.info('Group: {} Users: {}'.format(
|
||||||
|
group_name,
|
||||||
|
account_users))
|
||||||
|
if user.id in account_users:
|
||||||
|
current_app.logger.info('User id {} is already in account {}'.format(
|
||||||
|
user.id, group_name))
|
||||||
|
else:
|
||||||
|
account.add_user(user)
|
||||||
|
history = History(msg='Update account {0}'.format(
|
||||||
|
account.name),
|
||||||
|
created_by='System')
|
||||||
|
history.add()
|
||||||
|
current_app.logger.info('User {} added to Account {}'.format(
|
||||||
|
user.username, account.name))
|
||||||
|
else:
|
||||||
|
account.name = group_name
|
||||||
|
account.description = group_description
|
||||||
|
account.contact = ''
|
||||||
|
account.mail = ''
|
||||||
|
account.create_account()
|
||||||
|
history = History(msg='Create account {0}'.format(
|
||||||
|
account.name),
|
||||||
|
created_by='System')
|
||||||
|
history.add()
|
||||||
|
|
||||||
|
account.add_user(user)
|
||||||
|
history = History(msg='Update account {0}'.format(account.name),
|
||||||
|
created_by='System')
|
||||||
|
history.add()
|
||||||
|
current_app.logger.warning('group info: {} '.format(account_id))
|
||||||
|
|
||||||
|
return authenticate_user(user, 'Azure OAuth')
|
||||||
|
|
||||||
if 'oidc_token' in session:
|
if 'oidc_token' in session:
|
||||||
me = json.loads(oidc.get('userinfo').text)
|
me = json.loads(oidc.get('userinfo').text)
|
||||||
oidc_username = me["preferred_username"]
|
oidc_username = me[Setting().get('oidc_oauth_username')]
|
||||||
oidc_givenname = me["name"]
|
oidc_givenname = me[Setting().get('oidc_oauth_firstname')]
|
||||||
oidc_familyname = ""
|
oidc_familyname = me[Setting().get('oidc_oauth_last_name')]
|
||||||
oidc_email = me["email"]
|
oidc_email = me[Setting().get('oidc_oauth_email')]
|
||||||
|
|
||||||
user = User.query.filter_by(username=oidc_username).first()
|
user = User.query.filter_by(username=oidc_username).first()
|
||||||
if not user:
|
if not user:
|
||||||
|
@ -297,17 +379,55 @@ def login():
|
||||||
firstname=oidc_givenname,
|
firstname=oidc_givenname,
|
||||||
lastname=oidc_familyname,
|
lastname=oidc_familyname,
|
||||||
email=oidc_email)
|
email=oidc_email)
|
||||||
|
|
||||||
result = user.create_local_user()
|
result = user.create_local_user()
|
||||||
if not result['status']:
|
else:
|
||||||
session.pop('oidc_token', None)
|
user.firstname = oidc_givenname
|
||||||
return redirect(url_for('index.login'))
|
user.lastname = oidc_familyname
|
||||||
|
user.email = oidc_email
|
||||||
|
user.plain_text_password = None
|
||||||
|
result = user.update_local_user()
|
||||||
|
|
||||||
|
if not result['status']:
|
||||||
|
session.pop('oidc_token', None)
|
||||||
|
return redirect(url_for('index.login'))
|
||||||
|
|
||||||
|
#This checks if the account_name_property and account_description property were included in settings.
|
||||||
|
if Setting().get('oidc_oauth_account_name_property') and Setting().get('oidc_oauth_account_description_property'):
|
||||||
|
|
||||||
|
#Gets the name_property and description_property.
|
||||||
|
name_prop = Setting().get('oidc_oauth_account_name_property')
|
||||||
|
desc_prop = Setting().get('oidc_oauth_account_description_property')
|
||||||
|
|
||||||
|
account_to_add = []
|
||||||
|
#If the name_property and desc_property exist in me (A variable that contains all the userinfo from the IdP).
|
||||||
|
if name_prop in me and desc_prop in me:
|
||||||
|
accounts_name_prop = [me[name_prop]] if type(me[name_prop]) is not list else me[name_prop]
|
||||||
|
accounts_desc_prop = [me[desc_prop]] if type(me[desc_prop]) is not list else me[desc_prop]
|
||||||
|
|
||||||
|
#Run on all groups the user is in by the index num.
|
||||||
|
for i in range(len(accounts_name_prop)):
|
||||||
|
description = ''
|
||||||
|
if i < len(accounts_desc_prop):
|
||||||
|
description = accounts_desc_prop[i]
|
||||||
|
account = handle_account(accounts_name_prop[i], description)
|
||||||
|
|
||||||
|
account_to_add.append(account)
|
||||||
|
user_accounts = user.get_accounts()
|
||||||
|
|
||||||
|
# Add accounts
|
||||||
|
for account in account_to_add:
|
||||||
|
if account not in user_accounts:
|
||||||
|
account.add_user(user)
|
||||||
|
|
||||||
|
# Remove accounts if the setting is enabled
|
||||||
|
if Setting().get('delete_sso_accounts'):
|
||||||
|
for account in user_accounts:
|
||||||
|
if account not in account_to_add:
|
||||||
|
account.remove_user(user)
|
||||||
|
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
login_user(user, remember=False)
|
return authenticate_user(user, 'OIDC OAuth')
|
||||||
signin_history(user.username, 'OIDC OAuth', True)
|
|
||||||
return redirect(url_for('index.index'))
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return render_template('login.html', saml_enabled=SAML_ENABLED)
|
return render_template('login.html', saml_enabled=SAML_ENABLED)
|
||||||
|
@ -367,9 +487,36 @@ def login():
|
||||||
saml_enabled=SAML_ENABLED,
|
saml_enabled=SAML_ENABLED,
|
||||||
error='Token required')
|
error='Token required')
|
||||||
|
|
||||||
login_user(user, remember=remember_me)
|
if Setting().get('autoprovisioning') and auth_method!='LOCAL':
|
||||||
signin_history(user.username, 'LOCAL', True)
|
urn_value=Setting().get('urn_value')
|
||||||
return redirect(session.get('next', url_for('index.index')))
|
Entitlements=user.read_entitlements(Setting().get('autoprovisioning_attribute'))
|
||||||
|
if len(Entitlements)==0 and Setting().get('purge'):
|
||||||
|
user.set_role("User")
|
||||||
|
user.revoke_privilege(True)
|
||||||
|
|
||||||
|
elif len(Entitlements)!=0:
|
||||||
|
if checkForPDAEntries(Entitlements, urn_value):
|
||||||
|
user.updateUser(Entitlements)
|
||||||
|
else:
|
||||||
|
current_app.logger.warning('Not a single powerdns-admin record was found, possibly a typo in the prefix')
|
||||||
|
if Setting().get('purge'):
|
||||||
|
user.set_role("User")
|
||||||
|
user.revoke_privilege(True)
|
||||||
|
current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' )
|
||||||
|
|
||||||
|
return authenticate_user(user, 'LOCAL', remember_me)
|
||||||
|
|
||||||
|
def checkForPDAEntries(Entitlements, urn_value):
|
||||||
|
"""
|
||||||
|
Run through every record located in the ldap attribute given and determine if there are any valid powerdns-admin records
|
||||||
|
"""
|
||||||
|
urnArguments=[x.lower() for x in urn_value.split(':')]
|
||||||
|
for Entitlement in Entitlements:
|
||||||
|
entArguments=Entitlement.split(':powerdns-admin')
|
||||||
|
entArguments=[x.lower() for x in entArguments[0].split(':')]
|
||||||
|
if (entArguments==urnArguments):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def clear_session():
|
def clear_session():
|
||||||
|
@ -377,6 +524,7 @@ def clear_session():
|
||||||
session.pop('github_token', None)
|
session.pop('github_token', None)
|
||||||
session.pop('google_token', None)
|
session.pop('google_token', None)
|
||||||
session.pop('authentication_type', None)
|
session.pop('authentication_type', None)
|
||||||
|
session.pop('remote_user', None)
|
||||||
session.clear()
|
session.clear()
|
||||||
logout_user()
|
logout_user()
|
||||||
|
|
||||||
|
@ -411,6 +559,38 @@ def signin_history(username, authenticator, success):
|
||||||
}),
|
}),
|
||||||
created_by='System').add()
|
created_by='System').add()
|
||||||
|
|
||||||
|
# Get a list of Azure security groups the user is a member of
|
||||||
|
def get_azure_groups(uri):
|
||||||
|
azure_info = azure.get(uri).text
|
||||||
|
current_app.logger.info('Azure groups returned: ' + azure_info)
|
||||||
|
grouplookup = json.loads(azure_info)
|
||||||
|
if "value" in grouplookup:
|
||||||
|
mygroups = grouplookup["value"]
|
||||||
|
# If "@odata.nextLink" exists in the results, we need to get more groups
|
||||||
|
if "@odata.nextLink" in grouplookup:
|
||||||
|
# The additional groups are added to the existing array
|
||||||
|
mygroups.extend(get_azure_groups(grouplookup["@odata.nextLink"]))
|
||||||
|
else:
|
||||||
|
mygroups = []
|
||||||
|
return mygroups
|
||||||
|
|
||||||
|
# Handle user login, write history and, if set, handle showing the register_otp QR code.
|
||||||
|
# if Setting for OTP on first login is enabled, and OTP field is also enabled,
|
||||||
|
# but user isn't using it yet, enable OTP, get QR code and display it, logging the user out.
|
||||||
|
def authenticate_user(user, authenticator, remember=False):
|
||||||
|
login_user(user, remember=remember)
|
||||||
|
signin_history(user.username, authenticator, True)
|
||||||
|
if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret:
|
||||||
|
user.update_profile(enable_otp=True)
|
||||||
|
user_id = current_user.id
|
||||||
|
prepare_welcome_user(user_id)
|
||||||
|
return redirect(url_for('index.welcome'))
|
||||||
|
return redirect(url_for('index.login'))
|
||||||
|
|
||||||
|
# Prepare user to enter /welcome screen, otherwise they won't have permission to do so
|
||||||
|
def prepare_welcome_user(user_id):
|
||||||
|
logout_user()
|
||||||
|
session['welcome_user_id'] = user_id
|
||||||
|
|
||||||
@index_bp.route('/logout')
|
@index_bp.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
|
@ -434,8 +614,38 @@ def logout():
|
||||||
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
|
||||||
session_index=session['samlSessionIndex'],
|
session_index=session['samlSessionIndex'],
|
||||||
name_id=session['samlNameId']))
|
name_id=session['samlNameId']))
|
||||||
|
|
||||||
|
redirect_uri = url_for('index.login')
|
||||||
|
oidc_logout = Setting().get('oidc_oauth_logout_url')
|
||||||
|
|
||||||
|
if 'oidc_token' in session and oidc_logout:
|
||||||
|
redirect_uri = "{}?redirect_uri={}".format(
|
||||||
|
oidc_logout, url_for('index.login', _external=True))
|
||||||
|
|
||||||
|
# Clean cookies and flask session
|
||||||
clear_session()
|
clear_session()
|
||||||
return redirect(url_for('index.login'))
|
|
||||||
|
# If remote user authentication is enabled and a logout URL is configured for it,
|
||||||
|
# redirect users to that instead
|
||||||
|
remote_user_logout_url = current_app.config.get('REMOTE_USER_LOGOUT_URL')
|
||||||
|
if current_app.config.get('REMOTE_USER_ENABLED') and remote_user_logout_url:
|
||||||
|
current_app.logger.debug(
|
||||||
|
'Redirecting remote user "{0}" to logout URL {1}'
|
||||||
|
.format(current_user.username, remote_user_logout_url))
|
||||||
|
# Warning: if REMOTE_USER environment variable is still set and not cleared by
|
||||||
|
# some external module, not defining a custom logout URL will trigger a loop
|
||||||
|
# that will just log the user back in right after logging out
|
||||||
|
res = make_response(redirect(remote_user_logout_url.strip()))
|
||||||
|
|
||||||
|
# Remove any custom cookies the remote authentication mechanism may use
|
||||||
|
# (e.g.: MOD_AUTH_CAS and MOD_AUTH_CAS_S)
|
||||||
|
remote_cookies = current_app.config.get('REMOTE_USER_COOKIES')
|
||||||
|
for r_cookie_name in utils.ensure_list(remote_cookies):
|
||||||
|
res.delete_cookie(r_cookie_name)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
return redirect(redirect_uri)
|
||||||
|
|
||||||
|
|
||||||
@index_bp.route('/register', methods=['GET', 'POST'])
|
@index_bp.route('/register', methods=['GET', 'POST'])
|
||||||
|
@ -444,12 +654,12 @@ def register():
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return render_template('register.html')
|
return render_template('register.html')
|
||||||
elif request.method == 'POST':
|
elif request.method == 'POST':
|
||||||
username = request.form['username']
|
username = request.form.get('username', '').strip()
|
||||||
password = request.form['password']
|
password = request.form.get('password', '')
|
||||||
firstname = request.form.get('firstname')
|
firstname = request.form.get('firstname', '').strip()
|
||||||
lastname = request.form.get('lastname')
|
lastname = request.form.get('lastname', '').strip()
|
||||||
email = request.form.get('email')
|
email = request.form.get('email', '').strip()
|
||||||
rpassword = request.form.get('rpassword')
|
rpassword = request.form.get('rpassword', '')
|
||||||
|
|
||||||
if not username or not password or not email:
|
if not username or not password or not email:
|
||||||
return render_template(
|
return render_template(
|
||||||
|
@ -471,7 +681,12 @@ def register():
|
||||||
if result and result['status']:
|
if result and result['status']:
|
||||||
if Setting().get('verify_user_email'):
|
if Setting().get('verify_user_email'):
|
||||||
send_account_verification(email)
|
send_account_verification(email)
|
||||||
return redirect(url_for('index.login'))
|
if Setting().get('otp_force') and Setting().get('otp_field_enabled'):
|
||||||
|
user.update_profile(enable_otp=True)
|
||||||
|
prepare_welcome_user(user.id)
|
||||||
|
return redirect(url_for('index.welcome'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('index.login'))
|
||||||
else:
|
else:
|
||||||
return render_template('register.html',
|
return render_template('register.html',
|
||||||
error=result['msg'])
|
error=result['msg'])
|
||||||
|
@ -481,6 +696,28 @@ def register():
|
||||||
return render_template('errors/404.html'), 404
|
return render_template('errors/404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
# Show welcome page on first login if otp_force is enabled
|
||||||
|
@index_bp.route('/welcome', methods=['GET', 'POST'])
|
||||||
|
def welcome():
|
||||||
|
if 'welcome_user_id' not in session:
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
|
user = User(id=session['welcome_user_id'])
|
||||||
|
encoded_img_data = base64.b64encode(user.get_qrcode_value())
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user)
|
||||||
|
elif request.method == 'POST':
|
||||||
|
otp_token = request.form.get('otptoken', '')
|
||||||
|
if otp_token and otp_token.isdigit():
|
||||||
|
good_token = user.verify_totp(otp_token)
|
||||||
|
if not good_token:
|
||||||
|
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Invalid token")
|
||||||
|
else:
|
||||||
|
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Token required")
|
||||||
|
session.pop('welcome_user_id')
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
@index_bp.route('/confirm/<token>', methods=['GET'])
|
@index_bp.route('/confirm/<token>', methods=['GET'])
|
||||||
def confirm_email(token):
|
def confirm_email(token):
|
||||||
email = confirm_token(token)
|
email = confirm_token(token)
|
||||||
|
@ -619,7 +856,8 @@ def dyndns_update():
|
||||||
msg=
|
msg=
|
||||||
"DynDNS update: attempted update of {0} but record already up-to-date"
|
"DynDNS update: attempted update of {0} but record already up-to-date"
|
||||||
.format(hostname),
|
.format(hostname),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=domain.id)
|
||||||
history.add()
|
history.add()
|
||||||
else:
|
else:
|
||||||
oldip = r.data
|
oldip = r.data
|
||||||
|
@ -634,7 +872,8 @@ def dyndns_update():
|
||||||
"old_value": oldip,
|
"old_value": oldip,
|
||||||
"new_value": str(ip)
|
"new_value": str(ip)
|
||||||
}),
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=domain.id)
|
||||||
history.add()
|
history.add()
|
||||||
response = 'good'
|
response = 'good'
|
||||||
else:
|
else:
|
||||||
|
@ -673,7 +912,8 @@ def dyndns_update():
|
||||||
"record": hostname,
|
"record": hostname,
|
||||||
"value": str(ip)
|
"value": str(ip)
|
||||||
}),
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username,
|
||||||
|
domain_id=domain.id)
|
||||||
history.add()
|
history.add()
|
||||||
response = 'good'
|
response = 'good'
|
||||||
else:
|
else:
|
||||||
|
@ -788,7 +1028,7 @@ def saml_authorized():
|
||||||
else:
|
else:
|
||||||
user_groups = []
|
user_groups = []
|
||||||
if admin_attribute_name or group_attribute_name:
|
if admin_attribute_name or group_attribute_name:
|
||||||
user_accounts = set(user.get_account())
|
user_accounts = set(user.get_accounts())
|
||||||
saml_accounts = []
|
saml_accounts = []
|
||||||
for group_mapping in group_to_account_mapping:
|
for group_mapping in group_to_account_mapping:
|
||||||
mapping = group_mapping.split('=')
|
mapping = group_mapping.split('=')
|
||||||
|
@ -831,9 +1071,7 @@ def saml_authorized():
|
||||||
user.plain_text_password = None
|
user.plain_text_password = None
|
||||||
user.update_profile()
|
user.update_profile()
|
||||||
session['authentication_type'] = 'SAML'
|
session['authentication_type'] = 'SAML'
|
||||||
login_user(user, remember=False)
|
return authenticate_user(user, 'SAML')
|
||||||
signin_history(user.username, 'SAML', True)
|
|
||||||
return redirect(url_for('index.login'))
|
|
||||||
else:
|
else:
|
||||||
return render_template('errors/SAML.html', errors=errors)
|
return render_template('errors/SAML.html', errors=errors)
|
||||||
|
|
||||||
|
@ -849,7 +1087,7 @@ def create_group_to_account_mapping():
|
||||||
return group_to_account_mapping
|
return group_to_account_mapping
|
||||||
|
|
||||||
|
|
||||||
def handle_account(account_name):
|
def handle_account(account_name, account_description=""):
|
||||||
clean_name = ''.join(c for c in account_name.lower()
|
clean_name = ''.join(c for c in account_name.lower()
|
||||||
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
|
||||||
if len(clean_name) > Account.name.type.length:
|
if len(clean_name) > Account.name.type.length:
|
||||||
|
@ -858,13 +1096,16 @@ def handle_account(account_name):
|
||||||
account = Account.query.filter_by(name=clean_name).first()
|
account = Account.query.filter_by(name=clean_name).first()
|
||||||
if not account:
|
if not account:
|
||||||
account = Account(name=clean_name.lower(),
|
account = Account(name=clean_name.lower(),
|
||||||
description='',
|
description=account_description,
|
||||||
contact='',
|
contact='',
|
||||||
mail='')
|
mail='')
|
||||||
account.create_account()
|
account.create_account()
|
||||||
history = History(msg='Account {0} created'.format(account.name),
|
history = History(msg='Account {0} created'.format(account.name),
|
||||||
created_by='SAML Assertion')
|
created_by='OIDC/SAML Assertion')
|
||||||
history.add()
|
history.add()
|
||||||
|
else:
|
||||||
|
account.description = account_description
|
||||||
|
account.update_account()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import datetime
|
import datetime
|
||||||
import qrcode as qrc
|
|
||||||
import qrcode.image.svg as qrc_svg
|
|
||||||
from io import BytesIO
|
|
||||||
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app
|
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app
|
||||||
from flask_login import current_user, login_required, login_manager
|
from flask_login import current_user, login_required, login_manager
|
||||||
|
|
||||||
|
@ -41,16 +38,13 @@ def profile():
|
||||||
return render_template('user_profile.html')
|
return render_template('user_profile.html')
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
if session['authentication_type'] == 'LOCAL':
|
if session['authentication_type'] == 'LOCAL':
|
||||||
firstname = request.form[
|
firstname = request.form.get('firstname', '').strip()
|
||||||
'firstname'] if 'firstname' in request.form else ''
|
lastname = request.form.get('lastname', '').strip()
|
||||||
lastname = request.form[
|
email = request.form.get('email', '').strip()
|
||||||
'lastname'] if 'lastname' in request.form else ''
|
new_password = request.form.get('password', '')
|
||||||
email = request.form['email'] if 'email' in request.form else ''
|
|
||||||
new_password = request.form[
|
|
||||||
'password'] if 'password' in request.form else ''
|
|
||||||
else:
|
else:
|
||||||
firstname = lastname = email = new_password = ''
|
firstname = lastname = email = new_password = ''
|
||||||
logging.warning(
|
current_app.logger.warning(
|
||||||
'Authenticated externally. User {0} information will not allowed to update the profile'
|
'Authenticated externally. User {0} information will not allowed to update the profile'
|
||||||
.format(current_user.username))
|
.format(current_user.username))
|
||||||
|
|
||||||
|
@ -97,11 +91,7 @@ def qrcode():
|
||||||
if not current_user:
|
if not current_user:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
img = qrc.make(current_user.get_totp_uri(),
|
return current_user.get_qrcode_value(), 200, {
|
||||||
image_factory=qrc_svg.SvgPathImage)
|
|
||||||
stream = BytesIO()
|
|
||||||
img.save(stream)
|
|
||||||
return stream.getvalue(), 200, {
|
|
||||||
'Content-Type': 'image/svg+xml',
|
'Content-Type': 'image/svg+xml',
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
'Pragma': 'no-cache',
|
'Pragma': 'no-cache',
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
from authlib.flask.client import OAuth
|
from authlib.integrations.flask_client import OAuth\
|
||||||
|
|
||||||
authlib_oauth_client = OAuth()
|
authlib_oauth_client = OAuth()
|
|
@ -104,10 +104,10 @@ class SAML(object):
|
||||||
settings['sp']['entityId'] = current_app.config['SAML_SP_ENTITY_ID']
|
settings['sp']['entityId'] = current_app.config['SAML_SP_ENTITY_ID']
|
||||||
|
|
||||||
|
|
||||||
if ('SAML_CERT_FILE' in current_app.config) and ('SAML_KEY_FILE' in current_app.config):
|
if ('SAML_CERT' in current_app.config) and ('SAML_KEY' in current_app.config):
|
||||||
|
|
||||||
saml_cert_file = current_app.config['SAML_CERT_FILE']
|
saml_cert_file = current_app.config['SAML_CERT']
|
||||||
saml_key_file = current_app.config['SAML_KEY_FILE']
|
saml_key_file = current_app.config['SAML_KEY']
|
||||||
|
|
||||||
if os.path.isfile(saml_cert_file):
|
if os.path.isfile(saml_cert_file):
|
||||||
cert = open(saml_cert_file, "r").readlines()
|
cert = open(saml_cert_file, "r").readlines()
|
||||||
|
|
102
powerdnsadmin/static/assets/css/style.css
Normal file
102
powerdnsadmin/static/assets/css/style.css
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
/* Customize the label (the container) */
|
||||||
|
.container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
padding-left: 50px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
left:100px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 22px;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide the browser's default checkbox */
|
||||||
|
.container input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 0;
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a custom checkbox */
|
||||||
|
.checkmark {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
height: 25px;
|
||||||
|
width: 25px;
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* On mouse-over, add a grey background color */
|
||||||
|
.container:hover input ~ .checkmark {
|
||||||
|
background-color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* When the checkbox is checked, add a blue background */
|
||||||
|
.container input:checked ~ .checkmark {
|
||||||
|
background-color: #2196F3;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create the checkmark/indicator (hidden when not checked) */
|
||||||
|
.checkmark:after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Show the checkmark when checked */
|
||||||
|
.container input:checked ~ .checkmark:after {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the checkmark/indicator */
|
||||||
|
.container .checkmark:after {
|
||||||
|
left: 9px;
|
||||||
|
top: 5px;
|
||||||
|
width: 5px;
|
||||||
|
height: 10px;
|
||||||
|
border: solid white;
|
||||||
|
border-width: 0 3px 3px 0;
|
||||||
|
-webkit-transform: rotate(45deg);
|
||||||
|
-ms-transform: rotate(45deg);
|
||||||
|
transform: rotate(45deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
.autocomplete {
|
||||||
|
/*the container must be positioned relative:*/
|
||||||
|
position: relative;
|
||||||
|
/* display: inline-block; */
|
||||||
|
}
|
||||||
|
.autocomplete-items {
|
||||||
|
position: absolute;
|
||||||
|
border: 1px solid #d4d4d4;
|
||||||
|
border-bottom: none;
|
||||||
|
border-top: none;
|
||||||
|
z-index: 99;
|
||||||
|
/*position the autocomplete items to be the same width as the container:*/
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
.autocomplete-items div {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: #fff;
|
||||||
|
border-bottom: 1px solid #d4d4d4;
|
||||||
|
}
|
||||||
|
.autocomplete-items div:hover {
|
||||||
|
/*when hovering an item:*/
|
||||||
|
background-color: #e9e9e9;
|
||||||
|
}
|
||||||
|
.autocomplete-active {
|
||||||
|
/*when navigating through the items using the arrow keys:*/
|
||||||
|
background-color: DodgerBlue !important;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
|
@ -53,6 +53,7 @@ function applyRecordChanges(data, domain) {
|
||||||
var modal = $("#modal_success");
|
var modal = $("#modal_success");
|
||||||
modal.find('.modal-body p').text("Applied changes successfully");
|
modal.find('.modal-body p').text("Applied changes successfully");
|
||||||
modal.modal('show');
|
modal.modal('show');
|
||||||
|
setTimeout(() => {window.location.reload()}, 2000);
|
||||||
},
|
},
|
||||||
|
|
||||||
error : function(jqXHR, status) {
|
error : function(jqXHR, status) {
|
||||||
|
@ -285,3 +286,13 @@ function timer(elToUpdate, maxTime) {
|
||||||
|
|
||||||
return interval;
|
return interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy otp secret code to clipboard
|
||||||
|
function copy_otp_secret_to_clipboard() {
|
||||||
|
var copyBox = document.getElementById("otp_secret");
|
||||||
|
copyBox.select();
|
||||||
|
copyBox.setSelectionRange(0, 99999); /* For mobile devices */
|
||||||
|
navigator.clipboard.writeText(copyBox.value);
|
||||||
|
$("#copy_tooltip").css("visibility", "visible");
|
||||||
|
setTimeout(function(){ $("#copy_tooltip").css("visibility", "collapse"); }, 2000);
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -84,7 +84,7 @@
|
||||||
<select multiple="multiple" class="form-control" id="account_multi_user"
|
<select multiple="multiple" class="form-control" id="account_multi_user"
|
||||||
name="account_multi_user">
|
name="account_multi_user">
|
||||||
{% for user in users %}
|
{% for user in users %}
|
||||||
<option {% if user.id in account_user_ids %}selected{% endif %}
|
<option {% if user.id in account_user_ids|default([]) %}selected{% endif %}
|
||||||
value="{{ user.username }}">{{ user.username }}</option>
|
value="{{ user.username }}">{{ user.username }}</option>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
|
|
283
powerdnsadmin/templates/admin_edit_key.html
Normal file
283
powerdnsadmin/templates/admin_edit_key.html
Normal file
|
@ -0,0 +1,283 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% set active_page = "admin_keys" %}
|
||||||
|
{% if create or (key is not none and key.role.name != "User") %}{% set hide_opts = True %}{%else %}{% set hide_opts = False %}{% endif %}
|
||||||
|
{% block title %}
|
||||||
|
<title>Edit Key - {{ SITE_NAME }}</title>
|
||||||
|
{% endblock %}
|
||||||
|
{% block dashboard_stat %}
|
||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
Key
|
||||||
|
<small>{% if create %}New key{% else %}{{ key.id }}{% endif %}</small>
|
||||||
|
</h1>
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
|
||||||
|
<li><a href="{{ url_for('admin.manage_keys') }}">Key</a></li>
|
||||||
|
<li class="active">{% if create %}Add{% else %}Edit{% endif %} key</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="box box-primary">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">{% if create %}Add{% else %}Edit{% endif %} key</h3>
|
||||||
|
</div>
|
||||||
|
<!-- /.box-header -->
|
||||||
|
<!-- form start -->
|
||||||
|
<form role="form" method="post"
|
||||||
|
action="{% if create %}{{ url_for('admin.edit_key') }}{% else %}{{ url_for('admin.edit_key', key_id=key.id) }}{% endif %}">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="create" value="{{ create }}">
|
||||||
|
<div class="box-body">
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<label class="control-label" for="role">Role</label>
|
||||||
|
<select class="key_role form-control" id="key_role" name="key_role">
|
||||||
|
{% for role in roles %}
|
||||||
|
<option value="{{ role.name }}"
|
||||||
|
{% if (key is not none) and (role.id==key.role.id) %}selected{% endif %}>{{ role.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group has-feedback">
|
||||||
|
<label class="control-label" for="description">Description</label>
|
||||||
|
<input type="text" class="form-control" placeholder="Description" name="description"
|
||||||
|
{% if key is not none %} value="{{ key.description }}" {% endif %}> <span
|
||||||
|
class="glyphicon glyphicon-pencil form-control-feedback"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box-header with-border key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
|
<h3 class="box-title">Accounts Access Control</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
|
<p>This key will be linked to the accounts on the right,</p>
|
||||||
|
<p>thus granting access to domains owned by the selected accounts.</p>
|
||||||
|
<p>Click on accounts to move between the columns.</p>
|
||||||
|
<div class="form-group col-xs-2">
|
||||||
|
<select multiple="multiple" class="form-control" id="key_multi_account"
|
||||||
|
name="key_multi_account">
|
||||||
|
{% for account in accounts %}
|
||||||
|
<option {% if key and account in key.accounts %}selected{% endif %} value="{{ account.name }}">{{ account.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box-header with-border key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
|
<h3 class="box-title">Domain Access Control</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
|
<p>This key will have acess to the domains on the right.</p>
|
||||||
|
<p>Click on domains to move between the columns.</p>
|
||||||
|
<div class="form-group col-xs-2">
|
||||||
|
<select multiple="multiple" class="form-control" id="key_multi_domain"
|
||||||
|
name="key_multi_domain">
|
||||||
|
{% for domain in domains %}
|
||||||
|
<option {% if key and domain in key.domains %}selected{% endif %} value="{{ domain.name }}">{{ domain.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box-footer">
|
||||||
|
<button type="submit"
|
||||||
|
class="btn btn-flat btn-primary" id="key_submit">{% if create %}Create{% else %}Update{% endif %}
|
||||||
|
Key</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="box box-primary">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">Help with {% if create %}creating a new{% else%}updating a{% endif %} key
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<p>Fill in all the fields in the form to the left.</p>
|
||||||
|
<p><strong>Role</strong> The role of the key.</p>
|
||||||
|
<p><strong>Description</strong> The key description.</p>
|
||||||
|
<p><strong>Access Control</strong> The domains or accounts which the key has access to.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
{% block extrascripts %}
|
||||||
|
<script>
|
||||||
|
$('form').submit(function (e) {
|
||||||
|
var selectedRole = $("#key_role").val();
|
||||||
|
var selectedDomains = $("#key_multi_domain option:selected").length;
|
||||||
|
var selectedAccounts = $("#key_multi_account option:selected").length;
|
||||||
|
var warn_modal = $("#modal_warning");
|
||||||
|
|
||||||
|
if (selectedRole != "User" && selectedDomains > 0 && selectedAccounts > 0){
|
||||||
|
var warning = "Administrator and Operators have access to all domains. Your domain an account selection won't be saved.";
|
||||||
|
e.preventDefault(e);
|
||||||
|
warn_modal.modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedRole == "User" && selectedDomains == 0 && selectedAccounts == 0){
|
||||||
|
var warning = "User role must have at least one account or one domain bound. None selected.";
|
||||||
|
e.preventDefault(e);
|
||||||
|
warn_modal.modal('show');
|
||||||
|
}
|
||||||
|
|
||||||
|
warn_modal.find('.modal-body p').text(warning);
|
||||||
|
warn_modal.find('#button_key_confirm_warn').click(clearModal);
|
||||||
|
});
|
||||||
|
function clearModal(){
|
||||||
|
$("#modal_warning").modal('hide');
|
||||||
|
}
|
||||||
|
$('#key_role').on('change', function (e) {
|
||||||
|
var optionSelected = $("option:selected", this);
|
||||||
|
if (this.value != "User") {
|
||||||
|
// Clear the visible list
|
||||||
|
$('#ms-key_multi_domain .ms-selection li').each(function(){ $(this).css('display', 'none');})
|
||||||
|
$('#ms-key_multi_domain .ms-selectable li').each(function(){ $(this).css('display', '');})
|
||||||
|
$('#ms-key_multi_account .ms-selection li').each(function(){ $(this).css('display', 'none');})
|
||||||
|
$('#ms-key_multi_account .ms-selectable li').each(function(){ $(this).css('display', '');})
|
||||||
|
// Deselect invisible selectbox
|
||||||
|
$('#key_multi_domain option:selected').each(function(){ $(this).prop('selected', false);})
|
||||||
|
$('#key_multi_account option:selected').each(function(){ $(this).prop('selected', false);})
|
||||||
|
// Hide the lists
|
||||||
|
$(".key-opts").hide();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$(".key-opts").show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("#key_multi_domain").multiSelect({
|
||||||
|
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
||||||
|
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
||||||
|
afterInit: function (ms) {
|
||||||
|
var that = this,
|
||||||
|
$selectableSearch = that.$selectableUl.prev(),
|
||||||
|
$selectionSearch = that.$selectionUl.prev(),
|
||||||
|
selectableSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selectable:not(.ms-selected)',
|
||||||
|
selectionSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selection.ms-selected';
|
||||||
|
|
||||||
|
that.qs1 = $selectableSearch.quicksearch(selectableSearchString)
|
||||||
|
.on('keydown', function (e) {
|
||||||
|
if (e.which === 40) {
|
||||||
|
that.$selectableUl.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
that.qs2 = $selectionSearch.quicksearch(selectionSearchString)
|
||||||
|
.on('keydown', function (e) {
|
||||||
|
if (e.which == 40) {
|
||||||
|
that.$selectionUl.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
afterSelect: function () {
|
||||||
|
this.qs1.cache();
|
||||||
|
this.qs2.cache();
|
||||||
|
},
|
||||||
|
afterDeselect: function () {
|
||||||
|
this.qs1.cache();
|
||||||
|
this.qs2.cache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("#key_multi_account").multiSelect({
|
||||||
|
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Account Name'>",
|
||||||
|
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Account Name'>",
|
||||||
|
afterInit: function (ms) {
|
||||||
|
var that = this,
|
||||||
|
$selectableSearch = that.$selectableUl.prev(),
|
||||||
|
$selectionSearch = that.$selectionUl.prev(),
|
||||||
|
selectableSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selectable:not(.ms-selected)',
|
||||||
|
selectionSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selection.ms-selected';
|
||||||
|
|
||||||
|
that.qs1 = $selectableSearch.quicksearch(selectableSearchString)
|
||||||
|
.on('keydown', function (e) {
|
||||||
|
if (e.which === 40) {
|
||||||
|
that.$selectableUl.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
that.qs2 = $selectionSearch.quicksearch(selectionSearchString)
|
||||||
|
.on('keydown', function (e) {
|
||||||
|
if (e.which == 40) {
|
||||||
|
that.$selectionUl.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
afterSelect: function () {
|
||||||
|
this.qs1.cache();
|
||||||
|
this.qs2.cache();
|
||||||
|
},
|
||||||
|
afterDeselect: function () {
|
||||||
|
this.qs1.cache();
|
||||||
|
this.qs2.cache();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
{% if plain_key %}
|
||||||
|
$(document.body).ready(function () {
|
||||||
|
var modal = $("#modal_show_key");
|
||||||
|
var info = "{{ plain_key }}";
|
||||||
|
modal.find('.modal-body p').text(info);
|
||||||
|
modal.find('#button_key_confirm').click(redirect_modal);
|
||||||
|
modal.find('#button_close_modal').click(redirect_modal);
|
||||||
|
modal.modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
function redirect_modal() {
|
||||||
|
window.location.href = '{{ url_for('admin.manage_keys') }}';
|
||||||
|
modal.modal('hide');
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block modals %}
|
||||||
|
<div class="modal fade" id="modal_show_key">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content modal-sm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="button_close_modal">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title">Your API key</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-flat btn-primary center-block" id="button_key_confirm">
|
||||||
|
Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
<div class="modal fade" id="modal_warning">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content modal-sm">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="button_close_warn_modal">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title">WARNING</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-flat btn-primary center-block" id="button_key_confirm_warn">
|
||||||
|
OK</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -13,7 +13,12 @@
|
||||||
<li class="active">History</li>
|
<li class="active">History</li>
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %} {% block content %}
|
{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<section class="content">
|
<section class="content">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
|
@ -28,32 +33,134 @@
|
||||||
Clear History <i class="fa fa-trash"></i>
|
Clear History <i class="fa fa-trash"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-body">
|
|
||||||
<table id="tbl_history" class="table table-bordered table-striped">
|
<div class="box-body clearfix">
|
||||||
<thead>
|
<form id="history-search-form" autocomplete="off">
|
||||||
<tr>
|
<!-- Custom Tabs -->
|
||||||
<th>Changed by</th>
|
<div class="nav-tabs-custom" id="tabs">
|
||||||
<th>Content</th>
|
<ul class="nav nav-tabs" id="nav_nav_tabs" name="nav_nav_tabs">
|
||||||
<th>Time</th>
|
<li id="activity_tab" class="active"><a href="#tabs-act" data-toggle="tab">Search for All Activity</a></li>
|
||||||
<th>Detail</th>
|
<li id="domain_tab"><a href="#tabs-domain" data-toggle="tab">Search By Domain</a></li>
|
||||||
</tr>
|
<li id="account_tab"><a href="#tabs-account" data-toggle="tab">Search By Account</a></li>
|
||||||
</thead>
|
{% if current_user.role.name != 'User' %}
|
||||||
<tbody>
|
<li id="user_auth_tab"><a href="#tabs-auth" data-toggle="tab">Search for User Authentication</a></li>
|
||||||
{% for history in histories %}
|
{% endif %}
|
||||||
<tr class="odd gradeX">
|
</ul>
|
||||||
<td>{{ history.created_by }}</td>
|
<div class="tab-content">
|
||||||
<td>{{ history.msg }}</td>
|
<div class="tab-pane" id="tabs-act">
|
||||||
<td>{{ history.created_on }}</td>
|
</div>
|
||||||
<td width="6%">
|
<div class="tab-pane" id="tabs-domain">
|
||||||
<button type="button" class="btn btn-flat btn-primary history-info-button"
|
<td><label>Domain Name</label></td>
|
||||||
value='{{ history.detail }}'>Info <i class="fa fa-info"></i>
|
<td>
|
||||||
</button>
|
<div class="autocomplete" style="width:250px;">
|
||||||
</td>
|
<input type="text" class="form-control" id="domain_name_filter" name="domain_name_filter" placeholder="Enter * to search for any domain" value="">
|
||||||
</tr>
|
</div>
|
||||||
{% endfor %}
|
</td>
|
||||||
</tbody>
|
<td>
|
||||||
</table>
|
<div style="position: relative; top:10px;">
|
||||||
|
<td>Record Changelog only  </td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" id="domain_changelog_only_checkbox" name="domain_changelog_only_checkbox"
|
||||||
|
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||||
|
</td>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="tabs-account">
|
||||||
|
<td><label>Account Name</label></td>
|
||||||
|
<td>
|
||||||
|
<div class="autocomplete" style="width:250px;">
|
||||||
|
<input type="text" class="form-control" id="account_name_filter" name="account_name_filter" placeholder="Enter * to search for any account" value="">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</div>
|
||||||
|
<div class="tab-pane" id="tabs-auth">
|
||||||
|
<td><label>Username</label></td>
|
||||||
|
<td>
|
||||||
|
<div class="autocomplete" style="width:250px;">
|
||||||
|
<input type="text" class="form-control" id="auth_name_filter" name="auth_name_filter" placeholder="Enter * to search for any username" value="">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<div style="position: relative; top:10px;">
|
||||||
|
<td>Authenticator Types:  </td>
|
||||||
|
<td>  All</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" checked id="auth_all_checkbox" name="auth_all_checkbox"
|
||||||
|
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||||
|
</td>
|
||||||
|
<td>  LOCAL</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" checked id="auth_local_only_checkbox" name="auth_local_only_checkbox"
|
||||||
|
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||||
|
</td>
|
||||||
|
<td>  OAuth</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" checked id="auth_oauth_only_checkbox" name="auth_oauth_only_checkbox"
|
||||||
|
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||||
|
</td>
|
||||||
|
<td>  SAML</td>
|
||||||
|
<td>
|
||||||
|
<input type="checkbox" checked id="auth_saml_only_checkbox" name="auth_saml_only_checkbox"
|
||||||
|
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||||
|
</td>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- End Custom Tabs -->
|
||||||
|
|
||||||
|
<div class="box-body">
|
||||||
|
<table id="Filters-Table">
|
||||||
|
<thead>
|
||||||
|
<th>Filters</th>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td><label>Changed by:  </label></td>
|
||||||
|
<td>
|
||||||
|
<div class="autocomplete" style="width:250px;">
|
||||||
|
<input type="text" style=" border:1px solid #d2d6de; width:250px; height: 34px;" id="user_name_filter" name="user_name_filter" value="">
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="position: relative; top:10px;">
|
||||||
|
<label>Minimum date:  </label>
|
||||||
|
</td>
|
||||||
|
<td style="position: relative; top:10px;">
|
||||||
|
<input type="text" id="min" name="min" class="datepicker" autocomplete="off" style=" border:1px solid #d2d6de; width:250px; height: 34px;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td style="position: relative; top:20px;">
|
||||||
|
<label>Maximum date:  </label>
|
||||||
|
</td>
|
||||||
|
<td style="position: relative; top:20px;">
|
||||||
|
<input type="text" id="max" name="max" class="datepicker" autocomplete="off" style=" border:1px solid #d2d6de; width:250px; height: 34px;">
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr><td> </td></tr>
|
||||||
|
<tr><td> </td></tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<button type="submit" id="search-submit" name="search-submit" class="btn btn-flat btn-primary button-filter">Search <i class="fa fa-search"></i></button>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<!-- -->
|
||||||
|
<button id="clear-filters" name="clear-filters" class="btn btn-flat btn-warning button-clearf">Clear Filters <i class="fa fa-trash"></i></button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="table_from_ajax"></div>
|
||||||
|
|
||||||
|
|
||||||
<!-- /.box-body -->
|
<!-- /.box-body -->
|
||||||
</div>
|
</div>
|
||||||
<!-- /.box -->
|
<!-- /.box -->
|
||||||
|
@ -65,31 +172,304 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extrascripts %}
|
{% block extrascripts %}
|
||||||
<script>
|
<script>
|
||||||
// set up history data table
|
|
||||||
$("#tbl_history").DataTable({
|
/* Don't let user search with a blank main field */
|
||||||
"paging": true,
|
var canSearch=true;
|
||||||
"lengthChange": false,
|
|
||||||
"searching": true,
|
$(document).ready(function () {
|
||||||
"ordering": true,
|
$.ajax({
|
||||||
"info": true,
|
url: "/admin/history_table",
|
||||||
"autoWidth": false,
|
type: "get",
|
||||||
"order": [
|
success: function(response) {
|
||||||
[2, "desc"]
|
console.log('Submission was successful.');
|
||||||
],
|
$("#table_from_ajax").html(response);
|
||||||
"columnDefs": [{
|
|
||||||
"type": "time",
|
|
||||||
"render": function (data, type, row) {
|
|
||||||
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
|
|
||||||
},
|
},
|
||||||
"targets": 2
|
error: function(xhr) {
|
||||||
}]
|
console.log("Sending data: ", data, " failed")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var minDate = $('#min');
|
||||||
|
var maxDate = $('#max');
|
||||||
|
domain_changelog = $('domain_changelog_only_checkbox');
|
||||||
|
|
||||||
|
// Show/hide filters
|
||||||
|
$('#domain_name_filter, #account_name_filter, #auth_name_filter').on('keyup change', function (e) {
|
||||||
|
if ( $('#domain_name_filter').val() == "" && $('#account_name_filter').val() == "" && $('#auth_name_filter').val() == "")
|
||||||
|
canSearch=false;
|
||||||
|
else
|
||||||
|
canSearch=true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle giving later mindate than current max
|
||||||
|
$('#min').on('change', function () {
|
||||||
|
if (minDate.val() > maxDate.val())
|
||||||
|
$('#max').datepicker('setDate', minDate.val() );
|
||||||
|
});
|
||||||
|
|
||||||
|
// Handle giving earlier maxdate than current min
|
||||||
|
$('#max').on('keyup change', function () {
|
||||||
|
if (maxDate.val() < minDate.val())
|
||||||
|
$('#min').datepicker('setDate', maxDate.val() );
|
||||||
|
});
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
$( ".datepicker" ).datepicker({
|
||||||
|
changeMonth: true,
|
||||||
|
changeYear: true,
|
||||||
|
format: "yyyy-mm-dd",
|
||||||
|
endDate: '+0'
|
||||||
|
});
|
||||||
|
// $(".datepicker").datepicker("option", "format", "yy-mm-dd")
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
$(document.body).on('click', '.history-info-button', function () {
|
|
||||||
var modal = $("#modal_history_info");
|
$('.checkbox,.radio').iCheck({
|
||||||
var info = $(this).val();
|
checkboxClass: 'icheckbox_square-blue',
|
||||||
$('#modal-code-content').html(json_library.prettyPrint(info));
|
radioClass: 'iradio_square-blue',
|
||||||
modal.modal('show');
|
increaseArea: '20%'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//Handle "ALL" Checkbox
|
||||||
|
$('#auth_all_checkbox').on('ifChecked',function() {
|
||||||
|
$('#auth_local_only_checkbox').iCheck('check');
|
||||||
|
$('#auth_oauth_only_checkbox').iCheck('check');
|
||||||
|
$('#auth_saml_only_checkbox').iCheck('check');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#auth_all_checkbox').on('ifUnchecked',function() {
|
||||||
|
//check if all were checked
|
||||||
|
if($('#auth_local_only_checkbox').is(':checked') && $('#auth_oauth_only_checkbox').is(':checked') && $('#auth_saml_only_checkbox').is(':checked'))
|
||||||
|
{
|
||||||
|
$('#auth_local_only_checkbox').iCheck('uncheck');
|
||||||
|
$('#auth_oauth_only_checkbox').iCheck('uncheck');
|
||||||
|
$('#auth_saml_only_checkbox').iCheck('uncheck');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//Handle other auth checkboxes
|
||||||
|
$('#auth_local_only_checkbox').on('ifChecked',function() {
|
||||||
|
//check if all others were checked
|
||||||
|
if($('#auth_oauth_only_checkbox').is(':checked') && $('#auth_saml_only_checkbox').is(':checked'))
|
||||||
|
$('#auth_all_checkbox').iCheck('check');
|
||||||
|
});
|
||||||
|
$('#auth_local_only_checkbox').on('ifUnchecked',function() {
|
||||||
|
$('#auth_all_checkbox').iCheck('uncheck');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#auth_oauth_only_checkbox').on('ifChecked',function() {
|
||||||
|
if($('#auth_local_only_checkbox').is(':checked') && $('#auth_saml_only_checkbox').is(':checked'))
|
||||||
|
$('#auth_all_checkbox').iCheck('check');
|
||||||
|
});
|
||||||
|
$('#auth_oauth_only_checkbox').on('ifUnchecked',function() {
|
||||||
|
$('#auth_all_checkbox').iCheck('uncheck');
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#auth_saml_only_checkbox').on('ifChecked',function() {
|
||||||
|
if($('#auth_local_only_checkbox').is(':checked') && $('#auth_oauth_only_checkbox').is(':checked'))
|
||||||
|
$('#auth_all_checkbox').iCheck('check');
|
||||||
|
});
|
||||||
|
$('#auth_saml_only_checkbox').on('ifUnchecked',function() {
|
||||||
|
$('#auth_all_checkbox').iCheck('uncheck');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document.body).on("click", ".button-clearf", function (e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
$('#user_name_filter').val('');
|
||||||
|
$('#min').val('');
|
||||||
|
$('#max').val('');
|
||||||
|
$('#domain_name_filter').val('');
|
||||||
|
$('#account_name_filter').val('');
|
||||||
|
$('#auth_name_filter').val('');
|
||||||
|
$('#auth_all_checkbox').iCheck('check');
|
||||||
|
$('#domain_changelog_only_checkbox').iCheck('uncheck');
|
||||||
|
});
|
||||||
|
|
||||||
|
var all_doms = "{{all_domain_names}}".split(" ");
|
||||||
|
var all_accounts = "{{all_account_names}}".split(" ");
|
||||||
|
var all_usernames = "{{all_usernames}}".split(" ");
|
||||||
|
all_doms.pop(); // remove last element which is " "
|
||||||
|
all_accounts.pop();
|
||||||
|
all_usernames.pop();
|
||||||
|
|
||||||
|
function autocomplete(inp, arr) {
|
||||||
|
/*the autocomplete function takes two arguments,
|
||||||
|
the text field element and an array of possible autocompleted values:*/
|
||||||
|
var currentFocus;
|
||||||
|
/*execute a function when someone writes in the text field:*/
|
||||||
|
inp.addEventListener("input", function(e) {
|
||||||
|
var a, b, i, val = this.value;
|
||||||
|
/*close any already open lists of autocompleted values*/
|
||||||
|
closeAllLists();
|
||||||
|
if (!val) { return false;}
|
||||||
|
currentFocus = -1;
|
||||||
|
/*create a DIV element that will contain the items (values):*/
|
||||||
|
a = document.createElement("DIV");
|
||||||
|
a.setAttribute("id", this.id + "autocomplete-list");
|
||||||
|
a.setAttribute("class", "autocomplete-items");
|
||||||
|
/*append the DIV element as a child of the autocomplete container:*/
|
||||||
|
this.parentNode.appendChild(a);
|
||||||
|
/*for each item in the array...*/
|
||||||
|
for (i = 0; i < arr.length; i++) {
|
||||||
|
/*check if the item starts with the same letters as the text field value:*/
|
||||||
|
if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
|
||||||
|
/*create a DIV element for each matching element:*/
|
||||||
|
b = document.createElement("DIV");
|
||||||
|
/*make the matching letters bold:*/
|
||||||
|
b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>";
|
||||||
|
b.innerHTML += arr[i].substr(val.length);
|
||||||
|
/*insert a input field that will hold the current array item's value:*/
|
||||||
|
b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";
|
||||||
|
/*execute a function when someone clicks on the item value (DIV element):*/
|
||||||
|
b.addEventListener("click", function(e) {
|
||||||
|
/*insert the value for the autocomplete text field:*/
|
||||||
|
inp.value = this.getElementsByTagName("input")[0].value;
|
||||||
|
/*close the list of autocompleted values,
|
||||||
|
(or any other open lists of autocompleted values:*/
|
||||||
|
closeAllLists();
|
||||||
|
});
|
||||||
|
a.appendChild(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
/*execute a function presses a key on the keyboard:*/
|
||||||
|
inp.addEventListener("keydown", function(e) {
|
||||||
|
var x = document.getElementById(this.id + "autocomplete-list");
|
||||||
|
if (x) x = x.getElementsByTagName("div");
|
||||||
|
if (e.keyCode == 40) {
|
||||||
|
/*If the arrow DOWN key is pressed,
|
||||||
|
increase the currentFocus variable:*/
|
||||||
|
currentFocus++;
|
||||||
|
/*and and make the current item more visible:*/
|
||||||
|
addActive(x);
|
||||||
|
} else if (e.keyCode == 38) { //up
|
||||||
|
/*If the arrow UP key is pressed,
|
||||||
|
decrease the currentFocus variable:*/
|
||||||
|
currentFocus--;
|
||||||
|
/*and and make the current item more visible:*/
|
||||||
|
addActive(x);
|
||||||
|
} else if (e.keyCode == 13) {
|
||||||
|
/*If the ENTER key is pressed, prevent the form from being submitted,*/
|
||||||
|
e.preventDefault();
|
||||||
|
if (currentFocus > -1) {
|
||||||
|
/*and simulate a click on the "active" item:*/
|
||||||
|
if (x) x[currentFocus].click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
function addActive(x) {
|
||||||
|
/*a function to classify an item as "active":*/
|
||||||
|
if (!x) return false;
|
||||||
|
/*start by removing the "active" class on all items:*/
|
||||||
|
removeActive(x);
|
||||||
|
if (currentFocus >= x.length) currentFocus = 0;
|
||||||
|
if (currentFocus < 0) currentFocus = (x.length - 1);
|
||||||
|
/*add class "autocomplete-active":*/
|
||||||
|
x[currentFocus].classList.add("autocomplete-active");
|
||||||
|
}
|
||||||
|
function removeActive(x) {
|
||||||
|
/*a function to remove the "active" class from all autocomplete items:*/
|
||||||
|
for (var i = 0; i < x.length; i++) {
|
||||||
|
x[i].classList.remove("autocomplete-active");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function closeAllLists(elmnt) {
|
||||||
|
/*close all autocomplete lists in the document,
|
||||||
|
except the one passed as an argument:*/
|
||||||
|
var x = document.getElementsByClassName("autocomplete-items");
|
||||||
|
for (var i = 0; i < x.length; i++) {
|
||||||
|
if (elmnt != x[i] && elmnt != inp) {
|
||||||
|
x[i].parentNode.removeChild(x[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*execute a function when someone clicks in the document:*/
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
closeAllLists(e.target);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/*initiate the autocomplete function on the "myInput" element, and pass along the countries array as possible autocomplete values:*/
|
||||||
|
autocomplete(document.getElementById("domain_name_filter"), all_doms);
|
||||||
|
autocomplete(document.getElementById("account_name_filter"), all_accounts);
|
||||||
|
autocomplete(document.getElementById("auth_name_filter"), all_usernames);
|
||||||
|
autocomplete(document.getElementById("user_name_filter"), all_usernames);
|
||||||
|
|
||||||
|
|
||||||
|
// prevent multiple filter field at the same time
|
||||||
|
$('#domain_tab').click(function() {
|
||||||
|
$('#account_name_filter').val('');
|
||||||
|
$('#auth_name_filter').val('');
|
||||||
|
$('#user_name_filter').removeAttr('disabled');
|
||||||
|
canSearch=false;
|
||||||
|
main_field="Domain Name"
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#account_tab').click(function() {
|
||||||
|
$('#domain_name_filter').val('');
|
||||||
|
$('#auth_name_filter').val('');
|
||||||
|
$('#user_name_filter').removeAttr('disabled');
|
||||||
|
canSearch=false;
|
||||||
|
main_field="Account Name"
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#user_auth_tab').click( function() {
|
||||||
|
$('#domain_name_filter').val('');
|
||||||
|
$('#account_name_filter').val('');
|
||||||
|
$('#user_name_filter').val('');
|
||||||
|
$('#user_name_filter').attr('disabled','disabled');
|
||||||
|
canSearch=false;
|
||||||
|
main_field="Username"
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#activity_tab').click( function() {
|
||||||
|
$('#domain_name_filter').val('');
|
||||||
|
$('#account_name_filter').val('');
|
||||||
|
$('#auth_name_filter').val('');
|
||||||
|
$('#user_name_filter').removeAttr('disabled');
|
||||||
|
$('#search-submit').removeAttr('disabled','disabled');
|
||||||
|
canSearch=true;
|
||||||
|
main_field=""
|
||||||
|
});
|
||||||
|
|
||||||
|
// if search submit is pressed, and max date not initialized
|
||||||
|
// then initialize it
|
||||||
|
$('#search-submit').on('click', function() {
|
||||||
|
if ($('#max').val() === "" || $('#max').val() === undefined)
|
||||||
|
$('#max').datepicker('setDate', 'now');
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#history-search-form").submit(function(e){ // ajax call to load results on submition
|
||||||
|
e.preventDefault(); // prevent page reloading
|
||||||
|
|
||||||
|
if(!canSearch)
|
||||||
|
{
|
||||||
|
var modal = $("#modal_error");
|
||||||
|
modal.find('.modal-body p').text("Please fill out the " + main_field + " field.");
|
||||||
|
modal.modal('show');
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var form = $(this);
|
||||||
|
var tzoffset = (new Date()).getTimezoneOffset();
|
||||||
|
$.ajax({
|
||||||
|
url: "/admin/history_table",
|
||||||
|
type: "get",
|
||||||
|
data: form.serialize() + "&tzoffset=" + tzoffset,
|
||||||
|
success: function(response) {
|
||||||
|
console.log('Submission was successful.');
|
||||||
|
$("#table_from_ajax").html(response);
|
||||||
|
|
||||||
|
},
|
||||||
|
error: function(xhr) {
|
||||||
|
console.log("Sending data: ", data, " failed")
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block modals %}
|
{% block modals %}
|
||||||
|
@ -127,7 +507,7 @@
|
||||||
<h4 class="modal-title">History Details</h4>
|
<h4 class="modal-title">History Details</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pre><code id="modal-code-content"></code></pre>
|
<div id="modal-info-content"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-flat btn-default pull-right" data-dismiss="modal">Close</button>
|
<button type="button" class="btn btn-flat btn-default pull-right" data-dismiss="modal">Close</button>
|
||||||
|
|
90
powerdnsadmin/templates/admin_history_table.html
Normal file
90
powerdnsadmin/templates/admin_history_table.html
Normal file
|
@ -0,0 +1,90 @@
|
||||||
|
|
||||||
|
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||||
|
|
||||||
|
|
||||||
|
{% if len_histories >= lim %}
|
||||||
|
<p style="color: rgb(224, 3, 3);"><b>Limit of loaded history records has been reached! Only {{lim}} history records are shown. </b></p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<div class="box-body"></div>
|
||||||
|
<table id="tbl_history" class="table table-bordered table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Changed by</th>
|
||||||
|
<th>Content</th>
|
||||||
|
<th>Time</th>
|
||||||
|
<th>Detail</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for history in histories %}
|
||||||
|
<tr class="odd gradeX">
|
||||||
|
<td>{{ history.history.created_by }}</td>
|
||||||
|
<td>{{ history.history.msg }}</td>
|
||||||
|
<td>{{ history.history.created_on }}</td>
|
||||||
|
|
||||||
|
<td width="6%">
|
||||||
|
<div id="history-info-div-{{ loop.index0 }}" style="display: none;">
|
||||||
|
{{ history.detailed_msg | safe }}
|
||||||
|
{% if history.change_set %}
|
||||||
|
<div class="content">
|
||||||
|
<div id="change_index_definition"></div>
|
||||||
|
{% call applied_change_macro.applied_change_template(history.change_set) %}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-flat btn-primary history-info-button"
|
||||||
|
{% if history.detailed_msg == "" and history.change_set is none %}
|
||||||
|
style="visibility: hidden;"
|
||||||
|
{% endif %} value="{{ loop.index0 }}">Info <i class="fa fa-info"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
var table;
|
||||||
|
$(document).ready(function () {
|
||||||
|
|
||||||
|
table = $('#tbl_history').DataTable({
|
||||||
|
"order": [
|
||||||
|
[2, "desc"]
|
||||||
|
],
|
||||||
|
"searching": true,
|
||||||
|
"columnDefs": [{
|
||||||
|
"type": "time",
|
||||||
|
"render": function (data, type, row) {
|
||||||
|
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
},
|
||||||
|
"targets": 2
|
||||||
|
}],
|
||||||
|
"info": true,
|
||||||
|
"autoWidth": false,
|
||||||
|
orderCellsTop: true,
|
||||||
|
fixedHeader: true
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document.body).on('click', '.history-info-button', function () {
|
||||||
|
var modal = $("#modal_history_info");
|
||||||
|
var history_id = $(this).val();
|
||||||
|
var info = $("#history-info-div-" + history_id).html();
|
||||||
|
$('#modal-info-content').html(info);
|
||||||
|
modal.modal('show');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document.body).on("click", ".button-filter", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var nextRow = $("#filter-table")
|
||||||
|
if (nextRow.css("visibility") == "visible")
|
||||||
|
nextRow.css("visibility", "collapse")
|
||||||
|
else
|
||||||
|
nextRow.css("visibility", "visible")
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
133
powerdnsadmin/templates/admin_manage_keys.html
Normal file
133
powerdnsadmin/templates/admin_manage_keys.html
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% set active_page = "admin_keys" %}
|
||||||
|
{% block title %}
|
||||||
|
<title>Key Management - {{ SITE_NAME }}</title>
|
||||||
|
{% endblock %} {% block dashboard_stat %}
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
Key <small>Manage API keys</small>
|
||||||
|
</h1>
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||||
|
<li class="active">Key</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{% endblock %} {% block content %}
|
||||||
|
<section class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-header">
|
||||||
|
<h3 class="box-title">Key Management</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<a href="{{ url_for('admin.edit_key') }}">
|
||||||
|
<button type="button" class="btn btn-flat btn-primary pull-left button_add_key">
|
||||||
|
Add Key <i class="fa fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<table id="tbl_keys" class="table table-bordered table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Id</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Description</th>
|
||||||
|
<th>Domains</th>
|
||||||
|
<th>Accounts</th>
|
||||||
|
<th>Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for key in keys %}
|
||||||
|
<tr class="odd gradeX">
|
||||||
|
<td>{{ key.id }}</td>
|
||||||
|
<td>{{ key.role.name }}</td>
|
||||||
|
<td>{{ key.description }}</td>
|
||||||
|
<td>{% for domain in key.domains %}{{ domain.name }}{% if not loop.last %}, {% endif %}{% endfor %}</td>
|
||||||
|
<td>{% for account in key.accounts %}{{ account.name }}{% if not loop.last %}, {% endif %}{% endfor %}</td>
|
||||||
|
<td width="15%">
|
||||||
|
<button type="button" class="btn btn-flat btn-success button_edit"
|
||||||
|
onclick="window.location.href='{{ url_for('admin.edit_key', key_id=key.id) }}'">
|
||||||
|
Edit <i class="fa fa-lock"></i>
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-flat btn-danger button_delete"
|
||||||
|
id="{{ key.id }}">
|
||||||
|
Delete <i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- /.box-body -->
|
||||||
|
</div>
|
||||||
|
<!-- /.box -->
|
||||||
|
</div>
|
||||||
|
<!-- /.col -->
|
||||||
|
</div>
|
||||||
|
<!-- /.row -->
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
{% block extrascripts %}
|
||||||
|
<script>
|
||||||
|
// set up key data table
|
||||||
|
$("#tbl_keys").DataTable({
|
||||||
|
"paging": true,
|
||||||
|
"lengthChange": true,
|
||||||
|
"searching": true,
|
||||||
|
"ordering": true,
|
||||||
|
"info": false,
|
||||||
|
"autoWidth": false,
|
||||||
|
"lengthMenu": [
|
||||||
|
[10, 25, 50, 100, -1],
|
||||||
|
[10, 25, 50, 100, "All"]
|
||||||
|
],
|
||||||
|
"pageLength": 10
|
||||||
|
});
|
||||||
|
|
||||||
|
// handle deletion of keys
|
||||||
|
$(document.body).on('click', '.button_delete', function () {
|
||||||
|
var modal = $("#modal_delete");
|
||||||
|
var key_id = $(this).prop('id');
|
||||||
|
var info = "Are you sure you want to delete key #" + key_id + "?";
|
||||||
|
modal.find('.modal-body p').text(info);
|
||||||
|
modal.find('#button_delete_confirm').click(function () {
|
||||||
|
var postdata = {
|
||||||
|
'action': 'delete_key',
|
||||||
|
'data': key_id,
|
||||||
|
'_csrf_token': '{{ csrf_token() }}'
|
||||||
|
}
|
||||||
|
applyChanges(postdata, $SCRIPT_ROOT + '/admin/manage-keys', false, true);
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.modal('show');
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
{% block modals %}
|
||||||
|
<div class="modal fade modal-warning" id="modal_delete">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title">Confirmation</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-flat btn-default pull-left" data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -51,7 +51,7 @@
|
||||||
<div class="nav-tabs-custom" id="tabs">
|
<div class="nav-tabs-custom" id="tabs">
|
||||||
<ul class="nav nav-tabs">
|
<ul class="nav nav-tabs">
|
||||||
<li class="active"><a href="#tabs-general" data-toggle="tab">General</a></li>
|
<li class="active"><a href="#tabs-general" data-toggle="tab">General</a></li>
|
||||||
<li class="active"><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li>
|
<li><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li>
|
||||||
<li><a href="#tabs-google" data-toggle="tab">Google OAuth</a></li>
|
<li><a href="#tabs-google" data-toggle="tab">Google OAuth</a></li>
|
||||||
<li><a href="#tabs-github" data-toggle="tab">Github OAuth</a></li>
|
<li><a href="#tabs-github" data-toggle="tab">Github OAuth</a></li>
|
||||||
<li><a href="#tabs-azure" data-toggle="tab">Microsoft OAuth</a></li>
|
<li><a href="#tabs-azure" data-toggle="tab">Microsoft OAuth</a></li>
|
||||||
|
@ -73,11 +73,19 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane active" id="tabs-ldap">
|
<div class="tab-pane" id="tabs-ldap">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger alert-dismissible">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||||
|
<h4><i class="icon fa fa-ban"></i> Error!</h4>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<form role="form" method="post" data-toggle="validator">
|
<form role="form" method="post" data-toggle="validator">
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<input type="hidden" value="ldap" name="config_tab" />
|
<input type="hidden" value="ldap" name="config_tab" />
|
||||||
|
@ -186,6 +194,46 @@
|
||||||
<span class="help-block with-errors"></span>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>ADVANCE</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Roles Autoprovisioning</label>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="autoprovisioning" id="autoprovisioning_off" value="OFF" {% if not SETTING.get('autoprovisioning') %}checked{% endif %}> OFF
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="autoprovisioning" id="autoprovisioning_on" value="ON"
|
||||||
|
|
||||||
|
{% if SETTING.get('autoprovisioning') %}checked{% endif %}> ON
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="autoprovisioning_attribute">Roles provisioning field</label>
|
||||||
|
<input type="text" class="form-control" name="autoprovisioning_attribute" id="autoprovisioning_attribute" placeholder="e.g. eduPersonEntitlement" data-error=" Please input field responsible for autoprovisioning" value="{{ SETTING.get('autoprovisioning_attribute') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group {% if error %}has-error{% endif %}">
|
||||||
|
<label for="urn_value">Urn prefix</label>
|
||||||
|
<input type="text" class="form-control" name="urn_value" id="urn_value" placeholder="e.g. urn:mace:<yourOrganization>" data-error="Please fill this field" value="{{ SETTING.get('urn_value') }}">
|
||||||
|
{% if error %}
|
||||||
|
<span class="help-block with-errors">Please input the correct prefix for your urn value</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Purge Roles If Empty</label>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="purge" id="purge_off" value="OFF" {% if not SETTING.get('purge') %}checked{% endif %}> OFF
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="purge" id="purge_on" value="ON" {% if SETTING.get('purge') %}checked{% endif %}> ON
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -261,6 +309,24 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</dd>
|
</dd>
|
||||||
|
<dt>ADVANCE</dt>
|
||||||
|
<dd> Provision PDA user privileges based on LDAP Object Attributes. Alternative to Group Security Role Management.
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
Roles Autoprovisioning - If toggled on, the PDA Role and the associations of users found in the local db, will be instantly updated from the LDAP server every time they log in.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Roles provisioning field - The attribute in the ldap server populated by the urn values where PDA will look for a new Role and/or new associations to domains/accounts.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Urn prefix - The prefix used before the static keyword "powerdns-admin" for your entitlements in the ldap server. Must comply with RFC no.8141.
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Purge Roles If Empty - If toggled on, ldap entries that have no valid "powerdns-admin" records to their autoprovisioning field, will lose all their associations with any domain or account, also reverting to a User in the process, despite their current role in the local db.<br> If toggled off, in the same scenario they get to keep their existing associations and their current Role.
|
||||||
|
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</dd>
|
||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -456,6 +522,41 @@
|
||||||
<span class="help-block with-errors"></span>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>AZURE GROUP ACCOUNT SYNC/CREATION</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_group_accounts_enabled">Status</label>
|
||||||
|
<div class="radio">
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="azure_group_accounts_enabled" id="azure_group_accounts_off" value="OFF" {% if not SETTING.get('azure_group_accounts_enabled') %}checked{% endif %}> OFF
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label>
|
||||||
|
<input type="radio" name="azure_group_accounts_enabled" id="azure_group_accounts_on" value="ON" {% if SETTING.get('azure_group_accounts_enabled') %}checked{% endif %}> ON
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_group_accounts_name">Azure group name claim</label>
|
||||||
|
<input type="text" class="form-control" name="azure_group_accounts_name" id="azure_group_accounts_name" placeholder="e.g. displayName" data-error="Please input the Claim for Azure group name" value="{{ SETTING.get('azure_group_accounts_name') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_group_accounts_name_re">Azure group name claim regex</label>
|
||||||
|
<input type="text" class="form-control" name="azure_group_accounts_name_re" id="azure_group_accounts_name_re" placeholder="e.g. (.*)" data-error="Please input the regex for Azure group name" value="{{ SETTING.get('azure_group_accounts_name_re') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_group_accounts_description">Azure group description claim</label>
|
||||||
|
<input type="text" class="form-control" name="azure_group_accounts_description" id="azure_group_accounts_description" placeholder="e.g. description. If empty uses whole string" data-error="Please input the Claim for Azure group description" value="{{ SETTING.get('azure_group_accounts_description') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="azure_group_accounts_name_re">Azure group name description regex</label>
|
||||||
|
<input type="text" class="form-control" name="azure_group_accounts_description_re" id="azure_group_accounts_description_re" placeholder="e.g. (.*). If empty uses whole string" data-error="Please input the regex for Azure group description" value="{{ SETTING.get('azure_group_accounts_description_re') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -476,6 +577,7 @@
|
||||||
<li>For the Scope, use <b>User.Read openid mail profile</b></li>
|
<li>For the Scope, use <b>User.Read openid mail profile</b></li>
|
||||||
<li>Replace the [tenantID] in the default URLs for authorize and token with your Tenant ID.</li>
|
<li>Replace the [tenantID] in the default URLs for authorize and token with your Tenant ID.</li>
|
||||||
</ul></p>
|
</ul></p>
|
||||||
|
<p>If <b>AZURE GROUP ACCOUNT SYNC/CREATION</b> is enabled, Accounts will be created automatically based on group membership. If an Account exists, an authenticated user with group membership is added to the Account</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -502,9 +604,6 @@
|
||||||
<input type="text" class="form-control" name="oidc_oauth_secret" id="oidc_oauth_secret" placeholder="OIDC OAuth client secret" data-error="Please input Client secret" value="{{ SETTING.get('oidc_oauth_secret') }}">
|
<input type="text" class="form-control" name="oidc_oauth_secret" id="oidc_oauth_secret" placeholder="OIDC OAuth client secret" data-error="Please input Client secret" value="{{ SETTING.get('oidc_oauth_secret') }}">
|
||||||
<span class="help-block with-errors"></span>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
|
||||||
<fieldset>
|
|
||||||
<legend>ADVANCE</legend>
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="oidc_oauth_scope">Scope</label>
|
<label for="oidc_oauth_scope">Scope</label>
|
||||||
<input type="text" class="form-control" name="oidc_oauth_scope" id="oidc_oauth_scope" placeholder="e.g. email" data-error="Please input scope" value="{{ SETTING.get('oidc_oauth_scope') }}">
|
<input type="text" class="form-control" name="oidc_oauth_scope" id="oidc_oauth_scope" placeholder="e.g. email" data-error="Please input scope" value="{{ SETTING.get('oidc_oauth_scope') }}">
|
||||||
|
@ -525,6 +624,47 @@
|
||||||
<input type="text" class="form-control" name="oidc_oauth_authorize_url" id="oidc_oauth_authorize_url" placeholder="e.g. https://oidc.com/login/oauth/authorize" data-error="Plesae input Authorize URL" value="{{ SETTING.get('oidc_oauth_authorize_url') }}">
|
<input type="text" class="form-control" name="oidc_oauth_authorize_url" id="oidc_oauth_authorize_url" placeholder="e.g. https://oidc.com/login/oauth/authorize" data-error="Plesae input Authorize URL" value="{{ SETTING.get('oidc_oauth_authorize_url') }}">
|
||||||
<span class="help-block with-errors"></span>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidc_oauth_logout_url">Logout URL</label>
|
||||||
|
<input type="text" class="form-control" name="oidc_oauth_logout_url" id="oidc_oauth_logout_url" placeholder="e.g. https://oidc.com/login/oauth/logout" data-error="Please input Logout URL" value="{{ SETTING.get('oidc_oauth_logout_url') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>CLAIMS</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidc_oauth_username">Username</label>
|
||||||
|
<input type="text" class="form-control" name="oidc_oauth_username" id="oidc_oauth_username" placeholder="e.g. preferred_username" data-error="Please input Username claim" value="{{ SETTING.get('oidc_oauth_username') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidc_oauth_firstname">First Name</label>
|
||||||
|
<input type="text" class="form-control" name="oidc_oauth_firstname" id="oidc_oauth_firstname" placeholder="e.g. given_name" data-error="Please input First Name claim" value="{{ SETTING.get('oidc_oauth_firstname') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidc_oauth_last_name">Last Name</label>
|
||||||
|
<input type="text" class="form-control" name="oidc_oauth_last_name" id="oidc_oauth_last_name" placeholder="e.g. family_name" data-error="Please input Last Name claim" value="{{ SETTING.get('oidc_oauth_last_name') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidc_oauth_email">Email</label>
|
||||||
|
<input type="text" class="form-control" name="oidc_oauth_email" id="oidc_oauth_email" placeholder="e.g. email" data-error="Plesae input Email claim" value="{{ SETTING.get('oidc_oauth_email') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
<fieldset>
|
||||||
|
<legend>ADVANCE</legend>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidc_oauth_account_name_property">Autoprovision Account Name property</label>
|
||||||
|
<input type="text" class="form-control" name="oidc_oauth_account_name_property" id="oidc_oauth_account_name_property" placeholder="e.g. account_name" data-error="Please input property containing account_name" value="{{ SETTING.get('oidc_oauth_account_name_property') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="oidc_oauth_account_description_property">Autoprovision Account Description property</label>
|
||||||
|
<input type="text" class="form-control" name="oidc_oauth_account_description_property" id="oidc_oauth_account_description_property" placeholder="e.g. account_description" data-error="Please input property containing account_description" value="{{ SETTING.get('oidc_oauth_account_description_property') }}">
|
||||||
|
<span class="help-block with-errors"></span>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
<button type="submit" class="btn btn-flat btn-primary">Save</button>
|
||||||
|
@ -574,6 +714,11 @@
|
||||||
checkboxClass : 'icheckbox_square-blue',
|
checkboxClass : 'icheckbox_square-blue',
|
||||||
increaseArea : '20%'
|
increaseArea : '20%'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
$('#autoprovisioning').iCheck({
|
||||||
|
checkboxClass : 'icheckbox_square-blue',
|
||||||
|
increaseArea : '20%'
|
||||||
|
})
|
||||||
// END: General tab js
|
// END: General tab js
|
||||||
|
|
||||||
// START: LDAP tab js
|
// START: LDAP tab js
|
||||||
|
@ -605,7 +750,10 @@
|
||||||
$('#ldap_operator_group').prop('required', true);
|
$('#ldap_operator_group').prop('required', true);
|
||||||
$('#ldap_user_group').prop('required', true);
|
$('#ldap_user_group').prop('required', true);
|
||||||
}
|
}
|
||||||
|
if ($('#autoprovisioning').is(":checked")) {
|
||||||
|
$('#autoprovisioning_attribute').prop('required', true);
|
||||||
|
$('#urn_value').prop('required', true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$('#ldap_uri').prop('required', false);
|
$('#ldap_uri').prop('required', false);
|
||||||
$('#ldap_base_dn').prop('required', false);
|
$('#ldap_base_dn').prop('required', false);
|
||||||
|
@ -621,6 +769,10 @@
|
||||||
$('#ldap_operator_group').prop('required', false);
|
$('#ldap_operator_group').prop('required', false);
|
||||||
$('#ldap_user_group').prop('required', false);
|
$('#ldap_user_group').prop('required', false);
|
||||||
}
|
}
|
||||||
|
if ($('#autoprovisioning').is(":checked")) {
|
||||||
|
$('#autoprovisioning_attribute').prop('required', false);
|
||||||
|
$('#urn_value').prop('required', true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -634,8 +786,75 @@
|
||||||
$('#ldap_operator_group').prop('required', false);
|
$('#ldap_operator_group').prop('required', false);
|
||||||
$('#ldap_user_group').prop('required', false);
|
$('#ldap_user_group').prop('required', false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($('#ldap_sg_on').is(":checked") && $('#autoprovisioning_on').is(":checked")){
|
||||||
|
document.getElementById('ldap_sg_on').checked=false;
|
||||||
|
document.getElementById('ldap_sg_off').checked=true;
|
||||||
|
var modal = $("#modal_warning");
|
||||||
|
|
||||||
|
var info = "Group Security:Status and Advance:Autoprovisioning can not be both enabled at the same time. Please turn off Advance:Autoprovisioning first" ;
|
||||||
|
modal.find('.modal-body p').text(info);
|
||||||
|
modal.find('#button_warning_confirm').click(function () {
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.find('#warning_X').click(function () {
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.modal('show');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$("input[name='autoprovisioning']" ).change(function(){
|
||||||
|
if ($('#autoprovisioning_on').is(":checked") && $('#ldap_enabled').is(":checked")) {
|
||||||
|
$('#autoprovisioning_attribute').prop('required', true);
|
||||||
|
$('#urn_value').prop('required', true);
|
||||||
|
$('#purge').prop('required', true);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$('#autoprovisioning_attribute').prop('required', false);
|
||||||
|
$('#urn_value').prop('required', false);
|
||||||
|
$('#purge').prop('required', false);
|
||||||
|
}
|
||||||
|
if ($('#ldap_sg_on').is(":checked") && $('#autoprovisioning_on').is(":checked")){
|
||||||
|
document.getElementById('autoprovisioning_on').checked=false;
|
||||||
|
document.getElementById('autoprovisioning_off').checked=true;
|
||||||
|
var modal = $("#modal_warning");
|
||||||
|
var info = "Group Security:Status and Advance:Autoprovisioning can not be both enabled at the same time. Please turn off Group Security:Status first" ;
|
||||||
|
modal.find('.modal-body p').text(info);
|
||||||
|
modal.find('#button_warning_confirm').click(function () {
|
||||||
|
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.find('#warning_X').click(function () {
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.modal('show');
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("input[name='purge']" ).change(function(){
|
||||||
|
if ($("#purge_on").is(":checked")){
|
||||||
|
document.getElementById('purge_on').checked=false;
|
||||||
|
document.getElementById('purge_off').checked=true;
|
||||||
|
var modal = $("#modal_confirm");
|
||||||
|
var info = "Are you sure you want to do this? Users will lose their associated domains unless they already have their autoprovisioning field prepopulated." ;
|
||||||
|
modal.find('.modal-body p').text(info);
|
||||||
|
modal.find('#button_confirm').click(function () {
|
||||||
|
document.getElementById('purge_on').checked=true;
|
||||||
|
document.getElementById('purge_off').checked=false;
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.find('#button_cancel').click(function () {
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.find('#X').click(function () {
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.modal('show');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$("input[name='ldap_type']" ).change(function(){
|
$("input[name='ldap_type']" ).change(function(){
|
||||||
if ($('#ldap').is(":checked") && $('#ldap_enabled').is(":checked")) {
|
if ($('#ldap').is(":checked") && $('#ldap_enabled').is(":checked")) {
|
||||||
$('#ldap_admin_group').prop('required', true);
|
$('#ldap_admin_group').prop('required', true);
|
||||||
|
@ -673,7 +892,14 @@
|
||||||
$('#ldap_operator_group').prop('required', true);
|
$('#ldap_operator_group').prop('required', true);
|
||||||
$('#ldap_user_group').prop('required', true);
|
$('#ldap_user_group').prop('required', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($('#autoprovisioning_on').is(":checked")) {
|
||||||
|
$('#autoprovisioning_attribute').prop('required', true);
|
||||||
|
$('#urn_value').prop('required', true);
|
||||||
|
}
|
||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
// END: LDAP tab js
|
// END: LDAP tab js
|
||||||
|
|
||||||
// START: Google tab js
|
// START: Google tab js
|
||||||
|
@ -792,6 +1018,10 @@
|
||||||
$('#oidc_oauth_api_url').prop('required', true);
|
$('#oidc_oauth_api_url').prop('required', true);
|
||||||
$('#oidc_oauth_token_url').prop('required', true);
|
$('#oidc_oauth_token_url').prop('required', true);
|
||||||
$('#oidc_oauth_authorize_url').prop('required', true);
|
$('#oidc_oauth_authorize_url').prop('required', true);
|
||||||
|
$('#oidc_oauth_username').prop('required', true);
|
||||||
|
$('#oidc_oauth_firstname').prop('required', true);
|
||||||
|
$('#oidc_oauth_last_name').prop('required', true);
|
||||||
|
$('#oidc_oauth_email').prop('required', true);
|
||||||
} else {
|
} else {
|
||||||
$('#oidc_oauth_key').prop('required', false);
|
$('#oidc_oauth_key').prop('required', false);
|
||||||
$('#oidc_oauth_secret').prop('required', false);
|
$('#oidc_oauth_secret').prop('required', false);
|
||||||
|
@ -799,6 +1029,10 @@
|
||||||
$('#oidc_oauth_api_url').prop('required', false);
|
$('#oidc_oauth_api_url').prop('required', false);
|
||||||
$('#oidc_oauth_token_url').prop('required', false);
|
$('#oidc_oauth_token_url').prop('required', false);
|
||||||
$('#oidc_oauth_authorize_url').prop('required', false);
|
$('#oidc_oauth_authorize_url').prop('required', false);
|
||||||
|
$('#oidc_oauth_username').prop('required', false);
|
||||||
|
$('#oidc_oauth_firstname').prop('required', false);
|
||||||
|
$('#oidc_oauth_last_name').prop('required', false);
|
||||||
|
$('#oidc_oauth_email').prop('required', false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// init validation requirement at first time page load
|
// init validation requirement at first time page load
|
||||||
|
@ -809,8 +1043,60 @@
|
||||||
$('#oidc_oauth_api_url').prop('required', true);
|
$('#oidc_oauth_api_url').prop('required', true);
|
||||||
$('#oidc_oauth_token_url').prop('required', true);
|
$('#oidc_oauth_token_url').prop('required', true);
|
||||||
$('#oidc_oauth_authorize_url').prop('required', true);
|
$('#oidc_oauth_authorize_url').prop('required', true);
|
||||||
|
$('#oidc_oauth_username').prop('required', true);
|
||||||
|
$('#oidc_oauth_firstname').prop('required', true);
|
||||||
|
$('#oidc_oauth_last_name').prop('required', true);
|
||||||
|
$('#oidc_oauth_email').prop('required', true);
|
||||||
{% endif %}
|
{% endif %}
|
||||||
//END: OIDC Tab JS
|
//END: OIDC Tab JS
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modals %}
|
||||||
|
<div class="modal fade modal-warning" id="modal_confirm" data-keyboard="false" data-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="X" >
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title">Confirmation</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-flat btn-default pull-left" id="button_cancel" name="purge" value="OFF" data-dismiss="modal" >Cancel</button>
|
||||||
|
<button type="button" class="btn btn-flat btn-success" id="button_confirm">Confirm</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="modal fade modal-warning" id="modal_warning" data-keyboard="false" data-backdrop="static">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="warning_X" >
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title">Warning</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-flat btn-success" id="button_warning_confirm">Yes I understand</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
133
powerdnsadmin/templates/applied_change_macro.html
Normal file
133
powerdnsadmin/templates/applied_change_macro.html
Normal file
|
@ -0,0 +1,133 @@
|
||||||
|
{% macro applied_change_template(change_set) -%}
|
||||||
|
{{ caller() }}
|
||||||
|
{% for hist_rec_entry in change_set %}
|
||||||
|
<table id="tbl_records" class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th colspan="3">
|
||||||
|
{% if hist_rec_entry.change_type == "+" %}
|
||||||
|
<span
|
||||||
|
style="background-color: lightgreen">{{hist_rec_entry.add_rrest['name']}}
|
||||||
|
{{hist_rec_entry.add_rrest['type']}}</span>
|
||||||
|
{% elif hist_rec_entry.change_type == "-" %}
|
||||||
|
<s
|
||||||
|
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||||
|
{{hist_rec_entry.del_rrest['name']}}
|
||||||
|
{{hist_rec_entry.del_rrest['type']}}
|
||||||
|
</s>
|
||||||
|
{% else %}
|
||||||
|
{{hist_rec_entry.add_rrest['name']}}
|
||||||
|
{{hist_rec_entry.add_rrest['type']}}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
, TTL:
|
||||||
|
{% if "ttl" in hist_rec_entry.changed_fields %}
|
||||||
|
<s
|
||||||
|
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||||
|
{{hist_rec_entry.del_rrest['ttl']}}</s>
|
||||||
|
<span
|
||||||
|
style="background-color: lightgreen">{{hist_rec_entry.add_rrest['ttl']}}</span>
|
||||||
|
{% else %}
|
||||||
|
{{hist_rec_entry.add_rrest['ttl']}}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
|
||||||
|
<th style="width: 150px;">Status</th>
|
||||||
|
<th style="width: 400px;">Data</th>
|
||||||
|
<th style="width: 400px;">Comment</th>
|
||||||
|
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{% for changes in hist_rec_entry.changeSet %}
|
||||||
|
<tr>
|
||||||
|
{% if changes[2] == "unchanged" %}
|
||||||
|
<td>{{ "Activated" if changes[0]['disabled'] ==
|
||||||
|
False else
|
||||||
|
"Disabled"}} </td>
|
||||||
|
{% elif changes[2] == "addition" %}
|
||||||
|
<td>
|
||||||
|
<span style="background-color: lightgreen">
|
||||||
|
{{ "Activated" if changes[1]['disabled'] ==
|
||||||
|
False else
|
||||||
|
"Disabled"}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{% elif changes[2] == "status" %}
|
||||||
|
<td>
|
||||||
|
<s
|
||||||
|
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||||
|
{{ "Activated" if changes[0]['disabled'] ==
|
||||||
|
False else
|
||||||
|
"Disabled"}}</s>
|
||||||
|
<span style="background-color: lightgreen">{{
|
||||||
|
"Activated" if changes[1]['disabled'] ==
|
||||||
|
False else
|
||||||
|
"Disabled"}}</span>
|
||||||
|
</td>
|
||||||
|
{% elif changes[2] == "deletion" %}
|
||||||
|
<td>
|
||||||
|
<s
|
||||||
|
style="text-decoration-color: rgba(194, 10,10, 0.6); text-decoration-thickness: 2px;">
|
||||||
|
{{ "Activated" if changes[0]['disabled'] ==
|
||||||
|
False else
|
||||||
|
"Disabled"}}</s>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<table>
|
||||||
|
<tbody>
|
||||||
|
{% for changes in hist_rec_entry.changeSet %}
|
||||||
|
<tr>
|
||||||
|
{% if changes[2] == "unchanged" %}
|
||||||
|
<td>
|
||||||
|
{{changes[0]['content']}}
|
||||||
|
</td>
|
||||||
|
{% elif changes[2] == "addition" %}
|
||||||
|
<td>
|
||||||
|
<span style="background-color: lightgreen">
|
||||||
|
{{changes[1]['content']}}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
{% elif changes[2] == "deletion" %}
|
||||||
|
<td>
|
||||||
|
<s
|
||||||
|
style="text-decoration-color: rgba(194, 10, 10, 0.6); text-decoration-thickness: 2px;">
|
||||||
|
{{changes[0]['content']}}
|
||||||
|
</s>
|
||||||
|
</td>
|
||||||
|
{% elif changes[2] == "status" %}
|
||||||
|
<td>
|
||||||
|
{{changes[0]['content']}}
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
|
||||||
|
</table>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% for comments in hist_rec_entry.add_rrest['comments'] %}
|
||||||
|
{{comments['content'] }}
|
||||||
|
<br/>
|
||||||
|
{% endfor %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endfor %}
|
||||||
|
{%- endmacro %}
|
|
@ -4,21 +4,27 @@
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<link rel="icon" href="/static/img/favicon.png">
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||||
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
|
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}">
|
||||||
<!-- Get Google Fonts we like -->
|
<!-- Get Google Fonts we like -->
|
||||||
{% if OFFLINE_MODE %}
|
{% if OFFLINE_MODE %}
|
||||||
<link rel="stylesheet" href="/static/assets/css/source_sans_pro.css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/source_sans_pro.css') }}">
|
||||||
<link rel="stylesheet" href="/static/assets/css/roboto_mono.css">
|
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/roboto_mono.css') }}">
|
||||||
{% else %}
|
{% else %}
|
||||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
|
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
|
||||||
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Mono:400,300,700">
|
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Mono:400,300,700">
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<!-- Tell the browser to be responsive to screen width -->
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
<!-- Tell Safari to not recognise telephone numbers -->
|
||||||
|
<meta name="format-detection" content="telephone=no">
|
||||||
{% assets "css_main" -%}
|
{% assets "css_main" -%}
|
||||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||||
{%- endassets %}
|
{%- endassets %}
|
||||||
|
{% if SETTING.get('custom_css') %}
|
||||||
|
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
||||||
|
{% endif %}
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||||
<!--[if lt IE 9]>
|
<!--[if lt IE 9]>
|
||||||
|
@ -29,8 +35,8 @@
|
||||||
</head>
|
</head>
|
||||||
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
|
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
|
||||||
{% if OFFLINE_MODE %}
|
{% if OFFLINE_MODE %}
|
||||||
{% set gravatar_url = "/static/img/gravatar.png" %}
|
{% set gravatar_url = url_for('static', filename='img/gravatar.png') %}
|
||||||
{% else %}
|
{% elif current_user.email is defined %}
|
||||||
{% set gravatar_url = current_user.email|email_to_gravatar_url(size=80) %}
|
{% set gravatar_url = current_user.email|email_to_gravatar_url(size=80) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="wrapper">
|
<div class="wrapper">
|
||||||
|
@ -112,6 +118,11 @@
|
||||||
<a href="{{ url_for('domain.add') }}"><i class="fa fa-plus"></i> <span>New Domain</span></a>
|
<a href="{{ url_for('domain.add') }}"><i class="fa fa-plus"></i> <span>New Domain</span></a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if SETTING.get('allow_user_remove_domain') or current_user.role.name in ['Administrator', 'Operator'] %}
|
||||||
|
<li class="{{ 'active' if active_page == 'remove_domain' else '' }}">
|
||||||
|
<a href="{{ url_for('domain.remove') }}"><i class="fa fa-trash-o"></i> <span>Remove Domain</span></a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if current_user.role.name in ['Administrator', 'Operator'] %}
|
{% if current_user.role.name in ['Administrator', 'Operator'] %}
|
||||||
<li class="header">ADMINISTRATION</li>
|
<li class="header">ADMINISTRATION</li>
|
||||||
<li class="{{ 'active' if active_page == 'admin_console' else '' }}">
|
<li class="{{ 'active' if active_page == 'admin_console' else '' }}">
|
||||||
|
@ -132,6 +143,9 @@
|
||||||
<li class="{{ 'active' if active_page == 'admin_users' else '' }}">
|
<li class="{{ 'active' if active_page == 'admin_users' else '' }}">
|
||||||
<a href="{{ url_for('admin.manage_user') }}"><i class="fa fa-users"></i> <span>Users</span></a>
|
<a href="{{ url_for('admin.manage_user') }}"><i class="fa fa-users"></i> <span>Users</span></a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="{{ 'active' if active_page == 'admin_keys' else '' }}">
|
||||||
|
<a href="{{ url_for('admin.manage_keys') }}"><i class="fa fa-key"></i> <span>API Keys</span></a>
|
||||||
|
</li>
|
||||||
<li class="{{ 'treeview active' if active_page == 'admin_settings' else 'treeview' }}">
|
<li class="{{ 'treeview active' if active_page == 'admin_settings' else 'treeview' }}">
|
||||||
<a href="#">
|
<a href="#">
|
||||||
<i class="fa fa-cog"></i> <span>Settings</span>
|
<i class="fa fa-cog"></i> <span>Settings</span>
|
||||||
|
@ -148,6 +162,11 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
{% elif SETTING.get('allow_user_view_history') %}
|
||||||
|
<li class="header">ADMINISTRATION</li>
|
||||||
|
<li class="{{ 'active' if active_page == 'admin_history' else '' }}">
|
||||||
|
<a href="{{ url_for('admin.history') }}"><i class="fa fa-calendar"></i> <span>History</span></a>
|
||||||
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
{% set active_page = "dashboard" %}
|
{% set active_page = "dashboard" %}
|
||||||
{% block title %}<title>Dashboard - {{ SITE_NAME }}</title>{% endblock %}
|
{% block title %}<title>Dashboard - {{ SITE_NAME }}</title>{% endblock %}
|
||||||
|
|
||||||
|
|
||||||
{% block dashboard_stat %}
|
{% block dashboard_stat %}
|
||||||
<!-- Content Header (Page header) -->
|
<!-- Content Header (Page header) -->
|
||||||
<section class="content-header">
|
<section class="content-header">
|
||||||
|
@ -16,10 +17,12 @@
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<section class="content">
|
<section class="content">
|
||||||
{% if current_user.role.name in ['Administrator', 'Operator'] %}
|
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-3">
|
<div class="col-xs-3">
|
||||||
<div class="box">
|
<div class="box">
|
||||||
|
@ -40,6 +43,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] %}
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<a href="{{ url_for('admin.manage_user') }}">
|
<a href="{{ url_for('admin.manage_user') }}">
|
||||||
<div class="small-box bg-green">
|
<div class="small-box bg-green">
|
||||||
|
@ -53,6 +57,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
|
@ -68,6 +73,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] %}
|
||||||
<div class="col-lg-6">
|
<div class="col-lg-6">
|
||||||
<a href="{{ url_for('admin.pdns_stats') }}">
|
<a href="{{ url_for('admin.pdns_stats') }}">
|
||||||
<div class="small-box bg-green">
|
<div class="small-box bg-green">
|
||||||
|
@ -81,6 +87,7 @@
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -103,13 +110,25 @@
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for history in histories %}
|
{% for history in histories %}
|
||||||
<tr class="odd">
|
<tr class="odd">
|
||||||
<td>{{ history.created_by }}</td>
|
<td>{{ history.history.created_by }}</td>
|
||||||
<td>{{ history.msg }}</td>
|
<td>{{ history.history.msg }}</td>
|
||||||
<td>{{ history.created_on }}</td>
|
<td>{{ history.history.created_on }}</td>
|
||||||
<td width="6%">
|
<td width="6%">
|
||||||
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail }}'>
|
<div id="history-info-div-{{ loop.index0 }}" style="display: none;">
|
||||||
Info <i class="fa fa-info"></i>
|
{{ history.detailed_msg | safe }}
|
||||||
</button>
|
{% if history.change_set %}
|
||||||
|
<div class="content">
|
||||||
|
<div id="change_index_definition"></div>
|
||||||
|
{% call applied_change_macro.applied_change_template(history.change_set) %}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-flat btn-primary history-info-button"
|
||||||
|
{% if history.detailed_msg == "" and history.change_set is none %}
|
||||||
|
style="visibility: hidden;"
|
||||||
|
{% endif %} value="{{ loop.index0 }}">Info <i class="fa fa-info"></i>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -222,11 +241,11 @@
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document.body).on('click', '.history-info-button', function()
|
$(document.body).on('click', '.history-info-button', function () {
|
||||||
{
|
|
||||||
var modal = $("#modal_history_info");
|
var modal = $("#modal_history_info");
|
||||||
var info = $(this).val();
|
var history_id = $(this).val();
|
||||||
$('#modal-code-content').html(json_library.prettyPrint(info));
|
var info = $("#history-info-div-" + history_id).html();
|
||||||
|
$('#modal-info-content').html(info);
|
||||||
modal.modal('show');
|
modal.modal('show');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -293,7 +312,7 @@
|
||||||
<h4 class="modal-title">History Details</h4>
|
<h4 class="modal-title">History Details</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<pre><code id="modal-code-content"></code></pre>
|
<div id="modal-info-content"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-flat btn-default pull-right"
|
<button type="button" class="btn btn-flat btn-default pull-right"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{% macro name(domain) %}
|
{% macro name(domain) %}
|
||||||
<a href="{{ url_for('domain.domain', domain_name=domain.name) }}"><strong>{{ domain.name }}</strong></a>
|
<a href="{{ url_for('domain.domain', domain_name=domain.name) }}"><strong>{{ domain.name | pretty_domain_name }}</strong></a>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro dnssec(domain) %}
|
{% macro dnssec(domain) %}
|
||||||
|
@ -15,11 +15,11 @@
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro serial(domain) %}
|
{% macro serial(domain) %}
|
||||||
{% if domain.serial == 0 %}{{ domain.notified_serial }}{% else %}{{domain.serial}}{% endif %}
|
{% if domain.serial == '0' %}{{ domain.notified_serial }}{% else %}{{ domain.serial }}{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro master(domain) %}
|
{% macro master(domain) %}
|
||||||
{% if domain.master == '[]'%}-{% else %}{{ domain.master|display_master_name }}{% endif %}
|
{% if domain.master == '[]'%}-{% else %}{{ domain.master | display_master_name }}{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro account(domain) %}
|
{% macro account(domain) %}
|
||||||
|
@ -40,12 +40,20 @@
|
||||||
<button type="button" class="btn btn-flat btn-danger" onclick="window.location.href='{{ url_for('domain.setting', domain_name=domain.name) }}'">
|
<button type="button" class="btn btn-flat btn-danger" onclick="window.location.href='{{ url_for('domain.setting', domain_name=domain.name) }}'">
|
||||||
Admin <i class="fa fa-cog"></i>
|
Admin <i class="fa fa-cog"></i>
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain.changelog', domain_name=domain.name) }}'">
|
||||||
|
Changelog <i class="fa fa-history" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
</td>
|
</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
<td width="6%">
|
<td width="6%">
|
||||||
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain.domain', domain_name=domain.name) }}'">
|
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain.domain', domain_name=domain.name) }}'">
|
||||||
Manage <i class="fa fa-cog"></i>
|
Manage <i class="fa fa-cog"></i>
|
||||||
</button>
|
</button>
|
||||||
|
{% if allow_user_view_history %}
|
||||||
|
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain.changelog', domain_name=domain.name) }}'">
|
||||||
|
Changelog <i class="fa fa-history" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
64
powerdnsadmin/templates/domain.html
Normal file → Executable file
64
powerdnsadmin/templates/domain.html
Normal file → Executable file
|
@ -1,16 +1,16 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}<title>{{ domain.name }} - {{ SITE_NAME }}</title>{% endblock %}
|
{% block title %}<title>{{ domain.name | pretty_domain_name }} - {{ SITE_NAME }}</title>{% endblock %}
|
||||||
|
|
||||||
{% block dashboard_stat %}
|
{% block dashboard_stat %}
|
||||||
<section class="content-header">
|
<section class="content-header">
|
||||||
<h1>
|
<h1>
|
||||||
Manage domain: <b>{{ domain.name }}</b>
|
Manage domain: <b>{{ domain.name | pretty_domain_name }}</b>
|
||||||
</h1>
|
</h1>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i
|
<li><a href="{{ url_for('dashboard.dashboard') }}"><i
|
||||||
class="fa fa-dashboard"></i> Home</a></li>
|
class="fa fa-dashboard"></i> Home</a></li>
|
||||||
<li>Domain</li>
|
<li>Domain</li>
|
||||||
<li class="active">{{ domain.name }}</li>
|
<li class="active">{{ domain.name | pretty_domain_name }}</li>
|
||||||
</ol>
|
</ol>
|
||||||
</section>
|
</section>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -33,6 +33,17 @@
|
||||||
Update from Master <i class="fa fa-download"></i>
|
Update from Master <i class="fa fa-download"></i>
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] %}
|
||||||
|
<button type="button" style="position: relative; margin-left: 20px" class="btn btn-flat btn-primary pull-left btn-danger" onclick="window.location.href='{{ url_for('domain.setting', domain_name=domain.name) }}'">
|
||||||
|
Admin <i class="fa fa-cog"></i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||||
|
<button type="button" style="position: relative; margin-left: 20px" class="btn btn-flat btn-primary button_changelog" id="{{ domain.name }}">
|
||||||
|
Changelog <i class="fa fa-history" aria-hidden="true"></i>
|
||||||
|
</i>
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="box-body">
|
<div class="box-body">
|
||||||
<table id="tbl_records" class="table table-bordered table-striped">
|
<table id="tbl_records" class="table table-bordered table-striped">
|
||||||
|
@ -46,13 +57,16 @@
|
||||||
<th>Comment</th>
|
<th>Comment</th>
|
||||||
<th>Edit</th>
|
<th>Edit</th>
|
||||||
<th>Delete</th>
|
<th>Delete</th>
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||||
|
<th >Changelog</th>
|
||||||
|
{% endif %}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for record in records %}
|
{% for record in records %}
|
||||||
<tr class="odd row_record" id="{{ domain.name }}">
|
<tr class="odd row_record" id="{{ domain.name }}">
|
||||||
<td>
|
<td>
|
||||||
{{ (record.name,domain.name)|display_record_name }}
|
{{ (record.name,domain.name) | display_record_name | pretty_domain_name }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ record.type }}
|
{{ record.type }}
|
||||||
|
@ -64,7 +78,7 @@
|
||||||
{{ record.ttl }}
|
{{ record.ttl }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ record.data }}
|
{{ record.data | pretty_domain_name }}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{{ record.comment }}
|
{{ record.comment }}
|
||||||
|
@ -74,7 +88,7 @@
|
||||||
{% if record.is_allowed_edit() %}
|
{% if record.is_allowed_edit() %}
|
||||||
<button type="button" class="btn btn-flat btn-warning button_edit">Edit <i class="fa fa-edit"></i></button>
|
<button type="button" class="btn btn-flat btn-warning button_edit">Edit <i class="fa fa-edit"></i></button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button type="button" class="btn btn-flat btn-warning""> <i class="fa fa-exclamation-circle"></i> </button>
|
<button type="button" class="btn btn-flat btn-warning"> <i class="fa fa-exclamation-circle"></i></button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td width="6%">
|
<td width="6%">
|
||||||
|
@ -91,6 +105,13 @@
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||||
|
<td width="6%">
|
||||||
|
<button type="button" onclick="show_record_changelog('{{record.name}}','{{record.type}}',event)" class="btn btn-flat btn-primary">
|
||||||
|
<i class="fa fa-history" aria-hidden="true"></i>
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
{% endif %}
|
||||||
<!-- hidden column that we can sort on -->
|
<!-- hidden column that we can sort on -->
|
||||||
<td>1</td>
|
<td>1</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -110,8 +131,8 @@
|
||||||
{% block extrascripts %}
|
{% block extrascripts %}
|
||||||
<script>
|
<script>
|
||||||
// superglobals
|
// superglobals
|
||||||
window.records_allow_edit = {{ editable_records|tojson }};
|
window.records_allow_edit = {{ editable_records | tojson }};
|
||||||
window.ttl_options = {{ ttl_options|tojson }};
|
window.ttl_options = {{ ttl_options | tojson }};
|
||||||
window.nEditing = null;
|
window.nEditing = null;
|
||||||
window.nNew = false;
|
window.nNew = false;
|
||||||
|
|
||||||
|
@ -123,7 +144,7 @@
|
||||||
"ordering" : true,
|
"ordering" : true,
|
||||||
"info" : true,
|
"info" : true,
|
||||||
"autoWidth" : false,
|
"autoWidth" : false,
|
||||||
{% if SETTING.get('default_record_table_size')|string in ['5','15','20'] %}
|
{% if SETTING.get('default_record_table_size') | string in ['5','15','20'] %}
|
||||||
"lengthMenu": [ [5, 15, 20, -1],
|
"lengthMenu": [ [5, 15, 20, -1],
|
||||||
[5, 15, 20, "All"]],
|
[5, 15, 20, "All"]],
|
||||||
{% else %}
|
{% else %}
|
||||||
|
@ -144,14 +165,33 @@
|
||||||
// hidden column so that we can add new records on top
|
// hidden column so that we can add new records on top
|
||||||
// regardless of whatever sorting is done. See orderFixed
|
// regardless of whatever sorting is done. See orderFixed
|
||||||
visible: false,
|
visible: false,
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||||
|
targets: [ 9 ]
|
||||||
|
{% else %}
|
||||||
targets: [ 8 ]
|
targets: [ 8 ]
|
||||||
|
{% endif %}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
className: "length-break",
|
className: "length-break",
|
||||||
targets: [ 4, 5 ]
|
targets: [ 4, 5 ]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||||
|
"orderFixed": [[9, 'asc']]
|
||||||
|
{% else %}
|
||||||
"orderFixed": [[8, 'asc']]
|
"orderFixed": [[8, 'asc']]
|
||||||
|
{% endif %}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
function show_record_changelog(record_name, record_type, e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.location.href = "/domain/{{domain.name}}/changelog/" + record_name + ".-" + record_type;
|
||||||
|
}
|
||||||
|
// handle changelog button
|
||||||
|
$(document.body).on("click", ".button_changelog", function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.location.href = "/domain/{{domain.name}}/changelog";
|
||||||
});
|
});
|
||||||
|
|
||||||
// handle delete button
|
// handle delete button
|
||||||
|
@ -243,7 +283,11 @@
|
||||||
|
|
||||||
// add new row
|
// add new row
|
||||||
var default_type = records_allow_edit[0]
|
var default_type = records_allow_edit[0]
|
||||||
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', 3600, '', '', '', '', '0']);
|
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||||
|
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', window.ttl_options[0][0], '', '', '', '', '', '0']);
|
||||||
|
{% else %}
|
||||||
|
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', window.ttl_options[0][0], '', '', '', '', '0']);
|
||||||
|
{% endif %}
|
||||||
editRow($("#tbl_records").DataTable(), nRow);
|
editRow($("#tbl_records").DataTable(), nRow);
|
||||||
document.getElementById("edit-row-focus").focus();
|
document.getElementById("edit-row-focus").focus();
|
||||||
nEditing = nRow;
|
nEditing = nRow;
|
||||||
|
|
116
powerdnsadmin/templates/domain_changelog.html
Normal file
116
powerdnsadmin/templates/domain_changelog.html
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}<title>{{ domain.name | pretty_domain_name }} - {{ SITE_NAME }}</title>{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_stat %}
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
{% if record_name and record_type %}
|
||||||
|
Record changelog: <b>{{ record_name}}   {{ record_type }}</b>
|
||||||
|
{% else %}
|
||||||
|
Domain changelog: <b>{{ domain.name | pretty_domain_name }}</b>
|
||||||
|
{% endif %}
|
||||||
|
</h1>
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||||
|
<li>Domain</li>
|
||||||
|
<li class="active">{{ domain.name | pretty_domain_name }}</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="box">
|
||||||
|
<div class="box-body">
|
||||||
|
<button type="button" class="btn btn-flat btn-primary pull-left button_show_records"
|
||||||
|
id="{{ domain.name }}">
|
||||||
|
Manage <i class="fa fa-arrow-left"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
|
||||||
|
<table id="tbl_changelog" class="table table-bordered table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Changed on</th>
|
||||||
|
<th>Changed by</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for applied_change in allHistoryChanges %}
|
||||||
|
|
||||||
|
<tr class="odd row_record" id="{{ domain.name }}">
|
||||||
|
<td id="changed_on" class="changed_on">
|
||||||
|
{{ allHistoryChanges[applied_change][0].history_entry.created_on }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{allHistoryChanges[applied_change][0].history_entry.created_by }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- Nested Table -->
|
||||||
|
<tr style='visibility:collapse'>
|
||||||
|
<td colspan="2">
|
||||||
|
<div class="content">
|
||||||
|
{% call applied_change_macro.applied_change_template(allHistoryChanges[applied_change]) %}
|
||||||
|
{% endcall %}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<!-- end nested table -->
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block extrascripts %}
|
||||||
|
<script>
|
||||||
|
// handle "show records" button
|
||||||
|
$(document.body).on("click", ".button_show_records", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
window.location.href = "/domain/{{domain.name}}";
|
||||||
|
});
|
||||||
|
|
||||||
|
var coll = document.getElementsByClassName("collapsible");
|
||||||
|
var i;
|
||||||
|
|
||||||
|
for (i = 0; i < coll.length; i++) {
|
||||||
|
coll[i].addEventListener("click", function () {
|
||||||
|
this.classList.toggle("active");
|
||||||
|
var content = this.nextElementSibling;
|
||||||
|
if (content.style.maxHeight) {
|
||||||
|
content.style.maxHeight = null;
|
||||||
|
} else {
|
||||||
|
content.style.maxHeight = content.scrollHeight + "px";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle click on history record
|
||||||
|
$(document.body).on("click", ".row_record", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var nextRow = $(this).next('tr')
|
||||||
|
if (nextRow.css("visibility") == "visible")
|
||||||
|
nextRow.css("visibility", "collapse")
|
||||||
|
else
|
||||||
|
nextRow.css("visibility", "visible")
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
var els = document.getElementsByClassName("changed_on");
|
||||||
|
for (var i = 0; i < els.length; i++) {
|
||||||
|
// els[i].innerHTML = moment.utc(els[i].innerHTML).local().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
els[i].innerHTML = moment.utc(els[i].innerHTML,'YYYY-MM-DD HH:mm:ss').local().format('YYYY-MM-DD HH:mm:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
129
powerdnsadmin/templates/domain_remove.html
Normal file
129
powerdnsadmin/templates/domain_remove.html
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
{% extends "base.html" %}
|
||||||
|
{% set active_page = "remove_domain" %}
|
||||||
|
{% block title %}<title>Remove Domain - {{ SITE_NAME }}</title>{% endblock %}
|
||||||
|
|
||||||
|
{% block dashboard_stat %}
|
||||||
|
<!-- Content Header (Page header) -->
|
||||||
|
<section class="content-header">
|
||||||
|
<h1>
|
||||||
|
Domain
|
||||||
|
<small>Remove existing</small>
|
||||||
|
</h1>
|
||||||
|
<ol class="breadcrumb">
|
||||||
|
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i>Home</a></li>
|
||||||
|
<li><a href="{{ url_for('dashboard.dashboard') }}">Domain</a></li>
|
||||||
|
<li class="active">Remove Domain</li>
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<section class="content">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="box box-primary">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">Remove domain</h3>
|
||||||
|
</div>
|
||||||
|
<!-- /.box-header -->
|
||||||
|
<!-- form start -->
|
||||||
|
<form role="form" method="post" action="{{ url_for('domain.remove') }}">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="box-body">
|
||||||
|
<select id=domainid class="form-control" style="width:15em;">
|
||||||
|
<option value="0">- Select Domain -</option>
|
||||||
|
{% for domain in domainss %}
|
||||||
|
<option value="{{ domain.id }}">{{ domain.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select><br />
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<!-- /.box-body -->
|
||||||
|
|
||||||
|
<div class="box-footer">
|
||||||
|
<button type="button" class="btn btn-flat btn-danger button_delete">Remove</button>
|
||||||
|
<button type="button" class="btn btn-flat btn-default"
|
||||||
|
onclick="window.location.href='{{ url_for('dashboard.dashboard') }}'">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<!-- /.box -->
|
||||||
|
</div>
|
||||||
|
<div class="col-md-8">
|
||||||
|
<div class="box box-primary">
|
||||||
|
<div class="box-header with-border">
|
||||||
|
<h3 class="box-title">Help with removing a new domain</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body">
|
||||||
|
<dl class="dl-horizontal">
|
||||||
|
<dt>Domain name</dt>
|
||||||
|
<dd>Select domain you wish to remove from DNS.</dd>
|
||||||
|
</dl>
|
||||||
|
<p>Find more details at <a href="https://docs.powerdns.com/md/">https://docs.powerdns.com/md/</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endblock %}
|
||||||
|
{% block extrascripts %}
|
||||||
|
<script>
|
||||||
|
// handle delete button
|
||||||
|
$(document.body).on("click", ".button_delete", function(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
if ( $("#domainid").val() == 0 ){
|
||||||
|
var modal = $("#modal_error");
|
||||||
|
modal.find('.modal-body p').text("Please select domain to remove.");
|
||||||
|
modal.modal('show');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var modal = $("#modal_delete");
|
||||||
|
var domain = $("#domainid option:selected").text();
|
||||||
|
var info = "Are you sure you want to delete " + domain + "?";
|
||||||
|
modal.find('.modal-body p').text(info);
|
||||||
|
modal.find('#button_delete_confirm').click(function () {
|
||||||
|
$.post($SCRIPT_ROOT + '/domain/remove' , {
|
||||||
|
'_csrf_token': '{{ csrf_token() }}',
|
||||||
|
'domainid': domain,
|
||||||
|
}, function () {
|
||||||
|
window.location.href = '{{ url_for('dashboard.dashboard') }}';
|
||||||
|
});
|
||||||
|
modal.modal('hide');
|
||||||
|
})
|
||||||
|
modal.modal('show');
|
||||||
|
|
||||||
|
$("#button_delete_cancel").unbind().one('click', function(e) {
|
||||||
|
modal.modal('hide');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block modals %}
|
||||||
|
<div class="modal fade modal-warning" id="modal_delete">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<button type="button" class="close" data-dismiss="modal"
|
||||||
|
aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
<h4 class="modal-title">Confirmation</h4>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-flat btn-default pull-left" id="button_delete_cancel"
|
||||||
|
data-dismiss="modal">Close</button>
|
||||||
|
<button type="button" class="btn btn-flat btn-danger" id="button_delete_confirm">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-content -->
|
||||||
|
</div>
|
||||||
|
<!-- /.modal-dialog -->
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
|
@ -19,7 +19,7 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<section class="content-header">
|
<section class="content-header">
|
||||||
<h1>
|
<h1>
|
||||||
Manage domain <small>{{ domain.name }}</small>
|
Manage domain <small>{{ domain.name | pretty_domain_name }}</small>
|
||||||
</h1>
|
</h1>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-2">
|
<div class="col-xs-2">
|
||||||
<p>Users on the right have access to manage the records in
|
<p>Users on the right have access to manage the records in
|
||||||
the {{ domain.name }} domain.</p>
|
the {{ domain.name | pretty_domain_name }} domain.</p>
|
||||||
<p>Click on users to move from between columns.</p>
|
<p>Click on users to move from between columns.</p>
|
||||||
<p>
|
<p>
|
||||||
Users in <font style="color: red;">red</font> are Administrators
|
Users in <font style="color: red;">red</font> are Administrators
|
||||||
|
@ -94,7 +94,7 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select><br />
|
</select><br />
|
||||||
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
|
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
|
||||||
<i class="fa fa-check"></i> Change account for {{ domain.name }}
|
<i class="fa fa-check"></i> Change account for {{ domain.name | pretty_domain_name }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -113,9 +113,11 @@
|
||||||
<p><input type="checkbox" id="{{ domain.name }}" class="auto_ptr_toggle"
|
<p><input type="checkbox" id="{{ domain.name }}" class="auto_ptr_toggle"
|
||||||
{% for setting in domain.settings %}{% if setting.setting=='auto_ptr' and setting.value=='True' %}checked{% endif %}{% endfor %}
|
{% for setting in domain.settings %}{% if setting.setting=='auto_ptr' and setting.value=='True' %}checked{% endif %}{% endfor %}
|
||||||
{% if SETTING.get('auto_ptr') %}disabled="True" {% endif %}>
|
{% if SETTING.get('auto_ptr') %}disabled="True" {% endif %}>
|
||||||
Allow automatic reverse pointer creation on record updates?{% if
|
Allow automatic reverse pointer creation on record updates?
|
||||||
SETTING.get('auto_ptr') %}</br><code>Auto-ptr is enabled globally on the PDA
|
{% if SETTING.get('auto_ptr') %}
|
||||||
system!</code>{% endif %}</p>
|
<br/><code>Auto-ptr is enabled globally on the PDA system!</code>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -171,7 +173,7 @@
|
||||||
placeholder="Enter valid master ip addresses (separated by commas)">
|
placeholder="Enter valid master ip addresses (separated by commas)">
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-flat btn-primary" id="change_type">
|
<button type="submit" class="btn btn-flat btn-primary" id="change_type">
|
||||||
<i class="fa fa-check"></i> Change type for {{ domain.name }}
|
<i class="fa fa-check"></i> Change type for {{ domain.name | pretty_domain_name }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -214,7 +216,7 @@
|
||||||
<option>OFF</option>
|
<option>OFF</option>
|
||||||
</select><br />
|
</select><br />
|
||||||
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
|
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
|
||||||
<i class="fa fa-check"></i> Change SOA-EDIT-API setting for {{ domain.name }}
|
<i class="fa fa-check"></i> Change SOA-EDIT-API setting for {{ domain.name | pretty_domain_name }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -233,7 +235,7 @@
|
||||||
reverted.</p>
|
reverted.</p>
|
||||||
<button type="button" class="btn btn-flat btn-danger pull-left delete_domain"
|
<button type="button" class="btn btn-flat btn-danger pull-left delete_domain"
|
||||||
id="{{ domain.name }}">
|
id="{{ domain.name }}">
|
||||||
<i class="fa fa-trash"></i> DELETE DOMAIN {{ domain.name }}
|
<i class="fa fa-trash"></i> DELETE DOMAIN {{ domain.name | pretty_domain_name }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,40 +1,50 @@
|
||||||
{% extends "base.html" %}
|
<!DOCTYPE html>
|
||||||
{% block title %}<title>Email verification - {{ SITE_NAME }}</title>{% endblock %}
|
<html>
|
||||||
|
|
||||||
{% block dashboard_stat %}
|
<head>
|
||||||
{% endblock %}
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Email confirmation - {{ SITE_NAME }}</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||||
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
{% assets "css_login" -%}
|
||||||
|
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||||
|
{%- endassets %}
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
|
||||||
{% block content %}
|
<body class="hold-transition register-page">
|
||||||
<!-- Main content -->
|
<section class="content">
|
||||||
<section class="content">
|
<div class="error-page">
|
||||||
<div class="error-page">
|
<div class="error-content">
|
||||||
<div class="error-content">
|
{% if status == 1 %}
|
||||||
{% if status == 1 %}
|
<h3>
|
||||||
<h3>
|
<i class="fa fa-thumbs-o-up text-success"></i> Email verification successful!
|
||||||
<i class="fa fa-thumbs-o-up text-success"></i> Email verification successful!
|
</h3>
|
||||||
</h3>
|
<p>
|
||||||
<p>
|
You have confirmed your account. <a href="{{ url_for('index.login') }}">Click here</a> to login.
|
||||||
You have confirmed your account. <a href="{{ url_for('index.login') }}">Click here</a> to login.
|
</p>
|
||||||
</p>
|
{% elif status == 2 %}
|
||||||
{% elif status == 2 %}
|
<h3>
|
||||||
<h3>
|
<i class="fa fa-hand-stop-o text-info"></i> Already verified!
|
||||||
<i class="fa fa-hand-stop-o text-info"></i> Already verified!
|
</h3>
|
||||||
</h3>
|
<p>
|
||||||
<p>
|
You have confirmed your account already. <a href="{{ url_for('index.login') }}">Click here</a> to login.
|
||||||
You have confirmed your account already. <a href="{{ url_for('index.login') }}">Click here</a> to login.
|
</p>
|
||||||
</p>
|
{% else %}
|
||||||
{% else %}
|
<h3>
|
||||||
<h3>
|
<i class="fa fa-warning text-yellow"></i> Email verification failed!
|
||||||
<i class="fa fa-warning text-yellow"></i> Email verification failed!
|
</h3>
|
||||||
</h3>
|
<p>
|
||||||
<p>
|
The confirmation link is invalid or has expired. <a href="{{ url_for('index.resend_confirmation_email') }}">Click here</a> if you want to resend a new link.
|
||||||
The confirmation link is invalid or has expired. <a href="{{ url_for('index.resend_confirmation_email') }}">Click here</a> if you want to resend a new link.
|
</p>
|
||||||
</p>
|
{% endif %}
|
||||||
{% endif %}
|
</div>
|
||||||
|
<!-- /.error-content -->
|
||||||
</div>
|
</div>
|
||||||
<!-- /.error-content -->
|
<!-- /.error-page -->
|
||||||
</div>
|
</section>
|
||||||
<!-- /.error-page -->
|
</body>
|
||||||
</section>
|
|
||||||
<!-- /.content -->
|
</html>
|
||||||
{% endblock %}
|
|
||||||
|
|
|
@ -5,12 +5,15 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title>Log In - {{ SITE_NAME }}</title>
|
<title>Log In - {{ SITE_NAME }}</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||||
<!-- Tell the browser to be responsive to screen width -->
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
{% assets "css_login" -%}
|
{% assets "css_login" -%}
|
||||||
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||||
{%- endassets %}
|
{%- endassets %}
|
||||||
|
{% if SETTING.get('custom_css') %}
|
||||||
|
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
||||||
|
{% endif %}
|
||||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||||
<!--[if lt IE 9]>
|
<!--[if lt IE 9]>
|
||||||
|
@ -45,9 +48,11 @@
|
||||||
data-error="Please input your password" required {% if password %}value="{{ password }}" {% endif %}>
|
data-error="Please input your password" required {% if password %}value="{{ password }}" {% endif %}>
|
||||||
<span class="help-block with-errors"></span>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
|
{% if SETTING.get('otp_field_enabled') %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
|
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
|
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<select class="form-control" name="auth_method">
|
<select class="form-control" name="auth_method">
|
||||||
|
|
|
@ -37,7 +37,7 @@
|
||||||
<h1>We’ll be back soon!</h1>
|
<h1>We’ll be back soon!</h1>
|
||||||
<div>
|
<div>
|
||||||
<p>Sorry for the inconvenience but we’re performing some maintenance at the moment. Please contact the System
|
<p>Sorry for the inconvenience but we’re performing some maintenance at the moment. Please contact the System
|
||||||
Administrator if you need more information</a>, otherwise we’ll be back online shortly!</p>
|
Administrator if you need more information, otherwise we’ll be back online shortly!</p>
|
||||||
<p>— Team</p>
|
<p>— Team</p>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
|
@ -5,6 +5,7 @@
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
<title>Register - {{ SITE_NAME }}</title>
|
<title>Register - {{ SITE_NAME }}</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||||
<!-- Tell the browser to be responsive to screen width -->
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
{% assets "css_login" -%}
|
{% assets "css_login" -%}
|
||||||
|
|
90
powerdnsadmin/templates/register_otp.html
Executable file
90
powerdnsadmin/templates/register_otp.html
Executable file
|
@ -0,0 +1,90 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Welcome - {{ SITE_NAME }}</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||||
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
{% assets "css_login" -%}
|
||||||
|
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||||
|
{%- endassets %}
|
||||||
|
{% if SETTING.get('custom_css') %}
|
||||||
|
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="hold-transition register-page">
|
||||||
|
<div class="register-box">
|
||||||
|
<div class="register-logo">
|
||||||
|
<a><b>PowerDNS</b>-Admin</a>
|
||||||
|
</div>
|
||||||
|
<div class="register-box-body">
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger alert-dismissible">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
Welcome, {{user.firstname}}! <br />
|
||||||
|
You will need a Token on login. <br />
|
||||||
|
Your QR code is:
|
||||||
|
<div id="token_information">
|
||||||
|
{% if qrcode_image == None %}
|
||||||
|
<p><img id="qrcode" src="{{ url_for('user.qrcode') }}"></p>
|
||||||
|
{% else %}
|
||||||
|
<p><img id="qrcode" src="data:image/svg+xml;utf8;base64, {{qrcode_image}}"></p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
Your secret key is: <br />
|
||||||
|
<form>
|
||||||
|
<input type=text id="otp_secret" value={{user.otp_secret}} readonly>
|
||||||
|
<button type=button style="position:relative; right:28px" onclick="copy_otp_secret_to_clipboard()"> <i class="fa fa-clipboard"></i> </button>
|
||||||
|
<br /><font color="red" id="copy_tooltip" style="visibility:collapse">Copied.</font>
|
||||||
|
</form>
|
||||||
|
</p>
|
||||||
|
You can use Google Authenticator (<a target="_blank"
|
||||||
|
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
|
||||||
|
- <a target="_blank"
|
||||||
|
href="https://apps.apple.com/us/app/google-authenticator/id388497605">iOS</a>)
|
||||||
|
<br />
|
||||||
|
or FreeOTP (<a target="_blank"
|
||||||
|
href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=en">Android</a>
|
||||||
|
- <a target="_blank"
|
||||||
|
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
|
||||||
|
on your smartphone <br /> to scan the QR code or type the secret key.
|
||||||
|
<br /> <br />
|
||||||
|
<font color="red"><strong><i>Make sure only you can see this QR Code <br />
|
||||||
|
and secret key, and nobody can capture them.</i></strong></font>
|
||||||
|
</div>
|
||||||
|
</br>
|
||||||
|
Please input your OTP token to continue, to ensure the seed has been scanned correctly.
|
||||||
|
<form action="" method="post" data-toggle="validator">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" placeholder="OTP Token" name="otptoken"
|
||||||
|
data-error="Please input your OTP token" required>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<button type="submit" class="btn btn-flat btn-primary btn-block">Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="login-box-footer">
|
||||||
|
<center>
|
||||||
|
<p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
{% assets "js_login" -%}
|
||||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
|
{%- endassets %}
|
||||||
|
{% assets "js_validation" -%}
|
||||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
|
{%- endassets %}
|
||||||
|
</html>
|
|
@ -1,30 +1,52 @@
|
||||||
{% extends "base.html" %}
|
<!DOCTYPE html>
|
||||||
{% block title %}<title>Resend confirmation email - {{ SITE_NAME }}</title>{% endblock %}
|
<html>
|
||||||
|
|
||||||
{% block dashboard_stat %}
|
<head>
|
||||||
{% endblock %}
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Resend a confirmation email - {{ SITE_NAME }}</title>
|
||||||
|
<link rel="icon" href="/static/img/favicon.png">
|
||||||
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
{% assets "css_login" -%}
|
||||||
|
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||||
|
{%- endassets %}
|
||||||
|
<![endif]-->
|
||||||
|
</head>
|
||||||
|
|
||||||
{% block content %}
|
<body class="hold-transition register-page">
|
||||||
<!-- Main content -->
|
<div class="register-box">
|
||||||
<section class="content">
|
<div class="register-logo">
|
||||||
<div class="error-page">
|
<a href="{{ url_for('index.index') }}"><b>PowerDNS</b>-Admin</a>
|
||||||
<div class="error-content">
|
</div>
|
||||||
<h3>
|
<div class="register-box-body">
|
||||||
<i class="fa fa-hand-o-right text-info"></i> Resend a confirmation email
|
{% if error %}
|
||||||
</h3>
|
<div class="alert alert-danger alert-dismissible">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
<p>
|
<p>
|
||||||
Enter your email address to get new account confirmation link.
|
Enter your email address to get account confirmation link.
|
||||||
</p>
|
</p>
|
||||||
<form class="search-form" method="post">
|
<form method="post" data-toggle="validator">
|
||||||
<div class="input-group">
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
<div class="form-group has-feedback">
|
||||||
<input type="text" name="email" class="form-control" placeholder="Email address" data-error="Please input your email" required>
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
<div class="input-group-btn">
|
<input type="email" name="email" class="form-control" placeholder="Email address" data-error="Please input your email" required>
|
||||||
<button type="submit" name="submit" class="btn btn-success btn-flat"><i class="fa fa-mail-reply"></i>
|
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
|
||||||
</button>
|
<span class="help-block with-errors"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="row">
|
||||||
<!-- /.input-group -->
|
<div class="col-xs-4 pull-left">
|
||||||
|
<button type="button" class="btn btn-flat btn-block" id="button_back">Back</button>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4 pull-right">
|
||||||
|
<button type="submit" class="btn btn-flat btn-primary btn-block">Resend</button>
|
||||||
|
</div>
|
||||||
|
<!-- /.col -->
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
<p>
|
<p>
|
||||||
{% if status == 0 %}
|
{% if status == 0 %}
|
||||||
<font color="red">Email not found!</font>
|
<font color="red">Email not found!</font>
|
||||||
|
@ -34,11 +56,31 @@
|
||||||
<font color="green">Confirmation email sent!</font>
|
<font color="green">Confirmation email sent!</font>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- /.error-content -->
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<!-- /.error-page -->
|
<!-- /.form-box -->
|
||||||
</section>
|
<div class="login-box-footer">
|
||||||
<!-- /.content -->
|
<center>
|
||||||
{% endblock %}
|
<p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- /.login-box -->
|
||||||
|
|
||||||
|
{% assets "js_login" -%}
|
||||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
|
{%- endassets %}
|
||||||
|
{% assets "js_validation" -%}
|
||||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
|
{%- endassets %}
|
||||||
|
<script>
|
||||||
|
$(function () {
|
||||||
|
$('#button_back').click(function () {
|
||||||
|
window.location.href = '{{ url_for('index.login') }}';
|
||||||
|
})
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
|
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="email">E-mail</label> <input type="text" class="form-control"
|
<label for="email">E-mail</label> <input type="email" class="form-control"
|
||||||
name="email" id="email" placeholder="{{ current_user.email }}"
|
name="email" id="email" placeholder="{{ current_user.email }}"
|
||||||
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
|
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
|
||||||
</div>{% if session['authentication_type'] == 'LOCAL' %}
|
</div>{% if session['authentication_type'] == 'LOCAL' %}
|
||||||
|
@ -93,6 +93,14 @@
|
||||||
{% if current_user.otp_secret %}
|
{% if current_user.otp_secret %}
|
||||||
<div id="token_information">
|
<div id="token_information">
|
||||||
<p><img id="qrcode" src="{{ url_for('user.qrcode') }}"></p>
|
<p><img id="qrcode" src="{{ url_for('user.qrcode') }}"></p>
|
||||||
|
<div style="position: relative; left: 15px">
|
||||||
|
Your secret key is: <br />
|
||||||
|
<form>
|
||||||
|
<input type=text id="otp_secret" value={{current_user.otp_secret}} readonly>
|
||||||
|
<button type=button style="position:relative; right:28px" onclick="copy_otp_secret_to_clipboard()"> <i class="fa fa-clipboard"></i> </button>
|
||||||
|
<br /><font color="red" id="copy_tooltip" style="visibility:collapse">Copied.</font>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
You can use Google Authenticator (<a target="_blank"
|
You can use Google Authenticator (<a target="_blank"
|
||||||
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
|
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
|
||||||
- <a target="_blank"
|
- <a target="_blank"
|
||||||
|
@ -103,8 +111,8 @@
|
||||||
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
|
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
|
||||||
on your smartphone to scan the QR code.
|
on your smartphone to scan the QR code.
|
||||||
<br />
|
<br />
|
||||||
<font color="red"><strong><i>Make sure only you can see this QR Code and
|
<font color="red"><strong><i>Make sure only you can see this QR Code and secret key and
|
||||||
nobody can capture it.</i></strong></font>
|
nobody can capture them.</i></strong></font>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,29 +1,30 @@
|
||||||
Flask==1.1.1
|
Flask==1.1.2
|
||||||
Flask-Assets==0.12
|
Flask-Assets==2.0
|
||||||
Flask-Login==0.4.1
|
Flask-Login==0.5.0
|
||||||
Flask-SQLAlchemy==2.4.1
|
Flask-SQLAlchemy==2.4.4
|
||||||
Flask-Migrate==2.5.2
|
Flask-Migrate==2.5.3
|
||||||
SQLAlchemy==1.3.11
|
SQLAlchemy==1.3.19
|
||||||
mysqlclient==1.4.6
|
mysqlclient==2.0.1
|
||||||
configobj==5.0.6
|
configobj==5.0.6
|
||||||
bcrypt==3.1.4
|
bcrypt>=3.1.7
|
||||||
requests==2.20.0
|
requests==2.24.0
|
||||||
python-ldap==3.1.0
|
python-ldap==3.4.0
|
||||||
pyotp==2.2.6
|
pyotp==2.4.0
|
||||||
qrcode==6.0
|
qrcode==6.1
|
||||||
dnspython==1.15.0
|
dnspython>=1.16.0
|
||||||
gunicorn==20.0.4
|
gunicorn==20.0.4
|
||||||
python3-saml
|
python3-saml
|
||||||
pyOpenSSL>=0.15
|
pyOpenSSL==19.1.0
|
||||||
pytz>=2017.3
|
pytz==2020.1
|
||||||
cssmin==0.2.0
|
cssmin==0.2.0
|
||||||
jsmin==2.2.2
|
jsmin==3.0.0
|
||||||
Authlib==0.10
|
Authlib==0.15
|
||||||
Flask-SeaSurf==0.2.2
|
Flask-SeaSurf==0.2.2
|
||||||
bravado-core==5.13.1
|
bravado-core==5.17.0
|
||||||
lima==0.5
|
lima==0.5
|
||||||
pytest==5.0.1
|
pytest==6.1.1
|
||||||
pytimeparse==1.1.8
|
pytimeparse==1.1.8
|
||||||
PyYAML==5.1.1
|
PyYAML==5.4
|
||||||
Flask-SSLify==0.1.5
|
Flask-SSLify==0.1.5
|
||||||
Flask-Mail==0.9.1
|
Flask-Mail==0.9.1
|
||||||
|
flask-session==0.3.2
|
||||||
|
|
|
@ -38,6 +38,16 @@ def load_data(setting_name, *args, **kwargs):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_admin_user():
|
||||||
|
return app.config.get('TEST_ADMIN_USER')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def test_user():
|
||||||
|
return app.config.get('TEST_USER')
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def basic_auth_admin_headers():
|
def basic_auth_admin_headers():
|
||||||
test_admin_user = app.config.get('TEST_ADMIN_USER')
|
test_admin_user = app.config.get('TEST_ADMIN_USER')
|
||||||
|
@ -284,3 +294,29 @@ def create_apikey_headers(passw):
|
||||||
user_pass_base64 = b64encode(passw.encode('utf-8'))
|
user_pass_base64 = b64encode(passw.encode('utf-8'))
|
||||||
headers = {"X-API-KEY": "{0}".format(user_pass_base64.decode('utf-8'))}
|
headers = {"X-API-KEY": "{0}".format(user_pass_base64.decode('utf-8'))}
|
||||||
return headers
|
return headers
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def account_data():
|
||||||
|
data = {
|
||||||
|
"name": "test1",
|
||||||
|
"description": "test1 account",
|
||||||
|
"contact": "test1 contact",
|
||||||
|
"mail": "test1@example.com",
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def user1_data():
|
||||||
|
data = {
|
||||||
|
"username": "testuser1",
|
||||||
|
"plain_text_password": "ChangeMePlease",
|
||||||
|
"firstname": "firstname1",
|
||||||
|
"lastname": "lastname1",
|
||||||
|
"email": "testuser1@example.com",
|
||||||
|
"otp_secret": "",
|
||||||
|
"confirmed": False,
|
||||||
|
"role_name": "User",
|
||||||
|
}
|
||||||
|
return data
|
||||||
|
|
54
tests/integration/api/management/__init__.py
Normal file
54
tests/integration/api/management/__init__.py
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
|
||||||
|
|
||||||
|
class IntegrationApiManagement(object):
|
||||||
|
|
||||||
|
def get_account(self, account_name, status_code=200):
|
||||||
|
res = self.client.get(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_name),
|
||||||
|
headers=self.basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
if isinstance(status_code, (tuple, list)):
|
||||||
|
assert res.status_code in status_code
|
||||||
|
elif status_code:
|
||||||
|
assert res.status_code == status_code
|
||||||
|
if res.status_code == 200:
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert len(data) == 1
|
||||||
|
return data[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_account(self, cmpdata, data=None):
|
||||||
|
data = self.get_account(cmpdata["name"])
|
||||||
|
for key, value in cmpdata.items():
|
||||||
|
assert data[key] == value
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_user(self, username, status_code=200):
|
||||||
|
res = self.client.get(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(username),
|
||||||
|
headers=self.basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
if isinstance(status_code, (tuple, list)):
|
||||||
|
assert res.status_code in status_code
|
||||||
|
elif status_code:
|
||||||
|
assert res.status_code == status_code
|
||||||
|
assert res.status_code == status_code
|
||||||
|
if status_code == 200:
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert len(data) == 1
|
||||||
|
return data[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def check_user(self, cmpdata, data=None):
|
||||||
|
if data is None:
|
||||||
|
data = self.get_user(cmpdata["username"])
|
||||||
|
for key, value in data.items():
|
||||||
|
if key in ('username', 'firstname', 'lastname', 'email'):
|
||||||
|
assert cmpdata[key] == value
|
||||||
|
elif key == 'role':
|
||||||
|
assert data[key]['name'] == cmpdata['role_name']
|
||||||
|
else:
|
||||||
|
assert key in ("id",)
|
||||||
|
return data
|
367
tests/integration/api/management/test_admin_user.py
Normal file
367
tests/integration/api/management/test_admin_user.py
Normal file
|
@ -0,0 +1,367 @@
|
||||||
|
|
||||||
|
import json
|
||||||
|
from tests.fixtures import ( # noqa: F401
|
||||||
|
client, initial_data, basic_auth_admin_headers,
|
||||||
|
test_admin_user, test_user, account_data, user1_data,
|
||||||
|
)
|
||||||
|
from . import IntegrationApiManagement
|
||||||
|
|
||||||
|
|
||||||
|
class TestIntegrationApiManagementAdminUser(IntegrationApiManagement):
|
||||||
|
|
||||||
|
def test_accounts_empty_get(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
basic_auth_admin_headers): # noqa: F811
|
||||||
|
res = client.get("/api/v1/pdnsadmin/accounts",
|
||||||
|
headers=basic_auth_admin_headers)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert data == []
|
||||||
|
|
||||||
|
def test_users_empty_get(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
test_admin_user, test_user, # noqa: F811
|
||||||
|
basic_auth_admin_headers): # noqa: F811
|
||||||
|
res = client.get("/api/v1/pdnsadmin/users",
|
||||||
|
headers=basic_auth_admin_headers)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
# Initally contains 2 records
|
||||||
|
assert len(data) == 2
|
||||||
|
for user in data:
|
||||||
|
assert user["username"] in (test_admin_user, test_user)
|
||||||
|
|
||||||
|
def test_accounts(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
account_data, # noqa: F811
|
||||||
|
basic_auth_admin_headers): # noqa: F811
|
||||||
|
account_name = account_data["name"]
|
||||||
|
self.client = client
|
||||||
|
self.basic_auth_admin_headers = basic_auth_admin_headers
|
||||||
|
|
||||||
|
# Create account
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/accounts",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
|
||||||
|
# Check account
|
||||||
|
data = self.check_account(account_data)
|
||||||
|
account_id = data["id"]
|
||||||
|
|
||||||
|
updated = account_data.copy()
|
||||||
|
# Update and check values
|
||||||
|
for upd_key in ["description", "contact", "mail"]:
|
||||||
|
upd_value = "upd-{}".format(account_data[upd_key])
|
||||||
|
|
||||||
|
# Update
|
||||||
|
data = {"name": account_name, upd_key: upd_value}
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
updated[upd_key] = upd_value
|
||||||
|
|
||||||
|
# Check
|
||||||
|
data = self.check_account(updated)
|
||||||
|
|
||||||
|
# Update to defaults
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Check account
|
||||||
|
res = client.get(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_name),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert len(data) == 1
|
||||||
|
data = data[0]
|
||||||
|
account_id = data["id"]
|
||||||
|
for key, value in account_data.items():
|
||||||
|
assert data[key] == value
|
||||||
|
|
||||||
|
# Cleanup (delete account)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Get non-existing account (should fail)
|
||||||
|
data = self.get_account(account_name, status_code=404)
|
||||||
|
|
||||||
|
# Update non-existing account (should fail)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Delete non-existing account (should fail)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
def test_users(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
user1_data, # noqa: F811
|
||||||
|
basic_auth_admin_headers): # noqa: F811
|
||||||
|
user1name = user1_data["username"]
|
||||||
|
self.client = client
|
||||||
|
self.basic_auth_admin_headers = basic_auth_admin_headers
|
||||||
|
|
||||||
|
# Create user (user1)
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/users",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
assert len(data) == 1
|
||||||
|
|
||||||
|
# Check user
|
||||||
|
user1 = self.check_user(user1_data, data[0])
|
||||||
|
user1_id = user1["id"]
|
||||||
|
|
||||||
|
updated = user1_data.copy()
|
||||||
|
# Update and check values
|
||||||
|
for upd_key in ["firstname", "lastname", "email"]:
|
||||||
|
upd_value = "upd-{}".format(user1_data[upd_key])
|
||||||
|
|
||||||
|
# Update
|
||||||
|
data = {"username": user1name, upd_key: upd_value}
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
updated[upd_key] = upd_value
|
||||||
|
|
||||||
|
# Check
|
||||||
|
data = self.check_user(updated)
|
||||||
|
|
||||||
|
# Update to defaults
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Check user
|
||||||
|
self.check_user(user1_data)
|
||||||
|
|
||||||
|
# Cleanup (delete user)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Get non-existing user (should fail)
|
||||||
|
data = self.get_user(user1name, status_code=404)
|
||||||
|
|
||||||
|
# Update non-existing user (should fail)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Delete non-existing user (should fail)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
def test_account_users(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
test_user, account_data, user1_data, # noqa: F811
|
||||||
|
basic_auth_admin_headers): # noqa: F811
|
||||||
|
self.client = client
|
||||||
|
self.basic_auth_admin_headers = basic_auth_admin_headers
|
||||||
|
test_user_id = self.get_user(test_user)["id"]
|
||||||
|
|
||||||
|
# Create account
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/accounts",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
|
||||||
|
# Check account
|
||||||
|
data = self.check_account(account_data)
|
||||||
|
account_id = data["id"]
|
||||||
|
|
||||||
|
# Create user1
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/users",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
assert len(data) == 1
|
||||||
|
|
||||||
|
# Check user
|
||||||
|
user1 = self.check_user(user1_data, data[0])
|
||||||
|
user1_id = user1["id"]
|
||||||
|
|
||||||
|
# Assert test account has no users
|
||||||
|
res = client.get(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}".format(account_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert data == []
|
||||||
|
|
||||||
|
# Assert unlinking an unlinked account fails
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Link user to account
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Check user is linked to account
|
||||||
|
res = client.get(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}".format(account_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert len(data) == 1
|
||||||
|
self.check_user(user1_data, data[0])
|
||||||
|
|
||||||
|
# Unlink user from account
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Check user is unlinked from account
|
||||||
|
res = client.get(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}".format(account_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert data == []
|
||||||
|
|
||||||
|
# Unlink unlinked user from account (should fail)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Cleanup (delete user)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Link non-existing user to account (should fail)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Unlink non-exiting user from account (should fail)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Cleanup (delete account)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# List users in non-existing account (should fail)
|
||||||
|
res = client.get(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}".format(account_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
||||||
|
|
||||||
|
# Link existing user to non-existing account (should fail)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, test_user_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 404
|
252
tests/integration/api/management/test_user.py
Normal file
252
tests/integration/api/management/test_user.py
Normal file
|
@ -0,0 +1,252 @@
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from tests.fixtures import ( # noqa: F401
|
||||||
|
client, initial_data, basic_auth_admin_headers, basic_auth_user_headers,
|
||||||
|
test_admin_user, test_user, account_data, user1_data,
|
||||||
|
)
|
||||||
|
from . import IntegrationApiManagement
|
||||||
|
|
||||||
|
|
||||||
|
class TestIntegrationApiManagementUser(IntegrationApiManagement):
|
||||||
|
|
||||||
|
def test_accounts_empty_get(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
basic_auth_user_headers): # noqa: F811
|
||||||
|
res = client.get("/api/v1/pdnsadmin/accounts",
|
||||||
|
headers=basic_auth_user_headers)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
def test_users_empty_get(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
test_admin_user, test_user, # noqa: F811
|
||||||
|
basic_auth_user_headers): # noqa: F811
|
||||||
|
res = client.get("/api/v1/pdnsadmin/users",
|
||||||
|
headers=basic_auth_user_headers)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
def test_self_get(
|
||||||
|
self, initial_data, client, test_user, # noqa: F811
|
||||||
|
basic_auth_user_headers): # noqa: F811
|
||||||
|
self.user = None
|
||||||
|
res = client.get("/api/v1/pdnsadmin/users/{}".format(test_user),
|
||||||
|
headers=basic_auth_user_headers)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert len(data) == 1, data
|
||||||
|
self.user = data
|
||||||
|
|
||||||
|
def test_accounts(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
account_data, # noqa: F811
|
||||||
|
basic_auth_admin_headers, basic_auth_user_headers): # noqa: F811
|
||||||
|
self.client = client
|
||||||
|
self.basic_auth_admin_headers = basic_auth_admin_headers
|
||||||
|
|
||||||
|
# Create account (should fail)
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/accounts",
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Create account (as admin)
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/accounts",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
|
||||||
|
# Check account
|
||||||
|
data = self.check_account(account_data)
|
||||||
|
account_id = data["id"]
|
||||||
|
|
||||||
|
# Update to defaults (should fail)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Delete account (should fail)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Cleanup (delete account as admin)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
def test_users(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
user1_data, # noqa: F811
|
||||||
|
basic_auth_admin_headers, basic_auth_user_headers): # noqa: F811
|
||||||
|
self.client = client
|
||||||
|
self.basic_auth_admin_headers = basic_auth_admin_headers
|
||||||
|
|
||||||
|
# Create user1 (should fail)
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/users",
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Create user1 (as admin)
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/users",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
assert len(data) == 1
|
||||||
|
|
||||||
|
# Check user
|
||||||
|
user1 = self.check_user(user1_data, data[0])
|
||||||
|
user1_id = user1["id"]
|
||||||
|
|
||||||
|
# Update to defaults (should fail)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Delete user (should fail)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Cleanup (delete user as admin)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
def test_account_users(
|
||||||
|
self, client, initial_data, # noqa: F811
|
||||||
|
account_data, user1_data, # noqa: F811
|
||||||
|
basic_auth_admin_headers, basic_auth_user_headers): # noqa: F811
|
||||||
|
self.client = client
|
||||||
|
self.basic_auth_admin_headers = basic_auth_admin_headers
|
||||||
|
|
||||||
|
# Create account
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/accounts",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
|
||||||
|
# Check account
|
||||||
|
data = self.check_account(account_data)
|
||||||
|
account_id = data["id"]
|
||||||
|
|
||||||
|
# Create user1
|
||||||
|
res = client.post(
|
||||||
|
"/api/v1/pdnsadmin/users",
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 201
|
||||||
|
assert len(data) == 1
|
||||||
|
|
||||||
|
# Check user
|
||||||
|
user1 = self.check_user(user1_data, data[0])
|
||||||
|
user1_id = user1["id"]
|
||||||
|
|
||||||
|
# Assert test account has no users
|
||||||
|
res = client.get(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}".format(account_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
data = res.get_json(force=True)
|
||||||
|
assert res.status_code == 200
|
||||||
|
assert data == []
|
||||||
|
|
||||||
|
# Link user to account (as user, should fail)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Link user to account (as admin)
|
||||||
|
res = client.put(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Unlink user from account (as user, should fail)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_user_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
# Unlink user from account (as admin)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/users/{}/{}".format(
|
||||||
|
account_id, user1_id),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Cleanup (delete user)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/users/{}".format(user1_id),
|
||||||
|
data=json.dumps(user1_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
||||||
|
|
||||||
|
# Cleanup (delete account)
|
||||||
|
res = client.delete(
|
||||||
|
"/api/v1/pdnsadmin/accounts/{}".format(account_id),
|
||||||
|
data=json.dumps(account_data),
|
||||||
|
headers=basic_auth_admin_headers,
|
||||||
|
content_type="application/json",
|
||||||
|
)
|
||||||
|
assert res.status_code == 204
|
31
update_accounts.py
Normal file
31
update_accounts.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
####################################################################################################################################
|
||||||
|
# A CLI Script to update list of accounts. Can be useful for people who want to execute updates from a cronjob
|
||||||
|
#
|
||||||
|
# Tip:
|
||||||
|
# When running from a cron, use flock (you might need to install it) to be sure only one process is running a time. eg:
|
||||||
|
# */5 * * * * flock -xn "/tmp/pdns-update-zones.lock" python /var/www/html/apps/poweradmin/update_accounts.py >/dev/null 2>&1
|
||||||
|
#
|
||||||
|
##############################################################
|
||||||
|
|
||||||
|
### Imports
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from powerdnsadmin import create_app
|
||||||
|
from powerdnsadmin.models.account import Account
|
||||||
|
from powerdnsadmin.models.setting import Setting
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
app.logger.setLevel(logging.INFO)
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
status = Setting().get('bg_domain_updates')
|
||||||
|
|
||||||
|
### Check if bg_domain_updates is set to true
|
||||||
|
if not status:
|
||||||
|
app.logger.error('Please turn on "bg_domain_updates" setting to run this job.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
Account().update()
|
|
@ -29,6 +29,6 @@ with app.app_context():
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
### Start the update process
|
### Start the update process
|
||||||
app.logger.info('Update zones from nameserver API')
|
app.logger.info('Update domains from nameserver API')
|
||||||
|
|
||||||
d = Domain().update()
|
Domain().update()
|
||||||
|
|
54
yarn.lock
54
yarn.lock
|
@ -102,9 +102,10 @@ base64-js@^1.0.2:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
|
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.0.tgz#cab1e6118f051095e58b5281aea8c1cd22bfc0e3"
|
||||||
|
|
||||||
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
|
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.11.9:
|
||||||
version "4.11.8"
|
version "4.12.0"
|
||||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
|
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||||
|
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||||
|
|
||||||
bootstrap-colorpicker@^2.5.3:
|
bootstrap-colorpicker@^2.5.3:
|
||||||
version "2.5.3"
|
version "2.5.3"
|
||||||
|
@ -151,9 +152,10 @@ brace-expansion@^1.1.7:
|
||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
|
|
||||||
brorand@^1.0.1:
|
brorand@^1.0.1, brorand@^1.1.0:
|
||||||
version "1.1.0"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||||
|
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
|
||||||
|
|
||||||
browser-pack@^6.0.1:
|
browser-pack@^6.0.1:
|
||||||
version "6.1.0"
|
version "6.1.0"
|
||||||
|
@ -488,16 +490,17 @@ duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
|
||||||
readable-stream "^2.0.2"
|
readable-stream "^2.0.2"
|
||||||
|
|
||||||
elliptic@^6.0.0:
|
elliptic@^6.0.0:
|
||||||
version "6.4.0"
|
version "6.5.4"
|
||||||
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
|
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
|
||||||
|
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
bn.js "^4.4.0"
|
bn.js "^4.11.9"
|
||||||
brorand "^1.0.1"
|
brorand "^1.1.0"
|
||||||
hash.js "^1.0.0"
|
hash.js "^1.0.0"
|
||||||
hmac-drbg "^1.0.0"
|
hmac-drbg "^1.0.1"
|
||||||
inherits "^2.0.1"
|
inherits "^2.0.4"
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.1"
|
||||||
minimalistic-crypto-utils "^1.0.0"
|
minimalistic-crypto-utils "^1.0.1"
|
||||||
|
|
||||||
eve-raphael@0.5.0:
|
eve-raphael@0.5.0:
|
||||||
version "0.5.0"
|
version "0.5.0"
|
||||||
|
@ -565,15 +568,17 @@ hash-base@^3.0.0:
|
||||||
safe-buffer "^5.0.1"
|
safe-buffer "^5.0.1"
|
||||||
|
|
||||||
hash.js@^1.0.0, hash.js@^1.0.3:
|
hash.js@^1.0.0, hash.js@^1.0.3:
|
||||||
version "1.1.3"
|
version "1.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
|
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
|
||||||
|
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
|
||||||
dependencies:
|
dependencies:
|
||||||
inherits "^2.0.3"
|
inherits "^2.0.3"
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.1"
|
||||||
|
|
||||||
hmac-drbg@^1.0.0:
|
hmac-drbg@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||||
|
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
|
||||||
dependencies:
|
dependencies:
|
||||||
hash.js "^1.0.3"
|
hash.js "^1.0.3"
|
||||||
minimalistic-assert "^1.0.0"
|
minimalistic-assert "^1.0.0"
|
||||||
|
@ -602,14 +607,19 @@ inflight@^1.0.4:
|
||||||
once "^1.3.0"
|
once "^1.3.0"
|
||||||
wrappy "1"
|
wrappy "1"
|
||||||
|
|
||||||
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
|
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3:
|
||||||
version "2.0.3"
|
version "2.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||||
|
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||||
|
|
||||||
inherits@2.0.1:
|
inherits@2.0.1:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.1.tgz#b17d08d326b4423e568eff719f91b0b1cbdf69f1"
|
||||||
|
|
||||||
|
inherits@2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
|
||||||
|
|
||||||
inline-source-map@~0.6.0:
|
inline-source-map@~0.6.0:
|
||||||
version "0.6.2"
|
version "0.6.2"
|
||||||
resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5"
|
resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5"
|
||||||
|
@ -753,13 +763,15 @@ miller-rabin@^4.0.0:
|
||||||
bn.js "^4.0.0"
|
bn.js "^4.0.0"
|
||||||
brorand "^1.0.1"
|
brorand "^1.0.1"
|
||||||
|
|
||||||
minimalistic-assert@^1.0.0:
|
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz#2e194de044626d4a10e7f7fbc00ce73e83e4d5c7"
|
||||||
|
integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
|
||||||
|
|
||||||
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
|
minimalistic-crypto-utils@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
|
||||||
|
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||||
|
|
||||||
minimatch@^3.0.4:
|
minimatch@^3.0.4:
|
||||||
version "3.0.4"
|
version "3.0.4"
|
||||||
|
|
Loading…
Reference in a new issue