fix(security): play framework upgrade (#6626)

* fix(security): play framework upgrade
This commit is contained in:
david-leifker 2022-12-08 20:27:51 -06:00 committed by GitHub
parent 121e9c211c
commit 27ea3bf125
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 455 additions and 205 deletions

1
.gitignore vendored
View File

@ -72,6 +72,7 @@ temp/**
# frontend assets # frontend assets
datahub-frontend/public/** datahub-frontend/public/**
datahub-frontend/test/resources/public/**
.remote* .remote*
# Ignore runtime generated authenticatior/authorizer jar files # Ignore runtime generated authenticatior/authorizer jar files

View File

@ -10,6 +10,7 @@ buildscript {
ext.testContainersVersion = '1.17.4' ext.testContainersVersion = '1.17.4'
ext.jacksonVersion = '2.13.4' ext.jacksonVersion = '2.13.4'
ext.jettyVersion = '9.4.46.v20220331' ext.jettyVersion = '9.4.46.v20220331'
ext.playVersion = '2.8.18'
ext.log4jVersion = '2.19.0' ext.log4jVersion = '2.19.0'
ext.slf4jVersion = '1.7.32' ext.slf4jVersion = '1.7.32'
ext.logbackClassic = '1.2.11' ext.logbackClassic = '1.2.11'
@ -50,7 +51,7 @@ project.ext.spec = [
] ]
project.ext.externalDependency = [ project.ext.externalDependency = [
'akkaHttp': 'com.typesafe.akka:akka-http-core_2.12:10.1.15', 'akkaHttp': 'com.typesafe.akka:akka-http-core_2.12:10.2.10',
'antlr4Runtime': 'org.antlr:antlr4-runtime:4.7.2', 'antlr4Runtime': 'org.antlr:antlr4-runtime:4.7.2',
'antlr4': 'org.antlr:antlr4:4.7.2', 'antlr4': 'org.antlr:antlr4:4.7.2',
'assertJ': 'org.assertj:assertj-core:3.11.1', 'assertJ': 'org.assertj:assertj-core:3.11.1',
@ -82,7 +83,7 @@ project.ext.externalDependency = [
'graphqlJava': 'com.graphql-java:graphql-java:' + graphQLJavaVersion, 'graphqlJava': 'com.graphql-java:graphql-java:' + graphQLJavaVersion,
'graphqlJavaScalars': 'com.graphql-java:graphql-java-extended-scalars:' + graphQLJavaVersion, 'graphqlJavaScalars': 'com.graphql-java:graphql-java-extended-scalars:' + graphQLJavaVersion,
'gson': 'com.google.code.gson:gson:2.8.9', 'gson': 'com.google.code.gson:gson:2.8.9',
'guice': 'com.google.inject:guice:4.2.2', 'guice': 'com.google.inject:guice:4.2.3',
'guava': 'com.google.guava:guava:27.0.1-jre', 'guava': 'com.google.guava:guava:27.0.1-jre',
'h2': 'com.h2database:h2:2.1.214', 'h2': 'com.h2database:h2:2.1.214',
'hadoopClient': 'org.apache.hadoop:hadoop-client:3.2.1', 'hadoopClient': 'org.apache.hadoop:hadoop-client:3.2.1',
@ -125,6 +126,7 @@ project.ext.externalDependency = [
'log4jCore': "org.apache.logging.log4j:log4j-core:$log4jVersion", 'log4jCore': "org.apache.logging.log4j:log4j-core:$log4jVersion",
'log4jApi': "org.apache.logging.log4j:log4j-api:$log4jVersion", 'log4jApi': "org.apache.logging.log4j:log4j-api:$log4jVersion",
'log4j12Api': "org.slf4j:log4j-over-slf4j:$slf4jVersion", 'log4j12Api': "org.slf4j:log4j-over-slf4j:$slf4jVersion",
'log4j2Api': "org.apache.logging.log4j:log4j-to-slf4j:$log4jVersion",
'lombok': 'org.projectlombok:lombok:1.18.12', 'lombok': 'org.projectlombok:lombok:1.18.12',
'mariadbConnector': 'org.mariadb.jdbc:mariadb-java-client:2.6.0', 'mariadbConnector': 'org.mariadb.jdbc:mariadb-java-client:2.6.0',
'mavenArtifact': "org.apache.maven:maven-artifact:$mavenVersion", 'mavenArtifact': "org.apache.maven:maven-artifact:$mavenVersion",
@ -141,17 +143,18 @@ project.ext.externalDependency = [
'opentracingJdbc':'io.opentracing.contrib:opentracing-jdbc:0.2.15', 'opentracingJdbc':'io.opentracing.contrib:opentracing-jdbc:0.2.15',
'parquet': 'org.apache.parquet:parquet-avro:1.12.3', 'parquet': 'org.apache.parquet:parquet-avro:1.12.3',
'picocli': 'info.picocli:picocli:4.5.0', 'picocli': 'info.picocli:picocli:4.5.0',
'playCache': 'com.typesafe.play:play-cache_2.12:2.7.6', 'playCache': "com.typesafe.play:play-cache_2.12:$playVersion",
'playEhcache': 'com.typesafe.play:play-ehcache_2.12:2.7.6', 'playEhcache': "com.typesafe.play:play-ehcache_2.12:$playVersion",
'playWs': 'com.typesafe.play:play-ahc-ws-standalone_2.12:2.0.8', 'playWs': 'com.typesafe.play:play-ahc-ws-standalone_2.12:2.1.10',
'playDocs': 'com.typesafe.play:play-docs_2.12:2.7.6', 'playDocs': "com.typesafe.play:play-docs_2.12:$playVersion",
'playGuice': 'com.typesafe.play:play-guice_2.12:2.7.6', 'playGuice': "com.typesafe.play:play-guice_2.12:$playVersion",
'playJavaJdbc': 'com.typesafe.play:play-java-jdbc_2.12:2.7.6', 'playJavaJdbc': "com.typesafe.play:play-java-jdbc_2.12:$playVersion",
'playAkkaHttpServer': 'com.typesafe.play:play-akka-http-server_2.12:2.7.6', 'playAkkaHttpServer': "com.typesafe.play:play-akka-http-server_2.12:$playVersion",
'playServer': 'com.typesafe.play:play-server_2.12:2.7.6', 'playServer': "com.typesafe.play:play-server_2.12:$playVersion",
'playTest': 'com.typesafe.play:play-test_2.12:2.7.6', 'playTest': "com.typesafe.play:play-test_2.12:$playVersion",
'pac4j': 'org.pac4j:pac4j-oidc:3.6.0', 'playFilters': "com.typesafe.play:filters-helpers_2.12:$playVersion",
'playPac4j': 'org.pac4j:play-pac4j_2.12:8.0.2', 'pac4j': 'org.pac4j:pac4j-oidc:4.5.7',
'playPac4j': 'org.pac4j:play-pac4j_2.12:9.0.2',
'postgresql': 'org.postgresql:postgresql:42.3.8', 'postgresql': 'org.postgresql:postgresql:42.3.8',
'protobuf': 'com.google.protobuf:protobuf-java:3.19.6', 'protobuf': 'com.google.protobuf:protobuf-java:3.19.6',
'rangerCommons': 'org.apache.ranger:ranger-plugins-common:2.3.0', 'rangerCommons': 'org.apache.ranger:ranger-plugins-common:2.3.0',
@ -203,7 +206,6 @@ configure(subprojects.findAll {! it.name.startsWith('spark-lineage')}) {
exclude group: "io.netty", module: "netty" exclude group: "io.netty", module: "netty"
exclude group: "log4j", module: "log4j" exclude group: "log4j", module: "log4j"
exclude group: "org.springframework.boot", module: "spring-boot-starter-logging" exclude group: "org.springframework.boot", module: "spring-boot-starter-logging"
exclude group: "org.apache.logging.log4j", module: "log4j-to-slf4j"
exclude group: "com.vaadin.external.google", module: "android-json" exclude group: "com.vaadin.external.google", module: "android-json"
exclude group: "org.slf4j", module: "slf4j-reload4j" exclude group: "org.slf4j", module: "slf4j-reload4j"
exclude group: "org.slf4j", module: "slf4j-log4j12" exclude group: "org.slf4j", module: "slf4j-log4j12"
@ -230,7 +232,7 @@ subprojects {
dependencies { dependencies {
testCompile externalDependency.testng testCompile externalDependency.testng
constraints { constraints {
implementation('io.netty:netty-all:4.1.68.Final') implementation('io.netty:netty-all:4.1.85.Final')
implementation('org.apache.commons:commons-compress:1.21') implementation('org.apache.commons:commons-compress:1.21')
implementation('org.apache.velocity:velocity-engine-core:2.3') implementation('org.apache.velocity:velocity-engine-core:2.3')
implementation('org.hibernate:hibernate-validator:6.0.20.Final') implementation('org.hibernate:hibernate-validator:6.0.20.Final')

View File

@ -7,7 +7,7 @@ DataHub frontend is a [Play](https://www.playframework.com/) service written in
between [DataHub GMS](../metadata-service) which is the backend service and [DataHub Web](../datahub-web-react/README.md). between [DataHub GMS](../metadata-service) which is the backend service and [DataHub Web](../datahub-web-react/README.md).
## Pre-requisites ## Pre-requisites
* You need to have [JDK8](https://www.oracle.com/java/technologies/jdk8-downloads.html) * You need to have [JDK11](https://openjdk.org/projects/jdk/11/)
installed on your machine to be able to build `DataHub Frontend`. installed on your machine to be able to build `DataHub Frontend`.
* You need to have [Chrome](https://www.google.com/chrome/) web browser * You need to have [Chrome](https://www.google.com/chrome/) web browser
installed to be able to build because UI tests have a dependency on `Google Chrome`. installed to be able to build because UI tests have a dependency on `Google Chrome`.

View File

@ -89,7 +89,7 @@ public class AuthModule extends AbstractModule {
final String aesKeyHash = DigestUtils.sha1Hex(aesKeyBase.getBytes(StandardCharsets.UTF_8)); final String aesKeyHash = DigestUtils.sha1Hex(aesKeyBase.getBytes(StandardCharsets.UTF_8));
final String aesEncryptionKey = aesKeyHash.substring(0, 16); final String aesEncryptionKey = aesKeyHash.substring(0, 16);
playCacheCookieStore = new PlayCookieSessionStore( playCacheCookieStore = new PlayCookieSessionStore(
new ShiroAesDataEncrypter(aesEncryptionKey)); new ShiroAesDataEncrypter(aesEncryptionKey.getBytes()));
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to instantiate Pac4j cookie session store!", e); throw new RuntimeException("Failed to instantiate Pac4j cookie session store!", e);
} }

View File

@ -63,8 +63,8 @@ public class AuthUtils {
* *
* Returns true if the request is eligible to be forwarded to GMS, false otherwise. * Returns true if the request is eligible to be forwarded to GMS, false otherwise.
*/ */
public static boolean isEligibleForForwarding(Http.Context ctx) { public static boolean isEligibleForForwarding(Http.Request req) {
return hasValidSessionCookie(ctx) || hasAuthHeader(ctx); return hasValidSessionCookie(req) || hasAuthHeader(req);
} }
/** /**
@ -75,17 +75,17 @@ public class AuthUtils {
* Note that we depend on the presence of 2 cookies, one accessible to the browser and one not, * Note that we depend on the presence of 2 cookies, one accessible to the browser and one not,
* as well as their agreement to determine authentication status. * as well as their agreement to determine authentication status.
*/ */
public static boolean hasValidSessionCookie(final Http.Context ctx) { public static boolean hasValidSessionCookie(final Http.Request req) {
return ctx.session().containsKey(ACTOR) return req.session().data().containsKey(ACTOR)
&& ctx.request().cookie(ACTOR) != null && req.cookie(ACTOR) != null
&& ctx.session().get(ACTOR).equals(ctx.request().cookie(ACTOR).value()); && req.session().data().get(ACTOR).equals(req.cookie(ACTOR).value());
} }
/** /**
* Returns true if a request includes the Authorization header, false otherwise * Returns true if a request includes the Authorization header, false otherwise
*/ */
public static boolean hasAuthHeader(final Http.Context ctx) { public static boolean hasAuthHeader(final Http.Request req) {
return ctx.request().getHeaders().contains(Http.HeaderNames.AUTHORIZATION); return req.getHeaders().contains(Http.HeaderNames.AUTHORIZATION);
} }
/** /**

View File

@ -28,40 +28,21 @@ public class Authenticator extends Security.Authenticator {
} }
@Override @Override
public String getUsername(@Nonnull Http.Context ctx) { public Optional<String> getUsername(@Nonnull Http.Request req) {
if (this.metadataServiceAuthEnabled) { if (this.metadataServiceAuthEnabled) {
// If Metadata Service auth is enabled, we only want to verify presence of the // If Metadata Service auth is enabled, we only want to verify presence of the
// "Authorization" header OR the presence of a frontend generated session cookie. // "Authorization" header OR the presence of a frontend generated session cookie.
// At this time, the actor is still considered to be unauthenicated. // At this time, the actor is still considered to be unauthenicated.
return AuthUtils.isEligibleForForwarding(ctx) ? "urn:li:corpuser:UNKNOWN" : null; return Optional.ofNullable(AuthUtils.isEligibleForForwarding(req) ? "urn:li:corpuser:UNKNOWN" : null);
} else { } else {
// If Metadata Service auth is not enabled, verify the presence of a valid session cookie. // If Metadata Service auth is not enabled, verify the presence of a valid session cookie.
return AuthUtils.hasValidSessionCookie(ctx) ? ctx.session().get(ACTOR) : null; return Optional.ofNullable(AuthUtils.hasValidSessionCookie(req) ? req.session().data().get(ACTOR) : null);
}
}
@Override
public Optional<String> getUsername(@Nonnull Http.Request request) {
Http.Context ctx = Http.Context.current();
if (this.metadataServiceAuthEnabled) {
// If Metadata Service auth is enabled, we only want to verify presence of the
// "Authorization" header OR the presence of a frontend generated session cookie.
// At this time, the actor is still considered to be unauthenicated.
return Optional.ofNullable(AuthUtils.isEligibleForForwarding(ctx) ? "urn:li:corpuser:UNKNOWN" : null);
} else {
// If Metadata Service auth is not enabled, verify the presence of a valid session cookie.
return Optional.ofNullable(AuthUtils.hasValidSessionCookie(ctx) ? ctx.session().get(ACTOR) : null);
} }
} }
@Override @Override
@Nonnull @Nonnull
public Result onUnauthorized(@Nullable Http.Context ctx) { public Result onUnauthorized(@Nullable Http.Request req) {
return unauthorized();
}
@Override
public Result onUnauthorized(Http.Request req) {
return unauthorized(); return unauthorized();
} }
} }

View File

@ -1,6 +1,7 @@
package auth.sso; package auth.sso;
import org.pac4j.core.client.Client; import org.pac4j.core.client.Client;
import org.pac4j.core.credentials.Credentials;
/** /**
* A thin interface over a Pac4j {@link Client} object and its * A thin interface over a Pac4j {@link Client} object and its
@ -40,6 +41,6 @@ public interface SsoProvider<C extends SsoConfigs> {
/** /**
* Retrieves an initialized Pac4j {@link Client}. * Retrieves an initialized Pac4j {@link Client}.
*/ */
Client<?, ?> client(); Client<? extends Credentials> client();
} }

