Disable WebSession creation when using spring-security with spring-webflux - java

I am running a stateless spring-boot application with a rest api and want to disable the creation of WebSessions as described https://www.baeldung.com/spring-security-session
I have created my own WebSessionManager that does not store the session.
#Bean
public WebSessionManager webSessionManager() {
return new WebSessionManager() {
#Override
#NonNull
public Mono<WebSession> getSession(#NonNull final ServerWebExchange exchange) {
return Mono.just(new WebSession() {
#Override
#NonNull
public String getId() {
return "";
}
#Override
#NonNull
public Map<String, Object> getAttributes() {
return new HashMap<>();
}
#Override
public void start() {
}
#Override
public boolean isStarted() {
return true;
}
#Override
#NonNull
public Mono<Void> changeSessionId() {
return Mono.empty();
}
#Override
#NonNull
public Mono<Void> invalidate() {
return Mono.empty();
}
#Override
#NonNull
public Mono<Void> save() {
return Mono.empty();
}
#Override
public boolean isExpired() {
return false;
}
#Override
#NonNull
public Instant getCreationTime() {
return Instant.now();
}
#Override
#NonNull
public Instant getLastAccessTime() {
return Instant.now();
}
#Override
public void setMaxIdleTime(#NonNull final Duration maxIdleTime) {
}
#Override
#NonNull
public Duration getMaxIdleTime() {
return Duration.ofMinutes(1);
}
});
}
};
}
It works but I wonder if there is a better way to not create a session.

The Issue #6552: Session Creation Policy with Webflux Security is going to be fixed by Spring team.
The problem is that the request cache is being invoked for every request to see if there is a value saved to replay and thus the WebSession is being looked up for every request. Since the WebSession is being looked up with an invalid session id, Spring WebFlux invalidates the SESSION cookie. ~ rwinch
Solution suggested by DarrenJiang1990 is:
.and().securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
The security context in a WebFlux application is stored in a ServerSecurityContextRepository. Its WebSessionServerSecurityContextRepository implementation, which is used by default, stores the context in session. Configuring a NoOpServerSecurityContextRepository instead would make our application stateless
You can track the progress of patching in Issue #7157 ServerRequestCacheWebFilter causes WebSession to be read every request.

I've disabled WebSessionManager by the following trick
#Bean
public WebSessionManager webSessionManager() {
// Emulate SessionCreationPolicy.STATELESS
return exchange -> Mono.empty();
}
All other solutions didn't help for me.

