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

This commit is contained in:
root
2021-12-03 14:12:11 +00:00
parent 6c1dfd2408
commit 940551e99e
15 changed files with 766 additions and 2142 deletions

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