Implementazione della multitenancy mediante gli spazi dei nomi

L'API Namespaces consente di abilitare facilmente la multitenancy nell'applicazione, semplicemente selezionando una stringa dello spazio dei nomi per ogni tenant in web.xml utilizzando il pacchetto NamespaceManager.

Impostazione dello spazio dei nomi corrente

Puoi recuperare, impostare e convalidare gli spazi dei nomi utilizzando NamespaceManager. Il gestore dello spazio dei nomi ti consente di impostare uno spazio dei nomi corrente per le API abilitate per lo spazio dei nomi. Hai la possibilità di impostare in anticipo uno spazio dei nomi utilizzando web.xml, che sarà utilizzato automaticamente da datastore e memcache.

La maggior parte degli sviluppatori di App Engine utilizzerà il proprio dominio Google Workspace (in precedenza G Suite) come spazio dei nomi attuale. Google Workspace ti consente di eseguire il deployment dell'app in qualsiasi dominio di tua proprietà, in modo da poter utilizzare facilmente questo meccanismo per configurare diversi spazi dei nomi per domini diversi. Quindi, puoi utilizzare questi spazi dei nomi separati per isolare i dati tra i domini. Per ulteriori informazioni, consulta la sezione Mappatura dei domini personalizzati.

Il seguente esempio di codice mostra come impostare lo spazio dei nomi corrente sul dominio Google Workspace utilizzato per mappare l'URL. In particolare, questa stringa sarà la stessa per tutti gli URL mappati tramite lo stesso dominio Google Workspace.

Puoi impostare gli spazi dei nomi in Java utilizzando l'interfaccia Filtro servlet prima di richiamare i metodi servlet. Il seguente semplice esempio mostra come utilizzare il tuo dominio Google Workspace come spazio dei nomi attuale:

// Filter to set the Google Apps domain as the namespace.
public class NamespaceFilter implements Filter {

  @Override
  public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
      throws IOException, ServletException {
    // Make sure set() is only called if the current namespace is not already set.
    if (NamespaceManager.get() == null) {
      // If your app is hosted on appspot, this will be empty. Otherwise it will be the domain
      // the app is hosted on.
      NamespaceManager.set(NamespaceManager.getGoogleAppsNamespace());
    }
    chain.doFilter(req, res); // Pass request back down the filter chain
  }

Il filtro dello spazio dei nomi deve essere configurato nel file web.xml. Tieni presente che, se sono presenti più voci di filtro, verrà utilizzato il primo spazio dei nomi da impostare.

Il seguente esempio di codice mostra come configurare il filtro dello spazio dei nomi in web.xml:

<!-- Configure the namespace filter. -->
<filter>
    <filter-name>NamespaceFilter</filter-name>
    <filter-class>com.example.appengine.NamespaceFilter</filter-class>
</filter>

<filter-mapping>
    <filter-name>NamespaceFilter</filter-name>
    <url-pattern>/sign</url-pattern>
</filter-mapping>

Per informazioni più generiche sul file web.xml e sulla mappatura dei percorsi degli URL ai servlet, consulta la sezione Descrittore di deployment: web.xml.

Puoi anche impostare un nuovo spazio dei nomi per un'operazione temporanea, reimpostando lo spazio dei nomi originale una volta completata l'operazione, utilizzando il pattern try/finally mostrato di seguito:

// Set the namepace temporarily to "abc"
String oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  //      ... perform operation using current namespace ...
} finally {
  NamespaceManager.set(oldNamespace);
}

Se non specifichi un valore per namespace, lo spazio dei nomi viene impostato su una stringa vuota. La stringa namespace è arbitraria, ma anche limitata a un massimo di 100 caratteri alfanumerici, punti, trattini bassi e trattini. In modo più esplicito, le stringhe dello spazio dei nomi devono corrispondere all'espressione regolare [0-9A-Za-z._-]{0,100}.

Per convenzione, tutti gli spazi dei nomi che iniziano con"_" (trattino basso) sono riservati all'uso del sistema. Questa regola dello spazio dei nomi di sistema non viene applicata, ma potresti facilmente riscontrare conseguenze negative non definite se non la segui.

Evitare fughe di dati

