CcmNG: Backend for Theme editing

git-svn-id: https://svn.libreccm.org/ccm/ccm_ng@5693 8810af33-2d31-482b-a856-94f89814c4df
jensp 2018-09-19 18:14:14 +00:00
parent db9e9b9cfd
commit 5880af452e
13 changed files with 520 additions and 47 deletions

View File

@ -187,6 +187,19 @@
<artifactId>shiro-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>

View File

@ -79,6 +79,9 @@ public final class KernelConfig {
@Setting
private String importPath = "";
@Setting
private String jwtSecret = "";
public static KernelConfig getConfig() {
final ConfigurationManager confManager = CdiUtil.createCdiUtil()
.findBean(ConfigurationManager.class);
@ -234,6 +237,14 @@ public final class KernelConfig {
this.importPath = importPath;
}
public String getJwtSecret() {
return jwtSecret;
}
public void setJwtSecret(final String jwtSecret) {
this.jwtSecret = jwtSecret;
}
@Override
public int hashCode() {
int hash = 7;
@ -249,6 +260,7 @@ public final class KernelConfig {
hash = 61 * hash + Objects.hashCode(systemEmailAddress);
hash = 61 * hash + Objects.hashCode(exportPath);
hash = 61 * hash + Objects.hashCode(importPath);
hash = 61 * hash * Objects.hashCode(jwtSecret);
return hash;
}
@ -304,7 +316,11 @@ public final class KernelConfig {
return false;
}
return Objects.equals(importPath, other.getImportPath());
if (!Objects.equals(importPath, other.getImportPath())) {
return false;
}
return Objects.equals(jwtSecret, other.getJwtSecret());
}
@Override

View File

@ -47,6 +47,8 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
import javax.transaction.Transactional;
/**
*
@ -143,6 +145,7 @@ public class CcmCore implements CcmModule {
@Override
public void init(final InitEvent event) {
// Nothing
}
@Override

View File

@ -317,7 +317,10 @@ public class CcmFiles {
throws FileAccessException,
InsufficientPermissionsException {
return getFileSystemAdapter().existsFile(getDataPath(path));
final String dataPath = getDataPath(path);
final boolean result = getFileSystemAdapter().existsFile(dataPath);
return result;
}
/**

View File

@ -381,7 +381,8 @@ public class NIOFileSystemAdapter implements FileSystemAdapter {
final Path nioPath = Paths.get(path);
if (!Files.isDirectory(nioPath)) {
throw new FileAccessException(path);
throw new FileAccessException(String.format(
"%s is not a directory.", path));
}
final Stream<Path> paths;

View File

@ -0,0 +1,43 @@
/*
* Copyright (C) 2018 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.security;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@ApplicationPath("/jwt")
public class CcmJwtAuthentication extends Application{
@Override
public Set<Class<?>> getClasses() {
final Set<Class<?>> classes = new HashSet<>();
classes.add(JwtProvider.class);
return classes;
}
}

View File

@ -0,0 +1,158 @@
/*
* Copyright (C) 2018 LibreCCM Foundation.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA
*/
package org.libreccm.security;
import com.arsdigita.kernel.KernelConfig;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import io.jsonwebtoken.SignatureAlgorithm;
import org.libreccm.configuration.ConfigurationManager;
import java.io.StringReader;
import java.security.Key;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Random;
import javax.crypto.spec.SecretKeySpec;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.json.JsonReader;
import javax.transaction.Transactional;
import javax.ws.rs.POST;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
*/
@RequestScoped
@Path("/")
public class JwtProvider {
@Inject
private ConfigurationManager confManager;
@Inject
private Shiro shiro;
@POST
@Path("/")
@Transactional(Transactional.TxType.REQUIRED)
public Response getJsonWebToken(final String requestCredentials) {
if (requestCredentials == null) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity("No credentials provided")
.build();
}
final JsonObjectBuilder jsonObjBuilder = Json.createObjectBuilder();
final StringReader credentialsReader = new StringReader(
requestCredentials);
final JsonReader jsonReader = Json.createReader(credentialsReader);
final JsonObject credentials = jsonReader.readObject();
final String userName = credentials.getString("username", null);
final String password = credentials.getString("password", null);
if (userName == null
|| userName.isEmpty()
|| userName.matches("\\s*")) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity("No user name was provided")
.build();
}
if (password == null
|| password.isEmpty()
|| password.matches("\\s*")) {
return Response
.status(Response.Status.BAD_REQUEST)
.entity("No password was provided")
.build();
}
final UsernamePasswordToken token = new UsernamePasswordToken(
userName, password);
final Subject subject = shiro.getSubject();
final KernelConfig kernelConfig = confManager
.findConfiguration(KernelConfig.class);
if (kernelConfig.getJwtSecret() == null
|| kernelConfig.getJwtSecret().isEmpty()
|| kernelConfig.getJwtSecret().matches("\\s*")) {
shiro.getSystemUser().execute(this::generateSecret);
}
try {
subject.login(token);
final SignatureAlgorithm signAlgo = SignatureAlgorithm.HS512;
final Key key = new SecretKeySpec(
Base64.getDecoder().decode(kernelConfig.getJwtSecret()),
signAlgo.getJcaName());
final JwtBuilder jwtBuilder = Jwts
.builder()
.setSubject((String) subject.getPrincipal())
.signWith(key);
return Response
.ok(jwtBuilder.compact())
.build();
} catch (AuthenticationException ex) {
return Response
.status(Response.Status.FORBIDDEN)
.build();
}
}
private void generateSecret() {
final Random random = new SecureRandom();
final byte[] randomBytes = new byte[64];
random.nextBytes(randomBytes);
final String secret = Base64
.getEncoder()
.encodeToString(randomBytes);
final KernelConfig kernelConfig = confManager
.findConfiguration(KernelConfig.class);
kernelConfig.setJwtSecret(secret);
confManager.saveConfiguration(kernelConfig);
}
}

