Skip to content

Add Parsing functionality for service headers #213

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.box.l10n.mojito.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class ServiceIdentifierParser {
private final ServiceKeyValueParser keyValueParser = new ServiceKeyValueParser();

@Autowired ServiceIdentifierParserConfig serviceIdentifierParserConfig;

String parseHeader(String header) throws ServiceNotIdentifiableException {
if (serviceIdentifierParserConfig.isEnabled()) {
return keyValueParser.parseHeader(
header,
serviceIdentifierParserConfig.getServiceInstanceDelimiter(),
serviceIdentifierParserConfig.getKeyValueDelimiter(),
serviceIdentifierParserConfig.getValueDelimiter(),
serviceIdentifierParserConfig.getIdentifierKey());
}

return header;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.box.l10n.mojito.security;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class ServiceIdentifierParserConfig {
@Value("${l10n.spring.security.header.parser.serviceInstanceDelimiter:,}")
protected String serviceInstanceDelimiter;

@Value("${l10n.spring.security.header.parser.keyValueDelimiter:;}")
protected String keyValueDelimiter;

@Value("${l10n.spring.security.header.parser.valueDelimiter:=}")
protected String valueDelimiter;

@Value("${l10n.spring.security.header.parser.identifierKey:URI}")
protected String identifierKey;

@Value("${l10n.spring.security.header.serviceParser.enabled:false}")
protected boolean isEnabled;

public boolean isEnabled() {
return isEnabled;
}

public void setEnabled(boolean enabled) {
isEnabled = enabled;
}

public String getKeyValueDelimiter() {
return keyValueDelimiter;
}

public void setKeyValueDelimiter(String keyValueDelimiter) {
this.keyValueDelimiter = keyValueDelimiter;
}

public String getValueDelimiter() {
return valueDelimiter;
}

public void setValueDelimiter(String valueDelimiter) {
this.valueDelimiter = valueDelimiter;
}

public String getIdentifierKey() {
return identifierKey;
}

public void setIdentifierKey(String identifierKey) {
this.identifierKey = identifierKey;
}

public String getServiceInstanceDelimiter() {
return serviceInstanceDelimiter;
}

public void setServiceInstanceDelimiter(String serviceInstanceDelimiter) {
this.serviceInstanceDelimiter = serviceInstanceDelimiter;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.box.l10n.mojito.security;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/***
* Service for parsing the contents of a service header for identification
*/
public class ServiceKeyValueParser {

Logger logger = LoggerFactory.getLogger(ServiceKeyValueParser.class);

public String parseHeader(
String header,
String serviceInstanceDelimiter,
String keyValueDelimiter,
String valueDelimiter,
String identifierKey)
throws ServiceNotIdentifiableException {
logger.debug("Parsing header: {}", header);
if (header == null || header.isEmpty()) {
throw new ServiceNotIdentifiableException("Service identifier not found");
}

String[] serviceIdentifiers = header.split(serviceInstanceDelimiter);
// We assume that only the last service identifier is relevant
String relevantService = serviceIdentifiers[serviceIdentifiers.length - 1];

Map<String, String> keyValueMap =
Stream.of(relevantService.split(keyValueDelimiter))
.map(kv -> kv.split(valueDelimiter))
.filter(pair -> pair.length == 2)
.collect(Collectors.toMap(parts -> parts[0], parts -> parts[1]));

String serviceIdentifierValue = keyValueMap.get(identifierKey);
logger.debug("Service identifier: {}", serviceIdentifierValue);
if (serviceIdentifierValue == null || serviceIdentifierValue.isEmpty()) {
throw new ServiceNotIdentifiableException("Service identifier not found");
}

return serviceIdentifierValue;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.box.l10n.mojito.security;

public class ServiceNotIdentifiableException extends RuntimeException {

public ServiceNotIdentifiableException(String message) {
super(message);
}

public ServiceNotIdentifiableException(String message, Throwable t) {
super(message, t);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ public class UserDetailServiceAuthWrapper

protected PrincipalDetailService userDetailsService = null;
protected HeaderSecurityConfig headerSecurityConfig;
protected ServiceIdentifierParser serviceIdentifierParser;

public UserDetailServiceAuthWrapper(
PrincipalDetailService userDetailsService, HeaderSecurityConfig headerSecurityConfig) {
PrincipalDetailService userDetailsService,
HeaderSecurityConfig headerSecurityConfig,
ServiceIdentifierParser serviceIdentifierParser) {
this.userDetailsService = userDetailsService;
this.headerSecurityConfig = headerSecurityConfig;
this.serviceIdentifierParser = serviceIdentifierParser;
}

@Override
Expand All @@ -29,9 +33,10 @@ public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token)
boolean isService =
headerSecurityConfig != null && username.contains(headerSecurityConfig.servicePrefix);
logger.debug("User identifier: {}", username);
logger.debug("Following service logic: {}", isService);
logger.debug("Is using service authentication flow: {}", isService);
if (isService) {
return this.userDetailsService.loadServiceWithName(username);
String serviceName = serviceIdentifierParser.parseHeader(username);
return this.userDetailsService.loadServiceWithName(serviceName);
} else {
return this.userDetailsService.loadUserByUsername(username);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@
@Configuration
class WebSecurityHeaderConfig {
@Autowired HeaderSecurityConfig headerSecurityConfig;
@Autowired ServiceIdentifierParser serviceIdentifierParser;

@Bean
PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider() {
PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider =
new PreAuthenticatedAuthenticationProvider();
UserDetailServiceAuthWrapper userDetailsByNameServiceWrapper =
new UserDetailServiceAuthWrapper(
getPrincipalDetailsServiceCreatePartial(), headerSecurityConfig);
getPrincipalDetailsServiceCreatePartial(),
headerSecurityConfig,
serviceIdentifierParser);
preAuthenticatedAuthenticationProvider.setPreAuthenticatedUserDetailsService(
userDetailsByNameServiceWrapper);
return preAuthenticatedAuthenticationProvider;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.box.l10n.mojito.security;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.function.Executable;

public class ServiceKeyValueParserTest {

private ServiceKeyValueParser parser;

@BeforeEach
public void setUp() {
parser = new ServiceKeyValueParser();
}

@Test
public void testParseHeader_EmptyHeaderOrNull() {
Executable executable = () -> parser.parseHeader("", "/", ",", "=", "id");
assertThrows(ServiceNotIdentifiableException.class, executable);
executable = () -> parser.parseHeader(null, "&", ",", "=", "id");
assertThrows(ServiceNotIdentifiableException.class, executable);
}

@Test
public void testParseHeader_ValidCsvInput() {
String header = "key1=value1,key2=value2,id=someService";
String result = parser.parseHeader(header, "&", ",", "=", "id");
assertEquals("someService", result);
}

@Test
public void testParseHeader_ValidMultipleServiceInstanceInput() {
String header = "key1=value1,key2=value2,id=firstService&id=lastService";
String result = parser.parseHeader(header, "&", ",", "=", "id");
assertEquals("lastService", result);
}

@Test
public void testParseHeader_ValidSemicolonInput() {
String header = "key1:=value1;key2:=value2;id:=someService";
String result = parser.parseHeader(header, "&", ";", ":=", "id");
assertEquals("someService", result);
}

@Test
public void testParseHeader_NoIdentifierKey() {
String header = "key1=value1,key2=value2";
Executable executable = () -> parser.parseHeader(header, "&", ",", "=", "id");
ServiceNotIdentifiableException exception =
assertThrows(ServiceNotIdentifiableException.class, executable);
assertEquals("Service identifier not found", exception.getMessage());
}

@Test
public void testParseHeader_InvalidFormat() {
String header = "key1=value1&key2value2&id=someService";
Executable executable = () -> parser.parseHeader(header, "-", ";", "=", "id");
assertThrows(ServiceNotIdentifiableException.class, executable);
}

@Test
public void testParseHeader_EmptyServiceIdentifier() {
String header = "key1=value1,key2=value2,id=";
Executable executable = () -> parser.parseHeader(header, "&", ",", "=", "id");
ServiceNotIdentifiableException exception =
assertThrows(ServiceNotIdentifiableException.class, executable);
assertEquals("Service identifier not found", exception.getMessage());
}
}
Loading