View File

@ -1,11 +1,13 @@
package auth.sso.oidc; package auth.sso.oidc;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import org.pac4j.core.authorization.generator.AuthorizationGenerator; import org.pac4j.core.authorization.generator.AuthorizationGenerator;
import org.pac4j.core.context.WebContext; import org.pac4j.core.context.WebContext;
import org.pac4j.core.profile.AttributeLocation; import org.pac4j.core.profile.AttributeLocation;
import org.pac4j.core.profile.CommonProfile; import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.profile.UserProfile;
import org.pac4j.core.profile.definition.ProfileDefinition; import org.pac4j.core.profile.definition.ProfileDefinition;
import org.pac4j.oidc.profile.OidcProfile; import org.pac4j.oidc.profile.OidcProfile;
import org.slf4j.Logger; import org.slf4j.Logger;
@ -18,33 +20,38 @@ public class OidcAuthorizationGenerator implements AuthorizationGenerator {
private static final Logger logger = LoggerFactory.getLogger(OidcAuthorizationGenerator.class); private static final Logger logger = LoggerFactory.getLogger(OidcAuthorizationGenerator.class);
private final ProfileDefinition profileDef; private final ProfileDefinition<?> profileDef;
private final OidcConfigs oidcConfigs; private final OidcConfigs oidcConfigs;
public OidcAuthorizationGenerator(final ProfileDefinition profileDef, final OidcConfigs oidcConfigs) { public OidcAuthorizationGenerator(final ProfileDefinition<?> profileDef, final OidcConfigs oidcConfigs) {
this.profileDef = profileDef; this.profileDef = profileDef;
this.oidcConfigs = oidcConfigs; this.oidcConfigs = oidcConfigs;
} }
@Override @Override
public CommonProfile generate(WebContext context, CommonProfile profile) { public Optional<UserProfile> generate(WebContext context, UserProfile profile) {
if (oidcConfigs.getExtractJwtAccessTokenClaims().orElse(false)) { if (oidcConfigs.getExtractJwtAccessTokenClaims().orElse(false)) {
try { try {
final JWT jwt = JWTParser.parse(((OidcProfile) profile).getAccessToken().getValue()); final JWT jwt = JWTParser.parse(((OidcProfile) profile).getAccessToken().getValue());
CommonProfile commonProfile = new CommonProfile();
for (final Entry<String, Object> entry : jwt.getJWTClaimsSet().getClaims().entrySet()) { for (final Entry<String, Object> entry : jwt.getJWTClaimsSet().getClaims().entrySet()) {
final String claimName = entry.getKey(); final String claimName = entry.getKey();
if (profile.getAttribute(claimName) == null) { if (profile.getAttribute(claimName) == null) {
profileDef.convertAndAdd(profile, AttributeLocation.PROFILE_ATTRIBUTE, claimName, entry.getValue()); profileDef.convertAndAdd(commonProfile, AttributeLocation.PROFILE_ATTRIBUTE, claimName, entry.getValue());
} }
} }
return Optional.of(commonProfile);
} catch (Exception e) { } catch (Exception e) {
logger.warn("Cannot parse access token claims", e); logger.warn("Cannot parse access token claims", e);
} }
} }
return profile; return Optional.ofNullable(profile);
} }
} }

View File

