web-dev-qa-db-fra.com

L'accès est toujours refusé lors de la sécurité de printemps - DenyAllPermissionEvaluator

J'ai configuré ACL dans mon application Spring Boot. La configuration de la liste de contrôle d'accès est la suivante:

@Configuration
@ComponentScan(basePackages = "com.company")
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class ACLConfigration extends GlobalMethodSecurityConfiguration {

    @Autowired
    DataSource dataSource;

    @Bean
    public EhCacheBasedAclCache aclCache() {
        return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy());
    }

    @Bean
    public EhCacheFactoryBean aclEhCacheFactoryBean() {
        EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
        ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
        ehCacheFactoryBean.setCacheName("aclCache");
        return ehCacheFactoryBean;
    }

    @Bean
    public EhCacheManagerFactoryBean aclCacheManager() {
        return new EhCacheManagerFactoryBean();
    }

    @Bean
    public DefaultPermissionGrantingStrategy permissionGrantingStrategy() {
        ConsoleAuditLogger consoleAuditLogger = new ConsoleAuditLogger();
        return new DefaultPermissionGrantingStrategy(consoleAuditLogger);
    }

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ACL_ADMIN"));
    }

    @Bean
    public LookupStrategy lookupStrategy() {
        return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger());
    }

    @Bean
    public JdbcMutableAclService aclService() {
        return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
    }

    @Bean
    public DefaultMethodSecurityExpressionHandler defaultMethodSecurityExpressionHandler() {
        return new DefaultMethodSecurityExpressionHandler();
    }

    @Override
    public MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = defaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
        expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
        return expressionHandler;
    }
}

Références:

et la configuration de la sécurité est la suivante:

@Configuration
@EnableWebSecurity
public class CustomSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Bean
    public AuthenticationEntryPoint entryPoint() {
        return new LoginUrlAuthenticationEntryPoint("/authenticate");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
                .csrf()
                .disable()
                .authorizeRequests()
                .antMatchers("/authenticate/**").permitAll()
                .anyRequest().fullyAuthenticated()
                .and().requestCache().requestCache(new NullRequestCache())
                .and().addFilterBefore(authenticationFilter(), CustomUsernamePasswordAuthenticationFilter.class);
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.OPTIONS, "/**");
    }

    @Bean
    public CustomUsernamePasswordAuthenticationFilter authenticationFilter()
            throws Exception {
        CustomUsernamePasswordAuthenticationFilter authenticationFilter = new CustomUsernamePasswordAuthenticationFilter();
        authenticationFilter.setUsernameParameter("username");
        authenticationFilter.setPasswordParameter("password");
        authenticationFilter.setFilterProcessesUrl("/authenticate");
        authenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
        authenticationFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
        authenticationFilter.setAuthenticationManager(authenticationManagerBean());
        return authenticationFilter;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Ma classe CustomAuthenticationProvider:

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UsersService usersService;

    @Override
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {

        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        User user = usersService.findOne(username);

        if(user != null && usersService.comparePassword(user, password)){

            return new UsernamePasswordAuthenticationToken(
                    user.getUsername(),
                    user.getPassword(),
                    AuthorityUtils.commaSeparatedStringToAuthorityList(
                            user.getUserRoles().stream().collect(Collectors.joining(","))));
        } else {
            return null;
        }
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(UsernamePasswordAuthenticationToken.class);
    }
}

Voici ma CustomUsernamePasswordAuthenticationToken:

public class CustomUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException {

        if(!request.getMethod().equals("POST"))
            throw new AuthenticationServiceException(String.format("Authentication method not supported: %s", request.getMethod()));

        try {

            CustomUsernamePasswordAuthenticationForm form = new ObjectMapper().readValue(request.getReader(), CustomUsernamePasswordAuthenticationForm.class);

            String username = form.getUsername();
            String password = form.getPassword();

            if(username == null)
                username = "";

            if(password == null)
                password = "";

            UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);

            setDetails(request, token);

