Comment tester printemps-sécurité-oauth2 de ressources du serveur de sécurité?

après la sortie de Spring Security 4 et c'est support amélioré pour les tests j'ai voulu mettre à jour mes tests actuels du serveur de ressources OAuth2 de sécurité du printemps.

a l'heure actuelle, j'ai une classe de helper qui met en place un OAuth2RestTemplate en utilisant ResourceOwnerPasswordResourceDetails avec un test ClientId connexion à une réelle AccessTokenUri pour demander un token valide pour mes tests. Ce resttemplate est alors utilisé pour faire des requêtes dans mon @WebIntegrationTest S.

je voudrais laisser tomber la dépendance sur le le serveur D'autorisations réel, et l'utilisation des justificatifs d'utilisateur valides (si limités) dans mes tests, en profitant du nouveau support de test dans la sécurité de printemps 4.

Jusqu'à présent toutes mes tentatives d'utiliser @WithMockUser,@WithSecurityContext,SecurityMockMvcConfigurers.springSecurity()& SecurityMockMvcRequestPostProcessors.* n'ont pas réussi à faire des appels authentifiés par MockMvc, et je ne peux pas trouver de tels exemples dans le Ressort d'exemples de projets.

est-ce que quelqu'un peut m'aider à tester mon serveur de ressources oauth2 avec une sorte de moquerie de justificatifs d'identité, tout en testant les restrictions de sécurité imposées?

***EDIT ** Exemple de code disponible ici:https://github.com/timtebeek/resource-server-testing Pour chacune des classes de test, je comprends pourquoi ça ne fonctionne pas comme ça, mais je cherche des moyens qui me permettraient de tester la configuration de sécurité facilement.

je pense maintenant à créer un OAuthServer très permissif sous src/test/java, ce qui pourrait aider un peu. Quelqu'un a une autre des suggestions?

33
demandé sur Tim 2015-04-08 12:24:59

8 réponses

pour tester efficacement la sécurité du serveur de ressources, les deux avec MockMvc et RestTemplate il aide à configurer un AuthorizationServersrc/test/java:

AuthorizationServer

@Configuration
@EnableAuthorizationServer
@SuppressWarnings("static-method")
class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() throws Exception {
        JwtAccessTokenConverter jwt = new JwtAccessTokenConverter();
        jwt.setSigningKey(SecurityConfig.key("rsa"));
        jwt.setVerifierKey(SecurityConfig.key("rsa.pub"));
        jwt.afterPropertiesSet();
        return jwt;
    }

    @Autowired
    private AuthenticationManager   authenticationManager;

    @Override
    public void configure(final AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
        .authenticationManager(authenticationManager)
        .accessTokenConverter(accessTokenConverter());
    }

    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
        .withClient("myclientwith")
        .authorizedGrantTypes("password")
        .authorities("myauthorities")
        .resourceIds("myresource")
        .scopes("myscope")

        .and()
        .withClient("myclientwithout")
        .authorizedGrantTypes("password")
        .authorities("myauthorities")
        .resourceIds("myresource")
        .scopes(UUID.randomUUID().toString());
    }
}

test D'intégration

Pour les tests d'intégration, il est alors possible d'utiliser simplement la règle de support de test intégrée dans OAuth2 test et les irritations:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApp.class)
@WebIntegrationTest(randomPort = true)
@OAuth2ContextConfiguration(MyDetails.class)
public class MyControllerIT implements RestTemplateHolder {
    @Value("http://localhost:${local.server.port}")
    @Getter
    String                      host;

    @Getter
    @Setter
    RestOperations              restTemplate    = new TestRestTemplate();

    @Rule
    public OAuth2ContextSetup   context         = OAuth2ContextSetup.standard(this);

    @Test
    public void testHelloOAuth2WithRole() {
        ResponseEntity<String> entity = getRestTemplate().getForEntity(host + "/hello", String.class);
        assertTrue(entity.getStatusCode().is2xxSuccessful());
    }
}

class MyDetails extends ResourceOwnerPasswordResourceDetails {
    public MyDetails(final Object obj) {
        MyControllerIT it = (MyControllerIT) obj;
        setAccessTokenUri(it.getHost() + "/oauth/token");
        setClientId("myclientwith");
        setUsername("user");
        setPassword("password");
    }
}

test MockMvc

Test avec MockMvc est également possible, mais besoin d'un peu de classe d'assistance pour obtenir un RequestPostProcessor qui définit l' Authorization: Bearer <token> l'en-tête de demande:

