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