mirror of
https://github.com/cwinfo/powerdns-admin.git
synced 2024-12-04 19:15:30 +00:00
feat: Associate an API Key with accounts (#1044)
This commit is contained in:
parent
6c1dfd2408
commit
940551e99e
123
docs/API.md
123
docs/API.md
@ -1,105 +1,134 @@
|
||||
### API Usage
|
||||
|
||||
#### Getting started with docker
|
||||
|
||||
1. Run docker image docker-compose up, go to UI http://localhost:9191, at http://localhost:9191/swagger is swagger API specification
|
||||
2. Click to register user, type e.g. user: admin and password: admin
|
||||
3. Login to UI in settings enable allow domain creation for users, now you can create and manage domains with admin account and also ordinary users
|
||||
4. Encode your user and password to base64, in our example we have user admin and password admin so in linux cmd line we type:
|
||||
4. Click on the API Keys menu then click on teh "Add Key" button to add a new Administrator Key
|
||||
5. Keep the base64 encoded apikey somewhere safe as it won't be available in clear anymore
|
||||
|
||||
```
|
||||
|
||||
#### Accessing the API
|
||||
|
||||
The PDA API consists of two distinct parts:
|
||||
|
||||
- The /powerdnsadmin endpoints manages PDA content (accounts, users, apikeys) and also allow domain creation/deletion
|
||||
- The /server endpoints are proxying queries to the backend PowerDNS instance's API. PDA acts as a proxy managing several API Keys and permissions to the PowerDNS content.
|
||||
|
||||
The requests to the API needs two headers:
|
||||
|
||||
- The classic 'Content-Type: application/json' is required to all POST and PUT requests, though it's armless to use it on each call
|
||||
- The authentication header to provide either the login:password basic authentication or the Api Key authentication.
|
||||
|
||||
When you access the `/powerdnsadmin` endpoint, you must use the Basic Auth:
|
||||
|
||||
```bash
|
||||
# Encode your user and password to base64
|
||||
$ echo -n 'admin:admin'|base64
|
||||
YWRtaW46YWRtaW4=
|
||||
# Use the ouput as your basic auth header
|
||||
curl -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X <method> <url>
|
||||
```
|
||||
|
||||
we use generated output in basic authentication, we authenticate as user,
|
||||
with basic authentication, we can create/delete/get zone and create/delete/get/update apikeys
|
||||
|
||||
creating domain:
|
||||
When you access the `/server` endpoint, you must use the ApiKey
|
||||
|
||||
```bash
|
||||
# Use the already base64 encoded key in your header
|
||||
curl -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' -X <method> <url>
|
||||
```
|
||||
|
||||
Finally, the `/sync_domains` endpoint accepts both basic and apikey authentication
|
||||
|
||||
#### Examples
|
||||
|
||||
Creating domain via `/powerdnsadmin`:
|
||||
|
||||
```bash
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/zones --data '{"name": "yourdomain.com.", "kind": "NATIVE", "nameservers": ["ns1.mydomain.com."]}'
|
||||
```
|
||||
|
||||
creating apikey which has Administrator role, apikey can have also User role, when creating such apikey you have to specify also domain for which apikey is valid:
|
||||
Creating an apikey which has the Administrator role:
|
||||
|
||||
```
|
||||
```bash
|
||||
# Create the key
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'Authorization: Basic YWRtaW46YWRtaW4=' -X POST http://localhost:9191/api/v1/pdnsadmin/apikeys --data '{"description": "masterkey","domains":[], "role": "Administrator"}'
|
||||
```
|
||||
Example response (don't forget to save the plain key from the output)
|
||||
|
||||
call above will return response like this:
|
||||
|
||||
```
|
||||
[{"description": "samekey", "domains": [], "role": {"name": "Administrator", "id": 1}, "id": 2, "plain_key": "aGCthP3KLAeyjZI"}]
|
||||
```json
|
||||
[
|
||||
{
|
||||
"accounts": [],
|
||||
"description": "masterkey",
|
||||
"domains": [],
|
||||
"role": {
|
||||
"name": "Administrator",
|
||||
"id": 1
|
||||
},
|
||||
"id": 2,
|
||||
"plain_key": "aGCthP3KLAeyjZI"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
we take plain_key and base64 encode it, this is the only time we can get API key in plain text and save it somewhere:
|
||||
We can use the apikey for all calls to PowerDNS (don't forget to specify Content-Type):
|
||||
|
||||
```
|
||||
$ echo -n 'aGCthP3KLAeyjZI'|base64
|
||||
YUdDdGhQM0tMQWV5alpJ
|
||||
```
|
||||
Getting powerdns configuration (Administrator Key is needed):
|
||||
|
||||
We can use apikey for all calls specified in our API specification (it tries to follow powerdns API 1:1, only tsigkeys endpoints are not yet implemented), don't forget to specify Content-Type!
|
||||
|
||||
getting powerdns configuration:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/config
|
||||
```
|
||||
|
||||
creating and updating records:
|
||||
Creating and updating records:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -X PATCH -H 'Content-Type: application/json' --data '{"rrsets": [{"name": "test1.yourdomain.com.","type": "A","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "192.0.2.5", "disabled": false} ]},{"name": "test2.yourdomain.com.","type": "AAAA","ttl": 86400,"changetype": "REPLACE","records": [ {"content": "2001:db8::6", "disabled": false} ]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://127.0.0.1:9191/api/v1/servers/localhost/zones/yourdomain.com.
|
||||
```
|
||||
|
||||
getting domain:
|
||||
Getting a domain:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -L -vvv -H 'Content-Type: application/json' -H 'X-API-KEY: YUdDdGhQM0tMQWV5alpJ' -X GET http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||
```
|
||||
|
||||
list zone records:
|
||||
List a zone's records:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -H 'Content-Type: application/json' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com
|
||||
```
|
||||
|
||||
add new record:
|
||||
Add a new record:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.5.4", "disabled": false } ] } ] }' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||
```
|
||||
|
||||
update record:
|
||||
Update a record:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "REPLACE", "records": [ {"content": "192.0.2.5", "disabled": false, "name": "test.yourdomain.com.", "ttl": 86400, "type": "A"}]}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq .
|
||||
```
|
||||
|
||||
delete record:
|
||||
Delete a record:
|
||||
|
||||
```
|
||||
```bash
|
||||
curl -H 'Content-Type: application/json' -X PATCH --data '{"rrsets": [ {"name": "test.yourdomain.com.", "type": "A", "ttl": 86400, "changetype": "DELETE"}]}' -H 'X-API-Key: YUdDdGhQM0tMQWV5alpJ' http://localhost:9191/api/v1/servers/localhost/zones/yourdomain.com | jq
|
||||
```
|
||||
|
||||
### Generate ER diagram
|
||||
|
||||
```
|
||||
With docker
|
||||
|
||||
```bash
|
||||
# Install build packages
|
||||
apt-get install python-dev graphviz libgraphviz-dev pkg-config
|
||||
```
|
||||
|
||||
```
|
||||
# Get the required python libraries
|
||||
pip install graphviz mysqlclient ERAlchemy
|
||||
```
|
||||
|
||||
```
|
||||
# Start the docker container
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
```
|
||||
# Set environment variables
|
||||
source .env
|
||||
```
|
||||
|
||||
```
|
||||
# Generate the diagrams
|
||||
eralchemy -i 'mysql://${PDA_DB_USER}:${PDA_DB_PASSWORD}@'$(docker inspect powerdns-admin-mysql|jq -jr '.[0].NetworkSettings.Networks.powerdnsadmin_default.IPAddress')':3306/powerdns_admin' -o /tmp/output.pdf
|
||||
```
|
||||
|
@ -0,0 +1,41 @@
|
||||
"""add apikey account mapping table
|
||||
|
||||
Revision ID: 0967658d9c0d
|
||||
Revises: 0d3d93f1c2e0
|
||||
Create Date: 2021-11-13 22:28:46.133474
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '0967658d9c0d'
|
||||
down_revision = '0d3d93f1c2e0'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.create_table('apikey_account',
|
||||
sa.Column('id', sa.Integer(), nullable=False),
|
||||
sa.Column('apikey_id', sa.Integer(), nullable=False),
|
||||
sa.Column('account_id', sa.Integer(), nullable=False),
|
||||
sa.ForeignKeyConstraint(['account_id'], ['account.id'], ),
|
||||
sa.ForeignKeyConstraint(['apikey_id'], ['apikey.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||
batch_op.create_index(batch_op.f('ix_history_created_on'), ['created_on'], unique=False)
|
||||
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade():
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
with op.batch_alter_table('history', schema=None) as batch_op:
|
||||
batch_op.drop_index(batch_op.f('ix_history_created_on'))
|
||||
|
||||
op.drop_table('apikey_account')
|
||||
# ### end Alembic commands ###
|
@ -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)
|
||||
|
||||
|
@ -60,7 +60,8 @@ class ApiKeyNotUsable(StructuredException):
|
||||
def __init__(
|
||||
self,
|
||||
name=None,
|
||||
message="Api key must have domains or have administrative role"):
|
||||
message=("Api key must have domains or accounts"
|
||||
" or an administrative role")):
|
||||
StructuredException.__init__(self)
|
||||
self.message = message
|
||||
self.name = name
|
||||
@ -120,6 +121,15 @@ class AccountDeleteFail(StructuredException):
|
||||
self.name = name
|
||||
|
||||
|
||||
class AccountNotExists(StructuredException):
|
||||
status_code = 404
|
||||
|
||||
def __init__(self, name=None, message="Account does not exist"):
|
||||
StructuredException.__init__(self)
|
||||
self.message = message
|
||||
self.name = name
|
||||
|
||||
|
||||
class UserCreateFail(StructuredException):
|
||||
status_code = 500
|
||||
|
||||
|
@ -11,10 +11,21 @@ class RoleSchema(Schema):
|
||||
name = fields.String()
|
||||
|
||||
|
||||
class AccountSummarySchema(Schema):
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||
|
||||
class ApiKeySummarySchema(Schema):
|
||||
id = fields.Integer()
|
||||
description = fields.String()
|
||||
|
||||
|
||||
class ApiKeySchema(Schema):
|
||||
id = fields.Integer()
|
||||
role = fields.Embed(schema=RoleSchema)
|
||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||
description = fields.String()
|
||||
key = fields.String()
|
||||
|
||||
@ -23,15 +34,11 @@ class ApiPlainKeySchema(Schema):
|
||||
id = fields.Integer()
|
||||
role = fields.Embed(schema=RoleSchema)
|
||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||
accounts = fields.Embed(schema=AccountSummarySchema, many=True)
|
||||
description = fields.String()
|
||||
plain_key = fields.String()
|
||||
|
||||
|
||||
class AccountSummarySchema(Schema):
|
||||
id = fields.Integer()
|
||||
name = fields.String()
|
||||
|
||||
|
||||
class UserSchema(Schema):
|
||||
id = fields.Integer()
|
||||
username = fields.String()
|
||||
@ -56,3 +63,4 @@ class AccountSchema(Schema):
|
||||
contact = fields.String()
|
||||
mail = fields.String()
|
||||
domains = fields.Embed(schema=DomainSchema, many=True)
|
||||
apikeys = fields.Embed(schema=ApiKeySummarySchema, many=True)
|
||||
|
@ -8,6 +8,7 @@ from .account_user import AccountUser
|
||||
from .server import Server
|
||||
from .history import History
|
||||
from .api_key import ApiKey
|
||||
from .api_key_account import ApiKeyAccount
|
||||
from .setting import Setting
|
||||
from .domain import Domain
|
||||
from .domain_setting import DomainSetting
|
||||
|
@ -17,6 +17,9 @@ class Account(db.Model):
|
||||
contact = db.Column(db.String(128))
|
||||
mail = db.Column(db.String(128))
|
||||
domains = db.relationship("Domain", back_populates="account")
|
||||
apikeys = db.relationship("ApiKey",
|
||||
secondary="apikey_account",
|
||||
back_populates="accounts")
|
||||
|
||||
def __init__(self, name=None, description=None, contact=None, mail=None):
|
||||
self.name = name
|
||||
|
@ -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
|
||||
|
20
powerdnsadmin/models/api_key_account.py
Normal file
20
powerdnsadmin/models/api_key_account.py
Normal file
@ -0,0 +1,20 @@
|
||||
from .base import db
|
||||
|
||||
|
||||
class ApiKeyAccount(db.Model):
|
||||
__tablename__ = 'apikey_account'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
apikey_id = db.Column(db.Integer,
|
||||
db.ForeignKey('apikey.id'),
|
||||
nullable=False)
|
||||
account_id = db.Column(db.Integer,
|
||||
db.ForeignKey('account.id'),
|
||||
nullable=False)
|
||||
db.UniqueConstraint('apikey_id', 'account_id', name='uniq_apikey_account')
|
||||
|
||||
def __init__(self, apikey_id, account_id):
|
||||
self.apikey_id = apikey_id
|
||||
self.account_id = account_id
|
||||
|
||||
def __repr__(self):
|
||||
return '<ApiKey_Account {0} {1}>'.format(self.apikey_id, self.account_id)
|
@ -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'
|
||||
|
||||
]
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -1,5 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
{% set active_page = "admin_keys" %}
|
||||
{% if create or (key is not none and key.role.name != "User") %}{% set hide_opts = True %}{%else %}{% set hide_opts = False %}{% endif %}
|
||||
{% block title %}
|
||||
<title>Edit Key - {{ SITE_NAME }}</title>
|
||||
{% endblock %}
|
||||
@ -49,10 +50,26 @@
|
||||
class="glyphicon glyphicon-pencil form-control-feedback"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-header with-border">
|
||||
<h3 class="box-title">Access Control</h3>
|
||||
<div class="box-header with-border key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||
<h3 class="box-title">Accounts Access Control</h3>
|
||||
</div>
|
||||
<div class="box-body">
|
||||
<div class="box-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||
<p>This key will be linked to the accounts on the right,</p>
|
||||
<p>thus granting access to domains owned by the selected accounts.</p>
|
||||
<p>Click on accounts to move between the columns.</p>
|
||||
<div class="form-group col-xs-2">
|
||||
<select multiple="multiple" class="form-control" id="key_multi_account"
|
||||
name="key_multi_account">
|
||||
{% for account in accounts %}
|
||||
<option {% if key and account in key.accounts %}selected{% endif %} value="{{ account.name }}">{{ account.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="box-header with-border key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||
<h3 class="box-title">Domain Access Control</h3>
|
||||
</div>
|
||||
<div class="box-body key-opts"{% if hide_opts %} style="display: none;"{% endif %}>
|
||||
<p>This key will have acess to the domains on the right.</p>
|
||||
<p>Click on domains to move between the columns.</p>
|
||||
<div class="form-group col-xs-2">
|
||||
@ -66,7 +83,7 @@
|
||||
</div>
|
||||
<div class="box-footer">
|
||||
<button type="submit"
|
||||
class="btn btn-flat btn-primary">{% if create %}Create{% else %}Update{% endif %}
|
||||
class="btn btn-flat btn-primary" id="key_submit">{% if create %}Create{% else %}Update{% endif %}
|
||||
Key</button>
|
||||
</div>
|
||||
</form>
|
||||
@ -82,7 +99,7 @@
|
||||
<p>Fill in all the fields in the form to the left.</p>
|
||||
<p><strong>Role</strong> The role of the key.</p>
|
||||
<p><strong>Description</strong> The key description.</p>
|
||||
<p><strong>Access Control</strong> The domains which the key has access to.</p>
|
||||
<p><strong>Access Control</strong> The domains or accounts which the key has access to.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -91,6 +108,48 @@
|
||||
{% endblock %}
|
||||
{% block extrascripts %}
|
||||
<script>
|
||||
$('form').submit(function (e) {
|
||||
var selectedRole = $("#key_role").val();
|
||||
var selectedDomains = $("#key_multi_domain option:selected").length;
|
||||
var selectedAccounts = $("#key_multi_account option:selected").length;
|
||||
var warn_modal = $("#modal_warning");
|
||||
|
||||
if (selectedRole != "User" && selectedDomains > 0 && selectedAccounts > 0){
|
||||
var warning = "Administrator and Operators have access to all domains. Your domain an account selection won't be saved.";
|
||||
e.preventDefault(e);
|
||||
warn_modal.modal('show');
|
||||
}
|
||||
|
||||
if (selectedRole == "User" && selectedDomains == 0 && selectedAccounts == 0){
|
||||
var warning = "User role must have at least one account or one domain bound. None selected.";
|
||||
e.preventDefault(e);
|
||||
warn_modal.modal('show');
|
||||
}
|
||||
|
||||
warn_modal.find('.modal-body p').text(warning);
|
||||
warn_modal.find('#button_key_confirm_warn').click(clearModal);
|
||||
});
|
||||
function clearModal(){
|
||||
$("#modal_warning").modal('hide');
|
||||
}
|
||||
$('#key_role').on('change', function (e) {
|
||||
var optionSelected = $("option:selected", this);
|
||||
if (this.value != "User") {
|
||||
// Clear the visible list
|
||||
$('#ms-key_multi_domain .ms-selection li').each(function(){ $(this).css('display', 'none');})
|
||||
$('#ms-key_multi_domain .ms-selectable li').each(function(){ $(this).css('display', '');})
|
||||
$('#ms-key_multi_account .ms-selection li').each(function(){ $(this).css('display', 'none');})
|
||||
$('#ms-key_multi_account .ms-selectable li').each(function(){ $(this).css('display', '');})
|
||||
// Deselect invisible selectbox
|
||||
$('#key_multi_domain option:selected').each(function(){ $(this).prop('selected', false);})
|
||||
$('#key_multi_account option:selected').each(function(){ $(this).prop('selected', false);})
|
||||
// Hide the lists
|
||||
$(".key-opts").hide();
|
||||
}
|
||||
else {
|
||||
$(".key-opts").show();
|
||||
}
|
||||
});
|
||||
$("#key_multi_domain").multiSelect({
|
||||
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
||||
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Domain Name'>",
|
||||
@ -126,6 +185,41 @@
|
||||
this.qs2.cache();
|
||||
}
|
||||
});
|
||||
$("#key_multi_account").multiSelect({
|
||||
selectableHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Account Name'>",
|
||||
selectionHeader: "<input type='text' class='search-input' autocomplete='off' placeholder='Account Name'>",
|
||||
afterInit: function (ms) {
|
||||
var that = this,
|
||||
$selectableSearch = that.$selectableUl.prev(),
|
||||
$selectionSearch = that.$selectionUl.prev(),
|
||||
selectableSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selectable:not(.ms-selected)',
|
||||
selectionSearchString = '#' + that.$container.attr('id') + ' .ms-elem-selection.ms-selected';
|
||||
|
||||
that.qs1 = $selectableSearch.quicksearch(selectableSearchString)
|
||||
.on('keydown', function (e) {
|
||||
if (e.which === 40) {
|
||||
that.$selectableUl.focus();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
that.qs2 = $selectionSearch.quicksearch(selectionSearchString)
|
||||
.on('keydown', function (e) {
|
||||
if (e.which == 40) {
|
||||
that.$selectionUl.focus();
|
||||
return false;
|
||||
}
|
||||
});
|
||||
},
|
||||
afterSelect: function () {
|
||||
this.qs1.cache();
|
||||
this.qs2.cache();
|
||||
},
|
||||
afterDeselect: function () {
|
||||
this.qs1.cache();
|
||||
this.qs2.cache();
|
||||
}
|
||||
});
|
||||
{% if plain_key %}
|
||||
$(document.body).ready(function () {
|
||||
var modal = $("#modal_show_key");
|
||||
@ -165,4 +259,25 @@
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
<div class="modal fade" id="modal_warning">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content modal-sm">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" id="button_close_warn_modal">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<h4 class="modal-title">WARNING</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p></p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-flat btn-primary center-block" id="button_key_confirm_warn">
|
||||
OK</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- /.modal-content -->
|
||||
</div>
|
||||
<!-- /.modal-dialog -->
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
@ -35,6 +35,7 @@
|
||||
<th>Role</th>
|
||||
<th>Description</th>
|
||||
<th>Domains</th>
|
||||
<th>Accounts</th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@ -45,6 +46,7 @@
|
||||
<td>{{ key.role.name }}</td>
|
||||
<td>{{ key.description }}</td>
|
||||
<td>{% for domain in key.domains %}{{ domain.name }}{% if not loop.last %}, {% endif %}{% endfor %}</td>
|
||||
<td>{% for account in key.accounts %}{{ account.name }}{% if not loop.last %}, {% endif %}{% endfor %}</td>
|
||||
<td width="15%">
|
||||
<button type="button" class="btn btn-flat btn-success button_edit"
|
||||
onclick="window.location.href='{{ url_for('admin.edit_key', key_id=key.id) }}'">
|
||||
|
1990
swagger-specv2.yaml
1990
swagger-specv2.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user