Spring session replication with mongodb not working as expected - spring

I am replicating session using mongodb
below is the configuration I am using
#Configuration
#EnableMongoHttpSession
public class MongoSessionReplication {
#Bean
public AbstractMongoSessionConverter mongoSessionConverter() {
List<Module> securityModules = SecurityJackson2Modules.getModules(getClass().getClassLoader());
return new JacksonMongoSessionConverter(securityModules);
}
#Bean
public MongoTemplate mongoTemplate(#Qualifier("replicaSet") Datastore replicaSet){
MongoTemplate mongoTemplate = new MongoTemplate(replicaSet.getMongo(),replicaSet.getDB().getName());
return mongoTemplate;
}
}
Now everything is working fine except the Principal object that spring security creates after loggin in.
I have custom implementation of UserDetails
public class PortalUser extends User {
private String primaryEmailId;
private String redirectUrl;
public PortalUser(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
public PortalUser(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, true, true, true, true, authorities);
}
public String getPrimaryEmailId() {
return primaryEmailId;
}
public void setPrimaryEmailId(String primaryEmailId) {
this.primaryEmailId = primaryEmailId;
}
public String getRedirectUrl() {
return redirectUrl;
}
public void setRedirectUrl(String redirectUrl) {
this.redirectUrl = redirectUrl;
}
}
Below is UserDetailsService
#Service
public class PortalUserDetailService implements UserDetailsService {
#Autowired
private SSOServiceAPI ssoServiceAPI;
#Autowired
private UserProfileService userProfileService;
#Override
public UserDetails loadUserByUsername(String hexId) throws UsernameNotFoundException {
UserProfile userProfile = userProfileService.getUserProfileByUserId(hexId);
List<GrantedAuthority> grantedAuthority = new ArrayList<GrantedAuthority>();
if(userProfile!=null) {
grantedAuthority.add(new SimpleGrantedAuthority(userProfile.getSsmRoles().name()));
} else {
grantedAuthority.add(new SimpleGrantedAuthority("USER"));
}
SSOUsers ssoUser = ssoServiceAPI.findSSOUser(hexId, false);
PortalUser portalUser = new PortalUser(hexId, hexId, true, true, true, true, grantedAuthority);
portalUser.setPrimaryEmailId(ssoUser.getPrimaryUserId());
return portalUser;
}
}
Controller
public String getAllProducts(#RequestParam(value = "callback", required = true) String callback, Principal principal, HttpServletRequest request) {
String hexId = principal.getName();
String primaryEmailId = ((PortalUser) ((UsernamePasswordAuthenticationToken) principal).getPrincipal()).getPrimaryEmailId(); //----->> this line fails
}
Above highlighted typecasting failed as it returns instance of UserDetails instead of my custom PortalUser. But this isn't a case when I disable spring-session replication..

You need to implement Spring's Security UserDetails, not User.
update MyUser to the below:
public class SecUserDetails implements UserDetails {
private User user;
public SecUserDetails(User user) {
this.user = user;
}
......
......
......
}

Related

Use of jwt for authentication and signup on springboot webflux and microservice with user in database

