Skip to content
3 changes: 3 additions & 0 deletions design-docs/gravitino-local-authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,9 @@ fresh Gravitino deployment.
gravitino.authorization.serviceAdmins=admin1,admin2
```

Built-in IdP is incompatible with the `simple` authenticator. When the `idp-basic` plugin is
enabled, `gravitino.authenticators` must not include `simple`.

2. Export the initial service admin password before starting Gravitino:

```bash
Expand Down
5 changes: 4 additions & 1 deletion docs/open-api/idp/idp.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ paths:
tags:
- IDP
summary: Add built-in IDP user
description: Creates a built-in IDP user with the given username and password.
description: >
Creates a built-in IDP user with the given username and password.
Requires the `idp-basic` plugin and built-in IdP Basic authentication.
`gravitino.authenticators` must not include `simple`.
operationId: addIdpUser
requestBody:
required: true
Expand Down
5 changes: 4 additions & 1 deletion docs/open-api/idp/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ info:
version: 1.3.0-SNAPSHOT
description: |
OpenAPI specification for built-in IDP user and group management APIs exposed
by the `idp-basic` plugin when the `basic` authenticator is enabled.
by the `idp-basic` plugin. Clients authenticate with Basic credentials
validated against built-in IdP user metadata. Enable the plugin via
`gravitino.server.rest.extensionPackages`; `gravitino.authenticators` must not
include `simple` when IdP is enabled.

servers:
- url: "{scheme}://{host}:{port}/{basePath}"
Expand Down
3 changes: 3 additions & 0 deletions docs/security/how-to-authenticate.md
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,9 @@ This example shows how to enable built-in Basic authentication.

- Gravitino distribution package (includes the idp-basic plugin on the server classpath)

Built-in IdP is **incompatible** with the `simple` authenticator (the default). When the
`idp-basic` plugin is enabled, `gravitino.authenticators` must not include `simple`.

**Configuration:**

Append the following to `conf/gravitino.conf`:
Expand Down
13 changes: 9 additions & 4 deletions docs/security/how-to-use-built-in-idp.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,11 @@ Before you call `/api/idp/*`, ensure the following:
gravitino.server.rest.extensionPackages = org.apache.gravitino.idp.web.rest.feature
```

2. **Service admin passwords** — Built-in IDP requires every username in
2. **Server authenticator** — Built-in IdP is **incompatible** with the `simple` authenticator
(the default). When the `idp-basic` plugin is enabled, `gravitino.authenticators` must not
include `simple`.

3. **Service admin passwords** — Built-in IDP requires every username in
`gravitino.authorization.serviceAdmins` to have a password stored in `idp_user_meta` before you
can call management APIs.

Expand Down Expand Up @@ -61,9 +65,10 @@ Before you call `/api/idp/*`, ensure the following:

Set service admins in `gravitino.conf` (see also [Prerequisites](#prerequisites)):

| Configuration item | Description | Example |
|-----------------------------------------|-------------------------------------------------------------------------------------|---------|
| `gravitino.authorization.serviceAdmins` | Comma-separated service admin that can call built-in IDP management APIs | `admin` |
| Configuration item | Description | Example |
|-------------------------------------------|-------------------------------------------------------------------------------------|---------|
| `gravitino.server.rest.extensionPackages` | Registers built-in IdP REST APIs | `org.apache.gravitino.idp.web.rest.feature` |
| `gravitino.authorization.serviceAdmins` | Comma-separated service admin that can call built-in IDP management APIs | `admin` |

Example:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.GravitinoEnv;
import org.apache.gravitino.auth.AuthenticatorType;
import org.apache.gravitino.idp.IdpUserGroupManager;
import org.apache.gravitino.idp.auth.BasicAuthenticator;
import org.apache.gravitino.idp.web.rest.IdpAuthorizationFilter;
Expand Down Expand Up @@ -59,6 +60,7 @@ public class IdpRESTFeature implements Feature {
public boolean configure(FeatureContext context) {
GravitinoEnv env = GravitinoEnv.getInstance();
Config config = env.config();
validateConfiguration(config);
registerBasicAuthenticator(config);

try {
Expand All @@ -76,6 +78,23 @@ public boolean configure(FeatureContext context) {
return true;
}

/**
* Validates that the server configuration is compatible with the built-in IdP plugin.
*
* @param config The server configuration.
*/
static void validateConfiguration(Config config) {
boolean usesSimple =
config.get(Configs.AUTHENTICATORS).stream()
.anyMatch(name -> AuthenticatorType.SIMPLE.name().equalsIgnoreCase(name.trim()));
if (usesSimple) {
LOG.error(
"Built-in IdP is incompatible with Simple authentication. "
+ "Remove 'simple' from gravitino.authenticators (default is simple).");
System.exit(1);
}
}