@Component
public class OAuthHelper {
    // For use with MockMvc
    public RequestPostProcessor bearerToken(final String clientid) {
        return mockRequest -> {
            OAuth2AccessToken token = createAccessToken(clientid);
            mockRequest.addHeader("Authorization", "Bearer " + token.getValue());
            return mockRequest;
        };
    }

    @Autowired
    ClientDetailsService                clientDetailsService;
    @Autowired
    AuthorizationServerTokenServices    tokenservice;

    OAuth2AccessToken createAccessToken(final String clientId) {
        // Look up authorities, resourceIds and scopes based on clientId
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        Collection<GrantedAuthority> authorities = client.getAuthorities();
        Set<String> resourceIds = client.getResourceIds();
        Set<String> scopes = client.getScope();

        // Default values for other parameters
        Map<String, String> requestParameters = Collections.emptyMap();
        boolean approved = true;
        String redirectUrl = null;
        Set<String> responseTypes = Collections.emptySet();
        Map<String, Serializable> extensionProperties = Collections.emptyMap();

        // Create request
        OAuth2Request oAuth2Request = new OAuth2Request(requestParameters, clientId, authorities, approved, scopes,
                resourceIds, redirectUrl, responseTypes, extensionProperties);

        // Create OAuth2AccessToken
        User userPrincipal = new User("user", "", true, true, true, true, authorities);
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userPrincipal, null, authorities);
        OAuth2Authentication auth = new OAuth2Authentication(oAuth2Request, authenticationToken);
        return tokenservice.createAccessToken(auth);
    }
}

MockMvc les tests doivent alors obtenir un RequestPostProcessorOauthHelper classe et de transmettre les requêtes:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringApplicationConfiguration(classes = MyApp.class)
@WebAppConfiguration
public class MyControllerTest {
    @Autowired
    private WebApplicationContext   webapp;

    private MockMvc                 mvc;

    @Before
    public void before() {
        mvc = MockMvcBuilders.webAppContextSetup(webapp)
                .apply(springSecurity())
                .alwaysDo(print())
                .build();
    }

    @Autowired
    private OAuthHelper helper;

    @Test
    public void testHelloWithRole() throws Exception {
        RequestPostProcessor bearerToken = helper.bearerToken("myclientwith");
        mvc.perform(get("/hello").with(bearerToken)).andExpect(status().isOk());
    }

    @Test
    public void testHelloWithoutRole() throws Exception {
        RequestPostProcessor bearerToken = helper.bearerToken("myclientwithout");
        mvc.perform(get("/hello").with(bearerToken)).andExpect(status().isForbidden());
    }
}

Un échantillon complet du projet est disponible sur GitHub:

https://github.com/timtebeek/resource-server-testing

32
répondu Tim 2015-11-08 21:31:37

j'ai trouvé un moyen beaucoup plus facile de faire ce qui suit directions que j'ai lu ici: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-method-withsecuritycontext. Cette solution est spécifique aux tests @PreAuthorize#oauth2.hasScope mais je suis sûr qu'il pourrait être adapté à d'autres situations.

je crée une annotation qui peut être appliquée à @Test s:

WithMockOAuth2Scope

import org.springframework.security.test.context.support.WithSecurityContext;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithMockOAuth2ScopeSecurityContextFactory.class)
public @interface WithMockOAuth2Scope {

    String scope() default "";
}

WithMockOAuth2ScopeSecurityContextfactory

import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.test.context.support.WithSecurityContextFactory;

import java.util.HashSet;
import java.util.Set;

public class WithMockOAuth2ScopeSecurityContextFactory implements WithSecurityContextFactory<WithMockOAuth2Scope> {

    @Override
    public SecurityContext createSecurityContext(WithMockOAuth2Scope mockOAuth2Scope) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        Set<String> scope = new HashSet<>();
        scope.add(mockOAuth2Scope.scope());

        OAuth2Request request = new OAuth2Request(null, null, null, true, scope, null, null, null, null);

        Authentication auth = new OAuth2Authentication(request, null);

        context.setAuthentication(auth);

        return context;
    }
}

Exemple de test à l'aide de MockMvc:

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class LoadScheduleControllerTest {

    private MockMvc mockMvc;

    @Autowired
    LoadScheduleController loadScheduleController;

    @Before
    public void setup() {
        mockMvc = MockMvcBuilders.standaloneSetup(loadScheduleController)
                    .build();
    }

    @Test
    @WithMockOAuth2Scope(scope = "dataLicense")
    public void testSchedule() throws Exception {
        mockMvc.perform(post("/schedule").contentType(MediaType.APPLICATION_JSON_UTF8).content(json)).andDo(print());
    }
}

Et c'est le contrôleur en vertu de test:

@RequestMapping(value = "/schedule", method = RequestMethod.POST)
@PreAuthorize("#oauth2.hasScope('dataLicense')")
public int schedule() {
    return 0;
}
19
répondu mclaassen 2016-12-01 22:02:43

Tring Boot 1.5 introduced tester les tranches@WebMvcTest. En utilisant ces tranches d'essai et de charger manuellement le OAuth2AutoConfiguration donne à vos tests moins de boilerplate et ils seront plus rapides que ceux proposés @SpringBootTest solutions basées. Si vous importez également votre configuration de sécurité de production, vous pouvez tester que les chaînes de filtrage configurées fonctionnent pour vos services web.

Voici la configuration avec quelques classes supplémentaires que vous trouverez probablement bénéfique:

contrôleur:

@RestController
@RequestMapping(BookingController.API_URL)
public class BookingController {

    public static final String API_URL = "/v1/booking";

    @Autowired
    private BookingRepository bookingRepository;

    @PreAuthorize("#oauth2.hasScope('myapi:write')")
    @PatchMapping(consumes = APPLICATION_JSON_UTF8_VALUE, produces = APPLICATION_JSON_UTF8_VALUE)
    public Booking patchBooking(OAuth2Authentication authentication, @RequestBody @Valid Booking booking) {
        String subjectId = MyOAuth2Helper.subjectId(authentication);
        booking.setSubjectId(subjectId);
        return bookingRepository.save(booking);
    }
}

Test:

@RunWith(SpringRunner.class)
@AutoConfigureJsonTesters
@WebMvcTest
@Import(DefaultTestConfiguration.class)
public class BookingControllerTest {

    @Autowired
    private MockMvc mvc;

    @Autowired
    private JacksonTester<Booking> json;

    @MockBean
    private BookingRepository bookingRepository;

    @MockBean
    public ResourceServerTokenServices resourceServerTokenServices;

    @Before
    public void setUp() throws Exception {
        // Stub the remote call that loads the authentication object
        when(resourceServerTokenServices.loadAuthentication(anyString())).thenAnswer(invocation -> SecurityContextHolder.getContext().getAuthentication());
    }

    @Test
    @WithOAuthSubject(scopes = {"myapi:read", "myapi:write"})
    public void mustHaveValidBookingForPatch() throws Exception {
        mvc.perform(patch(API_URL)
            .header(AUTHORIZATION, "Bearer foo")
            .content(json.write(new Booking("myguid", "aes")).getJson())
            .contentType(MediaType.APPLICATION_JSON_UTF8)
        ).andExpect(status().is2xxSuccessful());
    }
}

DefaultTestConfiguration:

@TestConfiguration
@Import({MySecurityConfig.class, OAuth2AutoConfiguration.class})
public class DefaultTestConfiguration {

}

