[MERGE/RFC] GSSAPI authentication support for FTP

Vincent Ladeuil v.ladeuil+lp at free.fr
Thu Jun 12 22:17:36 BST 2008


>>>>> "Jelmer" == Jelmer Vernooij <jelmer at samba.org> writes:

    Jelmer> The attached patch adds support for GSSAPI
    Jelmer> authentication and encryption in the FTP transport,
    Jelmer> as documented in RFC2228.

Nice.

    Jelmer> GSSAPI is mainly used for Kerberos.

    Jelmer> The new transport class builds on top of the existing one and will try
    Jelmer> secure authentication before any other authentication. If GSSAPI is
    Jelmer> not available, it will also fall back to regular authentication.

    Jelmer> The existing FTP tests are being run against the new
    Jelmer> secure FTP server already.

But since medusa doesn't support GSSAPI, you're testing only the
fallback mechanism here, isn't it ?

    Jelmer> However, I couldn't find a good way to add tests for
    Jelmer> the GSSAPI authentication in medusa, which seems to
    Jelmer> be written around the idea that everything has a
    Jelmer> username and password.

    Jelmer> What amount of testing is required here? Would it be
    Jelmer> sufficient if I just tested that SecureFtp's methods
    Jelmer> attempt to send the right data?

If you have setup such a server locally, you may be able to
automate the tests by using
https://code.launchpad.net/~vila/bzr/local-test-server and adding
the missing bits for your server and be able to exercise your
implementation against the real server.

That will not be the same thing than providing the test in bzr
itself, but at least anyone will be able to reproduce your tests.


<snip/>

    Jelmer> +class SecureFtp(ftplib.FTP):
    Jelmer> +    """Extended version of ftplib.FTP that can authenticate using GSSAPI."""
    Jelmer> +    def mic_putcmd(self, line):
    Jelmer> +        rc = kerberos.authGSSClientWrap(self.vc, 
    Jelmer> +            base64.b64encode(line), 'jelmer at VERNSTOK.NL')

Really ?

    Jelmer> +        wrapped = kerberos.authGSSClientResponse(self.vc)
    Jelmer> +        ftplib.FTP.putcmd(self, "MIC " + wrapped)
    Jelmer> +
    Jelmer> +    def mic_getline(self):
    Jelmer> +        resp = ftplib.FTP.getline(self)
    Jelmer> +        assert resp[:4] == '631 '
    Jelmer> +        rc = kerberos.authGSSClientUnwrap(self.vc, resp[4:].strip("\r\n"))
    Jelmer> +        response = base64.b64decode(kerberos.authGSSClientResponse(self.vc))
    Jelmer> +        return response
    Jelmer> +
    Jelmer> +    def gssapi_login(self, user):
    Jelmer> +        # Try GSSAPI login first
    Jelmer> +        resp = self.sendcmd('AUTH GSSAPI')
    Jelmer> +        if resp[:3] == '334':
    Jelmer> +            rc, self.vc = kerberos.authGSSClientInit("ftp@%s" % self.host)
    Jelmer> +
    Jelmer> +            if kerberos.authGSSClientStep(self.vc, "") != 1:
    Jelmer> +                while resp[:3] in ('334', '335'):
    Jelmer> +                    authdata = kerberos.authGSSClientResponse(self.vc)
    Jelmer> +                    resp = self.sendcmd('ADAT ' + authdata)
    Jelmer> +                    if resp[:9] in ('235 ADAT=', '335 ADAT='):
    Jelmer> +                        rc = kerberos.authGSSClientStep(self.vc, resp[9:])
    Jelmer> +                        assert ((resp[:3] == '235' and rc == 1) or 
    Jelmer> +                                (resp[:3] == '335' and rc == 0))

raise AssertionError is preferred to stay active while running
under python -o (I thought the bzr test suite catched that ?).


    Jelmer> +            info("Authenticated as %s" % kerberos.authGSSClientUserName(
    Jelmer> +                    self.vc))
    Jelmer> +
    Jelmer> +            # Monkey patch ftplib
    Jelmer> +            self.putcmd = self.mic_putcmd
    Jelmer> +            self.getline = self.mic_getline
    Jelmer> +
    Jelmer> +            self.sendcmd('USER ' + user)
    Jelmer> +            return resp
    Jelmer> +
    Jelmer> +
    Jelmer> +class SecureFtpTransport(FtpTransport):
    Jelmer> +    def _create_connection(self, credentials=None):
    Jelmer> +        """Create a new connection with the provided credentials.
    Jelmer> +
    Jelmer> +        :param credentials: The credentials needed to establish the connection.
    Jelmer> +
    Jelmer> +        :return: The created connection and its associated credentials.
    Jelmer> +
    Jelmer> +        The credentials are only the password as it may have been entered
    Jelmer> +        interactively by the user and may be different from the one provided
    Jelmer> +        in base url at transport creation time.
    Jelmer> +        """
    Jelmer> +        if credentials is None:
    Jelmer> +            user, password = self._user, self._password
    Jelmer> +        else:
    Jelmer> +            user, password = credentials
    Jelmer> +
    Jelmer> +        auth = config.AuthenticationConfig()
    Jelmer> +        if user is None:
    Jelmer> +            user = auth.get_user('ftp', self._host, port=self._port)
    Jelmer> +            if user is None:
    Jelmer> +                # Default to local user
    Jelmer> +                user = getpass.getuser()
    Jelmer> +
    Jelmer> +        mutter("Constructing FTP instance against %r" %
    Jelmer> +               ((self._host, self._port, user, '********',
    Jelmer> +                self.is_active),))
    Jelmer> +        try:
    Jelmer> +            connection = SecureFtp()

A lot of duplication to be able to insert that one ? Why not add
a _connection_class attribute to the base class ?

<snip/>

    Jelmer> +def get_test_permutations():
    Jelmer> +    """Return the permutations to be used in testing."""
    Jelmer> +    from bzrlib import tests
    Jelmer> +    if tests.FTPServerFeature.available():
    Jelmer> +        from bzrlib.tests import ftp_server
    Jelmer> +        return [(SecureFtpTransport, ftp_server.FTPServer)]
    Jelmer> +    else:
    Jelmer> +        # Dummy server to have the test suite report the number of tests
    Jelmer> +        # needing that feature. We raise UnavailableFeature from methods before
    Jelmer> +        # the test server is being used. Doing so in the setUp method has bad
    Jelmer> +        # side-effects (tearDown is never called).
    Jelmer> +        class UnavailableFTPServer(object):
    Jelmer> +
    Jelmer> +            def setUp(self):
    Jelmer> +                pass
    Jelmer> +
    Jelmer> +            def tearDown(self):
    Jelmer> +                pass
    Jelmer> +
    Jelmer> +            def get_url(self):
    Jelmer> +                raise tests.UnavailableFeature(tests.FTPServerFeature)
    Jelmer> +
    Jelmer> +            def get_bogus_url(self):
    Jelmer> +                raise tests.UnavailableFeature(tests.FTPServerFeature)
    Jelmer> +
    Jelmer> +        return [(FtpTransport, UnavailableFTPServer)]

The dummy server was added to provide feedback about the skipped
tests when medusa wasn't available, I'm not sure the cost of
duplicating the code is worth the benefit of doubling the
reported skipped tests here...

Hth,

        Vincent



More information about the bazaar mailing list