Use the: NoOpServerSecurityContextRepository intended for this purpose.
#Configuration
#EnableWebFluxSecurity
#ComponentScan(value = {"my.package.security"})
public class SpringSecurityConfig2 {
#Autowired private MyHeaderExchangeMatcher myHeaderExchangeMatcher;
#Autowired private MyReactiveAuthenticationManager myReactiveAuthenticationManager;
#Autowired private MyTokenAuthenticationConverter myTokenAuthenticationConverter;
#Bean
SecurityWebFilterChain springWebFilterChain(ServerHttpSecurity http) {
http.httpBasic().disable().formLogin().disable().csrf().disable().logout().disable();
http...
.addFilterAt(webFilter(), SecurityWebFiltersOrder.AUTHORIZATION)
...;
return http.build();
}
#Bean
public AuthenticationWebFilter webFilter() {
AuthenticationWebFilter authenticationWebFilter =
new AuthenticationWebFilter(myReactiveAuthenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(myTokenAuthenticationConverter);
authenticationWebFilter.setRequiresAuthenticationMatcher(myHeaderExchangeMatcher);
// NoOpServerSecurityContextRepository is used to for stateless sessions so no session or state is persisted between requests.
// The client must send the Authorization header with every request.
NoOpServerSecurityContextRepository sessionConfig = NoOpServerSecurityContextRepository.getInstance();
authenticationWebFilter.setSecurityContextRepository(sessionConfig);
return authenticationWebFilter;
}
}

Related

Thread Local remove() in Spring Boot webflux

I have a Web Filter that sets an object in a ThreadLocal attribute and I'm trying to understand how/when this Thread local should be cleaned-up (ThreadLocal.remove()) to avoid the exception "User context already initiated." that happens because it is being retrieved from the Spring Boot Thread Pool with the previous values set.
I'm using Spring Webflux.
Where can I hook this SecurityAuthorizationContext.clean() call?
public class SecurityAuthorizationContext
{
private static final ThreadLocal<PrivilegeHolder> userContext = new ThreadLocal<>();
private final List<String> roles;
private SecurityAuthorizationContext(List<String> roles)
{
this.roles = roles;
}
public static void create(List<String> roles)
{
if (nonNull(userContext.get()))
{
log.error("User context already initiated.");
throw new AuthorizationException("User context already initiated.");
}
PrivilegeHolder privilegeHolder = new PrivilegeHolder();
userContext.set(privilegeHolder);
// example of privileges retrieved from database by the user roles
privilegeHolder.add(INSERT);
privilegeHolder.add(DELETE);
}
public static void clean()
{
userContext.remove();
}
public static boolean hasInsertPrivilege()
{
return userContext.get().hasPrivilege(INSERT);
}
public static boolean hasDeletePrivilege()
{
return userContext.get().hasPrivilege(DELETE);
}
}
public class AuthorizationFilter implements OrderedWebFilter
{
private static final String USER_ROLES = "user-roles";
#Override
public int getOrder()
{
return SecurityWebFiltersOrder.AUTHORIZATION.getOrder();
}
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain)
{
ServerHttpRequest request = serverWebExchange.getRequest();
HttpHeaders headers = request.getHeaders();
List<String> roles = headers.get(USER_ROLES);
SecurityAuthorizationContext.create(roles);
return webFilterChain.filter(serverWebExchange);
}
}
#Configuration
#EnableWebFluxSecurity
#EnableTransactionManagement
public class ApplicationConfiguration
{
#Autowired
private AuthorizationFilter authorizationFilter;
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
{
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/**").permitAll()
.and()
.addFilterAt(authorizationFilter, AUTHORIZATION)
.build();
}
}
UPDATE: Long story short ... I just want to extract something from request headers and make it available to all the stack without passing it as parameter.
So, better to use reactor context instead of ThreadLocal, here you can read about: https://projectreactor.io/docs/core/release/reference/#context

WebClient ExchangeFilterFunction caches value from ThreadLocal

I faced with the following issue while using Spring MVC with ThreadLocal and WebClient from Webflux.
My task is to:
Intercept the user's request to my application and get all the headers from it and save it in ThreadLocal.
After that, when my application makes a call to another service through the WebClient, intercept this request in ExchangeFilterFunction and supplement it with the Authorization header from p.1.
When I finish processing the user's request, I clear the context.
I use my custom class "RequestContext" to store headers in ThreadLocal:
public class RequestContext {
private HttpHeaders requestHeaders;
private String jwt;
private static final String BEARER_PREFIX = "Bearer ";
public RequestContext(HttpHeaders httpHeaders) {
this.requestHeaders = httpHeaders;
if (Objects.nonNull(httpHeaders)) {
init();
}
}
private void init() {
if (Objects.nonNull(requestHeaders)) {
extractJwt();
}
}
private void extractJwt() {
var jwtHeader = requestHeaders.getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.isNotBlank(jwtHeader) && jwtHeader.startsWith(BEARER_PREFIX)) {
jwt = jwtHeader.substring(7);
}
}
}
I use my custom clas "RequestContextService" to deal with ThreadLocal:
public class RequestContextService {
private static final ThreadLocal<RequestContext> CONTEXT = new InheritableThreadLocal<>();
public void init(RequestContext requestContext) {
if (Objects.isNull(CONTEXT.get())) {
CONTEXT.set(requestContext);
} else {
log.error("#init: Context init error");
}
}
public RequestContext get() {
return CONTEXT.get();
}
public void clear() {
CONTEXT.remove();
}
}
My app is a WebMvc app. To complete step 1, I intercept the request with an HandlerInterceptor and set all headers to Threadlocal.
public class HeaderInterceptor implements HandlerInterceptor {
private final RequestContextService requestContextService;
#Override
public boolean preHandle(#NonNull HttpServletRequest request,
#NonNull HttpServletResponse response,
#NonNull Object handler) {
if (Objects.equals(request.getDispatcherType(), DispatcherType.REQUEST)) {
var headers = new ServletServerHttpRequest(request).getHeaders();
requestContextService.init(new RequestContext(headers));
}
return true;
}
#Override
public void afterCompletion(#NonNull HttpServletRequest request, #NonNull HttpServletResponse response,
#NonNull Object handler, Exception ex) {
requestContextService.clear();
}
}
As you can see, after every request I call "requestContextService.clear()" method to clear ThreadLocal.
To perform step two, I use the ExchangeFilterFunction, where I turn to the threadlocal and get the title from there.
public class SamlExchangeFilterFunction implements ExchangeFilterFunction {
private final RequestContextService requestContextService;
private static final ClientResponse UNAUTHORIZED_CLIENT_RESPONSE =
ClientResponse.create(HttpStatus.UNAUTHORIZED).build();
#Override
public #NotNull Mono<ClientResponse> filter(#NotNull ClientRequest request, #NotNull ExchangeFunction next) {
var jwt = requestContextService.get().getJwt();
if (StringUtils.isNoneBlank(jwt)) {
var clientRequest = ClientRequest.from(request)
.headers(httpHeaders -> httpHeaders.set(SAML_HEADER_NAME, jwt))
.build();
return next.exchange(clientRequest);
}
return Mono.just(UNAUTHORIZED_CLIENT_RESPONSE);
}
}
The problem is that the SamlExchangeFilterFunction works correctly only once.
On the first request to the application, everything works as it should. But with further requests with different authorization headers, the ExchangeFilterFunction seems to cache the value from the first request and substitutes it despite the fact that the threadlocal itself contains a completely different meaning of Authorization header.

How to interact with multiple keyspaces with reactive Cassandra repositories in java Springboot WebFlux?

I am trying to connect to two different reactive keyspaces depending on the incoming http request.
The request will contains some information which is for the business logic/ service layer to retrieve data from either a keyspace A or a keyspace B (by invoking ReactiveARepository or ReactiveBRepository) in order to get the corresponding data.
A simple if statement is not the solution, since it requires to use the correct reactive repository with the corresponding reactive configuration, reactive template, etc…
On the repository layer, very straightforward, not even using any custom queries. I even put the repositories to their own respective packages.
package com.cassandra.repository.a;
#Repository
public interface ReactiveARepository extends ReactiveCassandraRepository<MyPojo, String> {
package com.cassandra.repository.b;
#Repository
public interface ReactiveBRepository extends ReactiveCassandraRepository<MyPojo, String> {
#Configuration
#EnableReactiveCassandraRepositories
public class AandBcommonCassandraConfiguration extends AbstractReactiveCassandraConfiguration {
//the #Value private String for contactPoints, keyspace, datacenter, port, username and password
#Bean
#NonNull
#Override
public CqlSessionFactoryBean cassandraSession() {
final CqlSessionFactoryBean cqlSessionFactoryBean = new CqlSessionFactoryBean();
cqlSessionFactoryBean.setContactPoints(contactPoints);
cqlSessionFactoryBean.setKeyspaceName(keyspace);
cqlSessionFactoryBean.setLocalDatacenter(datacenter);
cqlSessionFactoryBean.setPort(port);
cqlSessionFactoryBean.setUsername(username);
cqlSessionFactoryBean.setPassword(passPhrase);
return cqlSessionFactoryBean;
}
#NonNull
#Override
protected String getKeyspaceName() {
return keyspace;
}
#Override
protected String getLocalDataCenter() {
return datacenter;
}
//other getters
What I already tried:
#Configuration
#EnableReactiveCassandraRepositories(basePackages = "com.cassandra.repository.a”, reactiveCassandraTemplateRef = “keyspaceCassandraTemplateA”)
public class AkeyspaceCassandraConfiguration extends BaseCassandraConfiguration {
#Value("${spring.data.cassandra.keyspace-name.keyspaceA}”)
private String keyspace;
#NonNull
#Override
public String getKeyspaceName() {
return keyspace;
}
#NonNull
#Override
public CqlSessionFactoryBean cassandraSession() {
final CqlSessionFactoryBean cqlSessionFactoryBean = super.cassandraSession();
cqlSessionFactoryBean.setKeyspaceName(keyspace);
return cqlSessionFactoryBean;
}
#Bean(“keyspaceCassandraTemplateA”)
public ReactiveCassandraTemplate reactiveCassandraTemplate() {
final ReactiveSession reactiveSession = new DefaultBridgedReactiveSession(cassandraSession().getObject());
return new ReactiveCassandraTemplate(reactiveSession);
}
#Configuration
#EnableReactiveCassandraRepositories(basePackages = "com.cassandra.repository.B”, reactiveCassandraTemplateRef = “keyspaceCassandraTemplateB”)
public class BKeyspaceCassandraConfiguration extends BaseCassandraConfiguration {
#Value("${spring.data.cassandra.keyspace-name.keyspaceB}”)
private String keyspace;
#NonNull
#Override
public String getKeyspaceName() {
return keyspace;
}
#NonNull
#Override
public CqlSessionFactoryBean cassandraSession() {
final CqlSessionFactoryBean cqlSessionFactoryBean = super.cassandraSession();
cqlSessionFactoryBean.setKeyspaceName(keyspace);
return cqlSessionFactoryBean;
}
#Bean("keyspaceCassandraTemplateB")
public ReactiveCassandraTemplate reactiveCassandraTemplate() {
final ReactiveSession reactiveSession = new DefaultBridgedReactiveSession(cassandraSession().getObject());
return new ReactiveCassandraTemplate(reactiveSession);
}
However, it is only pointing the the keyspace A.
May I ask what is the proper way to have a reactive application with two keyspaces in Cassandra?
This may not be the answer you are looking for, but in Spring Data JPA, you can achieve connecting to multiple databases for different sets of entities by configuring separate EntityManagerFactory and PlatformTransactionManager beans for each data source.
Examples
HTH!

Firebase multi-tenancy with play framework depends on header value of HTTP request

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.
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.
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.
}

Instance variable in Spring Service

I have following Spring Service
#Service
class FeatureTogglesImpl implements FeatureToggles {
private final FeatureToggleRepository featureToggleRepository;
private Map<String, Feature> featuresCache;
#Autowired
public FeatureTogglesImpl(final FeatureToggleRepository featureToggleRepository) {
this.featureToggleRepository = featureToggleRepository;
this.featuresCache = loadAllFromRepository();
}
#Override
#Transactional
public void enable(Feature feature) {
Feature cachedFeature = loadFromCache(feature);
cachedFeature.enable();
featureToggleRepository.save(cachedFeature);
onFeatureToggled();
}
#Override
public boolean isEnabled(Feature feature) {
return loadFromCache(feature).isEnabled();
}
private Feature loadFromCache(Feature feature) {
checkNotNull(feature);
return featuresCache.get(feature.getKey());
}
private Map<String, Feature> loadAllFromRepository() {
return Maps.uniqueIndex(featureToggleRepository.findAll(), new Function<Feature, String>() {
#Override
public String apply(Feature feature) {
return feature.getKey();
}
});
}
void onFeatureToggled() {
featuresCache = loadAllFromRepository();
}
}
As you can see,I store loaded features into featuresCache, so that when client calls isEnabled() it is loading according feature from the cache.
There is a managed bean, who manages toggling the feature,
#Component
#ManagedBean
#Scope("view")
public class FeatureTogglesManager {
#Autowired
private FeatureToggles featureToggles;
#Secured({"ROLE_FEATURE_TOGGLES_EDIT"})
public String enable(Feature feature) {
featureToggles.enable(feature);
return null;
}
}
When I call enable() from AdminFeatureTogglesManager , I can see proper feature toggled, and cache pre-populated.
I have another service, which actually uses FeatureToggles.isEnabled() service
#Service
class ProductServieImpl implements ProductService {
#Autowired
private FeatureToggles featureToggles;
#Override
#Transactional
public void loadProducts() {
if (featureToggles.isEnabled(NewProducts.insance())) {
loadNewProducts();
return;
}
loadOldProducts();
}
}
The problem is that featureToggles.isEnabled() from this service always returns old instance from the cache, and when I debug the FeatureTogglesImpl, I do not see my pre-populated cache, although after toggle I could see correct/updated cache.
Isn't FeatureTogglesImpl supposed to be a singletong, so that if I change instance variable, it changes everywhere? Any help is appreciated.

Categories

Resources