spring-bootspring-securityweb.xmluser-rolesopen-liberty

Spring Security - avoiding in-web-container role binding removes flexibility


If using Spring Security in-app authentication/authorization, I would like the flexibility I have when I use J2EE web.xml descriptor and in-web-container authentication/authorization.

I am using LDAP (Active Directory as IP, not DAO).

I understand Spring Boot has moved from use of web.xml descriptor but I have a situation like this and I wonder if there is a workaround in Spring Boot.

Currently, most of our apps are using J2EE and in-container authentication/authorization mechanism (web containers like OpenLiberty, Jetty or similar). We are switching to Spring Boot and would prefer using in-app Spring Security authentication/authorization.

But I do have one concern.

Organizations can have number of different environments they deploy applications for testing, staging, until the production. Let's say an organization has btw 5-10 environments.

Typically, if I would start with a new app, I would follow these steps:

  1. use web.xml descriptor to define , say I define ROLE-A, ROLE-B, ROLE-C in web.xml of my app.

  2. once ready, hand in my application to infrastructure teams responsible for deployments to ENV1, ENV2, ENV3, ENV4, ENV5, ...

  3. infrastructure team would open web.xml and see defining ROLE-A, ROLE-B, and ROLE-C

  4. from here, they would define and externalize users and goups (so they can be resolved using ${...}) having certain roles in server.env file. For example

    GROUP-A_DEVS=devs_group1 ROLE-A_USER1=bob ROLE-A_USER2=mike GROUP-B_DEVS=devs_group2 ROLE-B_USER1=bob

  5. from here, infrastructure team would create server.xml file with section in which they would bind roles to users or groups. So, for ENV1, they could do something like:

<application-bnd>
 <security-role name="ROLE-A">
     <group name="${GROUP-A_DEVS}" />
     <user name="${ROLE-A_USER1}" />
     <user name="${ROLE-A_USER2}" />
 </security-role>
 <security-role name="ROLE-B">
     <group name="${GROUP-A_DEVS}" />
     <group name="${GROUP-B_DEVS}" />
    <user name="${ROLE-A_USER1}"/>
 </security-role>
</application-bnd>

From above, they defined that

But we have multiple environments and they have different mappings. For example ENV1 has mapping like above. Infrastructure team can now go and add new environment ENV2 or modify it if it existed as they see fit without involving developer at all.

So, they can create ENV2 mapping like:

<application-bnd>
     <security-role name="ROLE-B">
        <user name="${ROLE-B_USER3}"/>
     </security-role>
</application-bnd>

, where user3 is mapped to ROLE-B. This shows why this is very flexible. It is because now, infrastructure team can add all these users to the server.env file so:

etc, to externalize them to server.env file, THEN MAP THEM in server.xml file which is also in their ownership and control. Both of these files are owned by infrastructure (in fact, they are the only ones who might have access to these). Developers can independently create their own local versions and map them how ever they wish

This shows how infrastructure/security teams can control which users are assigned to which roles and they can also add them or remove them. The only time app needs to be changed is if a NEW role is added, for example ROLE-X, in which case, that role would have to be added to app web.xml. But that is all.

Obviously, roles once added change less often than users that belong to these roles and that is handled entirely by security or infrastructure teams, no development knowledge required. Devs can setup their own local instance of web-container however they want to do their work.

The above shows the flexibility of web.xml and in-web-container role mapping. I, as developer, define roles in the app web.xml file and hand that to infrastructure. And they can define mapping in the web-container server.xml section how to map these roles per each environment without my knowledge.

With Spring-Boot and in-app Spring Security handling authentication/authorization, I dont think this is possible.

I know Spring Boot uses application.properties. Perhaps I could define app roles there instead of web.xml. However, I see no way to map these roles to the groups and users defined in the section in server.xml.

Curently, I do it like this in my Spring Boot app

But as you can see this is very impractical.

  1. First flexibility is lost as I have to know in advance all settings on all environments's server.env files in order to code them in my application.properties then in my code get them using enviroment.getProperty() or @Value annotation or however.

  2. Second, I have to code them all in my application code so I can get them.

  3. Third, if infrastructure team might tomorrow decide to add a new user ROLE-X_USER5=bob, I have to modify the code or that user will never be accounted for.

  4. Fourth, if infrastructure names a variable differently than I did, I need to know that exact name in order to parameterize it in my application.properties, else I will not be able to get it

I know that in Spring Boot, I could use jee.mappableRoles(...) to pass the roles defined in the app to the web-container so it can map them to the mappings defined in server.xml <application-bnd> section. But this would be using J2EE in-container authorization role mapping.

I wonder if there is a way to do that without involvement of J2EE and to do it entirely from within the application. This is in order to make my app web container agnostic and to use entirely Spring Security in-app authentication/authorization as we find that that removes silos mentality btw 2 teams (developers and infrastructure) and gives better understanding and easier handling of issues if something goes wrong as developer has full knowledge and ability to do modifications.


