diff --git a/yubiadmin/apps/auth.py b/yubiadmin/apps/auth.py index 0eb040c..05735cb 100644 --- a/yubiadmin/apps/auth.py +++ b/yubiadmin/apps/auth.py @@ -26,13 +26,12 @@ # POSSIBILITY OF SUCH DAMAGE. import os -import re from wtforms import Form from wtforms.fields import (SelectField, TextField, BooleanField, IntegerField, PasswordField) from wtforms.widgets import PasswordInput from wtforms.validators import NumberRange, URL, EqualTo, Regexp, Optional -from yubiadmin.util.app import App, render +from yubiadmin.util.app import App, CollectionApp, render from yubiadmin.util.config import (python_handler, python_list_handler, FileConfig) from yubiadmin.util.form import ConfigForm, FileForm, ListField @@ -291,31 +290,29 @@ class AssignYubiKeyForm(Form): self.auth.commit() -class YubiAuthUsers(App): - user_range = re.compile('(\d+)-(\d+)') +class YubiAuthUsers(CollectionApp): + base_url = '/auth/users' + item_name = 'Users' + caption = 'YubiAuth Users' + columns = ['Username', 'YubiKeys'] + template = 'auth/list' def __init__(self): self.auth = YubiAuth() - def __call__(self, request): - sub_cmd = request.path_info_pop() - if sub_cmd == 'create': - return self.create(request) - elif sub_cmd == 'delete': - return self.delete(request) - elif sub_cmd == 'delete_confirm': - return self.delete_confirm(request) - elif sub_cmd == 'user': - return self.show_user(request) - else: - match = self.user_range.match(sub_cmd) if sub_cmd else None - if match: - offset = int(match.group(1)) - 1 - limit = int(match.group(2)) - offset - else: - offset = 0 - limit = 10 - return self.list_users(offset, limit) + def size(self): + return self.auth.session.query(User).count() + + def get(self, offset, limit): + users = self.auth.session.query(User).order_by(User.name) \ + .offset(offset).limit(limit) + + return map(lambda user: { + 'id': user.id, + 'Username': '%s' % (user.id, + user.name), + 'YubiKeys': ', '.join(user.yubikeys.keys()) + }, users) def create(self, request): return self.render_forms(request, [CreateUserForm(self.auth)], @@ -333,28 +330,7 @@ class YubiAuthUsers(App): self.auth.commit() return self.redirect('/auth/users') - def list_users(self, offset, limit): - users = self.auth.session.query(User).order_by(User.name) \ - .offset(offset).limit(limit) - num_users = self.auth.session.query(User).count() - shown = (min(offset + 1, num_users), min(offset + limit, num_users)) - if offset > 0: - st = max(0, offset - limit) - ed = st + limit - prev = '/auth/users/%d-%d' % (st + 1, ed) - else: - prev = None - if num_users > shown[1]: - next = '/auth/users/%d-%d' % (offset + limit + 1, shown[1] + limit) - else: - next = None - - return render( - 'auth/list', script='auth', users=users, offset=offset, - limit=limit, num_users=num_users, shown='%d-%d' % shown, prev=prev, - next=next) - - def show_user(self, request): + def show(self, request): id = int(request.path_info_pop()) user = self.auth.get_user(id) if 'unassign' in request.params: diff --git a/yubiadmin/apps/val.py b/yubiadmin/apps/val.py index a6ed17d..a6b0195 100644 --- a/yubiadmin/apps/val.py +++ b/yubiadmin/apps/val.py @@ -29,11 +29,11 @@ import re import os from wtforms.fields import IntegerField from wtforms.validators import NumberRange, IPAddress, URL -from yubiadmin.util.app import App +from yubiadmin.util.app import App, CollectionApp from yubiadmin.util.config import (RegexHandler, FileConfig, php_inserter, parse_block, strip_comments, strip_quotes) from yubiadmin.util.form import ConfigForm, FileForm, DBConfigForm, ListField -from yubiadmin.util.system import invoke_rc_d +from yubiadmin.util.system import invoke_rc_d, run __all__ = [ 'app' @@ -210,15 +210,25 @@ class YubikeyVal(App): """ name = 'val' - sections = ['general', 'database', 'synchronization', 'ksms', 'advanced'] + sections = ['general', 'clients', 'database', 'synchronization', 'ksms', + 'advanced'] disabled = not os.path.isfile(YKVAL_CONFIG_FILE) + def __init__(self): + self._clients = YubikeyValClients() + def general(self, request): """ General """ return self.render_forms(request, [SyncLevelsForm(), MiscForm()]) + def clients(self, request): + """ + API Clients + """ + return self._clients(request) + def database(self, request): """ Database Settings @@ -260,7 +270,40 @@ class YubikeyVal(App): FileForm(YKVAL_CONFIG_FILE, 'Configuration') ]) - #Pulls the tab to the right: + # Pulls the tab to the right: advanced.advanced = True + +class YubikeyValClients(CollectionApp): + base_url = '/val/clients' + item_name = 'Clients' + caption = 'Client API Keys' + columns = ['Client ID', 'API Key'] + template = 'val/client_list' + + def __call__(self, request): + self._data = None + return super(YubikeyValClients, self).__call__(request) + + @property + def data(self): + if self._data is None: + self._data = [] + status, output = run('ykval-export-clients') + for line in output.splitlines(): + parts = line.split(',') + self._data.append({ + 'id': parts[0], + 'Client ID': parts[0], + 'API Key': parts[3] + }) + + return self._data + + def size(self): + return len(self.data) + + def get(self, offset, limit): + return self.data[offset:offset + limit] + app = YubikeyVal() diff --git a/yubiadmin/static/js/table.js b/yubiadmin/static/js/table.js new file mode 100644 index 0000000..ae2b7d3 --- /dev/null +++ b/yubiadmin/static/js/table.js @@ -0,0 +1,17 @@ +$(document).ready(function() { + $('#delete_btn').attr('disabled', 'disabled'); + + $('#toggle_all').change(function() { + $('tbody :checkbox').prop('checked', $(this).is(':checked')); + }); + + $(':checkbox').change(function() { + if($('tbody :checkbox:checked').length > 0) { + console.log('enable'); + $('#delete_btn').removeAttr('disabled'); + } else { + console.log('disable'); + $('#delete_btn').attr('disabled', 'disabled'); + } + }); +}); diff --git a/yubiadmin/templates/auth/list.html b/yubiadmin/templates/auth/list.html index 1daf1c9..ff3f1de 100644 --- a/yubiadmin/templates/auth/list.html +++ b/yubiadmin/templates/auth/list.html @@ -1,40 +1,8 @@ -
diff --git a/yubiadmin/util/app.py b/yubiadmin/util/app.py index 05f69fb..cce6702 100644 --- a/yubiadmin/util/app.py +++ b/yubiadmin/util/app.py @@ -26,12 +26,14 @@ # POSSIBILITY OF SUCH DAMAGE. import os +import re from jinja2 import Environment, FileSystemLoader from webob import exc from webob.dec import wsgify __all__ = [ 'App', + 'CollectionApp', 'render', 'populate_forms', ] @@ -113,3 +115,57 @@ class App(object): return render(template, target=request.path, fieldsets=forms, alert=alert, **kwargs) + + +ITEM_RANGE = re.compile('(\d+)-(\d+)') + + +class CollectionApp(App): + base_url = '' + caption = 'Items' + item_name = 'Items' + columns = [] + template = 'table' + script = 'table' + + def size(self): + return 0 + + def get(self, offset, limit): + return [{}] + + def __call__(self, request): + sub_cmd = request.path_info_pop() + if sub_cmd and hasattr(self, sub_cmd): + return getattr(self, sub_cmd)(request) + else: + match = ITEM_RANGE.match(sub_cmd) if sub_cmd else None + if match: + offset = int(match.group(1)) - 1 + limit = int(match.group(2)) - offset + else: + offset = 0 + limit = 10 + return self.list(offset, limit) + + def list(self, offset, limit): + items = self.get(offset, limit) + total = self.size() + shown = (min(offset + 1, total), min(offset + limit, total)) + if offset > 0: + st = max(0, offset - limit) + ed = st + limit + prev = '%s/%d-%d' % (self.base_url, st + 1, ed) + else: + prev = None + if total > shown[1]: + next = '%s/%d-%d' % (self.base_url, offset + limit + 1, shown[1] + + limit) + else: + next = None + + return render( + self.template, script=self.script, items=items, offset=offset, + limit=limit, total=total, shown='%d-%d' % shown, prev=prev, + next=next, base_url=self.base_url, caption=self.caption, + cols=self.columns, item_name=self.item_name)