diff --git a/common/djangoapps/student/management/commands/create_user.py b/common/djangoapps/student/management/commands/create_user.py index bf4c084dca..d375dedc54 100644 --- a/common/djangoapps/student/management/commands/create_user.py +++ b/common/djangoapps/student/management/commands/create_user.py @@ -4,8 +4,10 @@ from student.models import CourseEnrollment, Registration from student.views import _do_create_account from django.contrib.auth.models import User +from track.management.tracked_command import TrackedCommand -class Command(BaseCommand): + +class Command(TrackedCommand): help = """ This command creates and registers a user in a given course as "audit", "verified" or "honor". diff --git a/common/djangoapps/track/management/__init__.py b/common/djangoapps/track/management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/common/djangoapps/track/management/tests/test_tracked_command.py b/common/djangoapps/track/management/tests/test_tracked_command.py new file mode 100644 index 0000000000..b064f0e7b2 --- /dev/null +++ b/common/djangoapps/track/management/tests/test_tracked_command.py @@ -0,0 +1,47 @@ +import json +from StringIO import StringIO +from django.test import TestCase + +from eventtracking import tracker as eventtracker + +from track.command import TrackedCommand + + +class DummyCommand(TrackedCommand): + """A locally-defined command, for testing, that returns the current context as a JSON string.""" + def handle(self, *args, **options): + return json.dumps(eventtracker.get_tracker().resolve_context()) + + +class CommandsTestBase(TestCase): + + def _run_dummy_command(self, *args, **kwargs): + """Runs the test command's execute method directly, and outputs a dict of the current context.""" + out = StringIO() + DummyCommand().execute(*args, stdout=out, **kwargs) + out.seek(0) + return json.loads(out.read()) + + def test_command(self): + args = ['whee'] + kwargs = {'key1': 'default', 'key2': True} + json_out = self._run_dummy_command(*args, **kwargs) + self.assertEquals(json_out['command'], 'unknown') + self.assertEquals(json_out['command_args'], args) + self.assertEquals(json_out['command_options'], kwargs) + + def test_password_in_command(self): + args = [] + kwargs = {'password': 'default'} + json_out = self._run_dummy_command(*args, **kwargs) + self.assertEquals(json_out['command'], 'unknown') + self.assertEquals(json_out['command_args'], args) + self.assertEquals(json_out['command_options'], {'password': '********'}) + + def test_removed_args_in_command(self): + args = [] + kwargs = {'settings': 'dummy', 'pythonpath': 'whee'} + json_out = self._run_dummy_command(*args, **kwargs) + self.assertEquals(json_out['command'], 'unknown') + self.assertEquals(json_out['command_args'], args) + self.assertEquals(json_out['command_options'], {}) diff --git a/common/djangoapps/track/management/tracked_command.py b/common/djangoapps/track/management/tracked_command.py new file mode 100644 index 0000000000..d03dfb21e4 --- /dev/null +++ b/common/djangoapps/track/management/tracked_command.py @@ -0,0 +1,94 @@ +"""Provides management command calling info to tracking context.""" + +from django.core.management.base import BaseCommand + +from eventtracking import tracker + + +class TrackedCommand(BaseCommand): + """ + Provides management command calling info to tracking context. + + Information provided to context includes three values: + + 'command': the program name and the subcommand used to run a management command. + 'command_args': the argument list passed to the command. + 'command_options': the option dict passed to the command. This includes options + that were not explicitly specified, and receive default values. + + Special treatment are provided for several options, including obfuscation and filtering. + + The values for the following options are filtered entirely: + 'settings', 'pythonpath', 'verbosity', 'traceback', 'stdout', 'stderr'. + The values for the following options are replaced with eight asterisks: + 'password'. + + An example tracking log entry resulting from running the 'create_user' management command: + + { + "username": "anonymous", + "host": "", + "event_source": "server", + "event_type": "edx.course.enrollment.activated", + "context": { + "course_id": "edX/Open_DemoX/edx_demo_course", + "org_id": "edX", + "command_options": { + "username": null, + "name": null, + "course": "edX/Open_DemoX/edx_demo_course", + "mode": "verified", + "password": "********", + "email": "rando9c@example.com", + "staff": false + }, + "command": "./manage.py create_user", + "command_args": [] + }, + "time": "2014-01-06T15:59:49.599522+00:00", + "ip": "", + "event": { + "course_id": "edX/Open_DemoX/edx_demo_course", + "user_id": 29, + "mode": "verified" + }, + "agent": "", + "page": null + } + + The name of the context used to add (and remove) these values is "edx.mgmt.command". + The context name is used to allow the context additions to be scoped, but doesn't + appear in the context itself. + """ + prog_name = 'unknown' + + def create_parser(self, prog_name, subcommand): + """Wraps create_parser to snag command line info.""" + self.prog_name = "{} {}".format(prog_name, subcommand) + return super(TrackedCommand, self).create_parser(prog_name, subcommand) + + def execute(self, *args, **options): + """Wraps base execute() to add command line to tracking context.""" + # Make a copy of options, and obfuscate or filter particular values. + options_dict = dict(options) + + # Stuff to obfuscate: + censored_opts = ['password'] + for opt in censored_opts: + if opt in options_dict: + options_dict[opt] = '*' * 8 + + # Stuff to filter: + removed_opts = ['settings', 'pythonpath', 'verbosity', 'traceback', 'stdout', 'stderr'] + for opt in removed_opts: + if opt in options_dict: + del options_dict[opt] + + context = { + 'command': self.prog_name, + 'command_args': args, + 'command_options': options_dict, + } + COMMAND_CONTEXT_NAME = 'edx.mgmt.command' + with tracker.get_tracker().context(COMMAND_CONTEXT_NAME, context): + super(TrackedCommand, self).execute(*args, **options)