Commit 90e8f80e authored by Valentin Suhnjov's avatar Valentin Suhnjov

Merge branch 'releases/release-0.6.0' into master

parents 661f25b5 242ded14
......@@ -10,7 +10,7 @@
<parent>
<groupId>ee.ria.riha</groupId>
<artifactId>browser</artifactId>
<version>0.5.0</version>
<version>0.6.0</version>
</parent>
<dependencies>
......@@ -37,11 +37,26 @@
<artifactId>jackson-databind</artifactId>
<version>2.8.4</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20160810</version>
</dependency>
<dependency>
<groupId>com.github.fge</groupId>
<artifactId>json-schema-validator</artifactId>
<version>2.2.6</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.7.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
......@@ -77,8 +92,36 @@
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>com.github.kongchen</groupId>
<artifactId>swagger-maven-plugin</artifactId>
<version>3.1.5</version>
<configuration>
<apiSources>
<apiSource>
<springmvc>true</springmvc>
<info>
<title>RIHA-Browser API</title>
<version>1.0</version>
<description>RIHA-Browser API documentation</description>
<license>
<name>MIT</name>
</license>
</info>
<schemes>
<scheme>http</scheme>
</schemes>
<basePath>/</basePath>
<locations>
<location>ee.ria.riha.web</location>
</locations>
<swaggerDirectory>${basedir}/src/main/resources/static</swaggerDirectory>
<outputFormats>yaml</outputFormats>
</apiSource>
</apiSources>
</configuration>
</plugin>
</plugins>
</build>
</project>
</project>
\ No newline at end of file
......@@ -2,14 +2,22 @@ package ee.ria.riha.authentication;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter;
import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import javax.naming.ldap.Rdn;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
......@@ -25,7 +33,7 @@ import static org.springframework.util.StringUtils.hasText;
* @author Valentin Suhnjov
*/
@Slf4j
public class EstEIDRequestHeaderAuthenticationFilter extends RequestHeaderAuthenticationFilter {
public class EstEIDRequestHeaderAuthenticationFilter extends AbstractPreAuthenticatedProcessingFilter {
private static final String PEM_CERTIFICATE_HEADER = "-----BEGIN CERTIFICATE-----";
private static final String PEM_CERTIFICATE_FOOTER = "-----END CERTIFICATE-----";
......@@ -36,18 +44,51 @@ public class EstEIDRequestHeaderAuthenticationFilter extends RequestHeaderAuthen
private static final String GIVEN_NAME = "gn";
private static final String SURNAME = "sn";
private String principalHeader = "SSL_CLIENT_S_DN";
private String credentialsHeader = "SSL_CLIENT_CERT";
private RequestMatcher requestMatcher = new AntPathRequestMatcher("/login/esteid", "GET");
public EstEIDRequestHeaderAuthenticationFilter() {
setExceptionIfHeaderMissing(false);
setPrincipalRequestHeader("SSL_CLIENT_S_DN");
setCredentialsRequestHeader("SSL_CLIENT_CERT");
setContinueFilterChainOnUnsuccessfulAuthentication(false);
}
/**
* Try to authenticate pre-authenticated EstEID user. In case of successful authentication, filter chain execution
* will stop resulting in status 200 response. In case of exception, exception will be propagated resulting in error
* response.
*
* @param req servlet request
* @param res servlet response
* @param chain filter chain
*/
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String subjectDn = (String) super.getPreAuthenticatedPrincipal(request);
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
if (requestMatcher.matches(((HttpServletRequest) req))) {
if (log.isDebugEnabled()) {
log.debug("Checking EstEID pre-authentication");
}
// Filtering ends here regardless of authentication outcome. Response status indicates authentication result.
super.doFilter(req, res, (request, response) -> { /* Empty filter chain */ });
} else {
chain.doFilter(req, res);
}
}
/**
* Extracts pre-authenticated principal from request headers.
*
* @param request servlet request
* @return extracted {@link EstEIDPrincipal}
* @throws BadCredentialsException in case principal header is null or empty, or when serial number extraction
* fails
*/
@Override
protected EstEIDPrincipal getPreAuthenticatedPrincipal(HttpServletRequest request) {
String subjectDn = request.getHeader(principalHeader);
if (!hasText(subjectDn)) {
return null;
throw new BadCredentialsException("Header does not contain pre-authenticated principal");
}
log.debug("Extracting principal from subject DN: {}", subjectDn);
......@@ -69,19 +110,31 @@ public class EstEIDRequestHeaderAuthenticationFilter extends RequestHeaderAuthen
return principal;
}
/**
* Extracts pre-authenticated credentials from request headers assuming that it is PEM encoded {@link
* X509Certificate}.
*
* @param request servlet request
* @return instance of {@link java.security.cert.X509Certificate}
* @throws BadCredentialsException in case credentials header is null or empty, or certificate could not be
* extracted
*/
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
String pem = (String) super.getPreAuthenticatedCredentials(request);
protected X509Certificate getPreAuthenticatedCredentials(HttpServletRequest request) {
String pem = request.getHeader(credentialsHeader);
if (!hasText(pem)) {
return null;
throw new BadCredentialsException("Header does not contain pre-authenticated credentials");
}
if (log.isDebugEnabled()) {
log.debug("Extracting credentials certificate from: {}", pem);
}
log.debug("Extracting credentials certificate from: {}", pem);
byte[] certificate = getCertificateBytes(pem);
try (ByteArrayInputStream certStream = new ByteArrayInputStream(certificate)) {
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
return certFactory.generateCertificate(certStream);
return (X509Certificate) certFactory.generateCertificate(certStream);
} catch (IOException | CertificateException e) {
throw new BadCredentialsException("Could not generate certificate from certificate data", e);
}
......@@ -105,4 +158,26 @@ public class EstEIDRequestHeaderAuthenticationFilter extends RequestHeaderAuthen
return Base64.getDecoder().decode(binary);
}
public void setRequestMatcher(RequestMatcher requestMatcher) {
Assert.notNull(requestMatcher, "requestMatcher should not be null");
this.requestMatcher = requestMatcher;
}
public String getPrincipalHeader() {
return principalHeader;
}
public void setPrincipalHeader(String principalHeader) {
Assert.hasText(principalHeader, "principalHeader must not be null or empty");
this.principalHeader = principalHeader;
}
public String getCredentialsHeader() {
return credentialsHeader;
}
public void setCredentialsHeader(String credentialsHeader) {
Assert.hasText(credentialsHeader, "credentialsHeader must not be null or empty");
this.credentialsHeader = credentialsHeader;
}
}
package ee.ria.riha.authentication;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Multimap;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ldap.core.DirContextOperations;
......@@ -7,16 +9,17 @@ import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.ldap.support.LdapUtils;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.ldap.userdetails.LdapUserDetailsMapper;
import org.springframework.util.Assert;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.ldap.LdapName;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.springframework.ldap.support.LdapUtils.convertLdapException;
import static org.springframework.ldap.support.LdapUtils.newLdapName;
......@@ -33,28 +36,31 @@ public class RihaLdapUserDetailsContextMapper extends LdapUserDetailsMapper {
private static final String MEMBER_OF_ATTRIBUTE = "memberof";
private static final String COMMON_NAME_ATTRIBUTE = "cn";
private static final String DISPLAY_NAME_ATTRIBUTE = "displayname";
private static final String ROLE_PREFIX = "ROLE_";
private static final String DEFAULT_RIHA_USER_ROLE = "ROLE_RIHA_USER";
private LdapTemplate ldapTemplate;
public RihaLdapUserDetailsContextMapper(LdapContextSource ldapContextSource) {
Assert.notNull(ldapContextSource, "LDAP context source must not be null");
ldapTemplate = new LdapTemplate(ldapContextSource);
}
@Override
public UserDetails mapUserFromContext(DirContextOperations ctx, String username,
Collection<? extends GrantedAuthority> authorities) {
UserDetails userDetails = super.mapUserFromContext(ctx, username, authorities);
// User that was found in the context is a valid RIHA user, so apply default role
List<GrantedAuthority> grantedAuthorities = new ArrayList<>(authorities);
grantedAuthorities.add(new SimpleGrantedAuthority(DEFAULT_RIHA_USER_ROLE));
RihaUserDetails rihaUserDetails = new RihaUserDetails(userDetails, ctx.getStringAttribute(UID_ATTRIBUTE));
rihaUserDetails.getOrganizations().putAll(getUserOrganizationRoles(ctx));
UserDetails userDetails = super.mapUserFromContext(ctx, username, grantedAuthorities);
return rihaUserDetails;
return new RihaUserDetails(userDetails, ctx.getStringAttribute(UID_ATTRIBUTE),
getUserOrganizationRoles(ctx));
}
private MultiValueMap<RihaOrganization, String> getUserOrganizationRoles(DirContextOperations ctx) {
MultiValueMap<RihaOrganization, String> organizationRoles = new LinkedMultiValueMap<>();
private Multimap<RihaOrganization, GrantedAuthority> getUserOrganizationRoles(DirContextOperations ctx) {
Multimap<RihaOrganization, GrantedAuthority> organizationRoles = ArrayListMultimap.create();
String[] groupDns = ctx.getStringAttributes(MEMBER_OF_ATTRIBUTE);
if (groupDns != null) {
......@@ -66,7 +72,7 @@ public class RihaLdapUserDetailsContextMapper extends LdapUserDetailsMapper {
if (organizationRoleMapping != null) {
RihaOrganization rihaOrganization = new RihaOrganization(organizationRoleMapping.getCode(),
organizationRoleMapping.getName());
organizationRoles.add(rihaOrganization, organizationRoleMapping.getRole());
organizationRoles.put(rihaOrganization, organizationRoleMapping.getAuthority());
}
}
}
......@@ -113,7 +119,7 @@ public class RihaLdapUserDetailsContextMapper extends LdapUserDetailsMapper {
}
organizationRoleMapping.setCode(cnTokens[0]);
organizationRoleMapping.setRole(cnTokens[1].toUpperCase());
organizationRoleMapping.setAuthority(new SimpleGrantedAuthority(ROLE_PREFIX + cnTokens[1].toUpperCase()));
organizationRoleMapping.setName(groupCtx.getStringAttribute(DISPLAY_NAME_ATTRIBUTE));
return organizationRoleMapping;
......@@ -131,6 +137,6 @@ public class RihaLdapUserDetailsContextMapper extends LdapUserDetailsMapper {
private class OrganizationRoleMapping {
private String name;
private String code;
private String role;
private GrantedAuthority authority;
}
}
package ee.ria.riha.authentication;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.springframework.util.Assert;
/**
* Immutable representation of organization that user belongs to.
*
* @author Valentin Suhnjov
*/
@EqualsAndHashCode(of = "code")
@AllArgsConstructor
@NoArgsConstructor
@Getter
@ToString
public class RihaOrganization {
private String code;
private String name;
public RihaOrganization(String code, String name) {
Assert.hasText(code, "code must not be null");
this.code = code;
this.name = name;
}
public String getCode() {
return code;
}
public String getName() {
return name;
}
}
package ee.ria.riha.authentication;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import java.util.*;
/**
* Authentication token that is aware of {@link RihaUserDetails} organizations and is able to select one organization as
* active. Effective set of authorities is a combination of base and active organization authorities.
*
* @author Valentin Suhnjov
*/
@Slf4j
public class RihaOrganizationAwareAuthenticationToken extends PreAuthenticatedAuthenticationToken {
private RihaOrganization activeOrganization;
private Multimap<RihaOrganization, GrantedAuthority> organizationAuthorities = ImmutableMultimap.of();
private Map<String, RihaOrganization> organizationsByCode = ImmutableMap.of();
private Set<GrantedAuthority> effectiveAuthorities;
public RihaOrganizationAwareAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> baseAuthorities) {
super(principal, credentials, baseAuthorities);
initOrganizations(principal);
this.effectiveAuthorities = getEffectiveAuthorities();
setAuthenticated(true);
}
private void initOrganizations(Object principal) {
if (!(principal instanceof RihaUserDetails)) {
return;
}
RihaUserDetails userDetails = (RihaUserDetails) principal;
this.organizationAuthorities = ImmutableMultimap.copyOf(userDetails.getOrganizationAuthorities());
Map<String, RihaOrganization> orgRoles = new HashMap<>();
userDetails.getOrganizationAuthorities().keys().forEach(o -> orgRoles.put(o.getCode(), o));
this.organizationsByCode = ImmutableMap.copyOf(orgRoles);
}
private Set<GrantedAuthority> getEffectiveAuthorities() {
Set<GrantedAuthority> combinedAuthorities = new HashSet<>();
if (super.getAuthorities() != null) {
combinedAuthorities.addAll(super.getAuthorities());
}
if (this.activeOrganization != null) {
combinedAuthorities.addAll(organizationAuthorities.get(this.activeOrganization));
}
return combinedAuthorities.isEmpty() ? null : ImmutableSet.copyOf(combinedAuthorities);
}
@Override
public Collection<GrantedAuthority> getAuthorities() {
return this.effectiveAuthorities;
}
public RihaOrganization getActiveOrganization() {
return activeOrganization;
}
/**
* Either sets or clears active organization. Active organization must be one of the users organizationRoles.
*
* @param organizationCode - new active organization
*/
public void setActiveOrganization(String organizationCode) {
if (log.isDebugEnabled()) {
log.debug("Setting active organization to organization with code: {}", organizationCode);
}
if (organizationCode == null) {
if (log.isDebugEnabled()) {
log.debug("Clearing active organization");
}
this.activeOrganization = null;
} else {
if (!organizationsByCode.containsKey(organizationCode)) {
throw new IllegalArgumentException("User is not part of organization with code: " + organizationCode);
}
this.activeOrganization = organizationsByCode.get(organizationCode);
if (log.isDebugEnabled()) {
log.debug("Active organization is set to {}", activeOrganization);
}
}
this.effectiveAuthorities = getEffectiveAuthorities();
}
public Multimap<RihaOrganization, GrantedAuthority> getOrganizationAuthorities() {
return organizationAuthorities;
}
}
package ee.ria.riha.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
/**
* Wraps successful authentication with {@link RihaOrganizationAwareAuthenticationToken}.
*
* @author Valentin Suhnjov
*/
public class RihaPreAuthenticatedAuthenticationProvider extends PreAuthenticatedAuthenticationProvider {
@Override
public Authentication authenticate(Authentication authentication) {
Authentication authResult = super.authenticate(authentication);
if (authResult == null) {
return null;
}
return new RihaOrganizationAwareAuthenticationToken(
authResult.getPrincipal(),
authResult.getCredentials(),
authResult.getAuthorities());
}
}
package ee.ria.riha.authentication;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.Multimap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.util.Assert;
import java.util.Collection;
......@@ -12,6 +15,7 @@ import java.util.Collection;
*
* @author Valentin Suhnjov
*/
@Slf4j
public class RihaUserDetails implements UserDetails {
private UserDetails delegate;
......@@ -19,11 +23,22 @@ public class RihaUserDetails implements UserDetails {
private String personalCode;
private String firstName;
private String lastName;
private MultiValueMap<RihaOrganization, String> organizations = new LinkedMultiValueMap<>();
private Multimap<RihaOrganization, GrantedAuthority> organizationAuthorities;
public RihaUserDetails(UserDetails delegate, String personalCode) {
this(delegate, personalCode, null);
}
public RihaUserDetails(UserDetails delegate, String personalCode,
Multimap<RihaOrganization, GrantedAuthority> organizationAuthorities) {
Assert.notNull(delegate, "delegate should not be null");
Assert.hasText(personalCode, "personalCode should not be empty");
this.delegate = delegate;
this.personalCode = personalCode;
this.organizationAuthorities = organizationAuthorities != null
? ImmutableListMultimap.copyOf(organizationAuthorities)
: ImmutableMultimap.of();
}
public String getPersonalCode() {
......@@ -46,8 +61,8 @@ public class RihaUserDetails implements UserDetails {
this.lastName = lastName;
}
public MultiValueMap<RihaOrganization, String> getOrganizations() {
return organizations;
public Multimap<RihaOrganization, GrantedAuthority> getOrganizationAuthorities() {
return organizationAuthorities;
}
@Override
......@@ -84,4 +99,5 @@ public class RihaUserDetails implements UserDetails {
public boolean isEnabled() {
return delegate.isEnabled();
}
}
package ee.ria.riha.conf;
import com.github.fge.jackson.JsonLoader;
import ee.ria.riha.domain.InfoSystemRepository;
import ee.ria.riha.domain.RihaStorageInfoSystemRepository;
import ee.ria.riha.service.JsonValidationService;
import ee.ria.riha.storage.client.StorageClient;
import ee.ria.riha.storage.domain.MainResourceRepository;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
......@@ -7,6 +11,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
/**
* @author Valentin Suhnjov
*/
......@@ -24,4 +30,14 @@ public class ApplicationConfiguration {
return new StorageClient(restTemplate, applicationProperties.getStorageClient().getBaseUrl());
}
@Bean
public InfoSystemRepository infoSystemRepository(MainResourceRepository mainResourceRepository) {
return new RihaStorageInfoSystemRepository(mainResourceRepository);
}
@Bean
public JsonValidationService jsonValidationService(ApplicationProperties applicationProperties) throws IOException {
return new JsonValidationService(
JsonLoader.fromResource(applicationProperties.getValidation().getJsonSchemaUrl()));
}
}
......@@ -14,9 +14,12 @@ import org.springframework.context.annotation.Configuration;
@Getter
public class ApplicationProperties {
public static final String API_V1_PREFIX = "/api/v1";
private final RemoteApi remoteApi = new RemoteApi();
private final StorageClientProperties storageClient = new StorageClientProperties();
private final AuthenticationProperties authentication = new AuthenticationProperties();
private final ValidationProperties validation = new ValidationProperties();
@Getter
@Setter
......@@ -28,7 +31,6 @@ public class ApplicationProperties {
@Getter
@Setter
public static class RemoteApi {
private String producerUrl;
private String approverUrl;
}
......@@ -42,4 +44,10 @@ public class ApplicationProperties {
private String ldapUser;
private String ldapPassword;
}
@Getter
@Setter
public static class ValidationProperties {
private String jsonSchemaUrl;
}
}
package ee.ria.riha.conf;
import ee.ria.riha.authentication.EstEIDRequestHeaderAuthenticationFilter;
import ee.ria.riha.authentication.RihaFilterBasedLdapUserSearch;
import ee.ria.riha.authentication.RihaLdapUserDetailsContextMapper;
import ee.ria.riha.authentication.RihaPreAuthenticatedUserDetailsService;
import ee.ria.riha.authentication.*;
import ee.ria.riha.conf.ApplicationProperties.AuthenticationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.ldap.core.support.LdapContextSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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;
......@@ -21,7 +20,9 @@ import org.springframework.security.web.authentication.preauth.PreAuthenticatedA
/**
* @author Valentin Suhnjov
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
......@@ -61,7 +62,7 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
}
private PreAuthenticatedAuthenticationProvider getEsteidPreAuthenticatedAuthenticationProvider() {
PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
PreAuthenticatedAuthenticationProvider authenticationProvider = new RihaPreAuthenticatedAuthenticationProvider();
authenticationProvider.setPreAuthenticatedUserDetailsService(
new RihaPreAuthenticatedUserDetailsService(ldapUserDetailsService));
......@@ -74,9 +75,7 @@ public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
http.addFilter(esteidRequestHeaderAuthenticationFilter(authenticationManager()));
http.authorizeRequests()
.antMatchers("/idlogin").authenticated()
.anyRequest().permitAll();
http.authorizeRequests().anyRequest().permitAll();
http.logout().logoutSuccessHandler((new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK)));