Tailoring the UI with authorization checks

With the REST endpoints locked down, it's nice to know things are secure. However, it doesn't make sense to display options in the UI that will get cut off. Instead, it's better to simply not show them. For that, we can leverage a custom Thymeleaf security rule.

Normally, we would make use of Thymeleaf's Spring Security extension. Unfortunately, the Thymeleaf team has yet to write such support for Spring Framework 5's WebFlux module. No problem! We can craft our own and register it inside the Thymeleaf engine.

For starters, we want to define an authorization scoped operation that could be embedded inside a Thymeleaf th:if="${}" expression, conditionally displaying HTML elements. We can start by adding SecurityExpressionObjectFactory to the images microservice, since that fragment of HTML is where we wish to apply it:

    public class SecurityExpressionObjectFactory 
     implements IExpressionObjectFactory { 
 
       private final 
        SecurityExpressionHandler<MethodInvocation> handler; 
 
       public SecurityExpressionObjectFactory( 
         SecurityExpressionHandler<MethodInvocation> handler) { 
           this.handler = handler; 
         } 
 
         @Override 
         public Set<String> getAllExpressionObjectNames() { 
           return Collections.unmodifiableSet( 
             new HashSet<>(Arrays.asList( 
               "authorization" 
           ))); 
         } 
 
         @Override 
         public boolean isCacheable(String expressionObjectName) { 
           return true; 
         } 
 
         @Override 
         public Object buildObject(IExpressionContext context, 
          String expressionObjectName) { 
            if (expressionObjectName.equals("authorization")) { 
              if (context instanceof ISpringWebFluxContext) { 
                return new Authorization( 
                  (ISpringWebFluxContext) context, handler); 
              } 
            } 
            return null; 
         } 
    } 

The preceding Thymeleaf expression object factory can be described as follows:

  • This class implements Thymeleaf's IExpressionObjectFactory, the key toward writing custom expressions.
  • To do its thing, this factory requires a copy of Spring Security's SecurityExpressionHandler, aimed at method invocations. It's injected into this factory through constructor injection.
  • To advertize the expression objects provided in this class, we implement getAllExpressionObjectNames, which returns an unmodifiable Set containing authorization, the token of our custom expression.
  • We implement the interface's isCacheable and point blank say that all expressions may be cached by the Thymeleaf engine.
  • buildObject is where we create objects based on the token name. When we see authorization, we narrow the template's context down to a WebFlux-based context and then create an Authorization object with the context, Spring Security's expression handler, and a copy of the current ServerWebExchange, giving us all the details we need.
  • Anything else, and we return null, indicating this factory doesn't apply.

Our expression object, Authorization, is defined as follows:

    public class Authorization { 
 
      private static final Logger log = 
        LoggerFactory.getLogger(Authorization.class); 
 
      private ISpringWebFluxContext context; 
      private SecurityExpressionHandler<MethodInvocation> handler; 
 
      public Authorization(ISpringWebFluxContext context, 
        SecurityExpressionHandler<MethodInvocation> handler) { 
        this.context = context; 
        this.handler = handler; 
      } 
      ... 
    } 

The code can be described as follows:

  • It has an Slf4j log so that we can print access checks to the console, giving developers the ability to debug their authorization expressions
  • Through constructor injection, we load a copy of the Thymeleaf ISpringWebFluxContext and the Spring Security SecurityExpressionHandler

With this setup, we can now code the actual function we wish to use, authorization.expr(), as follows:

    public boolean expr(String accessExpression) { 
      Authentication authentication = 
        (Authentication) this.context.getExchange() 
         .getPrincipal().block(); 
 
      log.debug("Checking if user "{}" meets expr "{}".", 
       new Object[] { 
         (authentication == null ? 
          null : authentication.getName()), 
           accessExpression}); 
 
      /* 
      * In case this expression is specified as a standard 
      * variable expression (${...}), clean it. 
      */ 
      String expr = 
        ((accessExpression != null 
            && 
            accessExpression.startsWith("${") 
            && 
            accessExpression.endsWith("}")) ? 
 
            accessExpression.substring(2, 
                accessExpression.length()-1) : 
            accessExpression); 
 
      try { 
        if (ExpressionUtils.evaluateAsBoolean( 
          handler.getExpressionParser().parseExpression(expr), 
          handler.createEvaluationContext(authentication, 
           new SimpleMethodInvocation()))) { 
 
             log.debug("Checked "{}" for user "{}". " + 
                    "Access GRANTED", 
                new Object[] { 
                    accessExpression, 
                    (authentication == null ? 
                        null : authentication.getName())}); 
 
             return true; 
           } else { 
              log.debug("Checked "{}" for user "{}". " + 
               "Access DENIED", 
                new Object[] { 
                  accessExpression, 
                   (authentication == null ? 
                    null : authentication.getName())}); 
 
              return false; 
           } 
      } catch (ParseException e) { 
          throw new TemplateProcessingException( 
            "An error happened parsing "" + expr + """, e); 
      } 
    } 