View File

@ -33,6 +33,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@ -125,8 +126,7 @@ public class FileSystemThemeProvider implements ThemeProvider {
public Optional<ThemeInfo> getThemeInfo(final String theme,
final ThemeVersion version) {
final String themePath = createThemePath(theme, version);
return readInfo(themePath);
return readInfo(theme);
}
@Override
@ -228,15 +228,23 @@ public class FileSystemThemeProvider implements ThemeProvider {
final String path) {
final String themePath = createThemePath(theme, version);
final String filePath = String.join(themePath, path, "/");
final String filePath = String.join("/", themePath, path);
try {
if (ccmFiles.isDirectory(filePath)) {
return ccmFiles
.listFiles(filePath)
.stream()
.map(currentPath -> buildThemeFileInfo(currentPath))
.map(currentPath -> buildThemeFileInfo(
String.join("/", theme, currentPath)))
.collect(Collectors.toList());
} else {
final List<ThemeFileInfo> result = new ArrayList<>();
final ThemeFileInfo fileInfo = buildThemeFileInfo(filePath);
result.add(fileInfo);
return result;
}
} catch (FileAccessException
| FileDoesNotExistException
@ -246,12 +254,32 @@ public class FileSystemThemeProvider implements ThemeProvider {
}
}
@Override
public Optional<ThemeFileInfo> getThemeFileInfo(
final String theme, final ThemeVersion version, final String path) {
final String themePath = createThemePath(theme, version);
final String filePath = String.join("/", themePath, path);
try {
if (ccmFiles.existsFile(filePath)) {
return Optional.of(buildThemeFileInfo(filePath));
} else {
return Optional.empty();
}
} catch (FileAccessException | InsufficientPermissionsException ex) {
throw new UnexpectedErrorException(ex);
}
}
@Override
public Optional<InputStream> getThemeFileAsStream(
final String theme, final ThemeVersion version, final String path) {
final String themePath = createThemePath(theme, version);
final String filePath = String.join(themePath, path, "/");
final String filePath = String.join("/", themePath, path);
try {
if (ccmFiles.existsFile(path)) {
@ -272,7 +300,7 @@ public class FileSystemThemeProvider implements ThemeProvider {
final String path) {
final String themePath = createThemePath(theme, ThemeVersion.DRAFT);
final String filePath = String.join(themePath, path, "/");
final String filePath = String.join("/", themePath, path);
try {
@ -289,7 +317,7 @@ public class FileSystemThemeProvider implements ThemeProvider {
public void deleteThemeFile(final String theme, final String path) {
final String themePath = createThemePath(theme, ThemeVersion.DRAFT);
final String filePath = String.join(themePath, path, "/");
final String filePath = String.join("/", themePath, path);
try {
ccmFiles.deleteFile(filePath, true);
@ -383,15 +411,15 @@ public class FileSystemThemeProvider implements ThemeProvider {
}
}
private Optional<ThemeInfo> readInfo(final String themePath) {
private Optional<ThemeInfo> readInfo(final String themeName) {
final ThemeManifest manifest;
try {
final String jsonPath = String.format(
DRAFT_THEMES_PATH + "/" + THEME_JSON, themePath);
DRAFT_THEMES_PATH + "/" + THEME_JSON, themeName);
final String xmlPath = String.format(
DRAFT_THEMES_PATH + "/" + THEME_XML, themePath);
DRAFT_THEMES_PATH + "/" + THEME_XML, themeName);
if (ccmFiles.existsFile(jsonPath)) {
final InputStream inputStream = ccmFiles

View File

@ -321,9 +321,37 @@ public class StaticThemeProvider implements ThemeProvider {
}
@Override
public Optional<InputStream> getThemeFileAsStream(final String theme,
final ThemeVersion version,
final String path) {
public Optional<ThemeFileInfo> getThemeFileInfo(
final String theme, final ThemeVersion version, final String path) {
Objects.requireNonNull(theme);
Objects.requireNonNull(version);
Objects.requireNonNull(path);
final List<String> pathTokens = Arrays.asList(path.split("/"));
final String indexFilePath = String.format("/"
+ THEMES_PACKAGE
+ "/%s/theme-index.json",
theme);
final InputStream stream = getClass()
.getResourceAsStream(indexFilePath);
final JsonReader reader = Json.createReader(stream);
final JsonObject indexObj = reader.readObject();
final JsonArray currentDir = indexObj.getJsonArray("files");
final Optional<JsonObject> targetFile = findFile(pathTokens,
currentDir);
if (targetFile.isPresent()) {
return Optional.of(generateFileInfo(targetFile.get()));
} else {
return Optional.empty();
}
}
@Override
public Optional<InputStream> getThemeFileAsStream(
final String theme, final ThemeVersion version, final String path) {
Objects.requireNonNull(theme);
Objects.requireNonNull(path);

View File

@ -137,6 +137,21 @@ public interface ThemeProvider extends Serializable {
ThemeVersion version,
String path);
/**
* Retrieve the metadata for a particular file in the theme.
*
* @param theme The name of the theme.
* @param version The version of the theme
* @param path The path of the file, relative to the theme directory.
*
* @return An {@link Optional} containing a {@link ThemeInfo} object with
* the metadata of the file is its exists. Otherwise an empty
* {@link Optional}.
*/
Optional<ThemeFileInfo> getThemeFileInfo(String theme,
ThemeVersion version,
String path);
/**
* Retrieve a file from a theme. We use an {@link InputStream} here because
* that is the most universal interface in the Java API which works for all
@ -210,9 +225,9 @@ public interface ThemeProvider extends Serializable {
void publishTheme(String theme);
/**
* Unpublishes (deletes) the live version of a theme. For
* implementations which do not support draft/live themes the implementation
* of this method should be a noop, but not throw an exception.
* Unpublishes (deletes) the live version of a theme. For implementations
* which do not support draft/live themes the implementation of this method
* should be a noop, but not throw an exception.
*
* @param theme The theme to publish.
*/

View File

@ -182,11 +182,29 @@ public class DatabaseThemeProvider implements ThemeProvider {
return result;
}
@Override
public Optional<ThemeFileInfo> getThemeFileInfo(
final String themeName, final ThemeVersion version, final String path) {
final Theme theme = themeRepository
.findThemeByName(path, version)
.orElseThrow(() -> new IllegalArgumentException(String
.format("No Theme \"%s\" in the database.", themeName)));
final Optional<ThemeFile> themeFile = fileRepository
.findByPath(theme, path, version);
if (themeFile.isPresent()) {
return Optional.of(createThemeFileInfo(themeFile.get()));
} else {
return Optional.empty();
}
}
@Override
@Transactional(Transactional.TxType.REQUIRED)
public Optional<InputStream> getThemeFileAsStream(final String themeName,
final ThemeVersion version,
final String path) {
public Optional<InputStream> getThemeFileAsStream(
final String themeName, final ThemeVersion version, final String path) {
final Optional<Theme> theme = themeRepository
.findThemeByName(themeName, version);

View File

@ -20,12 +20,16 @@ package org.libreccm.theming.manager;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.libreccm.security.AuthorizationRequired;
import org.libreccm.security.RequiresPrivilege;
import org.libreccm.theming.ThemeFileInfo;
import org.libreccm.theming.ThemeInfo;
import org.libreccm.theming.ThemeProvider;
import org.libreccm.theming.ThemeVersion;
import org.libreccm.theming.ThemingPrivileges;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.io.StringWriter;
import java.util.ArrayList;
@ -52,7 +56,6 @@ import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
/**
*
* @author <a href="mailto:jens.pelzetter@googlemail.com">Jens Pelzetter</a>
@ -75,7 +78,7 @@ public class Themes implements Serializable {
@GET
@Path("/providers")
@Produces(MediaType.APPLICATION_JSON)
//@AuthorizationRequired
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public String getThemeProviders() {
@ -103,7 +106,7 @@ public class Themes implements Serializable {
@GET
@Path("/themes")
@Produces(MediaType.APPLICATION_JSON)
//@AuthorizationRequired
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public List<ThemeInfo> getAvailableThemes() {
@ -120,7 +123,7 @@ public class Themes implements Serializable {
@GET
@Path("/themes/{theme}")
@Produces(MediaType.APPLICATION_JSON)
//@AuthorizationRequired
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.EDIT_THEME)
public ThemeInfo getTheme(@PathParam("theme") final String themeName) {
@ -140,7 +143,7 @@ public class Themes implements Serializable {
@Path("/themes/{theme}")
@Produces(MediaType.APPLICATION_JSON)
@SuppressWarnings("unchecked")
//@AuthorizationRequired
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public ThemeInfo createTheme(
@PathParam("theme") final String themeName,
@ -176,7 +179,7 @@ public class Themes implements Serializable {
@DELETE
@Path("/themes/{theme}")
//@AuthorizationRequired
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public void deleteTheme(@PathParam("theme") final String themeName) {
@ -195,7 +198,7 @@ public class Themes implements Serializable {
@POST
@Path("/themes/{theme}/live")
//@AuthorizationRequired
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public void publishTheme(@PathParam("theme") final String themeName) {
@ -213,7 +216,7 @@ public class Themes implements Serializable {
@DELETE
@Path("/themes/{theme}/live")
//@AuthorizationRequired
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public void unPublishTheme(@PathParam("theme") final String themeName) {
@ -226,7 +229,134 @@ public class Themes implements Serializable {
} else {
throw new WebApplicationException(Response.Status.NOT_FOUND);
}
}
@GET
@Path("/themes/{theme}/files/")
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public Response getThemeRootDir(@PathParam("theme") final String themeName) {
final Optional<ThemeProvider> provider = findProvider(themeName);
if (provider.isPresent()) {
final Optional<ThemeFileInfo> fileInfo = provider
.get()
.getThemeFileInfo(themeName, ThemeVersion.DRAFT, "/");
if (fileInfo.isPresent()) {
if (fileInfo.get().isDirectory()) {
final List<ThemeFileInfo> fileInfos = provider
.get()
.listThemeFiles(themeName, ThemeVersion.DRAFT, "/");
return Response
.ok(fileInfos)
.type(MediaType.APPLICATION_JSON)
.build();
} else {
throw new WebApplicationException(
String.format(
"File \"/\" in theme %s is not a directory.",
themeName),
Response.Status.INTERNAL_SERVER_ERROR);
}
} else {
return Response
.status(Response.Status.NOT_FOUND)
.entity(String.format(
"File \"/\" does not exist in theme %s.",
themeName))
.build();
}
} else {
return Response
.status(Response.Status.NOT_FOUND)
.entity(String.format("Theme \"%s\" does not exist.",
themeName))
.build();
}
}
@GET
@Path("/themes/{theme}/files/{path:.+}")
@AuthorizationRequired
@RequiresPrivilege(ThemingPrivileges.ADMINISTER_THEMES)
public Response getThemeFile(@PathParam("theme") final String themeName,
@PathParam("path") final String path) {
final Optional<ThemeProvider> provider = findProvider(themeName);
if (provider.isPresent()) {
final Optional<ThemeFileInfo> fileInfo = provider
.get()
.getThemeFileInfo(themeName, ThemeVersion.DRAFT, path);
if (fileInfo.isPresent()) {
final ThemeFileInfo themeFileInfo = fileInfo.get();
if (themeFileInfo.isDirectory()) {
final List<ThemeFileInfo> fileInfos = provider
.get()
.listThemeFiles(themeName, ThemeVersion.DRAFT, path);
return Response
.ok(fileInfos)
.type(MediaType.APPLICATION_JSON)
.build();
} else {
final Optional<InputStream> inputStream = provider
.get()
.getThemeFileAsStream(themeName,
ThemeVersion.DRAFT,
path);
if (inputStream.isPresent()) {
final InputStream inStream = inputStream.get();
return Response
.ok(inStream)
.type(themeFileInfo.getMimeType())
.build();
} else {
return Response
.status(Response.Status.NOT_FOUND)
.entity(String.format(
"File \"%s\" does not exist in theme %s.",
path,
themeName))
.build();
}
}
} else {
return Response
.status(Response.Status.NOT_FOUND)
.entity(String.format(
"File \"%s\" does not exist in theme %s.",
path,
themeName))
.build();
}
} else {
return Response
.status(Response.Status.NOT_FOUND)
.entity(String.format("Theme \"%s\" does not exist.",
themeName))
.build();
}
}
private String getProviderName(final ThemeProvider provider) {

17
pom.xml
View File

@ -463,6 +463,23 @@
<version>1.3.2</version>
</dependency>
<!-- Json Web Token support -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.5</version>
</dependency>
<!-- PrimeFaces for JSF prototype -->
<dependency>
<groupId>org.primefaces</groupId>