I am trying to create a standalone feature flag server (centrally managed feature flag micro-service) backed by spring boot starters provided by FF4J. I was able to get it up and running with the web-console and REST API as well. I am now trying to just add the support of custom authorization manager as provided in the wiki, but based on the sample provided there, I am unclear as to how the authorization manager would be aware of the user context when it gets accessed from a different microservice which is implementing the feature. Below I have provided all the relevant code snippets. If you notice in CustomAuthorizationManager
class, I have a currentUserThreadLocal
variable, not sure how or who is going to set that at run time for FF4J to verify the user's role. Any help on this is really appreciated, as I having issues understanding how this works.
Also note, there is a toJson
method in authorization manager that needs to be overridden, not sure what needs to go over there, any help with that is also appreciated.
Custom Authorization Manager
public class CustomAuthorizationManager implements AuthorizationsManager {
private static final Logger LOG = LoggerFactory.getLogger(FeatureFlagServerFeignTimeoutProperties.class);
private ThreadLocal<String> currentUserThreadLocal = new ThreadLocal<String>();
private List<UserRoleBean> userRoles;
@Autowired
private SecurityServiceFeignClient securityServiceFeignClient;
@PostConstruct
public void init() {
try {
userRoles = securityServiceFeignClient.fetchAllUserRoles();
} catch (Exception ex) {
LOG.error("Error while loading user roles", ex);
userRoles = new ArrayList<>();
}
}
@Override
public String getCurrentUserName() {
return currentUserThreadLocal.get();
}
@Override
public Set<String> getCurrentUserPermissions() {
String currentUser = getCurrentUserName();
Set<String> roles = new HashSet<>();
if (userRoles.size() != 0) {
roles = userRoles.stream().filter(userRole -> userRole.getUserLogin().equals(currentUser))
.map(userRole -> userRole.getRoleName()).collect(Collectors.toSet());
} else {
LOG.warn(
"No user roles available, check startup logs to check possible errors during loading of user roles, returning empty");
}
return roles;
}
@Override
public Set<String> listAllPermissions() {
Set<String> roles = new HashSet<>();
if (userRoles.size() != 0) {
roles = userRoles.stream().map(userRole -> userRole.getRoleName()).collect(Collectors.toSet());
} else {
LOG.warn(
"No user roles available, check startup logs to check possible errors during loading of user roles, returning empty");
}
return roles;
}
@Override
public String toJson() {
return null;
}
}
FF4J config
@Configuration
@ConditionalOnClass({ ConsoleServlet.class, FF4jDispatcherServlet.class })
public class Ff4jConfig extends SpringBootServletInitializer {
@Autowired
private DataSource dataSource;
@Bean
public ServletRegistrationBean<FF4jDispatcherServlet> ff4jDispatcherServletRegistrationBean(
FF4jDispatcherServlet ff4jDispatcherServlet) {
ServletRegistrationBean<FF4jDispatcherServlet> bean = new ServletRegistrationBean<FF4jDispatcherServlet>(
ff4jDispatcherServlet, "/feature-web-console/*");
bean.setName("ff4j-console");
bean.setLoadOnStartup(1);
return bean;
}
@Bean
@ConditionalOnMissingBean
public FF4jDispatcherServlet getFF4jDispatcherServlet() {
FF4jDispatcherServlet ff4jConsoleServlet = new FF4jDispatcherServlet();
ff4jConsoleServlet.setFf4j(getFF4j());
return ff4jConsoleServlet;
}
@Bean
public FF4j getFF4j() {
FF4j ff4j = new FF4j();
ff4j.setFeatureStore(new FeatureStoreSpringJdbc(dataSource));
ff4j.setPropertiesStore(new PropertyStoreSpringJdbc(dataSource));
ff4j.setEventRepository(new EventRepositorySpringJdbc(dataSource));
// Set authorization
CustomAuthorizationManager custAuthorizationManager = new CustomAuthorizationManager();
ff4j.setAuthorizationsManager(custAuthorizationManager);
// Enable audit mode
ff4j.audit(true);
return ff4j;
}
}
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>feature-flag-server</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>feature-flag-server</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version>
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.RC2</spring-cloud.version>
<ff4j.version>1.8.2</ff4j.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
<exclusions>
<!-- resolve swagger dependency issue - start -->
<exclusion>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</exclusion>
<!-- resolve swagger dependency issue - end -->
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- FF4J dependencies - start -->
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-spring-boot-starter</artifactId>
<version>${ff4j.version}</version>
</dependency>
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-store-springjdbc</artifactId>
<version>${ff4j.version}</version>
</dependency>
<dependency>
<groupId>org.ff4j</groupId>
<artifactId>ff4j-web</artifactId>
<version>${ff4j.version}</version>
</dependency>
<!-- FF4J dependencies - end -->
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>