Here's the low-down on authentication in Autotest: Django has an authentication middleware module. It has its own database of user/pass info. It allows you to check if requests are authenticated and return a login page if not.
We have that module enabled, because the we use the Django built-in admin interface, which depends on it. But we don't really let it do authentication. We manually bypass it. They key to this is the frontend/apache_auth.py module, which contains the class ApacheAuthMiddleware. This is enabled in the default frontend/settings.py in SVN (see MIDDLEWARE_CLASSES in that file). That middleware essentially assumes that Apache has already authorized the request and automatically authenticates the user with Django's auth system. More precisely, ApacheAuthMiddleware (plus its superclass, GetApacheUserMiddleware) does this: * look for a username in the REMOTE_USER header. This is where Apache puts the authenticated user when it authenticates a request. * if no username is found there, look for a username in the HTTP_AUTHORIZATION header, which comes directly from the "Authorization:" header on the HTTP request. This request header normally would contain authorization info for an auth-protected web site, but for an unauthenticated request, the server has no idea who's making the request. So we hijack this header in the CLI to tell the server what user is making the request. * if no username is found there, just default to "debug_user". That's what you get when making requests to an unauthenticated Apache server from the web client (which doesn't stick the username in the Authorization header), or any other custom client besides the CLI that doesn't include the Authorization header. And then we have a Django authentication backend, SimpleAuthBackend (again specified in settings.py) which does this: * create a Django User object for the given username, if one doesn't exist (this is in the auth_user table) * create an AFE user object, if one doesn't exist (we have our own User model, in the afe_users table) * assume the User is authenticated (no password checking or anything) and log them in (which happens automatically by just returning the user and not throwing any exception) I know this sounds unnecessarily complicated. The key issue that we want to use the Django admin interface, and that depends on the Django authentication middleware being used, so we have to ensure that middleware has a logged-in user without actually relying it on its own authentication mechanisms (since we defer the actual authentication responsibility to Apache). So we have all this nonsense in place to take the username reported by Apache and trick Django into thinking that user authenticated through its own system. If we were to stop using the builtin admin interface app, we could get rid of all this plumbing for the Django auth system. Incidentally, that's also the reason we still have to run syncdb, and the reason have to have custom code in frontend/afe/management.py, and the reason we have to deal with the django_sessions table. Lots of junk would go away if we replaced the Django admin app with our own admin app. I hope that made some sense. I'm happy to clarify if you have questions. And then maybe someone can put this into a nice wiki page... Steve On Mon, Feb 1, 2010 at 3:44 PM, Steve Howard <[email protected]> wrote: > On Mon, Feb 1, 2010 at 3:34 PM, Martin Bligh <[email protected]> wrote: >> On Mon, Feb 1, 2010 at 3:28 PM, Steve Howard <[email protected]> wrote: >>> Remove /noauth/ usage in CLI. Extracted the bit of the CLI that generates >>> authorization headers into a function with a site override. >>> >>> If you have site-specific authorization in place on your Autotest web >>> service, you'll need to write a site-specific function to generate >>> authorization info to continue using the CLI. I'll send a patch in a week >>> or so to remove the /noauth/ endpoint entirely. >> >> What happens if you don't have a site-specific function, and use the default >> auth? > > Should just keep working like it does now. CLI commands will be > attributed to your user, since the CLI by default includes your > username in the authorization header. Web interface will continue to > show up as debug_user, unchanged. Not sure exactly what auth systems > if any people have put into place out there on Autotest servers, I've > never heard anyone ask about it as far as I can remember. > > Steve > > >> >>> Let me know if you have any questions or feedback, I'm happy to help. >>> >>> Signed-off-by: Steve Howard <[email protected]> >>> >>> --- autotest/cli/cli_mock.py 2010-02-01 11:06:34.000000000 -0800 >>> +++ autotest/cli/cli_mock.py 2010-02-01 11:06:34.000000000 -0800 >>> @@ -32,6 +32,11 @@ >>> self.god.stub_class_method(rpc.afe_comm, 'run') >>> self.god.stub_function(sys, 'exit') >>> >>> + def stub_authorization_headers(*args, **kwargs): >>> + return {} >>> + self.god.stub_with(rpc, 'authorization_headers', >>> + stub_authorization_headers) >>> + >>> >>> def tearDown(self): >>> super(cli_unittest, self).tearDown() >>> --- autotest/cli/job_unittest.py 2010-02-01 11:06:34.000000000 -0800 >>> +++ autotest/cli/job_unittest.py 2010-02-01 11:06:34.000000000 -0800 >>> @@ -15,8 +15,6 @@ >>> class job_unittest(cli_mock.cli_unittest): >>> def setUp(self): >>> super(job_unittest, self).setUp() >>> - self.god.stub_function(getpass, 'getuser') >>> - getpass.getuser.expect_call().and_return('user0') >>> self.values = copy.deepcopy(self.values_template) >>> >>> results = [{u'status_counts': {u'Aborted': 1}, >>> @@ -103,6 +101,7 @@ >>> >>> class job_list_unittest(job_unittest): >>> def test_job_list_jobs(self): >>> + self.god.stub_function(getpass, 'getuser') >>> getpass.getuser.expect_call().and_return('user0') >>> self.run_cmd(argv=['atest', 'job', 'list', '--ignore_site_file'], >>> rpcs=[('get_jobs_summary', {'owner': 'user0', >>> --- autotest/cli/rpc.py 2010-02-01 11:06:34.000000000 -0800 >>> +++ autotest/cli/rpc.py 2010-02-01 11:06:34.000000000 -0800 >>> @@ -5,12 +5,12 @@ >>> import os, getpass >>> from autotest_lib.frontend.afe import rpc_client_lib >>> from autotest_lib.frontend.afe.json_rpc import proxy >>> -from autotest_lib.client.common_lib import global_config >>> +from autotest_lib.client.common_lib import global_config, utils >>> >>> GLOBAL_CONFIG = global_config.global_config >>> DEFAULT_SERVER = 'autotest' >>> -AFE_RPC_PATH = '/afe/server/noauth/rpc/' >>> -TKO_RPC_PATH = '/new_tko/server/noauth/rpc/' >>> +AFE_RPC_PATH = '/afe/server/rpc/' >>> +TKO_RPC_PATH = '/new_tko/server/rpc/' >>> >>> >>> def get_autotest_server(web_server=None): >>> @@ -29,6 +29,20 @@ >>> return web_server >>> >>> >>> +def base_authorization_headers(username, server): >>> + if not username: >>> + if 'AUTOTEST_USER' in os.environ: >>> + username = os.environ['AUTOTEST_USER'] >>> + else: >>> + username = getpass.getuser() >>> + return {'AUTHORIZATION' : username} >>> + >>> + >>> +authorization_headers = utils.import_site_function( >>> + __file__, 'autotest_lib.cli.site_rpc', 'authorization_headers', >>> + base_authorization_headers) >>> + >>> + >>> class rpc_comm(object): >>> """Shared AFE/TKO RPC class stuff""" >>> def __init__(self, web_server, rpc_path, username): >>> @@ -40,13 +54,7 @@ >>> def _connect(self, rpc_path): >>> # This does not fail even if the address is wrong. >>> # We need to wait for an actual RPC to fail >>> - if self.username: >>> - username = self.username >>> - elif 'AUTOTEST_USER' in os.environ: >>> - username = os.environ['AUTOTEST_USER'] >>> - else: >>> - username = getpass.getuser() >>> - headers = {'AUTHORIZATION' : username} >>> + headers = authorization_headers(self.username, self.web_server) >>> rpc_server = self.web_server + rpc_path >>> return rpc_client_lib.get_proxy(rpc_server, headers=headers) >>> _______________________________________________ >>> Autotest mailing list >>> [email protected] >>> http://test.kernel.org/cgi-bin/mailman/listinfo/autotest >>> >> > _______________________________________________ Autotest mailing list [email protected] http://test.kernel.org/cgi-bin/mailman/listinfo/autotest
