Skip to content

Commit

Permalink
control-service: add OAuth authN & authZ for data jobs
Browse files Browse the repository at this point in the history
Extend the add OAuth authN & authZ for data jobs to allow
OAuth authN & authZ for data jobs

Signed-off-by: Dako Dakov <[email protected]>
  • Loading branch information
ddakov committed Jul 25, 2024
1 parent 2c0104c commit 2743e55
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 56 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,20 @@
import com.vmware.taurus.authorization.webhook.AuthorizationBody;
import com.vmware.taurus.authorization.webhook.AuthorizationWebHookProvider;
import com.vmware.taurus.base.FeatureFlags;
import com.vmware.taurus.secrets.service.JobSecretsService;
import com.vmware.taurus.secrets.service.vault.VaultTeamCredentials;
import com.vmware.taurus.service.diag.OperationContext;
import com.vmware.taurus.service.webhook.WebHookResult;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.lang.Nullable;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

Expand All @@ -35,50 +42,86 @@
@RequiredArgsConstructor
public class AuthorizationInterceptor implements HandlerInterceptor {

private static Logger log = LoggerFactory.getLogger(AuthorizationInterceptor.class);
@Value("${datajobs.authorization.jwt.claim.username}")
private String usernameField;

private final FeatureFlags featureFlags;
private static Logger log = LoggerFactory.getLogger(AuthorizationInterceptor.class);

private final AuthorizationWebHookProvider webhookProvider;
private final FeatureFlags featureFlags;

private final AuthorizationProvider authorizationProvider;
private final AuthorizationWebHookProvider webhookProvider;

private final OperationContext opCtx;
private final AuthorizationProvider authorizationProvider;

@Override
public boolean preHandle(
final HttpServletRequest request, final HttpServletResponse response, final Object handler)
throws IOException {
if (featureFlags.isSecurityEnabled()) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
AuthorizationBody body =
authorizationProvider.createAuthorizationBody(request, authentication);
private final OperationContext opCtx;

@Nullable
private final JobSecretsService secretsService;

@Override
public boolean preHandle(
final HttpServletRequest request, final HttpServletResponse response, final Object handler)
throws IOException {
if (!featureFlags.isSecurityEnabled()) {
return true;
}

Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated() || !featureFlags.isAuthorizationEnabled()) {
return true;
}

if (!(authentication instanceof JwtAuthenticationToken jwtToken)) {
return true;
}

if (jwtToken.getTokenAttributes().get(usernameField) != null) {
return handleVmwCspToken(request, response, jwtToken);
} else if (secretsService != null) {
return handleOAuthApplicationToken(request, jwtToken);
}

return true;
}

private boolean handleVmwCspToken(HttpServletRequest request, HttpServletResponse response, JwtAuthenticationToken token) throws IOException {
AuthorizationBody body = authorizationProvider.createAuthorizationBody(request, token);
updateOperationContext(body);
if (featureFlags.isAuthorizationEnabled()) {
return isRequestAuthorized(request, response, body);
return isRequestAuthorized(request, response, body);
}

private boolean handleOAuthApplicationToken(HttpServletRequest request, JwtAuthenticationToken token) {
Object tokenSubject = token.getTokenAttributes().get(OAuth2TokenIntrospectionClaimNames.SUB);
if (!(tokenSubject instanceof String subject)) {
return false;
}

String teamClientId = StringUtils.substringAfter(subject, ":");
String teamName = authorizationProvider.getJobTeam(request);
String newTeam = authorizationProvider.getJobNewTeam(request, teamName);

// The reqested operation is for a resource owned by another team
if (!teamName.equals(newTeam)) {
return false;
}
}
// If we are at this stage - either we are authenticated or authentication is disabled for
// that endpoint
return true;

VaultTeamCredentials teamCredentials = secretsService.readTeamOauthCredentials(teamName);
return teamCredentials != null && teamClientId.equals(teamCredentials.getClientId());
}
return true;
}

