diff --git a/datahub-frontend/app/auth/AuthModule.java b/datahub-frontend/app/auth/AuthModule.java index b95515684f01fc..3de0170fc70389 100644 --- a/datahub-frontend/app/auth/AuthModule.java +++ b/datahub-frontend/app/auth/AuthModule.java @@ -181,7 +181,12 @@ protected OperationContext provideOperationContext( final Authentication systemAuthentication, final ConfigurationProvider configurationProvider) { ActorContext systemActorContext = - ActorContext.builder().systemAuth(true).authentication(systemAuthentication).build(); + ActorContext.builder() + .systemAuth(true) + .authentication(systemAuthentication) + .enforceExistenceEnabled( + configurationProvider.getAuthentication().isEnforceExistenceEnabled()) + .build(); OperationContextConfig systemConfig = OperationContextConfig.builder() .viewAuthorizationConfiguration(configurationProvider.getAuthorization().getView()) @@ -197,7 +202,9 @@ protected OperationContext provideOperationContext( .entityRegistryContext(EntityRegistryContext.builder().build(EmptyEntityRegistry.EMPTY)) .validationContext(ValidationContext.builder().alternateValidation(false).build()) .retrieverContext(RetrieverContext.EMPTY) - .build(systemAuthentication); + .build( + systemAuthentication, + configurationProvider.getAuthentication().isEnforceExistenceEnabled()); } @Provides diff --git a/datahub-frontend/app/config/ConfigurationProvider.java b/datahub-frontend/app/config/ConfigurationProvider.java index 97e916769a6c45..9bc28be1bfc89f 100644 --- a/datahub-frontend/app/config/ConfigurationProvider.java +++ b/datahub-frontend/app/config/ConfigurationProvider.java @@ -1,5 +1,6 @@ package config; +import com.datahub.authentication.AuthenticationConfiguration; import com.datahub.authorization.AuthorizationConfiguration; import com.linkedin.metadata.config.VisualConfiguration; import com.linkedin.metadata.config.cache.CacheConfiguration; @@ -30,4 +31,7 @@ public class ConfigurationProvider { /** Configuration for authorization */ private AuthorizationConfiguration authorization; + + /** Configuration for authentication */ + private AuthenticationConfiguration authentication; } diff --git a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java index fdd84da6044f73..d0493019a40af2 100644 --- a/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java +++ b/datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java @@ -194,7 +194,8 @@ protected OperationContext javaSystemOperationContext( ValidationContext.builder() .alternateValidation( configurationProvider.getFeatureFlags().isAlternateMCPValidation()) - .build()); + .build(), + true); entityServiceAspectRetriever.setSystemOperationContext(systemOperationContext); systemGraphRetriever.setSystemOperationContext(systemOperationContext); diff --git a/docs/authentication/guides/add-users.md b/docs/authentication/guides/add-users.md index 30da5c9f229f94..dbd44b63086783 100644 --- a/docs/authentication/guides/add-users.md +++ b/docs/authentication/guides/add-users.md @@ -1,3 +1,6 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + # Onboarding Users to DataHub New user accounts can be provisioned on DataHub in 3 ways: @@ -94,6 +97,11 @@ using this mechanism. It is highly recommended that admins change or remove the ## Adding new users using a user.props file +:::NOTE +Adding users via the `user.props` will require disabling existence checks on GMS using the `METADATA_SERVICE_AUTH_ENFORCE_EXISTENCE_ENABLED=false` environment variable or using the API to enable the user prior to login. +The directions below demonstrate using the API to enable the user. +::: + To define a set of username / password combinations that should be allowed to log in to DataHub (in addition to the root 'datahub' user), create a new file called `user.props` at the file path `${HOME}/.datahub/plugins/frontend/auth/user.props` within the `datahub-frontend-react` container or pod. @@ -107,6 +115,28 @@ janesmith:janespassword johndoe:johnspassword ``` +In order to enable the user access with the credential defined in `user.props`, set the `status` aspect on the user with an Admin user. This can be done using an API call or via the [OpenAPI UI interface](/docs/api/openapi/openapi-usage-guide.md). + + + + +Example enabling login for the `janesmith` user from the example above. Make sure to update the example with your access token. + +```shell +curl -X 'POST' \ + 'http://localhost:9002/openapi/v3/entity/corpuser/urn%3Ali%3Acorpuser%3Ajanesmith/status?async=false&systemMetadata=false&createIfEntityNotExists=false&createIfNotExists=true' \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H 'Authorization: Bearer ' \ + -d '{ + "value": { + "removed": false + } +}' +``` + + + Once you've saved the file, simply start the DataHub containers & navigate to `http://localhost:9002/login` to verify that your new credentials work. diff --git a/docs/how/updating-datahub.md b/docs/how/updating-datahub.md index 19261da23bcf96..07577079d66d12 100644 --- a/docs/how/updating-datahub.md +++ b/docs/how/updating-datahub.md @@ -66,6 +66,7 @@ This file documents any backwards-incompatible changes in DataHub and assists pe changed to NOT fill out `created` and `lastModified` auditstamps by default for input and output dataset edges. This should not have any user-observable impact (time-based lineage viz will still continue working based on observed time), but could break assumptions previously being made by clients. +- #12158 - Users provisioned with `user.props` will need to be enabled before login in order to be granted access to DataHub. ### Potential Downtime diff --git a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java index 5e387d7d88292a..968f0dd4dd61ef 100644 --- a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java +++ b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java @@ -137,7 +137,7 @@ protected OperationContext sampleDataOperationContext( return testOpContext.toBuilder() .searchContext(SearchContext.builder().indexConvention(indexConvention).build()) - .build(testOpContext.getSessionAuthentication()); + .build(testOpContext.getSessionAuthentication(), true); } @Bean(name = "longTailOperationContext") @@ -148,7 +148,7 @@ protected OperationContext longTailOperationContext( return testOpContext.toBuilder() .searchContext(SearchContext.builder().indexConvention(indexConvention).build()) - .build(testOpContext.getSessionAuthentication()); + .build(testOpContext.getSessionAuthentication(), true); } protected EntityIndexBuilders entityIndexBuildersHelper(OperationContext opContext) { diff --git a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java index b7b698c73ddac3..26443e019829bf 100644 --- a/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java +++ b/metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java @@ -162,7 +162,7 @@ protected OperationContext searchLineageOperationContext( return testOpContext.toBuilder() .searchContext(SearchContext.builder().indexConvention(indexConvention).build()) - .build(testOpContext.getSessionAuthentication()); + .build(testOpContext.getSessionAuthentication(), true); } @Bean(name = "searchLineageESIndexBuilder") diff --git a/metadata-jobs/mae-consumer/src/test/java/com/linkedin/metadata/kafka/hook/spring/MCLSpringCommonTestConfiguration.java b/metadata-jobs/mae-consumer/src/test/java/com/linkedin/metadata/kafka/hook/spring/MCLSpringCommonTestConfiguration.java index f16c9dbd82e749..c92749385145de 100644 --- a/metadata-jobs/mae-consumer/src/test/java/com/linkedin/metadata/kafka/hook/spring/MCLSpringCommonTestConfiguration.java +++ b/metadata-jobs/mae-consumer/src/test/java/com/linkedin/metadata/kafka/hook/spring/MCLSpringCommonTestConfiguration.java @@ -95,7 +95,8 @@ public OperationContext operationContext( mock(ServicesRegistryContext.class), indexConvention, TestOperationContexts.emptyActiveUsersRetrieverContext(() -> entityRegistry), - mock(ValidationContext.class)); + mock(ValidationContext.class), + true); } @MockBean SpringStandardPluginConfiguration springStandardPluginConfiguration; diff --git a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ActorContext.java b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ActorContext.java index c08b7fad4dee32..11e38dfb179e0c 100644 --- a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ActorContext.java +++ b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ActorContext.java @@ -29,23 +29,31 @@ @EqualsAndHashCode public class ActorContext implements ContextInterface { - public static ActorContext asSystem(Authentication systemAuthentication) { - return ActorContext.builder().systemAuth(true).authentication(systemAuthentication).build(); + public static ActorContext asSystem( + Authentication systemAuthentication, boolean enforceExistenceEnabled) { + return ActorContext.builder() + .systemAuth(true) + .authentication(systemAuthentication) + .enforceExistenceEnabled(enforceExistenceEnabled) + .build(); } public static ActorContext asSessionRestricted( Authentication authentication, Set dataHubPolicySet, - Collection groupMembership) { + Collection groupMembership, + boolean enforceExistenceEnabled) { return ActorContext.builder() .systemAuth(false) .authentication(authentication) .policyInfoSet(dataHubPolicySet) .groupMembership(groupMembership) + .enforceExistenceEnabled(enforceExistenceEnabled) .build(); } private final Authentication authentication; + private final boolean enforceExistenceEnabled; @EqualsAndHashCode.Exclude @Builder.Default private final Set policyInfoSet = Collections.emptySet(); @@ -79,7 +87,7 @@ public boolean isActive(AspectRetriever aspectRetriever) { Map aspectMap = urnAspectMap.getOrDefault(selfUrn, Map.of()); - if (!aspectMap.containsKey(CORP_USER_KEY_ASPECT_NAME)) { + if (enforceExistenceEnabled && !aspectMap.containsKey(CORP_USER_KEY_ASPECT_NAME)) { // user is hard deleted return false; } diff --git a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java index 9158129235b39e..30255f7ebcac36 100644 --- a/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java +++ b/metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java @@ -152,7 +152,8 @@ public static OperationContext asSystem( @Nullable ServicesRegistryContext servicesRegistryContext, @Nullable IndexConvention indexConvention, @Nullable RetrieverContext retrieverContext, - @Nonnull ValidationContext validationContext) { + @Nonnull ValidationContext validationContext, + boolean enforceExistenceEnabled) { return asSystem( config, systemAuthentication, @@ -161,7 +162,8 @@ public static OperationContext asSystem( indexConvention, retrieverContext, validationContext, - ObjectMapperContext.DEFAULT); + ObjectMapperContext.DEFAULT, + enforceExistenceEnabled); } public static OperationContext asSystem( @@ -172,10 +174,15 @@ public static OperationContext asSystem( @Nullable IndexConvention indexConvention, @Nullable RetrieverContext retrieverContext, @Nonnull ValidationContext validationContext, - @Nonnull ObjectMapperContext objectMapperContext) { + @Nonnull ObjectMapperContext objectMapperContext, + boolean enforceExistenceEnabled) { ActorContext systemActorContext = - ActorContext.builder().systemAuth(true).authentication(systemAuthentication).build(); + ActorContext.builder() + .systemAuth(true) + .authentication(systemAuthentication) + .enforceExistenceEnabled(enforceExistenceEnabled) + .build(); OperationContextConfig systemConfig = config.toBuilder().allowSystemAuthentication(true).build(); SearchContext systemSearchContext = @@ -457,13 +464,16 @@ public int hashCode() { public static class OperationContextBuilder { @Nonnull - public OperationContext build(@Nonnull Authentication sessionAuthentication) { - return build(sessionAuthentication, false); + public OperationContext build( + @Nonnull Authentication sessionAuthentication, boolean enforceExistenceEnabled) { + return build(sessionAuthentication, false, enforceExistenceEnabled); } @Nonnull public OperationContext build( - @Nonnull Authentication sessionAuthentication, boolean skipCache) { + @Nonnull Authentication sessionAuthentication, + boolean skipCache, + boolean enforceExistenceEnabled) { final Urn actorUrn = UrnUtils.getUrn(sessionAuthentication.getActor().toUrnStr()); final ActorContext sessionActor = ActorContext.builder() @@ -476,6 +486,7 @@ public OperationContext build( .equals(sessionAuthentication.getActor())) .policyInfoSet(this.authorizationContext.getAuthorizer().getActorPolicies(actorUrn)) .groupMembership(this.authorizationContext.getAuthorizer().getActorGroups(actorUrn)) + .enforceExistenceEnabled(enforceExistenceEnabled) .build(); return build(sessionActor, skipCache); } diff --git a/metadata-operation-context/src/main/java/io/datahubproject/test/metadata/context/TestOperationContexts.java b/metadata-operation-context/src/main/java/io/datahubproject/test/metadata/context/TestOperationContexts.java index 4abfbb196f067c..92d62d42295b92 100644 --- a/metadata-operation-context/src/main/java/io/datahubproject/test/metadata/context/TestOperationContexts.java +++ b/metadata-operation-context/src/main/java/io/datahubproject/test/metadata/context/TestOperationContexts.java @@ -260,7 +260,8 @@ public static OperationContext systemContext( servicesRegistryContext, indexConvention, retrieverContext, - validationContext); + validationContext, + true); if (postConstruct != null) { postConstruct.accept(operationContext); diff --git a/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/ActorContextTest.java b/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/ActorContextTest.java index 15fe2bc277b9b9..de6f71408e2589 100644 --- a/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/ActorContextTest.java +++ b/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/ActorContextTest.java @@ -87,42 +87,43 @@ public void actorContextId() { Authentication userAuth = new Authentication(new Actor(ActorType.USER, "USER"), ""); assertEquals( - ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of()).getCacheKeyComponent(), - ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of()).getCacheKeyComponent(), + ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of(), true).getCacheKeyComponent(), + ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of(), true).getCacheKeyComponent(), "Expected equality across instances"); assertEquals( - ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of()).getCacheKeyComponent(), + ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of(), true).getCacheKeyComponent(), ActorContext.asSessionRestricted( - userAuth, Set.of(), Set.of(UrnUtils.getUrn("urn:li:corpGroup:group1"))) + userAuth, Set.of(), Set.of(UrnUtils.getUrn("urn:li:corpGroup:group1")), true) .getCacheKeyComponent(), "Expected no impact to cache context from group membership"); assertEquals( - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of()) + ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of(), true) .getCacheKeyComponent(), - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of()) + ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of(), true) .getCacheKeyComponent(), "Expected equality when non-ownership policies are identical"); assertNotEquals( - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC_RESOURCE, POLICY_D), Set.of()) + ActorContext.asSessionRestricted( + userAuth, Set.of(POLICY_ABC_RESOURCE, POLICY_D), Set.of(), true) .getCacheKeyComponent(), - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of()) + ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of(), true) .getCacheKeyComponent(), "Expected differences with non-identical resource policy"); assertNotEquals( - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER), Set.of()) + ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER), Set.of(), true) .getCacheKeyComponent(), - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of()) + ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of(), true) .getCacheKeyComponent(), "Expected differences with ownership policy"); assertNotEquals( - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER_TYPE), Set.of()) + ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER_TYPE), Set.of(), true) .getCacheKeyComponent(), - ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of()) + ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of(), true) .getCacheKeyComponent(), "Expected differences with ownership type policy"); } diff --git a/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/OperationContextTest.java b/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/OperationContextTest.java index f77b244d8f2d86..a2575c1c562209 100644 --- a/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/OperationContextTest.java +++ b/metadata-operation-context/src/test/java/io/datahubproject/metadata/context/OperationContextTest.java @@ -27,7 +27,8 @@ public void testSystemPrivilegeEscalation() { mock(ServicesRegistryContext.class), null, TestOperationContexts.emptyActiveUsersRetrieverContext(null), - mock(ValidationContext.class)); + mock(ValidationContext.class), + true); OperationContext opContext = systemOpContext.asSession(RequestContext.TEST, Authorizer.EMPTY, userAuth); @@ -51,7 +52,7 @@ public void testSystemPrivilegeEscalation() { systemOpContext.getOperationContextConfig().toBuilder() .allowSystemAuthentication(false) .build()) - .build(userAuth); + .build(userAuth, true); assertEquals( opContextNoSystem.getAuthentication(), diff --git a/metadata-service/auth-config/src/main/java/com/datahub/authentication/AuthenticationConfiguration.java b/metadata-service/auth-config/src/main/java/com/datahub/authentication/AuthenticationConfiguration.java index 442263bbd6b43e..81cc5e60552a77 100644 --- a/metadata-service/auth-config/src/main/java/com/datahub/authentication/AuthenticationConfiguration.java +++ b/metadata-service/auth-config/src/main/java/com/datahub/authentication/AuthenticationConfiguration.java @@ -9,6 +9,9 @@ public class AuthenticationConfiguration { /** Whether authentication is enabled */ private boolean enabled; + /** Whether user existence is enforced */ + private boolean enforceExistenceEnabled; + /** * List of configurations for {@link com.datahub.plugins.auth.authentication.Authenticator}s to be * registered diff --git a/metadata-service/auth-impl/src/test/java/com/datahub/authorization/DataHubAuthorizerTest.java b/metadata-service/auth-impl/src/test/java/com/datahub/authorization/DataHubAuthorizerTest.java index 4437682bfeb0a1..ce9c636be16ac7 100644 --- a/metadata-service/auth-impl/src/test/java/com/datahub/authorization/DataHubAuthorizerTest.java +++ b/metadata-service/auth-impl/src/test/java/com/datahub/authorization/DataHubAuthorizerTest.java @@ -320,7 +320,8 @@ public void setupTest() throws Exception { mock(ServicesRegistryContext.class), mock(IndexConvention.class), mock(RetrieverContext.class), - mock(ValidationContext.class)); + mock(ValidationContext.class), + true); _dataHubAuthorizer = new DataHubAuthorizer( diff --git a/metadata-service/configuration/src/main/resources/application.yaml b/metadata-service/configuration/src/main/resources/application.yaml index f6fa4a37fdadbc..c029cb4648d012 100644 --- a/metadata-service/configuration/src/main/resources/application.yaml +++ b/metadata-service/configuration/src/main/resources/application.yaml @@ -6,6 +6,9 @@ authentication: # Enable if you want all requests to the Metadata Service to be authenticated. enabled: ${METADATA_SERVICE_AUTH_ENABLED:true} + # Disable if you want to skip validation of deleted user's tokens + enforceExistenceEnabled: ${METADATA_SERVICE_AUTH_ENFORCE_EXISTENCE_ENABLED:true} + # Required if enabled is true! A configurable chain of Authenticators authenticators: # Required for authenticating requests with DataHub-issued Access Tokens - best not to remove. diff --git a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/context/SystemOperationContextFactory.java b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/context/SystemOperationContextFactory.java index 3e2823591e168c..78107cc0ecc900 100644 --- a/metadata-service/factories/src/main/java/com/linkedin/gms/factory/context/SystemOperationContextFactory.java +++ b/metadata-service/factories/src/main/java/com/linkedin/gms/factory/context/SystemOperationContextFactory.java @@ -79,7 +79,8 @@ protected OperationContext javaSystemOperationContext( ValidationContext.builder() .alternateValidation( configurationProvider.getFeatureFlags().isAlternateMCPValidation()) - .build()); + .build(), + configurationProvider.getAuthentication().isEnforceExistenceEnabled()); entityClientAspectRetriever.setSystemOperationContext(systemOperationContext); entityServiceAspectRetriever.setSystemOperationContext(systemOperationContext); @@ -134,7 +135,8 @@ protected OperationContext restliSystemOperationContext( ValidationContext.builder() .alternateValidation( configurationProvider.getFeatureFlags().isAlternateMCPValidation()) - .build()); + .build(), + configurationProvider.getAuthentication().isEnforceExistenceEnabled()); entityClientAspectRetriever.setSystemOperationContext(systemOperationContext); systemGraphRetriever.setSystemOperationContext(systemOperationContext); diff --git a/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataPlatformInstancesStepTest.java b/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataPlatformInstancesStepTest.java index cc21819cf4ab58..b47c779f768a9b 100644 --- a/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataPlatformInstancesStepTest.java +++ b/metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataPlatformInstancesStepTest.java @@ -87,7 +87,7 @@ public void testExecuteChecksKeySpecForAllUrns() throws Exception { mockOpContext = mockOpContext.toBuilder() .entityRegistryContext(spyEntityRegistryContext) - .build(mockOpContext.getSessionAuthentication()); + .build(mockOpContext.getSessionAuthentication(), true); mockDBWithWorkToDo(migrationsDao, countOfCorpUserEntities, countOfChartEntities);