Skip to content

Commit 6c57057

Browse files
feat(auth): support guest access (#12619)
Co-authored-by: david-leifker <[email protected]>
1 parent 3dac08d commit 6c57057

File tree

15 files changed

+353
-21
lines changed

15 files changed

+353
-21
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package auth;
2+
3+
public class GuestAuthenticationConfigs {
4+
public static final String GUEST_ENABLED_CONFIG_PATH = "auth.guest.enabled";
5+
public static final String GUEST_USER_CONFIG_PATH = "auth.guest.user";
6+
public static final String GUEST_PATH_CONFIG_PATH = "auth.guest.path";
7+
public static final String DEFAULT_GUEST_USER_NAME = "guest";
8+
public static final String DEFAULT_GUEST_PATH = "/public";
9+
10+
private Boolean isEnabled = false;
11+
private String guestUser =
12+
DEFAULT_GUEST_USER_NAME; // Default if not defined but guest auth is enabled.
13+
private String guestPath =
14+
DEFAULT_GUEST_PATH; // The path for initial access to login as guest and bypass login page.
15+
16+
public GuestAuthenticationConfigs(final com.typesafe.config.Config configs) {
17+
if (configs.hasPath(GUEST_ENABLED_CONFIG_PATH)
18+
&& configs.getBoolean(GUEST_ENABLED_CONFIG_PATH)) {
19+
isEnabled = true;
20+
}
21+
if (configs.hasPath(GUEST_USER_CONFIG_PATH)) {
22+
guestUser = configs.getString(GUEST_USER_CONFIG_PATH);
23+
}
24+
if (configs.hasPath(GUEST_PATH_CONFIG_PATH)) {
25+
guestPath = configs.getString(GUEST_PATH_CONFIG_PATH);
26+
}
27+
}
28+
29+
public boolean isGuestEnabled() {
30+
return isEnabled;
31+
}
32+
33+
public String getGuestUser() {
34+
return guestUser;
35+
}
36+
37+
public String getGuestPath() {
38+
return guestPath;
39+
}
40+
}

datahub-frontend/app/controllers/AuthenticationController.java

+21
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import auth.AuthUtils;
88
import auth.CookieConfigs;
9+
import auth.GuestAuthenticationConfigs;
910
import auth.JAASConfigs;
1011
import auth.NativeAuthenticationConfigs;
1112
import auth.sso.SsoManager;
@@ -58,6 +59,8 @@ public class AuthenticationController extends Controller {
5859
private final CookieConfigs cookieConfigs;
5960
private final JAASConfigs jaasConfigs;
6061
private final NativeAuthenticationConfigs nativeAuthenticationConfigs;
62+
private final GuestAuthenticationConfigs guestAuthenticationConfigs;
63+
6164
private final boolean verbose;
6265

6366
@Inject private org.pac4j.core.config.Config ssoConfig;
@@ -73,6 +76,7 @@ public AuthenticationController(@Nonnull Config configs) {
7376
cookieConfigs = new CookieConfigs(configs);
7477
jaasConfigs = new JAASConfigs(configs);
7578
nativeAuthenticationConfigs = new NativeAuthenticationConfigs(configs);
79+
guestAuthenticationConfigs = new GuestAuthenticationConfigs(configs);
7680
verbose = configs.hasPath(AUTH_VERBOSE_LOGGING) && configs.getBoolean(AUTH_VERBOSE_LOGGING);
7781
}
7882

@@ -110,6 +114,23 @@ public Result authenticate(Http.Request request) {
110114
return Results.redirect(redirectPath);
111115
}
112116

117+
if (guestAuthenticationConfigs.isGuestEnabled()
118+
&& guestAuthenticationConfigs.getGuestPath().equals(redirectPath)) {
119+
final String accessToken =
120+
authClient.generateSessionTokenForUser(guestAuthenticationConfigs.getGuestUser());
121+
redirectPath =
122+
"/"; // We requested guest login by accessing {guestPath} URL. It is not really a target.
123+
CorpuserUrn guestUserUrn = new CorpuserUrn(guestAuthenticationConfigs.getGuestUser());
124+
return Results.redirect(redirectPath)
125+
.withSession(createSessionMap(guestUserUrn.toString(), accessToken))
126+
.withCookies(
127+
createActorCookie(
128+
guestUserUrn.toString(),
129+
cookieConfigs.getTtlInHours(),
130+
cookieConfigs.getAuthCookieSameSite(),
131+
cookieConfigs.getAuthCookieSecure()));
132+
}
133+
113134
// 1. If SSO is enabled, redirect to IdP if not authenticated.
114135
if (ssoManager.isSsoEnabled()) {
115136
return redirectToIdentityProvider(request, redirectPath)

datahub-frontend/conf/application.conf

+5
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,11 @@ auth.oidc.grantType = ${?AUTH_OIDC_GRANT_TYPE}
203203
#
204204
auth.jaas.enabled = ${?AUTH_JAAS_ENABLED}
205205
auth.native.enabled = ${?AUTH_NATIVE_ENABLED}
206+
auth.guest.enabled = ${?GUEST_AUTHENTICATION_ENABLED}
207+
# The name of the guest user id
208+
auth.guest.user = ${?GUEST_AUTHENTICATION_USER}
209+
# The path to bypass login page and get logged in as guest
210+
auth.guest.path = ${?GUEST_AUTHENTICATION_PATH}
206211

207212
# Enforces the usage of a valid email for user sign up
208213
auth.native.signUp.enforceValidEmail = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package security;
2+
3+
import static org.junit.jupiter.api.Assertions.*;
4+
5+
import auth.GuestAuthenticationConfigs;
6+
import com.typesafe.config.Config;
7+
import com.typesafe.config.ConfigFactory;
8+
import org.junit.jupiter.api.BeforeEach;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.TestInstance;
11+
import org.junitpioneer.jupiter.ClearEnvironmentVariable;
12+
import org.junitpioneer.jupiter.SetEnvironmentVariable;
13+
14+
@TestInstance(TestInstance.Lifecycle.PER_METHOD)
15+
@SetEnvironmentVariable(key = "DATAHUB_SECRET", value = "test")
16+
@SetEnvironmentVariable(key = "KAFKA_BOOTSTRAP_SERVER", value = "")
17+
@SetEnvironmentVariable(key = "DATAHUB_ANALYTICS_ENABLED", value = "false")
18+
@SetEnvironmentVariable(key = "AUTH_OIDC_ENABLED", value = "true")
19+
@SetEnvironmentVariable(key = "AUTH_OIDC_JIT_PROVISIONING_ENABLED", value = "false")
20+
@SetEnvironmentVariable(key = "AUTH_OIDC_CLIENT_ID", value = "testclient")
21+
@SetEnvironmentVariable(key = "AUTH_OIDC_CLIENT_SECRET", value = "testsecret")
22+
@SetEnvironmentVariable(key = "AUTH_VERBOSE_LOGGING", value = "true")
23+
class GuestAuthenticationConfigsTest {
24+
25+
@BeforeEach
26+
@ClearEnvironmentVariable(key = "GUEST_AUTHENTICATION_ENABLED")
27+
@ClearEnvironmentVariable(key = "GUEST_AUTHENTICATION_USER")
28+
@ClearEnvironmentVariable(key = "GUEST_AUTHENTICATION_PATH")
29+
public void clearConfigCache() {
30+
ConfigFactory.invalidateCaches();
31+
}
32+
33+
@Test
34+
public void testGuestConfigDisabled() {
35+
Config config = ConfigFactory.load();
36+
GuestAuthenticationConfigs guestAuthConfig = new GuestAuthenticationConfigs(config);
37+
assertFalse(guestAuthConfig.isGuestEnabled());
38+
}
39+
40+
@Test
41+
@SetEnvironmentVariable(key = "GUEST_AUTHENTICATION_ENABLED", value = "true")
42+
public void testGuestConfigEnabled() {
43+
Config config = ConfigFactory.load();
44+
GuestAuthenticationConfigs guestAuthConfig = new GuestAuthenticationConfigs(config);
45+
assertTrue(guestAuthConfig.isGuestEnabled());
46+
assertEquals("guest", guestAuthConfig.getGuestUser());
47+
assertEquals("/public", guestAuthConfig.getGuestPath());
48+
}
49+
50+
@Test
51+
@SetEnvironmentVariable(key = "GUEST_AUTHENTICATION_ENABLED", value = "true")
52+
@SetEnvironmentVariable(key = "GUEST_AUTHENTICATION_USER", value = "publicUser")
53+
@SetEnvironmentVariable(key = "GUEST_AUTHENTICATION_PATH", value = "/publicPath")
54+
public void testGuestConfigWithUserEnabled() {
55+
Config config = ConfigFactory.load();
56+
GuestAuthenticationConfigs guestAuthConfig = new GuestAuthenticationConfigs(config);
57+
assertTrue(guestAuthConfig.isGuestEnabled());
58+
assertEquals("publicUser", guestAuthConfig.getGuestUser());
59+
assertEquals("/publicPath", guestAuthConfig.getGuestPath());
60+
}
61+
}

docs/authentication/README.md

+8-1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ DataHub provides 3 mechanisms for authentication at login time:
2525
In subsequent requests, the session token is used to represent the authenticated identity of the user, and is validated by DataHub's backend service (discussed below).
2626
Eventually, the session token is expired (24 hours by default), at which point the end user is required to log in again.
2727

28+
DataHub also supports Guest users to access the system without requiring an explicit login when enabled. The default configuration disables guest authentication.
29+
When Guest access is enabled, accessing datahub with a configurable URL path logs the user in an existing user that is designated as the guest. The privileges of the guest user
30+
are controlled by adjusting privileges of that designated guest user.
31+
2832
### Authentication in the Backend (Metadata Service)
2933

3034
When a user makes a request for Data within DataHub, the request is authenticated by DataHub's Backend (Metadata Service) via a JSON Web Token. This applies to both requests originating from the DataHub application,
@@ -40,12 +44,15 @@ more about Personal Access Tokens [here](personal-access-tokens.md).
4044

4145
To learn more about DataHub's backend authentication, check out [Introducing Metadata Service Authentication](introducing-metadata-service-authentication.md).
4246

43-
Credentials must be provided as Bearer Tokens inside of the **Authorization** header in any request made to DataHub's API layer. To learn
47+
Credentials must be provided as Bearer Tokens inside of the **Authorization** header in any request made to DataHub's API layer.
4448

4549
```shell
4650
Authorization: Bearer <your-token>
4751
```
4852

53+
As with the frontend, the backend also can optionally enable Guest authentication. If Guest authentication is enabled, all API calls made to the backend
54+
without an Authorization header are treated as guest users and the privileges associated with the designated guest user apply to those requests.
55+
4956
Note that in DataHub local quickstarts, Authentication at the backend layer is disabled for convenience. This leaves the backend
5057
vulnerable to unauthenticated requests and should not be used in production. To enable
5158
backend (token-based) authentication, simply set the `METADATA_SERVICE_AUTH_ENABLED=true` environment variable

docs/authentication/concepts.md

+49-2
Original file line numberDiff line numberDiff line change
@@ -60,14 +60,17 @@ There can be many types of Authenticator. For example, there can be Authenticato
6060
and more! A key goal of the abstraction is *extensibility*: a custom Authenticator can be developed to authenticate requests
6161
based on an organization's unique needs.
6262

63-
DataHub ships with 2 Authenticators by default:
63+
DataHub ships with 3 Authenticators by default:
6464

6565
- **DataHubSystemAuthenticator**: Verifies that inbound requests have originated from inside DataHub itself using a shared system identifier
6666
and secret. This authenticator is always present.
6767

6868
- **DataHubTokenAuthenticator**: Verifies that inbound requests contain a DataHub-issued Access Token (discussed further in the "DataHub Access Token" section below) in their
6969
'Authorization' header. This authenticator is required if Metadata Service Authentication is enabled.
7070

71+
- **DataHubGuestAuthenticator**: Verifies if guest authentication is enabled with a guest user configured and allows unauthenticated users to perform operations as the designated
72+
guest user. By default, this Authenticator is disabled. If this is required, it needs to be explicitly enabled and requires a restart of the datahub GMS service.
73+
-
7174
## What is an AuthenticatorChain?
7275

7376
An **AuthenticatorChain** is a series of **Authenticators** that are configured to run one-after-another. This allows
@@ -124,4 +127,48 @@ Today, Access Tokens are granted by the Token Service under two scenarios:
124127
> At present, the Token Service supports the symmetric signing method `HS256` to generate and verify tokens.
125128
126129
Now that we're familiar with the concepts, we will talk concretely about what new capabilities have been built on top
127-
of Metadata Service Authentication.
130+
of Metadata Service Authentication.
131+
132+
## How do I enable Guest Authentication
133+
134+
The Guest Authentication configuration is present in two configuration files - the `application.conf` for DataHub frontend, and
135+
`application.yaml` for GMS. To enable Guest Authentication, set the environment variable `GUEST_AUTHENTICATION_ENABLED` to `true`
136+
for both the GMS and the frontend service and restart those services.
137+
If enabled, the default user designated as guest is called `guest`. This user must be explicitly created and privileges assigned
138+
to control the guest user privileges.
139+
140+
A recommended approach to operationalize guest access is, first, create a designated guest user account with login credentials,
141+
but keep guest access disabled. This allows you to configure and test the exact permissions this user should have. Once you've
142+
confirmed the privileges are set correctly, you can then enable guest access, which removes the need for login/credentials
143+
while maintaining the verified permission settings.
144+
145+
The name of the designated guest user can be changed by defining the env var `GUEST_AUTHENTICATION_USER`.
146+
The entry URL to authenticate as the guest user is `/public` and can be changed via the env var `GUEST_AUTHENTICATION_PATH`
147+
148+
Here are the relevant portions of the two configs
149+
150+
For the Frontend
151+
```yaml
152+
#application.conf
153+
...
154+
auth.guest.enabled = ${?GUEST_AUTHENTICATION_ENABLED}
155+
# The name of the guest user id
156+
auth.guest.user = ${?GUEST_AUTHENTICATION_USER}
157+
# The path to bypass login page and get logged in as guest
158+
auth.guest.path = ${?GUEST_AUTHENTICATION_PATH}
159+
...
160+
```
161+
162+
and for GMS
163+
```yaml
164+
#application.yaml
165+
# Required if enabled is true! A configurable chain of Authenticators
166+
...
167+
authenticators:
168+
...
169+
- type: com.datahub.authentication.authenticator.DataHubGuestAuthenticator
170+
configs:
171+
guestUser: ${GUEST_AUTHENTICATION_USER:guest}
172+
enabled: ${GUEST_AUTHENTICATION_ENABLED:false}
173+
...
174+
```

li-utils/src/main/java/com/linkedin/metadata/Constants.java

+3
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,9 @@ public class Constants {
456456
"dataHubStepStateProperties";
457457

458458
// Authorization
459+
// Do not use this env var directly to determine if REST API Auth is to be enabled. Instead, use
460+
// the spring property "authorization.restApiAuthorization" from application.yaml for
461+
// consistency. The spring property can be initialized by this env var (among other methods).
459462
public static final String REST_API_AUTHORIZATION_ENABLED_ENV = "REST_API_AUTHORIZATION_ENABLED";
460463

461464
// Metadata Change Event Parameter Names

metadata-auth/auth-api/build.gradle

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,15 @@ dependencies() {
2727

2828
implementation externalDependency.guava
2929
compileOnly externalDependency.lombok
30+
compileOnly externalDependency.springContext
3031

3132
annotationProcessor externalDependency.lombok
3233

3334
testImplementation externalDependency.testng
3435
testImplementation externalDependency.mockito
3536
testImplementation project(path: ':metadata-operation-context')
3637
testImplementation 'uk.org.webcompere:system-stubs-testng:2.1.6'
37-
38+
testImplementation externalDependency.springBootTest
3839
}
3940

4041
task sourcesJar(type: Jar) {

metadata-auth/auth-api/src/main/java/com/datahub/authorization/AuthUtil.java

+22-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
import static com.linkedin.metadata.Constants.ML_MODEL_GROUP_ENTITY_NAME;
1616
import static com.linkedin.metadata.Constants.ML_PRIMARY_KEY_ENTITY_NAME;
1717
import static com.linkedin.metadata.Constants.NOTEBOOK_ENTITY_NAME;
18-
import static com.linkedin.metadata.Constants.REST_API_AUTHORIZATION_ENABLED_ENV;
1918
import static com.linkedin.metadata.authorization.ApiGroup.ENTITY;
2019
import static com.linkedin.metadata.authorization.ApiOperation.CREATE;
2120
import static com.linkedin.metadata.authorization.ApiOperation.DELETE;
@@ -53,7 +52,10 @@
5352
import java.util.stream.Collectors;
5453
import javax.annotation.Nonnull;
5554
import javax.annotation.Nullable;
55+
import javax.annotation.PostConstruct;
5656
import org.apache.http.HttpStatus;
57+
import org.springframework.beans.factory.annotation.Value;
58+
import org.springframework.stereotype.Component;
5759

5860
/**
5961
* Notes: This class is an attempt to unify privilege checks across APIs.
@@ -67,8 +69,25 @@
6769
* <p>isAPI...() functions are intended for OpenAPI and Rest.li since they are governed by an enable
6870
* flag. GraphQL is always enabled and should use is...() functions.
6971
*/
72+
@Component
7073
public class AuthUtil {
7174

75+
// Since all methods of this class are static, need to postConstruct to initialize the static var
76+
// from the instance var that spring can initialize
77+
// TODO: Some unit tests seem to rely on this being false, so setting the default to false.
78+
// When running as the spring boot application, the default property value is true.
79+
private static boolean isRestApiAuthorizationEnabled = false;
80+
81+
// Eliminating the dependency on the env var REST_API_AUTHORIZATION_ENABLED and instead using the
82+
// application property to keep it consistent with all other usage of that property.
83+
@Value("${authorization.restApiAuthorization:true}")
84+
protected Boolean restApiAuthorizationEnabled;
85+
86+
@PostConstruct
87+
protected void init() {
88+
AuthUtil.isRestApiAuthorizationEnabled = this.restApiAuthorizationEnabled;
89+
}
90+
7291
/**
7392
* This should generally follow the policy creation UI with a few exceptions for users, groups,
7493
* containers, etc so that the platform still functions as expected.
@@ -340,7 +359,7 @@ private static boolean isAPIAuthorized(
340359
@Nonnull final AuthorizationSession session,
341360
@Nonnull final Disjunctive<Conjunctive<PoliciesConfig.Privilege>> privileges,
342361
@Nonnull final Collection<EntitySpec> resources) {
343-
if (Boolean.parseBoolean(System.getenv(REST_API_AUTHORIZATION_ENABLED_ENV))) {
362+
if (AuthUtil.isRestApiAuthorizationEnabled) {
344363
return isAuthorized(session, buildDisjunctivePrivilegeGroup(privileges), resources);
345364
} else {
346365
return true;
@@ -583,5 +602,5 @@ private static boolean isDenied(
583602
return AuthorizationResult.Type.DENY.equals(result.getType());
584603
}
585604

586-
private AuthUtil() {}
605+
protected AuthUtil() {}
587606
}

0 commit comments

Comments
 (0)