As this functinoality will be used in a PAM module, move it into an independent module.
Signed-off-by: Oleg Ponomarev <[email protected]> --- Makefile.am | 3 +- lib/http/auth.py | 53 ------------- lib/rapi/testutils.py | 5 +- lib/rapi/users_file.py | 137 +++++++++++++++++++++++++++++++++ lib/server/rapi.py | 52 +------------ qa/qa_rapi.py | 2 +- test/py/ganeti.http_unittest.py | 5 +- test/py/ganeti.server.rapi_unittest.py | 7 +- 8 files changed, 151 insertions(+), 113 deletions(-) create mode 100644 lib/rapi/users_file.py diff --git a/Makefile.am b/Makefile.am index 3c5c8ee..979f596 100644 --- a/Makefile.am +++ b/Makefile.am @@ -568,7 +568,8 @@ rapi_PYTHON = \ lib/rapi/client_utils.py \ lib/rapi/connector.py \ lib/rapi/rlib2.py \ - lib/rapi/testutils.py + lib/rapi/testutils.py \ + lib/rapi/users_file.py http_PYTHON = \ lib/http/__init__.py \ diff --git a/lib/http/auth.py b/lib/http/auth.py index 35b0b32..83f0cba 100644 --- a/lib/http/auth.py +++ b/lib/http/auth.py @@ -38,7 +38,6 @@ import binascii from ganeti import compat from ganeti import http -from ganeti import utils from cStringIO import StringIO @@ -285,55 +284,3 @@ class HttpServerRequestAuthentication(object): return False - -class PasswordFileUser(object): - """Data structure for users from password file. - - """ - def __init__(self, name, password, options): - self.name = name - self.password = password - self.options = options - - -def ParsePasswordFile(contents): - """Parses the contents of a password file. - - Lines in the password file are of the following format:: - - <username> <password> [options] - - Fields are separated by whitespace. Username and password are mandatory, - options are optional and separated by comma (','). Empty lines and comments - ('#') are ignored. - - @type contents: str - @param contents: Contents of password file - @rtype: dict - @return: Dictionary containing L{PasswordFileUser} instances - - """ - users = {} - - for line in utils.FilterEmptyLinesAndComments(contents): - parts = line.split(None, 2) - if len(parts) < 2: - # Invalid line - # TODO: Return line number from FilterEmptyLinesAndComments - logging.warning("Ignoring non-comment line with less than two fields") - continue - - name = parts[0] - password = parts[1] - - # Extract options - options = [] - if len(parts) >= 3: - for part in parts[2].split(","): - options.append(part.strip()) - else: - logging.warning("Ignoring values for user '%s': %s", name, parts[3:]) - - users[name] = PasswordFileUser(name, password, options) - - return users diff --git a/lib/rapi/testutils.py b/lib/rapi/testutils.py index 8f7a7ad..d4aa04b 100644 --- a/lib/rapi/testutils.py +++ b/lib/rapi/testutils.py @@ -50,6 +50,7 @@ from ganeti import rapi import ganeti.http.server # pylint: disable=W0611 import ganeti.server.rapi +from ganeti.rapi import users_file import ganeti.rapi.client @@ -363,8 +364,8 @@ class InputTestClient(object): """ assert username == wanted - return http.auth.PasswordFileUser(username, password, - [rapi.RAPI_ACCESS_WRITE]) + return users_file.PasswordFileUser(username, password, + [rapi.RAPI_ACCESS_WRITE]) self._lcr = _LuxiCallRecorder() diff --git a/lib/rapi/users_file.py b/lib/rapi/users_file.py new file mode 100644 index 0000000..e2a26d3 --- /dev/null +++ b/lib/rapi/users_file.py @@ -0,0 +1,137 @@ +# +# + +# Copyright (C) 2006, 2007, 2008, 2009, 2010, 2012, 2013, 2015 Google Inc. +# 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. + +"""RAPI users config file parser. + +""" + +import errno +import logging + +from ganeti import utils + + +class PasswordFileUser(object): + """Data structure for users from password file. + + """ + def __init__(self, name, password, options): + self.name = name + self.password = password + self.options = options + + +def ParsePasswordFile(contents): + """Parses the contents of a password file. + + Lines in the password file are of the following format:: + + <username> <password> [options] + + Fields are separated by whitespace. Username and password are mandatory, + options are optional and separated by comma (','). Empty lines and comments + ('#') are ignored. + + @type contents: str + @param contents: Contents of password file + @rtype: dict + @return: Dictionary containing L{PasswordFileUser} instances + + """ + users = {} + + for line in utils.FilterEmptyLinesAndComments(contents): + parts = line.split(None, 2) + if len(parts) < 2: + # Invalid line + # TODO: Return line number from FilterEmptyLinesAndComments + logging.warning("Ignoring non-comment line with less than two fields") + continue + + name = parts[0] + password = parts[1] + + # Extract options + options = [] + if len(parts) >= 3: + for part in parts[2].split(","): + options.append(part.strip()) + else: + logging.warning("Ignoring values for user '%s': %s", name, parts[3:]) + + users[name] = PasswordFileUser(name, password, options) + + return users + + +class RapiUsers(object): + def __init__(self): + """Initializes this class. + + """ + self._users = None + + def Get(self, username): + """Checks whether a user exists. + + """ + if self._users: + return self._users.get(username, None) + else: + return None + + def Load(self, filename): + """Loads a file containing users and passwords. + + @type filename: string + @param filename: Path to file + + """ + logging.info("Reading users file at %s", filename) + try: + try: + contents = utils.ReadFile(filename) + except EnvironmentError, err: + self._users = None + if err.errno == errno.ENOENT: + logging.warning("No users file at %s", filename) + else: + logging.warning("Error while reading %s: %s", filename, err) + return False + + users = ParsePasswordFile(contents) + + except Exception, err: # pylint: disable=W0703 + # We don't care about the type of exception + logging.error("Error while parsing %s: %s", filename, err) + return False + + self._users = users + + return True diff --git a/lib/server/rapi.py b/lib/server/rapi.py index 9782ada..2fd61f7 100644 --- a/lib/server/rapi.py +++ b/lib/server/rapi.py @@ -40,7 +40,6 @@ import optparse import sys import os import os.path -import errno try: from pyinotify import pyinotify # pylint: disable=E0611 @@ -55,10 +54,10 @@ from ganeti import ssconf import ganeti.rpc.errors as rpcerr from ganeti import serializer from ganeti import compat -from ganeti import utils from ganeti import pathutils from ganeti.rapi import connector from ganeti.rapi import baserlib +from ganeti.rapi import users_file import ganeti.http.auth # pylint: disable=W0611 import ganeti.http.server @@ -214,53 +213,6 @@ class RemoteApiHandler(http.auth.HttpServerRequestAuthentication, return serializer.DumpJson(result) -class RapiUsers(object): - def __init__(self): - """Initializes this class. - - """ - self._users = None - - def Get(self, username): - """Checks whether a user exists. - - """ - if self._users: - return self._users.get(username, None) - else: - return None - - def Load(self, filename): - """Loads a file containing users and passwords. - - @type filename: string - @param filename: Path to file - - """ - logging.info("Reading users file at %s", filename) - try: - try: - contents = utils.ReadFile(filename) - except EnvironmentError, err: - self._users = None - if err.errno == errno.ENOENT: - logging.warning("No users file at %s", filename) - else: - logging.warning("Error while reading %s: %s", filename, err) - return False - - users = http.auth.ParsePasswordFile(contents) - - except Exception, err: # pylint: disable=W0703 - # We don't care about the type of exception - logging.error("Error while parsing %s: %s", filename, err) - return False - - self._users = users - - return True - - class FileEventHandler(asyncnotifier.FileEventHandlerBase): def __init__(self, wm, path, cb): """Initializes this class. @@ -334,7 +286,7 @@ def PrepRapi(options, _): """ mainloop = daemon.Mainloop() - users = RapiUsers() + users = users_file.RapiUsers() handler = RemoteApiHandler(users.Get, options.reqauth) diff --git a/qa/qa_rapi.py b/qa/qa_rapi.py index d997c24..df3edcb 100644 --- a/qa/qa_rapi.py +++ b/qa/qa_rapi.py @@ -54,7 +54,7 @@ from ganeti import query from ganeti import rapi from ganeti import utils -from ganeti.http.auth import ParsePasswordFile +from ganeti.rapi.users_file import ParsePasswordFile import ganeti.rapi.client # pylint: disable=W0611 import ganeti.rapi.client_utils diff --git a/test/py/ganeti.http_unittest.py b/test/py/ganeti.http_unittest.py index 518f817..c713395 100755 --- a/test/py/ganeti.http_unittest.py +++ b/test/py/ganeti.http_unittest.py @@ -42,6 +42,7 @@ from cStringIO import StringIO from ganeti import http from ganeti import compat +from ganeti.rapi import users_file import ganeti.http.server import ganeti.http.client @@ -306,7 +307,7 @@ class TestHttpServerRequestAuthentication(unittest.TestCase): class TestReadPasswordFile(unittest.TestCase): def testSimple(self): - users = http.auth.ParsePasswordFile("user1 password") + users = users_file.ParsePasswordFile("user1 password") self.assertEqual(len(users), 1) self.assertEqual(users["user1"].password, "password") self.assertEqual(len(users["user1"].options), 0) @@ -321,7 +322,7 @@ class TestReadPasswordFile(unittest.TestCase): buf.write(" \t# Another comment\n") buf.write("invalidline\n") - users = http.auth.ParsePasswordFile(buf.getvalue()) + users = users_file.ParsePasswordFile(buf.getvalue()) self.assertEqual(len(users), 2) self.assertEqual(users["user1"].password, "password") self.assertEqual(len(users["user1"].options), 0) diff --git a/test/py/ganeti.server.rapi_unittest.py b/test/py/ganeti.server.rapi_unittest.py index ee879bd..1bdda1d 100755 --- a/test/py/ganeti.server.rapi_unittest.py +++ b/test/py/ganeti.server.rapi_unittest.py @@ -47,6 +47,7 @@ from ganeti import http from ganeti import objects import ganeti.rapi.baserlib +from ganeti.rapi import users_file import ganeti.rapi.testutils import ganeti.rapi.rlib2 import ganeti.http.auth @@ -178,16 +179,14 @@ class TestRemoteApiHandler(unittest.TestCase): def _LookupUserNoWrite(name): if name == username: - return http.auth.PasswordFileUser(name, password, []) + return users_file.PasswordFileUser(name, password, []) else: return None for access in [rapi.RAPI_ACCESS_WRITE, rapi.RAPI_ACCESS_READ]: def _LookupUserWithWrite(name): if name == username: - return http.auth.PasswordFileUser(name, password, [ - access, - ]) + return users_file.PasswordFileUser(name, password, [access]) else: return None -- 2.6.0.rc2.230.g3dd15c0
