Skip to content

Commit 4b86dbe

Browse files
committed
feat(auth): user.props authentication
* document breaking change to user.props * add flag for legacy behavior
1 parent 7f64ffd commit 4b86dbe

File tree

18 files changed

+112
-37
lines changed

18 files changed

+112
-37
lines changed

datahub-frontend/app/auth/AuthModule.java

+9-2
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,12 @@ protected OperationContext provideOperationContext(
181181
final Authentication systemAuthentication,
182182
final ConfigurationProvider configurationProvider) {
183183
ActorContext systemActorContext =
184-
ActorContext.builder().systemAuth(true).authentication(systemAuthentication).build();
184+
ActorContext.builder()
185+
.systemAuth(true)
186+
.authentication(systemAuthentication)
187+
.enforceExistenceEnabled(
188+
configurationProvider.getAuthentication().isEnforceExistenceEnabled())
189+
.build();
185190
OperationContextConfig systemConfig =
186191
OperationContextConfig.builder()
187192
.viewAuthorizationConfiguration(configurationProvider.getAuthorization().getView())
@@ -197,7 +202,9 @@ protected OperationContext provideOperationContext(
197202
.entityRegistryContext(EntityRegistryContext.builder().build(EmptyEntityRegistry.EMPTY))
198203
.validationContext(ValidationContext.builder().alternateValidation(false).build())
199204
.retrieverContext(RetrieverContext.EMPTY)
200-
.build(systemAuthentication);
205+
.build(
206+
systemAuthentication,
207+
configurationProvider.getAuthentication().isEnforceExistenceEnabled());
201208
}
202209

203210
@Provides

datahub-frontend/app/config/ConfigurationProvider.java

+4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package config;
22

3+
import com.datahub.authentication.AuthenticationConfiguration;
34
import com.datahub.authorization.AuthorizationConfiguration;
45
import com.linkedin.metadata.config.VisualConfiguration;
56
import com.linkedin.metadata.config.cache.CacheConfiguration;
@@ -30,4 +31,7 @@ public class ConfigurationProvider {
3031

3132
/** Configuration for authorization */
3233
private AuthorizationConfiguration authorization;
34+
35+
/** Configuration for authentication */
36+
private AuthenticationConfiguration authentication;
3337
}

datahub-upgrade/src/main/java/com/linkedin/datahub/upgrade/config/SystemUpdateConfig.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,8 @@ protected OperationContext javaSystemOperationContext(
194194
ValidationContext.builder()
195195
.alternateValidation(
196196
configurationProvider.getFeatureFlags().isAlternateMCPValidation())
197-
.build());
197+
.build(),
198+
true);
198199

199200
entityServiceAspectRetriever.setSystemOperationContext(systemOperationContext);
200201
systemGraphRetriever.setSystemOperationContext(systemOperationContext);

docs/authentication/guides/add-users.md

+30
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import Tabs from '@theme/Tabs';
2+
import TabItem from '@theme/TabItem';
3+
14
# Onboarding Users to DataHub
25

36
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
9497

9598
## Adding new users using a user.props file
9699

100+
:::NOTE
101+
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.
102+
The directions below demonstrate using the API to enable the user.
103+
:::
104+
97105
To define a set of username / password combinations that should be allowed to log in to DataHub (in addition to the root 'datahub' user),
98106
create a new file called `user.props` at the file path `${HOME}/.datahub/plugins/frontend/auth/user.props` within the `datahub-frontend-react` container
99107
or pod.
@@ -107,6 +115,28 @@ janesmith:janespassword
107115
johndoe:johnspassword
108116
```
109117

118+
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).
119+
120+
<Tabs>
121+
<TabItem value="openapi" label="OpenAPI" default>
122+
123+
Example enabling login for the `janesmith` user from the example above. Make sure to update the example with your access token.
124+
125+
```shell
126+
curl -X 'POST' \
127+
'http://localhost:9002/openapi/v3/entity/corpuser/urn%3Ali%3Acorpuser%3Ajanesmith/status?async=false&systemMetadata=false&createIfEntityNotExists=false&createIfNotExists=true' \
128+
-H 'accept: application/json' \
129+
-H 'Content-Type: application/json' \
130+
-H 'Authorization: Bearer <access token>' \
131+
-d '{
132+
"value": {
133+
"removed": false
134+
}
135+
}'
136+
```
137+
</TabItem>
138+
</Tabs>
139+
110140
Once you've saved the file, simply start the DataHub containers & navigate to `http://localhost:9002/login`
111141
to verify that your new credentials work.
112142

