From e04f9e1a7abdbb0c90b48e1fd714947f9e8a4be8 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 9 Jun 2023 10:28:05 -0400
Subject: [PATCH 1/5] Cleanup openapi spec to match more strict measures

Measures are applied through Insomnia on import, and while we could add
exceptions, being more specific with the fields isn't a bad thing to do
in this case.
---
 spec/openapi.yaml | 82 +++++++++++++++++++++++++++++++----------------
 1 file changed, 54 insertions(+), 28 deletions(-)

diff --git a/spec/openapi.yaml b/spec/openapi.yaml
index c1b7bcec..906bde10 100644
--- a/spec/openapi.yaml
+++ b/spec/openapi.yaml
@@ -2,6 +2,10 @@ openapi: "3.1.0"
 info:
   version: 1.1.0
   title: Eclipse Foundation Git ECA API
+  description: Collection of API endpoints used in the validation and management of external Git services, such as Gitlab and Github.
+  contact:
+    name: IT support
+    url: https://gitlab.eclipse.org/eclipsefdn/it/api/git-eca-rest-api/-/issues
   license:
     name: Eclipse Public License - 2.0
     url: https://www.eclipse.org/legal/epl-2.0/
@@ -11,10 +15,15 @@ servers:
 tags:
   - name: ECA Validation
     description: Definitions in relation to the validation of Git commits through ECA signage
+  - name: Reports
+    description: Reports on metadata associated with Git systems managed by the Eclipse Foundation
+  - name: Integration Webhooks
+    description: Endpoints related to binding to external Git services through a webhook
 
 paths:
   /eca:
     post:
+      operationId: validate
       tags:
         - ECA Validation
       summary: ECA validation
@@ -25,29 +34,36 @@ paths:
             schema:
               $ref: "#/components/schemas/ValidationRequest"
       responses:
-        200:
+        "200":
           description: Success
           content:
             application/json:
               schema:
                 $ref: "#/components/schemas/ValidationResponse"
-        500:
+        "500":
           description: Error while retrieving data
-
   /eca/status/{fingerprint}:
+    parameters:
+       - name: fingerprint
+         in: path
+         description: Unique ID for the request group
+         required: true
+         schema:
+            type: string
     get:
+      operationId: getCommitValidation
       tags:
-        - ECA Validation Status
+        - ECA Validation
       summary: Historic ECA validation status
       description: Returns a set of validation messages for the given unique fingerprint
       responses:
-        200:
+        "200":
           description: Success
           content:
             application/json:
               schema:
                 $ref: "#/components/schemas/CommitValidationStatuses"
-        500:
+        "500":
           description: Error while retrieving data
 
   /eca/status/{fingerprint}/ui:
@@ -59,34 +75,41 @@ paths:
          schema:
             type: string
     get:
+      operationId: getCommitValidationUI
+      tags:
+        - ECA Validation
       summary: Historic ECA validation status in a HTML format
       description: Returns an HTMl page containing validation messages
       responses:
-        200:
+        "200":
           description: Success. An HTML page containing status info
-        404:
+        "404":
           description: Not Found
-        500:
+        "500":
           description: Error while retrieving data
 
   /eca/lookup:
     get:
+      operationId: getUserStatus
+      tags:
+        - ECA Validation
       summary: User status lookup
       description: Returns wether or not the user has a signed ECA
       responses:
-        200:
+        "200":
           description: Success
-        403:
+        "403":
           description: User exists with no ECA
-        404:
+        "404":
           description: User not found
-        500:
+        "500":
           description: Error while retrieving data
 
   /webhooks/github:
     post:
+      operationId: processGithubWebhook
       tags:
-        - Github validation processing
+        - Integration Webhooks
       summary: Github incoming hook event processing
       description: Process incoming pull request hook events from Github
       parameters:
@@ -111,9 +134,9 @@ paths:
             schema:
               $ref: "#/components/schemas/GithubWebhookEvent"
       responses:
-        200:
+        "200":
           description: Success
-        500:
+        "500":
           description: Error while processing data
   /webhooks/github/revalidate/{fingerprint}:
     parameters:
@@ -124,8 +147,9 @@ paths:
          schema:
             type: string
     post:
+      operationId: revalidateWebhookRequest
       tags:
-        - Github validation processing
+        - Integration Webhooks
       summary: Gitlab webhook revalidation request
       description: Process incoming system hooks from GitLab
       requestBody:
@@ -134,20 +158,21 @@ paths:
             schema:
               $ref: '#/components/schemas/RevalidationRequest'
       responses:
-        200:
+        "200":
           description: Success
-        400:
+        "400":
           description: Bad request
           content:
             application/json:
               schema:
                 $ref: "#/components/schemas/Error"
-        404:
+        "404":
           description: Not found
   /webhooks/gitlab/system:
     post:
+      operationId: processGitlabHook
       tags:
-        - Gitlab system event processing
+        - Integration Webhooks
       summary: Gitlab event processing
       description: Process incoming system hooks from GitLab
       parameters:
@@ -162,9 +187,9 @@ paths:
             schema:
               $ref: "#/components/schemas/SystemHook"
       responses:
-        200:
+        "200":
           description: Success
-        500:
+        "500":
           description: Error while processing data
 
   /reports/gitlab/private-projects:
@@ -194,26 +219,27 @@ paths:
         schema:
           type: string
     get:
+      operationId: getPrivateProjectEvents
       tags:
-        - Private project event report
+        - Reports
       summary: Gitlab private project event report
       description: Returns list of private project events using desired filters
       responses:
-        200:
+        "200":
           description: Success
           content:
             application/json:
               schema:
                 $ref: "#/components/schemas/PrivateProjectEvents"
-        400:
+        "400":
           description: Bad Request - invalid non-null prams
           content:
             application/json:
               schema:
                 $ref: "#/components/schemas/Error"
-        401:
+        "401":
           description: Unauthorized - invalid key
-        500:
+        "500":
           description: Error while processing request
 
 components:
-- 
GitLab


From 3b63ee0260c26074e37aefa17e75ec4fc3ba35ea Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 9 Jun 2023 11:30:45 -0400
Subject: [PATCH 2/5] Iss #132 - Update API to use shared projects services

In the API common lib, there is a new efservices module which provides
access to the Accounts and Projects service in a standardized way. This
PR removes most of the code associated with previous projects logic and
keeps a helper to help with formating and filtering the data.

This currently uses a snapshot version, so there will need to be a
release for this code to go live and be stable.
---
 Jenkinsfile                                   |   2 +-
 pom.xml                                       |   9 +-
 .../git/eca/api/ProjectsAPI.java              |  52 ----
 .../git/eca/api/models/InterestGroupData.java |  64 -----
 .../git/eca/api/models/Project.java           | 194 --------------
 .../git/eca/helper/CommitHelper.java          |   2 +-
 .../git/eca/helper/ProjectHelper.java         | 163 ++++++++++++
 .../git/eca/resource/StatusResource.java      |   6 +-
 .../git/eca/resource/ValidationResource.java  |   7 +-
 .../git/eca/service/InterestGroupService.java |  30 ---
 .../git/eca/service/ProjectsService.java      |  54 ----
 .../git/eca/service/UserService.java          |   2 +-
 .../git/eca/service/ValidationService.java    |   2 +-
 .../eca/service/impl/CachedUserService.java   |   4 +-
 .../impl/DefaultInterestGroupService.java     |  64 -----
 .../impl/DefaultValidationService.java        |  11 +-
 .../impl/PaginationProjectsService.java       | 213 ----------------
 .../service/impl/CachedUserServiceTest.java   |   6 +-
 .../impl/PaginationProjectsServiceTest.java   |  92 -------
 .../git/eca/test/api/MockProjectsAPI.java     | 237 +++++++++++-------
 20 files changed, 330 insertions(+), 884 deletions(-)
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/models/InterestGroupData.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/api/models/Project.java
 create mode 100644 src/main/java/org/eclipsefoundation/git/eca/helper/ProjectHelper.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java
 delete mode 100644 src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java

diff --git a/Jenkinsfile b/Jenkinsfile
index 53f1a610..8216d104 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -125,7 +125,7 @@
             readTrusted 'pom.xml'
             sh 'make generate-spec'
             withCredentials([string(credentialsId: 'sonarcloud-token-git-eca-rest-api', variable: 'SONAR_TOKEN')]) {
-              sh 'mvn clean verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -B -Dsonar.login=${SONAR_TOKEN}'
+              sh 'mvn org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.login=${SONAR_TOKEN} -Dmaven.test.skip=true'
             }
             stash name: "target", includes: "target/**/*"
           }
diff --git a/pom.xml b/pom.xml
index fd2d12a4..e5500bc2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -5,7 +5,7 @@
   <artifactId>git-eca</artifactId>
   <version>1.1.0</version>
   <properties>
-    <eclipse-api-version>0.7.4</eclipse-api-version>
+    <eclipse-api-version>0.7.6-SNAPSHOT</eclipse-api-version>
     <compiler-plugin.version>3.8.1</compiler-plugin.version>
     <maven.compiler.parameters>true</maven.compiler.parameters>
     <maven.compiler.source>11</maven.compiler.source>
