Suite à la publication de Spring Security 4 et à son _ { amélioration de la prise en charge des tests } _, je souhaite mettre à jour mes tests actuels du serveur de ressources Spring Security oauth2.
Actuellement, j'ai une classe d'assistance qui configure un OAuth2RestTemplate
en utilisant ResourceOwnerPasswordResourceDetails
avec un test ClientId
se connectant à un AccessTokenUri
réel pour demander un jeton valide pour mes tests. Ce resttemplate est ensuite utilisé pour faire des demandes dans mon @WebIntegrationTest
s.
J'aimerais abandonner la dépendance vis-à-vis du AuthorizationServer lui-même et l'utilisation de données d'identification utilisateur valides (si elles sont limitées) dans mes tests, en tirant parti du nouveau support de test fourni dans Spring Security 4.
Jusqu'à présent, toutes mes tentatives d'utilisation de @WithMockUser
, @WithSecurityContext
, SecurityMockMvcConfigurers.springSecurity()
& SecurityMockMvcRequestPostProcessors.*
n'ont pas réussi à passer des appels authentifiés via MockMvc
, et je ne trouve aucun exemple de ce type dans les projets de Spring.
Quelqu'un peut-il m'aider à tester mon serveur de ressources oauth2 avec des identifiants fictifs, 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 I comprendre pourquoi cela ne fonctionnera pas comme cela, mais je cherche des moyens qui me permettraient de tester facilement la configuration de la sécurité.
Je pense maintenant créer un OAuthServer très permissif sous src/test/Java
, ce qui pourrait aider un peu. Quelqu'un a-t-il d'autres suggestions?
Pour tester efficacement la sécurité du serveur de ressources, avec MockMvc
et une RestTemplate
, il est utile de configurer une AuthorizationServer
sous src/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, on peut alors simplement utiliser la règle et les annotations de support de test OAuth2 intégrées:
@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
Tester avec MockMvc
est également possible, mais nécessite une petite classe d'assistance pour obtenir une RequestPostProcessor
qui définit l'en-tête Authorization: Bearer <token>
sur les demandes:
@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);
}
}
Vos tests MockMvc
doivent ensuite obtenir une RequestPostProcessor
de la classe OauthHelper
et la réussir lors de la création de 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 exemple de projet complet est disponible sur GitHub:
https://github.com/timtebeek/resource-server-testing
J'ai trouvé un moyen beaucoup plus facile de faire ceci en suivant les instructions que je lis ici: http://docs.spring.io/spring-security/site/docs/4.0.x/reference/htmlsingle/#test-method-withsecuritycontext . Cette solution est spécifique au test de @PreAuthorize
avec #oauth2.hasScope
, mais je suis sûr qu’elle pourrait également être adaptée à 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 utilisant 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 voici le contrôleur sous test:
@RequestMapping(value = "/schedule", method = RequestMethod.POST)
@PreAuthorize("#oauth2.hasScope('dataLicense')")
public int schedule() {
return 0;
}
Spring Boot 1.5 introduit test des tranches like @WebMvcTest
. En utilisant ces tranches de test et en chargeant manuellement le OAuth2AutoConfiguration
, vos tests sont moins chauds et ils s'exécutent plus rapidement que les solutions proposées basées sur le @SpringBootTest
. Si vous importez également votre configuration de sécurité de production, vous pouvez vérifier que les chaînes de filtres configurées fonctionnent pour vos services Web.
Voici la configuration avec quelques classes supplémentaires que vous trouverez probablement bénéfiques:
Manette:
@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 (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 de portées à partir de 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 gérer l'annotation personnalisée:
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 créer un OAuth2Authentication
réaliste. Vous pouvez probablement simplement copier mon code. Si vous souhaitez 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 ayez configuré une variable ResourceServerTokenServices
personnalisée ou non.
D'accord, je n'ai pas encore pu tester mon serveur de ressources protégé par jeton oauth2 JWT autonome à l'aide du nouveau @WithMockUser
ou des annotations associées.
En guise de solution de contournement, j'ai pu tester l'intégration de la sécurité de mon serveur de ressources en configurant une AuthorizationServer
permissive sous src/test/Java et en définissant deux clients que j'utilise via une classe d'assistance . Cela me donne une partie du chemin, mais ce n'est pas encore aussi facile que j'aimerais tester différents utilisateurs, rôles, champs d'application, etc.
Je suppose qu’à partir de maintenant, il devrait être plus facile de mettre en œuvre ma propre WithSecurityContextFactory
qui crée un OAuth2Authentication
, au lieu de la UsernamePasswordAuthentication
habituelle. Cependant, je n'ai pas encore été en mesure de déterminer en détail comment le configurer facilement. Tous les commentaires ou suggestions sur la façon de mettre cela en place sont les bienvenus.
J'ai trouvé un moyen simple et rapide de tester le serveur de ressources de sécurité Spring avec n'importe quel magasin de jetons. Je suis dans mon exemple @EnabledResourceServer
uses jwt Token Store.
La magie ici est que j'ai remplacé JwtTokenStore
par InMemoryTokenStore
lors du 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());
}
J'ai une autre solution pour cela. 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("[email protected]"));
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 va aider!
Il existe une approche alternative qui, à mon avis, est plus propre et plus significative.
L'approche consiste à autoriser automatiquement le magasin de jetons, puis à ajouter un jeton de test pouvant être utilisé par le client restant.
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, j'estime qu'il n'est pas approprié de tester un contrôleur avec la sécurité activée car la sécurité est une couche distincte de celle du contrôleur. Je créerais un test d'intégration qui teste toutes les couches ensemble. Cependant, l'approche ci-dessus peut facilement être modifiée pour créer un test unitaire avec MockMvc.
Le code ci-dessus est inspiré d'un Spring Security test écrit par Dave Syer.
Notez que cette approche concerne les serveurs de ressources partageant le même magasin de jetons que le serveur d'autorisation. Si votre serveur de ressources ne partage pas le même magasin de jetons que le serveur d'autorisations, je vous recommande d'utiliser wiremock pour simuler les réponses http .
Une autre solution que j'ai essayé de détailler suffisamment :-D
Il est basé sur la définition d'un en-tête d'autorisation, comme certains ci-dessus, mais je voulais:
Alors j'ai:
OAuth2Authentication
: @WithMockOAuth2Client
(connexion directe au client) & @WithMockOAuth2User
(le client agissant pour le compte d'un utilisateur final => inclut à la fois my custom @ WithMockOAuth2Client et Spring @WithMockUser)MockHttpServletRequestBuilder
qui définissent un en-tête d'autorisation spécifique intercepté par TokenStore fausse pour injecter l'authentification attendue.Le résultat pour vous faire tester:
@WebMvcTest(MyController.class) // Controller to unit-test
@Import(WebSecurityConfig.class) // your class extending WebSecurityConfigurerAdapter
public class MyControllerTest extends OAuth2ControllerTest {
@Test
public void testWithUnauthenticatedClient() throws Exception {
api.post(payload, "/endpoint")
.andExpect(...);
}
@Test
@WithMockOAuth2Client
public void testWithDefaultClient() throws Exception {
api.get("/endpoint")
.andExpect(...);
}
@Test
@WithMockOAuth2User
public void testWithDefaultClientOnBehalfDefaultUser() throws Exception {
MockHttpServletRequestBuilder req = api.postRequestBuilder(null, "/uaa/refresh")
.header("refresh_token", JWT_REFRESH_TOKEN);
api.perform(req)
.andExpect(status().isOk())
.andExpect(...)
}
@Test
@WithMockOAuth2User(
client = @WithMockOAuth2Client(
clientId = "custom-client",
scope = {"custom-scope", "other-scope"},
authorities = {"custom-authority", "ROLE_CUSTOM_CLIENT"}),
user = @WithMockUser(
username = "custom-username",
authorities = {"custom-user-authority"}))
public void testWithCustomClientOnBehalfCustomUser() throws Exception {
api.get(MediaType.APPLICATION_ATOM_XML, "/endpoint")
.andExpect(status().isOk())
.andExpect(xpath(...));
}
}