I have a play framework project that provides APIs that shared between multiple front-ends, currently, I'm working on single front-end but I want to create a multi-tenant backend, each front-end got its own Firebase account.
My problem that I have to consider which firebase project to access depends on the request header value, that came with different values depends on the front end.
What I have now: FirebaseAppProvider.java:
public class FirebaseAppProvider implements Provider<FirebaseApp> { private final Logger.ALogger logger; private final Environment environment; private final Configuration configuration; @Inject public FirebaseAppProvider(Environment environment, Configuration configuration) { this.logger = Logger.of(this.getClass()); this.environment = environment; this.configuration = configuration; } @Singleton @Override public FirebaseApp get() { HashMap<String, String> firebaseProjects = (HashMap<String, String>) configuration.getObject("firebase"); firebaseProjects.forEach((websiteId, projectId) -> { FileInputStream serviceAccount = null; try { serviceAccount = new FileInputStream(environment.classLoader().getResource(String.format("firebase/%s.json", projectId)).getPath()); } catch (FileNotFoundException e) { e.printStackTrace(); return; } FirebaseOptions options = new FirebaseOptions.Builder().setCredential(FirebaseCredentials.fromCertificate(serviceAccount)) .setDatabaseUrl(String.format("https://%s.firebaseio.com/", projectId)) .build(); FirebaseApp firebaseApp = FirebaseApp.initializeApp(options, projectId); logger.info("FirebaseApp initialized"); }); return FirebaseApp.getInstance(); } }
Also for Database: FirebaseDatabaseProvider.java
public class FirebaseDatabaseProvider implements Provider<FirebaseDatabase> { private final FirebaseApp firebaseApp; public static List<TaxItem> TAXES = new ArrayList<>(); @Inject public FirebaseDatabaseProvider(FirebaseApp firebaseApp) { this.firebaseApp = firebaseApp; fetchTaxes(); } @Singleton @Override public FirebaseDatabase get() { return FirebaseDatabase.getInstance(firebaseApp); } @Singleton public DatabaseReference getUserDataReference() { return this.get().getReference("/usersData"); } @Singleton public DatabaseReference getTaxesConfigurationReference() { return this.get().getReference("/appData/taxConfiguration"); } private void fetchTaxes() { DatabaseReference bundlesRef = getTaxesConfigurationReference().child("taxes"); bundlesRef.addValueEventListener(new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { TAXES.clear(); dataSnapshot.getChildren().forEach(tax -> TAXES.add(tax.getValue(TaxItem.class))); Logger.info(String.format("==> %d taxes records loaded", TAXES.size())); } @Override public void onCancelled(DatabaseError databaseError) { Logger.warn("The read failed: " + databaseError.getCode()); } }); } }
So I bind them as well from Module.java:
public class Module extends AbstractModule { @Override public void configure() { bind(FirebaseApp.class).toProvider(FirebaseAppProvider.class).asEagerSingleton(); bind(FirebaseAuth.class).toProvider(FirebaseAuthProvider.class).asEagerSingleton(); bind(FirebaseDatabase.class).toProvider(FirebaseDatabaseProvider.class).asEagerSingleton(); } }
my ActionCreator:
public class ActionCreator implements play.http.ActionCreator { @Inject public ActionCreator() { } @Override public Action createAction(Http.Request request, Method actionMethod) { switchTenancyId(request); return new Action.Simple() { @Override public CompletionStage<Result> call(Http.Context ctx) { return delegate.call(ctx); } }; } private void switchTenancyId(Http.RequestHeader request) { // DO something here } private Optional<String> getTenancyId(Http.RequestHeader request) { String websiteId = request.getHeader("Website-ID"); System.out.println(websiteId); return null; } }
What I want is when I use Database service, or auth service, I read the website id and decide which firebase project to access, I really tried the solution like this answer here: Multi tenancy with Guice Custom Scopes and Jersey
Please note I'm willing to use differents projects, not the same firebase project for each front-end.
But kinda lost, especially the request can be only accessed from controller or ActionCreator, so what I got from the question above is load providers by key into ThreadLocal
and switch them for each request depends on the annotation, but I was unable to do this because of the lack of knowledge.
The minimized version of my project can be found here: https://github.com/almothafar/play-with-multi-tenant-firebase
Also, I uploaded taxes-data-export.json
file to import inside firebase project for a test.
2 Answers
Answers 1
I believe Custom Scopes for this is overkill. I would recommend doing the Request-Scoped seeding from Guice's own wiki. In your case that would be something like
public class TenancyFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String tenancyId = httpRequest.getHeader("YOUR-TENANCY-ID-HEADER-NAME"); httpRequest.setAttribute( Key.get(String.class, Names.named("tenancyId")).toString(), userId ); chain.doFilter(request, response); } @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void destroy() { } };
It has to be bound in a ServletModule
public class YourModule extends ServletModule { @Override protected void configureServlets() { filter("/*").through(TenancyFilter.class); } @Provides @RequestScoped @Named("tenancyId") String provideTenancyId() { throw new IllegalStateException("user id must be manually seeded"); } }
Then anywhere you need to get the Tenancy ID you just inject
public class SomeClass { private final Provider<String> tenancyIdProvider; @Inject SomeClass(@Named("tenancyId") Provider<String> tenancyIdProvider) { this.tenancyIdProvider = tenancyIdProvider; } // Methods in request call tenancyIdProvider.get() to get and take action based on Tenancy ID. }
Answers 2
Right, so I know Play a lot better than FireBase, but it seems to me you want to extract a tenancy ID from the request prior to feeding this into your FrieBase backend? Context when writing Java in play is Thread local, but even when doing things async you can make sure the Http.context info goes along for the ride by injecting the execution context. I would not do this via the action creator, unless you want to intercept which action is called. (Though I have a hackish solution for that as well.)
So, after a comment I'll try to elucidate here, your incoming request will be routed to a controller, like below (let me know if you need clearing up on routing etc):
Below is a solution for caching a retrieved FireBaseApp based on a "Website-ID" retrieved from the request, though I would likely put the tenancyId in the session.
import javax.inject.Inject; import java.util.concurrent.CompletionStage; public class MyController extends Controller { private HttpExecutionContext ec; //This is the execution-context. private FirebaseAppProvider appProvider; private CacheApi cache; @Inject public MyController(HttpExecutionContext ec, FireBaseAppProvider provider,CacheApi cache) { this.ec = ec; this.appProvider = provider; this.cache = cache; } /** *Retrieves a website-id from request and attempts to retrieve *FireBaseApp object from Cache. *If not found a new FireBaseApp will be constructed by *FireBaseAppProvider and cached. **/ private FireBaseApp getFireBaseApp(){ String tenancyId = request.getHeader("Website-ID); FireBaseApp app = (FireBaseApp)cache.get(tenancyId); if(app==null){ app=appProvider.get(); cache.put(tenancyId,app); } return app; } public CompletionStage<Result> index() { return CompletableFuture.supplyAsync(() -> { FireBaseApp app = getFireBaseApp(); //Do things with app. }, ec.current()); //Used here. } }
Now in FireBaseAppProvider you can access the header via play.mvc.Controller, the only thing you need is to provide the HttpExecutionContext via ec.current. So (once again, I'm avoiding anything FireBase specific), in FireBaseProvider:
import play.mvc.Controller; public class FireBaseAppProvider { public String getWebsiteKey(){ String website = Controller.request().getHeader("Website-ID"); //If you need to handle a missing header etc, do it here. return website; } public FireBaseApp get(){ String tenancyId = getWebsiteKey(); //Code to do actual construction here. } }
Let me know if this is close to what you're asking and I'll clean it up for you.
Also, if you want to store token validations etc, it's best to put them in the "session" of the return request, this is signed by Play Framework and allows storing data over requests. For larger data you can cache this using the session-id as part of the key.
0 comments:
Post a Comment