Skip to content

Conversation

ethitter
Copy link
Contributor

@ethitter ethitter commented Sep 8, 2017

Introduces a way to create cron events to run whitelisted WP-CLI commands.

Does not yet support any commands that involve files, such as imports or exports.

Command output is directed to error_log().

@ethitter ethitter self-assigned this Sep 8, 2017
@ethitter ethitter added the WIP label Sep 8, 2017
@ethitter
Copy link
Contributor Author

The command validation needs work. Passing a flag right after wp will likely break things right now.

@ethitter
Copy link
Contributor Author

The command validation needs work. Passing a flag right after wp will likely break things right now.

Much better care of 5b60aee, thanks to WP_CLI\Configurator\extract_assoc().

@ethitter
Copy link
Contributor Author

Some sort of kill switch is probably a good idea. Not sure how exactly to implement that for a command that's already running.

@ethitter
Copy link
Contributor Author

If WP-CLI will support it, we should allow some sort of STDIN to be specified, as some commands use it. This might be something to pursue after the basics land, alongside file handling.

Copy link

@nickdaugherty nickdaugherty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks really cool! Left some feedback here and there.


Yes, using either the `WP_CLI_CRON_CONTROL_OFFLOAD_COMMAND_BLACKLIST` constant or the `wp_cli_cron_control_offload_command_blacklist` filter. If defined, the constant takes precedence and the filter is ignored.

Regardless of whether the constant or filter is used, either should provide an array of top-level commands to permit:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are the whitelist / blacklist examples reversed here? Blacklist should be the list of commands to block, and whitelist should be commands to permit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤦‍♂️