private boolean isRequestAuthorized(
HttpServletRequest request, HttpServletResponse response, AuthorizationBody body)
throws IOException {
WebHookResult decision = this.webhookProvider.invokeWebHook(body).get();
response.setStatus(decision.getStatus().value());
if (!decision.getMessage().isBlank()) {
response.getWriter().write(decision.getMessage());

private boolean isRequestAuthorized(
HttpServletRequest request, HttpServletResponse response, AuthorizationBody body)
throws IOException {
WebHookResult decision = this.webhookProvider.invokeWebHook(body).get();
response.setStatus(decision.getStatus().value());
if (!decision.getMessage().isBlank()) {
response.getWriter().write(decision.getMessage());
}
return decision.isSuccess();
}
return decision.isSuccess();
}

private void updateOperationContext(AuthorizationBody body) {
opCtx.setUser(body.getRequesterUserId());
opCtx.setTeam(body.getRequestedResourceTeam());
}
private void updateOperationContext(AuthorizationBody body) {
opCtx.setUser(body.getRequesterUserId());
opCtx.setTeam(body.getRequestedResourceTeam());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,8 @@ public AuthorizationBody createAuthorizationBody(
}

public String getUserId(Authentication authentication) {
if (authentication instanceof JwtAuthenticationToken) {
JwtAuthenticationToken oauthToken = (JwtAuthenticationToken) authentication;
return oauthToken.getTokenAttributes().get(usernameField).toString();
if (authentication instanceof JwtAuthenticationToken oauthToken) {
return oauthToken.getTokenAttributes().get(usernameField).toString();
} else {
var principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
Expand Down Expand Up @@ -145,9 +144,8 @@ protected String getAccessTokenReceived() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String accessToken = null;

if (authentication instanceof JwtAuthenticationToken) {
JwtAuthenticationToken oauthToken = (JwtAuthenticationToken) authentication;
accessToken =
if (authentication instanceof JwtAuthenticationToken oauthToken) {
accessToken =
Optional.ofNullable(oauthToken.getToken())
.map(AbstractOAuth2Token::getTokenValue)
.orElse(null);
Expand All @@ -170,15 +168,12 @@ String parsePropertyFromURI(String contextPath, String fullPath, int index) {
}
}

String getJobTeam(HttpServletRequest request) {
return this.parsePropertyFromURI(
request.getContextPath(), request.getRequestURI(), TEAM_NAME_INDEX);
public String getJobTeam(HttpServletRequest request) {
return this.parsePropertyFromURI(request.getContextPath(), request.getRequestURI(), TEAM_NAME_INDEX);
}

String getJobNewTeam(HttpServletRequest request, String existingTeam) {
String jobNewTeam =
this.parsePropertyFromURI(
request.getContextPath(), request.getRequestURI(), NEW_TEAM_NAME_INDEX);
public String getJobNewTeam(HttpServletRequest request, String existingTeam) {
String jobNewTeam = this.parsePropertyFromURI(request.getContextPath(), request.getRequestURI(), NEW_TEAM_NAME_INDEX);
if (jobNewTeam.isBlank()) {
return existingTeam;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import com.vmware.taurus.authorization.provider.AuthorizationProvider;
import com.vmware.taurus.authorization.webhook.AuthorizationWebHookProvider;
import com.vmware.taurus.base.FeatureFlags;
import com.vmware.taurus.secrets.service.JobSecretsService;
import com.vmware.taurus.secrets.service.vault.VaultTeamCredentials;
import com.vmware.taurus.security.SecurityConfiguration;
import com.vmware.taurus.service.repository.JobsRepository;
import com.vmware.taurus.service.diag.OperationContext;
Expand All @@ -19,17 +21,26 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.OAuth2TokenIntrospectionClaimNames;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.test.util.ReflectionTestUtils;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;

import static org.mockito.Mockito.mock;

@ExtendWith(MockitoExtension.class)
public class AuthorizationInterceptorTest {

Expand All @@ -48,6 +59,8 @@ public class AuthorizationInterceptorTest {

@Mock private OperationContext opCtx;

@Mock private JobSecretsService secretsService;

@InjectMocks private AuthorizationInterceptor authorizationInterceptor;

@Test
Expand All @@ -62,7 +75,11 @@ public void testAuthAndAuthzEnabledSuccessPOST() throws IOException {
Optional.of(
WebHookResult.builder().status(HttpStatus.OK).message("").success(true).build()));
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
ReflectionTestUtils.setField(authorizationInterceptor, "usernameField", "usernameField");
Map<String, Object> tokenAttributes = new HashMap<>();
tokenAttributes.put("usernameField", "testUser");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(tokenAttributes);
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(jwtAuthenticationToken);
SecurityContextHolder.setContext(securityContext);

Expand All @@ -85,7 +102,11 @@ public void testAuthAndAuthzEnabledSuccessGET() throws IOException {
Optional.of(
WebHookResult.builder().status(HttpStatus.OK).message("").success(true).build()));
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
ReflectionTestUtils.setField(authorizationInterceptor, "usernameField", "usernameField");
Map<String, Object> tokenAttributes = new HashMap<>();
tokenAttributes.put("usernameField", "testUser");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(tokenAttributes);
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(jwtAuthenticationToken);
SecurityContextHolder.setContext(securityContext);

Expand All @@ -104,7 +125,11 @@ public void testAuthAndAuthzEnabledSuccessPUT() throws IOException {
MockHttpServletResponse response = new MockHttpServletResponse();
request.setMethod("put");
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
ReflectionTestUtils.setField(authorizationInterceptor, "usernameField", "usernameField");
Map<String, Object> tokenAttributes = new HashMap<>();
tokenAttributes.put("usernameField", "testUser");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(tokenAttributes);
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(webhookProvider.invokeWebHook(Mockito.any()))
.thenReturn(
Optional.of(
Expand All @@ -124,10 +149,14 @@ public void testAuthAndAuthzEnabledUnauthorizedPUT() throws IOException {
Mockito.when(featureFlags.isSecurityEnabled()).thenReturn(true);
Mockito.when(featureFlags.isAuthorizationEnabled()).thenReturn(true);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
request.setMethod("put");
MockHttpServletResponse response = new MockHttpServletResponse();
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
ReflectionTestUtils.setField(authorizationInterceptor, "usernameField", "usernameField");
Map<String, Object> tokenAttributes = new HashMap<>();
tokenAttributes.put("usernameField", "testUser");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(tokenAttributes);
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(webhookProvider.invokeWebHook(Mockito.any()))
.thenReturn(
Optional.of(
Expand Down Expand Up @@ -160,15 +189,21 @@ public void testAuthDisabled() throws IOException {
}

@Test
@MockitoSettings(strictness = Strictness.LENIENT)
public void testAuthDisabledAuthzEnabled() throws IOException {
Mockito.when(featureFlags.isSecurityEnabled()).thenReturn(true);
Mockito.when(featureFlags.isAuthorizationEnabled()).thenReturn(false);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);
ReflectionTestUtils.setField(authorizationInterceptor, "usernameField", "usernameField");
Map<String, Object> tokenAttributes = new HashMap<>();
tokenAttributes.put("usernameField", "testUser");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(tokenAttributes);

var pass = authorizationInterceptor.preHandle(request, response, new Object());

Assertions.assertEquals(true, pass);
Assertions.assertTrue(pass);
Assertions.assertEquals(HttpStatus.OK.value(), response.getStatus());
Assertions.assertEquals("", response.getContentAsString());
}
Expand All @@ -181,7 +216,7 @@ public void testAuthDisabledAuthzEnabled() throws IOException {
@Test
public void testAuthenticationNullForDisabledEndpoints() throws IOException {
Mockito.when(featureFlags.isSecurityEnabled()).thenReturn(true);
SecurityContext securityContext = Mockito.mock(SecurityContext.class);
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(null);
SecurityContextHolder.setContext(securityContext);
MockHttpServletRequest request = new MockHttpServletRequest();
Expand All @@ -193,4 +228,73 @@ public void testAuthenticationNullForDisabledEndpoints() throws IOException {
Assertions.assertEquals(HttpStatus.OK.value(), response.getStatus());
Assertions.assertEquals("", response.getContentAsString());
}

@Test
void testPreHandle_OAuthApplicationToken_Success() throws IOException {
Mockito.when(featureFlags.isSecurityEnabled()).thenReturn(true);
Mockito.when(featureFlags.isAuthorizationEnabled()).thenReturn(true);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(jwtAuthenticationToken);
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);

Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2TokenIntrospectionClaimNames.SUB, "client:testClientId");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(attributes);

Mockito.when(authorizationProvider.getJobTeam(request)).thenReturn("testTeam");
Mockito.when(authorizationProvider.getJobNewTeam(request, "testTeam")).thenReturn("testTeam");

VaultTeamCredentials mockCredentials = new VaultTeamCredentials("testTeam","testClientId","testSecret");
Mockito.when(secretsService.readTeamOauthCredentials("testTeam")).thenReturn(mockCredentials);
SecurityContextHolder.setContext(securityContext);

Assertions.assertTrue(authorizationInterceptor.preHandle(request, response, new Object()));
}

@Test
void testPreHandle_OAuthApplicationToken_Failure() throws IOException {
Mockito.when(featureFlags.isSecurityEnabled()).thenReturn(true);
Mockito.when(featureFlags.isAuthorizationEnabled()).thenReturn(true);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(jwtAuthenticationToken);
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);

Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2TokenIntrospectionClaimNames.SUB, "client:testClientId");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(attributes);

Mockito.when(authorizationProvider.getJobTeam(request)).thenReturn("testTeam");
Mockito.when(authorizationProvider.getJobNewTeam(request, "testTeam")).thenReturn("testTeam");

VaultTeamCredentials mockCredentials = new VaultTeamCredentials("differentTeam","testClientId","testSecret");
Mockito.when(secretsService.readTeamOauthCredentials("testTeam")).thenReturn(mockCredentials);
SecurityContextHolder.setContext(securityContext);

Assertions.assertTrue(authorizationInterceptor.preHandle(request, response, new Object()));
}

@Test
void testPreHandle_OAuthApplicationToken_Failure_Operation_For_Different_Team() throws IOException {
Mockito.when(featureFlags.isSecurityEnabled()).thenReturn(true);
Mockito.when(featureFlags.isAuthorizationEnabled()).thenReturn(true);
MockHttpServletRequest request = new MockHttpServletRequest();
MockHttpServletResponse response = new MockHttpServletResponse();
SecurityContext securityContext = mock(SecurityContext.class);
Mockito.when(securityContext.getAuthentication()).thenReturn(jwtAuthenticationToken);
Mockito.when(jwtAuthenticationToken.isAuthenticated()).thenReturn(true);

Map<String, Object> attributes = new HashMap<>();
attributes.put(OAuth2TokenIntrospectionClaimNames.SUB, "client:testClientId");
Mockito.when(jwtAuthenticationToken.getTokenAttributes()).thenReturn(attributes);

Mockito.when(authorizationProvider.getJobTeam(request)).thenReturn("testTeam");
Mockito.when(authorizationProvider.getJobNewTeam(request, "testTeam")).thenReturn("differentTeam");
SecurityContextHolder.setContext(securityContext);

Assertions.assertFalse(authorizationInterceptor.preHandle(request, response, new Object()));
}
}

0 comments on commit 2743e55

Please sign in to comment.