From 2296ff435ea2544abb06c3b16ab511a4c8e54ffc Mon Sep 17 00:00:00 2001 From: skrzeminski Date: Thu, 7 Jan 2021 12:17:16 +0100 Subject: [PATCH 1/6] implementation of Spring security Config --- pom.xml | 4 + .../sfg/brewery/config/SecurityConfig.java | 26 ++++++ src/main/resources/application.properties | 5 +- .../sfg/brewery/web/controllers/BaseIT.java | 48 +++++++++++ .../web/controllers/BeerControllerIT.java | 80 +++++++++++++++++++ .../web/controllers/IndexControllerIT.java | 20 +++++ .../controllers/api/BeerRestControllerIT.java | 33 ++++++++ 7 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 src/main/java/guru/sfg/brewery/config/SecurityConfig.java create mode 100644 src/test/java/guru/sfg/brewery/web/controllers/BaseIT.java create mode 100644 src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java create mode 100644 src/test/java/guru/sfg/brewery/web/controllers/IndexControllerIT.java create mode 100644 src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java diff --git a/pom.xml b/pom.xml index df7a6cb36..4be087fcc 100644 --- a/pom.xml +++ b/pom.xml @@ -138,6 +138,10 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-security + org.springframework.security spring-security-test diff --git a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java new file mode 100644 index 000000000..f1063f5ae --- /dev/null +++ b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java @@ -0,0 +1,26 @@ +package guru.sfg.brewery.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; + +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.authorizeRequests(authorize -> { + authorize.antMatchers("/", "/webjars/**", "/login", "/resources/**").permitAll() + .antMatchers("/beers/find", "/beers*").permitAll() + .antMatchers(HttpMethod.GET, "/api/v1/beer/**", "/api/v1/beerUpc/**").permitAll(); + }) + .authorizeRequests() + .anyRequest() + .authenticated().and() + .formLogin().and() + .httpBasic(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 912080aaa..f3ced6867 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -23,4 +23,7 @@ spring.messages.basename=messages/messages logging.level.guru=debug # Spring Data hangs when not set under Spring Boot 2.3.0 -spring.data.jpa.repositories.bootstrap-mode=default \ No newline at end of file +spring.data.jpa.repositories.bootstrap-mode=default + +spring.security.user.name=spring +spring.security.user.password=password \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BaseIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BaseIT.java new file mode 100644 index 000000000..f5000e7bc --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/BaseIT.java @@ -0,0 +1,48 @@ +package guru.sfg.brewery.web.controllers; + +import guru.sfg.brewery.repositories.BeerInventoryRepository; +import guru.sfg.brewery.repositories.BeerRepository; +import guru.sfg.brewery.repositories.CustomerRepository; +import guru.sfg.brewery.services.BeerService; +import guru.sfg.brewery.services.BreweryService; +import org.junit.jupiter.api.BeforeEach; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; + +/** + * Created by jt on 6/13/20. + */ +public abstract class BaseIT { + @Autowired + WebApplicationContext wac; + + protected MockMvc mockMvc; + + @MockBean + BeerRepository beerRepository; + + @MockBean + BeerInventoryRepository beerInventoryRepository; + + @MockBean + BreweryService breweryService; + + @MockBean + CustomerRepository customerRepository; + + @MockBean + BeerService beerService; + + @BeforeEach + public void setup() { + mockMvc = MockMvcBuilders + .webAppContextSetup(wac) + .apply(springSecurity()) + .build(); + } +} diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java new file mode 100644 index 000000000..436e30710 --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java @@ -0,0 +1,80 @@ +package guru.sfg.brewery.web.controllers; + +import guru.sfg.brewery.repositories.BeerInventoryRepository; +import guru.sfg.brewery.repositories.BeerRepository; +import guru.sfg.brewery.repositories.CustomerRepository; +import guru.sfg.brewery.services.BeerService; +import guru.sfg.brewery.services.BreweryService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Created by jt on 6/12/20. + */ +@WebMvcTest +public class BeerControllerIT { + + @Autowired + WebApplicationContext wac; + + MockMvc mockMvc; + + @MockBean + BeerRepository beerRepository; + + @MockBean + BeerInventoryRepository beerInventoryRepository; + + @MockBean + BreweryService breweryService; + + @MockBean + CustomerRepository customerRepository; + + @MockBean + BeerService beerService; + + @BeforeEach + void setUp() { + mockMvc = MockMvcBuilders + .webAppContextSetup(wac) + .apply(springSecurity()) + .build(); + } + + @WithMockUser("spring") + @Test + void findBeers() throws Exception{ + mockMvc.perform(get("/beers/find")) + .andExpect(status().isOk()) + .andExpect(view().name("beers/findBeers")) + .andExpect(model().attributeExists("beer")); + } + + @Test + void findBeersWithHttpBasicFalse() throws Exception{ + mockMvc.perform(get("/beers/find").with(httpBasic("foo", "bar"))) + .andExpect(status().isUnauthorized()); + } + + + @Test + void findBeersWithHttpBasic() throws Exception{ + mockMvc.perform(get("/beers/find").with(httpBasic("spring", "password"))) + .andExpect(status().isOk()) + .andExpect(view().name("beers/findBeers")) + .andExpect(model().attributeExists("beer")); + } +} \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/IndexControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/IndexControllerIT.java new file mode 100644 index 000000000..d7d0735d7 --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/IndexControllerIT.java @@ -0,0 +1,20 @@ +package guru.sfg.brewery.web.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Created by jt on 6/13/20. + */ +@WebMvcTest +public class IndexControllerIT extends BaseIT { + + @Test + void testGetIndexSlash() throws Exception{ + mockMvc.perform(get("/" )) + .andExpect(status().isOk()); + } +} \ No newline at end of file diff --git a/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java new file mode 100644 index 000000000..703dc8b0b --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java @@ -0,0 +1,33 @@ +package guru.sfg.brewery.web.controllers.api; + +import guru.sfg.brewery.web.controllers.BaseIT; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Created by jt on 6/13/20. + */ +@WebMvcTest +public class BeerRestControllerIT extends BaseIT { + + @Test + void findBeers() throws Exception{ + mockMvc.perform(get("/api/v1/beer/")) + .andExpect(status().isOk()); + } + + @Test + void findBeerById() throws Exception{ + mockMvc.perform(get("/api/v1/beer/97df0c39-90c4-4ae0-b663-453e8e19c311")) + .andExpect(status().isOk()); + } + + @Test + void findBeerByUPC() throws Exception{ + mockMvc.perform(get("/api/v1/beerUpc/0631234200036")) + .andExpect(status().isOk()); + } +} \ No newline at end of file From 3f9676a6c2eb2a8d8e85becdd537facbca304c82 Mon Sep 17 00:00:00 2001 From: skrzeminski Date: Thu, 7 Jan 2021 13:15:30 +0100 Subject: [PATCH 2/6] adding UserDetailsService --- .../sfg/brewery/config/SecurityConfig.java | 23 +++++++ .../web/controllers/BeerControllerIT.java | 64 ++++--------------- 2 files changed, 37 insertions(+), 50 deletions(-) diff --git a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java index f1063f5ae..1fa997bdb 100644 --- a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java +++ b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java @@ -1,10 +1,15 @@ package guru.sfg.brewery.config; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration @EnableWebSecurity @@ -23,4 +28,22 @@ protected void configure(HttpSecurity http) throws Exception { .formLogin().and() .httpBasic(); } + + @Override + @Bean + protected UserDetailsService userDetailsService() { + UserDetails admin = User.withDefaultPasswordEncoder() + .username("spring") + .password("guru") + .roles("ADMIN") + .build(); + + UserDetails user = User.withDefaultPasswordEncoder() + .username("user") + .password("password") + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(admin, user); + } } diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java index 436e30710..a5c7ad530 100644 --- a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java +++ b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java @@ -1,22 +1,10 @@ package guru.sfg.brewery.web.controllers; -import guru.sfg.brewery.repositories.BeerInventoryRepository; -import guru.sfg.brewery.repositories.BeerRepository; -import guru.sfg.brewery.repositories.CustomerRepository; -import guru.sfg.brewery.services.BeerService; -import guru.sfg.brewery.services.BreweryService; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.anonymous; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.httpBasic; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @@ -24,37 +12,16 @@ * Created by jt on 6/12/20. */ @WebMvcTest -public class BeerControllerIT { +public class BeerControllerIT extends BaseIT{ - @Autowired - WebApplicationContext wac; - - MockMvc mockMvc; - - @MockBean - BeerRepository beerRepository; - - @MockBean - BeerInventoryRepository beerInventoryRepository; - - @MockBean - BreweryService breweryService; - - @MockBean - CustomerRepository customerRepository; - - @MockBean - BeerService beerService; - - @BeforeEach - void setUp() { - mockMvc = MockMvcBuilders - .webAppContextSetup(wac) - .apply(springSecurity()) - .build(); + @Test + void initCreationForm() throws Exception { + mockMvc.perform(get("/beers/new").with(httpBasic("user", "password"))) + .andExpect(status().isOk()) + .andExpect(view().name("beers/createBeer")) + .andExpect(model().attributeExists("beer")); } - @WithMockUser("spring") @Test void findBeers() throws Exception{ mockMvc.perform(get("/beers/find")) @@ -64,17 +31,14 @@ void findBeers() throws Exception{ } @Test - void findBeersWithHttpBasicFalse() throws Exception{ - mockMvc.perform(get("/beers/find").with(httpBasic("foo", "bar"))) - .andExpect(status().isUnauthorized()); - } - - - @Test - void findBeersWithHttpBasic() throws Exception{ - mockMvc.perform(get("/beers/find").with(httpBasic("spring", "password"))) + void findBeersWithAnonymous() throws Exception{ + mockMvc.perform(get("/beers/find").with(anonymous())) .andExpect(status().isOk()) .andExpect(view().name("beers/findBeers")) .andExpect(model().attributeExists("beer")); } + + + + } \ No newline at end of file From 9d2f3e0eac1b5cc2ab729d303e02084605c8b085 Mon Sep 17 00:00:00 2001 From: skrzeminski Date: Thu, 7 Jan 2021 13:16:09 +0100 Subject: [PATCH 3/6] comment out property of defoult user of spring security --- src/main/resources/application.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index f3ced6867..d22f7dcd4 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -25,5 +25,5 @@ logging.level.guru=debug # Spring Data hangs when not set under Spring Boot 2.3.0 spring.data.jpa.repositories.bootstrap-mode=default -spring.security.user.name=spring -spring.security.user.password=password \ No newline at end of file +#spring.security.user.name=spring +#spring.security.user.password=password \ No newline at end of file From b5fcb4939194ccb41931bc1267c9a7dc7af03710 Mon Sep 17 00:00:00 2001 From: skrzeminski Date: Thu, 7 Jan 2021 13:20:53 +0100 Subject: [PATCH 4/6] added new user with customer role --- src/main/java/guru/sfg/brewery/config/SecurityConfig.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java index 1fa997bdb..67833d9d1 100644 --- a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java +++ b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java @@ -44,6 +44,12 @@ protected UserDetailsService userDetailsService() { .roles("USER") .build(); - return new InMemoryUserDetailsManager(admin, user); + UserDetails customer = User.withDefaultPasswordEncoder() + .username("scott") + .password("tiger") + .roles("CUSTOMER") + .build(); + + return new InMemoryUserDetailsManager(admin, user, customer); } } From be1257db276e54b76521ff7f81a694b75f28c906 Mon Sep 17 00:00:00 2001 From: skrzeminski Date: Thu, 7 Jan 2021 13:47:28 +0100 Subject: [PATCH 5/6] added new user with customer role --- .../sfg/brewery/web/controllers/api/BeerRestControllerIT.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java index 703dc8b0b..580d98fcf 100644 --- a/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java +++ b/src/test/java/guru/sfg/brewery/web/controllers/api/BeerRestControllerIT.java @@ -17,6 +17,7 @@ public class BeerRestControllerIT extends BaseIT { void findBeers() throws Exception{ mockMvc.perform(get("/api/v1/beer/")) .andExpect(status().isOk()); + } @Test From 3b22bc2c3a591bf3660671a09d6da05c9812b612 Mon Sep 17 00:00:00 2001 From: skrzeminski Date: Thu, 7 Jan 2021 15:46:03 +0100 Subject: [PATCH 6/6] PasswordEncodingTests --- .../sfg/brewery/config/SecurityConfig.java | 56 +++++++++++++------ .../web/controllers/BeerControllerIT.java | 2 +- .../controllers/PasswordEncodingTests.java | 30 ++++++++++ 3 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java diff --git a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java index 67833d9d1..291ce67a1 100644 --- a/src/main/java/guru/sfg/brewery/config/SecurityConfig.java +++ b/src/main/java/guru/sfg/brewery/config/SecurityConfig.java @@ -3,12 +3,15 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.provisioning.InMemoryUserDetailsManager; @Configuration @@ -29,27 +32,46 @@ protected void configure(HttpSecurity http) throws Exception { .httpBasic(); } +// @Override +// @Bean +// protected UserDetailsService userDetailsService() { +// UserDetails admin = User.withDefaultPasswordEncoder() +// .username("spring") +// .password("password") +// .roles("ADMIN") +// .build(); +// +// UserDetails user = User.withDefaultPasswordEncoder() +// .username("user") +// .password("password") +// .roles("USER") +// .build(); +// +// UserDetails customer = User.withDefaultPasswordEncoder() +// .username("scott") +// .password("tiger") +// .roles("CUSTOMER") +// .build(); +// +// return new InMemoryUserDetailsManager(admin, user, customer); +// } + @Override - @Bean - protected UserDetailsService userDetailsService() { - UserDetails admin = User.withDefaultPasswordEncoder() - .username("spring") - .password("guru") + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.inMemoryAuthentication() + .withUser("spring") + .password("password") .roles("ADMIN") - .build(); - - UserDetails user = User.withDefaultPasswordEncoder() - .username("user") + .and() + .withUser("user") .password("password") - .roles("USER") - .build(); + .roles("USER"); - UserDetails customer = User.withDefaultPasswordEncoder() - .username("scott") - .password("tiger") - .roles("CUSTOMER") - .build(); + auth.inMemoryAuthentication().withUser("scott").password("tiger").roles("CUSTOMER"); + } - return new InMemoryUserDetailsManager(admin, user, customer); + @Bean + PasswordEncoder passwordEncoder() { + return NoOpPasswordEncoder.getInstance(); } } diff --git a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java index a5c7ad530..6b6bf7edb 100644 --- a/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java +++ b/src/test/java/guru/sfg/brewery/web/controllers/BeerControllerIT.java @@ -16,7 +16,7 @@ public class BeerControllerIT extends BaseIT{ @Test void initCreationForm() throws Exception { - mockMvc.perform(get("/beers/new").with(httpBasic("user", "password"))) + mockMvc.perform(get("/beers/new").with(httpBasic("spring", "password"))) .andExpect(status().isOk()) .andExpect(view().name("beers/createBeer")) .andExpect(model().attributeExists("beer")); diff --git a/src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java b/src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java new file mode 100644 index 000000000..928e21dbb --- /dev/null +++ b/src/test/java/guru/sfg/brewery/web/controllers/PasswordEncodingTests.java @@ -0,0 +1,30 @@ +package guru.sfg.brewery.web.controllers; + +import org.junit.jupiter.api.Test; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.util.DigestUtils; + +/** + * Created by jt on 6/16/20. + */ +public class PasswordEncodingTests { + + static final String PASSWORD = "password"; + + @Test + void testNoOp() { + PasswordEncoder noOp = NoOpPasswordEncoder.getInstance(); + + System.out.println(noOp.encode(PASSWORD)); + } + + @Test + void hashingExample() { + System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes())); + System.out.println(DigestUtils.md5DigestAsHex(PASSWORD.getBytes())); + + String salted = PASSWORD + "ThisIsMySALTVALUE"; + System.out.println(DigestUtils.md5DigestAsHex(salted.getBytes())); + } +} \ No newline at end of file