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

Merge branch 'zacharysabourin/main/3' into 'main'

feat: Phase 1 of the openvsx publisher agreement migration from Drupal

See merge request !3
parents fda9b44f 10220257
No related branches found
No related tags found
1 merge request!3feat: Phase 1 of the openvsx publisher agreement migration from Drupal
Pipeline #15224 passed
Showing
with 827 additions and 98 deletions
......@@ -38,6 +38,7 @@ nb-configuration.xml
# Local environment
/volumes
.env
config/mariadb/main/init.d/eclipsefoundation.sql
/config/**/*secret.properties
/node_modules
......
......@@ -3,7 +3,7 @@ SHELL = /bin/bash
pre-setup:;
@echo "Creating environment file from template"
@rm -f .env && envsubst < config/.env.sample > .env
[ -f ./config/mariadb/init.d/eclipsefoundation.sql ] && echo "EF DB dump already exists, skipping fetch" || scp api-vm1:~/webdev/sql/eclipsefoundation.sql ./config/mariadb/init.d/
[ -f ./config/mariadb/main/init.d/eclipsefoundation.sql ] && echo "EF DB dump already exists, skipping fetch" || (scp api-vm1:~/webdev/sql/eclipsefoundation.sql.gz ./config/mariadb/main/init.d/ && gzip -d ./config/mariadb/main/init.d/eclipsefoundation.sql.gz)
setup:;
@echo "Generating secret files from templates using environment file + variables"
......
......@@ -50,6 +50,6 @@ services:
MYSQL_ROOT_PASSWORD: ${MARIADB_PASSWORD}
MYSQL_DATABASE: ${KEYCLOAK_DB}
volumes:
- ./config/mariadb/init.d:/docker-entrypoint-initdb.d
- ./config/mariadb/main/init.d:/docker-entrypoint-initdb.d
- ./volumes/mariadb:/var/lib/mysql
- ./config/mariadb/conf:/etc/mysql/conf.d
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<project
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<groupId>org.eclipsefoundation</groupId>
<artifactId>eclipse-openvsx-api</artifactId>
......@@ -10,14 +12,13 @@
<maven.compiler.release>11</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.artifact-id> quarkus-bom</quarkus.platform.artifact-id>
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
<quarkus.platform.version>2.11.2.Final</quarkus.platform.version>
<quarkus.platform.version> 2.14.2.Final</quarkus.platform.version>
<surefire-plugin.version>3.0.0-M5</surefire-plugin.version>
<auto-value.version>1.8.2</auto-value.version>
<hibernate.version>5.5.6.Final</hibernate.version>
<eclipse-api-version>0.7.0-SNAPSHOT</eclipse-api-version>
<fdndb-api-version>1.0-SNAPSHOT</fdndb-api-version>
<eclipse-api-version> 0.7.1</eclipse-api-version>
<fdndb-api-version>1.0.3</fdndb-api-version>
</properties>
<repositories>
<repository>
......@@ -62,111 +63,31 @@
<artifactId>foundationdb-api-client</artifactId>
<version>${fdndb-api-version}</version>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.9</version>
</dependency>
<!-- Annotation preprocessors - reduce all of the boiler plate -->
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value</artifactId>
<version>${auto-value.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.auto.value</groupId>
<artifactId>auto-value-annotations</artifactId>
<version>${auto-value.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.findbugs</groupId>
<artifactId>jsr305</artifactId>
<version>3.0.0</version>
</dependency>
<!-- Testing dependencies only -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>json-schema-validator</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-mockito</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipsefoundation</groupId>
<artifactId>quarkus-test-common</artifactId>
<version>${eclipse-api-version}</version>
</dependency>
<!-- Following H2/devservices deps are made to circumvent need for docker -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-devservices-h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<!-- Flyway specific dependencies, used to setup tables in test -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-flyway</artifactId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
......@@ -235,10 +156,8 @@
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>
${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>
org.jboss.logmanager.LogManager</java.util.logging.manager>
<native.image.path> ${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager> org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
</configuration>
......@@ -248,8 +167,7 @@
</plugins>
</build>
<properties>
<quarkus.package.type>
native</quarkus.package.type>
<quarkus.package.type> native</quarkus.package.type>
</properties>
</profile>
</profiles>
......
......@@ -29,6 +29,10 @@ paths:
$ref: "#/components/schemas/PublisherAgreement"
404:
description: Agreement not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
500:
description: Error while retrieving data
post:
......@@ -158,3 +162,18 @@ components:
github_handle:
type: string
description: The GitHub username of the user. This must match what the Eclipse Foundation has on file for the user to successfully sign the puglisher agreement.
Error:
type: object
properties:
status_code:
type: integer
description: HTTP response code
message:
type: string
description: Message containing error information
url:
oneOf:
- type: string
- type: "null"
description: The URL
......@@ -9,16 +9,28 @@
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.resources;
package org.eclipsefoundation.openvsx.api;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
/**
* Drupal OAuth2 token validation API binding
*/
@Path("oauth2/tokens")
@Produces(MediaType.APPLICATION_JSON)
@ApplicationScoped
@RegisterRestClient(configKey = "accounts-api")
public interface DrupalOAuthAPI {
@GET
public String hello() {
return "Hello RESTEasy";
}
@Path("{token}")
DrupalOAuthData getTokenInfo(@PathParam("token") String token);
}
\ No newline at end of file
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.api;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipsefoundation.openvsx.api.models.EfUser;
@ApplicationScoped
@Produces("application/json")
@RegisterRestClient(configKey = "eclipse-api")
public interface EclipseAPI {
/**
* Retrieves user objects that matches the given Github username.
*
* @param username GH handle to match against EF user
* @return the matching Eclipse account or null
*/
@GET
@Path("/github/profile/{username}")
EfUser getUserByGithubName(@PathParam("username") String username);
}
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.api;
import java.util.List;
import javax.annotation.security.RolesAllowed;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
import io.quarkus.oidc.client.filter.OidcClientFilter;
@Path("people")
@OidcClientFilter
@Produces(MediaType.APPLICATION_JSON)
@RegisterRestClient(configKey = "fdndb-api")
@ApplicationScoped
public interface PeopleAPI {
@GET
@Path("{personId}/documents")
@RolesAllowed("fdb_read_people_documents")
List<PeopleDocumentData> getPeopleDocuments(@PathParam("personId") String personId,
@QueryParam("documentID") String documentId);
@PUT
@Path("{personId}/documents")
@RolesAllowed("fdb_write_people_documents")
List<PeopleDocumentData> createPeopleDocument(@PathParam("personId") String personId, PeopleDocumentData src);
@DELETE
@Path("{personId}/documents/{documentId}/{effectiveDate}")
@RolesAllowed("fdb_write_people_documents")
Response deletePeopleDocument(@PathParam("personId") String personId, @PathParam("documentId") String documentId,
@PathParam("effectiveDate") String effectiveDate);
}
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.api.models;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.google.auto.value.AutoValue;
@AutoValue
@JsonDeserialize(builder = AutoValue_AgreementSigningRequest.Builder.class)
public abstract class AgreementSigningRequest {
public abstract String getVersion();
public abstract String getGithubHandle();
public static Builder builder() {
return new AutoValue_AgreementSigningRequest.Builder();
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setVersion(String version);
public abstract Builder setGithubHandle(String handle);
public abstract AgreementSigningRequest build();
}
}
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.api.models;
import javax.annotation.Nullable;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.google.auto.value.AutoValue;
@AutoValue
@JsonDeserialize(builder = AutoValue_DrupalOAuthData.Builder.class)
public abstract class DrupalOAuthData {
public abstract String getClientId();
@Nullable
public abstract String getUserId();
public abstract String getAccessToken();
public abstract long getExpires();
public abstract String getScope();
public static Builder builder() {
return new AutoValue_DrupalOAuthData.Builder();
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setClientId(String id);
public abstract Builder setUserId(@Nullable String id);
public abstract Builder setAccessToken(String token);
public abstract Builder setExpires(long expires);
public abstract Builder setScope(String scope);
public abstract DrupalOAuthData build();
}
}
\ No newline at end of file
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.api.models;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
import com.google.auto.value.AutoValue;
@AutoValue
@JsonDeserialize(builder = AutoValue_EfUser.Builder.class)
public abstract class EfUser {
public abstract String getName();
public abstract String getGithubHandle();
public static Builder builder() {
return new AutoValue_EfUser.Builder();
}
@AutoValue.Builder
@JsonPOJOBuilder(withPrefix = "set")
public abstract static class Builder {
public abstract Builder setName(String name);
public abstract Builder setGithubHandle(String handle);
public abstract EfUser build();
}
}
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.helpers;
import java.time.Instant;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Defines helper methods for Drupal OAuth2 token validation.
*/
public class DrupalAuthHelper {
/**
* Validates whether a given expiry date has passed.
*
* @param expiryDate The token expiry date in seconds since epoch.
* @return True if expiry date is older than current time. False if not.
*/
public static boolean isExpired(long expiryDate) {
return expiryDate <= Instant.now().getEpochSecond();
}
/**
* Validates whether the token scopes and valid scopes are the same.
*
* @param tokenScope The space-separated token scopes.
* @param validScopes The list of valid scopes.
* @return Returns false if any token scopes are not in the list of valid
* scopes. True if all match.
*/
public static boolean hasScopes(String tokenScope, List<String> validScopes) {
List<String> tokenScopes = Arrays.asList(tokenScope.split(" "));
Collections.sort(tokenScopes);
Collections.sort(validScopes);
return tokenScopes.containsAll(validScopes);
}
/**
* Validates whether the the token's client id matches a desired client id.
*
* @param clientId The client id associated with the token
* @param validClientIds The valid client ids allowed in the service.
* @return True if ids match. False if not.
*/
public static boolean hasValidclientId(String clientId, List<String> validClientIds) {
return validClientIds.stream().anyMatch(id -> id.equalsIgnoreCase(clientId));
}
private DrupalAuthHelper() {
}
}
\ No newline at end of file
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.request;
import java.io.IOException;
import java.util.List;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipsefoundation.core.exception.FinalForbiddenException;
import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
import org.eclipsefoundation.openvsx.services.DrupalOAuthService;
import org.jboss.resteasy.util.HttpHeaderNames;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@Provider
public class OAuthFilter implements ContainerRequestFilter {
private static final Logger LOGGER = LoggerFactory.getLogger(OAuthFilter.class);
public static final String TOKEN_STATUS = "tokenStatus";
@ConfigProperty(name = "eclipse.openvsx.scopes", defaultValue = "openvsx_publisher_agreement")
Instance<List<String>> validScopes;
@ConfigProperty(name = "eclipse.openvsx.clients")
Instance<List<String>> validClientIds;
@ConfigProperty(name = "eclipse.openvsx.oauth-filter.enabled", defaultValue = "false")
Instance<Boolean> isEnabled;
@Inject
DrupalOAuthService oauthService;
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
if (Boolean.TRUE.equals(isEnabled.get())) {
String token = stripBearerToken(requestContext.getHeaderString(HttpHeaderNames.AUTHORIZATION));
DrupalOAuthData tokenStatus = oauthService.validateTokenStatus(token, validScopes.get(),
validClientIds.get());
if (tokenStatus != null && tokenStatus.getUserId() != null) {
requestContext.setProperty(TOKEN_STATUS, tokenStatus);
}
}
}
/**
* Strips the bearer token down to just the token. Throws a
* FinalForbiddenException if the token is in an invalid format.
*
* @param header the bearer token suthorization header
* @return The raw token string.
*/
private String stripBearerToken(String header) {
if (header == null || header.equals("")) {
throw new FinalForbiddenException("Invalid token");
}
String[] splits = header.split(" ");
// Invalid token if no space or more than one space
if (splits.length != 2 || !splits[0].equals("Bearer")) {
LOGGER.error("Invalid auth header: {}", header);
throw new FinalForbiddenException("Invalid token");
}
return splits[1];
}
}
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.resources;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.BadRequestException;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.NotFoundException;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import org.eclipsefoundation.core.exception.FinalForbiddenException;
import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
import org.eclipsefoundation.openvsx.api.models.AgreementSigningRequest;
import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
import org.eclipsefoundation.openvsx.request.OAuthFilter;
import org.eclipsefoundation.openvsx.services.PublisherAgreementService;
@Path("publisher_agreement")
public class PublisherAgreementResource {
@Context
HttpServletRequest request;
@Inject
PublisherAgreementService agreementService;
@GET
public Response getAgreement() {
// Endpoint uses currently logged in user. Token must have user_id
return getAgreementForUser(extractUserIdFromToken());
}
@POST
public Response createAgreement(AgreementSigningRequest body) {
// Endpoint uses currently logged in user. Token must have user_id
PeopleDocumentData result = agreementService.createPublisherAgreement(extractUserIdFromToken(), body);
if (result == null) {
throw new BadRequestException("Unable to create Publisher Agreement with current request parameters");
}
return Response.ok(result).build();
}
@GET
@Path("{efUsername}")
public Response getAgreementForUser(@PathParam("efUsername") String username) {
PeopleDocumentData result = agreementService.getPublisherAgreement(username);
if (result == null) {
throw new NotFoundException(String.format("Unable to find agreement for user: %s", username));
}
return Response.ok(result).build();
}
@DELETE
@Path("{efUsername}")
public Response deleteAgreementForUser(@PathParam("efUsername") String username) {
if (!agreementService.deletePublisherAgreement(username)) {
throw new BadRequestException(String.format("Unable to delete agreement for user: %s", username));
}
return Response.ok().build();
}
/**
* Extracts the user_id from the token data in order to determine a user is
* signed in. Throws a FinalForbiddenException if there is no user associated
* with this token.
*
* @return The token user id.
*/
private String extractUserIdFromToken() {
DrupalOAuthData tokenStatus = (DrupalOAuthData) request.getAttribute(OAuthFilter.TOKEN_STATUS);
if (tokenStatus == null || tokenStatus.getUserId() == null) {
throw new FinalForbiddenException("No user associated with this token");
}
return tokenStatus.getUserId();
}
}
\ No newline at end of file
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.services;
import java.util.List;
import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
/**
* Defines a Drupal Oauth validation service.
*/
public interface DrupalOAuthService {
/**
* Validates an OAuth2 token using a list of valid scopes. Also validates token
* expiry and client id. Returns an OAuthTokenStatus entity containing token
* validity information as well as the basis for denial if token deemed invalid.
*
* @param token The token string.
* @param validScopes A list of valid scopes for this token.
* @param validClientId A valid client_id for this token.
* @return A OAuthTokenStatus entity containing status info.
*/
DrupalOAuthData validateTokenStatus(String token, List<String> validScopes, List<String> validClientIds);
}
\ No newline at end of file
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.services;
import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
import org.eclipsefoundation.openvsx.api.models.AgreementSigningRequest;
/**
* Defines the service for fetching, creating and deleting openvsx publisher
* agreements.
*/
public interface PublisherAgreementService {
/**
* Retrieves the most recent openvsx publisher agreement for the given user.
*
* @param username The desired user's EF username.
* @return A PeopleDocumentData entity or null.
*/
public PeopleDocumentData getPublisherAgreement(String username);
/**
* Attempts to create a publisher agreement for the given user using the request
* body contining the user GH handle and document version. Validates the GH
* handle exists and is tied to the desired username. Returns null if the GH
* handle is incorrect, if it can't create the signed document, or if it
* encounters an error.
*
* @param username The desired user's EF username.
* @param request The rrequest body containing the GH handle and coument
* version.
* @return A PeopleDocumentData entity or null.
*/
public PeopleDocumentData createPublisherAgreement(String username, AgreementSigningRequest request);
/**
* Attempts to delete the most recent publisher agreement for the given user and
* removes the entry from the cache. Returns false if document can not be found.
* Returns true if deletion was successful.
*
* @param username the desired user's EF username.
* @return True if success, false if not.
*/
public boolean deletePublisherAgreement(String username);
}
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.services.impl;
import java.util.List;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.eclipsefoundation.core.exception.FinalForbiddenException;
import org.eclipsefoundation.openvsx.api.DrupalOAuthAPI;
import org.eclipsefoundation.openvsx.api.models.DrupalOAuthData;
import org.eclipsefoundation.openvsx.helpers.DrupalAuthHelper;
import org.eclipsefoundation.openvsx.services.DrupalOAuthService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
public class DefaultDrupalOAuthService implements DrupalOAuthService {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultDrupalOAuthService.class);
@Inject
@RestClient
DrupalOAuthAPI oauthAPI;
@Override
public DrupalOAuthData validateTokenStatus(String token, List<String> validScopes, List<String> validClientIds) {
try {
LOGGER.debug("Validating token: {}", token);
DrupalOAuthData tokenData = oauthAPI.getTokenInfo(token);
if (DrupalAuthHelper.isExpired(tokenData.getExpires())) {
throw new FinalForbiddenException("The Authorization token is expired");
}
if (!DrupalAuthHelper.hasScopes(tokenData.getScope(), validScopes)) {
throw new FinalForbiddenException("The Authorization token does not have the required permissions/scopes");
}
if (!DrupalAuthHelper.hasValidclientId(tokenData.getClientId(), validClientIds)) {
throw new FinalForbiddenException("The Authorization token has an Invalid client_id");
}
return tokenData;
} catch (Exception e) {
LOGGER.error("Error validating token", e);
throw new FinalForbiddenException("Error validating token");
}
}
}
\ No newline at end of file
/*********************************************************************
* 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: Zachary Sabourin <zachary.sabourin@eclipse-foundation.org>
*
* SPDX-License-Identifier: EPL-2.0
**********************************************************************/
package org.eclipsefoundation.openvsx.services.impl;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.core.Response;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import org.eclipsefoundation.core.helper.DateTimeHelper;
import org.eclipsefoundation.core.service.CachingService;
import org.eclipsefoundation.core.service.CachingService.ParameterizedCacheKey;
import org.eclipsefoundation.foundationdb.client.model.PeopleDocumentData;
import org.eclipsefoundation.openvsx.api.EclipseAPI;
import org.eclipsefoundation.openvsx.api.PeopleAPI;
import org.eclipsefoundation.openvsx.api.models.AgreementSigningRequest;
import org.eclipsefoundation.openvsx.api.models.EfUser;
import org.eclipsefoundation.openvsx.services.PublisherAgreementService;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ApplicationScoped
public class DefaultPublisherAgreementService implements PublisherAgreementService {
private static final Logger LOGGER = LoggerFactory.getLogger(DefaultPublisherAgreementService.class);
@ConfigProperty(name = "eclipse.openvsx.doc-id")
String openvsxDocId;
@Inject
@RestClient
PeopleAPI peopleAPI;
@Inject
@RestClient
EclipseAPI eclipseAPI;
@Inject
CachingService cache;
@Override
public PeopleDocumentData getPublisherAgreement(String username) {
try {
Optional<List<PeopleDocumentData>> results = cache.get(username, new MultivaluedMapImpl<>(),
PeopleDocumentData.class, () -> peopleAPI.getPeopleDocuments(username, openvsxDocId));
if (results.isEmpty() || results.get().isEmpty()) {
LOGGER.error("Unable to find agreements for user with name: {}", username);
return null;
}
// Sort by date. Returning most recent
return results.get().stream()
.sorted((pDoc1, pDoc2) -> pDoc2.getEffectiveDate().compareTo(pDoc1.getEffectiveDate()))
.findFirst().orElse(null);
} catch (Exception e) {
LOGGER.error("Error while fetching publisher agreements for user: {}", username, e);
return null;
}
}
@Override
public PeopleDocumentData createPublisherAgreement(String username, AgreementSigningRequest body) {
try {
Optional<EfUser> ghResult = cache.get("gh: " + body.getGithubHandle(), new MultivaluedMapImpl<>(),
Response.class, () -> eclipseAPI.getUserByGithubName(body.getGithubHandle()));
if (ghResult.isEmpty() || ghResult.get() == null) {
LOGGER.error("Unable to find account for GH user: {}", body.getGithubHandle());
return null;
}
// Ensure username in URL matches GH fetch username
if (!ghResult.get().getName().equalsIgnoreCase(username)) {
LOGGER.error("Invalid GH handle for use: {}", username);
return null;
}
PeopleDocumentData fdnDbRequest = PeopleDocumentData.builder()
.setPersonID(username)
.setDocumentID(openvsxDocId)
.setVersion(Double.parseDouble(body.getVersion()))
.setEffectiveDate(new Date())
.setReceivedDate(new Date())
.build();
// Submit new document to fdnDb for creation, can fail if invalid version is set
List<PeopleDocumentData> results = peopleAPI.createPeopleDocument(username, fdnDbRequest);
if (results == null || results.isEmpty()) {
LOGGER.error("Unable to create agreement for user with name: {}", username);
return null;
}
return results.get(0);
} catch (Exception e) {
LOGGER.error("Error while creating publisher agreement", e);
return null;
}
}
@Override
public boolean deletePublisherAgreement(String username) {
// Get most recent document if it exists
PeopleDocumentData mostRecent = getPublisherAgreement(username);
if (mostRecent == null) {
return false;
}
// Delete and remove from cache entry
peopleAPI.deletePeopleDocument(username, openvsxDocId, DateTimeHelper.toRFC3339(mostRecent.getEffectiveDate()));
cache.remove(new ParameterizedCacheKey(PeopleDocumentData.class, username, new MultivaluedMapImpl<>()));
return true;
}
}
quarkus.http.root-path=/openvsx
fdndb-api/mp-rest/url=http://localhost:8095
fdndb-api/mp-rest/scope=javax.inject.Singleton
fdndb-api/mp-rest/url=http://foundationdb:8095
%dev.fdndb-api/mp-rest/url=http://localhost:10112
accounts-api/mp-rest/url=https://accounts.eclipse.org
%dev.accounts-api/mp-rest/url=https://accounts-staging.eclipse.org
eclipse-api/mp-rest/url=https://api.eclipse.org
quarkus.oidc.enabled=false
eclipse.openvsx.oauth-filter.enabled=true
%dev.eclipse.openvsx.oauth-filter.enabled=true
quarkus.log.level=INFO
\ No newline at end of file
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