docs/how/updating-datahub.md

+1
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ This file documents any backwards-incompatible changes in DataHub and assists pe
6666
changed to NOT fill out `created` and `lastModified` auditstamps by default
6767
for input and output dataset edges. This should not have any user-observable
6868
impact (time-based lineage viz will still continue working based on observed time), but could break assumptions previously being made by clients.
69+
- #12158 - Users provisioned with `user.props` will need to be enabled before login in order to be granted access to DataHub.
6970

7071
### Potential Downtime
7172

metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SampleDataFixtureConfiguration.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ protected OperationContext sampleDataOperationContext(
137137

138138
return testOpContext.toBuilder()
139139
.searchContext(SearchContext.builder().indexConvention(indexConvention).build())
140-
.build(testOpContext.getSessionAuthentication());
140+
.build(testOpContext.getSessionAuthentication(), true);
141141
}
142142

143143
@Bean(name = "longTailOperationContext")
@@ -148,7 +148,7 @@ protected OperationContext longTailOperationContext(
148148

149149
return testOpContext.toBuilder()
150150
.searchContext(SearchContext.builder().indexConvention(indexConvention).build())
151-
.build(testOpContext.getSessionAuthentication());
151+
.build(testOpContext.getSessionAuthentication(), true);
152152
}
153153

154154
protected EntityIndexBuilders entityIndexBuildersHelper(OperationContext opContext) {

metadata-io/src/test/java/io/datahubproject/test/fixtures/search/SearchLineageFixtureConfiguration.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ protected OperationContext searchLineageOperationContext(
162162

163163
return testOpContext.toBuilder()
164164
.searchContext(SearchContext.builder().indexConvention(indexConvention).build())
165-
.build(testOpContext.getSessionAuthentication());
165+
.build(testOpContext.getSessionAuthentication(), true);
166166
}
167167

168168
@Bean(name = "searchLineageESIndexBuilder")

metadata-jobs/mae-consumer/src/test/java/com/linkedin/metadata/kafka/hook/spring/MCLSpringCommonTestConfiguration.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,8 @@ public OperationContext operationContext(
9595
mock(ServicesRegistryContext.class),
9696
indexConvention,
9797
TestOperationContexts.emptyActiveUsersRetrieverContext(() -> entityRegistry),
98-
mock(ValidationContext.class));
98+
mock(ValidationContext.class),
99+
true);
99100
}
100101

101102
@MockBean SpringStandardPluginConfiguration springStandardPluginConfiguration;

metadata-operation-context/src/main/java/io/datahubproject/metadata/context/ActorContext.java

