Registering a namespace without installing the corresponding app

I’m finding myself in quite a particular situation:

I’m developing a plugin for a platform called WebODM, which is Django-based. The plugin should leverage a package named social_django, which enables implementing OAuth2 authentication in Django applications.

The way you would normally use social_django is:

  • you add it to INSTALLED_APPS:
INSTALLED_APPS = (
    ...
    'social_django',
    ...
)
  • you add any authentication backends you wish to use to the AUTHENTICATION_BACKENDS setting:
AUTHENTICATION_BACKENDS = (
   'your_module.your_backend',
   ...
)
  • you add the login/logout URLs to the global urlpatterns:
urlpatterns = patterns('',
    ...
    url('', include('social_django.urls', namespace='social'))
    ...
)

Since all of these steps entail modifying the source code of WebODM, I’m looking for a way to not have to make these changes to the code.

As of now, I’ve been able to work around the second step by just adding the authentication backend at runtime, when the plugin is initialized. I read on Django docs that it is advised against mutating the settings after application startup, but this seems to work for my use case and I haven’t been able to come up with an alternative approach for this.

As for adding the entry to INSTALLED_APPS, I don’t think this is necessary to register the app for it to work.

Now comes the actual issue: making the URLs accessible.

WebODM plugins can define a method named root_mount_points() which allows defining URLs that are added directly to the global urlpatterns for the project. WebODM does this:

# urls.py

urlpatterns = [
    ...
] + root_url_patterns()
def root_url_patterns():
    result = []
    for p in get_active_plugins():
        for mount_point in p.root_mount_points():
            result.append(url(mount_point.url, mount_point.view, *mount_point.args, **mount_point.kwargs))
            
    return result

So what I simply did to expose the login path (and override Django normal login) is define this root mountpoint in the plugin:

    def root_mount_points(self):
        """
        Overwrite the login url of WebODM to only allow login via OAuth2
        """
        return [
            MountPoint("login/", LoginView.as_view()),
        ]

Where LoginView is defined like this in the plugin:

class LoginView(APIView):
    permission_classes = (AllowAny,)

    def get(self, request):
        from social_django.views import auth

        return auth(request, "name_of_my_backend")

It’s a bit of a hack, but basically I import the view (called auth) from social_django and return it manually called with my request and auth backend (which would normally have to be included in the URL, if using social_django).

Here’s the source of the view auth: social-app-django/social_django/views.py at 1c6f8535b0dffeeaa9b738868626bb66345c5a2a · python-social-auth/social-app-django · GitHub

The issue is that, when I visit the /login URL, I get “‘social’ is not a registered namespace”.

This is due to the fact that the following decorator is used: social-app-django/social_django/utils.py at 1c6f8535b0dffeeaa9b738868626bb66345c5a2a · python-social-auth/social-app-django · GitHub, which in turn uses reverse to set a redirect_uri to be used at the end of the login flow.

Because social_django is not an installed app and I haven’t included its URLs under the namespace, used on the annotation above the view function auth (@psa(f"{NAMESPACE}:complete")), reverse fails to resolve the URL.

Is there a way to get around this? Even monkey patching social_django in the plugin would be fine.

Am I going about this the complete wrong way? How would you make this hack work?

This actually is not an accurate statement. The majority of these changes are to the settings file, which is something that can be referenced at run-time. Run your project referencing your settings instead of the provided defaults.

You can also use the same technique to modify the urls file. Your settings file can reference your urls file, which can import the existing urls file and add your entries to it.

If I understand correctly, you mean changing the DJANGO_SETTINGS_MODULE setting to reference a settings file provided by me?

That could be a solution, but I would prefer to keep everything inside of the plugin code and not touch the host system at all, not even the settings file.

Is there another way?

Yes

You don’t have to touch the existing system. You’re replacing the default settings file with a reference to your own settings file.

I can’t think of another way that is as clean or works as Django is designed.

I understand that; I probably did not explain myself well.

In order to do this, I still have to include the new settings file in the repository of the main application, since it has to be available at startup time. This can be done relatively easily, even though the application is being launched via docker-compose and a pre-made script which cannot be touched.

However, given that I won’t be the person who maintains this system, and given that the plugin needs to be installed by uploading a zip package from a page in the admin panel of WebODM, I thought the “cleanest” solution (in terms of simplicity of maintenance for the future administrators) would be to just pack all changes that need to be done inside of the plugin, and the user simply has to install it on WebODM like they’d do with any other plugin, as opposed to having to install the plugin AND provide a custom settings file.

It seems to me that this:

may actually be the root problem.

From what you describe, I get the impression that you’re trying to do more with your plugin than what WebODM is designed to provide. (You might want to look at the existing plugins to see how they may address some of these issues. If they don’t, then I’d suggest either requesting information from them on this, or acknowledging that this really isn’t an appropriate approach.)

Under those circumstances, I wouldn’t be thinking of creating this as a plugin, but as a “wapper” or external project built on top of WebODM.