Getting Started Spring Content with RBAC

What you'll build

We'll build on the previous guide Getting Started with Spring Content REST API.

What you'll need

  • About 30 minutes

  • A favorite text editor or IDE

  • JDK 1.8 or later

  • Maven 3.0+

How to complete this guide

Before we begin let's set up our development environment:

  • Download and unzip the source repository for this guide, or clone it using Git: git clone https://github.com/paulcwarren/spring-content-gettingstarted.git

  • We are going to start where Getting Started with Spring Content REST API leaves off so cd into spring-content-gettingstarted/spring-content-rest/complete

When you’re finished, you can check your results against the code in spring-content-gettingstarted/spring-content-with-rbac/complete.

Update dependencies

Add org.springframework.boot:spring-boot-starter-security dependencies.

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-content-with-rbac</artifactId>

    <parent>
        <groupId>com.github.paulcwarren</groupId>
        <artifactId>gettingstarted-spring-content</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../../pom.xml</relativePath>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-parent</artifactId>
                <version>3.3.2</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-rest</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.paulcwarren</groupId>
            <artifactId>spring-content-fs-boot-starter</artifactId>
            <version>${spring.content.version}</version>
        </dependency>
        <dependency>
            <groupId>com.github.paulcwarren</groupId>
            <artifactId>spring-content-rest-boot-starter</artifactId>
            <version>${spring.content.version}</version>
        </dependency>
        <dependency>
          <groupId>org.projectlombok</groupId>
          <artifactId>lombok</artifactId>
        </dependency>

        <!-- Test dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>junit-jupiter-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.rest-assured</groupId>
            <artifactId>spring-mock-mvc</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.github.paulcwarren</groupId>
            <artifactId>ginkgo4j</artifactId>
            <version>${ginkgo.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Add Security Constraints

Enable web security.


package gettingstarted;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.AuthenticationException;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.www.BasicAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.io.IOException;
import java.io.PrintWriter;

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SpringSecurityConfig /*extends WebSecurityConfigurerAdapter*/ {

	@Autowired
	public void configureGlobalSecurity(AuthenticationManagerBuilder auth) throws Exception {
		auth.inMemoryAuthentication().
				withUser(User.withDefaultPasswordEncoder().username("paul").password("warren").roles("READER", "AUTHOR")).
				withUser(User.withDefaultPasswordEncoder().username("eric").password("wimp").roles("READER").
						build());
	}

	@Bean
	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
		http.csrf().disable()
				.authorizeRequests()
				.requestMatchers(new AntPathRequestMatcher("/files/**/content","GET")).hasRole("READER")
				.requestMatchers(new AntPathRequestMatcher("/files/**/content","PUT")).hasRole("AUTHOR")
				.requestMatchers(new AntPathRequestMatcher("/files/**/content","DELETE")).hasRole("AUTHOR")
				.anyRequest().permitAll()
				.and().httpBasic()
				.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		return http.build();
	}
}

First, define two users and two roles. Eric is a content reader and Paul is a content reader and author.

Second, secure the Spring Content endpoint /files/**/content that GETs and SETs content to the reader and author roles respectively.

@PreAuthorize

As an alternative secure removing content to the author role by using an alternative approach using the @PreAuthorize annotation on the FileContentStore's unset method.


package gettingstarted;

import org.springframework.content.commons.property.PropertyPath;
import org.springframework.content.commons.repository.ContentStore;
import org.springframework.content.commons.repository.UnsetContentParams;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Component;

@Component  // just to keep the ide happy!
public interface FileContentStore extends ContentStore<File, String> {

	@Override
	@PreAuthorize("hasRole('ROLE_AUTHOR')")
	public File unsetContent(File file, PropertyPath propertyPath, UnsetContentParams params);

}

Build an executable JAR

If you are using Maven, you can run the application using mvn spring-boot:run. Or you can build the JAR file with mvn clean package and run the JAR by typing:

java -jar target/gettingstarted-spring-content-with-rbac-0.0.1.jar

Create an Entity and version it

Create an entity:

curl -X POST -H 'Content-Type:application/hal+json' -d '{}' http://localhost:8080/files/

Attempt to associate content as Eric:

curl -u eric:wimp -X PUT -H 'Content-Type:text/plain' -d 'Hello Spring Content with RBAC World!' http://localhost:8080/files/1/content

Associate content as Paul:

curl -u paul:warren -X PUT -H 'Content-Type:text/plain' -d 'Hello Spring Content with RBAC World!' http://localhost:8080/files/1/content

Fetch the content as Eric:

curl -u eric:wimp -H 'Accept:text/plain' http://localhost:8080/files/1/content

And as Paul:

curl -u paul:warren -H 'Accept:text/plain' http://localhost:8080/files/1/content

Attempt to delete content as Eric:

curl -u eric:wimp -X DELETE http://localhost:8080/files/1/content

Delete content as Paul:

curl -u paul:warren -X DELETE http://localhost:8080/files/1/content

Summary

Congratulations! You've written a simple application that uses Spring Content secured with role-based access control.

Don't forget you can simply change the type of the spring-content bootstarter project on the classpath to switch from file storage to a different storage technology.

Spring Content supports the following implementations:-

  • Spring Content Filesystem; stores content as Files on the Filesystem (as used in this tutorial)

  • Spring Content S3; stores content as Objects in Amazon S3

  • Spring Content JPA; stores content as BLOBs in the database

  • Spring Content MongoDB; stores content as Resources in Mongo's GridFS