Merge branch 'master' into password-policy
This commit is contained in:
commit
8badb3e578
1
.github/FUNDING.yml
vendored
1
.github/FUNDING.yml
vendored
|
@ -1 +0,0 @@
|
||||||
github: [ngoduykhanh]
|
|
|
@ -58,7 +58,3 @@ You can then access PowerDNS-Admin by pointing your browser to http://localhost:
|
||||||
## LICENSE
|
## LICENSE
|
||||||
MIT. See [LICENSE](https://github.com/ngoduykhanh/PowerDNS-Admin/blob/master/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>
|
|
||||||
|
|
123
docs/API.md
123
docs/API.md
|
@ -1,105 +1,134 @@
|
||||||
### API Usage
|
### API Usage
|
||||||
|
|
||||||
|
#### Getting started with docker
|
||||||
|
|
||||||
1. Run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
|
1. Run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
|
||||||
2. Click to register user, type e.g. user: admin and password: admin
|
2. Click to register user, type e.g. user: admin and password: admin
|
||||||
3. Login to UI in settings enable allow domain creation for users, now you can create and manage domains with admin account and also ordinary users
|
3. Login to UI in settings enable allow domain creation for users, now you can create and manage domains with admin account and also ordinary users
|
||||||
4. Encode your user and password to base64, in our example we have user admin and password admin so in linux cmd line we type:
|
4. Click on the API Keys menu then click on teh "Add Key" button to add a new Administrator Key
|
||||||
|
5. Keep the base64 encoded apikey somewhere safe as it won't be available in clear anymore
|
||||||
|
|
||||||
```
|
|
||||||
|
#### Accessing the API
|
||||||
|
|
||||||
|
The PDA API consists of two distinct parts:
|
||||||
|
|
||||||
|
- The /powerdnsadmin endpoints manages PDA content (accounts, users, apikeys) and also allow domain creation/deletion
|
||||||
|
- The /server endpoints are proxying queries to the backend PowerDNS instance's API. PDA acts as a proxy managing several API Keys and permissions to the PowerDNS content.
|
||||||
|
|
||||||
|
The requests to the API needs two headers:
|
||||||
|
|
||||||
|
- The classic 'Content-Type: application/json' is required to all POST and PUT requests, though it's armless to use it on each call
|
||||||
|
- The authentication header to provide either the login:password basic authentication or the Api Key authentication.
|
||||||
|
|
||||||
|
When you access the `/powerdnsadmin` endpoint, you must use the Basic Auth:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Encode your user and password to base64
|
||||||
$ echo -n 'admin:admin'|base64
|
$ echo -n 'admin:admin'|base64
|
||||||
YWRtaW46YWRtaW4=
|
YWRtaW46YWRtaW4=
|
||||||
|
# Use the ouput as your basic auth header
|
||||||
|
curl -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X <method> <url>
|
||||||
```
|
```
|
||||||
|
|
||||||
we use generated output in basic authentication, we authenticate as user,
|
When you access the `/server` endpoint, you must use the ApiKey
|
||||||
with basic authentication, we can create/delete/get zone and create/delete/get/update apikeys
|
|
||||||
|
|
||||||
creating domain:
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Use the already base64 encoded key in your header
|
||||||
|
curl -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' -X <method> <url>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Finally, the `/sync_domains` endpoint accepts both basic and apikey authentication
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
Creating domain via `/powerdnsadmin`:
|
||||||
|
|
||||||
|
```bash
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
|
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
|
||||||
```
|
```
|
||||||
|
|
||||||
creating apikey which has Administrator role, apikey can have also User role, when creating such apikey you have to specify also domain for which apikey is valid:
|
Creating an apikey which has the Administrator role:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
|
# Create the key
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
|
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
|
||||||
```
|
```
|
||||||
|
Example response (don't forget to save the plain key from the output)
|
||||||
|
|
||||||
call above will return response like this:
|
```json
|
||||||
|
[
|
||||||
```
|
{
|
||||||
[{"description": "samekey", "domains": [], "role": {"name": "Administrator", "id": 1}, "id": 2, "plain_key": "aGCthP3KLAeyjZI"}]
|
"accounts": [],
|
||||||
|
"description": "masterkey",
|
||||||
|
"domains": [],
|
||||||
|
"role": {
|
||||||
|
"name": "Administrator",
|
||||||
|
"id": 1
|
||||||
|
},
|
||||||
|
"id": 2,
|
||||||
|
"plain_key": "aGCthP3KLAeyjZI"
|
||||||
|
}
|
||||||
|
]
|
||||||
```
|
```
|
||||||
|
|
||||||
we take plain_key and base64 encode it, this is the only time we can get API key in plain text and save it somewhere:
|
We can use the apikey for all calls to PowerDNS (don't forget to specify Content-Type):
|
||||||
|
|
||||||
```
|
Getting powerdns configuration (Administrator Key is needed):
|
||||||
$ echo -n 'aGCthP3KLAeyjZI'|base64
|
|
||||||
YUdDdGhQM0tMQWV5alpJ
|
|
||||||
```
|
|
||||||
|
|
||||||
We can use apikey for all calls specified in our API specification (it tries to follow powerdns API 1:1, only tsigkeys endpoints are not yet implemented), don't forget to specify Content-Type!
|
```bash
|
||||||
|
|
||||||
getting powerdns configuration:
|
|
||||||
|
|
||||||
```
|
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
|
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
|
||||||
```
|
```
|
||||||
|
|
||||||
creating and updating records:
|
Creating and updating records:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
|
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
|
||||||
```
|
```
|
||||||
|
|
||||||
getting domain:
|
Getting a domain:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
list zone records:
|
List a zone's records:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||||
```
|
```
|
||||||
|
|
||||||
add new record:
|
Add a new record:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||||
```
|
```
|
||||||
|
|
||||||
update record:
|
Update a record:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||||
```
|
```
|
||||||
|
|
||||||
delete record:
|
Delete a record:
|
||||||
|
|
||||||
```
|
```bash
|
||||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
|
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
|
||||||
```
|
```
|
||||||
|
|
||||||
### Generate ER diagram
|
### Generate ER diagram
|
||||||
|
|
||||||
```
|
With docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install build packages
|
||||||
apt-get install python-dev graphviz libgraphviz-dev pkg-config
|
apt-get install python-dev graphviz libgraphviz-dev pkg-config
|
||||||
```
|
# Get the required python libraries
|
||||||
|
|
||||||
```
|
|
||||||
pip install graphviz mysqlclient ERAlchemy
|
pip install graphviz mysqlclient ERAlchemy
|
||||||
```
|
# Start the docker container
|
||||||
|
|
||||||
```
|
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
# Set environment variables
|
||||||
|
|
||||||
```
|
|
||||||
source .env
|
source .env
|
||||||
```
|
# Generate the diagrams
|
||||||
|
|
||||||
```
|
|
||||||
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
|
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
|
||||||
```
|
```
|
||||||
|
|
|
@ -0,0 +1,41 @@
|
||||||
|
"""add apikey account mapping table
|
||||||
|
|
||||||
|
Revision ID: 0967658d9c0d
|
||||||
|
Revises: 0d3d93f1c2e0
|
||||||
|
Create Date: 2021-11-13 22:28:46.133474
|
||||||
|
|
||||||
|
"""
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0967658d9c0d'
|
||||||
|
down_revision = '0d3d93f1c2e0'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.create_table('apikey_account',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('apikey_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('account_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['account_id'], ['account.id'], ),
|
||||||
|
sa.ForeignKeyConstraint(['apikey_id'], ['apikey.id'], ),
|
||||||
|
sa.PrimaryKeyConstraint('id')
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||||
|
batch_op.create_index(batch_op.f('ix_history_created_on'), ['created_on'], unique=False)
|
||||||
|
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||||
|
batch_op.drop_index(batch_op.f('ix_history_created_on'))
|
||||||
|
|
||||||
|
op.drop_table('apikey_account')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -55,6 +55,8 @@ def create_app(config=None):
|
||||||
csrf.exempt(routes.api.api_list_account_users)
|
csrf.exempt(routes.api.api_list_account_users)
|
||||||
csrf.exempt(routes.api.api_add_account_user)
|
csrf.exempt(routes.api.api_add_account_user)
|
||||||
csrf.exempt(routes.api.api_remove_account_user)
|
csrf.exempt(routes.api.api_remove_account_user)
|
||||||
|
csrf.exempt(routes.api.api_zone_cryptokeys)
|
||||||
|
csrf.exempt(routes.api.api_zone_cryptokey)
|
||||||
|
|
||||||
# Load config from env variables if using docker
|
# Load config from env variables if using docker
|
||||||
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
|
if os.path.exists(os.path.join(app.root_path, 'docker_config.py')):
|
||||||
|
|
|
@ -23,6 +23,7 @@ css_login = Bundle('node_modules/bootstrap/dist/css/bootstrap.css',
|
||||||
js_login = Bundle('node_modules/jquery/dist/jquery.js',
|
js_login = Bundle('node_modules/jquery/dist/jquery.js',
|
||||||
'node_modules/bootstrap/dist/js/bootstrap.js',
|
'node_modules/bootstrap/dist/js/bootstrap.js',
|
||||||
'node_modules/icheck/icheck.js',
|
'node_modules/icheck/icheck.js',
|
||||||
|
'custom/js/custom.js',
|
||||||
filters=(ConcatFilter, 'jsmin'),
|
filters=(ConcatFilter, 'jsmin'),
|
||||||
output='generated/login.js')
|
output='generated/login.js')
|
||||||
|
|
||||||
|
|
|
@ -192,6 +192,24 @@ def is_json(f):
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def callback_if_request_body_contains_key(callback, http_methods=[], keys=[]):
|
||||||
|
"""
|
||||||
|
If request body contains one or more of specified keys, call
|
||||||
|
:param callback
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
if (check_current_http_method and
|
||||||
|
set(request.get_json(force=True).keys()).intersection(set(keys))
|
||||||
|
):
|
||||||
|
callback(*args, **kwargs)
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def api_role_can(action, roles=None, allow_self=False):
|
def api_role_can(action, roles=None, allow_self=False):
|
||||||
"""
|
"""
|
||||||
Grant access if:
|
Grant access if:
|
||||||
|
@ -246,6 +264,48 @@ def api_can_create_domain(f):
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def apikey_can_create_domain(f):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in Operator role or higher, or
|
||||||
|
- allow_user_create_domain is on
|
||||||
|
"""
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
if g.apikey.role.name not in [
|
||||||
|
'Administrator', 'Operator'
|
||||||
|
] and not Setting().get('allow_user_create_domain'):
|
||||||
|
msg = "ApiKey #{0} does not have enough privileges to create domain"
|
||||||
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
|
raise NotEnoughPrivileges()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def apikey_can_remove_domain(http_methods=[]):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in Operator role or higher, or
|
||||||
|
- allow_user_remove_domain is on
|
||||||
|
"""
|
||||||
|
def decorator(f):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
|
||||||
|
if (check_current_http_method and
|
||||||
|
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||||
|
not Setting().get('allow_user_remove_domain')
|
||||||
|
):
|
||||||
|
msg = "ApiKey #{0} does not have enough privileges to remove domain"
|
||||||
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
|
raise NotEnoughPrivileges()
|
||||||
|
return f(*args, **kwargs)
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def apikey_is_admin(f):
|
def apikey_is_admin(f):
|
||||||
"""
|
"""
|
||||||
Grant access if user is in Administrator role
|
Grant access if user is in Administrator role
|
||||||
|
@ -262,21 +322,52 @@ def apikey_is_admin(f):
|
||||||
|
|
||||||
|
|
||||||
def apikey_can_access_domain(f):
|
def apikey_can_access_domain(f):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user has Operator role or higher, or
|
||||||
|
- user has explicitly been granted access to domain
|
||||||
|
"""
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
apikey = g.apikey
|
|
||||||
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
if g.apikey.role.name not in ['Administrator', 'Operator']:
|
||||||
domains = apikey.domains
|
|
||||||
zone_id = kwargs.get('zone_id').rstrip(".")
|
zone_id = kwargs.get('zone_id').rstrip(".")
|
||||||
domain_names = [item.name for item in domains]
|
domain_names = [item.name for item in g.apikey.domains]
|
||||||
|
|
||||||
if zone_id not in domain_names:
|
accounts = g.apikey.accounts
|
||||||
|
accounts_domains = [domain.name for a in accounts for domain in a.domains]
|
||||||
|
|
||||||
|
allowed_domains = set(domain_names + accounts_domains)
|
||||||
|
|
||||||
|
if zone_id not in allowed_domains:
|
||||||
raise DomainAccessForbidden()
|
raise DomainAccessForbidden()
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
||||||
return decorated_function
|
return decorated_function
|
||||||
|
|
||||||
|
|
||||||
|
def apikey_can_configure_dnssec(http_methods=[]):
|
||||||
|
"""
|
||||||
|
Grant access if:
|
||||||
|
- user is in Operator role or higher, or
|
||||||
|
- dnssec_admins_only is off
|
||||||
|
"""
|
||||||
|
def decorator(f=None):
|
||||||
|
@wraps(f)
|
||||||
|
def decorated_function(*args, **kwargs):
|
||||||
|
check_current_http_method = not http_methods or request.method in http_methods
|
||||||
|
|
||||||
|
if (check_current_http_method and
|
||||||
|
g.apikey.role.name not in ['Administrator', 'Operator'] and
|
||||||
|
Setting().get('dnssec_admins_only')
|
||||||
|
):
|
||||||
|
msg = "ApiKey #{0} does not have enough privileges to configure dnssec"
|
||||||
|
current_app.logger.error(msg.format(g.apikey.id))
|
||||||
|
raise DomainAccessForbidden(message=msg)
|
||||||
|
return f(*args, **kwargs) if f else None
|
||||||
|
return decorated_function
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
def apikey_auth(f):
|
def apikey_auth(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
|
|
|
@ -60,7 +60,8 @@ class ApiKeyNotUsable(StructuredException):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
name=None,
|
name=None,
|
||||||
message="Api key must have domains or have administrative role"):
|
message=("Api key must have domains or accounts"
|
||||||
|
" or an administrative role")):
|
||||||
StructuredException.__init__(self)
|
StructuredException.__init__(self)
|
||||||
self.message = message
|
self.message = message
|
||||||
self.name = name
|
self.name = name
|
||||||
|
@ -120,6 +121,15 @@ class AccountDeleteFail(StructuredException):
|
||||||
self.name = name
|
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):
|
class UserCreateFail(StructuredException):
|
||||||
status_code = 500
|
status_code = 500
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,21 @@ class RoleSchema(Schema):
|
||||||
name = fields.String()
|
name = fields.String()
|
||||||
|
|
||||||
|
|
||||||
|
class AccountSummarySchema(Schema):
|
||||||
|
id = fields.Integer()
|
||||||
|
name = fields.String()
|
||||||
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
|
||||||
|
class ApiKeySummarySchema(Schema):
|
||||||
|
id = fields.Integer()
|
||||||
|
description = fields.String()
|
||||||
|
|
||||||
|
|
||||||
class ApiKeySchema(Schema):
|
class ApiKeySchema(Schema):
|
||||||
id = fields.Integer()
|
id = fields.Integer()
|
||||||
role = fields.Embed(schema=RoleSchema)
|
role = fields.Embed(schema=RoleSchema)
|
||||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||||
description = fields.String()
|
description = fields.String()
|
||||||
key = fields.String()
|
key = fields.String()
|
||||||
|
|
||||||
|
@ -23,15 +34,11 @@ class ApiPlainKeySchema(Schema):
|
||||||
id = fields.Integer()
|
id = fields.Integer()
|
||||||
role = fields.Embed(schema=RoleSchema)
|
role = fields.Embed(schema=RoleSchema)
|
||||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||||
description = fields.String()
|
description = fields.String()
|
||||||
plain_key = fields.String()
|
plain_key = fields.String()
|
||||||
|
|
||||||
|
|
||||||
class AccountSummarySchema(Schema):
|
|
||||||
id = fields.Integer()
|
|
||||||
name = fields.String()
|
|
||||||
|
|
||||||
|
|
||||||
class UserSchema(Schema):
|
class UserSchema(Schema):
|
||||||
id = fields.Integer()
|
id = fields.Integer()
|
||||||
username = fields.String()
|
username = fields.String()
|
||||||
|
@ -56,3 +63,4 @@ class AccountSchema(Schema):
|
||||||
contact = fields.String()
|
contact = fields.String()
|
||||||
mail = fields.String()
|
mail = fields.String()
|
||||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||||
|
apikeys = fields.Embed(schema=ApiKeySummarySchema, many=True)
|
||||||
|
|
|
@ -8,6 +8,7 @@ from .account_user import AccountUser
|
||||||
from .server import Server
|
from .server import Server
|
||||||
from .history import History
|
from .history import History
|
||||||
from .api_key import ApiKey
|
from .api_key import ApiKey
|
||||||
|
from .api_key_account import ApiKeyAccount
|
||||||
from .setting import Setting
|
from .setting import Setting
|
||||||
from .domain import Domain
|
from .domain import Domain
|
||||||
from .domain_setting import DomainSetting
|
from .domain_setting import DomainSetting
|
||||||
|
|
|
@ -17,6 +17,9 @@ class Account(db.Model):
|
||||||
contact = db.Column(db.String(128))
|
contact = db.Column(db.String(128))
|
||||||
mail = db.Column(db.String(128))
|
mail = db.Column(db.String(128))
|
||||||
domains = db.relationship("Domain", back_populates="account")
|
domains = db.relationship("Domain", back_populates="account")
|
||||||
|
apikeys = db.relationship("ApiKey",
|
||||||
|
secondary="apikey_account",
|
||||||
|
back_populates="accounts")
|
||||||
|
|
||||||
def __init__(self, name=None, description=None, contact=None, mail=None):
|
def __init__(self, name=None, description=None, contact=None, mail=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import random
|
import secrets
|
||||||
import string
|
import string
|
||||||
import bcrypt
|
import bcrypt
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from .base import db, domain_apikey
|
from .base import db
|
||||||
from ..models.role import Role
|
from ..models.role import Role
|
||||||
from ..models.domain import Domain
|
from ..models.domain import Domain
|
||||||
|
from ..models.account import Account
|
||||||
|
|
||||||
class ApiKey(db.Model):
|
class ApiKey(db.Model):
|
||||||
__tablename__ = "apikey"
|
__tablename__ = "apikey"
|
||||||
|
@ -16,17 +16,21 @@ class ApiKey(db.Model):
|
||||||
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
role_id = db.Column(db.Integer, db.ForeignKey('role.id'))
|
||||||
role = db.relationship('Role', back_populates="apikeys", lazy=True)
|
role = db.relationship('Role', back_populates="apikeys", lazy=True)
|
||||||
domains = db.relationship("Domain",
|
domains = db.relationship("Domain",
|
||||||
secondary=domain_apikey,
|
secondary="domain_apikey",
|
||||||
back_populates="apikeys")
|
back_populates="apikeys")
|
||||||
|
accounts = db.relationship("Account",
|
||||||
|
secondary="apikey_account",
|
||||||
|
back_populates="apikeys")
|
||||||
|
|
||||||
def __init__(self, key=None, desc=None, role_name=None, domains=[]):
|
def __init__(self, key=None, desc=None, role_name=None, domains=[], accounts=[]):
|
||||||
self.id = None
|
self.id = None
|
||||||
self.description = desc
|
self.description = desc
|
||||||
self.role_name = role_name
|
self.role_name = role_name
|
||||||
self.domains[:] = domains
|
self.domains[:] = domains
|
||||||
|
self.accounts[:] = accounts
|
||||||
if not key:
|
if not key:
|
||||||
rand_key = ''.join(
|
rand_key = ''.join(
|
||||||
random.choice(string.ascii_letters + string.digits)
|
secrets.choice(string.ascii_letters + string.digits)
|
||||||
for _ in range(15))
|
for _ in range(15))
|
||||||
self.plain_key = rand_key
|
self.plain_key = rand_key
|
||||||
self.key = self.get_hashed_password(rand_key).decode('utf-8')
|
self.key = self.get_hashed_password(rand_key).decode('utf-8')
|
||||||
|
@ -54,7 +58,7 @@ class ApiKey(db.Model):
|
||||||
db.session.rollback()
|
db.session.rollback()
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
def update(self, role_name=None, description=None, domains=None):
|
def update(self, role_name=None, description=None, domains=None, accounts=None):
|
||||||
try:
|
try:
|
||||||
if role_name:
|
if role_name:
|
||||||
role = Role.query.filter(Role.name == role_name).first()
|
role = Role.query.filter(Role.name == role_name).first()
|
||||||
|
@ -63,12 +67,18 @@ class ApiKey(db.Model):
|
||||||
if description:
|
if description:
|
||||||
self.description = description
|
self.description = description
|
||||||
|
|
||||||
if domains:
|
if domains is not None:
|
||||||
domain_object_list = Domain.query \
|
domain_object_list = Domain.query \
|
||||||
.filter(Domain.name.in_(domains)) \
|
.filter(Domain.name.in_(domains)) \
|
||||||
.all()
|
.all()
|
||||||
self.domains[:] = domain_object_list
|
self.domains[:] = domain_object_list
|
||||||
|
|
||||||
|
if accounts is not None:
|
||||||
|
account_object_list = Account.query \
|
||||||
|
.filter(Account.name.in_(accounts)) \
|
||||||
|
.all()
|
||||||
|
self.accounts[:] = account_object_list
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg_str = 'Update of apikey failed. Error: {0}'
|
msg_str = 'Update of apikey failed. Error: {0}'
|
||||||
|
@ -121,3 +131,12 @@ class ApiKey(db.Model):
|
||||||
raise Exception("Unauthorized")
|
raise Exception("Unauthorized")
|
||||||
|
|
||||||
return apikey
|
return apikey
|
||||||
|
|
||||||
|
def associate_account(self, account):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def dissociate_account(self, account):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def get_accounts(self):
|
||||||
|
return True
|
||||||
|
|
20
powerdnsadmin/models/api_key_account.py
Normal file
20
powerdnsadmin/models/api_key_account.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from .base import db
|
||||||
|
|
||||||
|
|
||||||
|
class ApiKeyAccount(db.Model):
|
||||||
|
__tablename__ = 'apikey_account'
|
||||||
|
id = db.Column(db.Integer, primary_key=True)
|
||||||
|
apikey_id = db.Column(db.Integer,
|
||||||
|
db.ForeignKey('apikey.id'),
|
||||||
|
nullable=False)
|
||||||
|
account_id = db.Column(db.Integer,
|
||||||
|
db.ForeignKey('account.id'),
|
||||||
|
nullable=False)
|
||||||
|
db.UniqueConstraint('apikey_id', 'account_id', name='uniq_apikey_account')
|
||||||
|
|
||||||
|
def __init__(self, apikey_id, account_id):
|
||||||
|
self.apikey_id = apikey_id
|
||||||
|
self.account_id = account_id
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return '<ApiKey_Account {0} {1}>'.format(self.apikey_id, self.account_id)
|
|
@ -198,6 +198,8 @@ class Setting(db.Model):
|
||||||
'max_history_records': 1000,
|
'max_history_records': 1000,
|
||||||
'zxcvbn_enabled': False,
|
'zxcvbn_enabled': False,
|
||||||
'zxcvbn_guesses_log' : 11
|
'zxcvbn_guesses_log' : 11
|
||||||
|
'otp_force': False,
|
||||||
|
'max_history_records': 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, id=None, name=None, value=None):
|
def __init__(self, id=None, name=None, value=None):
|
||||||
|
|
|
@ -8,6 +8,9 @@ import ldap.filter
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from flask_login import AnonymousUserMixin
|
from flask_login import AnonymousUserMixin
|
||||||
from sqlalchemy import orm
|
from sqlalchemy import orm
|
||||||
|
import qrcode as qrc
|
||||||
|
import qrcode.image.svg as qrc_svg
|
||||||
|
from io import BytesIO
|
||||||
|
|
||||||
from .base import db
|
from .base import db
|
||||||
from .role import Role
|
from .role import Role
|
||||||
|
@ -628,10 +631,18 @@ class User(db.Model):
|
||||||
Account)\
|
Account)\
|
||||||
.filter(self.id == AccountUser.user_id)\
|
.filter(self.id == AccountUser.user_id)\
|
||||||
.filter(Account.id == AccountUser.account_id)\
|
.filter(Account.id == AccountUser.account_id)\
|
||||||
|
.order_by(Account.name)\
|
||||||
.all()
|
.all()
|
||||||
for q in query:
|
for q in query:
|
||||||
accounts.append(q[1])
|
accounts.append(q[1])
|
||||||
return accounts
|
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):
|
def read_entitlements(self, key):
|
||||||
|
@ -793,7 +804,4 @@ def getUserInfo(DomainsOrAccounts):
|
||||||
current=[]
|
current=[]
|
||||||
for DomainOrAccount in DomainsOrAccounts:
|
for DomainOrAccount in DomainsOrAccounts:
|
||||||
current.append(DomainOrAccount.name)
|
current.append(DomainOrAccount.name)
|
||||||
return current
|
return current
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import traceback
|
||||||
import re
|
import re
|
||||||
from base64 import b64encode
|
from base64 import b64encode
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from flask import Blueprint, render_template, make_response, url_for, current_app, request, redirect, jsonify, abort, flash, session
|
from flask import Blueprint, render_template, render_template_string, make_response, url_for, current_app, request, redirect, jsonify, abort, flash, session
|
||||||
from flask_login import login_required, current_user
|
from flask_login import login_required, current_user
|
||||||
|
|
||||||
from ..decorators import operator_role_required, admin_role_required, history_access_required
|
from ..decorators import operator_role_required, admin_role_required, history_access_required
|
||||||
|
@ -40,7 +40,7 @@ old_state: dictionary with "disabled" and "content" keys. {"disabled" : False, "
|
||||||
new_state: similarly
|
new_state: similarly
|
||||||
change_type: "addition" or "deletion" or "status" for status change or "unchanged" for no change
|
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,
|
Note: A change in "content", is considered a deletion and recreation of the same record,
|
||||||
holding the new content value.
|
holding the new content value.
|
||||||
"""
|
"""
|
||||||
def get_record_changes(del_rrest, add_rrest):
|
def get_record_changes(del_rrest, add_rrest):
|
||||||
|
@ -57,12 +57,12 @@ def get_record_changes(del_rrest, add_rrest):
|
||||||
{"disabled":a['disabled'],"content":a['content']},
|
{"disabled":a['disabled'],"content":a['content']},
|
||||||
"status") )
|
"status") )
|
||||||
break
|
break
|
||||||
|
|
||||||
if not exists: # deletion
|
if not exists: # deletion
|
||||||
changeSet.append( ({"disabled":d['disabled'],"content":d['content']},
|
changeSet.append( ({"disabled":d['disabled'],"content":d['content']},
|
||||||
None,
|
None,
|
||||||
"deletion") )
|
"deletion") )
|
||||||
|
|
||||||
for a in addSet: # get the additions
|
for a in addSet: # get the additions
|
||||||
exists = False
|
exists = False
|
||||||
for d in delSet:
|
for d in delSet:
|
||||||
|
@ -78,7 +78,7 @@ def get_record_changes(del_rrest, add_rrest):
|
||||||
exists = False
|
exists = False
|
||||||
for c in changeSet:
|
for c in changeSet:
|
||||||
if c[1] != None and c[1]["content"] == a['content']:
|
if c[1] != None and c[1]["content"] == a['content']:
|
||||||
exists = True
|
exists = True
|
||||||
break
|
break
|
||||||
if not exists:
|
if not exists:
|
||||||
changeSet.append( ( {"disabled":a['disabled'], "content":a['content']}, {"disabled":a['disabled'], "content":a['content']}, "unchanged") )
|
changeSet.append( ( {"disabled":a['disabled'], "content":a['content']}, {"disabled":a['disabled'], "content":a['content']}, "unchanged") )
|
||||||
|
@ -92,8 +92,9 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n
|
||||||
if history_entry.detail is None:
|
if history_entry.detail is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
detail_dict = json.loads(history_entry.detail.replace("'", '"'))
|
if "add_rrests" in history_entry.detail:
|
||||||
if "add_rrests" not in detail_dict:
|
detail_dict = json.loads(history_entry.detail.replace("\'", ''))
|
||||||
|
else: # not a record entry
|
||||||
return
|
return
|
||||||
|
|
||||||
add_rrests = detail_dict['add_rrests']
|
add_rrests = detail_dict['add_rrests']
|
||||||
|
@ -123,7 +124,7 @@ def extract_changelogs_from_a_history_entry(out_changes, history_entry, change_n
|
||||||
if change_num not in out_changes:
|
if change_num not in out_changes:
|
||||||
out_changes[change_num] = []
|
out_changes[change_num] = []
|
||||||
out_changes[change_num].append(HistoryRecordEntry(history_entry, del_rrest, [], "-"))
|
out_changes[change_num].append(HistoryRecordEntry(history_entry, del_rrest, [], "-"))
|
||||||
|
|
||||||
|
|
||||||
# only used for changelog per record
|
# 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 record_name != None and record_type != None: # then get only the records with the specific (record_name, record_type) tuple
|
||||||
|
@ -172,7 +173,7 @@ class HistoryRecordEntry:
|
||||||
if add_rrest['ttl'] != del_rrest['ttl']:
|
if add_rrest['ttl'] != del_rrest['ttl']:
|
||||||
self.changed_fields.append("ttl")
|
self.changed_fields.append("ttl")
|
||||||
self.changeSet = get_record_changes(del_rrest, add_rrest)
|
self.changeSet = get_record_changes(del_rrest, add_rrest)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def toDict(self):
|
def toDict(self):
|
||||||
|
@ -300,6 +301,7 @@ def edit_user(user_username=None):
|
||||||
@operator_role_required
|
@operator_role_required
|
||||||
def edit_key(key_id=None):
|
def edit_key(key_id=None):
|
||||||
domains = Domain.query.all()
|
domains = Domain.query.all()
|
||||||
|
accounts = Account.query.all()
|
||||||
roles = Role.query.all()
|
roles = Role.query.all()
|
||||||
apikey = None
|
apikey = None
|
||||||
create = True
|
create = True
|
||||||
|
@ -316,6 +318,7 @@ def edit_key(key_id=None):
|
||||||
return render_template('admin_edit_key.html',
|
return render_template('admin_edit_key.html',
|
||||||
key=apikey,
|
key=apikey,
|
||||||
domains=domains,
|
domains=domains,
|
||||||
|
accounts=accounts,
|
||||||
roles=roles,
|
roles=roles,
|
||||||
create=create)
|
create=create)
|
||||||
|
|
||||||
|
@ -323,14 +326,21 @@ def edit_key(key_id=None):
|
||||||
fdata = request.form
|
fdata = request.form
|
||||||
description = fdata['description']
|
description = fdata['description']
|
||||||
role = fdata.getlist('key_role')[0]
|
role = fdata.getlist('key_role')[0]
|
||||||
doamin_list = fdata.getlist('key_multi_domain')
|
domain_list = fdata.getlist('key_multi_domain')
|
||||||
|
account_list = fdata.getlist('key_multi_account')
|
||||||
|
|
||||||
# Create new apikey
|
# Create new apikey
|
||||||
if create:
|
if create:
|
||||||
domain_obj_list = Domain.query.filter(Domain.name.in_(doamin_list)).all()
|
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 = [], []
|
||||||
|
|
||||||
apikey = ApiKey(desc=description,
|
apikey = ApiKey(desc=description,
|
||||||
role_name=role,
|
role_name=role,
|
||||||
domains=domain_obj_list)
|
domains=domain_obj_list,
|
||||||
|
accounts=account_obj_list)
|
||||||
try:
|
try:
|
||||||
apikey.create()
|
apikey.create()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -344,7 +354,9 @@ def edit_key(key_id=None):
|
||||||
# Update existing apikey
|
# Update existing apikey
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
apikey.update(role,description,doamin_list)
|
if role != "User":
|
||||||
|
domain_list, account_list = [], []
|
||||||
|
apikey.update(role,description,domain_list, account_list)
|
||||||
history_message = "Updated API key {0}".format(apikey.id)
|
history_message = "Updated API key {0}".format(apikey.id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error('Error: {0}'.format(e))
|
current_app.logger.error('Error: {0}'.format(e))
|
||||||
|
@ -354,14 +366,16 @@ def edit_key(key_id=None):
|
||||||
'key': apikey.id,
|
'key': apikey.id,
|
||||||
'role': apikey.role.name,
|
'role': apikey.role.name,
|
||||||
'description': apikey.description,
|
'description': apikey.description,
|
||||||
'domain_acl': [domain.name for domain in apikey.domains]
|
'domains': [domain.name for domain in apikey.domains],
|
||||||
|
'accounts': [a.name for a in apikey.accounts]
|
||||||
}),
|
}),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
|
|
||||||
return render_template('admin_edit_key.html',
|
return render_template('admin_edit_key.html',
|
||||||
key=apikey,
|
key=apikey,
|
||||||
domains=domains,
|
domains=domains,
|
||||||
|
accounts=accounts,
|
||||||
roles=roles,
|
roles=roles,
|
||||||
create=create,
|
create=create,
|
||||||
plain_key=plain_key)
|
plain_key=plain_key)
|
||||||
|
@ -390,7 +404,7 @@ def manage_keys():
|
||||||
history_apikey_role = apikey.role.name
|
history_apikey_role = apikey.role.name
|
||||||
history_apikey_description = apikey.description
|
history_apikey_description = apikey.description
|
||||||
history_apikey_domains = [ domain.name for domain in apikey.domains]
|
history_apikey_domains = [ domain.name for domain in apikey.domains]
|
||||||
|
|
||||||
apikey.delete()
|
apikey.delete()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error('Error: {0}'.format(e))
|
current_app.logger.error('Error: {0}'.format(e))
|
||||||
|
@ -740,110 +754,140 @@ def manage_account():
|
||||||
|
|
||||||
|
|
||||||
class DetailedHistory():
|
class DetailedHistory():
|
||||||
def __init__(self, history, change_set):
|
def __init__(self, history, change_set):
|
||||||
self.history = history
|
self.history = history
|
||||||
self.detailed_msg = ""
|
self.detailed_msg = ""
|
||||||
self.change_set = change_set
|
self.change_set = change_set
|
||||||
|
|
||||||
if history.detail is None:
|
|
||||||
self.detailed_msg = ""
|
|
||||||
# if 'Create account' in history.msg:
|
|
||||||
# account = Account.query.filter(
|
|
||||||
# Account.name == history.msg.split(' ')[2]).first()
|
|
||||||
# self.detailed_msg = str(account.get_user())
|
|
||||||
# # WRONG, cannot do query afterwards, db may have changed
|
|
||||||
return
|
|
||||||
|
|
||||||
detail_dict = json.loads(history.detail.replace("'", '"'))
|
if not history.detail:
|
||||||
if 'domain_type' in detail_dict.keys() and 'account_id' in detail_dict.keys(): # this is a domain creation
|
self.detailed_msg = ""
|
||||||
self.detailed_msg = """
|
return
|
||||||
<table class="table table-bordered table-striped"><tr><td>Domain type:</td><td>{0}</td></tr> <tr><td>Account:</td><td>{1}</td></tr></table>
|
|
||||||
""".format(detail_dict['domain_type'],
|
|
||||||
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.keys(): # this is a user authentication
|
|
||||||
self.detailed_msg = """
|
|
||||||
<table class="table table-bordered table-striped" style="width:565px;">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th colspan="3" style="background:
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Change table header background colour depending on auth success or failure
|
|
||||||
if detail_dict['success'] == 1:
|
|
||||||
self.detailed_msg+= """
|
|
||||||
rgba(68,157,68);"> <p style="color:white;">
|
|
||||||
User {0} authentication success
|
|
||||||
</p></th>
|
|
||||||
""".format(detail_dict['username'])
|
|
||||||
|
|
||||||
else:
|
if 'add_rrest' in history.detail:
|
||||||
self.detailed_msg+= """
|
detail_dict = json.loads(history.detail.replace("\'", ''))
|
||||||
rgba(201,48,44);"> <p style="color:white;">
|
else:
|
||||||
User {0} authentication failure
|
detail_dict = json.loads(history.detail.replace("'", '"'))
|
||||||
</th>
|
|
||||||
""".format(detail_dict['username'])
|
|
||||||
|
|
||||||
self.detailed_msg+= """
|
if 'domain_type' in detail_dict and 'account_id' in detail_dict: # this is a domain creation
|
||||||
</tr>
|
self.detailed_msg = render_template_string("""
|
||||||
</thead>
|
<table class="table table-bordered table-striped">
|
||||||
<tbody>
|
<tr><td>Domain type:</td><td>{{ domaintype }}</td></tr>
|
||||||
<tr>
|
<tr><td>Account:</td><td>{{ account }}</td></tr>
|
||||||
<td>Authenticator Type:</td>
|
</table>
|
||||||
<td colspan="2">{0}</td>
|
""",
|
||||||
</tr>
|
domaintype=detail_dict['domain_type'],
|
||||||
<tr>
|
account=Account.get_name_by_id(self=None, account_id=detail_dict['account_id']) if detail_dict['account_id'] != "0" else "None")
|
||||||
<td>IP Address</td>
|
|
||||||
<td colspan="2">{1}</td>
|
elif 'authenticator' in detail_dict: # this is a user authentication
|
||||||
</tr>
|
self.detailed_msg = render_template_string("""
|
||||||
</tbody>
|
<table class="table table-bordered table-striped" style="width:565px;">
|
||||||
</table>
|
<thead>
|
||||||
""".format(detail_dict['authenticator'], detail_dict['ip_address'])
|
<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(), '')))
|
||||||
|
|
||||||
elif 'add_rrests' in detail_dict.keys(): # 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.keys() and 'template' in history.msg: # template creation
|
|
||||||
self.detailed_msg = """
|
|
||||||
<table class="table table-bordered table-striped"><tr><td>Template name:</td><td>{0}</td></tr> <tr><td>Description:</td><td>{1}</td></tr></table>
|
|
||||||
""".format(detail_dict['name'], detail_dict['description'])
|
|
||||||
elif 'Change domain' in history.msg and 'access control' in history.msg: # added or removed a user from a domain
|
|
||||||
self.detailed_msg = """
|
|
||||||
<table class="table table-bordered table-striped"><tr><td>Users with access to this domain</td><td>{0}</td></tr><tr><td>Number of users:</td><td>{1}</td><tr></table>
|
|
||||||
""".format(str(detail_dict['user_has_access']).replace("]","").replace("[", ""), len((detail_dict['user_has_access'])))
|
|
||||||
elif 'Created API key' in history.msg or 'Updated API key' in history.msg:
|
|
||||||
self.detailed_msg = """
|
|
||||||
<table class="table table-bordered table-striped">
|
|
||||||
<tr><td>Key: </td><td>{0}</td></tr>
|
|
||||||
<tr><td>Role:</td><td>{1}</td></tr>
|
|
||||||
<tr><td>Description:</td><td>{2}</td></tr>
|
|
||||||
<tr><td>Accessible domains with this API key:</td><td>{3}</td></tr>
|
|
||||||
</table>
|
|
||||||
""".format(detail_dict['key'], detail_dict['role'], detail_dict['description'], str(detail_dict['domain_acl']).replace("]","").replace("[", ""))
|
|
||||||
elif 'Update type for domain' in history.msg:
|
|
||||||
self.detailed_msg = """
|
|
||||||
<table class="table table-bordered table-striped">
|
|
||||||
<tr><td>Domain: </td><td>{0}</td></tr>
|
|
||||||
<tr><td>Domain type:</td><td>{1}</td></tr>
|
|
||||||
<tr><td>Masters:</td><td>{2}</td></tr>
|
|
||||||
</table>
|
|
||||||
""".format(detail_dict['domain'], detail_dict['type'], str(detail_dict['masters']).replace("]","").replace("[", ""))
|
|
||||||
elif 'Delete API key' in history.msg:
|
|
||||||
self.detailed_msg = """
|
|
||||||
<table class="table table-bordered table-striped">
|
|
||||||
<tr><td>Key: </td><td>{0}</td></tr>
|
|
||||||
<tr><td>Role:</td><td>{1}</td></tr>
|
|
||||||
<tr><td>Description:</td><td>{2}</td></tr>
|
|
||||||
<tr><td>Accessible domains with this API key:</td><td>{3}</td></tr>
|
|
||||||
</table>
|
|
||||||
""".format(detail_dict['key'], detail_dict['role'], detail_dict['description'], str(detail_dict['domains']).replace("]","").replace("[", ""))
|
|
||||||
elif 'reverse' in history.msg:
|
|
||||||
self.detailed_msg = """
|
|
||||||
<table class="table table-bordered table-striped">
|
|
||||||
<tr><td>Domain Type: </td><td>{0}</td></tr>
|
|
||||||
<tr><td>Domain Master IPs:</td><td>{1}</td></tr>
|
|
||||||
</table>
|
|
||||||
""".format(detail_dict['domain_type'], detail_dict['domain_master_ips'])
|
|
||||||
|
|
||||||
# convert a list of History objects into DetailedHistory objects
|
# convert a list of History objects into DetailedHistory objects
|
||||||
def convert_histories(histories):
|
def convert_histories(histories):
|
||||||
|
@ -851,8 +895,7 @@ def convert_histories(histories):
|
||||||
detailedHistories = []
|
detailedHistories = []
|
||||||
j = 0
|
j = 0
|
||||||
for i in range(len(histories)):
|
for i in range(len(histories)):
|
||||||
# if histories[i].detail != None and 'add_rrests' in json.loads(histories[i].detail.replace("'", '"')):
|
if histories[i].detail and ('add_rrests' in histories[i].detail or 'del_rrests' in histories[i].detail):
|
||||||
if histories[i].detail != None and ('add_rrests' in json.loads(histories[i].detail.replace("'", '"')) or 'del_rrests' in json.loads(histories[i].detail.replace("'", '"'))):
|
|
||||||
extract_changelogs_from_a_history_entry(changes_set, histories[i], j)
|
extract_changelogs_from_a_history_entry(changes_set, histories[i], j)
|
||||||
if j in changes_set:
|
if j in changes_set:
|
||||||
detailedHistories.append(DetailedHistory(histories[i], changes_set[j]))
|
detailedHistories.append(DetailedHistory(histories[i], changes_set[j]))
|
||||||
|
@ -895,7 +938,7 @@ def history():
|
||||||
}), 500)
|
}), 500)
|
||||||
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
doms = accounts = users = ""
|
doms = accounts = users = ""
|
||||||
if current_user.role.name in [ 'Administrator', 'Operator']:
|
if current_user.role.name in [ 'Administrator', 'Operator']:
|
||||||
all_domain_names = Domain.query.all()
|
all_domain_names = Domain.query.all()
|
||||||
|
@ -903,7 +946,7 @@ def history():
|
||||||
all_user_names = User.query.all()
|
all_user_names = User.query.all()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
for d in all_domain_names:
|
for d in all_domain_names:
|
||||||
doms += d.name + " "
|
doms += d.name + " "
|
||||||
for acc in all_account_names:
|
for acc in all_account_names:
|
||||||
|
@ -931,9 +974,9 @@ def history():
|
||||||
AccountUser.user_id == current_user.id
|
AccountUser.user_id == current_user.id
|
||||||
)).all()
|
)).all()
|
||||||
|
|
||||||
|
|
||||||
all_user_names = []
|
all_user_names = []
|
||||||
for a in all_account_names:
|
for a in all_account_names:
|
||||||
temp = db.session.query(User) \
|
temp = db.session.query(User) \
|
||||||
.join(AccountUser, AccountUser.user_id == User.id) \
|
.join(AccountUser, AccountUser.user_id == User.id) \
|
||||||
.outerjoin(Account, Account.id == AccountUser.account_id) \
|
.outerjoin(Account, Account.id == AccountUser.account_id) \
|
||||||
|
@ -951,11 +994,11 @@ def history():
|
||||||
|
|
||||||
for d in all_domain_names:
|
for d in all_domain_names:
|
||||||
doms += d.name + " "
|
doms += d.name + " "
|
||||||
|
|
||||||
for a in all_account_names:
|
for a in all_account_names:
|
||||||
accounts += a.name + " "
|
accounts += a.name + " "
|
||||||
for u in all_user_names:
|
for u in all_user_names:
|
||||||
users += u.username + " "
|
users += u.username + " "
|
||||||
return render_template('admin_history.html', all_domain_names=doms, all_account_names=accounts, all_usernames=users)
|
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
|
# local_offset is the offset of the utc to the local time
|
||||||
|
@ -1005,7 +1048,7 @@ def history_table(): # ajax call data
|
||||||
if current_user.role.name in [ 'Administrator', 'Operator' ]:
|
if current_user.role.name in [ 'Administrator', 'Operator' ]:
|
||||||
base_query = History.query
|
base_query = History.query
|
||||||
else:
|
else:
|
||||||
# if the user isn't an administrator or operator,
|
# if the user isn't an administrator or operator,
|
||||||
# allow_user_view_history must be enabled to get here,
|
# allow_user_view_history must be enabled to get here,
|
||||||
# so include history for the domains for the user
|
# so include history for the domains for the user
|
||||||
base_query = db.session.query(History) \
|
base_query = db.session.query(History) \
|
||||||
|
@ -1020,7 +1063,7 @@ def history_table(): # ajax call data
|
||||||
))
|
))
|
||||||
|
|
||||||
domain_name = request.args.get('domain_name_filter') if request.args.get('domain_name_filter') != None \
|
domain_name = request.args.get('domain_name_filter') if request.args.get('domain_name_filter') != None \
|
||||||
and len(request.args.get('domain_name_filter')) != 0 else 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 \
|
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
|
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 \
|
user_name = request.args.get('auth_name_filter') if request.args.get('auth_name_filter') != None \
|
||||||
|
@ -1217,8 +1260,7 @@ def setting_basic():
|
||||||
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
|
'allow_user_create_domain', 'allow_user_remove_domain', 'allow_user_view_history', 'bg_domain_updates', 'site_name',
|
||||||
'session_timeout', 'warn_session_timeout', 'ttl_options',
|
'session_timeout', 'warn_session_timeout', 'ttl_options',
|
||||||
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
|
'pdns_api_timeout', 'verify_ssl_connections', 'verify_user_email',
|
||||||
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records'
|
'delete_sso_accounts', 'otp_field_enabled', 'custom_css', 'enable_api_rr_history', 'max_history_records', 'otp_force'
|
||||||
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return render_template('admin_setting_basic.html', settings=settings)
|
return render_template('admin_setting_basic.html', settings=settings)
|
||||||
|
|
|
@ -21,16 +21,18 @@ from ..lib.errors import (
|
||||||
DomainNotExists, DomainAlreadyExists, DomainAccessForbidden,
|
DomainNotExists, DomainAlreadyExists, DomainAccessForbidden,
|
||||||
RequestIsNotJSON, ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges,
|
RequestIsNotJSON, ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges,
|
||||||
AccountCreateFail, AccountUpdateFail, AccountDeleteFail,
|
AccountCreateFail, AccountUpdateFail, AccountDeleteFail,
|
||||||
AccountCreateDuplicate,
|
AccountCreateDuplicate, AccountNotExists,
|
||||||
UserCreateFail, UserCreateDuplicate, UserUpdateFail, UserDeleteFail,
|
UserCreateFail, UserCreateDuplicate, UserUpdateFail, UserDeleteFail,
|
||||||
UserUpdateFailEmail,
|
UserUpdateFailEmail,
|
||||||
)
|
)
|
||||||
from ..decorators import (
|
from ..decorators import (
|
||||||
api_basic_auth, api_can_create_domain, is_json, apikey_auth,
|
api_basic_auth, api_can_create_domain, is_json, apikey_auth,
|
||||||
apikey_is_admin, apikey_can_access_domain, api_role_can,
|
apikey_can_create_domain, apikey_can_remove_domain,
|
||||||
apikey_or_basic_auth,
|
apikey_is_admin, apikey_can_access_domain, apikey_can_configure_dnssec,
|
||||||
|
api_role_can, apikey_or_basic_auth,
|
||||||
|
callback_if_request_body_contains_key,
|
||||||
)
|
)
|
||||||
import random
|
import secrets
|
||||||
import string
|
import string
|
||||||
|
|
||||||
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
api_bp = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||||
|
@ -307,6 +309,7 @@ def api_generate_apikey():
|
||||||
role_name = None
|
role_name = None
|
||||||
apikey = None
|
apikey = None
|
||||||
domain_obj_list = []
|
domain_obj_list = []
|
||||||
|
account_obj_list = []
|
||||||
|
|
||||||
abort(400) if 'role' not in data else None
|
abort(400) if 'role' not in data else None
|
||||||
|
|
||||||
|
@ -317,6 +320,13 @@ def api_generate_apikey():
|
||||||
else:
|
else:
|
||||||
domains = [d['name'] if isinstance(d, dict) else d for d in data['domains']]
|
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
|
description = data['description'] if 'description' in data else None
|
||||||
|
|
||||||
if isinstance(data['role'], str):
|
if isinstance(data['role'], str):
|
||||||
|
@ -326,16 +336,24 @@ def api_generate_apikey():
|
||||||
else:
|
else:
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
if role_name == 'User' and len(domains) == 0:
|
if role_name == 'User' and len(domains) == 0 and len(accounts) == 0:
|
||||||
current_app.logger.error("Apikey with User role must have domains")
|
current_app.logger.error("Apikey with User role must have domains or accounts")
|
||||||
raise ApiKeyNotUsable()
|
raise ApiKeyNotUsable()
|
||||||
elif role_name == 'User':
|
|
||||||
|
if role_name == 'User' and len(domains) > 0:
|
||||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
||||||
if len(domain_obj_list) == 0:
|
if len(domain_obj_list) == 0:
|
||||||
msg = "One of supplied domains does not exist"
|
msg = "One of supplied domains does not exist"
|
||||||
current_app.logger.error(msg)
|
current_app.logger.error(msg)
|
||||||
raise DomainNotExists(message=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']:
|
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||||
# domain list of domain api key should be valid for
|
# domain list of domain api key should be valid for
|
||||||
# if not any domain error
|
# if not any domain error
|
||||||
|
@ -345,6 +363,11 @@ def api_generate_apikey():
|
||||||
current_app.logger.error(msg)
|
current_app.logger.error(msg)
|
||||||
raise NotEnoughPrivileges(message=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()
|
user_domain_obj_list = get_user_domains()
|
||||||
|
|
||||||
domain_list = [item.name for item in domain_obj_list]
|
domain_list = [item.name for item in domain_obj_list]
|
||||||
|
@ -363,7 +386,8 @@ def api_generate_apikey():
|
||||||
|
|
||||||
apikey = ApiKey(desc=description,
|
apikey = ApiKey(desc=description,
|
||||||
role_name=role_name,
|
role_name=role_name,
|
||||||
domains=domain_obj_list)
|
domains=domain_obj_list,
|
||||||
|
accounts=account_obj_list)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
apikey.create()
|
apikey.create()
|
||||||
|
@ -476,9 +500,16 @@ def api_update_apikey(apikey_id):
|
||||||
# if role different and user is allowed to change it, update
|
# if role different and user is allowed to change it, update
|
||||||
# if apikey domains are different and user is allowed to handle
|
# if apikey domains are different and user is allowed to handle
|
||||||
# that domains update domains
|
# 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()
|
data = request.get_json()
|
||||||
description = data['description'] if 'description' in data else None
|
description = data['description'] if 'description' in data else None
|
||||||
domain_obj_list = None
|
|
||||||
|
|
||||||
if 'role' in data:
|
if 'role' in data:
|
||||||
if isinstance(data['role'], str):
|
if isinstance(data['role'], str):
|
||||||
|
@ -487,8 +518,11 @@ def api_update_apikey(apikey_id):
|
||||||
role_name = data['role']['name']
|
role_name = data['role']['name']
|
||||||
else:
|
else:
|
||||||
abort(400)
|
abort(400)
|
||||||
|
|
||||||
|
target_role = role_name
|
||||||
else:
|
else:
|
||||||
role_name = None
|
role_name = None
|
||||||
|
target_role = apikey.role.name
|
||||||
|
|
||||||
if 'domains' not in data:
|
if 'domains' not in data:
|
||||||
domains = None
|
domains = None
|
||||||
|
@ -497,22 +531,54 @@ def api_update_apikey(apikey_id):
|
||||||
else:
|
else:
|
||||||
domains = [d['name'] if isinstance(d, dict) else d for d in data['domains']]
|
domains = [d['name'] if isinstance(d, dict) else d for d in data['domains']]
|
||||||
|
|
||||||
apikey = ApiKey.query.get(apikey_id)
|
if 'accounts' not in data:
|
||||||
|
accounts = None
|
||||||
if not apikey:
|
elif not isinstance(data['accounts'], (list, )):
|
||||||
abort(404)
|
abort(400)
|
||||||
|
else:
|
||||||
|
accounts = [a['name'] if isinstance(a, dict) else a for a in data['accounts']]
|
||||||
|
|
||||||
current_app.logger.debug('Updating apikey with id {0}'.format(apikey_id))
|
current_app.logger.debug('Updating apikey with id {0}'.format(apikey_id))
|
||||||
|
|
||||||
if role_name == 'User' and len(domains) == 0:
|
if target_role == 'User':
|
||||||
current_app.logger.error("Apikey with User role must have domains")
|
current_domains = [item.name for item in apikey.domains]
|
||||||
raise ApiKeyNotUsable()
|
current_accounts = [item.name for item in apikey.accounts]
|
||||||
elif role_name == 'User':
|
|
||||||
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
if domains is not None:
|
||||||
if len(domain_obj_list) == 0:
|
domain_obj_list = Domain.query.filter(Domain.name.in_(domains)).all()
|
||||||
msg = "One of supplied domains does not exist"
|
if len(domain_obj_list) != len(domains):
|
||||||
current_app.logger.error(msg)
|
msg = "One of supplied domains does not exist"
|
||||||
raise DomainNotExists(message=msg)
|
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 current_user.role.name not in ['Administrator', 'Operator']:
|
if current_user.role.name not in ['Administrator', 'Operator']:
|
||||||
if role_name != 'User':
|
if role_name != 'User':
|
||||||
|
@ -520,8 +586,12 @@ def api_update_apikey(apikey_id):
|
||||||
current_app.logger.error(msg)
|
current_app.logger.error(msg)
|
||||||
raise NotEnoughPrivileges(message=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()
|
apikeys = get_user_apikeys()
|
||||||
apikey_domains = [item.name for item in apikey.domains]
|
|
||||||
apikeys_ids = [apikey_item.id for apikey_item in apikeys]
|
apikeys_ids = [apikey_item.id for apikey_item in apikeys]
|
||||||
|
|
||||||
user_domain_obj_list = current_user.get_domain().all()
|
user_domain_obj_list = current_user.get_domain().all()
|
||||||
|
@ -545,12 +615,7 @@ def api_update_apikey(apikey_id):
|
||||||
current_app.logger.error(msg)
|
current_app.logger.error(msg)
|
||||||
raise DomainAccessForbidden()
|
raise DomainAccessForbidden()
|
||||||
|
|
||||||
if set(domains) == set(apikey_domains):
|
if role_name == apikey.role.name:
|
||||||
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")
|
current_app.logger.debug("Role is same, apikey role won't be updated")
|
||||||
role_name = None
|
role_name = None
|
||||||
|
|
||||||
|
@ -559,10 +624,13 @@ def api_update_apikey(apikey_id):
|
||||||
current_app.logger.debug(msg)
|
current_app.logger.debug(msg)
|
||||||
description = None
|
description = None
|
||||||
|
|
||||||
|
if target_role != "User":
|
||||||
|
domains, accounts = [], []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
apikey = ApiKey.query.get(apikey_id)
|
|
||||||
apikey.update(role_name=role_name,
|
apikey.update(role_name=role_name,
|
||||||
domains=domains,
|
domains=domains,
|
||||||
|
accounts=accounts,
|
||||||
description=description)
|
description=description)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error('Error: {0}'.format(e))
|
current_app.logger.error('Error: {0}'.format(e))
|
||||||
|
@ -621,7 +689,7 @@ def api_create_user():
|
||||||
|
|
||||||
if not plain_text_password and not password:
|
if not plain_text_password and not password:
|
||||||
plain_text_password = ''.join(
|
plain_text_password = ''.join(
|
||||||
random.choice(string.ascii_letters + string.digits)
|
secrets.choice(string.ascii_letters + string.digits)
|
||||||
for _ in range(15))
|
for _ in range(15))
|
||||||
if not role_name and not role_id:
|
if not role_name and not role_id:
|
||||||
role_name = 'User'
|
role_name = 'User'
|
||||||
|
@ -856,7 +924,7 @@ def api_update_account(account_id):
|
||||||
"Updating account {} ({})".format(account_id, account.name))
|
"Updating account {} ({})".format(account_id, account.name))
|
||||||
result = account.update_account()
|
result = account.update_account()
|
||||||
if not result['status']:
|
if not result['status']:
|
||||||
raise AccountDeleteFail(message=result['msg'])
|
raise AccountUpdateFail(message=result['msg'])
|
||||||
history = History(msg='Update account {0}'.format(account.name),
|
history = History(msg='Update account {0}'.format(account.name),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
history.add()
|
history.add()
|
||||||
|
@ -876,7 +944,7 @@ def api_delete_account(account_id):
|
||||||
"Deleting account {} ({})".format(account_id, account.name))
|
"Deleting account {} ({})".format(account_id, account.name))
|
||||||
result = account.delete_account()
|
result = account.delete_account()
|
||||||
if not result:
|
if not result:
|
||||||
raise AccountUpdateFail(message=result['msg'])
|
raise AccountDeleteFail(message=result['msg'])
|
||||||
|
|
||||||
history = History(msg='Delete account {0}'.format(account.name),
|
history = History(msg='Delete account {0}'.format(account.name),
|
||||||
created_by=current_user.username)
|
created_by=current_user.username)
|
||||||
|
@ -957,6 +1025,28 @@ def api_remove_account_user(account_id, user_id):
|
||||||
return '', 204
|
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(
|
@api_bp.route(
|
||||||
'/servers/<string:server_id>/zones/<string:zone_id>/<path:subpath>',
|
'/servers/<string:server_id>/zones/<string:zone_id>/<path:subpath>',
|
||||||
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
methods=['GET', 'POST', 'PUT', 'PATCH', 'DELETE'])
|
||||||
|
@ -971,6 +1061,10 @@ def api_zone_subpath_forward(server_id, zone_id, subpath):
|
||||||
methods=['GET', 'PUT', 'PATCH', 'DELETE'])
|
methods=['GET', 'PUT', 'PATCH', 'DELETE'])
|
||||||
@apikey_auth
|
@apikey_auth
|
||||||
@apikey_can_access_domain
|
@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):
|
def api_zone_forward(server_id, zone_id):
|
||||||
resp = helper.forward_request()
|
resp = helper.forward_request()
|
||||||
if not Setting().get('bg_domain_updates'):
|
if not Setting().get('bg_domain_updates'):
|
||||||
|
@ -1004,6 +1098,7 @@ def api_zone_forward(server_id, zone_id):
|
||||||
history.add()
|
history.add()
|
||||||
return resp.content, resp.status_code, resp.headers.items()
|
return resp.content, resp.status_code, resp.headers.items()
|
||||||
|
|
||||||
|
|
||||||
@api_bp.route('/servers/<path:subpath>', methods=['GET', 'PUT'])
|
@api_bp.route('/servers/<path:subpath>', methods=['GET', 'PUT'])
|
||||||
@apikey_auth
|
@apikey_auth
|
||||||
@apikey_is_admin
|
@apikey_is_admin
|
||||||
|
@ -1014,6 +1109,7 @@ def api_server_sub_forward(subpath):
|
||||||
|
|
||||||
@api_bp.route('/servers/<string:server_id>/zones', methods=['POST'])
|
@api_bp.route('/servers/<string:server_id>/zones', methods=['POST'])
|
||||||
@apikey_auth
|
@apikey_auth
|
||||||
|
@apikey_can_create_domain
|
||||||
def api_create_zone(server_id):
|
def api_create_zone(server_id):
|
||||||
resp = helper.forward_request()
|
resp = helper.forward_request()
|
||||||
|
|
||||||
|
@ -1055,8 +1151,13 @@ def api_get_zones(server_id):
|
||||||
and resp.status_code == 200):
|
and resp.status_code == 200):
|
||||||
domain_list = [d['name']
|
domain_list = [d['name']
|
||||||
for d in domain_schema.dump(g.apikey.domains)]
|
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)
|
content = json.dumps([i for i in json.loads(resp.content)
|
||||||
if i['name'].rstrip('.') in domain_list])
|
if i['name'].rstrip('.') in allowed_domains])
|
||||||
return content, resp.status_code, resp.headers.items()
|
return content, resp.status_code, resp.headers.items()
|
||||||
else:
|
else:
|
||||||
return resp.content, resp.status_code, resp.headers.items()
|
return resp.content, resp.status_code, resp.headers.items()
|
||||||
|
|
|
@ -340,7 +340,8 @@ def record_changelog(domain_name, record_name, record_type):
|
||||||
for i in indexes_to_pop:
|
for i in indexes_to_pop:
|
||||||
changes_set_of_record.pop(i)
|
changes_set_of_record.pop(i)
|
||||||
|
|
||||||
return render_template('domain_changelog.html', domain=domain, allHistoryChanges=changes_set_of_record)
|
return render_template('domain_changelog.html', domain=domain, allHistoryChanges=changes_set_of_record,
|
||||||
|
record_name = record_name, record_type = record_type)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import json
|
||||||
import traceback
|
import traceback
|
||||||
import datetime
|
import datetime
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import base64
|
||||||
from distutils.util import strtobool
|
from distutils.util import strtobool
|
||||||
from yaml import Loader, load
|
from yaml import Loader, load
|
||||||
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
from onelogin.saml2.utils import OneLogin_Saml2_Utils
|
||||||
|
@ -168,10 +169,8 @@ def login():
|
||||||
return redirect(url_for('index.login'))
|
return redirect(url_for('index.login'))
|
||||||
|
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
login_user(user, remember=False)
|
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
signin_history(user.username, 'Google OAuth', True)
|
return authenticate_user(user, 'Google OAuth')
|
||||||
return redirect(url_for('index.index'))
|
|
||||||
|
|
||||||
if 'github_token' in session:
|
if 'github_token' in session:
|
||||||
me = json.loads(github.get('user').text)
|
me = json.loads(github.get('user').text)
|
||||||
|
@ -196,9 +195,7 @@ def login():
|
||||||
|
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
login_user(user, remember=False)
|
return authenticate_user(user, 'Github OAuth')
|
||||||
signin_history(user.username, 'Github OAuth', True)
|
|
||||||
return redirect(url_for('index.index'))
|
|
||||||
|
|
||||||
if 'azure_token' in session:
|
if 'azure_token' in session:
|
||||||
azure_info = azure.get('me?$select=displayName,givenName,id,mail,surname,userPrincipalName').text
|
azure_info = azure.get('me?$select=displayName,givenName,id,mail,surname,userPrincipalName').text
|
||||||
|
@ -367,10 +364,7 @@ def login():
|
||||||
history.add()
|
history.add()
|
||||||
current_app.logger.warning('group info: {} '.format(account_id))
|
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:
|
if 'oidc_token' in session:
|
||||||
me = json.loads(oidc.get('userinfo').text)
|
me = json.loads(oidc.get('userinfo').text)
|
||||||
|
@ -434,9 +428,7 @@ def login():
|
||||||
|
|
||||||
session['user_id'] = user.id
|
session['user_id'] = user.id
|
||||||
session['authentication_type'] = 'OAuth'
|
session['authentication_type'] = 'OAuth'
|
||||||
login_user(user, remember=False)
|
return authenticate_user(user, 'OIDC OAuth')
|
||||||
signin_history(user.username, 'OIDC OAuth', True)
|
|
||||||
return redirect(url_for('index.index'))
|
|
||||||
|
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
return render_template('login.html', saml_enabled=SAML_ENABLED)
|
return render_template('login.html', saml_enabled=SAML_ENABLED)
|
||||||
|
@ -513,9 +505,7 @@ def login():
|
||||||
user.revoke_privilege(True)
|
user.revoke_privilege(True)
|
||||||
current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' )
|
current_app.logger.warning('Procceding to revoke every privilige from ' + user.username + '.' )
|
||||||
|
|
||||||
login_user(user, remember=remember_me)
|
return authenticate_user(user, 'LOCAL', remember_me)
|
||||||
signin_history(user.username, 'LOCAL', True)
|
|
||||||
return redirect(session.get('next', url_for('index.index')))
|
|
||||||
|
|
||||||
def checkForPDAEntries(Entitlements, urn_value):
|
def checkForPDAEntries(Entitlements, urn_value):
|
||||||
"""
|
"""
|
||||||
|
@ -585,6 +575,23 @@ def get_azure_groups(uri):
|
||||||
mygroups = []
|
mygroups = []
|
||||||
return mygroups
|
return mygroups
|
||||||
|
|
||||||
|
# Handle user login, write history and, if set, handle showing the register_otp QR code.
|
||||||
|
# if Setting for OTP on first login is enabled, and OTP field is also enabled,
|
||||||
|
# but user isn't using it yet, enable OTP, get QR code and display it, logging the user out.
|
||||||
|
def authenticate_user(user, authenticator, remember=False):
|
||||||
|
login_user(user, remember=remember)
|
||||||
|
signin_history(user.username, authenticator, True)
|
||||||
|
if Setting().get('otp_force') and Setting().get('otp_field_enabled') and not user.otp_secret:
|
||||||
|
user.update_profile(enable_otp=True)
|
||||||
|
user_id = current_user.id
|
||||||
|
prepare_welcome_user(user_id)
|
||||||
|
return redirect(url_for('index.welcome'))
|
||||||
|
return redirect(url_for('index.login'))
|
||||||
|
|
||||||
|
# Prepare user to enter /welcome screen, otherwise they won't have permission to do so
|
||||||
|
def prepare_welcome_user(user_id):
|
||||||
|
logout_user()
|
||||||
|
session['welcome_user_id'] = user_id
|
||||||
|
|
||||||
@index_bp.route('/logout')
|
@index_bp.route('/logout')
|
||||||
def logout():
|
def logout():
|
||||||
|
@ -778,7 +785,12 @@ def register():
|
||||||
if result and result['status']:
|
if result and result['status']:
|
||||||
if Setting().get('verify_user_email'):
|
if Setting().get('verify_user_email'):
|
||||||
send_account_verification(email)
|
send_account_verification(email)
|
||||||
return redirect(url_for('index.login'))
|
if Setting().get('otp_force') and Setting().get('otp_field_enabled'):
|
||||||
|
user.update_profile(enable_otp=True)
|
||||||
|
prepare_welcome_user(user.id)
|
||||||
|
return redirect(url_for('index.welcome'))
|
||||||
|
else:
|
||||||
|
return redirect(url_for('index.login'))
|
||||||
else:
|
else:
|
||||||
return render_template('register.html',
|
return render_template('register.html',
|
||||||
error=result['msg'])
|
error=result['msg'])
|
||||||
|
@ -788,6 +800,28 @@ def register():
|
||||||
return render_template('errors/404.html'), 404
|
return render_template('errors/404.html'), 404
|
||||||
|
|
||||||
|
|
||||||
|
# Show welcome page on first login if otp_force is enabled
|
||||||
|
@index_bp.route('/welcome', methods=['GET', 'POST'])
|
||||||
|
def welcome():
|
||||||
|
if 'welcome_user_id' not in session:
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
|
user = User(id=session['welcome_user_id'])
|
||||||
|
encoded_img_data = base64.b64encode(user.get_qrcode_value())
|
||||||
|
|
||||||
|
if request.method == 'GET':
|
||||||
|
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user)
|
||||||
|
elif request.method == 'POST':
|
||||||
|
otp_token = request.form.get('otptoken', '')
|
||||||
|
if otp_token and otp_token.isdigit():
|
||||||
|
good_token = user.verify_totp(otp_token)
|
||||||
|
if not good_token:
|
||||||
|
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Invalid token")
|
||||||
|
else:
|
||||||
|
return render_template('register_otp.html', qrcode_image=encoded_img_data.decode(), user=user, error="Token required")
|
||||||
|
session.pop('welcome_user_id')
|
||||||
|
return redirect(url_for('index.index'))
|
||||||
|
|
||||||
@index_bp.route('/confirm/<token>', methods=['GET'])
|
@index_bp.route('/confirm/<token>', methods=['GET'])
|
||||||
def confirm_email(token):
|
def confirm_email(token):
|
||||||
email = confirm_token(token)
|
email = confirm_token(token)
|
||||||
|
@ -1141,9 +1175,7 @@ def saml_authorized():
|
||||||
user.plain_text_password = None
|
user.plain_text_password = None
|
||||||
user.update_profile()
|
user.update_profile()
|
||||||
session['authentication_type'] = 'SAML'
|
session['authentication_type'] = 'SAML'
|
||||||
login_user(user, remember=False)
|
return authenticate_user(user, 'SAML')
|
||||||
signin_history(user.username, 'SAML', True)
|
|
||||||
return redirect(url_for('index.login'))
|
|
||||||
else:
|
else:
|
||||||
return render_template('errors/SAML.html', errors=errors)
|
return render_template('errors/SAML.html', errors=errors)
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
import datetime
|
import datetime
|
||||||
import qrcode as qrc
|
|
||||||
import qrcode.image.svg as qrc_svg
|
|
||||||
from io import BytesIO
|
|
||||||
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app
|
from flask import Blueprint, request, render_template, make_response, jsonify, redirect, url_for, g, session, current_app
|
||||||
from flask_login import current_user, login_required, login_manager
|
from flask_login import current_user, login_required, login_manager
|
||||||
|
|
||||||
|
@ -97,13 +94,9 @@ def qrcode():
|
||||||
if not current_user:
|
if not current_user:
|
||||||
return redirect(url_for('index'))
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
img = qrc.make(current_user.get_totp_uri(),
|
return current_user.get_qrcode_value(), 200, {
|
||||||
image_factory=qrc_svg.SvgPathImage)
|
|
||||||
stream = BytesIO()
|
|
||||||
img.save(stream)
|
|
||||||
return stream.getvalue(), 200, {
|
|
||||||
'Content-Type': 'image/svg+xml',
|
'Content-Type': 'image/svg+xml',
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
'Pragma': 'no-cache',
|
'Pragma': 'no-cache',
|
||||||
'Expires': '0'
|
'Expires': '0'
|
||||||
}
|
}
|
|
@ -285,4 +285,14 @@ function timer(elToUpdate, maxTime) {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
|
|
||||||
return interval;
|
return interval;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy otp secret code to clipboard
|
||||||
|
function copy_otp_secret_to_clipboard() {
|
||||||
|
var copyBox = document.getElementById("otp_secret");
|
||||||
|
copyBox.select();
|
||||||
|
copyBox.setSelectionRange(0, 99999); /* For mobile devices */
|
||||||
|
navigator.clipboard.writeText(copyBox.value);
|
||||||
|
$("#copy_tooltip").css("visibility", "visible");
|
||||||
|
setTimeout(function(){ $("#copy_tooltip").css("visibility", "collapse"); }, 2000);
|
||||||
|
}
|
|
@ -797,6 +797,11 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/PDNSAdminZones'
|
$ref: '#/definitions/PDNSAdminZones'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
post:
|
post:
|
||||||
security:
|
security:
|
||||||
- basicAuth: []
|
- basicAuth: []
|
||||||
|
@ -816,6 +821,23 @@ paths:
|
||||||
description: A zone
|
description: A zone
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Zone'
|
$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}':
|
'/pdnsadmin/zones/{zone_id}':
|
||||||
parameters:
|
parameters:
|
||||||
- name: zone_id
|
- name: zone_id
|
||||||
|
@ -839,6 +861,23 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: 'Returns 204 No Content on success.'
|
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':
|
'/pdnsadmin/apikeys':
|
||||||
get:
|
get:
|
||||||
security:
|
security:
|
||||||
|
@ -854,15 +893,23 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/ApiKey'
|
$ref: '#/definitions/ApiKey'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
|
'403':
|
||||||
|
description: 'Domain Access Forbidden'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
'500':
|
||||||
description: 'Internal Server Error, keys could not be retrieved. Contains error message'
|
description: 'Internal Server Error. There was a problem creating the key'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
post:
|
post:
|
||||||
security:
|
security:
|
||||||
- basicAuth: []
|
- basicAuth: []
|
||||||
summary: 'Add a ApiKey key'
|
summary: 'Add a ApiKey key'
|
||||||
description: 'This methods add a new ApiKey. The actual key can be generated by the server or be provided by the client'
|
description: 'This methods add a new ApiKey. The actual key is generated by the server'
|
||||||
operationId: api_generate_apikey
|
operationId: api_generate_apikey
|
||||||
tags:
|
tags:
|
||||||
- apikey
|
- apikey
|
||||||
|
@ -878,14 +925,27 @@ paths:
|
||||||
description: Created
|
description: Created
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ApiKey'
|
$ref: '#/definitions/ApiKey'
|
||||||
'422':
|
'400':
|
||||||
description: 'Unprocessable Entry, the ApiKey provided has issues.'
|
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'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
'500':
|
||||||
description: 'Internal Server Error. There was a problem creating the key'
|
description: 'Internal Server Error. There was a problem creating the key'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
'/pdnsadmin/apikeys/{apikey_id}':
|
'/pdnsadmin/apikeys/{apikey_id}':
|
||||||
parameters:
|
parameters:
|
||||||
- name: apikey_id
|
- name: apikey_id
|
||||||
|
@ -905,14 +965,16 @@ paths:
|
||||||
description: OK.
|
description: OK.
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ApiKey'
|
$ref: '#/definitions/ApiKey'
|
||||||
'403':
|
'401':
|
||||||
description: 'The authenticated user has User role and is not allowed on any of the domains assigned to the key'
|
description: 'Unauthorized'
|
||||||
'404':
|
|
||||||
description: 'Not found. The ApiKey with the specified apikey_id does not exist'
|
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
'403':
|
||||||
description: 'Internal Server Error, keys could not be retrieved. Contains error message'
|
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:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
delete:
|
delete:
|
||||||
|
@ -925,6 +987,14 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: 'OK, key was deleted'
|
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':
|
'404':
|
||||||
description: 'Not found. The ApiKey with the specified apikey_id does not exist'
|
description: 'Not found. The ApiKey with the specified apikey_id does not exist'
|
||||||
schema:
|
schema:
|
||||||
|
@ -938,9 +1008,11 @@ paths:
|
||||||
- basicAuth: []
|
- basicAuth: []
|
||||||
description: |
|
description: |
|
||||||
The ApiKey at apikey_id can be changed in multiple ways:
|
The ApiKey at apikey_id can be changed in multiple ways:
|
||||||
* Role, description, domains can be updated
|
* Role, description, accounts and domains can be updated
|
||||||
* Role can be changed to Administrator only if user has Operator or Administrator privileges
|
* 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
|
* 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.
|
Only the relevant fields have to be provided in the request body.
|
||||||
operationId: api_update_apikey
|
operationId: api_update_apikey
|
||||||
tags:
|
tags:
|
||||||
|
@ -957,14 +1029,27 @@ paths:
|
||||||
description: OK. ApiKey is changed.
|
description: OK. ApiKey is changed.
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/ApiKey'
|
$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':
|
'404':
|
||||||
description: 'Not found. The TSIGKey with the specified tsigkey_id does not exist'
|
description: 'Not found (ApiKey, Domain or Account)'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
'500':
|
||||||
description: 'Internal Server Error. Contains error message'
|
description: 'Internal Server Error. Contains error message'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
'/pdnsadmin/users':
|
'/pdnsadmin/users':
|
||||||
get:
|
get:
|
||||||
security:
|
security:
|
||||||
|
@ -980,6 +1065,10 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/User'
|
$ref: '#/definitions/User'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
'500':
|
||||||
description: Internal Server Error, users could not be retrieved. Contains error message
|
description: Internal Server Error, users could not be retrieved. Contains error message
|
||||||
schema:
|
schema:
|
||||||
|
@ -1038,7 +1127,11 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/User'
|
$ref: '#/definitions/User'
|
||||||
'400':
|
'400':
|
||||||
description: Unprocessable Entry, the User data provided has issues
|
description: 'Request is not JSON'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
'409':
|
'409':
|
||||||
|
@ -1049,6 +1142,7 @@ paths:
|
||||||
description: Internal Server Error. There was a problem creating the user
|
description: Internal Server Error. There was a problem creating the user
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
'/pdnsadmin/users/{username}':
|
'/pdnsadmin/users/{username}':
|
||||||
parameters:
|
parameters:
|
||||||
- name: username
|
- name: username
|
||||||
|
@ -1068,6 +1162,10 @@ paths:
|
||||||
description: Retrieve a specific User
|
description: Retrieve a specific User
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/UserDetailed'
|
$ref: '#/definitions/UserDetailed'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'404':
|
'404':
|
||||||
description: Not found. The User with the specified username does not exist
|
description: Not found. The User with the specified username does not exist
|
||||||
schema:
|
schema:
|
||||||
|
@ -1076,6 +1174,7 @@ paths:
|
||||||
description: Internal Server Error, user could not be retrieved. Contains error message
|
description: Internal Server Error, user could not be retrieved. Contains error message
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
'/pdnsadmin/users/{user_id}':
|
'/pdnsadmin/users/{user_id}':
|
||||||
parameters:
|
parameters:
|
||||||
- name: user_id
|
- name: user_id
|
||||||
|
@ -1129,10 +1228,22 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: OK. User is modified (empty response body)
|
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':
|
'404':
|
||||||
description: Not found. The User with the specified user_id does not exist
|
description: Not found. The User with the specified user_id does not exist
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
'409':
|
||||||
|
description: Duplicate (Email already assigned to another user)
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
'500':
|
||||||
description: Internal Server Error. Contains error message
|
description: Internal Server Error. Contains error message
|
||||||
schema:
|
schema:
|
||||||
|
@ -1147,6 +1258,10 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: OK. User is deleted (empty response body)
|
description: OK. User is deleted (empty response body)
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'404':
|
'404':
|
||||||
description: Not found. The User with the specified user_id does not exist
|
description: Not found. The User with the specified user_id does not exist
|
||||||
schema:
|
schema:
|
||||||
|
@ -1155,6 +1270,7 @@ paths:
|
||||||
description: Internal Server Error. Contains error message
|
description: Internal Server Error. Contains error message
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
'/pdnsadmin/accounts':
|
'/pdnsadmin/accounts':
|
||||||
get:
|
get:
|
||||||
security:
|
security:
|
||||||
|
@ -1170,8 +1286,8 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/Account'
|
$ref: '#/definitions/Account'
|
||||||
'500':
|
'401':
|
||||||
description: Internal Server Error, accounts could not be retrieved. Contains error message
|
description: 'Unauthorized'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
post:
|
post:
|
||||||
|
@ -1207,7 +1323,11 @@ paths:
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Account'
|
$ref: '#/definitions/Account'
|
||||||
'400':
|
'400':
|
||||||
description: Unprocessable Entry, the Account data provided has issues.
|
description: 'Request is not JSON'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
'409':
|
'409':
|
||||||
|
@ -1218,6 +1338,7 @@ paths:
|
||||||
description: Internal Server Error. There was a problem creating the account
|
description: Internal Server Error. There was a problem creating the account
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
'/pdnsadmin/accounts/{account_name}':
|
'/pdnsadmin/accounts/{account_name}':
|
||||||
parameters:
|
parameters:
|
||||||
- name: account_name
|
- name: account_name
|
||||||
|
@ -1237,14 +1358,15 @@ paths:
|
||||||
description: Retrieve a specific account
|
description: Retrieve a specific account
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Account'
|
$ref: '#/definitions/Account'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'404':
|
'404':
|
||||||
description: Not found. The Account with the specified name does not exist
|
description: Not found. The Account with the specified name does not exist
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
|
||||||
description: Internal Server Error, account could not be retrieved. Contains error message
|
|
||||||
schema:
|
|
||||||
$ref: '#/definitions/Error'
|
|
||||||
'/pdnsadmin/accounts/{account_id}':
|
'/pdnsadmin/accounts/{account_id}':
|
||||||
parameters:
|
parameters:
|
||||||
- name: account_id
|
- name: account_id
|
||||||
|
@ -1281,6 +1403,14 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: OK. Account is modified (empty response body)
|
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':
|
'404':
|
||||||
description: Not found. The Account with the specified account_id does not exist
|
description: Not found. The Account with the specified account_id does not exist
|
||||||
schema:
|
schema:
|
||||||
|
@ -1299,6 +1429,10 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: OK. Account is deleted (empty response body)
|
description: OK. Account is deleted (empty response body)
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'404':
|
'404':
|
||||||
description: Not found. The Account with the specified account_id does not exist
|
description: Not found. The Account with the specified account_id does not exist
|
||||||
schema:
|
schema:
|
||||||
|
@ -1307,6 +1441,7 @@ paths:
|
||||||
description: Internal Server Error. Contains error message
|
description: Internal Server Error. Contains error message
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
|
|
||||||
'/pdnsadmin/accounts/{account_id}/users':
|
'/pdnsadmin/accounts/{account_id}/users':
|
||||||
parameters:
|
parameters:
|
||||||
- name: account_id
|
- name: account_id
|
||||||
|
@ -1329,14 +1464,46 @@ paths:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
$ref: '#/definitions/User'
|
$ref: '#/definitions/User'
|
||||||
|
'401':
|
||||||
|
description: 'Unauthorized'
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/Error'
|
||||||
'404':
|
'404':
|
||||||
description: Not found. The Account with the specified account_id does not exist
|
description: Not found. The Account with the specified account_id does not exist
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$ref: '#/definitions/Error'
|
||||||
'500':
|
|
||||||
description: Internal Server Error, accounts could not be retrieved. Contains error message
|
'/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'
|
||||||
schema:
|
schema:
|
||||||
$ref: '#/definitions/Error'
|
$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}':
|
'/pdnsadmin/accounts/{account_id}/users/{user_id}':
|
||||||
parameters:
|
parameters:
|
||||||
- name: account_id
|
- name: account_id
|
||||||
|
@ -1360,6 +1527,14 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: OK. User is linked (empty response body)
|
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':
|
'404':
|
||||||
description: Not found. The Account or User with the specified id does not exist
|
description: Not found. The Account or User with the specified id does not exist
|
||||||
schema:
|
schema:
|
||||||
|
@ -1379,6 +1554,73 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'204':
|
'204':
|
||||||
description: OK. User is unlinked (empty response body)
|
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':
|
'404':
|
||||||
description: Not found. The Account or User with the specified id does not exist or user was not linked to account
|
description: Not found. The Account or User with the specified id does not exist or user was not linked to account
|
||||||
schema:
|
schema:
|
||||||
|
@ -1598,8 +1840,9 @@ definitions:
|
||||||
|
|
||||||
PDNSAdminZones:
|
PDNSAdminZones:
|
||||||
title: PDNSAdminZones
|
title: PDNSAdminZones
|
||||||
description: A ApiKey that can be used to manage domains through API
|
description: 'A list of domains'
|
||||||
type: array
|
type: array
|
||||||
|
x-omitempty: false
|
||||||
items:
|
items:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
|
@ -1624,7 +1867,7 @@ definitions:
|
||||||
|
|
||||||
ApiKey:
|
ApiKey:
|
||||||
title: ApiKey
|
title: ApiKey
|
||||||
description: A ApiKey that can be used to manage domains through API
|
description: 'An ApiKey that can be used to manage domains through API'
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
type: integer
|
type: integer
|
||||||
|
@ -1644,6 +1887,23 @@ definitions:
|
||||||
description:
|
description:
|
||||||
type: string
|
type: string
|
||||||
description: 'Some user defined description'
|
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:
|
User:
|
||||||
title: User
|
title: User
|
||||||
|
@ -1751,6 +2011,12 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
description: The email address of the contact for this account
|
description: The email address of the contact for this account
|
||||||
readOnly: false
|
readOnly: false
|
||||||
|
apikeys:
|
||||||
|
type: array
|
||||||
|
description: A list of API Keys bound to this account
|
||||||
|
readOnly: true
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/ApiKeySummary'
|
||||||
|
|
||||||
AccountSummary:
|
AccountSummary:
|
||||||
title: AccountSummry
|
title: AccountSummry
|
||||||
|
@ -1764,6 +2030,9 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
description: The name for this account (unique, immutable)
|
description: The name for this account (unique, immutable)
|
||||||
readOnly: false
|
readOnly: false
|
||||||
|
domains:
|
||||||
|
description: The list of domains owned by this account
|
||||||
|
$ref: '#/definitions/PDNSAdminZones'
|
||||||
|
|
||||||
ConfigSetting:
|
ConfigSetting:
|
||||||
title: ConfigSetting
|
title: ConfigSetting
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "admin_keys" %}
|
{% 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 %}
|
{% block title %}
|
||||||
<title>Edit Key - {{ SITE_NAME }}</title>
|
<title>Edit Key - {{ SITE_NAME }}</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
@ -49,10 +50,26 @@
|
||||||
class="glyphicon glyphicon-pencil form-control-feedback"></span>
|
class="glyphicon glyphicon-pencil form-control-feedback"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-header with-border">
|
<div class="box-header with-border key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
<h3 class="box-title">Access Control</h3>
|
<h3 class="box-title">Accounts Access Control</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-body">
|
<div class="box-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
|
<p>This key will be linked to the accounts on the right,</p>
|
||||||
|
<p>thus granting access to domains owned by the selected accounts.</p>
|
||||||
|
<p>Click on accounts to move between the columns.</p>
|
||||||
|
<div class="form-group col-xs-2">
|
||||||
|
<select multiple="multiple" class="form-control" id="key_multi_account"
|
||||||
|
name="key_multi_account">
|
||||||
|
{% for account in accounts %}
|
||||||
|
<option {% if key and account in key.accounts %}selected{% endif %} value="{{ account.name }}">{{ account.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="box-header with-border key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
|
<h3 class="box-title">Domain Access Control</h3>
|
||||||
|
</div>
|
||||||
|
<div class="box-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||||
<p>This key will have acess to the domains on the right.</p>
|
<p>This key will have acess to the domains on the right.</p>
|
||||||
<p>Click on domains to move between the columns.</p>
|
<p>Click on domains to move between the columns.</p>
|
||||||
<div class="form-group col-xs-2">
|
<div class="form-group col-xs-2">
|
||||||
|
@ -66,7 +83,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="box-footer">
|
<div class="box-footer">
|
||||||
<button type="submit"
|
<button type="submit"
|
||||||
class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %}
|
class="btn btn-flat btn-primary" id="key_submit">{% if create %}Create{% else %}Update{% endif %}
|
||||||
Key</button>
|
Key</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -82,7 +99,7 @@
|
||||||
<p>Fill in all the fields in the form to the left.</p>
|
<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>Role</strong> The role of the key.</p>
|
||||||
<p><strong>Description</strong> The key description.</p>
|
<p><strong>Description</strong> The key description.</p>
|
||||||
<p><strong>Access Control</strong> The domains which the key has access to.</p>
|
<p><strong>Access Control</strong> The domains or accounts which the key has access to.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -91,6 +108,48 @@
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block extrascripts %}
|
{% block extrascripts %}
|
||||||
<script>
|
<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({
|
$("#key_multi_domain").multiSelect({
|
||||||
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
||||||
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
||||||
|
@ -126,6 +185,41 @@
|
||||||
this.qs2.cache();
|
this.qs2.cache();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
$("#key_multi_account").multiSelect({
|
||||||
|
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Account Name'>",
|
||||||
|
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Account Name'>",
|
||||||
|
afterInit: function (ms) {
|
||||||
|
var that = this,
|
||||||
|
$selectableSearch = that.$selectableUl.prev(),
|
||||||
|
$selectionSearch = that.$selectionUl.prev(),
|
||||||
|
selectableSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selectable:not(.ms-selected)',
|
||||||
|
selectionSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selection.ms-selected';
|
||||||
|
|
||||||
|
that.qs1 = $selectableSearch.quicksearch(selectableSearchString)
|
||||||
|
.on('keydown', function (e) {
|
||||||
|
if (e.which === 40) {
|
||||||
|
that.$selectableUl.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
that.qs2 = $selectionSearch.quicksearch(selectionSearchString)
|
||||||
|
.on('keydown', function (e) {
|
||||||
|
if (e.which == 40) {
|
||||||
|
that.$selectionUl.focus();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
afterSelect: function () {
|
||||||
|
this.qs1.cache();
|
||||||
|
this.qs2.cache();
|
||||||
|
},
|
||||||
|
afterDeselect: function () {
|
||||||
|
this.qs1.cache();
|
||||||
|
this.qs2.cache();
|
||||||
|
}
|
||||||
|
});
|
||||||
{% if plain_key %}
|
{% if plain_key %}
|
||||||
$(document.body).ready(function () {
|
$(document.body).ready(function () {
|
||||||
var modal = $("#modal_show_key");
|
var modal = $("#modal_show_key");
|
||||||
|
@ -165,4 +259,25 @@
|
||||||
</div>
|
</div>
|
||||||
<!-- /.modal-dialog -->
|
<!-- /.modal-dialog -->
|
||||||
</div>
|
</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 %}
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
<th>Role</th>
|
<th>Role</th>
|
||||||
<th>Description</th>
|
<th>Description</th>
|
||||||
<th>Domains</th>
|
<th>Domains</th>
|
||||||
|
<th>Accounts</th>
|
||||||
<th>Action</th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
@ -45,6 +46,7 @@
|
||||||
<td>{{ key.role.name }}</td>
|
<td>{{ key.role.name }}</td>
|
||||||
<td>{{ key.description }}</td>
|
<td>{{ key.description }}</td>
|
||||||
<td>{% for domain in key.domains %}{{ domain.name }}{% if not loop.last %}, {% endif %}{% endfor %}</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%">
|
<td width="15%">
|
||||||
<button type="button" class="btn btn-flat btn-success button_edit"
|
<button type="button" class="btn btn-flat btn-success button_edit"
|
||||||
onclick="window.location.href='{{ url_for('admin.edit_key', key_id=key.id) }}'">
|
onclick="window.location.href='{{ url_for('admin.edit_key', key_id=key.id) }}'">
|
||||||
|
|
|
@ -4,7 +4,11 @@
|
||||||
{% block dashboard_stat %}
|
{% block dashboard_stat %}
|
||||||
<section class="content-header">
|
<section class="content-header">
|
||||||
<h1>
|
<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>
|
Domain changelog: <b>{{ domain.name | pretty_domain_name }}</b>
|
||||||
|
{% endif %}
|
||||||
</h1>
|
</h1>
|
||||||
<ol class="breadcrumb">
|
<ol class="breadcrumb">
|
||||||
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
<li><a href="{{ url_for('dashboard.dashboard') }}"><i class="fa fa-dashboard"></i> Home</a></li>
|
||||||
|
|
|
@ -50,7 +50,7 @@
|
||||||
</div>
|
</div>
|
||||||
{% if SETTING.get('otp_field_enabled') %}
|
{% if SETTING.get('otp_field_enabled') %}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken">
|
<input type="otptoken" class="form-control" placeholder="OTP Token" name="otptoken" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
|
{% if SETTING.get('ldap_enabled') and SETTING.get('local_db_enabled') %}
|
||||||
|
|
90
powerdnsadmin/templates/register_otp.html
Executable file
90
powerdnsadmin/templates/register_otp.html
Executable file
|
@ -0,0 +1,90 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Welcome - {{ SITE_NAME }}</title>
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='img/favicon.png') }}">
|
||||||
|
<!-- Tell the browser to be responsive to screen width -->
|
||||||
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
{% assets "css_login" -%}
|
||||||
|
<link rel="stylesheet" href="{{ ASSET_URL }}">
|
||||||
|
{%- endassets %}
|
||||||
|
{% if SETTING.get('custom_css') %}
|
||||||
|
<link rel="stylesheet" href="/static/custom/{{ SETTING.get('custom_css') }}">
|
||||||
|
{% endif %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="hold-transition register-page">
|
||||||
|
<div class="register-box">
|
||||||
|
<div class="register-logo">
|
||||||
|
<a><b>PowerDNS</b>-Admin</a>
|
||||||
|
</div>
|
||||||
|
<div class="register-box-body">
|
||||||
|
{% if error %}
|
||||||
|
<div class="alert alert-danger alert-dismissible">
|
||||||
|
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
|
||||||
|
{{ error }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
Welcome, {{user.firstname}}! <br />
|
||||||
|
You will need a Token on login. <br />
|
||||||
|
Your QR code is:
|
||||||
|
<div id="token_information">
|
||||||
|
{% if qrcode_image == None %}
|
||||||
|
<p><img id="qrcode" src="{{ url_for('user.qrcode') }}"></p>
|
||||||
|
{% else %}
|
||||||
|
<p><img id="qrcode" src="data:image/svg+xml;utf8;base64, {{qrcode_image}}"></p>
|
||||||
|
{% endif %}
|
||||||
|
<p>
|
||||||
|
Your secret key is: <br />
|
||||||
|
<form>
|
||||||
|
<input type=text id="otp_secret" value={{user.otp_secret}} readonly>
|
||||||
|
<button type=button style="position:relative; right:28px" onclick="copy_otp_secret_to_clipboard()"> <i class="fa fa-clipboard"></i> </button>
|
||||||
|
<br /><font color="red" id="copy_tooltip" style="visibility:collapse">Copied.</font>
|
||||||
|
</form>
|
||||||
|
</p>
|
||||||
|
You can use Google Authenticator (<a target="_blank"
|
||||||
|
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
|
||||||
|
- <a target="_blank"
|
||||||
|
href="https://apps.apple.com/us/app/google-authenticator/id388497605">iOS</a>)
|
||||||
|
<br />
|
||||||
|
or FreeOTP (<a target="_blank"
|
||||||
|
href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=en">Android</a>
|
||||||
|
- <a target="_blank"
|
||||||
|
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
|
||||||
|
on your smartphone <br /> to scan the QR code or type the secret key.
|
||||||
|
<br /> <br />
|
||||||
|
<font color="red"><strong><i>Make sure only you can see this QR Code <br />
|
||||||
|
and secret key, and nobody can capture them.</i></strong></font>
|
||||||
|
</div>
|
||||||
|
</br>
|
||||||
|
Please input your OTP token to continue, to ensure the seed has been scanned correctly.
|
||||||
|
<form action="" method="post" data-toggle="validator">
|
||||||
|
<input type="hidden" name="_csrf_token" value="{{ csrf_token() }}">
|
||||||
|
<div class="form-group">
|
||||||
|
<input type="text" class="form-control" placeholder="OTP Token" name="otptoken"
|
||||||
|
data-error="Please input your OTP token" required>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4">
|
||||||
|
<button type="submit" class="btn btn-flat btn-primary btn-block">Continue</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="login-box-footer">
|
||||||
|
<center>
|
||||||
|
<p>Powered by <a href="https://github.com/ngoduykhanh/PowerDNS-Admin">PowerDNS-Admin</a></p>
|
||||||
|
</center>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
{% assets "js_login" -%}
|
||||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
|
{%- endassets %}
|
||||||
|
{% assets "js_validation" -%}
|
||||||
|
<script type="text/javascript" src="{{ ASSET_URL }}"></script>
|
||||||
|
{%- endassets %}
|
||||||
|
</html>
|
|
@ -114,6 +114,14 @@
|
||||||
{% if current_user.otp_secret %}
|
{% if current_user.otp_secret %}
|
||||||
<div id="token_information">
|
<div id="token_information">
|
||||||
<p><img id="qrcode" src="{{ url_for('user.qrcode') }}"></p>
|
<p><img id="qrcode" src="{{ url_for('user.qrcode') }}"></p>
|
||||||
|
<div style="position: relative; left: 15px">
|
||||||
|
Your secret key is: <br />
|
||||||
|
<form>
|
||||||
|
<input type=text id="otp_secret" value={{current_user.otp_secret}} readonly>
|
||||||
|
<button type=button style="position:relative; right:28px" onclick="copy_otp_secret_to_clipboard()"> <i class="fa fa-clipboard"></i> </button>
|
||||||
|
<br /><font color="red" id="copy_tooltip" style="visibility:collapse">Copied.</font>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
You can use Google Authenticator (<a target="_blank"
|
You can use Google Authenticator (<a target="_blank"
|
||||||
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
|
href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Android</a>
|
||||||
- <a target="_blank"
|
- <a target="_blank"
|
||||||
|
@ -124,8 +132,8 @@
|
||||||
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
|
href="https://itunes.apple.com/en/app/freeotp-authenticator/id872559395?mt=8">iOS</a>)
|
||||||
on your smartphone to scan the QR code.
|
on your smartphone to scan the QR code.
|
||||||
<br />
|
<br />
|
||||||
<font color="red"><strong><i>Make sure only you can see this QR Code and
|
<font color="red"><strong><i>Make sure only you can see this QR Code and secret key and
|
||||||
nobody can capture it.</i></strong></font>
|
nobody can capture them.</i></strong></font>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ mysqlclient==2.0.1
|
||||||
configobj==5.0.6
|
configobj==5.0.6
|
||||||
bcrypt>=3.1.7
|
bcrypt>=3.1.7
|
||||||
requests==2.24.0
|
requests==2.24.0
|
||||||
python-ldap==3.3.1
|
python-ldap==3.4.0
|
||||||
pyotp==2.4.0
|
pyotp==2.4.0
|
||||||
qrcode==6.1
|
qrcode==6.1
|
||||||
dnspython>=1.16.0
|
dnspython>=1.16.0
|
||||||
|
|
1990
swagger-specv2.yaml
1990
swagger-specv2.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue