Compare commits
1 commit
master
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
cdbab241f5 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
github: [ngoduykhanh]
|
56
.github/workflows/build-and-publish.yml
vendored
56
.github/workflows/build-and-publish.yml
vendored
|
@ -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 }}
|
5
.travis.yml
Normal file
5
.travis.yml
Normal 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
|
|
@ -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)
|
||||
|
||||
|
@ -58,3 +59,7 @@ You can then access PowerDNS-Admin by pointing your browser to http://localhost:
|
|||
## LICENSE
|
||||
MIT. See [LICENSE](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/LICENSE)
|
||||
|
||||
## Support
|
||||
If you like the project and want to support it, you can *buy me a coffee* ☕
|
||||
|
||||
<a href="https://www.buymeacoffee.com/khanhngo" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -65,6 +65,14 @@ 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 -L packaging && \
|
||||
pip uninstall -y pip-autoremove && \
|
||||
apk del ${BUILD_DEPENDENCIES}
|
||||
|
||||
# Build image
|
||||
FROM alpine:3.13
|
||||
|
||||
|
|
123
docs/API.md
123
docs/API.md
|
@ -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
|
||||
```
|
||||
|
|
|
@ -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.
|
|
@ -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 ###
|
|
@ -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",
|
||||
|
|
|
@ -55,8 +55,6 @@ def create_app(config=None):
|
|||
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')):
|
||||
|
|
|
@ -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')
|
||||
|
||||
|
|
|
@ -93,23 +93,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,24 +175,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:
|
||||
|
@ -264,48 +229,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 +245,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']:
|
||||
domains = apikey.domains
|
||||
zone_id = kwargs.get('zone_id').rstrip(".")
|
||||
domain_names = [item.name for item in g.apikey.domains]
|
||||
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):
|
||||
|
|
|
@ -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'
|
||||
|
@ -19,12 +18,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')
|
||||
|
|
|
@ -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
|
||||
|
@ -121,15 +120,6 @@ class AccountDeleteFail(StructuredException):
|
|||
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
|
||||
|
||||
|
|
|
@ -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,11 +23,15 @@ 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 AccountSummarySchema(Schema):
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
|
||||
|
||||
class UserSchema(Schema):
|
||||
id = fields.Integer()
|
||||
username = fields.String()
|
||||
|
@ -54,7 +47,7 @@ class UserDetailedSchema(Schema):
|
|||
lastname = fields.String()
|
||||
email = fields.String()
|
||||
role = fields.Embed(schema=RoleSchema)
|
||||
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||
accounts = fields.Embed(schema=AccountSummarySchema)
|
||||
|
||||
class AccountSchema(Schema):
|
||||
id = fields.Integer()
|
||||
|
@ -63,4 +56,3 @@ class AccountSchema(Schema):
|
|||
contact = fields.String()
|
||||
mail = fields.String()
|
||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||
apikeys = fields.Embed(schema=ApiKeySummarySchema, many=True)
|
||||
|
|
|
@ -104,13 +104,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
|
||||
|
@ -246,7 +239,7 @@ def pretty_domain_name(value):
|
|||
"""
|
||||
if isinstance(value, str):
|
||||
if value.startswith('xn--') \
|
||||
or value.find('.xn--') != -1:
|
||||
or value.find('.xn--'):
|
||||
try:
|
||||
return value.encode().decode('idna')
|
||||
except:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,9 +17,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
|
||||
|
|
|
@ -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,15 +87,6 @@ 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'))
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -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)
|
|
@ -8,7 +8,6 @@ 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())
|
||||
|
|
|
@ -65,9 +65,6 @@ class Record(object):
|
|||
|
||||
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)))
|
||||
|
@ -165,8 +162,6 @@ class Record(object):
|
|||
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()
|
||||
|
|
|
@ -26,11 +26,8 @@ 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,
|
||||
|
@ -187,10 +184,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 +264,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() +
|
||||
|
|
|
@ -8,9 +8,6 @@ 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
|
||||
|
@ -631,18 +628,10 @@ class User(db.Model):
|
|||
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):
|
||||
|
@ -804,4 +793,7 @@ def getUserInfo(DomainsOrAccounts):
|
|||
current=[]
|
||||
for DomainOrAccount in DomainsOrAccounts:
|
||||
current.append(DomainOrAccount.name)
|
||||
return current
|
||||
return current
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import traceback
|
|||
import re
|
||||
from base64 import b64encode
|
||||
from ast import literal_eval
|
||||
from flask import Blueprint, render_template, render_template_string, make_response, url_for, current_app, request, redirect, jsonify, abort, flash, session
|
||||
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, jsonify, abort, flash, session
|
||||
from flask_login import login_required, current_user
|
||||
|
||||
from ..decorators import operator_role_required, admin_role_required, history_access_required
|
||||
|
@ -32,172 +32,13 @@ admin_bp = Blueprint('admin',
|
|||
template_folder='templates',
|
||||
url_prefix='/admin')
|
||||
|
||||
"""
|
||||
changeSet is a list of tuples, in the following format
|
||||
(old_state, new_state, change_type)
|
||||
|
||||
old_state: dictionary with "disabled" and "content" keys. {"disabled" : False, "content" : "1.1.1.1" }
|
||||
new_state: similarly
|
||||
change_type: "addition" or "deletion" or "status" for status change or "unchanged" for no change
|
||||
|
||||
Note: A change in "content", is considered a deletion and recreation of the same record,
|
||||
holding the new content value.
|
||||
"""
|
||||
def get_record_changes(del_rrest, add_rrest):
|
||||
changeSet = []
|
||||
delSet = del_rrest['records'] if 'records' in del_rrest else []
|
||||
addSet = add_rrest['records'] if 'records' in add_rrest else []
|
||||
for d in delSet: # get the deletions and status changes
|
||||
exists = False
|
||||
for a in addSet:
|
||||
if d['content'] == a['content']:
|
||||
exists = True
|
||||
if d['disabled'] != a['disabled']:
|
||||
changeSet.append( ({"disabled":d['disabled'],"content":d['content']},
|
||||
{"disabled":a['disabled'],"content":a['content']},
|
||||
"status") )
|
||||
break
|
||||
|
||||
if not exists: # deletion
|
||||
changeSet.append( ({"disabled":d['disabled'],"content":d['content']},
|
||||
None,
|
||||
"deletion") )
|
||||
|
||||
for a in addSet: # get the additions
|
||||
exists = False
|
||||
for d in delSet:
|
||||
if d['content'] == a['content']:
|
||||
exists = True
|
||||
# already checked for status change
|
||||
break
|
||||
if not exists:
|
||||
changeSet.append( (None, {"disabled":a['disabled'], "content":a['content']}, "addition") )
|
||||
continue
|
||||
|
||||
for a in addSet: # get the unchanged
|
||||
exists = False
|
||||
for c in changeSet:
|
||||
if c[1] != None and c[1]["content"] == a['content']:
|
||||
exists = True
|
||||
break
|
||||
if not exists:
|
||||
changeSet.append( ( {"disabled":a['disabled'], "content":a['content']}, {"disabled":a['disabled'], "content":a['content']}, "unchanged") )
|
||||
|
||||
return changeSet
|
||||
|
||||
# out_changes is a list of HistoryRecordEntry objects in which we will append the new changes
|
||||
# a HistoryRecordEntry represents a pair of add_rrest and del_rrest
|
||||
def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_num, record_name=None, record_type=None):
|
||||
|
||||
if history_entry.detail is None:
|
||||
return
|
||||
|
||||
if "add_rrests" in history_entry.detail:
|
||||
detail_dict = json.loads(history_entry.detail.replace("\'", ''))
|
||||
else: # not a record entry
|
||||
return
|
||||
|
||||
add_rrests = detail_dict['add_rrests']
|
||||
del_rrests = detail_dict['del_rrests']
|
||||
|
||||
|
||||
for add_rrest in add_rrests:
|
||||
exists = False
|
||||
for del_rrest in del_rrests:
|
||||
if del_rrest['name'] == add_rrest['name'] and del_rrest['type'] == add_rrest['type']:
|
||||
exists = True
|
||||
if change_num not in out_changes:
|
||||
out_changes[change_num] = []
|
||||
out_changes[change_num].append(HistoryRecordEntry(history_entry, del_rrest, add_rrest, "*"))
|
||||
break
|
||||
if not exists: # this is a new record
|
||||
if change_num not in out_changes:
|
||||
out_changes[change_num] = []
|
||||
out_changes[change_num].append(HistoryRecordEntry(history_entry, [], add_rrest, "+")) # (add_rrest, del_rrest, change_type)
|
||||
for del_rrest in del_rrests:
|
||||
exists = False
|
||||
for add_rrest in add_rrests:
|
||||
if del_rrest['name'] == add_rrest['name'] and del_rrest['type'] == add_rrest['type']:
|
||||
exists = True # no need to add in the out_changes set
|
||||
break
|
||||
if not exists: # this is a deletion
|
||||
if change_num not in out_changes:
|
||||
out_changes[change_num] = []
|
||||
out_changes[change_num].append(HistoryRecordEntry(history_entry, del_rrest, [], "-"))
|
||||
|
||||
|
||||
# only used for changelog per record
|
||||
if record_name != None and record_type != None: # then get only the records with the specific (record_name, record_type) tuple
|
||||
if change_num in out_changes:
|
||||
changes_i = out_changes[change_num]
|
||||
else:
|
||||
return
|
||||
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:
|
||||
out_changes[change_num].remove(hre)
|
||||
|
||||
|
||||
|
||||
# records with same (name,type) are considered as a single HistoryRecordEntry
|
||||
# history_entry is of type History - used to extract created_by and created_on
|
||||
# add_rrest is a dictionary of replace
|
||||
# del_rrest is a dictionary of remove
|
||||
class HistoryRecordEntry:
|
||||
def __init__(self, history_entry, del_rrest, add_rrest, change_type):
|
||||
# search the add_rrest index into the add_rrest set for the key (name, type)
|
||||
|
||||
self.history_entry = history_entry
|
||||
self.add_rrest = add_rrest
|
||||
self.del_rrest = del_rrest
|
||||
self.change_type = change_type # "*": edit or unchanged, "+" new tuple(name,type), "-" deleted (name,type) tuple
|
||||
self.changed_fields = [] # contains a subset of : [ttl, name, type]
|
||||
self.changeSet = [] # all changes for the records of this add_rrest-del_rrest pair
|
||||
|
||||
|
||||
if change_type == "+": # addition
|
||||
self.changed_fields.append("name")
|
||||
self.changed_fields.append("type")
|
||||
self.changed_fields.append("ttl")
|
||||
self.changeSet = get_record_changes(del_rrest, add_rrest)
|
||||
elif change_type == "-": # removal
|
||||
self.changed_fields.append("name")
|
||||
self.changed_fields.append("type")
|
||||
self.changed_fields.append("ttl")
|
||||
self.changeSet = get_record_changes(del_rrest, add_rrest)
|
||||
|
||||
elif change_type == "*": # edit of unchanged
|
||||
if add_rrest['ttl'] != del_rrest['ttl']:
|
||||
self.changed_fields.append("ttl")
|
||||
self.changeSet = get_record_changes(del_rrest, add_rrest)
|
||||
|
||||
|
||||
|
||||
def toDict(self):
|
||||
return {
|
||||
"add_rrest" : self.add_rrest,
|
||||
"del_rrest" : self.del_rrest,
|
||||
"changed_fields" : self.changed_fields,
|
||||
"created_on" : self.history_entry.created_on,
|
||||
"created_by" : self.history_entry.created_by,
|
||||
"change_type" : self.change_type,
|
||||
"changeSet" : self.changeSet
|
||||
}
|
||||
|
||||
def __eq__(self, obj2): # used for removal of objects from a list
|
||||
return True if obj2.toDict() == self.toDict() else False
|
||||
|
||||
@admin_bp.before_request
|
||||
def before_request():
|
||||
# Manage session timeout
|
||||
session.permanent = True
|
||||
# current_app.permanent_session_lifetime = datetime.timedelta(
|
||||
# minutes=int(Setting().get('session_timeout')))
|
||||
current_app.permanent_session_lifetime = datetime.timedelta(
|
||||
minutes=int(Setting().get('session_timeout')))
|
||||
minutes=int(Setting().get('session_timeout')))
|
||||
session.modified = True
|
||||
|
||||
|
||||
|
@ -261,17 +102,17 @@ def edit_user(user_username=None):
|
|||
fdata = request.form
|
||||
|
||||
if create:
|
||||
user_username = fdata.get('username', '').strip()
|
||||
user_username = fdata['username']
|
||||
|
||||
user = User(username=user_username,
|
||||
plain_text_password=fdata.get('password', ''),
|
||||
firstname=fdata.get('firstname', '').strip(),
|
||||
lastname=fdata.get('lastname', '').strip(),
|
||||
email=fdata.get('email', '').strip(),
|
||||
plain_text_password=fdata['password'],
|
||||
firstname=fdata['firstname'],
|
||||
lastname=fdata['lastname'],
|
||||
email=fdata['email'],
|
||||
reload_info=False)
|
||||
|
||||
if create:
|
||||
if not fdata.get('password', ''):
|
||||
if fdata['password'] == "":
|
||||
return render_template('admin_edit_user.html',
|
||||
user=user,
|
||||
create=create,
|
||||
|
@ -301,7 +142,6 @@ def edit_user(user_username=None):
|
|||
@operator_role_required
|
||||
def edit_key(key_id=None):
|
||||
domains = Domain.query.all()
|
||||
accounts = Account.query.all()
|
||||
roles = Role.query.all()
|
||||
apikey = None
|
||||
create = True
|
||||
|
@ -318,7 +158,6 @@ def edit_key(key_id=None):
|
|||
return render_template('admin_edit_key.html',
|
||||
key=apikey,
|
||||
domains=domains,
|
||||
accounts=accounts,
|
||||
roles=roles,
|
||||
create=create)
|
||||
|
||||
|
@ -326,21 +165,14 @@ def edit_key(key_id=None):
|
|||
fdata = request.form
|
||||
description = fdata['description']
|
||||
role = fdata.getlist('key_role')[0]
|
||||
domain_list = fdata.getlist('key_multi_domain')
|
||||
account_list = fdata.getlist('key_multi_account')
|
||||
doamin_list = fdata.getlist('key_multi_domain')
|
||||
|
||||
# Create new apikey
|
||||
if create:
|
||||
if role == "User":
|
||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domain_list)).all()
|
||||
account_obj_list = Account.query.filter(Account.name.in_(account_list)).all()
|
||||
else:
|
||||
account_obj_list, domain_obj_list = [], []
|
||||
|
||||
domain_obj_list = Domain.query.filter(Domain.name.in_(doamin_list)).all()
|
||||
apikey = ApiKey(desc=description,
|
||||
role_name=role,
|
||||
domains=domain_obj_list,
|
||||
accounts=account_obj_list)
|
||||
domains=domain_obj_list)
|
||||
try:
|
||||
apikey.create()
|
||||
except Exception as e:
|
||||
|
@ -354,9 +186,7 @@ def edit_key(key_id=None):
|
|||
# Update existing apikey
|
||||
else:
|
||||
try:
|
||||
if role != "User":
|
||||
domain_list, account_list = [], []
|
||||
apikey.update(role,description,domain_list, account_list)
|
||||
apikey.update(role,description,doamin_list)
|
||||
history_message = "Updated API key {0}".format(apikey.id)
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
|
@ -366,16 +196,14 @@ def edit_key(key_id=None):
|
|||
'key': apikey.id,
|
||||
'role': apikey.role.name,
|
||||
'description': apikey.description,
|
||||
'domains': [domain.name for domain in apikey.domains],
|
||||
'accounts': [a.name for a in apikey.accounts]
|
||||
'domain_acl': [domain.name for domain in apikey.domains]
|
||||
}),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
||||
|
||||
return render_template('admin_edit_key.html',
|
||||
key=apikey,
|
||||
domains=domains,
|
||||
accounts=accounts,
|
||||
roles=roles,
|
||||
create=create,
|
||||
plain_key=plain_key)
|
||||
|
@ -404,7 +232,7 @@ def manage_keys():
|
|||
history_apikey_role = apikey.role.name
|
||||
history_apikey_description = apikey.description
|
||||
history_apikey_domains = [ domain.name for domain in apikey.domains]
|
||||
|
||||
|
||||
apikey.delete()
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
|
@ -753,498 +581,55 @@ def manage_account():
|
|||
}), 400)
|
||||
|
||||
|
||||
class DetailedHistory():
|
||||
def __init__(self, history, change_set):
|
||||
self.history = history
|
||||
self.detailed_msg = ""
|
||||
self.change_set = change_set
|
||||
|
||||
if not history.detail:
|
||||
self.detailed_msg = ""
|
||||
return
|
||||
|
||||
if 'add_rrest' in history.detail:
|
||||
detail_dict = json.loads(history.detail.replace("\'", ''))
|
||||
else:
|
||||
detail_dict = json.loads(history.detail.replace("'", '"'))
|
||||
|
||||
if 'domain_type' in detail_dict and 'account_id' in detail_dict: # this is a domain creation
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr><td>Domain type:</td><td>{{ domaintype }}</td></tr>
|
||||
<tr><td>Account:</td><td>{{ account }}</td></tr>
|
||||
</table>
|
||||
""",
|
||||
domaintype=detail_dict['domain_type'],
|
||||
account=Account.get_name_by_id(self=None, account_id=detail_dict['account_id']) if detail_dict['account_id'] != "0" else "None")
|
||||
|
||||
elif 'authenticator' in detail_dict: # this is a user authentication
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped" style="width:565px;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="3" style="background: rgba({{ background_rgba }});">
|
||||
<p style="color:white;">User {{ username }} authentication {{ auth_result }}</p>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Authenticator Type:</td>
|
||||
<td colspan="2">{{ authenticator }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>IP Address</td>
|
||||
<td colspan="2">{{ ip_address }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
""",
|
||||
background_rgba="68,157,68" if detail_dict['success'] == 1 else "201,48,44",
|
||||
username=detail_dict['username'],
|
||||
auth_result="success" if detail_dict['success'] == 1 else "failure",
|
||||
authenticator=detail_dict['authenticator'],
|
||||
ip_address=detail_dict['ip_address'])
|
||||
|
||||
elif 'add_rrests' in detail_dict: # this is a domain record change
|
||||
# changes_set = []
|
||||
self.detailed_msg = ""
|
||||
# extract_changelogs_from_a_history_entry(changes_set, history, 0)
|
||||
|
||||
elif 'name' in detail_dict and 'template' in history.msg: # template creation / deletion
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr><td>Template name:</td><td>{{ template_name }}</td></tr>
|
||||
<tr><td>Description:</td><td>{{ description }}</td></tr>
|
||||
</table>
|
||||
""",
|
||||
template_name=DetailedHistory.get_key_val(detail_dict, "name"),
|
||||
description=DetailedHistory.get_key_val(detail_dict, "description"))
|
||||
|
||||
elif 'Change domain' in history.msg and 'access control' in history.msg: # added or removed a user from a domain
|
||||
users_with_access = DetailedHistory.get_key_val(detail_dict, "user_has_access")
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr><td>Users with access to this domain</td><td>{{ users_with_access }}</td></tr>
|
||||
<tr><td>Number of users:</td><td>{{ users_with_access | length }}</td><tr>
|
||||
</table>
|
||||
""",
|
||||
users_with_access=users_with_access)
|
||||
|
||||
elif 'Created API key' in history.msg or 'Updated API key' in history.msg:
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr><td>Key: </td><td>{{ keyname }}</td></tr>
|
||||
<tr><td>Role:</td><td>{{ rolename }}</td></tr>
|
||||
<tr><td>Description:</td><td>{{ description }}</td></tr>
|
||||
<tr><td>Accessible domains with this API key:</td><td>{{ linked_domains }}</td></tr>
|
||||
<tr><td>Accessible accounts with this API key:</td><td>{{ linked_accounts }}</td></tr>
|
||||
</table>
|
||||
""",
|
||||
keyname=DetailedHistory.get_key_val(detail_dict, "key"),
|
||||
rolename=DetailedHistory.get_key_val(detail_dict, "role"),
|
||||
description=DetailedHistory.get_key_val(detail_dict, "description"),
|
||||
linked_domains=DetailedHistory.get_key_val(detail_dict, "domains" if "domains" in detail_dict else "domain_acl"),
|
||||
linked_accounts=DetailedHistory.get_key_val(detail_dict, "accounts"))
|
||||
|
||||
elif 'Delete API key' in history.msg:
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr><td>Key: </td><td>{{ keyname }}</td></tr>
|
||||
<tr><td>Role:</td><td>{{ rolename }}</td></tr>
|
||||
<tr><td>Description:</td><td>{{ description }}</td></tr>
|
||||
<tr><td>Accessible domains with this API key:</td><td>{{ linked_domains }}</td></tr>
|
||||
</table>
|
||||
""",
|
||||
keyname=DetailedHistory.get_key_val(detail_dict, "key"),
|
||||
rolename=DetailedHistory.get_key_val(detail_dict, "role"),
|
||||
description=DetailedHistory.get_key_val(detail_dict, "description"),
|
||||
linked_domains=DetailedHistory.get_key_val(detail_dict, "domains"))
|
||||
|
||||
elif 'Update type for domain' in history.msg:
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr><td>Domain: </td><td>{{ domain }}</td></tr>
|
||||
<tr><td>Domain type:</td><td>{{ domain_type }}</td></tr>
|
||||
<tr><td>Masters:</td><td>{{ masters }}</td></tr>
|
||||
</table>
|
||||
""",
|
||||
domain=DetailedHistory.get_key_val(detail_dict, "domain"),
|
||||
domain_type=DetailedHistory.get_key_val(detail_dict, "type"),
|
||||
masters=DetailedHistory.get_key_val(detail_dict, "masters"))
|
||||
|
||||
elif 'reverse' in history.msg:
|
||||
self.detailed_msg = render_template_string("""
|
||||
<table class="table table-bordered table-striped">
|
||||
<tr><td>Domain Type: </td><td>{{ domain_type }}</td></tr>
|
||||
<tr><td>Domain Master IPs:</td><td>{{ domain_master_ips }}</td></tr>
|
||||
</table>
|
||||
""",
|
||||
domain_type=DetailedHistory.get_key_val(detail_dict, "domain_type"),
|
||||
domain_master_ips=DetailedHistory.get_key_val(detail_dict, "domain_master_ips"))
|
||||
|
||||
# check for lower key as well for old databases
|
||||
@staticmethod
|
||||
def get_key_val(_dict, key):
|
||||
return str(_dict.get(key, _dict.get(key.title(), '')))
|
||||
|
||||
|
||||
# convert a list of History objects into DetailedHistory objects
|
||||
def convert_histories(histories):
|
||||
changes_set = dict()
|
||||
detailedHistories = []
|
||||
j = 0
|
||||
for i in range(len(histories)):
|
||||
if histories[i].detail and ('add_rrests' in histories[i].detail or 'del_rrests' in histories[i].detail):
|
||||
extract_changelogs_from_a_history_entry(changes_set, histories[i], j)
|
||||
if j in changes_set:
|
||||
detailedHistories.append(DetailedHistory(histories[i], changes_set[j]))
|
||||
else: # no changes were found
|
||||
detailedHistories.append(DetailedHistory(histories[i], None))
|
||||
j += 1
|
||||
|
||||
else:
|
||||
detailedHistories.append(DetailedHistory(histories[i], None))
|
||||
return detailedHistories
|
||||
|
||||
@admin_bp.route('/history', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@history_access_required
|
||||
def history():
|
||||
if request.method == 'POST':
|
||||
if current_user.role.name != 'Administrator':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'You do not have permission to remove history.'
|
||||
}), 401)
|
||||
if request.method == 'POST':
|
||||
if current_user.role.name != 'Administrator':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'You do not have permission to remove history.'
|
||||
}), 401)
|
||||
|
||||
h = History()
|
||||
result = h.remove_all()
|
||||
if result:
|
||||
history = History(msg='Remove all histories',
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Changed user role successfully.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Can not remove histories.'
|
||||
}), 500)
|
||||
h = History()
|
||||
result = h.remove_all()
|
||||
if result:
|
||||
history = History(msg='Remove all histories',
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Changed user role successfully.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Can not remove histories.'
|
||||
}), 500)
|
||||
|
||||
if request.method == 'GET':
|
||||
if current_user.role.name in [ 'Administrator', 'Operator' ]:
|
||||
histories = History.query.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) \
|
||||
.filter(
|
||||
db.or_(
|
||||
DomainUser.user_id == current_user.id,
|
||||
AccountUser.user_id == current_user.id
|
||||
))
|
||||
|
||||
if request.method == 'GET':
|
||||
doms = accounts = users = ""
|
||||
if current_user.role.name in [ 'Administrator', 'Operator']:
|
||||
all_domain_names = Domain.query.all()
|
||||
all_account_names = Account.query.all()
|
||||
all_user_names = User.query.all()
|
||||
|
||||
|
||||
|
||||
for d in all_domain_names:
|
||||
doms += d.name + " "
|
||||
for acc in all_account_names:
|
||||
accounts += acc.name + " "
|
||||
for usr in all_user_names:
|
||||
users += usr.username + " "
|
||||
else: # special autocomplete for users
|
||||
all_domain_names = 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
|
||||
)).all()
|
||||
|
||||
all_account_names = db.session.query(Account) \
|
||||
.outerjoin(Domain, Domain.account_id == Account.id) \
|
||||
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||
.outerjoin(AccountUser, Account.id == AccountUser.account_id) \
|
||||
.filter(
|
||||
db.or_(
|
||||
DomainUser.user_id == current_user.id,
|
||||
AccountUser.user_id == current_user.id
|
||||
)).all()
|
||||
|
||||
|
||||
all_user_names = []
|
||||
for a in all_account_names:
|
||||
temp = db.session.query(User) \
|
||||
.join(AccountUser, AccountUser.user_id == User.id) \
|
||||
.outerjoin(Account, Account.id == AccountUser.account_id) \
|
||||
.filter(
|
||||
db.or_(
|
||||
Account.id == a.id,
|
||||
AccountUser.account_id == a.id
|
||||
)
|
||||
) \
|
||||
.all()
|
||||
for u in temp:
|
||||
if u in all_user_names:
|
||||
continue
|
||||
all_user_names.append(u)
|
||||
|
||||
for d in all_domain_names:
|
||||
doms += d.name + " "
|
||||
|
||||
for a in all_account_names:
|
||||
accounts += a.name + " "
|
||||
for u in all_user_names:
|
||||
users += u.username + " "
|
||||
return render_template('admin_history.html', all_domain_names=doms, all_account_names=accounts, all_usernames=users)
|
||||
|
||||
# local_offset is the offset of the utc to the local time
|
||||
# offset must be int
|
||||
# return the date converted and simplified
|
||||
def from_utc_to_local(local_offset, timeframe):
|
||||
offset = str(local_offset *(-1))
|
||||
date_split = str(timeframe).split(".")[0]
|
||||
date_converted = datetime.datetime.strptime(date_split, '%Y-%m-%d %H:%M:%S') + datetime.timedelta(minutes=int(offset))
|
||||
return date_converted
|
||||
|
||||
@admin_bp.route('/history_table', methods=['GET', 'POST'])
|
||||
@login_required
|
||||
@history_access_required
|
||||
def history_table(): # ajax call data
|
||||
|
||||
if request.method == 'POST':
|
||||
if current_user.role.name != 'Administrator':
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'You do not have permission to remove history.'
|
||||
}), 401)
|
||||
|
||||
h = History()
|
||||
result = h.remove_all()
|
||||
if result:
|
||||
history = History(msg='Remove all histories',
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'ok',
|
||||
'msg': 'Changed user role successfully.'
|
||||
}), 200)
|
||||
else:
|
||||
return make_response(
|
||||
jsonify({
|
||||
'status': 'error',
|
||||
'msg': 'Can not remove histories.'
|
||||
}), 500)
|
||||
|
||||
detailedHistories = []
|
||||
lim = int(Setting().get('max_history_records')) # max num of records
|
||||
|
||||
if request.method == 'GET':
|
||||
if current_user.role.name in [ 'Administrator', 'Operator' ]:
|
||||
base_query = History.query
|
||||
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
|
||||
base_query = 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) \
|
||||
.filter(
|
||||
db.or_(
|
||||
DomainUser.user_id == current_user.id,
|
||||
AccountUser.user_id == current_user.id
|
||||
))
|
||||
|
||||
domain_name = request.args.get('domain_name_filter') if request.args.get('domain_name_filter') != None \
|
||||
and len(request.args.get('domain_name_filter')) != 0 else None
|
||||
account_name = request.args.get('account_name_filter') if request.args.get('account_name_filter') != None \
|
||||
and len(request.args.get('account_name_filter')) != 0 else None
|
||||
user_name = request.args.get('auth_name_filter') if request.args.get('auth_name_filter') != None \
|
||||
and len(request.args.get('auth_name_filter')) != 0 else None
|
||||
|
||||
min_date = request.args.get('min') if request.args.get('min') != None and len( request.args.get('min')) != 0 else None
|
||||
if min_date != None: # get 1 day earlier, to check for timezone errors
|
||||
min_date = str(datetime.datetime.strptime(min_date, '%Y-%m-%d') - datetime.timedelta(days=1))
|
||||
max_date = request.args.get('max') if request.args.get('max') != None and len( request.args.get('max')) != 0 else None
|
||||
if max_date != None: # get 1 day later, to check for timezone errors
|
||||
max_date = str(datetime.datetime.strptime(max_date, '%Y-%m-%d') + datetime.timedelta(days=1))
|
||||
tzoffset = request.args.get('tzoffset') if request.args.get('tzoffset') != None and len(request.args.get('tzoffset')) != 0 else None
|
||||
changed_by = request.args.get('user_name_filter') if request.args.get('user_name_filter') != None \
|
||||
and len(request.args.get('user_name_filter')) != 0 else None
|
||||
"""
|
||||
Auth methods: LOCAL, Github OAuth, Azure OAuth, SAML, OIDC OAuth, Google OAuth
|
||||
"""
|
||||
auth_methods = []
|
||||
if (request.args.get('auth_local_only_checkbox') is None \
|
||||
and request.args.get('auth_oauth_only_checkbox') is None \
|
||||
and request.args.get('auth_saml_only_checkbox') is None and request.args.get('auth_all_checkbox') is None):
|
||||
auth_methods = []
|
||||
if request.args.get('auth_all_checkbox') == "on":
|
||||
auth_methods.append("")
|
||||
if request.args.get('auth_local_only_checkbox') == "on":
|
||||
auth_methods.append("LOCAL")
|
||||
if request.args.get('auth_oauth_only_checkbox') == "on":
|
||||
auth_methods.append("OAuth")
|
||||
if request.args.get('auth_saml_only_checkbox') == "on":
|
||||
auth_methods.append("SAML")
|
||||
|
||||
if request.args.get('domain_changelog_only_checkbox') != None:
|
||||
changelog_only = True if request.args.get('domain_changelog_only_checkbox') == "on" else False
|
||||
else:
|
||||
changelog_only = False
|
||||
|
||||
|
||||
|
||||
|
||||
# users cannot search for authentication
|
||||
if user_name != None and current_user.role.name not in [ 'Administrator', 'Operator']:
|
||||
histories = []
|
||||
elif domain_name != None:
|
||||
|
||||
if not changelog_only:
|
||||
histories = base_query \
|
||||
.filter(
|
||||
db.and_(
|
||||
db.or_(
|
||||
History.msg.like("%domain "+ domain_name) if domain_name != "*" else History.msg.like("%domain%"),
|
||||
History.msg.like("%domain "+ domain_name + " access control") if domain_name != "*" else History.msg.like("%domain%access control")
|
||||
),
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
History.created_by == changed_by if changed_by != None else True
|
||||
)
|
||||
).order_by(History.created_on.desc()).limit(lim).all()
|
||||
else:
|
||||
# search for records changes only
|
||||
histories = base_query \
|
||||
.filter(
|
||||
db.and_(
|
||||
History.msg.like("Apply record changes to domain " + domain_name) if domain_name != "*" \
|
||||
else History.msg.like("Apply record changes to domain%"),
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
History.created_by == changed_by if changed_by != None else True
|
||||
|
||||
)
|
||||
).order_by(History.created_on.desc()) \
|
||||
.limit(lim).all()
|
||||
elif account_name != None:
|
||||
if current_user.role.name in ['Administrator', 'Operator']:
|
||||
histories = base_query \
|
||||
.join(Domain, History.domain_id == Domain.id) \
|
||||
.outerjoin(Account, Domain.account_id == Account.id) \
|
||||
.filter(
|
||||
db.and_(
|
||||
Account.id == Domain.account_id,
|
||||
account_name == Account.name if account_name != "*" else True,
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
History.created_by == changed_by if changed_by != None else True
|
||||
)
|
||||
).order_by(History.created_on.desc()) \
|
||||
.limit(lim).all()
|
||||
else:
|
||||
histories = base_query \
|
||||
.filter(
|
||||
db.and_(
|
||||
Account.id == Domain.account_id,
|
||||
account_name == Account.name if account_name != "*" else True,
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
History.created_by == changed_by if changed_by != None else True
|
||||
)
|
||||
).order_by(History.created_on.desc()) \
|
||||
.limit(lim).all()
|
||||
elif user_name != None and current_user.role.name in [ 'Administrator', 'Operator']: # only admins can see the user login-logouts
|
||||
|
||||
histories = History.query \
|
||||
.filter(
|
||||
db.and_(
|
||||
db.or_(
|
||||
History.msg.like("User "+ user_name + " authentication%") if user_name != "*" and user_name != None else History.msg.like("%authentication%"),
|
||||
History.msg.like("User "+ user_name + " was not authorized%") if user_name != "*" and user_name != None else History.msg.like("User%was not authorized%")
|
||||
),
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
History.created_by == changed_by if changed_by != None else True
|
||||
)
|
||||
) \
|
||||
.order_by(History.created_on.desc()).limit(lim).all()
|
||||
temp = []
|
||||
for h in histories:
|
||||
for method in auth_methods:
|
||||
if method in h.detail:
|
||||
temp.append(h)
|
||||
break
|
||||
histories = temp
|
||||
elif (changed_by != None or max_date != None) and current_user.role.name in [ 'Administrator', 'Operator'] : # select changed by and date filters only
|
||||
histories = History.query \
|
||||
.filter(
|
||||
db.and_(
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
History.created_by == changed_by if changed_by != None else True
|
||||
)
|
||||
) \
|
||||
.order_by(History.created_on.desc()).limit(lim).all()
|
||||
elif (changed_by != None or max_date != None): # special filtering for user because one user does not have access to log-ins logs
|
||||
histories = base_query \
|
||||
.filter(
|
||||
db.and_(
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
History.created_by == changed_by if changed_by != None else True
|
||||
)
|
||||
) \
|
||||
.order_by(History.created_on.desc()).limit(lim).all()
|
||||
elif max_date != None: # if changed by == null and only date is applied
|
||||
histories = base_query.filter(
|
||||
db.and_(
|
||||
History.created_on <= max_date if max_date != None else True,
|
||||
History.created_on >= min_date if min_date != None else True,
|
||||
)
|
||||
).order_by(History.created_on.desc()).limit(lim).all()
|
||||
else: # default view
|
||||
if current_user.role.name in [ 'Administrator', 'Operator']:
|
||||
histories = History.query.order_by(History.created_on.desc()).limit(lim).all()
|
||||
else:
|
||||
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.or_(
|
||||
DomainUser.user_id == current_user.id,
|
||||
AccountUser.user_id == current_user.id
|
||||
)).limit(lim).all()
|
||||
|
||||
detailedHistories = convert_histories(histories)
|
||||
|
||||
# Remove dates from previous or next day that were brought over
|
||||
if tzoffset != None:
|
||||
if min_date != None:
|
||||
min_date_split = min_date.split()[0]
|
||||
if max_date != None:
|
||||
max_date_split = max_date.split()[0]
|
||||
for i, history_rec in enumerate(detailedHistories):
|
||||
local_date = str(from_utc_to_local(int(tzoffset), history_rec.history.created_on).date())
|
||||
if (min_date != None and local_date == min_date_split) or (max_date != None and local_date == max_date_split):
|
||||
detailedHistories[i] = None
|
||||
|
||||
# Remove elements previously flagged as None
|
||||
detailedHistories = [h for h in detailedHistories if h is not None]
|
||||
|
||||
return render_template('admin_history_table.html', histories=detailedHistories, len_histories=len(detailedHistories), lim=lim)
|
||||
|
||||
return render_template('admin_history.html', histories=histories)
|
||||
|
||||
|
||||
@admin_bp.route('/setting/basic', methods=['GET'])
|
||||
|
@ -1257,10 +642,9 @@ def setting_basic():
|
|||
'login_ldap_first', 'default_record_table_size',
|
||||
'default_domain_table_size', 'auto_ptr', 'record_quick_edit',
|
||||
'pretty_ipv6_ptr', 'dnssec_admins_only',
|
||||
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
|
||||
'allow_user_create_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
|
||||
'session_timeout', 'warn_session_timeout', 'ttl_options',
|
||||
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
|
||||
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force'
|
||||
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email'
|
||||
]
|
||||
|
||||
return render_template('admin_setting_basic.html', settings=settings)
|
||||
|
@ -1602,8 +986,6 @@ def setting_authentication():
|
|||
request.form.get('oidc_oauth_token_url'))
|
||||
Setting().set('oidc_oauth_authorize_url',
|
||||
request.form.get('oidc_oauth_authorize_url'))
|
||||
Setting().set('oidc_oauth_logout_url',
|
||||
request.form.get('oidc_oauth_logout_url'))
|
||||
Setting().set('oidc_oauth_username',
|
||||
request.form.get('oidc_oauth_username'))
|
||||
Setting().set('oidc_oauth_firstname',
|
||||
|
|
|
@ -21,18 +21,16 @@ from ..lib.errors import (
|
|||
DomainNotExists, DomainAlreadyExists, DomainAccessForbidden,
|
||||
RequestIsNotJSON, ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges,
|
||||
AccountCreateFail, AccountUpdateFail, AccountDeleteFail,
|
||||
AccountCreateDuplicate, AccountNotExists,
|
||||
AccountCreateDuplicate,
|
||||
UserCreateFail, UserCreateDuplicate, UserUpdateFail, UserDeleteFail,
|
||||
UserUpdateFailEmail,
|
||||
)
|
||||
from ..decorators import (
|
||||
api_basic_auth, api_can_create_domain, is_json, apikey_auth,
|
||||
apikey_can_create_domain, apikey_can_remove_domain,
|
||||
apikey_is_admin, apikey_can_access_domain, apikey_can_configure_dnssec,
|
||||
api_role_can, apikey_or_basic_auth,
|
||||
callback_if_request_body_contains_key,
|
||||
apikey_is_admin, apikey_can_access_domain, api_role_can,
|
||||
apikey_or_basic_auth,
|
||||
)
|
||||
import secrets
|
||||
import random
|
||||
import string
|
||||
|
||||
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||
|
@ -309,7 +307,6 @@ def api_generate_apikey():
|
|||
role_name = None
|
||||
apikey = None
|
||||
domain_obj_list = []
|
||||
account_obj_list = []
|
||||
|
||||
abort(400) if 'role' not in data else None
|
||||
|
||||
|
@ -320,13 +317,6 @@ def api_generate_apikey():
|
|||
else:
|
||||
domains = [d['name'] if isinstance(d, dict) else d for d in data['domains']]
|
||||
|
||||
if 'accounts' not in data:
|
||||
accounts = []
|
||||
elif not isinstance(data['accounts'], (list, )):
|
||||
abort(400)
|
||||
else:
|
||||
accounts = [a['name'] if isinstance(a, dict) else a for a in data['accounts']]
|
||||
|
||||
description = data['description'] if 'description' in data else None
|
||||
|
||||
if isinstance(data['role'], str):
|
||||
|
@ -336,24 +326,16 @@ def api_generate_apikey():
|
|||
else:
|
||||
abort(400)
|
||||
|
||||
if role_name == 'User' and len(domains) == 0 and len(accounts) == 0:
|
||||
current_app.logger.error("Apikey with User role must have domains or accounts")
|
||||
if role_name == 'User' and len(domains) == 0:
|
||||
current_app.logger.error("Apikey with User role must have domains")
|
||||
raise ApiKeyNotUsable()
|
||||
|
||||
if role_name == 'User' and len(domains) > 0:
|
||||
elif role_name == 'User':
|
||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
||||
if len(domain_obj_list) == 0:
|
||||
msg = "One of supplied domains does not exist"
|
||||
current_app.logger.error(msg)
|
||||
raise DomainNotExists(message=msg)
|
||||
|
||||
if role_name == 'User' and len(accounts) > 0:
|
||||
account_obj_list = Account.query.filter(Account.name.in_(accounts)).all()
|
||||
if len(account_obj_list) == 0:
|
||||
msg = "One of supplied accounts does not exist"
|
||||
current_app.logger.error(msg)
|
||||
raise AccountNotExists(message=msg)
|
||||
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
# domain list of domain api key should be valid for
|
||||
# if not any domain error
|
||||
|
@ -363,11 +345,6 @@ def api_generate_apikey():
|
|||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
if len(accounts) > 0:
|
||||
msg = "User cannot assign accounts"
|
||||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
user_domain_obj_list = get_user_domains()
|
||||
|
||||
domain_list = [item.name for item in domain_obj_list]
|
||||
|
@ -386,8 +363,7 @@ def api_generate_apikey():
|
|||
|
||||
apikey = ApiKey(desc=description,
|
||||
role_name=role_name,
|
||||
domains=domain_obj_list,
|
||||
accounts=account_obj_list)
|
||||
domains=domain_obj_list)
|
||||
|
||||
try:
|
||||
apikey.create()
|
||||
|
@ -500,16 +476,9 @@ def api_update_apikey(apikey_id):
|
|||
# if role different and user is allowed to change it, update
|
||||
# if apikey domains are different and user is allowed to handle
|
||||
# that domains update domains
|
||||
domain_obj_list = None
|
||||
account_obj_list = None
|
||||
|
||||
apikey = ApiKey.query.get(apikey_id)
|
||||
|
||||
if not apikey:
|
||||
abort(404)
|
||||
|
||||
data = request.get_json()
|
||||
description = data['description'] if 'description' in data else None
|
||||
domain_obj_list = None
|
||||
|
||||
if 'role' in data:
|
||||
if isinstance(data['role'], str):
|
||||
|
@ -518,11 +487,8 @@ def api_update_apikey(apikey_id):
|
|||
role_name = data['role']['name']
|
||||
else:
|
||||
abort(400)
|
||||
|
||||
target_role = role_name
|
||||
else:
|
||||
role_name = None
|
||||
target_role = apikey.role.name
|
||||
|
||||
if 'domains' not in data:
|
||||
domains = None
|
||||
|
@ -531,54 +497,22 @@ def api_update_apikey(apikey_id):
|
|||
else:
|
||||
domains = [d['name'] if isinstance(d, dict) else d for d in data['domains']]
|
||||
|
||||
if 'accounts' not in data:
|
||||
accounts = None
|
||||
elif not isinstance(data['accounts'], (list, )):
|
||||
abort(400)
|
||||
else:
|
||||
accounts = [a['name'] if isinstance(a, dict) else a for a in data['accounts']]
|
||||
apikey = ApiKey.query.get(apikey_id)
|
||||
|
||||
if not apikey:
|
||||
abort(404)
|
||||
|
||||
current_app.logger.debug('Updating apikey with id {0}'.format(apikey_id))
|
||||
|
||||
if target_role == 'User':
|
||||
current_domains = [item.name for item in apikey.domains]
|
||||
current_accounts = [item.name for item in apikey.accounts]
|
||||
|
||||
if domains is not None:
|
||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
||||
if len(domain_obj_list) != len(domains):
|
||||
msg = "One of supplied domains does not exist"
|
||||
current_app.logger.error(msg)
|
||||
raise DomainNotExists(message=msg)
|
||||
|
||||
target_domains = domains
|
||||
else:
|
||||
target_domains = current_domains
|
||||
|
||||
if accounts is not None:
|
||||
account_obj_list = Account.query.filter(Account.name.in_(accounts)).all()
|
||||
if len(account_obj_list) != len(accounts):
|
||||
msg = "One of supplied accounts does not exist"
|
||||
current_app.logger.error(msg)
|
||||
raise AccountNotExists(message=msg)
|
||||
|
||||
target_accounts = accounts
|
||||
else:
|
||||
target_accounts = current_accounts
|
||||
|
||||
if len(target_domains) == 0 and len(target_accounts) == 0:
|
||||
current_app.logger.error("Apikey with User role must have domains or accounts")
|
||||
raise ApiKeyNotUsable()
|
||||
|
||||
if domains is not None and set(domains) == set(current_domains):
|
||||
current_app.logger.debug(
|
||||
"Domains are the same, apikey domains won't be updated")
|
||||
domains = None
|
||||
|
||||
if accounts is not None and set(accounts) == set(current_accounts):
|
||||
current_app.logger.debug(
|
||||
"Accounts are the same, apikey accounts won't be updated")
|
||||
accounts = None
|
||||
if role_name == 'User' and len(domains) == 0:
|
||||
current_app.logger.error("Apikey with User role must have domains")
|
||||
raise ApiKeyNotUsable()
|
||||
elif role_name == 'User':
|
||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
||||
if len(domain_obj_list) == 0:
|
||||
msg = "One of supplied domains does not exist"
|
||||
current_app.logger.error(msg)
|
||||
raise DomainNotExists(message=msg)
|
||||
|
||||
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||
if role_name != 'User':
|
||||
|
@ -586,12 +520,8 @@ def api_update_apikey(apikey_id):
|
|||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
if len(accounts) > 0:
|
||||
msg = "User cannot assign accounts"
|
||||
current_app.logger.error(msg)
|
||||
raise NotEnoughPrivileges(message=msg)
|
||||
|
||||
apikeys = get_user_apikeys()
|
||||
apikey_domains = [item.name for item in apikey.domains]
|
||||
apikeys_ids = [apikey_item.id for apikey_item in apikeys]
|
||||
|
||||
user_domain_obj_list = current_user.get_domain().all()
|
||||
|
@ -615,7 +545,12 @@ def api_update_apikey(apikey_id):
|
|||
current_app.logger.error(msg)
|
||||
raise DomainAccessForbidden()
|
||||
|
||||
if role_name == apikey.role.name:
|
||||
if set(domains) == set(apikey_domains):
|
||||
current_app.logger.debug(
|
||||
"Domains are same, apikey domains won't be updated")
|
||||
domains = None
|
||||
|
||||
if role_name == apikey.role:
|
||||
current_app.logger.debug("Role is same, apikey role won't be updated")
|
||||
role_name = None
|
||||
|
||||
|
@ -624,13 +559,10 @@ def api_update_apikey(apikey_id):
|
|||
current_app.logger.debug(msg)
|
||||
description = None
|
||||
|
||||
if target_role != "User":
|
||||
domains, accounts = [], []
|
||||
|
||||
try:
|
||||
apikey = ApiKey.query.get(apikey_id)
|
||||
apikey.update(role_name=role_name,
|
||||
domains=domains,
|
||||
accounts=accounts,
|
||||
description=description)
|
||||
except Exception as e:
|
||||
current_app.logger.error('Error: {0}'.format(e))
|
||||
|
@ -689,7 +621,7 @@ def api_create_user():
|
|||
|
||||
if not plain_text_password and not password:
|
||||
plain_text_password = ''.join(
|
||||
secrets.choice(string.ascii_letters + string.digits)
|
||||
random.choice(string.ascii_letters + string.digits)
|
||||
for _ in range(15))
|
||||
if not role_name and not role_id:
|
||||
role_name = 'User'
|
||||
|
@ -924,7 +856,7 @@ def api_update_account(account_id):
|
|||
"Updating account {} ({})".format(account_id, account.name))
|
||||
result = account.update_account()
|
||||
if not result['status']:
|
||||
raise AccountUpdateFail(message=result['msg'])
|
||||
raise AccountDeleteFail(message=result['msg'])
|
||||
history = History(msg='Update account {0}'.format(account.name),
|
||||
created_by=current_user.username)
|
||||
history.add()
|
||||
|
@ -944,7 +876,7 @@ def api_delete_account(account_id):
|
|||
"Deleting account {} ({})".format(account_id, account.name))
|
||||
result = account.delete_account()
|
||||
if not result:
|
||||
raise AccountDeleteFail(message=result['msg'])
|
||||
raise AccountUpdateFail(message=result['msg'])
|
||||
|
||||
history = History(msg='Delete account {0}'.format(account.name),
|
||||
created_by=current_user.username)
|
||||
|
@ -1025,28 +957,6 @@ def api_remove_account_user(account_id, user_id):
|
|||
return '', 204
|
||||
|
||||
|
||||
@api_bp.route(
|
||||
'/servers/<string:server_id>/zones/<string:zone_id>/cryptokeys',
|
||||
methods=['GET', 'POST'])
|
||||
@apikey_auth
|
||||
@apikey_can_access_domain
|
||||
@apikey_can_configure_dnssec(http_methods=['POST'])
|
||||
def api_zone_cryptokeys(server_id, zone_id):
|
||||
resp = helper.forward_request()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_bp.route(
|
||||
'/servers/<string:server_id>/zones/<string:zone_id>/cryptokeys/<string:cryptokey_id>',
|
||||
methods=['GET', 'PUT', 'DELETE'])
|
||||
@apikey_auth
|
||||
@apikey_can_access_domain
|
||||
@apikey_can_configure_dnssec()
|
||||
def api_zone_cryptokey(server_id, zone_id, cryptokey_id):
|
||||
resp = helper.forward_request()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_bp.route(
|
||||
'/servers/<string:server_id>/zones/<string:zone_id>/<path:subpath>',
|
||||
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
||||
|
@ -1061,10 +971,6 @@ def api_zone_subpath_forward(server_id, zone_id, subpath):
|
|||
methods=['GET', 'PUT', 'PATCH', 'DELETE'])
|
||||
@apikey_auth
|
||||
@apikey_can_access_domain
|
||||
@apikey_can_remove_domain(http_methods=['DELETE'])
|
||||
@callback_if_request_body_contains_key(apikey_can_configure_dnssec()(),
|
||||
http_methods=['PUT'],
|
||||
keys=['dnssec', 'nsec3param'])
|
||||
def api_zone_forward(server_id, zone_id):
|
||||
resp = helper.forward_request()
|
||||
if not Setting().get('bg_domain_updates'):
|
||||
|
@ -1073,32 +979,30 @@ def api_zone_forward(server_id, zone_id):
|
|||
status = resp.status_code
|
||||
if 200 <= status < 300:
|
||||
current_app.logger.debug("Request to powerdns API successful")
|
||||
if Setting().get('enable_api_rr_history'):
|
||||
if request.method in ['POST', 'PATCH'] :
|
||||
data = request.get_json(force=True)
|
||||
for rrset_data in data['rrsets']:
|
||||
history = History(msg='{0} zone {1} record of {2}'.format(
|
||||
rrset_data['changetype'].lower(), rrset_data['type'],
|
||||
rrset_data['name'].rstrip('.')),
|
||||
detail=json.dumps(data),
|
||||
created_by=g.apikey.description,
|
||||
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
|
||||
history.add()
|
||||
elif request.method == 'DELETE':
|
||||
history = History(msg='Deleted zone {0}'.format(zone_id.rstrip('.')),
|
||||
detail='',
|
||||
created_by=g.apikey.description,
|
||||
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
|
||||
history.add()
|
||||
elif request.method != 'GET':
|
||||
history = History(msg='Updated zone {0}'.format(zone_id.rstrip('.')),
|
||||
detail='',
|
||||
created_by=g.apikey.description,
|
||||
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
|
||||
if request.method in ['POST', 'PATCH'] :
|
||||
data = request.get_json(force=True)
|
||||
for rrset_data in data['rrsets']:
|
||||
history = History(msg='{0} zone {1} record of {2}'.format(
|
||||
rrset_data['changetype'].lower(), rrset_data['type'],
|
||||
rrset_data['name'].rstrip('.')),
|
||||
detail=json.dumps(data),
|
||||
created_by=g.apikey.description,
|
||||
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
|
||||
history.add()
|
||||
elif request.method == 'DELETE':
|
||||
history = History(msg='Deleted zone {0}'.format(zone_id.rstrip('.')),
|
||||
detail='',
|
||||
created_by=g.apikey.description,
|
||||
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
|
||||
history.add()
|
||||
elif request.method != 'GET':
|
||||
history = History(msg='Updated zone {0}'.format(zone_id.rstrip('.')),
|
||||
detail='',
|
||||
created_by=g.apikey.description,
|
||||
domain_id=Domain().get_id_by_name(zone_id.rstrip('.')))
|
||||
history.add()
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
||||
|
||||
@api_bp.route('/servers/<path:subpath>', methods=['GET', 'PUT'])
|
||||
@apikey_auth
|
||||
@apikey_is_admin
|
||||
|
@ -1109,7 +1013,6 @@ def api_server_sub_forward(subpath):
|
|||
|
||||
@api_bp.route('/servers/<string:server_id>/zones', methods=['POST'])
|
||||
@apikey_auth
|
||||
@apikey_can_create_domain
|
||||
def api_create_zone(server_id):
|
||||
resp = helper.forward_request()
|
||||
|
||||
|
@ -1151,13 +1054,8 @@ def api_get_zones(server_id):
|
|||
and resp.status_code == 200):
|
||||
domain_list = [d['name']
|
||||
for d in domain_schema.dump(g.apikey.domains)]
|
||||
|
||||
accounts_domains = [d.name for a in g.apikey.accounts for d in a.domains]
|
||||
allowed_domains = set(domain_list + accounts_domains)
|
||||
current_app.logger.debug("Account domains: {}".format(
|
||||
'/'.join(accounts_domains)))
|
||||
content = json.dumps([i for i in json.loads(resp.content)
|
||||
if i['name'].rstrip('.') in allowed_domains])
|
||||
if i['name'].rstrip('.') in domain_list])
|
||||
return content, resp.status_code, resp.headers.items()
|
||||
else:
|
||||
return resp.content, resp.status_code, resp.headers.items()
|
||||
|
|
|
@ -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,10 +150,6 @@ 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
|
||||
|
@ -163,20 +158,19 @@ def dashboard():
|
|||
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()
|
||||
history = History.query.order_by(History.created_on.desc()).limit(4)
|
||||
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()
|
||||
)).order_by(History.created_on.desc())
|
||||
history_number = history.count()
|
||||
history = history[:4]
|
||||
domain_count = db.session.query(Domain) \
|
||||
.outerjoin(DomainUser, Domain.id == DomainUser.domain_id) \
|
||||
|
@ -187,10 +181,6 @@ def dashboard():
|
|||
DomainUser.user_id == current_user.id,
|
||||
AccountUser.user_id == current_user.id
|
||||
)).count()
|
||||
|
||||
from .admin import convert_histories, DetailedHistory
|
||||
detailedHistories = convert_histories(history)
|
||||
|
||||
server = Server(server_id='localhost')
|
||||
statistics = server.get_statistic()
|
||||
if statistics:
|
||||
|
@ -207,14 +197,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()
|
||||
|
|
|
@ -10,7 +10,7 @@ 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 +21,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',
|
||||
|
@ -132,217 +128,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'])
|
||||
|
|
|
@ -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
|
||||
|
@ -363,7 +367,10 @@ def login():
|
|||
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)
|
||||
|
@ -391,43 +398,22 @@ def login():
|
|||
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)
|
||||
account = handle_account(me[name_prop], me[desc_prop])
|
||||
account.add_user(user)
|
||||
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)
|
||||
for ua in user_accounts:
|
||||
if ua.name != account.name:
|
||||
ua.remove_user(user)
|
||||
|
||||
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)
|
||||
|
@ -504,7 +490,9 @@ def login():
|
|||
user.revoke_privilege(True)
|
||||
current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' )
|
||||
|
||||
return authenticate_user(user, 'LOCAL', remember_me)
|
||||
login_user(user, remember=remember_me)
|
||||
signin_history(user.username, 'LOCAL', True)
|
||||
return redirect(session.get('next', url_for('index.index')))
|
||||
|
||||
def checkForPDAEntries(Entitlements, urn_value):
|
||||
"""
|
||||
|
@ -574,23 +562,6 @@ def get_azure_groups(uri):
|
|||
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():
|
||||
|
@ -654,12 +625,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 +652,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 +662,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)
|
||||
|
@ -1071,7 +1015,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)
|
||||
|
||||
|
|
|
@ -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,10 +41,13 @@ 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(
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -797,11 +797,6 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/PDNSAdminZones'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
post:
|
||||
security:
|
||||
- basicAuth: []
|
||||
|
@ -821,23 +816,6 @@ paths:
|
|||
description: A zone
|
||||
schema:
|
||||
$ref: '#/definitions/Zone'
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'409':
|
||||
description: 'Domain already exists (conflict)'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: 'Internal Server Error'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/zones/{zone_id}':
|
||||
parameters:
|
||||
- name: zone_id
|
||||
|
@ -861,23 +839,6 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: 'Returns 204 No Content on success.'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'403':
|
||||
description: 'Forbidden'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: 'Not found'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: 'Internal Server Error'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/apikeys':
|
||||
get:
|
||||
security:
|
||||
|
@ -893,23 +854,15 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/ApiKey'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'403':
|
||||
description: 'Domain Access Forbidden'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: 'Internal Server Error. There was a problem creating the key'
|
||||
description: 'Internal Server Error, keys could not be retrieved. Contains error message'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
post:
|
||||
security:
|
||||
- basicAuth: []
|
||||
summary: 'Add a ApiKey key'
|
||||
description: 'This methods add a new ApiKey. The actual key is generated by the server'
|
||||
description: 'This methods add a new ApiKey. The actual key can be generated by the server or be provided by the client'
|
||||
operationId: api_generate_apikey
|
||||
tags:
|
||||
- apikey
|
||||
|
@ -925,27 +878,14 @@ paths:
|
|||
description: Created
|
||||
schema:
|
||||
$ref: '#/definitions/ApiKey'
|
||||
'400':
|
||||
description: 'Request is not JSON or does not respect required format'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'403':
|
||||
description: 'Domain Access Forbidden'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: 'Domain or Account Not found'
|
||||
'422':
|
||||
description: 'Unprocessable Entry, the ApiKey provided has issues.'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: 'Internal Server Error. There was a problem creating the key'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/apikeys/{apikey_id}':
|
||||
parameters:
|
||||
- name: apikey_id
|
||||
|
@ -965,18 +905,16 @@ paths:
|
|||
description: OK.
|
||||
schema:
|
||||
$ref: '#/definitions/ApiKey'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'403':
|
||||
description: 'The authenticated user has User role and is not allowed on any of the domains assigned to the key'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: 'Not found. The ApiKey with the specified apikey_id does not exist'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: 'Internal Server Error, keys could not be retrieved. Contains error message'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
delete:
|
||||
security:
|
||||
- basicAuth: []
|
||||
|
@ -987,14 +925,6 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: 'OK, key was deleted'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'403':
|
||||
description: 'The authenticated user has User role and is not allowed on any of the domains assigned to the key'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: 'Not found. The ApiKey with the specified apikey_id does not exist'
|
||||
schema:
|
||||
|
@ -1008,11 +938,9 @@ paths:
|
|||
- basicAuth: []
|
||||
description: |
|
||||
The ApiKey at apikey_id can be changed in multiple ways:
|
||||
* Role, description, accounts and domains can be updated
|
||||
* Role, description, domains can be updated
|
||||
* Role can be changed to Administrator only if user has Operator or Administrator privileges
|
||||
* Domains will be updated only if user has access to them
|
||||
* Accounts can be updated only by a privileged user
|
||||
* With a User role, an ApiKey needs at least one account or one domain
|
||||
Only the relevant fields have to be provided in the request body.
|
||||
operationId: api_update_apikey
|
||||
tags:
|
||||
|
@ -1029,27 +957,14 @@ paths:
|
|||
description: OK. ApiKey is changed.
|
||||
schema:
|
||||
$ref: '#/definitions/ApiKey'
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'403':
|
||||
description: 'Domain Access Forbidden'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: 'Not found (ApiKey, Domain or Account)'
|
||||
description: 'Not found. The TSIGKey with the specified tsigkey_id does not exist'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: 'Internal Server Error. Contains error message'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/users':
|
||||
get:
|
||||
security:
|
||||
|
@ -1065,10 +980,6 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/User'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: Internal Server Error, users could not be retrieved. Contains error message
|
||||
schema:
|
||||
|
@ -1127,11 +1038,7 @@ paths:
|
|||
schema:
|
||||
$ref: '#/definitions/User'
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
description: Unprocessable Entry, the User data provided has issues
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'409':
|
||||
|
@ -1142,7 +1049,6 @@ paths:
|
|||
description: Internal Server Error. There was a problem creating the user
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/users/{username}':
|
||||
parameters:
|
||||
- name: username
|
||||
|
@ -1162,10 +1068,6 @@ paths:
|
|||
description: Retrieve a specific User
|
||||
schema:
|
||||
$ref: '#/definitions/UserDetailed'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The User with the specified username does not exist
|
||||
schema:
|
||||
|
@ -1174,7 +1076,6 @@ paths:
|
|||
description: Internal Server Error, user could not be retrieved. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/users/{user_id}':
|
||||
parameters:
|
||||
- name: user_id
|
||||
|
@ -1228,22 +1129,10 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: OK. User is modified (empty response body)
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The User with the specified user_id does not exist
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'409':
|
||||
description: Duplicate (Email already assigned to another user)
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: Internal Server Error. Contains error message
|
||||
schema:
|
||||
|
@ -1258,10 +1147,6 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: OK. User is deleted (empty response body)
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The User with the specified user_id does not exist
|
||||
schema:
|
||||
|
@ -1270,7 +1155,6 @@ paths:
|
|||
description: Internal Server Error. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/accounts':
|
||||
get:
|
||||
security:
|
||||
|
@ -1286,8 +1170,8 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/Account'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
'500':
|
||||
description: Internal Server Error, accounts could not be retrieved. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
post:
|
||||
|
@ -1323,11 +1207,7 @@ paths:
|
|||
schema:
|
||||
$ref: '#/definitions/Account'
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
description: Unprocessable Entry, the Account data provided has issues.
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'409':
|
||||
|
@ -1338,7 +1218,6 @@ paths:
|
|||
description: Internal Server Error. There was a problem creating the account
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/accounts/{account_name}':
|
||||
parameters:
|
||||
- name: account_name
|
||||
|
@ -1358,15 +1237,14 @@ paths:
|
|||
description: Retrieve a specific account
|
||||
schema:
|
||||
$ref: '#/definitions/Account'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account with the specified name does not exist
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'500':
|
||||
description: Internal Server Error, account could not be retrieved. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'/pdnsadmin/accounts/{account_id}':
|
||||
parameters:
|
||||
- name: account_id
|
||||
|
@ -1403,14 +1281,6 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: OK. Account is modified (empty response body)
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account with the specified account_id does not exist
|
||||
schema:
|
||||
|
@ -1429,10 +1299,6 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: OK. Account is deleted (empty response body)
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account with the specified account_id does not exist
|
||||
schema:
|
||||
|
@ -1441,7 +1307,6 @@ paths:
|
|||
description: Internal Server Error. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/accounts/{account_id}/users':
|
||||
parameters:
|
||||
- name: account_id
|
||||
|
@ -1464,46 +1329,14 @@ paths:
|
|||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/User'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account with the specified account_id does not exist
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/accounts/users/{account_id}':
|
||||
parameters:
|
||||
- name: account_id
|
||||
type: integer
|
||||
in: path
|
||||
required: true
|
||||
description: The id of the account to list users linked to account
|
||||
get:
|
||||
security:
|
||||
- basicAuth: []
|
||||
summary: List users linked to a specific account
|
||||
operationId: api_list_users_account
|
||||
tags:
|
||||
- account
|
||||
- user
|
||||
responses:
|
||||
'200':
|
||||
description: List of Summarized User objects
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/User'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
'500':
|
||||
description: Internal Server Error, accounts could not be retrieved. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account with the specified account_id does not exist
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/accounts/{account_id}/users/{user_id}':
|
||||
parameters:
|
||||
- name: account_id
|
||||
|
@ -1527,14 +1360,6 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: OK. User is linked (empty response body)
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account or User with the specified id does not exist
|
||||
schema:
|
||||
|
@ -1554,73 +1379,6 @@ paths:
|
|||
responses:
|
||||
'204':
|
||||
description: OK. User is unlinked (empty response body)
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account or User with the specified id does not exist or user was not linked to account
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: Internal Server Error. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
|
||||
'/pdnsadmin/accounts/users/{account_id}/{user_id}':
|
||||
parameters:
|
||||
- name: account_id
|
||||
type: integer
|
||||
in: path
|
||||
required: true
|
||||
description: The id of the account to link/unlink users to account
|
||||
- name: user_id
|
||||
type: integer
|
||||
in: path
|
||||
required: true
|
||||
description: The id of the user to (un)link to/from account
|
||||
put:
|
||||
security:
|
||||
- basicAuth: []
|
||||
summary: Link user to account
|
||||
operationId: api_add_user_account
|
||||
tags:
|
||||
- account
|
||||
- user
|
||||
responses:
|
||||
'204':
|
||||
description: OK. User is linked (empty response body)
|
||||
'400':
|
||||
description: 'Request is not JSON'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account or User with the specified id does not exist
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'500':
|
||||
description: Internal Server Error. Contains error message
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
delete:
|
||||
security:
|
||||
- basicAuth: []
|
||||
summary: Unlink user from account
|
||||
operationId: api_remove_user_account
|
||||
tags:
|
||||
- account
|
||||
- user
|
||||
responses:
|
||||
'204':
|
||||
description: OK. User is unlinked (empty response body)
|
||||
'401':
|
||||
description: 'Unauthorized'
|
||||
schema:
|
||||
$ref: '#/definitions/Error'
|
||||
'404':
|
||||
description: Not found. The Account or User with the specified id does not exist or user was not linked to account
|
||||
schema:
|
||||
|
@ -1840,9 +1598,8 @@ definitions:
|
|||
|
||||
PDNSAdminZones:
|
||||
title: PDNSAdminZones
|
||||
description: 'A list of domains'
|
||||
description: A ApiKey that can be used to manage domains through API
|
||||
type: array
|
||||
x-omitempty: false
|
||||
items:
|
||||
properties:
|
||||
id:
|
||||
|
@ -1867,7 +1624,7 @@ definitions:
|
|||
|
||||
ApiKey:
|
||||
title: ApiKey
|
||||
description: 'An ApiKey that can be used to manage domains through API'
|
||||
description: A ApiKey that can be used to manage domains through API
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
|
@ -1887,23 +1644,6 @@ definitions:
|
|||
description:
|
||||
type: string
|
||||
description: 'Some user defined description'
|
||||
accounts:
|
||||
type: array
|
||||
description: 'A list of accounts bound to this ApiKey'
|
||||
items:
|
||||
$ref: '#/definitions/AccountSummary'
|
||||
|
||||
ApiKeySummary:
|
||||
title: ApiKeySummary
|
||||
description: Summary of an ApiKey
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
description: 'The ID for this key, used in the ApiKey URL endpoint.'
|
||||
readOnly: true
|
||||
description:
|
||||
type: string
|
||||
description: 'Some user defined description'
|
||||
|
||||
User:
|
||||
title: User
|
||||
|
@ -2011,12 +1751,6 @@ definitions:
|
|||
type: string
|
||||
description: The email address of the contact for this account
|
||||
readOnly: false
|
||||
apikeys:
|
||||
type: array
|
||||
description: A list of API Keys bound to this account
|
||||
readOnly: true
|
||||
items:
|
||||
$ref: '#/definitions/ApiKeySummary'
|
||||
|
||||
AccountSummary:
|
||||
title: AccountSummry
|
||||
|
@ -2030,9 +1764,6 @@ definitions:
|
|||
type: string
|
||||
description: The name for this account (unique, immutable)
|
||||
readOnly: false
|
||||
domains:
|
||||
description: The list of domains owned by this account
|
||||
$ref: '#/definitions/PDNSAdminZones'
|
||||
|
||||
ConfigSetting:
|
||||
title: ConfigSetting
|
||||
|
|
|
@ -162,4 +162,4 @@
|
|||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
|
@ -1,6 +1,5 @@
|
|||
{% 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 %}
|
||||
|
@ -50,40 +49,24 @@
|
|||
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 class="box-header with-border">
|
||||
<h3 class="box-title">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 %}>
|
||||
<div class="box-body">
|
||||
<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>
|
||||
<option {% if 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 %}
|
||||
class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %}
|
||||
Key</button>
|
||||
</div>
|
||||
</form>
|
||||
|
@ -99,7 +82,7 @@
|
|||
<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>
|
||||
<p><strong>Access Control</strong> The domains which the key has access to.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -108,48 +91,6 @@
|
|||
{% 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'>",
|
||||
|
@ -185,41 +126,6 @@
|
|||
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");
|
||||
|
@ -259,25 +165,4 @@
|
|||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
<div class="modal fade" id="modal_warning">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content modal-sm">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="button_close_warn_modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">WARNING</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-primary center-block" id="button_key_confirm_warn">
|
||||
OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
|
@ -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 <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  </td>
|
||||
<td>
|
||||
<input type="checkbox" id="domain_changelog_only_checkbox" name="domain_changelog_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-account">
|
||||
<td><label>Account Name</label></td>
|
||||
<td>
|
||||
<div class="autocomplete" style="width:250px;">
|
||||
<input type="text" class="form-control" id="account_name_filter" name="account_name_filter" placeholder="Enter * to search for any account" value="">
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
||||
<div class="tab-pane" id="tabs-auth">
|
||||
<td><label>Username</label></td>
|
||||
<td>
|
||||
<div class="autocomplete" style="width:250px;">
|
||||
<input type="text" class="form-control" id="auth_name_filter" name="auth_name_filter" placeholder="Enter * to search for any username" value="">
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div style="position: relative; top:10px;">
|
||||
<td>Authenticator Types:  </td>
|
||||
<td>  All</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_all_checkbox" name="auth_all_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
<td>  LOCAL</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_local_only_checkbox" name="auth_local_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
<td>  OAuth</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_oauth_only_checkbox" name="auth_oauth_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
<td>  SAML</td>
|
||||
<td>
|
||||
<input type="checkbox" checked id="auth_saml_only_checkbox" name="auth_saml_only_checkbox"
|
||||
class="checkbox" style="border:2px dotted #00f;display:block;background:#ff0000;">
|
||||
</td>
|
||||
</div>
|
||||
</td>
|
||||
</div>
|
||||
</div>
|
||||
<!-- End Custom Tabs -->
|
||||
|
||||
<div class="box-body">
|
||||
<table id="Filters-Table">
|
||||
<thead>
|
||||
<th>Filters</th>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><label>Changed by:  </label></td>
|
||||
<td>
|
||||
<div class="autocomplete" style="width:250px;">
|
||||
<input type="text" style=" border:1px solid #d2d6de; width:250px; height: 34px;" id="user_name_filter" name="user_name_filter" value="">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="position: relative; top:10px;">
|
||||
<label>Minimum date:  </label>
|
||||
</td>
|
||||
<td style="position: relative; top:10px;">
|
||||
<input type="text" id="min" name="min" class="datepicker" autocomplete="off" style=" border:1px solid #d2d6de; width:250px; height: 34px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="position: relative; top:20px;">
|
||||
<label>Maximum date:  </label>
|
||||
</td>
|
||||
<td style="position: relative; top:20px;">
|
||||
<input type="text" id="max" name="max" class="datepicker" autocomplete="off" style=" border:1px solid #d2d6de; width:250px; height: 34px;">
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td> </td></tr>
|
||||
<tr><td> </td></tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button type="submit" id="search-submit" name="search-submit" class="btn btn-flat btn-primary button-filter">Search <i class="fa fa-search"></i></button>
|
||||
</td>
|
||||
<td>
|
||||
<!-- -->
|
||||
<button id="clear-filters" name="clear-filters" class="btn btn-flat btn-warning button-clearf">Clear Filters <i class="fa fa-trash"></i></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</form>
|
||||
<div 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 <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 %}
|
||||
|
|
|
@ -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 <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>
|
|
@ -35,7 +35,6 @@
|
|||
<th>Role</th>
|
||||
<th>Description</th>
|
||||
<th>Domains</th>
|
||||
<th>Accounts</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -46,7 +45,6 @@
|
|||
<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) }}'">
|
||||
|
|
|
@ -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>
|
||||
|
@ -76,7 +76,7 @@
|
|||
|
||||
</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 %}
|
||||
|
@ -626,7 +626,7 @@
|
|||
</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') }}">
|
||||
<input type="text" class="form-control" name="oidc_oauth_logout_url" id="oidc_oauth_authorize_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>
|
||||
|
|
|
@ -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 %}
|
|
@ -6,7 +6,6 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<link rel="icon" href="{{ url_for('static', filename='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') }}">
|
||||
|
@ -22,9 +21,6 @@
|
|||
{% 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]>
|
||||
|
@ -118,11 +114,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 '' }}">
|
||||
|
|
|
@ -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,8 +16,6 @@
|
|||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||
|
||||
{% block content %}
|
||||
<!-- Main content -->
|
||||
<section class="content">
|
||||
|
@ -110,25 +107,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 <i class="fa fa-info"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-flat btn-primary history-info-button" value='{{ history.detail }}'>
|
||||
Info <i class="fa fa-info"></i>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -241,11 +226,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 +297,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"
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
{% 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) %}
|
||||
|
@ -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 <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 <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 <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 <i class="fa fa-history" aria-hidden="true"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
|
46
powerdnsadmin/templates/domain.html
Executable file → Normal file
46
powerdnsadmin/templates/domain.html
Executable file → Normal file
|
@ -33,17 +33,6 @@
|
|||
Update from Master <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 <i class="fa fa-cog"></i>
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if current_user.role.name in ['Administrator', 'Operator'] or SETTING.get('allow_user_view_history') %}
|
||||
<button type="button" style="position: relative; margin-left: 20px" class="btn btn-flat btn-primary button_changelog" id="{{ domain.name }}">
|
||||
Changelog <i class="fa fa-history" aria-hidden="true"></i>
|
||||
</i>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<table id="tbl_records" class="table table-bordered table-striped">
|
||||
|
@ -57,9 +46,6 @@
|
|||
<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>
|
||||
|
@ -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">
|
||||
<i class="fa fa-history" aria-hidden="true"></i>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
<!-- hidden column that we can sort on -->
|
||||
<td>1</td>
|
||||
</tr>
|
||||
|
@ -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', window.ttl_options[0][0], '', '', '', '', '0']);
|
||||
editRow($("#tbl_records").DataTable(), nRow);
|
||||
document.getElementById("edit-row-focus").focus();
|
||||
nEditing = nRow;
|
||||
|
|
|
@ -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}}   {{ record_type }}</b>
|
||||
{% else %}
|
||||
Domain changelog: <b>{{ domain.name | pretty_domain_name }}</b>
|
||||
{% endif %}
|
||||
</h1>
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||
<li>Domain</li>
|
||||
<li class="active">{{ domain.name | pretty_domain_name }}</li>
|
||||
</ol>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% import 'applied_change_macro.html' as applied_change_macro %}
|
||||
|
||||
{% block content %}
|
||||
<section class="content">
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<div class="box">
|
||||
<div class="box-body">
|
||||
<button type="button" class="btn btn-flat btn-primary pull-left button_show_records"
|
||||
id="{{ domain.name }}">
|
||||
Manage <i class="fa fa-arrow-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
|
||||
<table id="tbl_changelog" class="table table-bordered table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Changed on</th>
|
||||
<th>Changed by</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for applied_change in allHistoryChanges %}
|
||||
|
||||
<tr class="odd row_record" id="{{ domain.name }}">
|
||||
<td id="changed_on" class="changed_on">
|
||||
{{ allHistoryChanges[applied_change][0].history_entry.created_on }}
|
||||
</td>
|
||||
<td>
|
||||
{{allHistoryChanges[applied_change][0].history_entry.created_by }}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Nested Table -->
|
||||
<tr style='visibility:collapse'>
|
||||
<td colspan="2">
|
||||
<div class="content">
|
||||
{% call applied_change_macro.applied_change_template(allHistoryChanges[applied_change]) %}
|
||||
{% endcall %}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- end nested table -->
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
// handle "show records" button
|
||||
$(document.body).on("click", ".button_show_records", function (e) {
|
||||
e.stopPropagation();
|
||||
window.location.href = "/domain/{{domain.name}}";
|
||||
});
|
||||
|
||||
var coll = document.getElementsByClassName("collapsible");
|
||||
var i;
|
||||
|
||||
for (i = 0; i < coll.length; i++) {
|
||||
coll[i].addEventListener("click", function () {
|
||||
this.classList.toggle("active");
|
||||
var content = this.nextElementSibling;
|
||||
if (content.style.maxHeight) {
|
||||
content.style.maxHeight = null;
|
||||
} else {
|
||||
content.style.maxHeight = content.scrollHeight + "px";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// handle click on history record
|
||||
$(document.body).on("click", ".row_record", function (e) {
|
||||
e.stopPropagation();
|
||||
var nextRow = $(this).next('tr')
|
||||
if (nextRow.css("visibility") == "visible")
|
||||
nextRow.css("visibility", "collapse")
|
||||
else
|
||||
nextRow.css("visibility", "visible")
|
||||
|
||||
});
|
||||
|
||||
|
||||
var els = document.getElementsByClassName("changed_on");
|
||||
for (var i = 0; i < els.length; i++) {
|
||||
// els[i].innerHTML = moment.utc(els[i].innerHTML).local().format('YYYY-MM-DD HH:mm:ss');
|
||||
els[i].innerHTML = moment.utc(els[i].innerHTML,'YYYY-MM-DD HH:mm:ss').local().format('YYYY-MM-DD HH:mm:ss');
|
||||
}
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
|
@ -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">×</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 %}
|
|
@ -11,9 +11,7 @@
|
|||
{% 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 +46,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">
|
||||
|
|
|
@ -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">×</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>
|
|
@ -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>
|
||||
|
|
|
@ -8,7 +8,7 @@ mysqlclient==2.0.1
|
|||
configobj==5.0.6
|
||||
bcrypt>=3.1.7
|
||||
requests==2.24.0
|
||||
python-ldap==3.4.0
|
||||
python-ldap==3.3.1
|
||||
pyotp==2.4.0
|
||||
qrcode==6.1
|
||||
dnspython>=1.16.0
|
||||
|
@ -17,7 +17,7 @@ python3-saml
|
|||
pyOpenSSL==19.1.0
|
||||
pytz==2020.1
|
||||
cssmin==0.2.0
|
||||
jsmin==3.0.0
|
||||
jsmin==2.2.2
|
||||
Authlib==0.15
|
||||
Flask-SeaSurf==0.2.2
|
||||
bravado-core==5.17.0
|
||||
|
@ -27,4 +27,4 @@ pytimeparse==1.1.8
|
|||
PyYAML==5.4
|
||||
Flask-SSLify==0.1.5
|
||||
Flask-Mail==0.9.1
|
||||
flask-session==0.3.2
|
||||
flask-session==0.3.2
|
1990
swagger-specv2.yaml
Normal file
1990
swagger-specv2.yaml
Normal file
File diff suppressed because it is too large
Load diff
|
@ -877,8 +877,9 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f"
|
||||
|
||||
path-parse@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735"
|
||||
integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
|
||||
|
||||
path-platform@~0.11.15:
|
||||
version "0.11.15"
|
||||
|
|
Loading…
Reference in a new issue