This last Thymeleaf custom function can be described as follows:

  • Our custom expr() function is named in the first line, is publicly visible, and returns a Boolean, making it suitable for th:if={} expressions.
  • The first thing we need is to grab the Authentication object from the context's ServerWebExchange. Because we are inside an inherently blocking API, we must use block() and cast it to a Spring Security Authentication.
  • To help developers, we log the current user's authentication details along with the authorization expression.
  • In the event the whole expression is wrapped with ${}, we need to strip that off.
  • We tap into Spring Security's SpEL support by invoking ExpressionUtils.evaluateAsBoolean().
  • That method requires that we parse the expression via handler.getExpressionParser().parseExpression(expr).
  • We must also supply the SpEL evaluator with a context, including the current authentication as well as SimpleMethodInvocation, since we are focused on method-level security expressions.
  • If the results are true, it means access has been granted. We log it and return true.
  • If the results are false, it means access has been denied. We log that and return false.
  • In the event of a badly written SpEL expression, we catch it with an exception handler and throw a Thymeleaf TemplateProcessingException.

The preceding code defines the expr() function, while the enclosing SecurityExpressionObjectFactory scopes the function inside authorization, setting us up to embed #authorization.expr(/* my Spring Security SpEL expression*/) inside Thymeleaf templates.

The next step in extending Thymeleaf is to define a Dialect with our expression object factory, as follows:

    public class SecurityDialect extends AbstractDialect 
     implements IExpressionObjectDialect { 
 
       private final 
        SecurityExpressionHandler<MethodInvocation> handler; 
 
       public SecurityDialect( 
         SecurityExpressionHandler<MethodInvocation> handler) { 
           super("Security Dialect"); 
           this.handler = handler; 
       } 
 
       @Override 
       public IExpressionObjectFactory getExpressionObjectFactory()
{ return new SecurityExpressionObjectFactory(handler); } }

This previous code can be described as follows:

  • SecurityDialect extends AbstractDialect and implements IExpressionObjectDialect
  • We need a copy of Spring Security's SecurityExpressionHandler in order to parse Spring Security SpEL expression, and it's provided by constructor injection
  • To support IExpressionObjectDialect, we supply a copy of our custom SecurityExpressionObjectFactory factory inside the getExpressionObjectFactory() method

With our tiny extension dialect defined, we must register it with Thymeleaf's template engine. To do so, the easiest thing is to write a custom Spring post processor, like this:

    @Component 
    public class SecurityDialectPostProcessor 
     implements BeanPostProcessor, ApplicationContextAware { 
 
       private ApplicationContext applicationContext; 
 
       @Override 
       public void setApplicationContext( 
         ApplicationContext applicationContext) 
         throws BeansException { 
           this.applicationContext = applicationContext; 
       } 
 
       @Override 
       public Object postProcessBeforeInitialization( 
         Object bean, String beanName) throws BeansException { 
           if (bean instanceof SpringTemplateEngine) { 
             SpringTemplateEngine engine = 
               (SpringTemplateEngine) bean; 
             SecurityExpressionHandler<MethodInvocation> handler = 
               applicationContext.getBean( 
                 SecurityExpressionHandler.class); 
             SecurityDialect dialect = 
new SecurityDialect(handler); engine.addDialect(dialect); } return bean; } @Override public Object postProcessAfterInitialization( Object bean, String beanName) throws BeansException { return bean; } }

The preceding code can be defined as follows:

  • @Component signals Spring Boot to register this class.
  • By implementing the BeanPostProcessor interface, Spring will run every bean in the application context through it, giving our SecurityDialectPostProcessor the opportunity to find Thymeleaf's engine and register our custom dialect.
  • Since our custom dialect needs a handle on the SecurityExpressionHandler bean, we also implement the ApplicationContextAware interface, giving it a handle on the application context.
  • It all comes together in postProcessBeforeInitialization, which is invoked against every bean in the application context. When we spot one that implements Thymeleaf's SpringTemplateEngine, we grab that bean, fetch SecurityExpressionHandler from the app context, create a new SecurityDialect, and add that dialect to the engine. Every bean, modified or not, is returned back to the app context.
  • Because we don't need any processing before initialization, postProcessAfterInitialization just passes through every bean.

