-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'releases/release-0.5.0' into master
- Loading branch information
Showing
54 changed files
with
1,133 additions
and
191 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
47 changes: 47 additions & 0 deletions
47
backend/src/main/java/ee/ria/riha/authentication/EstEIDPrincipal.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
...end/src/main/java/ee/ria/riha/authentication/EstEIDRequestHeaderAuthenticationFilter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
|
||
} |
23 changes: 23 additions & 0 deletions
23
backend/src/main/java/ee/ria/riha/authentication/RihaFilterBasedLdapUserSearch.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
136 changes: 136 additions & 0 deletions
136
backend/src/main/java/ee/ria/riha/authentication/RihaLdapUserDetailsContextMapper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
backend/src/main/java/ee/ria/riha/authentication/RihaOrganization.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
|
||
} |
Oops, something went wrong.