Blogs

Developing CLI application with Spring Shell (part 5)

Theme
Software development

In this post, we are going to configure Spring Security for our Spring Shell clidemo application. Spring Security is a very powerful library, rich in features, with support for a wide range of security mechanisms, from JAAS, CAS, to OAuth2 and many others, and has become a defacto standard security library for Spring applications. There are tons of available tutorials and examples of how to configure spring security for web applications. However, there are only a few workable tutorials on how to configure it for Java standalone applications (non-web applications).

What will we be building in this post?

In this post, we are going to configure Spring Security for our Spring Shell clidemo application. Spring Security is a very powerful library, rich in features, with support for a wide range of security mechanisms, from JAAS, CAS, to OAuth2 and many others, and has become a defacto standard security library for Spring applications. There are tons of available tutorials and examples of how to configure spring security for web applications. However, there are only a few workable tutorials on how to configure it for Java standalone applications (non-web applications).

What will we be building in this post?

If you run clidemo application and type help you should see the list of available commands, as displayed below:

CLI-DEMO:>help
AVAILABLE COMMANDS
Built-In Commands
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        history: Display or save the history of previously run commands
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.
Echo Command
        echo: Displays greeting message to the user whose name is supplied
        progress-bar: Displays progress bar
        progress-counter: Displays progress counter (with spinner)
        progress-spinner: Displays progress spinner
Table Examples Command
        sample-tables: Display sample tables
        table-formatter-demo: Table formatter demo
User Command
        create-user: Create new user with supplied username
        update-all-users: Update and synchronize all users in local database with external source
        user-details: Display details of user with supplied username
        user-list: Display list of users

At the moment anyone who starts clidemo application can access every one of these commands. What we would like to achieve is that only those users (CliUser) who have signed in with their credentials (username/password) can access user-details and user-list commands, and only those signed-in users who have their property superuser set to TRUE can access create-user, user-details, and update-all-users commands.

As the first step of this post, add the spring security dependency to an existing build.gradle file:

dependencies {
   implementation 'com.fasterxml.jackson.core:jackson-databind'
   implementation 'org.springframework.shell:spring-shell-starter:2.0.1.RELEASE'
   implementation 'org.springframework.security:spring-security-config'
   testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Restrict User Commands Access

Spring Shell has a built-in mechanism for controlling which commands are available to the user, due to the current internal state of the application, and which are not. This feature is called Dynamic Command Availability.

Basically, it works as follows: with each command method one control method can be associated, that will be responsible to decide whether this method is available or not. For details please consult the official Spring Shell documentation on the following link.

We are going to use this Spring Shell feature to protect our user commands, that is to make them available only to the signed in users. For start, let us create a new abstract class SecuredCommand, with code as shown in the snippet below:

package com.ag04.clidemo.command;

import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellMethodAvailability;

public abstract class SecuredCommand {

    public Availability isUserSignedIn() {
           return Availability.unavailable("you are not signedIn. Please sign in to be able to use this command!");
    }

}

The current implementation of isUserSignedIn() method will always simply return result indicating to the Spring Shell that command is not available, we will fix this later. Now declare UserCommand class to extend SecuredCommand class, and annotate all command methods in it with the following annotation:

@ShellMethodAvailability("isUserSignedIn")

Modified UserCommand class should, in the end, reassemble the following code snippet:

@ShellComponent
public class UserCommand extends SecuredCommand {
    // properties omitted
    @ShellMethod("Display list of users")
    @ShellMethodAvailability("isUserSignedIn")
    public void userList() {
        // method body omitted ...
    }
    @ShellMethod("Create new user with supplied username")
    @ShellMethodAvailability("isUserSignedIn")
    public void createUser(@ShellOption({"-U", "--username"}) String username) {
        //  method body omitted ...
    }
    ...
}

Rebuilding and running clidemo help command now produces the following output:

CLI-DEMO:>help
AVAILABLE COMMANDS
Built-In Commands
        clear: Clear the shell screen.
        exit, quit: Exit the shell.
        help: Display help about available commands.
        history: Display or save the history of previously run commands
        script: Read and execute commands from a file.
        stacktrace: Display the full stacktrace of the last error.
Echo Command
        echo: Displays greeting message to the user whose name is supplied
        progress-bar: Displays progress bar
        progress-counter: Displays progress counter (with spinner)
        progress-spinner: Displays progress spinner
Table Examples Command
        sample-tables: Display sample tables
        table-formatter-demo: Table formatter demo
User Command
      * create-user: Create new user with supplied username
      * my-details: Display details of signedIn user
      * update-all-users: Update and synchronize all users in local database with external source
      * user-details: Display details of user with supplied username
      * user-list: Display list of users
Commands marked with (*) are currently unavailable.
Type `help <command>` to learn more.

As you can see, all user commands are marked with asterix (*) signaling that they are not available at the moment. For example, an attempt to run user-list command would result in the following response:

CLI-DEMO:>user-list
Command ‘user-list’ exists but is not currently available because you are not signedIn. Please sign in to be able to use this command!
Details of the error have been omitted. You can use the stacktrace command to print the full stacktrace.

Now that user commands are “protected” in this manner, we can proceed with implementing a real spring security protection.

Configuring Spring Security

First, we will change the existing SecuredCommand class and implement real isUserSignedIn() method, as shown in the code below:

package com.ag04.clidemo.command;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellMethodAvailability;

public abstract class SecuredCommand {

    public Availability isUserSignedIn() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !(authentication instanceof  UsernamePasswordAuthenticationToken)) {
            return Availability.unavailable("you are not signedIn. Please sign in to be able to use this command!");
        }
        return Availability.available();
    }

}