+12-4
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,31 @@
2929
@EqualsAndHashCode
3030
public class ActorContext implements ContextInterface {
3131

32-
public static ActorContext asSystem(Authentication systemAuthentication) {
33-
return ActorContext.builder().systemAuth(true).authentication(systemAuthentication).build();
32+
public static ActorContext asSystem(
33+
Authentication systemAuthentication, boolean enforceExistenceEnabled) {
34+
return ActorContext.builder()
35+
.systemAuth(true)
36+
.authentication(systemAuthentication)
37+
.enforceExistenceEnabled(enforceExistenceEnabled)
38+
.build();
3439
}
3540

3641
public static ActorContext asSessionRestricted(
3742
Authentication authentication,
3843
Set<DataHubPolicyInfo> dataHubPolicySet,
39-
Collection<Urn> groupMembership) {
44+
Collection<Urn> groupMembership,
45+
boolean enforceExistenceEnabled) {
4046
return ActorContext.builder()
4147
.systemAuth(false)
4248
.authentication(authentication)
4349
.policyInfoSet(dataHubPolicySet)
4450
.groupMembership(groupMembership)
51+
.enforceExistenceEnabled(enforceExistenceEnabled)
4552
.build();
4653
}
4754

4855
private final Authentication authentication;
56+
private final boolean enforceExistenceEnabled;
4957

5058
@EqualsAndHashCode.Exclude @Builder.Default
5159
private final Set<DataHubPolicyInfo> policyInfoSet = Collections.emptySet();
@@ -79,7 +87,7 @@ public boolean isActive(AspectRetriever aspectRetriever) {
7987

8088
Map<String, Aspect> aspectMap = urnAspectMap.getOrDefault(selfUrn, Map.of());
8189

82-
if (!aspectMap.containsKey(CORP_USER_KEY_ASPECT_NAME)) {
90+
if (enforceExistenceEnabled && !aspectMap.containsKey(CORP_USER_KEY_ASPECT_NAME)) {
8391
// user is hard deleted
8492
return false;
8593
}

metadata-operation-context/src/main/java/io/datahubproject/metadata/context/OperationContext.java

+18-7
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,8 @@ public static OperationContext asSystem(
152152
@Nullable ServicesRegistryContext servicesRegistryContext,
153153
@Nullable IndexConvention indexConvention,
154154
@Nullable RetrieverContext retrieverContext,
155-
@Nonnull ValidationContext validationContext) {
155+
@Nonnull ValidationContext validationContext,
156+
boolean enforceExistenceEnabled) {
156157
return asSystem(
157158
config,
158159
systemAuthentication,
@@ -161,7 +162,8 @@ public static OperationContext asSystem(
161162
indexConvention,
162163
retrieverContext,
163164
validationContext,
164-
ObjectMapperContext.DEFAULT);
165+
ObjectMapperContext.DEFAULT,
166+
enforceExistenceEnabled);
165167
}
166168

167169
public static OperationContext asSystem(
@@ -172,10 +174,15 @@ public static OperationContext asSystem(
172174
@Nullable IndexConvention indexConvention,
173175
@Nullable RetrieverContext retrieverContext,
174176
@Nonnull ValidationContext validationContext,
175-
@Nonnull ObjectMapperContext objectMapperContext) {
177+
@Nonnull ObjectMapperContext objectMapperContext,
178+
boolean enforceExistenceEnabled) {
176179

177180
ActorContext systemActorContext =
178-
ActorContext.builder().systemAuth(true).authentication(systemAuthentication).build();
181+
ActorContext.builder()
182+
.systemAuth(true)
183+
.authentication(systemAuthentication)
184+
.enforceExistenceEnabled(enforceExistenceEnabled)
185+
.build();
179186
OperationContextConfig systemConfig =
180187
config.toBuilder().allowSystemAuthentication(true).build();
181188
SearchContext systemSearchContext =
@@ -457,13 +464,16 @@ public int hashCode() {
457464
public static class OperationContextBuilder {
458465

459466
@Nonnull
460-
public OperationContext build(@Nonnull Authentication sessionAuthentication) {
461-
return build(sessionAuthentication, false);
467+
public OperationContext build(
468+
@Nonnull Authentication sessionAuthentication, boolean enforceExistenceEnabled) {
469+
return build(sessionAuthentication, false, enforceExistenceEnabled);
462470
}
463471

464472
@Nonnull
465473
public OperationContext build(
466-
@Nonnull Authentication sessionAuthentication, boolean skipCache) {
474+
@Nonnull Authentication sessionAuthentication,
475+
boolean skipCache,
476+
boolean enforceExistenceEnabled) {
467477
final Urn actorUrn = UrnUtils.getUrn(sessionAuthentication.getActor().toUrnStr());
468478
final ActorContext sessionActor =
469479
ActorContext.builder()
@@ -476,6 +486,7 @@ public OperationContext build(
476486
.equals(sessionAuthentication.getActor()))
477487
.policyInfoSet(this.authorizationContext.getAuthorizer().getActorPolicies(actorUrn))
478488
.groupMembership(this.authorizationContext.getAuthorizer().getActorGroups(actorUrn))
489+
.enforceExistenceEnabled(enforceExistenceEnabled)
479490
.build();
480491
return build(sessionActor, skipCache);
481492
}

metadata-operation-context/src/main/java/io/datahubproject/test/metadata/context/TestOperationContexts.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,8 @@ public static OperationContext systemContext(
260260
servicesRegistryContext,
261261
indexConvention,
262262
retrieverContext,
263-
validationContext);
263+
validationContext,
264+
true);
264265

265266
if (postConstruct != null) {
266267
postConstruct.accept(operationContext);

metadata-operation-context/src/test/java/io/datahubproject/metadata/context/ActorContextTest.java

+13-12
Original file line numberDiff line numberDiff line change
@@ -87,42 +87,43 @@ public void actorContextId() {
8787
Authentication userAuth = new Authentication(new Actor(ActorType.USER, "USER"), "");
8888

8989
assertEquals(
90-
ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of()).getCacheKeyComponent(),
91-
ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of()).getCacheKeyComponent(),
90+
ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of(), true).getCacheKeyComponent(),
91+
ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of(), true).getCacheKeyComponent(),
9292
"Expected equality across instances");
9393

9494
assertEquals(
95-
ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of()).getCacheKeyComponent(),
95+
ActorContext.asSessionRestricted(userAuth, Set.of(), Set.of(), true).getCacheKeyComponent(),
9696
ActorContext.asSessionRestricted(
97-
userAuth, Set.of(), Set.of(UrnUtils.getUrn("urn:li:corpGroup:group1")))
97+
userAuth, Set.of(), Set.of(UrnUtils.getUrn("urn:li:corpGroup:group1")), true)
9898
.getCacheKeyComponent(),
9999
"Expected no impact to cache context from group membership");
100100

101101
assertEquals(
102-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of())
102+
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of(), true)
103103
.getCacheKeyComponent(),
104-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of())
104+
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of(), true)
105105
.getCacheKeyComponent(),
106106
"Expected equality when non-ownership policies are identical");
107107