Uno dei rischi comunemente associati alle app multi-tenant è il rischio che i dati si verifichino attraverso gli spazi dei nomi. La fuga di dati non intenzionale può derivare da molte fonti, tra cui:

  • Utilizzo di spazi dei nomi con le API di App Engine che non supportano ancora gli spazi dei nomi. Ad esempio, Blobstore non supporta gli spazi dei nomi. Se utilizzi spazi dei nomi con l'archivio BLOB, devi evitare di utilizzare query dell'archivio BLOB per le richieste degli utenti finali o chiavi dell'archivio BLOB provenienti da fonti non attendibili.
  • Utilizzo di un supporto di archiviazione esterno (anziché memcache e datastore) tramite URL Fetch o un altro meccanismo senza fornire uno schema di compartimentazione per gli spazi dei nomi.
  • Impostazione di uno spazio dei nomi basato su un dominio email di un utente. Nella maggior parte dei casi non vuoi che tutti gli indirizzi email di un dominio abbiano accesso a uno spazio dei nomi. Inoltre, l'utilizzo del dominio email impedisce all'applicazione di utilizzare uno spazio dei nomi finché l'utente non ha eseguito l'accesso.

Deployment di spazi dei nomi

Le sezioni seguenti descrivono come eseguire il deployment degli spazi dei nomi con altri strumenti e API di App Engine.

Creazione di spazi dei nomi per singolo utente

Alcune applicazioni devono creare spazi dei nomi per utente. Se vuoi suddividere i dati a livello di utente per gli utenti che hanno eseguito l'accesso, puoi utilizzare User.getUserId(), che restituisce un ID univoco e permanente per l'utente. Il seguente esempio di codice mostra come utilizzare l'API Users a tale scopo:

if (com.google.appengine.api.NamespaceManager.get() == null) {
  // Assuming there is a logged in user.
  namespace = UserServiceFactory.getUserService().getCurrentUser().getUserId();
  NamespaceManager.set(namespace);
}

In genere, le app che creano spazi dei nomi per utente forniscono pagine di destinazione specifiche a utenti diversi. In questi casi, l'applicazione deve fornire uno schema di URL che indichi quale pagina di destinazione mostrare a un utente.

Utilizzo degli spazi dei nomi con Datastore

Per impostazione predefinita, il datastore utilizza l'impostazione dello spazio dei nomi corrente nel gestore dello spazio dei nomi per le richieste di datastore. L'API applica questo spazio dei nomi corrente agli oggetti Key o Query quando vengono creati. Devi quindi fare attenzione se un'applicazione archivia oggetti Key o Query in moduli serializzati, poiché lo spazio dei nomi viene conservato in tali serializzazioni.

Se utilizzi oggetti Key e Query deserializzati, verifica che si comportino come previsto. La maggior parte delle applicazioni semplici che utilizzano datastore (put/query/get) senza utilizzare altri meccanismi di archiviazione funzioneranno come previsto impostando lo spazio dei nomi corrente prima di chiamare qualsiasi API datastore.

Gli oggetti Query e Key dimostrano i seguenti comportamenti univoci in relazione agli spazi dei nomi:

  • Gli oggetti Query e Key ereditano lo spazio dei nomi corrente quando vengono creati, a meno che non imposti uno spazio dei nomi esplicito.
  • Quando un'applicazione crea un nuovo elemento Key da un predecessore, il nuovo Key eredita lo spazio dei nomi del predecessore.
  • Non esiste un'API per Java per impostare esplicitamente lo spazio dei nomi di Key o Query.