I am writing a microservice project with springboot that require jwt auth for spring webflux security for user in database .I have idea on how to implement jwt on basic jwt authentication on normal spring security,but it fails when applying to webflux.
The main problem is that i have no idea how to turn the entity class to "mono" class.
My tokenservice responsible for signup:
public String signup(SignupDto user){
Mono<User> saveduser=webclient.baseUrl("http://USER").build().post()
.uri("/User/adduser/")
.header(MediaType.APPLICATION_JSON_VALUE)
.body(Mono.just(
new User(
0, user.getUsername(),
passwordEncoder.encode(user.getPassword()),
user.getFullName(),
user.getEmail(),
user.getSkill_set(),user.getContact(),user.getCv(),user.getAddress_id()
,user.getAddress(),null,3
)
),User.class)
.retrieve()
.bodyToMono(User.class);
//make syn request
Mono<Authentication> authentication= reactiveAuthenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(user.getUsername()
,user.getPassword())
);
return generateToken( authentication);
}
generate token function:
public String generateToken(Authentication authentication){
Instant now=Instant.now();
String scope=authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(" "));
String secret="AMDM:LM:LSMLdfsf";
JwtClaimsSet claims=JwtClaimsSet.builder()
.issuer("http://localhost:8080")
.issuedAt(now)
.expiresAt(now.plus(2, ChronoUnit.HOURS))
.subject(authentication.getName())
.claim("scope",scope)
.claim("secret",secret)
.build();
return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
The webclient is calling the user at the other microservice which the api should send back the "user" class.The problem is that i cant cast the Mono class in Mono to normal authentication.
User entity:
#Data
#AllArgsConstructor
#NoArgsConstructor
public class User {
private int id;
private String username;
private String password;
private String fullName;
private String email;
private String skill_set;
private String contact;
private String cv;
private int Address_id;
private String address;
private String role;
private double score=3.0;
The reactiveauthenticationManager bean:
#Bean
protected ReactiveAuthenticationManager reactiveAuthenticationManager() {
log.info("Received authentication request");
return authentication -> {
UserDetailsRepositoryReactiveAuthenticationManager authenticator = new UserDetailsRepositoryReactiveAuthenticationManager(securityUserService);
authenticator.setPasswordEncoder(passwordEncoder());
return authenticator.authenticate(authentication);
};
}
The SecurityUserService:
#Service
public class SecurityUserService implements ReactiveUserDetailsService {
#Autowired
private WebClient.Builder webClientBuilder;
#Override
public Mono<UserDetails> findByUsername(String username) {
return webClientBuilder.baseUrl("http://USER").build().get()
.uri(uriBuilder -> uriBuilder
.path("/User/AuthUser/{username}")
.build(username))
.retrieve()
.bodyToMono(UserAuthdto.class)
.map(
SecurityUser::new
//res->new SecurityUser(res)
)
SecurityUser:
public class SecurityUser implements UserDetails {
private final UserAuthdto user;
public SecurityUser(UserAuthdto user) {
this.user = user;
}
public int getID(){
return user.getId();
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getUsername();
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Arrays.stream(user
.getRole()
.split(","))
.map(SimpleGrantedAuthority::new)
.toList();}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
Passwword Encoder:
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
The api at the other microservice are tested and should fine.Please help to see any problem on my code.Any clue is also fine.

How to resolve getPrincipal() method error

While accessing the services using JWT token i was trying to print auth.getPrincipal() method it returning packageName.model.CustomUserDetails#3f97b72b instead of the UserDto details.
CustomUserDetails
public class CustomUserDetails implements UserDetails {
private static final long serialVersionUID = 8632209412694363798L;
private UsersDto userDto;
private UserEntity user;
public CustomUserDetails(UsersDto userDto) {
super();
this.userDto = userDto;
}
public UsersDto getUserDto() {
return userDto;
}
public void setUserDto(UsersDto userDto) {
this.userDto = userDto;
}
public CustomUserDetails(UserEntity user) {
this.user = user;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
// UserRole role = userDto.getRole();
// authorities.add(new SimpleGrantedAuthority(role.name()));
// return authorities;
return null;
}
#Override
public String getPassword() {
return user.getPassword();
}
#Override
public String getUsername() {
return user.getEmail();
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
}
UsersDto
#Data
#NoArgsConstructor
#AllArgsConstructor
public class UsersDto extends User {
private Integer userId;
public UserEntity convertToUserEntity() throws NoSuchAlgorithmException {
UserEntity user=new UserEntity();
user.setUserId(this.userId);
user.setName(this.getName());
user.setEmail(this.getEmail());
user.setDateAdded(this.getDateAdded());
user.setDateModified(this.getDateModified());
user.setPassword(this.getPassword());
user.setPassword(this.getPassword());
return user;
}
}
UserController
#GetMapping("/getUser/{userId}")
public ResponseEntity<SuccessResponse> getuser(#PathVariable Integer userId,Authentication auth) throws Exception{
System.out.println(auth);
System.out.println(auth.getPrincipal());
UsersDto userFromAuth = ((CustomUserDetails) auth.getPrincipal()).getUserDto();
return ResponseEntity
.ok(new SuccessResponse(HttpStatus.OK.value(), SuccessMessage.SUCCESS,
userService.getUserById(userId)));
}
error
**packagename.CustomUserDetails#3f97b72b**
Instead of class details I want userdetails but it returning className with uniformed number so when i store this data into a variable it always stores NULL values.

Spring Security 5.7 - How to return custom UserDetails

I've seen a lot of examples where a user creates a custom UserDetailsService in order to override the loadUserByUsername method and return a custom implementation of a UserDetails object.
This was done previously with sth like this
#Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
Now with the new version I'm confused on how to do this
I created a Bean and used the JdbcUserDetailsManager, I can configure my custom queries for users and authorities tables
#Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
String usersByUsernameQuery = "select username, password, enabled from tbl_users where username = ?";
String authsByUserQuery = "select username, authority from tbl_authorities where username = ?";
JdbcUserDetailsManager userDetailsManager = new JdbcUserDetailsManager(dataSource);
userDetailsManager.setUsersByUsernameQuery(usersByUsernameQuery);
userDetailsManager.setAuthoritiesByUsernameQuery(authsByUserQuery);
return userDetailsManager;
}
but how to return a custom UserDetails object with an extra field, e.g. an email with the new version?
OK after many tries what I did was to remove completely JdbcUserDetailsManager stuff from my custom SecurityConfig class and I created a custom UserDetailsService and custom UserDetails class and it worked.
So security config class had no code regarding the authentication of the users.
I was very confused because I thought that somehow I had to create a #Bean inside the config class, implement the authentication myself and in general that all this authentication code had to be done inside the config class, but it worked with this approach.
#Service
public class MyCustomUserDetailsService implements UserDetailsService {
#Autowired
UserRepository userRepository;
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User Not Found with username: " + username);
}
return MyUserDetails.build(user);
}
}
And the details class
public class MyUserDetails implements UserDetails {
private String username;
private String firstName;
private String lastName;
#JsonIgnore
private String password;
private Collection<? extends GrantedAuthority> authorities;
public MyUserDetails(String username, String firstName, String lastName, String password,
Collection<? extends GrantedAuthority> authorities) {
this.username = username;
this.firstName = firstName;
this.lastName = lastName;
this.password = password;
this.authorities = authorities;
}
public static MyUserDetails build(User user) {
List<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getAuthority()))
.collect(Collectors.toList());
return new MyUserDetails(
user.getUsername(),
user.getFirstName(),
user.getLastName(),
user.getPassword(),
authorities);
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
public String getFirstName() {
return firstName;
}
public String getLastName() {
return lastName;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return username;
}
#Override
public boolean isAccountNonExpired() {
return true;
}
#Override
public boolean isAccountNonLocked() {
return true;
}
#Override
public boolean isCredentialsNonExpired() {
return true;
}
#Override
public boolean isEnabled() {
return true;
}
#Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
MyUserDetails user = (MyUserDetails) o;
return Objects.equals(username, user.username);
}
}
Also check Spring Security Architecture

