errbotio / errbot

Errbot is a chatbot, a daemon that connects to your favorite chat service and bring your tools and some fun into the conversation.
http://errbot.io
GNU General Public License v3.0
3.09k stars 608 forks source link

Support a way to issue admin commands via a local socket? #824

Open harlowja opened 7 years ago

harlowja commented 7 years ago

During discussion (and agreement) that openstack would move its various bots (based on various python bot frameworks and code and such) @ http://lists.openstack.org/pipermail/openstack-dev/2016-July/thread.html#100409 one of the outcomes of that discussion was the the current configuration mechanism that is used to reload plugins and such is not really desired to be based on private IRC (or other backend) commands from a certain user to the bot.

The folks there (that will manage these plugins and the associated bot) have the following ideas (discussion for this is @ http://eavesdrop.openstack.org/irclogs/%23openstack-infra/%23openstack-infra.2016-07-29.log.html#t2016-07-29T18:17:00) or desires.

The first one there seems do-able with what exists, the second one seems like it could require some work(?), although it seems like the 'text' backend could be easily used to create a private-application socket backend that is used for this (so that there would be a public backend, and a private 'text' backend that is reachable via a local app socket).

gbin commented 7 years ago

Glad to hear that openstack is moving to Errbot !

I though a little bit about your point 2 over the weekend.

What about using the webhook feature for this, porting the features of https://github.com/errbotio/errbot/blob/master/errbot/core_plugins/plugins.py ?

(untested pseudocode to get the idea).

Should work by posting a {'name': 'myplugin'} to /reload_plugin:

    @webhook
    def reload_plugin(self, payload):
        if 'name' not in payload:
             return 'ERROR: malformed request'
        name = payload['name']
        if name not in self._bot.plugin_manager.get_all_plugin_names():
            return ("{} isn't a valid plugin name. The current plugins are:\n"
                   "{}".format(name, self.formatted_plugin_list(active_only=False)))
        try:
            self._bot.plugin_manager.reload_plugin_by_name(name)
            return "Plugin %s reloaded." % name
        except PluginActivationException as pae:
            return 'Error activating plugin %s: %s' % (name, pae)
harlowja commented 7 years ago

That might work, is that restricted to who can call those webhooks? I'd rather not have someone who gets the IP address of the bot (not a hard thing to get via a whois equivalent in IRC?) be able to trigger that reload (maliciously or not)? That's the nice thing about a local 'admin' socket is that its pretty hard to maliciously via some external actor trigger that.

gbin commented 7 years ago

The auth part:

You can use webhooks with a server-side SSL certificate + retrieve auth headers from the raw request:

https://github.com/errbotio/errbot/blob/master/docs/user_guide/plugin_development/webhooks.rst#the-raw-request.

Alternatively, I think a better solution would be to use client side certificates with a small nginx frontend, it has also the advantage of beeing flexible for the other public webhooks you implement (throttling etc, ...).

The local socket:

So unfortunately rocket doesn't seem to support local sockets, the closest thing would be a localhost one 127.x.x.x. rocket is basically unmaintained at that point so it might push us to come back to something like Flask that now support py3.

harlowja commented 7 years ago

So why wouldn't just a local backend that runs the text backend work? Would running a private to the app (local socket) backend and a public backend be that hard?

Something @ https://github.com/errbotio/errbot/blob/master/errbot/bootstrap.py#L122 that takes a backend like "text+irc" or "slack+irc" (the latter "slack+irc" doesn't yet need to happen, but would be nice).

What would happen if a thread was spun up for each backend there? Would they share the same plugins, or perhaps we can just have a tiny new backend, call it 'admin' that responds to a few selected local commands (and that's it); maybe its not a backend at that point but just a tiny class that is setup (as a thread) in https://github.com/errbotio/errbot/blob/master/errbot/bootstrap.py#L122

Something like:

try:
        bot = backendpm.get_plugin_by_name(backend_name)
        bot.attach_storage_plugin(storage_plugin)
        bot.attach_repo_manager(repo_manager)
        bot.attach_plugin_manager(botpm)
        bot.initialize_backend_storage()
        bot.initialize_admin_port()
    except Exception:
        log.exception("Unable to load or configure the backend.")
exit(-1)
harlowja commented 7 years ago

Any further thoughts here?

gbin commented 7 years ago

So it goes back to have a multibackend support: it would require a profound refactoring and would make Errbot a chat "bridge". I think while backends are maturing and have a cool stable API across we might try out one day to make this bridge, but for "feature creep" reasons it would be a separate product (imagine all the bloat about mapping identities between backends etc...).

Maybe making a simple socket on a thread on a plugin might be better if you really want to go the socket way: you will have access to the full bot from self._bot from the plugin so you can hook into the bot like:

        msg = Message(text_from_the_socket)
        msg.frm = self.build_identifier(self._bot.config['ADMINS'][0])  # pick any admin so it is recognized as an admin
        msg.to = self._bot.identifier  # direct message to the bot 
        self._bot.callback_message(msg)  # trigger the message ingestion

That should be all you need to do to inject an admin message from a socket buffer.

zoni commented 7 years ago

@gbin how would you feel about offering an optional way to override the configuration of plugins via config.py?

I'm envisioning something like this:


# Default, no override:
CONFIG_OVERRIDE = None
# Override configuration for the webserver plugin:
CONFIG_OVERRIDE = {
    'Webserver': 
        {'HOST': '0.0.0.0',
         'PORT': 3141,
         'SSL': {'certificate': '',
                 'enabled': False,
                 'host': '0.0.0.0',
                 'key': '',
                 'port': 3142
        }
}

This would allow people to set configuration from config.py which can easily be managed via a configuration management system such as puppet, chef, etc. Doing this is a lot easier to manage than doing it via errbot --storage-set, errbot --storage-merge, etc.

Going through the openstack discussions referenced in the original issue, I believe that if we offered this, we would have everything to satisfy their use-case (it's already possible to restrict/disable the option of on-the-fly (un)loading of plugins or changing their config via ACLs).

I'm aware you're more in the camp of dynamically configuring errbot via chat command but I see the value of a more static way to configure it via static configuration files.

sijis commented 4 years ago

I realize i'm going back to an older unresolved discussion but i thought I thought it would be beneficial to add some additional notes for future folks searching a similar issue.

Before closing this out, I just wanted to point out a few things that are now possible which could help.