Solution

  • To summarize (correct me if anything is wrong), you are working with the following concepts and attempting to build a replacement using only Spring Security's authorization concepts:

    1. A way to communicate roles to an infrastructure team (equivalent to list of role names in web.xml)
    2. A way to define mappings between roles and a list of users, list of groups, or both (equivalent to mappings in server.xml)
    3. A means of specifying the actual runtime values of these mappings using environment variables (?)

    #1: As mentioned in comments, communicating the roles is simply documentation because the container doesn't need to know about roles ahead of time. We develop the application to use roles however we wish (request-level authorization rules, @PreAuthorize, etc.), and simply document that in whatever way makes the most sense. For example, we could provide a sample properties file as an example, document it in a readme, etc.

    #2: Defining mappings between users and roles requires populating a list of authorities when the user logs in. How this would work is entirely up to you. For example, we could define Spring Boot application properties to do the mapping, and create a UserDetailsService that applies the mappings.

    @Configuration
    @ConfigurationProperties("application.security")
    public class ApplicationSecurityProperties {
    
        private Map<String, Role> roles = new LinkedHashMap<>();
    
        // getters and setters...
    
        public static class Role {
    
            private List<String> groups = new ArrayList<>();
    
            private List<String> users = new ArrayList<>();
    
            // getters and setters...
    
        }
    
    }
    

    For testing, we could define an application-dev.yml (or properties) which enables the use of a dev profile:

    application:
      security:
        roles:
          ROLE_A:
            groups:
              - GROUP_A
            users:
              - user1
              - user2
          ROLE_B:
            groups:
              - GROUP_A
              - GROUP_B
            users:
              - user1
    

    Note: I used underscores instead of dashes for consistency with Spring Security default of ROLE_ prefix.

    The infrastructure team can define their own files with profiles for env1, env2, etc.

    How we apply these mappings to authorities depends on what type of authentication is being performed, but I'll just provide an example using the in-memory UserDetailsService, with groups provided as authorities (for example purposes):

    @Configuration
    @EnableWebSecurity
    public class SecurityConfiguration {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            // @formatter:off
            http
                .authorizeHttpRequests((authorize) -> authorize
                    .requestMatchers("/role-a").hasRole("A")
                    .requestMatchers("/role-b").hasRole("B")
                    .anyRequest().denyAll()
                )
                .httpBasic(Customizer.withDefaults());
            // @formatter:on
    
            return http.build();
        }
    
        @Bean
        public UserDetailsService userDetailsService(ApplicationSecurityProperties applicationSecurityProperties) {
            UserDetailsService delegate = new InMemoryUserDetailsManager(users());
            return (username) -> {
                UserDetails user = delegate.loadUserByUsername(username);
                Set<String> groups = AuthorityUtils.authorityListToSet(user.getAuthorities());
    
                List<String> authorities = new ArrayList<>();
                applicationSecurityProperties.getRoles().forEach((roleName, role) -> {
                    boolean hasMatchingUser = role.getUsers().contains(user.getUsername());
                    boolean hasMatchingGroup = role.getGroups().stream().anyMatch(groups::contains);
                    if (hasMatchingUser || hasMatchingGroup) {
                        authorities.add(roleName);
                    }
                });
    
                return User.withUserDetails(user)
                    .authorities(AuthorityUtils.createAuthorityList(authorities))
                    .build();
            };
        }
    
        private static List<UserDetails> users() {
            User.UserBuilder builder = User.builder().password("{noop}password");
            UserDetails user1 = builder.username("user1").build();
            UserDetails user2 = builder.username("user2").build();
            UserDetails user3 = builder.username("user3").authorities("GROUP_A").build();
            UserDetails user4 = builder.username("user4").authorities("GROUP_B").build();
            UserDetails user5 = builder.username("user5").authorities("GROUP_A", "GROUP_B").build();
            UserDetails user6 = builder.username("user6").authorities("GROUP_C").build();
    
            return Arrays.asList(user1, user2, user3, user4, user5, user6);
        }
    
    }
    

    #3: The solution above is simple and fairly flexible, so I'm not sure there's a need for another level of abstraction to perform the mappings. However, if you want to use environment variables to define the actual usernames, group names, or whatever, Spring Boot provides many ways of externalizing the configuration and binding properties.

    For example, the above properties could instead use environment variables like this:

    application:
      security:
        roles:
          ROLE_A:
            groups:
              - GROUP_A
            users:
              - ${ROLE_A_USER1}
              - ${ROLE_A_USER2}
          ROLE_B:
            groups:
              - GROUP_A
              - GROUP_B
            users:
              - ${ROLE_B_USER1}
    

    But again, this doesn't seem necessary since the environment-specific application properties could be provided and activated via a profile controlled by infrastructure team. There's no need to further externalize what's already externalized.