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

Merge branch 'malowe/main/75-2' into 'main'

Iss #75 - Update lookup to allow for user or email, switch to fetch

See merge request !205
parents d79b441b 09c76d02
No related branches found
No related tags found
1 merge request!205Iss #75 - Update lookup to allow for user or email, switch to fetch
Pipeline #52884 passed
Showing
with 412 additions and 353 deletions
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<quarkus.platform.version>3.8.3</quarkus.platform.version> <quarkus.platform.version>3.8.3</quarkus.platform.version>
<surefire-plugin.version>3.1.2</surefire-plugin.version> <surefire-plugin.version>3.1.2</surefire-plugin.version>
<maven.compiler.parameters>true</maven.compiler.parameters> <maven.compiler.parameters>true</maven.compiler.parameters>
<eclipse-api-version>1.0.1</eclipse-api-version> <eclipse-api-version>1.0.2</eclipse-api-version>
<auto-value.version>1.10.4</auto-value.version> <auto-value.version>1.10.4</auto-value.version>
<org.mapstruct.version>1.5.5.Final</org.mapstruct.version> <org.mapstruct.version>1.5.5.Final</org.mapstruct.version>
<sonar.sources>src/main</sonar.sources> <sonar.sources>src/main</sonar.sources>
......
...@@ -89,6 +89,18 @@ paths: ...@@ -89,6 +89,18 @@ paths:
description: Error while retrieving data description: Error while retrieving data
/eca/lookup: /eca/lookup:
parameters:
- name: q
in: query
description: Query string containing either email or username. Email is only valid for logged in committers/project leads
schema:
type: string
- name: email
in: query
deprecated: true
description: Email is only valid for logged in committers/project leads. For removal at the end of 2024.
schema:
type: string
get: get:
operationId: getUserStatus operationId: getUserStatus
tags: tags:
......
/*********************************************************************
* Copyright (c) 2020 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.api;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipsefoundation.git.eca.api.models.EclipseUser;
/**
* Binding interface for the Eclipse Foundation user account API. Runtime
* implementations are automatically generated by
* Quarkus at compile time. As the API deals with sensitive information,
* authentication is required to access this endpoint.
*
* @author Martin Lowe
*
*/
@ApplicationScoped
@RegisterRestClient
@Produces("application/json")
public interface AccountsAPI {
/**
* Retrieves all user objects that use the given mail address.
*
* @param mail the email address to match against for Eclipse accounts
* @return all matching eclipse accounts
*/
@GET
@Path("/account/profile")
List<EclipseUser> getUsers(@HeaderParam("Authorization") String token, @QueryParam("mail") String mail);
/**
* Retrieves user objects that matches the given Github username.
*
* @param authBearer authorization header value for validating call
* @param uname username of the Github account to retrieve Eclipse Account
* for
* @return the matching Eclipse account or null
*/
@GET
@Path("/github/profile/{uname}")
EclipseUser getUserByGithubUname(@HeaderParam("Authorization") String token, @PathParam("uname") String uname);
}
/*********************************************************************
* Copyright (c) 2020 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/
*
* Author: Martin Lowe <martin.lowe@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.git.eca.api.models;
import jakarta.annotation.Nullable;
import org.eclipsefoundation.git.eca.model.GitUser;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.google.auto.value.AutoValue;
/**
* Represents a users Eclipse Foundation account
*
* @author Martin Lowe
*/
@AutoValue
@JsonDeserialize(builder = AutoValue_EclipseUser.Builder.class)
public abstract class EclipseUser {
public abstract int getUid();
public abstract String getName();
public abstract String getMail();
public abstract ECA getECA();
public abstract boolean getIsCommitter();
@Nullable
public abstract String getGithubHandle();
@Nullable
@JsonIgnore
public abstract Boolean getIsBot();
/**
* Create a bot user stub when there is no real Eclipse account for the bot.
*
* @param user the Git user that was detected to be a bot.
* @return a stubbed Eclipse user bot object.
*/
public static EclipseUser createBotStub(GitUser user) {
return EclipseUser
.builder()
.setUid(0)
.setName(user.getName())
.setMail(user.getMail())
.setECA(ECA.builder().build())
.setIsBot(true)
.build();
}
public static Builder builder() {
return new AutoValue_EclipseUser.Builder().setIsCommitter(false).setIsBot(false);
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setUid(int id);
public abstract Builder setName(String name);
public abstract Builder setMail(String mail);
public abstract Builder setECA(ECA eca);
public abstract Builder setIsCommitter(boolean isCommitter);
public abstract Builder setGithubHandle(@Nullable String githubHandle);
@JsonIgnore
public abstract Builder setIsBot(@Nullable Boolean isBot);
public abstract EclipseUser build();
}
@AutoValue
@JsonDeserialize(builder = AutoValue_EclipseUser_ECA.Builder.class)
public abstract static class ECA {
public abstract boolean getSigned();
public abstract boolean getCanContributeSpecProject();
public static Builder builder() {
return new AutoValue_EclipseUser_ECA.Builder().setCanContributeSpecProject(false).setSigned(false);
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setSigned(boolean signed);
public abstract Builder setCanContributeSpecProject(boolean canContributeSpecProject);
public abstract ECA build();
}
}
}
...@@ -38,8 +38,9 @@ import jakarta.ws.rs.core.MultivaluedMap; ...@@ -38,8 +38,9 @@ import jakarta.ws.rs.core.MultivaluedMap;
@Table @Table
@Entity @Entity
public class CommitValidationMessage extends BareNode implements Serializable{ public class CommitValidationMessage extends BareNode implements Serializable {
public static final DtoTable TABLE = new DtoTable(CommitValidationMessage.class, "cvm"); public static final DtoTable TABLE = new DtoTable(CommitValidationMessage.class, "cvm");
private static final long serialVersionUID = 1L;
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
...@@ -203,21 +204,21 @@ public class CommitValidationMessage extends BareNode implements Serializable{ ...@@ -203,21 +204,21 @@ public class CommitValidationMessage extends BareNode implements Serializable{
// id check // id check
String id = params.getFirst(DefaultUrlParameterNames.ID.getName()); String id = params.getFirst(DefaultUrlParameterNames.ID.getName());
if (StringUtils.isNumeric(id)) { if (StringUtils.isNumeric(id)) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?", stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id = ?", new Object[] { Long.valueOf(id) }));
new Object[] { Long.valueOf(id) }));
} }
// commit id check // commit id check
String commitId = params.getFirst(GitEcaParameterNames.COMMIT_ID.getName()); String commitId = params.getFirst(GitEcaParameterNames.COMMIT_ID.getName());
if (StringUtils.isNumeric(commitId)) { if (StringUtils.isNumeric(commitId)) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitId = ?", stmt
new Object[] { Integer.valueOf(commitId) })); .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".commitId = ?",
new Object[] { Integer.valueOf(commitId) }));
} }
// ids check // ids check
List<String> ids = params.get(DefaultUrlParameterNames.IDS.getName()); List<String> ids = params.get(DefaultUrlParameterNames.IDS.getName());
if (ids != null && !ids.isEmpty()) { if (ids != null && !ids.isEmpty()) {
stmt.addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id IN ?", stmt
new Object[] { ids.stream().filter(StringUtils::isNumeric).map(Long::valueOf) .addClause(new ParameterizedSQLStatement.Clause(TABLE.getAlias() + ".id IN ?",
.toList() })); new Object[] { ids.stream().filter(StringUtils::isNumeric).map(Long::valueOf).toList() }));
} }
} }
return stmt; return stmt;
......
...@@ -45,6 +45,7 @@ import jakarta.ws.rs.core.MultivaluedMap; ...@@ -45,6 +45,7 @@ import jakarta.ws.rs.core.MultivaluedMap;
@Entity @Entity
public class CommitValidationStatus extends BareNode implements Serializable { public class CommitValidationStatus extends BareNode implements Serializable {
public static final DtoTable TABLE = new DtoTable(CommitValidationStatus.class, "cvs"); public static final DtoTable TABLE = new DtoTable(CommitValidationStatus.class, "cvs");
private static final long serialVersionUID = 1L;
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
......
...@@ -13,7 +13,7 @@ package org.eclipsefoundation.git.eca.resource; ...@@ -13,7 +13,7 @@ package org.eclipsefoundation.git.eca.resource;
import java.util.Arrays; import java.util.Arrays;
import org.eclipsefoundation.git.eca.api.models.EclipseUser; import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking; import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking;
import org.eclipsefoundation.git.eca.service.UserService; import org.eclipsefoundation.git.eca.service.UserService;
import org.eclipsefoundation.http.model.RequestWrapper; import org.eclipsefoundation.http.model.RequestWrapper;
...@@ -52,7 +52,7 @@ public abstract class CommonResource { ...@@ -52,7 +52,7 @@ public abstract class CommonResource {
dao.add(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class)), Arrays.asList(tracking)); dao.add(new RDBMSQuery<>(wrapper, filters.get(GithubWebhookTracking.class)), Arrays.asList(tracking));
} }
EclipseUser getUserForLoggedInAccount() { EfUser getUserForLoggedInAccount() {
if (ident.isAnonymous()) { if (ident.isAnonymous()) {
return null; return null;
} }
......
...@@ -14,11 +14,12 @@ package org.eclipsefoundation.git.eca.resource; ...@@ -14,11 +14,12 @@ package org.eclipsefoundation.git.eca.resource;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Optional;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipsefoundation.caching.service.CachingService; import org.eclipsefoundation.caching.service.CachingService;
import org.eclipsefoundation.git.eca.api.models.EclipseUser; import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.efservices.services.ProfileService;
import org.eclipsefoundation.git.eca.helper.ProjectHelper; import org.eclipsefoundation.git.eca.helper.ProjectHelper;
import org.eclipsefoundation.git.eca.model.ValidationRequest; import org.eclipsefoundation.git.eca.model.ValidationRequest;
import org.eclipsefoundation.git.eca.model.ValidationResponse; import org.eclipsefoundation.git.eca.model.ValidationResponse;
...@@ -30,8 +31,8 @@ import org.jboss.resteasy.annotations.jaxrs.QueryParam; ...@@ -30,8 +31,8 @@ import org.jboss.resteasy.annotations.jaxrs.QueryParam;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject; import jakarta.inject.Inject;
import jakarta.ws.rs.BadRequestException;
import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET; import jakarta.ws.rs.GET;
import jakarta.ws.rs.NotFoundException; import jakarta.ws.rs.NotFoundException;
...@@ -40,9 +41,10 @@ import jakarta.ws.rs.Path; ...@@ -40,9 +41,10 @@ import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces; import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status;
/** /**
* ECA validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate commits passed to * Eca validation endpoint for Git commits. Will use information from the bots, projects, and accounts API to validate commits passed to
* this endpoint. Should be as system agnostic as possible to allow for any service to request validation with less reliance on services * this endpoint. Should be as system agnostic as possible to allow for any service to request validation with less reliance on services
* external to the Eclipse foundation. * external to the Eclipse foundation.
* *
...@@ -62,9 +64,12 @@ public class ValidationResource extends CommonResource { ...@@ -62,9 +64,12 @@ public class ValidationResource extends CommonResource {
@Inject @Inject
ValidationService validation; ValidationService validation;
@Inject
ProfileService profileService;
/** /**
* Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository provider. These commits * Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository provider. These commits
* will be validated to ensure that all users are covered either by an ECA, or are committers on the project. In the case of ECA-only * will be validated to ensure that all users are covered either by an Eca, or are committers on the project. In the case of Eca-only
* contributors, an additional sign off footer is required in the body of the commit. * contributors, an additional sign off footer is required in the body of the commit.
* *
* @param req the request containing basic data plus the commits to be validated * @param req the request containing basic data plus the commits to be validated
...@@ -86,25 +91,68 @@ public class ValidationResource extends CommonResource { ...@@ -86,25 +91,68 @@ public class ValidationResource extends CommonResource {
} }
} }
/**
* Do a lookup of an email or username to check if the user has an ECA. Does basic checks on query to discover if email or username,
*
* @param email deprecated field, to be removed end of 2024. Contains the email to lookup
* @param query query string for lookup, can contain either an email address or username
* @return
*/
@GET @GET
@Path("/lookup") @Path("/lookup")
@Authenticated public Response getUserStatus(@QueryParam("email") String email, @QueryParam("q") String query) {
public Response getUserStatus(@QueryParam("email") String email) { // 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 // check that the user has committer level access
EclipseUser loggedInUser = getUserForLoggedInAccount(); EfUser loggedInUser = getUserForLoggedInAccount();
if (loggedInUser == null || !loggedInUser.getIsCommitter()) { if (StringUtils.isNotBlank(email)) {
throw new FinalForbiddenException("User must be logged in and have committer level access to use this endpoint."); // 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().getEca().getSigned()) {
return Response.status(Status.FORBIDDEN).build();
}
} else {
throw new BadRequestException("A username or email must be set to look up a user account");
}
return Response.ok().build();
}
/**
* Check permissions on the logged in user, and if permitted, check if the designated email is associated with an EF account with a
* valid ECA. If there is a problem or there is no match, a corresponding error will be thrown.
*
* @param loggedInUser the currently logged in user converted to an EF user object
* @param email the email to search for
*/
private void handleUserEmailLookup(EfUser loggedInUser, String email) {
// check if there is a user logged in, as this always requires authentication
if (ident.isAnonymous()) {
throw new BadRequestException("User must be logged in and have committer level access to search by email");
}
// check that user has a project relation as a way of checking user trust
boolean isKnownCommitterOrPL = loggedInUser != null && loggedInUser.getIsCommitter();
if (!isKnownCommitterOrPL) {
throw new FinalForbiddenException("User must be logged in and have committer level access to search by email");
} }
// do the lookup of the passed email // do the lookup of the passed email
EclipseUser user = users.getUser(email); EfUser user = users.getUser(email);
if (Objects.isNull(user)) { if (user == null) {
throw new NotFoundException(String.format("No user found with mail '%s'", TransformationHelper.formatLog(email))); throw new NotFoundException(String.format("No user found with mail '%s'", TransformationHelper.formatLog(email)));
} }
// if the user doesn't have a signed Eca, return an empty 403
if (!user.getECA().getSigned()) { if (!user.getEca().getSigned()) {
throw new FinalForbiddenException(""); throw new FinalForbiddenException("");
} }
return Response.ok().build();
} }
/** /**
......
...@@ -13,8 +13,8 @@ package org.eclipsefoundation.git.eca.service; ...@@ -13,8 +13,8 @@ package org.eclipsefoundation.git.eca.service;
import java.util.List; import java.util.List;
import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.efservices.api.models.Project; import org.eclipsefoundation.efservices.api.models.Project;
import org.eclipsefoundation.git.eca.api.models.EclipseUser;
public interface UserService { public interface UserService {
...@@ -28,7 +28,7 @@ public interface UserService { ...@@ -28,7 +28,7 @@ public interface UserService {
* @return the Eclipse Account user information if found, or null if there was * @return the Eclipse Account user information if found, or null if there was
* an error or no user exists. * an error or no user exists.
*/ */
EclipseUser getUser(String mail); EfUser getUser(String mail);
/** /**
* Retrieves an Eclipse Account user object given the Github username. This is * Retrieves an Eclipse Account user object given the Github username. This is
...@@ -41,7 +41,7 @@ public interface UserService { ...@@ -41,7 +41,7 @@ public interface UserService {
* @return the Eclipse Account user information if found, or null if there was * @return the Eclipse Account user information if found, or null if there was
* an error or no user exists. * an error or no user exists.
*/ */
EclipseUser getUserByGithubUsername(String username); EfUser getUserByGithubUsername(String username);
/** /**
* Checks the bot API to see whether passed email address is registered to a bot * Checks the bot API to see whether passed email address is registered to a bot
......
...@@ -15,17 +15,18 @@ import java.util.Collections; ...@@ -15,17 +15,18 @@ import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.eclipsefoundation.caching.model.CacheWrapper; import org.eclipsefoundation.caching.model.CacheWrapper;
import org.eclipsefoundation.caching.service.CachingService; 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.Project;
import org.eclipsefoundation.efservices.services.DrupalTokenService; import org.eclipsefoundation.efservices.api.models.UserSearchParams;
import org.eclipsefoundation.git.eca.api.AccountsAPI; import org.eclipsefoundation.efservices.services.ProfileService;
import org.eclipsefoundation.git.eca.api.BotsAPI; import org.eclipsefoundation.git.eca.api.BotsAPI;
import org.eclipsefoundation.git.eca.api.models.EclipseUser;
import org.eclipsefoundation.git.eca.config.MailValidationConfig; import org.eclipsefoundation.git.eca.config.MailValidationConfig;
import org.eclipsefoundation.git.eca.service.UserService; import org.eclipsefoundation.git.eca.service.UserService;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
...@@ -52,19 +53,15 @@ public class CachedUserService implements UserService { ...@@ -52,19 +53,15 @@ public class CachedUserService implements UserService {
@Inject @Inject
MailValidationConfig config; MailValidationConfig config;
// eclipse API rest client interfaces
@Inject @Inject
@RestClient CachingService cache;
AccountsAPI accounts;
@Inject @Inject
ProfileService profile;
// eclipse API rest client interfaces
@RestClient @RestClient
BotsAPI bots; BotsAPI bots;
@Inject
DrupalTokenService oauth;
@Inject
CachingService cache;
// rendered list of regex values // rendered list of regex values
List<Pattern> patterns; List<Pattern> patterns;
...@@ -75,30 +72,29 @@ public class CachedUserService implements UserService { ...@@ -75,30 +72,29 @@ public class CachedUserService implements UserService {
} }
@Override @Override
public EclipseUser getUser(String mail) { public EfUser getUser(String mail) {
if (StringUtils.isBlank(mail)) { if (StringUtils.isBlank(mail)) {
return null; return null;
} }
CacheWrapper<EclipseUser> u = cache.get(mail, new MultivaluedMapImpl<>(), EclipseUser.class, () -> retrieveUser(mail)); CacheWrapper<EfUser> result = cache.get(mail, new MultivaluedMapImpl<>(), EfUser.class, () -> retrieveUser(mail));
if (u.getData().isPresent()) { Optional<EfUser> user = result.getData();
if (user.isPresent()) {
LOGGER.debug("Found user with email {}", mail); LOGGER.debug("Found user with email {}", mail);
return u.getData().get(); return user.get();
} }
LOGGER.debug("Could not find user with email {}", mail); LOGGER.debug("Could not find user with email {}", mail);
return null; return null;
} }
@Override @Override
public EclipseUser getUserByGithubUsername(String username) { public EfUser getUserByGithubUsername(String username) {
if (StringUtils.isBlank(username)) { if (StringUtils.isBlank(username)) {
return null; return null;
} }
CacheWrapper<EclipseUser> u = cache Optional<EfUser> user = profile.fetchUserByGhHandle(username, true);
.get("gh:" + username, new MultivaluedMapImpl<>(), EclipseUser.class, if (user.isPresent()) {
() -> accounts.getUserByGithubUname(getBearerToken(), username));
if (u.getData().isPresent()) {
LOGGER.debug("Found user with name {}", username); LOGGER.debug("Found user with name {}", username);
return u.getData().get(); return user.get();
} }
LOGGER.debug("Could not find user with name {}", username); LOGGER.debug("Could not find user with name {}", username);
return null; return null;
...@@ -140,29 +136,28 @@ public class CachedUserService implements UserService { ...@@ -140,29 +136,28 @@ public class CachedUserService implements UserService {
} }
/** /**
* Checks for standard and noreply email address matches for a Git user and converts to a Eclipse Foundation account * Checks for standard and noreply email address matches for a Git user and converts to a Eclipse Foundation account object.
* object.
* *
* @param user the user to attempt account retrieval for. * @param user the user to attempt account retrieval for.
* @return the user account if found by mail, or null if none found. * @return the user account if found by mail, or null if none found.
*/ */
private EclipseUser retrieveUser(String mail) { private EfUser retrieveUser(String mail) {
if (StringUtils.isBlank(mail)) { if (StringUtils.isBlank(mail)) {
LOGGER.debug("Blank mail passed, cannot fetch user"); LOGGER.debug("Blank mail passed, cannot fetch user");
return null; return null;
} }
LOGGER.debug("Getting fresh user for {}", mail); LOGGER.debug("Getting fresh user for {}", mail);
// check for noreply (no reply will never have user account, and fails fast) // check for noreply (no reply will never have user account, and fails fast)
EclipseUser eclipseUser = checkForNoReplyUser(mail); EfUser noReplyUser = checkForNoReplyUser(mail);
if (eclipseUser != null) { if (noReplyUser != null) {
return eclipseUser; return noReplyUser;
} }
// standard user check (returns best match) // standard user check (returns best match)
LOGGER.debug("Checking user with mail {}", mail); LOGGER.debug("Checking user with mail {}", mail);
try { try {
List<EclipseUser> matches = accounts.getUsers(getBearerToken(), mail); Optional<EfUser> user = profile.performUserSearch(UserSearchParams.builder().setMail(mail).build());
if (matches != null && !matches.isEmpty()) { if (user.isPresent()) {
return matches.get(0); return user.get();
} }
} catch (WebApplicationException e) { } catch (WebApplicationException e) {
LOGGER.warn("Could not find user account with mail '{}'", mail); LOGGER.warn("Could not find user account with mail '{}'", mail);
...@@ -171,13 +166,13 @@ public class CachedUserService implements UserService { ...@@ -171,13 +166,13 @@ public class CachedUserService implements UserService {
} }
/** /**
* Checks git user for no-reply address, and attempts to ratify user through reverse lookup in API service. Currently, * Checks git user for no-reply address, and attempts to ratify user through reverse lookup in API service. Currently, this service only
* this service only recognizes Github no-reply addresses as they have a route to be mapped. * recognizes Github no-reply addresses as they have a route to be mapped.
* *
* @param user the Git user account to check for no-reply mail address * @param user the Git user account to check for no-reply mail address
* @return the Eclipse user if email address is detected no reply and one can be mapped, otherwise null * @return the Eclipse user if email address is detected no reply and one can be mapped, otherwise null
*/ */
private EclipseUser checkForNoReplyUser(String mail) { private EfUser checkForNoReplyUser(String mail) {
if (StringUtils.isBlank(mail)) { if (StringUtils.isBlank(mail)) {
LOGGER.debug("Blank mail passed, cannot fetch user"); LOGGER.debug("Blank mail passed, cannot fetch user");
return null; return null;
...@@ -254,15 +249,10 @@ public class CachedUserService implements UserService { ...@@ -254,15 +249,10 @@ public class CachedUserService implements UserService {
} }
private List<JsonNode> getBots() { private List<JsonNode> getBots() {
CacheWrapper<List<JsonNode>> allBots = cache.get("allBots", new MultivaluedMapImpl<>(), JsonNode.class, () -> bots.getBots()); return cache
if (allBots.getData().isEmpty()) { .get("allBots", new MultivaluedMapImpl<>(), JsonNode.class, () -> bots.getBots())
return Collections.emptyList(); .getData()
} .orElse(Collections.emptyList());
return allBots.getData().get();
}
private String getBearerToken() {
return "Bearer " + oauth.getToken();
} }
} }
...@@ -11,12 +11,15 @@ ...@@ -11,12 +11,15 @@
**********************************************************************/ **********************************************************************/
package org.eclipsefoundation.git.eca.service.impl; package org.eclipsefoundation.git.eca.service.impl;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.efservices.api.models.EfUser.Country;
import org.eclipsefoundation.efservices.api.models.EfUser.Eca;
import org.eclipsefoundation.efservices.api.models.Project; import org.eclipsefoundation.efservices.api.models.Project;
import org.eclipsefoundation.git.eca.api.models.EclipseUser;
import org.eclipsefoundation.git.eca.config.MailValidationConfig; import org.eclipsefoundation.git.eca.config.MailValidationConfig;
import org.eclipsefoundation.git.eca.dto.CommitValidationStatus; import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
import org.eclipsefoundation.git.eca.helper.CommitHelper; import org.eclipsefoundation.git.eca.helper.CommitHelper;
...@@ -40,8 +43,8 @@ import jakarta.ws.rs.core.Response; ...@@ -40,8 +43,8 @@ import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.Response.Status; import jakarta.ws.rs.core.Response.Status;
/** /**
* Default service for validating external requests for ECA validation, as well as storing and retrieving information * Default service for validating external requests for ECA validation, as well as storing and retrieving information about historic
* about historic requests. * requests.
* *
* @author Martin Lowe * @author Martin Lowe
* *
...@@ -60,7 +63,6 @@ public class DefaultValidationService implements ValidationService { ...@@ -60,7 +63,6 @@ public class DefaultValidationService implements ValidationService {
@Inject @Inject
ValidationStatusService statusService; ValidationStatusService statusService;
@Override @Override
public ValidationResponse validateIncomingRequest(ValidationRequest req, RequestWrapper wrapper) { public ValidationResponse validateIncomingRequest(ValidationRequest req, RequestWrapper wrapper) {
// get the projects associated with the current request, if any // get the projects associated with the current request, if any
...@@ -106,8 +108,8 @@ public class DefaultValidationService implements ValidationService { ...@@ -106,8 +108,8 @@ public class DefaultValidationService implements ValidationService {
} }
/** /**
* Process the current request, validating that the passed commit is valid. The author and committers Eclipse Account is * Process the current request, validating that the passed commit is valid. The author and committers Eclipse Account is retrieved,
* retrieved, which are then used to check if the current commit is valid for the current project. * which are then used to check if the current commit is valid for the current project.
* *
* @param c the commit to process * @param c the commit to process
* @param response the response container * @param response the response container
...@@ -130,13 +132,13 @@ public class DefaultValidationService implements ValidationService { ...@@ -130,13 +132,13 @@ public class DefaultValidationService implements ValidationService {
} }
// retrieve the eclipse account for the author // retrieve the eclipse account for the author
EclipseUser eclipseAuthor = getIdentifiedUser(author); EfUser eclipseAuthor = getIdentifiedUser(author);
// if the user is a bot, generate a stubbed user // if the user is a bot, generate a stubbed user
if (isAllowedUser(author.getMail()) || users.userIsABot(author.getMail(), filteredProjects)) { if (isAllowedUser(author.getMail()) || users.userIsABot(author.getMail(), filteredProjects)) {
response response
.addMessage(c.getHash(), .addMessage(c.getHash(),
String.format("Automated user '%1$s' detected for author of commit %2$s", author.getMail(), c.getHash())); String.format("Automated user '%1$s' detected for author of commit %2$s", author.getMail(), c.getHash()));
eclipseAuthor = EclipseUser.createBotStub(author); eclipseAuthor = createBotStub(author);
} else if (eclipseAuthor == null) { } else if (eclipseAuthor == null) {
response response
.addMessage(c.getHash(), .addMessage(c.getHash(),
...@@ -149,13 +151,13 @@ public class DefaultValidationService implements ValidationService { ...@@ -149,13 +151,13 @@ public class DefaultValidationService implements ValidationService {
GitUser committer = c.getCommitter(); GitUser committer = c.getCommitter();
// retrieve the eclipse account for the committer // retrieve the eclipse account for the committer
EclipseUser eclipseCommitter = getIdentifiedUser(committer); EfUser eclipseCommitter = getIdentifiedUser(committer);
// check if whitelisted or bot // check if whitelisted or bot
if (isAllowedUser(committer.getMail()) || users.userIsABot(committer.getMail(), filteredProjects)) { if (isAllowedUser(committer.getMail()) || users.userIsABot(committer.getMail(), filteredProjects)) {
response response
.addMessage(c.getHash(), .addMessage(c.getHash(),
String.format("Automated user '%1$s' detected for committer of commit %2$s", committer.getMail(), c.getHash())); String.format("Automated user '%1$s' detected for committer of commit %2$s", committer.getMail(), c.getHash()));
eclipseCommitter = EclipseUser.createBotStub(committer); eclipseCommitter = createBotStub(committer);
} else if (eclipseCommitter == null) { } else if (eclipseCommitter == null) {
response response
.addMessage(c.getHash(), .addMessage(c.getHash(),
...@@ -174,75 +176,72 @@ public class DefaultValidationService implements ValidationService { ...@@ -174,75 +176,72 @@ public class DefaultValidationService implements ValidationService {
} }
/** /**
* Validates author access for the current commit. If there are errors, they are recorded in the response for the * Validates author access for the current commit. If there are errors, they are recorded in the response for the current request to be
* current request to be returned once all validation checks are completed. * returned once all validation checks are completed.
* *
* @param r the current response object for the request * @param r the current response object for the request
* @param c the commit that is being validated * @param c the commit that is being validated
* @param eclipseUser the user to validate on a branch * @param EfUser the user to validate on a branch
* @param filteredProjects tracked projects for the current request * @param filteredProjects tracked projects for the current request
* @param errorCode the error code to display if the user does not have access * @param errorCode the error code to display if the user does not have access
*/ */
private void validateUserAccess(ValidationResponse r, Commit c, EclipseUser eclipseUser, List<Project> filteredProjects, private void validateUserAccess(ValidationResponse r, Commit c, EfUser EfUser, List<Project> filteredProjects,
APIStatusCode errorCode) { APIStatusCode errorCode) {
// call isCommitter inline and pass to partial call // call isCommitter inline and pass to partial call
validateUserAccessPartial(r, c, eclipseUser, isCommitter(r, eclipseUser, c.getHash(), filteredProjects), errorCode); validateUserAccessPartial(r, c, EfUser, isCommitter(r, EfUser, c.getHash(), filteredProjects), errorCode);
} }
/** /**
* Allows for isCommitter to be called external to this method. This was extracted to ensure that isCommitter isn't * Allows for isCommitter to be called external to this method. This was extracted to ensure that isCommitter isn't called twice for the
* called twice for the same user when checking committer proxy push rules and committer general access. * same user when checking committer proxy push rules and committer general access.
* *
* @param r the current response object for the request * @param r the current response object for the request
* @param c the commit that is being validated * @param c the commit that is being validated
* @param eclipseUser the user to validate on a branch * @param EfUser the user to validate on a branch
* @param isCommitter the results of the isCommitter call from this class. * @param isCommitter the results of the isCommitter call from this class.
* @param errorCode the error code to display if the user does not have access * @param errorCode the error code to display if the user does not have access
*/ */
private void validateUserAccessPartial(ValidationResponse r, Commit c, EclipseUser eclipseUser, boolean isCommitter, private void validateUserAccessPartial(ValidationResponse r, Commit c, EfUser EfUser, boolean isCommitter, APIStatusCode errorCode) {
APIStatusCode errorCode) {
String userType = "author"; String userType = "author";
if (APIStatusCode.ERROR_COMMITTER.equals(errorCode)) { if (APIStatusCode.ERROR_COMMITTER.equals(errorCode)) {
userType = "committer"; userType = "committer";
} }
if (isCommitter) { if (isCommitter) {
r r.addMessage(c.getHash(), String.format("Eclipse user '%s'(%s) is a committer on the project.", EfUser.getName(), userType));
.addMessage(c.getHash(),
String.format("Eclipse user '%s'(%s) is a committer on the project.", eclipseUser.getName(), userType));
} else { } else {
r r
.addMessage(c.getHash(), .addMessage(c.getHash(),
String.format("Eclipse user '%s'(%s) is not a committer on the project.", eclipseUser.getName(), userType)); String.format("Eclipse user '%s'(%s) is not a committer on the project.", EfUser.getName(), userType));
// check if the author is signed off if not a committer // check if the author is signed off if not a committer
if (eclipseUser.getECA().getSigned()) { if (EfUser.getEca().getSigned()) {
r r
.addMessage(c.getHash(), .addMessage(c.getHash(),
String String
.format("Eclipse user '%s'(%s) has a current Eclipse Contributor Agreement (ECA) on file.", .format("Eclipse user '%s'(%s) has a current Eclipse Contributor Agreement (ECA) on file.",
eclipseUser.getName(), userType)); EfUser.getName(), userType));
} else { } else {
r r
.addMessage(c.getHash(), String .addMessage(c.getHash(), String
.format("Eclipse user '%s'(%s) does not have a current Eclipse Contributor Agreement (ECA) on file.\n" .format("Eclipse user '%s'(%s) does not have a current Eclipse Contributor Agreement (ECA) on file.\n"
+ "If there are multiple commits, please ensure that each author has a ECA.", eclipseUser.getName(), + "If there are multiple commits, please ensure that each author has a ECA.", EfUser.getName(),
userType)); userType));
r r
.addError(c.getHash(), .addError(c.getHash(),
String String
.format("An Eclipse Contributor Agreement is required for Eclipse user '%s'(%s).", .format("An Eclipse Contributor Agreement is required for Eclipse user '%s'(%s).", EfUser.getName(),
eclipseUser.getName(), userType), userType),
errorCode); errorCode);
} }
} }
} }
/** /**
* Checks whether the given user is a committer on the project. If they are and the project is also a specification for * Checks whether the given user is a committer on the project. If they are and the project is also a specification for a working group,
* a working group, an additional access check is made against the user. * an additional access check is made against the user.
* *
* <p> * <p>
* Additionally, a check is made to see if the user is a registered bot user for the given project. If they match for * Additionally, a check is made to see if the user is a registered bot user for the given project. If they match for the given project,
* the given project, they are granted committer-like access to the repository. * they are granted committer-like access to the repository.
* *
* @param r the current response object for the request * @param r the current response object for the request
* @param user the user to validate on a branch * @param user the user to validate on a branch
...@@ -250,7 +249,7 @@ public class DefaultValidationService implements ValidationService { ...@@ -250,7 +249,7 @@ public class DefaultValidationService implements ValidationService {
* @param filteredProjects tracked projects for the current request * @param filteredProjects tracked projects for the current request
* @return true if user is considered a committer, false otherwise. * @return true if user is considered a committer, false otherwise.
*/ */
private boolean isCommitter(ValidationResponse r, EclipseUser user, String hash, List<Project> filteredProjects) { private boolean isCommitter(ValidationResponse r, EfUser user, String hash, List<Project> filteredProjects) {
// iterate over filtered projects // iterate over filtered projects
for (Project p : filteredProjects) { for (Project p : filteredProjects) {
LOGGER.debug("Checking project '{}' for user '{}'", p.getName(), user.getName()); LOGGER.debug("Checking project '{}' for user '{}'", p.getName(), user.getName());
...@@ -258,7 +257,7 @@ public class DefaultValidationService implements ValidationService { ...@@ -258,7 +257,7 @@ public class DefaultValidationService implements ValidationService {
if (p.getCommitters().stream().anyMatch(u -> u.getUsername().equals(user.getName()))) { if (p.getCommitters().stream().anyMatch(u -> u.getUsername().equals(user.getName()))) {
// check if the current project is a committer project, and if the user can // check if the current project is a committer project, and if the user can
// commit to specs // commit to specs
if (p.getSpecWorkingGroup().isPresent() && !user.getECA().getCanContributeSpecProject()) { if (p.getSpecWorkingGroup().isPresent() && !user.getEca().getCanContributeSpecProject()) {
// set error + update response status // set error + update response status
r r
.addError(hash, String .addError(hash, String
...@@ -292,18 +291,17 @@ public class DefaultValidationService implements ValidationService { ...@@ -292,18 +291,17 @@ public class DefaultValidationService implements ValidationService {
} }
/** /**
* Retrieves an Eclipse Account user object given the Git users email address (at minimum). This is facilitated using * Retrieves an Eclipse Account user object given the Git users email address (at minimum). This is facilitated using the Eclipse
* the Eclipse Foundation accounts API, along short lived in-memory caching for performance and some protection against * Foundation accounts API, along short lived in-memory caching for performance and some protection against duplicate requests.
* duplicate requests.
* *
* @param user the user to retrieve Eclipse Account information for * @param user the user to retrieve Eclipse Account information for
* @return the Eclipse Account user information if found, or null if there was an error or no user exists. * @return the Eclipse Account user information if found, or null if there was an error or no user exists.
*/ */
private EclipseUser getIdentifiedUser(GitUser user) { private EfUser getIdentifiedUser(GitUser user) {
// check if the external ID is set, and if so, attempt to look the user up. // check if the external ID is set, and if so, attempt to look the user up.
if (StringUtils.isNotBlank(user.getExternalId())) { if (StringUtils.isNotBlank(user.getExternalId())) {
// right now this is only supported for Github account lookups, so that will be used // right now this is only supported for Github account lookups, so that will be used
EclipseUser actualUser = users.getUserByGithubUsername(user.getExternalId()); EfUser actualUser = users.getUserByGithubUsername(user.getExternalId());
// if present, return the user. Otherwise, log and continue processing // if present, return the user. Otherwise, log and continue processing
if (actualUser != null) { if (actualUser != null) {
return actualUser; return actualUser;
...@@ -322,7 +320,7 @@ public class DefaultValidationService implements ValidationService { ...@@ -322,7 +320,7 @@ public class DefaultValidationService implements ValidationService {
// get the Eclipse account for the user // get the Eclipse account for the user
try { try {
// use cache to avoid asking for the same user repeatedly on repeated requests // use cache to avoid asking for the same user repeatedly on repeated requests
EclipseUser foundUser = users.getUser(user.getMail()); EfUser foundUser = users.getUser(user.getMail());
if (foundUser == null) { if (foundUser == null) {
LOGGER.warn("No users found for mail '{}'", user.getMail()); LOGGER.warn("No users found for mail '{}'", user.getMail());
} }
...@@ -362,4 +360,26 @@ public class DefaultValidationService implements ValidationService { ...@@ -362,4 +360,26 @@ public class DefaultValidationService implements ValidationService {
&& status.get().getUserMail().equalsIgnoreCase(c.getAuthor().getMail()) && status.get().getUserMail().equalsIgnoreCase(c.getAuthor().getMail())
&& (c.getLastModificationDate() == null || status.get().getLastModified().equals(c.getLastModificationDate())); && (c.getLastModificationDate() == null || status.get().getLastModified().equals(c.getLastModificationDate()));
} }
private EfUser createBotStub(GitUser user) {
return EfUser
.builder()
.setUid("0")
.setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setName(user.getName())
.setMail(user.getMail())
.setEca(Eca.builder().build())
.setIsBot(true)
.build();
}
} }
...@@ -179,19 +179,23 @@ ...@@ -179,19 +179,23 @@
<section id="block-site-login-eclipse-eca-sle-eca-lookup-tool" <section id="block-site-login-eclipse-eca-sle-eca-lookup-tool"
class="margin-bottom-30 clearfix"> class="margin-bottom-30 clearfix">
<h2>ECA Validation Tool</h2> <h2>ECA Validation Tool</h2>
{#if currentUser and currentUser.isCommitter}
<form id="eclipse-eca-lookup-form" accept-charset="UTF-8"> <form id="eclipse-eca-lookup-form" accept-charset="UTF-8">
<div class="form-item form-item-input form-type-textfield form-group"> <div class="form-item form-item-input form-type-textfield form-group">
<input placeholder="Enter email address" class="form-control form-text" type="text" id="email-input" {#if currentUser and currentUser.isCommitter}
name="email-input" value="" size="21" maxlength="128" autofill-prediction="UNKNOWN_TYPE"> <input placeholder="Enter email address or username" class="form-control form-text" type="text" id="query-input"
<div class="help-block">Enter email address of an Eclipse account.</div> name="query-input" value="" size="21" maxlength="128" autofill-prediction="UNKNOWN_TYPE">
<div class="help-block">Enter email address or username of an Eclipse account.</div>
{#else}
<input placeholder="Enter username" class="form-control form-text" type="text" id="query-input"
name="query-input" value="" size="21" maxlength="128" autofill-prediction="UNKNOWN_TYPE">
<div class="help-block">Enter username of an Eclipse account.</div>
{/if}
</div> </div>
<button class="btn-success btn form-submit" type="submit" id="edit-submit">Verify ECA</button> <button class="btn-success btn form-submit" type="submit" id="edit-submit">Verify ECA</button>
</form> </form>
{#else if currentUser} {#if currentUser == null}
<p>Logged in users must have committer level access to use the ECA Validation tool.</p> <hr />
{#else} <p>If you are a committer or project lead and wish to query for users by their email address, please login.</p>
<p>Please login to use the ECA Validation tool. Please note that only committers are able to use this tool.</p>
<a class="btn btn-primary" href="/git/login?redirect={redirectUri}">Login</a> <a class="btn btn-primary" href="/git/login?redirect={redirectUri}">Login</a>
{/if} {/if}
</section> </section>
...@@ -260,30 +264,27 @@ ...@@ -260,30 +264,27 @@
// don't submit the form as we will handle it w/ ajax // don't submit the form as we will handle it w/ ajax
e.preventDefault(); e.preventDefault();
// grab the constants from the form and perform a check // grab the constants from the form and perform a check
const inputVal = $(e.target).find('#email-input').val(); const inputVal = $(e.target).find('#query-input').val();
const $submitButton = $(e.target).find('button'); const $submitButton = $(e.target).find('button');
// disable the button so that requests won't be spammed // disable the button so that requests won't be spammed
$submitButton.attr("disabled", "disabled"); $submitButton.attr("disabled", "disabled");
// use ajax to check the ECA status of the user // use ajax to check the ECA status of the user
$.ajax({ fetch('/git/eca/lookup?' + new URLSearchParams({
url: `/git/eca/lookup`, q: inputVal,
data: { }).toString()).then(d => {
email: inputVal if (d.status === 200) {
}, toast(`There is a valid ECA on file for <em>${escapeHTML(inputVal)}</em>`, 'success');
success: function (data) { } else if (d.status === 400) {
toast(`There is a valid ECA on file for <em>${escapeHTML(inputVal)}</em>`, 'success'); // we pull out the message from the error to give user some context
}, d.json().then(error => {
error: function (xhr) { toast(`<p>Could not lookup <em>${escapeHTML(inputVal)}</em></p><p>Reason: ${error.message}</p>`, 'warning');
console.log(xhr.status); });
if (xhr.status == '403') { } else if (d.status === 404) {
toast(`There is no valid ECA on file for <em>${escapeHTML(inputVal)}</em>`, 'danger');
} else {
toast(`No Eclipse Foundation account found for <em>${escapeHTML(inputVal)}</em>`, 'warning'); toast(`No Eclipse Foundation account found for <em>${escapeHTML(inputVal)}</em>`, 'warning');
} else {
toast(`Error encountered while checking for an ECA on file for <em>${escapeHTML(inputVal)}</em>`, 'danger');
} }
},
complete: function () {
$submitButton.removeAttr("disabled"); $submitButton.removeAttr("disabled");
}
}); });
}); });
} }
......
...@@ -59,7 +59,8 @@ import jakarta.ws.rs.core.Response.Status; ...@@ -59,7 +59,8 @@ import jakarta.ws.rs.core.Response.Status;
@QuarkusTest @QuarkusTest
class ValidationResourceTest { class ValidationResourceTest {
public static final String ECA_BASE_URL = "/eca"; public static final String ECA_BASE_URL = "/eca";
public static final String LOOKUP_URL = ECA_BASE_URL + "/lookup?email={param}"; public static final String LOOKUP_URL = ECA_BASE_URL + "/lookup?q={param}";
public static final String LOOKUP_LEGACY_URL = ECA_BASE_URL + "/lookup?email={param}";
public static final String STATUS_URL = ECA_BASE_URL + "/status/{fingerprint}"; public static final String STATUS_URL = ECA_BASE_URL + "/status/{fingerprint}";
public static final String STATUS_UI_URL = STATUS_URL + "/ui"; public static final String STATUS_UI_URL = STATUS_URL + "/ui";
...@@ -116,16 +117,36 @@ class ValidationResourceTest { ...@@ -116,16 +117,36 @@ class ValidationResourceTest {
/* /*
* LOOKUP CASES * LOOKUP CASES
*/ */
// by email
public static final EndpointTestCase LOOKUP_SUCCESS_CASE = TestCaseHelper public static final EndpointTestCase LOOKUP_SUCCESS_CASE = TestCaseHelper
.buildSuccessCase(LOOKUP_URL, new String[] { "slom@eclipse-foundation.org" }, ""); .buildSuccessCase(LOOKUP_URL, new String[] { "slom@eclipse-foundation.org" }, "");
public static final EndpointTestCase LOOKUP_ANONYMOUS_CASE = TestCaseHelper public static final EndpointTestCase LOOKUP_ANONYMOUS_CASE = TestCaseHelper
.prepareTestCase(LOOKUP_URL, new String[] { "slom@eclipse-foundation.org" }, "") .prepareTestCase(LOOKUP_URL, new String[] { "slom@eclipse-foundation.org" }, "")
.setStatusCode(Status.UNAUTHORIZED.getStatusCode()) .setStatusCode(Status.BAD_REQUEST.getStatusCode())
.build(); .build();
public static final EndpointTestCase LOOKUP_FORBIDDEN_CASE = TestCaseHelper public static final EndpointTestCase LOOKUP_FORBIDDEN_CASE = TestCaseHelper
.buildForbiddenCase(LOOKUP_URL, new String[] { "newbie@important.co" }, ""); .buildForbiddenCase(LOOKUP_URL, new String[] { "newbie@important.co" }, "");
public static final EndpointTestCase LOOKUP_NOT_FOUND_CASE = TestCaseHelper public static final EndpointTestCase LOOKUP_NOT_FOUND_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_URL, new String[] { "dummy@fake.co" }, ""); .buildNotFoundCase(LOOKUP_URL, new String[] { "dummy@fake.co" }, "");
// by username
public static final EndpointTestCase LOOKUP_USERNAME_SUCCESS_CASE = TestCaseHelper
.buildSuccessCase(LOOKUP_URL, new String[] { "barshall_blathers" }, "");
public static final EndpointTestCase LOOKUP_USERNAME_FORBIDDEN_CASE = TestCaseHelper
.buildForbiddenCase(LOOKUP_URL, new String[] { "newbieAnon" }, "");
public static final EndpointTestCase LOOKUP_USERNAME_NOT_FOUND_CASE = TestCaseHelper
.buildNotFoundCase(LOOKUP_URL, new String[] { "dummy11" }, "");
// lookup cases with email param
public static final EndpointTestCase LOOKUP_SUCCESS_EMAIL_PARAM_CASE = TestCaseHelper
.buildSuccessCase(LOOKUP_LEGACY_URL, new String[] { "slom@eclipse-foundation.org" }, "");
public static final EndpointTestCase LOOKUP_ANONYMOUS_EMAIL_PARAM_CASE = TestCaseHelper
.prepareTestCase(LOOKUP_LEGACY_URL, new String[] { "slom@eclipse-foundation.org" }, "")
.setStatusCode(Status.UNAUTHORIZED.getStatusCode())
.build();
public static final EndpointTestCase LOOKUP_FORBIDDEN_EMAIL_PARAM_CASE = TestCaseHelper
.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" }, "");
@Inject @Inject
CachingService cs; CachingService cs;
...@@ -880,10 +901,34 @@ class ValidationResourceTest { ...@@ -880,10 +901,34 @@ class ValidationResourceTest {
EndpointTestBuilder.from(LOOKUP_ANONYMOUS_CASE).run(); EndpointTestBuilder.from(LOOKUP_ANONYMOUS_CASE).run();
} }
// by username
@Test
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
void validateUserLookup_username_userNotFound() {
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_userNoECA() {
EndpointTestBuilder.from(LOOKUP_USERNAME_FORBIDDEN_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_userSuccess() {
EndpointTestBuilder.from(LOOKUP_USERNAME_SUCCESS_CASE).run();
}
// by email
@Test @Test
@TestSecurity(user = "newbieAnon", roles = AuthHelper.DEFAULT_ROLE) @TestSecurity(user = "newbieAnon", roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "newbie@important.co") }) @OidcSecurity(claims = { @Claim(key = "email", value = "newbie@important.co") })
void validateUserLookup_failure_nonCommitter() { void validateUserLookup_failure_nonCommitter() {
// committer required for email lookup
EndpointTestBuilder.from(LOOKUP_FORBIDDEN_CASE).run(); EndpointTestBuilder.from(LOOKUP_FORBIDDEN_CASE).run();
} }
...@@ -905,7 +950,37 @@ class ValidationResourceTest { ...@@ -905,7 +950,37 @@ class ValidationResourceTest {
@TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE) @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") }) @OidcSecurity(claims = { @Claim(key = "email", value = "opearson@important.co") })
void validateUserLookup_userSuccess() { void validateUserLookup_userSuccess() {
EndpointTestBuilder.from(LOOKUP_NOT_FOUND_CASE).run(); EndpointTestBuilder.from(LOOKUP_SUCCESS_CASE).run();
}
// legacy lookup cases using email param
@Test
@TestSecurity(user = "newbieAnon", roles = AuthHelper.DEFAULT_ROLE)
@OidcSecurity(claims = { @Claim(key = "email", value = "newbie@important.co") })
void validateUserLookup_legacy_failure_nonCommitter() {
EndpointTestBuilder.from(LOOKUP_FORBIDDEN_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_userNotFound() {
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_userNoECA() {
EndpointTestBuilder.from(LOOKUP_FORBIDDEN_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_userSuccess() {
EndpointTestBuilder.from(LOOKUP_SUCCESS_EMAIL_PARAM_CASE).run();
} }
// The default commit for most users. Used for most user tests // The default commit for most users. Used for most user tests
......
...@@ -16,16 +16,15 @@ import java.util.Collections; ...@@ -16,16 +16,15 @@ import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import jakarta.inject.Inject; import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.efservices.api.models.Project; import org.eclipsefoundation.efservices.api.models.Project;
import org.eclipsefoundation.git.eca.api.models.EclipseUser;
import org.eclipsefoundation.git.eca.helper.ProjectHelper; import org.eclipsefoundation.git.eca.helper.ProjectHelper;
import org.eclipsefoundation.git.eca.service.UserService; import org.eclipsefoundation.git.eca.service.UserService;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
/** /**
* Tests user service impl using test stub data available from API stubs. While not a perfect test as there is no auth, * Tests user service impl using test stub data available from API stubs. While not a perfect test as there is no auth,
...@@ -44,7 +43,7 @@ class CachedUserServiceTest { ...@@ -44,7 +43,7 @@ class CachedUserServiceTest {
@Test @Test
void getUser_success() { void getUser_success() {
EclipseUser u = users.getUser("grunt@important.co"); EfUser u = users.getUser("grunt@important.co");
// assert that this is the user we expect and that it exists // assert that this is the user we expect and that it exists
Assertions.assertNotNull(u); Assertions.assertNotNull(u);
Assertions.assertEquals("grunter", u.getName()); Assertions.assertEquals("grunter", u.getName());
...@@ -52,7 +51,7 @@ class CachedUserServiceTest { ...@@ -52,7 +51,7 @@ class CachedUserServiceTest {
@Test @Test
void getUser_noReplyGH_success() { void getUser_noReplyGH_success() {
EclipseUser u = users.getUser("123456789+grunter2@users.noreply.github.com"); EfUser u = users.getUser("123456789+grunter2@users.noreply.github.com");
// assert that this is the user we expect and that it exists // assert that this is the user we expect and that it exists
Assertions.assertNotNull(u); Assertions.assertNotNull(u);
Assertions.assertEquals("grunter", u.getName()); Assertions.assertEquals("grunter", u.getName());
...@@ -76,7 +75,7 @@ class CachedUserServiceTest { ...@@ -76,7 +75,7 @@ class CachedUserServiceTest {
@Test @Test
void getUserByGithubUsername_success() { void getUserByGithubUsername_success() {
EclipseUser u = users.getUserByGithubUsername("grunter2"); EfUser u = users.getUserByGithubUsername("grunter2");
// assert that this is the user we expect and that it exists // assert that this is the user we expect and that it exists
Assertions.assertNotNull(u); Assertions.assertNotNull(u);
Assertions.assertEquals("grunt@important.co", u.getMail()); Assertions.assertEquals("grunt@important.co", u.getMail());
......
...@@ -11,19 +11,21 @@ ...@@ -11,19 +11,21 @@
**********************************************************************/ **********************************************************************/
package org.eclipsefoundation.git.eca.test.api; package org.eclipsefoundation.git.eca.test.api;
import java.util.Arrays; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.function.Predicate;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.rest.client.inject.RestClient; import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.eclipsefoundation.git.eca.api.AccountsAPI; import org.eclipsefoundation.efservices.api.ProfileAPI;
import org.eclipsefoundation.git.eca.api.models.EclipseUser; import org.eclipsefoundation.efservices.api.models.EfUser;
import org.eclipsefoundation.git.eca.api.models.EclipseUser.ECA; import org.eclipsefoundation.efservices.api.models.EfUser.Country;
import org.eclipsefoundation.efservices.api.models.EfUser.Eca;
import org.eclipsefoundation.efservices.api.models.UserSearchParams;
import io.quarkus.test.Mock; import io.quarkus.test.Mock;
import jakarta.enterprise.context.ApplicationScoped;
/** /**
* Simple stub for accounts API. Allows for easy testing of users that don't really exist upstream, and so that we don't need a real auth * Simple stub for accounts API. Allows for easy testing of users that don't really exist upstream, and so that we don't need a real auth
...@@ -35,103 +37,185 @@ import io.quarkus.test.Mock; ...@@ -35,103 +37,185 @@ import io.quarkus.test.Mock;
@Mock @Mock
@RestClient @RestClient
@ApplicationScoped @ApplicationScoped
public class MockAccountsAPI implements AccountsAPI { public class MockAccountsAPI implements ProfileAPI {
private Map<String, EclipseUser> users; private Map<String, EfUser> users;
public MockAccountsAPI() { public MockAccountsAPI() {
int id = 0; int id = 0;
this.users = new HashMap<>(); this.users = new HashMap<>();
users users
.put("newbie@important.co", .put("newbie@important.co",
EclipseUser EfUser
.builder() .builder()
.setIsCommitter(false) .setIsCommitter(false)
.setUid(id++) .setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setUid(Integer.toString(id++))
.setMail("newbie@important.co") .setMail("newbie@important.co")
.setName("newbieAnon") .setName("newbieAnon")
.setECA(ECA.builder().build()) .setEca(Eca.builder().build())
.build()); .build());
users users
.put("slom@eclipse-foundation.org", .put("slom@eclipse-foundation.org",
EclipseUser EfUser
.builder() .builder()
.setIsCommitter(false) .setIsCommitter(false)
.setUid(id++) .setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setUid(Integer.toString(id++))
.setMail("slom@eclipse-foundation.org") .setMail("slom@eclipse-foundation.org")
.setName("barshall_blathers") .setName("barshall_blathers")
.setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()) .setEca(Eca.builder().setCanContributeSpecProject(true).setSigned(true).build())
.build()); .build());
users users
.put("tester@eclipse-foundation.org", .put("tester@eclipse-foundation.org",
EclipseUser EfUser
.builder() .builder()
.setIsCommitter(false) .setIsCommitter(false)
.setUid(id++) .setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setUid(Integer.toString(id++))
.setMail("tester@eclipse-foundation.org") .setMail("tester@eclipse-foundation.org")
.setName("mctesterson") .setName("mctesterson")
.setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()) .setEca(Eca.builder().setCanContributeSpecProject(false).setSigned(true).build())
.build()); .build());
users users
.put("code.wiz@important.co", .put("code.wiz@important.co",
EclipseUser EfUser
.builder() .builder()
.setIsCommitter(true) .setIsCommitter(true)
.setUid(id++) .setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setUid(Integer.toString(id++))
.setMail("code.wiz@important.co") .setMail("code.wiz@important.co")
.setName("da_wizz") .setName("da_wizz")
.setECA(ECA.builder().setCanContributeSpecProject(true).setSigned(true).build()) .setEca(Eca.builder().setCanContributeSpecProject(true).setSigned(true).build())
.setGithubHandle("wiz_in_da_hub") .setGithubHandle("wiz_in_da_hub")
.build()); .build());
users users
.put("grunt@important.co", .put("grunt@important.co",
EclipseUser EfUser
.builder() .builder()
.setIsCommitter(true) .setIsCommitter(true)
.setUid(id++) .setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setUid(Integer.toString(id++))
.setMail("grunt@important.co") .setMail("grunt@important.co")
.setName("grunter") .setName("grunter")
.setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()) .setEca(Eca.builder().setCanContributeSpecProject(false).setSigned(true).build())
.setGithubHandle("grunter2") .setGithubHandle("grunter2")
.build()); .build());
users users
.put("paper.pusher@important.co", .put("paper.pusher@important.co",
EclipseUser EfUser
.builder() .builder()
.setIsCommitter(false) .setIsCommitter(false)
.setUid(id++) .setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setUid(Integer.toString(id++))
.setMail("paper.pusher@important.co") .setMail("paper.pusher@important.co")
.setName("sumAnalyst") .setName("sumAnalyst")
.setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()) .setEca(Eca.builder().setCanContributeSpecProject(false).setSigned(true).build())
.build()); .build());
users users
.put("opearson@important.co", .put("opearson@important.co",
EclipseUser EfUser
.builder() .builder()
.setIsCommitter(true) .setIsCommitter(true)
.setUid(id++) .setPicture("")
.setFirstName("")
.setLastName("")
.setFullName("")
.setPublisherAgreements(Collections.emptyMap())
.setTwitterHandle("")
.setJobTitle("")
.setWebsite("")
.setCountry(Country.builder().build())
.setInterests(Collections.emptyList())
.setUid(Integer.toString(id++))
.setMail("opearson@important.co") .setMail("opearson@important.co")
.setName("opearson") .setName("opearson")
.setECA(ECA.builder().setCanContributeSpecProject(false).setSigned(true).build()) .setEca(Eca.builder().setCanContributeSpecProject(false).setSigned(true).build())
.setGithubHandle("opearson") .setGithubHandle("opearson")
.setIsCommitter(true)
.build()); .build());
} }
@Override @Override
public List<EclipseUser> getUsers(String token, String mail) { public EfUser getUserByEfUsername(String token, String uname) {
return Arrays.asList(users.get(mail)); return users.values().stream().filter(usernamePredicate(uname)).findFirst().orElseGet(() -> null);
} }
@Override @Override
public EclipseUser getUserByGithubUname(String token, String uname) { public EfUser getUserByGithubHandle(String token, String ghHandle) {
// assumes github id is same as uname for purposes of lookup (simplifies fetch logic)
return users return users
.values() .values()
.stream() .stream()
.filter(u -> u.getGithubHandle() != null && u.getGithubHandle().equals(uname)) .filter(u -> u.getGithubHandle() != null && u.getGithubHandle().equals(ghHandle))
.findFirst() .findFirst()
.orElseGet(() -> null); .orElseGet(() -> null);
} }
@Override
public List<EfUser> getUsers(String token, UserSearchParams params) {
return users
.values()
.stream()
.filter(usernamePredicate(params.getName()))
.filter(u -> params.getMail() == null || u.getMail().equals(params.getMail()))
.filter(u -> params.getUid() == null || u.getUid().equals(params.getUid()))
.toList();
}
private Predicate<EfUser> usernamePredicate(String target) {
return u -> target == null || u.getName().equals(target);
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment