Skip to content

Commit

Permalink
Merge branch 'releases/release-0.5.0' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark Kimask committed Sep 5, 2017
2 parents cb63454 + 230cd54 commit 661f25b
Show file tree
Hide file tree
Showing 54 changed files with 1,133 additions and 191 deletions.
14 changes: 11 additions & 3 deletions backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
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;

}
Loading

0 comments on commit 661f25b

Please sign in to comment.