If SecurityContextHolder contains valid Authentication object this method returns positive result (access to the command is granted), otherwise negative (access to the command is denied). For more on SecurityContextHolder see the following link. Now, only signed in users will have access to the user commands.

The next step is to configure spring security for clidemo application. At the core of our configuration is custom ClidemoUserDetailsServiceimplementation of Spring security UserDetailsService interface. Create this class with the code from the following code snippet:

package com.ag04.clidemo.security;

import com.ag04.clidemo.model.CliUser;
import com.ag04.clidemo.service.UserService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class ClidemoUserDetailsService implements UserDetailsService {

    private UserService userService;

    public ClidemoUserDetailsService(UserService userService) {
        this.userService = userService;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        CliUser cliUser = userService.findByUsername(username);

        if (cliUser == null) {
            throw new UsernameNotFoundException("User not found.");
        }

        User.UserBuilder builder = User.withUsername(username);
        builder.password(new BCryptPasswordEncoder().encode(cliUser.getPassword()));
        if (cliUser.isSuperuser()) {
            builder.roles("USER", "ADMIN");
        } else {
            builder.roles("USER");
        }
        return builder.build();
    }
}

For more on UserDetailsService interface see this article. With this class in place, we can proceed and create a separate spring configuration file that will hold all spring security related configurations. To do so, create SpringSecurityConfig class as shown below:

package com.ag04.clidemo.config;

import com.ag04.clidemo.security.ClidemoUserDetailsService;
import com.ag04.clidemo.service.UserService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import java.util.Arrays;

@Configuration
public class SpringSecurityConfig {

    @Bean
    public UserDetailsService userDetailsService(UserService userService) {
        return new ClidemoUserDetailsService(userService);
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    };

    @Bean
    public AuthenticationManager authenticationManager(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder);

        ProviderManager authenticationManager = new ProviderManager(Arrays.asList(authenticationProvider));
        return authenticationManager;
    }
}

Now it is time to enable our users to actually sign in. For that purpose, we will create a new command that will prompt users to provide their username and password. Create new SigninCommand class as shown in the code below:

package com.ag04.clidemo.command;

import com.ag04.clidemo.shell.InputReader;
import com.ag04.clidemo.shell.ShellHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.shell.standard.ShellComponent;
import org.springframework.shell.standard.ShellMethod;
import org.springframework.util.StringUtils;

@ShellComponent
public class SigninCommand extends SecuredCommand {

    @Lazy
    @Autowired
    ShellHelper shellHelper;

    @Lazy
    @Autowired
    InputReader inputReader;

    @Autowired
    AuthenticationManager authenticationManager;

    @ShellMethod("Sign in as clidemo user")
    public void signin() {
        String username;
        boolean usernameInvalid = true;
        do {
            username = inputReader.prompt("Please enter your username");
            if (StringUtils.hasText(username)) {
                usernameInvalid = false;
            } else {
                shellHelper.printWarning("Username can not be empty string!");
            }
        } while (usernameInvalid);
        String password = inputReader.prompt("Please enter your password", null, false);
        Authentication request = new UsernamePasswordAuthenticationToken(username, password);

        try {
            Authentication result = authenticationManager.authenticate(request);
            SecurityContextHolder.getContext().setAuthentication(result);
            shellHelper.printSuccess("Credentials successfully authenticated! " + username + " -> welcome to CliDemo.");
        } catch (AuthenticationException e) {
            shellHelper.printWarning("Authentication failed: " + e.getMessage());
        }
    }
}

