mirror of
https://github.com/datahub-project/datahub.git
synced 2025-06-27 05:03:31 +00:00
feat(auth): user.props authentication (#12259)
This commit is contained in:
parent
f396d8d87a
commit
4a898e1594
@ -181,7 +181,12 @@ public class AuthModule extends AbstractModule {
|
||||
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 @@ public class AuthModule extends AbstractModule {
|
||||
.entityRegistryContext(EntityRegistryContext.builder().build(EmptyEntityRegistry.EMPTY))
|
||||
.validationContext(ValidationContext.builder().alternateValidation(false).build())
|
||||
.retrieverContext(RetrieverContext.EMPTY)
|
||||
.build(systemAuthentication);
|
||||
.build(
|
||||
systemAuthentication,
|
||||
configurationProvider.getAuthentication().isEnforceExistenceEnabled());
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -194,7 +194,8 @@ public class SystemUpdateConfig {
|
||||
ValidationContext.builder()
|
||||
.alternateValidation(
|
||||
configurationProvider.getFeatureFlags().isAlternateMCPValidation())
|
||||
.build());
|
||||
.build(),
|
||||
true);
|
||||
|
||||
entityServiceAspectRetriever.setSystemOperationContext(systemOperationContext);
|
||||
systemGraphRetriever.setSystemOperationContext(systemOperationContext);
|
||||
|
@ -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).
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="openapi" label="OpenAPI" default>
|
||||
|
||||
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 <access token>' \
|
||||
-d '{
|
||||
"value": {
|
||||
"removed": false
|
||||
}
|
||||
}'
|
||||
```
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
Once you've saved the file, simply start the DataHub containers & navigate to `http://localhost:9002/login`
|
||||
to verify that your new credentials work.
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -137,7 +137,7 @@ public class SampleDataFixtureConfiguration {
|
||||
|
||||
return testOpContext.toBuilder()
|
||||
.searchContext(SearchContext.builder().indexConvention(indexConvention).build())
|
||||
.build(testOpContext.getSessionAuthentication());
|
||||
.build(testOpContext.getSessionAuthentication(), true);
|
||||
}
|
||||
|
||||
@Bean(name = "longTailOperationContext")
|
||||
@ -148,7 +148,7 @@ public class SampleDataFixtureConfiguration {
|
||||
|
||||
return testOpContext.toBuilder()
|
||||
.searchContext(SearchContext.builder().indexConvention(indexConvention).build())
|
||||
.build(testOpContext.getSessionAuthentication());
|
||||
.build(testOpContext.getSessionAuthentication(), true);
|
||||
}
|
||||
|
||||
protected EntityIndexBuilders entityIndexBuildersHelper(OperationContext opContext) {
|
||||
|
@ -162,7 +162,7 @@ public class SearchLineageFixtureConfiguration {
|
||||
|
||||
return testOpContext.toBuilder()
|
||||
.searchContext(SearchContext.builder().indexConvention(indexConvention).build())
|
||||
.build(testOpContext.getSessionAuthentication());
|
||||
.build(testOpContext.getSessionAuthentication(), true);
|
||||
}
|
||||
|
||||
@Bean(name = "searchLineageESIndexBuilder")
|
||||
|
@ -95,7 +95,8 @@ public class MCLSpringCommonTestConfiguration {
|
||||
mock(ServicesRegistryContext.class),
|
||||
indexConvention,
|
||||
TestOperationContexts.emptyActiveUsersRetrieverContext(() -> entityRegistry),
|
||||
mock(ValidationContext.class));
|
||||
mock(ValidationContext.class),
|
||||
true);
|
||||
}
|
||||
|
||||
@MockBean SpringStandardPluginConfiguration springStandardPluginConfiguration;
|
||||
|
@ -29,23 +29,31 @@ import lombok.Getter;
|
||||
@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<DataHubPolicyInfo> dataHubPolicySet,
|
||||
Collection<Urn> groupMembership) {
|
||||
Collection<Urn> 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<DataHubPolicyInfo> policyInfoSet = Collections.emptySet();
|
||||
@ -79,7 +87,7 @@ public class ActorContext implements ContextInterface {
|
||||
|
||||
Map<String, Aspect> 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;
|
||||
}
|
||||
|
@ -152,7 +152,8 @@ public class OperationContext implements AuthorizationSession {
|
||||
@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 class OperationContext implements AuthorizationSession {
|
||||
indexConvention,
|
||||
retrieverContext,
|
||||
validationContext,
|
||||
ObjectMapperContext.DEFAULT);
|
||||
ObjectMapperContext.DEFAULT,
|
||||
enforceExistenceEnabled);
|
||||
}
|
||||
|
||||
public static OperationContext asSystem(
|
||||
@ -172,10 +174,15 @@ public class OperationContext implements AuthorizationSession {
|
||||
@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 class OperationContext implements AuthorizationSession {
|
||||
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 class OperationContext implements AuthorizationSession {
|
||||
.equals(sessionAuthentication.getActor()))
|
||||
.policyInfoSet(this.authorizationContext.getAuthorizer().getActorPolicies(actorUrn))
|
||||
.groupMembership(this.authorizationContext.getAuthorizer().getActorGroups(actorUrn))
|
||||
.enforceExistenceEnabled(enforceExistenceEnabled)
|
||||
.build();
|
||||
return build(sessionActor, skipCache);
|
||||
}
|
||||
|
@ -260,7 +260,8 @@ public class TestOperationContexts {
|
||||
servicesRegistryContext,
|
||||
indexConvention,
|
||||
retrieverContext,
|
||||
validationContext);
|
||||
validationContext,
|
||||
true);
|
||||
|
||||
if (postConstruct != null) {
|
||||
postConstruct.accept(operationContext);
|
||||
|
@ -87,42 +87,43 @@ public class ActorContextTest {
|
||||
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");
|
||||
}
|
||||
|
@ -27,7 +27,8 @@ public class OperationContextTest {
|
||||
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 class OperationContextTest {
|
||||
systemOpContext.getOperationContextConfig().toBuilder()
|
||||
.allowSystemAuthentication(false)
|
||||
.build())
|
||||
.build(userAuth);
|
||||
.build(userAuth, true);
|
||||
|
||||
assertEquals(
|
||||
opContextNoSystem.getAuthentication(),
|
||||
|
@ -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
|
||||
|
@ -320,7 +320,8 @@ public class DataHubAuthorizerTest {
|
||||
mock(ServicesRegistryContext.class),
|
||||
mock(IndexConvention.class),
|
||||
mock(RetrieverContext.class),
|
||||
mock(ValidationContext.class));
|
||||
mock(ValidationContext.class),
|
||||
true);
|
||||
|
||||
_dataHubAuthorizer =
|
||||
new DataHubAuthorizer(
|
||||
|
@ -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.
|
||||
|
@ -79,7 +79,8 @@ public class SystemOperationContextFactory {
|
||||
ValidationContext.builder()
|
||||
.alternateValidation(
|
||||
configurationProvider.getFeatureFlags().isAlternateMCPValidation())
|
||||
.build());
|
||||
.build(),
|
||||
configurationProvider.getAuthentication().isEnforceExistenceEnabled());
|
||||
|
||||
entityClientAspectRetriever.setSystemOperationContext(systemOperationContext);
|
||||
entityServiceAspectRetriever.setSystemOperationContext(systemOperationContext);
|
||||
@ -134,7 +135,8 @@ public class SystemOperationContextFactory {
|
||||
ValidationContext.builder()
|
||||
.alternateValidation(
|
||||
configurationProvider.getFeatureFlags().isAlternateMCPValidation())
|
||||
.build());
|
||||
.build(),
|
||||
configurationProvider.getAuthentication().isEnforceExistenceEnabled());
|
||||
|
||||
entityClientAspectRetriever.setSystemOperationContext(systemOperationContext);
|
||||
systemGraphRetriever.setSystemOperationContext(systemOperationContext);
|
||||
|
@ -87,7 +87,7 @@ public class IngestDataPlatformInstancesStepTest {
|
||||
mockOpContext =
|
||||
mockOpContext.toBuilder()
|
||||
.entityRegistryContext(spyEntityRegistryContext)
|
||||
.build(mockOpContext.getSessionAuthentication());
|
||||
.build(mockOpContext.getSessionAuthentication(), true);
|
||||
|
||||
mockDBWithWorkToDo(migrationsDao, countOfCorpUserEntities, countOfChartEntities);
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user