diff --git a/CHANGELOG b/CHANGELOG index d714369d..d760cde9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +v 2.1.0 + - dropwizard upgrade to 0.8.0 v 2.0.3 - internet explorer ajax cache workaround - categories are now deletable again diff --git a/pom.xml b/pom.xml index 0b00a49f..82e7fb08 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ 4.0.0 com.commafeed commafeed - 2.0.3 + 2.1.0-SNAPSHOT jar CommaFeed @@ -14,12 +14,25 @@ UTF-8 - 0.7.1 + 0.8.0-SNAPSHOT 3.0 3.5.0 1.5.0 + + + oss_snapshots + https://oss.sonatype.org/content/repositories/snapshots/ + + false + + + true + + + + scm:git:https://github.com/Athou/commafeed.git scm:git:https://github.com/Athou/commafeed.git @@ -220,7 +233,7 @@ ${dropwizard.version} pom - + com.wordnik swagger-jaxrs_2.10 @@ -232,7 +245,7 @@ - + com.mysema.querydsl querydsl-apt @@ -245,7 +258,12 @@ querydsl-jpa ${querydsl.version} - + + + commons-lang + commons-lang + 2.6 + commons-io commons-io @@ -266,7 +284,7 @@ commons-math3 3.3 - + redis.clients jedis @@ -277,7 +295,7 @@ javax.mail 1.5.2 - + com.rometools rome diff --git a/src/main/java/com/commafeed/CommaFeedApplication.java b/src/main/java/com/commafeed/CommaFeedApplication.java index 41018307..60651267 100644 --- a/src/main/java/com/commafeed/CommaFeedApplication.java +++ b/src/main/java/com/commafeed/CommaFeedApplication.java @@ -5,6 +5,7 @@ import io.dropwizard.assets.AssetsBundle; import io.dropwizard.db.DataSourceFactory; import io.dropwizard.hibernate.HibernateBundle; import io.dropwizard.migrations.MigrationsBundle; +import io.dropwizard.server.DefaultServerFactory; import io.dropwizard.servlets.CacheBustingFilter; import io.dropwizard.setup.Bootstrap; import io.dropwizard.setup.Environment; @@ -22,6 +23,7 @@ import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import org.eclipse.jetty.server.session.SessionHandler; +import org.glassfish.jersey.media.multipart.MultiPartFeature; import com.commafeed.backend.feed.FeedRefreshTaskGiver; import com.commafeed.backend.feed.FeedRefreshUpdater; @@ -41,8 +43,7 @@ import com.commafeed.backend.service.StartupService; import com.commafeed.backend.service.UserService; import com.commafeed.backend.task.OldStatusesCleanupTask; import com.commafeed.backend.task.OrphansCleanupTask; -import com.commafeed.frontend.auth.SecurityCheckProvider; -import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckUserServiceProvider; +import com.commafeed.frontend.auth.SecurityCheckFactoryProvider; import com.commafeed.frontend.resource.AdminREST; import com.commafeed.frontend.resource.CategoryREST; import com.commafeed.frontend.resource.EntryREST; @@ -54,10 +55,9 @@ import com.commafeed.frontend.servlet.AnalyticsServlet; import com.commafeed.frontend.servlet.CustomCssServlet; import com.commafeed.frontend.servlet.LogoutServlet; import com.commafeed.frontend.servlet.NextUnreadServlet; -import com.commafeed.frontend.session.SessionHelperProvider; +import com.commafeed.frontend.session.SessionHelperFactoryProvider; import com.google.inject.Guice; import com.google.inject.Injector; -import com.sun.jersey.api.core.ResourceConfig; import com.wordnik.swagger.config.ConfigFactory; import com.wordnik.swagger.config.ScannerFactory; import com.wordnik.swagger.config.SwaggerConfig; @@ -113,12 +113,12 @@ public class CommaFeedApplication extends Application { // Auth/session management environment.servlets().setSessionHandler(new SessionHandler(config.getSessionManagerFactory().build())); - environment.jersey().register(new SecurityCheckUserServiceProvider(injector.getInstance(UserService.class))); - environment.jersey().register(SecurityCheckProvider.class); - environment.jersey().register(SessionHelperProvider.class); + environment.jersey().register(new SecurityCheckFactoryProvider.Binder(injector.getInstance(UserService.class))); + environment.jersey().register(new SessionHelperFactoryProvider.Binder()); // REST resources environment.jersey().setUrlPattern("/rest/*"); + ((DefaultServerFactory) config.getServerFactory()).setJerseyRootPath("/rest/*"); environment.jersey().register(injector.getInstance(AdminREST.class)); environment.jersey().register(injector.getInstance(CategoryREST.class)); environment.jersey().register(injector.getInstance(EntryREST.class)); @@ -127,6 +127,8 @@ public class CommaFeedApplication extends Application { environment.jersey().register(injector.getInstance(ServerREST.class)); environment.jersey().register(injector.getInstance(UserREST.class)); + environment.jersey().register(MultiPartFeature.class); + // Servlets environment.servlets().addServlet("next", injector.getInstance(NextUnreadServlet.class)).addMapping("/next"); environment.servlets().addServlet("logout", injector.getInstance(LogoutServlet.class)).addMapping("/logout"); @@ -170,8 +172,6 @@ public class CommaFeedApplication extends Application { } }).addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST), false, "/rest/*"); - // enable wadl - environment.jersey().disable(ResourceConfig.FEATURE_DISABLE_WADL); } public static void main(String[] args) throws Exception { diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java new file mode 100644 index 00000000..6f4dd3c9 --- /dev/null +++ b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactory.java @@ -0,0 +1,96 @@ +package com.commafeed.frontend.auth; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import lombok.RequiredArgsConstructor; + +import org.eclipse.jetty.util.B64Code; +import org.eclipse.jetty.util.StringUtil; +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; + +import com.commafeed.backend.model.User; +import com.commafeed.backend.model.UserRole.Role; +import com.commafeed.backend.service.UserService; +import com.commafeed.frontend.session.SessionHelper; +import com.google.common.base.Optional; + +@RequiredArgsConstructor +public class SecurityCheckFactory extends AbstractContainerRequestValueFactory { + + private static final String PREFIX = "Basic"; + + @Context + HttpServletRequest request; + + @Inject + UserService userService; + + private final Role role; + private final boolean apiKeyAllowed; + + @Override + public User provide() { + Optional user = apiKeyLogin(); + if (!user.isPresent()) { + user = basicAuthenticationLogin(); + } + if (!user.isPresent()) { + user = cookieSessionLogin(new SessionHelper(request)); + } + + if (user.isPresent()) { + if (user.get().hasRole(role)) { + return user.get(); + } else { + throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) + .entity("You don't have the required role to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + } else { + throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) + .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); + } + } + + Optional cookieSessionLogin(SessionHelper sessionHelper) { + Optional loggedInUser = sessionHelper.getLoggedInUser(); + if (loggedInUser.isPresent()) { + userService.performPostLoginActivities(loggedInUser.get()); + } + return loggedInUser; + } + + private Optional basicAuthenticationLogin() { + String header = request.getHeader(HttpHeaders.AUTHORIZATION); + if (header != null) { + int space = header.indexOf(' '); + if (space > 0) { + String method = header.substring(0, space); + if (PREFIX.equalsIgnoreCase(method)) { + String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1); + int i = decoded.indexOf(':'); + if (i > 0) { + String username = decoded.substring(0, i); + String password = decoded.substring(i + 1); + return userService.login(username, password); + } + } + } + } + return Optional.absent(); + } + + private Optional apiKeyLogin() { + String apiKey = request.getParameter("apiKey"); + if (apiKey != null && apiKeyAllowed) { + return userService.login(apiKey); + } + return Optional.absent(); + } + +} diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java new file mode 100644 index 00000000..8d389c00 --- /dev/null +++ b/src/main/java/com/commafeed/frontend/auth/SecurityCheckFactoryProvider.java @@ -0,0 +1,64 @@ +package com.commafeed.frontend.auth; + +import javax.inject.Inject; +import javax.inject.Singleton; + +import lombok.RequiredArgsConstructor; + +import org.glassfish.hk2.api.Factory; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; + +import com.commafeed.backend.model.User; +import com.commafeed.backend.service.UserService; + +@Singleton +public class SecurityCheckFactoryProvider extends AbstractValueFactoryProvider { + + @Inject + public SecurityCheckFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, final ServiceLocator injector) { + super(extractorProvider, injector, Parameter.Source.UNKNOWN); + } + + @Override + protected Factory createValueFactory(final Parameter parameter) { + final Class classType = parameter.getRawType(); + + SecurityCheck securityCheck = parameter.getAnnotation(SecurityCheck.class); + if (securityCheck == null) + return null; + + if (classType.isAssignableFrom(User.class)) { + return new SecurityCheckFactory(securityCheck.value(), securityCheck.apiKeyAllowed()); + } else { + return null; + } + } + + public static class SecurityCheckInjectionResolver extends ParamInjectionResolver { + public SecurityCheckInjectionResolver() { + super(SecurityCheckFactoryProvider.class); + } + } + + @RequiredArgsConstructor + public static class Binder extends AbstractBinder { + + private final UserService userService; + + @Override + protected void configure() { + bind(SecurityCheckFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); + bind(SecurityCheckInjectionResolver.class).to(new TypeLiteral>() { + }).in(Singleton.class); + bind(userService).to(UserService.class); + } + } +} diff --git a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java b/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java deleted file mode 100644 index 030187e7..00000000 --- a/src/main/java/com/commafeed/frontend/auth/SecurityCheckProvider.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.commafeed.frontend.auth; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.WebApplicationException; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; - -import lombok.RequiredArgsConstructor; - -import org.eclipse.jetty.util.B64Code; -import org.eclipse.jetty.util.StringUtil; - -import com.commafeed.backend.model.User; -import com.commafeed.backend.model.UserRole.Role; -import com.commafeed.backend.service.UserService; -import com.commafeed.frontend.session.SessionHelper; -import com.google.common.base.Optional; -import com.sun.jersey.api.core.HttpContext; -import com.sun.jersey.api.model.Parameter; -import com.sun.jersey.core.spi.component.ComponentContext; -import com.sun.jersey.core.spi.component.ComponentScope; -import com.sun.jersey.server.impl.inject.AbstractHttpContextInjectable; -import com.sun.jersey.spi.inject.Injectable; -import com.sun.jersey.spi.inject.InjectableProvider; -import com.sun.jersey.spi.inject.SingletonTypeInjectableProvider; - -public class SecurityCheckProvider implements InjectableProvider { - - public static class SecurityCheckUserServiceProvider extends SingletonTypeInjectableProvider { - - public SecurityCheckUserServiceProvider(UserService userService) { - super(UserService.class, userService); - } - } - - @RequiredArgsConstructor - static class SecurityCheckInjectable extends AbstractHttpContextInjectable { - private static final String PREFIX = "Basic"; - - private final SessionHelper sessionHelper; - private final UserService userService; - private final Role role; - private final boolean apiKeyAllowed; - - @Override - public User getValue(HttpContext c) { - Optional user = apiKeyLogin(c); - if (!user.isPresent()) { - user = basicAuthenticationLogin(c); - } - if (!user.isPresent()) { - user = cookieSessionLogin(); - } - - if (user.isPresent()) { - if (user.get().hasRole(role)) { - return user.get(); - } else { - throw new WebApplicationException(Response.status(Response.Status.FORBIDDEN) - .entity("You don't have the required role to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); - } - } else { - throw new WebApplicationException(Response.status(Response.Status.UNAUTHORIZED) - .entity("Credentials are required to access this resource.").type(MediaType.TEXT_PLAIN_TYPE).build()); - } - } - - Optional cookieSessionLogin() { - Optional loggedInUser = sessionHelper.getLoggedInUser(); - if (loggedInUser.isPresent()) { - userService.performPostLoginActivities(loggedInUser.get()); - } - return loggedInUser; - } - - private Optional basicAuthenticationLogin(HttpContext c) { - String header = c.getRequest().getHeaderValue(HttpHeaders.AUTHORIZATION); - if (header != null) { - int space = header.indexOf(' '); - if (space > 0) { - String method = header.substring(0, space); - if (PREFIX.equalsIgnoreCase(method)) { - String decoded = B64Code.decode(header.substring(space + 1), StringUtil.__ISO_8859_1); - int i = decoded.indexOf(':'); - if (i > 0) { - String username = decoded.substring(0, i); - String password = decoded.substring(i + 1); - return userService.login(username, password); - } - } - } - } - return Optional.absent(); - } - - private Optional apiKeyLogin(HttpContext c) { - String apiKey = c.getUriInfo().getQueryParameters().getFirst("apiKey"); - if (apiKey != null && apiKeyAllowed) { - return userService.login(apiKey); - } - return Optional.absent(); - } - } - - private SessionHelper sessionHelper; - private UserService userService; - - public SecurityCheckProvider(@Context HttpServletRequest req, @Context UserService userService) { - this.sessionHelper = new SessionHelper(req); - this.userService = userService; - } - - @Override - public ComponentScope getScope() { - return ComponentScope.PerRequest; - } - - @Override - public Injectable getInjectable(ComponentContext ic, SecurityCheck sc, Parameter c) { - return new SecurityCheckInjectable(sessionHelper, userService, sc.value(), sc.apiKeyAllowed()); - } -} diff --git a/src/main/java/com/commafeed/frontend/resource/FeedREST.java b/src/main/java/com/commafeed/frontend/resource/FeedREST.java index 7dc5cbba..cd1ef48a 100644 --- a/src/main/java/com/commafeed/frontend/resource/FeedREST.java +++ b/src/main/java/com/commafeed/frontend/resource/FeedREST.java @@ -35,6 +35,7 @@ import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.ObjectUtils; import org.apache.commons.lang.StringUtils; +import org.glassfish.jersey.media.multipart.FormDataParam; import com.commafeed.CommaFeedApplication; import com.commafeed.CommaFeedConfiguration; @@ -78,7 +79,6 @@ import com.rometools.rome.feed.synd.SyndFeed; import com.rometools.rome.feed.synd.SyndFeedImpl; import com.rometools.rome.io.SyndFeedOutput; import com.rometools.rome.io.WireFeedOutput; -import com.sun.jersey.multipart.FormDataParam; import com.wordnik.swagger.annotations.Api; import com.wordnik.swagger.annotations.ApiOperation; import com.wordnik.swagger.annotations.ApiParam; diff --git a/src/main/java/com/commafeed/frontend/session/SessionHelperFactory.java b/src/main/java/com/commafeed/frontend/session/SessionHelperFactory.java new file mode 100644 index 00000000..a6ecad6b --- /dev/null +++ b/src/main/java/com/commafeed/frontend/session/SessionHelperFactory.java @@ -0,0 +1,17 @@ +package com.commafeed.frontend.session; + +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Context; + +import org.glassfish.jersey.server.internal.inject.AbstractContainerRequestValueFactory; + +public class SessionHelperFactory extends AbstractContainerRequestValueFactory { + + @Context + HttpServletRequest request; + + @Override + public SessionHelper provide() { + return new SessionHelper(request); + } +} \ No newline at end of file diff --git a/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java b/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java new file mode 100644 index 00000000..7eb0cbe9 --- /dev/null +++ b/src/main/java/com/commafeed/frontend/session/SessionHelperFactoryProvider.java @@ -0,0 +1,56 @@ +package com.commafeed.frontend.session; + +import javax.inject.Inject; +import javax.inject.Singleton; +import javax.ws.rs.core.Context; + +import org.glassfish.hk2.api.Factory; +import org.glassfish.hk2.api.InjectionResolver; +import org.glassfish.hk2.api.ServiceLocator; +import org.glassfish.hk2.api.TypeLiteral; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.internal.inject.AbstractValueFactoryProvider; +import org.glassfish.jersey.server.internal.inject.MultivaluedParameterExtractorProvider; +import org.glassfish.jersey.server.internal.inject.ParamInjectionResolver; +import org.glassfish.jersey.server.model.Parameter; +import org.glassfish.jersey.server.spi.internal.ValueFactoryProvider; + +@Singleton +public class SessionHelperFactoryProvider extends AbstractValueFactoryProvider { + + @Inject + public SessionHelperFactoryProvider(final MultivaluedParameterExtractorProvider extractorProvider, final ServiceLocator injector) { + super(extractorProvider, injector, Parameter.Source.CONTEXT); + } + + @Override + protected Factory createValueFactory(final Parameter parameter) { + final Class classType = parameter.getRawType(); + + Context context = parameter.getAnnotation(Context.class); + if (context == null) + return null; + + if (classType.isAssignableFrom(SessionHelper.class)) { + return new SessionHelperFactory(); + } else { + return null; + } + } + + public static class SessionHelperInjectionResolver extends ParamInjectionResolver { + public SessionHelperInjectionResolver() { + super(SessionHelperFactoryProvider.class); + } + } + + public static class Binder extends AbstractBinder { + + @Override + protected void configure() { + bind(SessionHelperFactoryProvider.class).to(ValueFactoryProvider.class).in(Singleton.class); + bind(SessionHelperInjectionResolver.class).to(new TypeLiteral>() { + }).in(Singleton.class); + } + } +} diff --git a/src/main/java/com/commafeed/frontend/session/SessionHelperProvider.java b/src/main/java/com/commafeed/frontend/session/SessionHelperProvider.java deleted file mode 100644 index aba77faf..00000000 --- a/src/main/java/com/commafeed/frontend/session/SessionHelperProvider.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.commafeed.frontend.session; - -import java.lang.reflect.Type; - -import javax.servlet.http.HttpServletRequest; -import javax.ws.rs.core.Context; -import javax.ws.rs.ext.Provider; - -import com.sun.jersey.core.spi.component.ComponentContext; -import com.sun.jersey.core.spi.component.ComponentScope; -import com.sun.jersey.spi.inject.Injectable; -import com.sun.jersey.spi.inject.InjectableProvider; - -@Provider -public class SessionHelperProvider implements InjectableProvider { - - private final ThreadLocal request; - - public SessionHelperProvider(@Context ThreadLocal request) { - this.request = request; - } - - @Override - public ComponentScope getScope() { - return ComponentScope.PerRequest; - } - - @Override - public Injectable getInjectable(ComponentContext ic, final Context session, Type type) { - if (type.equals(SessionHelper.class)) { - return new Injectable() { - @Override - public SessionHelper getValue() { - final HttpServletRequest req = request.get(); - if (req != null) { - return new SessionHelper(req); - } - return null; - } - }; - } - return null; - } -} \ No newline at end of file diff --git a/src/test/java/com/commafeed/frontend/auth/SecurityCheckInjectableTest.java b/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java similarity index 78% rename from src/test/java/com/commafeed/frontend/auth/SecurityCheckInjectableTest.java rename to src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java index 429b055d..5c5f5861 100644 --- a/src/test/java/com/commafeed/frontend/auth/SecurityCheckInjectableTest.java +++ b/src/test/java/com/commafeed/frontend/auth/SecurityCheckFactoryTest.java @@ -9,11 +9,10 @@ import org.junit.Test; import com.commafeed.backend.model.User; import com.commafeed.backend.service.UserService; import com.commafeed.backend.service.internal.PostLoginActivities; -import com.commafeed.frontend.auth.SecurityCheckProvider.SecurityCheckInjectable; import com.commafeed.frontend.session.SessionHelper; import com.google.common.base.Optional; -public class SecurityCheckInjectableTest { +public class SecurityCheckFactoryTest { @Test public void cookie_login_should_perform_post_login_activities_if_user_is_logged_in() { @@ -26,8 +25,9 @@ public class SecurityCheckInjectableTest { UserService service = new UserService(null, null, null, null, null, postLoginActivities); - SecurityCheckInjectable injectable = new SecurityCheckInjectable(sessionHelper, service, null, false); - injectable.cookieSessionLogin(); + SecurityCheckFactory factory = new SecurityCheckFactory(null, false); + factory.userService = service; + factory.cookieSessionLogin(sessionHelper); verify(postLoginActivities).executeFor(userInSession); }