问题
For some weird reason my @PreAuthorize annotation with a hasPermission expression is triggered from ONE of my repositories but not in another that seems almost identical.
Just to get this out of the way first, I have enabled GlobalMethodSecurity
with prePostEnabled = true
. I have also written a custom PermissionEvaluator.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Autowired
private CustomPermissionEvaluator permissionEvaluator;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}
For the Project Repository below everything is working well, my hasPermission expression is correctly triggered and the custom PermissionEvaluator is run.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.security.access.prepost.PreAuthorize;
@PreAuthorize("hasRole('" + Constants.ROLE_USER + "')")
public interface ProjectRepository extends PagingAndSortingRepository<Project, Long> {
@Override
@PreAuthorize("hasPermission(#project, " + Constants.WRITE + ")")
Project save(@Param("project") Project project);
@Override
//TODO: add security based on id delete
//@PreAuthorize("hasPermission(#project, " + Constants.DELETE + ")")
void delete(@Param("id") Long id);
@Override
@PreAuthorize("hasPermission(#project, " + Constants.DELETE + ")")
void delete(@Param("project") Project project);
@Override
@Query("select p from Project p left join p.roles r left join r.account a where p.id = ?1 and ?#{principal.username} = a.username")
Project findOne(@Param("id") Long id);
@Override
@Query("select p from Project p left join p.roles r left join r.account a where ?#{principal.username} = a.username")
Page<Project> findAll(Pageable pageable);
}
For completeness, this is the Project entity:
import com.fasterxml.jackson.annotation.JsonBackReference;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import javax.persistence.*;
import java.util.List;
@Data
@Entity
@RequiredArgsConstructor
public class Project {
private @Id @GeneratedValue Long id;
private @Column(nullable = false) @NonNull String name;
private @Column(nullable = false) @NonNull String description;
private @Version @JsonIgnore Long version;
private @OneToMany(mappedBy = "project", cascade = CascadeType.REMOVE) List<ProjectRole> roles;
private Project() {
}
}
However, for the ProjectRole Repository below, the hasPermission expression is NOT triggered. In fact, even the PreAuthorize on the whole class isn't triggered (I set the required user role to some random string to test this). The @Query annotations are used correctly though.
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.data.repository.query.Param;
import org.springframework.security.access.prepost.PreAuthorize;
@PreAuthorize("hasRole('" + Constants.ROLE_USER + "')")
public interface ProjectRoleRepository extends PagingAndSortingRepository<ProjectRole, Long> {
//TODO: why is this not triggering?
@Override
@PreAuthorize("hasPermission(#projectRole, " + Constants.WRITE + ")")
ProjectRole save(@Param("projectRole") ProjectRole projectRole);
//TODO: add security based on id delete
@Override
void delete(@Param("id") Long id);
@Override
@PreAuthorize("hasPermission(#projectRole, " + Constants.DELETE + ")")
void delete(@Param("projectRole") ProjectRole projectRole);
@Override
@Query("select r from ProjectRole r left join r.account a where r.id = ?1 and ?#{principal.username} = a.username")
ProjectRole findOne(@Param("id") Long id);
@Override
@Query("select r from ProjectRole r left join r.account a where ?#{principal.username} = a.username")
Page<ProjectRole> findAll(Pageable pageable);
ProjectRole findByProjectAndAccount(Project project, Account account);
}
And the accompanying ProjectRole class... just in case it has something to do with that.
import lombok.Data;
import lombok.NonNull;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
@Data
@Entity
@Table(uniqueConstraints = {
@UniqueConstraint(columnNames = {"project_id", "account_id"})
})
public class ProjectRole {
private @Id @GeneratedValue Long id;
private @JoinColumn(name = "project_id", nullable = false) @NonNull @ManyToOne Project project;
private @JoinColumn(name = "account_id", nullable = false) @NonNull @ManyToOne Account account;
private @Enumerated ProjectRoleEnum name;
protected ProjectRole() {}
public ProjectRole(Project project, Account account, ProjectRoleEnum name) {
this.project = project;
this.account = account;
this.name = name;
}
}
The code of the custom PermissionEvaluator itself is below. I set a breakpoint on the first line of the hasPermission method, it's triggered for other repositories but not for ProjectRole.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import java.io.Serializable;
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
private final ProjectRoleRepository projectRoleRepository;
private final AccountRepository accountRepository;
@Autowired
public CustomPermissionEvaluator(ProjectRoleRepository projectRoleRepository, AccountRepository accountRepository) {
this.projectRoleRepository = projectRoleRepository;
this.accountRepository = accountRepository;
}
@Override
public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
if (targetDomainObject == null)
return false;
if ((auth == null) || !(permission instanceof String))
return false;
Object principal = auth.getPrincipal();
if (!(principal instanceof String || principal instanceof CustomUserPrincipal))
throw new UnsupportedOperationException("Principal should be instance of String or CustomUserPrincipal.");
Account account;
if (principal instanceof String) account = accountRepository.findByUsername((String) principal);
else account = ((CustomUserPrincipal) principal).getAccount();
if (targetDomainObject instanceof Project)
return hasPermissionOnProject(account, (Project) targetDomainObject, (String) permission);
else if (targetDomainObject instanceof ProjectRole)
return hasPermissionOnProjectRole(account, (ProjectRole) targetDomainObject, (String) permission);
else
throw new RuntimeException("targetDomainObject should be instance of Project or ProjectRole but was " + targetDomainObject);
}
private boolean hasPermissionOnProjectRole(Account account, ProjectRole projectRole, String permission) {
//code here
}
private boolean hasPermissionOnProject(Account account, Project project, String permission) {
//code here
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
return false;
}
}
I have also set a breakpoint on the ProjectRoleRepository interface on the save method, it takes a while to trigger if I add a breakpoint there but it does trigger and shows the values are filled correctly.
Is there ANYTHING I am doing differently that could be the cause for the @PreAuthorize annotation/hasPermission expression not triggering for the ProjectRoleRepository class? I have 2 more repositories with security, including the one I showed here, where everything is working correctly. If needed I can show more code but I think this is all that's relevant.
Edit: some code showing in which case the evaluator is triggered for Project but not ProjectRole. This is in a CommandLineRunner class that we use to initially fill the database with some testdata.
@Override
public void run(String... strings) {
Account user1 = this.accounts.save(new Account("user1", "xxx",
Constants.ROLE_USER));
SecurityContextHolder.getContext().setAuthentication(
new UsernamePasswordAuthenticationToken("user1", "doesn't matter",
AuthorityUtils.createAuthorityList(Constants.ROLE_USER)));
Project p1 = new Project("Name", "Blabla.");
this.projects.save(p1);
ProjectRole ownerRolep1 = new ProjectRole();
ownerRolep1.setAccount(user1);
ownerRolep1.setProject(p1);
ownerRolep1.setName(ProjectRoleEnum.OWNER);
this.projectRoles.save(ownerRolep1);
}
The other way I'm reproducing this is by actually doing REST calls, posting the entity triggers the evaluator for Project but not for ProjectRole
curl -X POST -u username:pw localhost:8080/projects -d "{\"name\": \"projectName\", \"description\": \"projectDesc\"}" -H "Content-Type:application/json"
curl -X POST -u username:pw localhost:8080/projectRoles -d "{\"name\":\"1\", \"account\": \"/accounts/3\", \"project\": \"/projects/8\"}" -H "Content-Type:application/json"
回答1:
We found the culprit.
@EnableGlobalMethodSecurity(prePostEnabled = true)
had previously been moved to our MethodSecurityConfiguration
class, in which we set up the custom PermissionEvaluator. I removed it from there and put it back in our public class SecurityConfiguration extends WebSecurityConfigurerAdapter
and that fixed things. But I still find it very odd that it would still work for some repositories but not for others. If someone could shine a light on that, that would be great. For completeness sake, the class below was where we incorrectly defined it.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.GlobalMethodSecurityConfiguration;
@Configuration
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
@Autowired
private CustomPermissionEvaluator permissionEvaluator;
@Override
protected MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler =
new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}
回答2:
Note for Spring Data Rest users, you do not need to create a GlobalMethodSecurityConfiguration
to use a custom PermissionEvaluator
. In fact, this will lead down a rabbit hole of hurt.
Instead, simply define your custom PermissionEvaluator
as a normal bean in your context and Spring security will pick it up.
来源:https://stackoverflow.com/questions/48859093/why-is-preauthorize-with-haspermission-in-custom-permissionevaluator-triggere