mirror of
https://github.com/datahub-project/datahub.git
synced 2025-06-27 05:03:31 +00:00
318 lines
13 KiB
Java
318 lines
13 KiB
Java
package auth;
|
|
|
|
import static auth.AuthUtils.*;
|
|
import static utils.ConfigUtil.*;
|
|
|
|
import auth.sso.SsoManager;
|
|
import client.AuthServiceClient;
|
|
import com.datahub.authentication.Actor;
|
|
import com.datahub.authentication.ActorType;
|
|
import com.datahub.authentication.Authentication;
|
|
import com.datahub.plugins.auth.authorization.Authorizer;
|
|
import com.google.inject.AbstractModule;
|
|
import com.google.inject.Provides;
|
|
import com.google.inject.Singleton;
|
|
import com.google.inject.name.Named;
|
|
import com.linkedin.entity.client.EntityClientConfig;
|
|
import com.linkedin.entity.client.SystemEntityClient;
|
|
import com.linkedin.entity.client.SystemRestliEntityClient;
|
|
import com.linkedin.metadata.models.registry.EmptyEntityRegistry;
|
|
import com.linkedin.metadata.restli.DefaultRestliClientFactory;
|
|
import com.linkedin.parseq.retry.backoff.ExponentialBackoff;
|
|
import com.linkedin.util.Configuration;
|
|
import config.ConfigurationProvider;
|
|
import controllers.SsoCallbackController;
|
|
import io.datahubproject.metadata.context.ActorContext;
|
|
import io.datahubproject.metadata.context.AuthorizationContext;
|
|
import io.datahubproject.metadata.context.EntityRegistryContext;
|
|
import io.datahubproject.metadata.context.OperationContext;
|
|
import io.datahubproject.metadata.context.OperationContextConfig;
|
|
import io.datahubproject.metadata.context.RetrieverContext;
|
|
import io.datahubproject.metadata.context.SearchContext;
|
|
import io.datahubproject.metadata.context.ValidationContext;
|
|
import java.nio.charset.StandardCharsets;
|
|
import java.util.Collections;
|
|
import javax.annotation.Nonnull;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import org.apache.commons.codec.digest.DigestUtils;
|
|
import org.apache.http.impl.client.CloseableHttpClient;
|
|
import org.apache.http.impl.client.HttpClients;
|
|
import org.pac4j.core.config.Config;
|
|
import org.pac4j.core.context.session.SessionStore;
|
|
import org.pac4j.core.profile.ProfileManager;
|
|
import org.pac4j.core.util.serializer.JavaSerializer;
|
|
import org.pac4j.play.LogoutController;
|
|
import org.pac4j.play.http.PlayHttpActionAdapter;
|
|
import org.pac4j.play.store.PlayCacheSessionStore;
|
|
import org.pac4j.play.store.PlayCookieSessionStore;
|
|
import org.pac4j.play.store.ShiroAesDataEncrypter;
|
|
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
|
import play.Environment;
|
|
import play.cache.SyncCacheApi;
|
|
import utils.ConfigUtil;
|
|
|
|
/** Responsible for configuring, validating, and providing authentication related components. */
|
|
@Slf4j
|
|
public class AuthModule extends AbstractModule {
|
|
|
|
/**
|
|
* Pac4j Stores Session State in a browser-side cookie in encrypted fashion. This configuration
|
|
* value provides a stable encryption base from which to derive the encryption key.
|
|
*
|
|
* <p>We hash this value (SHA256), then take the first 16 bytes as the AES key.
|
|
*/
|
|
private static final String PAC4J_AES_KEY_BASE_CONF = "play.http.secret.key";
|
|
|
|
private static final String PAC4J_SESSIONSTORE_PROVIDER_CONF = "pac4j.sessionStore.provider";
|
|
private static final String ENTITY_CLIENT_RETRY_INTERVAL = "entityClient.retryInterval";
|
|
private static final String ENTITY_CLIENT_NUM_RETRIES = "entityClient.numRetries";
|
|
private static final String ENTITY_CLIENT_RESTLI_GET_BATCH_SIZE =
|
|
"entityClient.restli.get.batchSize";
|
|
private static final String ENTITY_CLIENT_RESTLI_GET_BATCH_CONCURRENCY =
|
|
"entityClient.restli.get.batchConcurrency";
|
|
private static final String GET_SSO_SETTINGS_ENDPOINT = "auth/getSsoSettings";
|
|
|
|
private final com.typesafe.config.Config configs;
|
|
|
|
public AuthModule(final Environment environment, final com.typesafe.config.Config configs) {
|
|
this.configs = configs;
|
|
}
|
|
|
|
@Override
|
|
protected void configure() {
|
|
/**
|
|
* In Pac4J, you are given the option to store the profiles of authenticated users in either (i)
|
|
* PlayCacheSessionStore - saves your data in the Play cache or (ii) PlayCookieSessionStore
|
|
* saves your data in the Play session cookie However there is problem
|
|
* (https://github.com/datahub-project/datahub/issues/4448) observed when storing the Pac4j
|
|
* profile in cookie. Whenever the profile returned by Pac4j is greater than 4096 characters,
|
|
* the response will be rejected by the browser. Default to PlayCacheCookieStore so that
|
|
* datahub-frontend container remains as a stateless service
|
|
*/
|
|
String sessionStoreProvider = configs.getString(PAC4J_SESSIONSTORE_PROVIDER_CONF);
|
|
|
|
if (sessionStoreProvider.equals("PlayCacheSessionStore")) {
|
|
final PlayCacheSessionStore playCacheSessionStore =
|
|
new PlayCacheSessionStore(getProvider(SyncCacheApi.class));
|
|
bind(SessionStore.class).toInstance(playCacheSessionStore);
|
|
bind(PlayCacheSessionStore.class).toInstance(playCacheSessionStore);
|
|
} else {
|
|
PlayCookieSessionStore playCacheCookieStore;
|
|
try {
|
|
// To generate a valid encryption key from an input value, we first
|
|
// hash the input to generate a fixed-length string. Then, we convert
|
|
// it to hex and slice the first 16 bytes, because AES key length must strictly
|
|
// have a specific length.
|
|
final String aesKeyBase = configs.getString(PAC4J_AES_KEY_BASE_CONF);
|
|
final String aesKeyHash =
|
|
DigestUtils.sha256Hex(aesKeyBase.getBytes(StandardCharsets.UTF_8));
|
|
final String aesEncryptionKey = aesKeyHash.substring(0, 16);
|
|
playCacheCookieStore =
|
|
new PlayCookieSessionStore(new ShiroAesDataEncrypter(aesEncryptionKey.getBytes()));
|
|
playCacheCookieStore.setSerializer(new JavaSerializer());
|
|
} catch (Exception e) {
|
|
throw new RuntimeException("Failed to instantiate Pac4j cookie session store!", e);
|
|
}
|
|
bind(SessionStore.class).toInstance(playCacheCookieStore);
|
|
bind(PlayCookieSessionStore.class).toInstance(playCacheCookieStore);
|
|
}
|
|
|
|
try {
|
|
bind(SsoCallbackController.class)
|
|
.toConstructor(
|
|
SsoCallbackController.class.getConstructor(
|
|
SsoManager.class,
|
|
OperationContext.class,
|
|
SystemEntityClient.class,
|
|
AuthServiceClient.class,
|
|
org.pac4j.core.config.Config.class,
|
|
com.typesafe.config.Config.class));
|
|
} catch (NoSuchMethodException | SecurityException e) {
|
|
throw new RuntimeException(
|
|
"Failed to bind to SsoCallbackController. Cannot find constructor", e);
|
|
}
|
|
// logout
|
|
final LogoutController logoutController = new LogoutController();
|
|
logoutController.setDefaultUrl("/");
|
|
bind(LogoutController.class).toInstance(logoutController);
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected Config provideConfig(@Nonnull SessionStore sessionStore) {
|
|
Config config = new Config();
|
|
config.setSessionStoreFactory(parameters -> sessionStore);
|
|
config.setHttpActionAdapter(new PlayHttpActionAdapter());
|
|
config.setProfileManagerFactory(ProfileManager::new);
|
|
|
|
return config;
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected SsoManager provideSsoManager(
|
|
Authentication systemAuthentication, CloseableHttpClient httpClient) {
|
|
SsoManager manager =
|
|
new SsoManager(
|
|
configs, systemAuthentication, getSsoSettingsRequestUrl(configs), httpClient);
|
|
manager.initializeSsoProvider();
|
|
return manager;
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected Authentication provideSystemAuthentication() {
|
|
// Returns an instance of Authentication used to authenticate system initiated calls to Metadata
|
|
// Service.
|
|
String systemClientId = configs.getString(SYSTEM_CLIENT_ID_CONFIG_PATH);
|
|
String systemSecret = configs.getString(SYSTEM_CLIENT_SECRET_CONFIG_PATH);
|
|
final Actor systemActor =
|
|
new Actor(ActorType.USER, systemClientId); // TODO: Change to service actor once supported.
|
|
return new Authentication(
|
|
systemActor,
|
|
String.format("Basic %s:%s", systemClientId, systemSecret),
|
|
Collections.emptyMap());
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
@Named("systemOperationContext")
|
|
protected OperationContext provideOperationContext(
|
|
final Authentication systemAuthentication,
|
|
final ConfigurationProvider configurationProvider) {
|
|
ActorContext systemActorContext =
|
|
ActorContext.builder()
|
|
.systemAuth(true)
|
|
.authentication(systemAuthentication)
|
|
.enforceExistenceEnabled(
|
|
configurationProvider.getAuthentication().isEnforceExistenceEnabled())
|
|
.build();
|
|
OperationContextConfig systemConfig =
|
|
OperationContextConfig.builder()
|
|
.viewAuthorizationConfiguration(configurationProvider.getAuthorization().getView())
|
|
.allowSystemAuthentication(true)
|
|
.build();
|
|
|
|
return OperationContext.builder()
|
|
.operationContextConfig(systemConfig)
|
|
.systemActorContext(systemActorContext)
|
|
// Authorizer.EMPTY is fine since it doesn't actually apply to system auth
|
|
.authorizationContext(AuthorizationContext.builder().authorizer(Authorizer.EMPTY).build())
|
|
.searchContext(SearchContext.EMPTY)
|
|
.entityRegistryContext(EntityRegistryContext.builder().build(EmptyEntityRegistry.EMPTY))
|
|
.validationContext(ValidationContext.builder().alternateValidation(false).build())
|
|
.retrieverContext(RetrieverContext.EMPTY)
|
|
.build(
|
|
systemAuthentication,
|
|
configurationProvider.getAuthentication().isEnforceExistenceEnabled());
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected ConfigurationProvider provideConfigurationProvider() {
|
|
AnnotationConfigApplicationContext context =
|
|
new AnnotationConfigApplicationContext(ConfigurationProvider.class);
|
|
return context.getBean(ConfigurationProvider.class);
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected SystemEntityClient provideEntityClient(
|
|
@Named("systemOperationContext") final OperationContext systemOperationContext,
|
|
final ConfigurationProvider configurationProvider) {
|
|
|
|
return new SystemRestliEntityClient(
|
|
buildRestliClient(),
|
|
EntityClientConfig.builder()
|
|
.backoffPolicy(new ExponentialBackoff(configs.getInt(ENTITY_CLIENT_RETRY_INTERVAL)))
|
|
.retryCount(configs.getInt(ENTITY_CLIENT_NUM_RETRIES))
|
|
.batchGetV2Size(configs.getInt(ENTITY_CLIENT_RESTLI_GET_BATCH_SIZE))
|
|
.batchGetV2Concurrency(2)
|
|
.build(),
|
|
configurationProvider.getCache().getClient().getEntityClient());
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected AuthServiceClient provideAuthClient(
|
|
Authentication systemAuthentication, CloseableHttpClient httpClient) {
|
|
// Init a GMS auth client
|
|
final String metadataServiceHost = getMetadataServiceHost(configs);
|
|
|
|
final int metadataServicePort = getMetadataServicePort(configs);
|
|
|
|
final boolean metadataServiceUseSsl = doesMetadataServiceUseSsl(configs);
|
|
|
|
return new AuthServiceClient(
|
|
metadataServiceHost,
|
|
metadataServicePort,
|
|
metadataServiceUseSsl,
|
|
systemAuthentication,
|
|
httpClient);
|
|
}
|
|
|
|
@Provides
|
|
@Singleton
|
|
protected CloseableHttpClient provideHttpClient() {
|
|
return HttpClients.createDefault();
|
|
}
|
|
|
|
private com.linkedin.restli.client.Client buildRestliClient() {
|
|
final String metadataServiceHost =
|
|
utils.ConfigUtil.getString(
|
|
configs,
|
|
METADATA_SERVICE_HOST_CONFIG_PATH,
|
|
utils.ConfigUtil.DEFAULT_METADATA_SERVICE_HOST);
|
|
final int metadataServicePort =
|
|
utils.ConfigUtil.getInt(
|
|
configs,
|
|
utils.ConfigUtil.METADATA_SERVICE_PORT_CONFIG_PATH,
|
|
utils.ConfigUtil.DEFAULT_METADATA_SERVICE_PORT);
|
|
final boolean metadataServiceUseSsl =
|
|
utils.ConfigUtil.getBoolean(
|
|
configs,
|
|
utils.ConfigUtil.METADATA_SERVICE_USE_SSL_CONFIG_PATH,
|
|
ConfigUtil.DEFAULT_METADATA_SERVICE_USE_SSL);
|
|
final String metadataServiceSslProtocol =
|
|
utils.ConfigUtil.getString(
|
|
configs,
|
|
utils.ConfigUtil.METADATA_SERVICE_SSL_PROTOCOL_CONFIG_PATH,
|
|
ConfigUtil.DEFAULT_METADATA_SERVICE_SSL_PROTOCOL);
|
|
return DefaultRestliClientFactory.getRestLiClient(
|
|
metadataServiceHost,
|
|
metadataServicePort,
|
|
metadataServiceUseSsl,
|
|
metadataServiceSslProtocol);
|
|
}
|
|
|
|
protected boolean doesMetadataServiceUseSsl(com.typesafe.config.Config configs) {
|
|
return configs.hasPath(METADATA_SERVICE_USE_SSL_CONFIG_PATH)
|
|
? configs.getBoolean(METADATA_SERVICE_USE_SSL_CONFIG_PATH)
|
|
: Boolean.parseBoolean(
|
|
Configuration.getEnvironmentVariable(GMS_USE_SSL_ENV_VAR, DEFAULT_GMS_USE_SSL));
|
|
}
|
|
|
|
protected String getMetadataServiceHost(com.typesafe.config.Config configs) {
|
|
return configs.hasPath(METADATA_SERVICE_HOST_CONFIG_PATH)
|
|
? configs.getString(METADATA_SERVICE_HOST_CONFIG_PATH)
|
|
: Configuration.getEnvironmentVariable(GMS_HOST_ENV_VAR, DEFAULT_GMS_HOST);
|
|
}
|
|
|
|
protected Integer getMetadataServicePort(com.typesafe.config.Config configs) {
|
|
return configs.hasPath(METADATA_SERVICE_PORT_CONFIG_PATH)
|
|
? configs.getInt(METADATA_SERVICE_PORT_CONFIG_PATH)
|
|
: Integer.parseInt(
|
|
Configuration.getEnvironmentVariable(GMS_PORT_ENV_VAR, DEFAULT_GMS_PORT));
|
|
}
|
|
|
|
protected String getSsoSettingsRequestUrl(com.typesafe.config.Config configs) {
|
|
final String protocol = doesMetadataServiceUseSsl(configs) ? "https" : "http";
|
|
final String metadataServiceHost = getMetadataServiceHost(configs);
|
|
final Integer metadataServicePort = getMetadataServicePort(configs);
|
|
|
|
return String.format(
|
|
"%s://%s:%s/%s",
|
|
protocol, metadataServiceHost, metadataServicePort, GET_SSO_SETTINGS_ENDPOINT);
|
|
}
|
|
}
|