@ -52,13 +52,16 @@ import org.pac4j.core.engine.DefaultCallbackLogic;
import org.pac4j.core.http.adapter.HttpActionAdapter; import org.pac4j.core.http.adapter.HttpActionAdapter;
import org.pac4j.core.profile.CommonProfile; import org.pac4j.core.profile.CommonProfile;
import org.pac4j.core.profile.ProfileManager; import org.pac4j.core.profile.ProfileManager;
import org.pac4j.core.profile.UserProfile;
import org.pac4j.play.PlayWebContext; import org.pac4j.play.PlayWebContext;
import play.mvc.Result; import play.mvc.Result;
import auth.sso.SsoManager; import auth.sso.SsoManager;
import static auth.AuthUtils.ACCESS_TOKEN;
import static auth.AuthUtils.ACTOR;
import static auth.AuthUtils.createActorCookie;
import static com.linkedin.metadata.Constants.*; import static com.linkedin.metadata.Constants.*;
import static play.mvc.Results.*; import static play.mvc.Results.internalServerError;
import static auth.AuthUtils.*;
/** /**
@ -101,17 +104,17 @@ public class OidcCallbackLogic extends DefaultCallbackLogic<Result, PlayWebConte
// By this point, we know that OIDC is the enabled provider. // By this point, we know that OIDC is the enabled provider.
final OidcConfigs oidcConfigs = (OidcConfigs) _ssoManager.getSsoProvider().configs(); final OidcConfigs oidcConfigs = (OidcConfigs) _ssoManager.getSsoProvider().configs();
return handleOidcCallback(oidcConfigs, result, context, getProfileManager(context, config)); return handleOidcCallback(oidcConfigs, result, context, getProfileManager(context));
} }
private Result handleOidcCallback(final OidcConfigs oidcConfigs, final Result result, final PlayWebContext context, private Result handleOidcCallback(final OidcConfigs oidcConfigs, final Result result, final PlayWebContext context,
final ProfileManager<CommonProfile> profileManager) { final ProfileManager<UserProfile> profileManager) {
log.debug("Beginning OIDC Callback Handling..."); log.debug("Beginning OIDC Callback Handling...");
if (profileManager.isAuthenticated()) { if (profileManager.isAuthenticated()) {
// If authenticated, the user should have a profile. // If authenticated, the user should have a profile.
final CommonProfile profile = profileManager.get(true).get(); final CommonProfile profile = (CommonProfile) profileManager.get(true).get();
log.debug(String.format("Found authenticated user with profile %s", profile.getAttributes().toString())); log.debug(String.format("Found authenticated user with profile %s", profile.getAttributes().toString()));
// Extract the User name required to log into DataHub. // Extract the User name required to log into DataHub.
@ -149,8 +152,8 @@ public class OidcCallbackLogic extends DefaultCallbackLogic<Result, PlayWebConte
// Successfully logged in - Generate GMS login token // Successfully logged in - Generate GMS login token
final String accessToken = _authClient.generateSessionTokenForUser(corpUserUrn.getId()); final String accessToken = _authClient.generateSessionTokenForUser(corpUserUrn.getId());
context.getJavaSession().put(ACCESS_TOKEN, accessToken); context.getNativeSession().adding(ACCESS_TOKEN, accessToken);
context.getJavaSession().put(ACTOR, corpUserUrn.toString()); context.getNativeSession().adding(ACTOR, corpUserUrn.toString());
return result.withCookies(createActorCookie(corpUserUrn.toString(), oidcConfigs.getSessionTtlInHours())); return result.withCookies(createActorCookie(corpUserUrn.toString(), oidcConfigs.getSessionTtlInHours()));
} }
return internalServerError( return internalServerError(

View File

@ -8,7 +8,6 @@ import org.pac4j.core.client.Client;
import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver; import org.pac4j.core.http.callback.PathParameterCallbackUrlResolver;
import org.pac4j.oidc.config.OidcConfiguration; import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.oidc.credentials.OidcCredentials; import org.pac4j.oidc.credentials.OidcCredentials;
import org.pac4j.oidc.profile.OidcProfile;
import org.pac4j.oidc.profile.OidcProfileDefinition; import org.pac4j.oidc.profile.OidcProfileDefinition;
@ -27,7 +26,7 @@ public class OidcProvider implements SsoProvider<OidcConfigs> {
private static final String OIDC_CLIENT_NAME = "oidc"; private static final String OIDC_CLIENT_NAME = "oidc";
private final OidcConfigs _oidcConfigs; private final OidcConfigs _oidcConfigs;
private final Client<OidcCredentials, OidcProfile> _oidcClient; // Used primarily for redirecting to IdP. private final Client<OidcCredentials> _oidcClient; // Used primarily for redirecting to IdP.
public OidcProvider(final OidcConfigs configs) { public OidcProvider(final OidcConfigs configs) {
_oidcConfigs = configs; _oidcConfigs = configs;
@ -35,7 +34,7 @@ public class OidcProvider implements SsoProvider<OidcConfigs> {
} }
@Override @Override
public Client<OidcCredentials, OidcProfile> client() { public Client<OidcCredentials> client() {
return _oidcClient; return _oidcClient;
} }
@ -49,7 +48,7 @@ public class OidcProvider implements SsoProvider<OidcConfigs> {
return SsoProtocol.OIDC; return SsoProtocol.OIDC;
} }
private Client<OidcCredentials, OidcProfile> createPac4jClient() { private Client<OidcCredentials> createPac4jClient() {
final OidcConfiguration oidcConfiguration = new OidcConfiguration(); final OidcConfiguration oidcConfiguration = new OidcConfiguration();
oidcConfiguration.setClientId(_oidcConfigs.getClientId()); oidcConfiguration.setClientId(_oidcConfigs.getClientId());
oidcConfiguration.setSecret(_oidcConfigs.getClientSecret()); oidcConfiguration.setSecret(_oidcConfigs.getClientSecret());

View File

@ -5,6 +5,8 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import play.mvc.Result; import play.mvc.Result;
import java.util.Optional;
import static play.mvc.Results.internalServerError; import static play.mvc.Results.internalServerError;
import static play.mvc.Results.unauthorized; import static play.mvc.Results.unauthorized;
@ -26,7 +28,7 @@ public class OidcResponseErrorHandler {
getError(context), getError(context),
getErrorDescription(context)); getErrorDescription(context));
if (getError(context).equals("access_denied")) { if (getError(context).isPresent() && getError(context).get().equals("access_denied")) {
return unauthorized(String.format("Access denied. " return unauthorized(String.format("Access denied. "
+ "The OIDC service responded with 'Access denied'. " + "The OIDC service responded with 'Access denied'. "
+ "It seems that you don't have access to this application yet. Please apply for access. \n\n" + "It seems that you don't have access to this application yet. Please apply for access. \n\n"
@ -38,18 +40,18 @@ public class OidcResponseErrorHandler {
return internalServerError( return internalServerError(
String.format("Internal server error. The OIDC service responded with an error: '%s'.\n" String.format("Internal server error. The OIDC service responded with an error: '%s'.\n"
+ "Error description: '%s'", getError(context), getErrorDescription(context))); + "Error description: '%s'", getError(context).orElse(""), getErrorDescription(context).orElse("")));
} }
public static boolean isError(final PlayWebContext context) { public static boolean isError(final PlayWebContext context) {
return getError(context) != null && !getError(context).isEmpty(); return getError(context).isPresent() && !getError(context).get().isEmpty();
} }
public static String getError(final PlayWebContext context) { public static Optional<String> getError(final PlayWebContext context) {
return context.getRequestParameter(ERROR_FIELD_NAME); return context.getRequestParameter(ERROR_FIELD_NAME);
} }
public static String getErrorDescription(final PlayWebContext context) { public static Optional<String> getErrorDescription(final PlayWebContext context) {
return context.getRequestParameter(ERROR_DESCRIPTION_FIELD_NAME); return context.getRequestParameter(ERROR_DESCRIPTION_FIELD_NAME);
} }
} }

View File

@ -49,14 +49,14 @@ public class CustomOidcAuthenticator implements Authenticator<OidcCredentials> {
protected OidcConfiguration configuration; protected OidcConfiguration configuration;
protected OidcClient client; protected OidcClient<OidcConfiguration> client;
private ClientAuthentication clientAuthentication; private final ClientAuthentication clientAuthentication;
public CustomOidcAuthenticator(final OidcConfiguration configuration, final OidcClient client) { public CustomOidcAuthenticator(final OidcClient<OidcConfiguration> client) {
CommonHelper.assertNotNull("configuration", configuration); CommonHelper.assertNotNull("configuration", client.getConfiguration());
CommonHelper.assertNotNull("client", client); CommonHelper.assertNotNull("client", client);
this.configuration = configuration; this.configuration = client.getConfiguration();
this.client = client; this.client = client;
// check authentication methods // check authentication methods

View File

@ -5,11 +5,10 @@ import org.pac4j.oidc.client.OidcClient;
import org.pac4j.oidc.config.OidcConfiguration; import org.pac4j.oidc.config.OidcConfiguration;
import org.pac4j.oidc.credentials.extractor.OidcExtractor; import org.pac4j.oidc.credentials.extractor.OidcExtractor;
import org.pac4j.oidc.logout.OidcLogoutActionBuilder; import org.pac4j.oidc.logout.OidcLogoutActionBuilder;
import org.pac4j.oidc.profile.OidcProfile;
import org.pac4j.oidc.profile.creator.OidcProfileCreator; import org.pac4j.oidc.profile.creator.OidcProfileCreator;
import org.pac4j.oidc.redirect.OidcRedirectActionBuilder; import org.pac4j.oidc.redirect.OidcRedirectionActionBuilder;
public class CustomOidcClient extends OidcClient<OidcProfile, OidcConfiguration> { public class CustomOidcClient extends OidcClient<OidcConfiguration> {
public CustomOidcClient(final OidcConfiguration configuration) { public CustomOidcClient(final OidcConfiguration configuration) {
setConfiguration(configuration); setConfiguration(configuration);
@ -19,10 +18,10 @@ public class CustomOidcClient extends OidcClient<OidcProfile, OidcConfiguration>
protected void clientInit() { protected void clientInit() {
CommonHelper.assertNotNull("configuration", getConfiguration()); CommonHelper.assertNotNull("configuration", getConfiguration());
getConfiguration().init(); getConfiguration().init();
defaultRedirectActionBuilder(new OidcRedirectActionBuilder(getConfiguration(), this)); defaultRedirectionActionBuilder(new OidcRedirectionActionBuilder(getConfiguration(), this));
defaultCredentialsExtractor(new OidcExtractor(getConfiguration(), this)); defaultCredentialsExtractor(new OidcExtractor(getConfiguration(), this));
defaultAuthenticator(new CustomOidcAuthenticator(getConfiguration(), this)); defaultAuthenticator(new CustomOidcAuthenticator(this));
defaultProfileCreator(new OidcProfileCreator<>(getConfiguration())); defaultProfileCreator(new OidcProfileCreator<>(getConfiguration(), this));
defaultLogoutActionBuilder(new OidcLogoutActionBuilder<>(getConfiguration())); defaultLogoutActionBuilder(new OidcLogoutActionBuilder(getConfiguration()));
} }
} }

View File

@ -15,7 +15,8 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import play.api.Play;
import play.Environment;
import play.http.HttpEntity; import play.http.HttpEntity;
import play.libs.ws.InMemoryBodyWritable; import play.libs.ws.InMemoryBodyWritable;
import play.libs.ws.StandaloneWSClient; import play.libs.ws.StandaloneWSClient;
@ -37,18 +38,20 @@ import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClientConfig;
import utils.ConfigUtil; import utils.ConfigUtil;
import java.time.Duration; import java.time.Duration;
import static auth.AuthUtils.*; import static auth.AuthUtils.ACTOR;
import static auth.AuthUtils.SESSION_COOKIE_GMS_TOKEN_NAME;
public class Application extends Controller { public class Application extends Controller {
private final Config _config; private final Config _config;
private final StandaloneWSClient _ws; private final StandaloneWSClient _ws;
private final Environment _environment;
@Inject @Inject
public Application(@Nonnull Config config) { public Application(Environment environment, @Nonnull Config config) {
_config = config; _config = config;
_ws = createWsClient(); _ws = createWsClient();
_environment = environment;
} }
/** /**
@ -60,9 +63,10 @@ public class Application extends Controller {
*/ */
@Nonnull @Nonnull
private Result serveAsset(@Nullable String path) { private Result serveAsset(@Nullable String path) {
InputStream indexHtml = Play.current().classloader().getResourceAsStream("public/index.html"); InputStream indexHtml = _environment.resourceAsStream("public/index.html");
response().setHeader("Cache-Control", "no-cache"); return ok(indexHtml)
return ok(indexHtml).as("text/html"); .withHeader("Cache-Control", "no-cache")
.as("text/html");
} }
@Nonnull @Nonnull
@ -87,9 +91,9 @@ public class Application extends Controller {
* TODO: Investigate using mutual SSL authentication to call Metadata Service. * TODO: Investigate using mutual SSL authentication to call Metadata Service.
*/ */
@Security.Authenticated(Authenticator.class) @Security.Authenticated(Authenticator.class)
public CompletableFuture<Result> proxy(String path) throws ExecutionException, InterruptedException { public CompletableFuture<Result> proxy(String path, Http.Request request) throws ExecutionException, InterruptedException {
final String authorizationHeaderValue = getAuthorizationHeaderValueToProxy(); final String authorizationHeaderValue = getAuthorizationHeaderValueToProxy(request);
final String resolvedUri = mapPath(request().uri()); final String resolvedUri = mapPath(request.uri());
final String metadataServiceHost = ConfigUtil.getString( final String metadataServiceHost = ConfigUtil.getString(
_config, _config,
@ -108,14 +112,14 @@ public class Application extends Controller {
// TODO: Fully support custom internal SSL. // TODO: Fully support custom internal SSL.
final String protocol = metadataServiceUseSsl ? "https" : "http"; final String protocol = metadataServiceUseSsl ? "https" : "http";
final Map<String, List<String>> headers = request().getHeaders().toMap(); final Map<String, List<String>> headers = request.getHeaders().toMap();
if (headers.containsKey(Http.HeaderNames.HOST) && !headers.containsKey(Http.HeaderNames.X_FORWARDED_HOST)) { if (headers.containsKey(Http.HeaderNames.HOST) && !headers.containsKey(Http.HeaderNames.X_FORWARDED_HOST)) {
headers.put(Http.HeaderNames.X_FORWARDED_HOST, headers.get(Http.HeaderNames.HOST)); headers.put(Http.HeaderNames.X_FORWARDED_HOST, headers.get(Http.HeaderNames.HOST));
} }
return _ws.url(String.format("%s://%s:%s%s", protocol, metadataServiceHost, metadataServicePort, resolvedUri)) return _ws.url(String.format("%s://%s:%s%s", protocol, metadataServiceHost, metadataServicePort, resolvedUri))
.setMethod(request().method()) .setMethod(request.method())
.setHeaders(headers .setHeaders(headers
.entrySet() .entrySet()
.stream() .stream()
@ -129,8 +133,8 @@ public class Application extends Controller {
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)) .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))
) )
.addHeader(Http.HeaderNames.AUTHORIZATION, authorizationHeaderValue) .addHeader(Http.HeaderNames.AUTHORIZATION, authorizationHeaderValue)
.addHeader(AuthenticationConstants.LEGACY_X_DATAHUB_ACTOR_HEADER, getDataHubActorHeader()) .addHeader(AuthenticationConstants.LEGACY_X_DATAHUB_ACTOR_HEADER, getDataHubActorHeader(request))
.setBody(new InMemoryBodyWritable(ByteString.fromByteBuffer(request().body().asBytes().asByteBuffer()), "application/json")) .setBody(new InMemoryBodyWritable(ByteString.fromByteBuffer(request.body().asBytes().asByteBuffer()), "application/json"))
.setRequestTimeout(Duration.ofSeconds(120)) .setRequestTimeout(Duration.ofSeconds(120))
.execute() .execute()
.thenApply(apiResponse -> { .thenApply(apiResponse -> {
@ -272,14 +276,14 @@ public class Application extends Controller {
* *
* If neither are found, an empty string is returned. * If neither are found, an empty string is returned.
*/ */
private String getAuthorizationHeaderValueToProxy() { private String getAuthorizationHeaderValueToProxy(Http.Request request) {
// If the session cookie has an authorization token, use that. If there's an authorization header provided, simply // If the session cookie has an authorization token, use that. If there's an authorization header provided, simply
// use that. // use that.
String value = ""; String value = "";
if (ctx().session().containsKey(SESSION_COOKIE_GMS_TOKEN_NAME)) { if (request.session().data().containsKey(SESSION_COOKIE_GMS_TOKEN_NAME)) {
value = "Bearer " + ctx().session().get(SESSION_COOKIE_GMS_TOKEN_NAME); value = "Bearer " + request.session().data().get(SESSION_COOKIE_GMS_TOKEN_NAME);
} else if (request().getHeaders().contains(Http.HeaderNames.AUTHORIZATION)) { } else if (request.getHeaders().contains(Http.HeaderNames.AUTHORIZATION)) {
value = request().getHeaders().get(Http.HeaderNames.AUTHORIZATION).get(); value = request.getHeaders().get(Http.HeaderNames.AUTHORIZATION).get();
} }
return value; return value;
} }
@ -291,8 +295,8 @@ public class Application extends Controller {
* If Metadata Service authentication is enabled, this value is not required because Actor context will most often come * If Metadata Service authentication is enabled, this value is not required because Actor context will most often come
* from the authentication credentials provided in the Authorization header. * from the authentication credentials provided in the Authorization header.
*/ */
private String getDataHubActorHeader() { private String getDataHubActorHeader(Http.Request request) {
String actor = ctx().session().get(ACTOR); String actor = request.session().data().get(ACTOR);
return actor == null ? "" : actor; return actor == null ? "" : actor;
} }

View File

@ -10,8 +10,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.linkedin.common.urn.CorpuserUrn; import com.linkedin.common.urn.CorpuserUrn;
import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.Urn;
import com.typesafe.config.Config; import com.typesafe.config.Config;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -19,20 +19,34 @@ import javax.annotation.Nonnull;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.pac4j.core.client.Client; import org.pac4j.core.client.Client;
import org.pac4j.core.context.session.SessionStore; import org.pac4j.core.exception.http.RedirectionAction;
import org.pac4j.core.exception.HttpAction;
import org.pac4j.play.PlayWebContext; import org.pac4j.play.PlayWebContext;
import org.pac4j.play.http.PlayHttpActionAdapter; import org.pac4j.play.http.PlayHttpActionAdapter;
import org.pac4j.play.store.PlaySessionStore;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import play.libs.Json; import play.libs.Json;
import play.mvc.Controller; import play.mvc.Controller;
import play.mvc.Http; import play.mvc.Http;
import play.mvc.Result; import play.mvc.Result;
import play.mvc.Results;
import security.AuthenticationManager; import security.AuthenticationManager;
import static auth.AuthUtils.*; import static auth.AuthUtils.ACCESS_TOKEN;
import static org.pac4j.core.client.IndirectClient.*; import static auth.AuthUtils.ACTOR;
import static auth.AuthUtils.DEFAULT_ACTOR_URN;
import static auth.AuthUtils.DEFAULT_SESSION_TTL_HOURS;
import static auth.AuthUtils.EMAIL;
import static auth.AuthUtils.FULL_NAME;
import static auth.AuthUtils.INVITE_TOKEN;
import static auth.AuthUtils.LOGIN_ROUTE;
import static auth.AuthUtils.PASSWORD;
import static auth.AuthUtils.RESET_TOKEN;
import static auth.AuthUtils.SESSION_TTL_CONFIG_PATH;
import static auth.AuthUtils.TITLE;
import static auth.AuthUtils.USER_NAME;
import static auth.AuthUtils.createActorCookie;
import static org.pac4j.core.client.IndirectClient.ATTEMPTED_AUTHENTICATION_SUFFIX;
// TODO add logging. // TODO add logging.
@ -42,6 +56,8 @@ public class AuthenticationController extends Controller {
private static final String ERROR_MESSAGE_URI_PARAM = "error_msg"; private static final String ERROR_MESSAGE_URI_PARAM = "error_msg";
private static final String SSO_DISABLED_ERROR_MESSAGE = "SSO is not configured"; private static final String SSO_DISABLED_ERROR_MESSAGE = "SSO is not configured";
private static final String SSO_NO_REDIRECT_MESSAGE = "SSO is configured, however missing redirect from idp";
private final Logger _logger = LoggerFactory.getLogger(AuthenticationController.class.getName()); private final Logger _logger = LoggerFactory.getLogger(AuthenticationController.class.getName());
private final Config _configs; private final Config _configs;
private final JAASConfigs _jaasConfigs; private final JAASConfigs _jaasConfigs;
@ -51,7 +67,7 @@ public class AuthenticationController extends Controller {
private org.pac4j.core.config.Config _ssoConfig; private org.pac4j.core.config.Config _ssoConfig;
@Inject @Inject
private SessionStore _playSessionStore; private PlaySessionStore _playSessionStore;
@Inject @Inject
private SsoManager _ssoManager; private SsoManager _ssoManager;
@ -80,25 +96,27 @@ public class AuthenticationController extends Controller {
final Optional<String> maybeRedirectPath = Optional.ofNullable(request.getQueryString(AUTH_REDIRECT_URI_PARAM)); final Optional<String> maybeRedirectPath = Optional.ofNullable(request.getQueryString(AUTH_REDIRECT_URI_PARAM));
final String redirectPath = maybeRedirectPath.orElse("/"); final String redirectPath = maybeRedirectPath.orElse("/");
if (AuthUtils.hasValidSessionCookie(ctx())) { if (AuthUtils.hasValidSessionCookie(request)) {
return redirect(redirectPath); return Results.redirect(redirectPath);
} }
// 1. If SSO is enabled, redirect to IdP if not authenticated. // 1. If SSO is enabled, redirect to IdP if not authenticated.
if (_ssoManager.isSsoEnabled()) { if (_ssoManager.isSsoEnabled()) {
return redirectToIdentityProvider(); return redirectToIdentityProvider(request).orElse(
Results.redirect(LOGIN_ROUTE + String.format("?%s=%s", ERROR_MESSAGE_URI_PARAM, SSO_NO_REDIRECT_MESSAGE))
);
} }
// 2. If either JAAS auth or Native auth is enabled, fallback to it // 2. If either JAAS auth or Native auth is enabled, fallback to it
if (_jaasConfigs.isJAASEnabled() || _nativeAuthenticationConfigs.isNativeAuthenticationEnabled()) { if (_jaasConfigs.isJAASEnabled() || _nativeAuthenticationConfigs.isNativeAuthenticationEnabled()) {
return redirect( return Results.redirect(
LOGIN_ROUTE + String.format("?%s=%s", AUTH_REDIRECT_URI_PARAM, encodeRedirectUri(redirectPath))); LOGIN_ROUTE + String.format("?%s=%s", AUTH_REDIRECT_URI_PARAM, encodeRedirectUri(redirectPath)));
} }
// 3. If no auth enabled, fallback to using default user account & redirect. // 3. If no auth enabled, fallback to using default user account & redirect.
// Generate GMS session token, TODO: // Generate GMS session token, TODO:
final String accessToken = _authClient.generateSessionTokenForUser(DEFAULT_ACTOR_URN.getId()); final String accessToken = _authClient.generateSessionTokenForUser(DEFAULT_ACTOR_URN.getId());
return redirect(redirectPath).withSession(createSessionMap(DEFAULT_ACTOR_URN.toString(), accessToken)) return Results.redirect(redirectPath).withSession(createSessionMap(DEFAULT_ACTOR_URN.toString(), accessToken))
.withCookies(createActorCookie(DEFAULT_ACTOR_URN.toString(), .withCookies(createActorCookie(DEFAULT_ACTOR_URN.toString(),
_configs.hasPath(SESSION_TTL_CONFIG_PATH) ? _configs.getInt(SESSION_TTL_CONFIG_PATH) _configs.hasPath(SESSION_TTL_CONFIG_PATH) ? _configs.getInt(SESSION_TTL_CONFIG_PATH)
: DEFAULT_SESSION_TTL_HOURS)); : DEFAULT_SESSION_TTL_HOURS));
@ -108,11 +126,13 @@ public class AuthenticationController extends Controller {
* Redirect to the identity provider for authentication. * Redirect to the identity provider for authentication.
*/ */
@Nonnull @Nonnull
public Result sso() { public Result sso(Http.Request request) {
if (_ssoManager.isSsoEnabled()) { if (_ssoManager.isSsoEnabled()) {
return redirectToIdentityProvider(); return redirectToIdentityProvider(request).orElse(
Results.redirect(LOGIN_ROUTE + String.format("?%s=%s", ERROR_MESSAGE_URI_PARAM, SSO_NO_REDIRECT_MESSAGE))
);
} }
return redirect(LOGIN_ROUTE + String.format("?%s=%s", ERROR_MESSAGE_URI_PARAM, SSO_DISABLED_ERROR_MESSAGE)); return Results.redirect(LOGIN_ROUTE + String.format("?%s=%s", ERROR_MESSAGE_URI_PARAM, SSO_DISABLED_ERROR_MESSAGE));
} }
/** /**
@ -131,7 +151,7 @@ public class AuthenticationController extends Controller {
String message = "Neither JAAS nor native authentication is enabled on the server."; String message = "Neither JAAS nor native authentication is enabled on the server.";
final ObjectNode error = Json.newObject(); final ObjectNode error = Json.newObject();
error.put("message", message); error.put("message", message);
return badRequest(error); return Results.badRequest(error);
} }
final JsonNode json = request.body().asJson(); final JsonNode json = request.body().asJson();
@ -140,14 +160,14 @@ public class AuthenticationController extends Controller {
if (StringUtils.isBlank(username)) { if (StringUtils.isBlank(username)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "User name must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "User name must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
JsonNode invalidCredsJson = Json.newObject().put("message", "Invalid Credentials"); JsonNode invalidCredsJson = Json.newObject().put("message", "Invalid Credentials");
boolean loginSucceeded = tryLogin(username, password); boolean loginSucceeded = tryLogin(username, password);
if (!loginSucceeded) { if (!loginSucceeded) {
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
final Urn actorUrn = new CorpuserUrn(username); final Urn actorUrn = new CorpuserUrn(username);
@ -167,7 +187,7 @@ public class AuthenticationController extends Controller {
String message = "Native authentication is not enabled on the server."; String message = "Native authentication is not enabled on the server.";
final ObjectNode error = Json.newObject(); final ObjectNode error = Json.newObject();
error.put("message", message); error.put("message", message);
return badRequest(error); return Results.badRequest(error);
} }
final JsonNode json = request.body().asJson(); final JsonNode json = request.body().asJson();
@ -179,27 +199,27 @@ public class AuthenticationController extends Controller {
if (StringUtils.isBlank(fullName)) { if (StringUtils.isBlank(fullName)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Full name must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Full name must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
if (StringUtils.isBlank(email)) { if (StringUtils.isBlank(email)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Email must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Email must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
if (StringUtils.isBlank(password)) { if (StringUtils.isBlank(password)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Password must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Password must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
if (StringUtils.isBlank(title)) { if (StringUtils.isBlank(title)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Title must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Title must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
if (StringUtils.isBlank(inviteToken)) { if (StringUtils.isBlank(inviteToken)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Invite token must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Invite token must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
final Urn userUrn = new CorpuserUrn(email); final Urn userUrn = new CorpuserUrn(email);
@ -231,17 +251,17 @@ public class AuthenticationController extends Controller {
if (StringUtils.isBlank(email)) { if (StringUtils.isBlank(email)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Email must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Email must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
if (StringUtils.isBlank(password)) { if (StringUtils.isBlank(password)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Password must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Password must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
if (StringUtils.isBlank(resetToken)) { if (StringUtils.isBlank(resetToken)) {
JsonNode invalidCredsJson = Json.newObject().put("message", "Reset token must not be empty."); JsonNode invalidCredsJson = Json.newObject().put("message", "Reset token must not be empty.");
return badRequest(invalidCredsJson); return Results.badRequest(invalidCredsJson);
} }
final Urn userUrn = new CorpuserUrn(email); final Urn userUrn = new CorpuserUrn(email);
@ -251,9 +271,9 @@ public class AuthenticationController extends Controller {
return createSession(userUrnString, accessToken); return createSession(userUrnString, accessToken);
} }
private Result redirectToIdentityProvider() { private Optional<Result> redirectToIdentityProvider(Http.RequestHeader request) {
final PlayWebContext playWebContext = new PlayWebContext(ctx(), _playSessionStore); final PlayWebContext playWebContext = new PlayWebContext(request, _playSessionStore);
final Client<?, ?> client = _ssoManager.getSsoProvider().client(); final Client client = _ssoManager.getSsoProvider().client();
// This is to prevent previous login attempts from being cached. // This is to prevent previous login attempts from being cached.
// We replicate the logic here, which is buried in the Pac4j client. // We replicate the logic here, which is buried in the Pac4j client.
@ -263,23 +283,19 @@ public class AuthenticationController extends Controller {
} }
try { try {
final HttpAction action = client.redirect(playWebContext); final Optional<RedirectionAction> action = client.getRedirectionAction(playWebContext);
return new PlayHttpActionAdapter().adapt(action.getCode(), playWebContext); return action.map(act -> new PlayHttpActionAdapter().adapt(act, playWebContext));
} catch (Exception e) { } catch (Exception e) {
_logger.error("Caught exception while attempting to redirect to SSO identity provider! It's likely that SSO integration is mis-configured", e); _logger.error("Caught exception while attempting to redirect to SSO identity provider! It's likely that SSO integration is mis-configured", e);
return redirect( return Optional.of(Results.redirect(
String.format("/login?error_msg=%s", String.format("/login?error_msg=%s",
URLEncoder.encode("Failed to redirect to Single Sign-On provider. Please contact your DataHub Administrator, " URLEncoder.encode("Failed to redirect to Single Sign-On provider. Please contact your DataHub Administrator, "
+ "or refer to server logs for more information."))); + "or refer to server logs for more information.", StandardCharsets.UTF_8))));
} }
} }
private String encodeRedirectUri(final String redirectUri) { private String encodeRedirectUri(final String redirectUri) {
try { return URLEncoder.encode(redirectUri, StandardCharsets.UTF_8);
return URLEncoder.encode(redirectUri, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(String.format("Failed to encode redirect URI %s", redirectUri), e);
}
} }
private boolean tryLogin(String username, String password) { private boolean tryLogin(String username, String password) {
@ -310,7 +326,7 @@ public class AuthenticationController extends Controller {
private Result createSession(String userUrnString, String accessToken) { private Result createSession(String userUrnString, String accessToken) {
int ttlInHours = _configs.hasPath(SESSION_TTL_CONFIG_PATH) ? _configs.getInt(SESSION_TTL_CONFIG_PATH) int ttlInHours = _configs.hasPath(SESSION_TTL_CONFIG_PATH) ? _configs.getInt(SESSION_TTL_CONFIG_PATH)
: DEFAULT_SESSION_TTL_HOURS; : DEFAULT_SESSION_TTL_HOURS;
return ok().withSession(createSessionMap(userUrnString, accessToken)) return Results.ok().withSession(createSessionMap(userUrnString, accessToken))
.withCookies(createActorCookie(userUrnString, ttlInHours)); .withCookies(createActorCookie(userUrnString, ttlInHours));
} }

View File

@ -4,10 +4,12 @@ import com.typesafe.config.Config;
import java.net.URLEncoder; import java.net.URLEncoder;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.pac4j.play.LogoutController; import org.pac4j.play.LogoutController;
import play.mvc.Http;
import play.mvc.Result; import play.mvc.Result;
import play.mvc.Results;
import javax.inject.Inject; import javax.inject.Inject;
import java.util.concurrent.ExecutionException; import java.nio.charset.StandardCharsets;
/** /**
* Responsible for handling logout logic with oidc providers * Responsible for handling logout logic with oidc providers
@ -31,18 +33,20 @@ public class CentralLogoutController extends LogoutController {
/** /**
* logout() method should not be called if oidc is not enabled * logout() method should not be called if oidc is not enabled
*/ */
public Result executeLogout() throws ExecutionException, InterruptedException { public Result executeLogout(Http.Request request) {
if (_isOidcEnabled) { if (_isOidcEnabled) {
try { try {
return logout().toCompletableFuture().get().withNewSession(); return Results.redirect(DEFAULT_BASE_URL_PATH)
.removingFromSession(request);
} catch (Exception e) { } catch (Exception e) {
log.error("Caught exception while attempting to perform SSO logout! It's likely that SSO integration is mis-configured.", e); log.error("Caught exception while attempting to perform SSO logout! It's likely that SSO integration is mis-configured.", e);
return redirect( return redirect(
String.format("/login?error_msg=%s", String.format("/login?error_msg=%s",
URLEncoder.encode("Failed to sign out using Single Sign-On provider. Please contact your DataHub Administrator, " URLEncoder.encode("Failed to sign out using Single Sign-On provider. Please contact your DataHub Administrator, "
+ "or refer to server logs for more information."))); + "or refer to server logs for more information.", StandardCharsets.UTF_8)));
} }
} }
return redirect(DEFAULT_BASE_URL_PATH).withNewSession(); return Results.redirect(DEFAULT_BASE_URL_PATH)
.withNewSession();
} }
} }

View File

@ -4,6 +4,7 @@ import client.AuthServiceClient;
import com.datahub.authentication.Authentication; import com.datahub.authentication.Authentication;
import com.linkedin.entity.client.EntityClient; import com.linkedin.entity.client.EntityClient;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage; import java.util.concurrent.CompletionStage;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
@ -14,10 +15,12 @@ import org.pac4j.core.engine.CallbackLogic;
import org.pac4j.core.http.adapter.HttpActionAdapter; import org.pac4j.core.http.adapter.HttpActionAdapter;
import org.pac4j.play.CallbackController; import org.pac4j.play.CallbackController;
import org.pac4j.play.PlayWebContext; import org.pac4j.play.PlayWebContext;
import play.mvc.Http;
import play.mvc.Result; import play.mvc.Result;
import auth.sso.oidc.OidcCallbackLogic; import auth.sso.oidc.OidcCallbackLogic;
import auth.sso.SsoManager; import auth.sso.SsoManager;
import auth.sso.SsoProvider; import auth.sso.SsoProvider;
import play.mvc.Results;
/** /**
@ -44,21 +47,21 @@ public class SsoCallbackController extends CallbackController {
setCallbackLogic(new SsoCallbackLogic(ssoManager, systemAuthentication, entityClient, authClient)); setCallbackLogic(new SsoCallbackLogic(ssoManager, systemAuthentication, entityClient, authClient));
} }
public CompletionStage<Result> handleCallback(String protocol) { public CompletionStage<Result> handleCallback(String protocol, Http.Request request) {
if (shouldHandleCallback(protocol)) { if (shouldHandleCallback(protocol)) {
log.debug(String.format("Handling SSO callback. Protocol: %s", protocol)); log.debug(String.format("Handling SSO callback. Protocol: %s", protocol));
return callback().handle((res, e) -> { return callback(request).handle((res, e) -> {
if (e != null) { if (e != null) {
log.error("Caught exception while attempting to handle SSO callback! It's likely that SSO integration is mis-configured.", e); log.error("Caught exception while attempting to handle SSO callback! It's likely that SSO integration is mis-configured.", e);
return redirect( return Results.redirect(
String.format("/login?error_msg=%s", String.format("/login?error_msg=%s",
URLEncoder.encode("Failed to sign in using Single Sign-On provider. Please contact your DataHub Administrator, " URLEncoder.encode("Failed to sign in using Single Sign-On provider. Please contact your DataHub Administrator, "
+ "or refer to server logs for more information."))); + "or refer to server logs for more information.", StandardCharsets.UTF_8)));
} }
return res; return res;
}); });
} }
return CompletableFuture.completedFuture(internalServerError( return CompletableFuture.completedFuture(Results.internalServerError(
String.format("Failed to perform SSO callback. SSO is not enabled for protocol: %s", protocol))); String.format("Failed to perform SSO callback. SSO is not enabled for protocol: %s", protocol)));
} }

View File

@ -74,7 +74,7 @@ public class TrackingController extends Controller {
} catch (Exception e) { } catch (Exception e) {
return badRequest(); return badRequest();
} }
final String actor = ctx().session().get(ACTOR); final String actor = request.session().data().get(ACTOR);
try { try {
_logger.debug(String.format("Emitting product analytics event. actor: %s, event: %s", actor, event)); _logger.debug(String.format("Emitting product analytics event. actor: %s, event: %s", actor, event));
final ProducerRecord<String, String> record = new ProducerRecord<>( final ProducerRecord<String, String> record = new ProducerRecord<>(

View File

@ -2,7 +2,6 @@ package server
import play.api.Logger import play.api.Logger
import play.core.server.AkkaHttpServer import play.core.server.AkkaHttpServer
import play.core.server.AkkaHttpServerProvider
import play.core.server.ServerProvider import play.core.server.ServerProvider
import akka.http.scaladsl.settings.ParserSettings import akka.http.scaladsl.settings.ParserSettings

View File

@ -1,7 +1,7 @@
plugins { plugins {
id "io.github.kobylynskyi.graphql.codegen" version "4.1.1" id "io.github.kobylynskyi.graphql.codegen" version "4.1.1"
} }
apply plugin: 'java' apply plugin: 'scala'
apply from: './play.gradle' apply from: './play.gradle'

View File

@ -19,25 +19,39 @@ play.application.loader = play.inject.guice.GuiceApplicationLoader
# Play http parser settings # Play http parser settings
# # # #
# # Increase default buffer size to handle large post request # # Increase default buffer size to handle large post request
play.http.parser.maxMemoryBuffer = ${DATAHUB_PLAY_MEM_BUFFER_SIZE} play.http.parser.maxMemoryBuffer = 10MB
play.http.parser.maxMemoryBuffer = ${?DATAHUB_PLAY_MEM_BUFFER_SIZE}
# TODO: Disable legacy URL encoding eventually # TODO: Disable legacy URL encoding eventually
play.modules.disabled += "play.api.mvc.CookiesModule" play.modules.disabled += "play.api.mvc.CookiesModule"
play.modules.enabled += "play.api.mvc.LegacyCookiesModule" play.modules.enabled += "play.api.mvc.LegacyCookiesModule"
play.modules.enabled += "auth.AuthModule" play.modules.enabled += "auth.AuthModule"
# Legacy Configuration to avoid code changes, update to modern approaches eventually
play.allowHttpContext = true
play.allowGlobalApplication = true
# We override the Akka server provider to allow setting the max header count to a higher value # We override the Akka server provider to allow setting the max header count to a higher value
# This is useful while using proxies like Envoy that result in the frontend server rejecting GMS # This is useful while using proxies like Envoy that result in the frontend server rejecting GMS
# responses as there's more than the max of 64 allowed headers # responses as there's more than the max of 64 allowed headers
play.server.provider = server.CustomAkkaHttpServerProvider play.server.provider = server.CustomAkkaHttpServerProvider
play.http.server.akka.max-header-count = 64 play.http.server.akka.max-header-count = 64
play.http.server.akka.max-header-count = ${?DATAHUB_AKKA_MAX_HEADER_COUNT} play.http.server.akka.max-header-count = ${?DATAHUB_AKKA_MAX_HEADER_COUNT}
play.server.akka.max-header-value-length = 8k play.server.akka.max-header-size = 8k
play.server.akka.max-header-value-length = ${?DATAHUB_AKKA_MAX_HEADER_VALUE_LENGTH} play.server.akka.max-header-size = ${?DATAHUB_AKKA_MAX_HEADER_VALUE_LENGTH}
play.filters {
enabled = [
play.filters.gzip.GzipFilter
]
gzip {
contentType {
# If non empty, then a response will only be compressed if its content type is in this list.
whiteList = [ "text/*", "application/javascript", "application/json" ]
# The black list is only used if the white list is empty.
# Compress all responses except the ones whose content type is in this list.
blackList = []
}
}
}
# pac4j configuration # pac4j configuration
# default to PlayCookieSessionStore to keep datahub-frontend's statelessness # default to PlayCookieSessionStore to keep datahub-frontend's statelessness
@ -62,7 +76,8 @@ pac4j.sessionStore.provider= ${?PAC4J_SESSIONSTORE_PROVIDER}
# You can disable evolutions for a specific datasource if necessary # You can disable evolutions for a specific datasource if necessary
# play.evolutions.db.default.enabled=false # play.evolutions.db.default.enabled=false
app.version = ${DATAHUB_APP_VERSION} app.version = 0.0.0
app.version = ${?DATAHUB_APP_VERSION}
dataset.hdfs_browser.link = "" dataset.hdfs_browser.link = ""
linkedin.internal = false linkedin.internal = false
@ -174,7 +189,8 @@ analytics.enabled = ${?DATAHUB_ANALYTICS_ENABLED}
# Kafka Producer Configuration # Kafka Producer Configuration
analytics.kafka.bootstrap.server = ${KAFKA_BOOTSTRAP_SERVER} analytics.kafka.bootstrap.server = ${KAFKA_BOOTSTRAP_SERVER}
analytics.tracking.topic = ${DATAHUB_TRACKING_TOPIC} analytics.tracking.topic = DataHubUsageEvent_v1
analytics.tracking.topic = ${?DATAHUB_TRACKING_TOPIC}
# Kafka Producer SSL Configs. All must be provided to enable SSL. # Kafka Producer SSL Configs. All must be provided to enable SSL.
analytics.kafka.security.protocol = ${?KAFKA_PROPERTIES_SECURITY_PROTOCOL} analytics.kafka.security.protocol = ${?KAFKA_PROPERTIES_SECURITY_PROTOCOL}
@ -194,8 +210,8 @@ analytics.kafka.sasl.kerberos.service.name = ${?KAFKA_PROPERTIES_SASL_KERBEROS_S
analytics.kafka.sasl.login.callback.handler.class = ${?KAFKA_PROPERTIES_SASL_LOGIN_CALLBACK_HANDLER_CLASS} analytics.kafka.sasl.login.callback.handler.class = ${?KAFKA_PROPERTIES_SASL_LOGIN_CALLBACK_HANDLER_CLASS}
# Required Elastic Client Configuration # Required Elastic Client Configuration
analytics.elastic.host = ${ELASTIC_CLIENT_HOST} analytics.elastic.host = ${?ELASTIC_CLIENT_HOST}
analytics.elastic.port = ${ELASTIC_CLIENT_PORT} analytics.elastic.port = ${?ELASTIC_CLIENT_PORT}
# Optional Elastic Client Configurations # Optional Elastic Client Configurations
analytics.elastic.threadCount = ${?ELASTIC_CLIENT_THREAD_COUNT} analytics.elastic.threadCount = ${?ELASTIC_CLIENT_THREAD_COUNT}

View File

@ -7,37 +7,38 @@
GET / controllers.Application.index(path="index.html") GET / controllers.Application.index(path="index.html")
GET /admin controllers.Application.healthcheck() GET /admin controllers.Application.healthcheck()
GET /health controllers.Application.healthcheck()
GET /config controllers.Application.appConfig() GET /config controllers.Application.appConfig()
# Routes used exclusively by the React application. # Routes used exclusively by the React application.
# Authentication in React # Authentication in React
GET /authenticate controllers.AuthenticationController.authenticate(request: Request) GET /authenticate controllers.AuthenticationController.authenticate(request: Request)
GET /sso controllers.AuthenticationController.sso() GET /sso controllers.AuthenticationController.sso(request: Request)
POST /logIn controllers.AuthenticationController.logIn(request: Request) POST /logIn controllers.AuthenticationController.logIn(request: Request)
POST /signUp controllers.AuthenticationController.signUp(request: Request) POST /signUp controllers.AuthenticationController.signUp(request: Request)
POST /resetNativeUserCredentials controllers.AuthenticationController.resetNativeUserCredentials(request: Request) POST /resetNativeUserCredentials controllers.AuthenticationController.resetNativeUserCredentials(request: Request)
GET /callback/:protocol controllers.SsoCallbackController.handleCallback(protocol: String) GET /callback/:protocol controllers.SsoCallbackController.handleCallback(protocol: String, request: Request)
POST /callback/:protocol controllers.SsoCallbackController.handleCallback(protocol: String) POST /callback/:protocol controllers.SsoCallbackController.handleCallback(protocol: String, request: Request)
GET /logOut controllers.CentralLogoutController.executeLogout() GET /logOut controllers.CentralLogoutController.executeLogout(request: Request)
# Proxies API requests to the metadata service api # Proxies API requests to the metadata service api
GET /api/*path controllers.Application.proxy(path) GET /api/*path controllers.Application.proxy(path: String, request: Request)
POST /api/*path controllers.Application.proxy(path) POST /api/*path controllers.Application.proxy(path: String, request: Request)
DELETE /api/*path controllers.Application.proxy(path) DELETE /api/*path controllers.Application.proxy(path: String, request: Request)
PUT /api/*path controllers.Application.proxy(path) PUT /api/*path controllers.Application.proxy(path: String, request: Request)
# Proxies API requests to the metadata service api # Proxies API requests to the metadata service api
GET /openapi/*path controllers.Application.proxy(path) GET /openapi/*path controllers.Application.proxy(path: String, request: Request)
POST /openapi/*path controllers.Application.proxy(path) POST /openapi/*path controllers.Application.proxy(path: String, request: Request)
DELETE /openapi/*path controllers.Application.proxy(path) DELETE /openapi/*path controllers.Application.proxy(path: String, request: Request)
PUT /openapi/*path controllers.Application.proxy(path) PUT /openapi/*path controllers.Application.proxy(path: String, request: Request)
# Map static resources from the /public folder to the /assets URL path # Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.at(path="/public", file) GET /assets/*file controllers.Assets.at(path="/public", file)
# Analytics route # Analytics route
POST /track controllers.TrackingController.track(request: Request) POST /track controllers.TrackingController.track(request: Request)
# Wildcard route accepts any routes and delegates to serveAsset which in turn serves the React Bundle # Wildcard route accepts any routes and delegates to serveAsset which in turn serves the React Bundle
GET /*path controllers.Application.index(path) GET /*path controllers.Application.index(path)

View File

@ -10,22 +10,28 @@ tasks.withType(PlayRun) {
configurations { configurations {
assets assets
play
} }
dependencies { dependencies {
assets project(path: ':datahub-web-react', configuration: 'assets') assets project(path: ':datahub-web-react', configuration: 'assets')
constraints { constraints {
play('org.springframework:spring-core:5.2.3.RELEASE') play(externalDependency.springCore)
play(externalDependency.springBeans)
play(externalDependency.springContext)
play(externalDependency.jacksonDataBind) play(externalDependency.jacksonDataBind)
play('com.nimbusds:nimbus-jose-jwt:7.9') play('com.nimbusds:oauth2-oidc-sdk:8.36.2')
play('com.typesafe.akka:akka-actor_2.12:2.5.16') play('com.nimbusds:nimbus-jose-jwt:8.18')
play('net.minidev:json-smart:2.4.1') play('com.typesafe.akka:akka-actor_2.12:2.6.20')
play('io.netty:netty-all:4.1.68.Final') play('net.minidev:json-smart:2.4.8')
play('io.netty:netty-all:4.1.85.Final')
} }
compile project(":metadata-service:restli-client") compile project(":metadata-service:restli-client")
compile project(":metadata-service:auth-api") compile project(":metadata-service:auth-api")
implementation externalDependency.jettyJaas implementation externalDependency.jettyJaas
implementation externalDependency.graphqlJava implementation externalDependency.graphqlJava
implementation externalDependency.antlr4Runtime implementation externalDependency.antlr4Runtime
@ -39,7 +45,8 @@ dependencies {
exclude group: "net.minidev", module: "json-smart" exclude group: "net.minidev", module: "json-smart"
exclude group: "com.nimbusds", module: "nimbus-jose-jwt" exclude group: "com.nimbusds", module: "nimbus-jose-jwt"
} }
implementation "com.nimbusds:nimbus-jose-jwt:7.9"
implementation 'com.nimbusds:nimbus-jose-jwt:8.18'
implementation externalDependency.jsonSmart implementation externalDependency.jsonSmart
implementation externalDependency.playPac4j implementation externalDependency.playPac4j
implementation externalDependency.shiroCore implementation externalDependency.shiroCore
@ -48,12 +55,16 @@ dependencies {
implementation externalDependency.playWs implementation externalDependency.playWs
implementation externalDependency.playServer implementation externalDependency.playServer
implementation externalDependency.playAkkaHttpServer implementation externalDependency.playAkkaHttpServer
implementation externalDependency.playFilters
implementation externalDependency.kafkaClients implementation externalDependency.kafkaClients
implementation externalDependency.awsMskIamAuth implementation externalDependency.awsMskIamAuth
testImplementation externalDependency.mockito testImplementation externalDependency.mockito
testImplementation externalDependency.playTest testImplementation externalDependency.playTest
testCompile externalDependency.testng testImplementation 'no.nav.security:mock-oauth2-server:0.3.1'
testImplementation 'org.junit-pioneer:junit-pioneer:1.9.1'
testImplementation externalDependency.junitJupiterApi
testRuntime externalDependency.junitJupiterEngine
implementation externalDependency.slf4jApi implementation externalDependency.slf4jApi
compileOnly externalDependency.lombok compileOnly externalDependency.lombok
@ -62,16 +73,19 @@ dependencies {
exclude group: 'com.typesafe.akka', module: 'akka-http-core_2.12' exclude group: 'com.typesafe.akka', module: 'akka-http-core_2.12'
} }
runtime externalDependency.playGuice runtime externalDependency.playGuice
implementation externalDependency.log4j2Api
implementation externalDependency.logbackClassic implementation externalDependency.logbackClassic
annotationProcessor externalDependency.lombok annotationProcessor externalDependency.lombok
} }
dist.dependsOn(':datahub-web-react:copyAssets') dist.dependsOn(':datahub-web-react:copyAssets')
test.dependsOn(':datahub-frontend:testResources')
play { play {
platform { platform {
playVersion = '2.7.6' playVersion = '2.8.18'
scalaVersion = '2.12' scalaVersion = '2.12'
javaVersion = JavaVersion.VERSION_11 javaVersion = JavaVersion.VERSION_11
} }
@ -82,7 +96,7 @@ play {
model { model {
components { components {
play { play {
platform play: '2.7.6', scala: '2.12', java: '11' platform play: '2.8.18', scala: '2.12', java: '11'
injectedRoutesGenerator = true injectedRoutesGenerator = true
binaries.all { binaries.all {
@ -99,6 +113,26 @@ model {
} }
} }
test {
useJUnitPlatform()
}
sourceSets {
test {
resources {
srcDirs = ['test/resources']
}
}
}
// minimal files for low-level tests
task testResources {
copy {
from '../datahub-web-react/public'
into 'test/resources/public'
}
}
task unzipAssets(type: Copy, dependsOn: [configurations.assets, ':datahub-web-react:yarnBuild']) { task unzipAssets(type: Copy, dependsOn: [configurations.assets, ':datahub-web-react:yarnBuild']) {
into "${buildDir}/assets" into "${buildDir}/assets"
from { from {
@ -121,4 +155,5 @@ clean {
delete 'public/logo.png' delete 'public/logo.png'
delete 'public/index.html' delete 'public/index.html'
delete 'public/favicon.ico' delete 'public/favicon.ico'
delete 'test/resources/public'
} }

View File

@ -1,11 +1,147 @@
package app; package app;
import org.junit.Test; import controllers.routes;
import no.nav.security.mock.oauth2.MockOAuth2Server;
import no.nav.security.mock.oauth2.token.DefaultOAuth2TokenCallback;
import okhttp3.mockwebserver.MockResponse;
import okhttp3.mockwebserver.MockWebServer;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
import org.junitpioneer.jupiter.SetEnvironmentVariable;
import org.openqa.selenium.Cookie;
import play.Application;
import play.Environment;
import play.Mode;
import play.inject.guice.GuiceApplicationBuilder;
import play.mvc.Http;
import play.mvc.Result;
import play.test.Helpers;
import play.test.TestBrowser;
import play.test.WithBrowser;
public class ApplicationTest { import java.io.IOException;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static play.mvc.Http.Status.NOT_FOUND;
import static play.mvc.Http.Status.OK;
import static play.test.Helpers.fakeRequest;
import static play.test.Helpers.route;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@SetEnvironmentVariable(key = "DATAHUB_SECRET", value = "test")
@SetEnvironmentVariable(key = "KAFKA_BOOTSTRAP_SERVER", value = "")
@SetEnvironmentVariable(key = "DATAHUB_ANALYTICS_ENABLED", value = "false")
@SetEnvironmentVariable(key = "AUTH_OIDC_ENABLED", value = "true")
@SetEnvironmentVariable(key = "AUTH_OIDC_JIT_PROVISIONING_ENABLED", value = "false")
@SetEnvironmentVariable(key = "AUTH_OIDC_CLIENT_ID", value = "testclient")
@SetEnvironmentVariable(key = "AUTH_OIDC_CLIENT_SECRET", value = "testsecret")
public class ApplicationTest extends WithBrowser {
private static final String ISSUER_ID = "testIssuer";
@Override
protected Application provideApplication() {
return new GuiceApplicationBuilder()
.configure("metadataService.port", String.valueOf(gmsServerPort()))
.configure("auth.baseUrl", "http://localhost:" + providePort())
.configure("auth.oidc.discoveryUri", "http://localhost:" + oauthServerPort()
+ "/testIssuer/.well-known/openid-configuration")
.in(new Environment(Mode.TEST)).build();
}
public int oauthServerPort() {
return providePort() + 1;
}
public int gmsServerPort() {
return providePort() + 2;
}
@Override
protected TestBrowser provideBrowser(int port) {
return Helpers.testBrowser(providePort());
}
private MockOAuth2Server _oauthServer;
private MockWebServer _gmsServer;
private String _wellKnownUrl;
@BeforeAll
public void init() throws IOException, InterruptedException {
_gmsServer = new MockWebServer();
_gmsServer.enqueue(new MockResponse().setBody("{\"value\":\"urn:li:corpuser:testUser@myCompany.com\"}"));
_gmsServer.enqueue(new MockResponse().setBody("{\"accessToken\":\"faketoken_YCpYIrjQH4sD3_rAc3VPPFg4\"}"));
_gmsServer.start(gmsServerPort());
_oauthServer = new MockOAuth2Server();
_oauthServer.enqueueCallback(
new DefaultOAuth2TokenCallback(ISSUER_ID, "testUser", List.of(), Map.of(
"email", "testUser@myCompany.com",
"groups", "myGroup"
), 600)
);
_oauthServer.start(InetAddress.getByName("localhost"), oauthServerPort());
// Discovery url to authorization server metadata
_wellKnownUrl = _oauthServer.wellKnownUrl(ISSUER_ID).toString();
startServer();
createBrowser();
Thread.sleep(5000);
}
@AfterAll
public void shutdown() throws IOException {
if (_gmsServer != null) {
_gmsServer.shutdown();
}
if (_oauthServer != null) {
_oauthServer.shutdown();
}
stopServer();
}
@Test @Test
public void renderTemplate() { public void testHealth() {
Http.RequestBuilder request = fakeRequest(routes.Application.healthcheck());
Result result = route(app, request);
assertEquals(OK, result.status());
} }
@Test
public void testIndex() {
Http.RequestBuilder request = fakeRequest(routes.Application.index(""));
Result result = route(app, request);
assertEquals(OK, result.status());
}
@Test
public void testIndexNotFound() {
Http.RequestBuilder request = fakeRequest(routes.Application.index("/other"));
Result result = route(app, request);
assertEquals(NOT_FOUND, result.status());
}
@Test
public void testOpenIdConfig() {
assertEquals("http://localhost:" + oauthServerPort()
+ "/testIssuer/.well-known/openid-configuration", _wellKnownUrl);
}
@Test
public void testHappyPathOidc() throws InterruptedException {
browser.goTo("/authenticate");
assertEquals("", browser.url());
Cookie actorCookie = browser.getCookie("actor");
assertEquals("urn:li:corpuser:testUser@myCompany.com", actorCookie.getValue());
}
} }

View File

@ -1,12 +1,13 @@
package security; package security;
import com.sun.security.auth.callback.TextCallbackHandler; import com.sun.security.auth.callback.TextCallbackHandler;
import org.junit.jupiter.api.Test;
import java.util.HashMap; import java.util.HashMap;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.security.auth.login.LoginException; import javax.security.auth.login.LoginException;
import org.testng.annotations.Test;
import static org.junit.Assert.*; import static org.junit.jupiter.api.Assertions.*;
public class DummyLoginModuleTest { public class DummyLoginModuleTest {
@ -17,10 +18,10 @@ public class DummyLoginModuleTest {
lmodule.initialize(new Subject(), new TextCallbackHandler(), null, new HashMap<>()); lmodule.initialize(new Subject(), new TextCallbackHandler(), null, new HashMap<>());
try { try {
assertTrue("Failed to login", lmodule.login()); assertTrue(lmodule.login(), "Failed to login");
assertTrue("Failed to logout", lmodule.logout()); assertTrue(lmodule.logout(), "Failed to logout");
assertTrue("Failed to commit", lmodule.commit()); assertTrue(lmodule.commit(), "Failed to commit");
assertTrue("Failed to abort", lmodule.abort()); assertTrue(lmodule.abort(), "Failed to abort");
} catch (LoginException e) { } catch (LoginException e) {
fail(e.toString()); fail(e.toString());
} }

View File

@ -19,11 +19,12 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.junit.jupiter.api.Test;
import org.pac4j.oidc.client.OidcClient; import org.pac4j.oidc.client.OidcClient;
import org.testng.annotations.Test;
import static auth.sso.oidc.OidcConfigs.*; import static auth.sso.oidc.OidcConfigs.*;
import static org.junit.Assert.*; import static org.junit.jupiter.api.Assertions.assertEquals;
public class OidcConfigurationTest { public class OidcConfigurationTest {

View File

@ -1,8 +1,8 @@
package utils; package utils;
import org.testng.annotations.Test; import org.junit.jupiter.api.Test;
import static org.junit.Assert.*; import static org.junit.jupiter.api.Assertions.assertEquals;
public class SearchUtilTest { public class SearchUtilTest {
@Test @Test

28
smoke-test/cypress-dev.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
set -euxo pipefail
DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )"
cd "$DIR"
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip wheel setuptools
pip install -r requirements.txt
mkdir -p ~/.datahub/plugins/frontend/auth/
echo "test_user:test_pass" > ~/.datahub/plugins/frontend/auth/user.props
echo "DATAHUB_VERSION = ${DATAHUB_VERSION:=acryl-datahub 0.0.0.dev0}"
DATAHUB_TELEMETRY_ENABLED=false \
DOCKER_COMPOSE_BASE="file://$( dirname "$DIR" )" \
datahub docker quickstart --build-locally --standalone_consumers --dump-logs-on-failure
python -c 'from tests.cypress.integration_test import ingest_data; ingest_data()'
cd tests/cypress
npm install
export CYPRESS_ADMIN_USERNAME=${ADMIN_USERNAME:-test_user}
export CYPRESS_ADMIN_PASSWORD=${ADMIN_PASSWORD:-test_pass}
npx cypress open

View File

@ -1,3 +1,8 @@
# Quick Run Tests with UI
cd smoke-test/
./cypress-dev.sh
# Running Cypress Tests Locally # Running Cypress Tests Locally
1. Make sure the packages are installed. It uses some node modules to run locally. Run `yarn install` from this directory. If you don't have `yarn`, download it. 1. Make sure the packages are installed. It uses some node modules to run locally. Run `yarn install` from this directory. If you don't have `yarn`, download it.
@ -9,3 +14,5 @@
4. Set the port that you want to run your cypress tests against in ./cypress.json. The default is 9002- if you are developing on react locally, you probably want 3000. Do not commit this change to github. 4. Set the port that you want to run your cypress tests against in ./cypress.json. The default is 9002- if you are developing on react locally, you probably want 3000. Do not commit this change to github.
5. Now, start the local cypress server: `npx cypress open`. 5. Now, start the local cypress server: `npx cypress open`.

View File

@ -99,8 +99,7 @@ for id_list in ONBOARDING_ID_LISTS:
ONBOARDING_IDS.extend(id_list) ONBOARDING_IDS.extend(id_list)
@pytest.fixture(scope="module", autouse=True) def ingest_data():
def ingest_cleanup_data():
print("creating onboarding data file") print("creating onboarding data file")
create_datahub_step_state_aspects( create_datahub_step_state_aspects(
get_admin_username(), get_admin_username(),
@ -113,6 +112,11 @@ def ingest_cleanup_data():
ingest_file_via_rest(f"{CYPRESS_TEST_DATA_DIR}/{TEST_DBT_DATA_FILENAME}") ingest_file_via_rest(f"{CYPRESS_TEST_DATA_DIR}/{TEST_DBT_DATA_FILENAME}")
ingest_file_via_rest(f"{CYPRESS_TEST_DATA_DIR}/{TEST_SCHEMA_BLAME_DATA_FILENAME}") ingest_file_via_rest(f"{CYPRESS_TEST_DATA_DIR}/{TEST_SCHEMA_BLAME_DATA_FILENAME}")
ingest_file_via_rest(f"{CYPRESS_TEST_DATA_DIR}/{TEST_ONBOARDING_DATA_FILENAME}") ingest_file_via_rest(f"{CYPRESS_TEST_DATA_DIR}/{TEST_ONBOARDING_DATA_FILENAME}")
@pytest.fixture(scope="module", autouse=True)
def ingest_cleanup_data():
ingest_data()
yield yield
print("removing test data") print("removing test data")
delete_urns_from_file(f"{CYPRESS_TEST_DATA_DIR}/{TEST_DATA_FILENAME}") delete_urns_from_file(f"{CYPRESS_TEST_DATA_DIR}/{TEST_DATA_FILENAME}")