roryqi commented on code in PR #11226: URL: https://github.com/apache/gravitino/pull/11226#discussion_r3310232170
########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/auth/BasicAuthenticator.java: ########## @@ -0,0 +1,159 @@ +/* + * 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.auth; + +import com.google.common.base.Preconditions; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.UserGroup; +import org.apache.gravitino.UserPrincipal; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.exceptions.BadRequestException; +import org.apache.gravitino.exceptions.UnauthorizedException; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.model.IdpUser; +import org.apache.gravitino.server.authentication.Authenticator; +import org.apache.gravitino.storage.IdGenerator; +import org.apache.gravitino.storage.RandomIdGenerator; + +/** Authenticates HTTP Basic credentials against built-in IdP user metadata. */ +public class BasicAuthenticator implements Authenticator { + + private static final String BASIC_CHALLENGE = AuthConstants.AUTHORIZATION_BASIC_HEADER.trim(); + + private IdpUserGroupManager userGroupManager; + + public BasicAuthenticator() {} + + @Override + public boolean isDataFromToken() { + return true; + } + + @Override + public Principal authenticateToken(byte[] tokenData) { + Preconditions.checkState( + userGroupManager != null, "Basic authenticator has not been initialized"); + String authData = requireBasicAuthHeader(tokenData); + BasicCredentials credentials = parseBasicCredentials(authData); + return authenticate(credentials, authData); + } + + @Override + public void initialize(Config config) { + GravitinoEnv env = GravitinoEnv.getInstance(); + IdGenerator idGenerator = + env.idGenerator() != null ? env.idGenerator() : RandomIdGenerator.INSTANCE; + this.userGroupManager = IdpUserGroupManager.getInstance(config, idGenerator); + } + + @Override + public boolean supportsToken(byte[] tokenData) { + return tokenData != null + && new String(tokenData, StandardCharsets.UTF_8) + .startsWith(AuthConstants.AUTHORIZATION_BASIC_HEADER); + } + + private String requireBasicAuthHeader(byte[] tokenData) { + if (tokenData == null) { + throw unauthorized("Empty token authorization header"); + } + + String authData = new String(tokenData, StandardCharsets.UTF_8); + if (authData.trim().isEmpty()) { + throw unauthorized("Empty token authorization header"); + } + if (!authData.startsWith(AuthConstants.AUTHORIZATION_BASIC_HEADER)) { + throw unauthorized("Invalid token authorization header"); + } + return authData; + } + + private BasicCredentials parseBasicCredentials(String authData) { + String credential = authData.substring(AuthConstants.AUTHORIZATION_BASIC_HEADER.length()); + credential = credential.trim(); + if (credential.isEmpty()) { + throw new BadRequestException("Malformed Basic authorization header: missing credentials"); + } + + try { + String decodedCredential = + new String(Base64.getDecoder().decode(credential), StandardCharsets.UTF_8); + String[] parts = decodedCredential.split(":", 2); + if (parts.length != 2) { + throw new BadRequestException( + "Malformed Basic authorization header: credentials must be in username:password format"); + } + + String username = parts[0]; + if (username.isEmpty()) { Review Comment: StringUtils.isBlank. ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -96,8 +163,22 @@ public boolean removeUser(String username) { * @return The built-in IdP user. */ public IdpUser getUser(String username) { - IdpUserPO userPO = USER_SERVICE.getIdpUserByUsername(username); - return new IdpUser(userPO.getUsername(), USER_SERVICE.listGroupNamesByUsername(username)); + IdpUserPO userPO = userMetaService.getIdpUserByUsername(username); + return new IdpUser(userPO.getUsername(), userMetaService.listGroupNamesByUsername(username)); + } + + public IdpUser authenticate(String username, String password) { + try { + IdpUserPO userPO = userMetaService.getIdpUserByUsername(username); + if (!passwordHasher.verify(password, userPO.getPasswordHash())) { + throw new UnauthorizedException( + "Invalid username or password", AuthConstants.AUTHORIZATION_BASIC_HEADER.trim()); + } + return new IdpUser(username, userMetaService.listGroupNamesByUsername(username)); Review Comment: Could u use once IO operations instead of twice IO operations. This is the critical path. ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -45,28 +53,87 @@ */ public class IdpUserGroupManager implements Closeable { - private static final IdpUserMetaService USER_SERVICE = IdpUserMetaService.getInstance(); - private static final IdpGroupMetaService GROUP_SERVICE = IdpGroupMetaService.getInstance(); + private static final String BASIC_AUTHENTICATOR_CLASS_NAME = + BasicAuthenticator.class.getCanonicalName(); + + private static volatile IdpUserGroupManager instance; private final IdpRelationalStorage relationalStorage; private final IdGenerator idGenerator; private final PasswordHasher passwordHasher; + private final IdpUserMetaService userMetaService; + private final IdpGroupMetaService groupMetaService; private final IdpGarbageCollector garbageCollector; - /** - * Creates a built-in IdP user and group manager. - * - * @param config The server configuration. - * @param idGenerator The id generator. - */ - public IdpUserGroupManager(Config config, IdGenerator idGenerator) { + public static IdpUserGroupManager getInstance(Config config, IdGenerator idGenerator) { + IdpUserGroupManager local = instance; + if (local == null) { + synchronized (IdpUserGroupManager.class) { + local = instance; + if (local == null) { + instance = new IdpUserGroupManager(config, idGenerator); + local = instance; + } + } + } + return local; + } + + private IdpUserGroupManager(Config config, IdGenerator idGenerator) { this.relationalStorage = new IdpRelationalStorage(config); this.idGenerator = idGenerator; this.passwordHasher = PasswordHasherFactory.create(); + this.userMetaService = IdpUserMetaService.getInstance(); + this.groupMetaService = IdpGroupMetaService.getInstance(); this.garbageCollector = new IdpGarbageCollector(config); garbageCollector.start(); } + IdpUserGroupManager( + IdGenerator idGenerator, IdpUserMetaService userMetaService, PasswordHasher passwordHasher) { + this.relationalStorage = null; + this.idGenerator = idGenerator; + this.passwordHasher = passwordHasher; + this.userMetaService = userMetaService; + this.groupMetaService = null; + this.garbageCollector = null; + } + + public void initializeConfiguredServiceAdmins(Config config, String initialAdminPassword) + throws IOException { + if (!basicAuthenticatorEnabled(config)) { + return; + } + + List<String> serviceAdmins = config.get(Configs.SERVICE_ADMINS); + if (serviceAdmins == null || serviceAdmins.isEmpty()) { + return; + } + + Map<String, String> passwordsByAdmin = Review Comment: We don't need the map here. ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -182,6 +281,29 @@ private IdpUserPO newUserPO(String username, String passwordHash) { .build(); } + private boolean userExists(String username) { + try { + userMetaService.getIdpUserByUsername(username); + return true; + } catch (NotFoundException e) { + return false; + } + } + + private static Map<String, String> parseInitialAdminPasswords( Review Comment: Do we need this method? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/auth/BasicAuthenticator.java: ########## @@ -0,0 +1,159 @@ +/* + * 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.auth; + +import com.google.common.base.Preconditions; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.Base64; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.UserGroup; +import org.apache.gravitino.UserPrincipal; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.exceptions.BadRequestException; +import org.apache.gravitino.exceptions.UnauthorizedException; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.model.IdpUser; +import org.apache.gravitino.server.authentication.Authenticator; +import org.apache.gravitino.storage.IdGenerator; +import org.apache.gravitino.storage.RandomIdGenerator; + +/** Authenticates HTTP Basic credentials against built-in IdP user metadata. */ +public class BasicAuthenticator implements Authenticator { + + private static final String BASIC_CHALLENGE = AuthConstants.AUTHORIZATION_BASIC_HEADER.trim(); + + private IdpUserGroupManager userGroupManager; + + public BasicAuthenticator() {} + + @Override + public boolean isDataFromToken() { + return true; + } + + @Override + public Principal authenticateToken(byte[] tokenData) { + Preconditions.checkState( + userGroupManager != null, "Basic authenticator has not been initialized"); + String authData = requireBasicAuthHeader(tokenData); + BasicCredentials credentials = parseBasicCredentials(authData); + return authenticate(credentials, authData); + } + + @Override + public void initialize(Config config) { + GravitinoEnv env = GravitinoEnv.getInstance(); + IdGenerator idGenerator = + env.idGenerator() != null ? env.idGenerator() : RandomIdGenerator.INSTANCE; + this.userGroupManager = IdpUserGroupManager.getInstance(config, idGenerator); + } + + @Override + public boolean supportsToken(byte[] tokenData) { + return tokenData != null + && new String(tokenData, StandardCharsets.UTF_8) + .startsWith(AuthConstants.AUTHORIZATION_BASIC_HEADER); + } + + private String requireBasicAuthHeader(byte[] tokenData) { + if (tokenData == null) { + throw unauthorized("Empty token authorization header"); + } + + String authData = new String(tokenData, StandardCharsets.UTF_8); + if (authData.trim().isEmpty()) { + throw unauthorized("Empty token authorization header"); + } + if (!authData.startsWith(AuthConstants.AUTHORIZATION_BASIC_HEADER)) { + throw unauthorized("Invalid token authorization header"); + } + return authData; + } + + private BasicCredentials parseBasicCredentials(String authData) { + String credential = authData.substring(AuthConstants.AUTHORIZATION_BASIC_HEADER.length()); + credential = credential.trim(); + if (credential.isEmpty()) { + throw new BadRequestException("Malformed Basic authorization header: missing credentials"); Review Comment: Could u throw UnAuthorizationException? ########## plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRestApiIT.java: ########## @@ -0,0 +1,482 @@ +/* + * 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.integration.test; + +import static org.apache.gravitino.integration.test.util.BaseIT.setEnv; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.Configs; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.auxiliary.AuxiliaryServiceManager; +import org.apache.gravitino.config.ConfigConstants; +import org.apache.gravitino.dto.responses.ErrorConstants; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.dto.requests.AddGroupRequest; +import org.apache.gravitino.idp.dto.requests.AddUserRequest; +import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest; +import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest; +import org.apache.gravitino.idp.dto.responses.IdpGroupResponse; +import org.apache.gravitino.idp.dto.responses.IdpUserResponse; +import org.apache.gravitino.integration.test.container.ContainerSuite; +import org.apache.gravitino.integration.test.container.MySQLContainer; +import org.apache.gravitino.integration.test.container.PostgreSQLContainer; +import org.apache.gravitino.integration.test.util.TestDatabaseName; +import org.apache.gravitino.json.JsonUtils; +import org.apache.gravitino.rest.RESTUtils; +import org.apache.gravitino.server.GravitinoServer; +import org.apache.gravitino.server.ServerConfig; +import org.apache.gravitino.server.web.JettyServerConfig; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +/** + * End-to-end tests for built-in IdP REST APIs on an embedded Gravitino server. + * + * <p>Runs the same REST scenario against H2, MySQL, and PostgreSQL relational backends. + */ +@Tag("gravitino-docker-test") +public class IdpRestApiIT { + + private static final String H2_BACKEND = "h2"; + private static final String MYSQL_BACKEND = "mysql"; + private static final String POSTGRESQL_BACKEND = "postgresql"; + + private static final TestDatabaseName MYSQL_TEST_DATABASE = TestDatabaseName.MYSQL_JDBC_BACKEND; + private static final TestDatabaseName POSTGRESQL_TEST_DATABASE = TestDatabaseName.PG_JDBC_BACKEND; + + private static final String ACCEPT = "application/vnd.gravitino.v1+json"; + private static final String IDP_REST_EXTENSION_PACKAGE = + "org.apache.gravitino.idp.web.rest.feature"; + private static final String BASIC_AUTHENTICATOR_CLASS = + "org.apache.gravitino.idp.auth.BasicAuthenticator"; + private static final String INITIAL_ADMIN_PASSWORD_ENV = "GRAVITINO_INITIAL_ADMIN_PASSWORD"; + private static final String ADMIN = "admin"; + private static final String ADMIN_PASSWORD = "Passw0rd-For-Admin1"; + private static final String USER1 = "user1"; + private static final String USER2 = "user2"; + private static final String GROUP1 = "group1"; + private static final String USER_PASSWORD = "Passw0rd-For-User1"; + private static final String UPDATED_PASSWORD = "Passw0rd-For-User2"; + + private static final HttpClient HTTP = HttpClient.newHttpClient(); + + private Path h2Path; + + static Stream<String> jdbcBackends() { + return Stream.of(H2_BACKEND, MYSQL_BACKEND, POSTGRESQL_BACKEND); + } + + @ParameterizedTest + @MethodSource("jdbcBackends") + public void testIdpRestApis(String backendType) throws Exception { + setEnv(INITIAL_ADMIN_PASSWORD_ENV, ADMIN_PASSWORD); + Config relationalConfig = createRelationalBackendConfig(backendType); + int httpPort = RESTUtils.findAvailablePort(5000, 6000); + ServerConfig serverConfig = newServerConfig(httpPort, relationalConfig); + + GravitinoServer gravitinoServer = new GravitinoServer(serverConfig, GravitinoEnv.getInstance()); + IdpUserGroupManager idpUserGroupManager = null; + try { + gravitinoServer.initialize(); + gravitinoServer.start(); + String apiBase = String.format("http://localhost:%d/api", httpPort); + idpUserGroupManager = + IdpUserGroupManager.getInstance(serverConfig, GravitinoEnv.getInstance().idGenerator()); + + assertEquals(200, get(apiBase, "/version", ADMIN, ADMIN_PASSWORD).statusCode()); + assertEquals(401, get(apiBase, "/idp/users/" + USER1, null, null).statusCode()); + + postUser(apiBase, USER2, USER_PASSWORD); + assertEquals(403, get(apiBase, "/idp/users/" + USER2, USER2, USER_PASSWORD).statusCode()); + deleteUser(apiBase, USER2); + + postUser(apiBase, USER1, USER_PASSWORD); + IdpUserResponse user = getUser(apiBase, USER1); + assertEquals(USER1, user.getUser().name()); + assertTrue(user.getUser().groups().isEmpty()); + + changePassword(apiBase, USER1, UPDATED_PASSWORD); + assertEquals(USER1, getUser(apiBase, USER1).getUser().name()); + + assertTrue(deleteUser(apiBase, USER1)); + assertEquals( + ErrorConstants.NOT_FOUND_CODE, + errorCode(get(apiBase, "/idp/users/" + USER1, ADMIN, ADMIN_PASSWORD))); + + postUser(apiBase, USER1, USER_PASSWORD); + postUser(apiBase, USER2, USER_PASSWORD); + + IdpGroupResponse group = postGroup(apiBase, GROUP1); + assertEquals(GROUP1, group.getGroup().name()); + assertTrue(group.getGroup().users().isEmpty()); + + group = + putMembership( + apiBase, GROUP1, new GroupMembershipChangeRequest(new String[] {USER1, USER2}, null)); + assertEquals(List.of(USER1, USER2), group.getGroup().users()); + + group = + putMembership( + apiBase, GROUP1, new GroupMembershipChangeRequest(null, new String[] {USER1, USER2})); + assertTrue(group.getGroup().users().isEmpty()); + + assertTrue(deleteGroup(apiBase, GROUP1, false)); + + deleteUser(apiBase, USER1); + deleteUser(apiBase, USER2); + } finally { + if (gravitinoServer != null) { + gravitinoServer.stop(); + } + if (idpUserGroupManager != null) { + idpUserGroupManager.close(); + } + cleanupRelationalBackend(backendType); + } + } + + private Config createRelationalBackendConfig(String backendType) throws IOException { + Config backendConfig = new Config(false) {}; + backendConfig.set(Configs.ENTITY_STORE, Configs.RELATIONAL_ENTITY_STORE); + backendConfig.set(Configs.ENTITY_RELATIONAL_STORE, backendType); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS, 100); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS, 1000L); + + switch (backendType) { + case MYSQL_BACKEND: + initializeMySQLBackend(backendConfig); + break; + case POSTGRESQL_BACKEND: + initializePostgreSQLBackend(backendConfig); + break; + case H2_BACKEND: + initializeH2Backend(backendConfig); + break; + default: + throw new IllegalArgumentException("Unsupported backend type: " + backendType); + } + return backendConfig; + } + + private void cleanupRelationalBackend(String backendType) throws IOException { + if (H2_BACKEND.equals(backendType) && h2Path != null && Files.exists(h2Path)) { + try (Stream<Path> paths = Files.walk(h2Path)) { + paths.sorted(Comparator.reverseOrder()).forEach(IdpRestApiIT::deletePath); + } + h2Path = null; + } + if (MYSQL_BACKEND.equals(backendType) || POSTGRESQL_BACKEND.equals(backendType)) { + ContainerSuite.getInstance().close(); + } + } + + private void initializeMySQLBackend(Config backendConfig) throws IOException { + ContainerSuite containerSuite = ContainerSuite.getInstance(); + containerSuite.startMySQLContainer(MYSQL_TEST_DATABASE); + MySQLContainer mySQLContainer = containerSuite.getMySQLContainer(); + String jdbcUrl = mySQLContainer.getJdbcUrl(MYSQL_TEST_DATABASE); + + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, jdbcUrl); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER, mySQLContainer.getUsername()); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD, mySQLContainer.getPassword()); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER, "com.mysql.cj.jdbc.Driver"); + + try (Connection connection = + DriverManager.getConnection( + StringUtils.substringBeforeLast(jdbcUrl, "/"), + mySQLContainer.getUsername(), + mySQLContainer.getPassword()); + Statement statement = connection.createStatement()) { + statement.execute("DROP DATABASE IF EXISTS " + MYSQL_TEST_DATABASE); + statement.execute("CREATE DATABASE " + MYSQL_TEST_DATABASE); + statement.execute("USE " + MYSQL_TEST_DATABASE); + executeSqlStatements(statement, loadSchemaStatements(MYSQL_BACKEND)); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize MySQL backend for IdP REST API IT", e); + } + } + + private void initializePostgreSQLBackend(Config backendConfig) throws IOException { + ContainerSuite containerSuite = ContainerSuite.getInstance(); + containerSuite.startPostgreSQLContainer(POSTGRESQL_TEST_DATABASE); + PostgreSQLContainer postgreSQLContainer = containerSuite.getPostgreSQLContainer(); + String schemaName = "idp_rest_" + UUID.randomUUID().toString().replace("-", ""); + String jdbcUrl = postgreSQLContainer.getJdbcUrl(POSTGRESQL_TEST_DATABASE); + + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, jdbcUrl + "?currentSchema=" + schemaName); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER, postgreSQLContainer.getUsername()); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD, postgreSQLContainer.getPassword()); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER, "org.postgresql.Driver"); + + try (Connection connection = + DriverManager.getConnection( + jdbcUrl, postgreSQLContainer.getUsername(), postgreSQLContainer.getPassword()); + Statement statement = connection.createStatement()) { + statement.execute("DROP SCHEMA IF EXISTS " + schemaName + " CASCADE"); + statement.execute("CREATE SCHEMA " + schemaName); + statement.execute("SET search_path TO " + schemaName); + executeSqlStatements(statement, loadSchemaStatements(POSTGRESQL_BACKEND)); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize PostgreSQL backend for IdP REST API IT", e); + } + } + + private void initializeH2Backend(Config backendConfig) throws IOException { + h2Path = Files.createTempDirectory("gravitino_idp_rest_api_it_"); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, + String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", h2Path)); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER, "root"); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD, "123456"); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER, "org.h2.Driver"); + } + + private static String[] loadSchemaStatements(String databaseType) throws IOException { + Path scriptPath = + resolveProjectRoot() + .resolve("scripts") + .resolve(databaseType) + .resolve( + String.format( + "schema-%s-%s.sql", ConfigConstants.CURRENT_SCRIPT_VERSION, databaseType)); + return Arrays.stream(Files.readString(scriptPath).split(";")) + .map(String::trim) + .filter(sql -> !sql.isEmpty()) + .toArray(String[]::new); + } + + private static Path resolveProjectRoot() { + String rootDir = System.getenv("GRAVITINO_ROOT_DIR"); + if (StringUtils.isBlank(rootDir)) { + rootDir = System.getenv("GRAVITINO_HOME"); + } + if (StringUtils.isBlank(rootDir)) { + throw new IllegalStateException( + "GRAVITINO_ROOT_DIR or GRAVITINO_HOME must be set for IdP REST API IT"); + } + return Path.of(rootDir); + } + + private static void executeSqlStatements(Statement statement, String[] sqlStatements) + throws SQLException { + for (String sql : sqlStatements) { + statement.execute(sql); + } + } + + private static void deletePath(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new RuntimeException("Delete path failed: " + path, e); + } + } + + private static ServerConfig newServerConfig(int httpPort, Config relationalConfig) { + ImmutableMap.Builder<String, String> builder = + ImmutableMap.<String, String>builder() + .put( + GravitinoServer.WEBSERVER_CONF_PREFIX + + JettyServerConfig.WEBSERVER_HTTP_PORT.getKey(), + String.valueOf(httpPort)) + .put(Configs.ENTITY_STORE.getKey(), Configs.RELATIONAL_ENTITY_STORE) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS.getKey(), + String.valueOf( + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS))) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS.getKey(), + String.valueOf( + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))) + .put(Configs.STORE_DELETE_AFTER_TIME.getKey(), String.valueOf(20 * 60 * 1000L)) + .put(Configs.CACHE_ENABLED.getKey(), "false") + .put(Configs.ENABLE_AUTHORIZATION.getKey(), "false") + .put(Configs.AUTHENTICATORS.getKey(), BASIC_AUTHENTICATOR_CLASS) + .put(Configs.SERVICE_ADMINS.getKey(), ADMIN) + .put(Configs.REST_API_EXTENSION_PACKAGES.getKey(), IDP_REST_EXTENSION_PACKAGE); + + ServerConfig config = new ServerConfig(); + config.loadFromMap(builder.build(), key -> true); + ServerConfig spyConfig = Mockito.spy(config); + Mockito.when( + spyConfig.getConfigsWithPrefix(AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX)) + .thenReturn(ImmutableMap.of(AuxiliaryServiceManager.AUX_SERVICE_NAMES, "")); + return spyConfig; + } + + private static HttpResponse<String> get( + String apiBase, String path, String username, String password) throws Exception { + HttpRequest.Builder builder = + HttpRequest.newBuilder().uri(URI.create(apiBase + path)).header("Accept", ACCEPT).GET(); + if (username != null) { + builder.header("Authorization", basicAuth(username, password)); + } + return HTTP.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + } + + private static void postUser(String apiBase, String username, String password) throws Exception { + HttpResponse<String> response = + post(apiBase, "/idp/users", new AddUserRequest(username, password)); + assertEquals(200, response.statusCode(), response.body()); + JsonUtils.objectMapper().readValue(response.body(), IdpUserResponse.class).validate(); + } + + private static IdpUserResponse getUser(String apiBase, String username) throws Exception { + HttpResponse<String> response = get(apiBase, "/idp/users/" + username, ADMIN, ADMIN_PASSWORD); + assertEquals(200, response.statusCode(), response.body()); + IdpUserResponse userResponse = + JsonUtils.objectMapper().readValue(response.body(), IdpUserResponse.class); + userResponse.validate(); + return userResponse; + } + + private static void changePassword(String apiBase, String username, String password) + throws Exception { + HttpResponse<String> response = + put(apiBase, "/idp/users/" + username, new ChangePasswordRequest(password)); + assertEquals(200, response.statusCode(), response.body()); + } + + private static boolean deleteUser(String apiBase, String username) throws Exception { + HttpResponse<String> response = + HTTP.send( + authorized(ADMIN, ADMIN_PASSWORD) + .uri(URI.create(apiBase + "/idp/users/" + username)) + .DELETE() + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode(), response.body()); + return JsonUtils.objectMapper().readTree(response.body()).get("removed").asBoolean(); + } + + private static IdpGroupResponse postGroup(String apiBase, String groupName) throws Exception { + HttpResponse<String> response = post(apiBase, "/idp/groups", new AddGroupRequest(groupName)); + assertEquals(200, response.statusCode(), response.body()); + IdpGroupResponse groupResponse = + JsonUtils.objectMapper().readValue(response.body(), IdpGroupResponse.class); + groupResponse.validate(); + return groupResponse; + } + + private static IdpGroupResponse putMembership( Review Comment: changeMembership? ########## plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRestApiIT.java: ########## @@ -0,0 +1,482 @@ +/* + * 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.integration.test; + +import static org.apache.gravitino.integration.test.util.BaseIT.setEnv; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.Configs; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.auxiliary.AuxiliaryServiceManager; +import org.apache.gravitino.config.ConfigConstants; +import org.apache.gravitino.dto.responses.ErrorConstants; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.dto.requests.AddGroupRequest; +import org.apache.gravitino.idp.dto.requests.AddUserRequest; +import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest; +import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest; +import org.apache.gravitino.idp.dto.responses.IdpGroupResponse; +import org.apache.gravitino.idp.dto.responses.IdpUserResponse; +import org.apache.gravitino.integration.test.container.ContainerSuite; +import org.apache.gravitino.integration.test.container.MySQLContainer; +import org.apache.gravitino.integration.test.container.PostgreSQLContainer; +import org.apache.gravitino.integration.test.util.TestDatabaseName; +import org.apache.gravitino.json.JsonUtils; +import org.apache.gravitino.rest.RESTUtils; +import org.apache.gravitino.server.GravitinoServer; +import org.apache.gravitino.server.ServerConfig; +import org.apache.gravitino.server.web.JettyServerConfig; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +/** + * End-to-end tests for built-in IdP REST APIs on an embedded Gravitino server. + * + * <p>Runs the same REST scenario against H2, MySQL, and PostgreSQL relational backends. + */ +@Tag("gravitino-docker-test") +public class IdpRestApiIT { + + private static final String H2_BACKEND = "h2"; + private static final String MYSQL_BACKEND = "mysql"; + private static final String POSTGRESQL_BACKEND = "postgresql"; + + private static final TestDatabaseName MYSQL_TEST_DATABASE = TestDatabaseName.MYSQL_JDBC_BACKEND; + private static final TestDatabaseName POSTGRESQL_TEST_DATABASE = TestDatabaseName.PG_JDBC_BACKEND; + + private static final String ACCEPT = "application/vnd.gravitino.v1+json"; + private static final String IDP_REST_EXTENSION_PACKAGE = + "org.apache.gravitino.idp.web.rest.feature"; + private static final String BASIC_AUTHENTICATOR_CLASS = + "org.apache.gravitino.idp.auth.BasicAuthenticator"; + private static final String INITIAL_ADMIN_PASSWORD_ENV = "GRAVITINO_INITIAL_ADMIN_PASSWORD"; + private static final String ADMIN = "admin"; + private static final String ADMIN_PASSWORD = "Passw0rd-For-Admin1"; + private static final String USER1 = "user1"; + private static final String USER2 = "user2"; + private static final String GROUP1 = "group1"; + private static final String USER_PASSWORD = "Passw0rd-For-User1"; + private static final String UPDATED_PASSWORD = "Passw0rd-For-User2"; + + private static final HttpClient HTTP = HttpClient.newHttpClient(); + + private Path h2Path; + + static Stream<String> jdbcBackends() { + return Stream.of(H2_BACKEND, MYSQL_BACKEND, POSTGRESQL_BACKEND); + } + + @ParameterizedTest + @MethodSource("jdbcBackends") + public void testIdpRestApis(String backendType) throws Exception { + setEnv(INITIAL_ADMIN_PASSWORD_ENV, ADMIN_PASSWORD); + Config relationalConfig = createRelationalBackendConfig(backendType); + int httpPort = RESTUtils.findAvailablePort(5000, 6000); + ServerConfig serverConfig = newServerConfig(httpPort, relationalConfig); + + GravitinoServer gravitinoServer = new GravitinoServer(serverConfig, GravitinoEnv.getInstance()); + IdpUserGroupManager idpUserGroupManager = null; + try { + gravitinoServer.initialize(); + gravitinoServer.start(); + String apiBase = String.format("http://localhost:%d/api", httpPort); + idpUserGroupManager = + IdpUserGroupManager.getInstance(serverConfig, GravitinoEnv.getInstance().idGenerator()); + + assertEquals(200, get(apiBase, "/version", ADMIN, ADMIN_PASSWORD).statusCode()); + assertEquals(401, get(apiBase, "/idp/users/" + USER1, null, null).statusCode()); + + postUser(apiBase, USER2, USER_PASSWORD); + assertEquals(403, get(apiBase, "/idp/users/" + USER2, USER2, USER_PASSWORD).statusCode()); + deleteUser(apiBase, USER2); + + postUser(apiBase, USER1, USER_PASSWORD); + IdpUserResponse user = getUser(apiBase, USER1); + assertEquals(USER1, user.getUser().name()); + assertTrue(user.getUser().groups().isEmpty()); + + changePassword(apiBase, USER1, UPDATED_PASSWORD); + assertEquals(USER1, getUser(apiBase, USER1).getUser().name()); + + assertTrue(deleteUser(apiBase, USER1)); + assertEquals( + ErrorConstants.NOT_FOUND_CODE, + errorCode(get(apiBase, "/idp/users/" + USER1, ADMIN, ADMIN_PASSWORD))); + + postUser(apiBase, USER1, USER_PASSWORD); + postUser(apiBase, USER2, USER_PASSWORD); + + IdpGroupResponse group = postGroup(apiBase, GROUP1); + assertEquals(GROUP1, group.getGroup().name()); + assertTrue(group.getGroup().users().isEmpty()); + + group = + putMembership( + apiBase, GROUP1, new GroupMembershipChangeRequest(new String[] {USER1, USER2}, null)); + assertEquals(List.of(USER1, USER2), group.getGroup().users()); + + group = + putMembership( + apiBase, GROUP1, new GroupMembershipChangeRequest(null, new String[] {USER1, USER2})); + assertTrue(group.getGroup().users().isEmpty()); + + assertTrue(deleteGroup(apiBase, GROUP1, false)); + + deleteUser(apiBase, USER1); + deleteUser(apiBase, USER2); + } finally { + if (gravitinoServer != null) { + gravitinoServer.stop(); + } + if (idpUserGroupManager != null) { + idpUserGroupManager.close(); Review Comment: Why do we need to close user group manager here? ########## plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRestApiIT.java: ########## @@ -0,0 +1,482 @@ +/* + * 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.integration.test; + +import static org.apache.gravitino.integration.test.util.BaseIT.setEnv; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.Configs; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.auxiliary.AuxiliaryServiceManager; +import org.apache.gravitino.config.ConfigConstants; +import org.apache.gravitino.dto.responses.ErrorConstants; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.dto.requests.AddGroupRequest; +import org.apache.gravitino.idp.dto.requests.AddUserRequest; +import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest; +import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest; +import org.apache.gravitino.idp.dto.responses.IdpGroupResponse; +import org.apache.gravitino.idp.dto.responses.IdpUserResponse; +import org.apache.gravitino.integration.test.container.ContainerSuite; +import org.apache.gravitino.integration.test.container.MySQLContainer; +import org.apache.gravitino.integration.test.container.PostgreSQLContainer; +import org.apache.gravitino.integration.test.util.TestDatabaseName; +import org.apache.gravitino.json.JsonUtils; +import org.apache.gravitino.rest.RESTUtils; +import org.apache.gravitino.server.GravitinoServer; +import org.apache.gravitino.server.ServerConfig; +import org.apache.gravitino.server.web.JettyServerConfig; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +/** + * End-to-end tests for built-in IdP REST APIs on an embedded Gravitino server. + * + * <p>Runs the same REST scenario against H2, MySQL, and PostgreSQL relational backends. + */ +@Tag("gravitino-docker-test") +public class IdpRestApiIT { + + private static final String H2_BACKEND = "h2"; + private static final String MYSQL_BACKEND = "mysql"; + private static final String POSTGRESQL_BACKEND = "postgresql"; + + private static final TestDatabaseName MYSQL_TEST_DATABASE = TestDatabaseName.MYSQL_JDBC_BACKEND; + private static final TestDatabaseName POSTGRESQL_TEST_DATABASE = TestDatabaseName.PG_JDBC_BACKEND; + + private static final String ACCEPT = "application/vnd.gravitino.v1+json"; + private static final String IDP_REST_EXTENSION_PACKAGE = + "org.apache.gravitino.idp.web.rest.feature"; + private static final String BASIC_AUTHENTICATOR_CLASS = + "org.apache.gravitino.idp.auth.BasicAuthenticator"; + private static final String INITIAL_ADMIN_PASSWORD_ENV = "GRAVITINO_INITIAL_ADMIN_PASSWORD"; + private static final String ADMIN = "admin"; + private static final String ADMIN_PASSWORD = "Passw0rd-For-Admin1"; + private static final String USER1 = "user1"; + private static final String USER2 = "user2"; + private static final String GROUP1 = "group1"; + private static final String USER_PASSWORD = "Passw0rd-For-User1"; + private static final String UPDATED_PASSWORD = "Passw0rd-For-User2"; + + private static final HttpClient HTTP = HttpClient.newHttpClient(); + + private Path h2Path; + + static Stream<String> jdbcBackends() { + return Stream.of(H2_BACKEND, MYSQL_BACKEND, POSTGRESQL_BACKEND); + } + + @ParameterizedTest + @MethodSource("jdbcBackends") + public void testIdpRestApis(String backendType) throws Exception { + setEnv(INITIAL_ADMIN_PASSWORD_ENV, ADMIN_PASSWORD); + Config relationalConfig = createRelationalBackendConfig(backendType); + int httpPort = RESTUtils.findAvailablePort(5000, 6000); + ServerConfig serverConfig = newServerConfig(httpPort, relationalConfig); + + GravitinoServer gravitinoServer = new GravitinoServer(serverConfig, GravitinoEnv.getInstance()); + IdpUserGroupManager idpUserGroupManager = null; + try { + gravitinoServer.initialize(); + gravitinoServer.start(); + String apiBase = String.format("http://localhost:%d/api", httpPort); + idpUserGroupManager = + IdpUserGroupManager.getInstance(serverConfig, GravitinoEnv.getInstance().idGenerator()); + + assertEquals(200, get(apiBase, "/version", ADMIN, ADMIN_PASSWORD).statusCode()); + assertEquals(401, get(apiBase, "/idp/users/" + USER1, null, null).statusCode()); + + postUser(apiBase, USER2, USER_PASSWORD); + assertEquals(403, get(apiBase, "/idp/users/" + USER2, USER2, USER_PASSWORD).statusCode()); + deleteUser(apiBase, USER2); + + postUser(apiBase, USER1, USER_PASSWORD); + IdpUserResponse user = getUser(apiBase, USER1); + assertEquals(USER1, user.getUser().name()); + assertTrue(user.getUser().groups().isEmpty()); + + changePassword(apiBase, USER1, UPDATED_PASSWORD); + assertEquals(USER1, getUser(apiBase, USER1).getUser().name()); + + assertTrue(deleteUser(apiBase, USER1)); + assertEquals( + ErrorConstants.NOT_FOUND_CODE, + errorCode(get(apiBase, "/idp/users/" + USER1, ADMIN, ADMIN_PASSWORD))); + + postUser(apiBase, USER1, USER_PASSWORD); + postUser(apiBase, USER2, USER_PASSWORD); + + IdpGroupResponse group = postGroup(apiBase, GROUP1); + assertEquals(GROUP1, group.getGroup().name()); + assertTrue(group.getGroup().users().isEmpty()); + + group = + putMembership( + apiBase, GROUP1, new GroupMembershipChangeRequest(new String[] {USER1, USER2}, null)); + assertEquals(List.of(USER1, USER2), group.getGroup().users()); + + group = + putMembership( + apiBase, GROUP1, new GroupMembershipChangeRequest(null, new String[] {USER1, USER2})); + assertTrue(group.getGroup().users().isEmpty()); + + assertTrue(deleteGroup(apiBase, GROUP1, false)); + + deleteUser(apiBase, USER1); + deleteUser(apiBase, USER2); + } finally { + if (gravitinoServer != null) { + gravitinoServer.stop(); + } + if (idpUserGroupManager != null) { + idpUserGroupManager.close(); + } + cleanupRelationalBackend(backendType); + } + } + + private Config createRelationalBackendConfig(String backendType) throws IOException { + Config backendConfig = new Config(false) {}; + backendConfig.set(Configs.ENTITY_STORE, Configs.RELATIONAL_ENTITY_STORE); + backendConfig.set(Configs.ENTITY_RELATIONAL_STORE, backendType); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS, 100); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS, 1000L); + + switch (backendType) { + case MYSQL_BACKEND: + initializeMySQLBackend(backendConfig); + break; + case POSTGRESQL_BACKEND: + initializePostgreSQLBackend(backendConfig); + break; + case H2_BACKEND: + initializeH2Backend(backendConfig); + break; + default: + throw new IllegalArgumentException("Unsupported backend type: " + backendType); + } + return backendConfig; + } + + private void cleanupRelationalBackend(String backendType) throws IOException { + if (H2_BACKEND.equals(backendType) && h2Path != null && Files.exists(h2Path)) { + try (Stream<Path> paths = Files.walk(h2Path)) { + paths.sorted(Comparator.reverseOrder()).forEach(IdpRestApiIT::deletePath); + } + h2Path = null; + } + if (MYSQL_BACKEND.equals(backendType) || POSTGRESQL_BACKEND.equals(backendType)) { + ContainerSuite.getInstance().close(); + } + } + + private void initializeMySQLBackend(Config backendConfig) throws IOException { + ContainerSuite containerSuite = ContainerSuite.getInstance(); + containerSuite.startMySQLContainer(MYSQL_TEST_DATABASE); + MySQLContainer mySQLContainer = containerSuite.getMySQLContainer(); + String jdbcUrl = mySQLContainer.getJdbcUrl(MYSQL_TEST_DATABASE); + + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, jdbcUrl); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER, mySQLContainer.getUsername()); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD, mySQLContainer.getPassword()); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER, "com.mysql.cj.jdbc.Driver"); + + try (Connection connection = + DriverManager.getConnection( + StringUtils.substringBeforeLast(jdbcUrl, "/"), + mySQLContainer.getUsername(), + mySQLContainer.getPassword()); + Statement statement = connection.createStatement()) { + statement.execute("DROP DATABASE IF EXISTS " + MYSQL_TEST_DATABASE); + statement.execute("CREATE DATABASE " + MYSQL_TEST_DATABASE); + statement.execute("USE " + MYSQL_TEST_DATABASE); + executeSqlStatements(statement, loadSchemaStatements(MYSQL_BACKEND)); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize MySQL backend for IdP REST API IT", e); + } + } + + private void initializePostgreSQLBackend(Config backendConfig) throws IOException { + ContainerSuite containerSuite = ContainerSuite.getInstance(); + containerSuite.startPostgreSQLContainer(POSTGRESQL_TEST_DATABASE); + PostgreSQLContainer postgreSQLContainer = containerSuite.getPostgreSQLContainer(); + String schemaName = "idp_rest_" + UUID.randomUUID().toString().replace("-", ""); + String jdbcUrl = postgreSQLContainer.getJdbcUrl(POSTGRESQL_TEST_DATABASE); + + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, jdbcUrl + "?currentSchema=" + schemaName); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER, postgreSQLContainer.getUsername()); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD, postgreSQLContainer.getPassword()); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER, "org.postgresql.Driver"); + + try (Connection connection = + DriverManager.getConnection( + jdbcUrl, postgreSQLContainer.getUsername(), postgreSQLContainer.getPassword()); + Statement statement = connection.createStatement()) { + statement.execute("DROP SCHEMA IF EXISTS " + schemaName + " CASCADE"); + statement.execute("CREATE SCHEMA " + schemaName); + statement.execute("SET search_path TO " + schemaName); + executeSqlStatements(statement, loadSchemaStatements(POSTGRESQL_BACKEND)); + } catch (SQLException e) { + throw new RuntimeException("Failed to initialize PostgreSQL backend for IdP REST API IT", e); + } + } + + private void initializeH2Backend(Config backendConfig) throws IOException { + h2Path = Files.createTempDirectory("gravitino_idp_rest_api_it_"); + backendConfig.set( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL, + String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", h2Path)); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER, "root"); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD, "123456"); + backendConfig.set(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER, "org.h2.Driver"); + } + + private static String[] loadSchemaStatements(String databaseType) throws IOException { + Path scriptPath = + resolveProjectRoot() + .resolve("scripts") + .resolve(databaseType) + .resolve( + String.format( + "schema-%s-%s.sql", ConfigConstants.CURRENT_SCRIPT_VERSION, databaseType)); + return Arrays.stream(Files.readString(scriptPath).split(";")) + .map(String::trim) + .filter(sql -> !sql.isEmpty()) + .toArray(String[]::new); + } + + private static Path resolveProjectRoot() { + String rootDir = System.getenv("GRAVITINO_ROOT_DIR"); + if (StringUtils.isBlank(rootDir)) { + rootDir = System.getenv("GRAVITINO_HOME"); + } + if (StringUtils.isBlank(rootDir)) { + throw new IllegalStateException( + "GRAVITINO_ROOT_DIR or GRAVITINO_HOME must be set for IdP REST API IT"); + } + return Path.of(rootDir); + } + + private static void executeSqlStatements(Statement statement, String[] sqlStatements) + throws SQLException { + for (String sql : sqlStatements) { + statement.execute(sql); + } + } + + private static void deletePath(Path path) { + try { + Files.deleteIfExists(path); + } catch (IOException e) { + throw new RuntimeException("Delete path failed: " + path, e); + } + } + + private static ServerConfig newServerConfig(int httpPort, Config relationalConfig) { + ImmutableMap.Builder<String, String> builder = + ImmutableMap.<String, String>builder() + .put( + GravitinoServer.WEBSERVER_CONF_PREFIX + + JettyServerConfig.WEBSERVER_HTTP_PORT.getKey(), + String.valueOf(httpPort)) + .put(Configs.ENTITY_STORE.getKey(), Configs.RELATIONAL_ENTITY_STORE) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_USER)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD.getKey(), + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_PASSWORD)) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS.getKey(), + String.valueOf( + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_MAX_CONNECTIONS))) + .put( + Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS.getKey(), + String.valueOf( + relationalConfig.get(Configs.ENTITY_RELATIONAL_JDBC_BACKEND_WAIT_MILLISECONDS))) + .put(Configs.STORE_DELETE_AFTER_TIME.getKey(), String.valueOf(20 * 60 * 1000L)) + .put(Configs.CACHE_ENABLED.getKey(), "false") + .put(Configs.ENABLE_AUTHORIZATION.getKey(), "false") + .put(Configs.AUTHENTICATORS.getKey(), BASIC_AUTHENTICATOR_CLASS) + .put(Configs.SERVICE_ADMINS.getKey(), ADMIN) + .put(Configs.REST_API_EXTENSION_PACKAGES.getKey(), IDP_REST_EXTENSION_PACKAGE); + + ServerConfig config = new ServerConfig(); + config.loadFromMap(builder.build(), key -> true); + ServerConfig spyConfig = Mockito.spy(config); + Mockito.when( + spyConfig.getConfigsWithPrefix(AuxiliaryServiceManager.GRAVITINO_AUX_SERVICE_PREFIX)) + .thenReturn(ImmutableMap.of(AuxiliaryServiceManager.AUX_SERVICE_NAMES, "")); + return spyConfig; + } + + private static HttpResponse<String> get( + String apiBase, String path, String username, String password) throws Exception { + HttpRequest.Builder builder = + HttpRequest.newBuilder().uri(URI.create(apiBase + path)).header("Accept", ACCEPT).GET(); + if (username != null) { + builder.header("Authorization", basicAuth(username, password)); + } + return HTTP.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + } + + private static void postUser(String apiBase, String username, String password) throws Exception { + HttpResponse<String> response = + post(apiBase, "/idp/users", new AddUserRequest(username, password)); + assertEquals(200, response.statusCode(), response.body()); + JsonUtils.objectMapper().readValue(response.body(), IdpUserResponse.class).validate(); + } + + private static IdpUserResponse getUser(String apiBase, String username) throws Exception { + HttpResponse<String> response = get(apiBase, "/idp/users/" + username, ADMIN, ADMIN_PASSWORD); + assertEquals(200, response.statusCode(), response.body()); + IdpUserResponse userResponse = + JsonUtils.objectMapper().readValue(response.body(), IdpUserResponse.class); + userResponse.validate(); + return userResponse; + } + + private static void changePassword(String apiBase, String username, String password) + throws Exception { + HttpResponse<String> response = + put(apiBase, "/idp/users/" + username, new ChangePasswordRequest(password)); + assertEquals(200, response.statusCode(), response.body()); + } + + private static boolean deleteUser(String apiBase, String username) throws Exception { + HttpResponse<String> response = + HTTP.send( + authorized(ADMIN, ADMIN_PASSWORD) + .uri(URI.create(apiBase + "/idp/users/" + username)) + .DELETE() + .build(), + HttpResponse.BodyHandlers.ofString()); + assertEquals(200, response.statusCode(), response.body()); + return JsonUtils.objectMapper().readTree(response.body()).get("removed").asBoolean(); + } + + private static IdpGroupResponse postGroup(String apiBase, String groupName) throws Exception { Review Comment: addGroup? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -161,17 +243,34 @@ public IdpGroup changeGroupMembership( Preconditions.checkArgument( !usersToAddList.isEmpty() || !usersToRemoveList.isEmpty(), "usersToAdd and usersToRemove cannot both be empty"); - GROUP_SERVICE.changeGroupMembership(groupName, usersToAddList, usersToRemoveList); + groupMetaService.changeGroupMembership(groupName, usersToAddList, usersToRemoveList); return getGroup(groupName); } @Override public void close() throws IOException { - garbageCollector.close(); - relationalStorage.close(); + try { + if (garbageCollector != null) { + garbageCollector.close(); + } + if (relationalStorage != null) { + relationalStorage.close(); + } + } finally { + synchronized (IdpUserGroupManager.class) { Review Comment: It is weird to recursive close. ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -45,28 +53,87 @@ */ public class IdpUserGroupManager implements Closeable { - private static final IdpUserMetaService USER_SERVICE = IdpUserMetaService.getInstance(); - private static final IdpGroupMetaService GROUP_SERVICE = IdpGroupMetaService.getInstance(); + private static final String BASIC_AUTHENTICATOR_CLASS_NAME = + BasicAuthenticator.class.getCanonicalName(); + + private static volatile IdpUserGroupManager instance; private final IdpRelationalStorage relationalStorage; private final IdGenerator idGenerator; private final PasswordHasher passwordHasher; + private final IdpUserMetaService userMetaService; + private final IdpGroupMetaService groupMetaService; private final IdpGarbageCollector garbageCollector; - /** - * Creates a built-in IdP user and group manager. - * - * @param config The server configuration. - * @param idGenerator The id generator. - */ - public IdpUserGroupManager(Config config, IdGenerator idGenerator) { + public static IdpUserGroupManager getInstance(Config config, IdGenerator idGenerator) { + IdpUserGroupManager local = instance; + if (local == null) { + synchronized (IdpUserGroupManager.class) { + local = instance; + if (local == null) { + instance = new IdpUserGroupManager(config, idGenerator); + local = instance; + } + } + } + return local; + } + + private IdpUserGroupManager(Config config, IdGenerator idGenerator) { this.relationalStorage = new IdpRelationalStorage(config); this.idGenerator = idGenerator; this.passwordHasher = PasswordHasherFactory.create(); + this.userMetaService = IdpUserMetaService.getInstance(); + this.groupMetaService = IdpGroupMetaService.getInstance(); this.garbageCollector = new IdpGarbageCollector(config); garbageCollector.start(); } + IdpUserGroupManager( + IdGenerator idGenerator, IdpUserMetaService userMetaService, PasswordHasher passwordHasher) { + this.relationalStorage = null; + this.idGenerator = idGenerator; + this.passwordHasher = passwordHasher; + this.userMetaService = userMetaService; + this.groupMetaService = null; + this.garbageCollector = null; + } + + public void initializeConfiguredServiceAdmins(Config config, String initialAdminPassword) + throws IOException { + if (!basicAuthenticatorEnabled(config)) { + return; + } + + List<String> serviceAdmins = config.get(Configs.SERVICE_ADMINS); + if (serviceAdmins == null || serviceAdmins.isEmpty()) { Review Comment: Could we use StringUtils.isNotBlank? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/IdpBasicBinder.java: ########## @@ -28,7 +28,7 @@ public class IdpBasicBinder extends AbstractBinder { @Override protected void configure() { GravitinoEnv gravitinoEnv = GravitinoEnv.getInstance(); - bind(new IdpUserGroupManager(gravitinoEnv.config(), gravitinoEnv.idGenerator())) + bind(IdpUserGroupManager.getInstance(gravitinoEnv.config(), gravitinoEnv.idGenerator())) Review Comment: Do u have the method to avoid singleton here? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -182,6 +281,29 @@ private IdpUserPO newUserPO(String username, String passwordHash) { .build(); } + private boolean userExists(String username) { Review Comment: checkUserExistence. ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -45,28 +53,87 @@ */ public class IdpUserGroupManager implements Closeable { - private static final IdpUserMetaService USER_SERVICE = IdpUserMetaService.getInstance(); - private static final IdpGroupMetaService GROUP_SERVICE = IdpGroupMetaService.getInstance(); + private static final String BASIC_AUTHENTICATOR_CLASS_NAME = + BasicAuthenticator.class.getCanonicalName(); + + private static volatile IdpUserGroupManager instance; private final IdpRelationalStorage relationalStorage; private final IdGenerator idGenerator; private final PasswordHasher passwordHasher; + private final IdpUserMetaService userMetaService; + private final IdpGroupMetaService groupMetaService; private final IdpGarbageCollector garbageCollector; - /** - * Creates a built-in IdP user and group manager. - * - * @param config The server configuration. - * @param idGenerator The id generator. - */ - public IdpUserGroupManager(Config config, IdGenerator idGenerator) { + public static IdpUserGroupManager getInstance(Config config, IdGenerator idGenerator) { Review Comment: Could u use lazy holder here? ########## server-common/src/test/java/org/apache/gravitino/server/authentication/TestBasicAuthentication.java: ########## @@ -0,0 +1,120 @@ +/* + * 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.server.authentication; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.google.common.collect.Lists; +import java.lang.reflect.Field; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Collections; +import java.util.Vector; +import javax.servlet.FilterChain; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.apache.gravitino.UserPrincipal; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.auth.BasicAuthenticator; +import org.apache.gravitino.idp.model.IdpUser; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +public class TestBasicAuthentication { Review Comment: Why do u put this class into this package? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -45,28 +53,87 @@ */ public class IdpUserGroupManager implements Closeable { - private static final IdpUserMetaService USER_SERVICE = IdpUserMetaService.getInstance(); - private static final IdpGroupMetaService GROUP_SERVICE = IdpGroupMetaService.getInstance(); + private static final String BASIC_AUTHENTICATOR_CLASS_NAME = + BasicAuthenticator.class.getCanonicalName(); + + private static volatile IdpUserGroupManager instance; private final IdpRelationalStorage relationalStorage; private final IdGenerator idGenerator; private final PasswordHasher passwordHasher; + private final IdpUserMetaService userMetaService; + private final IdpGroupMetaService groupMetaService; private final IdpGarbageCollector garbageCollector; - /** - * Creates a built-in IdP user and group manager. - * - * @param config The server configuration. - * @param idGenerator The id generator. - */ - public IdpUserGroupManager(Config config, IdGenerator idGenerator) { + public static IdpUserGroupManager getInstance(Config config, IdGenerator idGenerator) { + IdpUserGroupManager local = instance; + if (local == null) { + synchronized (IdpUserGroupManager.class) { + local = instance; + if (local == null) { + instance = new IdpUserGroupManager(config, idGenerator); + local = instance; + } + } + } + return local; + } + + private IdpUserGroupManager(Config config, IdGenerator idGenerator) { this.relationalStorage = new IdpRelationalStorage(config); this.idGenerator = idGenerator; this.passwordHasher = PasswordHasherFactory.create(); + this.userMetaService = IdpUserMetaService.getInstance(); + this.groupMetaService = IdpGroupMetaService.getInstance(); this.garbageCollector = new IdpGarbageCollector(config); garbageCollector.start(); } + IdpUserGroupManager( Review Comment: Why do we need this constructor? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/auth/ServiceAdminInitializer.java: ########## @@ -0,0 +1,179 @@ +/* + * 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.auth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import javax.annotation.Nullable; +import org.apache.commons.lang3.StringUtils; +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.basic.IdpCredentialValidator; +import org.apache.gravitino.idp.basic.password.PasswordHasher; +import org.apache.gravitino.idp.basic.password.PasswordHasherFactory; +import org.apache.gravitino.idp.exception.NotFoundException; +import org.apache.gravitino.idp.storage.po.IdpUserPO; +import org.apache.gravitino.idp.storage.service.IdpUserMetaService; +import org.apache.gravitino.json.JsonUtils; +import org.apache.gravitino.storage.IdGenerator; +import org.apache.gravitino.storage.relational.utils.POConverters; + +/** Initializes configured service admins in the built-in IdP during server startup. */ +public final class ServiceAdminInitializer { Review Comment: You can remove this class directly. ########## plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRestApiIT.java: ########## @@ -0,0 +1,482 @@ +/* + * 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.integration.test; + +import static org.apache.gravitino.integration.test.util.BaseIT.setEnv; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.Configs; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.auxiliary.AuxiliaryServiceManager; +import org.apache.gravitino.config.ConfigConstants; +import org.apache.gravitino.dto.responses.ErrorConstants; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.dto.requests.AddGroupRequest; +import org.apache.gravitino.idp.dto.requests.AddUserRequest; +import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest; +import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest; +import org.apache.gravitino.idp.dto.responses.IdpGroupResponse; +import org.apache.gravitino.idp.dto.responses.IdpUserResponse; +import org.apache.gravitino.integration.test.container.ContainerSuite; +import org.apache.gravitino.integration.test.container.MySQLContainer; +import org.apache.gravitino.integration.test.container.PostgreSQLContainer; +import org.apache.gravitino.integration.test.util.TestDatabaseName; +import org.apache.gravitino.json.JsonUtils; +import org.apache.gravitino.rest.RESTUtils; +import org.apache.gravitino.server.GravitinoServer; +import org.apache.gravitino.server.ServerConfig; +import org.apache.gravitino.server.web.JettyServerConfig; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +/** + * End-to-end tests for built-in IdP REST APIs on an embedded Gravitino server. + * + * <p>Runs the same REST scenario against H2, MySQL, and PostgreSQL relational backends. + */ +@Tag("gravitino-docker-test") +public class IdpRestApiIT { Review Comment: cc @mchades Could u help me review this IT? ########## plugins/idp-basic/src/test/java/org/apache/gravitino/idp/integration/test/IdpRestApiIT.java: ########## @@ -0,0 +1,482 @@ +/* + * 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.integration.test; + +import static org.apache.gravitino.integration.test.util.BaseIT.setEnv; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.google.common.collect.ImmutableMap; +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Arrays; +import java.util.Base64; +import java.util.Comparator; +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.Configs; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.auxiliary.AuxiliaryServiceManager; +import org.apache.gravitino.config.ConfigConstants; +import org.apache.gravitino.dto.responses.ErrorConstants; +import org.apache.gravitino.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.dto.requests.AddGroupRequest; +import org.apache.gravitino.idp.dto.requests.AddUserRequest; +import org.apache.gravitino.idp.dto.requests.ChangePasswordRequest; +import org.apache.gravitino.idp.dto.requests.GroupMembershipChangeRequest; +import org.apache.gravitino.idp.dto.responses.IdpGroupResponse; +import org.apache.gravitino.idp.dto.responses.IdpUserResponse; +import org.apache.gravitino.integration.test.container.ContainerSuite; +import org.apache.gravitino.integration.test.container.MySQLContainer; +import org.apache.gravitino.integration.test.container.PostgreSQLContainer; +import org.apache.gravitino.integration.test.util.TestDatabaseName; +import org.apache.gravitino.json.JsonUtils; +import org.apache.gravitino.rest.RESTUtils; +import org.apache.gravitino.server.GravitinoServer; +import org.apache.gravitino.server.ServerConfig; +import org.apache.gravitino.server.web.JettyServerConfig; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Mockito; + +/** + * End-to-end tests for built-in IdP REST APIs on an embedded Gravitino server. + * + * <p>Runs the same REST scenario against H2, MySQL, and PostgreSQL relational backends. + */ +@Tag("gravitino-docker-test") +public class IdpRestApiIT { Review Comment: IdpRESTApiIT ########## docs/security/how-to-authenticate.md: ########## @@ -43,6 +43,42 @@ curl -v -X GET \ http://localhost:8090/api/version ``` +### Basic mode + +In Basic mode, Gravitino verifies HTTP Basic credentials against built-in IDP user metadata stored +in the relational entity store. + +To enable Basic mode: + +- Set `gravitino.authenticators` to `org.apache.gravitino.idp.auth.BasicAuthenticator`. Review Comment: You seems that we can't put it into the configuration because of initialization orders. -- This is an automated message from the Apache Git Service. To respond to the message, please log on to GitHub and use the URL above to go to the specific comment. To unsubscribe, e-mail: [email protected] For queries about this service, please contact Infrastructure at: [email protected]