Il seguente esempio di codice mostra il gestore di richieste SomeRequest per incrementare il conteggio per lo spazio dei nomi corrente e lo spazio dei nomi -global- con nome arbitrario in un'entità datastore Counter.

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws java.io.IOException {

    // Update the count for the current namespace.
    updateCount("request");

    // Update the count for the "-global-" namespace.
    String namespace = NamespaceManager.get();
    try {
      // "-global-" is namespace reserved by the application.
      NamespaceManager.set("-global-");
      updateCount("request");
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are now updated.");
  }

Utilizzo degli spazi dei nomi con Memcache

Per impostazione predefinita, memcache utilizza lo spazio dei nomi corrente del gestore dello spazio dei nomi per le richieste memcache. Nella maggior parte dei casi, non è necessario impostare esplicitamente uno spazio dei nomi nella memcache, poiché questa operazione potrebbe introdurre bug imprevisti.

Tuttavia, esistono alcune istanze uniche in cui è appropriato impostare esplicitamente uno spazio dei nomi nella memcache. Ad esempio, l'applicazione potrebbe avere dati comuni condivisi in tutti gli spazi dei nomi (come una tabella contenente i codici paese).

Il seguente snippet di codice mostra come impostare esplicitamente lo spazio dei nomi nella memcache:

Per impostazione predefinita, l'API memcache per Java esegue una query sul gestore dello spazio dei nomi per lo spazio dei nomi corrente da MemcacheService. Puoi anche indicare esplicitamente uno spazio dei nomi quando crei la memcache utilizzando getMemcacheService(Namespace). Per la maggior parte delle applicazioni, non c'è bisogno di specificare esplicitamente uno spazio dei nomi.

Il seguente esempio di codice mostra come creare una memcache che utilizzi lo spazio dei nomi corrente nel gestore dello spazio dei nomi.

// Create a MemcacheService that uses the current namespace by
// calling NamespaceManager.get() for every access.
MemcacheService current = MemcacheServiceFactory.getMemcacheService();

// stores value in namespace "abc"
oldNamespace = NamespaceManager.get();
NamespaceManager.set("abc");
try {
  current.put("key", value); // stores value in namespace “abc”
} finally {
  NamespaceManager.set(oldNamespace);
}

Questo esempio di codice specifica esplicitamente uno spazio dei nomi durante la creazione di un servizio memcache:

// Create a MemcacheService that uses the namespace "abc".
MemcacheService explicit = MemcacheServiceFactory.getMemcacheService("abc");
explicit.put("key", value); // stores value in namespace "abc"

Utilizzo degli spazi dei nomi con la coda di attività

Per impostazione predefinita, le code in coda utilizzano lo spazio dei nomi corrente impostato nel gestore dello spazio dei nomi al momento della creazione dell'attività. Nella maggior parte dei casi, non è necessario impostare esplicitamente uno spazio dei nomi nella coda delle attività, poiché ciò potrebbe introdurre bug imprevisti.

I nomi delle attività sono condivisi tra tutti gli spazi dei nomi. Non puoi creare due attività con lo stesso nome, anche se utilizzano spazi dei nomi diversi. Se vuoi utilizzare lo stesso nome dell'attività per molti spazi dei nomi, puoi semplicemente aggiungere ogni spazio dei nomi al nome dell'attività.

Quando una nuova attività chiama il metodo add() della coda attività, la coda attività copia lo spazio dei nomi corrente e, se applicabile, il dominio Google Workspace dal gestore dello spazio dei nomi. Quando l'attività viene eseguita, lo spazio dei nomi e lo spazio dei nomi di Google Workspace correnti vengono ripristinati.

Se lo spazio dei nomi corrente non è impostato nella richiesta di origine (ovvero, se get() restituisce null), la coda delle attività imposta lo spazio dei nomi su "" nelle attività eseguite.

Esistono alcuni casi unici in cui è appropriato impostare esplicitamente uno spazio dei nomi per un'attività che funziona per tutti gli spazi dei nomi. Ad esempio, potresti creare un'attività che aggrega le statistiche sull'utilizzo in tutti gli spazi dei nomi. Quindi, potresti impostare esplicitamente lo spazio dei nomi dell'attività. Il seguente esempio di codice mostra come impostare esplicitamente gli spazi dei nomi con la coda delle attività.

Innanzitutto, crea un gestore di code di attività che incrementa il conteggio in un'entità datastore Counter:

public class UpdateCountsServlet extends HttpServlet {

  private static final int NUM_RETRIES = 10;

  @Entity
  public class CounterPojo {

    @Id
    public Long id;
    @Index
    public String name;
    public Long count;

    public CounterPojo() {
      this.count = 0L;
    }

    public CounterPojo(String name) {
      this.name = name;
      this.count = 0L;
    }

    public void increment() {
      count++;
    }
  }

  /**
   * Increment the count in a Counter datastore entity.
   **/
  public long updateCount(String countName) {

    CounterPojo cp = ofy().load().type(CounterPojo.class).filter("name", countName).first().now();
    if (cp == null) {
      cp = new CounterPojo(countName);
    }
    cp.increment();
    ofy().save().entity(cp).now();

    return cp.count;
  }

e

// called from Task Queue
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
  String[] countName = req.getParameterValues("countName");
  if (countName.length != 1) {
    resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    return;
  }
  updateCount(countName[0]);
}

Quindi, crea attività con un servlet:

public class SomeRequestServlet extends HttpServlet {

  // Handler for URL get requests.
  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {

    // Increment the count for the current namespace asynchronously.
    QueueFactory.getDefaultQueue()
        .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    // Increment the global count and set the
    // namespace locally.  The namespace is
    // transferred to the invoked request and
    // executed asynchronously.
    String namespace = NamespaceManager.get();
    try {
      NamespaceManager.set("-global-");
      QueueFactory.getDefaultQueue()
          .add(TaskOptions.Builder.withUrl("/_ah/update_count").param("countName", "SomeRequest"));
    } finally {
      NamespaceManager.set(namespace);
    }
    resp.setContentType("text/plain");
    resp.getWriter().println("Counts are being updated.");
  }
}

Utilizzo degli spazi dei nomi con l'archivio BLOB

L'archivio BLOB non è segmentato per spazio dei nomi. Per conservare uno spazio dei nomi in Blobstore, è necessario accedere a Blobstore tramite un mezzo di archiviazione che lo conosca (al momento solo memcache, datastore e coda di attività). Ad esempio, se Key è memorizzato in un'entità datastore, puoi accedervi con un datastore Key o Query che conosca lo spazio dei nomi.

Se l'applicazione accede a Blobstore tramite chiavi archiviate nell'archiviazione con spazio dei nomi, non è necessario segmentare l'archivio BLOB in base allo spazio dei nomi. Le applicazioni devono evitare perdite di blob tra gli spazi dei nomi:

  • com.google.appengine.api.blobstore.BlobInfoFactory non in uso per le richieste dell'utente finale. Puoi utilizzare le query BlobInfo per le richieste amministrative (ad esempio la generazione di rapporti su tutti i blob di applicazioni), ma il suo utilizzo per le richieste dell'utente finale potrebbe causare fughe di dati perché tutti i record BlobInfo non sono compartimentati per spazio dei nomi.
  • Mancato utilizzo delle chiavi dell'archivio BLOB da fonti non attendibili.

Impostazione degli spazi dei nomi per le query Datastore

In Google Cloud Console puoi impostare lo spazio dei nomi per le query Datastore.

Se non vuoi utilizzare l'impostazione predefinita, seleziona lo spazio dei nomi che vuoi utilizzare dal menu a discesa.

Utilizzare gli spazi dei nomi con il caricatore collettivo

Il caricamento collettivo supporta un flag --namespace=NAMESPACE che consente di specificare lo spazio dei nomi da utilizzare. Ogni spazio dei nomi viene gestito separatamente e, se vuoi accedere a tutti gli spazi dei nomi, dovrai eseguirne l'iterazione.

Una nuova istanza di Index eredita lo spazio dei nomi di SearchService utilizzato per crearla. Una volta creato un riferimento a un indice, il relativo spazio dei nomi non può essere modificato. Esistono due modi per impostare lo spazio dei nomi per un SearchService prima di utilizzarlo per creare un indice:

  • Per impostazione predefinita, un nuovo SearchService prende lo spazio dei nomi corrente. Puoi impostare lo spazio dei nomi corrente prima di creare il servizio:
// Set the current namespace to "aSpace"
NamespaceManager.set("aSpace");
// Create a SearchService with the namespace "aSpace"
SearchService searchService = SearchServiceFactory.getSearchService();
// Create an IndexSpec
IndexSpec indexSpec = IndexSpec.newBuilder().setName("myIndex").build();
// Create an Index with the namespace "aSpace"
Index index = searchService.getIndex(indexSpec);

Puoi specificare uno spazio dei nomi in SearchServiceConfig durante la creazione di un servizio:

// Create a SearchServiceConfig, specifying the namespace "anotherSpace"
SearchServiceConfig config =
    SearchServiceConfig.newBuilder().setNamespace("anotherSpace").build();
// Create a SearchService with the namespace "anotherSpace"
searchService = SearchServiceFactory.getSearchService(config);
// Create an IndexSpec
indexSpec = IndexSpec.newBuilder().setName("myindex").build();
// Create an Index with the namespace "anotherSpace"
index = searchService.getIndex(indexSpec);