8000 Use UserInfoExtractor in session Context by vpaturet · Pull Request #718 · entur/uttu · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Use UserInfoExtractor in session Context #718

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

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
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
Expand Up @@ -16,40 +16,46 @@

package no.entur.uttu.ext.entur.export.messaging;

import static org.junit.Assert.*;

import com.google.api.gax.core.CredentialsProvider;
import com.google.api.gax.core.NoCredentialsProvider;
import com.google.cloud.spring.pubsub.core.PubSubTemplate;
import com.google.cloud.spring.pubsub.core.subscriber.PubSubSubscriberTemplate;
import com.google.pubsub.v1.PubsubMessage;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import no.entur.uttu.UttuIntegrationTest;
import no.entur.uttu.config.Context;
import no.entur.uttu.export.messaging.spi.MessagingService;
import org.entur.pubsub.base.EnturGooglePubSubAdmin;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.rutebanken.helper.organisation.user.UserInfoExtractor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.testcontainers.containers.PubSubEmulatorContainer;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;

@Testcontainers
@ActiveProfiles({ "in-memory-blobstore", "entur-pubsub-messaging-service" })
@ActiveProfiles({ "in-memory-blobstore", "test", "entur-pubsub-messaging-service" })
public class EnturPubSubMessagingServiceTest extends UttuIntegrationTest {

public static final String TEST_CODESPACE = "rut";
private static final String TEST_CODESPACE = "rut";
private static final String TEST_EXPORT_FILE_NAME = "netex.zip";
private static final String TEST_USERNAME = "TEST_USERNAME";

public static final String TEST_EXPORT_FILE_NAME = "netex.zip";
private static PubSubEmulatorContainer pubsubEmulator;

@Autowired
Expand All @@ -67,6 +73,9 @@ public class EnturPubSubMessagingServiceTest extends UttuIntegrationTest {
@Value("${export.notify.queue.name:FlexibleLinesExportQueue}")
private String queueName;

@MockitoBean
private UserInfoExtractor userInfoExtractor;

@DynamicPropertySource
static void emulatorProperties(DynamicPropertyRegistry registry) {
registry.add(
Expand Down Expand Up @@ -97,6 +106,7 @@ public static void tearDown() {
@Before
public void setup() {
enturGooglePubSubAdmin.createSubscriptionIfMissing(queueName);
Context.setUserName(TEST_USERNAME);
}

@After
Expand All @@ -120,13 +130,20 @@ public void testNotifyExport() {
messagingService.notifyExport(TEST_CODESPACE, TEST_EXPORT_FILE_NAME);

List<PubsubMessage> messages = pubSubTemplate.pullAndAck(queueName, 1, false);
Assert.assertEquals(1, messages.size());
PubsubMessage pubsubMessage = messages.get(0);
String codespace = pubsubMessage
.getAttributesMap()
.get(EnturPubSubMessagingService.HEADER_CHOUETTE_REFERENTIAL);
Assert.assertEquals("rb_" + TEST_CODESPACE, codespace);
Assert.assertEquals(
assertEquals(1, messages.size());
PubsubMessage pubsubMessage = messages.getFirst();
Map<String, String> headers = pubsubMessage.getAttributesMap();

String codespace = headers.get(
EnturPubSubMessagingService.HEADER_CHOUETTE_REFERENTIAL
);
assertEquals("rb_" + TEST_CODESPACE, codespace);

String userName = headers.get(EnturPubSubMessagingService.HEADER_USERNAME);
assertNotNull(userName);
assertTrue(userName.startsWith(TEST_USERNAME));

assertEquals(
TEST_EXPORT_FILE_NAME,
pubsubMessage.getData().toString(StandardCharsets.UTF_8)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,10 @@ public void notifyExport(final String codespace, String filename) {
HEADER_CHOUETTE_REFERENTIAL,
ExportUtil.getMigratedReferential(codespace)
);
pubSubAttributes.put(HEADER_USERNAME, Context.getUsername() + " (via NPlan)");
pubSubAttributes.put(
HEADER_USERNAME,
Context.getVerifiedUsername() + " (via NPlan)"
);
pubSubAttributes.put(HEADER_CORRELATION_ID, UUID.randomUUID().toString());
pubSubTemplate.publish(queueName, filename, pubSubAttributes);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,14 @@ WebClient internalWebClient(
.build();
}

@ConditionalOnProperty(
value = "entur.security.role.assignment.extractor",
havingValue = "jwt",
matchIfMissing = true
)
@ConditionalOnProperty(value = "uttu.security.user.info.extractor", havingValue = "jwt")
@Bean
public UserInfoExtractor jwtUserInfoExtractor() {
return new JwtUserInfoExtractor();
}

@ConditionalOnProperty(
value = "entur.security.role.assignment.extractor",
value = "uttu.security.user.info.extractor",
havingValue = "baba"
)
@Bean
Expand Down
38 changes: 20 additions & 18 deletions src/main/java/no/entur/uttu/config/Context.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,11 @@
package no.entur.uttu.config;

import no.entur.uttu.util.Preconditions;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;

public class Context {

private static ThreadLocal<String> providerPerThread = new ThreadLocal<>();
private static ThreadLocal<String> userNamePerThread = new ThreadLocal<>();

public static void setProvider(String providerCode) {
Preconditions.checkArgument(
Expand All @@ -36,24 +34,28 @@ public static String getProvider() {
return providerPerThread.get();
}

public static void clear() {
providerPerThread.remove();
}

public static String getUsername() {
String user = null;
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (
auth != null && auth.getPrincipal() != null && auth.getPrincipal() instanceof Jwt
) {
user = ((Jwt) auth.getPrincipal()).getClaimAsString("preferred_username");
}
return (user == null) ? "unknown" : user;
}

public static String getVerifiedProviderCode() {
String providerCode = Context.getProvider();
Preconditions.checkArgument(providerCode != null, "Provider not set for session");
return providerCode;
}

public static void setUserName(String userName) {
Preconditions.checkArgument(
userName != null,
"Attempt to set userName = null for session"
);
userNamePerThread.set(userName);
}

public static String getVerifiedUsername() {
String userName = userNamePerThread.get();
Preconditions.checkArgument(userName != null, "Username not set for session");
return user 9E12 Name;
}

public static void clear() {
providerPerThread.remove();
userNamePerThread.remove();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,30 +57,23 @@
@PathParam("id") String exportId
) {
Context.setProvider(providerCode);
try {
Export export = exportRepository.findByNetexIdAndProviderCode(
exportId,
providerCode
Export export = exportRepository.findByNetexIdAndProviderCode(exportId, providerCode);
if (export == null) {
throw new EntityNotFoundException("No export with id= " + exportId + " found");

Check warning on line 62 in src/main/java/no/entur/uttu/export/resource/ExportFileDownloadResource.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/export/resource/ExportFileDownloadResource.java#L62

Added line #L62 was not covered by tests
}
if (!StringUtils.hasText(export.getFileName())) {
throw new EntityNotFoundException(

Check warning on line 65 in src/main/java/no/entur/uttu/export/resource/ExportFileDownloadResource.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/export/resource/ExportFileDownloadResource.java#L65

Added line #L65 was not covered by tests
"Export with id= " + exportId + " does not have a reference to export file"
);
if (export == null) {
throw new EntityNotFoundException("No export with id= " + exportId + " found");
}
if (!StringUtils.hasText(export.getFileName())) {
throw new EntityNotFoundException(
"Export with id= " + exportId + " does not have a reference to export file"
);
}
}

InputStream content = blobStoreRepository.getBlob(export.getFileName());
InputStream content = blobStoreRepository.getBlob(export.getFileName());

String fileNameOnly = Paths.get(export.getFileName()).getFileName().toString();
String fileNameOnly = Paths.get(export.getFileName()).getFileName().toString();

return Response
.ok(content, MediaType.APPLICATION_OCTET_STREAM)
.header("content-disposition", "attachment; filename = " + fileNameOnly)
.build();
} finally {
Context.clear();
}
return Response
.ok(content, MediaType.APPLICATION_OCTET_STREAM)
.header("content-disposition", "attachment; filename = " + fileNameOnly)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,7 @@
Map<String, Object> request
) {
Context.setProvider(providerCode);
try {
return graphQLResourceHelper.executeStatement(linesGraphQL, request);
} finally {
Context.clear();
}
return graphQLResourceHelper.executeStatement(linesGraphQL, request);
}

@POST
Expand All @@ -78,15 +74,11 @@
String query
) {
Context.setProvider(providerCode);
try {
return graphQLResourceHelper.getGraphQLResponse(
linesGraphQL,
"query",
query,
new HashMap<>()
);
} finally {
Context.clear();
}
return graphQLResourceHelper.getGraphQLResponse(

Check warning on line 77 in src/main/java/no/entur/uttu/graphql/resource/LinesGraphQLResource.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/graphql/resource/LinesGraphQLResource.java#L77

Added line #L77 was not covered by tests
linesGraphQL,
"query",
query,
new HashMap<>()
);
}
}
2 changes: 1 addition & 1 deletion src/main/java/no/entur/uttu/model/IdentifiedEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public void setVersion(Long version) {
@PrePersist
@PreUpdate
protected void setMetaData() {
String user = Context.getUsername();
String user = Context.getVerifiedUsername();
Instant now = Instant.now();
this.setChanged(now);
this.setChangedBy(user);
Expand Down
39 changes: 39 additions & 0 deletions src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package no.entur.uttu.security;

import javax.annotation.Nullable;
import org.rutebanken.helper.organisation.user.UserInfoExtractor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

/**
* Default UserInfoExtractor that retrieves the username from the standard OIDC claim "preferred_username".
* The preferred named is equal to the preferred username.
*/
public class DefaultUserInfoExtractor implements UserInfoExtractor {

Check warning on line 14 in src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java#L14

Added line #L14 was not covered by tests

private static final String CLAIM_OIDC_PREFERRED_USERNAME = "preferred_username";

@Nullable
@Override
public String getPreferredName() {
return getPreferredUsername();

Check warning on line 21 in src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java#L21

Added line #L21 was not covered by tests
}

@Nullable
@Override
public String getPreferredUsername() {
return getClaim(CLAIM_OIDC_PREFERRED_USERNAME);

Check warning on line 27 in src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java#L27

Added line #L27 was not covered by tests
}

private String getClaim(String claim) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();

Check warning on line 31 in src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java#L31

Added line #L31 was not covered by tests
if (auth instanceof JwtAuthenticationToken jwtAuthenticationToken) {
Jwt jwt = (Jwt) jwtAuthenticationToken.getPrincipal();
return jwt.getClaimAsString(claim);

Check warning on line 34 in src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java#L33-L34

Added lines #L33 - L34 were not covered by tests
} else {
return null;

Check warning on line 36 in src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java

View check run for this annotation

Codecov / codecov/patch

src/main/java/no/entur/uttu/security/DefaultUserInfoExtractor.java#L36

Added line #L36 was not covered by tests
}
}
}
38 changes: 38 additions & 0 deletions src/main/java/no/entur/uttu/security/UserInfoFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package no.entur.uttu.security;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import no.entur.uttu.config.Context;
import org.rutebanken.helper.organisation.user.UserInfoExtractor;
import org.springframework.web.filter.OncePerRequestFilter;

/**
* Set the username in the session context after successful authentication and
* clear the context after the response is sent.
*/
public class UserInfoFilter extends OncePerRequestFilter {

private final UserInfoExtractor userInfoExtractor;

public UserInfoFilter(UserInfoExtractor userInfoExtractor) {
this.userInfoExtractor = userInfoExtractor;
}

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain
) throws ServletException, IOException {
try {
String preferredUsername = userInfoExtractor.getPreferredUsername();
Context.setUserName(preferredUsername == null ? "unknown" : preferredUsername);
filterChain.doFilter(request, response);
} finally {
Context.clear();
}
}
}
Loading
0