Search code examples
javaspringsecurityauthenticationspring-data-rest

Spring Data Rest only use path with parent


Short Description: I want spring-data-rest to only generate endpoints like "/users/{userId}/settings", "/users/{userId}/values" and not "/settings" or "/values".

More Details:

I started to get annoyed with checking in every controller function whether the logged in user has access to the requested resource and had the following idea to automate it (if someone has a better idea which doesn't cause the problems below, I would be happy to hear)

In my application the user only has access to his own resource and can't see any data of other users.

So my idea was to restrict (with the Websecurity config) all paths starting with "/users/{id}**" to only allow access when the logged in user has the given id. That way if my endpoints are e.g. "/users/{userId}/userValues" or "/users/{userId}/settings" etc. each user can only access his own data because of the permission rule

To further automate the process and to ensure that I don't forget the check about userId in the controller, I tried to use spring-data-rest which is great except one problem:

spring-data-rest generates an API-endpoint for each object. They look like this:
"/settings", "/users", "/values".

The paths I want are generated as well: "/users/{userId}/values" and "/users/{userId}/values" and it correctly checks the userId so it my idea seems to work in principle.

However I can't figure out how to disable the endpoints where the path starts with the child ( "/settings", "/values".) since I only want the endpoints that start with "/users/{userId}" to be available.

Is this possible? Or is there a better way to achieve that users can only see and edit their own data?

Here are the classes of my project for testing this scenario (I know that the naming is horrible, I will setup a new project if I know that I can make this work, this is just for testing):

Gradle dependencies:

dependencies {
    compile("org.springframework.boot:spring-boot-starter")
    //web frontend
    compile("org.springframework.boot:spring-boot-starter-web")
    //rest endpoint generator
    compile("org.springframework.boot:spring-boot-starter-data-rest")
    //database connection
    compile("mysql:mysql-connector-java")
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    //Swagger (/v2/api-docs)
    compile("io.springfox:springfox-swagger2:2.9.2")
    //pretty swagger (/swagger-ui.html)
    compile("io.springfox:springfox-swagger-ui:2.9.2")
    //Lombok (automatic getter / setter)
    compile("org.projectlombok:lombok:1.18.6")
}

User:

@Entity
@Getter
@Setter
public class User {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Integer id;

  private String firstName;

  private String lastName;

  @OneToMany(mappedBy="user", cascade= CascadeType.ALL)
  private List<UserValue> values;

  @OneToOne
  @RestResource(path = "settings", rel = "SettingsRel")
  private Settings settings;
}

UserValue:

@Entity
public class UserValue {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;

  String value;

  @ManyToOne
  @JoinColumn(name = "user_id")
  @JsonIgnore
  private User user;

  public Integer getId() {
    return id;
  }

  public void setId(Integer id) {
    this.id = id;
  }

  public String getValue() {
    return value;
  }

  public void setValue(String value) {
    this.value = value;
  }

  public User getUser() {
    return user;
  }

  public void setUser(User user) {
    this.user = user;
  }
}

Settings:

@Entity
@Getter
@Setter
public class Settings {

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  Integer id;

  private HashMap<String, Object> settings;
}

UserValueRepository:

public interface UserValueRepository extends CrudRepository<UserValue, Integer> {
}

UserRepository:

   public interface UserRepository extends CrudRepository<User, Integer> {
    }

SettingsRepository:

  public interface SettingsRepository extends CrudRepository<Settings, Integer> {
    }

Two additional problems I have with spring-data-rest are that it doesn' seem to work with Swagger and Lombok. So I am even more open to alternative ways to achieve my goal


Solution

  • So concerning my original question: There are two ways to achieve the disabling of paths that don't start with "/users/{userId}": Either disabling it directly in the WebSecurityConfigurerAdapter or using a filter. I used a filter that forwarded for example the request "/settings" to "/users/{idOfLoggedInUser}/settings" and it all worked great for GET-Request.

    However it seems that spring-data-rest only processes POST and PUT Request for the root-path of an object, so while GET("/users/{userId}/settings") works without a problem, if you want to POST it needs to be "/settings" and can't be automatically bound to a user.

    So it looks like I need to write the Controller manually after all.

    However I still found a more elegant solution to the underlying authentication problem than manually selecting the correct user from the database and then setting that user on the given object with @PreAuthorize and @PostAuthorize in Spring. Both concepts are still a bit tricky for me but they can get the job done and will later be expandable in a very good way for rules like "friends can also see data if you gave them access" or similar things.