2021-08-20 10:58:07 -07:00
package auth.sso.oidc ;
2021-08-20 07:42:18 -07:00
2023-12-06 11:02:42 +05:30
import static auth.AuthUtils.* ;
import static com.linkedin.metadata.Constants.CORP_USER_ENTITY_NAME ;
import static com.linkedin.metadata.Constants.GROUP_MEMBERSHIP_ASPECT_NAME ;
import static org.pac4j.play.store.PlayCookieSessionStore.* ;
import static play.mvc.Results.internalServerError ;
2023-02-14 13:36:47 -05:00
import auth.CookieConfigs ;
2023-12-06 11:02:42 +05:30
import auth.sso.SsoManager ;
2021-11-22 16:33:14 -08:00
import client.AuthServiceClient ;
2023-12-26 09:04:05 -05:00
import com.fasterxml.jackson.core.type.TypeReference ;
import com.fasterxml.jackson.databind.ObjectMapper ;
2021-10-20 17:09:02 -07:00
import com.linkedin.common.AuditStamp ;
2021-08-20 07:42:18 -07:00
import com.linkedin.common.CorpGroupUrnArray ;
import com.linkedin.common.CorpuserUrnArray ;
import com.linkedin.common.UrnArray ;
import com.linkedin.common.url.Url ;
import com.linkedin.common.urn.CorpGroupUrn ;
import com.linkedin.common.urn.CorpuserUrn ;
import com.linkedin.common.urn.Urn ;
import com.linkedin.data.template.SetMode ;
import com.linkedin.entity.Entity ;
2023-09-21 22:00:14 -05:00
import com.linkedin.entity.client.SystemEntityClient ;
2021-10-07 16:14:35 -07:00
import com.linkedin.events.metadata.ChangeType ;
2021-08-20 07:42:18 -07:00
import com.linkedin.identity.CorpGroupInfo ;
import com.linkedin.identity.CorpUserEditableInfo ;
import com.linkedin.identity.CorpUserInfo ;
2021-10-07 16:14:35 -07:00
import com.linkedin.identity.CorpUserStatus ;
2021-08-20 07:42:18 -07:00
import com.linkedin.identity.GroupMembership ;
2021-09-28 16:30:49 -07:00
import com.linkedin.metadata.Constants ;
2021-08-20 07:42:18 -07:00
import com.linkedin.metadata.aspect.CorpGroupAspect ;
import com.linkedin.metadata.aspect.CorpGroupAspectArray ;
import com.linkedin.metadata.aspect.CorpUserAspect ;
import com.linkedin.metadata.aspect.CorpUserAspectArray ;
import com.linkedin.metadata.snapshot.CorpGroupSnapshot ;
import com.linkedin.metadata.snapshot.CorpUserSnapshot ;
import com.linkedin.metadata.snapshot.Snapshot ;
2022-03-29 18:32:04 -07:00
import com.linkedin.metadata.utils.GenericRecordUtils ;
2021-10-07 16:14:35 -07:00
import com.linkedin.mxe.MetadataChangeProposal ;
2021-08-20 07:42:18 -07:00
import com.linkedin.r2.RemoteInvocationException ;
2024-10-28 09:05:16 -05:00
import com.linkedin.util.Pair ;
import io.datahubproject.metadata.context.OperationContext ;
2021-08-20 07:42:18 -07:00
import java.io.UnsupportedEncodingException ;
import java.net.MalformedURLException ;
import java.net.URI ;
import java.net.URLEncoder ;
import java.nio.charset.StandardCharsets ;
import java.util.ArrayList ;
2022-03-21 20:33:53 +00:00
import java.util.Arrays ;
2023-10-17 15:50:32 -05:00
import java.util.Base64 ;
2021-08-20 07:42:18 -07:00
import java.util.Collection ;
import java.util.Collections ;
import java.util.List ;
import java.util.Map ;
2024-10-28 09:05:16 -05:00
import java.util.Objects ;
2021-08-20 07:42:18 -07:00
import java.util.Optional ;
import java.util.Set ;
import java.util.regex.Matcher ;
import java.util.regex.Pattern ;
import java.util.stream.Collectors ;
2024-10-28 09:05:16 -05:00
import javax.annotation.Nonnull ;
2021-08-20 07:42:18 -07:00
import lombok.extern.slf4j.Slf4j ;
2024-10-28 09:05:16 -05:00
import org.pac4j.core.client.BaseClient ;
import org.pac4j.core.client.Client ;
import org.pac4j.core.client.Clients ;
2021-08-20 07:42:18 -07:00
import org.pac4j.core.config.Config ;
2024-10-28 09:05:16 -05:00
import org.pac4j.core.context.CallContext ;
2023-10-17 15:50:32 -05:00
import org.pac4j.core.context.Cookie ;
2024-10-28 09:05:16 -05:00
import org.pac4j.core.context.FrameworkParameters ;
import org.pac4j.core.context.WebContext ;
import org.pac4j.core.credentials.Credentials ;
2021-08-20 07:42:18 -07:00
import org.pac4j.core.engine.DefaultCallbackLogic ;
2024-10-28 09:05:16 -05:00
import org.pac4j.core.exception.http.HttpAction ;
2021-08-20 07:42:18 -07:00
import org.pac4j.core.http.adapter.HttpActionAdapter ;
import org.pac4j.core.profile.CommonProfile ;
import org.pac4j.core.profile.ProfileManager ;
2022-12-08 20:27:51 -06:00
import org.pac4j.core.profile.UserProfile ;
2024-10-28 09:05:16 -05:00
import org.pac4j.core.util.CommonHelper ;
2023-10-17 15:50:32 -05:00
import org.pac4j.core.util.Pac4jConstants ;
2024-10-28 09:05:16 -05:00
import org.pac4j.play.store.PlayCookieSessionStore ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
2021-08-20 07:42:18 -07:00
import play.mvc.Result ;
/ * *
2023-12-06 11:02:42 +05:30
* This class contains the logic that is executed when an OpenID Connect Identity Provider redirects
* back to D DataHub after an authentication attempt .
2021-08-20 07:42:18 -07:00
*
2023-12-06 11:02:42 +05:30
* < p > On receiving a user profile from the IdP ( using / userInfo endpoint ) , we attempt to extract
* basic information about the user including their name , email , groups , & more . If just - in - time
* provisioning is enabled , we also attempt to create a DataHub User ( { @link CorpUserSnapshot } ) for
* the user , along with any Groups ( { @link CorpGroupSnapshot } ) that can be extracted , only doing so
* if the user does not already exist .
2021-08-20 07:42:18 -07:00
* /
@Slf4j
2024-10-28 09:05:16 -05:00
public class OidcCallbackLogic extends DefaultCallbackLogic {
private static final Logger LOGGER = LoggerFactory . getLogger ( OidcCallbackLogic . class ) ;
2021-08-20 07:42:18 -07:00
2024-04-16 10:12:48 -05:00
private final SsoManager ssoManager ;
private final SystemEntityClient systemEntityClient ;
private final OperationContext systemOperationContext ;
private final AuthServiceClient authClient ;
private final CookieConfigs cookieConfigs ;
2021-11-22 16:33:14 -08:00
2023-12-06 11:02:42 +05:30
public OidcCallbackLogic (
final SsoManager ssoManager ,
2024-04-16 10:12:48 -05:00
final OperationContext systemOperationContext ,
2023-12-06 11:02:42 +05:30
final SystemEntityClient entityClient ,
final AuthServiceClient authClient ,
final CookieConfigs cookieConfigs ) {
2024-04-16 10:12:48 -05:00
this . ssoManager = ssoManager ;
this . systemOperationContext = systemOperationContext ;
systemEntityClient = entityClient ;
this . authClient = authClient ;
this . cookieConfigs = cookieConfigs ;
2021-08-20 07:42:18 -07:00
}
@Override
2024-10-28 09:05:16 -05:00
public Object perform (
2023-12-06 11:02:42 +05:30
Config config ,
2024-10-28 09:05:16 -05:00
String inputDefaultUrl ,
Boolean inputRenewSession ,
String defaultClient ,
FrameworkParameters parameters ) {
final Pair < CallContext , Object > ctxResult =
superPerform ( config , inputDefaultUrl , inputRenewSession , defaultClient , parameters ) ;
CallContext ctx = ctxResult . getFirst ( ) ;
Result result = ( Result ) ctxResult . getSecond ( ) ;
2021-08-20 07:42:18 -07:00
// Handle OIDC authentication errors.
2024-10-28 09:05:16 -05:00
if ( OidcResponseErrorHandler . isError ( ctx ) ) {
return OidcResponseErrorHandler . handleError ( ctx ) ;
2021-08-20 07:42:18 -07:00
}
// By this point, we know that OIDC is the enabled provider.
2024-04-16 10:12:48 -05:00
final OidcConfigs oidcConfigs = ( OidcConfigs ) ssoManager . getSsoProvider ( ) . configs ( ) ;
2024-10-28 09:05:16 -05:00
return handleOidcCallback ( systemOperationContext , ctx , oidcConfigs , result ) ;
}
/** Overriding this to be able to intercept the CallContext being created */
private Pair < CallContext , Object > superPerform (
Config config ,
String inputDefaultUrl ,
Boolean inputRenewSession ,
String defaultClient ,
FrameworkParameters parameters ) {
LOGGER . debug ( " === CALLBACK === " ) ;
CallContext ctx = this . buildContext ( config , parameters ) ;
WebContext webContext = ctx . webContext ( ) ;
HttpActionAdapter httpActionAdapter = config . getHttpActionAdapter ( ) ;
CommonHelper . assertNotNull ( " httpActionAdapter " , httpActionAdapter ) ;
HttpAction action ;
try {
CommonHelper . assertNotNull ( " clientFinder " , getClientFinder ( ) ) ;
String defaultUrl = ( String ) Objects . requireNonNullElse ( inputDefaultUrl , " / " ) ;
boolean renewSession = inputRenewSession = = null | | inputRenewSession ;
CommonHelper . assertNotBlank ( " defaultUrl " , defaultUrl ) ;
Clients clients = config . getClients ( ) ;
CommonHelper . assertNotNull ( " clients " , clients ) ;
List < Client > foundClients = getClientFinder ( ) . find ( clients , webContext , defaultClient ) ;
CommonHelper . assertTrue (
foundClients ! = null & & foundClients . size ( ) = = 1 ,
" unable to find one indirect client for the callback: check the callback URL for a client name parameter or suffix path or ensure that your configuration defaults to one indirect client " ) ;
Client foundClient = ( Client ) foundClients . get ( 0 ) ;
LOGGER . debug ( " foundClient: {} " , foundClient ) ;
CommonHelper . assertNotNull ( " foundClient " , foundClient ) ;
Credentials credentials = ( Credentials ) foundClient . getCredentials ( ctx ) . orElse ( null ) ;
LOGGER . debug ( " extracted credentials: {} " , credentials ) ;
credentials = ( Credentials ) foundClient . validateCredentials ( ctx , credentials ) . orElse ( null ) ;
LOGGER . debug ( " validated credentials: {} " , credentials ) ;
if ( credentials ! = null & & ! credentials . isForAuthentication ( ) ) {
action = foundClient . processLogout ( ctx , credentials ) ;
} else {
if ( credentials ! = null ) {
Optional < UserProfile > optProfile = foundClient . getUserProfile ( ctx , credentials ) ;
LOGGER . debug ( " optProfile: {} " , optProfile ) ;
if ( optProfile . isPresent ( ) ) {
UserProfile profile = ( UserProfile ) optProfile . get ( ) ;
Boolean saveProfileInSession =
( ( BaseClient ) foundClient ) . getSaveProfileInSession ( webContext , profile ) ;
boolean multiProfile = ( ( BaseClient ) foundClient ) . isMultiProfile ( webContext , profile ) ;
LOGGER . debug (
" saveProfileInSession: {} / multiProfile: {} " , saveProfileInSession , multiProfile ) ;
this . saveUserProfile (
ctx , config , profile , saveProfileInSession , multiProfile , renewSession ) ;
}
}
2024-11-21 01:37:07 +05:30
// Set the redirect url from cookie before creating action
setContextRedirectUrl ( ctx ) ;
2024-10-28 09:05:16 -05:00
action = this . redirectToOriginallyRequestedUrl ( ctx , defaultUrl ) ;
}
} catch ( RuntimeException var20 ) {
RuntimeException e = var20 ;
return Pair . of ( ctx , this . handleException ( e , httpActionAdapter , webContext ) ) ;
}
return Pair . of ( ctx , httpActionAdapter . adapt ( action , webContext ) ) ;
2021-08-20 07:42:18 -07:00
}
2024-10-28 09:05:16 -05:00
private void setContextRedirectUrl ( CallContext ctx ) {
WebContext context = ctx . webContext ( ) ;
PlayCookieSessionStore sessionStore = ( PlayCookieSessionStore ) ctx . sessionStore ( ) ;
2023-12-06 11:02:42 +05:30
Optional < Cookie > redirectUrl =
context . getRequestCookies ( ) . stream ( )
. filter ( cookie - > REDIRECT_URL_COOKIE_NAME . equals ( cookie . getName ( ) ) )
. findFirst ( ) ;
2023-10-17 15:50:32 -05:00
redirectUrl . ifPresent (
2023-12-06 11:02:42 +05:30
cookie - >
2024-10-28 09:05:16 -05:00
sessionStore . set (
context ,
Pac4jConstants . REQUESTED_URL ,
sessionStore
. getSerializer ( )
. deserializeFromBytes (
2023-12-06 11:02:42 +05:30
uncompressBytes ( Base64 . getDecoder ( ) . decode ( cookie . getValue ( ) ) ) ) ) ) ;
2023-10-17 15:50:32 -05:00
}
2023-12-06 11:02:42 +05:30
private Result handleOidcCallback (
2024-04-16 10:12:48 -05:00
final OperationContext opContext ,
2024-10-28 09:05:16 -05:00
final CallContext ctx ,
2023-12-06 11:02:42 +05:30
final OidcConfigs oidcConfigs ,
2024-10-28 09:05:16 -05:00
final Result result ) {
2021-08-20 07:42:18 -07:00
log . debug ( " Beginning OIDC Callback Handling... " ) ;
2024-10-28 09:05:16 -05:00
ProfileManager profileManager =
ctx . profileManagerFactory ( ) . apply ( ctx . webContext ( ) , ctx . sessionStore ( ) ) ;
2021-08-20 07:42:18 -07:00
if ( profileManager . isAuthenticated ( ) ) {
// If authenticated, the user should have a profile.
2024-10-28 09:05:16 -05:00
final Optional < UserProfile > optProfile = profileManager . getProfile ( ) ;
if ( optProfile . isEmpty ( ) ) {
return internalServerError (
" Failed to authenticate current user. Cannot find valid identity provider profile in session. " ) ;
}
final CommonProfile profile = ( CommonProfile ) optProfile . get ( ) ;
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" Found authenticated user with profile %s " , profile . getAttributes ( ) . toString ( ) ) ) ;
2021-08-20 07:42:18 -07:00
// Extract the User name required to log into DataHub.
final String userName = extractUserNameOrThrow ( oidcConfigs , profile ) ;
final CorpuserUrn corpUserUrn = new CorpuserUrn ( userName ) ;
try {
2023-12-06 11:02:42 +05:30
// If just-in-time User Provisioning is enabled, try to create the DataHub user if it does
// not exist.
2021-08-20 07:42:18 -07:00
if ( oidcConfigs . isJitProvisioningEnabled ( ) ) {
2021-10-05 19:30:51 -07:00
log . debug ( " Just-in-time provisioning is enabled. Beginning provisioning process... " ) ;
2021-08-20 07:42:18 -07:00
CorpUserSnapshot extractedUser = extractUser ( corpUserUrn , profile ) ;
2024-04-16 10:12:48 -05:00
tryProvisionUser ( opContext , extractedUser ) ;
2021-08-20 07:42:18 -07:00
if ( oidcConfigs . isExtractGroupsEnabled ( ) ) {
// Extract groups & provision them.
List < CorpGroupSnapshot > extractedGroups = extractGroups ( profile ) ;
2024-04-16 10:12:48 -05:00
tryProvisionGroups ( opContext , extractedGroups ) ;
2023-12-06 11:02:42 +05:30
// Add users to groups on DataHub. Note that this clears existing group membership for a
// user if it already exists.
2024-04-16 10:12:48 -05:00
updateGroupMembership ( opContext , corpUserUrn , createGroupMembership ( extractedGroups ) ) ;
2021-08-20 07:42:18 -07:00
}
} else if ( oidcConfigs . isPreProvisioningRequired ( ) ) {
// We should only allow logins for user accounts that have been pre-provisioned
log . debug ( " Pre Provisioning is required. Beginning validation of extracted user... " ) ;
2024-04-16 10:12:48 -05:00
verifyPreProvisionedUser ( opContext , corpUserUrn ) ;
2021-08-20 07:42:18 -07:00
}
2021-10-07 16:14:35 -07:00
// Update user status to active on login.
// If we want to prevent certain users from logging in, here's where we'll want to do it.
2024-10-28 09:05:16 -05:00
setUserStatus (
opContext ,
2023-12-06 11:02:42 +05:30
corpUserUrn ,
new CorpUserStatus ( )
. setStatus ( Constants . CORP_USER_STATUS_ACTIVE )
. setLastModified (
new AuditStamp ( )
. setActor ( Urn . createFromString ( Constants . SYSTEM_ACTOR ) )
. setTime ( System . currentTimeMillis ( ) ) ) ) ;
2021-08-20 07:42:18 -07:00
} catch ( Exception e ) {
log . error ( " Failed to perform post authentication steps. Redirecting to error page. " , e ) ;
2022-03-21 20:33:53 +00:00
return internalServerError (
2023-12-06 11:02:42 +05:30
String . format (
" Failed to perform post authentication steps. Error message: %s " , e . getMessage ( ) ) ) ;
2021-08-20 07:42:18 -07:00
}
2024-05-03 11:54:51 -07:00
log . info ( " OIDC callback authentication successful for user: {} " , userName ) ;
2021-11-22 16:33:14 -08:00
// Successfully logged in - Generate GMS login token
2024-04-16 10:12:48 -05:00
final String accessToken = authClient . generateSessionTokenForUser ( corpUserUrn . getId ( ) ) ;
2022-12-22 16:12:51 -06:00
return result
2023-12-06 11:02:42 +05:30
. withSession ( createSessionMap ( corpUserUrn . toString ( ) , accessToken ) )
. withCookies (
createActorCookie (
corpUserUrn . toString ( ) ,
2024-04-16 10:12:48 -05:00
cookieConfigs . getTtlInHours ( ) ,
cookieConfigs . getAuthCookieSameSite ( ) ,
cookieConfigs . getAuthCookieSecure ( ) ) ) ;
2021-08-20 07:42:18 -07:00
}
2022-03-21 20:33:53 +00:00
return internalServerError (
" Failed to authenticate current user. Cannot find valid identity provider profile in session. " ) ;
2021-08-20 07:42:18 -07:00
}
2023-12-06 11:02:42 +05:30
private String extractUserNameOrThrow (
final OidcConfigs oidcConfigs , final CommonProfile profile ) {
2021-08-20 07:42:18 -07:00
// Ensure that the attribute exists (was returned by IdP)
if ( ! profile . containsAttribute ( oidcConfigs . getUserNameClaim ( ) ) ) {
2023-12-06 11:02:42 +05:30
throw new RuntimeException (
String . format (
" Failed to resolve user name claim from profile provided by Identity Provider. Missing attribute. Attribute: '%s', Regex: '%s', Profile: %s " ,
oidcConfigs . getUserNameClaim ( ) ,
oidcConfigs . getUserNameClaimRegex ( ) ,
profile . getAttributes ( ) . toString ( ) ) ) ;
2021-08-20 07:42:18 -07:00
}
final String userNameClaim = ( String ) profile . getAttribute ( oidcConfigs . getUserNameClaim ( ) ) ;
2023-12-06 11:02:42 +05:30
final Optional < String > mappedUserName =
extractRegexGroup ( oidcConfigs . getUserNameClaimRegex ( ) , userNameClaim ) ;
return mappedUserName . orElseThrow (
( ) - >
new RuntimeException (
String . format (
" Failed to extract DataHub username from username claim %s using regex %s. Profile: %s " ,
userNameClaim ,
oidcConfigs . getUserNameClaimRegex ( ) ,
profile . getAttributes ( ) . toString ( ) ) ) ) ;
2021-08-20 07:42:18 -07:00
}
2023-12-06 11:02:42 +05:30
/** Attempts to map to an OIDC {@link CommonProfile} (userInfo) to a {@link CorpUserSnapshot}. */
2021-08-20 07:42:18 -07:00
private CorpUserSnapshot extractUser ( CorpuserUrn urn , CommonProfile profile ) {
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" Attempting to extract user from OIDC profile %s " , profile . getAttributes ( ) . toString ( ) ) ) ;
2021-08-20 07:42:18 -07:00
// Extracts these based on the default set of OIDC claims, described here:
// https://developer.okta.com/blog/2017/07/25/oidc-primer-part-1
String firstName = profile . getFirstName ( ) ;
String lastName = profile . getFamilyName ( ) ;
String email = profile . getEmail ( ) ;
URI picture = profile . getPictureUrl ( ) ;
String displayName = profile . getDisplayName ( ) ;
2023-12-06 11:02:42 +05:30
String fullName =
( String )
profile . getAttribute ( " name " ) ; // Name claim is sometimes provided, including by Google.
2021-10-13 18:56:20 -07:00
if ( fullName = = null & & firstName ! = null & & lastName ! = null ) {
fullName = String . format ( " %s %s " , firstName , lastName ) ;
}
2021-08-20 07:42:18 -07:00
// TODO: Support custom claims mapping. (e.g. department, title, etc)
final CorpUserInfo userInfo = new CorpUserInfo ( ) ;
userInfo . setActive ( true ) ;
userInfo . setFirstName ( firstName , SetMode . IGNORE_NULL ) ;
userInfo . setLastName ( lastName , SetMode . IGNORE_NULL ) ;
2021-10-13 18:56:20 -07:00
userInfo . setFullName ( fullName , SetMode . IGNORE_NULL ) ;
2021-10-05 19:30:51 -07:00
userInfo . setEmail ( email , SetMode . IGNORE_NULL ) ;
2021-08-20 07:42:18 -07:00
// If there is a display name, use it. Otherwise fall back to full name.
2023-12-06 11:02:42 +05:30
userInfo . setDisplayName (
displayName = = null ? userInfo . getFullName ( ) : displayName , SetMode . IGNORE_NULL ) ;
2021-08-20 07:42:18 -07:00
final CorpUserEditableInfo editableInfo = new CorpUserEditableInfo ( ) ;
try {
if ( picture ! = null ) {
editableInfo . setPictureLink ( new Url ( picture . toURL ( ) . toString ( ) ) ) ;
}
} catch ( MalformedURLException e ) {
log . error ( " Failed to extract User Profile URL skipping. " , e ) ;
}
final CorpUserSnapshot corpUserSnapshot = new CorpUserSnapshot ( ) ;
corpUserSnapshot . setUrn ( urn ) ;
final CorpUserAspectArray aspects = new CorpUserAspectArray ( ) ;
aspects . add ( CorpUserAspect . create ( userInfo ) ) ;
aspects . add ( CorpUserAspect . create ( editableInfo ) ) ;
corpUserSnapshot . setAspects ( aspects ) ;
return corpUserSnapshot ;
}
2024-10-28 09:05:16 -05:00
public static Collection < String > getGroupNames (
CommonProfile profile , Object groupAttribute , String groupsClaimName ) {
Collection < String > groupNames = Collections . emptyList ( ) ;
try {
if ( groupAttribute instanceof Collection ) {
// List of group names
groupNames = ( Collection < String > ) profile . getAttribute ( groupsClaimName , Collection . class ) ;
} else if ( groupAttribute instanceof String ) {
String groupString = ( String ) groupAttribute ;
ObjectMapper objectMapper = new ObjectMapper ( ) ;
try {
// Json list of group names
groupNames = objectMapper . readValue ( groupString , new TypeReference < List < String > > ( ) { } ) ;
} catch ( Exception e ) {
groupNames = Arrays . asList ( groupString . split ( " , " ) ) ;
2023-12-26 09:04:05 -05:00
}
}
2024-10-28 09:05:16 -05:00
} catch ( Exception e ) {
log . error (
String . format (
" Failed to parse group names: Expected to find a list of strings for attribute with name %s, found %s " ,
groupsClaimName , profile . getAttribute ( groupsClaimName ) . getClass ( ) ) ) ;
}
return groupNames ;
2023-12-26 09:04:05 -05:00
}
2024-10-28 09:05:16 -05:00
2021-08-20 07:42:18 -07:00
private List < CorpGroupSnapshot > extractGroups ( CommonProfile profile ) {
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" Attempting to extract groups from OIDC profile %s " ,
profile . getAttributes ( ) . toString ( ) ) ) ;
2024-04-16 10:12:48 -05:00
final OidcConfigs configs = ( OidcConfigs ) ssoManager . getSsoProvider ( ) . configs ( ) ;
2021-08-20 07:42:18 -07:00
2023-12-06 11:02:42 +05:30
// First, attempt to extract a list of groups from the profile, using the group name attribute
// config.
2022-03-21 20:33:53 +00:00
final List < CorpGroupSnapshot > extractedGroups = new ArrayList < > ( ) ;
final List < String > groupsClaimNames =
2023-12-06 11:02:42 +05:30
new ArrayList < String > ( Arrays . asList ( configs . getGroupsClaimName ( ) . split ( " , " ) ) )
. stream ( ) . map ( String : : trim ) . collect ( Collectors . toList ( ) ) ;
2022-03-21 20:33:53 +00:00
for ( final String groupsClaimName : groupsClaimNames ) {
if ( profile . containsAttribute ( groupsClaimName ) ) {
try {
final List < CorpGroupSnapshot > groupSnapshots = new ArrayList < > ( ) ;
2024-10-28 09:05:16 -05:00
Collection < String > groupNames =
getGroupNames ( profile , profile . getAttribute ( groupsClaimName ) , groupsClaimName ) ;
2022-03-15 17:41:19 -07:00
2022-03-21 20:33:53 +00:00
for ( String groupName : groupNames ) {
// Create a basic CorpGroupSnapshot from the information.
try {
final CorpGroupInfo corpGroupInfo = new CorpGroupInfo ( ) ;
corpGroupInfo . setAdmins ( new CorpuserUrnArray ( ) ) ;
corpGroupInfo . setGroups ( new CorpGroupUrnArray ( ) ) ;
corpGroupInfo . setMembers ( new CorpuserUrnArray ( ) ) ;
corpGroupInfo . setEmail ( " " ) ;
corpGroupInfo . setDisplayName ( groupName ) ;
// To deal with the possibility of spaces, we url encode the URN group name.
2023-12-06 11:02:42 +05:30
final String urlEncodedGroupName =
URLEncoder . encode ( groupName , StandardCharsets . UTF_8 . toString ( ) ) ;
2022-03-21 20:33:53 +00:00
final CorpGroupUrn groupUrn = new CorpGroupUrn ( urlEncodedGroupName ) ;
final CorpGroupSnapshot corpGroupSnapshot = new CorpGroupSnapshot ( ) ;
corpGroupSnapshot . setUrn ( groupUrn ) ;
final CorpGroupAspectArray aspects = new CorpGroupAspectArray ( ) ;
aspects . add ( CorpGroupAspect . create ( corpGroupInfo ) ) ;
corpGroupSnapshot . setAspects ( aspects ) ;
groupSnapshots . add ( corpGroupSnapshot ) ;
} catch ( UnsupportedEncodingException ex ) {
2023-12-06 11:02:42 +05:30
log . error (
String . format (
" Failed to URL encoded extracted group name %s. Skipping " , groupName ) ) ;
2022-03-21 20:33:53 +00:00
}
2021-08-20 07:42:18 -07:00
}
2022-03-21 20:33:53 +00:00
if ( groupSnapshots . isEmpty ( ) ) {
2023-12-06 11:02:42 +05:30
log . warn (
String . format (
" Failed to extract groups: No OIDC claim with name %s found " , groupsClaimName ) ) ;
2022-03-21 20:33:53 +00:00
} else {
extractedGroups . addAll ( groupSnapshots ) ;
}
} catch ( Exception e ) {
2023-12-06 11:02:42 +05:30
log . error (
String . format (
" Failed to extract groups: Expected to find a list of strings for attribute with name %s, found %s " ,
groupsClaimName , profile . getAttribute ( groupsClaimName ) . getClass ( ) ) ) ;
2021-08-20 07:42:18 -07:00
}
}
}
2022-03-21 20:33:53 +00:00
return extractedGroups ;
2021-08-20 07:42:18 -07:00
}
private GroupMembership createGroupMembership ( final List < CorpGroupSnapshot > extractedGroups ) {
final GroupMembership groupMembershipAspect = new GroupMembership ( ) ;
2022-03-21 20:33:53 +00:00
groupMembershipAspect . setGroups (
2023-12-06 11:02:42 +05:30
new UrnArray (
extractedGroups . stream ( ) . map ( CorpGroupSnapshot : : getUrn ) . collect ( Collectors . toList ( ) ) ) ) ;
2021-08-20 07:42:18 -07:00
return groupMembershipAspect ;
}
2024-10-28 09:05:16 -05:00
private void tryProvisionUser (
@Nonnull OperationContext opContext , CorpUserSnapshot corpUserSnapshot ) {
2021-08-20 07:42:18 -07:00
log . debug ( String . format ( " Attempting to provision user with urn %s " , corpUserSnapshot . getUrn ( ) ) ) ;
// 1. Check if this user already exists.
try {
2024-04-16 10:12:48 -05:00
final Entity corpUser = systemEntityClient . get ( opContext , corpUserSnapshot . getUrn ( ) ) ;
2021-10-07 16:14:35 -07:00
final CorpUserSnapshot existingCorpUserSnapshot = corpUser . getValue ( ) . getCorpUserSnapshot ( ) ;
2021-08-20 07:42:18 -07:00
2022-03-21 20:33:53 +00:00
log . debug ( String . format ( " Fetched GMS user with urn %s " , corpUserSnapshot . getUrn ( ) ) ) ;
2021-08-20 07:42:18 -07:00
// If we find more than the key aspect, then the entity "exists".
2021-10-07 16:14:35 -07:00
if ( existingCorpUserSnapshot . getAspects ( ) . size ( ) < = 1 ) {
2022-03-21 20:33:53 +00:00
log . debug (
2023-12-06 11:02:42 +05:30
String . format (
" Extracted user that does not yet exist %s. Provisioning... " ,
corpUserSnapshot . getUrn ( ) ) ) ;
2021-08-20 07:42:18 -07:00
// 2. The user does not exist. Provision them.
final Entity newEntity = new Entity ( ) ;
newEntity . setValue ( Snapshot . create ( corpUserSnapshot ) ) ;
2024-04-16 10:12:48 -05:00
systemEntityClient . update ( opContext , newEntity ) ;
2021-08-20 07:42:18 -07:00
log . debug ( String . format ( " Successfully provisioned user %s " , corpUserSnapshot . getUrn ( ) ) ) ;
}
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" User %s already exists. Skipping provisioning " , corpUserSnapshot . getUrn ( ) ) ) ;
2021-08-20 07:42:18 -07:00
// Otherwise, the user exists. Skip provisioning.
} catch ( RemoteInvocationException e ) {
// Failing provisioning is something worth throwing about.
2023-12-06 11:02:42 +05:30
throw new RuntimeException (
String . format ( " Failed to provision user with urn %s. " , corpUserSnapshot . getUrn ( ) ) , e ) ;
2021-08-20 07:42:18 -07:00
}
}
2024-10-28 09:05:16 -05:00
private void tryProvisionGroups (
@Nonnull OperationContext opContext , List < CorpGroupSnapshot > corpGroups ) {
2021-08-20 07:42:18 -07:00
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" Attempting to provision groups with urns %s " ,
corpGroups . stream ( ) . map ( CorpGroupSnapshot : : getUrn ) . collect ( Collectors . toList ( ) ) ) ) ;
2021-08-20 07:42:18 -07:00
// 1. Check if this user already exists.
try {
2023-12-06 11:02:42 +05:30
final Set < Urn > urnsToFetch =
corpGroups . stream ( ) . map ( CorpGroupSnapshot : : getUrn ) . collect ( Collectors . toSet ( ) ) ;
2024-10-28 09:05:16 -05:00
final Map < Urn , Entity > existingGroups = systemEntityClient . batchGet ( opContext , urnsToFetch ) ;
2021-08-20 07:42:18 -07:00
log . debug ( String . format ( " Fetched GMS groups with urns %s " , existingGroups . keySet ( ) ) ) ;
final List < CorpGroupSnapshot > groupsToCreate = new ArrayList < > ( ) ;
for ( CorpGroupSnapshot extractedGroup : corpGroups ) {
if ( existingGroups . containsKey ( extractedGroup . getUrn ( ) ) ) {
final Entity groupEntity = existingGroups . get ( extractedGroup . getUrn ( ) ) ;
final CorpGroupSnapshot corpGroupSnapshot = groupEntity . getValue ( ) . getCorpGroupSnapshot ( ) ;
// If more than the key aspect exists, then the group already "exists".
if ( corpGroupSnapshot . getAspects ( ) . size ( ) < = 1 ) {
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" Extracted group that does not yet exist %s. Provisioning... " ,
corpGroupSnapshot . getUrn ( ) ) ) ;
2021-08-20 07:42:18 -07:00
groupsToCreate . add ( extractedGroup ) ;
}
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" Group %s already exists. Skipping provisioning " , corpGroupSnapshot . getUrn ( ) ) ) ;
2021-08-20 07:42:18 -07:00
} else {
// Should not occur until we stop returning default Key aspects for unrecognized entities.
2022-03-21 20:33:53 +00:00
log . debug (
2023-12-06 11:02:42 +05:30
String . format (
" Extracted group that does not yet exist %s. Provisioning... " ,
extractedGroup . getUrn ( ) ) ) ;
2021-08-20 07:42:18 -07:00
groupsToCreate . add ( extractedGroup ) ;
}
}
2022-03-21 20:33:53 +00:00
List < Urn > groupsToCreateUrns =
groupsToCreate . stream ( ) . map ( CorpGroupSnapshot : : getUrn ) . collect ( Collectors . toList ( ) ) ;
2021-08-20 07:42:18 -07:00
log . debug ( String . format ( " Provisioning groups with urns %s " , groupsToCreateUrns ) ) ;
// Now batch create all entities identified to create.
2024-10-28 09:05:16 -05:00
systemEntityClient . batchUpdate (
opContext ,
2023-12-06 11:02:42 +05:30
groupsToCreate . stream ( )
. map ( groupSnapshot - > new Entity ( ) . setValue ( Snapshot . create ( groupSnapshot ) ) )
2024-04-16 10:12:48 -05:00
. collect ( Collectors . toSet ( ) ) ) ;
2021-08-20 07:42:18 -07:00
log . debug ( String . format ( " Successfully provisioned groups with urns %s " , groupsToCreateUrns ) ) ;
} catch ( RemoteInvocationException e ) {
// Failing provisioning is something worth throwing about.
2023-12-06 11:02:42 +05:30
throw new RuntimeException (
String . format (
" Failed to provision groups with urns %s. " ,
corpGroups . stream ( ) . map ( CorpGroupSnapshot : : getUrn ) . collect ( Collectors . toList ( ) ) ) ,
e ) ;
2021-08-20 07:42:18 -07:00
}
}
2024-10-28 09:05:16 -05:00
private void updateGroupMembership (
@Nonnull OperationContext opContext , Urn urn , GroupMembership groupMembership ) {
2022-03-11 08:49:31 -08:00
log . debug ( String . format ( " Updating group membership for user %s " , urn ) ) ;
final MetadataChangeProposal proposal = new MetadataChangeProposal ( ) ;
proposal . setEntityUrn ( urn ) ;
proposal . setEntityType ( CORP_USER_ENTITY_NAME ) ;
proposal . setAspectName ( GROUP_MEMBERSHIP_ASPECT_NAME ) ;
2022-03-29 18:32:04 -07:00
proposal . setAspect ( GenericRecordUtils . serializeAspect ( groupMembership ) ) ;
2022-03-11 08:49:31 -08:00
proposal . setChangeType ( ChangeType . UPSERT ) ;
try {
2024-04-16 10:12:48 -05:00
systemEntityClient . ingestProposal ( opContext , proposal ) ;
2022-03-11 08:49:31 -08:00
} catch ( RemoteInvocationException e ) {
2023-12-06 11:02:42 +05:30
throw new RuntimeException (
String . format ( " Failed to update group membership for user with urn %s " , urn ) , e ) ;
2022-03-11 08:49:31 -08:00
}
}
2024-04-16 10:12:48 -05:00
private void verifyPreProvisionedUser ( @Nonnull OperationContext opContext , CorpuserUrn urn ) {
2023-12-06 11:02:42 +05:30
// Validate that the user exists in the system (there is more than just a key aspect for them,
// as of today).
2021-08-20 07:42:18 -07:00
try {
2024-04-16 10:12:48 -05:00
final Entity corpUser = systemEntityClient . get ( opContext , urn ) ;
2021-08-20 07:42:18 -07:00
log . debug ( String . format ( " Fetched GMS user with urn %s " , urn ) ) ;
// If we find more than the key aspect, then the entity "exists".
if ( corpUser . getValue ( ) . getCorpUserSnapshot ( ) . getAspects ( ) . size ( ) < = 1 ) {
2023-12-06 11:02:42 +05:30
log . debug (
String . format (
" Found user that does not yet exist %s. Invalid login attempt. Throwing... " , urn ) ) ;
throw new RuntimeException (
String . format (
" User with urn %s has not yet been provisioned in DataHub. "
+ " Please contact your DataHub admin to provision an account. " ,
urn ) ) ;
2021-08-20 07:42:18 -07:00
}
// Otherwise, the user exists.
} catch ( RemoteInvocationException e ) {
// Failing validation is something worth throwing about.
throw new RuntimeException ( String . format ( " Failed to validate user with urn %s. " , urn ) , e ) ;
}
}
2024-10-28 09:05:16 -05:00
private void setUserStatus (
@Nonnull OperationContext opContext , final Urn urn , final CorpUserStatus newStatus )
throws Exception {
2021-10-07 16:14:35 -07:00
// Update status aspect to be active.
final MetadataChangeProposal proposal = new MetadataChangeProposal ( ) ;
proposal . setEntityUrn ( urn ) ;
proposal . setEntityType ( Constants . CORP_USER_ENTITY_NAME ) ;
proposal . setAspectName ( Constants . CORP_USER_STATUS_ASPECT_NAME ) ;
2022-03-29 18:32:04 -07:00
proposal . setAspect ( GenericRecordUtils . serializeAspect ( newStatus ) ) ;
2021-10-07 16:14:35 -07:00
proposal . setChangeType ( ChangeType . UPSERT ) ;
2024-04-16 10:12:48 -05:00
systemEntityClient . ingestProposal ( opContext , proposal ) ;
2021-10-07 16:14:35 -07:00
}
2021-08-20 07:42:18 -07:00
private Optional < String > extractRegexGroup ( final String patternStr , final String target ) {
final Pattern pattern = Pattern . compile ( patternStr ) ;
final Matcher matcher = pattern . matcher ( target ) ;
if ( matcher . find ( ) ) {
final String extractedValue = matcher . group ( ) ;
return Optional . of ( extractedValue ) ;
}
return Optional . empty ( ) ;
}
}