PageRenderTime 355ms CodeModel.GetById 181ms app.highlight 16ms RepoModel.GetById 155ms app.codeStats 0ms

/neatx/lib/auth.py

http://neatx.googlecode.com/
Python | 249 lines | 138 code | 55 blank | 56 comment | 10 complexity | 58d67d11372d1c956c5df7331b5fff51 MD5 | raw file
  1#
  2#
  3
  4# Copyright (C) 2009 Google Inc.
  5#
  6# This program is free software; you can redistribute it and/or modify
  7# it under the terms of the GNU General Public License as published by
  8# the Free Software Foundation; either version 2 of the License, or
  9# (at your option) any later version.
 10#
 11# This program is distributed in the hope that it will be useful, but
 12# WITHOUT ANY WARRANTY; without even the implied warranty of
 13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 14# General Public License for more details.
 15#
 16# You should have received a copy of the GNU General Public License
 17# along with this program; if not, write to the Free Software
 18# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
 19# 02110-1301, USA.
 20
 21
 22"""Module for authentication"""
 23
 24
 25import logging
 26import os
 27import pexpect
 28import re
 29from cStringIO import StringIO
 30
 31from neatx import constants
 32from neatx import errors
 33from neatx import utils
 34
 35
 36class _AuthBase(object):
 37  def __init__(self, cfg,
 38               stdout_fileno=constants.STDOUT_FILENO,
 39               stdin_fileno=constants.STDIN_FILENO):
 40    self._cfg = cfg
 41    self._stdout_fileno = stdout_fileno
 42    self._stdin_fileno = stdin_fileno
 43
 44  def AuthenticateAndRun(self, username, password, args):
 45    raise NotImplementedError()
 46
 47
 48class _ExpectAuthBase(_AuthBase):
 49  def AuthenticateAndRun(self, username, password, args):
 50    logging.debug("Authenticating as '%s', running %r", username, args)
 51
 52    all_args = [self._GetTtySetupPath()] + self.GetCommand(username, args)
 53    logging.debug("Auth command %r", all_args)
 54
 55    # Avoid NLS issues by unsetting LC_*, and setting LANG=C
 56    env = os.environ.copy()
 57    env["LANG"] = "C"
 58    for key in env.keys():
 59      if key.startswith('LC_'):
 60        del env[key]
 61
 62    # Using variables instead of hardcoded indexes
 63    patterns = []
 64    password_prompt_idx = self._AddPattern(patterns,
 65                                           self.GetPasswordPrompt())
 66    nx_idx = self._AddPattern(patterns, re.compile("^NX> ", re.M))
 67
 68    # Start child process
 69    # TODO: Timeout in configuration and/or per auth method
 70    child = pexpect.spawn(all_args[0], args=all_args[1:], env=env,
 71                          timeout=30)
 72
 73    buf = StringIO()
 74    nxbuf = StringIO()
 75    auth_successful = False
 76
 77    try:
 78      while True:
 79        idx = child.expect(patterns)
 80
 81        # Store all output seen before the match
 82        buf.write(child.before)
 83        # Store the matched output
 84        buf.write(child.after)
 85
 86        if idx == password_prompt_idx:
 87          self._Send(child, password + os.linesep)
 88
 89          # Wait for end of password prompt
 90          child.expect(os.linesep)
 91
 92        # TODO: Timeout for programs not printing NX prompt within X seconds
 93        elif idx == nx_idx:
 94          # Program was started
 95          auth_successful = True
 96
 97          nxbuf.write(child.after)
 98          nxbuf.write(child.buffer)
 99          break
