Commit 661f25b5 authored by Mark Kimask's avatar Mark Kimask

Merge branch 'releases/release-0.5.0' into master

parents cb634546 230cd540
......@@ -10,7 +10,7 @@
<parent>
<groupId>ee.ria.riha</groupId>
<artifactId>browser</artifactId>
<version>0.4.0</version>
<version>0.5.0</version>
</parent>
<dependencies>
......@@ -26,14 +26,22 @@
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.4</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
......
package ee.ria.riha.authentication;
import lombok.ToString;
import java.security.Principal;
/**
* Principal representing EstEID user.
*
* @author Valentin Suhnjov
*/
@ToString
public class EstEIDPrincipal implements Principal {
private String serialNumber;
private String givenName;
private String surname;
public EstEIDPrincipal(String serialNumber) {
this.serialNumber = serialNumber;
}
@Override
public String getName() {
return serialNumber;
}
public String getSerialNumber() {
return serialNumber;
}
public String getGivenName() {
return givenName;
}
public void setGivenName(String givenName) {
this.givenName = givenName;
}
public String getSurname() {
return surname;
}
public void setSurname(String surname) {
this.surname = surname;
}
}
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 javax.naming.ldap.Rdn;
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.util.Base64;
import java.util.HashMap;
import java.util.Map;
import static org.springframework.ldap.support.LdapUtils.newLdapName;
import static org.springframework.util.StringUtils.hasText;
/**
* Pre-authenticated filter which obtains pre-authenticated user certificate subject and PEM encoded certificate from
* request headers. Produces {@link EstEIDPrincipal} from certificate subject and {@link
* java.security.cert.X509Certificate} as credential.
*
* @author Valentin Suhnjov
*/
@Slf4j
public class EstEIDRequestHeaderAuthenticationFilter extends RequestHeaderAuthenticationFilter {
private static final String PEM_CERTIFICATE_HEADER = "-----BEGIN CERTIFICATE-----";
private static final String PEM_CERTIFICATE_FOOTER = "-----END CERTIFICATE-----";
private static final String NON_BASE_64_CHARACTER = "[^A-Za-z0-9+/=]";
private static final String SERIAL_NUMBER = "serialnumber";
private static final String GIVEN_NAME = "gn";
private static final String SURNAME = "sn";
public EstEIDRequestHeaderAuthenticationFilter() {
setExceptionIfHeaderMissing(false);
setPrincipalRequestHeader("SSL_CLIENT_S_DN");
setCredentialsRequestHeader("SSL_CLIENT_CERT");
}
@Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String subjectDn = (String) super.getPreAuthenticatedPrincipal(request);
if (!hasText(subjectDn)) {
return null;
}
log.debug("Extracting principal from subject DN: {}", subjectDn);
Map<String, String> principalParts = new HashMap<>();
for (Rdn rdn : newLdapName(subjectDn).getRdns()) {
principalParts.put(rdn.getType().toLowerCase(), ((String) rdn.getValue()));
}
if (!principalParts.containsKey(SERIAL_NUMBER)) {
throw new BadCredentialsException(
"Subject DN does not contain serial number needed for principal extraction");
}
EstEIDPrincipal principal = new EstEIDPrincipal(principalParts.get(SERIAL_NUMBER));
principal.setGivenName(principalParts.get(GIVEN_NAME));
principal.setSurname(principalParts.get(SURNAME));
return principal;
}
@Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
String pem = (String) super.getPreAuthenticatedCredentials(request);
if (!hasText(pem)) {
return null;
}
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);
} catch (IOException | CertificateException e) {
throw new BadCredentialsException("Could not generate certificate from certificate data", e);
}
}
/**
* Normalize certificate and retrieve actual certificate bytes. Received PEM encoded certificate can have incorrect
* formatting caused by load balancer/proxy processing. Try to retrieve raw certificate bytes given that it is
* encoded with base64 encoding.
*
* @param pem PEM encoded certificate
* @return certificate actual bytes
*/
private byte[] getCertificateBytes(String pem) {
String binary = pem
.replace(PEM_CERTIFICATE_HEADER, "")
.replace(PEM_CERTIFICATE_FOOTER, "");
binary = binary.replaceAll(NON_BASE_64_CHARACTER, "");
return Base64.getDecoder().decode(binary);
}
}
package ee.ria.riha.authentication;
import org.springframework.ldap.core.support.BaseLdapPathContextSource;
import org.springframework.security.ldap.search.FilterBasedLdapUserSearch;
/**
* Ldap user search that searches for necessary attributes for RIHA LDAP authorization.
*
* @author Valentin Suhnjov
*/
public class RihaFilterBasedLdapUserSearch extends FilterBasedLdapUserSearch {
private static final String ALL_NON_OPERATIONAL_ATTRIBUTES = "*";
private static final String MEMBER_OF_ATTRIBUTE = "memberOf";
public RihaFilterBasedLdapUserSearch(String searchBase, String searchFilter,
BaseLdapPathContextSource contextSource) {
super(searchBase, searchFilter, contextSource);
setReturningAttributes(new String[]{ALL_NON_OPERATIONAL_ATTRIBUTES, MEMBER_OF_ATTRIBUTE});
setSearchSubtree(true);
}
}
package ee.ria.riha.authentication;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ldap.core.DirContextOperations;
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.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.Collection;
import static org.springframework.ldap.support.LdapUtils.convertLdapException;
import static org.springframework.ldap.support.LdapUtils.newLdapName;
/**
* @author Valentin Suhnjov
*/
@Slf4j
public class RihaLdapUserDetailsContextMapper extends LdapUserDetailsMapper {
private static final String COMMON_NAME_TOKEN_SEPARATOR = "-";
private static final String UID_ATTRIBUTE = "uid";
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 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);
RihaUserDetails rihaUserDetails = new RihaUserDetails(userDetails, ctx.getStringAttribute(UID_ATTRIBUTE));
rihaUserDetails.getOrganizations().putAll(getUserOrganizationRoles(ctx));
return rihaUserDetails;
}
private MultiValueMap<RihaOrganization, String> getUserOrganizationRoles(DirContextOperations ctx) {
MultiValueMap<RihaOrganization, String> organizationRoles = new LinkedMultiValueMap<>();
String[] groupDns = ctx.getStringAttributes(MEMBER_OF_ATTRIBUTE);
if (groupDns != null) {
for (String groupDn : groupDns) {
DirContextOperations groupCtx = lookupGroup(groupDn);
if (groupCtx != null) {
OrganizationRoleMapping organizationRoleMapping = getOrganizationRoleMapping(groupCtx);
if (organizationRoleMapping != null) {
RihaOrganization rihaOrganization = new RihaOrganization(organizationRoleMapping.getCode(),
organizationRoleMapping.getName());
organizationRoles.add(rihaOrganization, organizationRoleMapping.getRole());
}
}
}
}
return organizationRoles;
}
private DirContextOperations lookupGroup(String groupDnStr) {
LdapName groupDn = normalizeGroupDn(groupDnStr);
try {
return ldapTemplate.lookupContext(groupDn);
} catch (org.springframework.ldap.NamingException e) {
log.warn("Lookup for user group '" + groupDn + "' has failed");
return null;
}
}
private LdapName normalizeGroupDn(String groupDnStr) {
LdapName groupDn = newLdapName(groupDnStr);
Name baseDn = getBaseDn();
if (groupDn.startsWith(baseDn)) {
return LdapUtils.removeFirst(groupDn, baseDn);
}
return groupDn;
}
private OrganizationRoleMapping getOrganizationRoleMapping(DirContextOperations groupCtx) {
OrganizationRoleMapping organizationRoleMapping = new OrganizationRoleMapping();
String commonName = groupCtx.getStringAttribute(COMMON_NAME_ATTRIBUTE);
if (commonName == null) {
log.debug("Could not find common name of organization '{}'", groupCtx.getDn());
return null;
}
String[] cnTokens = commonName.split(COMMON_NAME_TOKEN_SEPARATOR);
if (cnTokens.length != 2) {
log.debug("Expecting two tokens in organization common name '{}' but found {}", commonName,
cnTokens.length);
}
organizationRoleMapping.setCode(cnTokens[0]);
organizationRoleMapping.setRole(cnTokens[1].toUpperCase());
organizationRoleMapping.setName(groupCtx.getStringAttribute(DISPLAY_NAME_ATTRIBUTE));
return organizationRoleMapping;
}
private Name getBaseDn() {
try {
return newLdapName(ldapTemplate.getContextSource().getReadOnlyContext().getNameInNamespace());
} catch (NamingException e) {
throw convertLdapException(e);
}
}
@Data
private class OrganizationRoleMapping {
private String name;
private String code;
private String role;
}
}
package ee.ria.riha.authentication;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* @author Valentin Suhnjov
*/
@EqualsAndHashCode(of = "code")
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class RihaOrganization {
private String code;
private String name;
}
package ee.ria.riha.authentication;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.*;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
/**
* @author Valentin Suhnjov
*/
public class RihaPreAuthenticatedUserDetailsService extends UserDetailsByNameServiceWrapper<PreAuthenticatedAuthenticationToken> {
public RihaPreAuthenticatedUserDetailsService(UserDetailsService userDetailsService) {
super(userDetailsService);
}
/**
* Retrieves user details from wrapped {@link UserDetailsService} and in case {@link UsernameNotFoundException}
* produces {@link RihaUserDetails}. In case principal is of type {@link EstEIDPrincipal}, populates {@link
* RihaUserDetails} with principal data.
*
* @param authentication pre-authenticated authentication tokem
* @return UserDetails
*/
@Override
public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken authentication) {
UserDetails userDetails;
try {
userDetails = super.loadUserDetails(authentication);
} catch (UsernameNotFoundException e) {
userDetails = new RihaUserDetails(
new User(authentication.getName(), "", AuthorityUtils.NO_AUTHORITIES),
authentication.getName());
}
if (userDetails instanceof RihaUserDetails) {
populateRihaUserDetails(((RihaUserDetails) userDetails), authentication);
}
return userDetails;
}
private void populateRihaUserDetails(RihaUserDetails userDetails, Authentication authentication) {
if (!(authentication.getPrincipal() instanceof EstEIDPrincipal)) {
return;
}
EstEIDPrincipal principal = ((EstEIDPrincipal) authentication.getPrincipal());
userDetails.setFirstName(principal.getGivenName());
userDetails.setLastName(principal.getSurname());
}
}
package ee.ria.riha.authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.Collection;
/**
* RIHA user details including additional information like personal code and name.
*
* @author Valentin Suhnjov
*/
public class RihaUserDetails implements UserDetails {
private UserDetails delegate;
private String personalCode;
private String firstName;
private String lastName;
private MultiValueMap<RihaOrganization, String> organizations = new LinkedMultiValueMap<>();
public RihaUserDetails(UserDetails delegate, String personalCode) {
this.delegate = delegate;
this.personalCode = personalCode;
}
public String getPersonalCode() {
return personalCode;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public MultiValueMap<RihaOrganization, String> getOrganizations() {
return organizations;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return delegate.getAuthorities();
}
@Override
public String getPassword() {
return delegate.getPassword();
}
@Override
public String getUsername() {
return delegate.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return delegate.isAccountNonExpired();
}
@Override
public boolean isAccountNonLocked() {
return delegate.isAccountNonLocked();
}
@Override
public boolean isCredentialsNonExpired() {
return delegate.isCredentialsNonExpired();
}
@Override
public boolean isEnabled() {
return delegate.isEnabled();
}
}
package ee.ria.riha.conf;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
......@@ -9,51 +11,35 @@ import org.springframework.context.annotation.Configuration;
*/
@Configuration
@ConfigurationProperties(prefix = "browser")
@Getter
public class ApplicationProperties {
private final RemoteApi remoteApi = new RemoteApi();
private final StorageClientProperties storageClient = new StorageClientProperties();
private final AuthenticationProperties authentication = new AuthenticationProperties();
public RemoteApi getRemoteApi() {
return remoteApi;
}
public StorageClientProperties getStorageClient() {
return storageClient;
}
@Getter
@Setter
public static class StorageClientProperties {
@NotEmpty
private String baseUrl;
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(String baseUrl) {
this.baseUrl = baseUrl;
}
}
@Getter
@Setter
public static class RemoteApi {
private String producerUrl;
private String approverUrl;
}
public String getProducerUrl() {
return producerUrl;
}
public void setProducerUrl(String producerUrl) {
this.producerUrl = producerUrl;
}
public String getApproverUrl() {
return approverUrl;
}
public void setApproverUrl(String approverUrl) {
this.approverUrl = approverUrl;
}
@Getter
@Setter
public static class AuthenticationProperties {
private String userSearchBase;
private String userSearchFilter;
private String ldapUrl;
private String ldapBaseDn;
private String ldapUser;
private String ldapPassword;
}
}
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.conf.ApplicationProperties.AuthenticationProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.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.ldap.userdetails.LdapUserDetailsService;
import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;
/**
* @author Valentin Suhnjov
*/
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private LdapUserDetailsService ldapUserDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(getEsteidPreAuthenticatedAuthenticationProvider());
}
@Bean
public LdapUserDetailsService ldapUserDetailsService(ApplicationProperties applicationProperties,
LdapContextSource contextSource) {
AuthenticationProperties authenticationProperties = applicationProperties.getAuthentication();
RihaFilterBasedLdapUserSearch userSearch = new RihaFilterBasedLdapUserSearch(
authenticationProperties.getUserSearchBase(),
authenticationProperties.getUserSearchFilter(),
contextSource);
LdapUserDetailsService userDetailsService = new LdapUserDetailsService(userSearch);
userDetailsService.setUserDetailsMapper(new RihaLdapUserDetailsContextMapper(contextSource));
return userDetailsService;
}
@Bean
public LdapContextSource contextSource(ApplicationProperties applicationProperties) {
LdapContextSource contextSource = new LdapContextSource();
AuthenticationProperties authentication = applicationProperties.getAuthentication();
contextSource.setUrl(authentication.getLdapUrl());
contextSource.setBase(authentication.getLdapBaseDn());
contextSource.setUserDn(authentication.getLdapUser());
contextSource.setPassword(authentication.getLdapPassword());
return contextSource;
}
private PreAuthenticatedAuthenticationProvider getEsteidPreAuthenticatedAuthenticationProvider() {
PreAuthenticatedAuthenticationProvider authenticationProvider = new PreAuthenticatedAuthenticationProvider();
authenticationProvider.setPreAuthenticatedUserDetailsService(
new RihaPreAuthenticatedUserDetailsService(ldapUserDetailsService));
return authenticationProvider;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();