FF4J - Spring Boot - Custom Authorization Manager

2.3k Views Asked by At

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>
1

There are 1 best solutions below

1
On BEST ANSWER

Full disclosure I am the maintainer of the framework.

The documentation is not good on this part, improvements are in progress. But here is some explanation for a working project.

When using AuthorizationManager:

  1. AuthorizationManager principle should be used only if you already enabled authentication in your application (LOGIN FORM, ROLES...). If not you can think about FlipStrategy to create your own predicates.

  2. FF4j will rely on existing security frameworks to retrieve context of logged user, this is called the principal. As such this is unlikely for you to create your own custom implementation of AuthorizationManager except you are building your own authentication mechanism.

What to do:

You will use well known framework such as Spring Security of Apache Shiro to secure your applications and simply tell ff4j to rely on it.

How to do:

Here is working example using SPRING SECURITY: https://github.com/ff4j/ff4j-samples/tree/master/spring-boot-2x/ff4j-sample-security-spring

Here is working example using APACHE SHIRO: https://github.com/ff4j/ff4j-samples/tree/master/spring-boot-2x/ff4j-sample-security-shiro