Skip to content
Snippets Groups Projects
Commit 843f7742 authored by Martin Lowe's avatar Martin Lowe :flag_ca:
Browse files

feat: Add handling for blocked users in validation and lookups

This adds checks + tests for handling blocked users from the Profile
API, checking and catching the blocked user exception that can bubble
up.
parent 51429ab2
Branches
No related tags found
1 merge request!227feat: Add handling for blocked users in validation and lookups
Pipeline #81603 passed
......@@ -27,7 +27,9 @@ public enum APIStatusCode {
ERROR_COMMITTER(-405),
ERROR_PROXY_PUSH(-406),
ERROR_COMMITTER_NOT_FOUND(-407),
ERROR_AUTHOR_NOT_FOUND(-408);
ERROR_AUTHOR_NOT_FOUND(-408),
ERROR_AUTHOR_BLOCKED(-409),
ERROR_COMMITTER_BLOCKED(-410);
private int code;
......
......@@ -19,6 +19,7 @@ import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.eclipsefoundation.caching.service.CachingService;
import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.efservices.exceptions.BlockedUserException;
import org.eclipsefoundation.efservices.services.ProfileService;
import org.eclipsefoundation.git.eca.helper.ProjectHelper;
import org.eclipsefoundation.git.eca.model.ValidationRequest;
......@@ -103,25 +104,30 @@ public class ValidationResource extends CommonResource {
public Response getUserStatus(@QueryParam("email") String email, @QueryParam("q") String query) {
// really basic check. A username will never have an @, while an email will always have it.
boolean queryLikeEmail = query != null && query.contains("@");
// check that the user has committer level access
EfUser loggedInUser = getUserForLoggedInAccount();
if (StringUtils.isNotBlank(email)) {
// do the checks to see if the user is missing or has not signed the ECA using the legacy field
handleUserEmailLookup(loggedInUser, email);
} else if (queryLikeEmail) {
// do the checks to see if the user is missing or has not signed the ECA
handleUserEmailLookup(loggedInUser, query);
} else if (StringUtils.isNotBlank(query)) {
// if username is set, look up the user and check if it has an Eca
Optional<EfUser> user = profileService.fetchUserByUsername(query, false);
if (user.isEmpty()) {
throw new NotFoundException(String.format("No user found with username '%s'", TransformationHelper.formatLog(query)));
try {
// check that the user has committer level access
EfUser loggedInUser = getUserForLoggedInAccount();
if (StringUtils.isNotBlank(email)) {
// do the checks to see if the user is missing or has not signed the ECA using the legacy field
handleUserEmailLookup(loggedInUser, email);
} else if (queryLikeEmail) {
// do the checks to see if the user is missing or has not signed the ECA
handleUserEmailLookup(loggedInUser, query);
} else if (StringUtils.isNotBlank(query)) {
// if username is set, look up the user and check if it has an Eca
Optional<EfUser> user = profileService.fetchUserByUsername(query, false);
if (user.isEmpty()) {
throw new NotFoundException(String.format("No user found with username '%s'", TransformationHelper.formatLog(query)));
}
if (!user.get().eca().signed()) {
return Response.status(Status.FORBIDDEN).build();
}
} else {
throw new BadRequestException("A username or email must be set to look up a user account");
}
if (!user.get().eca().signed()) {
return Response.status(Status.FORBIDDEN).build();
}
} else {
throw new BadRequestException("A username or email must be set to look up a user account");
} catch(BlockedUserException e) {
// if the user was blocked, we want to catch and return a not found in this case
throw new NotFoundException(String.format("Passed user with username '%s' is blocked", TransformationHelper.formatLog(query)));
}
return Response.ok().build();
}
......
/*********************************************************************
* Copyright (c) 2025 Eclipse Foundation.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.git.eca.resource.mappers;
import org.eclipsefoundation.efservices.exceptions.BlockedUserException;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
/**
* Handler for blocked user exceptions, to be used when a blocked user exception bubbles up from a logged in blocked user.
*/
public class BlockedUserExceptionMapper implements ExceptionMapper<BlockedUserException> {
@Override
public Response toResponse(BlockedUserException exception) {
return Response.status(403).build();
}
}
......@@ -15,42 +15,36 @@ import java.util.List;
import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.efservices.api.models.Project;
import org.eclipsefoundation.efservices.exceptions.BlockedUserException;
public interface UserService {
/**
* Retrieves an Eclipse Account user object given the Git users email address
* (at minimum). This is facilitated using the Eclipse Foundation accounts API,
* along short lived in-memory caching for performance and some protection
* against duplicate requests.
*
* @param mail the email address to use to retrieve the Eclipse account for.
* @return the Eclipse Account user information if found, or null if there was
* an error or no user exists.
*/
EfUser getUser(String mail);
/**
* Retrieves an Eclipse Account user object given the Git users email address (at minimum). This is facilitated using the Eclipse
* Foundation accounts API, along short lived in-memory caching for performance and some protection against duplicate requests.
*
* @param mail the email address to use to retrieve the Eclipse account for.
* @return the Eclipse Account user information if found, or null if there was an error or no user exists.
* @throws BlockedUserException when the user is blocked in the accounts site
*/
EfUser getUser(String mail) throws BlockedUserException;
/**
* Retrieves an Eclipse Account user object given the Github username. This is
* facilitated using the Eclipse Foundation accounts API, along short lived
* in-memory caching for performance and some protection against duplicate
* requests.
*
* @param username the Github username used for retrieval of associated Eclipse
* Account if it exists.
* @return the Eclipse Account user information if found, or null if there was
* an error or no user exists.
*/
EfUser getUserByGithubUsername(String username);
/**
* Retrieves an Eclipse Account user object given the Github username. This is facilitated using the Eclipse Foundation accounts API,
* along short lived in-memory caching for performance and some protection against duplicate requests.
*
* @param username the Github username used for retrieval of associated Eclipse Account if it exists.
* @return the Eclipse Account user information if found, or null if there was an error or no user exists.
* @throws BlockedUserException when the user is blocked in the accounts site
*/
EfUser getUserByGithubUsername(String username) throws BlockedUserException;
/**
* Checks the bot API to see whether passed email address is registered to a bot
* under the passed projects.
*
* @param mail the potential bot user's email address
* @param filteredProjects the projects to check for bot presence.
* @return true if the user is a bot on at least one of the given projects,
* false otherwise.
*/
boolean userIsABot(String mail, List<Project> filteredProjects);
/**
* Checks the bot API to see whether passed email address is registered to a bot under the passed projects.
*
* @param mail the potential bot user's email address
* @param filteredProjects the projects to check for bot presence.
* @return true if the user is a bot on at least one of the given projects, false otherwise.
*/
boolean userIsABot(String mail, List<Project> filteredProjects);
}
......@@ -25,6 +25,7 @@ import org.eclipsefoundation.caching.service.CachingService;
import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.efservices.api.models.Project;
import org.eclipsefoundation.efservices.api.models.UserSearchParams;
import org.eclipsefoundation.efservices.exceptions.BlockedUserException;
import org.eclipsefoundation.efservices.services.ProfileService;
import org.eclipsefoundation.git.eca.api.BotsAPI;
import org.eclipsefoundation.git.eca.config.MailValidationConfig;
......@@ -77,6 +78,9 @@ public class CachedUserService implements UserService {
return null;
}
CacheWrapper<EfUser> result = cache.get(mail, new MultivaluedHashMap<>(), EfUser.class, () -> retrieveUser(mail));
if (result.errorType().isPresent() && BlockedUserException.class.equals(result.errorType().get())) {
throw new BlockedUserException();
}
Optional<EfUser> user = result.data();
if (user.isPresent()) {
LOGGER.debug("Found user with email {}", mail);
......@@ -140,8 +144,9 @@ public class CachedUserService implements UserService {
*
* @param user the user to attempt account retrieval for.
* @return the user account if found by mail, or null if none found.
* @throws BlockedUserException when the user has been blocked from accessing services
*/
private EfUser retrieveUser(String mail) {
private EfUser retrieveUser(String mail) throws BlockedUserException {
if (StringUtils.isBlank(mail)) {
LOGGER.debug("Blank mail passed, cannot fetch user");
return null;
......
......@@ -25,6 +25,7 @@ import org.eclipsefoundation.efservices.api.models.EfUserBuilder;
import org.eclipsefoundation.efservices.api.models.EfUserCountryBuilder;
import org.eclipsefoundation.efservices.api.models.EfUserEcaBuilder;
import org.eclipsefoundation.efservices.api.models.Project;
import org.eclipsefoundation.efservices.exceptions.BlockedUserException;
import org.eclipsefoundation.git.eca.config.ECAParallelProcessingConfig;
import org.eclipsefoundation.git.eca.config.MailValidationConfig;
import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
......@@ -216,35 +217,47 @@ public class DefaultValidationService implements ValidationService {
}
// retrieve the eclipse account for the author
EfUser eclipseAuthor = getIdentifiedUser(author);
// if the user is a bot, generate a stubbed user
if (isAllowedUser(author.getMail()) || users.userIsABot(author.getMail(), filteredProjects)) {
status.addMessage(String.format("Automated user '%1$s' detected for author of commit %2$s", author.getMail(), c.getHash()));
eclipseAuthor = createBotStub(author);
} else if (eclipseAuthor == null) {
status
.addMessage(String
.format("Could not find an Eclipse user with mail '%1$s' for author of commit %2$s", author.getMail(),
c.getHash()));
part.addError("Author must have an Eclipse Account", APIStatusCode.ERROR_AUTHOR_NOT_FOUND);
return part;
EfUser eclipseAuthor = null;
try {
eclipseAuthor = getIdentifiedUser(author);
// if the user is a bot, generate a stubbed user
if (isAllowedUser(author.getMail()) || users.userIsABot(author.getMail(), filteredProjects)) {
status.addMessage(String.format("Automated user '%1$s' detected for author of commit %2$s", author.getMail(), c.getHash()));
eclipseAuthor = createBotStub(author);
} else if (eclipseAuthor == null) {
status
.addMessage(String
.format("Could not find an Eclipse user with mail '%1$s' for author of commit %2$s", author.getMail(),
c.getHash()));
part.addError("Author must have an Eclipse Account", APIStatusCode.ERROR_AUTHOR_NOT_FOUND);
}
} catch (BlockedUserException e) {
part.addError("Author has been blocked from accessing Eclipse Foundation services", APIStatusCode.ERROR_AUTHOR_BLOCKED);
}
GitUser committer = c.getCommitter();
// retrieve the eclipse account for the committer
EfUser eclipseCommitter = getIdentifiedUser(committer);
// check if whitelisted or bot
if (isAllowedUser(committer.getMail()) || users.userIsABot(committer.getMail(), filteredProjects)) {
status
.addMessage(
String.format("Automated user '%1$s' detected for committer of commit %2$s", committer.getMail(), c.getHash()));
eclipseCommitter = createBotStub(committer);
} else if (eclipseCommitter == null) {
status
.addMessage(String
.format("Could not find an Eclipse user with mail '%1$s' for committer of commit %2$s", committer.getMail(),
c.getHash()));
part.addError("Committing user must have an Eclipse Account", APIStatusCode.ERROR_COMMITTER_NOT_FOUND);
EfUser eclipseCommitter = null;
try {
// retrieve the eclipse account for the committer
eclipseCommitter = getIdentifiedUser(committer);
// check if whitelisted or bot
if (isAllowedUser(committer.getMail()) || users.userIsABot(committer.getMail(), filteredProjects)) {
status
.addMessage(
String.format("Automated user '%1$s' detected for committer of commit %2$s", committer.getMail(), c.getHash()));
eclipseCommitter = createBotStub(committer);
} else if (eclipseCommitter == null) {
status
.addMessage(String
.format("Could not find an Eclipse user with mail '%1$s' for committer of commit %2$s", committer.getMail(),
c.getHash()));
part.addError("Committing user must have an Eclipse Account", APIStatusCode.ERROR_COMMITTER_NOT_FOUND);
}
} catch (BlockedUserException e) {
part.addError("Committer has been blocked from accessing Eclipse Foundation services", APIStatusCode.ERROR_COMMITTER_BLOCKED);
}
// if there was an error in validating the users or they are missing, there's no point in validating further
if (!part.commit().getErrors().isEmpty() || eclipseCommitter == null || eclipseAuthor == null) {
return part;
}
// validate author access to the current repo
......
......@@ -76,6 +76,7 @@ class ValidationResourceTest {
.build();
public static final GitUser USER_RANDO = GitUser.builder().setName("Rando Calressian").setMail("rando@nowhere.co").build();
public static final GitUser USER_NEWBIE = GitUser.builder().setName("Newbie Anon").setMail("newbie@important.co").build();
public static final GitUser USER_BLOCKED = GitUser.builder().setName("Newbie Anon").setMail("blockeduser@uh.oh").build();
/*
* BOTS
......@@ -136,6 +137,8 @@ class ValidationResourceTest {
.buildForbiddenCase(LOOKUP_URL, new String[] { "newbie@important.co" }, "");
public static final EndpointTestCase LOOKUP_NOT_FOUND_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_URL, new String[] { "dummy@fake.co" }, "");
public static final EndpointTestCase LOOKUP_BLOCKED_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_URL, new String[] { "blockeduser@uh.oh" }, "");
// by username
public static final EndpointTestCase LOOKUP_USERNAME_SUCCESS_CASE = TestCaseHelper
.buildSuccessCase(LOOKUP_URL, new String[] { "barshall_blathers" }, "");
......@@ -143,6 +146,8 @@ class ValidationResourceTest {
.buildForbiddenCase(LOOKUP_URL, new String[] { "newbieAnon" }, "");
public static final EndpointTestCase LOOKUP_USERNAME_NOT_FOUND_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_URL, new String[] { "dummy11" }, "");
public static final EndpointTestCase LOOKUP_USERNAME_BLOCKED_USER_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_URL, new String[] { "blockeduser" }, "");
// lookup cases with email param
public static final EndpointTestCase LOOKUP_SUCCESS_EMAIL_PARAM_CASE = TestCaseHelper
......@@ -155,6 +160,8 @@ class ValidationResourceTest {
.buildForbiddenCase(LOOKUP_LEGACY_URL, new String[] { "newbie@important.co" }, "");
public static final EndpointTestCase LOOKUP_NOT_FOUND_EMAIL_PARAM_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_LEGACY_URL, new String[] { "dummy@fake.co" }, "");
public static final EndpointTestCase LOOKUP_BLOCKED_EMAIL_PARAM_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_LEGACY_URL, new String[] { "blockeduser@uh.oh" }, "");
@Inject
CachingService cs;
......@@ -435,6 +442,18 @@ class ValidationResourceTest {
.run();
}
@Test
void validate_authorBlockedEclipseAccount() {
Commit c1 = createStandardUsercommit(USER_BLOCKED, USER_GRUNTS);
// Error should be singular + that there's no Eclipse Account on file for author
// Status 403 (forbidden) is the standard return for invalid requests
EndpointTestBuilder
.from(VALIDATE_FORBIDDEN_CASE)
.doPost(createGitHubRequest(false, "http://www.github.com/eclipsefdn/sample", Arrays.asList(c1)))
.run();
}
@Test
void validateCommitterNoEclipseAccount() {
Commit c1 = createStandardUsercommit(USER_GRUNTS, USER_RANDO);
......@@ -447,6 +466,18 @@ class ValidationResourceTest {
.run();
}
@Test
void validate_committerBlockedEclipseAccount() {
Commit c1 = createStandardUsercommit(USER_GRUNTS, USER_BLOCKED);
// Error should be singular + that there's no Eclipse Account on file for committer
// Status 403 (forbidden) is the standard return for invalid requests
EndpointTestBuilder
.from(VALIDATE_FORBIDDEN_CASE)
.doPost(createGitHubRequest(false, "http://www.github.com/eclipsefdn/sample", Arrays.asList(c1)))
.run();
}
@Test
void validateProxyCommitUntrackedProject() {
Commit c1 = createStandardUsercommit(USER_GRUNTS, USER_RANDO);
......@@ -927,6 +958,13 @@ class ValidationResourceTest {
EndpointTestBuilder.from(LOOKUP_USERNAME_NOT_FOUND_CASE).run();
}
@Test
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
void validateUserLookup_username_userBlocked() {
EndpointTestBuilder.from(LOOKUP_USERNAME_BLOCKED_USER_CASE).run();
}
@Test
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
......@@ -957,6 +995,13 @@ class ValidationResourceTest {
EndpointTestBuilder.from(LOOKUP_NOT_FOUND_CASE).run();
}
@Test
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
void validateUserLookup_userBlocked() {
EndpointTestBuilder.from(LOOKUP_BLOCKED_CASE).run();
}
@Test
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
......@@ -987,6 +1032,13 @@ class ValidationResourceTest {
EndpointTestBuilder.from(LOOKUP_NOT_FOUND_EMAIL_PARAM_CASE).run();
}
@Test
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
void validateUserLookup_legacy_userBlocked() {
EndpointTestBuilder.from(LOOKUP_BLOCKED_EMAIL_PARAM_CASE).run();
}
@Test
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
......
......@@ -17,6 +17,7 @@ import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.eclipsefoundation.efservices.api.ProfileAPI;
import org.eclipsefoundation.efservices.api.models.EfUser;
......@@ -24,6 +25,7 @@ import org.eclipsefoundation.efservices.api.models.EfUserBuilder;
import org.eclipsefoundation.efservices.api.models.EfUserCountryBuilder;
import org.eclipsefoundation.efservices.api.models.EfUserEcaBuilder;
import org.eclipsefoundation.efservices.api.models.UserSearchParams;
import org.eclipsefoundation.efservices.exceptions.BlockedUserException;
import io.quarkus.test.Mock;
import jakarta.enterprise.context.ApplicationScoped;
......@@ -38,11 +40,12 @@ import jakarta.enterprise.context.ApplicationScoped;
@Mock
@RestClient
@ApplicationScoped
public class MockAccountsAPI implements ProfileAPI {
public class MockProfileAPI implements ProfileAPI {
public static final String BLOCKED_USER_NAME = "blockeduser";
private Map<String, EfUser> users;
public MockAccountsAPI() {
public MockProfileAPI() {
int id = 0;
this.users = new HashMap<>();
users
......@@ -192,11 +195,17 @@ public class MockAccountsAPI implements ProfileAPI {
@Override
public EfUser getUserByEfUsername(String token, String uname) {
if (uname.startsWith(BLOCKED_USER_NAME)) {
throw new BlockedUserException();
}
return users.values().stream().filter(usernamePredicate(uname)).findFirst().orElseGet(() -> null);
}
@Override
public EfUser getUserByGithubHandle(String token, String ghHandle) {
if (ghHandle.startsWith(BLOCKED_USER_NAME)) {
throw new BlockedUserException();
}
return users
.values()
.stream()
......@@ -207,6 +216,10 @@ public class MockAccountsAPI implements ProfileAPI {
@Override
public List<EfUser> getUsers(String token, UserSearchParams params) {
if ((StringUtils.isNotBlank(params.name) && params.name.startsWith(BLOCKED_USER_NAME))
||(StringUtils.isNotBlank(params.mail) && params.mail.startsWith(BLOCKED_USER_NAME))) {
throw new BlockedUserException();
}
return users
.values()
.stream()
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment