autobahn-python icon indicating copy to clipboard operation
autobahn-python copied to clipboard

Make WAMP component API first class / recommended

Open oberstet opened this issue 6 years ago • 3 comments

This issue is to finally move the new **WAMP Component API ("new API") into "first class support" stage.

Eg here is an example https://github.com/crossbario/autobahn-python/blob/master/examples/twisted/wamp/component/frontend_scram.py


Things that come to mind for this:

  • docs, README, samples
  • compleness (auth methods etc)
  • ..

The issues that have been opened over the time where we discussed various approaches to "new API", until we settled on the component API:

  • https://github.com/crossbario/autobahn-python/issues/208
  • https://github.com/crossbario/autobahn-python/issues/313
  • https://github.com/crossbario/autobahn-python/issues/463
  • https://github.com/crossbario/autobahn-python/issues/472
  • https://github.com/crossbario/autobahn-python/issues/474
  • https://github.com/crossbario/autobahn-python/issues/674
  • https://github.com/crossbario/autobahn-python/issues/905

oberstet avatar Mar 23 '18 11:03 oberstet

May I suggest Flask Blueprints API design?

Flask, basically, splits Component into two parts: the top-level server (flask.Flask) and module-level components (flask.Blueprint). This allows developers to build modular components exposing blueprints and register all of them in the application root, thus leaving the configuration up to the application. Here is how I would love to use Autobahn:

# FILE: app/modules/demo/__init__.py

from autobahn.asyncio.component import Component

demo_component = Component(common_prefix='com.example.demo.')

# NOTE: the function gets automatically registered with
# `com.example.demo.random` id (i.e. `common_prefix` + function name)
@demo_component.register
async def random(*args, **kwargs):
    return 42


# FILE: app/modules/__init__.py
from . import demo

components = [
    demo.demo_component,
]


# FILE: app/__init__.py
from .modules import components

def create_app():
    app = autobahn.Server(
        'ws://127.0.0.1:8080/ws',
        authentication={...},
        ...
    )
    for component in components:
        app.register_component(component)

    return app

if __name__ == '__main__':
    app = create_app()
    app.run()

frol avatar Aug 04 '19 18:08 frol

@frol Some of the above registration stuff you can "kind-of" accomplish in certain ways .. I do like the idea of "an API-provider that can be registered at a WAMP prefix". I find myself wanting this very often. I think the Component API in WAMP already embodies the concept of "connecting to a place, and authorization plus possible re-connection" .. which is what I think you mean with Server above?

So perhaps what's wanted (instead of changing / expanding what Component abstracts) is a new thing that knows about "an API" and can register it (optionally at a prefix); then it can be paired with a Component (or even just a Session) to register that API (i.e. uses Component.register or Session.register to hook up the methods). I could imagine, for example, a third-party library that wants to provide optional Autobahn support could then implement one of these new things. Ideally maybe it also knows about the concept of "readiness" (i.e. "is this API ready to go") and can publish that fact. Perhaps it also knows about dependencies (i.e. "this API needs APIs A and B to be ready first").

meejah avatar Aug 04 '19 20:08 meejah

(Perhaps this discussion should move to some related-but-new ticket?) One thing the @demo_component.register sketch above misses is, "what about publishing?". So if random wants to publish something, it needs a currently-valid session.

Here is something similar, which should function right now:

# appfoo/login.py                                                                                              

import lmdb
from autobahn import wamp
from autobahn.wamp.types import Deny


async def on_join(session, details):
    env, db = await _create_db_connection()
    login = _ApplicationLogin(env, db, session)
    # if required, could session.on('leave', cleanup) for example
    await session.register(login, prefix="com.appfoo.auth.")   # like "register_component")
    session.publish("com.appfoo.auth.ready", True)
    return


async def _create_db_connection():
    env = lmdb.open(
        path='...',
        max_dbs=16,
    )
    db = env.open_db(b'auth_db')
    return env, db


class _ApplicationLogin(object):

    def __init__(self, env, db, session):
        self.env = env
        self.db = db
        self.session = session

    async def _get_pubkey_and_role(self, authid):
        with lmdb.Transaction(env, db) as txn:
            data = txn.get(pubkey.encode('ascii'))
            if data is None:
                raise KeyError("No such authid '{}'".format(authid))
            user = json.loads(data.decode('utf8'))
            return (user['pubkey'], user['role'])

    # this will end up at "com.appfoo.auth.authenticate"                                                       
    @wamp.register(None)
    async def authenticate(self, realm, authid, extra):
        try:
            pubkey, role = await self._get_pubkey_and_role(authid)
        except KeyError:
            return Deny()
        return {
            "role": role,
            "pubkey": pubkey,
        }

As a bonus, this lets you hook up "a valid session" (and, for example, register this thing on multiple components / session). It also handles dis- and re-connect well, because the "on_join" handler will run every time we re-connect to the router. There are two ways to hook this up; in code you could do this:

comp = Component(...)
from appfoo import login
comp.on('join', login.on_join)

If using crossbar, you can add a function type component in the config:

              {
                    "type": "function",
                    "callbacks": {
                        "join": "appfoo.login.on_join"
                    },
                    "realm": "com.example.authenticator",
                    "role": "auth"
              },

Perhaps there's a way to abstract this a little more and also possibly avoid some boilerplate?

meejah avatar Aug 04 '19 22:08 meejah