100
101        else:
102          raise AssertionError("Invalid index")
103
104    except pexpect.EOF:
105      buf.write(child.before)
106
107    except pexpect.TIMEOUT:
108      buf.write(child.before)
109      logging.debug("Authentication timed out (output=%r)", buf.getvalue())
110      raise errors.AuthTimeoutError()
111
112    if not auth_successful:
113      raise errors.AuthFailedError(("Authentication failed (output=%r, "
114                                    "exitstatus=%s, signum=%s)") %
115                                   (utils.NormalizeSpace(buf.getvalue()),
116                                    child.exitstatus, child.signalstatus))
117
118    # Write protocol buffer contents to stdout
119    os.write(self._stdout_fileno, nxbuf.getvalue())
120
121    utils.SetCloseOnExecFlag(child.fileno(), False)
122    utils.SetCloseOnExecFlag(self._stdin_fileno, False)
123    utils.SetCloseOnExecFlag(self._stdout_fileno, False)
124
125    cpargs = [self._GetFdCopyPath(),
126              "%s:%s" % (child.fileno(), self._stdout_fileno),
127              "%s:%s" % (self._stdin_fileno, child.fileno())]
128
129    # Run fdcopy to copy data between file descriptors
130    ret = os.spawnve(os.P_WAIT, cpargs[0], cpargs, env)
131    (exitcode, signum) = utils.GetExitcodeSignal(ret)
132    logging.debug("fdcopy exited (exitstatus=%s, signum=%s)",
133                  exitcode, signum)
134
135    # Discard anything left in buffer
136    child.read()
137
138    def _CheckChild():
139      if child.isalive():
140        raise utils.RetryAgain()
141
142    logging.info("Waiting for authenticated program to finish")
143    try:
144      utils.Retry(_CheckChild, 0.5, 1.1, 5.0, 30)
145    except utils.RetryTimeout:
146      logging.error("Timeout while waiting for authenticated program "
147                    "to finish")
148
149    child.close()
150
151    logging.debug(("Authenticated program finished (exitstatus=%s, "
152                   "signalstatus=%s)"), child.exitstatus, child.signalstatus)
153
154  def _GetFdCopyPath(self):
155    return constants.FDCOPY
156
157  def _GetTtySetupPath(self):
158    return constants.TTYSETUP
159
160
161  @staticmethod
162  def _Send(child, text):
163    """Write password to child program.
164
165    """
166    # child.send may not write everything in one go
167    pos = 0
168    while True:
169      pos += child.send(text[pos:])
170      if pos >= len(text):
171        break
172
173  @staticmethod
174  def _AddPattern(patterns, pattern):
175    """Adds pattern to list and returns new index.
176
177    """
178    patterns.append(pattern)
179    return len(patterns) - 1
180
181
182class SuAuth(_ExpectAuthBase):
183  def GetCommand(self, username, args):
184    cmd = " && ".join([
185      # Change to home directory
186      "cd",
187
188      # Run command
189      utils.ShellQuoteArgs(args)
190      ])
191    return [self._cfg.su, username, "-c", cmd]
192
193  def GetPasswordPrompt(self):
194    return re.compile(r"^(\S+\s)?Password:\s*", re.I | re.M)
195
196
197class SshAuth(_ExpectAuthBase):
198  def GetCommand(self, username, args):
199    # TODO: Allow for per-user hostname. A very flexible way would be to run an
200    # external script (e.g. "/.../userhost $username"), and let it print the
201    # target hostname on stdout. If the hostname is an absolute path it could
202    # be used as the script.
203    host = self._cfg.auth_ssh_host
204    port = self._cfg.auth_ssh_port
205
206    options = [
207      "-oNumberOfPasswordPrompts=1",
208      "-oPreferredAuthentications=password",
209      "-oEscapeChar=none",
210      "-oCompression=no",
211
212      # Always trust host keys
213      "-oStrictHostKeyChecking=no",
214      # Don't try to write a known_hosts file
215      "-oUserKnownHostsFile=/dev/null",
216      ]
217
218    cmd = utils.ShellQuoteArgs(args)
219    return ([self._cfg.ssh, "-2", "-x", "-l", username, "-p", str(port)] +
220            options + [host, "--", cmd])
221
222  def GetPasswordPrompt(self):
223    return re.compile(r"^.*@.*\s+password:\s*", re.I | re.M)
224
225
226_AUTH_METHOD_MAP = {
227  constants.AUTH_METHOD_SU: SuAuth,
228  constants.AUTH_METHOD_SSH: SshAuth,
229  }
230
231
232def GetAuthenticator(cfg, _method_map=_AUTH_METHOD_MAP):
233  """Returns the authenticator for an authentication method.
234
235  @type cfg: L{config.Config}
236  @param cfg: Configuration object
237  @rtype: class
238  @return: Authentication class
239  @raise errors.UnknownAuthMethod: Raised when an unknown authentication method
240    is requested
241
242  """
243  method = cfg.auth_method
244  try:
245    cls = _method_map[method]
246  except KeyError:
247    raise errors.UnknownAuthMethod("Unknown authentication method %r" % method)
248
249  return cls(cfg)