Compare commits

..

No commits in common. "master" and "v0.2.2" have entirely different histories.

79 changed files with 808 additions and 7417 deletions

19
.github/stale.yml vendored
View file

@ -1,19 +0,0 @@
# 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

View file

@ -1,56 +0,0 @@
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
View file

@ -39,4 +39,3 @@ powerdnsadmin/static/generated
.webassets-cache
.venv*
.pytest_cache
.DS_Store

5
.travis.yml Normal file
View file

@ -0,0 +1,5 @@
language: minimal
script:
- docker-compose -f docker-compose-test.yml up --exit-code-from powerdns-admin --abort-on-container-exit
services:
- docker

View file

@ -1,12 +0,0 @@
{
"scanSettings": {
"baseBranches": []
},
"checkRunSettings": {
"vulnerableCheckRunConclusionLevel": "failure",
"displayMode": "diff"
},
"issueSettings": {
"minSeverityLevel": "LOW"
}
}

View file

@ -1,6 +1,7 @@
# PowerDNS-Admin
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: 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)
@ -17,7 +18,6 @@ A PowerDNS web interface with advanced features.
- DynDNS 2 protocol support
- Edit IPv6 PTRs using IPv6 addresses directly (no more editing of literal addresses!)
- Limited API for manipulating zones and records
- Full IDN/Punycode support
## Running PowerDNS-Admin
There are several ways to run PowerDNS-Admin. The easiest way is to use Docker.
@ -31,19 +31,17 @@ 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:
```
$ docker run -d \
-e SECRET_KEY='a-very-secret-key' \
-v pda-data:/data \
-v pda-data:/data
-p 9191:80 \
ngoduykhanh/powerdns-admin:latest
```
This creates a volume called `pda-data` to persist the SQLite database with the configuration.
#### Option 2: Using docker-compose
1. Update the configuration
1. Update the configuration
Edit the `docker-compose.yml` file to update the database connection string in `SQLALCHEMY_DATABASE_URI`.
Other environment variables are mentioned in the [legal_envvars](https://github.com/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.
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)
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.
2. Start docker container
```
@ -54,7 +52,3 @@ You can then access PowerDNS-Admin by pointing your browser to http://localhost:
## Screenshots
![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)

View file

@ -1,6 +1,5 @@
import os
#import urllib.parse
basedir = os.path.abspath(os.path.dirname(__file__))
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
@ -17,12 +16,7 @@ SQLA_DB_NAME = 'pda'
SQLALCHEMY_TRACK_MODIFICATIONS = True
### DATABASE - MySQL
#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
#)
# SQLALCHEMY_DATABASE_URI = 'mysql://' + SQLA_DB_USER + ':' + SQLA_DB_PASSWORD + '@' + SQLA_DB_HOST + '/' + SQLA_DB_NAME
### DATABASE - SQLite
SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
@ -136,7 +130,7 @@ SAML_ENABLED = False
# SAML_CERT_FILE = '/etc/pki/powerdns-admin/cert.crt'
# SAML_CERT_KEY = '/etc/pki/powerdns-admin/key.pem'
# Configures if SAML tokens should be encrypted.
# Cofigures if SAML tokens should be encrypted.
# SAML_SIGN_REQUEST = False
# #Use SAML standard logout mechanism retreived from idp metadata
# #If configured false don't care about SAML session on logout.
@ -147,19 +141,3 @@ SAML_ENABLED = False
# #SAML_LOGOUT_URL = 'https://google.com'
# #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']

View file

@ -5,9 +5,6 @@ SQLALCHEMY_DATABASE_URI = 'sqlite:////data/powerdns-admin.db'
legal_envvars = (
'SECRET_KEY',
'OIDC_OAUTH_API_URL',
'OIDC_OAUTH_TOKEN_URL',
'OIDC_OAUTH_AUTHORIZE_URL',
'BIND_ADDRESS',
'PORT',
'LOG_LEVEL',
@ -48,15 +45,7 @@ legal_envvars = (
'SAML_LOGOUT',
'SAML_LOGOUT_URL',
'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_LOGOUT_URL',
'REMOTE_USER_COOKIES',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'SAML_CERT',
'SAML_KEY',
'FILESYSTEM_SESSIONS_ENABLED'
'OFFLINE_MODE'
)
legal_envvars_int = ('PORT', 'MAIL_PORT', 'SAML_METADATA_CACHE_LIFETIME')
@ -73,12 +62,7 @@ legal_envvars_bool = (
'SAML_WANT_MESSAGE_SIGNED',
'SAML_LOGOUT',
'SAML_ASSERTION_ENCRYPTED',
'OFFLINE_MODE',
'REMOTE_USER_ENABLED',
'SIGNUP_ENABLED',
'LOCAL_DB_ENABLED',
'LDAP_ENABLED',
'FILESYSTEM_SESSIONS_ENABLED'
'OFFLINE_MODE'
)
# import everything from environment variables

View file

@ -1,5 +1,5 @@
import os
basedir = os.path.abspath(os.path.dirname(__file__))
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'

View file

@ -1,4 +1,4 @@
FROM alpine:3.13 AS builder
FROM alpine:3.11 AS builder
LABEL maintainer="k@ndk.name"
ARG BUILD_DEPENDENCIES="build-base \
@ -6,10 +6,10 @@ ARG BUILD_DEPENDENCIES="build-base \
libxml2-dev \
mariadb-connector-c-dev \
openldap-dev \
py3-pip \
python3-dev \
xmlsec-dev \
yarn \
cargo"
yarn"
ENV LC_ALL=en_US.UTF-8 \
LANG=en_US.UTF-8 \
@ -17,11 +17,8 @@ ENV LC_ALL=en_US.UTF-8 \
FLASK_APP=/build/powerdnsadmin/__init__.py
# 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} && \
apk add --no-cache py3-pip
ln -s /usr/bin/pip3 /usr/bin/pip
WORKDIR /build
@ -65,31 +62,34 @@ RUN mkdir -p /app && \
mkdir -p /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
FROM alpine:3.13
FROM alpine:3.11
ENV FLASK_APP=/app/powerdnsadmin/__init__.py \
USER=pda
ENV FLASK_APP=/app/powerdnsadmin/__init__.py
RUN apk add --no-cache mariadb-connector-c postgresql-client py3-gunicorn py3-psycopg2 xmlsec tzdata libcap && \
addgroup -S ${USER} && \
adduser -S -D -G ${USER} ${USER} && \
RUN apk add --no-cache mariadb-connector-c postgresql-client py3-gunicorn py3-psycopg2 xmlsec tzdata && \
addgroup -S pda && \
adduser -S -D -G pda pda && \
mkdir /data && \
chown ${USER}:${USER} /data && \
setcap cap_net_bind_service=+ep $(readlink -f /usr/bin/python3) && \
apk del libcap
chown pda:pda /data
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 --chown=root:${USER} /app /app/
COPY --from=builder --chown=pda:pda /app /app/
COPY ./docker/entrypoint.sh /usr/bin/
WORKDIR /app
RUN chown ${USER}:${USER} ./configs /app && \
cat ./powerdnsadmin/default_config.py ./configs/docker_config.py > ./powerdnsadmin/docker_config.py
EXPOSE 80/tcp
USER ${USER}
HEALTHCHECK CMD ["wget","--output-document=-","--quiet","--tries=1","http://127.0.0.1/"]
ENTRYPOINT ["entrypoint.sh"]
CMD ["gunicorn","powerdnsadmin:create_app()"]
CMD ["gunicorn","powerdnsadmin:create_app()","--user","pda","--group","pda"]

View file

@ -2,14 +2,18 @@
set -euo pipefail
cd /app
GUNICORN_TIMEOUT="${GUNICORN_TIMEOUT:-120}"
GUNICORN_TIMEOUT="${GUINCORN_TIMEOUT:-120}"
GUNICORN_WORKERS="${GUNICORN_WORKERS:-4}"
GUNICORN_LOGLEVEL="${GUNICORN_LOGLEVEL:-info}"
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}"
if [ "$1" == gunicorn ]; then
/bin/sh -c "flask db upgrade"
# run as user pda so that if a SQLite database is generated it is writeable
# by that user
su pda -s /bin/sh -c "flask db upgrade"
exec "$@" $GUNICORN_ARGS
else

View file

@ -1,134 +1,105 @@
### 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
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
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
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:
#### 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
YWRtaW46YWRtaW4=
# Use the ouput as your basic auth header
curl -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X <method> <url>
```
When you access the `/server` endpoint, you must use the ApiKey
we use generated output in basic authentication, we authenticate as user,
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."]}'
```
Creating an apikey which has the Administrator role:
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:
```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"}'
```
Example response (don't forget to save the plain key from the output)
```json
[
{
"accounts": [],
"description": "masterkey",
"domains": [],
"role": {
"name": "Administrator",
"id": 1
},
"id": 2,
"plain_key": "aGCthP3KLAeyjZI"
}
]
call above will return response like this:
```
[{"description": "samekey", "domains": [], "role": {"name": "Administrator", "id": 1}, "id": 2, "plain_key": "aGCthP3KLAeyjZI"}]
```
We can use the apikey for all calls to PowerDNS (don't forget to specify Content-Type):
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:
Getting powerdns configuration (Administrator Key is needed):
```
$ echo -n 'aGCthP3KLAeyjZI'|base64
YUdDdGhQM0tMQWV5alpJ
```
```bash
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!
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
```
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.
```
Getting a domain:
getting 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
```
List a zone's records:
list zone records:
```bash
```
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
```
Add a new record:
add 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 .
```
Update a record:
update 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 .
```
Delete a record:
delete 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
```
### Generate ER diagram
With docker
```bash
# Install build packages
```
apt-get install python-dev graphviz libgraphviz-dev pkg-config
# Get the required python libraries
```
```
pip install graphviz mysqlclient ERAlchemy
# Start the docker container
```
```
docker-compose up -d
# Set environment variables
```
```
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
```

View file

@ -17,83 +17,4 @@ Now you can enable the OAuth in PowerDNS-Admin.
* Replace the [tenantID] in the default URLs for authorize and token with your Tenant ID.
* Restart PowerDNS-Admin
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.
This should allow you to log in using OAuth.

View file

@ -1,41 +0,0 @@
"""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 ###

View file

@ -1,34 +0,0 @@
"""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 ###

View file

@ -2,7 +2,6 @@
"dependencies": {
"admin-lte": "2.4.9",
"bootstrap": "^3.4.1",
"bootstrap-datepicker": "^1.8.0",
"bootstrap-validator": "^0.11.9",
"datatables.net-plugins": "^1.10.19",
"icheck": "^1.0.2",

View file

@ -4,7 +4,6 @@ from flask import Flask
from flask_seasurf import SeaSurf
from flask_mail import Mail
from werkzeug.middleware.proxy_fix import ProxyFix
from flask_session import Session
from .lib import utils
@ -46,17 +45,6 @@ def create_app(config=None):
csrf.exempt(routes.api.api_zone_subpath_forward)
csrf.exempt(routes.api.api_zone_forward)
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
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
@ -81,12 +69,6 @@ def create_app(config=None):
from flask_sslify import SSLify
_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
app.mail = Mail(app)
@ -104,7 +86,6 @@ def create_app(config=None):
'email_to_gravatar_url'] = utils.email_to_gravatar_url
app.jinja_env.filters[
'display_setting_state'] = utils.display_setting_state
app.jinja_env.filters['pretty_domain_name'] = utils.pretty_domain_name
# Register context proccessors
from .models.setting import Setting
@ -124,4 +105,4 @@ def create_app(config=None):
setting = app.config.get('OFFLINE_MODE', False)
return dict(OFFLINE_MODE=setting)
return app
return app

View file

@ -23,7 +23,6 @@ css_login = Bundle('node_modules/bootstrap/dist/css/bootstrap.css',
js_login = Bundle('node_modules/jquery/dist/jquery.js',
'node_modules/bootstrap/dist/js/bootstrap.js',
'node_modules/icheck/icheck.js',
'custom/js/custom.js',
filters=(ConcatFilter, 'jsmin'),
output='generated/login.js')
@ -40,7 +39,6 @@ css_main = Bundle(
'node_modules/admin-lte/dist/css/AdminLTE.css',
'node_modules/admin-lte/dist/css/skins/_all-skins.css',
'custom/css/custom.css',
'node_modules/bootstrap-datepicker/dist/css/bootstrap-datepicker.css',
filters=('cssmin', 'cssrewrite'),
output='generated/main.css')
@ -60,7 +58,6 @@ js_main = Bundle('node_modules/jquery/dist/jquery.js',
'node_modules/jtimeout/src/jTimeout.js',
'node_modules/jquery.quicksearch/src/jquery.quicksearch.js',
'custom/js/custom.js',
'node_modules/bootstrap-datepicker/dist/js/bootstrap-datepicker.js',
filters=(ConcatFilter, 'jsmin'),
output='generated/main.js')

View file

@ -8,6 +8,7 @@ from .models import User, ApiKey, Setting, Domain, Setting
from .lib.errors import RequestIsNotJSON, NotEnoughPrivileges
from .lib.errors import DomainAccessForbidden
def admin_role_required(f):
"""
Grant access if user is in Administrator role
@ -34,21 +35,6 @@ def operator_role_required(f):
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):
"""
Grant access if:
@ -93,23 +79,6 @@ def can_configure_dnssec(f):
return decorated_function
def can_remove_domain(f):
"""
Grant access if:
- user is in Operator role or higher, or
- allow_user_remove_domain is on
"""
@wraps(f)
def decorated_function(*args, **kwargs):
if current_user.role.name not in [
'Administrator', 'Operator'
] and not Setting().get('allow_user_remove_domain'):
abort(403)
return f(*args, **kwargs)
return decorated_function
def can_create_domain(f):
"""
@ -192,59 +161,6 @@ def is_json(f):
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):
"""
Grant access if:
@ -264,48 +180,6 @@ def api_can_create_domain(f):
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):
"""
Grant access if user is in Administrator role
@ -322,52 +196,21 @@ def apikey_is_admin(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)
def decorated_function(*args, **kwargs):
apikey = g.apikey
if g.apikey.role.name not in ['Administrator', 'Operator']:
zone_id = kwargs.get('zone_id').rstrip(".")
domain_names = [item.name for item in g.apikey.domains]
domains = apikey.domains
zone_id = kwargs.get('zone_id')
domain_names = [item.name for item in domains]
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:
if zone_id not in domain_names:
raise DomainAccessForbidden()
return f(*args, **kwargs)
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):
@wraps(f)
def decorated_function(*args, **kwargs):
@ -413,13 +256,3 @@ def dyndns_login_required(f):
return f(*args, **kwargs)
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

View file

@ -1,6 +1,5 @@
import os
import urllib.parse
basedir = os.path.abspath(os.path.dirname(__file__))
basedir = os.path.abspath(os.path.abspath(os.path.dirname(__file__)))
### BASIC APP CONFIG
SALT = '$2b$12$yLUMTIfl21FKJQpTkRQXCu'
@ -9,7 +8,6 @@ BIND_ADDRESS = '0.0.0.0'
PORT = 9191
HSTS_ENABLED = False
OFFLINE_MODE = False
FILESYSTEM_SESSIONS_ENABLED = False
### DATABASE CONFIG
SQLA_DB_USER = 'pda'
@ -18,17 +16,12 @@ SQLA_DB_HOST = '127.0.0.1'
SQLA_DB_NAME = 'pda'
SQLALCHEMY_TRACK_MODIFICATIONS = True
### DATABASE - MySQL
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
)
### DATBASE - MySQL
SQLALCHEMY_DATABASE_URI = 'mysql://'+SQLA_DB_USER+':'+SQLA_DB_PASSWORD+'@'+SQLA_DB_HOST+'/'+SQLA_DB_NAME
### DATABASE - SQLite
### DATABSE - SQLite
# SQLALCHEMY_DATABASE_URI = 'sqlite:///' + os.path.join(basedir, 'pdns.db')
# SAML Authnetication
SAML_ENABLED = False
SAML_ASSERTION_ENCRYPTED = True
SAML_ASSERTION_ENCRYPTED = True

View file

@ -60,8 +60,7 @@ class ApiKeyNotUsable(StructuredException):
def __init__(
self,
name=None,
message=("Api key must have domains or accounts"
" or an administrative role")):
message="Api key must have domains or have administrative role"):
StructuredException.__init__(self)
self.message = message
self.name = name
@ -83,91 +82,3 @@ class RequestIsNotJSON(StructuredException):
StructuredException.__init__(self)
self.message = message
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

View file

@ -11,21 +11,10 @@ class RoleSchema(Schema):
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):
id = fields.Integer()
role = fields.Embed(schema=RoleSchema)
domains = fields.Embed(schema=DomainSchema, many=True)
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
description = fields.String()
key = fields.String()
@ -34,33 +23,5 @@ class ApiPlainKeySchema(Schema):
id = fields.Integer()
role = fields.Embed(schema=RoleSchema)
domains = fields.Embed(schema=DomainSchema, many=True)
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
description = 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)

View file

@ -5,7 +5,6 @@ import requests
import hashlib
import ipaddress
from collections.abc import Iterable
from distutils.version import StrictVersion
from urllib.parse import urlparse
from datetime import datetime, timedelta
@ -104,13 +103,6 @@ def fetch_json(remote_url,
data = None
try:
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:
raise RuntimeError(
'Error while loading JSON data from {0}'.format(remote_url)) from e
@ -220,15 +212,6 @@ def pretty_json(data):
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:
boxes = {
"reverse": (" ", " "),
@ -236,22 +219,3 @@ class customBoxes:
"inaddrarpa": ("in-addr", "%.in-addr.arpa")
}
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")

View file

@ -8,7 +8,6 @@ from .account_user import AccountUser
from .server import Server
from .history import History
from .api_key import ApiKey
from .api_key_account import ApiKeyAccount
from .setting import Setting
from .domain import Domain
from .domain_setting import DomainSetting

View file

@ -1,10 +1,6 @@
import traceback
from flask import current_app
from urllib.parse import urljoin
from ..lib import utils
from .base import db
from .setting import Setting
from .user import User
from .account_user import AccountUser
@ -17,9 +13,6 @@ class Account(db.Model):
contact = db.Column(db.String(128))
mail = db.Column(db.String(128))
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):
self.name = name
@ -27,12 +20,6 @@ class Account(db.Model):
self.contact = contact
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:
self.name = ''.join(c for c in self.name.lower()
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
@ -101,7 +88,7 @@ class Account(db.Model):
db.session.commit()
return {'status': True, 'msg': 'Account updated successfully'}
def delete_account(self, commit=True):
def delete_account(self):
"""
Delete an account
"""
@ -110,14 +97,13 @@ class Account(db.Model):
try:
Account.query.filter(Account.name == self.name).delete()
if commit:
db.session.commit()
db.session.commit()
return True
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot delete account {0} from DB. DETAIL: {1}'.format(
self.name, e))
self.username, e))
return False
def get_user(self):
@ -214,59 +200,3 @@ class Account(db.Model):
'Cannot revoke user privileges on account {0}. DETAIL: {1}'.
format(self.name, e))
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'}

View file

@ -14,4 +14,4 @@ class AccountUser(db.Model):
self.user_id = user_id
def __repr__(self):
return '<Account_User {0} {1}>'.format(self.account_id, self.user_id)
return '<Account_User {0} {1}>'.format(self.account_id, self.user_id)

View file

@ -1,12 +1,12 @@
import secrets
import random
import string
import bcrypt
from flask import current_app
from .base import db
from .base import db, domain_apikey
from ..models.role import Role
from ..models.domain import Domain
from ..models.account import Account
class ApiKey(db.Model):
__tablename__ = "apikey"
@ -16,21 +16,17 @@ class ApiKey(db.Model):
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
role = db.relationship('Role', back_populates="apikeys", lazy=True)
domains = db.relationship("Domain",
secondary="domain_apikey",
secondary=domain_apikey,
back_populates="apikeys")
accounts = db.relationship("Account",
secondary="apikey_account",
back_populates="apikeys")
def __init__(self, key=None, desc=None, role_name=None, domains=[], accounts=[]):
def __init__(self, key=None, desc=None, role_name=None, domains=[]):
self.id = None
self.description = desc
self.role_name = role_name
self.domains[:] = domains
self.accounts[:] = accounts
if not key:
rand_key = ''.join(
secrets.choice(string.ascii_letters + string.digits)
random.choice(string.ascii_letters + string.digits)
for _ in range(15))
self.plain_key = rand_key
self.key = self.get_hashed_password(rand_key).decode('utf-8')
@ -58,7 +54,7 @@ class ApiKey(db.Model):
db.session.rollback()
raise e
def update(self, role_name=None, description=None, domains=None, accounts=None):
def update(self, role_name=None, description=None, domains=None):
try:
if role_name:
role = Role.query.filter(Role.name == role_name).first()
@ -67,18 +63,12 @@ class ApiKey(db.Model):
if description:
self.description = description
if domains is not None:
if domains:
domain_object_list = Domain.query \
.filter(Domain.name.in_(domains)) \
.all()
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()
except Exception as e:
msg_str = 'Update of apikey failed. Error: {0}'
@ -97,22 +87,13 @@ class ApiKey(db.Model):
else:
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'),
current_app.config.get('SALT').encode('utf-8'))
def check_password(self, hashed_password):
# Check hashed password. Using bcrypt,
# Check hased 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'),
hashed_password.encode('utf-8'))
return False
@ -131,12 +112,3 @@ class ApiKey(db.Model):
raise Exception("Unauthorized")
return apikey
def associate_account(self, account):
return True
def dissociate_account(self, account):
return True
def get_accounts(self):
return True

View file

@ -1,20 +0,0 @@
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)

View file

@ -74,21 +74,23 @@ class Domain(db.Model):
"""
Get all domains which has in PowerDNS
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
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,
timeout=int(
Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
'/servers/localhost/zones/{0}'.format(domain_name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
return jdata
def get_domains(self):
"""
Get all domains which has in PowerDNS
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
jdata = utils.fetch_json(
urljoin(self.PDNS_STATS_URL,
self.API_EXTENDED_URL + '/servers/localhost/zones'),
@ -116,9 +118,10 @@ class Domain(db.Model):
db_domain = Domain.query.all()
list_db_domain = [d.name for d in db_domain]
dict_db_domain = dict((x.name, x) for x in db_domain)
current_app.logger.info("Found {} domains in PowerDNS-Admin".format(
current_app.logger.info("Found {} entries in PowerDNS-Admin".format(
len(list_db_domain)))
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(
urljoin(self.PDNS_STATS_URL,
@ -128,7 +131,7 @@ class Domain(db.Model):
verify=Setting().get('verify_ssl_connections'))
list_jdomain = [d['name'].rstrip('.') for d in jdata]
current_app.logger.info(
"Found {} zones in PowerDNS server".format(len(list_jdomain)))
"Found {} entries in PowerDNS server".format(len(list_jdomain)))
try:
# domains should remove from db since it doesn't exist in powerdns anymore
@ -166,8 +169,8 @@ class Domain(db.Model):
except Exception as e:
db.session.rollback()
current_app.logger.error(
'Cannot update domain table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Cannot update domain table'}
'Can not update domain table. Error: {0}'.format(e))
return {'status': 'error', 'msg': 'Can not update domain table'}
def update_pdns_admin_domain(self, domain, account_id, data, do_commit=True):
# existing domain, only update if something actually has changed
@ -208,7 +211,8 @@ class Domain(db.Model):
Add a domain to power dns
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
domain_name = domain_name + '.'
domain_ns = [ns + '.' for ns in domain_ns]
@ -258,14 +262,15 @@ class Domain(db.Model):
"""
Read Domain from PowerDNS and add into PDNS-Admin
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
if not domain:
try:
domain = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(
domain_dict['name'])),
'/servers/localhost/zones/{0}'.format(
domain_dict['name'])),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
verify=Setting().get('verify_ssl_connections'))
@ -310,8 +315,8 @@ class Domain(db.Model):
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': 'error', 'msg': 'Domain does not exist.'}
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
if soa_edit_api not in ["DEFAULT", "INCREASE", "EPOCH", "OFF"]:
soa_edit_api = 'DEFAULT'
@ -324,13 +329,13 @@ class Domain(db.Model):
try:
jdata = utils.fetch_json(urljoin(
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='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
return {'status': 'error', 'msg': jdata['error']}
@ -360,21 +365,21 @@ class Domain(db.Model):
domain = Domain.query.filter(Domain.name == domain_name).first()
if not domain:
return {'status': 'error', 'msg': 'Domain does not exist.'}
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
post_data = {"kind": kind, "masters": masters}
try:
jdata = utils.fetch_json(urljoin(
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='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
return {'status': 'error', 'msg': jdata['error']}
@ -405,27 +410,27 @@ class Domain(db.Model):
domain_obj = Domain.query.filter(Domain.name == domain_name).first()
domain_auto_ptr = DomainSetting.query.filter(
DomainSetting.domain == domain_obj).filter(
DomainSetting.setting == 'auto_ptr').first()
DomainSetting.setting == 'auto_ptr').first()
domain_auto_ptr = strtobool(
domain_auto_ptr.value) if domain_auto_ptr else False
system_auto_ptr = Setting().get('auto_ptr')
self.name = domain_name
domain_id = self.get_id_by_name(domain_reverse_name)
if domain_id is None and \
(
system_auto_ptr or
domain_auto_ptr
):
result = self.add(domain_reverse_name, 'Master', 'DEFAULT', [], [])
if None == domain_id and \
(
system_auto_ptr or
domain_auto_ptr
):
result = self.add(domain_reverse_name, 'Master', 'DEFAULT', '', '')
self.update()
if result['status'] == 'ok':
history = History(msg='Add reverse lookup domain {0}'.format(
domain_reverse_name),
detail=str({
'domain_type': 'Master',
'domain_master_ips': ''
}),
created_by='System')
detail=str({
'domain_type': 'Master',
'domain_master_ips': ''
}),
created_by='System')
history.add()
else:
return {
@ -438,9 +443,9 @@ class Domain(db.Model):
self.grant_privileges(domain_user_ids)
return {
'status':
'ok',
'ok',
'msg':
'New reverse lookup domain created with granted privileges'
'New reverse lookup domain created with granted privileges'
}
return {
'status': 'ok',
@ -492,15 +497,16 @@ class Domain(db.Model):
"""
Delete a single domain name from powerdns
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
utils.fetch_json(urljoin(
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='DELETE',
verify=Setting().get('verify_ssl_connections'))
'/servers/localhost/zones/{0}'.format(domain_name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='DELETE',
verify=Setting().get('verify_ssl_connections'))
current_app.logger.info(
'Deleted domain successfully from PowerDNS: {0}'.format(
domain_name))
@ -519,13 +525,6 @@ class Domain(db.Model):
domain_setting.delete()
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
Domain.query.filter(Domain.name == domain_name).delete()
if do_commit:
@ -541,8 +540,8 @@ class Domain(db.Model):
user_ids = []
query = db.session.query(
DomainUser, Domain).filter(User.id == DomainUser.user_id).filter(
Domain.id == DomainUser.domain_id).filter(
Domain.name == self.name).all()
Domain.id == DomainUser.domain_id).filter(
Domain.name == self.name).all()
for q in query:
user_ids.append(q[0].user_id)
return user_ids
@ -567,7 +566,7 @@ class Domain(db.Model):
db.session.rollback()
current_app.logger.error(
'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()))
try:
@ -579,35 +578,8 @@ class Domain(db.Model):
db.session.rollback()
current_app.logger.error(
'Cannot grant user privileges to domain {0}. DETAIL: {1}'.
format(self.name, e))
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
current_app.logger.debug(print(traceback.format_exc()))
def update_from_master(self, domain_name):
"""
@ -615,26 +587,27 @@ class Domain(db.Model):
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
r = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}/axfr-retrieve'.format(
domain.name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'))
'/servers/localhost/zones/{0}/axfr-retrieve'.format(
domain.name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'))
return {'status': 'ok', 'msg': r.get('result')}
except Exception as e:
current_app.logger.error(
'Cannot update from master. DETAIL: {0}'.format(e))
return {
'status':
'error',
'error',
'msg':
'There was something wrong, please contact administrator'
'There was something wrong, please contact administrator'
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
@ -645,13 +618,14 @@ class Domain(db.Model):
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
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/{0}/cryptokeys'.format(
domain.name)),
'/servers/localhost/zones/{0}/cryptokeys'.format(
domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='GET',
@ -668,9 +642,9 @@ class Domain(db.Model):
'Cannot get domain dnssec. DETAIL: {0}'.format(e))
return {
'status':
'error',
'error',
'msg':
'There was something wrong, please contact administrator'
'There was something wrong, please contact administrator'
}
else:
return {'status': 'error', 'msg': 'This domain does not exist'}
@ -681,14 +655,15 @@ class Domain(db.Model):
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
# Enable API-RECTIFY for domain, BEFORE activating DNSSEC
post_data = {"api_rectify": True}
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain.name)),
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PUT',
@ -698,7 +673,7 @@ class Domain(db.Model):
return {
'status': 'error',
'msg':
'API-RECTIFY could not be enabled for this domain',
'API-RECTIFY could not be enabled for this domain',
'jdata': jdata
}
@ -707,8 +682,8 @@ class Domain(db.Model):
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}/cryptokeys'.format(
domain.name)),
'/servers/localhost/zones/{0}/cryptokeys'.format(
domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='POST',
@ -717,12 +692,12 @@ class Domain(db.Model):
if 'error' in jdata:
return {
'status':
'error',
'error',
'msg':
'Cannot enable DNSSEC for this domain. Error: {0}'.
format(jdata['error']),
'Cannot enable DNSSEC for this domain. Error: {0}'.
format(jdata['error']),
'jdata':
jdata
jdata
}
return {'status': 'ok'}
@ -733,9 +708,9 @@ class Domain(db.Model):
current_app.logger.debug(traceback.format_exc())
return {
'status':
'error',
'error',
'msg':
'There was something wrong, please contact administrator'
'There was something wrong, please contact administrator'
}
else:
@ -747,14 +722,15 @@ class Domain(db.Model):
"""
domain = Domain.query.filter(Domain.name == domain_name).first()
if domain:
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
# Deactivate DNSSEC
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
domain.name, key_id)),
'/servers/localhost/zones/{0}/cryptokeys/{1}'.format(
domain.name, key_id)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='DELETE',
@ -762,12 +738,12 @@ class Domain(db.Model):
if jdata != True:
return {
'status':
'error',
'error',
'msg':
'Cannot disable DNSSEC for this domain. Error: {0}'.
format(jdata['error']),
'Cannot disable DNSSEC for this domain. Error: {0}'.
format(jdata['error']),
'jdata':
jdata
jdata
}
# Disable API-RECTIFY for domain, AFTER deactivating DNSSEC
@ -775,7 +751,7 @@ class Domain(db.Model):
jdata = utils.fetch_json(
urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
'/servers/localhost/zones/{0}'.format(domain.name)),
'/servers/localhost/zones/{0}'.format(domain.name)),
headers=headers,
timeout=int(Setting().get('pdns_api_timeout')),
method='PUT',
@ -785,7 +761,7 @@ class Domain(db.Model):
return {
'status': 'error',
'msg':
'API-RECTIFY could not be disabled for this domain',
'API-RECTIFY could not be disabled for this domain',
'jdata': jdata
}
@ -798,7 +774,7 @@ class Domain(db.Model):
return {
'status': 'error',
'msg':
'There was something wrong, please contact administrator',
'There was something wrong, please contact administrator',
'domain': domain.name,
'id': key_id
}
@ -821,7 +797,8 @@ class Domain(db.Model):
if not domain:
return {'status': False, 'msg': 'Domain does not exist'}
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
account_name = Account().get_name_by_id(account_id)
@ -830,13 +807,13 @@ class Domain(db.Model):
try:
jdata = utils.fetch_json(urljoin(
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='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
'/servers/localhost/zones/{0}'.format(domain_name)),
headers=headers,
timeout=int(
Setting().get('pdns_api_timeout')),
method='PUT',
verify=Setting().get('verify_ssl_connections'),
data=post_data)
if 'error' in jdata.keys():
current_app.logger.error(jdata['error'])
@ -875,7 +852,7 @@ class Domain(db.Model):
.outerjoin(Account, Domain.account_id == Account.id) \
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
.filter(
db.or_(
DomainUser.user_id == user_id,
AccountUser.user_id == user_id
)).filter(Domain.id == self.id).first()
db.or_(
DomainUser.user_id == user_id,
AccountUser.user_id == user_id
)).filter(Domain.id == self.id).first()

View file

@ -1,5 +1,3 @@
import traceback
from flask import current_app
from .base import db
@ -34,4 +32,4 @@ class DomainSetting(db.Model):
'Unable to set DomainSetting value. DETAIL: {0}'.format(e))
current_app.logger.debug(traceback.format_exc())
db.session.rollback()
return False
return False

View file

@ -14,4 +14,4 @@ class DomainUser(db.Model):
self.user_id = user_id
def __repr__(self):
return '<Domain_User {0} {1}>'.format(self.domain_id, self.user_id)
return '<Domain_User {0} {1}>'.format(self.domain_id, self.user_id)

View file

@ -1,5 +1,3 @@
import traceback
from flask import current_app
from datetime import datetime
@ -8,22 +6,17 @@ from .base import db
class History(db.Model):
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))
# detail = db.Column(db.Text().with_variant(db.Text(length=2**24-2), 'mysql'))
detail = db.Column(db.Text())
created_by = db.Column(db.String(128))
created_on = db.Column(db.DateTime, index=True, default=datetime.utcnow)
domain_id = db.Column(db.Integer,
db.ForeignKey('domain.id'),
nullable=True)
created_on = db.Column(db.DateTime, default=datetime.utcnow)
def __init__(self, id=None, msg=None, detail=None, created_by=None, domain_id=None):
def __init__(self, id=None, msg=None, detail=None, created_by=None):
self.id = id
self.msg = msg
self.detail = detail
self.created_by = created_by
self.domain_id = domain_id
def __repr__(self):
return '<History {0}>'.format(self.msg)
@ -36,7 +29,6 @@ class History(db.Model):
h.msg = self.msg
h.detail = self.detail
h.created_by = self.created_by
h.domain_id = self.domain_id
db.session.add(h)
db.session.commit()

View file

@ -15,10 +15,6 @@ from .domain import Domain
from .domain_setting import DomainSetting
def by_record_content_pair(e):
return e[0]['content']
class Record(object):
"""
This is not a model, it's just an object
@ -48,7 +44,8 @@ class Record(object):
"""
Query domain's rrsets via PDNS API
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(
self.PDNS_STATS_URL, self.API_EXTENDED_URL +
@ -63,17 +60,7 @@ class Record(object):
.format(e))
return []
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
return jdata['rrsets']
def add(self, domain_name, rrset):
"""
@ -99,7 +86,8 @@ class Record(object):
}
# Continue if the record is ready to be added
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(
@ -129,26 +117,21 @@ class Record(object):
"""
Merge the rrsets that has same "name" and
"type".
Return: a new rrset which has multiple "records"
Return: a new rrest which has multiple "records"
and "comments"
"""
if not rrsets:
raise Exception("Empty rrsets to merge")
elif len(rrsets) == 1:
# It is unique rrset already
# It is unique rrest already
return rrsets[0]
else:
# Merge rrsets into one
rrset = rrsets[0]
rrest = rrsets[0]
for r in rrsets[1:]:
rrset['records'] = rrset['records'] + r['records']
rrset['comments'] = rrset['comments'] + r['comments']
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
rrest['records'] = rrest['records'] + r['records']
rrest['comments'] = rrest['comments'] + r['comments']
return rrest
def build_rrsets(self, domain_name, submitted_records):
"""
@ -159,23 +142,12 @@ class Record(object):
submitted_records(list): List of records submitted from PDA datatable
Returns:
transformed_rrsets(list): List of rrsets converted from PDA datatable
transformed_rrsets(list): List of rrests converted from PDA datatable
"""
rrsets = []
for record in submitted_records:
# 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,
# We convert ipv6 address back to reverse record format
# before submitting to PDNS API.
@ -203,7 +175,7 @@ class Record(object):
] and record["record_data"].strip()[-1:] != '.':
record["record_data"] += '.'
record_content = {
record_conntent = {
"content": record["record_data"],
"disabled":
False if record['record_status'] == 'Active' else True
@ -213,22 +185,19 @@ class Record(object):
record_comments = [{
"content": record["record_comment"],
"account": ""
}] if record.get("record_comment") else [{
"content": "",
"account": ""
}]
}] if record.get("record_comment") else []
# Add the formatted record to rrsets list
rrsets.append({
"name": record_name,
"type": record["record_type"],
"ttl": int(record["record_ttl"]),
"records": [record_content],
"records": [record_conntent],
"comments": record_comments
})
# Group the records which has the same name and type.
# The rrset then has multiple records inside.
# The rrest then has multiple records inside.
transformed_rrsets = []
# Sort the list before using groupby
@ -249,8 +218,8 @@ class Record(object):
submitted_records(list): List of records submitted from PDA datatable
Returns:
new_rrsets(list): List of rrsets to be added
del_rrsets(list): List of rrsets to be deleted
new_rrsets(list): List of rrests to be added
del_rrsets(list): List of rrests to be deleted
"""
# Create submitted rrsets from submitted records
submitted_rrsets = self.build_rrsets(domain_name, submitted_records)
@ -268,8 +237,7 @@ class Record(object):
# comparison between current and submitted rrsets
for r in current_rrsets:
for comment in r['comments']:
if 'modified_at' in comment:
del comment['modified_at']
del comment['modified_at']
# List of rrsets to be added
new_rrsets = {"rrsets": []}
@ -292,22 +260,11 @@ class Record(object):
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):
"""
Apply record changes to a domain. This function
will make 2 calls to the PDNS API to DELETE and
REPLACE records (rrsets)
REPLACE records (rrests)
"""
current_app.logger.debug(
"submitted_records: {}".format(submitted_records))
@ -315,67 +272,47 @@ class Record(object):
# Get the list of rrsets to be added and deleted
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
try:
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
if del_rrsets["rrsets"]:
result = self.apply_rrsets(domain_name, del_rrsets)
if 'error' in result.keys():
jdata1 = 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=del_rrsets)
if 'error' in jdata1.keys():
current_app.logger.error(
'Cannot apply record changes with deleting rrsets step. PDNS error: {}'
.format(result['error']))
.format(jdata1['error']))
print(jdata1['error'])
return {
'status': 'error',
'msg': result['error'].replace("'", "")
'msg': jdata1['error'].replace("'", "")
}
if new_rrsets["rrsets"]:
result = self.apply_rrsets(domain_name, new_rrsets)
if 'error' in result.keys():
jdata2 = utils.fetch_json(
urljoin(
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(
'Cannot apply record changes with adding rrsets step. PDNS error: {}'
.format(result['error']))
# rollback - re-add the removed record if the adding operation is failed.
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("'", "")
}
.format(jdata2['error']))
return {
'status': 'error',
'msg': jdata2['error'].replace("'", "")
}
self.auto_ptr(domain_name, new_rrsets, del_rrsets)
self.update_db_serial(domain_name)
@ -500,7 +437,8 @@ class Record(object):
"""
Delete a record from domain
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
data = {
"rrsets": [{
"name": self.name.rstrip('.') + '.',
@ -562,7 +500,8 @@ class Record(object):
"""
Update single record
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
data = {
"rrsets": [{
@ -603,7 +542,8 @@ class Record(object):
}
def update_db_serial(self, domain):
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
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)),

View file

@ -24,4 +24,4 @@ class RecordEntry(object):
return self._is_allowed_edit
def is_allowed_delete(self):
return self._is_allowed_delete
return self._is_allowed_delete

View file

@ -20,4 +20,4 @@ class Role(db.Model):
self.description = description
def __repr__(self):
return '<Role {0}r>'.format(self.name)
return '<Role {0}r>'.format(self.name)

View file

@ -24,7 +24,8 @@ class Server(object):
"""
Get server config
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(
@ -45,7 +46,8 @@ class Server(object):
"""
Get server statistics
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(
@ -66,7 +68,8 @@ class Server(object):
"""
Search zone/record/comment directly from PDNS API
"""
headers = {'X-API-Key': self.PDNS_API_KEY}
headers = {}
headers['X-API-Key'] = self.PDNS_API_KEY
try:
jdata = utils.fetch_json(urljoin(

View file

@ -1,6 +1,4 @@
import sys
import traceback
import pytimeparse
from ast import literal_eval
from distutils.util import strtobool
@ -26,11 +24,7 @@ class Setting(db.Model):
'pretty_ipv6_ptr': False,
'dnssec_admins_only': False,
'allow_user_create_domain': False,
'allow_user_remove_domain': False,
'allow_user_view_history': False,
'delete_sso_accounts': False,
'bg_domain_updates': False,
'enable_api_rr_history': True,
'site_name': 'PowerDNS-Admin',
'site_url': 'http://localhost:9191',
'session_timeout': 10,
@ -42,10 +36,6 @@ class Setting(db.Model):
'verify_ssl_connections': True,
'local_db_enabled': True,
'signup_enabled': True,
'autoprovisioning': False,
'urn_value':'',
'autoprovisioning_attribute': '',
'purge': False,
'verify_user_email': False,
'ldap_enabled': False,
'ldap_type': 'ldap',
@ -91,11 +81,6 @@ class Setting(db.Model):
'azure_admin_group': '',
'azure_operator_group': '',
'azure_user_group': '',
'azure_group_accounts_enabled': False,
'azure_group_accounts_name': 'displayName',
'azure_group_accounts_name_re': '',
'azure_group_accounts_description': 'description',
'azure_group_accounts_description_re': '',
'oidc_oauth_enabled': False,
'oidc_oauth_key': '',
'oidc_oauth_secret': '',
@ -103,13 +88,6 @@ class Setting(db.Model):
'oidc_oauth_api_url': '',
'oidc_oauth_token_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': {
'A': True,
'AAAA': True,
@ -187,10 +165,6 @@ class Setting(db.Model):
'URI': False
},
'ttl_options': '1 minute,5 minutes,30 minutes,60 minutes,24 hours',
'otp_field_enabled': True,
'custom_css': '',
'otp_force': False,
'max_history_records': 1000
}
def __init__(self, id=None, name=None, value=None):
@ -271,23 +245,16 @@ class Setting(db.Model):
def get(self, setting):
if setting in self.defaults:
if setting.upper() in current_app.config:
result = current_app.config[setting.upper()]
else:
result = self.query.filter(Setting.name == setting).first()
result = self.query.filter(Setting.name == setting).first()
if result is not None:
if hasattr(result,'value'):
result = result.value
return strtobool(result) if result in [
return strtobool(result.value) if result.value in [
'True', 'False'
] else result
] else result.value
else:
return self.defaults[setting]
else:
current_app.logger.error('Unknown setting queried: {0}'.format(setting))
def get_records_allow_to_edit(self):
return list(
set(self.get_forward_records_allow_to_edit() +

View file

@ -1,22 +1,17 @@
import os
import base64
import traceback
import bcrypt
import traceback
import pyotp
import ldap
import ldap.filter
from flask import current_app
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 .role import Role
from .setting import Setting
from .domain_user import DomainUser
from .account_user import AccountUser
class Anonymous(AnonymousUserMixin):
@ -34,7 +29,6 @@ class User(db.Model):
otp_secret = db.Column(db.String(16))
confirmed = db.Column(db.SmallInteger, nullable=False, default=0)
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
accounts = None
def __init__(self,
id=None,
@ -109,7 +103,7 @@ class User(db.Model):
return bcrypt.hashpw(pw.encode('utf-8'), bcrypt.gensalt())
def check_password(self, hashed_password):
# Check hashed password. Using bcrypt, the salt is saved into the hash itself
# Check hased password. Using bcrypt, the salt is saved into the hash itself
if (self.plain_text_password):
return bcrypt.checkpw(self.plain_text_password.encode('utf-8'),
hashed_password.encode('utf-8'))
@ -134,8 +128,9 @@ class User(db.Model):
conn.protocol_version = ldap.VERSION3
return conn
def ldap_search(self, searchFilter, baseDN, retrieveAttributes=None):
def ldap_search(self, searchFilter, baseDN):
searchScope = ldap.SCOPE_SUBTREE
retrieveAttributes = None
try:
conn = self.ldap_init_conn()
@ -196,7 +191,7 @@ class User(db.Model):
current_app.logger.exception("Recursive AD Group search error")
return result
def is_validate(self, method, src_ip='', trust_user=False):
def is_validate(self, method, src_ip=''):
"""
Validate user credential
"""
@ -207,8 +202,8 @@ class User(db.Model):
User.username == self.username).first()
if user_info:
if trust_user or (user_info.password and self.check_password(
user_info.password)):
if user_info.password and self.check_password(
user_info.password):
current_app.logger.info(
'User "{0}" logged in successfully. Authentication request from {1}'
.format(self.username, src_ip))
@ -236,7 +231,7 @@ class User(db.Model):
LDAP_GROUP_SECURITY_ENABLED = Setting().get('ldap_sg_enabled')
# validate AD user password
if Setting().get('ldap_type') == 'ad' and not trust_user:
if Setting().get('ldap_type') == 'ad':
ldap_username = "{0}@{1}".format(self.username,
Setting().get('ldap_domain'))
if not self.ldap_auth(ldap_username, self.password):
@ -263,7 +258,7 @@ class User(db.Model):
ldap_username = ldap.filter.escape_filter_chars(
ldap_result[0][0][0])
if Setting().get('ldap_type') != 'ad' and not trust_user:
if Setting().get('ldap_type') != 'ad':
# validate ldap user password
if not self.ldap_auth(ldap_username, self.password):
current_app.logger.error(
@ -436,8 +431,7 @@ class User(db.Model):
return {'status': False, 'msg': 'Email address is already in use'}
# first register user will be in Administrator role
if self.role_id is None:
self.role_id = Role.query.filter_by(name='User').first().id
self.role_id = Role.query.filter_by(name='User').first().id
if User.query.count() == 0:
self.role_id = Role.query.filter_by(
name='Administrator').first().id
@ -479,7 +473,7 @@ class User(db.Model):
user.email = self.email
# store new password hash (only if changed)
if self.plain_text_password:
if self.plain_text_password != "":
user.password = self.get_hashed_password(
self.plain_text_password).decode("utf-8")
@ -490,6 +484,7 @@ class User(db.Model):
"""
Update user profile
"""
user = User.query.filter(User.username == self.username).first()
if not user:
return False
@ -545,26 +540,9 @@ class User(db.Model):
Note: This doesn't include the permission granting from Account
which user belong to
"""
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):
"""
Delete a user
@ -582,7 +560,7 @@ class User(db.Model):
self.username, e))
return False
def revoke_privilege(self, update_user=False):
def revoke_privilege(self):
"""
Revoke all privileges from a user
"""
@ -592,8 +570,6 @@ class User(db.Model):
user_id = user.id
try:
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()
return True
except Exception as e:
@ -612,196 +588,4 @@ class User(db.Model):
db.session.commit()
return {'status': True, 'msg': 'Set user role successfully'}
else:
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
return {'status': False, 'msg': 'Role does not exist'}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -28,19 +28,6 @@ def handle_internal_server_error(e):
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
def load_user(id):
"""
@ -50,42 +37,29 @@ def load_user(id):
@login_manager.request_loader
def login_via_authorization_header_or_remote_user(request):
# Try to login using Basic Authentication
def login_via_authorization_header(request):
auth_header = request.headers.get('Authorization')
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)
try:
auth_header = str(base64.b64decode(auth_header), 'utf-8')
username, password = auth_header.split(":")
except TypeError as e:
return None
user = User(username=username,
password=password,
plain_text_password=password)
return load_if_valid(user, method=auth_method, src_ip=request.remote_addr)
# Try login by checking a REMOTE_USER environment variable
remote_user = request.remote_user
if remote_user and current_app.config.get('REMOTE_USER_ENABLED'):
session_remote_user = session.get('remote_user')
# If we already validated a remote user against an authorization method
# a local user should have been created in the database, so we force a 'LOCAL' auth_method
auth_method = 'LOCAL' if session_remote_user else current_app.config.get('REMOTE_AUTH_METHOD', 'LDAP')
current_app.logger.debug(
'REMOTE_USER environment variable found: attempting {0} authentication for username "{1}"'
.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
try:
auth_method = request.args.get('auth_method', 'LOCAL')
auth_method = 'LDAP' if auth_method != 'LOCAL' else 'LOCAL'
auth = user.is_validate(method=auth_method,
src_ip=request.remote_addr)
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
return None

View file

@ -3,7 +3,6 @@ from flask import Blueprint, render_template, url_for, current_app, request, jso
from flask_login import login_required, current_user, login_manager
from sqlalchemy import not_
from ..decorators import operator_role_required
from ..lib.utils import customBoxes
from ..models.user import User, Anonymous
from ..models.account import Account
@ -61,7 +60,7 @@ def domains_custom(boxId):
))
template = current_app.jinja_env.get_template("dashboard_domain.html")
render = template.make_module(vars={"current_user": current_user, "allow_user_view_history": Setting().get('allow_user_view_history')})
render = template.make_module(vars={"current_user": current_user})
columns = [
Domain.name, Domain.dnssec, Domain.type, Domain.serial, Domain.master,
@ -151,46 +150,11 @@ def dashboard():
else:
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
domain_count = 0
history_number = 0
history = []
domain_count = Domain.query.count()
user_num = User.query.count()
if current_user.role.name in ['Administrator', 'Operator']:
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)
history_number = History.query.count()
history = History.query.order_by(History.created_on.desc()).limit(4)
server = Server(server_id='localhost')
statistics = server.get_statistic()
if statistics:
@ -207,14 +171,13 @@ def dashboard():
user_num=user_num,
history_number=history_number,
uptime=uptime,
histories=detailedHistories,
show_bg_domain_button=show_bg_domain_button,
histories=history,
show_bg_domain_button=BG_DOMAIN_UPDATE,
pdns_version=Setting().get('pdns_version'))
@dashboard_bp.route('/domains-updater', methods=['GET', 'POST'])
@login_required
@operator_role_required
def domains_updater():
current_app.logger.debug('Update domains in background')
d = Domain().update()

View file

@ -8,9 +8,8 @@ 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_login import login_required, current_user, login_manager
from ..lib.utils import pretty_domain_name
from ..lib.utils import pretty_json
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec, can_remove_domain
from ..decorators import can_create_domain, operator_role_required, can_access_domain, can_configure_dnssec
from ..models.user import User, Anonymous
from ..models.account import Account
from ..models.setting import Setting
@ -21,11 +20,7 @@ from ..models.record_entry import RecordEntry
from ..models.domain_template import DomainTemplate
from ..models.domain_template_record import DomainTemplateRecord
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',
__name__,
template_folder='templates',
@ -66,7 +61,7 @@ def domain(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':
if not rrsets:
abort(500)
quick_edit = Setting().get('record_quick_edit')
@ -96,7 +91,7 @@ def domain(domain_name):
# 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:
'type'] == 'PTR' and 'ip6.arpa' in r_name:
r_name = dns.reversename.to_address(
dns.name.from_text(r_name))
@ -104,17 +99,14 @@ def domain(domain_name):
# 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,
comment=r['comments'][index]['content']
if r['comments'] else '',
is_allowed_edit=True)
index += 1
records.append(record_entry)
@ -132,217 +124,7 @@ def domain(domain_name):
records=records,
editable_records=editable_records,
quick_edit=quick_edit,
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)
ttl_options=ttl_options)
@domain_bp.route('/add', methods=['GET', 'POST'])
@ -363,30 +145,7 @@ def add():
'errors/400.html',
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
# 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 request.form.getlist('domain_master_address'):
domain_master_string = request.form.getlist(
@ -406,16 +165,13 @@ def add():
domain_master_ips=domain_master_ips,
account_name=account_name)
if result['status'] == 'ok':
domain_id = Domain().get_id_by_name(domain_name)
history = History(msg='Add domain {0}'.format(
pretty_domain_name(domain_name)),
history = History(msg='Add domain {0}'.format(domain_name),
detail=str({
'domain_type': domain_type,
'domain_master_ips': domain_master_ips,
'account_id': account_id
}),
created_by=current_user.username,
domain_id=domain_id)
created_by=current_user.username)
history.add()
# grant user access to the domain
@ -456,8 +212,7 @@ def add():
"del_rrests":
result['data'][1]['rrsets']
})),
created_by=current_user.username,
domain_id=domain_id)
created_by=current_user.username)
history.add()
else:
history = History(
@ -476,19 +231,13 @@ def add():
current_app.logger.debug(traceback.format_exc())
abort(500)
# Get
else:
# 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()
accounts = Account.query.all()
return render_template('domain_add.html',
templates=templates,
accounts=accounts)
@domain_bp.route('/setting/<path:domain_name>/delete', methods=['POST'])
@login_required
@operator_role_required
@ -499,8 +248,7 @@ def delete(domain_name):
if result['status'] == 'error':
abort(500)
history = History(msg='Delete domain {0}'.format(
pretty_domain_name(domain_name)),
history = History(msg='Delete domain {0}'.format(domain_name),
created_by=current_user.username)
history.add()
@ -516,7 +264,7 @@ def setting(domain_name):
if not domain:
abort(404)
users = User.query.all()
accounts = Account.query.order_by(Account.name).all()
accounts = Account.query.all()
# get list of user ids to initialize selection data
d = Domain(name=domain_name)
@ -543,11 +291,9 @@ def setting(domain_name):
d.grant_privileges(new_user_ids)
history = History(
msg='Change domain {0} access control'.format(
pretty_domain_name(domain_name)),
msg='Change domain {0} access control'.format(domain_name),
detail=str({'user_has_access': new_user_list}),
created_by=current_user.username,
domain_id=d.id)
created_by=current_user.username)
history.add()
return redirect(url_for('domain.setting', domain_name=domain_name))
@ -581,15 +327,13 @@ def change_type(domain_name):
kind=domain_type,
masters=domain_master_ips)
if status['status'] == 'ok':
history = History(msg='Update type for domain {0}'.format(
pretty_domain_name(domain_name)),
history = History(msg='Update type for domain {0}'.format(domain_name),
detail=str({
"domain": domain_name,
"type": domain_type,
"masters": domain_master_ips
}),
created_by=current_user.username,
domain_id=Domain().get_id_by_name(domain_name))
created_by=current_user.username)
history.add()
return redirect(url_for('domain.setting', domain_name = domain_name))
else:
@ -615,14 +359,12 @@ def change_soa_edit_api(domain_name):
soa_edit_api=new_setting)
if status['status'] == 'ok':
history = History(
msg='Update soa_edit_api for domain {0}'.format(
pretty_domain_name(domain_name)),
msg='Update soa_edit_api for domain {0}'.format(domain_name),
detail=str({
"domain": domain_name,
"soa_edit_api": new_setting
}),
created_by=current_user.username,
domain_id=d.get_id_by_name(domain_name))
created_by=current_user.username)
history.add()
return redirect(url_for('domain.setting', domain_name = domain_name))
else:
@ -676,35 +418,24 @@ def record_apply(domain_name):
'status':
'error',
'msg':
'Domain name {0} does not exist'.format(pretty_domain_name(domain_name))
'Domain name {0} does not exist'.format(domain_name)
}), 404)
r = Record()
result = r.apply(domain_name, submitted_record)
if result['status'] == 'ok':
history = History(
msg='Apply record changes to domain {0}'.format(pretty_domain_name(domain_name)),
msg='Apply record changes to domain {0}'.format(domain_name),
detail=str(
json.dumps({
"domain": domain_name,
"add_rrests": result['data'][0]['rrsets'],
"del_rrests": result['data'][1]['rrsets']
})),
created_by=current_user.username,
domain_id=domain.id)
created_by=current_user.username)
history.add()
return make_response(jsonify(result), 200)
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)
except Exception as e:
current_app.logger.error(
@ -823,10 +554,8 @@ def admin_setdomainsetting(domain_name):
if setting.set(new_value):
history = History(
msg='Setting {0} changed value to {1} for {2}'.
format(new_setting, new_value,
pretty_domain_name(domain_name)),
created_by=current_user.username,
domain_id=domain.id)
format(new_setting, new_value, domain.name),
created_by=current_user.username)
history.add()
return make_response(
jsonify({
@ -844,9 +573,8 @@ def admin_setdomainsetting(domain_name):
history = History(
msg=
'New setting {0} with value {1} for {2} has been created'
.format(new_setting, new_value, pretty_domain_name(domain_name)),
created_by=current_user.username,
domain_id=domain.id)
.format(new_setting, new_value, domain.name),
created_by=current_user.username)
history.add()
return make_response(
jsonify({

View file

@ -4,7 +4,6 @@ import json
import traceback
import datetime
import ipaddress
import base64
from distutils.util import strtobool
from yaml import Loader, load
from onelogin.saml2.utils import OneLogin_Saml2_Utils
@ -44,6 +43,7 @@ index_bp = Blueprint('index',
template_folder='templates',
url_prefix='/')
@index_bp.before_app_first_request
def register_modules():
global google
@ -168,8 +168,10 @@ def login():
return redirect(url_for('index.login'))
session['user_id'] = user.id
login_user(user, remember=False)
session['authentication_type'] = 'OAuth'
return authenticate_user(user, 'Google OAuth')
signin_history(user.username, 'Google OAuth', True)
return redirect(url_for('index.index'))
if 'github_token' in session:
me = json.loads(github.get('user').text)
@ -194,7 +196,9 @@ def login():
session['user_id'] = user.id
session['authentication_type'] = 'OAuth'
return authenticate_user(user, 'Github OAuth')
login_user(user, remember=False)
signin_history(user.username, 'Github OAuth', True)
return redirect(url_for('index.index'))
if 'azure_token' in session:
azure_info = azure.get('me?$select=displayName,givenName,id,mail,surname,userPrincipalName').text
@ -270,107 +274,21 @@ def login():
azure_username +
' has no relevant group memberships')
session.pop('azure_token', None)
return render_template('login.html',
saml_enabled=SAML_ENABLED,
return render_template('login.html',
saml_enabled=SAML_ENABLED,
error=('User ' + azure_username +
' is not in any authorised groups.'))
# Handle account/group creation, if enabled
if Setting().get('azure_group_accounts_enabled') and mygroups:
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')
login_user(user, remember=False)
signin_history(user.username, 'Azure OAuth', True)
return redirect(url_for('index.index'))
if 'oidc_token' in session:
me = json.loads(oidc.get('userinfo').text)
oidc_username = me[Setting().get('oidc_oauth_username')]
oidc_givenname = me[Setting().get('oidc_oauth_firstname')]
oidc_familyname = me[Setting().get('oidc_oauth_last_name')]
oidc_email = me[Setting().get('oidc_oauth_email')]
oidc_username = me["preferred_username"]
oidc_givenname = me["name"]
oidc_familyname = ""
oidc_email = me["email"]
user = User.query.filter_by(username=oidc_username).first()
if not user:
@ -379,55 +297,17 @@ def login():
firstname=oidc_givenname,
lastname=oidc_familyname,
email=oidc_email)
result = user.create_local_user()
else:
user.firstname = oidc_givenname
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)
if not result['status']:
session.pop('oidc_token', None)
return redirect(url_for('index.login'))
session['user_id'] = user.id
session['authentication_type'] = 'OAuth'
return authenticate_user(user, 'OIDC OAuth')
login_user(user, remember=False)
signin_history(user.username, 'OIDC OAuth', True)
return redirect(url_for('index.index'))
if request.method == 'GET':
return render_template('login.html', saml_enabled=SAML_ENABLED)
@ -487,36 +367,9 @@ def login():
saml_enabled=SAML_ENABLED,
error='Token required')
if Setting().get('autoprovisioning') and auth_method!='LOCAL':
urn_value=Setting().get('urn_value')
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
login_user(user, remember=remember_me)
signin_history(user.username, 'LOCAL', True)
return redirect(session.get('next', url_for('index.index')))
def clear_session():
@ -524,7 +377,6 @@ def clear_session():
session.pop('github_token', None)
session.pop('google_token', None)
session.pop('authentication_type', None)
session.pop('remote_user', None)
session.clear()
logout_user()
@ -559,38 +411,6 @@ def signin_history(username, authenticator, success):
}),
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')
def logout():
@ -614,38 +434,8 @@ def logout():
"urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress",
session_index=session['samlSessionIndex'],
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()
# 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)
return redirect(url_for('index.login'))
@index_bp.route('/register', methods=['GET', 'POST'])
@ -654,12 +444,12 @@ def register():
if request.method == 'GET':
return render_template('register.html')
elif request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
firstname = request.form.get('firstname', '').strip()
lastname = request.form.get('lastname', '').strip()
email = request.form.get('email', '').strip()
rpassword = request.form.get('rpassword', '')
username = request.form['username']
password = request.form['password']
firstname = request.form.get('firstname')
lastname = request.form.get('lastname')
email = request.form.get('email')
rpassword = request.form.get('rpassword')
if not username or not password or not email:
return render_template(
@ -681,12 +471,7 @@ def register():
if result and result['status']:
if Setting().get('verify_user_email'):
send_account_verification(email)
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'))
return redirect(url_for('index.login'))
else:
return render_template('register.html',
error=result['msg'])
@ -696,28 +481,6 @@ def register():
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'])
def confirm_email(token):
email = confirm_token(token)
@ -856,8 +619,7 @@ def dyndns_update():
msg=
"DynDNS update: attempted update of {0} but record already up-to-date"
.format(hostname),
created_by=current_user.username,
domain_id=domain.id)
created_by=current_user.username)
history.add()
else:
oldip = r.data
@ -872,8 +634,7 @@ def dyndns_update():
"old_value": oldip,
"new_value": str(ip)
}),
created_by=current_user.username,
domain_id=domain.id)
created_by=current_user.username)
history.add()
response = 'good'
else:
@ -912,8 +673,7 @@ def dyndns_update():
"record": hostname,
"value": str(ip)
}),
created_by=current_user.username,
domain_id=domain.id)
created_by=current_user.username)
history.add()
response = 'good'
else:
@ -1028,7 +788,7 @@ def saml_authorized():
else:
user_groups = []
if admin_attribute_name or group_attribute_name:
user_accounts = set(user.get_accounts())
user_accounts = set(user.get_account())
saml_accounts = []
for group_mapping in group_to_account_mapping:
mapping = group_mapping.split('=')
@ -1071,7 +831,9 @@ def saml_authorized():
user.plain_text_password = None
user.update_profile()
session['authentication_type'] = 'SAML'
return authenticate_user(user, 'SAML')
login_user(user, remember=False)
signin_history(user.username, 'SAML', True)
return redirect(url_for('index.login'))
else:
return render_template('errors/SAML.html', errors=errors)
@ -1087,7 +849,7 @@ def create_group_to_account_mapping():
return group_to_account_mapping
def handle_account(account_name, account_description=""):
def handle_account(account_name):
clean_name = ''.join(c for c in account_name.lower()
if c in "abcdefghijklmnopqrstuvwxyz0123456789")
if len(clean_name) > Account.name.type.length:
@ -1096,16 +858,13 @@ def handle_account(account_name, account_description=""):
account = Account.query.filter_by(name=clean_name).first()
if not account:
account = Account(name=clean_name.lower(),
description=account_description,
description='',
contact='',
mail='')
account.create_account()
history = History(msg='Account {0} created'.format(account.name),
created_by='OIDC/SAML Assertion')
created_by='SAML Assertion')
history.add()
else:
account.description = account_description
account.update_account()
return account

View file

@ -1,4 +1,7 @@
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_login import current_user, login_required, login_manager
@ -38,13 +41,16 @@ def profile():
return render_template('user_profile.html')
if request.method == 'POST':
if session['authentication_type'] == 'LOCAL':
firstname = request.form.get('firstname', '').strip()
lastname = request.form.get('lastname', '').strip()
email = request.form.get('email', '').strip()
new_password = request.form.get('password', '')
firstname = request.form[
'firstname'] if 'firstname' in request.form else ''
lastname = request.form[
'lastname'] if 'lastname' in request.form else ''
email = request.form['email'] if 'email' in request.form else ''
new_password = request.form[
'password'] if 'password' in request.form else ''
else:
firstname = lastname = email = new_password = ''
current_app.logger.warning(
logging.warning(
'Authenticated externally. User {0} information will not allowed to update the profile'
.format(current_user.username))
@ -91,9 +97,13 @@ def qrcode():
if not current_user:
return redirect(url_for('index'))
return current_user.get_qrcode_value(), 200, {
img = qrc.make(current_user.get_totp_uri(),
image_factory=qrc_svg.SvgPathImage)
stream = BytesIO()
img.save(stream)
return stream.getvalue(), 200, {
'Content-Type': 'image/svg+xml',
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': '0'
}
}

View file

@ -1,3 +1,3 @@
from authlib.integrations.flask_client import OAuth\
from authlib.flask.client import OAuth
authlib_oauth_client = OAuth()
authlib_oauth_client = OAuth()

View file

@ -104,10 +104,10 @@ class SAML(object):
settings['sp']['entityId'] = current_app.config['SAML_SP_ENTITY_ID']
if ('SAML_CERT' in current_app.config) and ('SAML_KEY' in current_app.config):
if ('SAML_CERT_FILE' in current_app.config) and ('SAML_KEY_FILE' in current_app.config):
saml_cert_file = current_app.config['SAML_CERT']
saml_key_file = current_app.config['SAML_KEY']
saml_cert_file = current_app.config['SAML_CERT_FILE']
saml_key_file = current_app.config['SAML_KEY_FILE']
if os.path.isfile(saml_cert_file):
cert = open(saml_cert_file, "r").readlines()

View file

@ -1,102 +0,0 @@
/* 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;
}

View file

@ -53,7 +53,6 @@ function applyRecordChanges(data, domain) {
var modal = $("#modal_success");
modal.find('.modal-body p').text("Applied changes successfully");
modal.modal('show');
setTimeout(() => {window.location.reload()}, 2000);
},
error : function(jqXHR, status) {
@ -285,14 +284,4 @@ function timer(elToUpdate, maxTime) {
}, 1000);
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

View file

@ -84,7 +84,7 @@
<select multiple="multiple" class="form-control" id="account_multi_user"
name="account_multi_user">
{% for user in users %}
<option {% if user.id in account_user_ids|default([]) %}selected{% endif %}
<option {% if user.id in account_user_ids %}selected{% endif %}
value="{{ user.username }}">{{ user.username }}</option>
{% endfor %}
</select>
@ -162,4 +162,4 @@
}
});
</script>
{% endblock %}
{% endblock %}

View file

@ -1,283 +0,0 @@
{% 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">&times;</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">&times;</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 %}

View file

@ -13,12 +13,7 @@
<li class="active">History</li>
</ol>
</section>
{% endblock %}
{% block content %}
{% import 'applied_change_macro.html' as applied_change_macro %}
{% endblock %} {% block content %}
<section class="content">
<div class="row">
<div class="col-xs-12">
@ -33,134 +28,32 @@
Clear History&nbsp;<i class="fa fa-trash"></i>
</button>
</div>
<div class="box-body clearfix">
<form id="history-search-form" autocomplete="off">
<!-- Custom Tabs -->
<div class="nav-tabs-custom" id="tabs">
<ul class="nav nav-tabs" id="nav_nav_tabs" name="nav_nav_tabs">
<li id="activity_tab" class="active"><a href="#tabs-act" data-toggle="tab">Search for All Activity</a></li>
<li id="domain_tab"><a href="#tabs-domain" data-toggle="tab">Search By Domain</a></li>
<li id="account_tab"><a href="#tabs-account" data-toggle="tab">Search By Account</a></li>
{% if current_user.role.name != 'User' %}
<li id="user_auth_tab"><a href="#tabs-auth" data-toggle="tab">Search for User Authentication</a></li>
{% endif %}
</ul>
<div class="tab-content">
<div class="tab-pane" id="tabs-act">
</div>
<div class="tab-pane" id="tabs-domain">
<td><label>Domain Name</label></td>
<td>
<div class="autocomplete" style="width:250px;">
<input type="text" class="form-control" id="domain_name_filter" name="domain_name_filter" placeholder="Enter * to search for any domain" value="">
</div>
</td>
<td>
<div style="position: relative; top:10px;">
<td>Record Changelog only &nbsp</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: &nbsp</td>
<td>&nbsp 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>&nbsp 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>&nbsp 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>&nbsp 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: &nbsp</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: &nbsp</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: &nbsp</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>&nbsp</td></tr>
<tr><td>&nbsp</td></tr>
<tr>
<td>
<button type="submit" id="search-submit" name="search-submit" class="btn btn-flat btn-primary button-filter">Search&nbsp;<i class="fa fa-search"></i></button>
</td>
<td>
<!-- &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -->
<button id="clear-filters" name="clear-filters" class="btn btn-flat btn-warning button-clearf">Clear Filters&nbsp;<i class="fa fa-trash"></i></button>
</td>
</tr>
</tbody>
</table>
</div>
</form>
<div class="box-body">
<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.created_by }}</td>
<td>{{ history.msg }}</td>
<td>{{ history.created_on }}</td>
<td width="6%">
<button type="button" class="btn btn-flat btn-primary history-info-button"
value='{{ history.detail }}'>Info&nbsp;<i class="fa fa-info"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div id="table_from_ajax"></div>
<!-- /.box-body -->
</div>
<!-- /.box -->
@ -172,304 +65,31 @@
{% endblock %}
{% block extrascripts %}
<script>
/* Don't let user search with a blank main field */
var canSearch=true;
$(document).ready(function () {
$.ajax({
url: "/admin/history_table",
type: "get",
success: function(response) {
console.log('Submission was successful.');
$("#table_from_ajax").html(response);
// set up history data table
$("#tbl_history").DataTable({
"paging": true,
"lengthChange": false,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false,
"order": [
[2, "desc"]
],
"columnDefs": [{
"type": "time",
"render": function (data, type, row) {
return moment.utc(data).local().format('YYYY-MM-DD HH:mm:ss');
},
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")
});
"targets": 2
}]
});
$('.checkbox,.radio').iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue',
increaseArea: '20%'
$(document.body).on('click', '.history-info-button', function () {
var modal = $("#modal_history_info");
var info = $(this).val();
$('#modal-code-content').html(json_library.prettyPrint(info));
modal.modal('show');
});
//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>
{% endblock %}
{% block modals %}
@ -507,7 +127,7 @@
<h4 class="modal-title">History Details</h4>
</div>
<div class="modal-body">
<div id="modal-info-content"></div>
<pre><code id="modal-code-content"></code></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-right" data-dismiss="modal">Close</button>
@ -518,4 +138,4 @@
<!-- /.modal-dialog -->
</div>
<!-- /.modal -->
{% endblock %}
{% endblock %}

View file

@ -1,90 +0,0 @@
{% 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&nbsp;<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>

View file

@ -1,133 +0,0 @@
{% 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&nbsp;<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&nbsp;<i class="fa fa-lock"></i>
</button>
<button type="button" class="btn btn-flat btn-danger button_delete"
id="{{ key.id }}">
Delete&nbsp;<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">&times;</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 %}

View file

@ -51,7 +51,7 @@
<div class="nav-tabs-custom" id="tabs">
<ul class="nav nav-tabs">
<li class="active"><a href="#tabs-general" data-toggle="tab">General</a></li>
<li><a href="#tabs-ldap" data-toggle="tab">LDAP</a></li>
<li class="active"><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-github" data-toggle="tab">Github OAuth</a></li>
<li><a href="#tabs-azure" data-toggle="tab">Microsoft OAuth</a></li>
@ -73,19 +73,11 @@
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
</form>
</div>
<div class="tab-pane" id="tabs-ldap">
<div class="tab-pane active" id="tabs-ldap">
<div class="row">
<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">&times;</button>
<h4><i class="icon fa fa-ban"></i> Error!</h4>
{{ error }}
</div>
{% endif %}
<form role="form" method="post" data-toggle="validator">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="hidden" value="ldap" name="config_tab" />
@ -194,46 +186,6 @@
<span class="help-block with-errors"></span>
</div>
</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>
&nbsp;&nbsp;&nbsp;
<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>
&nbsp;&nbsp;&nbsp;
<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">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
@ -309,24 +261,6 @@
</li>
</ul>
</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>
</div>
</div>
@ -522,41 +456,6 @@
<span class="help-block with-errors"></span>
</div>
</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>
&nbsp;&nbsp;&nbsp;
<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">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
</div>
@ -577,7 +476,6 @@
<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>
</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>
@ -604,6 +502,9 @@
<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>
</div>
</fieldset>
<fieldset>
<legend>ADVANCE</legend>
<div class="form-group">
<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') }}">
@ -624,47 +525,6 @@
<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>
</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>
<div class="form-group">
<button type="submit" class="btn btn-flat btn-primary">Save</button>
@ -691,7 +551,7 @@
{%- endassets %}
<script>
$(function() {
$('#tabs').tabs({
// add url anchor tags
@ -714,11 +574,6 @@
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
$('#autoprovisioning').iCheck({
checkboxClass : 'icheckbox_square-blue',
increaseArea : '20%'
})
// END: General tab js
// START: LDAP tab js
@ -750,10 +605,7 @@
$('#ldap_operator_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 {
$('#ldap_uri').prop('required', false);
$('#ldap_base_dn').prop('required', false);
@ -769,10 +621,6 @@
$('#ldap_operator_group').prop('required', false);
$('#ldap_user_group').prop('required', false);
}
if ($('#autoprovisioning').is(":checked")) {
$('#autoprovisioning_attribute').prop('required', false);
$('#urn_value').prop('required', true);
}
}
});
@ -786,75 +634,8 @@
$('#ldap_operator_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(){
if ($('#ldap').is(":checked") && $('#ldap_enabled').is(":checked")) {
$('#ldap_admin_group').prop('required', true);
@ -892,14 +673,7 @@
$('#ldap_operator_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 %}
// END: LDAP tab js
// START: Google tab js
@ -1018,10 +792,6 @@
$('#oidc_oauth_api_url').prop('required', true);
$('#oidc_oauth_token_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 {
$('#oidc_oauth_key').prop('required', false);
$('#oidc_oauth_secret').prop('required', false);
@ -1029,10 +799,6 @@
$('#oidc_oauth_api_url').prop('required', false);
$('#oidc_oauth_token_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
@ -1043,60 +809,8 @@
$('#oidc_oauth_api_url').prop('required', true);
$('#oidc_oauth_token_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 %}
//END: OIDC Tab JS
</script>
{% 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">&times;</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">&times;</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 %}

View file

@ -1,133 +0,0 @@
{% 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 %}

View file

@ -4,27 +4,21 @@
{% block head %}
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
<link rel="icon" href="/static/img/favicon.png">
{% block title %}<title>{{ SITE_NAME }}</title>{% endblock %}
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/style.css') }}">
<!-- Get Google Fonts we like -->
{% if OFFLINE_MODE %}
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/source_sans_pro.css') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='assets/css/roboto_mono.css') }}">
<link rel="stylesheet" href="/static/assets/css/source_sans_pro.css">
<link rel="stylesheet" href="/static/assets/css/roboto_mono.css">
{% else %}
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Roboto+Mono:400,300,700">
{% endif %}
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Tell Safari to not recognise telephone numbers -->
<meta name="format-detection" content="telephone=no">
{% assets "css_main" -%}
<link rel="stylesheet" href="{{ ASSET_URL }}">
{%- 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 -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
@ -35,8 +29,8 @@
</head>
<body class="hold-transition skin-blue sidebar-mini {% if not SETTING.get('fullscreen_layout') %}layout-boxed{% endif %}">
{% if OFFLINE_MODE %}
{% set gravatar_url = url_for('static', filename='img/gravatar.png') %}
{% elif current_user.email is defined %}
{% set gravatar_url = "/static/img/gravatar.png" %}
{% else %}
{% set gravatar_url = current_user.email|email_to_gravatar_url(size=80) %}
{% endif %}
<div class="wrapper">
@ -118,11 +112,6 @@
<a href="{{ url_for('domain.add') }}"><i class="fa fa-plus"></i> <span>New Domain</span></a>
</li>
{% 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'] %}
<li class="header">ADMINISTRATION</li>
<li class="{{ 'active' if active_page == 'admin_console' else '' }}">
@ -142,10 +131,7 @@
</li>
<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>
</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>
<li class="{{ 'treeview active' if active_page == 'admin_settings' else 'treeview' }}">
<a href="#">
<i class="fa fa-cog"></i> <span>Settings</span>
@ -162,11 +148,6 @@
{% endif %}
</ul>
</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 %}
</ul>
{% endif %}

View file

@ -2,7 +2,6 @@
{% set active_page = "dashboard" %}
{% block title %}<title>Dashboard - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<!-- Content Header (Page header) -->
<section class="content-header">
@ -17,12 +16,10 @@
</section>
{% endblock %}
{% import 'applied_change_macro.html' as applied_change_macro %}
{% block content %}
<!-- Main content -->
<section class="content">
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<div class="row">
<div class="col-xs-3">
<div class="box">
@ -43,7 +40,6 @@
</div>
</div>
</div>
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<div class="col-lg-6">
<a href="{{ url_for('admin.manage_user') }}">
<div class="small-box bg-green">
@ -57,7 +53,6 @@
</div>
</a>
</div>
{% endif %}
</div>
<div class="row">
<div class="col-lg-6">
@ -73,7 +68,6 @@
</div>
</a>
</div>
{% if current_user.role.name in ['Administrator', 'Operator'] %}
<div class="col-lg-6">
<a href="{{ url_for('admin.pdns_stats') }}">
<div class="small-box bg-green">
@ -87,7 +81,6 @@
</div>
</a>
</div>
{% endif %}
</div>
</div>
</div>
@ -110,25 +103,13 @@
<tbody>
{% for history in histories %}
<tr class="odd">
<td>{{ history.history.created_by }}</td>
<td>{{ history.history.msg }}</td>
<td>{{ history.history.created_on }}</td>
<td>{{ history.created_by }}</td>
<td>{{ history.msg }}</td>
<td>{{ 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&nbsp;<i class="fa fa-info"></i>
</button>
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail }}'>
Info&nbsp;<i class="fa fa-info"></i>
</button>
</td>
</tr>
{% endfor %}
@ -241,11 +222,11 @@
]
});
$(document.body).on('click', '.history-info-button', function () {
$(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);
var info = $(this).val();
$('#modal-code-content').html(json_library.prettyPrint(info));
modal.modal('show');
});
@ -312,7 +293,7 @@
<h4 class="modal-title">History Details</h4>
</div>
<div class="modal-body">
<div id="modal-info-content"></div>
<pre><code id="modal-code-content"></code></pre>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-default pull-right"

View file

@ -1,5 +1,5 @@
{% macro name(domain) %}
<a href="{{ url_for('domain.domain', domain_name=domain.name) }}"><strong>{{ domain.name | pretty_domain_name }}</strong></a>
<a href="{{ url_for('domain.domain', domain_name=domain.name) }}"><strong>{{ domain.name }}</strong></a>
{% endmacro %}
{% macro dnssec(domain) %}
@ -15,11 +15,11 @@
{% endmacro %}
{% 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 %}
{% macro master(domain) %}
{% if domain.master == '[]'%}-{% else %}{{ domain.master | display_master_name }}{% endif %}
{% if domain.master == '[]'%}-{% else %}{{ domain.master|display_master_name }}{% endif %}
{% endmacro %}
{% macro account(domain) %}
@ -40,20 +40,12 @@
<button type="button" class="btn btn-flat btn-danger" onclick="window.location.href='{{ url_for('domain.setting', domain_name=domain.name) }}'">
Admin&nbsp;<i class="fa fa-cog"></i>
</button>
<button type="button" class="btn btn-flat btn-primary" onclick="window.location.href='{{ url_for('domain.changelog', domain_name=domain.name) }}'">
Changelog&nbsp;<i class="fa fa-history" aria-hidden="true"></i>
</button>
</td>
{% else %}
<td width="6%">
<button type="button" class="btn btn-flat btn-success" onclick="window.location.href='{{ url_for('domain.domain', domain_name=domain.name) }}'">
Manage&nbsp;<i class="fa fa-cog"></i>
</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&nbsp;<i class="fa fa-history" aria-hidden="true"></i>
</button>
{% endif %}
</td>
{% endif %}
{% endif %}
{% endmacro %}

64
powerdnsadmin/templates/domain.html Executable file → Normal file
View file

@ -1,16 +1,16 @@
{% extends "base.html" %}
{% block title %}<title>{{ domain.name | pretty_domain_name }} - {{ SITE_NAME }}</title>{% endblock %}
{% block title %}<title>{{ domain.name }} - {{ SITE_NAME }}</title>{% endblock %}
{% block dashboard_stat %}
<section class="content-header">
<h1>
Manage domain: <b>{{ domain.name | pretty_domain_name }}</b>
Manage domain: <b>{{ domain.name }}</b>
</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>
<li class="active">{{ domain.name }}</li>
</ol>
</section>
{% endblock %}
@ -33,17 +33,6 @@
Update from Master&nbsp;<i class="fa fa-download"></i>
</button>
{% 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&nbsp;<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&nbsp;<i class="fa fa-history" aria-hidden="true"></i>
</i>
</button>
{% endif %}
</div>
<div class="box-body">
<table id="tbl_records" class="table table-bordered table-striped">
@ -57,16 +46,13 @@
<th>Comment</th>
<th>Edit</th>
<th>Delete</th>
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
<th >Changelog</th>
{% endif %}
</tr>
</thead>
<tbody>
{% for record in records %}
<tr class="odd row_record" id="{{ domain.name }}">
<td>
{{ (record.name,domain.name) | display_record_name | pretty_domain_name }}
{{ (record.name,domain.name)|display_record_name }}
</td>
<td>
{{ record.type }}
@ -78,7 +64,7 @@
{{ record.ttl }}
</td>
<td>
{{ record.data | pretty_domain_name }}
{{ record.data }}
</td>
<td>
{{ record.comment }}
@ -88,7 +74,7 @@
{% if record.is_allowed_edit() %}
<button type="button" class="btn btn-flat btn-warning button_edit">Edit&nbsp;<i class="fa fa-edit"></i></button>
{% else %}
<button type="button" class="btn btn-flat btn-warning">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i></button>
<button type="button" class="btn btn-flat btn-warning"">&nbsp;&nbsp;<i class="fa fa-exclamation-circle"></i>&nbsp;&nbsp;</button>
{% endif %}
</td>
<td width="6%">
@ -105,13 +91,6 @@
</td>
{% endif %}
</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">&nbsp;&nbsp;
<i class="fa fa-history" aria-hidden="true"></i>&nbsp;&nbsp;&nbsp;
</button>
</td>
{% endif %}
<!-- hidden column that we can sort on -->
<td>1</td>
</tr>
@ -131,8 +110,8 @@
{% block extrascripts %}
<script>
// superglobals
window.records_allow_edit = {{ editable_records | tojson }};
window.ttl_options = {{ ttl_options | tojson }};
window.records_allow_edit = {{ editable_records|tojson }};
window.ttl_options = {{ ttl_options|tojson }};
window.nEditing = null;
window.nNew = false;
@ -144,7 +123,7 @@
"ordering" : true,
"info" : true,
"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],
[5, 15, 20, "All"]],
{% else %}
@ -165,33 +144,14 @@
// hidden column so that we can add new records on top
// regardless of whatever sorting is done. See orderFixed
visible: false,
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
targets: [ 9 ]
{% else %}
targets: [ 8 ]
{% endif %}
},
{
className: "length-break",
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']]
{% 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
@ -283,11 +243,7 @@
// add new row
var default_type = records_allow_edit[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 %}
var nRow = jQuery('#tbl_records').dataTable().fnAddData(['', default_type, 'Active', 3600, '', '', '', '', '0']);
editRow($("#tbl_records").DataTable(), nRow);
document.getElementById("edit-row-focus").focus();
nEditing = nRow;

View file

@ -1,116 +0,0 @@
{% 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}} &nbsp {{ 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 &nbsp;<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 %}

View file

@ -1,129 +0,0 @@
{% 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">&times;</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 %}

View file

@ -19,7 +19,7 @@
{% endif %}
<section class="content-header">
<h1>
Manage domain <small>{{ domain.name | pretty_domain_name }}</small>
Manage domain <small>{{ domain.name }}</small>
</h1>
<ol class="breadcrumb">
<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="col-xs-2">
<p>Users on the right have access to manage the records in
the {{ domain.name | pretty_domain_name }} domain.</p>
the {{ domain.name }} domain.</p>
<p>Click on users to move from between columns.</p>
<p>
Users in <font style="color: red;">red</font> are Administrators
@ -94,7 +94,7 @@
{% endfor %}
</select><br />
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
<i class="fa fa-check"></i>&nbsp;Change account for {{ domain.name | pretty_domain_name }}
<i class="fa fa-check"></i>&nbsp;Change account for {{ domain.name }}
</button>
</form>
</div>
@ -113,11 +113,9 @@
<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 %}
{% if SETTING.get('auto_ptr') %}disabled="True" {% endif %}>
&nbsp;Allow automatic reverse pointer creation on record updates?
{% if SETTING.get('auto_ptr') %}
<br/><code>Auto-ptr is enabled globally on the PDA system!</code>
{% endif %}
</p>
&nbsp;Allow automatic reverse pointer creation on record updates?{% if
SETTING.get('auto_ptr') %}</br><code>Auto-ptr is enabled globally on the PDA
system!</code>{% endif %}</p>
</div>
</div>
</div>
@ -173,7 +171,7 @@
placeholder="Enter valid master ip addresses (separated by commas)">
</div>
<button type="submit" class="btn btn-flat btn-primary" id="change_type">
<i class="fa fa-check"></i>&nbsp;Change type for {{ domain.name | pretty_domain_name }}
<i class="fa fa-check"></i>&nbsp;Change type for {{ domain.name }}
</button>
</form>
</div>
@ -216,7 +214,7 @@
<option>OFF</option>
</select><br />
<button type="submit" class="btn btn-flat btn-primary" id="change_soa_edit_api">
<i class="fa fa-check"></i>&nbsp;Change SOA-EDIT-API setting for {{ domain.name | pretty_domain_name }}
<i class="fa fa-check"></i>&nbsp;Change SOA-EDIT-API setting for {{ domain.name }}
</button>
</form>
</div>
@ -235,7 +233,7 @@
reverted.</p>
<button type="button" class="btn btn-flat btn-danger pull-left delete_domain"
id="{{ domain.name }}">
<i class="fa fa-trash"></i>&nbsp;DELETE DOMAIN {{ domain.name | pretty_domain_name }}
<i class="fa fa-trash"></i>&nbsp;DELETE DOMAIN {{ domain.name }}
</button>
</div>
</div>
@ -371,4 +369,4 @@
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}
{% endblock %}

View file

@ -1,50 +1,40 @@
<!DOCTYPE html>
<html>
{% extends "base.html" %}
{% block title %}<title>Email verification - {{ SITE_NAME }}</title>{% endblock %}
<head>
<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 dashboard_stat %}
{% endblock %}
<body class="hold-transition register-page">
<section class="content">
<div class="error-page">
<div class="error-content">
{% if status == 1 %}
<h3>
<i class="fa fa-thumbs-o-up text-success"></i> Email verification successful!
</h3>
<p>
You have confirmed your account. <a href="{{ url_for('index.login') }}">Click here</a> to login.
</p>
{% elif status == 2 %}
<h3>
<i class="fa fa-hand-stop-o text-info"></i> Already verified!
</h3>
<p>
You have confirmed your account already. <a href="{{ url_for('index.login') }}">Click here</a> to login.
</p>
{% else %}
<h3>
<i class="fa fa-warning text-yellow"></i> Email verification failed!
</h3>
<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.
</p>
{% endif %}
</div>
<!-- /.error-content -->
{% block content %}
<!-- Main content -->
<section class="content">
<div class="error-page">
<div class="error-content">
{% if status == 1 %}
<h3>
<i class="fa fa-thumbs-o-up text-success"></i> Email verification successful!
</h3>
<p>
You have confirmed your account. <a href="{{ url_for('index.login') }}">Click here</a> to login.
</p>
{% elif status == 2 %}
<h3>
<i class="fa fa-hand-stop-o text-info"></i> Already verified!
</h3>
<p>
You have confirmed your account already. <a href="{{ url_for('index.login') }}">Click here</a> to login.
</p>
{% else %}
<h3>
<i class="fa fa-warning text-yellow"></i> Email verification failed!
</h3>
<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.
</p>
{% endif %}
</div>
<!-- /.error-page -->
</section>
</body>
</html>
<!-- /.error-content -->
</div>
<!-- /.error-page -->
</section>
<!-- /.content -->
{% endblock %}

View file

@ -5,15 +5,12 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<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 -->
<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 %}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
@ -48,11 +45,9 @@
data-error="Please input your password" required {% if password %}value="{{ password }}" {% endif %}>
<span class="help-block with-errors"></span>
</div>
{% if SETTING.get('otp_field_enabled') %}
<div class="form-group">
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken" autocomplete="off">
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
</div>
{% endif %}
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
<div class="form-group">
<select class="form-control" name="auth_method">

View file

@ -37,7 +37,7 @@
<h1>We&rsquo;ll be back soon!</h1>
<div>
<p>Sorry for the inconvenience but we&rsquo;re performing some maintenance at the moment. Please contact the System
Administrator if you need more information, otherwise we&rsquo;ll be back online shortly!</p>
Administrator if you need more information</a>, otherwise we&rsquo;ll be back online shortly!</p>
<p>&mdash; Team</p>
</div>
</article>
</article>

View file

@ -5,7 +5,6 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<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 -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
{% assets "css_login" -%}

View file

@ -1,90 +0,0 @@
<!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">&times;</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>

View file

@ -1,52 +1,30 @@
<!DOCTYPE html>
<html>
{% extends "base.html" %}
{% block title %}<title>Resend confirmation email - {{ SITE_NAME }}</title>{% endblock %}
<head>
<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 dashboard_stat %}
{% endblock %}
<body class="hold-transition register-page">
<div class="register-box">
<div class="register-logo">
<a href="{{ url_for('index.index') }}"><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">&times;</button>
{{ error }}
</div>
{% endif %}
{% block content %}
<!-- Main content -->
<section class="content">
<div class="error-page">
<div class="error-content">
<h3>
<i class="fa fa-hand-o-right text-info"></i> Resend a confirmation email
</h3>
<p>
Enter your email address to get account confirmation link.
Enter your email address to get new account confirmation link.
</p>
<form method="post" data-toggle="validator">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<div class="form-group has-feedback">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="email" name="email" class="form-control" placeholder="Email address" data-error="Please input your email" required>
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
<span class="help-block with-errors"></span>
</div>
<div class="row">
<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">
<form class="search-form" method="post">
<div class="input-group">
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
<input type="text" name="email" class="form-control" placeholder="Email address" data-error="Please input your email" required>
<div class="input-group-btn">
<button type="submit" name="submit" class="btn btn-success btn-flat"><i class="fa fa-mail-reply"></i>
</button>
</div>
</div>
<!-- /.input-group -->
<p>
{% if status == 0 %}
<font color="red">Email not found!</font>
@ -56,31 +34,11 @@
<font color="green">Confirmation email sent!</font>
{% endif %}
</p>
</form>
</div>
</form>
<!-- /.error-content -->
</div>
<!-- /.form-box -->
<div class="login-box-footer">
<center>
<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>
<!-- /.error-page -->
</section>
<!-- /.content -->
{% endblock %}

View file

@ -51,7 +51,7 @@
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>
<div class="form-group">
<label for="email">E-mail</label> <input type="email" class="form-control"
<label for="email">E-mail</label> <input type="text" class="form-control"
name="email" id="email" placeholder="{{ current_user.email }}"
{% if session['authentication_type'] != 'LOCAL' %}disabled{% endif %}>
</div>{% if session['authentication_type'] == 'LOCAL' %}
@ -93,14 +93,6 @@
{% if current_user.otp_secret %}
<div id="token_information">
<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"
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
- <a target="_blank"
@ -111,8 +103,8 @@
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
on your smartphone to scan the QR code.
<br />
<font color="red"><strong><i>Make sure only you can see this QR Code and secret key and
nobody can capture them.</i></strong></font>
<font color="red"><strong><i>Make sure only you can see this QR Code and
nobody can capture it.</i></strong></font>
</div>
{% endif %}
</div>

View file

@ -1,30 +1,29 @@
Flask==1.1.2
Flask-Assets==2.0
Flask-Login==0.5.0
Flask-SQLAlchemy==2.4.4
Flask-Migrate==2.5.3
SQLAlchemy==1.3.19
mysqlclient==2.0.1
Flask==1.1.1
Flask-Assets==0.12
Flask-Login==0.4.1
Flask-SQLAlchemy==2.4.1
Flask-Migrate==2.5.2
SQLAlchemy==1.3.11
mysqlclient==1.4.6
configobj==5.0.6
bcrypt>=3.1.7
requests==2.24.0
python-ldap==3.4.0
pyotp==2.4.0
qrcode==6.1
dnspython>=1.16.0
bcrypt==3.1.4
requests==2.20.0
python-ldap==3.1.0
pyotp==2.2.6
qrcode==6.0
dnspython==1.15.0
gunicorn==20.0.4
python3-saml
pyOpenSSL==19.1.0
pytz==2020.1
pyOpenSSL>=0.15
pytz>=2017.3
cssmin==0.2.0
jsmin==3.0.0
Authlib==0.15
jsmin==2.2.2
Authlib==0.10
Flask-SeaSurf==0.2.2
bravado-core==5.17.0
bravado-core==5.13.1
lima==0.5
pytest==6.1.1
pytest==5.0.1
pytimeparse==1.1.8
PyYAML==5.4
PyYAML==5.1.1
Flask-SSLify==0.1.5
Flask-Mail==0.9.1
flask-session==0.3.2

View file

@ -38,16 +38,6 @@ def load_data(setting_name, *args, **kwargs):
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
def basic_auth_admin_headers():
test_admin_user = app.config.get('TEST_ADMIN_USER')
@ -294,29 +284,3 @@ def create_apikey_headers(passw):
user_pass_base64 = b64encode(passw.encode('utf-8'))
headers = {"X-API-KEY": "{0}".format(user_pass_base64.decode('utf-8'))}
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

View file

@ -1,54 +0,0 @@
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

View file

@ -1,367 +0,0 @@
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

View file

@ -1,252 +0,0 @@
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

View file

@ -1,31 +0,0 @@
#!/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()

View file

@ -29,6 +29,6 @@ with app.app_context():
sys.exit(1)
### Start the update process
app.logger.info('Update domains from nameserver API')
app.logger.info('Update zones from nameserver API')
Domain().update()
d = Domain().update()

View file

@ -102,10 +102,9 @@ base64-js@^1.0.2:
version "1.3.0"
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.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0:
version "4.11.8"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f"
bootstrap-colorpicker@^2.5.3:
version "2.5.3"
@ -152,10 +151,9 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brorand@^1.0.1, brorand@^1.1.0:
brorand@^1.0.1:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
integrity sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=
browser-pack@^6.0.1:
version "6.1.0"
@ -490,17 +488,16 @@ duplexer2@^0.1.2, duplexer2@~0.1.0, duplexer2@~0.1.2:
readable-stream "^2.0.2"
elliptic@^6.0.0:
version "6.5.4"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.4.tgz#da37cebd31e79a1367e941b592ed1fbebd58abbb"
integrity sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==
version "6.4.0"
resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df"
dependencies:
bn.js "^4.11.9"
brorand "^1.1.0"
bn.js "^4.4.0"
brorand "^1.0.1"
hash.js "^1.0.0"
hmac-drbg "^1.0.1"
inherits "^2.0.4"
minimalistic-assert "^1.0.1"
minimalistic-crypto-utils "^1.0.1"
hmac-drbg "^1.0.0"
inherits "^2.0.1"
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.0"
eve-raphael@0.5.0:
version "0.5.0"
@ -568,17 +565,15 @@ hash-base@^3.0.0:
safe-buffer "^5.0.1"
hash.js@^1.0.0, hash.js@^1.0.3:
version "1.1.7"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.7.tgz#0babca538e8d4ee4a0f8988d68866537a003cf42"
integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
version "1.1.3"
resolved "https://registry.yarnpkg.com/hash.js/-/hash.js-1.1.3.tgz#340dedbe6290187151c1ea1d777a3448935df846"
dependencies:
inherits "^2.0.3"
minimalistic-assert "^1.0.1"
minimalistic-assert "^1.0.0"
hmac-drbg@^1.0.1:
hmac-drbg@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
integrity sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=
dependencies:
hash.js "^1.0.3"
minimalistic-assert "^1.0.0"
@ -607,19 +602,14 @@ inflight@^1.0.4:
once "^1.3.0"
wrappy "1"
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.4"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.1, inherits@~2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
inherits@2.0.1:
version "2.0.1"
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:
version "0.6.2"
resolved "https://registry.yarnpkg.com/inline-source-map/-/inline-source-map-0.6.2.tgz#f9393471c18a79d1724f863fa38b586370ade2a5"
@ -763,15 +753,13 @@ miller-rabin@^4.0.0:
bn.js "^4.0.0"
brorand "^1.0.1"
minimalistic-assert@^1.0.0, minimalistic-assert@^1.0.1:
minimalistic-assert@^1.0.0:
version "1.0.1"
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.1:
minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a"
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
minimatch@^3.0.4:
version "3.0.4"