mirror of
https://github.com/Yubico/yubiadmin.git
synced 2025-03-14 17:29:20 +01:00
Compare commits
82 Commits
yubiadmin-
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
50449e4588 | ||
|
130458b9d3 | ||
|
5c02e60afc | ||
|
70de2f37ff | ||
|
fbf1e0d000 | ||
|
93e7d25786 | ||
|
bd148d1a9e | ||
|
b559bcdc75 | ||
|
c811f30fb4 | ||
|
4ac015e0e6 | ||
|
8e0169263f | ||
|
f5bba0081f | ||
|
38afc25abd | ||
|
e25537245d | ||
|
59cddadd76 | ||
|
cee1426239 | ||
|
c8d0d2f3ed | ||
|
d9af09e5ae | ||
|
051189146b | ||
|
0838e3685f | ||
|
3588070168 | ||
|
b9fe736037 | ||
|
49db8acc98 | ||
|
b0370680fd | ||
|
705077b782 | ||
|
d106f5c0ee | ||
|
408598c94d | ||
|
d2d857a938 | ||
|
c3e7298241 | ||
|
6dabc61dce | ||
|
802bc8e43b | ||
|
e7f61a98ab | ||
|
bf8d639e10 | ||
|
7467700262 | ||
|
fb6e556c0c | ||
|
d79a9ffb43 | ||
|
54ac3a1bb8 | ||
|
1d3fb37561 | ||
|
d53602039e | ||
|
1e15bcba4d | ||
|
d3a499a554 | ||
|
8f9fd30a7c | ||
|
aa8712f46f | ||
|
fba0632a30 | ||
|
5887047f8e | ||
|
3de1a599cc | ||
|
2cf8a1a140 | ||
|
8b336df4a3 | ||
|
9b0e3f39db | ||
|
e441642d22 | ||
|
257939d725 | ||
|
a948d10792 | ||
|
3968b67aba | ||
|
5e79f93ea2 | ||
|
68f8c9c987 | ||
|
a7cc2438a5 | ||
|
5e0dcebf8c | ||
|
73f955c0a6 | ||
|
f2772b08cd | ||
|
31909f893c | ||
|
557616d891 | ||
|
e80ce1f952 | ||
|
e2570763ee | ||
|
15f0883588 | ||
|
8dcd391628 | ||
|
33846f45b3 | ||
|
49b235e6f0 | ||
|
1b31251430 | ||
|
281dd92e9c | ||
|
895de11057 | ||
|
79a0e719fa | ||
|
ebb0ba6542 | ||
|
229b40ca1d | ||
|
2341f8c086 | ||
|
e8b1d5f75a | ||
|
8b32cf89e9 | ||
|
2b4373cd43 | ||
|
34b5cc10f4 | ||
|
fcfc2d5626 | ||
|
08d0ab975d | ||
|
da42713c83 | ||
|
4f36f6624a |
@ -2,5 +2,7 @@ include release.py
|
||||
include COPYING
|
||||
include NEWS
|
||||
include ChangeLog
|
||||
include bin/*.1
|
||||
include conf/*
|
||||
recursive-include yubiadmin/static *
|
||||
recursive-include yubiadmin/templates *
|
||||
|
67
NEWS
67
NEWS
@ -1,3 +1,70 @@
|
||||
* Version 0.1.7 (released 2014-04-16)
|
||||
|
||||
* Fixed YubiAuth user deletion bug.
|
||||
|
||||
* Fixed bug with URL fields not working with "localhost".
|
||||
|
||||
* Now handles usernames/passwords/secrets with spaces in RADIUS test without
|
||||
manual quoting.
|
||||
|
||||
* Version 0.1.6 (released 2013-11-22)
|
||||
|
||||
* Added LDAP settings to YubiAuth.
|
||||
|
||||
* Fixed bug with YubiAuth user management tab not showing up.
|
||||
|
||||
* Added dashboard.
|
||||
|
||||
* Version 0.1.5 (released 2013-06-07)
|
||||
|
||||
* Fixed YubiAuth user management which sometimes failed due to database
|
||||
connection issues.
|
||||
|
||||
* Version 0.1.4 (released 2013-06-05)
|
||||
|
||||
* Bugfix release: Fixed YubiAuth user creation broken in last release.
|
||||
|
||||
* Version 0.1.3 (released 2013-06-05)
|
||||
|
||||
* Added YubiX system app.
|
||||
|
||||
* Updated YubiAuth app for YubiAuth 0.3.3.
|
||||
|
||||
* Version 0.1.2 (released 2013-05-28)
|
||||
|
||||
* Fixed "Reset form" button for Ace editors, which was broken by the last
|
||||
release.
|
||||
|
||||
* Version 0.1.1 (released 2013-05-28)
|
||||
|
||||
* Added yubiadmin-config CLI tool.
|
||||
|
||||
* Added API clients to val.
|
||||
|
||||
* Removed reverse lookup of clients which could cause long delays in
|
||||
answering requests.
|
||||
|
||||
* Re-read app data like sections and disabled state for each request
|
||||
so that changes can be detected.
|
||||
|
||||
* Added FreeRadius app.
|
||||
|
||||
* Use Ace editor for editing configuration files with syntax highlighting.
|
||||
|
||||
* Version 0.1.0 (released 2013-05-14)
|
||||
|
||||
* Added basic user management to YubiAuth app.
|
||||
|
||||
* Version 0.0.9 (released 2013-05-08)
|
||||
|
||||
* Added YubiAuth app.
|
||||
|
||||
* Version 0.0.8 (released 2013-05-03)
|
||||
|
||||
* Added configuration of YubiAdmin itself.
|
||||
|
||||
* Added command line arguments to the yubiadmin server.
|
||||
|
||||
* Version 0.0.7 (released 2013-05-02)
|
||||
|
||||
* Initial release!
|
||||
|
2
README
2
README
@ -1,6 +1,8 @@
|
||||
YubiAdmin
|
||||
=========
|
||||
|
||||
NOTE: This project is deprecated and is no longer being maintained.
|
||||
|
||||
YubiAdmin provides a web based administration interface to several of Yubicos
|
||||
services, allowing a user with proper credentials to change configuration in
|
||||
an easy to use way, instead of having to manually edit configuration files.
|
||||
|
100
bin/yubiadmin
Executable file
100
bin/yubiadmin
Executable file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import base64
|
||||
import argparse
|
||||
from wsgiref.simple_server import make_server, WSGIRequestHandler
|
||||
from webob.dec import wsgify
|
||||
from webob import exc
|
||||
from yubiadmin import server
|
||||
from yubiadmin.static import DirectoryApp
|
||||
from yubiadmin.config import settings
|
||||
|
||||
REALM = 'YubiADMIN'
|
||||
STATIC_ASSETS = ['js', 'css', 'img', 'favicon.ico']
|
||||
|
||||
|
||||
class CustomRequestHandler(WSGIRequestHandler):
|
||||
def address_string(self):
|
||||
return str(self.client_address[0])
|
||||
|
||||
|
||||
class StaticPasswordValidator(object):
|
||||
def __init__(self, username, password):
|
||||
self.auth = base64.b64encode('%s:%s' % (username, password))
|
||||
|
||||
def __call__(self, auth):
|
||||
return self.auth == auth
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser(
|
||||
description="",
|
||||
add_help=True,
|
||||
# formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
parser.add_argument('-i', '--interface', nargs='?',
|
||||
default=settings['iface'], help='Listening interface')
|
||||
parser.add_argument('-p', '--port', nargs='?', default=settings['port'],
|
||||
help='Listening port')
|
||||
parser.add_argument('-U', '--username', nargs='?',
|
||||
default=settings['user'],
|
||||
help='Username for authentication')
|
||||
parser.add_argument('-P', '--password', nargs='?',
|
||||
default=settings['pass'],
|
||||
help='Password for authentication')
|
||||
args = parser.parse_args()
|
||||
args.port = int(args.port)
|
||||
|
||||
module_dir = os.path.dirname(server.__file__)
|
||||
base_dir = os.path.abspath(module_dir)
|
||||
static_dir = os.path.join(base_dir, 'static')
|
||||
|
||||
static_app = DirectoryApp(static_dir)
|
||||
|
||||
validator = StaticPasswordValidator(args.username, args.password)
|
||||
|
||||
@wsgify
|
||||
def with_static(request):
|
||||
if request.authorization:
|
||||
_, auth = request.authorization
|
||||
if validator(auth):
|
||||
base = request.path_info_peek()
|
||||
if base in STATIC_ASSETS:
|
||||
return request.get_response(static_app)
|
||||
return request.get_response(server.application)
|
||||
|
||||
# Deny access
|
||||
response = exc.HTTPUnauthorized()
|
||||
response.www_authenticate = ('Basic', {'realm': REALM})
|
||||
return response
|
||||
|
||||
httpd = make_server(args.interface, args.port, with_static,
|
||||
handler_class=CustomRequestHandler)
|
||||
httpd.serve_forever()
|
100
bin/yubiadmin-config
Executable file
100
bin/yubiadmin-config
Executable file
@ -0,0 +1,100 @@
|
||||
#!/usr/bin/python
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
from yubiadmin.apps.admin import admin_config
|
||||
|
||||
|
||||
def get_interactive(config):
|
||||
print 'Specify which interface to listen to.'
|
||||
print '127.0.0.1 will only be accessible from the local machine.'
|
||||
print '0.0.0.0 will be accessible from any interface.'
|
||||
print 'Press enter to keep the current settings.'
|
||||
interface = raw_input('Interface [%s]: ' % config['interface'])
|
||||
if interface:
|
||||
config['interface'] = interface
|
||||
|
||||
port = raw_input('Port [%s]: ' % config['port'])
|
||||
if port:
|
||||
config['port'] = int(port)
|
||||
|
||||
print 'Set the credentials required for accessing YubiAdmin via the web ' \
|
||||
'interface.'
|
||||
print 'Press enter to keep the current settings.'
|
||||
username = raw_input('Username [%s]: ' % config['username'])
|
||||
if username:
|
||||
config['username'] = username
|
||||
|
||||
password = raw_input('Password [%s]: ' % ('*' * len(config['password'])))
|
||||
if password:
|
||||
password2 = raw_input('Repeat password: ')
|
||||
if password == password2:
|
||||
config['password'] = password
|
||||
else:
|
||||
print 'ERROR: Passwords did not match!'
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def get_args(config):
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Configure YubiAdmin\n'
|
||||
'Interactively configures YubiAdmin when run with no arguments,\n'
|
||||
'or sets the arguments given via the arguments passed to the program',
|
||||
add_help=True,
|
||||
# formatter_class=argparse.ArgumentDefaultsHelpFormatter
|
||||
)
|
||||
parser.add_argument('-i', '--interface', nargs='?',
|
||||
default=config['interface'],
|
||||
help='Listening interface')
|
||||
parser.add_argument('-p', '--port', type=int, nargs='?',
|
||||
default=config['port'], help='Listening port')
|
||||
parser.add_argument('-U', '--username', nargs='?',
|
||||
default=config['username'],
|
||||
help='Username for authentication')
|
||||
parser.add_argument('-P', '--password', nargs='?',
|
||||
default=config['password'],
|
||||
help='Password for authentication')
|
||||
|
||||
args = parser.parse_args()
|
||||
config.update(vars(args))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
admin_config.read()
|
||||
|
||||
if len(sys.argv) > 1:
|
||||
get_args(admin_config)
|
||||
else:
|
||||
get_interactive(admin_config)
|
||||
|
||||
admin_config.commit()
|
||||
|
||||
print '\n%s written!' % admin_config.filename
|
||||
print 'YubiAdmin needs to be restarted for these settings to take effect.'
|
||||
print '\n service yubiadmin restart'
|
66
bin/yubiadmin-config.1
Normal file
66
bin/yubiadmin-config.1
Normal file
@ -0,0 +1,66 @@
|
||||
.\" Copyright (c) 2013 Yubico AB
|
||||
.\" All rights reserved.
|
||||
.\"
|
||||
.\" Redistribution and use in source and binary forms, with or without
|
||||
.\" modification, are permitted provided that the following conditions are
|
||||
.\" met:
|
||||
.\"
|
||||
.\" * Redistributions of source code must retain the above copyright
|
||||
.\" notice, this list of conditions and the following disclaimer.
|
||||
.\"
|
||||
.\" * Redistributions in binary form must reproduce the above
|
||||
.\" copyright notice, this list of conditions and the following
|
||||
.\" disclaimer in the documentation and/or other materials provided
|
||||
.\" with the distribution.
|
||||
.\"
|
||||
.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
.\"
|
||||
.\" The following commands are required for all man pages.
|
||||
.de URL
|
||||
\\$2 \(laURL: \\$1 \(ra\\$3
|
||||
..
|
||||
.if \n[.g] .mso www.tmac
|
||||
.TH yubiadmin-config "1" "May 2013" "yubiadmin"
|
||||
.SH NAME
|
||||
yubiadmin-config - Command-line tool for configuring YubiAdmin.
|
||||
.SH SYNOPSIS
|
||||
.B yubiadmin-config
|
||||
[\fI--help\fR] [\fI--interface INTERFACE\fR] [\fI--port PORT\fR] [\fI--username USERNAME] [\fI--password PASSWORD]
|
||||
|
||||
.SH DESCRIPTION
|
||||
Edits the YubiAdmin configuration file. Run either without arguments to
|
||||
interactively set the available options, or with one or more of the
|
||||
command-line arguments set to set specific options.
|
||||
.HP
|
||||
\fB\-\-help, \-h\fR Usage help.
|
||||
.HP
|
||||
\fB\-\-interface \-i\fR Network interface to listen to.
|
||||
.HP
|
||||
\fB\-\-port \-p\fR TCP port to listen to.
|
||||
.HP
|
||||
\fB\-\-username \-U\fR Username to use for authentication.
|
||||
.HP
|
||||
\fB\-\-password \-P\fR Password to use for authentication.
|
||||
.PP
|
||||
Configuration is written to /etc/yubico/admin/yubiadmin.conf
|
||||
.SH BUGS
|
||||
Report yubiadmin-config bugs in
|
||||
.URL "https://github.com/Yubico/yubiadmin/issues" "the issue tracker"
|
||||
.SH "SEE ALSO"
|
||||
|
||||
The
|
||||
.URL "https://github.com/Yubico/yubiadmin" "yubiadmin home page"
|
||||
.PP
|
||||
YubiKeys can be obtained from
|
||||
.URL "http://www.yubico.com/" "Yubico" "."
|
||||
|
@ -1,42 +0,0 @@
|
||||
#!/usr/bin/python
|
||||
|
||||
import os
|
||||
import base64
|
||||
from wsgiref.simple_server import make_server
|
||||
from webob.dec import wsgify
|
||||
from webob import exc
|
||||
from yubiadmin import server
|
||||
from yubiadmin.static import FileApp, DirectoryApp
|
||||
from yubiadmin.config import settings
|
||||
|
||||
REALM = 'YubiADMIN'
|
||||
STATIC_ASSETS = ['js', 'css', 'img', 'favicon.ico']
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# TODO: Take command line args to set port.
|
||||
mod_dir = os.path.dirname(server.__file__)
|
||||
base_dir = os.path.abspath(os.path.join(mod_dir))
|
||||
static_dir = os.path.join(base_dir, 'static')
|
||||
|
||||
static_app = DirectoryApp(static_dir)
|
||||
favicon_app = FileApp(os.path.join(static_dir, 'favicon.ico'))
|
||||
|
||||
@wsgify
|
||||
def with_static(request):
|
||||
if request.authorization:
|
||||
_, auth = request.authorization
|
||||
if base64.b64decode(auth) == '%s:%s' % (settings['user'],
|
||||
settings['pass']):
|
||||
base = request.path_info_peek()
|
||||
if base in STATIC_ASSETS:
|
||||
return request.get_response(static_app)
|
||||
return request.get_response(server.application)
|
||||
|
||||
#Deny access
|
||||
response = exc.HTTPUnauthorized()
|
||||
response.www_authenticate = ('Basic', {'realm': REALM})
|
||||
return response
|
||||
|
||||
httpd = make_server(settings['iface'], settings['port'], with_static)
|
||||
httpd.serve_forever()
|
70
bin/yubiadmin.1
Normal file
70
bin/yubiadmin.1
Normal file
@ -0,0 +1,70 @@
|
||||
.\" Copyright (c) 2013 Yubico AB
|
||||
.\" All rights reserved.
|
||||
.\"
|
||||
.\" Redistribution and use in source and binary forms, with or without
|
||||
.\" modification, are permitted provided that the following conditions are
|
||||
.\" met:
|
||||
.\"
|
||||
.\" * Redistributions of source code must retain the above copyright
|
||||
.\" notice, this list of conditions and the following disclaimer.
|
||||
.\"
|
||||
.\" * Redistributions in binary form must reproduce the above
|
||||
.\" copyright notice, this list of conditions and the following
|
||||
.\" disclaimer in the documentation and/or other materials provided
|
||||
.\" with the distribution.
|
||||
.\"
|
||||
.\" THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
.\" "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
.\" LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
.\" A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
.\" OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
.\" SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
.\" LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
.\" DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
.\" THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
.\" (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
.\" OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
.\"
|
||||
.\" The following commands are required for all man pages.
|
||||
.de URL
|
||||
\\$2 \(laURL: \\$1 \(ra\\$3
|
||||
..
|
||||
.if \n[.g] .mso www.tmac
|
||||
.TH yubiadmin "1" "May 2013" "yubiadmin"
|
||||
.SH NAME
|
||||
yubiadmin - Web interface for configuring Yubico software components.
|
||||
.SH SYNOPSIS
|
||||
.B yubiadmin
|
||||
[\fI--help\fR] [\fI--interface INTERFACE\fR] [\fI--port PORT\fR] [\fI--username USERNAME] [\fI--password PASSWORD]
|
||||
|
||||
.SH DESCRIPTION
|
||||
Runs the YubiAdmin web server. To be able to read and write the various
|
||||
configuration files that the YubiAdmin server exposes, the server is intended
|
||||
to be run as root. By default the server can be accessed by pointing a web
|
||||
browser to http://localhost:8080/ on the machine which is running the server,
|
||||
using the username: "yubiadmin" and the password: "yubiadmin". These settings
|
||||
and many more are available by editing the configuration file, or by using the
|
||||
program arguments.
|
||||
.HP
|
||||
\fB\-\-help, \-h\fR Usage help.
|
||||
.HP
|
||||
\fB\-\-interface \-i\fR Network interface to listen to.
|
||||
.HP
|
||||
\fB\-\-port \-p\fR TCP port to listen to.
|
||||
.HP
|
||||
\fB\-\-username \-U\fR Username to use for authentication.
|
||||
.HP
|
||||
\fB\-\-password \-P\fR Password to use for authentication.
|
||||
.PP
|
||||
Configuration is read from /etc/yubico/admin/yubiadmin.conf
|
||||
.SH BUGS
|
||||
Report yubiadmin bugs in
|
||||
.URL "https://github.com/Yubico/yubiadmin/issues" "the issue tracker"
|
||||
.SH "SEE ALSO"
|
||||
|
||||
The
|
||||
.URL "https://github.com/Yubico/yubiadmin" "yubiadmin home page"
|
||||
.PP
|
||||
YubiKeys can be obtained from
|
||||
.URL "http://www.yubico.com/" "Yubico" "."
|
||||
|
21
conf/logging.conf
Normal file
21
conf/logging.conf
Normal file
@ -0,0 +1,21 @@
|
||||
[loggers]
|
||||
keys=root
|
||||
|
||||
[logger_root]
|
||||
level=INFO
|
||||
handlers=fileHandler
|
||||
|
||||
[formatters]
|
||||
keys=formatter
|
||||
|
||||
[handlers]
|
||||
keys=fileHandler
|
||||
|
||||
[formatter_formatter]
|
||||
format=[%(levelname)s] %(asctime)s %(name)s: %(message)s
|
||||
datefmt=%Y-%m-%d %I:%M:%S
|
||||
|
||||
[handler_fileHandler]
|
||||
class=handlers.WatchedFileHandler
|
||||
formatter=formatter
|
||||
args=("/var/log/yubiadmin.log",)
|
11
release.py
11
release.py
@ -87,6 +87,9 @@ class release(Command):
|
||||
tag_opts[0] = '-u ' + self.keyid
|
||||
self.execute(os.system, ('git tag ' + (' '.join(tag_opts)),))
|
||||
|
||||
def _do_call_publish(self, cmd):
|
||||
self._published = os.system(cmd) == 0
|
||||
|
||||
def _publish(self):
|
||||
web_repo = os.getenv('YUBICO_GITHUB_REPO')
|
||||
if web_repo and os.path.isdir(web_repo):
|
||||
@ -96,9 +99,13 @@ class release(Command):
|
||||
]
|
||||
cmd = '%s/publish %s %s %s' % (
|
||||
web_repo, self.name, self.version, ' '.join(artifacts))
|
||||
if self.execute(os.system, (cmd,)) == 0:
|
||||
|
||||
self.execute(self._do_call_publish, (cmd,))
|
||||
if self._published:
|
||||
self.announce("Release published! Don't forget to:", log.INFO)
|
||||
self.announce("")
|
||||
self.announce(" (cd %s && git push)" % web_repo, log.INFO)
|
||||
self.announce("")
|
||||
else:
|
||||
self.warn("There was a problem publishing the release!")
|
||||
else:
|
||||
@ -138,4 +145,6 @@ class release(Command):
|
||||
self._publish()
|
||||
|
||||
self.announce("Release complete! Don't forget to:", log.INFO)
|
||||
self.announce("")
|
||||
self.announce(" git push && git push --tags", log.INFO)
|
||||
self.announce("")
|
||||
|
8
setup.py
8
setup.py
@ -32,7 +32,7 @@ from release import release
|
||||
|
||||
setup(
|
||||
name='yubiadmin',
|
||||
version='0.0.7',
|
||||
version='0.1.7',
|
||||
author='Dain Nilsson',
|
||||
author_email='dain@yubico.com',
|
||||
maintainer='Yubico Open Source Maintainers',
|
||||
@ -41,9 +41,9 @@ setup(
|
||||
license='BSD 2 clause',
|
||||
packages=['yubiadmin', 'yubiadmin.apps', 'yubiadmin.util'],
|
||||
include_package_data=True,
|
||||
scripts=['bin/yubiadmin-server'],
|
||||
scripts=['bin/yubiadmin', 'bin/yubiadmin-config'],
|
||||
setup_requires=['nose>=1.0'],
|
||||
install_requires=['webob', 'Jinja2', 'WTForms'],
|
||||
install_requires=['webob', 'Jinja2', 'WTForms', 'requests'],
|
||||
test_suite='nose.collector',
|
||||
tests_require=[''],
|
||||
cmdclass={'release': release},
|
||||
@ -51,7 +51,7 @@ setup(
|
||||
'License :: OSI Approved :: BSD License',
|
||||
'Operating System :: OS Independent',
|
||||
'Programming Language :: Python',
|
||||
'Development Status :: 2 - Pre-Alpha',
|
||||
'Development Status :: 4 - Beta',
|
||||
'Environment :: Web Environment',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
|
@ -26,14 +26,21 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import sys
|
||||
from importlib import import_module
|
||||
|
||||
apps = []
|
||||
__all__ = ['apps']
|
||||
|
||||
|
||||
def get_name(app):
|
||||
return getattr(app, 'name', None) or sys.modules[app.__module__].__file__ \
|
||||
.split('/')[-1].rsplit('.', 1)[0]
|
||||
|
||||
for filename in os.listdir(os.path.dirname(__file__)):
|
||||
if filename.endswith('.py') and not filename.startswith('__'):
|
||||
module = import_module('yubiadmin.apps.%s' % filename[:-3])
|
||||
__all__.append(module)
|
||||
if hasattr(module, 'app'):
|
||||
apps.append(module.app)
|
||||
apps.sort(key=lambda app: (app.priority, app.name))
|
||||
|
97
yubiadmin/apps/admin.py
Normal file
97
yubiadmin/apps/admin.py
Normal file
@ -0,0 +1,97 @@
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from threading import Timer
|
||||
from wtforms.fields import IntegerField, TextField, PasswordField
|
||||
from wtforms.widgets import PasswordInput
|
||||
from wtforms.validators import NumberRange, IPAddress
|
||||
from yubiadmin.util.app import App
|
||||
from yubiadmin.util.config import python_handler, FileConfig
|
||||
from yubiadmin.util.form import ConfigForm
|
||||
from yubiadmin.util.system import invoke_rc_d
|
||||
|
||||
__all__ = [
|
||||
'app'
|
||||
]
|
||||
|
||||
|
||||
admin_config = FileConfig(
|
||||
'/etc/yubico/admin/yubiadmin.conf',
|
||||
[
|
||||
('interface', python_handler('INTERFACE', '127.0.0.1')),
|
||||
('port', python_handler('PORT', 8080)),
|
||||
('username', python_handler('USERNAME', 'yubiadmin')),
|
||||
('password', python_handler('PASSWORD', 'yubiadmin')),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ConnectionForm(ConfigForm):
|
||||
legend = 'Connection'
|
||||
description = 'Server network interface settings'
|
||||
config = admin_config
|
||||
|
||||
interface = TextField('Listening Interface', [IPAddress()])
|
||||
port = IntegerField('Listening Port', [NumberRange(1, 65535)])
|
||||
|
||||
|
||||
class CredentialsForm(ConfigForm):
|
||||
legend = 'Credentials'
|
||||
description = 'Credentials for accessing YubiAdmin'
|
||||
config = admin_config
|
||||
|
||||
username = TextField('Username', [])
|
||||
password = PasswordField('Password',
|
||||
widget=PasswordInput(hide_value=False))
|
||||
|
||||
|
||||
class YubiAdmin(App):
|
||||
"""
|
||||
YubiAdmin
|
||||
|
||||
Web based configuration server.
|
||||
"""
|
||||
|
||||
name = 'admin'
|
||||
priority = 10
|
||||
sections = ['general']
|
||||
|
||||
def general(self, request):
|
||||
return self.render_forms(request,
|
||||
[ConnectionForm(), CredentialsForm()],
|
||||
template='admin/general')
|
||||
|
||||
def restart(self, request):
|
||||
if 'now' in request.params:
|
||||
invoke_rc_d('yubiadmin', 'restart')
|
||||
else:
|
||||
timer = Timer(1, invoke_rc_d, args=('yubiadmin', 'restart'))
|
||||
timer.start()
|
||||
return self.redirect('/%s/general' % self.name)
|
||||
|
||||
|
||||
app = YubiAdmin()
|
515
yubiadmin/apps/auth.py
Normal file
515
yubiadmin/apps/auth.py
Normal file
@ -0,0 +1,515 @@
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import requests
|
||||
import imp
|
||||
from webob import exc
|
||||
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,
|
||||
Email)
|
||||
from yubiadmin.util.app import App, CollectionApp
|
||||
from yubiadmin.util.system import invoke_rc_d
|
||||
from yubiadmin.util.config import (python_handler, python_list_handler,
|
||||
FileConfig)
|
||||
from yubiadmin.util.form import ConfigForm, FileForm, ListField
|
||||
from yubiadmin.apps.dashboard import panel
|
||||
import logging
|
||||
|
||||
__all__ = [
|
||||
'app'
|
||||
]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
imp.find_module('yubiauth')
|
||||
YUBIAUTH_INSTALLED = True
|
||||
except ImportError:
|
||||
YUBIAUTH_INSTALLED = False
|
||||
YubiAuth = None
|
||||
|
||||
AUTH_CONFIG_FILE = '/etc/yubico/auth/yubiauth.conf'
|
||||
YKVAL_SERVERS = [
|
||||
'https://api.yubico.com/wsapi/2.0/verify',
|
||||
'https://api2.yubico.com/wsapi/2.0/verify',
|
||||
'https://api3.yubico.com/wsapi/2.0/verify',
|
||||
'https://api4.yubico.com/wsapi/2.0/verify',
|
||||
'https://api5.yubico.com/wsapi/2.0/verify'
|
||||
]
|
||||
YKVAL_DEFAULT_ID = 11004
|
||||
YKVAL_DEFAULT_SECRET = '5Vm3Zp2mUTQHMo1DeG9tdojpc1Y='
|
||||
|
||||
|
||||
auth_config = FileConfig(
|
||||
AUTH_CONFIG_FILE,
|
||||
[
|
||||
('server_list', python_list_handler('YKVAL_SERVERS', YKVAL_SERVERS)),
|
||||
('client_id', python_handler('YKVAL_CLIENT_ID', YKVAL_DEFAULT_ID)),
|
||||
('client_secret', python_handler('YKVAL_CLIENT_SECRET',
|
||||
YKVAL_DEFAULT_SECRET)),
|
||||
('auto_provision', python_handler('AUTO_PROVISION', True)),
|
||||
('allow_empty', python_handler('ALLOW_EMPTY_PASSWORDS', False)),
|
||||
('security_level', python_handler('SECURITY_LEVEL', 1)),
|
||||
('yubikey_id', python_handler('YUBIKEY_IDENTIFICATION', False)),
|
||||
('use_ldap', python_handler('USE_LDAP', False)),
|
||||
('ldap_server', python_handler('LDAP_SERVER', 'ldap://127.0.0.1')),
|
||||
('ldap_bind_dn', python_handler('LDAP_BIND_DN',
|
||||
'uid={user.name},ou=People,dc=lan')),
|
||||
('ldap_auto_import', python_handler('LDAP_AUTO_IMPORT', True)),
|
||||
('use_hsm', python_handler('USE_HSM', False)),
|
||||
('hsm_device', python_handler('YHSM_DEVICE', 'yhsm://localhost:5348')),
|
||||
('db_config', python_handler('DATABASE_CONFIGURATION',
|
||||
'sqlite:///:memory:')),
|
||||
('user_registration', python_handler('ENABLE_USER_REGISTRATION',
|
||||
True)),
|
||||
('user_deletion', python_handler('ALLOW_USER_DELETE', False)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class SecurityForm(ConfigForm):
|
||||
legend = 'Security'
|
||||
description = 'Security Settings for YubiAuth'
|
||||
config = auth_config
|
||||
|
||||
auto_provision = BooleanField(
|
||||
'Auto Provision YubiKeys',
|
||||
description="""
|
||||
When enabled, an attempt to authenticate a user that doesn't have a
|
||||
YubiKey assigned with a valid YubiKey OTP, will cause that YubiKey to
|
||||
become automatically assigned to the user.
|
||||
"""
|
||||
)
|
||||
yubikey_id = BooleanField(
|
||||
'Allow YubiKey Identification',
|
||||
description="""
|
||||
Allow users to authenticate using their YubiKey as identification,
|
||||
omitting the username.
|
||||
"""
|
||||
)
|
||||
allow_empty = BooleanField(
|
||||
'Allow Empty Passwords',
|
||||
description="""
|
||||
Allow users with no password to log in without providing a password.
|
||||
When not checked, a user with no password will be unable to log in.
|
||||
"""
|
||||
)
|
||||
user_registration = BooleanField(
|
||||
'Enable User Registration',
|
||||
description="""
|
||||
Allow users to register themselves using the YubiAuth client interface.
|
||||
"""
|
||||
)
|
||||
user_deletion = BooleanField(
|
||||
'Enable User Deletion',
|
||||
description="""
|
||||
Allow users to delete their own account using the YubiAuth client
|
||||
interface.
|
||||
"""
|
||||
)
|
||||
security_level = SelectField(
|
||||
'Security Level',
|
||||
coerce=int,
|
||||
choices=[(0, 'Never'), (1, 'When Provisioned'), (2, 'Always')],
|
||||
description="""
|
||||
Defines who is required to provide a YubiKey OTP when logging in.
|
||||
The available levels are:
|
||||
Never - OTPs are not required to authenticate, by anyone.
|
||||
|
||||
When Provisioned - OTPs are required by all users that have a
|
||||
YubiKey assigned to them.
|
||||
|
||||
Always - OTPs are required by all users. If no YubiKey has been
|
||||
assigned, that user cannot log in, unless auto-provisioning is enabled.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class HSMForm(ConfigForm):
|
||||
legend = 'YubiHSM'
|
||||
description = 'Settings for the YubiHSM hardware device.'
|
||||
config = auth_config
|
||||
|
||||
use_hsm = BooleanField(
|
||||
'Use a YubiHSM',
|
||||
description="""
|
||||
Check this if you have a YubiHSM to be used by YubiAuth for more
|
||||
secure local password validation.
|
||||
"""
|
||||
)
|
||||
hsm_device = TextField('YubiHSM device')
|
||||
|
||||
|
||||
class LDAPForm(ConfigForm):
|
||||
legend = 'LDAP authentication'
|
||||
description = """
|
||||
Settings for authenticating users against an LDAP server. When LDAP
|
||||
authentication is used only users that exist on the LDAP server will be
|
||||
permitted to log in, and password validatoin will be delegated to the LDAP
|
||||
server.
|
||||
"""
|
||||
config = auth_config
|
||||
attrs = {
|
||||
'ldap_server': {'class': 'input-xxlarge'},
|
||||
'ldap_bind_dn': {'class': 'input-xxlarge'}
|
||||
}
|
||||
|
||||
use_ldap = BooleanField(
|
||||
'Authenticate users against LDAP',
|
||||
description="""
|
||||
Check this to authenticate users passwords externally against an LDAP
|
||||
server.
|
||||
"""
|
||||
)
|
||||
ldap_server = TextField('LDAP server URL')
|
||||
ldap_bind_dn = TextField('Bind DN for user authentication')
|
||||
ldap_auto_import = BooleanField(
|
||||
'Automatically create users from LDAP',
|
||||
description="""
|
||||
Auto-create missing users in YubiAuth upon log in if the user is valid
|
||||
in the LDAP database.
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class DatabaseForm(ConfigForm):
|
||||
legend = 'Database'
|
||||
description = 'Settings for connecting to the database'
|
||||
config = auth_config
|
||||
attrs = {'db_config': {'class': 'input-xxlarge'}}
|
||||
|
||||
db_config = TextField(
|
||||
'Connection String',
|
||||
description="""
|
||||
SQLAlchemy connection string. For full details on syntax and supported
|
||||
database engines, see this section of the <a
|
||||
href="http://docs.sqlalchemy.org/en/rel_0_8/core/engines.html"
|
||||
>SQLAlchemy documentation</a>.
|
||||
Example: <code>postgresql://yubiauth:password@localhost/yubiauth</code>
|
||||
"""
|
||||
)
|
||||
|
||||
|
||||
class ValidationServerForm(ConfigForm):
|
||||
legend = 'Validation Servers'
|
||||
description = 'Configure servers used for YubiKey OTP validation'
|
||||
config = auth_config
|
||||
attrs = {
|
||||
'client_secret': {'class': 'input-xxlarge'},
|
||||
'server_list': {'rows': 5, 'class': 'input-xxlarge'}
|
||||
}
|
||||
|
||||
client_id = IntegerField('Client ID', [NumberRange(0)])
|
||||
client_secret = TextField('API key')
|
||||
server_list = ListField(
|
||||
'Validation Server URLs', [URL(require_tld=False)],
|
||||
description="""
|
||||
List of URLs to YubiKey validation servers.
|
||||
Example: <code>http://example.com/wsapi/2.0/verify</code>
|
||||
""")
|
||||
|
||||
|
||||
class GetApiKeyForm(Form):
|
||||
legend = 'Get a YubiCloud API key'
|
||||
description = """
|
||||
To validate YubiKey OTPS against the YubiCloud, you need to get a free API
|
||||
key. You need to authenticate yourself using a Yubikey One-Time Password
|
||||
and provide your e-mail address as a reference.
|
||||
"""
|
||||
email = TextField('E-mail address', [Email()])
|
||||
otp = TextField('YubiKey OTP', [Regexp(r'^[cbdefghijklnrtuv]{1,64}$')])
|
||||
attrs = {'otp': {'class': 'input-xxlarge'}}
|
||||
|
||||
@staticmethod
|
||||
def extract_param(matches, predicate):
|
||||
while len(matches) > 0:
|
||||
elem = matches.pop(0)
|
||||
if predicate(elem):
|
||||
return elem
|
||||
|
||||
def save(self):
|
||||
email = self.email.data
|
||||
otp = self.otp.data
|
||||
log.info('Attempting to register a new YubiCloud API key. '
|
||||
'Email: %s, OTP: %s' % (email, otp))
|
||||
response = requests.post(
|
||||
'https://upgrade.yubico.com/getapikey/?format=json', data=
|
||||
{'email': email, 'otp': otp})
|
||||
data = response.json()
|
||||
if data['status']:
|
||||
log.info('Registered YubiCloud Client with ID: %d', data['id'])
|
||||
auth_config.read()
|
||||
auth_config['client_id'] = data['id']
|
||||
auth_config['client_secret'] = data['key']
|
||||
auth_config.commit()
|
||||
else:
|
||||
log.error('Failed registering new YubiCloud client: %s',
|
||||
data['error'])
|
||||
raise Exception(data['error'])
|
||||
|
||||
|
||||
def using_default_client():
|
||||
auth_config.read()
|
||||
return auth_config['client_id'] == YKVAL_DEFAULT_ID and \
|
||||
auth_config['client_secret'] == YKVAL_DEFAULT_SECRET
|
||||
|
||||
|
||||
def using_ldap():
|
||||
auth_config.read()
|
||||
return auth_config['use_ldap']
|
||||
|
||||
|
||||
class YubiAuthApp(App):
|
||||
|
||||
"""
|
||||
YubiAuth
|
||||
|
||||
Web based configuration server.
|
||||
"""
|
||||
|
||||
name = 'auth'
|
||||
priority = 40
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
return not os.path.isfile(AUTH_CONFIG_FILE)
|
||||
|
||||
@property
|
||||
def sections(self):
|
||||
base = ['general', 'database', 'password', 'otp']
|
||||
if YUBIAUTH_INSTALLED:
|
||||
return base + ['users', 'advanced']
|
||||
return base + ['advanced']
|
||||
|
||||
@property
|
||||
def dash_panels(self):
|
||||
if using_default_client():
|
||||
yield panel('YubiAuth', 'Using default YubiCloud client!',
|
||||
'/%s/otp' % self.name, 'danger')
|
||||
if using_ldap():
|
||||
yield panel('YubiAuth',
|
||||
'Using LDAP: %s' % auth_config['ldap_server'],
|
||||
'/%s/password' % self.name, 'info')
|
||||
|
||||
def general(self, request):
|
||||
return self.render_forms(request, [SecurityForm()],
|
||||
template='auth/general')
|
||||
|
||||
def reload(self, request):
|
||||
invoke_rc_d('apache2', 'reload')
|
||||
return self.redirect('/auth/general')
|
||||
|
||||
def database(self, request):
|
||||
return self.render_forms(request, [DatabaseForm()])
|
||||
|
||||
def otp(self, request):
|
||||
"""
|
||||
OTP Validation
|
||||
"""
|
||||
form = ValidationServerForm()
|
||||
resp = self.render_forms(request, [form])
|
||||
if using_default_client():
|
||||
resp.data['alerts'].append(
|
||||
{
|
||||
'type': 'warning',
|
||||
'title': 'WARNING: Default Client ID used!<br />',
|
||||
'message': 'As the default key is publically known, it is '
|
||||
'not as secure as using a unique API key.\n'
|
||||
'<a href="/auth/getapikey" class="btn btn-primary">'
|
||||
'Generate unique API Key</a>'
|
||||
})
|
||||
return resp
|
||||
|
||||
def password(self, request):
|
||||
"""
|
||||
Password Validation
|
||||
"""
|
||||
return self.render_forms(request, [LDAPForm(), HSMForm()])
|
||||
|
||||
def getapikey(self, request):
|
||||
return self.render_forms(request, [GetApiKeyForm()], success_msg=
|
||||
"API Key registered!")
|
||||
|
||||
def advanced(self, request):
|
||||
return self.render_forms(request, [
|
||||
FileForm(AUTH_CONFIG_FILE, 'Configuration', lang='python')
|
||||
], scripts=['editor'])
|
||||
|
||||
def users(self, request):
|
||||
"""
|
||||
Manage Users
|
||||
"""
|
||||
global YubiAuth, User
|
||||
if YubiAuth is None:
|
||||
from yubiauth import YubiAuth as _yubiauth
|
||||
from yubiauth.core.model import User as _user
|
||||
YubiAuth = _yubiauth
|
||||
User = _user
|
||||
|
||||
with YubiAuth() as auth:
|
||||
app = YubiAuthUsers(auth)
|
||||
try:
|
||||
return app(request).prerendered
|
||||
except (exc.HTTPOk, exc.HTTPRedirection) as e:
|
||||
# Ensure auth is closed on 200-300 codes.
|
||||
exception = e
|
||||
raise exception
|
||||
|
||||
# Pulls the tab to the right:
|
||||
advanced.advanced = True
|
||||
|
||||
|
||||
class CreateUserForm(Form):
|
||||
legend = 'Create new User'
|
||||
username = TextField('Username')
|
||||
password = PasswordField('Password',
|
||||
widget=PasswordInput(hide_value=False))
|
||||
verify = PasswordField('Verify password',
|
||||
[EqualTo('password')],
|
||||
widget=PasswordInput(hide_value=False))
|
||||
|
||||
def save(self):
|
||||
with YubiAuth() as auth:
|
||||
auth.create_user(self.username.data, self.password.data)
|
||||
self.username.data = None
|
||||
self.password.data = None
|
||||
self.verify.data = None
|
||||
|
||||
|
||||
class SetPasswordForm(Form):
|
||||
legend = 'Change Password'
|
||||
password = PasswordField('New password',
|
||||
[Optional()],
|
||||
widget=PasswordInput(hide_value=False))
|
||||
verify = PasswordField('Verify password',
|
||||
[EqualTo('password')],
|
||||
widget=PasswordInput(hide_value=False))
|
||||
|
||||
def __init__(self, user_id, **kwargs):
|
||||
super(SetPasswordForm, self).__init__(**kwargs)
|
||||
self.user_id = user_id
|
||||
|
||||
def load(self):
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
if self.password.data:
|
||||
with YubiAuth() as auth:
|
||||
user = auth.get_user(self.user_id)
|
||||
user.set_password(self.password.data)
|
||||
self.password.data = None
|
||||
self.verify.data = None
|
||||
|
||||
|
||||
class SetPasswordDisabledForm(Form):
|
||||
legend = 'Change Password'
|
||||
description = 'Cannot change password when using LDAP for authentication.'
|
||||
|
||||
|
||||
class AssignYubiKeyForm(Form):
|
||||
legend = 'Assign YubiKey'
|
||||
assign = TextField('Assign YubiKey',
|
||||
[Regexp(r'^[cbdefghijklnrtuv]{1,64}$'),
|
||||
Optional()])
|
||||
|
||||
def __init__(self, user_id, **kwargs):
|
||||
super(AssignYubiKeyForm, self).__init__(**kwargs)
|
||||
self.user_id = user_id
|
||||
|
||||
def load(self):
|
||||
pass
|
||||
|
||||
def save(self):
|
||||
if self.assign.data:
|
||||
with YubiAuth() as auth:
|
||||
user = auth.get_user(self.user_id)
|
||||
user.assign_yubikey(self.assign.data)
|
||||
self.assign.data = None
|
||||
|
||||
|
||||
class YubiAuthUsers(CollectionApp):
|
||||
base_url = '/auth/users'
|
||||
item_name = 'Users'
|
||||
caption = 'YubiAuth Users'
|
||||
columns = ['Username', 'YubiKeys']
|
||||
template = 'auth/list'
|
||||
|
||||
def __init__(self, auth):
|
||||
self.auth = auth
|
||||
|
||||
def _size(self):
|
||||
return self.auth.session.query(User).count()
|
||||
|
||||
def _get(self, offset=0, limit=None):
|
||||
users = self.auth.session.query(User).order_by(User.name) \
|
||||
.offset(offset).limit(limit)
|
||||
|
||||
return map(lambda user: {
|
||||
'id': user.id,
|
||||
'label': user.name,
|
||||
'Username': '<a href="/auth/users/show/%d">%s</a>' % (user.id,
|
||||
user.name),
|
||||
'YubiKeys': ', '.join(user.yubikeys.keys())
|
||||
}, users)
|
||||
|
||||
def _labels(self, ids):
|
||||
users = self.auth.session.query(User.name) \
|
||||
.filter(User.id.in_(map(int, ids))).all()
|
||||
return map(lambda x: x[0], users)
|
||||
|
||||
def _delete(self, ids):
|
||||
self.auth.session.query(User) \
|
||||
.filter(User.id.in_(map(int, ids))).delete('fetch')
|
||||
|
||||
def create(self, request):
|
||||
return self.render_forms(request, [CreateUserForm()],
|
||||
success_msg='User created!')
|
||||
|
||||
def show(self, request):
|
||||
id = int(request.path_info_pop())
|
||||
user = self.auth.get_user(id)
|
||||
if 'unassign' in request.params:
|
||||
del user.yubikeys[request.params['unassign']]
|
||||
msg = None
|
||||
if request.params.get('password', None):
|
||||
msg = 'Password set!'
|
||||
elif request.params.get('assign', None):
|
||||
msg = 'YubiKey assigned!'
|
||||
elif request.params.get('unassign', None):
|
||||
msg = 'YubiKey unassigned!'
|
||||
pwd_form = SetPasswordDisabledForm() if using_ldap() else \
|
||||
SetPasswordForm(user.id)
|
||||
forms = [pwd_form, AssignYubiKeyForm(user.id)]
|
||||
return self.render_forms(request, forms, 'auth/user', user=user.data,
|
||||
success_msg=msg)
|
||||
|
||||
|
||||
app = YubiAuthApp()
|
57
yubiadmin/apps/dashboard.py
Normal file
57
yubiadmin/apps/dashboard.py
Normal file
@ -0,0 +1,57 @@
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from yubiadmin.util.app import App, render
|
||||
|
||||
__all__ = [
|
||||
'app',
|
||||
'panel'
|
||||
]
|
||||
|
||||
|
||||
def panel(title, content, link=None, level=None):
|
||||
return {
|
||||
'title': title,
|
||||
'content': content,
|
||||
'link': link,
|
||||
'level': level
|
||||
}
|
||||
|
||||
|
||||
class DashboardApp(App):
|
||||
hidden = True
|
||||
|
||||
def __call__(self, request):
|
||||
from yubiadmin.apps import apps
|
||||
panels = [panel for app in apps if not getattr(app, 'disabled', False)
|
||||
and hasattr(app, 'dash_panels') for panel in app.dash_panels]
|
||||
request.environ['yubiadmin.response'].extend('content',
|
||||
render('dashboard',
|
||||
panels=panels))
|
||||
|
||||
|
||||
app = DashboardApp()
|
218
yubiadmin/apps/freerad.py
Normal file
218
yubiadmin/apps/freerad.py
Normal file
@ -0,0 +1,218 @@
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from yubiadmin.util.app import App, CollectionApp, render
|
||||
from yubiadmin.util.system import run, invoke_rc_d
|
||||
from yubiadmin.util.form import FileForm
|
||||
from yubiadmin.util.config import parse_block
|
||||
from yubiadmin.apps.dashboard import panel
|
||||
from wtforms import Form
|
||||
from wtforms.fields import TextField
|
||||
import os
|
||||
import re
|
||||
|
||||
__all__ = [
|
||||
'app'
|
||||
]
|
||||
|
||||
CLIENTS_CONFIG_FILE = '/etc/freeradius/clients.conf'
|
||||
|
||||
|
||||
def is_freerad_running():
|
||||
status, _ = run('cat /var/run/freeradius/freeradius.pid | xargs kill -0')
|
||||
return status == 0
|
||||
|
||||
|
||||
class RadTestForm(Form):
|
||||
legend = 'RADIUS test'
|
||||
description = """
|
||||
This utility allows you to test authentication against the RADIUS server
|
||||
using the credentials entered below. By default there will be a client with
|
||||
the client secret: testing123, though you can change this under the RADIUS
|
||||
Clients tab.
|
||||
"""
|
||||
client_secret = TextField('Client Secret', default='testing123')
|
||||
username = TextField('Username')
|
||||
password = TextField('Password')
|
||||
|
||||
|
||||
class FreeRadius(App):
|
||||
"""
|
||||
FreeRADIUS
|
||||
|
||||
RADIUS Server
|
||||
"""
|
||||
|
||||
name = 'freerad'
|
||||
sections = ['general', 'clients']
|
||||
priority = 60
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
return not os.path.isdir('/etc/freeradius')
|
||||
|
||||
@property
|
||||
def dash_panels(self):
|
||||
running = is_freerad_running()
|
||||
yield panel('FreeRADIUS',
|
||||
'FreeRadius server is %s' %
|
||||
('running' if running else 'stopped'),
|
||||
'/%s/general' % self.name,
|
||||
'success' if running else 'danger')
|
||||
|
||||
def __init__(self):
|
||||
self._clients = RadiusClients()
|
||||
|
||||
def general(self, request):
|
||||
alerts = []
|
||||
form = RadTestForm()
|
||||
|
||||
if 'username' in request.params:
|
||||
form.process(request.params)
|
||||
username = form.username.data
|
||||
password = form.password.data
|
||||
secret = form.client_secret.data
|
||||
|
||||
cmd = 'radtest "%s" "%s" localhost 0 "%s"' % (username, password, secret)
|
||||
status, output = run(cmd)
|
||||
alert = {'title': 'Command: %s' % cmd}
|
||||
alert['message'] = '<pre style="white-space: pre-wrap;">%s</pre>' \
|
||||
% output
|
||||
if status == 0:
|
||||
alert['type'] = 'success'
|
||||
elif status == 1:
|
||||
alert['type'] = 'warn'
|
||||
else:
|
||||
alert['type'] = 'error'
|
||||
alert['message'] = 'There was an error running the command. ' \
|
||||
'Exit code: %d' % status
|
||||
alerts.append(alert)
|
||||
|
||||
return render('freerad/general', form=form, alerts=alerts,
|
||||
running=is_freerad_running())
|
||||
|
||||
def _unused_clients(self, request):
|
||||
"""
|
||||
RADIUS clients
|
||||
"""
|
||||
return self._clients(request)
|
||||
|
||||
def server(self, request):
|
||||
if request.params['server'] == 'toggle':
|
||||
if is_freerad_running():
|
||||
invoke_rc_d('freeradius', 'stop')
|
||||
else:
|
||||
invoke_rc_d('freeradius', 'start')
|
||||
else:
|
||||
invoke_rc_d('freeradius', 'restart')
|
||||
|
||||
return self.redirect('/%s/general' % self.name)
|
||||
|
||||
def clients(self, request):
|
||||
"""
|
||||
RADIUS Clients
|
||||
"""
|
||||
return self.render_forms(request, [
|
||||
FileForm(CLIENTS_CONFIG_FILE, 'clients.conf',
|
||||
'Changes require the FreeRADIUS server to be restarted.',
|
||||
lang='ini')
|
||||
], scripts=['editor'])
|
||||
|
||||
|
||||
CLIENT = re.compile('client\s+(.+)\s+{')
|
||||
ATTRIBUTE = re.compile('([^\s]+)\s+=\s+([^\s]+)')
|
||||
|
||||
|
||||
def parse_client(name, content):
|
||||
data = {}
|
||||
for line in content.splitlines():
|
||||
line = line.split('#', 1)[0].strip()
|
||||
match = ATTRIBUTE.match(line)
|
||||
if match:
|
||||
key = match.group(1)
|
||||
value = match.group(2)
|
||||
data[key] = value
|
||||
client = {
|
||||
'Name': name or data.get('shortname', data.get('ipaddr')),
|
||||
'data': data,
|
||||
'Attributes': ', '.join(['%s=%s' % (k, v) for (k, v) in data.items()])
|
||||
}
|
||||
return client
|
||||
|
||||
|
||||
def parse_clients(content):
|
||||
lines = content.splitlines()
|
||||
index = 0
|
||||
skip = 0
|
||||
for line in lines:
|
||||
if skip > 0:
|
||||
skip -= 1
|
||||
continue
|
||||
|
||||
match = CLIENT.match(line.strip())
|
||||
if match:
|
||||
name = match.group(1)
|
||||
c_content = parse_block('\n'.join(lines[index + 1:]), '{', '}')
|
||||
client = parse_client(name, c_content)
|
||||
skip = len(c_content.splitlines())
|
||||
client['id'] = index
|
||||
client['start'] = index
|
||||
client['end'] = index + skip + 2
|
||||
index += skip
|
||||
yield client
|
||||
|
||||
index += 1
|
||||
|
||||
|
||||
class RadiusClients(CollectionApp):
|
||||
base_url = '/freerad/clients'
|
||||
item_name = 'Clients'
|
||||
caption = 'RADIUS Clients'
|
||||
columns = ['Name', 'Attributes']
|
||||
template = 'freerad/client_list'
|
||||
|
||||
def _get(self, offset=0, limit=None):
|
||||
with open(CLIENTS_CONFIG_FILE, 'r') as f:
|
||||
self.content = f.read()
|
||||
|
||||
clients = list(parse_clients(self.content))
|
||||
if limit:
|
||||
limit += offset
|
||||
return clients[offset:limit]
|
||||
|
||||
def _delete(self, ids):
|
||||
ids = map(int, ids)
|
||||
clients = filter(lambda x: x['id'] in ids, self._get())
|
||||
lines = self.content.splitlines()
|
||||
|
||||
for client in reversed(clients):
|
||||
del lines[client['start']:client['end']]
|
||||
|
||||
with open(CLIENTS_CONFIG_FILE, 'w') as f:
|
||||
f.write(os.linesep.join(lines))
|
||||
|
||||
app = FreeRadius()
|
@ -25,6 +25,7 @@
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
from yubiadmin.util.app import App
|
||||
from yubiadmin.util.form import DBConfigForm
|
||||
|
||||
@ -39,10 +40,12 @@ class YubikeyKsm(App):
|
||||
|
||||
YubiKey KSM server
|
||||
"""
|
||||
|
||||
name = 'ksm'
|
||||
sections = ['database']
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
return not os.path.isfile('/etc/yubico/ksm/ykksm-config.php')
|
||||
|
||||
def database(self, request):
|
||||
"""
|
||||
Database Settings
|
||||
|
158
yubiadmin/apps/sys.py
Normal file
158
yubiadmin/apps/sys.py
Normal file
@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import subprocess
|
||||
from webob import Response
|
||||
from threading import Timer
|
||||
from yubiadmin.util.app import App, render
|
||||
from yubiadmin.util.system import run
|
||||
from yubiadmin.apps.dashboard import panel
|
||||
|
||||
__all__ = [
|
||||
'app'
|
||||
]
|
||||
|
||||
|
||||
UPGRADE_LOG = "/var/tmp/yubix-upgrade"
|
||||
|
||||
|
||||
def get_updates():
|
||||
s, o = run("apt-get upgrade -s | awk -F'[][() ]+' '/^Inst/{print $2}'")
|
||||
packages = o.splitlines()
|
||||
return packages
|
||||
|
||||
|
||||
def needs_restart():
|
||||
return os.path.isfile('/var/run/reboot-required')
|
||||
|
||||
|
||||
def reboot():
|
||||
run('reboot')
|
||||
|
||||
|
||||
class Updater(object):
|
||||
def __init__(self):
|
||||
self.proc = subprocess.Popen('DEBIAN_FRONTEND=noninteractive '
|
||||
'apt-get -y dist-upgrade -o '
|
||||
'Dpkg::Options::="--force-confdef" -o '
|
||||
'Dpkg::Options::="--force-confold" | '
|
||||
'tee %s' % UPGRADE_LOG,
|
||||
stdout=subprocess.PIPE, shell=True)
|
||||
|
||||
def __iter__(self):
|
||||
yield """
|
||||
<script type="text/javascript">
|
||||
function reload() {
|
||||
window.location.replace('/sys');
|
||||
}
|
||||
window.onload = function() {
|
||||
setTimeout(reload, 10000);
|
||||
}
|
||||
</script>
|
||||
<strong>Performing update, this may take a while...</strong><br/>
|
||||
<pre>
|
||||
"""
|
||||
|
||||
while True:
|
||||
line = self.proc.stdout.readline()
|
||||
if line:
|
||||
yield line
|
||||
else:
|
||||
yield '</pre><br /><strong>Update complete!</strong>'
|
||||
yield '<script type="text/javascript">reload();</script>'
|
||||
break
|
||||
|
||||
|
||||
class SystemApp(App):
|
||||
"""
|
||||
YubiX System
|
||||
"""
|
||||
sections = ['general']
|
||||
priority = 30
|
||||
|
||||
@property
|
||||
def disabled(self):
|
||||
#return not os.path.isdir('/usr/share/yubix')
|
||||
return False
|
||||
|
||||
@property
|
||||
def hidden(self):
|
||||
return self.disabled
|
||||
|
||||
@property
|
||||
def dash_panels(self):
|
||||
if needs_restart():
|
||||
yield panel('System', 'System restart required', level='danger')
|
||||
|
||||
updates = len(get_updates())
|
||||
if updates > 0:
|
||||
yield panel(
|
||||
'System',
|
||||
'There are <strong>%d</strong> updates available' % updates,
|
||||
'/%s/general' % self.name,
|
||||
'info'
|
||||
)
|
||||
|
||||
_, time = run('date "+%a, %d %b %Y %H:%M"')
|
||||
_, result = run('uptime')
|
||||
rest = [x.strip() for x in result.split('up', 1)][1]
|
||||
parts = [x.strip() for x in rest.split(',')]
|
||||
uptime = parts[0] if not 'days' in parts[0] else '%s, %s' % \
|
||||
tuple(parts[:2])
|
||||
yield panel('System', 'Date: %s<br />Uptime: %s' %
|
||||
(time, uptime), level='info')
|
||||
|
||||
def general(self, request):
|
||||
alerts = []
|
||||
if needs_restart():
|
||||
alerts.append({'message': 'The machine needs to reboot.',
|
||||
'type': 'error'})
|
||||
return render('/sys/general', alerts=alerts, updates=get_updates())
|
||||
|
||||
def update(self, request):
|
||||
run('apt-get update')
|
||||
return self.redirect('/sys')
|
||||
|
||||
def dist_upgrade(self, request):
|
||||
if get_updates():
|
||||
return Response(app_iter=Updater())
|
||||
else:
|
||||
alerts = [{'message': 'Software is up to date!'}]
|
||||
return render('/sys/general', alerts=alerts)
|
||||
|
||||
def reboot(self, request):
|
||||
if 'now' in request.params:
|
||||
run('reboot')
|
||||
else:
|
||||
timer = Timer(1, run, args=('reboot',))
|
||||
timer.start()
|
||||
alerts = [{'type': 'warn', 'message': 'Rebooting System...'}]
|
||||
return render('/sys/general', alerts=alerts)
|
||||
|
||||
|
||||
app = SystemApp()
|
@ -26,20 +26,22 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import re
|
||||
import subprocess
|
||||
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, render
|
||||
from yubiadmin.util.config import (RegexHandler, FileConfig, php_inserter,
|
||||
parse_block, strip_comments)
|
||||
parse_block, strip_comments, strip_quotes)
|
||||
from yubiadmin.util.form import ConfigForm, FileForm, DBConfigForm, ListField
|
||||
from yubiadmin.util.system import invoke_rc_d, run
|
||||
from yubiadmin.apps.dashboard import panel
|
||||
|
||||
__all__ = [
|
||||
'app'
|
||||
]
|
||||
|
||||
COMMENT = re.compile(r'(?ms)(/\*.*?\*/)|(//[^$]*$)|(#[^$]*$)')
|
||||
VALUE = re.compile(r'\s*[\'"](.*)[\'"]\s*')
|
||||
|
||||
YKVAL_CONFIG_FILE = '/etc/yubico/val/ykval-config.php'
|
||||
|
||||
|
||||
def yk_pattern(varname, prefix='', suffix='', flags=None):
|
||||
@ -60,13 +62,6 @@ def yk_handler(varname, default):
|
||||
inserter=php_inserter, default=default)
|
||||
|
||||
|
||||
def strip_quotes(value):
|
||||
match = VALUE.match(value)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return value
|
||||
|
||||
|
||||
def yk_parse_arraystring(value):
|
||||
return filter(None, [strip_quotes(x).strip() for x in strip_comments(value)
|
||||
.split(',')])
|
||||
@ -104,7 +99,7 @@ class KSMHandler(object):
|
||||
block = self._get_block(content)
|
||||
value = ('function otp2ksmurls($otp, $client) {\n' +
|
||||
'\treturn array (\n' +
|
||||
'\m'.join(['\t\t"%s",' % x for x in value]) +
|
||||
'\n'.join(['\t\t"%s",' % x for x in value]) +
|
||||
'\n\t);\n}')
|
||||
if block:
|
||||
match = self.FUNCTION.search(content)
|
||||
@ -115,28 +110,12 @@ class KSMHandler(object):
|
||||
return php_inserter(content, value)
|
||||
|
||||
|
||||
def run(cmd):
|
||||
p = subprocess.Popen(['sh', '-c', cmd], stdout=subprocess.PIPE)
|
||||
return p.wait(), p.stdout.read()
|
||||
|
||||
|
||||
def invoke_rc_d(cmd):
|
||||
if run('which invoke-rd.d')[0] == 0:
|
||||
return run('invoke-rc.d ykval-queue %s' % cmd)
|
||||
else:
|
||||
return run('/etc/init.d/ykval-queue %s' % cmd)
|
||||
|
||||
|
||||
def is_daemon_running():
|
||||
return invoke_rc_d('status')[0] == 0
|
||||
|
||||
|
||||
def restart_daemon():
|
||||
invoke_rc_d('restart')
|
||||
return invoke_rc_d('ykval-queue', 'status')[0] == 0
|
||||
|
||||
|
||||
ykval_config = FileConfig(
|
||||
'/etc/yubico/val/ykval-config.php',
|
||||
YKVAL_CONFIG_FILE,
|
||||
[
|
||||
('sync_default', yk_handler('SYNC_DEFAULT_LEVEL', 60)),
|
||||
('sync_secure', yk_handler('SYNC_SECURE_LEVEL', 40)),
|
||||
@ -189,23 +168,23 @@ class SyncPoolForm(ConfigForm):
|
||||
}
|
||||
|
||||
sync_pool = ListField(
|
||||
'Sync Pool URLs', [URL()],
|
||||
'Sync Pool URLs', [URL(require_tld=False)],
|
||||
description="""
|
||||
List of URLs to other servers in the sync pool.<br />
|
||||
List of URLs to other servers in the sync pool.
|
||||
Example: <code>http://example.com/wsapi/2.0/sync</code>
|
||||
""")
|
||||
allowed_sync_pool = ListField(
|
||||
'Allowed Sync IPs', [IPAddress()],
|
||||
description="""
|
||||
List of IP-addresses of other servers that are allowed to sync with
|
||||
this server.<br />
|
||||
this server.
|
||||
Example: <code>10.0.0.1</code>
|
||||
""")
|
||||
|
||||
def save(self):
|
||||
super(SyncPoolForm, self).save()
|
||||
if is_daemon_running():
|
||||
restart_daemon()
|
||||
invoke_rc_d('ykval-queue', 'restart')
|
||||
|
||||
|
||||
class KSMForm(ConfigForm):
|
||||
@ -214,11 +193,11 @@ class KSMForm(ConfigForm):
|
||||
attrs = {'ksm_urls': {'rows': 5, 'class': 'input-xxlarge'}}
|
||||
|
||||
ksm_urls = ListField(
|
||||
'KSM URLs', [URL()],
|
||||
'KSM URLs', [URL(require_tld=False)],
|
||||
description="""
|
||||
List of URLs to KSMs.<br />
|
||||
The URLs must be fully qualified, i.e., contain the OTP itself.<br />
|
||||
Example: <code>http://example.com/wsapi/decrypt?otp=$otp</code><br />
|
||||
List of URLs to KSMs.
|
||||
The URLs must be fully qualified, i.e., contain the OTP itself.
|
||||
Example: <code>http://example.com/wsapi/decrypt?otp=$otp</code>
|
||||
More advanced OTP to KSM mapping is possible by manually editing the
|
||||
configuration file.
|
||||
""")
|
||||
@ -230,16 +209,37 @@ class YubikeyVal(App):
|
||||
|
||||
YubiKey OTP validation server
|
||||
"""
|
||||
sections = ['general', 'clients', 'database', 'synchronization', 'ksms',
|
||||
'advanced']
|
||||
|
||||
name = 'val'
|
||||
sections = ['general', 'database', 'synchronization', 'ksms', 'advanced']
|
||||
@property
|
||||
def disabled(self):
|
||||
return not os.path.isfile(YKVAL_CONFIG_FILE)
|
||||
|
||||
@property
|
||||
def dash_panels(self):
|
||||
if not is_daemon_running():
|
||||
ykval_config.read()
|
||||
if len(ykval_config['sync_pool']) > 0:
|
||||
yield panel('YubiKey Validation Server',
|
||||
'The sync daemon is NOT running, '
|
||||
'though the sync pool is not empty!',
|
||||
'/%s/synchronization' % self.name,
|
||||
'danger'
|
||||
)
|
||||
|
||||
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
|
||||
@ -249,9 +249,6 @@ class YubikeyVal(App):
|
||||
return self.render_forms(request, [dbform])
|
||||
|
||||
def synchronization(self, request):
|
||||
"""
|
||||
Synchronization
|
||||
"""
|
||||
return self.render_forms(request, [DaemonForm(), SyncPoolForm()],
|
||||
template='val/synchronization',
|
||||
daemon_running=is_daemon_running())
|
||||
@ -259,11 +256,11 @@ class YubikeyVal(App):
|
||||
def daemon(self, request):
|
||||
if request.params['daemon'] == 'toggle':
|
||||
if is_daemon_running():
|
||||
invoke_rc_d('stop')
|
||||
invoke_rc_d('ykval-queue', 'stop')
|
||||
else:
|
||||
invoke_rc_d('start')
|
||||
invoke_rc_d('ykval-queue', 'start')
|
||||
else:
|
||||
restart_daemon()
|
||||
invoke_rc_d('ykval-queue', 'restart')
|
||||
|
||||
return self.redirect('/%s/synchronization' % self.name)
|
||||
|
||||
@ -274,14 +271,55 @@ class YubikeyVal(App):
|
||||
return self.render_forms(request, [KSMForm()])
|
||||
|
||||
def advanced(self, request):
|
||||
"""
|
||||
Advanced
|
||||
"""
|
||||
return self.render_forms(request, [
|
||||
FileForm('/etc/yubico/val/ykval-config.php', 'Configuration')
|
||||
])
|
||||
FileForm(YKVAL_CONFIG_FILE, 'Configuration', lang='php')
|
||||
], scripts=['editor'])
|
||||
|
||||
#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', 'Enabled', 'API Key']
|
||||
template = 'val/client_list'
|
||||
selectable = False
|
||||
|
||||
def _size(self):
|
||||
status, output = run('ykval-export-clients | wc -l')
|
||||
return int(output) if status == 0 else 0
|
||||
|
||||
def _get(self, offset=0, limit=None):
|
||||
cmd = 'ykval-export-clients'
|
||||
if offset > 0:
|
||||
cmd += '| tail -n+%d' % (offset + 1)
|
||||
if limit:
|
||||
cmd += '| head -n %d' % limit
|
||||
|
||||
status, output = run(cmd)
|
||||
if status != 0:
|
||||
return []
|
||||
|
||||
return [{
|
||||
'id': parts[0],
|
||||
'label': '%s - %s' % (parts[0], parts[3]),
|
||||
'Client ID': parts[0],
|
||||
'Enabled': parts[1] != '0',
|
||||
'API Key': parts[3]
|
||||
} for parts in [line.split(',') for line in output.splitlines()]]
|
||||
|
||||
def create(self, request):
|
||||
status, output = run('ykval-gen-clients --urandom')
|
||||
if status == 0:
|
||||
parts = [x.strip() for x in output.split(',')]
|
||||
return render('val/client_created', client_id=parts[0],
|
||||
api_key=parts[1])
|
||||
resp = self.list()
|
||||
resp.data['alerts'] = [
|
||||
{'type': 'error', 'title': 'Error generating client:',
|
||||
'message': 'Command exited with status: %d' % status}]
|
||||
return resp
|
||||
|
||||
app = YubikeyVal()
|
||||
|
@ -29,6 +29,8 @@ import sys
|
||||
import os
|
||||
import imp
|
||||
import errno
|
||||
import logging
|
||||
import logging.config
|
||||
from yubiadmin import default_settings
|
||||
|
||||
__all__ = [
|
||||
@ -37,9 +39,11 @@ __all__ = [
|
||||
|
||||
SETTINGS_FILE = os.getenv('YUBIADMIN_SETTINGS',
|
||||
'/etc/yubico/admin/yubiadmin.conf')
|
||||
LOG_CONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(SETTINGS_FILE)),
|
||||
'logging.conf')
|
||||
|
||||
VALUES = {
|
||||
#Web interface
|
||||
# Web interface
|
||||
'USERNAME': 'user',
|
||||
'PASSWORD': 'pass',
|
||||
'INTERFACE': 'iface',
|
||||
@ -49,10 +53,8 @@ VALUES = {
|
||||
|
||||
def parse(conf, settings={}):
|
||||
for confkey, settingskey in VALUES.items():
|
||||
try:
|
||||
settings[settingskey] = conf.__getattribute__(confkey)
|
||||
except AttributeError:
|
||||
pass
|
||||
if hasattr(conf, confkey):
|
||||
settings[settingskey] = getattr(conf, confkey)
|
||||
return settings
|
||||
|
||||
|
||||
@ -63,8 +65,16 @@ try:
|
||||
sys.dont_write_bytecode = True
|
||||
user_settings = imp.load_source('user_settings', SETTINGS_FILE)
|
||||
settings = parse(user_settings, settings)
|
||||
except IOError, e:
|
||||
except IOError as e:
|
||||
if not e.errno in [errno.ENOENT, errno.EACCES]:
|
||||
raise e
|
||||
finally:
|
||||
sys.dont_write_bytecode = dont_write_bytecode
|
||||
|
||||
# Set up logging
|
||||
try:
|
||||
logging.config.fileConfig(LOG_CONFIG_FILE)
|
||||
except:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
log = logging.getLogger(__name__)
|
||||
log.exception("Unable to configure logging. Logging to console.")
|
||||
|
@ -27,66 +27,63 @@
|
||||
|
||||
from webob import exc
|
||||
from webob.dec import wsgify
|
||||
from collections import OrderedDict
|
||||
from yubiadmin.util.app import render
|
||||
from yubiadmin.apps import apps
|
||||
|
||||
|
||||
def inspect_app(app):
|
||||
cls = app.__class__
|
||||
name = cls.name
|
||||
doc = app.__doc__.strip()
|
||||
title, desc = doc.split('\n', 1)
|
||||
desc = desc.strip()
|
||||
sections = [{
|
||||
'name': section,
|
||||
'title': app.__getattribute__(section).__doc__.strip(),
|
||||
'advanced': hasattr(app.__getattribute__(section), 'advanced')
|
||||
} for section in cls.sections]
|
||||
if app.__doc__:
|
||||
doc = app.__doc__.strip()
|
||||
if '\n' in doc:
|
||||
title, desc = doc.split('\n', 1)
|
||||
desc = desc.strip()
|
||||
else:
|
||||
title = desc = doc
|
||||
else:
|
||||
title = desc = app.__class__.__name__
|
||||
|
||||
return {
|
||||
'name': name,
|
||||
'name': app.name,
|
||||
'title': title,
|
||||
'description': desc,
|
||||
'sections': sections,
|
||||
'disabled': bool(getattr(app, 'disabled', False)),
|
||||
'hidden': bool(getattr(app, 'hidden', False))
|
||||
}
|
||||
|
||||
|
||||
class YubiAdmin(object):
|
||||
def __init__(self):
|
||||
self.apps = {}
|
||||
for app in apps:
|
||||
app_data = inspect_app(app)
|
||||
self.apps[app_data['name']] = (app, app_data)
|
||||
self.modules = [data for (_, data) in self.apps.values()]
|
||||
|
||||
@wsgify
|
||||
def __call__(self, request):
|
||||
module_name = request.path_info_pop()
|
||||
section_name = request.path_info_pop()
|
||||
|
||||
apps_data = OrderedDict()
|
||||
for app in apps:
|
||||
app_data = inspect_app(app)
|
||||
apps_data[app_data['name']] = (app, app_data)
|
||||
modules = [data for (_, data) in apps_data.values()]
|
||||
|
||||
if not module_name:
|
||||
return render('index', modules=self.modules)
|
||||
module_name = 'dashboard'
|
||||
|
||||
if not module_name in self.apps:
|
||||
if not module_name in apps_data:
|
||||
raise exc.HTTPNotFound
|
||||
|
||||
app, module = self.apps[module_name]
|
||||
if not section_name:
|
||||
section_name = module['sections'][0]['name']
|
||||
app, module = apps_data[module_name]
|
||||
|
||||
if not hasattr(app, section_name):
|
||||
if module['disabled']:
|
||||
raise exc.HTTPNotFound
|
||||
|
||||
section = next((section for section in module['sections']
|
||||
if section['name'] == section_name), None)
|
||||
|
||||
return render(
|
||||
'app_base',
|
||||
modules=self.modules,
|
||||
request.environ['yubiadmin.response'] = render(
|
||||
'content',
|
||||
modules=modules,
|
||||
module=module,
|
||||
section=section,
|
||||
title='YubiAdmin - %s - %s' % (module_name, section_name),
|
||||
page=app.__getattribute__(section_name)(request)
|
||||
title='YubiAdmin - %s' % module_name
|
||||
)
|
||||
|
||||
resp = app(request)
|
||||
if not resp:
|
||||
return request.environ['yubiadmin.response']
|
||||
return resp
|
||||
|
||||
application = YubiAdmin()
|
||||
|
@ -11,6 +11,30 @@ label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.pager li.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
caption {
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
margin-bottom: 20px;
|
||||
font-size: 21px;
|
||||
line-height: 40px;
|
||||
color: #333333;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #e5e5e5;
|
||||
}
|
||||
|
||||
.input>span.help-block {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
span.message {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #8bc53f;
|
||||
}
|
||||
@ -21,6 +45,26 @@ a:hover {
|
||||
background-color: #8bc53f;
|
||||
}
|
||||
|
||||
a.disabled, a.disabled:hover {
|
||||
color: #999999;
|
||||
}
|
||||
|
||||
.alert > a {
|
||||
color: #c09853;
|
||||
}
|
||||
|
||||
.alert-info > a {
|
||||
color: #3a87ad;
|
||||
}
|
||||
|
||||
.alert-danger > a {
|
||||
color: #b94a48;
|
||||
}
|
||||
|
||||
.alert-success > a {
|
||||
color: #468847;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #9edb48;
|
||||
color: #ffffff;
|
||||
|
11
yubiadmin/static/js/ace/ace.js
Normal file
11
yubiadmin/static/js/ace/ace.js
Normal file
File diff suppressed because one or more lines are too long
1
yubiadmin/static/js/ace/mode-ini.js
Normal file
1
yubiadmin/static/js/ace/mode-ini.js
Normal file
@ -0,0 +1 @@
|
||||
define("ace/mode/ini",["require","exports","module","ace/lib/oop","ace/mode/text","ace/tokenizer","ace/mode/ini_highlight_rules","ace/mode/folding/cstyle"],function(e,t,n){var r=e("../lib/oop"),i=e("./text").Mode,s=e("../tokenizer").Tokenizer,o=e("./ini_highlight_rules").IniHighlightRules,u=e("./folding/cstyle").FoldMode,a=function(){var e=new o;this.foldingRules=new u,this.$tokenizer=new s(e.getRules())};r.inherits(a,i),function(){this.lineCommentStart=";",this.blockComment={start:"/*",end:"*/"}}.call(a.prototype),t.Mode=a}),define("ace/mode/ini_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){this.$rules={start:[{token:"punctuation.definition.comment.ini",regex:"#.*",push_:[{token:"comment.line.number-sign.ini",regex:"$",next:"pop"},{defaultToken:"comment.line.number-sign.ini"}]},{token:"punctuation.definition.comment.ini",regex:";.*",push_:[{token:"comment.line.semicolon.ini",regex:"$",next:"pop"},{defaultToken:"comment.line.semicolon.ini"}]},{token:["keyword.other.definition.ini","text","punctuation.separator.key-value.ini"],regex:"\\b([a-zA-Z0-9_.-]+)\\b(\\s*)(=)"},{token:["punctuation.definition.entity.ini","constant.section.group-title.ini","punctuation.definition.entity.ini"],regex:"^(\\[)(.*?)(\\])"},{token:"punctuation.definition.string.begin.ini",regex:"'",push:[{token:"punctuation.definition.string.end.ini",regex:"'",next:"pop"},{token:"constant.character.escape.ini",regex:"\\\\."},{defaultToken:"string.quoted.single.ini"}]},{token:"punctuation.definition.string.begin.ini",regex:'"',push:[{token:"punctuation.definition.string.end.ini",regex:'"',next:"pop"},{defaultToken:"string.quoted.double.ini"}]}]},this.normalizeRules()};s.metaData={fileTypes:["ini","conf"],keyEquivalent:"^~I",name:"Ini",scopeName:"source.ini"},r.inherits(s,i),t.IniHighlightRules=s}),define("ace/mode/folding/cstyle",["require","exports","module","ace/lib/oop","ace/range","ace/mode/folding/fold_mode"],function(e,t,n){var r=e("../../lib/oop"),i=e("../../range").Range,s=e("./fold_mode").FoldMode,o=t.FoldMode=function(e){e&&(this.foldingStartMarker=new RegExp(this.foldingStartMarker.source.replace(/\|[^|]*?$/,"|"+e.start)),this.foldingStopMarker=new RegExp(this.foldingStopMarker.source.replace(/\|[^|]*?$/,"|"+e.end)))};r.inherits(o,s),function(){this.foldingStartMarker=/(\{|\[)[^\}\]]*$|^\s*(\/\*)/,this.foldingStopMarker=/^[^\[\{]*(\}|\])|^[\s\*]*(\*\/)/,this.getFoldWidgetRange=function(e,t,n){var r=e.getLine(n),i=r.match(this.foldingStartMarker);if(i){var s=i.index;return i[1]?this.openingBracketBlock(e,i[1],n,s):e.getCommentFoldRange(n,s+i[0].length,1)}if(t!=="markbeginend")return;var i=r.match(this.foldingStopMarker);if(i){var s=i.index+i[0].length;return i[1]?this.closingBracketBlock(e,i[1],n,s):e.getCommentFoldRange(n,s,-1)}}}.call(o.prototype)})
|
1
yubiadmin/static/js/ace/mode-php.js
Normal file
1
yubiadmin/static/js/ace/mode-php.js
Normal file
File diff suppressed because one or more lines are too long
1
yubiadmin/static/js/ace/mode-python.js
Normal file
1
yubiadmin/static/js/ace/mode-python.js
Normal file
@ -0,0 +1 @@
|
||||
define("ace/mode/python",["require","exports","module","ace/lib/oop","ace/mode/text","ace/tokenizer","ace/mode/python_highlight_rules","ace/mode/folding/pythonic","ace/range"],function(e,t,n){var r=e("../lib/oop"),i=e("./text").Mode,s=e("../tokenizer").Tokenizer,o=e("./python_highlight_rules").PythonHighlightRules,u=e("./folding/pythonic").FoldMode,a=e("../range").Range,f=function(){this.$tokenizer=new s((new o).getRules()),this.foldingRules=new u("\\:")};r.inherits(f,i),function(){this.lineCommentStart="#",this.getNextLineIndent=function(e,t,n){var r=this.$getIndent(t),i=this.$tokenizer.getLineTokens(t,e),s=i.tokens;if(s.length&&s[s.length-1].type=="comment")return r;if(e=="start"){var o=t.match(/^.*[\{\(\[\:]\s*$/);o&&(r+=n)}return r};var e={pass:1,"return":1,raise:1,"break":1,"continue":1};this.checkOutdent=function(t,n,r){if(r!=="\r\n"&&r!=="\r"&&r!=="\n")return!1;var i=this.$tokenizer.getLineTokens(n.trim(),t).tokens;if(!i)return!1;do var s=i.pop();while(s&&(s.type=="comment"||s.type=="text"&&s.value.match(/^\s+$/)));return s?s.type=="keyword"&&e[s.value]:!1},this.autoOutdent=function(e,t,n){n+=1;var r=this.$getIndent(t.getLine(n)),i=t.getTabString();r.slice(-i.length)==i&&t.remove(new a(n,r.length-i.length,n,r.length))}}.call(f.prototype),t.Mode=f}),define("ace/mode/python_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"],function(e,t,n){var r=e("../lib/oop"),i=e("./text_highlight_rules").TextHighlightRules,s=function(){var e="and|as|assert|break|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|not|or|pass|print|raise|return|try|while|with|yield",t="True|False|None|NotImplemented|Ellipsis|__debug__",n="abs|divmod|input|open|staticmethod|all|enumerate|int|ord|str|any|eval|isinstance|pow|sum|basestring|execfile|issubclass|print|super|binfile|iter|property|tuple|bool|filter|len|range|type|bytearray|float|list|raw_input|unichr|callable|format|locals|reduce|unicode|chr|frozenset|long|reload|vars|classmethod|getattr|map|repr|xrange|cmp|globals|max|reversed|zip|compile|hasattr|memoryview|round|__import__|complex|hash|min|set|apply|delattr|help|next|setattr|buffer|dict|hex|object|slice|coerce|dir|id|oct|sorted|intern",r=this.createKeywordMapper({"invalid.deprecated":"debugger","support.function":n,"constant.language":t,keyword:e},"identifier"),i="(?:r|u|ur|R|U|UR|Ur|uR)?",s="(?:(?:[1-9]\\d*)|(?:0))",o="(?:0[oO]?[0-7]+)",u="(?:0[xX][\\dA-Fa-f]+)",a="(?:0[bB][01]+)",f="(?:"+s+"|"+o+"|"+u+"|"+a+")",l="(?:[eE][+-]?\\d+)",c="(?:\\.\\d+)",h="(?:\\d+)",p="(?:(?:"+h+"?"+c+")|(?:"+h+"\\.))",d="(?:(?:"+p+"|"+h+")"+l+")",v="(?:"+d+"|"+p+")",m="\\\\(x[0-9A-Fa-f]{2}|[0-7]{3}|[\\\\abfnrtv'\"]|U[0-9A-Fa-f]{8}|u[0-9A-Fa-f]{4})";this.$rules={start:[{token:"comment",regex:"#.*$"},{token:"string",regex:i+'"{3}',next:"qqstring3"},{token:"string",regex:i+'"(?=.)',next:"qqstring"},{token:"string",regex:i+"'{3}",next:"qstring3"},{token:"string",regex:i+"'(?=.)",next:"qstring"},{token:"constant.numeric",regex:"(?:"+v+"|\\d+)[jJ]\\b"},{token:"constant.numeric",regex:v},{token:"constant.numeric",regex:f+"[lL]\\b"},{token:"constant.numeric",regex:f+"\\b"},{token:r,regex:"[a-zA-Z_$][a-zA-Z0-9_$]*\\b"},{token:"keyword.operator",regex:"\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|%|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|="},{token:"paren.lparen",regex:"[\\[\\(\\{]"},{token:"paren.rparen",regex:"[\\]\\)\\}]"},{token:"text",regex:"\\s+"}],qqstring3:[{token:"constant.language.escape",regex:m},{token:"string",regex:'"{3}',next:"start"},{defaultToken:"string"}],qstring3:[{token:"constant.language.escape",regex:m},{token:"string",regex:"'{3}",next:"start"},{defaultToken:"string"}],qqstring:[{token:"constant.language.escape",regex:m},{token:"string",regex:"\\\\$",next:"qqstring"},{token:"string",regex:'"|$',next:"start"},{defaultToken:"string"}],qstring:[{token:"constant.language.escape",regex:m},{token:"string",regex:"\\\\$",next:"qstring"},{token:"string",regex:"'|$",next:"start"},{defaultToken:"string"}]}};r.inherits(s,i),t.PythonHighlightRules=s}),define("ace/mode/folding/pythonic",["require","exports","module","ace/lib/oop","ace/mode/folding/fold_mode"],function(e,t,n){var r=e("../../lib/oop"),i=e("./fold_mode").FoldMode,s=t.FoldMode=function(e){this.foldingStartMarker=new RegExp("([\\[{])(?:\\s*)$|("+e+")(?:\\s*)(?:#.*)?$")};r.inherits(s,i),function(){this.getFoldWidgetRange=function(e,t,n){var r=e.getLine(n),i=r.match(this.foldingStartMarker);if(i)return i[1]?this.openingBracketBlock(e,i[1],n,i.index):i[2]?this.indentationBlock(e,n,i.index+i[2].length):this.indentationBlock(e,n)}}.call(s.prototype)})
|
1
yubiadmin/static/js/ace/theme-chrome.js
Normal file
1
yubiadmin/static/js/ace/theme-chrome.js
Normal file
@ -0,0 +1 @@
|
||||
define("ace/theme/chrome",["require","exports","module","ace/lib/dom"],function(e,t,n){t.isDark=!1,t.cssClass="ace-chrome",t.cssText='.ace-chrome .ace_gutter {background: #ebebeb;color: #333;overflow : hidden;}.ace-chrome .ace_print-margin {width: 1px;background: #e8e8e8;}.ace-chrome .ace_scroller {background-color: #FFFFFF;}.ace-chrome .ace_cursor {border-left: 2px solid black;}.ace-chrome .ace_overwrite-cursors .ace_cursor {border-left: 0px;border-bottom: 1px solid black;}.ace-chrome .ace_invisible {color: rgb(191, 191, 191);}.ace-chrome .ace_constant.ace_buildin {color: rgb(88, 72, 246);}.ace-chrome .ace_constant.ace_language {color: rgb(88, 92, 246);}.ace-chrome .ace_constant.ace_library {color: rgb(6, 150, 14);}.ace-chrome .ace_invalid {background-color: rgb(153, 0, 0);color: white;}.ace-chrome .ace_fold {}.ace-chrome .ace_support.ace_function {color: rgb(60, 76, 114);}.ace-chrome .ace_support.ace_constant {color: rgb(6, 150, 14);}.ace-chrome .ace_support.ace_type,.ace-chrome .ace_support.ace_class.ace-chrome .ace_support.ace_other {color: rgb(109, 121, 222);}.ace-chrome .ace_variable.ace_parameter {font-style:italic;color:#FD971F;}.ace-chrome .ace_keyword.ace_operator {color: rgb(104, 118, 135);}.ace-chrome .ace_comment {color: #236e24;}.ace-chrome .ace_comment.ace_doc {color: #236e24;}.ace-chrome .ace_comment.ace_doc.ace_tag {color: #236e24;}.ace-chrome .ace_constant.ace_numeric {color: rgb(0, 0, 205);}.ace-chrome .ace_variable {color: rgb(49, 132, 149);}.ace-chrome .ace_xml-pe {color: rgb(104, 104, 91);}.ace-chrome .ace_entity.ace_name.ace_function {color: #0000A2;}.ace-chrome .ace_markup.ace_heading {color: rgb(12, 7, 255);}.ace-chrome .ace_markup.ace_list {color:rgb(185, 6, 144);}.ace-chrome .ace_marker-layer .ace_selection {background: rgb(181, 213, 255);}.ace-chrome .ace_marker-layer .ace_step {background: rgb(252, 255, 0);}.ace-chrome .ace_marker-layer .ace_stack {background: rgb(164, 229, 101);}.ace-chrome .ace_marker-layer .ace_bracket {margin: -1px 0 0 -1px;border: 1px solid rgb(192, 192, 192);}.ace-chrome .ace_marker-layer .ace_active-line {background: rgba(0, 0, 0, 0.07);}.ace-chrome .ace_gutter-active-line {background-color : #dcdcdc;}.ace-chrome .ace_marker-layer .ace_selected-word {background: rgb(250, 250, 255);border: 1px solid rgb(200, 200, 250);}.ace-chrome .ace_storage,.ace-chrome .ace_keyword,.ace-chrome .ace_meta.ace_tag {color: rgb(147, 15, 128);}.ace-chrome .ace_string.ace_regex {color: rgb(255, 0, 0)}.ace-chrome .ace_string {color: #1A1AA6;}.ace-chrome .ace_entity.ace_other.ace_attribute-name {color: #994409;}.ace-chrome .ace_indent-guide {background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;}';var r=e("../lib/dom");r.importCssString(t.cssText,t.cssClass)})
|
1
yubiadmin/static/js/ace/worker-php.js
Normal file
1
yubiadmin/static/js/ace/worker-php.js
Normal file
File diff suppressed because one or more lines are too long
17
yubiadmin/static/js/auth.js
Normal file
17
yubiadmin/static/js/auth.js
Normal file
@ -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');
|
||||
}
|
||||
});
|
||||
});
|
31
yubiadmin/static/js/editor.js
Normal file
31
yubiadmin/static/js/editor.js
Normal file
@ -0,0 +1,31 @@
|
||||
$(document).ready(function() {
|
||||
$.ajaxSetup({cache: true});
|
||||
$.getScript("/js/ace/ace.js", function(data, textStatus, jqxhr) {
|
||||
ace.config.set("modePath", "/js/ace/");
|
||||
ace.config.set("workerPath", "/js/ace/");
|
||||
ace.config.set("themePath", "/js/ace/");
|
||||
$('textarea.editor').each(function() {
|
||||
var textarea = $(this);
|
||||
var text = textarea.text();
|
||||
var div = $('<div />');
|
||||
div.width(textarea.width());
|
||||
div.height(textarea.height());
|
||||
div.text(text);
|
||||
var editor = ace.edit(div.get(0));
|
||||
editor.setTheme('ace/theme/chrome');
|
||||
textarea.closest('form').bind('reset', function() {
|
||||
editor.setValue(text);
|
||||
editor.gotoLine(0);
|
||||
});
|
||||
var mode = textarea.attr('ace-mode');
|
||||
if(mode) {
|
||||
editor.getSession().setMode('ace/mode/'+mode);
|
||||
}
|
||||
textarea.after(div);
|
||||
textarea.hide();
|
||||
editor.getSession().on('change', function(e) {
|
||||
textarea.text(editor.getValue());
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1 +1,15 @@
|
||||
|
||||
$(document).ready(function() {
|
||||
$('body').on('click', '.selectable-text', function() {
|
||||
if (document.body.createTextRange) {
|
||||
var range = document.body.createTextRange();
|
||||
range.moveToElementText(this);
|
||||
range.select();
|
||||
} else if (window.getSelection) {
|
||||
var selection = window.getSelection();
|
||||
var range = document.createRange();
|
||||
range.selectNodeContents(this);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
19
yubiadmin/static/js/table.js
Normal file
19
yubiadmin/static/js/table.js
Normal file
@ -0,0 +1,19 @@
|
||||
$(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) {
|
||||
$('#delete_btn').removeAttr('disabled');
|
||||
} else {
|
||||
$('#delete_btn').attr('disabled', 'disabled');
|
||||
}
|
||||
});
|
||||
|
||||
if($('tbody :checkbox:checked').length > 0) {
|
||||
$('#delete_btn').removeAttr('disabled');
|
||||
}
|
||||
});
|
23
yubiadmin/templates/admin/general.html
Normal file
23
yubiadmin/templates/admin/general.html
Normal file
@ -0,0 +1,23 @@
|
||||
{% from 'form.html' import render_form %}
|
||||
|
||||
<div>
|
||||
<legend>YubiAdmin Server</legend>
|
||||
<span class="help-block">
|
||||
For any changes to the settings below to take place, the server will need to be restarted.<br /><br />
|
||||
<span class="label label-warning">WARNING</span> <strong>These settings may change the way you access the server, it is possible to lock yourself out!</strong>
|
||||
</span>
|
||||
<br />
|
||||
<form action="restart" method="post" onsubmit="return restart_submit();">
|
||||
<button id="restart" class="btn btn-danger" data-loading-text="Restarting...">Restart server</button>
|
||||
</form>
|
||||
<script>
|
||||
function restart_submit(e) {
|
||||
$.get('restart?now');
|
||||
setTimeout('location.reload();', 1000);
|
||||
$('#restart').button('loading');
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
|
||||
{{ render_form(fieldsets, target) }}
|
@ -1,26 +1,30 @@
|
||||
{% extends "content.html" %}
|
||||
|
||||
{% macro render_section(sect) %}
|
||||
<li {% if sect.name == section.name %}class="active"{% endif %}>
|
||||
<a href="/{{ module.name }}/{{ sect.name }}">{{ sect.title }}</a>
|
||||
<li {% if sect.active %}class="active"{% endif %}>
|
||||
<a href="/{{ name }}/{{ sect.name }}">{{ sect.title }}</a>
|
||||
</li>
|
||||
{% endmacro %}
|
||||
|
||||
{% block content %}
|
||||
<div class="navbar">
|
||||
<div class="navbar-inner">
|
||||
<ul class="nav">
|
||||
{% for sect in module.sections if not sect.advanced %}
|
||||
{% for sect in sections if not sect.advanced %}
|
||||
{{ render_section(sect) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<ul class="nav pull-right">
|
||||
{% for sect in module.sections if sect.advanced %}
|
||||
{% for sect in sections if sect.advanced %}
|
||||
{{ render_section(sect) }}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% for alert in alerts %}
|
||||
<div class="alert alert-{{ alert.type }}">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
<strong>{{ alert.title }}</strong>
|
||||
<span class="message">{{ alert.message }}</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{{ page }}
|
||||
{% endblock %}
|
||||
|
11
yubiadmin/templates/auth/general.html
Normal file
11
yubiadmin/templates/auth/general.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% from 'form.html' import render_form %}
|
||||
|
||||
<div>
|
||||
<legend>Web Server</legend>
|
||||
<span class="help-block">
|
||||
For any changes to the settings below to take place, the web server will need to be reloaded.<br /><br />
|
||||
</span>
|
||||
<a href="reload" class="btn btn-danger">Reload web server</a>
|
||||
</div>
|
||||
|
||||
{{ render_form(fieldsets, target) }}
|
9
yubiadmin/templates/auth/list.html
Normal file
9
yubiadmin/templates/auth/list.html
Normal file
@ -0,0 +1,9 @@
|
||||
{% from 'table.html' import table %}
|
||||
|
||||
<form action="/auth/users/delete" method="post">
|
||||
|
||||
{{ table(cols, items, caption, next, prev, shown, total, item_name) }}
|
||||
|
||||
<input id="delete_btn" type="submit" class="btn btn-danger" value="Delete selected" />
|
||||
<a href="/auth/users/create" class="btn btn-primary pull-right">Create new user</a>
|
||||
</form>
|
55
yubiadmin/templates/auth/user.html
Normal file
55
yubiadmin/templates/auth/user.html
Normal file
@ -0,0 +1,55 @@
|
||||
{% import 'form.html' as forms %}
|
||||
|
||||
<legend>User: {{ user.name }}</legend>
|
||||
|
||||
<div class="row-fluid">
|
||||
<div class="span6">
|
||||
<table class="table table-condensed table-striped">
|
||||
<caption>YubiKeys</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 80%">Prefix</th>
|
||||
<th style="width: 20%">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for prefix in user.yubikeys %}
|
||||
<tr>
|
||||
<td>{{ prefix }}</td>
|
||||
<td style="text-align: right;">
|
||||
<form method="post" style="margin: 0;">
|
||||
<input type="hidden" name="unassign" value="{{prefix}}" />
|
||||
<input type="submit" class="btn btn-danger" value="Remove" />
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% set field = fieldsets[1]['assign'] %}
|
||||
{{ field.label }}
|
||||
<form class="form-inline" method="post">
|
||||
{{ field(class='span10') }}
|
||||
<input class="btn btn-primary" type="submit" value="Assign" />
|
||||
{{ forms.form_field_errors(field) }}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="span6">
|
||||
{% if user.attributes %}
|
||||
<table class="table table-condensed table-striped">
|
||||
<caption>Attributes</caption>
|
||||
<thead>
|
||||
<tr><th>Attribute</th><th>Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key in user.attributes %}
|
||||
<tr><td>{{ key }}</td><td>{{ user.attributes[key] }}</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ forms.render_form(fieldsets[0:1]) }}
|
@ -29,15 +29,18 @@
|
||||
|
||||
<!-- This code is taken from http://twitter.github.com/bootstrap/examples/hero.html -->
|
||||
|
||||
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||
<script>window.jQuery || document.write('<script src="/js/vendor/jquery-1.9.1.min.js"><\/script>')</script>
|
||||
|
||||
{% block body %}
|
||||
<p>Hello world!</p>
|
||||
{% endblock %}
|
||||
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||
<script>window.jQuery || document.write('<script src="/js/vendor/jquery-1.9.1.min.js"><\/script>')</script>
|
||||
|
||||
<script src="/js/vendor/bootstrap.min.js"></script>
|
||||
|
||||
<script src="/js/main.js"></script>
|
||||
{% for script in scripts %}
|
||||
<script src="/js/{{script}}.js"></script>
|
||||
{% endfor %}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -4,17 +4,8 @@
|
||||
<div class="container">
|
||||
|
||||
<div class="row">
|
||||
<div class="span3">
|
||||
<h1>YubiADMIN</h1>
|
||||
</div>
|
||||
<div class="span9">
|
||||
{% if alert %}
|
||||
<div class="alert alert-{{ alert.type }}">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
<strong>{{ alert.title }}</strong>
|
||||
{{ alert.message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="span12">
|
||||
<h1><a href="/">YubiADMIN</a></h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -22,17 +13,29 @@
|
||||
<div class="span3">
|
||||
<div class="well">
|
||||
<ul class="nav nav-list">
|
||||
<li {% if module is defined and module.name == 'dashboard' %}class="active"{% endif %}><a href="/">Dashboard</a></li>
|
||||
<li class="nav-header">Modules</li>
|
||||
{% for mod in modules %}
|
||||
{% for mod in modules if not mod.disabled and not mod.hidden %}
|
||||
<li {% if module is defined and mod.name == module.name %}class="active"{% endif %} >
|
||||
<a href="/{{ mod.name }}">{{ mod.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
{% for mod in modules if mod.disabled and not mod.hidden %}
|
||||
{% if loop.first %}
|
||||
<li class="nav-header">Not installed</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a class="disabled">{{ mod.title }}</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="span9">
|
||||
{% block content %}
|
||||
{{ content }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</div>
|
||||
|
46
yubiadmin/templates/dashboard.html
Normal file
46
yubiadmin/templates/dashboard.html
Normal file
@ -0,0 +1,46 @@
|
||||
{% macro render_panel(panel) %}
|
||||
{% if panel.level == 'danger' %}
|
||||
{% set icon = "icon-exclamation-sign" %}
|
||||
{% elif panel.level == 'info' %}
|
||||
{% set icon = "icon-info-sign" %}
|
||||
{% elif panel.level == 'success' %}
|
||||
{% set icon = "icon-ok-sign" %}
|
||||
{% else %}
|
||||
{% set icon = "icon-question-sign" %}
|
||||
{% endif %}
|
||||
<div class="alert{% if panel.level %} alert-{{ panel.level }}{% endif %}">
|
||||
<i class="{{ icon }}"></i>
|
||||
{% if panel.link %}
|
||||
<a href="{{ panel.link }}"><strong>{{ panel.title }}</strong></a>
|
||||
{% else %}
|
||||
<strong>{{ panel.title }}</strong>
|
||||
{% endif %}
|
||||
<br />
|
||||
{{ panel.content }}
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
<h2>Dashboard</h2>
|
||||
<div class="row-fluid">
|
||||
<div class="span4">
|
||||
{% for panel in panels %}
|
||||
{% if loop.index0 % 3 == 0 %}
|
||||
{{ render_panel(panel) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="span4">
|
||||
{% for panel in panels %}
|
||||
{% if loop.index0 % 3 == 1 %}
|
||||
{{ render_panel(panel) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="span4">
|
||||
{% for panel in panels %}
|
||||
{% if loop.index0 % 3 == 2 %}
|
||||
{{ render_panel(panel) }}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
@ -7,7 +7,7 @@
|
||||
|
||||
{%- macro form_field_description(field) -%}
|
||||
{% if field.description %}
|
||||
<span class="help-block">{{ field.description }}</span>
|
||||
<span class="help-block">{{ field.description|trim }}</span>
|
||||
{% endif %}
|
||||
{%- endmacro -%}
|
||||
|
||||
@ -22,12 +22,12 @@
|
||||
|
||||
{%- macro form_field_boolean(field) -%}
|
||||
<div class="input">
|
||||
<label>
|
||||
<label class="checkbox">
|
||||
{{ field(**kwargs) }}
|
||||
<span>{{ field.label.text }}</span>
|
||||
{{ form_field_description(field) }}
|
||||
{{ form_field_errors(field) }}
|
||||
</label>
|
||||
{{ form_field_description(field) }}
|
||||
{{ form_field_errors(field) }}
|
||||
</div>
|
||||
{%- endmacro -%}
|
||||
|
||||
@ -62,7 +62,7 @@
|
||||
<legend>{{ fieldset.legend }}</legend>
|
||||
{% endif %}
|
||||
{% if fieldset.description %}
|
||||
<p>{{ fieldset.description }}</p>
|
||||
<p>{{ fieldset.description|trim }}</p>
|
||||
{% endif %}
|
||||
{% for field in fieldset %}
|
||||
{% if field.type == 'HiddenField' %}
|
||||
@ -78,15 +78,7 @@
|
||||
</fieldset>
|
||||
{%- endmacro -%}
|
||||
|
||||
{%- macro render_form(fieldsets, action, alert=None) -%}
|
||||
{% if alert %}
|
||||
<div class="alert alert-{{ alert.type }}">
|
||||
<button type="button" class="close" data-dismiss="alert">×</button>
|
||||
<strong>{{ alert.title }}</strong>
|
||||
{{ alert.message }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{%- macro render_form(fieldsets, action) -%}
|
||||
<form action="{{ action }}" method="post">
|
||||
{% for fieldset in fieldsets %}
|
||||
{{ form_fieldset(fieldset) }}
|
||||
@ -99,4 +91,4 @@
|
||||
</form>
|
||||
{%- endmacro -%}
|
||||
|
||||
{{ render_form(fieldsets, target, alert) }}
|
||||
{{ render_form(fieldsets, target) }}
|
||||
|
10
yubiadmin/templates/freerad/client_list.html
Normal file
10
yubiadmin/templates/freerad/client_list.html
Normal file
@ -0,0 +1,10 @@
|
||||
{% from 'table.html' import table %}
|
||||
|
||||
<form action="/freerad/clients/delete" method="post">
|
||||
|
||||
{{ table(cols, items, caption, next, prev, shown, total, item_name, selectable) }}
|
||||
|
||||
<input id="delete_btn" type="submit" class="btn btn-danger" value="Delete selected" />
|
||||
|
||||
<a href="/freerad/clients/create" class="btn btn-primary pull-right">Create RADIUS client</a>
|
||||
</form>
|
31
yubiadmin/templates/freerad/general.html
Normal file
31
yubiadmin/templates/freerad/general.html
Normal file
@ -0,0 +1,31 @@
|
||||
{% from 'form.html' import form_fieldset %}
|
||||
|
||||
{% if running %}
|
||||
{% set status_cls = 'label label-success' %}
|
||||
{% set status_txt = 'Running' %}
|
||||
{% set toggle_txt = 'Stop FreeRADIUS' %}
|
||||
{% set restart_cls = 'btn' %}
|
||||
{% else %}
|
||||
{% set status_cls = 'label label-important' %}
|
||||
{% set status_txt = 'Not Running' %}
|
||||
{% set toggle_txt = 'Start FreeRADIUS' %}
|
||||
{% set restart_cls = 'btn disabled' %}
|
||||
{% set restart_attrs = 'disabled="disabled"' %}
|
||||
{% endif %}
|
||||
|
||||
<div>
|
||||
<legend>FreeRADIUS Server</legend>
|
||||
Current status: <span class="{{ status_cls }}">{{ status_txt }}</span>
|
||||
</p>
|
||||
<form action="server" method="post">
|
||||
<button name="server" value="toggle" class="btn">{{ toggle_txt }}</button>
|
||||
<button name="server" value="restart" class="{{ restart_cls }}" {{ restart_attrs }}>Restart FreeRADIUS</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
|
||||
<form action="/freerad/general" method="post">
|
||||
{{ form_fieldset(form) }}
|
||||
<input type="submit" class="btn" value="Authenticate" />
|
||||
</form>
|
||||
|
18
yubiadmin/templates/sys/general.html
Normal file
18
yubiadmin/templates/sys/general.html
Normal file
@ -0,0 +1,18 @@
|
||||
<legend>System</legend>
|
||||
{% if updates %}
|
||||
<p>There are system updates available:<br/>
|
||||
<ul>
|
||||
{% for update in updates %}
|
||||
<li>{{ update }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<a href="dist_upgrade" class="btn">Update System</a>
|
||||
</p>
|
||||
{% else %}
|
||||
<p>Your system is up to date.</p>
|
||||
<a href="update" class="btn" data-loading-text="Checking for updates..." onclick="$(this).button('loading');">Check for updates</a>
|
||||
{% endif %}
|
||||
|
||||
<legend>Restart</legend>
|
||||
<p>Restarting the system may take several minutes, during which this web interface will be unavailable.</p>
|
||||
<a href="reboot" class="btn btn-danger" data-loading-text="Restarting..." onclick="$(this).button('loading');$.get('reboot?now');return false;">Restart System</a>
|
63
yubiadmin/templates/table.html
Normal file
63
yubiadmin/templates/table.html
Normal file
@ -0,0 +1,63 @@
|
||||
{% macro header(cols, next=None, prev=None, shown=0, total=0, item_name='Items', selectable=True) %}
|
||||
<thead>
|
||||
<tr>
|
||||
{% if selectable %}
|
||||
{% set width = 95 %}
|
||||
<th style="width: 5%">
|
||||
<input type="checkbox" id="toggle_all" />
|
||||
</th>
|
||||
{% else %}
|
||||
{% set width = 100 %}
|
||||
{% endif %}
|
||||
{% for col in cols %}
|
||||
<th style="width: 15%">
|
||||
{{ col }}
|
||||
</th>
|
||||
{% endfor %}
|
||||
{% set col_len = cols|length %}
|
||||
<th style="width: {{ width - col_len * 15 }}%; text-align:right;">
|
||||
{{ item_name }} {{ shown }} of {{ total }}
|
||||
|
||||
<div class="btn-group">
|
||||
{% if prev %}
|
||||
<a class="btn btn-small" href="{{ prev }}">Prev</a>
|
||||
{% else %}
|
||||
<a class="btn btn-small disabled">Prev</a>
|
||||
{% endif %}
|
||||
{% if next %}
|
||||
<a class="btn btn-small" href="{{ next }}">Next</a>
|
||||
{% else %}
|
||||
<a class="btn btn-small disabled">Next</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro body(cols, items, selectable=True) %}
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
{% if selectable %}
|
||||
<td><input type="checkbox" name="item/{{ item.id }}"/></td>
|
||||
{% endif %}
|
||||
{% for col in cols %}
|
||||
<td{% if loop.last %} colspan="2"{% endif %}>{{ item[col] }}</td>
|
||||
{% endfor %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro table(cols, items, caption=None, next=None, prev=None, shown=0, total=0, item_name='Items', selectable=True) %}
|
||||
<table class="table table-striped table-condensed">
|
||||
{% if caption %}
|
||||
<caption>{{ caption }}</caption>
|
||||
{% endif %}
|
||||
{{ header(cols, next, prev, shown, total, item_name, selectable) }}
|
||||
{{ body(cols, items, selectable) }}
|
||||
</table>
|
||||
{% endmacro %}
|
||||
|
||||
{{ table(cols, items, caption, next, prev, shown, total, item_name, selectable) }}
|
13
yubiadmin/templates/table_delete.html
Normal file
13
yubiadmin/templates/table_delete.html
Normal file
@ -0,0 +1,13 @@
|
||||
<legend>Confirm deletion</legend>
|
||||
<p>Are you sure you wish to delete the following {{ item_name | lower }}?</p>
|
||||
<ul>
|
||||
{% for label in labels %}
|
||||
<li>{{ label }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<form action="delete_confirm" method="post">
|
||||
<input type="submit" class="btn btn-danger" value="Delete {{ item_name | lower }}" />
|
||||
<input type="hidden" name="delete" value="{{ ids }}" />
|
||||
<a href="{{ base_url }}" class="btn">Cancel</a>
|
||||
</form>
|
9
yubiadmin/templates/val/client_created.html
Normal file
9
yubiadmin/templates/val/client_created.html
Normal file
@ -0,0 +1,9 @@
|
||||
<legend>API Client created</legend>
|
||||
<p>An API Client with the following ID and API key has been created, and is ready for use.</p>
|
||||
|
||||
<dl class="dl-horizontal">
|
||||
<dt>Client ID</dt>
|
||||
<dd>{{ client_id }}</dd>
|
||||
<dt>API Key</dt>
|
||||
<dd><span class="selectable-text">{{ api_key }}</span></dd>
|
||||
</dl>
|
11
yubiadmin/templates/val/client_list.html
Normal file
11
yubiadmin/templates/val/client_list.html
Normal file
@ -0,0 +1,11 @@
|
||||
{% from 'table.html' import table %}
|
||||
|
||||
{{ table(cols, items, caption, next, prev, shown, total, item_name, selectable) }}
|
||||
|
||||
<a href="/val/clients/create" class="btn btn-primary pull-right">Generate new API client</a>
|
||||
|
||||
<script>
|
||||
$(document).ready(function() {
|
||||
$('td[colspan=2]').addClass('selectable-text');
|
||||
});
|
||||
</script>
|
@ -26,11 +26,15 @@
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
from webob import exc
|
||||
from webob import exc, Response
|
||||
from webob.dec import wsgify
|
||||
|
||||
__all__ = [
|
||||
'App',
|
||||
'CollectionApp',
|
||||
'render',
|
||||
'populate_forms',
|
||||
]
|
||||
@ -41,14 +45,42 @@ template_dir = os.path.join(base_dir, 'templates')
|
||||
env = Environment(loader=FileSystemLoader(template_dir))
|
||||
|
||||
|
||||
class TemplateBinding(object):
|
||||
def __init__(self, template, **kwargs):
|
||||
self.template = env.get_template('%s.html' % template)
|
||||
self.data = kwargs
|
||||
for (key, val) in kwargs.items():
|
||||
if isinstance(val, TemplateBinding):
|
||||
self.data.update(val.data)
|
||||
|
||||
def extend(self, sub_variable, sub_binding):
|
||||
if hasattr(sub_binding, 'data'):
|
||||
self.data.update(sub_binding.data)
|
||||
sub_binding.data = self.data
|
||||
self.data[sub_variable] = sub_binding
|
||||
|
||||
@property
|
||||
def prerendered(self):
|
||||
self._rendered = str(self)
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
if hasattr(self, '_rendered'):
|
||||
return self._rendered
|
||||
return self.template.render(self.data)
|
||||
|
||||
@wsgify
|
||||
def __call__(self, request):
|
||||
return str(self)
|
||||
|
||||
|
||||
def render(tmpl, **kwargs):
|
||||
template = env.get_template('%s.html' % tmpl)
|
||||
return template.render(**kwargs)
|
||||
return TemplateBinding(tmpl, **kwargs)
|
||||
|
||||
|
||||
def populate_forms(forms, data):
|
||||
if not data:
|
||||
for form in forms:
|
||||
for form in filter(lambda x: hasattr(x, 'load'), forms):
|
||||
form.load()
|
||||
else:
|
||||
errors = False
|
||||
@ -56,21 +88,58 @@ def populate_forms(forms, data):
|
||||
form.process(data)
|
||||
errors = not form.validate() or errors
|
||||
if not errors:
|
||||
for form in forms:
|
||||
for form in filter(lambda x: hasattr(x, 'save'), forms):
|
||||
form.save()
|
||||
|
||||
|
||||
class App(object):
|
||||
name = None
|
||||
sections = []
|
||||
priority = 50
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
self.__class__.name = sys.modules[self.__module__].__file__ \
|
||||
.split('/')[-1].rsplit('.', 1)[0]
|
||||
return self.name
|
||||
|
||||
def __call__(self, request):
|
||||
section_name = request.path_info_pop()
|
||||
|
||||
if not section_name:
|
||||
return self.redirect('/%s/%s' % (self.name, self.sections[0]))
|
||||
|
||||
if not hasattr(self, section_name):
|
||||
raise exc.HTTPNotFound
|
||||
|
||||
sections = [{
|
||||
'name': section,
|
||||
'title': (getattr(self, section).__doc__ or section.capitalize()
|
||||
).strip(),
|
||||
'active': section == section_name,
|
||||
'advanced': bool(getattr(getattr(self, section), 'advanced',
|
||||
False))
|
||||
} for section in self.sections]
|
||||
|
||||
request.environ['yubiadmin.response'].extend('content', render(
|
||||
'app_base',
|
||||
name=self.name,
|
||||
sections=sections,
|
||||
title='YubiAdmin - %s - %s' % (self.name, section_name)
|
||||
))
|
||||
|
||||
resp = getattr(self, section_name)(request)
|
||||
if isinstance(resp, Response):
|
||||
return resp
|
||||
request.environ['yubiadmin.response'].extend('page', resp)
|
||||
|
||||
def redirect(self, url):
|
||||
raise exc.HTTPSeeOther(location=url)
|
||||
|
||||
def render_forms(self, request, forms, template='form', **kwargs):
|
||||
alert = None
|
||||
def render_forms(self, request, forms, template='form',
|
||||
success_msg='Settings updated!', **kwargs):
|
||||
alerts = []
|
||||
if not request.params:
|
||||
for form in forms:
|
||||
for form in filter(lambda x: hasattr(x, 'load'), forms):
|
||||
form.load()
|
||||
else:
|
||||
errors = False
|
||||
@ -79,14 +148,88 @@ class App(object):
|
||||
errors = not form.validate() or errors
|
||||
if not errors:
|
||||
try:
|
||||
alert = {'type': 'success', 'title': 'Settings updated!'}
|
||||
for form in forms:
|
||||
if success_msg:
|
||||
alerts = [{'type': 'success', 'title': success_msg}]
|
||||
for form in filter(lambda x: hasattr(x, 'save'), forms):
|
||||
form.save()
|
||||
except Exception as e:
|
||||
alert = {'type': 'error', 'title': 'Error:',
|
||||
'message': str(e)}
|
||||
alerts = [{'type': 'error', 'title': 'Error:',
|
||||
'message': str(e)}]
|
||||
else:
|
||||
alert = {'type': 'error', 'title': 'Invalid data!'}
|
||||
alerts = [{'type': 'error', 'title': 'Invalid data!'}]
|
||||
|
||||
return render(template, target=request.path, fieldsets=forms,
|
||||
alert=alert, **kwargs)
|
||||
alerts=alerts, **kwargs)
|
||||
|
||||
|
||||
ITEM_RANGE = re.compile('(\d+)-(\d+)')
|
||||
|
||||
|
||||
class CollectionApp(App):
|
||||
base_url = ''
|
||||
caption = 'Items'
|
||||
item_name = 'Items'
|
||||
columns = []
|
||||
template = 'table'
|
||||
scripts = ['table']
|
||||
selectable = True
|
||||
max_limit = 100
|
||||
|
||||
def _size(self):
|
||||
return len(self._get())
|
||||
|
||||
def _get(self, offset=0, limit=None):
|
||||
return [{}]
|
||||
|
||||
def _labels(self, ids):
|
||||
return [x['label'] for x in self._get() if x['id'] in ids]
|
||||
|
||||
def _delete(self, ids):
|
||||
raise Exception('Not implemented!')
|
||||
|
||||
def __call__(self, request):
|
||||
sub_cmd = request.path_info_pop()
|
||||
if sub_cmd and not sub_cmd.startswith('_') 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
|
||||
return self.list(offset, limit)
|
||||
else:
|
||||
return self.list()
|
||||
|
||||
def list(self, offset=0, limit=10):
|
||||
limit = min(self.max_limit, 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, scripts=self.scripts, 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,
|
||||
selectable=self.selectable)
|
||||
|
||||
def delete(self, request):
|
||||
ids = [x[5:] for x in request.params if request.params[x] == 'on']
|
||||
labels = self._labels(ids)
|
||||
return render('table_delete', ids=','.join(ids), labels=labels,
|
||||
item_name=self.item_name, base_url=self.base_url)
|
||||
|
||||
def delete_confirm(self, request):
|
||||
self._delete(request.params['delete'].split(','))
|
||||
return self.redirect(self.base_url)
|
||||
|
@ -28,7 +28,8 @@
|
||||
import os
|
||||
import re
|
||||
import errno
|
||||
import logging as log
|
||||
import csv
|
||||
import logging
|
||||
from collections import MutableMapping, OrderedDict
|
||||
|
||||
__all__ = [
|
||||
@ -36,10 +37,20 @@ __all__ = [
|
||||
'FileConfig',
|
||||
'strip_comments',
|
||||
'php_inserter',
|
||||
'parse_block'
|
||||
'python_handler',
|
||||
'python_list_handler',
|
||||
'parse_block',
|
||||
'parse_value'
|
||||
]
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
PHP_BLOCKS = re.compile('(?ms)<\?php(.*?)\s*\?>')
|
||||
QUOTED_STR = re.compile(r'\s*[\'"](.*)[\'"]\s*')
|
||||
COMMENTS = re.compile(
|
||||
r'#.*?$|//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
|
||||
re.DOTALL | re.MULTILINE
|
||||
)
|
||||
|
||||
|
||||
def php_inserter(content, value):
|
||||
@ -54,12 +65,50 @@ def php_inserter(content, value):
|
||||
return content
|
||||
|
||||
|
||||
def strip_comments(text):
|
||||
COMMENTS = re.compile(
|
||||
r'#.*?$|//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"',
|
||||
re.DOTALL | re.MULTILINE
|
||||
)
|
||||
def python_handler(varname, default):
|
||||
pattern = r'(?sm)^\s*%s\s*=\s*(.*?)\s*$' % varname
|
||||
reader = lambda match: parse_value(match.group(1))
|
||||
writer = lambda x: '%s = %r' % (varname, str(x) if isinstance(x, unicode)
|
||||
else x)
|
||||
return RegexHandler(pattern, writer, reader, default=default)
|
||||
|
||||
|
||||
class python_list_handler:
|
||||
def __init__(self, varname, default):
|
||||
self.pattern = re.compile(r'(?m)^\s*%s\s*=\s*\[' % varname)
|
||||
self.varname = varname
|
||||
self.default = default
|
||||
|
||||
def _get_block(self, content):
|
||||
match = self.pattern.search(content)
|
||||
if match:
|
||||
return parse_block(content[match.end():], '[', ']')
|
||||
return None
|
||||
|
||||
def read(self, content):
|
||||
block = self._get_block(content)
|
||||
if block:
|
||||
block = re.sub(r'(?m)\s+', '', block)
|
||||
parts = next(csv.reader([block], skipinitialspace=True), [])
|
||||
return [strip_quotes(x) for x in parts]
|
||||
else:
|
||||
return self.default
|
||||
|
||||
def write(self, content, value):
|
||||
block = self._get_block(content)
|
||||
value = ('%s = [\n' % self.varname +
|
||||
'\n'.join([' "%s",' % x for x in value]) +
|
||||
'\n]')
|
||||
if block:
|
||||
match = self.pattern.search(content)
|
||||
start = content[:match.start()]
|
||||
end = content[match.end() + len(block) + 1:]
|
||||
return start + value + end
|
||||
else:
|
||||
return '%s\n%s' % (content, value)
|
||||
|
||||
|
||||
def strip_comments(text, ):
|
||||
def replacer(match):
|
||||
s = match.group(0)
|
||||
if s[0] in ['/', '#']:
|
||||
@ -69,6 +118,13 @@ def strip_comments(text):
|
||||
return COMMENTS.sub(replacer, text)
|
||||
|
||||
|
||||
def strip_quotes(value):
|
||||
match = QUOTED_STR.match(value)
|
||||
if match:
|
||||
return match.group(1)
|
||||
return value
|
||||
|
||||
|
||||
def parse_block(content, opening='(', closing=')'):
|
||||
level = 0
|
||||
index = 0
|
||||
@ -83,6 +139,25 @@ def parse_block(content, opening='(', closing=')'):
|
||||
return content
|
||||
|
||||
|
||||
def parse_value(valrepr):
|
||||
try:
|
||||
return int(valrepr)
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return float(valrepr)
|
||||
except ValueError:
|
||||
pass
|
||||
val_lower = valrepr.lower()
|
||||
if val_lower == 'true':
|
||||
return True
|
||||
elif val_lower == 'false':
|
||||
return False
|
||||
elif val_lower in ['none', 'null']:
|
||||
return None
|
||||
return strip_quotes(valrepr)
|
||||
|
||||
|
||||
class RegexHandler(object):
|
||||
def __init__(self, pattern, writer, reader=lambda x: x.group(1),
|
||||
inserter=lambda x, y: '%s\n%s' % (x, y),
|
||||
|
@ -93,7 +93,7 @@ class FileForm(ConfigForm):
|
||||
Form that displays the entire content of a file.
|
||||
"""
|
||||
content = TextAreaField('File')
|
||||
attrs = {'content': {'class': 'span9 code', 'rows': 25}}
|
||||
attrs = {'content': {'class': 'span9 code editor', 'rows': 25}}
|
||||
|
||||
class Handler(object):
|
||||
def read(self, content):
|
||||
@ -102,12 +102,15 @@ class FileForm(ConfigForm):
|
||||
def write(self, content, value):
|
||||
return value
|
||||
|
||||
def __init__(self, filename, legend=None, description=None, *args,
|
||||
**kwargs):
|
||||
def __init__(self, filename, legend=None, description=None, lang=None,
|
||||
*args, **kwargs):
|
||||
self.config = FileConfig(filename, [('content', self.Handler())])
|
||||
self.legend = legend
|
||||
self.description = description
|
||||
if lang:
|
||||
self.attrs['content']['ace-mode'] = lang
|
||||
super(FileForm, self).__init__(*args, **kwargs)
|
||||
self.content.label.text = 'File: %s' % filename
|
||||
|
||||
|
||||
class DBConfigForm(ConfigForm):
|
||||
|
47
yubiadmin/util/system.py
Normal file
47
yubiadmin/util/system.py
Normal file
@ -0,0 +1,47 @@
|
||||
# Copyright (c) 2013 Yubico AB
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or
|
||||
# without modification, are permitted provided that the following
|
||||
# conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above
|
||||
# copyright notice, this list of conditions and the following
|
||||
# disclaimer in the documentation and/or other materials provided
|
||||
# with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
|
||||
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
|
||||
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
|
||||
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
||||
# POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
import subprocess
|
||||
|
||||
__all__ = [
|
||||
'run',
|
||||
'invoke_rc_d'
|
||||
]
|
||||
|
||||
|
||||
def run(cmd):
|
||||
p = subprocess.Popen(['sh', '-c', cmd], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE)
|
||||
output = p.communicate()
|
||||
return p.returncode, output[0]
|
||||
|
||||
|
||||
def invoke_rc_d(script, cmd):
|
||||
if run('which invoke-rd.d')[0] == 0:
|
||||
return run('invoke-rc.d %s %s' % (script, cmd))
|
||||
else:
|
||||
return run('/etc/init.d/%s %s' % (script, cmd))
|
Loading…
x
Reference in New Issue
Block a user