Commit 9a43522e authored by Martin Lowe's avatar Martin Lowe

Initial commit of commons code (WIP)

Signed-off-by: Martin Lowe's avatarMartin Lowe <martin.lowe@eclipse-foundation.org>
parents
target/
.settings/
.project
.classpath
bin/
\ No newline at end of file
# Build JAVA applications using Apache Maven (http://maven.apache.org)
# For docker image tags see https://hub.docker.com/_/maven/
#
# For general lifecycle information see https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
# This template will build and test your projects
# * Caches downloaded dependencies and plugins between invocation.
# * Verify but don't deploy merge requests.
# * Deploy built artifacts from master branch only.
variables:
# This will suppress any download for dependencies and plugins or upload messages which would clutter the console log.
# `showDateTime` will show the passed time in milliseconds. You need to specify `--batch-mode` to make this work.
MAVEN_OPTS: "-Dhttps.protocols=TLSv1.2 -Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository -Djava.awt.headless=true"
# As of Maven 3.3.0 instead of this you may define these options in `.mvn/maven.config` so the same config is used
# when running from the command line.
# `installAtEnd` and `deployAtEnd` are only effective with recent version of the corresponding plugins.
MAVEN_CLI_OPTS: "--batch-mode --errors --fail-at-end --show-version -DinstallAtEnd=true -DdeployAtEnd=true"
# This template uses jdk8 for verifying and deploying images
image: maven:3.3.9-jdk-8
# Cache downloaded dependencies and plugins between builds.
# To keep cache across branches add 'key: "$CI_JOB_NAME"'
cache:
paths:
- .m2/repository
# For merge requests do not `deploy` but only run `verify`.
# See https://maven.apache.org/guides/introduction/introduction-to-the-lifecycle.html
.verify: &verify
stage: test
script:
- 'mvn $MAVEN_CLI_OPTS verify -Dconfig.secret.path=config/sample.secret.properties'
except:
- master
# Verify merge requests using JDK8
verify:jdk8:
<<: *verify
# To deploy packages from CI, create a ci_settings.xml file
# For deploying packages to GitLab's Maven Repository: See https://docs.gitlab.com/ee/user/project/packages/maven_repository.html#creating-maven-packages-with-gitlab-cicd for more details.
# Please note: The GitLab Maven Repository is currently only available in GitLab Premium / Ultimate.
# For `master` branch run `mvn deploy` automatically.
deploy:jdk8:
stage: deploy
script:
- if [ ! -f ci_settings.xml ];
then echo "CI settings missing\! If deploying to GitLab Maven Repository, please see https://docs.gitlab.com/ee/user/project/packages/maven_repository.html#creating-maven-packages-with-gitlab-cicd for instructions.";
fi
- 'mvn $MAVEN_CLI_OPTS deploy -s ci_settings.xml'
only:
- master
quarkus.oauth2.client-id=sample
quarkus.oauth2.client-secret=sample
%test.sample.secret.property=secret-value
%test.eclipse.secret.token=example
\ No newline at end of file
quarkus.oauth2.client-id=1b8R2q6ZE0MA7lISWxXbOeqL9
quarkus.oauth2.client-secret=bNzI5juUFRXjeiFgsM2x05XUY
quarkus.datasource.password = my-secret-pw
%test.sample.secret.property=secret-value
%test.eclipse.secret.token=example
<?xml version="1.0"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://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>
<artifactId>eclipsefdn-java-sdk-core</artifactId>
<name>Core - Runtime</name>
<parent>
<groupId>org.eclipsefoundation</groupId>
<artifactId>eclipsefdn-java-sdk-commons</artifactId>
<version>0.1-BETA</version>
<relativePath>../pom.xml</relativePath>
</parent>
<properties>
</properties>
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy</artifactId>
</dependency>
<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.quarkus</groupId>
<artifactId>quarkus-undertow</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-jsonb</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-health</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-elytron-security-oauth2</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-cache</artifactId>
</dependency>
<dependency>
<groupId>org.jboss.logmanager</groupId>
<artifactId>jboss-logmanager</artifactId>
<version>2.1.14.Final</version>
</dependency>
</dependencies>
</project>
/* Copyright (c) 2019 Eclipse Foundation and others.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License 2.0
* which is available at http://www.eclipse.org/legal/epl-v20.html,
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.core.config;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import javax.json.bind.JsonbConfig;
import javax.json.bind.config.PropertyNamingStrategy;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;
import org.eclipsefoundation.core.helper.DateTimeHelper;
/**
* Updates JSONB config to use a naming convention when interacting with objects
* that match the API best practices set by internal documentation.
*
* @author Martin Lowe
*/
@Provider
public class JsonBConfig implements ContextResolver<Jsonb> {
@Override
public Jsonb getContext(Class<?> type) {
JsonbConfig config = new JsonbConfig();
// following strategy is defined as default by internal API guidelines
config.withPropertyNamingStrategy(PropertyNamingStrategy.LOWER_CASE_WITH_UNDERSCORES)
.withDateFormat(DateTimeHelper.RAW_RFC_3339_FORMAT, null);
return JsonbBuilder.create(config);
}
}
package org.eclipsefoundation.core.config;
import java.util.function.Supplier;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
/**
* Custom override for production that can be enabled to set user roles to
* include the role set in the property, defaulting to admin access.
*
* @author Martin Lowe
*/
@ApplicationScoped
public class RoleAugmentor implements SecurityIdentityAugmentor {
// properties that allow this functionality to be configured
@ConfigProperty(name = "eclipse.oauth.override", defaultValue = "false")
boolean overrideRole;
@ConfigProperty(name = "eclipse.oauth.override.role", defaultValue = "marketplace_admin_access")
String overrideRoleName;
@Override
public int priority() {
return 0;
}
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return context.runBlocking(build(identity));
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
if (overrideRole) {
// create a new builder and copy principal, attributes, credentials and roles
// from the original
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder()
.setPrincipal(identity.getPrincipal()).addAttributes(identity.getAttributes())
.addCredentials(identity.getCredentials()).addRoles(identity.getRoles());
// add custom role source here
builder.addRole(overrideRoleName);
// put the updated role in the future
return builder::build;
} else {
// put the unmodified identity in the future
return () -> identity;
}
}
}
\ No newline at end of file
/* Copyright (c) 2019 Eclipse Foundation and others.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License 2.0
* which is available at http://www.eclipse.org/legal/epl-v20.html,
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.core.config;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import org.eclipse.microprofile.config.spi.ConfigSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Reads in a set of secret configuration values from the secret.properties file
* in the resources folder. These values are only secret in that they are set to
* be ignored by Git and should be set on a per-server basis.
*
* ConfigSource implementation was used to enable the usage of the
* {@link ServiceLoader} to load the configurations.
*
* @author Martin Lowe
*/
public class SecretConfigSource implements ConfigSource {
private static final Logger LOGGER = LoggerFactory.getLogger(SecretConfigSource.class);
public static final String PROPERTY_NAME = "config.secret.path";
public static final String ENV_NAME = "CONFIG_SECRET_PATH";
private Map<String, String> secrets;
@Override
@SuppressWarnings({ "unchecked", "rawtypes" })
public Map<String, String> getProperties() {
if (secrets == null) {
this.secrets = new HashMap<>();
String secretPath = System.getProperty(PROPERTY_NAME);
// Fallback to checking env if not set in JVM
if (secretPath == null || secretPath.isEmpty()) {
secretPath = System.getenv(ENV_NAME);
}
if (secretPath == null || secretPath.isEmpty()) {
LOGGER.warn("Configuration '{}' not set, cannot generate secret properties.", PROPERTY_NAME);
return this.secrets;
}
// load the secrets file in
File f = new File(secretPath);
if (!f.exists() || !f.canRead()) {
LOGGER.error("File at path {} either does not exist or cannot be read", secretPath);
return this.secrets;
}
// read each of the lines of secret config that should be added
try (BufferedReader br = new BufferedReader(new FileReader(f))) {
Properties p = new Properties();
p.load(br);
secrets.putAll((Map) p);
} catch (IOException e) {
LOGGER.error("Error while reading in secrets configuration file.", e);
}
LOGGER.debug("Found secret keys: {}", secrets.keySet());
// add priority ordinal to map if missing. 260 ordinal sets the priority between
// container and environment variable priority.
secrets.computeIfAbsent(ConfigSource.CONFIG_ORDINAL, key -> "260");
}
return secrets;
}
@Override
public String getValue(String propertyName) {
return getProperties().get(propertyName);
}
@Override
public String getName() {
return "secret";
}
}
/* Copyright (c) 2019 Eclipse Foundation and others.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License 2.0
* which is available at http://www.eclipse.org/legal/epl-v20.html,
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.core.exception;
/**
* @author martin
*
*/
public class MaintenanceException extends RuntimeException {
private static final long serialVersionUID = 1L;
}
/* Copyright (c) 2019 Eclipse Foundation and others.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License 2.0
* which is available at http://www.eclipse.org/legal/epl-v20.html,
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.core.health;
import javax.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.health.HealthCheck;
import org.eclipse.microprofile.health.HealthCheckResponse;
import org.eclipse.microprofile.health.Liveness;
import org.eclipse.microprofile.health.Readiness;
/**
* Basic health check to check the responsiveness of the API on a whole.
*
* @author Martin Lowe
*/
@Readiness
@Liveness
@ApplicationScoped
public class BaseApplicationHealthCheck implements HealthCheck {
@Override
public HealthCheckResponse call() {
return HealthCheckResponse.named("Basic health check").up().build();
}
}
/* Copyright (c) 2019 Eclipse Foundation and others.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License 2.0
* which is available at http://www.eclipse.org/legal/epl-v20.html,
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.core.helper;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Central implementation for handling date time conversion in the service.
* Class uses Java8 DateTime formatters, creating an internal format that
* represents RFC 3339
*
* @author Martin Lowe
*/
public class DateTimeHelper {
private static final Logger LOGGER = LoggerFactory.getLogger(DateTimeHelper.class);
public static final String RAW_RFC_3339_FORMAT = "uuuu-MM-dd'T'HH:mm:ssXXX";
private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(RAW_RFC_3339_FORMAT);
/**
* Converts RFC 3339 compliant date string to date object. If non compliant
* string is passed, issue is logged and null is returned. If negative UTC
* timezone (-00:00) is passed, UTC time zone is assumed.
*
* @param dateString an RFC 3339 date string.
* @return a date object representing time in date string, or null if not in RFC
* 3339 format.
*/
public static Date toRFC3339(String dateString) {
if (dateString.isBlank()) return null;
try {
return Date.from(ZonedDateTime.parse(dateString, formatter).toInstant());
} catch (DateTimeParseException e) {
LOGGER.warn("Could not parse date from string '{}'", dateString, e);
return null;
}
}
/**
* Converts passed date to RFC 3339 compliant date string. Time is adjusted to
* be in UTC time.
*
* @param date the date object to convert to RFC 3339 format.
* @return the RFC 3339 format date string.
*/
public static String toRFC3339(Date date) {
if (date == null) return null;
return formatter.format(date.toInstant().atZone(ZoneId.of("UTC")));
}
// hide constructor
private DateTimeHelper() {
}
}
/* Copyright (c) 2019 Eclipse Foundation and others.
* This program and the accompanying materials are made available
* under the terms of the Eclipse Public License 2.0
* which is available at http://www.eclipse.org/legal/epl-v20.html,
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.core.helper;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.Objects;
import java.util.Optional;
import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.json.bind.Jsonb;
import javax.ws.rs.core.CacheControl;
import javax.ws.rs.core.Response;
import javax.xml.bind.DatatypeConverter;
import org.eclipsefoundation.core.model.RequestWrapper;
import org.eclipsefoundation.core.service.CachingService;
/**
* Helper class that transforms data into a response usable for the RESTeasy
* container. Uses injected JSON-B serializer and caching service to get current
* information on cache data.
*
* @author Martin Lowe
*
*/
@ApplicationScoped
public class ResponseHelper {
private static final MessageDigest DIGEST;
static {
try {
DIGEST = MessageDigest.getInstance("md5");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Could not create an MD5 hash digest");
}
}
@Inject
Jsonb jsonb;
@Inject
CachingService<?> cachingService;
/**
* Builds a response using passed data. Uses references to the caching service
* and the current request to add information about ETags and Cache-Control
* headers.
*
* @param id the ID of the object to be stored in cache
* @param wrapper the query parameters for the current request
* @param data the data to attach to the response
* @return a complete response object for the given data and request.
*/
public Response build(String id, RequestWrapper wrapper, Object data) {
// set default cache control flags for API responses
CacheControl cc = new CacheControl();
cc.setNoStore(wrapper.isCacheBypass());
if (!cc.isNoStore()) {
cc.setMaxAge((int) cachingService.getMaxAge());
// get the TTL for the current entry
Optional<Long> ttl = cachingService.getExpiration(id, wrapper);
if (!ttl.isPresent()) {
return Response.serverError().build();
}
// serialize the data to get an etag
String content = jsonb.toJson(Objects.requireNonNull(data));
// ingest the content and hash to create an etag for current content
String hash;
synchronized (this) {
DIGEST.update(content.getBytes(StandardCharsets.UTF_8));
hash = DatatypeConverter.printHexBinary(DIGEST.digest());
DIGEST.reset();
}
// check if etag matches
String etag = wrapper.getHeader("Etag");
if (hash.equals(etag)) {
return Response.notModified(etag).cacheControl(cc).expires(new Date(ttl.get())).build();
}
// return a response w/ the generated etag
return Response.ok(data).tag(hash).cacheControl(cc).expires(new Date(ttl.get())).build();
}
return Response.ok(data).cacheControl(cc).build();
}
}
/*
* Copyright (C) 2019 Eclipse Foundation and others.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*/
package org.eclipsefoundation.core.model;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
public class Error {
private int statusCode;
private String message;
private String url;
public Error(int statusCode, String message) {
this.statusCode = statusCode;
this.message = message;
}
/**
* Creates an error object using the JAX-RS status to fetch the status code.
*
* @param status the JAX-RS status for the error.