With all this in place, we are ready to make some security-specific tweaks to our templates.

In the main page (chat microservice's index.html template), it would be handy to put some user-specific information. To display the username and their roles, we can update the HomeController like this:

    @GetMapping("/") 
    public String index(@AuthenticationPrincipal Authentication auth,
Model model) { model.addAttribute("authentication", auth); return "index"; }

This preceding adjustment to the home controller can be described as follows:

  • @AuthenticationPrincipal Authentication auth grants us a copy of the current user's Authentication object
  • Model model gives us a model object to add data to the template
  • By simply sticking the Authentication object into the model, we can use it to display security details on the web page

Now, we can display the username and their roles, as follows:

    <div> 
      <span th:text="${authentication.name}" /> 
      <span th:text="${authentication.authorities}" /> 
    </div> 
    <hr /> 

This little DIV element that we just defined includes the following:

  • Displays the authentication's name property, which is the username
  • Displays the authentication's authorities properties, which are the user's roles
  • Draws a horizontal line, setting this bit of user specifics apart from the rest of the page
Since we are using HTTP Basic security, there is no value in putting a logout button on the screen. You have to shut down the browser (or close the incognito tab) to clear out security credentials and start afresh.

We can now expect to see the following when we log in as greg:

We mentioned limiting things that the user can't do. The big one in our social media platform is restricted access to deleting images. To enforce this in the UI, we need to parallel the authorization rule we wrote earlier in the images microservice's index.html, as shown here:

    <td> 
      <button th:if="${#authorization.expr('hasRole(''ROLE_ADMIN'')') 
or #authorization.expr('''__${image.owner}__'' ==
authentication.name')}" th:id="'/images/' + ${image.name}"
class="delete">Delete</button> </td>

This last code looks a bit more complex than the @PreAuthorize rule wrapping ImageService.deleteImage(), so let's take it apart:

  • We use Thymeleaf's th:if="... " expression along with ${} to construct a complex expression consisting of two #authorization.expr() functions chained by or.
  • #authorization.expr('hasRole(''ROLE_ADMIN'')') grants access if the user has ROLE_ADMIN.
  • #authorization.expr('''__${image.owner}__'' == authentication.name') grants access if the image.owner attribute matches the current authentication.name.
  • By the way, the double underscore before and after ${image.owner} is Thymeleaf's preprocessor. It indicates that this is done before any other part of the expression is evaluated. In essence, we need the image's owner attribute parsed first, stuffed into the authorization expression, and finally run through our custom tie-in to Spring Security's SpEL parser.
The expressions inside #authorization.expr() are supposed to be wrapped in single quotes. Literal values themselves have to be wrapped in single quotes. To escape a single quote in this context requires a double single quote. Confused yet? Thymeleaf's rules for concatenation, preprocessing, and nesting expressions can, at times, be daunting. To help debug an expression, pull up the Authorization class coded earlier inside your IDE and set breakpoints inside the proper security expression. This will pause code execution, allowing you to see the final expression before it gets passed, hopefully making it easier to craft a suitable authorization rule.

With our nice tweaks to the UI, let's see what things look like if we have two different images uploaded, one from an admin and one from a regular user.

If greg is logged in, we can see the following screenshot:

In the preceding screenshot, both images have a Delete button, since greg has ROLE_ADMIN.

If phil is logged in, we can see the following screenshot:

In the earlier screenshot, only the second image has the Delete button, since phil owns it.

With these nice details in place, we can easily check out the headers relayed to the backend using the browser's debug tools, as seen in this screenshot:

This collection of request and response headers shown in the last image lets us see the following things:

When we started building this social media platform early in this book, we had several operations tied into our Thymeleaf template. This type of tight interaction between controllers and views is of classic design. However, the more things shift to piecing together bits of HTML and leveraging JavaScript, the more it becomes useful to have REST-based services. Writing AJAX calls decouples the HTML from the server-side controls, which can be further leveraged if we use tools such as React.js (https://facebook.github.io/react/). This gets us out of the business of assembling DOM elements and lets us focus on the state of the frontend instead.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.239.41