1

Is it possible to expose a spring data rest generated API to manage the same users used with spring security for authentication and access control ?

Consider the entity:

@Entity
public class User implements UserDetails {
   ....
}

Which is used with spring security:

@Service
public class RepositoryUserDetailsService implements UserDetailsService{

    private final UnsecuredUserRepository users;

    @Autowired
    public RepositoryUserDetailsService(UnsecuredUserRepository users) {
        this.users = users;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User one = users.findOne(username);
        if (one == null) {
            throw new UsernameNotFoundException("No such user");
        }
        return one;
    }

}

It uses the following spring data repository:

public interface UnsecuredUserRepository extends CrudRepository<User, String> {
}

I now want to add an admin API to manage users. Spring data rest can do this for me, and I can use spring security to secure it.

@PreAuthorize("hasRole('ROLE_USER_MANAGER')")
public interface UserRepository extends CrudRepository<User, String>, UserSignUpExtension {

    @Override
    @PreAuthorize("hasRole('ROLE_USER_MANAGER') or #userName == authentication?.name")
    void delete(@Param("userName") String userName);

}

The problem is that one can't have multiple repositories for the same entities with spring data rest, using the secured repo creates a chicken egg problem, and prevents me from having startup code that creates default users (since the security checks are already enforced).

Alpar
  • 2,807
  • 23
  • 16
  • try the curl command : http://www.codingpedia.org/ama/how-to-test-a-rest-api-from-command-line-with-curl/ – AchillesVan Nov 01 '16 at 19:58
  • The curl command won't help. The issue is not with testing the API, but with exposing it in the first place. – Alpar Nov 02 '16 at 04:18
  • This is really funny hehe. Just have another repository with no permission enforcing or hibernate or something that runs at startup to create a user account. This repo should not be available to the client in anyway. – Derrops Nov 02 '16 at 05:51
  • yes, that was my initial line of thinking, but spring data rest picks up all repositories automatically, se the question linked in the original question for why this problematic. Essentially either one of the repositories gets exported at random, because this is governed by the entities and not the repositories in spring data rest. – Alpar Nov 02 '16 at 06:16
  • 1
    **1.** Can't you enforce security at the URL level rather than the repo level? This would allow unsecured non-rest calls. See: http://stackoverflow.com/a/29087933/1356423 **2.** You can easily configure SDR to export only repositories you wish to export: http://docs.spring.io/spring-data/rest/docs/current/reference/html/#_which_repositories_get_exposed_by_defaults – Alan Hay Nov 02 '16 at 09:49
  • 1. Securing at the URL would work and solve the issue. One would secure the SDR generated URLs. 2. I was aware of that, but I do want to export, since I want a user management API. – Alpar Nov 02 '16 at 10:37

1 Answers1

1

I managed to solve this eventually by embracing the spring security rather than trying to circumvent it.

I implemented this utility:

import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;

public class AsInternalUser implements AutoCloseable {

    private final SecurityContext previousContext;

    public AsInternalUser() {
        previousContext = SecurityContextHolder.getContext();
        SecurityContext context = SecurityContextHolder.createEmptyContext();
        context.setAuthentication(
                new AnonymousAuthenticationToken("INTERNAL","INTERNAL_USERNAME", AuthorityUtils.createAuthorityList("ROLE_INTERNAL"))
        );
        SecurityContextHolder.setContext(
                context
        );
    }

    @Override
    public void close() {
        if (previousContext == null) {
            SecurityContextHolder.clearContext();
        } else {
            SecurityContextHolder.setContext(previousContext);
        }
    }
}

My initial user creation thus becomes:

try (AsInternalUser __ = new AsInternalUser()) {
            if (!users.exists(DEFAULT_ADMIN_NAME)) {
                users.save(new User(DEFAULT_ADMIN_NAME, passwordEncoder.encode(DEFAULT_ADMIN_PASSWORD), Arrays.asList(Roles.values())));
            }
        }

And of course the repository needs to grant access to the new ROLE_INTERNAL

@PreAuthorize("hasAnyRole('ROLE_USER_MANAGER', 'ROLE_INTERNAL')")
public interface UserRepository extends CrudRepository<User, String>, UserSignUpExtension {
}

Other places like user registration will also have to escalate to the internal role.

I think this is an even better approach than circumventing the security model, as it allows finer grain control and reduces chances of accidentally calling the unsecured repository and compromising security.

Alpar
  • 2,807
  • 23
  • 16