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

153 Views Asked by At

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

  • Externalized GROUP-A , user1 and user2 are mapped to ROLE-A, and
  • Externalized GROUP-A, GROUP-B, and user1 and mapped to ROLE-B in ENV1.

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:

  • in server.env for ENV2 add

    ROLE-B_USER3=carl

  • in server.xml for ENV2, add:

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

  • for ENV1, server.env would have something like:

    ROLE-A_USER1=user1

    ROLE-A_USER2=user2

    ROLE-B_USER1=user1

  • for ENV2, server.env would have something like:

    ROLE-B_USER3=user3

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

  • in web-container server.env, I have defined variables like:

    ROLE-A_USER1=joe

    ROLE-A_USER2=mike

    ROLE-B_USER1=kevin

  • in Spring Boot application.properties, I parameterize these to get them from server.env at run-time:

    role.a.user1=${ROLE-A_USER1}

    role.a.user2=${ROLE-A_USER2}

    role.b.user1=${ROLE-B_USER1}

  • then in app, I get these from environment like:

    environment.getProperty("role.a.user1")

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.

1

There are 1 best solutions below

13
On BEST ANSWER

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.