/**
* Offload WP-CLI commands to cron
*/
class CLI extends WP_CLI_Command {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a thought - should this class be called Command instead of CLI? The former seems more descriptive, especially since it extends WP_CLI_Command.

if ( defined( 'WP_CLI_CRON_CONTROL_OFFLOAD_COMMAND_BLACKLIST' ) && is_array( \WP_CLI_CRON_CONTROL_OFFLOAD_COMMAND_BLACKLIST ) ) {
return \WP_CLI_CRON_CONTROL_OFFLOAD_COMMAND_BLACKLIST;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder here - there are probably going to be WP CLI commands that the hosting platform blacklists...then potentially additional commands that the developer of the site wishes to blacklist.

Should we support both? For example, on VIP we could blacklist a few (and those can never be reversed via filter), then clients could add additional commands to blacklist via a filter, and that's only additive.

$this->assertTrue( is_wp_error( WP_CLI_Cron_Control_Offload\validate_command( 'wp cli info' ) ) );
$this->assertTrue( is_wp_error( WP_CLI_Cron_Control_Offload\validate_command( 'cli info' ) ) );
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add some tests for improperly constructed commands? I'm thinking things like:

  • Missing wp (post list),
  • Trying to sneak additional commands on (wp post list && cat /etc/passwd) (even though this might not actually do anything)
  • Poorly constructed arguments, like wp do-thing --foo=bar--fubar=yes
  • Backtick operators
  • Trying to override --url, perhaps with a different site that's not part of this network
  • Anything else that isn't a standard wp command subcommand [positional_args] [--assoc_args]

I'd like to be as defensive and paranoid as possible here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call. Pipes and ampersands should also be dropped.

The leading wp is optional, mostly for convenience. WP_CLI::runcommand() doesn't want it, so the parse_command() method strips it. This has the added benefit of normalizing the command to ensure cron's duplicate scheduling kicks in when appropriate.

There's a limit to how much parsing we can do without recreating large portions of WP-CLI (or loading it, in the case of custom commands), but we clearly have room for improvement. That we trust the initial users for this (requires a Go sandbox) helps somewhat.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checks added in 94d630e, tests in 966d0f3.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much beyond this is difficult, as I haven't found a way yet to invoke WP-CLI's command validation outside of where WP-CLI uses it.

Copy link
Member

@mjangda mjangda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some suggestions on how to improve the tests.

$this->assertTrue( is_wp_error( WP_CLI_Cron_Control_Offload\validate_command( 'post list 2> /tmp/nope' ) ) );
$this->assertTrue( is_wp_error( WP_CLI_Cron_Control_Offload\validate_command( 'post list 1>&2 /tmp/nope' ) ) );
$this->assertTrue( is_wp_error( WP_CLI_Cron_Control_Offload\validate_command( 'post list 2>&1 /tmp/nope' ) ) );
$this->assertTrue( is_wp_error( WP_CLI_Cron_Control_Offload\validate_command( 'post list &> /tmp/nope' ) ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For tests like this when we are running through a list of inputs/outputs, it's better to use PHPUnit's data providers: https://phpunit.de/manual/current/en/writing-tests-for-phpunit.html#writing-tests-for-phpunit.data-providers -- each input gets executed as a separate test and we can label them so we know which ones fail.

Same thing applies to test_blocked_event_scheduling and a bunch of other ones above where the main action and assertion between each test is the same.

Finally, it's nice to use this pattern for tests: http://wiki.c2.com/?ArrangeActAssert -- i.e. instead of squeezing everything into one line, opt for readability instead:

$input = 'post list & date';

$output = WP_CLI_Cron_Control_Offload\validate_command( $input );

$this->assertWpError( $output );

* Test each blocked bash operator
*/
function test_for_invalid_bash_operators() {
$this->assertTrue( is_wp_error( WP_CLI_Cron_Control_Offload\validate_command( 'post list & date' ) ) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a assertWPError method which might be nicer than doing is_wp_error + assertTrue (https://make.wordpress.org/core/handbook/testing/automated-testing/writing-phpunit-tests/#assertions)

Copy link
Member

@mjangda mjangda left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Noted a few things that should probably be changed. I think the biggest thing is probably the parse/implode stuff (I think longer term maintenance is going to be challenging).

The other thought I had: what's the benefit of using this vs. just executing the command via vipd + host action?

*/
public function create( $args, $assoc_args ) {
$command = WP_CLI\Utils\get_flag_value( $assoc_args, 'command', '' );
$command = validate_command( $command );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is redundant, since schedule_cli_command runs it as well.

* @param string $filter_tag String for list filter.
* @return array
*/
function _filter_list_allow_only_additions( $constant, $filter_tag ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should definitely add some tests for this.

* @return array
*/
function get_command_blacklist() {
if ( defined( 'WP_CLI_CRON_CONTROL_OFFLOAD_COMMAND_BLACKLIST' ) && is_array( \WP_CLI_CRON_CONTROL_OFFLOAD_COMMAND_BLACKLIST ) ) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be blocking cli-cron-offload / CLI_NAMESPACE by default to avoid weird infinite loops?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes indeed. I thought I'd included that, but I can't find it. Adding to the is_command_allowed() function.


// If there's a whitelist, default to it.
if ( ! empty( get_command_whitelist() ) ) {
add_filter( 'wp_cli_cron_control_offload_is_command_allowed', __NAMESPACE__ . '\command_is_whitelisted', 9, 2 );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why does this need to be a filter? Why can't we evaulate the in_array and pass the value into apply_filters below?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used this approach to allow for no whitelist, if someone wanted to be dangerous. That said, they could hook __return_true late, so there's no need for it.

*/
function validate_command( $command ) {
$command = trim( $command );
$command = parse_command( $command );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the parse / implode feel a bit dangerous and maybe even unnecessary. Looking through, there are some things we want to do:

  1. find out which command is being called (so we can check against the whitelist/blacklist).
  2. prevent bad positionals (&, '|', etc.)

I think we could do both those things without the full complexity of breaking out the command and then rebuilding it.

The other option would be to change how the command is executed so that instead of accepting a freeform string, we ask for a explicit structure (i.e. the command needs to be formatted as {command} {subcommand} {flags}; anything else fails validation).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the logic comes from WP-CLI, and arose from wanting to accomodate WP_CLI::runcommand(). It doesn't expect the leading wp (trivial), but also passes along many of the global associative arguments, so there's a bit of need to clean the command. WP-CLI also doesn't expose its validation methods for reuse.

I like your idea to change how the command is accepted. That'll remove a lot of the magic.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm leaning towards breaking command, subcommand and flags into their own associative arguments, removing all of the magic from parsing the command.

@ethitter
Copy link
Contributor Author

what's the benefit of using this vs. just executing the command via vipd + host action?

This came originally from the idea of eliminating future-dated hosted actions, as well as being able to run commands on the CLI containers rather than our sandboxes (sitemaps, for example). It was also somewhat inspired by a client already doing essentially this--they've built a UI of preset commands, and can run them via cron as needed. While I see this as initially meant for those with a Go sandbox, I could see a UI and reviewed commands permitted via the whitelist.

@philipjohn
Copy link

Is this likely to get worked on at all? We've had a few examples recently where we've wanted to offload a CLI command to the CLI containers, and @david-binda came up with his own PHP-based workaround recently but it'd be good to something more permanent.

@mjangda
Copy link
Member

mjangda commented Feb 11, 2018

@philipjohn We're going to stick with @david-binda's workaround for now until we get proper wp-cli support baked in (i.e. can be run by VIPs, provides real-time-ish output, etc.).

We may revisit this if that ends up being pushed out in terms of timelines.

@ethitter ethitter removed their assignment Dec 29, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants