Cómo implementar la función multiusuario con espacios de nombres

La API de espacios de nombres te permite habilitar con facilidad la arquitectura multiusuario en tu aplicación mediante la selección de una string de espacio de nombres para cada instancia en appengine_config.py con el paquete namespace_manager.

Cómo configurar el espacio de nombres actual

Puedes obtener, configurar y validar espacios de nombres con namespace_manager. El administrador de espacios de nombres te permite configurar un espacio de nombres actual para las API con espacio de nombres habilitado. Configura un espacio de nombres actual inicial con appengine_config.py para que el almacén de datos y Memcache lo usen de forma automática.

La mayoría de los desarrolladores de App Engine usarán su dominio de G Suite como el espacio de nombres actual. G Suite te permite implementar tu aplicación en cualquiera de tus dominios, por lo que puedes usar fácilmente este mecanismo para configurar espacios de nombres diferentes para dominios diferentes. Luego, puedes usar esos espacios de nombres independientes para segregar datos en todos los dominios. Para obtener más información sobre la configuración de múltiples dominios en G Suite Dashboard, consulta Implementar tu aplicación en tu URL de G Suite.

El ejemplo de código a continuación te muestra cómo configurar el espacio de nombres actual en el dominio de G Suite que se usó para asignar la URL. En particular, esta string será la misma para todas las URL asignadas a través del mismo dominio de G Suite.

Para establecer un espacio de nombres en Python, usa el sistema de configuración App Engine appengine_config.py en el directorio raíz de tu aplicación. El sencillo ejemplo a continuación muestra cómo usar tu dominio de G Suite como el espacio de nombres actual:

from google.appengine.api import namespace_manager

# Called only if the current namespace is not set.
def namespace_manager_default_namespace_for_request():
    # The returned string will be used as the Google Apps domain.
    return namespace_manager.google_apps_namespace()

Si no especificas un valor para namespace, el espacio de nombres se establece en una string vacía. La string namespace es arbitraria, pero también se limita a un máximo de 100 caracteres alfanuméricos, puntos, guiones bajos y guiones. Más explícitamente, las strings de espacio de nombres deben coincidir con la expresión regular [0-9A-Za-z._-]{0,100}.

Por regla general, todos los espacios de nombres que comienzan con "_" (guion bajo) se reservan para el uso del sistema. La regla de espacio de nombres del sistema no es obligatoria, pero puedes encontrar fácilmente consecuencias negativas no definidas si no la respetas.

Para obtener más información general sobre la configuración de appengine_config.py, consulta la configuración del módulo de Python.

Cómo evitar la filtración de datos

Uno de los riesgos asociados comúnmente con las aplicaciones multiusuario es el peligro de que los datos se filtren a través de los espacios de nombres. Las filtraciones de datos no deseadas pueden surgir de varias fuentes, incluidas las siguientes:

  • El uso de espacios de nombres con las API de App Engine que todavía no admiten los espacios de nombres. Por ejemplo, Blobstore no admite espacios de nombres. Si usas Espacios de nombres con Blobstore, debes evitar el uso de consultas de Blobstore para solicitudes de usuario final, o claves de Blobstore de fuentes no confiables.
  • El uso de un medio de almacenamiento externo (en lugar de Memcache y almacén de datos), a través de URL Fetch o de algún otro mecanismo, sin proporcionar un esquema de compartimentación para los espacios de nombres.
  • La configuración de un espacio de nombres en función del dominio de correo electrónico del usuario. En la mayoría de los casos, no querrás que todas las direcciones de correo electrónico de un dominio tengas acceso a un espacio de nombres. El uso del dominio de correo electrónico también evita que tu aplicación use un espacio de nombres hasta que el usuario haya accedido.

Cómo implementar espacios de nombres

En las secciones siguientes se describe cómo implementar espacios de nombres con otras API y herramientas de App Engine.

Cómo crear espacios de nombres por usuario

Algunas aplicaciones necesitan crear espacios de nombres por usuario. Si deseas compartimentar datos a nivel de usuario para aquellos que accedieron, considera usar User.user_id(), que muestra un ID único permanente correspondiente al usuario. El siguiente ejemplo de código muestra cómo usar las API de usuario con este propósito:

from google.appengine.api import users

def namespace_manager_default_namespace_for_request():
    # assumes the user is logged in.
    return users.get_current_user().user_id()

Normalmente, las aplicaciones que crean espacios de nombres por usuario también proporcionan páginas de destino específicas a usuarios diferentes. En estos casos, la aplicación necesita proporcionar un esquema de URL que dicte qué página de destino se le debe mostrar al usuario.

Cómo usar espacios de nombres con Datastore