MySecurityConfig (c'est pour la production):

@Configuration
@EnableOAuth2Client
@EnableResourceServer
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/v1/**").authenticated();
    }

}

annotation personnalisée pour l'injection des scopes des tests:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@WithSecurityContext(factory = WithOAuthSubjectSecurityContextFactory.class)
public @interface WithOAuthSubject {

    String[] scopes() default {"myapi:write", "myapi:read"};

    String subjectId() default "a1de7cc9-1b3a-4ecd-96fa-dab6059ccf6f";

}

Classe D'usine pour la manipulation de la coutume annotation:

public class WithOAuthSubjectSecurityContextFactory implements WithSecurityContextFactory<WithOAuthSubject> {

    private DefaultAccessTokenConverter defaultAccessTokenConverter = new DefaultAccessTokenConverter();

    @Override
    public SecurityContext createSecurityContext(WithOAuthSubject withOAuthSubject) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();

        // Copy of response from https://myidentityserver.com/identity/connect/accesstokenvalidation
        Map<String, ?> remoteToken = ImmutableMap.<String, Object>builder()
            .put("iss", "https://myfakeidentity.example.com/identity")
            .put("aud", "oauth2-resource")
            .put("exp", OffsetDateTime.now().plusDays(1L).toEpochSecond() + "")
            .put("nbf", OffsetDateTime.now().plusDays(1L).toEpochSecond() + "")
            .put("client_id", "my-client-id")
            .put("scope", Arrays.asList(withOAuthSubject.scopes()))
            .put("sub", withOAuthSubject.subjectId())
            .put("auth_time", OffsetDateTime.now().toEpochSecond() + "")
            .put("idp", "idsrv")
            .put("amr", "password")
            .build();

        OAuth2Authentication authentication = defaultAccessTokenConverter.extractAuthentication(remoteToken);
        context.setAuthentication(authentication);
        return context;
    }
}

j'utilise une copie de la réponse de notre serveur d'identité pour la création d'un réaliste OAuth2Authentication. Tu peux probablement copier mon code. Si vous voulez répéter le processus pour votre serveur d'identité, placez un point d'arrêt dans org.springframework.security.oauth2.provider.token.RemoteTokenServices#loadAuthentication ou org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices#extractAuthentication, selon que vous avez configuré un custom ResourceServerTokenServices ou pas.

6
répondu gogstad 2018-05-17 05:16:39

OK, je n'ai pas encore été capable de tester mon serveur ressource protégé par JWT autonome oauth2 JWT en utilisant le nouveau @WithMockUser ou les annotations.

comme solution de contournement, j'ai pu tester la sécurité de mon serveur de ressources en mettant en place permissive AuthorizationServer sous src/test / java, et avoir qui définissent deux clients, j'utilise à travers une classe helper. Cela me fait un peu de chemin là-bas, mais ce n'est pas encore aussi facile que je voudrais tester divers utilisateurs, rôles, portées, etc.

je suppose qu'à partir de maintenant, il devrait être plus facile de mettre en œuvre le mien WithSecurityContextFactory qui crée un OAuth2Authentication, au lieu desUsernamePasswordAuthentication. Toutefois, je n'ai pas encore été en mesure d'élaborer les détails de la façon de mettre en place facilement ce système. Nous vous invitons à nous faire part de vos commentaires ou suggestions sur la façon de procéder.

4
répondu Tim 2015-04-15 09:31:13

j'ai une autre solution pour ce. Voir ci-dessous.

@RunWith(SpringRunner.class)
@SpringBootTest
@WebAppConfiguration
@ActiveProfiles("test")
public class AccountContollerTest {

    public static Logger log =  LoggerFactory.getLogger(AccountContollerTest.class);


    @Autowired
    private WebApplicationContext webApplicationContext;

    private MockMvc mvc;

    @Autowired
    FilterChainProxy springSecurityFilterChain;

    @Autowired
    UserRepository users;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    CustomClientDetailsService clientDetialsService;

    @Before
    public void setUp() {
         mvc = MockMvcBuilders
                 .webAppContextSetup(webApplicationContext)
                 .apply(springSecurity(springSecurityFilterChain))
                 .build();

         BaseClientDetails testClient = new ClientBuilder("testclient")
                    .secret("testclientsecret")
                    .authorizedGrantTypes("password")
                    .scopes("read", "wirte")
                    .autoApprove(true)
                    .build();

         clientDetialsService.addClient(testClient);

         User user = createDefaultUser("testuser", passwordEncoder.encode("testpassword"), "max", "Mustermann", new Email("myemail@test.de"));

         users.deleteAll();
         users.save(user);

    }

    @Test
    public void shouldRetriveAccountDetailsWithValidAccessToken() throws Exception {
        mvc.perform(get("/api/me")
                .header("Authorization", "Bearer " + validAccessToken())
                .accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.userAuthentication.name").value("testuser"))
                .andExpect(jsonPath("$.authorities[0].authority").value("ROLE_USER"));
    }

    @Test
    public void shouldReciveHTTPStatusUnauthenticatedWithoutAuthorizationHeader() throws Exception{
        mvc.perform(get("/api/me")
                .accept(MediaType.APPLICATION_JSON))
                .andDo(print())
                .andExpect(status().isUnauthorized());
    }

    private String validAccessToken() throws Exception {  
        String username = "testuser";
        String password = "testpassword";

        MockHttpServletResponse response = mvc
            .perform(post("/oauth/token")
                    .header("Authorization", "Basic "
                           + new String(Base64Utils.encode(("testclient:testclientsecret")
                            .getBytes())))
                    .param("username", username)
                    .param("password", password)
                    .param("grant_type", "password"))
            .andDo(print())
            .andReturn().getResponse();

    return new ObjectMapper()
            .readValue(response.getContentAsByteArray(), OAuthToken.class)
            .accessToken;
    }

    @JsonIgnoreProperties(ignoreUnknown = true)
    private static class OAuthToken {
        @JsonProperty("access_token")
        public String accessToken;
    }
}

j'Espère que ça aidera!

2
répondu Rocks360 2017-06-24 02:12:40

j'ai trouvé un moyen facile et rapide pour tester le serveur de ressources de sécurité de printemps avec n'importe quel magasin de token. Im mon exemple @EnabledResourceServerutilise jwt jeton magasin.

La magie est ici que j'ai remplacé JwtTokenStoreInMemoryTokenStore au test d'intégration.

@RunWith (SpringRunner.class)
@SpringBootTest (classes = {Application.class}, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ActiveProfiles ("test")
@TestPropertySource (locations = "classpath:application.yml")
@Transactional
public class ResourceServerIntegrationTest {

@Autowired
private TokenStore tokenStore;

@Autowired
private ObjectMapper jacksonObjectMapper;

@LocalServerPort
int port;

@Configuration
protected static class PrepareTokenStore {

    @Bean
    @Primary
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }

}

private OAuth2AccessToken token;
private OAuth2Authentication authentication;

@Before
public void init() {

    RestAssured.port = port;

    token = new DefaultOAuth2AccessToken("FOO");
    ClientDetails client = new BaseClientDetails("client", null, "read", "client_credentials", "ROLE_READER,ROLE_CLIENT");

    // Authorities
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    authorities.add(new SimpleGrantedAuthority("ROLE_READER"));
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken("writer", "writer", authorities);

    authentication = new OAuth2Authentication(new TokenRequest(null, "client", null, "client_credentials").createOAuth2Request(client), authenticationToken);
    tokenStore.storeAccessToken(token, authentication);

}

@Test
public void gbsUserController_findById() throws Exception {

    RestAssured.given().log().all().when().headers("Authorization", "Bearer FOO").get("/gbsusers/{id}", 2L).then().log().all().statusCode(HttpStatus.OK.value());

}
2
répondu talipkorkmaz 2018-03-04 19:17:09

Il y a une approche alternative que je crois plus propre et plus significative.

l'approche est d'autowire le token store puis d'ajouter un token test qui peut ensuite être utilisé par le client rest.

un exemple de test:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class UserControllerIT {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Autowired
    private TokenStore tokenStore;

    @Before
    public void setUp() {

        final OAuth2AccessToken token = new DefaultOAuth2AccessToken("FOO");
        final ClientDetails client = new BaseClientDetails("client", null, "read", "client_credentials", "ROLE_CLIENT");
        final OAuth2Authentication authentication = new OAuth2Authentication(
                new TokenRequest(null, "client", null, "client_credentials").createOAuth2Request(client), null);

        tokenStore.storeAccessToken(token, authentication);

    }

    @Test
    public void testGivenPathUsersWhenGettingForEntityThenStatusCodeIsOk() {

        final HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.AUTHORIZATION, "Bearer FOO");
        headers.setContentType(MediaType.APPLICATION_JSON);

        // Given Path Users
        final UriComponentsBuilder uri = UriComponentsBuilder.fromPath("/api/users");

        // When Getting For Entity
        final ResponseEntity<String> response = testRestTemplate.exchange(uri.build().toUri(), HttpMethod.GET,
                new HttpEntity<>(headers), String.class);

        // Then Status Code Is Ok
        assertThat(response.getStatusCode(), is(HttpStatus.OK));
    }

}

personnellement, je crois qu'il n'est pas approprié de tester un contrôleur unitaire avec la sécurité activée puisque la sécurité est un calque séparé du contrôleur. Je créerais un test d'intégration qui teste tout les couches ensemble. Toutefois, l'approche ci-dessus peut facilement être modifié pour créer un test unitaire qui utilise MockMvc.

Le code ci-dessus est inspiré par un test de sécurité du ressort écrit par Dave Syer.

Remarque: cette approche est pour les serveurs de ressources qui partagent le même magasin que le serveur d'autorisation. Si votre serveur de ressources ne partage pas le même magasin que le serveur d'autorisation je recommande utiliser wiremock pour se moquer du http réponses.

1
répondu Thomas Turrell-Croft 2017-06-23 15:21:57

une solution de plus que j'ai essayé de détailler assez: - D

Elle est basée sur l'établissement d'une en-tête d'Autorisation, à l'instar de certains ci-dessus, mais je voulais:

  • ne pas créer de tokens JWT réellement valides et en utilisant toute la pile d'authentification JWT (tests unitaires...)
  • authentification de Test pour contenir des portées et des autorités définies par cas de test

j'ai Donc:

  • créé une annotation personnalisée avec @WithSecurityContext et de l'usine, comme quelques autres n'a configurer un par test OAuth2Authentication (@WithJwt dans mon cas)
  • @MockBean la TokenStore pour retourner le OAuth2Authentication configuré avec @WithJwt
  • donner MockHttpServletRequestBuilder usines qui mettent un en-tête D'autorisation spécifique intercepté par TokenStore mock pour injecter l'authentification attendue.

P.S.

n'hésitez pas à accepter / voter ma solution si elle correspond le mieux à vos besoins ;-)

0
répondu ch4mp 2018-02-01 15:47:15