roryqi commented on code in PR #11226: URL: https://github.com/apache/gravitino/pull/11226#discussion_r3310784029
########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/auth/BasicAuthenticator.java: ########## @@ -0,0 +1,156 @@ +/* + * 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 = new IdpUserGroupManager(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 new UnauthorizedException("Empty token authorization header", BASIC_CHALLENGE); + } + + String authData = new String(tokenData, StandardCharsets.UTF_8); + if (StringUtils.isBlank(authData)) { + throw new UnauthorizedException("Empty token authorization header", BASIC_CHALLENGE); + } + if (!authData.startsWith(AuthConstants.AUTHORIZATION_BASIC_HEADER)) { + throw new UnauthorizedException("Invalid token authorization header", BASIC_CHALLENGE); + } + return authData; + } + + private BasicCredentials parseBasicCredentials(String authData) { + String credential = authData.substring(AuthConstants.AUTHORIZATION_BASIC_HEADER.length()); + if (StringUtils.isBlank(credential)) { + throw new BadRequestException("Malformed Basic authorization header: missing credentials"); + } + credential = credential.trim(); + + try { + String decodedCredential = + new String(Base64.getDecoder().decode(credential), StandardCharsets.UTF_8); + String[] parts = decodedCredential.split(":", 2); + if (parts.length != 2) { + throw new UnauthorizedException( + "Malformed Basic authorization header: credentials must be in username:password format", + BASIC_CHALLENGE); + } + + String username = parts[0]; + if (StringUtils.isBlank(username)) { + throw new UnauthorizedException( + "Malformed Basic authorization header: username must not be blank", BASIC_CHALLENGE); + } + + String password = parts[1]; + if (StringUtils.isBlank(password)) { + throw new UnauthorizedException("Invalid username or password", BASIC_CHALLENGE); + } + return new BasicCredentials(username, password); + } catch (IllegalArgumentException e) { + throw new BadRequestException(e, "Malformed Basic authorization header: invalid base64"); Review Comment: throw UnauthorizationException. ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/feature/IdpRESTFeature.java: ########## @@ -18,53 +18,81 @@ */ package org.apache.gravitino.idp.web.rest.feature; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.ext.Provider; +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.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.auth.BasicAuthenticator; +import org.apache.gravitino.idp.storage.relational.IdpGarbageCollector; import org.apache.gravitino.idp.web.rest.IdpAuthorizationFilter; import org.apache.gravitino.idp.web.rest.IdpBasicBinder; import org.apache.gravitino.idp.web.rest.IdpGroupOperations; import org.apache.gravitino.idp.web.rest.IdpUserOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Conditionally registers built-in IdP REST resources when {@code basic} is configured in {@link - * Configs#AUTHENTICATORS}. + * Registers built-in IdP REST resources for the idp-basic plugin. * * <p>Configure {@link Configs#REST_API_EXTENSION_PACKAGES} to {@code - * org.apache.gravitino.idp.web.rest.feature} so Jersey only auto-discovers this feature. IdP REST - * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here only - * when the {@code basic} authenticator is enabled. + * org.apache.gravitino.idp.web.rest.feature} so Jersey auto-discovers this feature. IdP REST + * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here. + * Also initializes configured service admins in the built-in IdP when they do not yet exist. */ @Provider public class IdpRESTFeature implements Feature { - /** Authenticator name that enables built-in IdP management APIs. */ - public static final String BASIC_AUTHENTICATOR = "basic"; + private static final Logger LOG = LoggerFactory.getLogger(IdpRESTFeature.class); + + public static final String IDP_REST_EXTENSION_PACKAGE = IdpRESTFeature.class.getPackageName(); + + public static final String BASIC_AUTHENTICATOR_CLASS = + BasicAuthenticator.class.getCanonicalName(); + + /** Environment variable for the initial password of configured service admins. */ + public static final String INITIAL_ADMIN_PASSWORD_ENV = "GRAVITINO_INITIAL_ADMIN_PASSWORD"; + + public static void registerBasicAuthenticator(Config config) { + List<String> extensionPackages = config.get(Configs.REST_API_EXTENSION_PACKAGES); + if (extensionPackages == null || !extensionPackages.contains(IDP_REST_EXTENSION_PACKAGE)) { + return; + } + + List<String> authenticators = new ArrayList<>(config.get(Configs.AUTHENTICATORS)); + if (authenticators.contains(BASIC_AUTHENTICATOR_CLASS)) { + return; + } + authenticators.add(0, BASIC_AUTHENTICATOR_CLASS); + config.set(Configs.AUTHENTICATORS, authenticators); + } @Override public boolean configure(FeatureContext context) { - if (!basicAuthenticatorEnabled( - GravitinoEnv.getInstance().config().get(Configs.AUTHENTICATORS))) { - return true; + GravitinoEnv env = GravitinoEnv.getInstance(); + Config config = env.config(); + try { + try (IdpUserGroupManager manager = new IdpUserGroupManager(config, env.idGenerator())) { + manager.initializeConfiguredServiceAdmins( + config, StringUtils.defaultString(System.getenv(INITIAL_ADMIN_PASSWORD_ENV))); + } + LOG.info("Initialized built-in IdP service admins"); + } catch (IOException e) { + throw new IllegalStateException("Failed to initialize built-in IdP service admins", e); } + new IdpGarbageCollector(config).start(); Review Comment: Could u put `IdpGrbageCollector` into `UserGroupManaer`? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/auth/BasicAuthenticator.java: ########## @@ -0,0 +1,156 @@ +/* + * 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; Review Comment: Why do we use RandomIdGenerator.INSTANCE? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/feature/IdpRESTFeature.java: ########## @@ -18,53 +18,81 @@ */ package org.apache.gravitino.idp.web.rest.feature; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.ext.Provider; +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.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.auth.BasicAuthenticator; +import org.apache.gravitino.idp.storage.relational.IdpGarbageCollector; import org.apache.gravitino.idp.web.rest.IdpAuthorizationFilter; import org.apache.gravitino.idp.web.rest.IdpBasicBinder; import org.apache.gravitino.idp.web.rest.IdpGroupOperations; import org.apache.gravitino.idp.web.rest.IdpUserOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Conditionally registers built-in IdP REST resources when {@code basic} is configured in {@link - * Configs#AUTHENTICATORS}. + * Registers built-in IdP REST resources for the idp-basic plugin. * * <p>Configure {@link Configs#REST_API_EXTENSION_PACKAGES} to {@code - * org.apache.gravitino.idp.web.rest.feature} so Jersey only auto-discovers this feature. IdP REST - * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here only - * when the {@code basic} authenticator is enabled. + * org.apache.gravitino.idp.web.rest.feature} so Jersey auto-discovers this feature. IdP REST + * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here. + * Also initializes configured service admins in the built-in IdP when they do not yet exist. */ @Provider public class IdpRESTFeature implements Feature { - /** Authenticator name that enables built-in IdP management APIs. */ - public static final String BASIC_AUTHENTICATOR = "basic"; + private static final Logger LOG = LoggerFactory.getLogger(IdpRESTFeature.class); + + public static final String IDP_REST_EXTENSION_PACKAGE = IdpRESTFeature.class.getPackageName(); + + public static final String BASIC_AUTHENTICATOR_CLASS = + BasicAuthenticator.class.getCanonicalName(); + + /** Environment variable for the initial password of configured service admins. */ + public static final String INITIAL_ADMIN_PASSWORD_ENV = "GRAVITINO_INITIAL_ADMIN_PASSWORD"; + + public static void registerBasicAuthenticator(Config config) { + List<String> extensionPackages = config.get(Configs.REST_API_EXTENSION_PACKAGES); + if (extensionPackages == null || !extensionPackages.contains(IDP_REST_EXTENSION_PACKAGE)) { + return; + } + + List<String> authenticators = new ArrayList<>(config.get(Configs.AUTHENTICATORS)); + if (authenticators.contains(BASIC_AUTHENTICATOR_CLASS)) { + return; + } + authenticators.add(0, BASIC_AUTHENTICATOR_CLASS); + config.set(Configs.AUTHENTICATORS, authenticators); + } @Override public boolean configure(FeatureContext context) { - if (!basicAuthenticatorEnabled( - GravitinoEnv.getInstance().config().get(Configs.AUTHENTICATORS))) { - return true; + GravitinoEnv env = GravitinoEnv.getInstance(); + Config config = env.config(); + try { + try (IdpUserGroupManager manager = new IdpUserGroupManager(config, env.idGenerator())) { + manager.initializeConfiguredServiceAdmins( + config, StringUtils.defaultString(System.getenv(INITIAL_ADMIN_PASSWORD_ENV))); Review Comment: What's the defaultString? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/storage/mapper/provider/IdpBasicMapperPackageProvider.java: ########## @@ -20,16 +20,22 @@ import com.google.common.collect.ImmutableList; import java.util.List; +import org.apache.gravitino.GravitinoEnv; import org.apache.gravitino.idp.storage.mapper.IdpGroupMetaMapper; import org.apache.gravitino.idp.storage.mapper.IdpUserGroupRelMapper; import org.apache.gravitino.idp.storage.mapper.IdpUserMetaMapper; +import org.apache.gravitino.idp.web.rest.feature.IdpRESTFeature; import org.apache.gravitino.storage.relational.mapper.provider.MapperPackageProvider; /** Supplies built-in IdP mapper classes from the idp-basic plugin. */ public class IdpBasicMapperPackageProvider implements MapperPackageProvider { @Override public List<Class<?>> getMapperClasses() { + GravitinoEnv env = GravitinoEnv.getInstance(); + if (env.config() != null) { Review Comment: This place is tricky ... Could we have a better place? ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/feature/IdpRESTFeature.java: ########## @@ -18,53 +18,81 @@ */ package org.apache.gravitino.idp.web.rest.feature; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.ext.Provider; +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.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.auth.BasicAuthenticator; +import org.apache.gravitino.idp.storage.relational.IdpGarbageCollector; import org.apache.gravitino.idp.web.rest.IdpAuthorizationFilter; import org.apache.gravitino.idp.web.rest.IdpBasicBinder; import org.apache.gravitino.idp.web.rest.IdpGroupOperations; import org.apache.gravitino.idp.web.rest.IdpUserOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Conditionally registers built-in IdP REST resources when {@code basic} is configured in {@link - * Configs#AUTHENTICATORS}. + * Registers built-in IdP REST resources for the idp-basic plugin. * * <p>Configure {@link Configs#REST_API_EXTENSION_PACKAGES} to {@code - * org.apache.gravitino.idp.web.rest.feature} so Jersey only auto-discovers this feature. IdP REST - * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here only - * when the {@code basic} authenticator is enabled. + * org.apache.gravitino.idp.web.rest.feature} so Jersey auto-discovers this feature. IdP REST + * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here. + * Also initializes configured service admins in the built-in IdP when they do not yet exist. */ @Provider public class IdpRESTFeature implements Feature { - /** Authenticator name that enables built-in IdP management APIs. */ - public static final String BASIC_AUTHENTICATOR = "basic"; + private static final Logger LOG = LoggerFactory.getLogger(IdpRESTFeature.class); + + public static final String IDP_REST_EXTENSION_PACKAGE = IdpRESTFeature.class.getPackageName(); + + public static final String BASIC_AUTHENTICATOR_CLASS = + BasicAuthenticator.class.getCanonicalName(); + + /** Environment variable for the initial password of configured service admins. */ + public static final String INITIAL_ADMIN_PASSWORD_ENV = "GRAVITINO_INITIAL_ADMIN_PASSWORD"; + + public static void registerBasicAuthenticator(Config config) { + List<String> extensionPackages = config.get(Configs.REST_API_EXTENSION_PACKAGES); + if (extensionPackages == null || !extensionPackages.contains(IDP_REST_EXTENSION_PACKAGE)) { Review Comment: We don't need this check. Because we must have this config option if we run the code. ########## plugins/idp-basic/build.gradle.kts: ########## @@ -26,6 +26,8 @@ plugins { dependencies { annotationProcessor(libs.lombok) + implementation(project(":api")) Review Comment: If we need to rely on the api dependencies, you should reuse NotFoundException... ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/web/rest/feature/IdpRESTFeature.java: ########## @@ -18,53 +18,81 @@ */ package org.apache.gravitino.idp.web.rest.feature; +import java.io.IOException; +import java.util.ArrayList; import java.util.List; import javax.ws.rs.core.Feature; import javax.ws.rs.core.FeatureContext; import javax.ws.rs.ext.Provider; +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.idp.IdpUserGroupManager; +import org.apache.gravitino.idp.auth.BasicAuthenticator; +import org.apache.gravitino.idp.storage.relational.IdpGarbageCollector; import org.apache.gravitino.idp.web.rest.IdpAuthorizationFilter; import org.apache.gravitino.idp.web.rest.IdpBasicBinder; import org.apache.gravitino.idp.web.rest.IdpGroupOperations; import org.apache.gravitino.idp.web.rest.IdpUserOperations; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** - * Conditionally registers built-in IdP REST resources when {@code basic} is configured in {@link - * Configs#AUTHENTICATORS}. + * Registers built-in IdP REST resources for the idp-basic plugin. * * <p>Configure {@link Configs#REST_API_EXTENSION_PACKAGES} to {@code - * org.apache.gravitino.idp.web.rest.feature} so Jersey only auto-discovers this feature. IdP REST - * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here only - * when the {@code basic} authenticator is enabled. + * org.apache.gravitino.idp.web.rest.feature} so Jersey auto-discovers this feature. IdP REST + * resource classes remain in {@code org.apache.gravitino.idp.web.rest} and are registered here. + * Also initializes configured service admins in the built-in IdP when they do not yet exist. */ @Provider public class IdpRESTFeature implements Feature { - /** Authenticator name that enables built-in IdP management APIs. */ - public static final String BASIC_AUTHENTICATOR = "basic"; + private static final Logger LOG = LoggerFactory.getLogger(IdpRESTFeature.class); + + public static final String IDP_REST_EXTENSION_PACKAGE = IdpRESTFeature.class.getPackageName(); + + public static final String BASIC_AUTHENTICATOR_CLASS = + BasicAuthenticator.class.getCanonicalName(); + + /** Environment variable for the initial password of configured service admins. */ + public static final String INITIAL_ADMIN_PASSWORD_ENV = "GRAVITINO_INITIAL_ADMIN_PASSWORD"; + + public static void registerBasicAuthenticator(Config config) { + List<String> extensionPackages = config.get(Configs.REST_API_EXTENSION_PACKAGES); + if (extensionPackages == null || !extensionPackages.contains(IDP_REST_EXTENSION_PACKAGE)) { + return; + } + + List<String> authenticators = new ArrayList<>(config.get(Configs.AUTHENTICATORS)); + if (authenticators.contains(BASIC_AUTHENTICATOR_CLASS)) { + return; + } + authenticators.add(0, BASIC_AUTHENTICATOR_CLASS); Review Comment: You can new a BasicAuthenticator and add it to ServerAuthenticator.authenticators. ########## plugins/idp-basic/src/main/java/org/apache/gravitino/idp/IdpUserGroupManager.java: ########## @@ -63,8 +70,42 @@ public IdpUserGroupManager(Config config, IdGenerator idGenerator) { this.relationalStorage = new IdpRelationalStorage(config); this.idGenerator = idGenerator; this.passwordHasher = PasswordHasherFactory.create(); - this.garbageCollector = new IdpGarbageCollector(config); - garbageCollector.start(); + } + + 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; + } + + if (StringUtils.isNotBlank(initialAdminPassword)) { + IdpCredentialValidator.validatePassword(initialAdminPassword); + } + + for (String serviceAdmin : serviceAdmins) { + IdpCredentialValidator.validateUsername(serviceAdmin); + if (checkUserExistence(serviceAdmin)) { + continue; + } + + Preconditions.checkArgument( Review Comment: Why do we check initialAdminPassword multiple times? ########## 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: This comment isn't addressed. ########## 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: This comment isn't addressed. ########## 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: This comment isn't addressed. ########## 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: This comment isn't addressed. -- 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]