Rebuild/rerun clidemo and try to signin with the following credentials: lmodric midfielder10, and you should be greeted with welcome message as shown in the picture below.

User successfully signed in to clidemo application.

Also, running a help command now would display that all user commands are available to currently signed in user (lmodric).

NOTE: other available user data can be found in src/main/resources/cli-user.json file.

Restrict user access based on the granted authorities

What remains to be implemented is to enable only users whose property superuser is set to TRUE to have access to create-user, user-details, and update-all-users commands. As can be seen from the implementation of CliudemoUserDetailsService all users are granted ROLE_USER and only those with superuser property are also granted ROLE_ADMIN. What remains to be done is:

  1. Add a new method to SecuredCommand that will check if user has ROLE_ADMIN among his granted authorities.
  2. Associate this new method with all relevant user command methods.

To do so, first, remove all @ShellMethodAvailability annotations from the UserCommand class. Then, modify SecuredCommand class as shown in the code snippet below:

package com.ag04.clidemo.command;

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.shell.Availability;
import org.springframework.shell.standard.ShellMethodAvailability;

public abstract class SecuredCommand {

    @ShellMethodAvailability({"user-list", "my-details"})
    public Availability isUserSignedIn() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !(authentication instanceof  UsernamePasswordAuthenticationToken)) {
            return Availability.unavailable("you are not signedIn. Please sign in to be able to use this command!");
        }
        return Availability.available();
    }

    @ShellMethodAvailability({"create-user", "update-all-users", "user-details"})
    public Availability isUserAdmin() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication == null || !(authentication instanceof  UsernamePasswordAuthenticationToken)) {
            return Availability.unavailable("you are not signedIn. Please sign in to be able to use this command!");
        }
        if (!authentication.getAuthorities().contains("ROLE_ADMIN")) {
            return Availability.unavailable("you have insufficient privileges to run this command!");
        }
        return Availability.available();
    }
}

Spring Shell provides several methods of associating control methods with command methods. In the previous implementation, we have annotated command methods with the @ShellMethodAvailability annotation that contained the name of the control method (ie. isUserSignedIn) that should be invoked to determine whether the particular command is available or not.

@ShellMethodAvailability("isUserSignedIn")

In the final implementation, this relationship is reversed! We have instead opted to annotate control methods with the @ShellMethodAvailabilityannotation and provided the list of command names(!), NOT method names, to which particular method should control access. Thus the method isUserAdmin() is configured to check if user is allowed to access create-user, user-details, and update-all-users commands.

@ShellMethodAvailability({"create-user", "update-all-users", "user-details"})
public Availability isUserAdmin() {
   ...
}

In larger applications, this approach is much more practical, since all command availability configurations are found in a single place, and it is rather easy to determine which method controls the access to which command.

Now if you want to play with this new configuration rebuild and rerun clidemo, sign in with the credentials: nacho/defender6. After attempting to access update-all-users command you should see the following output.

User is denied access to command due to lack of sufficient security privileges.

This completes the fifth and final part of this blog series. Of course, many more topics remain to be tackled but armed with the knowledge of this and previous posts I have no doubt that you will be able to overcome any challenges encountered while using Spring Shell library. I do hope that you have found this series of posts satisfying and that its content will prove helpful in your future (CLI) projects.

Rest of the series

Part 1: Conveying contextual messages to the users in the CLI application

Part 2: Capturing user’s input in the CLI application

Part 3: Displaying the progress of CLI command execution with the use of counters, spinners and progress bars

Part 4: Displaying the data with the use of tables in a Spring Shell based CLI application

Part 5: Securing CLI application with Spring Security

Sample Code:

Entire source code for this tutorial is available at GitHub repository:

https://github.com/dmadunic/clidemo

Additional resources:

Spring Shell project site:

https://projects.spring.io/spring-shell/

Spring Shell official documentation:

https://docs.spring.io/spring-shell/docs/current-SNAPSHOT/reference/htmlsingle/

https://docs.spring.io/spring-shell/docs/current/api/

Spring Security official documentation:

https://spring.io/projects/spring-security

Thank you for reading! I do hope you enjoyed it and please share if you did.

Next

Blog

Deep dive into new React context API

Company

Our people really love it here

How it all started

Est. in 2014., gathering eight employees with eyes set on the future. No matter how set they were, they couldn’t predict the success and extent of growth that would ensue. Today there are more than 100 of us, and people are here to stay.

Stability in unstable times

The turmoil of 2020 caused great inconvenience for people all over the world. However, this did not affect our business. Quite the opposite — we not only kept all jobs and salaries intact, but we also grew in size. And we keep expanding. 

Contact

We’d love to hear from you