De forma predeterminada, el almacén de datos usa el espacio de nombres actual configurado en el administrador de espacios de nombres para solicitudes del almacén de datos. La API aplica este espacio de nombres actual a los objetos Key o Query cuando se crean. Por lo tanto, debes tener cuidado si una aplicación almacena objetos Key o Query en formularios serializados, ya que el espacio de nombres se conserva en esas serializaciones.

Si usas objetos Key y Query deserializados, asegúrate de que se comporten como se espera. La mayoría de las aplicaciones simples que usan un almacén de datos (put/query/get) sin utilizar otros mecanismos de almacenamiento funcionarán como se espera configurando el nombre de espacio actual antes de llamar a cualquier API de almacén de datos.

Los objetos Query y Key demuestran los comportamientos únicos a continuación con respecto a los espacios de nombres:

  • Los objetos Query y Key heredan el espacio de nombres actual cuando se construyen, salvo que establezcas un espacio de nombres explícito.
  • Cuando una aplicación crea una Key nueva desde un principal, la Key nueva hereda el espacio de nombres del principal.

El ejemplo de código a continuación muestra un controlador de solicitudes de muestra que incrementa un recuento en el almacén de datos para el espacio de nombres global y un espacio de nombres especificado arbitrariamente.

from google.appengine.api import namespace_manager
from google.appengine.ext import ndb
import webapp2

class Counter(ndb.Model):
    count = ndb.IntegerProperty()

@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count

class DatastoreCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = update_counter('counter')

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = update_counter('counter')
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))

app = webapp2.WSGIApplication([
    (r'/datastore', DatastoreCounterHandler),
    (r'/datastore/(.*)', DatastoreCounterHandler)
], debug=True)

Usa espacios de nombres con Memcache

De forma predeterminada, Memcache usa el espacio de nombres actual del administrador de espacios de nombres para solicitudes de Memcache. En la mayoría de los casos, no es necesario que configures explícitamente un espacio de nombres en Memcache, y hacerlo podría generar errores inesperados.

Sin embargo, existen algunas instancias únicas donde es apropiado configurar explícitamente un espacio de nombres en Memcache. Por ejemplo, tu aplicación podría tener datos comunes compartidos a través de todos los espacios de nombres (como una tabla que contiene códigos de país).

El fragmento de código siguiente muestra cómo configurar el espacio de nombres en Memcache de manera explícita:

Mediante el uso de la API de Python para Memcache, puedes obtener el espacio de nombres actual del administrador del espacio de nombres o configurarlo de manera explícita cuando crees el servicio de Memcache.

El ejemplo de código siguiente muestra un controlador de solicitudes de muestra que incrementa un recuento en Memcache para el espacio de nombres global y un espacio de nombres especificado arbitrariamente.

from google.appengine.api import memcache
from google.appengine.api import namespace_manager
import webapp2

class MemcacheCounterHandler(webapp2.RequestHandler):
    """Increments counters in the global namespace as well as in whichever
    namespace is specified by the request, which is arbitrarily named 'default'
    if not specified."""

    def get(self, namespace='default'):
        global_count = memcache.incr('counter', initial_value=0)

        # Save the current namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            namespace_count = memcache.incr('counter', initial_value=0)
        finally:
            # Restore the saved namespace.
            namespace_manager.set_namespace(previous_namespace)

        self.response.write('Global: {}, Namespace {}: {}'.format(
            global_count, namespace, namespace_count))

app = webapp2.WSGIApplication([
    (r'/memcache', MemcacheCounterHandler),
    (r'/memcache/(.*)', MemcacheCounterHandler)
], debug=True)

El espacio de nombres se establece de manera explícita cuando almacena un valor en Memcache, como se muestra en el siguiente ejemplo:

  // Store an entry to the memcache explicitly
memcache.add("key", data, namespace='abc')

Usa espacios de nombres con la lista de tareas en cola

De forma predeterminada, las listas de aplicaciones en cola usan el espacio de nombres actual tal como se configura en el administrador de espacios de nombres en el momento que se creó la tarea. En la mayoría de los casos, no es necesario que configures explícitamente un espacio de nombres en la lista de tareas en cola, y hacerlo podría generar errores inesperados.

Los nombres de las tareas se comparten en todos los espacios de nombres. No puedes crear dos tareas con el mismo nombre, aunque usen espacios de nombres diferentes. Si deseas usar el mismo nombre de tarea para varios espacios de nombres, puedes simplemente agregar cada espacio de nombres al nombre de tarea.

Cuando una tarea nueva llama al método de lista de tareas en cola add(), esta lista copia el espacio de nombres actual y el dominio de G Suite del administrador de espacios de nombres (si corresponde). Cuando se ejecuta la tarea, se restablecen el espacio de nombres actual y el del G Suite.

Si el espacio de nombres actual no se configura en la solicitud inicial (en otras palabras, si get_namespace() muestra ''), puedes usar set_namespace() a fin de configurar el espacio de nombres actual para la tarea.