Basic Auth Spring security with enum Roles and Permissions always return 401

i am new to Spring Security, i just have a User with enum Role and enum permissions, i wanted to have a basic auth using postman and to test it , but i always get 401 status code.
I am not sure what is the problem exactly because no errors i receive or no exeption occured but all i know is that i can not log in with basic auth using postman perhaps my configuration is not perfect or UserDetails and UserDetailsServices are not like they should be or maybe capturing the authorities in UserDetails is not working at all.
or maybe my password is not encoded in database and that's why the authentication can not pass.
My ApplicationSecurityConfig:
`#Configuration
#EnableWebSecurity
public class ApplicationSecurityConfig extends WebSecurityConfigurerAdapter {
//private final PasswordEncoder passwordEncoder;
private final ApplicationUserDetailsService applicationUserDetailsService;
#Autowired
public ApplicationSecurityConfig(PasswordEncoder passwordEncoder,
ApplicationUserDetailsService applicationUserDetailsService) {
// this.passwordEncoder = passwordEncoder;
this.applicationUserDetailsService = applicationUserDetailsService;
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/","/index","/css/*","/js/*") .permitAll()
//MEMBER
.antMatchers("/api/**").hasAnyRole(
ApplicationUserRole.SUPER_ADMIN.name(),
ApplicationUserRole.ADMIN.name(),
ApplicationUserRole.MEMBER.name()
)
.antMatchers(HttpMethod.GET,"/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_READ.name(),
ApplicationUserPermissions.ADMIN_READ.name(),
ApplicationUserPermissions.MEMBER_READ.name()
)
//ADMIN
.antMatchers("/admin/api/**").hasAnyRole(ApplicationUserRole.ADMIN.name(),ApplicationUserRole.SUPER_ADMIN.name())
.antMatchers(HttpMethod.POST,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PUT,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PATCH,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.DELETE,"/admin/api/**").hasAnyAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name(),
ApplicationUserPermissions.ADMIN_WRITE.name()
)
//SUPER_ADMIN
.antMatchers("/super/admin/api/**").hasAnyRole(
ApplicationUserRole.SUPER_ADMIN.name()
)
.antMatchers(HttpMethod.POST,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PUT,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.PATCH,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.antMatchers(HttpMethod.DELETE,"/super/admin/api/**").hasAuthority(
ApplicationUserPermissions.SUPER_ADMIN_WRITE.name()
)
.anyRequest()
.authenticated()
.and()
.httpBasic();
}
#Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(applicationUserDetailsService);
}`
ApplicationUserRole:
package com.github.workTimeMangementGithub.security;
import com.google.common.collect.Sets; import
org.springframework.security.core.GrantedAuthority; import
org.springframework.security.core.authority.SimpleGrantedAuthority;
import java.util.Set; import java.util.stream.Collectors;
public enum ApplicationUserRole {
SUPER_ADMIN(Sets.newHashSet(ApplicationUserPermissions.SUPER_ADMIN_READ,ApplicationUserPermissions.SUPER_ADMIN_WRITE)),
ADMIN(Sets.newHashSet(ApplicationUserPermissions.ADMIN_READ,ApplicationUserPermissions.ADMIN_WRITE)),
MEMBER(Sets.newHashSet(ApplicationUserPermissions.MEMBER_READ,ApplicationUserPermissions.MEMBER_WRITE));
private final Set<ApplicationUserPermissions> permissions;
ApplicationUserRole(Set<ApplicationUserPermissions> permissions) {
this.permissions = permissions;
}
public Set<ApplicationUserPermissions> getPermissions() {
return permissions;
}
public Set<GrantedAuthority> getGrantedAuthorities() {
Set<GrantedAuthority> permissions = getPermissions().stream().map(permission-> new
SimpleGrantedAuthority(permission.getPermission())).collect(Collectors.toSet());
permissions.add(new SimpleGrantedAuthority("ROLE_"+this.name()));
return permissions;
}
}
Here i have implemented User Role for Role Based Auth and i connect them with their permissions
My ApplicationUserPermissions
public enum ApplicationUserPermissions {
SUPER_ADMIN_WRITE("super_admin:write"),
SUPER_ADMIN_READ("super_admin:read"),
ADMIN_WRITE("admin:write"),
ADMIN_READ("admin:read"),
MEMBER_WRITE("member:write"),
MEMBER_READ("member:read");
private final String permission;
ApplicationUserPermissions(String permission) {
this.permission = permission;
}
public String getPermission() {
return permission;
}
}
Here i Created the permissions for every User Role to determine all permissions and privileges for each role.
My ApplicationUserDetailsService
import java.util.Optional;
#Service
#Slf4j
public class ApplicationUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
#Autowired
public ApplicationUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Optional<User> user = Optional.of(userRepository.findUserByUsername(username));
if(user.get() != null){
UserDTO userDto = UserMapper.toDTO(user.get());
log.info("User Found "+ userDto.getUsername());
}else {
log.warn("User NOT Found ");
}
user.orElseThrow(() -> new UsernameNotFoundException("Not found: " + username));
return new ApplicationUserDetails(user.get());
}
}
Here i have implemented ApplicationUserDetailsService and called the method loadUserByUsername with handling UserNotFoundException in case the user is not found.
My ApplicationUserDetails:
#Slf4j
public class ApplicationUserDetails implements UserDetails {
private List<? extends GrantedAuthority> grantedAuthorities;
private String username;
private String password;
private boolean isAccountNonExpired;
private boolean isAccountNonLocked;
private boolean isCredentialsNonExpired;
private boolean isEnabled;
public ApplicationUserDetails(List<? extends GrantedAuthority> grantedAuthorities, String username, String password, boolean isAccountNonExpired, boolean isAccountNonLocked, boolean isCredentialsNonExpired, boolean isEnabled) {
this.grantedAuthorities = grantedAuthorities;
this.username = username;
this.password = password;
this.isAccountNonExpired = isAccountNonExpired;
this.isAccountNonLocked = isAccountNonLocked;
this.isCredentialsNonExpired = isCredentialsNonExpired;
this.isEnabled = isEnabled;
}
public ApplicationUserDetails(User user) {
List<? extends GrantedAuthority> authorities = new ArrayList<>(ApplicationUserRole.ADMIN.getGrantedAuthorities());
this.grantedAuthorities = authorities;
log.warn("authorities "+authorities);
this.username = user.getUsername();
this.password = user.getPassword();
this.isAccountNonExpired = true;
this.isAccountNonLocked = true;
this.isCredentialsNonExpired = true;
this.isEnabled = true;
}
#Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
#Override
public String getPassword() {
return password;
}
#Override
public String getUsername() {
return username;
}
#Override
public boolean isAccountNonExpired() {
return isAccountNonExpired;
}
#Override
public boolean isAccountNonLocked() {
return isAccountNonLocked;
}
#Override
public boolean isCredentialsNonExpired() {
return isCredentialsNonExpired;
}
#Override
public boolean isEnabled() {
return isEnabled;
}
Here i have implemented ApplicationUserDetails and override some methods.
My problem is that i can not authenticate using basic auth via Postman.
Here a screen capture of the users of the database:
I am trying to find out what is wrong with my code , i follow many tutorials but no full example of working with enum Roles and permissions with JPA authentication , i spend a lot of time and i still don't know what is wrong exactly with my code.
The logger Slf4j is no showing the authenticated user in console and i don't know why.
Postman:
Spring Boot log Captures:
Any help will be so appreciated.

How to provide custom UserDetails with additional fields for testing a secured controller method?

Assume I have the following #WebMvcTest and #RestController in a Spring boot applcation (version 2.4.2).
// the test
#Test
#WithUserDetails
public void should_return_ok() throws Exception {
mockMvc.perform(get("/api/products").andExpect(status().isOk());
}
// the controller
#GetMapping(path = "/api/products")
public ResponseEntity<List<Product>> getProducts(#AuthenticationPrincipal CustomUserDetails userDetails) {
List<Product> products = productService.getProductsByUserId(userDetails.getUserId());
return ResponseEntity.ok(products);
}
I also provided a CustomUserDetails class which adds a userId.
#Getter
#Setter
public class CustomUserDetails extends User {
private static final long serialVersionUID = 5540615754152379571L;
private Long userId;
public CustomUserDetails(String username, String password, Collection<? extends GrantedAuthority> authorities) {
super(username, password, authorities);
}
public CustomUserDetails(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities);
}
}
I understand that Spring provides the #WithUserDetails annotation to provide an adequate object for testing. And this also allows specifying a custom username, password, etc. However I don't know how I could provide the userId which is necessary so that the controller method can extract it from the CustomUserDetails object.
You can create your own custom UserDetails object in your test class and do the following:
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
CustomUserDetails customUserDetails = new CustomUserDetails(...);
mockMvc.perform(get("/api/products").with(user(customUserDetails))).andExpect(status().isOk());
In your implementation of UserDetailsService you should return your instance of UserDetails. For example:
#Override
public UserDetails loadByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("Username " + username + " not found");
}
CustomUserDetails customUserDetails = new CustomUserDetails(user);
customUserDetails.setUserId(user.getUserId());
return customUserDetails;
}
public class CustomUserDetails implements UserDetails {
private final Long userId;
private final User user;
...constructors
...getters and setters
}
In your code, you can cast the Authentication object to your CustomUserDetails.
CustomUserDetails customUserDetails = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication();
Long userId = customUserDetails.getUserId();

Resources