private static void registerBasicAuthenticator(Config config) {
List<Authenticator> authenticators = ServerAuthenticator.getInstance().authenticators();
if (authenticators == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.KeyPairGenerator;
import java.util.Base64;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.gravitino.Configs;
import org.apache.gravitino.auth.AuthConstants;
import org.apache.gravitino.auth.AuthenticatorType;
import org.apache.gravitino.dto.responses.ErrorConstants;
import org.apache.gravitino.idp.dto.requests.AddGroupRequest;
import org.apache.gravitino.idp.dto.requests.AddUserRequest;
Expand All @@ -48,6 +50,7 @@
import org.apache.gravitino.integration.test.util.BaseIT;
import org.apache.gravitino.integration.test.util.ITUtils;
import org.apache.gravitino.json.JsonUtils;
import org.apache.gravitino.server.authentication.OAuthConfig;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -84,6 +87,11 @@ public void startIntegrationTest() throws Exception {
configs.put(Configs.CACHE_ENABLED.getKey(), String.valueOf(false));
configs.put(Configs.STORE_DELETE_AFTER_TIME.getKey(), String.valueOf(20 * 60 * 1000L));
configs.put(Configs.SERVICE_ADMINS.getKey(), ADMIN);
configs.put(Configs.AUTHENTICATORS.getKey(), AuthenticatorType.OAUTH.name().toLowerCase());
configs.put(OAuthConfig.SERVICE_AUDIENCE.getKey(), "service1");
configs.put(OAuthConfig.DEFAULT_SIGN_KEY.getKey(), oauthPublicSignKey());
configs.put(OAuthConfig.DEFAULT_SERVER_URI.getKey(), "test");
configs.put(OAuthConfig.DEFAULT_TOKEN_PATH.getKey(), "test");
configs.put(
Configs.REST_API_EXTENSION_PACKAGES.getKey(), IdpRESTFeature.IDP_REST_EXTENSION_PACKAGE);
registerCustomConfigs(configs);
Expand Down Expand Up @@ -117,8 +125,8 @@ private static void ensureDeployInitialAdminPasswordInDistributionEnv() throws I
@Test
void testIdpAuthorization() throws Exception {
Assertions.assertEquals(200, get("/version", ADMIN, ADMIN_PASSWORD).statusCode());
// No Authorization: simple authenticator allows anonymous access; IdP filter rejects.
assertError(403, get("/idp/users/" + USER1, null, null), ErrorConstants.FORBIDDEN_CODE);
// No Authorization: OAuth rejects the request before the IdP filter runs.
Assertions.assertEquals(401, get("/idp/users/" + USER1, null, null).statusCode());

postUser(USER2, USER_PASSWORD);
assertError(
Expand Down Expand Up @@ -371,4 +379,10 @@ private static void assertError(int expectedStatus, HttpResponse<String> respons
private static int errorCode(HttpResponse<String> response) throws Exception {
return JsonUtils.objectMapper().readTree(response.body()).get("code").asInt();
}

private static String oauthPublicSignKey() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
generator.initialize(2048);
return Base64.getEncoder().encodeToString(generator.generateKeyPair().getPublic().getEncoded());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.apache.gravitino.idp.web.rest.feature;

import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import com.google.common.collect.Lists;
import org.apache.gravitino.Config;
import org.apache.gravitino.Configs;
import org.apache.gravitino.auth.AuthenticatorType;
import org.junit.jupiter.api.Test;

class TestIdpRESTFeature {

@Test
void testSimpleFails() {
Config config = newConfig(AuthenticatorType.SIMPLE.name().toLowerCase());

SystemExitException exception =
assertThrows(SystemExitException.class, () -> validateWithExitGuard(config));

assertEquals(1, exception.status());
}

@Test
void testDefaultSimpleFails() {
Config config = new Config(false) {};

assertThrows(SystemExitException.class, () -> validateWithExitGuard(config));
}

@Test
void testOAuthOk() {
Config config = newConfig(AuthenticatorType.OAUTH.name().toLowerCase());

assertDoesNotThrow(() -> IdpRESTFeature.validateConfiguration(config));
}

private static Config newConfig(String... authenticators) {
Config config = new Config(false) {};
config.set(Configs.AUTHENTICATORS, Lists.newArrayList(authenticators));
return config;
}

@SuppressWarnings("removal")
private static void validateWithExitGuard(Config config) {
SecurityManager original = System.getSecurityManager();
System.setSecurityManager(
new SecurityManager() {
@Override
public void checkExit(int status) {
throw new SystemExitException(status);
}

@Override
public void checkPermission(java.security.Permission perm) {
// Allow test execution.
}
});
try {
IdpRESTFeature.validateConfiguration(config);
} finally {
System.setSecurityManager(original);
}
}

private static final class SystemExitException extends SecurityException {
private final int status;

private SystemExitException(int status) {
super("System.exit(" + status + ")");
this.status = status;
}

private int status() {
return status;
}
}
}
Loading