Existen algunas instancias únicas donde es apropiado configurar de manera explícita un espacio de nombres para una tarea que funcione en todos los espacios de nombres. Por ejemplo, puedes crear una tarea que acumule estadísticas de uso a través de todos los espacios de nombres. Podrás entonces configurar explícitamente el espacio de nombres de la tarea. El siguiente ejemplo de código muestra cómo configurar de manera explícita los espacios de nombres con la lista de tareas en cola.

from google.appengine.api import namespace_manager
from google.appengine.api import taskqueue
from google.appengine.ext import ndb
import webapp2

class Counter(ndb.Model):
    count = ndb.IntegerProperty()

@ndb.transactional
def update_counter(name):
    """Increment the named counter by 1."""
    counter = Counter.get_by_id(name)
    if counter is None:
        counter = Counter(id=name, count=0)

    counter.count += 1
    counter.put()

    return counter.count

def get_count(name):
    counter = Counter.get_by_id(name)
    if not counter:
        return 0
    return counter.count

class DeferredCounterHandler(webapp2.RequestHandler):
    def post(self):
        name = self.request.get('counter_name')
        update_counter(name)

class TaskQueueCounterHandler(webapp2.RequestHandler):
    """Queues two tasks to increment a counter in global namespace as well as
    the namespace is specified by the request, which is arbitrarily named
    'default' if not specified."""
    def get(self, namespace='default'):
        # Queue task to update global counter.
        current_global_count = get_count('counter')
        taskqueue.add(
            url='/tasks/counter',
            params={'counter_name': 'counter'})

        # Queue task to update counter in specified namespace.
        previous_namespace = namespace_manager.get_namespace()
        try:
            namespace_manager.set_namespace(namespace)
            current_namespace_count = get_count('counter')
            taskqueue.add(
                url='/tasks/counter',
                params={'counter_name': 'counter'})
        finally:
            namespace_manager.set_namespace(previous_namespace)

        self.response.write(
            'Counters will be updated asyncronously.'
            'Current values: Global: {}, Namespace {}: {}'.format(
                current_global_count, namespace, current_namespace_count))

app = webapp2.WSGIApplication([
    (r'/tasks/counter', DeferredCounterHandler),
    (r'/taskqueue', TaskQueueCounterHandler),
    (r'/taskqueue/(.*)', TaskQueueCounterHandler)
], debug=True)

Cómo usar espacios de nombres con Blobstore

Blobstore no está segmentado por espacio de nombres. Para conservar un espacio de nombres en Blobstore, debes acceder a Blobstore a través de un medio de almacenamiento que tenga en cuenta el espacio de nombres (actualmente solo Memcache, almacén de datos y lista de tareas en cola). Por ejemplo, si una Key de blob se almacena en una entidad de almacén de datos, puede acceder con una Key o Query de almacén de datos que tiene en cuenta el espacio de nombres.

Si una aplicación accede a Blobstore a través de claves guardadas en un almacenamiento que tiene en cuenta el espacio de nombres, Blobstore no necesita ser segmentado por espacio de nombres. Las aplicaciones deben evitar las filtraciones de BLOB entre espacios de nombres al:

  • No usar BlobInfo.gql() para solicitudes de usuario final. Puedes usar las consultas de BlobInfo para solicitudes administrativas (como la creación de informes sobre todos los BLOB de aplicación), pero usarlas para solicitudes de usuario final puede provocar filtraciones de datos, ya que no todos los registros de BlobInfo están compartimentados por espacio de nombres.
  • No deben usar claves de Blobstore de fuentes no confiables.

Cómo configurar espacios de nombres para consultas de Datastore

En Google Cloud Platform Console, puedes configurar el espacio de nombres para consultas de Datastore.

Si no quieres usar el predeterminado, selecciona el espacio de nombres que deseas usar del menú desplegable.

Cómo usar espacios de nombres con el cargador masivo

El cargador masivo admite una marca --namespace=NAMESPACE que te permite especificar el espacio de nombres que usar. Cada espacio de nombres se maneja de manera independiente y, si deseas acceder a todos los espacios de nombres, deberás iterar a través de ellos.

Cuando creas una instancia nueva de Index, se asigna al espacio de nombres actual de forma predeterminada:

# set the current namespace
namespace_manager.set_namespace("aSpace")
index = search.Index(name="myIndex")
# index namespace is now fixed to "aSpace"

También puedes asignar un espacio de nombres de manera explícita en el constructor:

index = search.Index(name="myIndex", namespace="aSpace")

Una vez que creaste una especificación de índice, su espacio de nombres no se puede cambiar, como se muestra a continuación:

# change the current namespace
namespace_manager.set_namespace("anotherSpace")
# the namespaceof 'index' is still "aSpace" because it was bound at create time
index.search('hello')