Art of Making Plugins

Hi.
I’m making a new application which is full and full powered by plugins. After discussing with a few friends and developers, they suggested me to make the application in such a way that the plugins have restrictive access, so that users privacy and security can be ensured.

The application and plugins will be in Python. I decided to make the application using an API-kinda thing. That is, my application will be a separate object (Python object, based on Gtk.Application), there will be a bridge object and plugins.

Plugins --- Bridge --- Core

So whenever a plugin wants to do something, like say it wants to show a notification to user, it will make a request to Bridge. The Bridge will forward the request to Core. If the request is legible, then Core will reply that it has accepted and do the required action (like showing a notification), otherwise it will give a reject message and Bridge will tell this to the plugin.

As I said already, everything will be plugin based, so Core itself behaves like a plugin. If a plugin wants to communicate with other plugin, it will use the same mechanism. The request mechanism.

At programming level, this is how I have planned to implement, (rough sketch):

import gi, blah, blah # All import statements


class MyPlugin(GObject.Object):
    def __init__(self, name, bridge):
        self.name = name
        self.bridge = bridge

   def send_notification_request(self):
        reply = self.bridge.request(id=123, from=self.name,
                                    from_obj=self, to="Core",
                                    message="notify-users",
                                    args=["Hello users this is from your MyPlugin...",])
        if reply:
            print("Request Accepted")
        else:
            print("Request Rejected")

Now Bridge will take the request. First it will verify if the from_obj is the same as the one mentioned in from string. (This is done to prevent other plugins from cheating). Bridge has an internal dictionary, that maps the name of strings to the plugins with that name. Now, Bridge will hash from_obj using Python’s hash function. Similarly it will obtain the hash of plugin pointed by the from string from the dictionary, and check if the hashes are equal. Using this validation is done.

After validation, it will access the to plugin and send the message by calling its request method with all the arguments.

So this is how the application works. Whenever a plugin wants to communicate, it will use the bridge to send the request to another plugin. The request calls and communications will be pure Python function calls. Every plugin will have reference only to the bridge and no other plugin. This prevents any mishandling or misuse of plugins. The entire application life-cycle based on this working. How is it? What improvements do you want suggest? Please share your feedback.

Now comes the second part. As I said, the requests will be pure Python function calls, like obj.call(msg), this Pythonic way could be slow, inefficient or even not so attractive. I had heard some fancy names like DBus, Sockets, Process, Threads etc. Should I use something like that instead? What is the proper and best way to handle the mechanism I described above? Please tell your comments on this too

Thank You for reading till here. I welcome all kinda feedback, and whenever you are suggesting a new “mechanism”, please explain it as much as you can, or point to a nice tutorial, it will really be helpful for me.

This is kind of what I’m doing with Geary’s plugins. There is a set of limited interfaces (e.g. Plugin.Folder in pligin-folder.vala) and implementations of what you call the bridge (I’d call them proxies). Plugins are given access to objects that implement these interfaces, but the implementations of these are private. As a result, the guts don’t leak to plugins and plugins can only call the methods defined by the interfaces.

However, the aim of this approach is something different to yours - it’s done for API stability. The plugin interfaces are simple and frozen, so they won’t change in an incompatible way. But since the underlying implementation is private, it can change as needed to fix bugs, add new features, etc. This means that plugins should keep working between different version of Geary, unlike in the bad old days for Firefox, Thunderbird and GNOME shell, where each new version required every plugin to be updated.

Your goal of ensuring people’s privacy is an important and noble, but I don’t think it is actually possible in the way you describe. A plugin has access to anything that is global in a process (such as singleton objects in your app, in GTK, and GObject, etc), which can be used to work around any restrictions you put in place. Also, even if you lock all of that down, a plugin can access things it shouldn’t via the file system. For example, with Geary, if I denied access to a plugin to the sender’s name or email address for an account, a plugin could just open the config file on the file system and read these itself.

The only way to prevent this sort of thing happening is to launch the plugins in a separate process, and isolate that process from your app. This is exactly what Flatpak does - each app is isolated from your computer and from other apps, because they run isolated in their own containers. It may be possible to achieve what you are looking for, but it may well be a lot of work, it might not perform very well, and it might still be possible for clever plugin authors to work around - look at how much work Flatpak has to put in to make that work. Note that not even Flatpak can protect (or tries to protect) an app from its plugins.

1 Like

Wow. Thanks for your comment. You said you use frozen and private methods in Vala plugins. But unfortunately (or fortunately) it is impossible to define a private attribute in Python. So I think I should make my plugins like this : Keep the trust on plugins and keep the basic code secure.

That is, I will try to make the application and bridge as secure as possible. The rest I leave it to the plugin developer. I will make an official list of plugins supported for my applications. All the plugins uploaded to the list will be scanned and checked if they contain any malicious habit (not by inspecting code but by behavior) . Users can still report any plugin for its misbehavior. Its left to user if they want to install an unapproved plugin.

So considering the lack of private attributes in Python and “complete secure” way, I’m forced to settle with the idea of separate process. All the plugins will be in a separate thread, the bridge will be in a separate thread and application in a separate thread.

The question is; can you tell me how can I do it? How can I enable communications between the different threads? Please tell me a method that supports Python objects (at least basic types), I know Redis can solve such trouble but I want to use (as far as possible) only GI modules and standard Python modules. Can you help me here?

Hello,

I agree with @mjog, preventing plugins from invading privacy looks really hard. It also put a lot of constraints on your design. For small applications (let’s say less than 1 millions users), I doubt it’s worth the effort.

For Paperwork 2, I’m going precisely the other way: I’ve reduced the core to a bare minimum. I’m letting plugins publish methods and call any other plugin methods in a huge free-for-all. It works surprisingly well with Python and GTK. In my opinion, this has made the code much more flexible and well-organized (well, compared to that spaghetti mess that is Paperwork 1.3.1 anyway).

2 Likes

That’s also nice. My approach is however, as you all know is still not a bullet-proof one. I’m now trying to use a DBus based approach. Let me see how your method is.

@jflesch sorry it got late to take a look at Paperwork. It is awesome and read the docs on how your plugins work. In that, your are using something like: core.call(method_name, args). Here since, the method_name is a string, will core look for methods named method_name in all of plugins? (by using, dir, getattr) ? What will happen if two plugins have same method_name? Whose method will be called?

Here since, the method_name is a string, will core look for methods named method_name in all of plugins?

Yes. That’s the idea.

It does the lookup when the plugin is loaded and index their methods based on the method names.

What will happen if two plugins have same method_name ? Whose method will be called?

That depends on whether you used core.call_all(), core.call_success(), core.call_one():

  • call_all() will call them all
  • call_success() will call them one after the other until one of them return a value != None.
  • call_one() will call only one

https://doc.openpaper.work/openpaperwork_core/latest/core_api.html

The order in which the plugin methods are called is defined by the plugin priority. A plugin with a priority of 9999 will always get its methods called before those of a plugin with a priority of 10. It allows overloading plugin methods with other methods.

If you want to have a look at the code, it is in the branch ‘develop’.

1 Like

Well, that’s the thing - you can’t use threads, they suffer all of the problems I mention above. Each plugin would need to be a separate process (i.e. a separate program), and communicate via an IPC mechanism such as DBus (for a GNOME-specific technology) or the Python multiprocessing module (for a Python-specific technology), and maybe use Linux containers for good measure (depending on your application).

I can’t give you any more help than that however, sorry. The right answer depends on your application and its requirements.

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.