Skip to content

Commit 3c60292

Browse files
[#10959] feat(idp-basic): Add Basic authentication for built-in IdP (#11226)
### What changes were proposed in this pull request? Add built-in IdP **Basic** authentication and wire it into the Gravitino server, Java client, and distribution: - **`plugins:idp-basic`**: `BasicAuthenticator` validates HTTP Basic credentials against relational IdP user metadata with Argon2id password hashes; `ServiceAdminInitializer` creates configured service admins on first startup when `GRAVITINO_INITIAL_ADMIN_PASSWORD` is set. - **Server bootstrap**: Optional plugins load through `ServerPluginBootstrap` SPI (`ServerPluginBootstrapper` in `server-common`, `IdpServerPluginBootstrap` in `idp-basic`) so the server module does not compile-depend on `idp-basic`. - **Java client**: `GravitinoClient.builder(...).withBasicAuth(username, password)` and `BasicTokenProvider`. - **Docs**: Basic mode configuration and client usage in `docs/security/how-to-authenticate.md`. ### Why are the changes needed? Gravitino needs a built-in authentication path for deployments that do not use an external OAuth provider. This PR implements the `basic` authenticator mode, service-admin bootstrap for initial login, client support, and packaging so the feature is available in official binaries. Fix: #10965 ### Does this PR introduce _any_ user-facing change? Yes. 1. **Server configuration**: New authenticator value `basic` in `gravitino.authenticators` (requires `gravitino.entity.store=relational`). 2. **Environment variable**: `GRAVITINO_INITIAL_ADMIN_PASSWORD` — JSON array of `username:password` entries for first-time service admin creation. 3. **Java client API**: `GravitinoClientBase.Builder.withBasicAuth(String username, String password)`. ### How was this patch tested? Unit tests added/updated in `plugins:idp-basic`, `server-common`, and `clients:client-java`. Locally ran: - `./gradlew :plugins:idp-basic:test -PskipITs -PskipDockerTests=true` - `./gradlew :server-common:test --tests "*BasicAuthentication*" -PskipITs -PskipDockerTests=true` - `./gradlew :clients:client-java:test --tests "*BasicTokenProvider*" -PskipITs` --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 52da61f commit 3c60292

49 files changed

Lines changed: 2093 additions & 361 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/backend-integration-test-action.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ jobs:
8888
-x :spark-connector:spark-3.3:test -x :spark-connector:spark-3.4:test -x :spark-connector:spark-3.5:test \
8989
-x :spark-connector:spark-runtime-3.3:test -x :spark-connector:spark-runtime-3.4:test -x :spark-connector:spark-runtime-3.5:test \
9090
-x :trino-connector:integration-test:test -x :trino-connector:trino-connector:test \
91+
-x :plugins:idp-basic:test \
9192
-x :authorizations:authorization-chain:test -x :authorizations:authorization-ranger:test \
9293
-x :clients:cli:test -x :maintenance:jobs:test -x :maintenance:optimizer:test \
9394
$EXCLUDE_CONTRIB_TESTS

.github/workflows/idp-basic-test.yml

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ jobs:
4646
needs: changes
4747
if: needs.changes.outputs.source_changes == 'true' && needs.changes.outputs.module_exists == 'true'
4848
runs-on: ubuntu-22.04
49-
timeout-minutes: 60
49+
timeout-minutes: 30
5050
steps:
5151
- uses: actions/checkout@v4
5252

@@ -56,21 +56,52 @@ jobs:
5656
distribution: "temurin"
5757
cache: "gradle"
5858

59+
- name: Set up QEMU
60+
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
61+
62+
- name: Check required command
63+
run: |
64+
dev/ci/check_commands.sh
65+
66+
- name: Package Gravitino
67+
run: |
68+
./gradlew compileDistribution -PskipWeb=true -x test
69+
5970
- name: Free up disk space
6071
run: |
6172
dev/ci/util_free_space.sh
6273
6374
- name: Run idp-basic tests
75+
id: idpBasicTest
6476
env:
6577
dockerTest: true
78+
GRAVITINO_INITIAL_ADMIN_PASSWORD: Passw0rd-For-Admin1
6679
run: |
67-
./gradlew :plugins:idp-basic:test -PskipITs -PskipDockerTests=false -PskipWeb=true
80+
set -euo pipefail
81+
82+
# Unit and in-process tests (no Docker, no REST IT).
83+
./gradlew :plugins:idp-basic:test -PskipITs -PskipDockerTests=true
84+
85+
# REST IT: embedded and deploy, each against h2 / MySQL / PostgreSQL.
86+
for test_mode in embedded deploy; do
87+
for backend in h2 mysql postgresql; do
88+
./gradlew :plugins:idp-basic:test \
89+
-PtestMode="${test_mode}" \
90+
-PjdbcBackend="${backend}" \
91+
-PskipDockerTests=false \
92+
--tests "org.apache.gravitino.idp.integration.test.**"
93+
done
94+
done
6895
6996
- name: Upload idp-basic test reports
7097
uses: actions/upload-artifact@v7
71-
if: failure()
98+
if: ${{ (failure() && steps.idpBasicTest.outcome == 'failure') || contains(github.event.pull_request.labels.*.name, 'upload log') }}
7299
with:
73100
name: idp-basic-test-reports
74101
path: |
75102
plugins/idp-basic/build/reports
76103
plugins/idp-basic/build/test-results
104+
distribution/package-all/logs/gravitino-server.out
105+
distribution/package-all/logs/gravitino-server.log
106+
distribution/package/logs/gravitino-server.out
107+
distribution/package/logs/gravitino-server.log
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.apache.gravitino.client;
21+
22+
import java.io.IOException;
23+
import java.nio.charset.StandardCharsets;
24+
import java.util.Base64;
25+
import org.apache.gravitino.auth.AuthConstants;
26+
27+
/** Provides HTTP Basic credentials for Gravitino built-in IdP authentication. */
28+
final class BasicTokenProvider implements AuthDataProvider {
29+
30+
private final byte[] token;
31+
32+
BasicTokenProvider(String username, String password) {
33+
this.token = buildToken(username, password);
34+
}
35+
36+
private static byte[] buildToken(String username, String password) {
37+
String userInformation = username + ":" + password;
38+
return (AuthConstants.AUTHORIZATION_BASIC_HEADER
39+
+ new String(
40+
Base64.getEncoder().encode(userInformation.getBytes(StandardCharsets.UTF_8)),
41+
StandardCharsets.UTF_8))
42+
.getBytes(StandardCharsets.UTF_8);
43+
}
44+
45+
@Override
46+
public boolean hasTokenData() {
47+
return true;
48+
}
49+
50+
@Override
51+
public byte[] getTokenData() {
52+
return token;
53+
}
54+
55+
@Override
56+
public void close() throws IOException {}
57+
}

clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClientBase.java

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,20 @@ public Builder<T> withSimpleAuth(String userName) {
276276
return this;
277277
}
278278

279+
/**
280+
* Sets HTTP Basic authentication for Gravitino.
281+
*
282+
* @param username The username for Basic authentication.
283+
* @param password The password for Basic authentication.
284+
* @return This Builder instance for method chaining.
285+
*/
286+
public Builder<T> withBasicAuth(String username, String password) {
287+
Preconditions.checkArgument(StringUtils.isNotBlank(username), "username can't be blank");
288+
Preconditions.checkArgument(StringUtils.isNotBlank(password), "password can't be blank");
289+
this.authDataProvider = new BasicTokenProvider(username, password);
290+
return this;
291+
}
292+
279293
/**
280294
* Optional, set a flag to verify the client is supported to connector the server
281295
*
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one
3+
* or more contributor license agreements. See the NOTICE file
4+
* distributed with this work for additional information
5+
* regarding copyright ownership. The ASF licenses this file
6+
* to you under the Apache License, Version 2.0 (the
7+
* "License"); you may not use this file except in compliance
8+
* with the License. You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
package org.apache.gravitino.client;
20+
21+
import java.io.IOException;
22+
import java.nio.charset.StandardCharsets;
23+
import java.util.Base64;
24+
import org.apache.gravitino.auth.AuthConstants;
25+
import org.junit.jupiter.api.Assertions;
26+
import org.junit.jupiter.api.Test;
27+
28+
public class TestBasicTokenProvider {
29+
30+
@Test
31+
public void testAuthentication() throws IOException {
32+
try (AuthDataProvider provider = new BasicTokenProvider("admin", "YourSecurePassword12")) {
33+
Assertions.assertTrue(provider.hasTokenData());
34+
String token = new String(provider.getTokenData(), StandardCharsets.UTF_8);
35+
Assertions.assertTrue(token.startsWith(AuthConstants.AUTHORIZATION_BASIC_HEADER));
36+
String tokenString =
37+
new String(
38+
Base64.getDecoder()
39+
.decode(
40+
token
41+
.substring(AuthConstants.AUTHORIZATION_BASIC_HEADER.length())
42+
.getBytes(StandardCharsets.UTF_8)),
43+
StandardCharsets.UTF_8);
44+
Assertions.assertEquals("admin:YourSecurePassword12", tokenString);
45+
}
46+
}
47+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
import base64
19+
20+
from gravitino.auth.auth_constants import AuthConstants
21+
from gravitino.auth.auth_data_provider import AuthDataProvider
22+
from gravitino.exceptions.base import IllegalArgumentException
23+
24+
25+
class BasicAuthProvider(AuthDataProvider):
26+
"""Provides HTTP Basic credentials for Gravitino built-in IdP authentication."""
27+
28+
_token: bytes
29+
30+
def __init__(self, username: str, password: str):
31+
if username is None or not username.strip():
32+
raise IllegalArgumentException("username can't be blank")
33+
if password is None or not password.strip():
34+
raise IllegalArgumentException("password can't be blank")
35+
36+
self._token = self._build_basic_auth_token(username, password)
37+
38+
def has_token_data(self) -> bool:
39+
return True
40+
41+
def get_token_data(self) -> bytes:
42+
return self._token
43+
44+
def close(self):
45+
pass
46+
47+
@staticmethod
48+
def _build_basic_auth_token(username: str, password: str) -> bytes:
49+
credentials = f"{username}:{password}"
50+
encoded_credentials = base64.b64encode(credentials.encode("utf-8")).decode(
51+
"utf-8"
52+
)
53+
authorization_header = (
54+
AuthConstants.AUTHORIZATION_BASIC_HEADER + encoded_credentials
55+
)
56+
return authorization_header.encode("utf-8")

clients/client-python/gravitino/filesystem/gvfs_config.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class GVFSConfig:
2525

2626
AUTH_TYPE = "auth_type"
2727
SIMPLE_AUTH_TYPE = "simple"
28+
BASIC_AUTH_TYPE = "basic"
29+
BASIC_USERNAME = "basic_username"
30+
BASIC_PASSWORD = "basic_password"
2831

2932
OAUTH2_AUTH_TYPE = "oauth2"
3033
OAUTH2_SERVER_URI = "oauth2_server_uri"

clients/client-python/gravitino/filesystem/gvfs_utils.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from typing import Dict
1919

2020
from gravitino.client.gravitino_client import GravitinoClient
21+
from gravitino.auth.basic_auth_provider import BasicAuthProvider
2122
from gravitino.auth.default_oauth2_token_provider import DefaultOAuth2TokenProvider
2223
from gravitino.auth.oauth2_token_provider import OAuth2TokenProvider
2324
from gravitino.auth.simple_auth_provider import SimpleAuthProvider
@@ -73,6 +74,21 @@ def create_client(
7374
client_config=client_config,
7475
)
7576

77+
if auth_type == GVFSConfig.BASIC_AUTH_TYPE:
78+
basic_username = options.get(GVFSConfig.BASIC_USERNAME)
79+
_check_auth_config(auth_type, GVFSConfig.BASIC_USERNAME, basic_username)
80+
81+
basic_password = options.get(GVFSConfig.BASIC_PASSWORD)
82+
_check_auth_config(auth_type, GVFSConfig.BASIC_PASSWORD, basic_password)
83+
84+
return GravitinoClient(
85+
uri=server_uri,
86+
metalake_name=metalake_name,
87+
auth_data_provider=BasicAuthProvider(basic_username, basic_password),
88+
request_headers=request_headers,
89+
client_config=client_config,
90+
)
91+
7692
if auth_type == GVFSConfig.OAUTH2_AUTH_TYPE:
7793
oauth2_server_uri = options.get(GVFSConfig.OAUTH2_SERVER_URI)
7894
_check_auth_config(auth_type, GVFSConfig.OAUTH2_SERVER_URI, oauth2_server_uri)
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Licensed to the Apache Software Foundation (ASF) under one
2+
# or more contributor license agreements. See the NOTICE file
3+
# distributed with this work for additional information
4+
# regarding copyright ownership. The ASF licenses this file
5+
# to you under the Apache License, Version 2.0 (the
6+
# "License"); you may not use this file except in compliance
7+
# with the License. You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing,
12+
# software distributed under the License is distributed on an
13+
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
# KIND, either express or implied. See the License for the
15+
# specific language governing permissions and limitations
16+
# under the License.
17+
18+
import base64
19+
import unittest
20+
21+
from gravitino.auth.auth_constants import AuthConstants
22+
from gravitino.auth.auth_data_provider import AuthDataProvider
23+
from gravitino.auth.basic_auth_provider import BasicAuthProvider
24+
from gravitino.exceptions.base import IllegalArgumentException
25+
26+
27+
class TestBasicAuthProvider(unittest.TestCase):
28+
def test_auth_provider(self):
29+
provider: AuthDataProvider = BasicAuthProvider(
30+
"admin", "YourSecureGravitinoPassword"
31+
)
32+
self.assertTrue(provider.has_token_data())
33+
token = provider.get_token_data().decode("utf-8")
34+
self.assertTrue(token.startswith(AuthConstants.AUTHORIZATION_BASIC_HEADER))
35+
token_string = base64.b64decode(
36+
token[len(AuthConstants.AUTHORIZATION_BASIC_HEADER) :]
37+
).decode("utf-8")
38+
self.assertEqual("admin:YourSecureGravitinoPassword", token_string)
39+
provider.close()
40+
41+
def test_blank_username(self):
42+
with self.assertRaises(IllegalArgumentException):
43+
BasicAuthProvider(" ", "password")
44+
45+
def test_blank_password(self):
46+
with self.assertRaises(IllegalArgumentException):
47+
BasicAuthProvider("admin", " ")

clients/client-python/tests/unittests/test_gvfs_with_local.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,25 @@ def test_simple_auth(self, *mock_methods):
197197
if current_user is not None:
198198
os.environ["user.name"] = current_user
199199

200+
def test_basic_auth(self, *mock_methods):
201+
options = {
202+
GVFSConfig.AUTH_TYPE: GVFSConfig.BASIC_AUTH_TYPE,
203+
GVFSConfig.BASIC_USERNAME: "admin",
204+
GVFSConfig.BASIC_PASSWORD: "YourSecureGravitinoPassword",
205+
}
206+
fs = gvfs.GravitinoVirtualFileSystem(
207+
server_uri=self._server_uri,
208+
metalake_name=self._metalake_name,
209+
options=options,
210+
skip_instance_cache=True,
211+
)
212+
client = fs._operations._get_gravitino_client()
213+
token = client._rest_client.auth_data_provider.get_token_data()
214+
token_string = base64.b64decode(
215+
token.decode("utf-8")[len(AuthConstants.AUTHORIZATION_BASIC_HEADER) :]
216+
).decode("utf-8")
217+
self.assertEqual("admin:YourSecureGravitinoPassword", token_string)
218+
200219
def test_oauth2_auth(self, *mock_methods):
201220
fs_options = {
202221
GVFSConfig.AUTH_TYPE: GVFSConfig.OAUTH2_AUTH_TYPE,

0 commit comments

Comments
 (0)