feat: Associate an API Key with accounts (#1044)

This commit is contained in:
jbe-dw 2021-12-03 14:35:15 +01:00 committed by GitHub
parent 6c1dfd2408
commit f45ff2ce03
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 766 additions and 2142 deletions

View File

@ -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
```

View File

@ -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 ###

View File

@ -270,7 +270,12 @@ def apikey_can_access_domain(f):
zone_id = kwargs.get('zone_id').rstrip(".")
domain_names = [item.name for item in domains]
if zone_id not in domain_names:
accounts = 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)

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -3,10 +3,10 @@ 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,14 +16,18 @@ 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)
@ -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

View 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)

View File

@ -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") )
@ -123,7 +123,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 +172,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 +300,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 +317,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 +325,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 +353,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 +365,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 +403,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))
@ -744,7 +757,7 @@ class DetailedHistory():
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:
@ -758,16 +771,16 @@ class DetailedHistory():
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'],
""".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:
<th colspan="3" style="background:
"""
# Change table header background colour depending on auth success or failure
if detail_dict['success'] == 1:
self.detailed_msg+= """
@ -785,7 +798,7 @@ class DetailedHistory():
self.detailed_msg+= """
</tr>
</thead>
</thead>
<tbody>
<tr>
<td>Authenticator Type:</td>
@ -812,18 +825,23 @@ class DetailedHistory():
<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:
domains = detail_dict['domains' if 'domains' in detail_dict.keys() else 'domain_acl']
accounts = detail_dict['accounts'] if 'accounts' in detail_dict.keys() else 'None'
self.detailed_msg = """
<table class="table table-bordered table-striped">
<tr><td>Key: </td><td>{0}</td></tr>
<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>
<tr><td>Accounts bound to this API key:</td><td>{3}</td></tr>
<tr><td>Accessible domains with this API key:</td><td>{4}</td></tr>
</table>
""".format(detail_dict['key'], detail_dict['role'], detail_dict['description'], str(detail_dict['domain_acl']).replace("]","").replace("[", ""))
""".format(detail_dict['key'], detail_dict['role'], detail_dict['description'],
str(accounts).replace("]","").replace("[", ""),
str(domains).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: </td><td>{0}</td></tr>
<tr><td>Domain type:</td><td>{1}</td></tr>
<tr><td>Masters:</td><td>{2}</td></tr>
</table>
@ -831,7 +849,7 @@ class DetailedHistory():
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>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>
@ -840,7 +858,7 @@ class DetailedHistory():
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 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'])
@ -895,7 +913,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 +921,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 +949,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 +969,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 +1023,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 +1038,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,7 +1235,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'
]

View File

@ -21,7 +21,7 @@ from ..lib.errors import (
DomainNotExists, DomainAlreadyExists, DomainAccessForbidden,
RequestIsNotJSON, ApiKeyCreateFail, ApiKeyNotUsable, NotEnoughPrivileges,
AccountCreateFail, AccountUpdateFail, AccountDeleteFail,
AccountCreateDuplicate,
AccountCreateDuplicate, AccountNotExists,
UserCreateFail, UserCreateDuplicate, UserUpdateFail, UserDeleteFail,
UserUpdateFailEmail,
)
@ -307,6 +307,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 +318,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 +334,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 +361,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 +384,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 +498,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 +516,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 +529,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 +584,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 +613,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 +622,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))
@ -856,7 +922,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 +942,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)
@ -1055,8 +1121,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()

View File

@ -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,12 @@ definitions:
type: string
description: The name for this account (unique, immutable)
readOnly: false
domains:
type: array
description: The list of domains owned by this account
readOnly: true
items:
$ref: '#/definitions/PDNSAdminZones'
ConfigSetting:
title: ConfigSetting

View File

@ -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">&times;</span>
</button>
<h4 class="modal-title">WARNING</h4>
</div>
<div class="modal-body">
<p></p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-flat btn-primary center-block" id="button_key_confirm_warn">
OK</button>
</div>
</div>
<!-- /.modal-content -->
</div>
<!-- /.modal-dialog -->
</div>
{% endblock %}

View File

@ -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) }}'">

File diff suppressed because it is too large Load Diff