108108
assertNotEquals(
109-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC_RESOURCE, POLICY_D), Set.of())
109+
ActorContext.asSessionRestricted(
110+
userAuth, Set.of(POLICY_ABC_RESOURCE, POLICY_D), Set.of(), true)
110111
.getCacheKeyComponent(),
111-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of())
112+
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_ABC, POLICY_D), Set.of(), true)
112113
.getCacheKeyComponent(),
113114
"Expected differences with non-identical resource policy");
114115

115116
assertNotEquals(
116-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER), Set.of())
117+
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER), Set.of(), true)
117118
.getCacheKeyComponent(),
118-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of())
119+
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of(), true)
119120
.getCacheKeyComponent(),
120121
"Expected differences with ownership policy");
121122

122123
assertNotEquals(
123-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER_TYPE), Set.of())
124+
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D_OWNER_TYPE), Set.of(), true)
124125
.getCacheKeyComponent(),
125-
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of())
126+
ActorContext.asSessionRestricted(userAuth, Set.of(POLICY_D), Set.of(), true)
126127
.getCacheKeyComponent(),
127128
"Expected differences with ownership type policy");
128129
}

metadata-operation-context/src/test/java/io/datahubproject/metadata/context/OperationContextTest.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ public void testSystemPrivilegeEscalation() {
2727
mock(ServicesRegistryContext.class),
2828
null,
2929
TestOperationContexts.emptyActiveUsersRetrieverContext(null),
30-
mock(ValidationContext.class));
30+
mock(ValidationContext.class),
31+
true);
3132