@@ -14,7 +14,7 @@
     <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
     <quarkus.platform.artifact-id>quarkus-universe-bom</quarkus.platform.artifact-id>
     <quarkus.platform.group-id>io.quarkus</quarkus.platform.group-id>
-    <quarkus.platform.version>2.14.2.Final</quarkus.platform.version>
+    <quarkus.platform.version>2.16.3.Final</quarkus.platform.version>
     <surefire-plugin.version>2.22.1</surefire-plugin.version>
     <auto-value.version>1.8.2</auto-value.version>
     <org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
@@ -63,6 +63,11 @@
       <artifactId>quarkus-persistence</artifactId>
       <version>${eclipse-api-version}</version>
     </dependency>
+    <dependency>
+      <groupId>org.eclipsefoundation</groupId>
+      <artifactId>quarkus-efservices</artifactId>
+      <version>${eclipse-api-version}</version>
+    </dependency>
     <dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-resteasy</artifactId>
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java b/src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java
deleted file mode 100644
index 9e951212..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/api/ProjectsAPI.java
+++ /dev/null
@@ -1,52 +0,0 @@
-/*********************************************************************
-* Copyright (c) 2022 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;
-
-import javax.enterprise.context.ApplicationScoped;
-import javax.ws.rs.BeanParam;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.Produces;
-import javax.ws.rs.core.Response;
-
-import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
-import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters;
-import org.jboss.resteasy.annotations.GZIP;
-
-/**
- * Interface for interacting with the PMI Projects API. Used to link Git
- * repos/projects with an Eclipse project to validate committer access.
- * 
- * @author Martin Lowe
- *
- */
-@ApplicationScoped
-@Path("/api")
-@RegisterRestClient
-@GZIP
-public interface ProjectsAPI {
-
-	/**
-	 * Retrieves all projects with the given repo URL.
-	 * 
-	 * @param repoUrl the target repos URL
-	 * @return a list of Eclipse Foundation projects.
-	 */
-	@GET
-	@Path("projects")
-	Response getProjects(@BeanParam BaseAPIParameters baseParams);
-
-	@GET
-	@Path("interest-groups")
-	@Produces("application/json")
-	Response getInterestGroups(@BeanParam BaseAPIParameters params);
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/InterestGroupData.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/InterestGroupData.java
deleted file mode 100644
index e70f8306..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/api/models/InterestGroupData.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.eclipsefoundation.git.eca.api.models;
-
-import java.util.List;
-
-import org.eclipsefoundation.git.eca.api.models.Project.GitlabProject;
-import org.eclipsefoundation.git.eca.api.models.Project.User;
-
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
-import com.google.auto.value.AutoValue;
-
-@AutoValue
-@JsonDeserialize(builder = AutoValue_InterestGroupData.Builder.class)
-public abstract class InterestGroupData {
-    public abstract String getId();
-    public abstract String getTitle();
-    public abstract String getLogo();
-    public abstract String getState();
-    public abstract String getProjectId();
-    public abstract Descriptor getDescription();
-    public abstract Descriptor getScope();
-    public abstract List<User> getLeads();
-    public abstract List<User> getParticipants();
-    public abstract GitlabProject getGitlab();
-
-    public static Builder builder() {
-        return new AutoValue_InterestGroupData.Builder();
-    }
-
-    @AutoValue.Builder
-    @JsonPOJOBuilder(withPrefix = "set")
-    public abstract static class Builder {
-        public abstract Builder setId(String id);
-        public abstract Builder setTitle(String title);
-        public abstract Builder setGitlab(GitlabProject gitlab);
-        public abstract Builder setLogo(String logo);
-        public abstract Builder setState(String state);
-        public abstract Builder setProjectId(String foundationdbProjectId);
-        public abstract Builder setDescription(Descriptor description);
-        public abstract Builder setScope(Descriptor scope);
-        public abstract Builder setLeads(List<User> leads);
-        public abstract Builder setParticipants(List<User> participants);
-        public abstract InterestGroupData build();
-    }
-
-    @AutoValue
-    @JsonDeserialize(builder = AutoValue_InterestGroupData_Descriptor.Builder.class)
-    public abstract static class Descriptor {
-        public abstract String getSummary();
-        public abstract String getFull();
-
-        public static Builder builder() {
-            return new AutoValue_InterestGroupData_Descriptor.Builder();
-        }
-
-        @AutoValue.Builder
-        @JsonPOJOBuilder(withPrefix = "set")
-        public abstract static class Builder {
-            public abstract Builder setSummary(String summary);
-            public abstract Builder setFull(String full);
-            public abstract Descriptor build();
-        }
-    }
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/api/models/Project.java b/src/main/java/org/eclipsefoundation/git/eca/api/models/Project.java
deleted file mode 100644
index 7b30c1be..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/api/models/Project.java
+++ /dev/null
@@ -1,194 +0,0 @@
-/*********************************************************************
-* 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 java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-import javax.annotation.Nullable;
-
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
-import com.google.auto.value.AutoValue;
-import com.google.auto.value.extension.memoized.Memoized;
-
-/**
- * Represents a project in the Eclipse API, along with the users and repos that exist within the context of the project.
- * 
- * @author Martin Lowe
- *
- */
-@AutoValue
-@JsonDeserialize(builder = $AutoValue_Project.Builder.class)
-public abstract class Project {
-    public abstract String getProjectId();
-
-    public abstract String getName();
-
-    public abstract List<User> getCommitters();
-
-    public abstract List<User> getProjectLeads();
-
-    @Nullable
-    public abstract List<Repo> getRepos();
-
-    public abstract List<Repo> getGitlabRepos();
-
-    public abstract List<Repo> getGithubRepos();
-
-    public abstract List<Repo> getGerritRepos();
-
-    public abstract Object getSpecProjectWorkingGroup();
-
-    public abstract GitlabProject getGitlab();
-
-    public abstract GithubProject getGithub();
-
-    @Nullable
-    @Memoized
-    public String getSpecWorkingGroup() {
-        // stored as map as empty returns an array instead of a map
-        Object specProjectWorkingGroup = getSpecProjectWorkingGroup();
-        if (specProjectWorkingGroup instanceof Map) {
-            // we checked in line above that the map exists, so we can safely cast it
-            @SuppressWarnings("unchecked")
-            Object raw = ((Map<Object, Object>) specProjectWorkingGroup).get("id");
-            if (raw instanceof String) {
-                return (String) raw;
-            }
-        }
-        return null;
-    }
-
-    public static Builder builder() {
-        // adds empty lists as default values
-        return new AutoValue_Project.Builder()
-                .setRepos(new ArrayList<>())
-                .setCommitters(new ArrayList<>())
-                .setGithubRepos(new ArrayList<>())
-                .setGitlabRepos(new ArrayList<>())
-                .setGerritRepos(new ArrayList<>());
-    }
-
-    @AutoValue.Builder
-    @JsonPOJOBuilder(withPrefix = "set")
-    public abstract static class Builder {
-        public abstract Builder setProjectId(String projectId);
-
-        public abstract Builder setName(String name);
-
-        public abstract Builder setProjectLeads(List<User> projectLeads);
-
-        public abstract Builder setCommitters(List<User> committers);
-
-        public abstract Builder setRepos(@Nullable List<Repo> repos);
-
-        public abstract Builder setGitlabRepos(List<Repo> gitlabRepos);
-
-        public abstract Builder setGithubRepos(List<Repo> githubRepos);
-
-        public abstract Builder setGerritRepos(List<Repo> gerritRepos);
-
-        public abstract Builder setSpecProjectWorkingGroup(Object specProjectWorkingGroup);
-
-        public abstract Builder setGitlab(GitlabProject gitlab);
-
-        public abstract Builder setGithub(GithubProject github);
-
-        public abstract Project build();
-    }
-
-    @AutoValue
-    @JsonDeserialize(builder = AutoValue_Project_User.Builder.class)
-    public abstract static class User {
-        public abstract String getUsername();
-
-        public abstract String getUrl();
-
-        public static Builder builder() {
-            return new AutoValue_Project_User.Builder();
-        }
-
-        @AutoValue.Builder
-        @JsonPOJOBuilder(withPrefix = "set")
-        public abstract static class Builder {
-            public abstract Builder setUsername(String username);
-
-            public abstract Builder setUrl(String url);
-
-            public abstract User build();
-        }
-    }
-
-    @AutoValue
-    @JsonDeserialize(builder = AutoValue_Project_GitlabProject.Builder.class)
-    public abstract static class GitlabProject {
-        public abstract String getProjectGroup();
-
-        public abstract List<String> getIgnoredSubGroups();
-
-        public static Builder builder() {
-            return new AutoValue_Project_GitlabProject.Builder();
-        }
-
-        @AutoValue.Builder
-        @JsonPOJOBuilder(withPrefix = "set")
-        public abstract static class Builder {
-            public abstract Builder setProjectGroup(String projectGroup);
-
-            public abstract Builder setIgnoredSubGroups(List<String> ignoredSubGroups);
-
-            public abstract GitlabProject build();
-        }
-    }
-
-    @AutoValue
-    @JsonDeserialize(builder = AutoValue_Project_GithubProject.Builder.class)
-    public abstract static class GithubProject {
-        public abstract String getOrg();
-
-        public abstract List<String> getIgnoredRepos();
-
-        public static Builder builder() {
-            return new AutoValue_Project_GithubProject.Builder();
-        }
-
-        @AutoValue.Builder
-        @JsonPOJOBuilder(withPrefix = "set")
-        public abstract static class Builder {
-            public abstract Builder setOrg(String org);
-
-            public abstract Builder setIgnoredRepos(List<String> ignoredRepos);
-
-            public abstract GithubProject build();
-        }
-    }
-
-    /**
-     * Does not use autovalue as the value should be mutable.
-     * 
-     * @author Martin Lowe
-     *
-     */
-    public static class Repo {
-        private String url;
-
-        public String getUrl() {
-            return this.url;
-        }
-
-        public void setUrl(String url) {
-            this.url = url;
-        }
-    }
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java
index 0650a2e8..eaa36924 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/helper/CommitHelper.java
@@ -16,7 +16,7 @@ import java.util.stream.Collectors;
 
 import javax.ws.rs.core.MultivaluedMap;
 
-import org.eclipsefoundation.git.eca.api.models.Project;
+import org.eclipsefoundation.efservices.api.models.Project;
 import org.eclipsefoundation.git.eca.model.Commit;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
diff --git a/src/main/java/org/eclipsefoundation/git/eca/helper/ProjectHelper.java b/src/main/java/org/eclipsefoundation/git/eca/helper/ProjectHelper.java
new file mode 100644
index 00000000..49c85eb2
--- /dev/null
+++ b/src/main/java/org/eclipsefoundation/git/eca/helper/ProjectHelper.java
@@ -0,0 +1,163 @@
+/**
+ * Copyright (c) 2023 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.helper;
+
+import java.net.URI;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import javax.enterprise.context.ApplicationScoped;
+import javax.inject.Inject;
+
+import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.core.service.CachingService;
+import org.eclipsefoundation.efservices.api.models.InterestGroup;
+import org.eclipsefoundation.efservices.api.models.Project;
+import org.eclipsefoundation.efservices.api.models.Project.GithubProject;
+import org.eclipsefoundation.efservices.api.models.Project.ProjectParticipant;
+import org.eclipsefoundation.efservices.services.ProjectService;
+import org.eclipsefoundation.git.eca.model.ValidationRequest;
+import org.eclipsefoundation.git.eca.namespace.ProviderType;
+import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Helps manage projects by providing filters on top of the generic service as well as operations like adapting interest
+ * groups to projects for easier processing.
+ * 
+ * @author Martin Lowe
+ *
+ */
+@ApplicationScoped
+public final class ProjectHelper {
+    private static final Logger LOGGER = LoggerFactory.getLogger(ProjectHelper.class);
+
+    @Inject
+    CachingService cache;
+    @Inject
+    ProjectService projects;
+
+    public List<Project> retrieveProjectsForRequest(ValidationRequest req) {
+        String repoUrl = req.getRepoUrl().getPath();
+        if (repoUrl == null) {
+            LOGGER.warn("Can not match null repo URL to projects");
+            return Collections.emptyList();
+        }
+        return retrieveProjectsForRepoURL(repoUrl, req.getProvider());
+    }
+
+    public List<Project> retrieveProjectsForRepoURL(String repoUrl, ProviderType provider) {
+        if (repoUrl == null) {
+            LOGGER.warn("Can not match null repo URL to projects");
+            return Collections.emptyList();
+        }
+        // check for all projects that make use of the given repo
+        List<Project> availableProjects = getProjects();
+        if (availableProjects.isEmpty()) {
+            LOGGER.warn("Could not find any projects to match against");
+            return Collections.emptyList();
+        }
+        LOGGER.debug("Checking projects for repos that end with: {}", repoUrl);
+
+        String projectNamespace = URI.create(repoUrl).getPath().substring(1).toLowerCase();
+        // filter the projects based on the repo URL. At least one repo in project must
+        // match the repo URL to be valid
+        switch (provider) {
+            case GITLAB:
+                return availableProjects
+                        .stream()
+                        .filter(p -> projectNamespace.startsWith(p.getGitlab().getProjectGroup() + "/")
+                                && p.getGitlab().getIgnoredSubGroups().stream().noneMatch(sg -> projectNamespace.startsWith(sg + "/")))
+                        .collect(Collectors.toList());
+            case GITHUB:
+                return availableProjects
+                        .stream()
+                        .filter(p -> p.getGithubRepos().stream().anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl))
+                                || (StringUtils.isNotBlank(p.getGithub().getOrg()) && projectNamespace.startsWith(p.getGithub().getOrg())
+                                        && p.getGithub().getIgnoredRepos().stream().noneMatch(repoUrl::endsWith)))
+                        .collect(Collectors.toList());
+            case GERRIT:
+                return availableProjects
+                        .stream()
+                        .filter(p -> p.getGerritRepos().stream().anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
+                        .collect(Collectors.toList());
+            default:
+                return Collections.emptyList();
+        }
+    }
+
+    /**
+     * Retrieve cached and adapted projects list to avoid having to create all interest group adaptations on every request.
+     * 
+     * @return list of available projects or empty list if none found.
+     */
+    public List<Project> getProjects() {
+        return cache.get("all-combined", new MultivaluedMapImpl<>(), Project.class, () -> {
+            List<Project> availableProjects = projects.getAllProjects();
+            availableProjects.addAll(adaptInterestGroups(projects.getAllInterestGroups()));
+            return availableProjects;
+        }).orElseGet(Collections::emptyList);
+    }
+
+    private List<Project> adaptInterestGroups(List<InterestGroup> igs) {
+        return igs
+                .stream()
+                .map(ig -> Project
+                        .builder()
+                        .setProjectId(ig.getProjectId())
+                        .setGerritRepos(Collections.emptyList())
+                        .setGithubRepos(Collections.emptyList())
+                        .setGitlabRepos(Collections.emptyList())
+                        .setGitlab(ig.getGitlab())
+                        .setCommitters(ig
+                                .getParticipants()
+                                .stream()
+                                .map(p -> ProjectParticipant
+                                        .builder()
+                                        .setFullName(p.getFullName())
+                                        .setUrl(p.getUrl())
+                                        .setUsername(p.getUsername())
+                                        .build())
+                                .collect(Collectors.toList()))
+                        .setProjectLeads(ig
+                                .getLeads()
+                                .stream()
+                                .map(p -> ProjectParticipant
+                                        .builder()
+                                        .setFullName(p.getFullName())
+                                        .setUrl(p.getUrl())
+                                        .setUsername(p.getUsername())
+                                        .build())
+                                .collect(Collectors.toList()))
+                        .setContributors(Collections.emptyList())
+                        .setShortProjectId(ig.getShortProjectId())
+                        .setSlsaLevel("")
+                        .setSummary("")
+                        .setWebsiteUrl("")
+                        .setWebsiteRepo(Collections.emptyList())
+                        .setGitlab(ig.getGitlab())
+                        .setGithub(GithubProject.builder().setOrg("").setIgnoredRepos(Collections.emptyList()).build())
+                        .setWorkingGroups(Collections.emptyList())
+                        .setIndustryCollaborations(Collections.emptyList())
+                        .setReleases(Collections.emptyList())
+                        .setTopLevelProject("")
+                        .setUrl("")
+                        .setLogo(ig.getLogo())
+                        .setTags(Collections.emptyList())
+                        .setName(ig.getTitle())
+                        .setSpecProjectWorkingGroup(Collections.emptyMap())
+                        .build())
+                .collect(Collectors.toList());
+    }
+}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
index b2bd8371..8c612938 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/StatusResource.java
@@ -27,14 +27,14 @@ import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.Response;
 
 import org.apache.commons.lang3.StringUtils;
+import org.eclipsefoundation.efservices.api.models.Project;
 import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest;
 import org.eclipsefoundation.git.eca.api.models.GithubWebhookRequest.PullRequest;
-import org.eclipsefoundation.git.eca.api.models.Project;
 import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
 import org.eclipsefoundation.git.eca.dto.GithubWebhookTracking;
+import org.eclipsefoundation.git.eca.helper.ProjectHelper;
 import org.eclipsefoundation.git.eca.model.Commit;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
-import org.eclipsefoundation.git.eca.service.ProjectsService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -52,7 +52,7 @@ public class StatusResource extends GithubAdjacentResource {
     private static final Logger LOGGER = LoggerFactory.getLogger(StatusResource.class);
 
     @Inject
-    ProjectsService projects;
+    ProjectHelper projects;
 
     // Qute templates, generates UI status page
     @Location("simple_fingerprint_ui")
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
index 90df077e..a567bcda 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ValidationResource.java
@@ -29,11 +29,10 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.git.eca.api.models.EclipseUser;
+import org.eclipsefoundation.git.eca.helper.ProjectHelper;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
-import org.eclipsefoundation.git.eca.service.InterestGroupService;
-import org.eclipsefoundation.git.eca.service.ProjectsService;
 import org.eclipsefoundation.git.eca.service.UserService;
 import org.eclipsefoundation.git.eca.service.ValidationService;
 import org.jboss.resteasy.annotations.jaxrs.QueryParam;
@@ -67,13 +66,11 @@ public class ValidationResource {
     @Inject
     CachingService cache;
     @Inject
-    ProjectsService projects;
+    ProjectHelper projects;
     @Inject
     UserService users;
     @Inject
     ValidationService validation;
-    @Inject
-    InterestGroupService ig;
 
     /**
      * Consuming a JSON request, this method will validate all passed commits, using the repo URL and the repository
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java b/src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java
deleted file mode 100644
index 8c2d182b..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/InterestGroupService.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.eclipsefoundation.git.eca.service;
-
-import java.util.List;
-
-import org.eclipsefoundation.git.eca.api.models.InterestGroupData;
-import org.eclipsefoundation.git.eca.api.models.Project;
-
-/**
- * Service for retrieving and interacting with interest groups.
- * 
- * @author Martin Lowe
- *
- */
-public interface InterestGroupService {
-
-    /**
-     * Retrieve all available interest groups.
-     * 
-     * @return list of all available interest groups
-     */
-    List<InterestGroupData> getInterestGroups();
-
-    /**
-     * Converts interest groups into projects for processing downstream.
-     * 
-     * @param igs the interest groups to convert
-     * @return the converted interest groups
-     */
-    List<Project> adaptInterestGroups(List<InterestGroupData> igs);
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java
deleted file mode 100644
index 54c2199f..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/ProjectsService.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*********************************************************************
-* 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.service;
-
-import java.util.List;
-
-import org.eclipsefoundation.git.eca.api.models.Project;
-import org.eclipsefoundation.git.eca.model.ValidationRequest;
-import org.eclipsefoundation.git.eca.namespace.ProviderType;
-
-/**
- * Intermediate layer between resource and API layers that handles retrieval of all projects and caching of that data
- * for availability purposes.
- * 
- * @author Martin Lowe
- *
- */
-public interface ProjectsService {
-
-    /**
-     * Retrieves all currently available projects from cache if available, otherwise going to API to retrieve a fresh copy
-     * of the data.
-     * 
-     * @return list of projects available from API.
-     */
-    List<Project> getProjects();
-
-    /**
-     * Retrieves projects valid for the current request, or an empty list if no data or matching project repos could be
-     * found.
-     *
-     * @param req the current request
-     * @return list of matching projects for the current request, or an empty list if none found.
-     */
-    List<Project> retrieveProjectsForRequest(ValidationRequest req);
-
-    /**
-     * Retrieves projects for given provider, using the repo URL to match to a stored repository.
-     * 
-     * @param repoUrl the repo URL to match
-     * @param provider the provider that is being served for the request.
-     * @return a list of matching projects, or an empty list if none are found.
-     */
-    List<Project> retrieveProjectsForRepoURL(String repoUrl, ProviderType provider);
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
index 06c01484..1134f1d9 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/UserService.java
@@ -13,8 +13,8 @@ package org.eclipsefoundation.git.eca.service;
 
 import java.util.List;
 
+import org.eclipsefoundation.efservices.api.models.Project;
 import org.eclipsefoundation.git.eca.api.models.EclipseUser;
-import org.eclipsefoundation.git.eca.api.models.Project;
 
 public interface UserService {
 
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
index 1f441ca0..b26a3fb8 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/ValidationService.java
@@ -16,7 +16,7 @@ import java.security.NoSuchAlgorithmException;
 import java.util.List;
 
 import org.eclipsefoundation.core.model.RequestWrapper;
-import org.eclipsefoundation.git.eca.api.models.Project;
+import org.eclipsefoundation.efservices.api.models.Project;
 import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
index 45ad5017..1d576e0d 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
@@ -14,8 +14,8 @@ package org.eclipsefoundation.git.eca.service.impl;
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
-import java.util.Optional;
 import java.util.Map.Entry;
+import java.util.Optional;
 import java.util.regex.Pattern;
 import java.util.stream.Collectors;
 
@@ -28,10 +28,10 @@ import org.apache.commons.lang3.StringUtils;
 import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
 import org.eclipsefoundation.core.service.CachingService;
+import org.eclipsefoundation.efservices.api.models.Project;
 import org.eclipsefoundation.git.eca.api.AccountsAPI;
 import org.eclipsefoundation.git.eca.api.BotsAPI;
 import org.eclipsefoundation.git.eca.api.models.EclipseUser;
-import org.eclipsefoundation.git.eca.api.models.Project;
 import org.eclipsefoundation.git.eca.service.OAuthService;
 import org.eclipsefoundation.git.eca.service.UserService;
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java
deleted file mode 100644
index c56f56da..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultInterestGroupService.java
+++ /dev/null
@@ -1,64 +0,0 @@
-package org.eclipsefoundation.git.eca.service.impl;
-
-import java.util.Collections;
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.enterprise.context.ApplicationScoped;
-import javax.inject.Inject;
-
-import org.eclipse.microprofile.rest.client.inject.RestClient;
-import org.eclipsefoundation.core.service.APIMiddleware;
-import org.eclipsefoundation.core.service.CachingService;
-import org.eclipsefoundation.git.eca.api.ProjectsAPI;
-import org.eclipsefoundation.git.eca.api.models.InterestGroupData;
-import org.eclipsefoundation.git.eca.api.models.Project;
-import org.eclipsefoundation.git.eca.service.InterestGroupService;
-import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
-
-/**
- * Default implementation of interest group service.
- * 
- * @author Martin Lowe
- */
-@ApplicationScoped
-public class DefaultInterestGroupService implements InterestGroupService {
-
-    @Inject
-    APIMiddleware middleware;
-    @Inject
-    CachingService cache;
-
-    @Inject
-    @RestClient
-    ProjectsAPI api;
-
-    @Override
-    public List<InterestGroupData> getInterestGroups() {
-        return cache
-                .get("all", new MultivaluedMapImpl<>(), InterestGroupData.class,
-                        () -> middleware.getAll(api::getInterestGroups, InterestGroupData.class))
-                .orElse(Collections.emptyList());
-    }
-
-    @Override
-    public List<Project> adaptInterestGroups(List<InterestGroupData> igs) {
-        return igs
-                .stream()
-                .map(ig -> Project
-                        .builder()
-                        .setProjectId(ig.getProjectId())
-                        .setGerritRepos(Collections.emptyList())
-                        .setGithubRepos(Collections.emptyList())
-                        .setGitlabRepos(Collections.emptyList())
-                        .setRepos(Collections.emptyList())
-                        .setGitlab(ig.getGitlab())
-                        .setCommitters(ig.getParticipants())
-                        .setProjectLeads(ig.getLeads())
-                        .setName(ig.getTitle())
-                        .setSpecProjectWorkingGroup(Collections.emptyMap())
-                        .build())
-                .collect(Collectors.toList());
-    }
-
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
index 647252e0..7c45cd78 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultValidationService.java
@@ -28,20 +28,19 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipsefoundation.core.helper.DateTimeHelper;
 import org.eclipsefoundation.core.model.RequestWrapper;
 import org.eclipsefoundation.core.service.CachingService;
+import org.eclipsefoundation.efservices.api.models.Project;
 import org.eclipsefoundation.git.eca.api.models.EclipseUser;
-import org.eclipsefoundation.git.eca.api.models.Project;
 import org.eclipsefoundation.git.eca.dto.CommitValidationMessage;
 import org.eclipsefoundation.git.eca.dto.CommitValidationStatus;
 import org.eclipsefoundation.git.eca.dto.CommitValidationStatusGrouping;
 import org.eclipsefoundation.git.eca.helper.CommitHelper;
+import org.eclipsefoundation.git.eca.helper.ProjectHelper;
 import org.eclipsefoundation.git.eca.model.Commit;
 import org.eclipsefoundation.git.eca.model.GitUser;
 import org.eclipsefoundation.git.eca.model.ValidationRequest;
 import org.eclipsefoundation.git.eca.model.ValidationResponse;
 import org.eclipsefoundation.git.eca.namespace.APIStatusCode;
 import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
-import org.eclipsefoundation.git.eca.service.InterestGroupService;
-import org.eclipsefoundation.git.eca.service.ProjectsService;
 import org.eclipsefoundation.git.eca.service.UserService;
 import org.eclipsefoundation.git.eca.service.ValidationService;
 import org.eclipsefoundation.persistence.dao.PersistenceDao;
@@ -66,9 +65,7 @@ public class DefaultValidationService implements ValidationService {
     List<String> allowListUsers;
 
     @Inject
-    ProjectsService projects;
-    @Inject
-    InterestGroupService ig;
+    ProjectHelper projects;
     @Inject
     UserService users;
 
@@ -386,7 +383,7 @@ public class DefaultValidationService implements ValidationService {
             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
                 // commit to specs
-                if (p.getSpecWorkingGroup() != null && !user.getECA().getCanContributeSpecProject()) {
+                if (p.getSpecWorkingGroup().isPresent() && !user.getECA().getCanContributeSpecProject()) {
                     // set error + update response status
                     r
                             .addError(hash, String
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java
deleted file mode 100644
index 9b48cac9..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsService.java
+++ /dev/null
@@ -1,213 +0,0 @@
-/*********************************************************************
-* 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>
-*	      Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
-*
-* SPDX-License-Identifier: EPL-2.0
-**********************************************************************/
-package org.eclipsefoundation.git.eca.service.impl;
-
-import java.net.URI;
-import java.util.Collections;
-import java.util.List;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
-
-import javax.annotation.PostConstruct;
-import javax.enterprise.context.ApplicationScoped;
-import javax.inject.Inject;
-
-import org.apache.commons.lang3.StringUtils;
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipse.microprofile.context.ManagedExecutor;
-import org.eclipse.microprofile.rest.client.inject.RestClient;
-import org.eclipsefoundation.core.service.APIMiddleware;
-import org.eclipsefoundation.core.service.CachingService;
-import org.eclipsefoundation.git.eca.api.ProjectsAPI;
-import org.eclipsefoundation.git.eca.api.models.Project;
-import org.eclipsefoundation.git.eca.model.ValidationRequest;
-import org.eclipsefoundation.git.eca.namespace.ProviderType;
-import org.eclipsefoundation.git.eca.service.InterestGroupService;
-import org.eclipsefoundation.git.eca.service.ProjectsService;
-import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.google.common.cache.CacheBuilder;
-import com.google.common.cache.CacheLoader;
-import com.google.common.cache.LoadingCache;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListenableFutureTask;
-
-import io.quarkus.runtime.Startup;
-
-/**
- * Projects service implementation that handles pagination of data manually, as well as makes use of a loading cache to
- * have data be always available with as little latency to the user as possible.
- *
- * @author Martin Lowe
- * @author Zachary Sabourin
- */
-@Startup
-@ApplicationScoped
-public class PaginationProjectsService implements ProjectsService {
-    private static final Logger LOGGER = LoggerFactory.getLogger(PaginationProjectsService.class);
-
-    @ConfigProperty(name = "eclipse.projects.precache.enabled", defaultValue = "true")
-    boolean isEnabled;
-    @ConfigProperty(name = "cache.pagination.refresh-frequency-seconds", defaultValue = "3600")
-    long refreshAfterWrite;
-
-    @Inject
-    @RestClient
-    ProjectsAPI projects;
-    @Inject
-    InterestGroupService ig;
-    @Inject
-    CachingService cache;
-    @Inject
-    APIMiddleware middleware;
-
-    @Inject
-    ManagedExecutor exec;
-    // this class has a separate cache as this data is long to load and should be
-    // always available.
-    LoadingCache<String, List<Project>> internalCache;
-
-    /**
-     * Initializes the internal loader cache and pre-populates the data with the one available key. If more than one key is
-     * used, eviction of previous results will happen and create degraded performance.
-     */
-    @PostConstruct
-    public void init() {
-        // set up the internal cache
-        this.internalCache = CacheBuilder
-                .newBuilder()
-                .maximumSize(1)
-                .refreshAfterWrite(refreshAfterWrite, TimeUnit.SECONDS)
-                .build(new CacheLoader<String, List<Project>>() {
-                    @Override
-                    public List<Project> load(String key) throws Exception {
-                        return getProjectsInternal();
-                    }
-
-                    /**
-                     * Implementation required for refreshAfterRewrite to be async rather than sync and blocking while awaiting for
-                     * expensive reload to complete.
-                     */
-                    @Override
-                    public ListenableFuture<List<Project>> reload(String key, List<Project> oldValue) throws Exception {
-                        ListenableFutureTask<List<Project>> task = ListenableFutureTask.create(() -> {
-                            LOGGER.debug("Retrieving new project data async");
-                            List<Project> newProjects = oldValue;
-                            try {
-                                newProjects = getProjectsInternal();
-                            } catch (Exception e) {
-                                LOGGER.error("Error while reloading internal projects data, data will be stale for current cycle.", e);
-                            }
-                            LOGGER.debug("Done refreshing project values");
-                            return newProjects;
-                        });
-                        // run the task using the Quarkus managed executor
-                        exec.execute(task);
-                        return task;
-                    }
-                });
-
-        if (isEnabled) {
-            // pre-cache the projects to reduce load time for other users
-            LOGGER.debug("Starting pre-cache of projects");
-            if (getProjects() == null) {
-                LOGGER.warn("Unable to populate pre-cache for Eclipse projects. Calls may experience degraded performance.");
-            }
-            LOGGER.debug("Completed pre-cache of projects assets");
-        }
-    }
-
-    @Override
-    public List<Project> getProjects() {
-        try {
-            return internalCache.get("projects");
-        } catch (ExecutionException e) {
-            throw new RuntimeException("Could not load Eclipse projects", e);
-        }
-    }
-
-    @Override
-    public List<Project> retrieveProjectsForRequest(ValidationRequest req) {
-        String repoUrl = req.getRepoUrl().getPath();
-        if (repoUrl == null) {
-            LOGGER.warn("Can not match null repo URL to projects");
-            return Collections.emptyList();
-        }
-        return retrieveProjectsForRepoURL(repoUrl, req.getProvider());
-    }
-
-    @Override
-    public List<Project> retrieveProjectsForRepoURL(String repoUrl, ProviderType provider) {
-        if (repoUrl == null) {
-            LOGGER.warn("Can not match null repo URL to projects");
-            return Collections.emptyList();
-        }
-        // check for all projects that make use of the given repo
-        List<Project> availableProjects = getProjects();
-        availableProjects
-                .addAll(cache
-                        .get("all", new MultivaluedMapImpl<>(), Project.class, () -> ig.adaptInterestGroups(ig.getInterestGroups()))
-                        .orElse(Collections.emptyList()));
-        if (availableProjects.isEmpty()) {
-            LOGGER.warn("Could not find any projects to match against");
-            return Collections.emptyList();
-        }
-        LOGGER.debug("Checking projects for repos that end with: {}", repoUrl);
-
-        String projectNamespace = URI.create(repoUrl).getPath().substring(1).toLowerCase();
-        // filter the projects based on the repo URL. At least one repo in project must
-        // match the repo URL to be valid
-        switch (provider) {
-            case GITLAB:
-                return availableProjects
-                        .stream()
-                        .filter(p -> projectNamespace.startsWith(p.getGitlab().getProjectGroup() + "/")
-                                && p.getGitlab().getIgnoredSubGroups().stream().noneMatch(sg -> projectNamespace.startsWith(sg + "/")))
-                        .collect(Collectors.toList());
-            case GITHUB:
-                return availableProjects
-                        .stream()
-                        .filter(p -> p.getGithubRepos().stream().anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl))
-                                || (StringUtils.isNotBlank(p.getGithub().getOrg()) && projectNamespace.startsWith(p.getGithub().getOrg())
-                                        && p.getGithub().getIgnoredRepos().stream().noneMatch(repoUrl::endsWith)))
-                        .collect(Collectors.toList());
-            case GERRIT:
-                return availableProjects
-                        .stream()
-                        .filter(p -> p.getGerritRepos().stream().anyMatch(re -> re.getUrl() != null && re.getUrl().endsWith(repoUrl)))
-                        .collect(Collectors.toList());
-            default:
-                return Collections.emptyList();
-        }
-    }
-
-    /**
-     * Logic for retrieving projects from API.
-     *
-     * @return list of projects for the cache
-     */
-    private List<Project> getProjectsInternal() {
-
-        return middleware.getAll(params -> projects.getProjects(params), Project.class).stream().map(proj -> {
-            proj.getGerritRepos().forEach(repo -> {
-                if (repo.getUrl().endsWith(".git")) {
-                    repo.setUrl(repo.getUrl().substring(0, repo.getUrl().length() - 4));
-                }
-            });
-            return proj;
-        }).collect(Collectors.toList());
-    }
-}
diff --git a/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java b/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java
index 231b401c..b0c25a00 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/service/impl/CachedUserServiceTest.java
@@ -18,9 +18,9 @@ import java.util.Optional;
 
 import javax.inject.Inject;
 
+import org.eclipsefoundation.efservices.api.models.Project;
 import org.eclipsefoundation.git.eca.api.models.EclipseUser;
-import org.eclipsefoundation.git.eca.api.models.Project;
-import org.eclipsefoundation.git.eca.service.ProjectsService;
+import org.eclipsefoundation.git.eca.helper.ProjectHelper;
 import org.eclipsefoundation.git.eca.service.UserService;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
@@ -40,7 +40,7 @@ class CachedUserServiceTest {
     @Inject
     UserService users;
     @Inject
-    ProjectsService projects;
+    ProjectHelper projects;
 
     @Test
     void getUser_success() {
diff --git a/src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java b/src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java
deleted file mode 100644
index 3ebbabce..00000000
--- a/src/test/java/org/eclipsefoundation/git/eca/service/impl/PaginationProjectsServiceTest.java
+++ /dev/null
@@ -1,92 +0,0 @@
-/*********************************************************************
-* 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.service.impl;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.inject.Inject;
-
-import org.eclipsefoundation.git.eca.api.models.Project;
-import org.eclipsefoundation.git.eca.api.models.Project.Repo;
-import org.eclipsefoundation.git.eca.service.ProjectsService;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Test;
-
-import io.quarkus.test.junit.QuarkusTest;
-
-@QuarkusTest
-class PaginationProjectsServiceTest {
-  // get the projects service
-  @Inject
-  ProjectsService ps;
-
-  @Test
-  void validateGerritUrlScrubbed() {
-    // get all projects
-    List<Project> projectsAll = ps.getProjects();
-    // get projects that have gerrit repos
-    List<Project> projs = projectsAll
-        .stream()
-        .filter(p -> !p.getGerritRepos().isEmpty())
-        .collect(Collectors.toList());
-    // for all repos, check that none end with .git (this doesn't account for
-    // .git.git, but I assume that would be an entry error)
-    for (Project p : projs) {
-      for (Repo r : p.getGerritRepos()) {
-        Assertions.assertFalse(r.getUrl().endsWith(".git"), "Expected no URLs to end with '.git'");
-      }
-    }
-  }
-
-  @Test
-  void validateGithubUrlNotScrubbed() {
-    // get all projects
-    List<Project> projectsAll = ps.getProjects();
-    // get projects that have github repos
-    List<Project> projs = projectsAll
-        .stream()
-        .filter(p -> !p.getGithubRepos().isEmpty())
-        .collect(Collectors.toList());
-    // for all repos, check that at least one ends with .git
-    boolean foundGitSuffix = false;
-    for (Project p : projs) {
-      for (Repo r : p.getGithubRepos()) {
-        if (r.getUrl().endsWith(".git")) {
-          foundGitSuffix = true;
-        }
-      }
-    }
-    Assertions.assertTrue(foundGitSuffix, "Expected a URL to end with '.git'");
-  }
-
-  @Test
-  void validateGitlabUrlNotScrubbed() {
-    // get all projects
-    List<Project> projectsAll = ps.getProjects();
-    // get projects that have gitlab repos
-    List<Project> projs = projectsAll
-        .stream()
-        .filter(p -> !p.getGitlabRepos().isEmpty())
-        .collect(Collectors.toList());
-    // for all repos, check that at least one ends with .git
-    boolean foundGitSuffix = false;
-    for (Project p : projs) {
-      for (Repo r : p.getGitlabRepos()) {
-        if (r.getUrl().endsWith(".git")) {
-          foundGitSuffix = true;
-        }
-      }
-    }
-    Assertions.assertTrue(foundGitSuffix, "Expected a URL to end with '.git'");
-  }
-}
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java
index 0528b1ea..4be050fc 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/api/MockProjectsAPI.java
@@ -1,11 +1,11 @@
 /*********************************************************************
-* Copyright (c) 2020 Eclipse Foundation.
+* Copyright (c) 2023 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>
+* Author: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
 *
 * SPDX-License-Identifier: EPL-2.0
 **********************************************************************/
@@ -14,24 +14,26 @@ package org.eclipsefoundation.git.eca.test.api;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
-import javax.annotation.PostConstruct;
 import javax.enterprise.context.ApplicationScoped;
 import javax.ws.rs.core.Response;
 
 import org.eclipse.microprofile.rest.client.inject.RestClient;
 import org.eclipsefoundation.core.service.APIMiddleware.BaseAPIParameters;
-import org.eclipsefoundation.git.eca.api.ProjectsAPI;
-import org.eclipsefoundation.git.eca.api.models.InterestGroupData;
-import org.eclipsefoundation.git.eca.api.models.Project;
-import org.eclipsefoundation.git.eca.api.models.InterestGroupData.Descriptor;
-import org.eclipsefoundation.git.eca.api.models.Project.GithubProject;
-import org.eclipsefoundation.git.eca.api.models.Project.GitlabProject;
-import org.eclipsefoundation.git.eca.api.models.Project.Repo;
-import org.eclipsefoundation.git.eca.api.models.Project.User;
+import org.eclipsefoundation.efservices.api.ProjectsAPI;
+import org.eclipsefoundation.efservices.api.models.GitlabProject;
+import org.eclipsefoundation.efservices.api.models.InterestGroup;
+import org.eclipsefoundation.efservices.api.models.InterestGroup.Descriptor;
+import org.eclipsefoundation.efservices.api.models.InterestGroup.InterestGroupParticipant;
+import org.eclipsefoundation.efservices.api.models.InterestGroup.Organization;
+import org.eclipsefoundation.efservices.api.models.InterestGroup.Resource;
+import org.eclipsefoundation.efservices.api.models.Project;
+import org.eclipsefoundation.efservices.api.models.Project.GithubProject;
+import org.eclipsefoundation.efservices.api.models.Project.ProjectParticipant;
+import org.eclipsefoundation.efservices.api.models.Project.Repo;
 
 import io.quarkus.test.Mock;
 
@@ -40,124 +42,169 @@ import io.quarkus.test.Mock;
 @ApplicationScoped
 public class MockProjectsAPI implements ProjectsAPI {
 
-    private List<Project> src;
+    private List<Project> projects;
 
-    @PostConstruct
-    public void build() {
-        this.src = new ArrayList<>();
+    public MockProjectsAPI() {
+        this.projects = new ArrayList<>();
 
         // sample repos
-        Repo r1 = new Repo();
-        r1.setUrl("http://www.github.com/eclipsefdn/sample");
-        Repo r2 = new Repo();
-        r2.setUrl("http://www.github.com/eclipsefdn/test");
-        Repo r3 = new Repo();
-        r3.setUrl("http://www.github.com/eclipsefdn/prototype.git");
-        Repo r5 = new Repo();
-        r5.setUrl("/gitroot/sample/gerrit.project.git");
-        Repo r6 = new Repo();
-        r6.setUrl("/gitroot/sample/gerrit.other-project");
-        Repo r7 = new Repo();
-        r7.setUrl("https://gitlab.eclipse.org/eclipse/dash/dash.git");
-        Repo r8 = new Repo();
-        r8.setUrl("https://gitlab.eclipse.org/eclipse/dash-second/dash.handbook.test");
-
-        // sample users, correlates to users in Mock projects API
-        User u1 = User.builder().setUrl("").setUsername("da_wizz").build();
-        User u2 = User.builder().setUrl("").setUsername("grunter").build();
-
-        // projects
-        Project p1 = Project
-                .builder()
+        Repo r1 = Repo.builder().setUrl("http://www.github.com/eclipsefdn/sample").build();
+        Repo r2 = Repo.builder().setUrl("http://www.github.com/eclipsefdn/test").build();
+        Repo r3 = Repo.builder().setUrl("http://www.github.com/eclipsefdn/prototype.git").build();
+        Repo r4 = Repo.builder().setUrl("http://www.github.com/eclipsefdn/tck-proto").build();
+        Repo r5 = Repo.builder().setUrl("/gitroot/sample/gerrit.project.git").build();
+        Repo r6 = Repo.builder().setUrl("/gitroot/sample/gerrit.other-project").build();
+        Repo r7 = Repo.builder().setUrl("https://gitlab.eclipse.org/eclipse/dash/dash.git").build();
+        Repo r8 = Repo.builder().setUrl("https://gitlab.eclipse.org/eclipse/dash-second/dash.handbook.test").build();
+
+        // sample users
+        ProjectParticipant u1 = ProjectParticipant.builder().setUrl("").setUsername("da_wizz").setFullName("da_wizz")
+                .build();
+        ProjectParticipant u2 = ProjectParticipant.builder().setUrl("").setUsername("grunter").setFullName("grunter")
+                .build();
+
+        this.projects.add(Project.builder()
                 .setName("Sample project")
                 .setProjectId("sample.proj")
-                .setSpecProjectWorkingGroup(Collections.emptyList())
+                .setSpecProjectWorkingGroup(Collections.emptyMap())
                 .setGithubRepos(Arrays.asList(r1, r2))
                 .setGerritRepos(Arrays.asList(r5))
                 .setCommitters(Arrays.asList(u1, u2))
                 .setProjectLeads(Collections.emptyList())
-                .setGitlab(GitlabProject.builder().setIgnoredSubGroups(Collections.emptyList()).setProjectGroup("").build())
-                .setGithub(GithubProject.builder().setIgnoredRepos(Collections.emptyList()).setOrg("").build())
-                .build();
-        src.add(p1);
+                .setGitlab(GitlabProject.builder().setIgnoredSubGroups(Collections.emptyList()).setProjectGroup("")
+                        .build())
+                .setShortProjectId("sample.proj")
+                .setSummary("summary")
+                .setUrl("project.url.com")
+                .setWebsiteUrl("someproject.com")
+                .setWebsiteRepo(Collections.emptyList())
+                .setLogo("logoUrl.com")
+                .setTags(Collections.emptyList())
+                .setGithub(GithubProject.builder()
+                        .setOrg("")
+                        .setIgnoredRepos(Collections.emptyList()).build())
+                .setGitlabRepos(Collections.emptyList())
+                .setContributors(Collections.emptyList())
+                .setWorkingGroups(Collections.emptyList())
+                .setIndustryCollaborations(Collections.emptyList())
+                .setReleases(Collections.emptyList())
+                .setTopLevelProject("eclipse")
+                .setSlsaLevel("1")
+                .build());
 
-        Project p2 = Project
-                .builder()
+        this.projects.add(Project.builder()
                 .setName("Prototype thing")
                 .setProjectId("sample.proto")
-                .setSpecProjectWorkingGroup(Collections.emptyList())
+                .setSpecProjectWorkingGroup(Collections.emptyMap())
                 .setGithubRepos(Arrays.asList(r3))
                 .setGerritRepos(Arrays.asList(r6))
                 .setGitlabRepos(Arrays.asList(r8))
                 .setCommitters(Arrays.asList(u2))
                 .setProjectLeads(Collections.emptyList())
                 .setGitlab(
-                        GitlabProject.builder().setIgnoredSubGroups(Collections.emptyList()).setProjectGroup("eclipse/dash-second").build())
-                .setGithub(GithubProject.builder().setIgnoredRepos(Collections.emptyList()).setOrg("").build())
-                .build();
-        src.add(p2);
+                        GitlabProject.builder().setIgnoredSubGroups(Collections.emptyList())
+                                .setProjectGroup("eclipse/dash-second").build())
+                .setShortProjectId("sample.proto")
+                .setWebsiteUrl("someproject.com")
+                .setSummary("summary")
+                .setUrl("project.url.com")
+                .setWebsiteRepo(Collections.emptyList())
+                .setLogo("logoUrl.com")
+                .setTags(Collections.emptyList())
+                .setGithub(GithubProject.builder()
+                        .setOrg("")
+                        .setIgnoredRepos(Collections.emptyList()).build())
+                .setContributors(Collections.emptyList())
+                .setWorkingGroups(Collections.emptyList())
+                .setIndustryCollaborations(Collections.emptyList())
+                .setReleases(Collections.emptyList())
+                .setTopLevelProject("eclipse")
+                .setSlsaLevel("1")
+                .build());
 
-        Map<String, String> map = new HashMap<>();
-        map.put("id", "proj1");
-        Project p3 = Project
-                .builder()
+        this.projects.add(Project.builder()
                 .setName("Spec project")
                 .setProjectId("spec.proj")
-                .setSpecProjectWorkingGroup(map)
-                .setGithubRepos(Collections.emptyList())
+                .setSpecProjectWorkingGroup(Map.of("id", "proj1", "name", "proj1"))
+                .setGithubRepos(Arrays.asList(r4))
                 .setGitlabRepos(Arrays.asList(r7))
+                .setGerritRepos(Collections.emptyList())
                 .setGitlab(GitlabProject
                         .builder()
                         .setIgnoredSubGroups(Arrays.asList("eclipse/dash/mirror"))
                         .setProjectGroup("eclipse/dash")
                         .build())
-                .setGithub(GithubProject
-                        .builder()
-                        .setIgnoredRepos(Arrays.asList("eclipsefdn-tck/tck-ignored"))
-                        .setOrg("eclipsefdn-tck")
-                        .build())
                 .setCommitters(Arrays.asList(u1, u2))
                 .setProjectLeads(Collections.emptyList())
-                .build();
-        src.add(p3);
+                .setShortProjectId("spec.proj")
+                .setSummary("summary")
+                .setUrl("project.url.com")
+                .setWebsiteUrl("someproject.com")
+                .setWebsiteRepo(Collections.emptyList())
+                .setLogo("logoUrl.com")
+                .setTags(Collections.emptyList())
+                .setGithub(GithubProject.builder()
+                        .setOrg("eclipsefdn-tck")
+                        .setIgnoredRepos(Arrays.asList("eclipsefdn-tck/tck-ignored")).build())
+                .setContributors(Collections.emptyList())
+                .setWorkingGroups(Collections.emptyList())
+                .setIndustryCollaborations(Collections.emptyList())
+                .setReleases(Collections.emptyList())
+                .setTopLevelProject("eclipse")
+                .setSlsaLevel("1")
+                .build());
     }
 
     @Override
-    public Response getProjects(BaseAPIParameters baseParams) {
-        return Response.ok(baseParams.getPage() == 1 ? new ArrayList<>(src) : Collections.emptyList()).build();
+    public Response getProjects(BaseAPIParameters params, int isSpecProject) {
+
+        if (isSpecProject == 1) {
+            return Response
+                    .ok(projects.stream().filter(p -> p.getSpecWorkingGroup().isPresent()).collect(Collectors.toList()))
+                    .build();
+        }
+
+        return Response.ok(projects).build();
     }
 
     @Override
     public Response getInterestGroups(BaseAPIParameters params) {
-        return Response
-                .ok(Arrays
-                        .asList(InterestGroupData
-                                .builder()
-                                .setProjectId("foundation-internal.ig.mittens")
-                                .setId("1")
-                                .setLogo("")
-                                .setState("active")
-                                .setTitle("Magical IG Tributed To Eclipse News Sources")
-                                .setDescription(Descriptor.builder().setFull("Sample").setSummary("Sample").build())
-                                .setScope(Descriptor.builder().setFull("Sample").setSummary("Sample").build())
-                                .setGitlab(GitlabProject
-                                        .builder()
-                                        .setIgnoredSubGroups(Collections.emptyList())
-                                        .setProjectGroup("eclipse-ig/mittens")
-                                        .build())
-                                .setLeads(Arrays
-                                        .asList(User
-                                                .builder()
-                                                .setUrl("https://api.eclipse.org/account/profile/zacharysabourin")
-                                                .setUsername("zacharysabourin")
-                                                .build()))
-                                .setParticipants(Arrays
-                                        .asList(User
-                                                .builder()
-                                                .setUrl("https://api.eclipse.org/account/profile/skilpatrick")
-                                                .setUsername("skilpatrick")
-                                                .build()))
-                                .build()))
-                .build();
+        return Response.ok(Arrays.asList(InterestGroup
+                .builder()
+                .setProjectId("foundation-internal.ig.mittens")
+                .setId("1")
+                .setLogo("")
+                .setState("active")
+                .setTitle("Magical IG Tributed To Eclipse News Sources")
+                .setDescription(Descriptor.builder().setFull("Sample").setSummary("Sample").build())
+                .setScope(Descriptor.builder().setFull("Sample").setSummary("Sample").build())
+                .setGitlab(GitlabProject.builder()
+                        .setIgnoredSubGroups(Collections.emptyList())
+                        .setProjectGroup("eclipse-ig/mittens")
+                        .build())
+                .setLeads(Arrays.asList(InterestGroupParticipant
+                        .builder()
+                        .setUrl("https://api.eclipse.org/account/profile/zacharysabourin")
+                        .setUsername("zacharysabourin")
+                        .setFullName("zachary sabourin")
+                        .setOrganization(Organization.builder()
+                                .setDocuments(Collections.emptyMap())
+                                .setId("id")
+                                .setName("org").build())
+                        .build()))
+                .setParticipants(Arrays.asList(InterestGroupParticipant
+                        .builder()
+                        .setUrl("https://api.eclipse.org/account/profile/skilpatrick")
+                        .setUsername("skilpatrick")
+                        .setFullName("Skil Patrick")
+                        .setOrganization(Organization.builder()
+                                .setDocuments(Collections.emptyMap())
+                                .setId("id")
+                                .setName("org").build())
+                        .build()))
+                .setShortProjectId("mittens")
+                .setResources(Resource.builder().setMembers("members").setWebsite("google.com").build())
+                .setMailingList("mailinglist.com")
+                .build())).build();
     }
 }
-- 
GitLab


From f0517bcf80e2225cb5c085c8135a6f89e37f0224 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 9 Jun 2023 13:38:51 -0400
Subject: [PATCH 3/5] Iss #132 - Replace auth token code with commons version

As there is a drupal auth token code that will handle creating auth
tokens for non-standard flows, we can now remove the code and use the
version in the lib.

In the future, some of the user service will be replaced, but most of it
can't be yet due to missing fields for Github account lookups, so it
will be left alone for stability.
---
 pom.xml                                       | 14 ---
 .../git/eca/oauth/EclipseApi.java             | 49 ----------
 .../git/eca/service/OAuthService.java         | 33 -------
 .../eca/service/impl/CachedUserService.java   |  4 +-
 .../eca/service/impl/DefaultOAuthService.java | 93 -------------------
 ...AuthService.java => MockTokenService.java} |  4 +-
 6 files changed, 4 insertions(+), 193 deletions(-)
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
 delete mode 100644 src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
 rename src/test/java/org/eclipsefoundation/git/eca/test/service/impl/{MockOAuthService.java => MockTokenService.java} (83%)

diff --git a/pom.xml b/pom.xml
index e5500bc2..0905d4ab 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,6 @@
     <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
     <sonar.coverage.jacoco.xmlReportPaths>${project.basedir}/target/jacoco-report/jacoco.xml</sonar.coverage.jacoco.xmlReportPaths>
     <sonar.junit.reportPath>${project.build.directory}/surefire-reports</sonar.junit.reportPath>
-    <sonar.host.url>https://sonarcloud.io</sonar.host.url>
     <sonar.organization>eclipse-foundation-it</sonar.organization>
     <sonar.projectKey>eclipse-foundation-it_git-eca-rest-api</sonar.projectKey>
     <sonar.projectName>Git ECA REST API</sonar.projectName>
@@ -92,7 +91,6 @@
     <dependency>
       <groupId>org.bouncycastle</groupId>
       <artifactId>bcpkix-jdk15on</artifactId>
-      <version>1.70</version>
     </dependency>
 
     <dependency>
@@ -134,18 +132,6 @@
       <scope>provided</scope>
     </dependency>
 
-    <!-- Third-party reqs -->
-    <dependency>
-      <groupId>com.github.scribejava</groupId>
-      <artifactId>scribejava-apis</artifactId>
-      <version>6.4.1</version>
-    </dependency>
-    <!-- Caching -->
-    <dependency>
-      <groupId>com.google.guava</groupId>
-      <artifactId>guava</artifactId>
-    </dependency>
-
     <!-- Test requirements -->
     <dependency>
       <groupId>io.quarkus</groupId>
diff --git a/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java b/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
deleted file mode 100644
index b89a3703..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/oauth/EclipseApi.java
+++ /dev/null
@@ -1,49 +0,0 @@
-/*********************************************************************
-* 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.oauth;
-
-import com.github.scribejava.core.builder.api.DefaultApi20;
-import com.github.scribejava.core.oauth2.clientauthentication.ClientAuthentication;
-import com.github.scribejava.core.oauth2.clientauthentication.RequestBodyAuthenticationScheme;
-
-/**
- * Wrapper around the OAuth API for Scribejava. Enables OAuth2.0 binding to the
- * Eclipse Foundation OAuth server.
- * 
- * @author Martin Lowe
- *
- */
-public class EclipseApi extends DefaultApi20 {
-
-    @Override
-    public String getAccessTokenEndpoint() {
-        return "https://accounts.eclipse.org/oauth2/token";
-    }
-
-    @Override
-    protected String getAuthorizationBaseUrl() {
-        return null;
-    }
-
-    @Override
-    public ClientAuthentication getClientAuthentication() {
-        return RequestBodyAuthenticationScheme.instance();
-    }
-
-    private static class InstanceHolder {
-        private static final EclipseApi INSTANCE = new EclipseApi();
-    }
-
-    public static EclipseApi instance() {
-        return InstanceHolder.INSTANCE;
-    }
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java b/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
deleted file mode 100644
index 144601ae..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/OAuthService.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*********************************************************************
-* 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.service;
-
-/**
- * Used to generate OAuth tokens for use with internal services rather than
- * bolted on introspection. This is required over the (now deprecated) Elytron
- * plugin or the OIDC plugin as those plugins work with requests to validate
- * incoming rather than outgoing requests.
- * 
- * @author Martin Lowe
- *
- */
-public interface OAuthService {
-
-    /**
-     * Retrieve an access token for the service from the Eclipse API for internal
-     * usage.
-     * 
-     * @return current access token, or null if none could be retrieved for current
-     *         API credentials/settings.
-     */
-    String getToken();
-}
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
index 1d576e0d..de6836a8 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/service/impl/CachedUserService.java
@@ -29,10 +29,10 @@ import org.eclipse.microprofile.config.inject.ConfigProperty;
 import org.eclipse.microprofile.rest.client.inject.RestClient;
 import org.eclipsefoundation.core.service.CachingService;
 import org.eclipsefoundation.efservices.api.models.Project;
+import org.eclipsefoundation.efservices.services.DrupalTokenService;
 import org.eclipsefoundation.git.eca.api.AccountsAPI;
 import org.eclipsefoundation.git.eca.api.BotsAPI;
 import org.eclipsefoundation.git.eca.api.models.EclipseUser;
-import org.eclipsefoundation.git.eca.service.OAuthService;
 import org.eclipsefoundation.git.eca.service.UserService;
 import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
 import org.slf4j.Logger;
@@ -63,7 +63,7 @@ public class CachedUserService implements UserService {
     BotsAPI bots;
 
     @Inject
-    OAuthService oauth;
+    DrupalTokenService oauth;
     @Inject
     CachingService cache;
 
diff --git a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java b/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
deleted file mode 100644
index 713b0988..00000000
--- a/src/main/java/org/eclipsefoundation/git/eca/service/impl/DefaultOAuthService.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*********************************************************************
-* 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.service.impl;
-
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.PostConstruct;
-import javax.enterprise.context.ApplicationScoped;
-
-import org.eclipse.microprofile.config.inject.ConfigProperty;
-import org.eclipsefoundation.git.eca.oauth.EclipseApi;
-import org.eclipsefoundation.git.eca.service.OAuthService;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import com.github.scribejava.core.builder.ServiceBuilder;
-import com.github.scribejava.core.model.OAuth2AccessToken;
-import com.github.scribejava.core.oauth.OAuth20Service;
-
-/**
- * Default implementation for requesting an OAuth request token. The reason that
- * this class is implemented over the other implementations baked into Quarkus
- * is to better bind to the Drupal OAuth APIs.
- * 
- * @author Martin Lowe
- *
- */
-@ApplicationScoped
-public class DefaultOAuthService implements OAuthService {
-    private static final Logger LOGGER = LoggerFactory.getLogger(DefaultOAuthService.class);
-
-    @ConfigProperty(name = "oauth2.client-id")
-    String id;
-    @ConfigProperty(name = "oauth2.client-secret")
-    String secret;
-    @ConfigProperty(name = "oauth2.scope")
-    String scope;
-
-    // service reference (as we only need one)
-    private OAuth20Service service;
-
-    // token state vars
-    private long expirationTime;
-    private String accessToken;
-
-    /**
-     * Create an OAuth service reference.
-     */
-    @PostConstruct
-    void createServiceRef() {
-        this.service = new ServiceBuilder(id).apiSecret(secret).scope(scope).build(EclipseApi.instance());
-    }
-
-    @Override
-    public String getToken() {
-        // lock on the class instance to stop multiple threads from requesting new
-        // tokens at the same time
-        synchronized (this) {
-            if (accessToken == null || System.currentTimeMillis() >= expirationTime) {
-                // clear access token
-                this.accessToken = null;
-                try {
-                    OAuth2AccessToken requestToken = service.getAccessTokenClientCredentialsGrant();
-                    if (requestToken != null) {
-                        this.accessToken = requestToken.getAccessToken();
-                        this.expirationTime = System.currentTimeMillis()
-                                + TimeUnit.SECONDS.toMillis(requestToken.getExpiresIn().longValue());
-                    }
-                } catch (IOException e) {
-                    LOGGER.error("Issue communicating with OAuth server for authentication", e);
-                } catch (InterruptedException e) {
-                    LOGGER.error("Authentication communication was interrupted before completion", e);
-                    Thread.currentThread().interrupt();
-                } catch (ExecutionException e) {
-                    LOGGER.error("Error while retrieving access token for request", e);
-                }
-            }
-        }
-        return accessToken;
-    }
-
-}
diff --git a/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockOAuthService.java b/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockTokenService.java
similarity index 83%
rename from src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockOAuthService.java
rename to src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockTokenService.java
index 8ce32a29..4b8bc5ce 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockOAuthService.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/test/service/impl/MockTokenService.java
@@ -13,13 +13,13 @@ package org.eclipsefoundation.git.eca.test.service.impl;
 
 import javax.inject.Singleton;
 
-import org.eclipsefoundation.git.eca.service.OAuthService;
+import org.eclipsefoundation.efservices.services.DrupalTokenService;
 
 import io.quarkus.test.Mock;
 
 @Mock
 @Singleton
-public class MockOAuthService implements OAuthService {
+public class MockTokenService implements DrupalTokenService {
 
     @Override
     public String getToken() {
-- 
GitLab


From 7b70fa6f444467cf2e2a079639ce3ccc4f0c5124 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 9 Jun 2023 13:52:24 -0400
Subject: [PATCH 4/5] Fix projects precache timing out before load is finished

---
 src/main/resources/application.properties | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 33b399d0..34c35e6a 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1,6 +1,7 @@
 quarkus.rest-client."org.eclipsefoundation.git.eca.api.AccountsAPI".scope=javax.enterprise.context.ApplicationScoped
 quarkus.rest-client."org.eclipsefoundation.git.eca.api.AccountsAPI".url=https://api.eclipse.org
-org.eclipsefoundation.git.eca.api.ProjectsAPI/mp-rest/url=https://projects.eclipse.org
+projects-api/mp-rest/url=https://projects.eclipse.org
+eclipse.cache.loading."projects".timeout=10
 org.eclipsefoundation.git.eca.api.BotsAPI/mp-rest/url=https://api.eclipse.org
 quarkus.rest-client."org.eclipsefoundation.git.eca.api.GitlabAPI".url=https://gitlab.eclipse.org/api/v4/
 
-- 
GitLab


From 50711952b3de8b2d2d1ddd8a6dceb35cf50b8240 Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Fri, 9 Jun 2023 14:18:08 -0400
Subject: [PATCH 5/5] Add start at boot for projects loading

---
 src/main/resources/application.properties | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 34c35e6a..09df93d1 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -2,6 +2,7 @@ quarkus.rest-client."org.eclipsefoundation.git.eca.api.AccountsAPI".scope=javax.
 quarkus.rest-client."org.eclipsefoundation.git.eca.api.AccountsAPI".url=https://api.eclipse.org
 projects-api/mp-rest/url=https://projects.eclipse.org
 eclipse.cache.loading."projects".timeout=10
+eclipse.cache.loading."projects".start-at-boot=true
 org.eclipsefoundation.git.eca.api.BotsAPI/mp-rest/url=https://api.eclipse.org
 quarkus.rest-client."org.eclipsefoundation.git.eca.api.GitlabAPI".url=https://gitlab.eclipse.org/api/v4/
 
-- 
GitLab