            return getAuthenticationManager().authenticate(token);

        } catch (IOException exception) {
            throw new CustomAuthenticationException(exception);
        }
    }

    private class CustomAuthenticationException extends RuntimeException {
        private CustomAuthenticationException(Throwable throwable) {
            super(throwable);
        }
    }
}

En dehors de ce qui précède, j'ai CustomAuthenticationFailureHandler, CustomAuthenticationSuccessHandler, CustomNoRedirectStrategy et CustomUsernamePasswordAuthenticationForm que j'ai sauté pour des raisons de longueur.

Et j'utilise un schéma MySQL qui peut être trouvé ici .

J'ajoute des entrées à mes tables connexes acl comme suit:

INSERT INTO acl_class VALUES (1, com.company.project.domain.users.User)
INSERT INTO acl_sid VALUES (1, 1, "demo")

(J'ai un utilisateur avec le nom d'utilisateur demo)

INSERT INTO acl_object_identity VALUES (1, 1, 1, NULL, 1, 0)
INSERT INTO acl_entry VALUES (1, 1, 1, 1, 1, 1, 1, 1)

Mais tout ce que je reçois, c'est:

Denying user demo permission 'READ' on object com.company.project.domain.users.User@4a49e9b4

dans mon

@PostFilter("hasPermission(filterObject, 'READ')")

Je soupçonne plusieurs problèmes ici:

  1. L'expression hasPermission: Je l'ai remplacée par «READ» et «1», mais pas du tout.
  2. Les entrées de ma base de données ne sont pas correctes
  3. Je ne mets pas en œuvre un évaluateur d'autorisation personnalisé. Est-ce nécessaire ou est-ce que expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService())); est suffisant?

Mettre à jour

Exemple de méthode où @PostFilter est utilisé:

@RequestMapping(method = RequestMethod.GET)
    @PostFilter("hasPermission(filterObject, 'READ')")
    List<User> find(@Min(0) @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit,
                    @Min(0) @RequestParam(value = "page", required = false, defaultValue = "0") Integer page,
                    @RequestParam(value = "email", required = false) String email,
                    @RequestParam(value = "firstName", required = false) String firstName,
                    @RequestParam(value = "lastName", required = false) String lastName,
                    @RequestParam(value = "userRole", required = false) String userRole) {

        return usersService.find(
                limit,
                page,
                email,
                firstName,
                lastName,
                userRole);
    }

Mise à jour n ° 2:

La question reflète maintenant tout ce qui a été mis en place en matière d’authentification/autorisation/ACL.

Mise à jour n ° 3:

Je suis maintenant très proche pour résoudre le problème, la seule chose qui reste à faire est de résoudre ceci:

(https://stackoverflow.com/questions/42996579/custom-permissionevaluator-not-called-alothing-alothing-set-as-permissionevaluator-deny }

Si quelqu'un peut m'aider avec cette question, je peux enfin avoir un compte rendu de ce que j'ai vécu pour résoudre ce problème.

18
Hasan Can Saral

Voici la réponse attendue depuis longtemps:

La documentation décrit clairement:

Pour utiliser les expressions hasPermission (), vous devez configurer explicitement un PermissionEvaluator dans votre contexte d'application. Cela ressemblerait quelque chose comme ça:

donc en gros je faisais dans ma AclConfiguration qui étend GlobalMethodSecurityConfiguration:

    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(new AclPermissionEvaluator(aclService()));
        expressionHandler.setPermissionCacheOptimizer(new AclPermissionCacheOptimizer(aclService()));
        return expressionHandler;
    }

Ce qui n'était pas traité par Spring!

J'ai dû séparer AclConfig et GlobalMethodSecurityConfiguration. Quand il y a @Beans définis dans ce dernier, la méthode ci-dessus n'est pas traitée, ce qui pourrait être un bug (sinon, toute clarification sur le sujet est la bienvenue).

3
Hasan Can Saral

J'ai mis à niveau mon application pour utiliser Spring Security 4.2.1.RELEASE, puis un accès inattendu a été refusé dans toutes les méthodes annotées avec @PreAuthorize, qui fonctionnait parfaitement avant la mise à niveau . J'ai débogué le code de sécurité Spring et j'ai réalisé que le problème était que tous les rôles à vérifier étaient précédés d'une chaîne par défaut "ROLE_", même si j'avais défini mon préfixe par défaut sur vide, comme indiqué dans le code ci-dessous.

auth.ldapAuthentication()
        .groupSearchBase(ldapProperties.getProperty("groupSearchBase"))
        .groupRoleAttribute(ldapProperties.getProperty("groupRoleAttribute"))
        .groupSearchFilter(ldapProperties.getProperty("groupSearchFilter"))

        //this call used to be plenty to override the default prefix
        .rolePrefix("")

        .userSearchBase(ldapProperties.getProperty("userSearchBase"))
        .userSearchFilter(ldapProperties.getProperty("userSearchFilter"))
        .contextSource(this.ldapContextSource);

Toutes mes méthodes de contrôleur ont été annotées avec @PreAuthorize("hasRole('my_ldap_group_name')"); cependant, la structure ne prenait pas en compte mon paramètre de préfixe de rôle vide et utilisait donc ROLE_my_ldap_group_name pour vérifier le rôle réel.

Après avoir approfondi le code du framework, je me suis rendu compte que le préfixe de rôle par défaut de la classe org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler était toujours défini sur "ROLE_". J'ai suivi la source de sa valeur et j'ai découvert qu'il recherchait d'abord un bean déclaré de la classe org.springframework.security.config.core.GrantedAuthorityDefaults afin de rechercher un préfixe par défaut lors de la première initialisation du bean org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; toutefois, cet initialiseur n'ayant pas pu le trouver déclaré , il a fini par utiliser le préfixe par défaut susmentionné.

Je pense que ce n'est pas un comportement attendu: Spring Security aurait dû prendre en compte le même rolePrefix de ldapAuthentication. Cependant, pour résoudre ce problème, il était nécessaire d'ajouter le bean org.springframework.security.config.core.GrantedAuthorityDefaults à mon contexte d'application (j'utilise une configuration basée sur des annotations). Suivant:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class CesSecurityConfiguration extends WebSecurityConfigurerAdapter {

    private static final String ROLE_PREFIX = "";
    //... ommited code ...
    @Bean
    public GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults(ROLE_PREFIX);
    }

}

Vous rencontrez peut-être le même problème - je pourrais voir que vous utilisez DefaultMethodSecurityExpressionHandler et qu'il utilise également le bean GrantedAuthorityDefaults. Par conséquent, si vous utilisez la même version de Spring Security que moi - 4.2.1.RELEASE, vous êtes probablement confronté le même problème.

6
s_bighead

Vos données dans la base de données et votre configuration sont bonnes. J'utilise @PostFilter("hasPermission(filterObject, 'READ')") tout le temps. 

Je vérifierais que votre classe d'utilisateurs qui étend UserDetails renvoie le même nom d'utilisateur via getUsername () que vous avez dans la base de données. En plus de vérifier que la sécurité et l'application sont dans le même contexte. 

la méthode hasPermission prend un objet Authentication en tant que premier paramètre.

boolean hasPermission(Authentication authentication,
                      Object targetDomainObject,
                      Object permission)

L'objet Authentification est une classe d'implémentation, généralement de UsernamePasswordAuthenticationToken . Ainsi, la méthode getPrincipal () doit renvoyer un objet doté d'une méthode getUserName () qui renvoie la même chose que votre base de données. 

Jetez un oeil à PrincipalSid

public PrincipalSid(Authentication authentication) {
    Assert.notNull(authentication, "Authentication required");
    Assert.notNull(authentication.getPrincipal(), "Principal required");

    if (authentication.getPrincipal() instanceof UserDetails) {
        this.principal = ((UserDetails) authentication.getPrincipal()).getUsername();
    }
    else {
        this.principal = authentication.getPrincipal().toString();
    }
}
0
denov