3233
OperationContext opContext =
3334
systemOpContext.asSession(RequestContext.TEST, Authorizer.EMPTY, userAuth);
@@ -51,7 +52,7 @@ public void testSystemPrivilegeEscalation() {
5152
systemOpContext.getOperationContextConfig().toBuilder()
5253
.allowSystemAuthentication(false)
5354
.build())
54-
.build(userAuth);
55+
.build(userAuth, true);
5556

5657
assertEquals(
5758
opContextNoSystem.getAuthentication(),

metadata-service/auth-config/src/main/java/com/datahub/authentication/AuthenticationConfiguration.java

+3
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ public class AuthenticationConfiguration {
99
/** Whether authentication is enabled */
1010
private boolean enabled;
1111

12+
/** Whether user existence is enforced */
13+
private boolean enforceExistenceEnabled;
14+
1215
/**
1316
* List of configurations for {@link com.datahub.plugins.auth.authentication.Authenticator}s to be
1417
* registered

metadata-service/auth-impl/src/test/java/com/datahub/authorization/DataHubAuthorizerTest.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,8 @@ public void setupTest() throws Exception {
320320
mock(ServicesRegistryContext.class),
321321
mock(IndexConvention.class),
322322
mock(RetrieverContext.class),
323-
mock(ValidationContext.class));
323+
mock(ValidationContext.class),
324+
true);
324325

325326
_dataHubAuthorizer =
326327
new DataHubAuthorizer(

metadata-service/configuration/src/main/resources/application.yaml

+3
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ authentication:
66
# Enable if you want all requests to the Metadata Service to be authenticated.
77
enabled: ${METADATA_SERVICE_AUTH_ENABLED:true}
88

9+
# Disable if you want to skip validation of deleted user's tokens
10+
enforceExistenceEnabled: ${METADATA_SERVICE_AUTH_ENFORCE_EXISTENCE_ENABLED:true}
11+
912
# Required if enabled is true! A configurable chain of Authenticators
1013
authenticators:
1114
# Required for authenticating requests with DataHub-issued Access Tokens - best not to remove.

metadata-service/factories/src/main/java/com/linkedin/gms/factory/context/SystemOperationContextFactory.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ protected OperationContext javaSystemOperationContext(
7979
ValidationContext.builder()
8080
.alternateValidation(
8181
configurationProvider.getFeatureFlags().isAlternateMCPValidation())
82-
.build());
82+
.build(),
83+
configurationProvider.getAuthentication().isEnforceExistenceEnabled());
8384

8485
entityClientAspectRetriever.setSystemOperationContext(systemOperationContext);
8586
entityServiceAspectRetriever.setSystemOperationContext(systemOperationContext);
@@ -134,7 +135,8 @@ protected OperationContext restliSystemOperationContext(
134135
ValidationContext.builder()
135136
.alternateValidation(
136137
configurationProvider.getFeatureFlags().isAlternateMCPValidation())
137-
.build());
138+
.build(),
139+
configurationProvider.getAuthentication().isEnforceExistenceEnabled());
138140

139141
entityClientAspectRetriever.setSystemOperationContext(systemOperationContext);
140142
systemGraphRetriever.setSystemOperationContext(systemOperationContext);

metadata-service/factories/src/test/java/com/linkedin/metadata/boot/steps/IngestDataPlatformInstancesStepTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ public void testExecuteChecksKeySpecForAllUrns() throws Exception {
8787
mockOpContext =
8888
mockOpContext.toBuilder()
8989
.entityRegistryContext(spyEntityRegistryContext)
90-
.build(mockOpContext.getSessionAuthentication());
90+
.build(mockOpContext.getSessionAuthentication(), true);
9191

9292
mockDBWithWorkToDo(migrationsDao, countOfCorpUserEntities, countOfChartEntities);
9393

0 commit comments

Comments
 (0)