Skip to content

Commit

Permalink
feat(auth): Metadata Service Authentication! (#3598)
Browse files Browse the repository at this point in the history
  • Loading branch information
jjoyce0510 authored Nov 23, 2021
1 parent fde42e0 commit f49666a
Show file tree
Hide file tree
Showing 204 changed files with 3,966 additions and 1,193 deletions.
89 changes: 87 additions & 2 deletions datahub-frontend/app/auth/AuthModule.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,17 @@
package auth;

import client.AuthServiceClient;
import com.datahub.authentication.Actor;
import com.datahub.authentication.ActorType;
import com.google.inject.AbstractModule;
import com.google.inject.Provides;
import com.google.inject.Singleton;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.entity.client.RestliEntityClient;
import com.linkedin.metadata.restli.DefaultRestliClientFactory;
import com.linkedin.util.Configuration;
import com.datahub.authentication.Authentication;
import java.util.Collections;
import org.pac4j.core.client.Client;
import org.pac4j.core.client.Clients;
import org.pac4j.core.config.Config;
Expand All @@ -20,8 +29,12 @@
import auth.sso.SsoConfigs;
import auth.sso.SsoManager;
import controllers.SsoCallbackController;
import utils.ConfigUtil;

import static auth.AuthUtils.*;
import static auth.sso.oidc.OidcConfigs.*;
import static utils.ConfigUtil.*;


/**
* Responsible for configuring, validating, and providing authentication related components.
Expand All @@ -42,9 +55,12 @@ protected void configure() {

try {
bind(SsoCallbackController.class).toConstructor(SsoCallbackController.class.getConstructor(
SsoManager.class));
SsoManager.class,
Authentication.class,
EntityClient.class,
AuthServiceClient.class));
} catch (NoSuchMethodException | SecurityException e) {
System.out.println("Required constructor missing");
throw new RuntimeException("Failed to bind to SsoCallbackController. Cannot find constructor, e");
}
// logout
final LogoutController logoutController = new LogoutController();
Expand Down Expand Up @@ -83,11 +99,80 @@ protected SsoManager provideSsoManager() {
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
protected EntityClient provideEntityClient() {
return new RestliEntityClient(buildRestliClient());
}

@Provides @Singleton
protected AuthServiceClient provideAuthClient(Authentication systemAuthentication) {
// Init a GMS auth client
final String metadataServiceHost = _configs.hasPath(METADATA_SERVICE_HOST_CONFIG_PATH)
? _configs.getString(METADATA_SERVICE_HOST_CONFIG_PATH)
: Configuration.getEnvironmentVariable(GMS_HOST_ENV_VAR, DEFAULT_GMS_HOST);

final int metadataServicePort = _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));

final Boolean metadataServiceUseSsl = _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));

return new AuthServiceClient(
metadataServiceHost,
metadataServicePort,
metadataServiceUseSsl,
systemAuthentication);
}

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 isSsoEnabled(com.typesafe.config.Config configs) {
// If OIDC is enabled, we infer SSO to be enabled.
return configs.hasPath(OIDC_ENABLED_CONFIG_PATH)
&& Boolean.TRUE.equals(
Boolean.parseBoolean(configs.getString(OIDC_ENABLED_CONFIG_PATH)));
}

protected boolean isMetadataServiceAuthEnabled(com.typesafe.config.Config configs) {
// If OIDC is enabled, we infer SSO to be enabled.
return configs.hasPath(METADATA_SERVICE_AUTH_ENABLED_CONFIG_PATH)
&& Boolean.TRUE.equals(
Boolean.parseBoolean(configs.getString(METADATA_SERVICE_AUTH_ENABLED_CONFIG_PATH)));
}
}

58 changes: 56 additions & 2 deletions datahub-frontend/app/auth/AuthUtils.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,43 @@
package auth;

import com.linkedin.common.urn.CorpuserUrn;
import lombok.extern.slf4j.Slf4j;
import play.mvc.Http;

import java.time.Duration;
import java.time.temporal.ChronoUnit;

@Slf4j
public class AuthUtils {

/**
* The config path that determines whether Metadata Service Authentication is enabled.
*
* When enabled, the frontend server will proxy requests to the Metadata Service without requiring them to have a valid
* frontend-issued Session Cookie. This effectively means delegating the act of authentication to the Metadata Service. It
* is critical that if Metadata Service authentication is enabled at the frontend service layer, it is also enabled in the
* Metadata Service itself. Otherwise, unauthenticated traffic may reach the Metadata itself.
*
* When disabled, the frontend server will require that all requests have a valid Session Cookie associated with them. Otherwise,
* requests will be denied with an Unauthorized error.
*/
public static final String METADATA_SERVICE_AUTH_ENABLED_CONFIG_PATH = "metadataService.auth.enabled";

/**
* The attribute inside session cookie representing a GMS-issued access token
*/
public static final String SESSION_COOKIE_GMS_TOKEN_NAME = "token";

/**
* An ID used to identify system callers that are internal to DataHub. Provided via configuration.
*/
public static final String SYSTEM_CLIENT_ID_CONFIG_PATH = "systemClientId";

/**
* An Secret used to authenticate system callers that are internal to DataHub. Provided via configuration.
*/
public static final String SYSTEM_CLIENT_SECRET_CONFIG_PATH = "systemClientSecret";

public static final String SESSION_TTL_CONFIG_PATH = "auth.session.ttlInHours";
public static final Integer DEFAULT_SESSION_TTL_HOURS = 720;
public static final CorpuserUrn DEFAULT_ACTOR_URN = new CorpuserUrn("datahub");
Expand All @@ -16,19 +46,43 @@ public class AuthUtils {
public static final String USER_NAME = "username";
public static final String PASSWORD = "password";
public static final String ACTOR = "actor";
public static final String ACCESS_TOKEN = "token";

/**
* Determines whether the inbound request should be forward to downstream Metadata Service. Today, this simply
* checks for the presence of an "Authorization" header or the presence of a valid session cookie issued
* by the frontend.
*
* Note that this method DOES NOT actually verify the authentication token of an inbound request. That will
* be handled by the downstream Metadata Service. Until then, the request should be treated as UNAUTHENTICATED.
*
* Returns true if the request is eligible to be forwarded to GMS, false otherwise.
*/
public static boolean isEligibleForForwarding(Http.Context ctx) {
return hasValidSessionCookie(ctx) || hasAuthHeader(ctx);
}

/**
* Returns true if a request is authenticated, false otherwise.
* Returns true if a request has a valid session cookie issued by the frontend server.
* Note that this DOES NOT verify whether the token within the session cookie will be accepted
* by the downstream GMS service.
*
* 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.
*/
public static boolean isAuthenticated(final Http.Context ctx) {
public static boolean hasValidSessionCookie(final Http.Context ctx) {
return ctx.session().containsKey(ACTOR)
&& ctx.request().cookie(ACTOR) != null
&& ctx.session().get(ACTOR).equals(ctx.request().cookie(ACTOR).value());
}

/**
* Returns true if a request includes the Authorization header, false otherwise
*/
public static boolean hasAuthHeader(final Http.Context ctx) {
return ctx.request().getHeaders().contains(Http.HeaderNames.AUTHORIZATION);
}

/**
* Creates a client authentication cookie (actor cookie) with a specified TTL in hours.
*
Expand Down
23 changes: 21 additions & 2 deletions datahub-frontend/app/auth/Authenticator.java
Original file line number Diff line number Diff line change
@@ -1,22 +1,41 @@
package auth;

import com.typesafe.config.Config;
import javax.inject.Inject;
import play.mvc.Http;
import play.mvc.Result;
import play.mvc.Security;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import static auth.AuthUtils.ACTOR;
import static auth.AuthUtils.*;


/**
* Implementation of base Play Authentication used to determine if a request to a route should be
* authenticated.
*/
public class Authenticator extends Security.Authenticator {

private final boolean metadataServiceAuthEnabled;

@Inject
public Authenticator(@Nonnull Config config) {
this.metadataServiceAuthEnabled = config.hasPath(METADATA_SERVICE_AUTH_ENABLED_CONFIG_PATH) && config.getBoolean(METADATA_SERVICE_AUTH_ENABLED_CONFIG_PATH);
}

@Override
public String getUsername(@Nonnull Http.Context ctx) {
return AuthUtils.isAuthenticated(ctx) ? ctx.session().get(ACTOR) : null;
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 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 AuthUtils.hasValidSessionCookie(ctx) ? ctx.session().get(ACTOR) : null;
}
}

@Override
Expand Down
36 changes: 23 additions & 13 deletions datahub-frontend/app/auth/sso/oidc/OidcCallbackLogic.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package auth.sso.oidc;

import client.AuthServiceClient;
import com.datahub.authentication.Authentication;
import com.linkedin.common.AuditStamp;
import com.linkedin.common.CorpGroupUrnArray;
import com.linkedin.common.CorpuserUrnArray;
Expand All @@ -9,9 +11,7 @@
import com.linkedin.common.urn.CorpuserUrn;
import com.linkedin.common.urn.Urn;
import com.linkedin.data.template.SetMode;
import com.linkedin.datahub.graphql.GmsClientFactory;
import com.linkedin.entity.Entity;
import com.linkedin.entity.client.AspectClient;
import com.linkedin.entity.client.EntityClient;
import com.linkedin.events.metadata.ChangeType;
import com.linkedin.identity.CorpGroupInfo;
Expand Down Expand Up @@ -71,13 +71,20 @@
@Slf4j
public class OidcCallbackLogic extends DefaultCallbackLogic<Result, PlayWebContext> {

private static final String SYSTEM_ACTOR = Constants.SYSTEM_ACTOR;
private final EntityClient _entityClient = GmsClientFactory.getEntitiesClient();
private final AspectClient _aspectClient = GmsClientFactory.getAspectsClient();
private final SsoManager _ssoManager;

public OidcCallbackLogic(final SsoManager ssoManager) {
private final EntityClient _entityClient;
private final Authentication _systemAuthentication;
private final AuthServiceClient _authClient;

public OidcCallbackLogic(
final SsoManager ssoManager,
final Authentication systemAuthentication,
final EntityClient entityClient,
final AuthServiceClient authClient) {
_ssoManager = ssoManager;
_systemAuthentication = systemAuthentication;
_entityClient = entityClient;
_authClient = authClient;
}

@Override
Expand Down Expand Up @@ -146,6 +153,9 @@ private Result handleOidcCallback(
return internalServerError(String.format("Failed to perform post authentication steps. Error message: %s", e.getMessage()));
}

// Successfully logged in - Generate GMS login token
final String accessToken = _authClient.generateSessionTokenForUser(corpUserUrn.getId());
context.getJavaSession().put(ACCESS_TOKEN, accessToken);
context.getJavaSession().put(ACTOR, corpUserUrn.toString());
return result.withCookies(createActorCookie(corpUserUrn.toString(), oidcConfigs.getSessionTtlInHours()));
}
Expand Down Expand Up @@ -287,7 +297,7 @@ private void tryProvisionUser(CorpUserSnapshot corpUserSnapshot) {

// 1. Check if this user already exists.
try {
final Entity corpUser = _entityClient.get(corpUserSnapshot.getUrn(), SYSTEM_ACTOR);
final Entity corpUser = _entityClient.get(corpUserSnapshot.getUrn(), _systemAuthentication);
final CorpUserSnapshot existingCorpUserSnapshot = corpUser.getValue().getCorpUserSnapshot();

log.debug(String.format("Fetched GMS user with urn %s",corpUserSnapshot.getUrn()));
Expand All @@ -298,7 +308,7 @@ private void tryProvisionUser(CorpUserSnapshot corpUserSnapshot) {
// 2. The user does not exist. Provision them.
final Entity newEntity = new Entity();
newEntity.setValue(Snapshot.create(corpUserSnapshot));
_entityClient.update(newEntity, SYSTEM_ACTOR);
_entityClient.update(newEntity, _systemAuthentication);
log.debug(String.format("Successfully provisioned user %s", corpUserSnapshot.getUrn()));
}
log.debug(String.format("User %s already exists. Skipping provisioning", corpUserSnapshot.getUrn()));
Expand All @@ -318,7 +328,7 @@ private void tryProvisionGroups(List<CorpGroupSnapshot> corpGroups) {
// 1. Check if this user already exists.
try {
final Set<Urn> urnsToFetch = corpGroups.stream().map(CorpGroupSnapshot::getUrn).collect(Collectors.toSet());
final Map<Urn, Entity> existingGroups = _entityClient.batchGet(urnsToFetch, SYSTEM_ACTOR);
final Map<Urn, Entity> existingGroups = _entityClient.batchGet(urnsToFetch, _systemAuthentication);

log.debug(String.format("Fetched GMS groups with urns %s", existingGroups.keySet()));

Expand Down Expand Up @@ -351,7 +361,7 @@ private void tryProvisionGroups(List<CorpGroupSnapshot> corpGroups) {
// Now batch create all entities identified to create.
_entityClient.batchUpdate(groupsToCreate.stream().map(groupSnapshot ->
new Entity().setValue(Snapshot.create(groupSnapshot))
).collect(Collectors.toSet()), SYSTEM_ACTOR);
).collect(Collectors.toSet()), _systemAuthentication);

log.debug(String.format("Successfully provisioned groups with urns %s", groupsToCreateUrns));

Expand All @@ -365,7 +375,7 @@ private void tryProvisionGroups(List<CorpGroupSnapshot> corpGroups) {
private void verifyPreProvisionedUser(CorpuserUrn urn) {
// Validate that the user exists in the system (there is more than just a key aspect for them, as of today).
try {
final Entity corpUser = _entityClient.get(urn, SYSTEM_ACTOR);
final Entity corpUser = _entityClient.get(urn, _systemAuthentication);

log.debug(String.format("Fetched GMS user with urn %s", urn));

Expand All @@ -391,7 +401,7 @@ private void setUserStatus(final Urn urn, final CorpUserStatus newStatus) throws
proposal.setAspectName(Constants.CORP_USER_STATUS_ASPECT_NAME);
proposal.setAspect(GenericAspectUtils.serializeAspect(newStatus));
proposal.setChangeType(ChangeType.UPSERT);
_aspectClient.ingestProposal(proposal, Constants.SYSTEM_ACTOR).getEntity();
_entityClient.ingestProposal(proposal, _systemAuthentication);
}

private Optional<String> extractRegexGroup(final String patternStr, final String target) {
Expand Down
Loading

0 comments on commit f49666a

Please sign in to comment.