We’ve got hooks in the client that surround API calls. These are pretty awkward, because they don’t correlate with user actions. For example, suppose we wanted a policy that said users weren’t allowed to kill all instances of a production job at once.
Right now, all that we could hook would be the “killJob” api call. But kill (at least in newer versions of the client) normally runs in batches. If a user called killall, what we would see on the API level is a series of “killJob” calls, each of which specified a batch of instances. We woudn’t be able to distinguish between really killing all instances of a job (which is forbidden under this policy), and carefully killing in batches (which is permitted.) In each case, the hook would just see a series of API calls, and couldn’t find out what the actual command being executed was!
For most policy enforcement, what we really want to be able to do is look at and vet the commands that a user is performing, not the API calls that the client uses to implement those commands.
So I propose that we add a new kind of hooks, which surround noun/verb commands. A hook will register itself to handle a collection of (noun, verb) pairs. Whenever any of those noun/verb commands are invoked, the hooks methods will be called around the execution of the verb. A pre-hook will have the ability to reject a command, preventing the verb from being executed.
These hooks will be registered via configuration plugins. A configuration plugin can register hooks using an API. Hooks registered this way are, effectively, hardwired into the client executable.
The order of execution of hooks is unspecified: they may be called in any order. There is no way to guarantee that one hook will execute before some other hook.
Commands registered by the python call are called global hooks, because they will run for all configurations, whether or not they specify any hooks in the configuration file.
In the implementation, hooks are registered in the module
apache.aurora.client.cli.command_hooks, using the class
GlobalCommandHookRegistry. A global hook can be registered by calling
GlobalCommandHookRegistry.register_command_hook in a configuration plugin.
class CommandHook(object) @property def name(self): """Returns a name for the hook." def get_nouns(self): """Return the nouns that have verbs that should invoke this hook.""" def get_verbs(self, noun): """Return the verbs for a particular noun that should invoke his hook.""" @abstractmethod def pre_command(self, noun, verb, context, commandline): """Execute a hook before invoking a verb. * noun: the noun being invoked. * verb: the verb being invoked. * context: the context object that will be used to invoke the verb. The options object will be initialized before calling the hook * commandline: the original argv collection used to invoke the client. Returns: True if the command should be allowed to proceed; False if the command should be rejected. """ def post_command(self, noun, verb, context, commandline, result): """Execute a hook after invoking a verb. * noun: the noun being invoked. * verb: the verb being invoked. * context: the context object that will be used to invoke the verb. The options object will be initialized before calling the hook * commandline: the original argv collection used to invoke the client. * result: the result code returned by the verb. Returns: nothing """ class GlobalCommandHookRegistry(object): @classmethod def register_command_hook(self, hook): pass
To skip a hook, a user uses a command-line option,
--skip-hooks. The option can either
specify specific hooks to skip, or “all”:
aurora --skip-hooks=all job create east/bozo/devel/myjobwill create a job without running any hooks.
aurora --skip-hooks=test,iq create east/bozo/devel/myjobwill create a job, and will skip only the hooks named “test” and “iq”.