diff --git a/yubiadmin/admin.py b/yubiadmin/admin.py index 1c2427e..e400ae9 100644 --- a/yubiadmin/admin.py +++ b/yubiadmin/admin.py @@ -2,7 +2,7 @@ import os from wsgiref.simple_server import make_server from webob.dec import wsgify -from yubiadmin.util import render +from yubiadmin.util.app import render from yubiadmin.apps import apps diff --git a/yubiadmin/apps/ksm.py b/yubiadmin/apps/ksm.py index 655eb02..78bb144 100644 --- a/yubiadmin/apps/ksm.py +++ b/yubiadmin/apps/ksm.py @@ -1,4 +1,5 @@ -from yubiadmin.util import App, DBConfigForm +from yubiadmin.util.app import App +from yubiadmin.util.form import DBConfigForm __all__ = [ 'app' @@ -19,7 +20,8 @@ class YubikeyKsm(App): """ Database Settings """ - return self.render_forms(request, [ - DBConfigForm('/etc/yubico/ksm/config-db.php')]) + dbform = DBConfigForm('/etc/yubico/ksm/config-db.php', + dbname='ykksm', dbuser='ykksmreader') + return self.render_forms(request, [dbform]) app = YubikeyKsm() diff --git a/yubiadmin/apps/val.py b/yubiadmin/apps/val.py index 30a7735..5f138fd 100644 --- a/yubiadmin/apps/val.py +++ b/yubiadmin/apps/val.py @@ -1,15 +1,20 @@ import re -from wtforms.fields import IntegerField, StringField, Field -from wtforms.widgets import TextInput +import os +from wtforms.fields import IntegerField from wtforms.validators import NumberRange -from yubiadmin.util import App, DBConfigForm, ConfigForm, FileConfig +from yubiadmin.util.app import App +from yubiadmin.util.config import ValueHandler, FileConfig +from yubiadmin.util.form import ConfigForm, DBConfigForm, ListField __all__ = [ 'app' ] +COMMENT = re.compile(r'/\*.*?\*/') +VALUE = re.compile(r'\s*[\'"](.*)[\'"]\s*') -def yk_read(varname, prefix='', suffix='', flags=None): + +def yk_pattern(varname, prefix='', suffix='', flags=None): regex = r'(?m)^(?!#)\$baseParams\[\'__YKVAL_%s__\'\]\s*=' \ '\s*%s(.*?)%s\s*;\s*$' % (varname, prefix, suffix) if flags: @@ -22,141 +27,76 @@ def yk_write(varname, prefix='', suffix=''): (varname, prefix, x, suffix) -def yk_read_str(varname): - return yk_read(varname, '[\'"]', '[\'"]') +def yk_handler(varname, default): + return ValueHandler(yk_pattern(varname), yk_write(varname), + default=default) -def yk_write_str(varname): - return yk_write(varname, '"', '"') +def strip_quotes(value): + match = VALUE.match(value) + if match: + return match.group(1) + return value + + +def strip_comments(value): + return COMMENT.sub('', value) + + +def yk_parse_arraystring(value): + value = strip_comments(value).strip() + return [strip_quotes(x) for x in value.split(',')] + + +def yk_array_handler(varname): + pattern = yk_pattern(varname, 'array\(', '\)', 's') + str_write = yk_write(varname, 'array(' + os.linesep, os.linesep + ')') + writer = lambda xs: str_write((',' + os.linesep) + .join(['\t"%s"' % x for x in xs])) + reader = lambda match: yk_parse_arraystring(match.group(1)) + return ValueHandler(pattern, writer, reader, []) + + +ykval_config = FileConfig( + '/home/dain/yubico/yubiadmin/ykval-config.php', + [ + ('sync_default', yk_handler('SYNC_DEFAULT_LEVEL', 60)), + ('sync_secure', yk_handler('SYNC_SECURE_LEVEL', 40)), + ('sync_fast', yk_handler('SYNC_FAST_LEVEL', 1)), + ('default_timeout', yk_handler('SYNC_DEFAULT_TIMEOUT', 1)), + ('sync_interval', yk_handler('SYNC_INTERVAL', 10)), + ('resync_timeout', yk_handler('SYNC_RESYNC_TIMEOUT', 30)), + ('old_limit', yk_handler('SYNC_OLD_LIMIT', 10)), + ('sync_pool', yk_array_handler('SYNC_POOL')) + ] +) class SyncLevelsForm(ConfigForm): legend = 'Sync Levels' + config = ykval_config sync_default = IntegerField('Default', [NumberRange(1, 100)]) sync_secure = IntegerField('Secure', [NumberRange(1, 100)]) sync_fast = IntegerField('Fast', [NumberRange(1, 100)]) - config = FileConfig( - '/home/dain/yubico/yubiadmin/ykval-config.php', - [ - ( - 'sync_default', - yk_read('SYNC_DEFAULT_LEVEL'), - yk_write('SYNC_DEFAULT_LEVEL'), - 60 - ), ( - 'sync_secure', - yk_read('SYNC_SECURE_LEVEL'), - yk_write('SYNC_SECURE_LEVEL'), - 40 - ), ( - 'sync_fast', - yk_read('SYNC_FAST_LEVEL'), - yk_write('SYNC_FAST_LEVEL'), - 1 - ), - - ] - ) - class MiscForm(ConfigForm): legend = 'Misc' + config = ykval_config + default_timeout = IntegerField('Default Timeout', [NumberRange(0)]) - config = FileConfig( - '/home/dain/yubico/yubiadmin/ykval-config.php', - [( - 'default_timeout', - yk_read('SYNC_DEFAULT_TIMEOUT'), - yk_write('SYNC_DEFAULT_TIMEOUT'), - 1 - )] - ) - - -class ListField(Field): - COMMENT = re.compile(r'/\*.*?\*/') - VALUE = re.compile(r'\s*[\'"](.*)[\'"]\s*') - widget = TextInput() - - def process_formdata(self, values): - if values: - self.data = filter(None, [x.strip() for x in values[0].split(',')]) - - def process_data(self, value): - if value: - data = [] - value = self.COMMENT.sub('', value) - for val in value.split(','): - match = self.VALUE.match(val) - if match: - data.append(match.group(1)) - self.data = data - else: - self.data = [] - - def _value(self): - if self.data: - return ', '.join(self.data) - else: - return '' - - -def yk_array_write(varname): - str_write = yk_write(varname, 'array(', ')') - return lambda xs: str_write(', '.join(['"%s"' % x for x in xs])) - class SyncPoolForm(ConfigForm): legend = 'Sync Settings' + config = ykval_config + attrs = {'sync_pool': {'rows': 5, 'class': 'input-xlarge'}} + sync_interval = IntegerField('Sync Interval', [NumberRange(1)]) resync_timeout = IntegerField('Resync Timeout', [NumberRange(1)]) old_limit = IntegerField('Old Limit', [NumberRange(1)]) sync_pool = ListField('Servers') - sync_pool_add = StringField('Add Server') - - config = FileConfig( - '/home/dain/yubico/yubiadmin/ykval-config.php', - [ - ( - 'sync_interval', - yk_read('SYNC_INTERVAL'), - yk_write('SYNC_INTERVAL'), - 10 - ), ( - 'resync_timeout', - yk_read('SYNC_RESYNC_TIMEOUT'), - yk_write('SYNC_RESYNC_TIMEOUT'), - 30 - ), ( - 'old_limit', - yk_read('SYNC_OLD_LIMIT'), - yk_write('SYNC_OLD_LIMIT'), - 10 - ), ( - 'sync_pool', - yk_read('SYNC_POOL', 'array\(', '\)', 's'), - yk_array_write('SYNC_POOL'), - '' - ) - ] - ) - - def validate(self): - if super(SyncPoolForm, self).validate(): - if self.sync_pool_add.data: - self.sync_pool.data.append(self.sync_pool_add.data) - self.sync_pool_add.process_data(None) - return True - return False - -COMMENT = re.compile(r'/\*.*?\*/') - - -def remove_comments(content): - return COMMENT.sub('', content) class YubikeyVal(App): @@ -179,8 +119,9 @@ class YubikeyVal(App): """ Database Settings """ - return self.render_forms(request, [ - DBConfigForm('/home/dain/yubico/yubiadmin/config-db.php')]) + dbform = DBConfigForm('/home/dain/yubico/yubiadmin/config-db.php', + dbname='ykval', dbuser='ykval_verifier') + return self.render_forms(request, [dbform]) def syncpool(self, request): """ @@ -189,8 +130,7 @@ class YubikeyVal(App): sync_pool_form = SyncPoolForm() form_page = self.render_forms(request, [sync_pool_form]) - print 'Sync pool: %s' % \ - remove_comments(sync_pool_form.config['sync_pool']) + print 'Sync pool: %r' % sync_pool_form.config['sync_pool'] return form_page def ksms(self, request): diff --git a/yubiadmin/util.py b/yubiadmin/util.py deleted file mode 100644 index 17b8811..0000000 --- a/yubiadmin/util.py +++ /dev/null @@ -1,171 +0,0 @@ -import os -import re -from UserDict import DictMixin -from wtforms import Form, StringField, IntegerField, PasswordField -from wtforms.widgets import PasswordInput -from wtforms.validators import Optional, NumberRange -from jinja2 import Environment, FileSystemLoader - -__all__ = [ - 'App', - 'FileConfig', - 'ConfigForm', - 'DBConfigForm', - 'render', - 'populate_forms', -] - -cwd = os.path.dirname(__file__) -base_dir = os.path.abspath(os.path.join(cwd, os.pardir)) -template_dir = os.path.join(base_dir, 'templates') -env = Environment(loader=FileSystemLoader(template_dir)) - - -def render(tmpl, **kwargs): - template = env.get_template('%s.html' % tmpl) - return template.render(**kwargs) - - -def populate_forms(forms, data): - if not data: - for form in forms: - form.load() - else: - errors = False - for form in forms: - form.process(data) - errors = not form.validate() or errors - if not errors: - for form in forms: - form.save() - else: - print 'Errors!' - - -class App(object): - name = None - sections = [] - - def render_forms(self, request, forms): - populate_forms(forms, request.params) - return render('form', target=request.path, fieldsets=forms) - - -class ValueHandler(object): - def __init__(self, pattern, writer, default=None, group=1): - self.pattern = re.compile(pattern) - self.writer = writer - self.default = default - self.group = group - - def read(self, content): - match = self.pattern.search(content) - if match: - return match.group(self.group) - return self.default - - def write(self, content, value): - if value is None: - value = '' - if self.pattern.search(content): - content = self.pattern.sub(self.writer(value), content, 1) - else: - content += os.linesep + self.writer(value) - return content - - -class FileConfig(DictMixin): - """ - Maps key-value pairs to a backing config file. - You can manually edit the file by modifying self.content. - """ - def __init__(self, filename, params=[]): - self.filename = filename - self.params = {} - for param in params: - self.add_param(*param) - - def read(self): - try: - with open(self.filename, 'r') as file: - self.content = file.read() - except IOError as e: - print e - self.content = '' - - def commit(self): - with open(self.filename, 'w+') as file: - file.write(self.content) - - def add_param(self, key, pattern, writer, default=None, group=1): - self.params[key] = ValueHandler(pattern, writer, default, group) - - def __getitem__(self, key): - return self.params[key].read(self.content) - - def __setitem__(self, key, value): - self.content = self.params[key].write(self.content, value) - - def keys(self): - return self.params.keys() - - def __delitem__(self, key): - del self.params[key] - - -class ConfigForm(Form): - """ - Form that can load and save data to a config. - """ - config = None - - def load(self): - self.config.read() - for field in self: - if field.id in self.config: - field.process_data(self.config[field.id]) - - def save(self): - self.config.read() - for field in self: - if field.id in self.config: - self.config[field.id] = field.data - self.config.commit() - - -def db_read(varname): - return r'\$db%s=\'(.*)\';' % varname - - -def db_write(varname): - return lambda x: '$db%s=\'%s\';' % (varname, x) - - -class DBConfigForm(ConfigForm): - """ - Complete form for editing a dbconfig-common generated for PHP. - """ - legend = 'Database' - dbtype = StringField('DB type') - dbserver = StringField('Host') - dbport = IntegerField('Port', [Optional(), NumberRange(1, 65535)]) - dbname = StringField('DB name') - dbuser = StringField('DB username') - dbpass = PasswordField('DB password', - widget=PasswordInput(hide_value=False)) - - config = FileConfig( - '/dev/null', - [ - ('dbtype', db_read('type'), db_write('type'), 'mysql'), - ('dbserver', db_read('server'), db_write('server'), 'localhost'), - ('dbport', db_read('port'), db_write('port'), ''), - ('dbname', db_read('name'), db_write('name'), 'ykval'), - ('dbuser', db_read('user'), db_write('user'), 'ykval_verifier'), - ('dbpass', db_read('pass'), db_write('pass'), ''), - ] - ) - - def __init__(self, filename, *args, **kwargs): - self.__class__.config.filename = filename - super(DBConfigForm, self).__init__(*args, **kwargs) diff --git a/yubiadmin/util/__init__.py b/yubiadmin/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/yubiadmin/util/app.py b/yubiadmin/util/app.py new file mode 100644 index 0000000..6ee23f6 --- /dev/null +++ b/yubiadmin/util/app.py @@ -0,0 +1,43 @@ +import os +from jinja2 import Environment, FileSystemLoader + +__all__ = [ + 'App', + 'render', + 'populate_forms', +] + +cwd = os.path.dirname(__file__) +base_dir = os.path.abspath(os.path.join(cwd, os.pardir, os.pardir)) +template_dir = os.path.join(base_dir, 'templates') +env = Environment(loader=FileSystemLoader(template_dir)) + + +def render(tmpl, **kwargs): + template = env.get_template('%s.html' % tmpl) + return template.render(**kwargs) + + +def populate_forms(forms, data): + if not data: + for form in forms: + form.load() + else: + errors = False + for form in forms: + form.process(data) + errors = not form.validate() or errors + if not errors: + for form in forms: + form.save() + else: + print 'Errors!' + + +class App(object): + name = None + sections = [] + + def render_forms(self, request, forms, template='form'): + populate_forms(forms, request.params) + return render(template, target=request.path, fieldsets=forms) diff --git a/yubiadmin/util/config.py b/yubiadmin/util/config.py new file mode 100644 index 0000000..e80e8ab --- /dev/null +++ b/yubiadmin/util/config.py @@ -0,0 +1,71 @@ +import os +import re +from UserDict import DictMixin + +__all__ = [ + 'ValueHandler', + 'FileConfig' +] + + +class ValueHandler(object): + def __init__(self, pattern, writer, reader=lambda x: x.group(1), + default=None): + self.pattern = re.compile(pattern) + self.writer = writer + self.reader = reader + self.default = default + + def read(self, content): + match = self.pattern.search(content) + if match: + return self.reader(match) + return self.default + + def write(self, content, value): + if value is None: + value = '' + if self.pattern.search(content): + content = self.pattern.sub(self.writer(value), content, 1) + else: + content += os.linesep + self.writer(value) + return content + + +class FileConfig(DictMixin): + """ + Maps key-value pairs to a backing config file. + You can manually edit the file by modifying self.content. + """ + def __init__(self, filename, params=[]): + self.filename = filename + self.params = {} + for param in params: + self.add_param(*param) + + def read(self): + try: + with open(self.filename, 'r') as file: + self.content = file.read() + except IOError as e: + print e + self.content = '' + + def commit(self): + with open(self.filename, 'w+') as file: + file.write(self.content) + + def add_param(self, key, handler): + self.params[key] = handler + + def __getitem__(self, key): + return self.params[key].read(self.content) + + def __setitem__(self, key, value): + self.content = self.params[key].write(self.content, value) + + def keys(self): + return self.params.keys() + + def __delitem__(self, key): + del self.params[key] diff --git a/yubiadmin/util/form.py b/yubiadmin/util/form.py new file mode 100644 index 0000000..b2c2614 --- /dev/null +++ b/yubiadmin/util/form.py @@ -0,0 +1,85 @@ +from wtforms import Form, StringField, IntegerField, PasswordField, Field +from wtforms.widgets import PasswordInput, TextArea +from wtforms.validators import Optional, NumberRange +from yubiadmin.util.config import ValueHandler, FileConfig + +__all__ = [ + 'ListField', + 'ConfigForm', + 'DBConfigForm' +] + + +class ListField(Field): + widget = TextArea() + + def process_formdata(self, values): + if values: + self.data = filter(None, [x.strip() for x in values[0].split()]) + + def _value(self): + if self.data: + return '\n'.join(self.data) + else: + return '' + + +class ConfigForm(Form): + """ + Form that can load and save data to a config. + """ + config = None + + def load(self): + self.config.read() + for field in self: + if field.id in self.config: + field.process_data(self.config[field.id]) + + def save(self): + self.config.read() + for field in self: + if field.id in self.config: + self.config[field.id] = field.data + self.config.commit() + + +class DBConfigForm(ConfigForm): + """ + Complete form for editing a dbconfig-common generated for PHP. + """ + legend = 'Database' + dbtype = StringField('DB type') + dbserver = StringField('Host') + dbport = IntegerField('Port', [Optional(), NumberRange(1, 65535)]) + dbname = StringField('DB name') + dbuser = StringField('DB username') + dbpass = PasswordField('DB password', + widget=PasswordInput(hide_value=False)) + + def db_handler(self, varname, default): + pattern = r'\$%s=\'(.*)\';' % varname + writer = lambda x: '$%s=\'%s\';' % (varname, x) + return ValueHandler(pattern, writer, default=default) + + def __init__(self, filename, *args, **kwargs): + if not self.config: + self.config = FileConfig( + filename, + [ + ('dbtype', self.db_handler( + 'dbtype', kwargs.pop('dbtype', 'mysql'))), + ('dbserver', self.db_handler( + 'dbserver', kwargs.pop('dbserver', 'localhost'))), + ('dbport', self.db_handler( + 'dbport', kwargs.pop('dbport', ''))), + ('dbname', self.db_handler( + 'dbname', kwargs.pop('dbname', ''))), + ('dbuser', self.db_handler( + 'dbuser', kwargs.pop('dbuser', ''))), + ('dbpass', self.db_handler( + 'dbpass', kwargs.pop('dbpass', ''))), + ] + ) + + super(DBConfigForm, self).__init__(*args, **kwargs)