Skip to content
Snippets Groups Projects
Commit 23b86944 authored by Martin Lowe's avatar Martin Lowe :palm_tree:
Browse files

Merge branch 'malowe/main/migrate-projects' into 'main'

Iss #132 - Projects and auth token code migrations, update openapi spec

See merge request !140
parents c001ecdd 50711952
No related branches found
No related tags found
1 merge request!140Iss #132 - Projects and auth token code migrations, update openapi spec
Pipeline #18523 failed
Showing
with 241 additions and 700 deletions
......@@ -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/**/*"
}
......
......@@ -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>
......@@ -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>
......@@ -63,6 +62,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>
......@@ -87,7 +91,6 @@
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>
<dependency>
......@@ -129,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>
......
......@@ -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:
......
/*********************************************************************
* 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);
}
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();
}
}
}
/*********************************************************************
* 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;
}
}
}
......@@ -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;
......
/*********************************************************************
* 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;
/**
* 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.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.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.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.
*
* 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
* @author Zachary Sabourin
*
*/
@Startup
@ApplicationScoped
public class PaginationProjectsService implements ProjectsService {
private static final Logger LOGGER = LoggerFactory.getLogger(PaginationProjectsService.class);
public final class ProjectHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(ProjectHelper.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;
ProjectService projects;
/**
* 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) {
......@@ -149,7 +57,6 @@ public class PaginationProjectsService implements ProjectsService {
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");
......@@ -157,10 +64,6 @@ public class PaginationProjectsService implements ProjectsService {
}
// 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();
......@@ -195,19 +98,66 @@ public class PaginationProjectsService implements ProjectsService {
}
/**
* Logic for retrieving projects from API.
*
* @return list of projects for the cache
* 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.
*/
private List<Project> getProjectsInternal() {
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);
}
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());
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());
}
}
/*********************************************************************
* 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;
}
}
......@@ -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")
......
......@@ -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
......
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);
}
/*********************************************************************
* 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();
}
/*********************************************************************
* 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);
}
......@@ -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 {
......
......@@ -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;
......
......@@ -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,11 +28,11 @@ 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.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.api.models.Project;
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;
......
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());
}
}
/*********************************************************************
* 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;
}
}
......@@ -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
......
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