Portable Classes and Dependencies

What I’m trying to do:
I’d like to return an instance of a portable class from a server function in a dependency app.

What I’ve tried and what’s not working:
I can’t find a way to specify the class to instantiate.

The dependency can’t import from the main app, so an import doesn’t work.

Classes aren’t serializable, so I can’t pass it as an argument to the function.

The only way I’ve found is to pass a different instance as an argument and use its class in the function, but that’s revolting!

Anyone got any ideas?

I’ve been thinking about this since you posted it, but haven’t come up with any better approaches.

The normal Python approaches to providing info to a module (callbacks, etc) don’t work in this context. What you really need is a way to package the class up in a way that is serializable, and your workaround effectively does that.

1 Like

Hi @owen.campbell,

I’m having to infer in between the blanks here, but it sounds to me like what you want is a server function that will instantiate an arbitrary(?) class nominated by client code(!). (For clarity, if it weren’t client code, the answer would be easy - if you’re already on the server, you don’t have to anvil.server.call() into your dependency, just import the module and call a function, passing in a class as an argument if you want.)

Now, my first reaction is alarm, because allowing the client to control what class you instantiate is a really scary idea. Untrusted client code could get up to all sorts of mischief, and even if you believe that all the classes in your code are safe to instantiate in arbitrary ways, there is an entire class of vulnerabilities (they’re generally called “deseralisation vulnerabilities”) proving that this belief is usually wrong - if an attacker can cause your trusted code to instantiate a class of its choice, you’re probably hosed.

Fortunately, the answer to this is the same thing I would have proposed anyway, which is to maintain an “allow-list”. Maintain a set of classes that your server function is allowed to instantiate, and allow the client to pick only from that list. Something like:

ALLOWED_CLASSES = {'Widget': Module1.Widget, 'Badger': Animals.Badger}

@anvil.server.callable
def instantiator(cls_name, arg):
  cls = ALLOWED_CLASSES[cls_name]
  return cls(arg)

If the instantiator function is defined in a dependency, no problem! Your main app’s code can add classes to that dictionary, like so:

# In my main app
import MyDependency.Instantiation

class Wombat:
  def __init__(self, x):
     # This code must be very, very careful because it can be called by
     # untrusted code with an arbitrary argument

MyDependency.Instantiation.ALLOWED_CLASSES["Wombat"] = Wombat

This code is now safe, because only nominated classes can be instantiated, but flexible, because your main app can add additional classes to that list.


If you want to be even fancier, you could make it a decorator. For example:

# In the dependency:

ALLOWED_CLASSES = {'Widget': Module1.Widget, 'Badger': Animals.Badger}

def untrusted_code_can_instantiate_me(cls):
  """Use this function as a decorator for classes that are safe for
     untrusted code to instantiate."""
  ALLOWED_CLASSES[cls.__name__] = cls
  return cls

@anvil.server.callable
def instantiator(cls_name, arg):
  # ..etc..

and then:

# in the main app:
from MyDependency.Instantiation import untrusted_code_can_instantiate_me

@untrusted_code_can_instantiate_me
class Wombat:
  def __init__(self, x):
     # This code must be very, very careful because it can be called by
     # untrusted code with an arbitrary argument
2 Likes

In a dependency server module , how does Module1 get imported so it can be referred to in the ALLOWED_CLASSES dict ?

That’s the nub of this.

I’m not looking for arbitrary instantiation. I just have no way of importing from the main app to define what’s OK.

In my example above, Module1 was a module in the dependency. The Wombat class (which is defined in the main app) is “pushed” into the ALLOWED_CLASSES dict when the main app’s server module loads.

Before handling an anvil.server.call(), all of an app’s Server Modules are loaded (this is necessary to trigger all the @anvil.server.callable decorators so Anvil knows what server functions exist!). This means that, by the time the instantiate() server function runs, the top-level code in the main app’s server modules has already executed, so Wombat is already in ALLOWED_CLASSES.

Does that make sense?

1 Like

OK. I think I can get that idea to do what I need. Thank you!

I promise I’m not trying to get client code to define the required class here. I already have that info on the server side, just not in the dependency. I may use a server side init function to do the injection.

2 Likes