1
0
mirror of https://github.com/Yubico/yubiadmin.git synced 2025-03-14 17:29:20 +01:00

Compare commits

...

110 Commits

Author SHA1 Message Date
Dain Nilsson
50449e4588
Add deprecation notice 2019-03-20 14:17:49 +01:00
Dain Nilsson
130458b9d3 Updated NEWS and version for release. 2014-04-16 09:38:25 +02:00
Dain Nilsson
5c02e60afc Do not require TLD in URL field validation (fixes #4). 2014-03-04 15:37:38 +01:00
Dain Nilsson
70de2f37ff Properly commit YubiAuth changes when redirecting (fixes #6). 2014-03-04 12:04:48 +01:00
Dain Nilsson
fbf1e0d000 Handle usernames/passwords/secrets with spaces in the RADIUS test
utility.
2014-02-04 11:54:37 +01:00
Dain Nilsson
93e7d25786 Select API keys on click (val). 2014-01-13 11:59:53 +01:00
Dain Nilsson
bd148d1a9e Show info on dashboard when using LDAP. 2013-11-25 11:26:27 +01:00
Dain Nilsson
b559bcdc75 Updated NEWS for release. 2013-11-22 15:34:28 +01:00
Dain Nilsson
c811f30fb4 Incorrect usage of imp. 2013-11-21 14:55:50 +01:00
Dain Nilsson
4ac015e0e6 Delay import of YubiAuth until needed (fixes #3). 2013-11-21 14:47:09 +01:00
Dain Nilsson
8e0169263f Added LDAP settings to YubiAuth. 2013-11-21 10:32:25 +01:00
Dain Nilsson
f5bba0081f Sys panel fixes. 2013-09-02 11:17:33 +02:00
Dain Nilsson
38afc25abd Added dashboard. 2013-06-11 17:32:17 +02:00
Dain Nilsson
e25537245d Moved section logic into App. 2013-06-11 11:48:25 +02:00
Dain Nilsson
59cddadd76 Removed print statement. 2013-06-10 17:01:31 +02:00
Dain Nilsson
cee1426239 Added automatic YKVAL API Key registration to auth. 2013-06-10 16:46:39 +02:00
Dain Nilsson
c8d0d2f3ed Removed unneeded commits. 2013-06-10 09:48:52 +02:00
Dain Nilsson
d9af09e5ae Check for checked boxes on load. 2013-06-07 11:20:27 +02:00
Dain Nilsson
051189146b Bumped version for release. 2013-06-07 11:16:52 +02:00
Dain Nilsson
0838e3685f Fixed bug with yubiauth connection failing. 2013-06-07 11:14:32 +02:00
Dain Nilsson
3588070168 Fixed broken server. 2013-06-05 10:30:25 +02:00
Dain Nilsson
b9fe736037 Updated NEWS for release. 2013-06-05 10:07:28 +02:00
Dain Nilsson
49db8acc98 Added reload web server button to auth. 2013-06-05 10:01:38 +02:00
Dain Nilsson
b0370680fd Added user delete setting to YubiAuth. 2013-06-04 16:57:05 +02:00
Dain Nilsson
705077b782 Moved to beta. 2013-06-04 16:53:18 +02:00
Dain Nilsson
d106f5c0ee Better upgrading in sys app. 2013-06-03 16:14:15 +02:00
Dain Nilsson
408598c94d Added sys app. 2013-06-03 13:24:03 +02:00
Dain Nilsson
d2d857a938 Fix ordering when missing app.name 2013-05-31 16:44:31 +02:00
Dain Nilsson
c3e7298241 Use filename base as app name when missing. 2013-05-31 16:36:30 +02:00
Dain Nilsson
6dabc61dce Added fallbacks for title and description if docstring is missing. 2013-05-31 15:30:02 +02:00
Dain Nilsson
802bc8e43b Bumped versions. 2013-05-30 15:48:08 +02:00
Dain Nilsson
e7f61a98ab Fix displaying users to delete. 2013-05-30 15:42:49 +02:00
Dain Nilsson
bf8d639e10 Bumped version. 2013-05-28 11:05:48 +02:00
Dain Nilsson
7467700262 Fix reset form button for Ace editors. 2013-05-28 11:04:16 +02:00
Dain Nilsson
fb6e556c0c Updated NEWS for release. 2013-05-28 10:28:06 +02:00
Dain Nilsson
d79a9ffb43 Use correct clients.conf path. 2013-05-28 10:15:17 +02:00
Dain Nilsson
54ac3a1bb8 Added Ace editor. 2013-05-28 09:59:16 +02:00
Dain Nilsson
1d3fb37561 Added Ace editor for config files. 2013-05-27 17:48:50 +02:00
Dain Nilsson
d53602039e Add RADIUS Clients tab. 2013-05-27 16:43:15 +02:00
Dain Nilsson
1e15bcba4d Added FreeRADIUS daemon status and controls. 2013-05-27 14:00:01 +02:00
Dain Nilsson
d3a499a554 Added freerad app with radtest. 2013-05-27 12:06:03 +02:00
Dain Nilsson
8f9fd30a7c Limit max number of items displayed per page. 2013-05-24 16:44:46 +02:00
Dain Nilsson
aa8712f46f Better performing pagination of API Clients. 2013-05-24 16:43:02 +02:00
Dain Nilsson
fba0632a30 Use --urandom when creating API Clients. 2013-05-24 14:38:35 +02:00
Dain Nilsson
5887047f8e Incorrect method name. 2013-05-24 14:18:26 +02:00
Dain Nilsson
3de1a599cc Added YVAL Client generation. 2013-05-24 13:44:58 +02:00
Dain Nilsson
2cf8a1a140 Improved common table code. 2013-05-24 12:43:29 +02:00
Dain Nilsson
8b336df4a3 Better restarting for yubiadmin. 2013-05-23 16:02:16 +02:00
Dain Nilsson
9b0e3f39db Updated NEWS. 2013-05-23 15:50:47 +02:00
Dain Nilsson
e441642d22 Dynamically detect changes to apps. 2013-05-23 15:49:22 +02:00
Dain Nilsson
257939d725 Updated NEWS. 2013-05-20 17:13:27 +02:00
Dain Nilsson
a948d10792 Removed reverse lookup of clients which could cause long delays. 2013-05-20 17:12:15 +02:00
Dain Nilsson
3968b67aba Updated NEWS. 2013-05-17 17:14:53 +02:00
Dain Nilsson
5e79f93ea2 Started adding client table for YKVAL. 2013-05-17 17:14:03 +02:00
Dain Nilsson
68f8c9c987 Added yubiadmin-config script to setup.py 2013-05-16 11:22:09 +02:00
Dain Nilsson
a7cc2438a5 Bumped version post release. 2013-05-16 11:20:59 +02:00
Dain Nilsson
5e0dcebf8c Added yubiadmin-config tool. 2013-05-16 11:19:45 +02:00
Dain Nilsson
73f955c0a6 Updated NEWS and version for release. 2013-05-14 13:27:09 +02:00
Dain Nilsson
f2772b08cd Added assign/unassign Yubikey to auth. 2013-05-14 13:22:13 +02:00
Dain Nilsson
31909f893c Bumped version 2013-05-13 17:27:12 +02:00
Dain Nilsson
557616d891 Added create/delete users. 2013-05-13 16:53:53 +02:00
Dain Nilsson
e80ce1f952 Started adding user management for YubiAuth. 2013-05-13 13:03:42 +02:00
Dain Nilsson
e2570763ee Updated NEWS for release. 2013-05-08 16:56:21 +02:00
Dain Nilsson
15f0883588 Better restart of admin server. 2013-05-08 16:54:52 +02:00
Dain Nilsson
8dcd391628 Updated NEWS. 2013-05-08 13:24:05 +02:00
Dain Nilsson
33846f45b3 Added disable to apps. 2013-05-08 13:22:54 +02:00
Dain Nilsson
49b235e6f0 Added db settings to auth. 2013-05-08 12:05:32 +02:00
Dain Nilsson
1b31251430 Added validation servers to auth. 2013-05-08 11:22:28 +02:00
Dain Nilsson
281dd92e9c Added YubiHSM settings to auth. 2013-05-07 17:36:15 +02:00
Dain Nilsson
895de11057 Added YubiAuth app. 2013-05-07 17:25:26 +02:00
Dain Nilsson
79a0e719fa Bumped versions post release. 2013-05-03 12:33:47 +02:00
Dain Nilsson
ebb0ba6542 Better publishing. 2013-05-03 12:25:33 +02:00
Dain Nilsson
229b40ca1d Updated NEWS for release. 2013-05-03 12:04:05 +02:00
Dain Nilsson
2341f8c086 Improve warning for restarting the server. 2013-05-03 11:46:44 +02:00
Dain Nilsson
e8b1d5f75a Added yubiadmin app. 2013-05-03 11:40:14 +02:00
Dain Nilsson
8b32cf89e9 Added priority and disable to apps. 2013-05-03 11:40:10 +02:00
Dain Nilsson
2b4373cd43 Nitpicking. 2013-05-03 09:47:30 +02:00
Dain Nilsson
34b5cc10f4 Add command line arguments to yubiadmin script. 2013-05-02 17:36:23 +02:00
Dain Nilsson
fcfc2d5626 Added man pages to MANIFEST. 2013-05-02 17:09:02 +02:00
Dain Nilsson
08d0ab975d Incremented version number post release. 2013-05-02 17:06:14 +02:00
Dain Nilsson
da42713c83 Added man page for yubiadmin. 2013-05-02 17:04:33 +02:00
Dain Nilsson
4f36f6624a Renamed yubiadmin-server to yubiadmin. 2013-05-02 16:48:16 +02:00
Dain Nilsson
9296041bb9 Updated NEWS for release. 2013-05-02 16:02:02 +02:00
Dain Nilsson
3c285d49e1 Improved error handling. 2013-05-02 15:51:13 +02:00
Dain Nilsson
c6cc6d31b4 Attempt to reintroduce python3 2013-05-02 14:44:47 +02:00
Dain Nilsson
6eefc09cae Use MutableMapping instead of DictMixin. 2013-05-02 14:41:19 +02:00
Dain Nilsson
97de3abbdb Don't build for python3, we don't support it. 2013-05-02 14:36:06 +02:00
Dain Nilsson
d0ba5af7f0 Remove unicode literal to work under python3. 2013-05-02 14:25:39 +02:00
Dain Nilsson
b3a150a8d9 Use logging instead of print. 2013-05-02 14:08:29 +02:00
Dain Nilsson
d9032f1550 We don't support python 2.6 2013-05-02 13:58:27 +02:00
Dain Nilsson
da4f4af69a Bumped version post release. 2013-05-02 13:52:58 +02:00
Dain Nilsson
6b9ee2bc37 Added Travis CI 2013-05-02 13:48:37 +02:00
Dain Nilsson
10e097efd4 Fix publishing. 2013-05-02 13:36:10 +02:00
Dain Nilsson
4047e7f576 Fixed publishing. 2013-05-02 13:16:38 +02:00
Dain Nilsson
17e7423bb0 Better logging. 2013-05-02 12:38:01 +02:00
Dain Nilsson
166e432332 Formatting. 2013-05-02 12:25:07 +02:00
Dain Nilsson
083d714d53 Added optional uploading to PyPI. 2013-05-02 12:22:39 +02:00
Dain Nilsson
a185dd0b1e Better release publishing. 2013-05-02 11:50:31 +02:00
Dain Nilsson
588f3df72b Handle tests. 2013-05-02 11:43:48 +02:00
Dain Nilsson
fda9e818e0 Added release.py to MANIFEST. 2013-05-02 11:24:05 +02:00
Dain Nilsson
cf89fb0de7 Removed maintainer-scripts. 2013-05-02 11:22:27 +02:00
Dain Nilsson
e098766c04 Better publishing. 2013-05-02 11:21:52 +02:00
Dain Nilsson
83db66b1ae Added publishing. 2013-05-02 11:18:48 +02:00
Dain Nilsson
945aab6768 Cleaned up release.py 2013-05-02 11:04:50 +02:00
Dain Nilsson
9e2bffbc53 Moved release into own file. 2013-05-02 10:55:51 +02:00
Dain Nilsson
be1225164a Missing comma. 2013-05-02 10:53:18 +02:00
Dain Nilsson
e137733a20 Fix skip-tests option. 2013-05-02 10:52:19 +02:00
Dain Nilsson
832051a02e Bumped version. 2013-05-02 10:47:53 +02:00
Dain Nilsson
0d061635e7 Added release command. 2013-05-02 10:46:02 +02:00
Dain Nilsson
d7f8586479 Handle invalid URLs better. 2013-04-30 15:52:22 +02:00
54 changed files with 2608 additions and 255 deletions

10
.travis.yml Normal file
View File

@ -0,0 +1,10 @@
language: python
python:
- "2.7"
- "3.3"
# command to install dependencies
install: "pip install -q -e . --use-mirrors"
# # command to run tests
script: python setup.py nosetests
git:
submodules: false

View File

@ -1,5 +1,8 @@
include release.py
include COPYING
include NEWS
include ChangeLog
include bin/*.1
include conf/*
recursive-include yubiadmin/static *
recursive-include yubiadmin/templates *

71
NEWS
View File

@ -1,3 +1,70 @@
* Version 0.0.5 (released 2013-04-30)
* Version 0.1.7 (released 2014-04-16)
* Initial (internal) release.
* 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
View File

@ -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
View 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
View 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
View 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" "."

View File

@ -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
View 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
View 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",)

View File

@ -1,65 +0,0 @@
#!/bin/bash
if [ ! -f yubiadmin/__init__.py ]; then
echo "$0: Must be executed from top yubiadmin dir."
exit 1
fi
do_test="true"
if [ "x$1" == "x--no-test" ]; then
do_test="false"
shift
fi
keyid="$1"
if [ "x$keyid" = "x" ]; then
echo "Syntax: $0 [--no-test] <KEYID>"
exit 1
fi
set -e
version=$(grep "version=" setup.py | sed "s/^.\{1,\}version='\(.\{1,\}\)'.\{1,\}$/\1/")
tagname="yubiadmin-$version"
if ! head -1 NEWS | grep -q "Version $version (released $(date -I))"; then
echo "You need to update date/version in NEWS"
exit 1
fi
if git tag | grep -q "^$tagname\$"; then
echo "Tag $tagname already exists!"
echo "Did you remember to update the version in setup.py?"
exit 1
fi
git2cl > ChangeLog
if [ "x$do_test" != "xfalse" ]; then
python setup.py check nosetests
fi
python setup.py sdist #upload --sign --identity $keyid
gpg --detach-sign --default-key $keyid dist/$tagname.tar.gz
gpg --verify dist/$tagname.tar.gz.sig
#gpg --output dist/$tagname.tar.gz.sig --dearmor dist/$tagname.tar.gz.asc
#gpg --verify dist/$tagname.tar.gz.sig
git tag -u $keyid -m $tagname $tagname
#Remove once the page has been made:
exit 0
#Publish release
if test ! -d "$YUBICO_GITHUB_REPO"; then
echo "warn: YUBICO_GITHUB_REPO not set or invalid!"
echo " This release will not be published!"
else
$YUBICO_GITHUB_REPO/publish yubiadmin $version dist/$tagname.tar.gz*
fi
echo "Done! Don't forget to git push && git push --tags"

150
release.py Normal file
View File

@ -0,0 +1,150 @@
# 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 distutils import log
from distutils.core import Command
from distutils.errors import DistutilsSetupError
import os
import re
from datetime import date
class release(Command):
description = "create and release a new version"
user_options = [
('keyid', None, "GPG key to sign with"),
('skip-tests', None, "skip running the tests"),
('pypi', None, "publish to pypi"),
]
boolean_options = ['skip-tests', 'pypi']
def initialize_options(self):
self.keyid = None
self.skip_tests = 0
self.pypi = 0
def finalize_options(self):
self.cwd = os.getcwd()
self.fullname = self.distribution.get_fullname()
self.name = self.distribution.get_name()
self.version = self.distribution.get_version()
def _verify_version(self):
with open('NEWS', 'r') as news_file:
line = news_file.readline()
now = date.today().strftime('%Y-%m-%d')
if not re.search(r'Version %s \(released %s\)' % (self.version, now),
line):
raise DistutilsSetupError("Incorrect date/version in NEWS!")
def _verify_tag(self):
if os.system('git tag | grep -q "^%s\$"' % self.fullname) == 0:
raise DistutilsSetupError(
"Tag '%s' already exists!" % self.fullname)
def _sign(self):
if os.path.isfile('dist/%s.tar.gz.asc' % self.fullname):
# Signature exists from upload, re-use it:
sign_opts = ['--output dist/%s.tar.gz.sig' % self.fullname,
'--dearmor dist/%s.tar.gz.asc' % self.fullname]
else:
# No signature, create it:
sign_opts = ['--detach-sign', 'dist/%s.tar.gz' % self.fullname]
if self.keyid:
sign_opts.insert(1, '--default-key ' + self.keyid)
self.execute(os.system, ('gpg ' + (' '.join(sign_opts)),))
if os.system('gpg --verify dist/%s.tar.gz.sig' % self.fullname) != 0:
raise DistutilsSetupError("Error verifying signature!")
def _tag(self):
tag_opts = ['-s', '-m ' + self.fullname, self.fullname]
if self.keyid:
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):
artifacts = [
'dist/%s.tar.gz' % self.fullname,
'dist/%s.tar.gz.sig' % self.fullname
]
cmd = '%s/publish %s %s %s' % (
web_repo, self.name, self.version, ' '.join(artifacts))
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:
self.warn("YUBICO_GITHUB_REPO not set or invalid!")
self.warn("This release will not be published!")
def run(self):
if os.getcwd() != self.cwd:
raise DistutilsSetupError("Must be in package root!")
self._verify_version()
self._verify_tag()
self.execute(os.system, ('git2cl > ChangeLog',))
if not self.skip_tests:
self.run_command('check')
# Nosetests calls sys.exit(status)
try:
self.run_command('nosetests')
except SystemExit as e:
if e.code != 0:
raise DistutilsSetupError("There were test failures!")
self.run_command('sdist')
if self.pypi:
cmd_obj = self.distribution.get_command_obj('upload')
cmd_obj.sign = True
if self.keyid:
cmd_obj.identity = self.keyid
self.run_command('upload')
self._sign()
self._tag()
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("")

View File

@ -28,10 +28,11 @@
# POSSIBILITY OF SUCH DAMAGE.
from setuptools import setup
from release import release
setup(
name='yubiadmin',
version='0.0.5',
version='0.1.7',
author='Dain Nilsson',
author_email='dain@yubico.com',
maintainer='Yubico Open Source Maintainers',
@ -40,16 +41,17 @@ 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},
classifiers=[
'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',

View File

@ -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
View 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
View 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()

View 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
View 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()

View File

@ -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
View 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()

View File

@ -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(',')])
@ -94,18 +89,17 @@ class KSMHandler(object):
def read(self, content):
block = self._get_block(content)
print block
if block:
quoted = QUOTED_STRS.findall(strip_comments(block))
return [strip_quotes(x) for x in quoted]
else:
[]
return []
def write(self, content, value):
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)
@ -116,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)),
@ -190,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):
@ -215,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.
""")
@ -231,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
@ -250,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())
@ -260,13 +256,13 @@ 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/syncpool' % self.name)
return self.redirect('/%s/synchronization' % self.name)
def ksms(self, request):
"""
@ -275,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()

View File

@ -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.")

View File

@ -25,61 +25,65 @@
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
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'
app, module = self.apps[module_name]
if not section_name:
section_name = module['sections'][0]['name']
if not module_name in apps_data:
raise exc.HTTPNotFound
section = next((section for section in module['sections']
if section['name'] == section_name), None)
app, module = apps_data[module_name]
return render(
'app_base',
modules=self.modules,
if module['disabled']:
raise exc.HTTPNotFound
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()

View File

@ -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;

File diff suppressed because one or more lines are too long

View 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)})

File diff suppressed because one or more lines are too long

View 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)})

View 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)})

File diff suppressed because one or more lines are too long

View 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');
}
});
});

View 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());
});
});
});
});

View File

@ -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);
}
});
});

View 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');
}
});

View 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) }}

View File

@ -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">&times;</button>
<strong>{{ alert.title }}</strong>
<span class="message">{{ alert.message }}</span>
</div>
{% endfor %}
{{ page }}
{% endblock %}

View 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) }}

View 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>

View 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]) }}

View File

@ -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>

View File

@ -5,7 +5,7 @@
<div class="row">
<div class="span12">
<h1>YubiADMIN</h1>
<h1><a href="/">YubiADMIN</a></h1>
</div>
</div>
@ -13,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>

View 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>

View File

@ -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' %}

View 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>

View 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>

View 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>

View 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 }}
&nbsp;
<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) }}

View 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>

View 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>

View 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>

View File

@ -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,19 +88,148 @@ 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()
else:
print 'Errors!'
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):
populate_forms(forms, request.params)
return render(template, target=request.path, fieldsets=forms, **kwargs)
def render_forms(self, request, forms, template='form',
success_msg='Settings updated!', **kwargs):
alerts = []
if not request.params:
for form in filter(lambda x: hasattr(x, 'load'), forms):
form.load()
else:
errors = False
for form in forms:
form.process(request.params)
errors = not form.validate() or errors
if not errors:
try:
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:
alerts = [{'type': 'error', 'title': 'Error:',
'message': str(e)}]
else:
alerts = [{'type': 'error', 'title': 'Invalid data!'}]
return render(template, target=request.path, fieldsets=forms,
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)

View File

@ -27,17 +27,30 @@
import os
import re
from UserDict import DictMixin
import errno
import csv
import logging
from collections import MutableMapping, OrderedDict
__all__ = [
'RegexHandler',
'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):
@ -52,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 ['/', '#']:
@ -67,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
@ -81,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),
@ -111,14 +188,14 @@ class RegexHandler(object):
return self.inserter(content, self.writer(value))
class FileConfig(DictMixin, object):
class FileConfig(MutableMapping):
"""
Maps key-value pairs to a backing config file.
You can manually edit the file by modifying self.content.
"""
def __init__(self, filename, params=[]):
self.filename = filename
self.params = {}
self.params = OrderedDict()
for param in params:
self.add_param(*param)
@ -127,10 +204,20 @@ class FileConfig(DictMixin, object):
with open(self.filename, 'r') as file:
self.content = unicode(file.read())
except IOError as e:
print e
log.error(e)
self.content = u''
#Initialize all params from default values.
for key in self.params:
self[key] = self[key]
def commit(self):
if not os.path.isfile(self.filename):
dir = os.path.dirname(self.filename)
try:
os.makedirs(dir)
except OSError as e:
if e.errno != errno.EEXIST:
raise e
with open(self.filename, 'w+') as file:
#Fix all linebreaks
file.write(os.linesep.join(self.content.splitlines()))
@ -138,6 +225,12 @@ class FileConfig(DictMixin, object):
def add_param(self, key, handler):
self.params[key] = handler
def __iter__(self):
return self.params.__iter__()
def __len__(self):
return len(self.params)
def __getitem__(self, key):
return self.params[key].read(self.content)

View File

@ -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
View 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))